diff --git a/.gitignore b/.gitignore index dc17729f7..207dbe4cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ deps/deps.jl docs/build -Manifest.toml \ No newline at end of file +Manifest.toml + +.ipynb_checkpoints/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 53958e0bc..091430536 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ os: - windows julia: - 1.0 - - 1.3 + - 1.4 - nightly codecov: true jobs: @@ -13,7 +13,7 @@ jobs: - julia: nightly include: - stage: "Documentation" - julia: 1.3 + julia: 1.4 os: linux script: - julia --project=docs/ -e 'using Pkg; Pkg.instantiate(); Pkg.develop(PackageSpec(path=pwd()))' diff --git a/CHANGELOG.md b/CHANGELOG.md index a7db1de35..d58cce1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ ## staged +- none + +## v0.9.0 + +- Add `instantiate_mc_model` to aid in building JuMP model from ENGINEERING data model +- SDP and SOC relaxations were broken but are fixed again (unit tests added) +- Remove `run_mc_opf_iv`, `run_mc_opf_bf`, `run_mc_opf_bf_lm`, `run_mc_pf_bf`, `run_mc_pf_iv`, these can be accessed by using the correct formulation with `run_mc_opf` and `run_mc_pf` +- Add support for Memento 1.1 +- Add support for PowerModels v0.17 (breaking) +- Add support for InfrastructureModels v0.5 +- Updates JSON parser to handle enum (`"data_model"` values) +- Adds some commonly used InfrastructureModels and PowerModels functions as exports +- Adds model building functions `add_{component}!` to aid in building simple models for testing (experimental) +- Add `run_mc_model` (adds `ref_add_arcs_transformer!` to ref_extensions, and sets `multiconductor=true` by default) (breaking) +- Rename `ref_add_arcs_trans` -> `ref_add_arcs_transformer` (breaking) +- Update `count_nodes`, now counts source nodes as well, excludes \_virtual objects +- Change \_PMs and \_IMs to \_PM, \_IM, respectively +- Add example for PowerModelsDistribution usage (see Jupyter notebooks in `/examples`) +- Update transformer mathematical model +- Introduce new data models: ENGINEERING, MATHEMATICAL (see data model documentation) (breaking) +- Update DSS parser to be more robust, and parse into new format (breaking) +- Updates DSS paser to parse more options/commands, moves these into `"options"` dict (breaking) +- Updates how dss `like` is applied to better match opendss (almost all properties are copied with like) (breaking) +- Add support for new OpenDSS components (loadshape, xfmrcode, xycurve) +- Add support for JuMP v0.22 (exports `optimizer_with_attributtes` by default) +- Add support for PowerModels v0.16 (breaking) - Add support for Memento v0.13, v1.0 ## v0.8.1 @@ -9,7 +35,7 @@ - Update to support JuMP v0.21 - Makes bounds optional, turned on by default (#250) - Updated transformer data model in the mathematical model (#250) -- Add automatic parsing of lon,lat from buscoords file into PMD data structure (#245, #249) +- Add automatic parsing of lon,lat from buscoords file into PowerModelsDistribution data structure (#245, #249) - Updates virtual_sourcebus, which is intended to represent a voltage source, to have a fixed voltage magnitude (#246,#248) - Add parsing of series data files into array fields in OpenDSS parser - Add LoadShape parsing to OpenDSS parser (#247) @@ -22,9 +48,9 @@ ## v0.8.0 -- Update solution building infrastructure (PMs #77) (breaking). The reported solution is now consistent with the variable space of the formulation. +- Update solution building infrastructure (PowerModels #77) (breaking). The reported solution is now consistent with the variable space of the formulation. - Moved multi-conductor support from PowerModels into PowerModelsDistribution. (breaking) -- PMs.var no longer takes conductor as an argument +- PowerModels.var no longer takes conductor as an argument - Constraints have been (partially) re-written to use vectorized JuMP syntax where possible. - Bugfixes: generator on-off and storage on-off constraints were incorrect - Removal of SOCWRPowerModel diff --git a/Project.toml b/Project.toml index 4b47e6a76..8b7ec0e83 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "PowerModelsDistribution" uuid = "d7431456-977f-11e9-2de3-97ff7677985e" authors = ["David M Fobes ", "Carleton Coffrin"] repo = "https://github.com/lanl-ansi/PowerModelsDistribution.jl.git" -version = "0.8.1" +version = "0.9.0" [deps] InfrastructureModels = "2030c09a-7f63-5d83-885d-db604e0e9cc0" @@ -16,14 +16,14 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] Cbc = ">= 0.4" -InfrastructureModels = "~0.4" +InfrastructureModels = "~0.5" Ipopt = ">= 0.4" JSON = "~0.18, ~0.19, ~0.20, ~0.21" JuMP = "~0.19.2, ~0.20, ~0.21" Juniper = ">= 0.4" MathOptInterface = "~0.8, ~0.9" -Memento = "~0.10, ~0.11, ~0.12, ~0.13, ~1.0" -PowerModels = "~0.15" +Memento = "~0.10, ~0.11, ~0.12, ~0.13, ~1.0, ~1.1" +PowerModels = "~0.17" SCS = ">= 0.4" julia = "^1" diff --git a/README.md b/README.md index d354b5e4b..bfef1c1ff 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,37 @@ This enables the definition of a wide variety of power network formulations and ## Core Problem Specifications - Power Flow (pf) -- Optimal Power Flow (opf), for the Bus Injection Model (BIM) as well as the Branch Flow Model (BFM) -- Continuous load shed, minimum load delta (mld), for the Branch Flow Model (LPLinUBFPowerModel), AC Polar (ACPPowerModel), and Network Flow Approximation (NFAPowerModel) + - ACP, ACR, IVR, LinDist3Flow, NFA, DCP +- Optimal Power Flow (opf) + - ACP, ACR, IVR, LinDist3Flow, NFA, DCP +- Continuous load shed, minimum load delta (mld) + - ACP, LinDist3Flow, NFA +- Optimal Power Flow with on-load tap-changer (opf_oltc) + - ACP + +**Note: The documentation is somewhat lagging behind development and the parings of network features with problem specifications with formulations has not been enumerated. We are working to correct this for you.** ## Core Network Formulations -- AC (polar and rectangular coordinates) -- SDP BFM relaxation -- SOC BFM and BIM relaxation (W-space) -- Linear approximation (LinDist3Flow and simplified unbalanced DistFlow) +- Nonlinear + - ACP + - ACR + - IVR +- Relaxations + - SDP BFM + - SOC BFM and BIM relaxation (W-space) +- Linear Approximations + - LinDist3Flow + - NFA + - DCP ## Network Data Formats -- Matlab ".m" files (extended for three-phase) - OpenDSS ".dss" files -**Warning:** This package is under active development and may change drastically without warning. +## Examples + +Examples of how to use PowerModelsDistribution can be found in the main documentation and in Jupyter Notebooks inside the `/examples` directory ## Development @@ -44,6 +59,21 @@ This code has been developed as part of the Advanced Network Science Initiative - Sander Claeys (@sanderclaeys) KU Leuven, transformer models and ACR formulation - Frederik Geth (@frederikgeth) CSIRO, Distribution modeling advise +## Citing PowerModelsDistribution + +If you find PowerModelsDistribution useful for your work, we kindly request that you cite the following [https://arxiv.org/abs/2004.10081](publication): + +```bibtex +@misc{fobes2020powermodelsdistributionjl, + title={PowerModelsDistribution.jl: An Open-Source Framework for Exploring Distribution Power Flow Formulations}, + author={David M Fobes and Sander Claeys and Frederik Geth and Carleton Coffrin}, + year={2020}, + eprint={2004.10081}, + archivePrefix={arXiv}, + primaryClass={cs.CE} +} +``` + ## License This code is provided under a BSD license as part of the Multi-Infrastructure Control and Optimization Toolkit (MICOT) project, LA-CC-13-108. diff --git a/docs/Project.toml b/docs/Project.toml index 5ed439199..a8526fabc 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,5 +4,5 @@ InfrastructureModels = "2030c09a-7f63-5d83-885d-db604e0e9cc0" PowerModels = "c36e90e8-916a-50a6-bd94-075b64ef4655" [compat] -InfrastructureModels = "~0.4" -PowerModels = "~0.15" +InfrastructureModels = "~0.5" +PowerModels = "~0.17" diff --git a/docs/make.jl b/docs/make.jl index fde2aa1e5..0583f77d9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -9,8 +9,13 @@ makedocs( "Home" => "index.md", "Manual" => [ "Getting Started" => "quickguide.md", + "Connecting Components" => "connections.md", "Mathematical Model" => "math-model.md", - "Data Formats" => "data-formats.md", + "Engineering Data Model" => "eng-data-model.md", + "Enums in Engineering Model" => "enums.md", + "Conversion to Mathematical Model" => "eng2math.md", + "External Data Formats" => "external-data-formats.md", + "Examples" => "engineering_model.md", ], "Library" => [ "Network Formulations" => "formulations.md", diff --git a/docs/src/assets/line_connection_example.svg b/docs/src/assets/line_connection_example.svg new file mode 100644 index 000000000..20a055522 --- /dev/null +++ b/docs/src/assets/line_connection_example.svg @@ -0,0 +1,436 @@ + +image/svg+xmlf +connections=[“a”,“n”] +1 +2 +1 +2 +line +a +f +bus +b +c +n +a +t +bus +n +t +connections=[“a”,“n”] + \ No newline at end of file diff --git a/docs/src/assets/loads.svg b/docs/src/assets/loads.svg new file mode 100644 index 000000000..63d13178b --- /dev/null +++ b/docs/src/assets/loads.svg @@ -0,0 +1,100 @@ + +image/svg+xmlsd[1] +1 +2 + \ No newline at end of file diff --git a/docs/src/assets/loads_connection_example.svg b/docs/src/assets/loads_connection_example.svg new file mode 100644 index 000000000..cba7a40e7 --- /dev/null +++ b/docs/src/assets/loads_connection_example.svg @@ -0,0 +1,334 @@ + +image/svg+xmla +bus +b +c +n +1 +2 +3 +sd[1] +sd[2] +load +connections=[“a”,“c”,“n”] +terminals=[“a”,“b”,“c”,“n”] + \ No newline at end of file diff --git a/docs/src/assets/loads_delta_3ph.svg b/docs/src/assets/loads_delta_3ph.svg new file mode 100644 index 000000000..96324c607 --- /dev/null +++ b/docs/src/assets/loads_delta_3ph.svg @@ -0,0 +1,183 @@ + +image/svg+xmlsd[2] +sd[1] +sd[3] +1 +2 +3 + \ No newline at end of file diff --git a/docs/src/assets/loads_wye_1ph.svg b/docs/src/assets/loads_wye_1ph.svg new file mode 100644 index 000000000..bc40a154f --- /dev/null +++ b/docs/src/assets/loads_wye_1ph.svg @@ -0,0 +1,176 @@ + +image/svg+xml1 +2 +sd[1] +1 +2 +sd[1] + \ No newline at end of file diff --git a/docs/src/assets/loads_wye_2ph.svg b/docs/src/assets/loads_wye_2ph.svg new file mode 100644 index 000000000..31fdd0df2 --- /dev/null +++ b/docs/src/assets/loads_wye_2ph.svg @@ -0,0 +1,153 @@ + +image/svg+xml1 +2 +3 +sd[1] +sd[2] + \ No newline at end of file diff --git a/docs/src/assets/loads_wye_3ph.svg b/docs/src/assets/loads_wye_3ph.svg new file mode 100644 index 000000000..d663078b8 --- /dev/null +++ b/docs/src/assets/loads_wye_3ph.svg @@ -0,0 +1,206 @@ + +image/svg+xml1 +2 +3 +4 +sd[1] +sd[2] +sd[3] + \ No newline at end of file diff --git a/docs/src/connections.md b/docs/src/connections.md new file mode 100644 index 000000000..f76beddb5 --- /dev/null +++ b/docs/src/connections.md @@ -0,0 +1,45 @@ +# Connecting Components + +One of the main goals of a network model, is specifying how constituent components are connected together. The patterns explained here, are equally applicable to the engineering and mathematical data model. + +The available connections of each component connect to bus terminals. Take for example a bus with four terminals, `terminals=["a","b","c","n"]`. + +## Node objects + +Node objects always connect to a single bus (and perhaps also the universal ground, as can be the case for shunts). Therefore, they always have at least two key fields controlling the connectivity: `bus` and `connections`. Most node objects also have a `configuration` field, which affects the interpretation of the values supplied in `connections`. We will illustrate this for loads below. + +### Loads + +A multi-phase load consists of several individual loads, the number of which is implied by the length of properties such as `pd_nom`, `qd_nom` etc. The table below illustrates how the *length* of the field `connections` and the *value* of the field `configuration` determines the layout of the load. + +| `connections` | `configuration==WYE` | `configuration==DELTA` | +| ------------- | ----------------------------------------------- | --------------------------------------------------- | +| 2 | ![2 connections, wye](assets/loads_wye_1ph.svg) | ![2 connections, delta](assets/loads_wye_1ph.svg) | +| 3 | ![3 connections, wye](assets/loads_wye_2ph.svg) | ![3 connections, delta](assets/loads_delta_3ph.svg) | +| 4 | ![4 connections, wye](assets/loads_wye_3ph.svg) | undefined | + +For example, we wish to connect a wye-connected load consisting of 2 individual loads (`|connections|=3` and `configuration=WYE`) to our example bus with four available terminals. If we specify `connections=["a","c","n"]`, this leads to + +![loads connection example](assets/loads_connection_example.svg). + +## Edge objects + +Edge objects connect two buses (except for generic `transformers`, which can connect `N` buses). Therefore, they have the fields + +- `f_bus` and `f_connections`, specifying the from-side bus and how the object connects to it; +- `t_bus` and `t_connections`, specifying the same for the to-side. + +### Lines + +A line can have a variable number of conductors, which is implied by the size of the fields `rs`, `xs`, `g_fr`, `b_fr`, `g_to` and `b_to`. The fields `f_connections` and `t_connections` should specify for each conductor, to which terminals it connects. The figure below illustrates this for a line with 2 conuctors, + +![line connection example](assets/line_connection_example.svg). + +### Transformers + +Transformers also have a `configuration` field. For + +- generic transformers, this is specified per winding, and `configuration` is therefore a vector of `ConnConfig` enums (`WYE` or `DELTA`); +- AL2W transformers however are always two-winding, and the secondary is always wye-connected. Therefore, `configuration` is a scalar, specifying the configuration of the from-side winding. + +Generic transformers have a field `buses`, a Vector containing the buses to which each winding connects respectively (these do not have to be unique; a split-phase transformer is typically represented by having two windings connect to the same bus). The AL2W transformer however, since it is always two-winding, follows the `f_connections`/`t_connections` pattern. diff --git a/docs/src/eng-data-model.md b/docs/src/eng-data-model.md new file mode 100644 index 000000000..23b31b9c2 --- /dev/null +++ b/docs/src/eng-data-model.md @@ -0,0 +1,459 @@ +# Engineering Data Model + +This document describes the `ENGINEERING` data model type in PowerModelsDistribution, which is transformed at runtime, or at the user's direction into a `MATHEMATICAL` data model for optimization. + +In this document, + +- `nphases` refers to the number of non-neutral, non-ground active phases connected to a component, +- `nconductors` refers to all active conductors connected to a component, _i.e._ `length(connections)`, and +- `nwindings` refers to the number of windings of a transformer. + +The data structure is in the following format + +```julia +Dict{String,Any}( + "data_model" => ENGINEERING, + "component_type" => Dict{Any,Dict{String,Any}}( + id => Dict{String,Any}( + "parameter" => value, + ... + ), + ... + ), + ... +) +``` + +Valid component types are those that are documented in the sectios below. Each component object is identified by an `id`, which can be any immutable value (`id <: Any`), but `id` does not appear inside of the component dictionary, and only appears as keys to the component dictionaries under each component type. Note that by default, if using one of the parsers, component `id` will be of type `String`, and if a model is created that uses `id` which is not type `String`, it will not be JSON serializable (_i.e._ the `id` will be converted to its `String` representation on export to JSON). + +Each edge or node component (_i.e._ all those that are not data objects or buses), is expected to have `status` fields to specify whether the component is active or disabled, `bus` or `f_bus` and `t_bus`, to specify the buses that are connected to the component, and `connections` or `f_connections` and `t_connections`, to specify the terminals of the buses that are actively connected in an ordered list. Terminals/connections can be any immutable value, as can bus ids. __NOTE__: `terminals`, `connections`, `f_connections`, and `t_connections`, can either be type `Vector{Int}` _or_ `Vector{String}`, as long as they are consistent across the whole model. + +Parameter values on components are expected to be specified in SI units by default (where applicable) in the engineering data model. Relevant expected units are noted in the sections below. It is possible for the user to select universal scalar factors for power and voltages. For example, if `power_scalar_factor` and `voltage_scalar_factor` are their default values given below, where units is listed as watt or var, real units will be kW and kvar. Where units are listed as volt, real units will be kV (multiplied by `vm_nom`, where that value exists). + +The Used column describes the situtations where certain parameters are used. "always" indicates those values are used in all contexts, `opf`, `mld`, or any other problem name abbreviation indicate they are used in particular for those problems. "solution" indicates that those parameters are outputs from the solvers. "multinetwork" indicates these values are only used to build multinetwork problems. + +Those parameters that have a default may be omitted by the user from the data model, they will be populated by the specified default values. + +Components that support "codes", such as lines, switches, and transformers, behave such that any property on said object that conflicts with a value in the code will override the value given in the code object. This is noted on each object where this is relevant. + +## Root-Level Properties + +At the root level of the data structure, the following fields can be found. + +| Name | Default | Type | Used | Description | +| ------------ | ------------- | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------- | +| `name` | | `String` | | Case name | +| `data_model` | `ENGINEERING` | `DataModel` | always | `ENGINEERING`, `MATHEMATICAL`, or `DSS`. Type of the data model (this document describes `data_model==ENGINEERING`) | +| `settings` | `Dict()` | `Dict{String,<:Any}` | always | Base settings for the data model, see Settings section below for details | + +## Settings (`settings`) + +At the root-level of the data model a `settings` dictionary object is expected, containing the following fields. + +| Name | Default | Type | Units | Used | Description | +| ---------------------- | ------- | ------------------ | ----- | ------ | ---------------------------------------------------------------------------- | +| `voltage_scale_factor` | `1e3` | `Real` | | always | Scalar multiplier for voltage values | +| `power_scale_factor` | `1e3` | `Real` | | always | Scalar multiplier for power values | +| `vbases_default` | | `Dict{<:Any,Real}` | | always | Instruction to set the vbase at a number of buses for non-dimensionalization | +| `sbase_default` | | `Real` | | always | Instruction to set the power base (baseMVA) for non-dimensionalization | +| `base_frequency` | `60.0` | `Real` | Hz | always | Frequency base, _i.e._ the base frequency of the whole circuit | + +The parameters `voltage_scale_factor` and `power_scale_factor`determine the base +for all voltage and power parameters in this data model. For example, + +- `voltage_scale_factor=1E3` and `vm_nom=4.0`: `vm_nom` is `4.0 kV`/`4.0E3 V`, +- `power_scale_factor=1E6` and `pd_nom=2.0`: `pd_nom` is `2.0 MW`/`2.0E6 W`, +- `power_scale_factor=1E6` and `qd_nom=5.0`: `qd_nom` is `5.0 MVAr`/`5.0E6 VAr`, + +where the mentioned fields `vm_nom`, `pd_nom` and `qd_nom` are sample voltage and power variables which are defined later. + +On the other hand,`vbase_default` and `sbase_default` provide default values for a 'per unit' conversion; these do not affect the interpretation of the parameters in this model, like the scale factors do. Note that `vbase_default` is a `Dict{Any,Real}`, with pairs of bus ids and voltage magnitude levels, since in per unit conversion, the voltage base can change from bus to bus. The power base is the same everywhere, and therefore `sbase_default` has a single value. + +## Buses (`bus`) + +The data model below allows us to include buses of arbitrary many terminals (_i.e._, more than the usual four). This would be useful for + +- underground lines with multiple neutrals which are not joined at every bus; +- distribution lines that carry several conventional lines in parallel (see for example the quad circuits in NEVTestCase). + +| Name | Default | Type | Units | Used | Description | +| ------------- | ----------- | ----------------------------- | ------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `terminals` | `[1,2,3,4]` | `Vector{Int}||Vector{String}` | | always | Terminals for which the bus has active connections; __NOTE__: type can be either `Vector{Int}` or `Vector{String}`, but has to be consistent across all fields referring to terminals | +| `vm_lb` | | `Vector{Real}` | volt | opf | Minimum conductor-to-ground voltage magnitude, `size=nphases` | +| `vm_ub` | | `Vector{Real}` | volt | opf | Maximum conductor-to-ground voltage magnitude, `size=nphases` | +| `vm_pair_ub` | | `Vector{Tuple}` | | opf | _e.g._ `[(1,2,210)]` means \|U1-U2\|>210 | +| `vm_pair_lb` | | `Vector{Tuple}` | | opf | _e.g._ `[(1,2,230)]` means \|U1-U2\|<230 | +| `grounded` | `[]` | `Vector{Int}` | | always | List of terminals which are grounded | +| `rg` | `[]` | `Vector{Real}` | | always | Resistance of each defined grounding, `size=length(grounded)` | +| `xg` | `[]` | `Vector{Real}` | | always | Reactance of each defined grounding, `size=length(grounded)` | +| `vm` | | `Vector{Real}` | volt | always | Voltage magnitude at bus. If set, voltage magnitude at bus is fixed | +| `va` | | `Vector{Real}` | degree | always | Voltage angle at bus. If set, voltage angle at bus is fixed | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +Each terminal `c` of the bus has an associated complex voltage phasor `v[c]`. There are two types of voltage magnitude bounds. The first type bounds the voltage magnitude of each `v[c]` individually, + +- `lb <= |v[c]| <= ub` + +However, especially in four-wire networks, bounds are more naturally imposed on the difference of two terminal voltages instead, e.g. for terminals `c` and `d`, + +- `lb <= |v[c]-v[d]| <= ub` + +This is why we introduce the fields `vm_pair_lb` and `vm_pair_ub`, which define bounds for pairs of terminals, + +- $\forall$ `(c,d,lb)` $\in$ `vm_pair_lb`: `|v[c]-v[d]| >= lb` +- $\forall$ `(c,d,ub)` $\in$ `vm_pair_ub`: `|v[c]-v[d]| <= ub` + +Finally, we give an example of how grounding impedances should be entered. If terminal `4` is grounded through an impedance `Z=1+j2`, we write + +- `grounded=[4]`, `rg=[1]`, `xg=[2]` + +### Special Case: three-phase bus + +For three-phase buses, instead of specifying bounds explicitly for each pair of windings, often we want to specify 'phase-to-phase', 'phase-to-neutral' and 'neutral-to-ground' bounds. This can be done conveniently with a number of additional fields. First, `phases` is a list of the phase terminals, and `neutral` designates a single terminal to be the neutral. + +- The bounds `vm_pn_lb` and `vm_pn_ub` specify the same lower and upper bound for the magnitude of the difference of each phase terminal and the neutral. +- The bounds `vm_pp_lb` and `vm_pp_ub` specify the same lower and upper bound for the magnitude of the difference of all phase terminals. +- `vm_ng_ub` specifies an upper bound for the neutral terminal, the lower bound is typically zero. + +If all of these are specified, these bounds also imply valid bounds for the individual voltage magnitudes, + +- $\forall$ `c` $\in$ `phases`: `vm_pn_lb - vm_ng_ub <= |v[c]| <= vm_pn_ub + vm_ng_ub` +- `0 <= |v[neutral]|<= vm_ng_ub` + +Instead of defining the bounds directly, they can be specified through an associated voltage zone. + +| Name | Default | Type | Units | Used | Description | +| ---------- | ------- | ----------------------------- | ----- | ------ | ------------------------------------------------------------- | +| `phases` | | `Vector{Int}||Vector{String}` | | always | Identifies the terminal that represents the neutral conductor | +| `neutral` | | `Int||String` | | always | Identifies the terminal that represents the neutral conductor | +| `vm_pn_lb` | | `Real` | | opf | Minimum phase-to-neutral voltage magnitude for all phases | +| `vm_pn_ub` | | `Real` | | opf | Maximum phase-to-neutral voltage magnitude for all phases | +| `vm_pp_lb` | | `Real` | | opf | Minimum phase-to-phase voltage magnitude for all phases | +| `vm_pp_ub` | | `Real` | | opf | Maximum phase-to-phase voltage magnitude for all phases | +| `vm_ng_ub` | | `Real` | | opf | Maximum neutral-to-ground voltage magnitude | + +## Edge Objects + +These objects represent edges on the power grid and therefore require `f_bus` and `t_bus` (or `buses` in the case of transformers), and `f_connections` and `t_connections` (or `connections` in the case of transformers). + +### Lines (`line`) + +This is a pi-model branch. When a `linecode` is given, and any of `rs`, `xs`, `b_fr`, `b_to`, `g_fr` or `g_to` are specified, any of those overwrite the values on the linecode. + +| Name | Default | Type | Units | Used | Description | +| --------------- | --------------------------------- | ----------------------------- | ---------------- | ------ | ------------------------------------------------------------------------------------ | +| `f_bus` | | `Any` | | always | id of from-side bus connection | +| `t_bus` | | `Any` | | always | id of to-side bus connection | +| `f_connections` | | `Vector{Int}||Vector{String}` | | always | Indicates for each conductor, to which terminal of the `f_bus` it connects | +| `t_connections` | | `Vector{Int}||Vector{String}` | | always | Indicates for each conductor, to which terminal of the `t_bus` it connects | +| `linecode` | | `Any` | | always | id of an associated linecode | +| `rs` | | `Matrix{Real}` | ohm/meter | always | Series resistance matrix, `size=(nconductors,nconductors)` | +| `xs` | | `Matrix{Real}` | ohm/meter | always | Series reactance matrix, `size=(nconductors,nconductors)` | +| `g_fr` | `zeros(nconductors, nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | From-side conductance, `size=(nconductors,nconductors)` | +| `b_fr` | `zeros(nconductors, nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | From-side susceptance, `size=(nconductors,nconductors)` | +| `g_to` | `zeros(nconductors, nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | To-side conductance, `size=(nconductors,nconductors)` | +| `b_to` | `zeros(nconductors, nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | To-side susceptance, `size=(nconductors,nconductors)` | +| `length` | `1.0` | `Real` | meter | always | Length of the line | +| `cm_ub` | | `Vector{Real}` | amp | opf | Symmetrically applicable current rating, `size=nconductors` | +| `sm_ub` | | `Vector{Real}` | watt | opf | Symmetrically applicable power rating, `size=nconductors` | +| `vad_lb` | | `Vector{Real}` | degree | opf | Voltage angle difference lower bound | +| `vad_ub` | | `Vector{Real}` | degree | opf | Voltage angle difference upper bound | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | + +### Transformers (`transformer`) + +These are n-winding (`nwinding`), n-phase (`nphase`), lossy transformers. Note that most properties are now Vectors (or Vectors of Vectors), indexed over the windings. + +| Name | Default | Type | Units | Used | Description | +| ---------------- | ------------------------------------ | ------------------------------------- | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `buses` | | `Vector{Any}` | | always | List of bus for each winding, `size=nwindings` | +| `connections` | | `Vector{Vector{Int}||Vector{String}}` | | always | List of connection for each winding, `size=((nconductors),nwindings)` | +| `configurations` | `fill(WYE, nwindings)` | `Vector{ConnConfig}` | | always | `WYE` or `DELTA`. List of configuration for each winding, `size=nwindings` | +| `xfmrcode` | | `Any` | | always | id of | +| `xsc` | `zeros(nwindings*(nwindings-1)/2)` | `Vector{Real}` | `sm_nom[1]` | always | List of short-circuit reactances between each pair of windings, relative to the VA rating of the first winding; enter as a list of the upper-triangle elements | +| `rw` | `zeros(nwindings)` | `Vector{Real}` | `sm_nom[1]` | always | Active power lost due to resistance of each winding, relative to the VA rating of each winding winding | +| `cmag` | `0.0` | `Real` | `sm_nom[1]` | always | Total no-load reactive power drawn by the transformer, relative to VA rating of the first winding (magnetizing current) | +| `noloadloss` | `0.0` | `Real` | `sm_nom[1]` | always | Total no-load active power drawn by the transformer, relative to VA rating of the first winding | +| `tm_nom` | `ones(nwindings)` | `Vector{Real}` | | always | Nominal tap ratio for the transformer, `size=nwindings` (multiplier) | +| `tm_ub` | | `Vector{Vector{Real}}` | | opf | Maximum tap ratio for each winding and phase, `size=((nphases),nwindings)` (base=`tm_nom`) | +| `tm_lb` | | `Vector{Vector{Real}}` | | opf | Minimum tap ratio for for each winding and phase, `size=((nphases),nwindings)` (base=`tm_nom`) | +| `tm_set` | `fill(fill(1.0,nphases),nwindings)` | `Vector{Vector{Real}}` | | always | Set tap ratio for each winding and phase, `size=((nphases),nwindings)` (base=`tm_nom`) | +| `tm_fix` | `fill(fill(true,nphases),nwindings)` | `Vector{Vector{Bool}}` | | oltc | Indicates for each winding and phase whether the tap ratio is fixed, `size=((nphases),nwindings)` | +| `polarity` | `fill(1,nwindings)` | `Vector{Int}` | | always | | +| `vm_nom` | | `Vector{Real}` | volt | always | | +| `sm_nom` | | `Vector{Real}` | watt | always | | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | + +#### Asymmetric, Lossless, Two-Winding (AL2W) Transformers (`transformer`) + +Special case of the Generic transformer, which is still a `transformer` object, but has a simplified method for its definition. These are transformers are asymmetric (A), lossless (L) and two-winding (2W). Asymmetric refers to the fact that the secondary is always has a `WYE` configuration, whilst the primary can be `DELTA`. The table below indicates alternate, more simple ways to specify the special case of an AL2W Transformer. `xsc` and `rw` cannot be specified for an AL2W transformer, because it is lossless. To use this definition format, all of `f_bus`, `t_bus`, `f_connections`, `t_connections`, and `configuration` must be used, and none of `buses`, `connections`, `configurations` may be used. `xfmrcode` is ignored for this component. + +| Name | Default | Type | Units | Used | Description | +| --------------- | -------------------- | ----------------------------- | ----- | ------ | ----------------------------------------------------------------------------------------------------------- | +| `f_bus` | | `Any` | | always | Alternative way to specify `buses`, requires both `f_bus` and `t_bus` | +| `t_bus` | | `Any` | | always | Alternative way to specify `buses`, requires both `f_bus` and `t_bus` | +| `f_connections` | | `Vector{Int}||Vector{String}` | | always | Alternative way to specify `connections`, requires both `f_connections` and `t_connections`, `size=nphases` | +| `t_connections` | | `Vector{Int}||Vector{String}` | | always | Alternative way to specify `connections`, requires both `f_connections` and `t_connections`, `size=nphases` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. Alternative way to specify the from-side configuration, to-side is always `WYE` | +| `tm_nom` | `1.0` | `Real` | | always | Nominal tap ratio for the transformer (multiplier) | +| `tm_ub` | | `Vector{Real}` | | opf | Maximum tap ratio for each phase (base=`tm_nom`), `size=nphases` | +| `tm_lb` | | `Vector{Real}` | | opf | Minimum tap ratio for each phase (base=`tm_nom`), `size=nphases` | +| `tm_set` | `fill(1.0,nphases)` | `Vector{Real}` | | always | Set tap ratio for each phase (base=`tm_nom`), `size=nphases` | +| `tm_fix` | `fill(true,nphases)` | `Vector{Bool}` | | oltc | Indicates for each phase whether the tap ratio is fixed, `size=nphases` | + +### Switches (`switch`) + +Switches without `rs`, `xs` or a linecode (conductance/susceptance not considered), defined the switch will be treated as lossless. If lossy parameters are defined, `switch` objects will be decomposed into virtual `branch` & `bus`, and an ideal `switch`. + +| Name | Default | Type | Units | Used | Description | +| --------------- | ------------------------ | ----------------------------- | ----- | ------ | ------------------------------------------------------------------------------------------------ | +| `f_bus` | | `Any` | | always | id of from-side bus connection | +| `t_bus` | | `Any` | | always | id of to-side bus connection | +| `f_connections` | | `Vector{Int}||Vector{String}` | | always | Indicates for each conductor, to which terminal of the `f_bus` it connects | +| `t_connections` | | `Vector{Int}||Vector{String}` | | always | Indicates for each conductor, to which terminal of the `t_bus` it connects | +| `cm_ub` | | `Vector{Real}` | amp | opf | Symmetrically applicable current rating | +| `sm_ub` | | `Vector{Real}` | watt | opf | Symmetrically applicable power rating | +| `linecode` | | `Any` | | always | id of an associated linecode, does not take into account conductance/susceptance | +| `rs` | `zeros(nphases,nphases)` | `Matrix{Real}` | ohm | always | Series resistance matrix, `size=(nphases,nphases)` | +| `xs` | `zeros(nphases,nphases)` | `Matrix{Real}` | ohm | always | Series reactance matrix, `size=(nphases,nphases)` | +| `dispatchable` | `NO` | `Dispatchable` | | | `NO` or `YES`, indicates whether switch state can be changed in a switching optimization problem | +| `state` | `CLOSED` | `SwitchState` | | always | `CLOSED`: closed or `OPEN`: open, to indicate state of switch | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | + +## Node Objects + +These are objects that have single bus connections. Every object will have at least `bus`, `connections`, and `status`. + +### Shunts (`shunt`) + +| Name | Default | Type | Units | Used | Description | +| -------------- | --------- | ----------------------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `gs` | | `Matrix{Real}` | siemens | always | Conductance, `size=(nconductors,nconductors)` | +| `bs` | | `Matrix{Real}` | siemens | always | Susceptance, `size=(nconductors,nconductors)` | +| `model` | `GENERIC` | `ShuntModel` | | | `GENERIC`, `CAPACITOR`, or `REACTOR`. Indicates the type of shunt which may be necessary for transient stability analysis | +| `dispatchable` | `NO` | `Dispatchable` | | mld | `NO` or `YES`, indicates whether a shunt can be shed | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +### Loads (`load`) + +| Name | Default | Type | Units | Used | Description | +| --------------- | --------- | ----------------------------- | ----- | -------------- | -------------------------------------------------------------------------------------------------- | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. If `WYE`, `connections[end]=neutral` | +| `model` | `POWER` | `LoadModel` | | always | `POWER`, `IMPEDANCE`, `CURRENT`, `EXPONENTIAL`, or `ZIP`. Indicates the type of voltage-dependency | +| `pd_nom` | | `Vector{Real}` | watt | always | Nominal active load, with respect to `vm_nom`, `size=nphases` | +| `qd_nom` | | `Vector{Real}` | var | always | Nominal reactive load, with respect to `vm_nom`, `size=nphases` | +| `vm_nom` | | `Real` | volt | `model!=POWER` | Nominal voltage (multiplier) | +| `dispatchable` | `NO` | `Dispatchable` | | mld | `NO` or `YES`, indicates whether a load can be shed | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +Multi-phase loads define a number of individual loads connected between two terminals each. How they are connected, is defined both by `configuration` and `connections`. The table below indicates the value of `configuration` and lengths of the other properties for a consistent definition, + +| `configuration` | `|connections|` | `|pd_nom|=|qd_nom|=|pd_exp|=...` | +| --------------- | --------------- | -------------------------------- | +| `DELTA` | `2` | `1` | +| `DELTA` | `3` | `3` | +| `WYE` | `2` | `1` | +| `WYE` | `3` | `2` | +| `WYE` | `N` | `N-1` | + +Note that for delta loads, only 2 and 3 connections are allowed. Each individual load `i` is connected between two terminals, exposed to a voltage magnitude `v[i]`, which leads to a consumption `pd[i]+j*qd[i]`. The `model` then defines the relationship between these quantities, + +| model | `pd[i]/pd_nom[i]=` | `qd[i]/qd_nom[i]=` | +| ----------- | ------------------ | ------------------ | +| `POWER` | `1` | `1` | +| `CURRENT` | `(v[i]/vm_nom)` | `(v[i]/vm_nom)` | +| `IMPEDANCE` | `(v[i]/vm_nom)^2` | `(v[i]/vm_nom)^2` | + +Two more model types are supported, which need additional fields and are defined below. + +#### `model == EXPONENTIAL` + +- `(pd[i]/pd_nom[i]) = (v[i]/vm_nom)^pd_exp[i]` +- `(qd[i]/qd_nom[i]) = (v[i]/vm_nom)^qd_exp[i]` + +| Name | Default | Type | Units | Used | Description | +| -------- | ------- | ------ | ----- | -------------------- | ----------- | +| `pd_exp` | | `Real` | | `model==EXPONENTIAL` | | +| `qd_exp` | | `Real` | | `model==EXPONENTIAL` | | + +#### `model == ZIP` + +- `(pd[i]/pd_nom) = pd_cz[i]*(v[i]/vm_nom)^2 + pd_ci[i]*(v[i]/vm_nom) + pd_cp[i]` +- `(qd[i]/qd_nom) = qd_cz[i]*(v[i]/vm_nom)^2 + qd_ci[i]*(v[i]/vm_nom) + qd_cp[i]` + +| Name | Default | Type | Units | Used | Description | +| -------- | ------- | ------ | ----- | ------------ | ---------------------------- | +| `vm_nom` | | `Real` | volt | `model==ZIP` | Nominal voltage (multiplier) | +| `pd_cz` | | `Real` | | `model==ZIP` | | +| `pd_ci` | | `Real` | | `model==ZIP` | | +| `pd_cp` | | `Real` | | `model==ZIP` | | +| `qd_cz` | | `Real` | | `model==ZIP` | | +| `qd_ci` | | `Real` | | `model==ZIP` | | +| `qd_cp` | | `Real` | | `model==ZIP` | | + +### Generators (`generator`) + +| Name | Default | Type | Units | Used | Description | +| --------------- | -------------------- | ----------------------------- | ----- | --------------------------- | ------------------------------------------------------------------------------------ | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. If `WYE`, `connections[end]=neutral` | +| `vg` | | `Vector{Real}` | volt | `control_mode==ISOCHRONOUS` | Voltage magnitude setpoint | +| `pg_lb` | `zeros(nphases)` | `Vector{Real}` | watt | opf | Lower bound on active power generation per phase, `size=nphases` | +| `pg_ub` | `fill(Inf, nphases)` | `Vector{Real}` | watt | opf | Upper bound on active power generation per phase, `size=nphases` | +| `qg_lb` | `-pg_ub` | `Vector{Real}` | var | opf | Lower bound on reactive power generation per phase, `size=nphases` | +| `qg_ub` | `pg_ub` | `Vector{Real}` | var | opf | Upper bound on reactive power generation per phase, `size=nphases` | +| `pg` | | `Vector{Real}` | watt | solution | Present active power generation per phase, `size=nphases` | +| `qg` | | `Vector{Real}` | var | solution | Present reactive power generation per phase, `size=nphases` | +| `control_mode` | `FREQUENCYDROOP` | `ControlMode` | | | `FREQUENCYDROOP` or `ISOCHRONOUS` | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +#### `generator` Cost Model + +The generator cost model is currently specified by the following fields. + +| Name | Default | Type | Units | Used | Description | +| -------------------- | ----------------- | -------------- | ----- | ---- | --------------------------------------------------------- | +| `cost_pg_model` | `2` | `Int` | | opf | Cost model type, `1` = piecewise-linear, `2` = polynomial | +| `cost_pg_parameters` | `[0.0, 1.0, 0.0]` | `Vector{Real}` | $/MVA | opf | Cost model polynomial | + +### Photovoltaic Systems (`solar`) + +| Name | Default | Type | Units | Used | Description | +| --------------- | --------- | ----------------------------- | ----- | ------------ | ------------------------------------------------------------------------------------ | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. If `WYE`, `connections[end]=neutral` | +| `pg_lb` | | `Vector{Real}` | watt | opf | Lower bound on active power generation per phase, `size=nphases` | +| `pg_ub` | | `Vector{Real}` | watt | opf | Upper bound on active power generation per phase, `size=nphases` | +| `qg_lb` | | `Vector{Real}` | var | opf | Lower bound on reactive power generation per phase, `size=nphases` | +| `qg_ub` | | `Vector{Real}` | var | opf | Upper bound on reactive power generation per phase, `size=nphases` | +| `pg` | | `Vector{Real}` | watt | solution | Present active power generation per phase, `size=nphases` | +| `qg` | | `Vector{Real}` | var | solution | Present reactive power generation per phase, `size=nphases` | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +#### `solar` Cost Model + +The cost model for a photovoltaic system currently matches that of generators. + +| Name | Default | Type | Units | Used | Description | +| -------------------- | ----------------- | -------------- | ----- | ---- | --------------------------------------------------------- | +| `cost_pg_model` | `2` | `Int` | | opf | Cost model type, `1` = piecewise-linear, `2` = polynomial | +| `cost_pg_parameters` | `[0.0, 1.0, 0.0]` | `Vector{Real}` | $/MVA | opf | Cost model polynomial | + +### Wind Turbine Systems (`wind`) + +Wind turbine systems are most closely approximated by induction machines, also known as asynchronous machines. These are not currently supported, but there is plans to support them in the future. + +### Storage (`storage`) + +A storage object is a flexible component that can represent a variety of energy storage objects, like Li-ion batteries, hydrogen fuel cells, flywheels, etc. + +- How to include the inverter model for this? Similar issue as for a PV generator + +| Name | Default | Type | Units | Used | Description | +| ---------------------- | --------- | ----------------------------- | ------- | ------------ | ------------------------------------------------------------------------------------ | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. If `WYE`, `connections[end]=neutral` | +| `energy` | | `Real` | watt-hr | always | Stored energy | +| `energy_ub` | | `Real` | | opf | maximum energy rating | +| `charge_ub` | | `Real` | | opf | maximum charge rating | +| `discharge_ub` | | `Real` | | opf | maximum discharge rating | +| `sm_ub` | | `Vector{Real}` | watt | opf | Power rating, `size=nphases` | +| `cm_ub` | | `Vector{Real}` | amp | opf | Current rating, `size=nphases` | +| `charge_efficiency` | | `Real` | percent | always | charging efficiency (losses) | +| `discharge_efficiency` | | `Real` | percent | always | disharging efficiency (losses) | +| `qs_ub` | | `Vector{Real}` | | opf | Maximum reactive power injection, `size=nphases` | +| `qs_lb` | | `Vector{Real}` | | opf | Minimum reactive power injection, `size=nphases` | +| `rs` | | `Vector{Real}` | ohm | always | converter resistance | +| `xs` | | `Vector{Real}` | ohm | always | converter reactance | +| `pex` | | `Real` | | always | Total active power standby exogenous flow (loss) | +| `qex` | | `Real` | | always | Total reactive power standby exogenous flow (loss) | +| `ps` | | `Vector{Real}` | watt | solution | Present active power injection | +| `qs` | | `Vector{Real}` | var | solution | Present reactive power injection | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +### Voltage Sources (`voltage_source`) + +A voltage source is a source of power at a set voltage magnitude and angle connected to a slack bus. If `rs` or `xs` are not specified, the voltage source is assumed to be lossless, otherwise virtual `branch` and `bus` will be created in the mathematical model to represent the internal losses of the voltage source. + +| Name | Default | Type | Units | Used | Description | +| --------------- | -------------------------------- | ----------------------------- | ------ | ------------ | ------------------------------------------------------------------------------------ | +| `bus` | | `Any` | | always | id of bus connection | +| `connections` | | `Vector{Int}||Vector{String}` | | always | Ordered list of connected conductors, `size=nconductors` | +| `configuration` | `WYE` | `ConnConfig` | | always | `WYE` or `DELTA`. If `WYE`, `connections[end]=neutral` | +| `vm` | `ones(nphases)` | `Vector{Real}` | volt | always | Voltage magnitude set at slack bus, `size=nphases` | +| `va` | `zeros(nphases)` | `Real` | degree | always | Voltage angle offsets at slack bus, applies symmetrically to each phase angle | +| `rs` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | ohm | always | Internal series resistance of voltage source, `size=(nconductors,nconductors)` | +| `xs` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | ohm | always | Internal series reactance of voltage soure, `size=(nconductors,nconductors)` | +| `status` | `ENABLED` | `Status` | | always | `ENABLED` or `DISABLED`. Indicates if component is enabled or disabled, respectively | +| `time_series` | | `Dict{String,Any}` | | multinetwork | Dictionary containing time series parameters. | + +## Data Objects (codes, time series, etc.) + +These objects are referenced by node and edge objects, but are not part of the network themselves, only containing data. + +### Linecodes (`linecode`) + +Linecodes are easy ways to specify properties common to multiple lines. + +| Name | Default | Type | Units | Used | Description | +| ------- | -------------------------------- | -------------- | ---------------- | ------ | ------------------------------------------------------- | +| `rs` | | `Matrix{Real}` | ohm/meter | always | Series resistance, `size=(nconductors,nconductors)` | +| `xs` | | `Matrix{Real}` | ohm/meter | always | Series reactance, `size=(nconductors,nconductors)` | +| `g_fr` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | From-side conductance, `size=(nconductors,nconductors)` | +| `b_fr` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | From-side susceptance, `size=(nconductors,nconductors)` | +| `g_to` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | To-side conductance, `size=(nconductors,nconductors)` | +| `b_to` | `zeros(nconductors,nconductors)` | `Matrix{Real}` | siemens/meter/Hz | always | To-side susceptance, `size=(nconductors,nconductors)` | +| `cm_ub` | | `Vector{Real}` | ampere | always | maximum current per conductor, symmetrically applicable | + +### Transformer Codes (`xfmrcode`) + +Transformer codes are easy ways to specify properties common to multiple transformers + +| Name | Default | Type | Units | Used | Description | +| ---------------- | -------------------------------------- | ---------------------- | ----- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `configurations` | `fill(WYE, nwindings)` | `Vector{ConnConfig}` | | always | `WYE` or `DELTA`. List of configuration for each winding, `size=nwindings` | +| `xsc` | `[0.0]` | `Vector{Real}` | ohm | always | List of short-circuit reactances between each pair of windings; enter as a list of the upper-triangle elements, `size=(nwindings == 2 ? 1 : 3)` | +| `rw` | `zeros(nwindings)` | `Vector{Real}` | ohm | always | List of the winding resistance for each winding, `size=nwindings` | +| `tm_nom` | `ones(nwindings)` | `Vector{Real}` | | always | Nominal tap ratio for the transformer, `size=nwindings` (multiplier) | +| `tm_ub` | | `Vector{Vector{Real}}` | | opf | Maximum tap ratio for each winding and phase, `size=((nphases), nwindings)` (base=`tm_nom`) | +| `tm_lb` | | `Vector{Vector{Real}}` | | opf | Minimum tap ratio for for each winding and phase, `size=((nphases), nwindings)` (base=`tm_nom`) | +| `tm_set` | `fill(fill(1.0, nphases), nwindings)` | `Vector{Vector{Real}}` | | always | Set tap ratio for each winding and phase, `size=((nphases), nwindings)` (base=`tm_nom`) | +| `tm_fix` | `fill(fill(true, nphases), nwindings)` | `Vector{Vector{Bool}}` | | always | Indicates for each winding and phase whether the tap ratio is fixed, `size=((nphases), nwindings)` | + +### Time Series (`time_series`) + +Time series objects are used to specify time series for _e.g._ load or generation forecasts. + +Some parameters for components specified in this document can support a time series by inserting a referece to a `time_series` object into the `time_series` dictionary inside a component under the relevant parameter name. For example, for a `load`, if `pd_nom` is supposed to be a time series, the user would specify `"time_series" => Dict("pd_nom" => time_series_id)` where `time_series_id` is the `id` of an object in `time_series`, and has type `Any`. + +| Name | Default | Type | Units | Used | Description | +| --------- | ------- | -------------- | ----- | ------ | ------------------------------------------------------------------------------------- | +| `time` | | `Vector{Real}` | hour | always | Time points at which values are specified | +| `values` | | `Vector{Real}` | | always | Multipers at each time step given in `time` | +| `offset` | `0` | `Real` | hour | always | Start time offset | +| `replace` | `true` | `Bool` | | always | Indicates to replace with data, instead of multiply. Will only work on non-Array data | + +### Fuses (`fuse`) + +Fuses can be defined on any terminal of any physical component + +| Name | Default | Type | Units | Used | Description | +| ----------------------- | ------- | ----------------------------- | ----- | ---- | ---------------------------------------------------- | +| `component_type` | | `String` | | | | +| `component_id` | | `Any` | | | | +| `terminals` | | `Vector{Int}||Vector{String}` | | | | +| `fuse_curve` | | `Array{Vector{Real},2}` | | | specifies the fuse blowing condition | +| `minimum_melting_curve` | | `Array{Vector{Real},2}` | | | specifies the minimum melting conditions of the fuse | diff --git a/docs/src/eng2math.md b/docs/src/eng2math.md new file mode 100644 index 000000000..3004ae573 --- /dev/null +++ b/docs/src/eng2math.md @@ -0,0 +1,61 @@ +# Engineering to Mathematical Data Model Mapping + +In this document we define the mapping from the engineering data model down to the mathematical data model for each physical component. + +## `bus` + +Buses are parsed into `bus` and potentially `shunt` objects. + +The mathematical bus model contains only lossless connections to ground. All other connections to grounds are converted to equivalent shunts at that bus. For example, take a bus defined as + +`bus_eng = Dict("grounded"=>[4, 5], "rg"=>[1.0, 0.0], "xg"=>[2.0, 0.0],...)`. + +This is equivalent to a shunt `g+im*b = 1/(1.0+im*2.0)` connected to terminal `4`, and a lossless grounding at terminal `5` (since `rg[2]==xg[2]==0.0`). This is mapped to + +`bus_math = Dict("grounded"=>[5], ...)`, + +`shunt_math = Dict("connections"=>[4], "b"=>[b], "g"=>[g]...)`. + +This simplifies the mathematical model, as the modeller does no longer have to consider lossy groundings explicitly. + +## `line` + +Lines are parsed into `branch` objects with `transformer=false` + +## `switch` + +Switches are parsed into `switch`. If there are loss parameters provided (_i.e._ `rs` and/or `xs`) then a virtual branch and virtual bus are created to model the impedance + +## `transformer` + +A transformer can have N windings, each with its own configuration (`delta` or `wye` are supported). This is decomposed to a network of N lossless, two-winding transformers which connect to an internal loss model. The to-winding is always wye-connected, hence we refer to these transformers as 'asymmetric'. + +The internal loss model is a function of + +- the winding resistance `rw`, +- the short-circuit reactance `xsc`, +- the no-load loss properties `noloadloss` (resistive) and magnetizing current `imag` (reactive). + +If all of these are non-zero, this leads to an internal loss model consisting of `N` virtual buses, `(N^2+N)/2` virtual branches, and `1` shunt. These virtual buses and branches are automatically merged and simplified whenever possible; e.g., when all these loss parameters are zero, this simplifies to a single virtual bus, to which all two-winding transformers connect. + +For more detail, please refer to [upcoming technical paper]. #TODO add link to paper + +## `shunt` + +Shunts are parsed directly into `shunt` objects. + +## `load` + +Loads are parsed into `load` objects. See the discussion under the Load Model documentation on the sidebar, for a detailed discussion of the various load models. + +## `generator` + +Generators are parsed into `gen` objects. + +## `solar` + +Solar objects (photovoltaic systems) are parsed into `gen` objects. + +## `voltage_source` + +Voltage sources are parsed into `gen` objects. If loss parameters are specified (_i.e._ `rs` and/or `xs`) then a virtual bus and branch are created to model the internal impedance. diff --git a/docs/src/engineering_model.md b/docs/src/engineering_model.md new file mode 100644 index 000000000..f3963813b --- /dev/null +++ b/docs/src/engineering_model.md @@ -0,0 +1,982 @@ + +# Introduction to the PowerModelsDistribution Data Models + +In this notebook we introduce the engineering data model added to PowerModelsDistribution in version v0.9.0. We will give several examples of how to use this new data model directly, including new transformations that have become easier with its introduction, how to convert it to the the lower-level mathematical model that was previously the only user interface we offered, and how to get various types of results using this new model. + +## Imports + +All commands in this document with no package namespace specified are directly exported by PowerModelsDistribution or already available in Julia base. Any commands that are only avaiable via an external package will be specified by including by using `import`, which will require specifying the originating package before the command, _e.g._ `Ipopt.Optimizer` as you will see below. + + +```julia +using PowerModelsDistribution +``` + +In these examples we will use the following optimization solvers, specified using `optimizer_with_attributes` from JuMP v0.21 + + +```julia +import Ipopt + +ipopt_solver = optimizer_with_attributes(Ipopt.Optimizer, "tol"=>1e-6, "print_level"=>0) +``` + + + + + MathOptInterface.OptimizerWithAttributes(Ipopt.Optimizer, Pair{MathOptInterface.AbstractOptimizerAttribute,Any}[MathOptInterface.RawParameter("tol") => 1.0e-6, MathOptInterface.RawParameter("print_level") => 0]) + + + +## Parsing Data + +Here we give the first example of how to parse data into the `ENGINEERING` data model structure, which is the default data structure type that the user will see without passing additional arguments, as we demonstrate later. + +We start with a 3 bus unbalanced load case provided as a dss file in the `test` folder of the PowerModelsDistribution.jl repository + + +```julia +eng = parse_file("../test/data/opendss/case3_unbalanced.dss") +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "case3_unbalanced.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "case3_unbalanced.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 37 in "case3_unbalanced.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 9 entries: + "voltage_source" => Dict{Any,Any}("source"=>Dict{String,Any}("source_id"=>"vs… + "name" => "3bus_example" + "line" => Dict{Any,Any}("quad"=>Dict{String,Any}("cm_ub"=>[400.0, 4… + "settings" => Dict{String,Any}("sbase_default"=>500.0,"vbases_default"=… + "files" => ["case3_unbalanced.dss"] + "load" => Dict{Any,Any}("l2"=>Dict{String,Any}("source_id"=>"load.l… + "bus" => Dict{Any,Any}("primary"=>Dict{String,Any}("rg"=>Float64[]… + "linecode" => Dict{Any,Any}("556mcm"=>Dict{String,Any}("b_fr"=>[25.4648… + "data_model" => ENGINEERING + + + +Different information and warning messages will be given depending on the input file. In the case above, these messages all related to various parse notifications that arise during a parse of a dss file, and can be safely ignored + +The resulting data structure is a Julia dictionary. The first notable field is `"data_model"` which specifies which data model this data structure corresponds to, in this case `ENGINEERING`. This value is expected to be an `Enum` of type `DataModel` + +The next notable field is `"settings"`, which contains some important default/starting values for the distribution network + + +```julia +eng["settings"] +``` + + + + + Dict{String,Any} with 5 entries: + "sbase_default" => 500.0 + "vbases_default" => Dict("sourcebus"=>0.23094) + "voltage_scale_factor" => 1000.0 + "power_scale_factor" => 1000.0 + "base_frequency" => 50.0 + + + +- `"sbase_default"` is the starting value for the power base, +- `"vbases_default"` is the starting voltage base for the case, and multiple voltage bases can be specified, which would be useful in cases where there are multiple isolated islands with their own generation, +- `"voltage_scale_factor"` is a scaling factor for all voltage values, which in the case of OpenDSS is in kV by default +- `"power_scale_factor"` is a scaling factor for all power values +- `"base_frequency"` is the base frequency of the network in Hz, which is useful to know for mixed frequency networks + +Next we look at the `"bus"` components + + +```julia +eng["bus"] +``` + + + + + Dict{Any,Any} with 3 entries: + "primary" => Dict{String,Any}("rg"=>Float64[],"grounded"=>Int64[],"status"=… + "sourcebus" => Dict{String,Any}("rg"=>Float64[],"grounded"=>Int64[],"status"=… + "loadbus" => Dict{String,Any}("rg"=>[0.0],"grounded"=>[4],"status"=>ENABLED… + + + +We can see there are three buses in this system, identified by ids `"primary"`, `"sourcebus"`, and `"loadbus"`. + +__NOTE__: In Julia, order of Dictionary keys is not fixed, nor does it retain the order in which it was parsed like _e.g._ `Vectors`. + +Identifying components by non-integer names is a new feature of the `ENGINEERING` model, and makes network debugging more straightforward. + +__NOTE__: all names are converted to lowercase on parse from the originating dss file. + +Each bus component has the following properties in the `ENGINEERING` model + + +```julia +eng["bus"]["sourcebus"] +``` + + + + + Dict{String,Any} with 5 entries: + "rg" => Float64[] + "grounded" => Int64[] + "status" => ENABLED + "terminals" => [1, 2, 3] + "xg" => Float64[] + + + +- `"terminals"` indicates which terminals on the bus have active connections +- `"grounded"` indicates which terminals are grounded +- `"rg"` and `"xg"` indicate the grounding resistance and reactance of the ground +- `"status"` indicates whether a bus is `ENABLED` or `DISABLED`, and is specified for every component in the engineering model + +Next, we look at the `"line"` components, which is a generic name for both overhead lines and underground cables, which we do not differentiate between in the nomenclature + + +```julia +eng["line"] +``` + + + + + Dict{Any,Any} with 2 entries: + "quad" => Dict{String,Any}("cm_ub"=>[400.0, 400.0, 400.0],"cm_ub_c"=>[600.0… + "ohline" => Dict{String,Any}("cm_ub"=>[400.0, 400.0, 400.0],"cm_ub_c"=>[600.0… + + + + +```julia +eng["line"]["quad"] +``` + + + + + Dict{String,Any} with 11 entries: + "cm_ub" => [400.0, 400.0, 400.0] + "cm_ub_c" => [600.0, 600.0, 600.0] + "f_connections" => [1, 2, 3] + "length" => 1.0 + "status" => ENABLED + "source_id" => "line.quad" + "t_connections" => [1, 2, 3] + "f_bus" => "primary" + "t_bus" => "loadbus" + "cm_ub_b" => [600.0, 600.0, 600.0] + "linecode" => "4/0quad" + + + +Again, we see components identified by their OpenDSS names. A `"line"` is an edge object, which will always have the following properties: + +- `"f_bus"` +- `"t_bus"` +- `"f_connections"` - list of terminals to which the line is connected on the from-side +- `"t_connections"` - list of terminals to which the line is connected on the to-side + +Here we are also introduced to two important concepts, the `"source_id"`, which is an easy way to identify from where an object originates in the dss file, and a data type element, pointed to by `"linecode"` in this case. + +A data type element is an element that does not represent a real engineering object, but only contains data that one of those real objects can refer to, in this case a linecode, which contains information like line resistance/reactance and conductance/susceptance. + + +```julia +eng["linecode"]["4/0quad"] +``` + + + + + Dict{String,Any} with 6 entries: + "b_fr" => [25.4648 -0.0 -0.0; -0.0 25.4648 -0.0; -0.0 -0.0 25.4648] + "rs" => [0.1167 0.0467 0.0467; 0.0467 0.1167 0.0467; 0.0467 0.0467 0.1167] + "xs" => [0.0667 0.0267 0.0267; 0.0267 0.0667 0.0267; 0.0267 0.0267 0.0667] + "b_to" => [25.4648 -0.0 -0.0; -0.0 25.4648 -0.0; -0.0 -0.0 25.4648] + "g_to" => [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0] + "g_fr" => [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0] + + + +Next, we introduce a node element, the `"load"` object, where we also see the first example of a specification of less than three phases at a time + + +```julia +eng["load"]["l1"] +``` + + + + + Dict{String,Any} with 10 entries: + "source_id" => "load.l1" + "qd_nom" => [3.0] + "status" => ENABLED + "model" => POWER + "connections" => [1, 4] + "vm_nom" => 0.23094 + "pd_nom" => [9.0] + "dispatchable" => NO + "bus" => "loadbus" + "configuration" => WYE + + + +We can see that the length of the Vectors for `"pd_nom"` and `"qd_nom"` are only one, although the number of terminals listed in `"connections"` is two. This is because the connection is WYE, and therefore the final connection is a grounded neutral + +Here we are also introduced to two new Enums, `WYE`, which gives the connection configuration, and `NO` under dispatchable, which indicates that if this case were used in an MLD problem, _i.e._ with `run_mc_mld` that this load would not be sheddable. + +Finally, we show the generation source for this case, which in opendss is a voltage source named `"source"` + + +```julia +eng["voltage_source"]["source"] +``` + + + + + Dict{String,Any} with 8 entries: + "source_id" => "vsource.source" + "rs" => [4.27691e-8 3.96342e-9 3.96342e-9; 3.96342e-9 4.27691e-8 3.9… + "va" => [0.0, -120.0, 120.0] + "status" => ENABLED + "connections" => [1, 2, 3] + "vm" => [0.229993, 0.229993, 0.229993] + "xs" => [1.54178e-7 -1.04497e-9 -1.04497e-9; -1.04497e-9 1.54178e-7 … + "bus" => "sourcebus" + + + +- `"vm"` - specifies the fixed voltage magnitudes per phase at the bus +- `"va"` - specifies the fixed reference angles per phases at the bus +- `"rs"` and `"xs"` specifies internal impedances of the voltage source + +### Importing raw dss properties + +In case there are additional properties that you want to use from dss, it is possible to import those directly into the `ENGINEERING` (and `MATHEMATICAL`) data structure with the `import_all` keyword argument + + +```julia +eng_all = parse_file("../test/data/opendss/case3_unbalanced.dss"; import_all=true) + +eng_all["line"] +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "case3_unbalanced.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "case3_unbalanced.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 37 in "case3_unbalanced.dss" is not supported, skipping. + + + + + + Dict{Any,Any} with 2 entries: + "quad" => Dict{String,Any}("cm_ub"=>[400.0, 400.0, 400.0],"cm_ub_c"=>[600.0… + "ohline" => Dict{String,Any}("cm_ub"=>[400.0, 400.0, 400.0],"cm_ub_c"=>[600.0… + + + +You will note the presence of `"dss"` dictionaries under components, and `"dss_options"` at the root level + +### Time Series Parsing Example + +In the `ENGINEERING` model, we have included the `time_series` data type, which holds all time series data and can be referred to similar to `"linecode"` as demonstrated above. + +Below we can see an example of a parse that includes some time_series components + + +```julia +eng_ts = parse_file("../test/data/opendss/case3_balanced.dss"; time_series="daily") +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "case3_balanced.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 36 in "case3_balanced.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 38 in "case3_balanced.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 10 entries: + "voltage_source" => Dict{Any,Any}("source"=>Dict{String,Any}("source_id"=>"vs… + "name" => "3bus_example" + "line" => Dict{Any,Any}("quad"=>Dict{String,Any}("cm_ub"=>[400.0, 4… + "settings" => Dict{String,Any}("sbase_default"=>100000.0,"vbases_defaul… + "files" => ["case3_balanced.dss"] + "load" => Dict{Any,Any}("l2"=>Dict{String,Any}("model"=>POWER,"conn… + "bus" => Dict{Any,Any}("primary"=>Dict{String,Any}("rg"=>Float64[]… + "linecode" => Dict{Any,Any}("556mcm"=>Dict{String,Any}("b_fr"=>[25.4648… + "time_series" => Dict{Any,Any}("ls1"=>Dict{String,Any}("source_id"=>"loads… + "data_model" => ENGINEERING + + + + +```julia +eng_ts["load"]["l1"]["time_series"] +``` + + + + + Dict{String,Any} with 2 entries: + "qd_nom" => "ls1" + "pd_nom" => "ls1" + + + +You can see that under the actual component, in this case a `"load"`, that there is a `"time_series"` dictionary that contains `ENGINEERING` model variable names and references to the identifiers of a root-level `time_series` object, + + +```julia +eng_ts["time_series"]["ls1"] +``` + + + + + Dict{String,Any} with 5 entries: + "source_id" => "loadshape.ls1" + "time" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + "replace" => true + "values" => [0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.035, 0.04, 0.045, 0.… + "offset" => 0.0 + + + +This feature is useful for building multinetwork data structures, which will be described below in the section on the `MATHEMATICAL` model + +## Running Optimal Power Flow + +In this section we introduce how to run an optimal power flow (opf) in PowerModelsDistribution on an engineering data model + +In order to run an OPF problem you will need + +1. a data model +2. a formulation +3. a solver + +In these examples we will use the `eng` model we worked with above, the `ACPPowerModel`, which is a AC power flow formulation in polar coordinates, and the `ipopt_solver` we already defined above + + +```julia +result = run_mc_opf(eng, ACPPowerModel, ipopt_solver) +``` + + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + + ****************************************************************************** + This program contains Ipopt, a library for large-scale nonlinear optimization. + Ipopt is released as open source code under the Eclipse Public License (EPL). + For more information visit http://projects.coin-or.org/Ipopt + ****************************************************************************** + + + + + + + Dict{String,Any} with 8 entries: + "solve_time" => 3.23448 + "optimizer" => "Ipopt" + "termination_status" => LOCALLY_SOLVED + "dual_status" => FEASIBLE_POINT + "primal_status" => FEASIBLE_POINT + "objective" => 0.0214812 + "solution" => Dict{String,Any}("voltage_source"=>Dict{Any,Any}("sou… + "objective_lb" => -Inf + + + +The result of `run_mc_opf` will be very familiar to those who are already familiar with PowerModels and PowerModelsDistribution. The notable difference will be in the `"solution"` dictionary + + +```julia +result["solution"] +``` + + + + + Dict{String,Any} with 6 entries: + "voltage_source" => Dict{Any,Any}("source"=>Dict{String,Any}("qg_bus"=>[3.177… + "line" => Dict{Any,Any}("quad"=>Dict{String,Any}("qf"=>[3.09488, 3.… + "settings" => Dict{String,Any}("sbase"=>0.5) + "load" => Dict{Any,Any}("l2"=>Dict{String,Any}("qd_bus"=>[0.0, 3.0,… + "bus" => Dict{Any,Any}("primary"=>Dict{String,Any}("va"=>[-0.22425… + "per_unit" => false + + + +Here you can see that the solution comes back out by default into the same data model as is provided by the user to the `run_` command, as well as being in SI units, as opposed to per unit, which is used during the solve. For example, + + +```julia +result["solution"]["bus"]["loadbus"] +``` + + + + + Dict{String,Any} with 2 entries: + "va" => [-0.484238, -120.243, 120.274] + "vm" => [0.222521, 0.226727, 0.225577] + + + +If for some reason you want to return the result in per-unit rather than SI, you can specify this in the `run_` command by + + +```julia +result_pu = run_mc_opf(eng, ACPPowerModel, ipopt_solver; make_si=false) + +result_pu["solution"]["bus"]["loadbus"] +``` + + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + + + + + + Dict{String,Any} with 2 entries: + "va" => [-0.484238, -120.243, 120.274] + "vm" => [0.963546, 0.981757, 0.976779] + + + +### Branch Flow formulations + +Previously, to use a branch flow formulation, such as `SOCNLPUBFPowerModel`, it was required to use a different `run_` command, but now, by using multiple dispatch we have simplified this for the user + + +```julia +result_bf = run_mc_opf(eng, SOCNLPUBFPowerModel, ipopt_solver) +``` + + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + + + + + + Dict{String,Any} with 8 entries: + "solve_time" => 0.172447 + "optimizer" => "Ipopt" + "termination_status" => LOCALLY_SOLVED + "dual_status" => FEASIBLE_POINT + "primal_status" => FEASIBLE_POINT + "objective" => 0.0211303 + "solution" => Dict{String,Any}("voltage_source"=>Dict{Any,Any}("sou… + "objective_lb" => -Inf + + + +### Running Time Series Models + +By default, `time_series` object will be ignored when running a model. To use the time series information you will need to have a multinetwork problem specification + +In the example below we use a test case, which is not exported by default, and therefore requires the specification of the PowerModelsDistribution namespace + + +```julia +result_mn = PowerModelsDistribution._run_mc_mn_opb(eng_ts, NFAPowerModel, ipopt_solver) +``` + + + + + Dict{String,Any} with 8 entries: + "solve_time" => 0.262751 + "optimizer" => "Ipopt" + "termination_status" => LOCALLY_SOLVED + "dual_status" => FEASIBLE_POINT + "primal_status" => FEASIBLE_POINT + "objective" => 0.0 + "solution" => Dict{String,Any}("nw"=>Dict{String,Any}("8"=>Dict{Any… + "objective_lb" => -Inf + + + +## Engineering Model Transformations + +One of the power things about the engineering model is that data transformations are much more simple. Here we illustrate two examples that are currently included in PowerModelsDistribution, but writing your own data transformation functions will be trivial, as we will show + +First, there are several objects that have loss models by default when parsing from dss files, such as voltage sources, transformers, and switches. To remove these loss models, therefore making these components lossless, we can use the included `make_lossess!` function. Here we use a basic 2-winding wye-wye connected transformer case from `test` to illustrate this + + +```julia +eng_ut = parse_file("../test/data/opendss/ut_trans_2w_yy.dss") + +eng_ut["transformer"]["tx1"] +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "ut_trans_2w_yy.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "ut_trans_2w_yy.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 38 in "ut_trans_2w_yy.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 16 entries: + "polarity" => [1, 1] + "sm_nom" => [500.0, 500.0] + "tm_lb" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]] + "connections" => [[1, 2, 3, 4], [1, 2, 3, 4]] + "tm_set" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]] + "tm_step" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]] + "bus" => ["1", "2"] + "configuration" => ConnConfig[WYE, WYE] + "noloadloss" => 0.05 + "xsc" => [0.05] + "source_id" => "transformer.tx1" + "rw" => [0.01, 0.02] + "tm_fix" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]] + "vm_nom" => [11.0, 4.0] + "tm_ub" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]] + "cmag" => 0.11 + + + +We can see that `"noloadloss"`, `"rw"`, and `"cmag"` are non-zero, but if we apply the `make_lossless!` function we can see these parameters are set to zero, effectively eliminating the losses + + +```julia +make_lossless!(eng_ut) + +eng_ut["transformer"]["tx1"] +``` + + + + + Dict{String,Any} with 16 entries: + "polarity" => [1, 1] + "sm_nom" => [500.0, 500.0] + "tm_lb" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]] + "connections" => [[1, 2, 3, 4], [1, 2, 3, 4]] + "tm_set" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]] + "tm_step" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]] + "bus" => ["1", "2"] + "configuration" => ConnConfig[WYE, WYE] + "noloadloss" => 0.0 + "xsc" => [0.0] + "source_id" => "transformer.tx1" + "rw" => [0.0, 0.0] + "tm_fix" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]] + "vm_nom" => [11.0, 4.0] + "tm_ub" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]] + "cmag" => 0.0 + + + +Alternatively, we can apply this function at parse + + +```julia +eng_ut = parse_file("../test/data/opendss/ut_trans_2w_yy.dss"; transformations=[make_lossless!]) + +eng_ut["transformer"]["tx1"] +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "ut_trans_2w_yy.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "ut_trans_2w_yy.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 38 in "ut_trans_2w_yy.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 16 entries: + "polarity" => [1, 1] + "sm_nom" => [500.0, 500.0] + "tm_lb" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]] + "connections" => [[1, 2, 3, 4], [1, 2, 3, 4]] + "tm_set" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]] + "tm_step" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]] + "bus" => ["1", "2"] + "configuration" => ConnConfig[WYE, WYE] + "noloadloss" => 0.0 + "xsc" => [0.0] + "source_id" => "transformer.tx1" + "rw" => [0.0, 0.0] + "tm_fix" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]] + "vm_nom" => [11.0, 4.0] + "tm_ub" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]] + "cmag" => 0.0 + + + +Another transformation function included in PowerModelsDistribution is the `apply_voltage_bounds!` function, which will apply some voltage bounds in SI units, given some percent value, _e.g._ if we want the lower bound on voltage to be `0.9` and upper bound `1.1` after per-unit conversion + + +```julia +apply_voltage_bounds!(eng_ut; vm_lb=0.9, vm_ub=1.1) + +eng_ut["bus"]["2"] +``` + + + + + Dict{String,Any} with 7 entries: + "rg" => [0.0] + "grounded" => [4] + "status" => ENABLED + "terminals" => [1, 2, 3, 4] + "vm_ub" => [2.54034, 2.54034, 2.54034, 2.54034] + "vm_lb" => [2.07846, 2.07846, 2.07846, 2.07846] + "xg" => [0.0] + + + +Alternatively, this can be specified at parse by + + +```julia +eng_ut = parse_file("../test/data/opendss/ut_trans_2w_yy.dss"; transformations=[make_lossless!, (apply_voltage_bounds!, "vm_lb"=>0.9, "vm_ub"=>1.1)]) + +eng_ut["bus"]["2"] +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "ut_trans_2w_yy.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "ut_trans_2w_yy.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 38 in "ut_trans_2w_yy.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 7 entries: + "rg" => [0.0] + "grounded" => [4] + "status" => ENABLED + "terminals" => [1, 2, 3, 4] + "vm_ub" => [2.54034, 2.54034, 2.54034, 2.54034] + "vm_lb" => [2.07846, 2.07846, 2.07846, 2.07846] + "xg" => [0.0] + + + +### Transformations on Multinetworks + +Transformations on Multinetworks should happen __before__ the network is converted into a `MATHEMATICAL` data model, so that they can generally follow the same pattern as shown above and can be seen in the `make_lossless!` and `apply_voltage_bounds!` functions already in PowerModelsDistribution + +## Mathematical Model + +In this section we introduce the mathematical model, which was the previous user-facing model in PowerModelsDistribution, explain how conversions between the model happen in practice, and give an example of how to do this conversion manually + +In practice, unless the user is interested, the conversion between the `ENGINEERING` and `MATHEMATICAL` models should be seemless and invisible to the user. By providing an `ENGINEERING` model to a `run_` command the `run_mc_model` command will know to convert the model to `MATHEMATICAL`, which will be used to the generate the JuMP model that will actually be optimized. Similarly, the solution generated by this optimization will be automatically converted back to the format of the `ENGINEERING` model. + +Let's first take a look at how to convert to the `MATHEMATICAL` model + + +```julia +math = transform_data_model(eng) +``` + + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + + + + + + Dict{String,Any} with 18 entries: + "bus" => Dict{String,Any}("4"=>Dict{String,Any}("grounded"=>Bool[0, 0… + "name" => "3bus_example" + "dcline" => Dict{String,Any}() + "map" => Dict{String,Any}[Dict("unmap_function"=>"_map_math2eng_root!… + "settings" => Dict{String,Any}("sbase_default"=>500.0,"vbases_default"=>Di… + "gen" => Dict{String,Any}("1"=>Dict{String,Any}("pg"=>[0.0, 0.0, 0.0]… + "branch" => Dict{String,Any}("1"=>Dict{String,Any}("br_r"=>[1.09406 0.43… + "storage" => Dict{String,Any}() + "switch" => Dict{String,Any}() + "basekv" => 0.23094 + "baseMVA" => 0.5 + "conductors" => 3 + "per_unit" => true + "data_model" => MATHEMATICAL + "shunt" => Dict{String,Any}() + "transformer" => Dict{String,Any}() + "bus_lookup" => Dict{Any,Int64}("primary"=>1,"sourcebus"=>2,"loadbus"=>3) + "load" => Dict{String,Any}("1"=>Dict{String,Any}("model"=>POWER,"conne… + + + +There are a couple of things to notice right away. First, the data model transform automatically converts the model to per-unit. Second, there are a lot of empty component sets, whereas in the `ENGINEERING` model, only component types that had components in them were listed. In the `MATHEMATICAL` model certain component dictionaries are always expected to exist, and the `eng2math` conversion functions will automatically populate these. + +Next, there are a few unusal fields, such as `"settings"`, which previously didn't exist in the `MATHEMATICAL` model. This is used for the per-unit conversion specifically in PowerModelsDistribution. Also, is the `"map"` field, which is a `Vector` of Dictionaries that enable the conversion back to `ENGINEERING` from `MATHEMATICAL`. Without this it would be impossible to convert back, and in fact only the solution can be converted, because some properties are combined destructively during the conversion to the `MATHEMATICAL` model, and therefore cannot be reverse engineered. However, since the conversion to `MATHEMATICAL` is not in-place, you will always have a copy of `eng` alongside `math`. + +Here is an example of one of the `"map"` entries + + +```julia +math["map"][end] +``` + + + + + Dict{String,Any} with 3 entries: + "to" => ["gen.1", "bus.4", "branch.3"] + "from" => "source" + "unmap_function" => "_map_math2eng_voltage_source!" + + + +Alternatively, the `MATHEMATICAL` model can be returned directly from the `parse_file` command with the `data_model` keyword argument + + +```julia +math = parse_file("../test/data/opendss/case3_unbalanced.dss"; data_model=MATHEMATICAL) +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "case3_unbalanced.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 35 in "case3_unbalanced.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 37 in "case3_unbalanced.dss" is not supported, skipping. + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + + + + + + Dict{String,Any} with 18 entries: + "bus" => Dict{String,Any}("4"=>Dict{String,Any}("grounded"=>Bool[0, 0… + "name" => "3bus_example" + "dcline" => Dict{String,Any}() + "map" => Dict{String,Any}[Dict("unmap_function"=>"_map_math2eng_root!… + "settings" => Dict{String,Any}("sbase_default"=>500.0,"vbases_default"=>Di… + "gen" => Dict{String,Any}("1"=>Dict{String,Any}("pg"=>[0.0, 0.0, 0.0]… + "branch" => Dict{String,Any}("1"=>Dict{String,Any}("br_r"=>[1.09406 0.43… + "storage" => Dict{String,Any}() + "switch" => Dict{String,Any}() + "basekv" => 0.23094 + "baseMVA" => 0.5 + "conductors" => 3 + "per_unit" => true + "data_model" => MATHEMATICAL + "shunt" => Dict{String,Any}() + "transformer" => Dict{String,Any}() + "bus_lookup" => Dict{Any,Int64}("primary"=>1,"sourcebus"=>2,"loadbus"=>3) + "load" => Dict{String,Any}("1"=>Dict{String,Any}("model"=>POWER,"conne… + + + +### Multinetworks + +In this subsection we cover parsing into a multinetwork data structure, which is a structure that only exists in the `MATHEMATICAL` model + +For those unfamiliar, the InfrastructureModels family of packages has a feature called multinetworks, which is useful for, among other things, running optimization problems on time series type problems. + +Multinetwork data structures are formatted like so +mn = Dict{String,Any}( + "multinetwork" => true, + "nw" => Dict{String,Any}( + "1" => Dict{String,Any}( + "bus" => Dict{String,Any}(), + ... + ), + ... + ), + ... +) +To automatically create a multinetwork structure from an engineering model that contains `time_series` elements, we can use the `build_multinetwork` keyword argument in `transform_data_model` + + +```julia +math_mn = transform_data_model(eng_ts; build_multinetwork=true) +``` + + + + + Dict{String,Any} with 8 entries: + "name" => "10 replicates of 3bus_example" + "map" => Dict{String,Any}[Dict("unmap_function"=>"_map_math2eng_root… + "settings" => Dict{String,Any}("sbase_default"=>100000.0,"vbases_default"… + "bus_lookup" => Dict{Any,Int64}("primary"=>1,"sourcebus"=>2,"loadbus"=>3) + "multinetwork" => true + "nw" => Dict{String,Any}("3"=>Dict{String,Any}("bus"=>Dict{String,A… + "per_unit" => true + "data_model" => MATHEMATICAL + + + +Alternatively, we can use `parse_file` with the `build_multinetwork` keyword argument combined with `data_model=MATHEMATICAL` + + +```julia +math_mn = parse_file("../test/data/opendss/case3_balanced.dss"; build_multinetwork=true, data_model=MATHEMATICAL) +``` + + [info | PowerModels]: Circuit has been reset with the "clear" on line 1 in "case3_balanced.dss" + [warn | PowerModels]: Command "calcvoltagebases" on line 36 in "case3_balanced.dss" is not supported, skipping. + [warn | PowerModels]: Command "solve" on line 38 in "case3_balanced.dss" is not supported, skipping. + + + + + + Dict{String,Any} with 8 entries: + "name" => "10 replicates of 3bus_example" + "map" => Dict{String,Any}[Dict("unmap_function"=>"_map_math2eng_root… + "settings" => Dict{String,Any}("sbase_default"=>100000.0,"vbases_default"… + "bus_lookup" => Dict{Any,Int64}("primary"=>1,"sourcebus"=>2,"loadbus"=>3) + "multinetwork" => true + "nw" => Dict{String,Any}("3"=>Dict{String,Any}("bus"=>Dict{String,A… + "per_unit" => true + "data_model" => MATHEMATICAL + + + +### Running `MATHEMATICAL` models + +There is very little difference from the user point-of-view in running `MATHEMATICAL` models other than the results will not be automatically converted back to the the format of the `ENGINEERING` model + + +```julia +result_math = run_mc_opf(math, ACPPowerModel, ipopt_solver) + +result_math["solution"] +``` + + + + + Dict{String,Any} with 7 entries: + "baseMVA" => 0.5 + "branch" => Dict{String,Any}("1"=>Dict{String,Any}("qf"=>[0.00618975, 0.0… + "gen" => Dict{String,Any}("1"=>Dict{String,Any}("qg_bus"=>[0.00635515,… + "load" => Dict{String,Any}("1"=>Dict{String,Any}("qd_bus"=>[0.0, 0.006,… + "conductors" => 3 + "bus" => Dict{String,Any}("4"=>Dict{String,Any}("va"=>[0.0, -2.0944, 2… + "per_unit" => true + + + +It is also possible to manually convert the solution back to the `ENGINEERING` format, provided you have the __map__ + + +```julia +sol_eng = transform_solution(result_math["solution"], math) +``` + + + + + Dict{String,Any} with 6 entries: + "voltage_source" => Dict{Any,Any}("source"=>Dict{String,Any}("qg_bus"=>[3.177… + "line" => Dict{Any,Any}("quad"=>Dict{String,Any}("qf"=>[3.09488, 3.… + "settings" => Dict{String,Any}("sbase"=>0.5) + "load" => Dict{Any,Any}("l2"=>Dict{String,Any}("qd_bus"=>[0.0, 3.0,… + "bus" => Dict{Any,Any}("primary"=>Dict{String,Any}("va"=>[-0.22425… + "per_unit" => false + + + +#### Running `MATHEMATICAL` Multinetworks + +As with the `ENGINEERING` example of running a multinetwork problem, you will need a multinetwork problem specification, and as with the previous single `MATHEMATICAL` network example above, we only obtain the `MATHEMATICAL` solution, and can transform the solution in the same manner as before + + +```julia +result_math_mn = PowerModelsDistribution._run_mc_mn_opb(math_mn, NFAPowerModel, ipopt_solver) + +result_math_mn["solution"]["nw"]["1"] +``` + + + + + Dict{String,Any} with 3 entries: + "baseMVA" => 100.0 + "gen" => Dict{String,Any}("1"=>Dict{String,Any}("pg"=>[0.0, 0.0, 0.0])) + "conductors" => 3 + + + + +```julia +sol_eng_mn = transform_solution(result_math_mn["solution"], math_mn) + +sol_eng_mn["nw"]["1"] +``` + + + + + Dict{Any,Any} with 2 entries: + "voltage_source" => Dict{Any,Any}("source"=>Dict{String,Any}("pg"=>[0.0, 0.0,… + "settings" => Dict{String,Any}("sbase"=>100.0) + + + +## Building the JuMP Model + +In some cases the user will want to directly build the JuMP model, which would traditionally be done with `instantiate_model` from PowerModels. In order to facilitate using the `ENGINEERING` model we have introduced `instantiate_mc_model` to aid in the generation of the JuMP model. `instantiate_mc_model` will automatically convert the data model to MATHEMATICAL if necessary (notifying the user of the conversion), and pass the MATHEMATICAL model off to PowerModels' `instantiate_model` with `ref_add_arcs_transformer!` in `ref_extensions`, which is a required ref extension for PowerModelsDistribution. + + +```julia +pm_eng = instantiate_mc_model(eng, NFAPowerModel, build_mc_opf) + +print(pm_eng.model) +``` + + [info | PowerModels]: Converting ENGINEERING data model to MATHEMATICAL first to build JuMP model + [warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0] + Min 0.5 0_pg_1[1] + 0.5 0_pg_1[2] + 0.5 0_pg_1[3] + Subject to + 0_(3,4,2)_p[1] - 0_pg_1[1] = 0.0 + 0_(3,4,2)_p[2] - 0_pg_1[2] = 0.0 + 0_(3,4,2)_p[3] - 0_pg_1[3] = 0.0 + 0_(2,2,1)_p[1] - 0_(3,4,2)_p[1] = 0.0 + 0_(2,2,1)_p[2] - 0_(3,4,2)_p[2] = 0.0 + 0_(2,2,1)_p[3] - 0_(3,4,2)_p[3] = 0.0 + -0_(1,1,3)_p[1] = -0.018000000000000002 + -0_(1,1,3)_p[2] = -0.012 + -0_(1,1,3)_p[3] = -0.012 + 0_(1,1,3)_p[1] - 0_(2,2,1)_p[1] = 0.0 + 0_(1,1,3)_p[2] - 0_(2,2,1)_p[2] = 0.0 + 0_(1,1,3)_p[3] - 0_(2,2,1)_p[3] = 0.0 + + +This is equivalent to + + +```julia +import PowerModels + +pm_math = PowerModels.instantiate_model(math, NFAPowerModel, build_mc_opf; ref_extensions=[ref_add_arcs_transformer!]) + +print(pm_math.model) +``` + + Min 0.5 0_pg_1[1] + 0.5 0_pg_1[2] + 0.5 0_pg_1[3] + Subject to + 0_(3,4,2)_p[1] - 0_pg_1[1] = 0.0 + 0_(3,4,2)_p[2] - 0_pg_1[2] = 0.0 + 0_(3,4,2)_p[3] - 0_pg_1[3] = 0.0 + 0_(2,2,1)_p[1] - 0_(3,4,2)_p[1] = 0.0 + 0_(2,2,1)_p[2] - 0_(3,4,2)_p[2] = 0.0 + 0_(2,2,1)_p[3] - 0_(3,4,2)_p[3] = 0.0 + -0_(1,1,3)_p[1] = -0.018000000000000002 + -0_(1,1,3)_p[2] = -0.012 + -0_(1,1,3)_p[3] = -0.012 + 0_(1,1,3)_p[1] - 0_(2,2,1)_p[1] = 0.0 + 0_(1,1,3)_p[2] - 0_(2,2,1)_p[2] = 0.0 + 0_(1,1,3)_p[3] - 0_(2,2,1)_p[3] = 0.0 + + +## Conclusion + +This concludes the introduction to the `ENGINEERING` data model and conversion to the `MATHEMATICAL` model. We hope that you will find this new data model abstraction easy to use and simple to understand diff --git a/docs/src/enums.md b/docs/src/enums.md new file mode 100644 index 000000000..44bc15712 --- /dev/null +++ b/docs/src/enums.md @@ -0,0 +1,71 @@ +# PowerModelsDistribution Enum Types + +Within the PowerModelsDistribution Engineering Model we have included the use of Enums. Here we document the fields for which Enums are expected and the possible Enums available + +## Data Model + +Any place in PowerModelsDistribution that calls for specifying the `data_model`, either in function calls or the `"data_model"` field inside the data structure itself, will expect a `DataModel` type + +```julia +@enum DataModel ENGINEERING MATHEMATICAL DSS +``` + +The `DSS` data model is an output of `parse_dss`, and is an untranslated raw parse of a DSS file. This Enum exists for use by `count_nodes`, where the method to count the number of active nodes is different between all three models + +## Component statuses + +All `"status"` fields in the `ENGINEERING` model expect a `Status` type: + +```julia +@enum Status ENABLED DISABLED +``` + +## Connection Configurations + +All `"configuration"` fields in the `ENGINEERING` model expect a `ConnConfig` type: + +```julia +@enum ConnConfig WYE DELTA +``` + +## Load Models + +For `load` objects, the `"model"` field expects a `LoadModel` type to specify the type of load model to use: + +```julia +@enum LoadModel POWER CURRENT IMPEDANCE EXPONENTIAL ZIP +``` + +where `POWER` indicates constant power, `CURRENT` indicates constant current, `IMPEDANCE` indicates constant impedance, `EXPONENTIAL` indicates an exponential load model, and `ZIP` indicates a ZIP model + +## Shunt Models + +For `shunt` objects, the `"model"` field expects a `ShuntModel` type to specify the origin of the shunt object, which is important for transient analysis: + +```julia +@enum ShuntModel GENERIC CAPACITOR REACTOR +``` + +## Switch States + +For `switch` objects, the `"state"` field expects a `SwitchState` type to specify whether the switch is currently open or closed: + +```julia +@enum SwitchState OPEN CLOSED +``` + +## Dispatchable Components + +Some components can be dispatchable, _e.g._ if a switch is dispatchable that means it is free to open or close, but if not then it is fixed in place, or if a load is dispatchable it implies that it can be shed in a `run_mc_mld` problem: + +```julia +@enum Dispatchable NO YES +``` + +## Generator Control Modes + +For generator objects, the `"control_mode"` field expects a `ControlMode` type to specify whether the generator is operating in an isochronous mode (_i.e._ is frequency forming) or droop mode (_i.e._ is frequency following): + +```julia +@enum ControlMode FREQUENCYDROOP ISOCHRONOUS +``` diff --git a/docs/src/data-formats.md b/docs/src/external-data-formats.md similarity index 59% rename from docs/src/data-formats.md rename to docs/src/external-data-formats.md index f7b2f8b4a..f02bc50b3 100644 --- a/docs/src/data-formats.md +++ b/docs/src/external-data-formats.md @@ -1,4 +1,4 @@ -# Data Formats +# External Data Formats ## OpenDSS @@ -7,38 +7,46 @@ PowerModelsDistribution supports parsing OpenDSS format files. In particular, we - Line - Load - Generator -- Capactior +- Capactior (shunt capacitors only) - Reactor - Transformer - Linecode +- Xfmrcode +- Loadshape +- XYCurve - Circuit - VSource - PVSystem - Storage -Of those, a subset of configurations are converted into a PowerModelsDistribution internal data model, namely +Of those, a subset of configurations are converted into a PowerModelsDistribution internal data model, namely: -- Branch (from Lines (incl. Linecodes), Reactors) -- Transformer (arbitrary winding, all connections except zig-zag) -- Generator (from Generators, PVSystems) -- Load (incl. support for Const. Power, Const. Impedance, Const. Current models) -- Shunt (from Capacitors and Reactors) -- Storage +### Edge Elements + +- line (from lines and line reactors) +- transformer (arbitrary winding, all connections except zig-zag) +- switch (from lines w/ switch=y) + +### Node Elements + +- generator +- voltage_source +- solar (from PVSystem) +- load (incl. support for constant POWER, constant IMPEDANCE, constant CURRENT, and EXPONENTIAL models) +- shunt (from shunt capacitors and shunt reactors) +- storage -Except for a small subset, in general, commands are not support, e.g. `solve` or `calcvoltagebases` (this is done automatically on parse in PowerModelsDistribution). We support the following commands +### Data Elements -- `clear` -- `redirect` -- `compile` -- `set` -- `buscoords` -- `new` +- linecode +- xfmrcode +- time_series (from loadshapes) Several notes about the specific design choices w.r.t. OpenDSS are explained below. ### Circuit -The default connection to the transmission system is modeled as an ideal voltage source in OpenDSS; we chosen to model the trunk connection as a loosely bounded generator at a reference bus which is connected to the distribution network via a branch in order to model the inherent impedance of the voltage source. +The default connection to the transmission system is modeled as an ideal voltage source named "source" in OpenDSS, which is connected by default to a node named "sourcebus", but this can be changed. ### Lines @@ -52,6 +60,6 @@ Unfortunately, in the OpenDSS format, multi-phase transformers with different ta Capacitors and reactors are supported as shunts, although shunts to ground via delta connections are not yet supported. Furthermore, generic reactors are not supported, only those whose second terminal is wye connected to ground (default for unspecified second terminal). Reactors are also supported as a resistanceless line if their second terminal is connected, but only for topological continuity of the network. -## Matlab +## PowerModelsDistribution JSON -We also include a matlab-base format similar in conception to Matpower. This format is in development and details will come later. +You can export a PowerModelsDistribution data structure to a JSON file using the `print_file` command and parse one in using the `parse_file` command diff --git a/docs/src/formulation-details.md b/docs/src/formulation-details.md index 7ae886f66..72036c9b7 100644 --- a/docs/src/formulation-details.md +++ b/docs/src/formulation-details.md @@ -1,46 +1,58 @@ # Three-phase formulation details - ## `AbstractACPModel` -Real-valued formulation from: -- Formulation without shunts: Mahdad, B., Bouktir, T., & Srairi, K. (2006). A three-phase power flow modelization: a tool for optimal location and control of FACTS devices in unbalanced power systems. In IEEE Industrial Electronics IECON (pp. 2238–2243). +Real-valued formulation from: +- Formulation without shunts: Mahdad, B., Bouktir, T., & Srairi, K. (2006). A three-phase power flow modelization: a tool for optimal location and control of FACTS devices in unbalanced power systems. In IEEE Industrial Electronics IECON (pp. 2238–2243). ## `AbstractDCPModel` + Applying all of the standard DC linearization tricks to the `AbstractACPModel` ## `SOCWRModel` + Applying the standard BIM voltage cross-product (sine and cosine) substitution tricks to `AbstractACPModel` results immediately in a SOC formulation. ## `SDPUBFModel` + The BFM SDP relaxation as described in: -- Gan, L., & Low, S. H. (2014). Convex relaxations and linear approximation for optimal power flow in multiphase radial networks. In PSSC (pp. 1–9). Wroclaw, Poland. https://doi.org/10.1109/PSCC.2014.7038399 + +- Gan, L., & Low, S. H. (2014). Convex relaxations and linear approximation for optimal power flow in multiphase radial networks. In PSSC (pp. 1–9). Wroclaw, Poland. [doi:10.1109/PSCC.2014.7038399](https://doi.org/10.1109/PSCC.2014.7038399) + Note that this formulation is complex-valued and additional steps are needed to implement this in JuMP. ## `SOCNLPUBFModel` + The starting point is `SDPUBFModel`. The SDP constraint can be relaxed to a set of SOC constraints, starting from either the real or complex form of the matrix on which the PSD-ness constraint is applied. -- Kim, S., Kojima, M., & Yamashita, M. (2003). Second order cone programming relaxation of a positive semidefinite constraint. Optimization Methods and Software, 18(5), 535–541. https://doi.org/10.1080/1055678031000148696 -- Andersen, M. S., Hansson, A., & Vandenberghe, L. (2014). Reduced-complexity semidefinite relaxations of optimal power flow problems. IEEE Trans. Power Syst., 29(4), 1855–1863. +- Kim, S., Kojima, M., & Yamashita, M. (2003). Second order cone programming relaxation of a positive semidefinite constraint. Optimization Methods and Software, 18(5), 535–541. [doi:10.1080/1055678031000148696](https://doi.org/10.1080/1055678031000148696) +- Andersen, M. S., Hansson, A., & Vandenberghe, L. (2014). Reduced-complexity semidefinite relaxations of optimal power flow problems. IEEE Trans. Power Syst., 29(4), 1855–1863. ## `SOCConicUBFModel` -See `SOCNLPUBFModel` +See `SOCNLPUBFModel` ## `LPUBFFullModel` + Matrix formulation that generalizes `simplified DistFlow equations`, as introduced in : -- Gan, L., & Low, S. H. (2014). Convex relaxations and linear approximation for optimal power flow in multiphase radial networks. In PSSC (pp. 1–9). Wroclaw, Poland. https://doi.org/10.1109/PSCC.2014.7038399 + +- Gan, L., & Low, S. H. (2014). Convex relaxations and linear approximation for optimal power flow in multiphase radial networks. In PSSC (pp. 1–9). Wroclaw, Poland. [doi:10.1109/PSCC.2014.7038399](https://doi.org/10.1109/PSCC.2014.7038399) Note that this formulation is complex-valued and additional steps are needed to implement this in JuMP. ## `LPUBFDiagModel` + This formulation has originally been developed by Sankur et al. -- Sankur, M. D., Dobbe, R., Stewart, E., Callaway, D. S., & Arnold, D. B. (2016). A linearized power flow model for optimization in unbalanced distribution systems. https://arxiv.org/abs/1606.04492v2 + +- Sankur, M. D., Dobbe, R., Stewart, E., Callaway, D. S., & Arnold, D. B. (2016). A linearized power flow model for optimization in unbalanced distribution systems. [arXiv:1606.04492v2](https://arxiv.org/abs/1606.04492v2) This formulation is here cast as only considering the diagonal elements defined in `LPUBFFullModel`, which furthermore leads to the imaginary part of the lifted node voltage variable W being redundant and substituted out. ## `LPLinUBFModel` + Scalar reformulation of: -- Sankur, M. D., Dobbe, R., Stewart, E., Callaway, D. S., & Arnold, D. B. (2016). A linearized power flow model for optimization in unbalanced distribution systems. https://arxiv.org/abs/1606.04492v2 + +- Sankur, M. D., Dobbe, R., Stewart, E., Callaway, D. S., & Arnold, D. B. (2016). A linearized power flow model for optimization in unbalanced distribution systems. [arXiv:1606.04492v2](https://arxiv.org/abs/1606.04492v2) + This formulation was already derived in real variables and parameters. diff --git a/docs/src/formulations.md b/docs/src/formulations.md index 6761db8b0..84c29c6c2 100644 --- a/docs/src/formulations.md +++ b/docs/src/formulations.md @@ -1,73 +1,80 @@ # Network Formulations ## Type Hierarchy -We begin with the top of the hierarchy, where we can distinguish between conic and non-conic power flow models. + +PowerModelsDistribution shares a rich model type hierarchy with PowerModels. The relevant abstract models from PowerModels are documented here for context. At the top of the type hierarchy, starting in PowerModels, we can distinguish between conic, active power only, and branch flow models: + +```julia +abstract type PowerModels.AbstractConicModel <: PowerModels.AbstractPowerModel end +abstract type PowerModels.AbstractActivePowerModel <: PowerModels.AbstractPowerModel end +abstract type PowerModels.AbstractBFModel <: PowerModels.AbstractPowerModel end +abstract type PowerModels.AbstractBFQPModel <: PowerModels.AbstractBFModel end +abstract type PowerModels.AbstractBFConicModel <: PowerModels.AbstractBFModel end +const PowerModels.AbstractConicModels = Union{PowerModels.AbstractConicModel, PowerModels.AbstractBFConicModel} +``` + +Several nonlinear (non-convex) models are available at the top level: + ```julia -PowerModels.AbstractConicModels = Union{PowerModels.AbstractConicModel, PowerModels.AbstractBFConicModel} -PowerModels.AbstractConicModel <: PowerModels.AbstractPowerModel -PowerModels.AbstractBFModel <: PowerModels.AbstractPowerModel -PowerModels.AbstractBFQPModel <: PowerModels.AbstractBFModel -PowerModels.AbstractBFConicModel <: PowerModels.AbstractBFModel +abstract type PowerModels.AbstractACPModel <: PowerModels.AbstractPowerModel end +abstract type PowerModels.AbstractACRModel <: PowerModels.AbstractPowerModel end +abstract type PowerModels.AbstractIVRModel <: PowerModels.AbstractACRModel end ``` -We begin with the top of the hierarchy, where we can distinguish between AC and DC power flow models. +In PowerModelsDistribution, the following relaxations are available under these hierarchies: + ```julia -PowerModels.AbstractACPModel <: PowerModels.AbstractPowerModel -PowerModels.AbstractDCPModel <: PowerModels.AbstractPowerModel -PowerModelsDistribution.AbstractNLPUBFModel <: PowerModels.AbstractBFQPModel -PowerModelsDistribution.AbstractConicUBFModel <: PowerModels.AbstractBFConicModel -PowerModelsDistribution.AbstractLPUBFModel <: PowerModelsDistribution.AbstractNLPUBFModel +abstract type PowerModelsDistribution.AbstractNLPUBFModel <: PowerModels.AbstractBFQPModel end +abstract type PowerModelsDistribution.AbstractConicUBFModel <: PowerModels.AbstractBFConicModel end +const PowerModelsDistribution.AbstractUBFModels = Union{PowerModelsDistribution.AbstractNLPUBFModel, PowerModelsDistribution.AbstractConicUBFModel} + +abstract type PowerModelsDistribution.SDPUBFModel <: PowerModelsDistribution.AbstractConicUBFModel end +abstract type PowerModelsDistribution.SDPUBFKCLMXModel <: PowerModelsDistribution.SDPUBFModel end +abstract type PowerModelsDistribution.SOCNLPUBFModel <: PowerModelsDistribution.AbstractNLPUBFModel end +abstract type PowerModelsDistribution.SOCConicUBFModel <: PowerModelsDistribution.AbstractConicUBFModel end +const PowerModelsDistribution.SOCUBFModels = Union{PowerModelsDistribution.SOCNLPUBFModel, PowerModelsDistribution.SOCConicUBFModel} ``` -From there, different Models are possible: +where `UBF` is an unbalanced variant of the __Branch Flow__ models from PowerModels. Models which do not contain `UBF` in their name are __Bus Injection__ Models _e.g._ `AbstractACPModel`. Finally, some linear unbalanced power flow models are available under the following hierarchy: + ```julia -#Bus injection models: -PowerModels.AbstractACPModel <: PowerModels.AbstractPowerModel -PowerModels.AbstractDCPModel <: PowerModels.AbstractPowerModel - -#Branch flow models: -PowerModelsDistribution.SDPUBFModel <: PowerModelsDistribution.AbstractConicUBFModel -PowerModelsDistribution.SOCNLPUBFModel <: PowerModelsDistribution.AbstractNLPUBFModel -PowerModelsDistribution.SOCConicUBFModel <: PowerModelsDistribution.AbstractConicUBFModel - -PowerModelsDistribution.LPLinUBFModel <: PowerModels.AbstractBFModel -PowerModelsDistribution.LPUBFFullModel <: PowerModelsDistribution.AbstractLPUBFModel -PowerModelsDistribution.LPUBFDiagModel <: PowerModelsDistribution.AbstractLPUBFModel +abstract type PowerModels.AbstractDCPModel <: PowerModels.AbstractActivePowerModel end +abstract type PowerModels.AbstractNFAModel <: PowerModels.AbstractDCPModel end +abstract type PowerModelsDistribution.AbstractLPUBFModel <: PowerModelsDistribution.AbstractNLPUBFModel end +abstract type PowerModelsDistribution.LPUBFDiagModel <: PowerModelsDistribution.AbstractLPUBFModel end +const PowerModelsDistribution.LinDist3FlowModel = PowerModelsDistribution.LPUBFDiagModel ``` ## Power Models + Each of these Models can be used as the type parameter for a PowerModel: + ```julia mutable struct PowerModels.ACPPowerModel <: PowerModels.AbstractACPModel PowerModels.@pm_fields end +mutable struct PowerModels.ACRPowerModel <: PowerModels.AbstractACRModel PowerModels.@pm_fields end mutable struct PowerModels.DCPPowerModel <: PowerModels.AbstractDCPModel PowerModels.@pm_fields end - -mutable struct PowerModels.SOCWRPowerModel <: PowerModels.SOCWRModel PowerModels.@pm_fields end +mutable struct PowerModels.NFAPowerModel <: PowerModels.AbstractNFAModel PowerModels.@pm_fields end mutable struct PowerModelsDistribution.SDPUBFPowerModel <: PowerModelsDistribution.SDPUBFModel PowerModels.@pm_fields end +mutable struct PowerModelsDistribution.SDPUBFKCLMXPowerModel <: PowerModelsDistribution.SDPUBFKCLMXModel PowerModels.@pm_fields end + mutable struct PowerModelsDistribution.SOCNLPUBFPowerModel <: PowerModelsDistribution.SOCNLPUBFModel PowerModels.@pm_fields end mutable struct PowerModelsDistribution.SOCConicUBFPowerModel <: PowerModelsDistribution.SOCConicUBFModel PowerModels.@pm_fields end -mutable struct PowerModelsDistribution.LPUBFFullPowerModel <: PowerModelsDistribution.LPUBFFullModel PowerModels.@pm_fields end mutable struct PowerModelsDistribution.LPUBFDiagPowerModel <: PowerModelsDistribution.LPUBFDiagModel PowerModels.@pm_fields end -mutable struct PowerModelsDistribution.LPLinUBFPowerModel <: PowerModelsDistribution.LPLinUBFModel PowerModels.@pm_fields end -``` - -## Union Types - -To support both conic and quadratically-constrained formulation variants for the unbalanced branch flow model, the union type `AbstractUBFModels` is defined. These formulations extend `AbstractBFModel` and are therefore also `AbstractWModels` (as defined in PowerModels proper). - -```julia -AbstractUBFModels = Union{AbstractNLPUBFModel, AbstractConicUBFModel} +const PowerModelsDistribution.LinDist3FlowPowerModel = PowerModelsDistribution.LPUBFDiagPowerModel ``` ## Optimization problem classes -- NLP (nonconvex): ACPPowerModel -- SDP: SDPUBFPowerModel -- SOC(-representable): SOCWRPowerModel, SOCNLPUBFPowerModel, SOCConicUBFPowerModel -- Linear: LPUBFFullPowerModel, LPUBFDiagPowerModel, LPLinUBFPowerModel, DCPPowerModel +- NLP (nonconvex): ACPPowerModel, ACRPowerModel, IVRPowerModel +- SDP: SDPUBFPowerModel, SDPUBFKCLMXPowerModel +- SOC(-representable): SOCNLPUBFPowerModel, SOCConicUBFPowerModel +- Linear: LPUBFDiagPowerModel (LinDist3FlowPowerModel), DCPPowerModel, NFAPowerModel ## Matrix equations versus scalar equations + JuMP supports vectorized syntax, but not for nonlinear constraints. Therefore, certain formulations must be implemented in a scalar fashion. Other formulations can be written as matrix (in)equalities. The current implementations are categorized as follows: -- Scalar: ACPPowerModel, DCPPowerModel, LPLinUBFPowerModel, SOCWRPowerModel -- Matrix: SDPUBFPowerModel, SOCNLPUBFPowerModel, SOCConicUBFPowerModel, LPUBFFullPowerModel, LPUBFDiagPowerModel + +- Scalar: ACPPowerModel, ACRPowerModel, IVRPowerModel, DCPPowerModel, NFAPowerMoel +- Matrix: SDPUBFPowerModel, SDPUBFKCLMXPowerModel, SOCNLPUBFPowerModel, SOCConicUBFPowerModel, LPUBFDiagPowerModel diff --git a/docs/src/load-model.md b/docs/src/load-model.md index 3873133ea..df181f816 100644 --- a/docs/src/load-model.md +++ b/docs/src/load-model.md @@ -24,7 +24,7 @@ The general, exponential load model is defined as $$P^d_i = P^{d,0}_i \left(\frac{V^d_i}{V^{d,0}_i}\right)^{\alpha_i} = a_i \left(V^d_i\right)^{\alpha_i}$$ $$Q^d_i = Q^{d,0}_i \left(\frac{V^d_i}{V^{d,0}_i}\right)^{\beta_i} = b_i \left(V^d_i\right)^{\beta_i}.$$ -This might seem overly complicated, but occurs in distribution network data due to experimental model fitting of loads. There are a few cases which get a special name: constant power ($\alpha=\beta=0$), constant current ($\alpha=\beta=1$), and constant impedance ($\alpha=\beta=2$). +There are a few cases which get a special name: constant power ($\alpha=\beta=0$), constant current ($\alpha=\beta=1$), and constant impedance ($\alpha=\beta=2$). ## Wye-connected Loads A wye-connected load connects between a set of phases $\mathcal{P}$ and a neutral conductor $n$. The voltage as seen by each individual load is then diff --git a/docs/src/quickguide.md b/docs/src/quickguide.md index 7e03117a8..9edff17a4 100644 --- a/docs/src/quickguide.md +++ b/docs/src/quickguide.md @@ -1,5 +1,6 @@ # Quick Start Guide -Once PowerModelsDistribution is installed, Ipopt is installed, and a network data file (e.g. `"case5_c_r_a.m"` or `"case3_unbalanced.dss"` in the package folder under `./test/data`) has been acquired, an unbalanced AC Optimal Power Flow can be executed with, + +Once PowerModelsDistribution is installed, Ipopt is installed, and a network data file (e.g. `"case3_unbalanced.dss"` in the package folder under `./test/data`) has been acquired, an unbalanced AC Optimal Power Flow can be executed with, ```julia using PowerModelsDistribution @@ -8,9 +9,29 @@ using Ipopt run_ac_mc_opf("case3_unbalanced.dss", with_optimizer(Ipopt.Optimizer)) ``` +## Parsing files + +To parse an OpenDSS file into PowerModelsDistribution's default `ENGINEERING` format, use the `parse_file` command + +```julia +eng = parse_file("case3_unbalanced.dss") +``` + +To examine the `MATHEMATICAL` model it is possible to transform the data model using the `transform_data_model` command, but this step is not necessary to run a problem. + +```julia +math = transform_data_model(eng) +``` + ## Getting Results -The run commands in PowerModelsDistribution return detailed results data in the form of a dictionary. Results dictionaries from either Matpower-style `.m` or OpenDSS' `.dss` files will be identical in format. This dictionary can be saved for further processing as follows, +The run commands in PowerModelsDistribution return detailed results data in the form of a dictionary. This dictionary can be saved for further processing as follows, + +```julia +result = run_ac_mc_opf(eng, with_optimizer(Ipopt.Optimizer)) +``` + +Alternatively, you can pass the file path string directly: ```julia result = run_ac_mc_opf("case3_unbalanced.dss", with_optimizer(Ipopt.Optimizer)) @@ -18,30 +39,33 @@ result = run_ac_mc_opf("case3_unbalanced.dss", with_optimizer(Ipopt.Optimizer)) ## Accessing Different Formulations -The function "run_ac_mc_opf" is a shorthands for a more general formulation-independent OPF execution, "run_mc_opf". +The function "run_ac_mc_opf" is a short-hand for a more general formulation-independent OPF execution, "run_mc_opf". For example, `run_ac_mc_opf` is equivalent to, ```julia -using PowerModels -run_mc_opf("case3_unbalanced.dss", ACPPowerModel, with_optimizer(Ipopt.Optimizer)) +run_mc_opf(eng, ACPPowerModel, with_optimizer(Ipopt.Optimizer)) ``` -Note that PowerModels needs to be loaded to access formulations which are extended by PowerModelsDistribution, here "ACPPowerModel". The PowerModel "ACPPowerModel" indicates an AC formulation in polar coordinates. This more generic `run_mc_opf()` allows one to solve an OPF problem with any power network formulation implemented in PowerModels or PowerModelsDistribution. For example, the SDP relaxation of unbalanced Optimal Power Flow (branch flow model) can be run with, +`ACPPowerModel` indicates an AC formulation in polar coordinates. This more generic `run_mc_opf()` allows one to solve an OPF problem with any power network formulation implemented in PowerModels or PowerModelsDistribution. For example, the SDPUBFPowerModel relaxation of unbalanced Optimal Power Flow (branch flow model) can be run with, ```julia using SCS -run_mc_opf_bf("case3_unbalanced.dss", SDPUBFPowerModel, with_optimizer(SCS.Optimizer)) +run_mc_opf(eng, SDPUBFPowerModel, with_optimizer(SCS.Optimizer)) ``` Note that you have to use a SDP-capable solver, e.g. the open-source solver SCS, to solve SDP models. ## Inspecting the Formulation -The following example demonstrates how to break a `run_mc_opf` call into seperate model building and solving steps. This allows inspection of the JuMP model created by PowerModelsDistribution for the AC-OPF problem, +The following example demonstrates how to break a `run_mc_opf` call into seperate model building and solving steps. This allows inspection of the JuMP model created by PowerModelsDistribution for the AC-OPF problem. Note that the `MATHEMATICAL` model must be passed to `instantiate_model`, so the data model must either be transformed with `transform_data_model` or parsed directly to a `MATHEMATICAL` model using the `data_model` keyword argument: ```julia -data = PowerModelsDistribution.parse_file("case3_unbalanced.dss") -pm = PowerModels.instantiate_model(data, ACPPowerModel, PowerModelsDistribution.build_mc_opf; multiconductor=true, ref_extensions=[ref_add_arcs_trans!]) +math = parse_file("case3_unbalanced.dss"; data_model=MATHEMATICAL) +pm = instantiate_model(math, ACPPowerModel, build_mc_opf; ref_extensions=[ref_add_arcs_trans!]) print(pm.model) optimize_model!(pm, optimizer=with_optimizer(Ipopt.Optimizer)) ``` + +## Examples + +More examples of working with the engineering data model can be found in the `/examples` folder of the PowerModelsDistribution.jl repository. diff --git a/docs/src/specifications.md b/docs/src/specifications.md index 8d64946b3..288f1876b 100644 --- a/docs/src/specifications.md +++ b/docs/src/specifications.md @@ -1,225 +1,70 @@ # Problem Specifications +In addition to the standard power flow `run_mc_pf`, and optimal power flow `run_mc_opf`, there are several notable problem specifications included in PowerModelsDistribution + ## Optimal Power Flow (OPF) with On-Load Tap Changers (OLTC) This problem is identical to `mc_opf`, except that all transformers are now modelled as on-load tap changers (OLTCs). Each phase has an individual tap ratio, which can be either variable or fixed, as specified in the data model. -### Objective +### OLTC Objective ```julia objective_min_fuel_cost(pm) ``` -### Variables +### OLTC Variables ```julia variable_mc_voltage(pm) variable_mc_branch_flow(pm) -for c in PMs.conductor_ids(pm) - PMs.variable_generation(pm, cnd=c) - PMs.variable_dcline_flow(pm, cnd=c) +for c in conductor_ids(pm) + PowerModels.variable_generation(pm, cnd=c) + PowerModels.variable_dcline_flow(pm, cnd=c) end variable_mc_transformer_flow(pm) variable_mc_oltc_tap(pm) ``` -### Constraints +### OLTC Constraints ```julia constraint_mc_model_voltage(pm) -for i in PMs.ids(pm, :ref_buses) +for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end -for i in PMs.ids(pm, :bus), c in PMs.conductor_ids(pm) +for i in ids(pm, :bus), c in conductor_ids(pm) constraint_mc_power_balance(pm, i, cnd=c) end -for i in PMs.ids(pm, :branch) +for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) - for c in PMs.conductor_ids(pm) - PMs.constraint_voltage_angle_difference(pm, i, cnd=c) + for c in conductor_ids(pm) + PowerModels.constraint_voltage_angle_difference(pm, i, cnd=c) - PMs.constraint_thermal_limit_from(pm, i, cnd=c) - PMs.constraint_thermal_limit_to(pm, i, cnd=c) + PowerModels.constraint_thermal_limit_from(pm, i, cnd=c) + PowerModels.constraint_thermal_limit_to(pm, i, cnd=c) end end -for i in PMs.ids(pm, :dcline), c in PMs.conductor_ids(pm) - PMs.constraint_dcline(pm, i, cnd=c) +for i in ids(pm, :dcline), c in conductor_ids(pm) + PowerModels.constraint_dcline(pm, i, cnd=c) end -for i in PMs.ids(pm, :transformer) +for i in ids(pm, :transformer) constraint_mc_oltc(pm, i) end ``` -## Optimal Power Flow (OPF) with Load Models (LM) - -Unlike `mc_opf`, which models all loads as constant power loads, this problem specification additionally supports loads proportional to the voltage magnitude (a.k.a. constant current) and the square of the voltage magnitude (a.k.a. constant impedance). Each load now has associated active and reactive power variables. In `mc_opf`, loads are directly added as parameters in KCL. - -### Objective - -```julia -objective_min_fuel_cost(pm) -``` - -### Variables - -```julia -variable_mc_voltage(pm) -variable_mc_branch_flow(pm) - -for c in PMs.conductor_ids(pm) - PMs.variable_generation(pm, cnd=c) - PMs.variable_dcline_flow(pm, cnd=c) -end -variable_mc_transformer_flow(pm) -variable_mc_oltc_tap(pm) -``` - -### Constraints - -```julia -constraint_mc_model_voltage(pm) - -for i in PMs.ids(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) -end - -for i in PMs.ids(pm, :bus) - constraint_mc_power_balance_load(pm, i) -end - -for id in PMs.ids(pm, :load) - model = PMs.ref(pm, pm.cnw, :load, id, "model") - if model=="constant_power" - constraint_mc_load_power_setpoint(pm, id) - elseif model=="proportional_vm" - constraint_mc_load_power_prop_vm(pm, id) - elseif model=="proportional_vmsqr" - constraint_mc_load_power_prop_vmsqr(pm, id) - else - Memento.@error(LOGGER, "Unknown model $model for load $id.") - end -end - -for i in PMs.ids(pm, :branch) - constraint_mc_ohms_yt_from(pm, i) - constraint_mc_ohms_yt_to(pm, i) - - for c in PMs.conductor_ids(pm) - PMs.constraint_voltage_angle_difference(pm, i, cnd=c) - - PMs.constraint_thermal_limit_from(pm, i, cnd=c) - PMs.constraint_thermal_limit_to(pm, i, cnd=c) - end -end - -for i in PMs.ids(pm, :dcline), c in PMs.conductor_ids(pm) - PMs.constraint_dcline(pm, i, cnd=c) -end - -for i in PMs.ids(pm, :transformer) - constraint_mc_transformer(pm, i) -end -``` - -## Power Flow (PF) with Load Models (LM) - -Unlike `mc_pf`, which models all loads as constant power loads, this problem specification additionally supports loads proportional to the voltage magnitude (a.k.a. constant current) and the square of the voltage magnitude (a.k.a. constant impedance). Each load now has associated active and reactive power variables. In `mc_pf`, loads are directly added as parameters in KCL. - -### Variables - -```julia -variable_mc_voltage(pm, bounded=false) -variable_mc_branch_flow(pm, bounded=false) -variable_mc_transformer_flow(pm, bounded=false) -variable_mc_load(pm) - -for c in PMs.conductor_ids(pm) - PMs.variable_generation(pm, bounded=false, cnd=c) - PMs.variable_dcline_flow(pm, bounded=false, cnd=c) -end -``` - -### Constraints - -```julia -constraint_mc_model_voltage(pm, bounded=false) - -for (i,bus) in PMs.ref(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) - - for c in PMs.conductor_ids(pm) - @assert bus["bus_type"] == 3 - PMs.constraint_voltage_magnitude_setpoint(pm, i, cnd=c) - end -end - -for (i,bus) in PMs.ref(pm, :bus) - constraint_mc_power_balance_load(pm, i) - - for c in PM.conductor_ids(pm) - # PV Bus Constraints - if length(PMs.ref(pm, :bus_gens, i)) > 0 && !(i in PMs.ids(pm,:ref_buses)) - # this assumes inactive generators are filtered out of bus_gens - @assert bus["bus_type"] == 2 - - PMs.constraint_voltage_magnitude_setpoint(pm, i, cnd=c) - for j in PMs.ref(pm, :bus_gens, i) - PMs.constraint_active_gen_setpoint(pm, j, cnd=c) - end - end - end -end - -for id in PMs.ids(pm, :load) - model = PMs.ref(pm, pm.cnw, :load, id, "model") - if model=="constant_power" - constraint_mc_load_power_setpoint(pm, id) - elseif model=="proportional_vm" - constraint_mc_load_power_prop_vm(pm, id) - elseif model=="proportional_vmsqr" - constraint_mc_load_power_prop_vmsqr(pm, id) - else - Memento.@error(LOGGER, "Unknown model $model for load $id.") - end -end - -for i in PMs.ids(pm, :branch) - constraint_mc_ohms_yt_from(pm, i) - constraint_mc_ohms_yt_to(pm, i) -end - -for (i,dcline) in PMs.ref(pm, :dcline), c in PMs.conductor_ids(pm) - PMs.constraint_active_dcline_setpoint(pm, i, cnd=c) - - f_bus = PMs.ref(pm, :bus)[dcline["f_bus"]] - if f_bus["bus_type"] == 1 - PMs.constraint_voltage_magnitude_setpoint(pm, f_bus["index"], cnd=c) - end - - t_bus = PMs.ref(pm, :bus)[dcline["t_bus"]] - if t_bus["bus_type"] == 1 - PMs.constraint_voltage_magnitude_setpoint(pm, t_bus["index"], cnd=c) - end -end - -for i in PMs.ids(pm, :transformer) - constraint_mc_transformer(pm, i) -end -``` - ## Minimal Load Delta (MLD) Problem Specification Load shed (continuous) problem. See "Relaxations of AC Maximal Load Delivery for Severe Contingency Analysis" by C. Coffrin _et al._ (DOI: [10.1109/TPWRS.2018.2876507](https://ieeexplore.ieee.org/document/8494809)) for single-phase case. -### Variables +### MLD Variables ```math \begin{align} @@ -232,7 +77,7 @@ Load shed (continuous) problem. See "Relaxations of AC Maximal Load Delivery for \end{align} ``` -### Objective +### MLD Objective ```math \begin{align} @@ -240,6 +85,7 @@ Load shed (continuous) problem. See "Relaxations of AC Maximal Load Delivery for \sum_{\substack{i\in N,c\in C}}{10 \left (1-z^v_i \right )} + \sum_{\substack{i\in L,c\in C}}{10 \omega_{i,c}\left |\Re{\left (S^d_i\right )}\right |\left ( 1-z^d_i \right ) } + \sum_{\substack{i\in H,c\in C}}{\left | \Re{\left (S^s_i \right )}\right | \left (1-z^s_i \right ) } + \sum_{\substack{i\in G,c\in C}}{\Delta^g_i } + \sum_{\substack{i\in B,c\in C}}{\Delta^b_i} \right ) \end{align} ``` + where ```math @@ -251,7 +97,7 @@ where \end{align} ``` -### Constraints +### MLD Constraints ```math \begin{align} diff --git a/examples/eng_model_helper_functions.ipynb b/examples/eng_model_helper_functions.ipynb new file mode 100644 index 000000000..89c96110d --- /dev/null +++ b/examples/eng_model_helper_functions.ipynb @@ -0,0 +1,332 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Building Engineering Data Models with Helper Functions\n", + "\n", + "In this notebook we will demonstrate an easy way to start building new distribution network models in the engineering format using new helper functions add in PowerModelsDistribution v0.9" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "┌ Info: Precompiling PowerModelsDistribution [d7431456-977f-11e9-2de3-97ff7677985e]\n", + "└ @ Base loading.jl:1260\n" + ] + } + ], + "source": [ + "using PowerModelsDistribution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we need a optimizer. In this case we will use Ipopt and initialize it with JuMP's `optimizer_with_attributes`, which we have exported from PowerModelsDistribution by default for the user" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MathOptInterface.OptimizerWithAttributes(Ipopt.Optimizer, Pair{MathOptInterface.AbstractOptimizerAttribute,Any}[MathOptInterface.RawParameter(\"print_level\") => 0, MathOptInterface.RawParameter(\"tol\") => 1.0e-6])" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "using LinearAlgebra: diagm\n", + "import Ipopt\n", + "\n", + "ipopt_solver = optimizer_with_attributes(Ipopt.Optimizer, \"print_level\"=>0, \"tol\"=>1e-6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Initialize an empty `ENGINEERING` model" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 3 entries:\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>1.0,\"vbases_default\"=>Dict{…\n", + " \"per_unit\" => false\n", + " \"data_model\" => ENGINEERING" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng = Model()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this block we build a three bus network, with neutrals grounded at the source and loads.\n", + "\n", + "We start with buses, with the sourcebus and loadbus having 4 terminals, with the last terminal grounded.\n", + "\n", + "Then we add a generation source, in this case a voltage source, which is `WYE` configured by default, and therefore expects the last conductor to be a grounded neutral\n", + "\n", + "We add two three phase lines to connect the buses `sourcebus`, `primary`, and `loadbus`. Note that none of the lines have a neutral conductor.\n", + "\n", + "We finally add a three-phase load a the `loadbus` bus, but note again that this is a `WYE` configured load, and like the voltage source, this implies that the last conductor is a grounded neutral for the purposes of kron reduction (which is required by default until explicit 4-wire modeling is added to PowerModelsDistribution)\n", + " \n", + "Lastly, we need to define the default vbase of the system at the `sourcebus`" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 7 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"source_id\"=>\"vo…\n", + " \"line\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"xs\"=>[0.2 0.2 …\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>1.0,\"vbases_default\"=>D…\n", + " \"load\" => Dict{Any,Any}(\"balanced\"=>Dict{String,Any}(\"source_id\"=>\"…\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"source_id\"=>\"b…\n", + " \"per_unit\" => false\n", + " \"data_model\" => ENGINEERING" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "add_bus!(eng, \"sourcebus\"; terminals=[1,2,3,4], grounded=[4])\n", + "add_bus!(eng, \"primary\"; terminals=[1,2,3])\n", + "add_bus!(eng, \"loadbus\"; terminals=[1,2,3,4], grounded=[4])\n", + "\n", + "add_voltage_source!(eng, \"source\", \"sourcebus\", [1,2,3,4]; vm=[1, 1, 1])\n", + "\n", + "add_line!(eng, \"trunk\", \"sourcebus\", \"primary\", [1,2,3], [1,2,3])\n", + "add_line!(eng, \"primary\", \"primary\", \"loadbus\", [1,2,3], [1,2,3])\n", + "\n", + "add_load!(eng, \"balanced\", \"loadbus\", [1,2,3,4]; pd_nom=[5, 5, 5], qd_nom=[1, 1, 1])\n", + "\n", + "add_vbase_default!(eng, \"sourcebus\", 1)\n", + "\n", + "eng" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Running this case with OPF gives the results below" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.001, 0.0]\u001b[39m\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "******************************************************************************\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"solve_time\" => 3.70515\n", + " \"optimizer\" => \"Ipopt\"\n", + " \"termination_status\" => LOCALLY_SOLVED\n", + " \"dual_status\" => FEASIBLE_POINT\n", + " \"primal_status\" => FEASIBLE_POINT\n", + " \"objective\" => 0.0150047\n", + " \"solution\" => Dict{String,Any}(\"voltage_source\"=>Dict{Any,Any}(\"sou…\n", + " \"objective_lb\" => -Inf" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_mc_opf(eng, ACPPowerModel, ipopt_solver)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the following example, we provide examples of a wider range of component types that can be added using helper functions to give a flavor of what is possible in PowerModelsDistribution v0.9" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 11 entries:\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"source_id\"=>\"b…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>1.0,\"vbases_default\"=>D…\n", + " \"switch\" => Dict{Any,Any}(\"breaker\"=>Dict{String,Any}(\"source_id\"=>\"s…\n", + " \"generator\" => Dict{Any,Any}(\"secondary\"=>Dict{String,Any}(\"source_id\"=>…\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"source_id\"=>\"vo…\n", + " \"line\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"xs\"=>[0.2 0.2 …\n", + " \"per_unit\" => false\n", + " \"data_model\" => ENGINEERING\n", + " \"transformer\" => Dict{Any,Any}(\"tx1\"=>Dict{String,Any}(\"source_id\"=>\"trans…\n", + " \"shunt\" => Dict{Any,Any}(\"cap\"=>Dict{String,Any}(\"source_id\"=>\"shunt…\n", + " \"load\" => Dict{Any,Any}(\"tload\"=>Dict{String,Any}(\"source_id\"=>\"loa…" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng2 = deepcopy(eng)\n", + "\n", + "add_bus!(eng2, \"ttbus\"; terminals=[1,2,3,4], grounded=[4])\n", + "\n", + "add_transformer!(eng2, \"tx1\", \"sourcebus\", \"ttbus\", [1,2,3,4], [1,2,3,4])\n", + "\n", + "add_bus!(eng2, \"loadbus2\"; terminals=[1,2,3,4], grounded=[4])\n", + "\n", + "add_switch!(eng2, \"breaker\", \"ttbus\", \"loadbus2\", [1,2,3], [1,2,3])\n", + "\n", + "add_load!(eng2, \"tload\", \"loadbus2\", [1,2,3,4]; pd_nom=[5, 5, 5], qd_nom=[1, 1, 1])\n", + "\n", + "add_generator!(eng2, \"secondary\", \"loadbus2\", [1,2,3,4]; cost_pg_parameters=[0.0, 1.2, 0])\n", + "\n", + "add_shunt!(eng2, \"cap\", \"loadbus2\", [1,2,3,4]; bs=diagm(0=>fill(1, 3)))\n", + "\n", + "eng2" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: active generators found at bus 4, updating to bus type from 1 to 2\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.0012, 0.0]\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Updated generator 2 cost function with order 3 to a function of order 2: [0.001, 0.0]\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"solve_time\" => 0.087898\n", + " \"optimizer\" => \"Ipopt\"\n", + " \"termination_status\" => LOCALLY_SOLVED\n", + " \"dual_status\" => FEASIBLE_POINT\n", + " \"primal_status\" => FEASIBLE_POINT\n", + " \"objective\" => -83.3003\n", + " \"solution\" => Dict{String,Any}(\"voltage_source\"=>Dict{Any,Any}(\"sou…\n", + " \"objective_lb\" => -Inf" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result2 = run_mc_opf(eng2, ACPPowerModel, ipopt_solver)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.4.1", + "language": "julia", + "name": "julia-1.4" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.4.1" + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/engineering_model.ipynb b/examples/engineering_model.ipynb new file mode 100644 index 000000000..3749d51f4 --- /dev/null +++ b/examples/engineering_model.ipynb @@ -0,0 +1,1586 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to the PowerModelsDistribution Data Models\n", + "\n", + "In this notebook we introduce the engineering data model added to PowerModelsDistribution in version v0.9.0. We will give several examples of how to use this new data model directly, including new transformations that have become easier with its introduction, how to convert it to the the lower-level mathematical model that was previously the only user interface we offered, and how to get various types of results using this new model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports\n", + "\n", + "All commands in this document with no package namespace specified are directly exported by PowerModelsDistribution or already available in Julia base. Any commands that are only avaiable via an external package will be specified by including by using `import`, which will require specifying the originating package before the command, _e.g._ `Ipopt.Optimizer` as you will see below." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "using PowerModelsDistribution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In these examples we will use the following optimization solvers, specified using `optimizer_with_attributes` from JuMP v0.21" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MathOptInterface.OptimizerWithAttributes(Ipopt.Optimizer, Pair{MathOptInterface.AbstractOptimizerAttribute,Any}[MathOptInterface.RawParameter(\"tol\") => 1.0e-6, MathOptInterface.RawParameter(\"print_level\") => 0])" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import Ipopt\n", + "\n", + "ipopt_solver = optimizer_with_attributes(Ipopt.Optimizer, \"tol\"=>1e-6, \"print_level\"=>0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parsing Data\n", + "\n", + "Here we give the first example of how to parse data into the `ENGINEERING` data model structure, which is the default data structure type that the user will see without passing additional arguments, as we demonstrate later.\n", + "\n", + "We start with a 3 bus unbalanced load case provided as a dss file in the `test` folder of the PowerModelsDistribution.jl repository" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"case3_unbalanced.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 37 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 9 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"source_id\"=>\"vs…\n", + " \"name\" => \"3bus_example\"\n", + " \"line\" => Dict{Any,Any}(\"quad\"=>Dict{String,Any}(\"cm_ub\"=>[400.0, 4…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>500.0,\"vbases_default\"=…\n", + " \"files\" => [\"case3_unbalanced.dss\"]\n", + " \"load\" => Dict{Any,Any}(\"l2\"=>Dict{String,Any}(\"source_id\"=>\"load.l…\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"rg\"=>Float64[]…\n", + " \"linecode\" => Dict{Any,Any}(\"556mcm\"=>Dict{String,Any}(\"b_fr\"=>[25.4648…\n", + " \"data_model\" => ENGINEERING" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng = parse_file(\"../test/data/opendss/case3_unbalanced.dss\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Different information and warning messages will be given depending on the input file. In the case above, these messages all related to various parse notifications that arise during a parse of a dss file, and can be safely ignored\n", + "\n", + "The resulting data structure is a Julia dictionary. The first notable field is `\"data_model\"` which specifies which data model this data structure corresponds to, in this case `ENGINEERING`. This value is expected to be an `Enum` of type `DataModel`\n", + "\n", + "The next notable field is `\"settings\"`, which contains some important default/starting values for the distribution network" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 5 entries:\n", + " \"sbase_default\" => 500.0\n", + " \"vbases_default\" => Dict(\"sourcebus\"=>0.23094)\n", + " \"voltage_scale_factor\" => 1000.0\n", + " \"power_scale_factor\" => 1000.0\n", + " \"base_frequency\" => 50.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"settings\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- `\"sbase_default\"` is the starting value for the power base,\n", + "- `\"vbases_default\"` is the starting voltage base for the case, and multiple voltage bases can be specified, which would be useful in cases where there are multiple isolated islands with their own generation,\n", + "- `\"voltage_scale_factor\"` is a scaling factor for all voltage values, which in the case of OpenDSS is in kV by default\n", + "- `\"power_scale_factor\"` is a scaling factor for all power values\n", + "- `\"base_frequency\"` is the base frequency of the network in Hz, which is useful to know for mixed frequency networks\n", + "\n", + "Next we look at the `\"bus\"` components" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{Any,Any} with 3 entries:\n", + " \"primary\" => Dict{String,Any}(\"rg\"=>Float64[],\"grounded\"=>Int64[],\"status\"=…\n", + " \"sourcebus\" => Dict{String,Any}(\"rg\"=>Float64[],\"grounded\"=>Int64[],\"status\"=…\n", + " \"loadbus\" => Dict{String,Any}(\"rg\"=>[0.0],\"grounded\"=>[4],\"status\"=>ENABLED…" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"bus\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see there are three buses in this system, identified by ids `\"primary\"`, `\"sourcebus\"`, and `\"loadbus\"`. \n", + "\n", + "__NOTE__: In Julia, order of Dictionary keys is not fixed, nor does it retain the order in which it was parsed like _e.g._ `Vectors`. \n", + "\n", + "Identifying components by non-integer names is a new feature of the `ENGINEERING` model, and makes network debugging more straightforward. \n", + "\n", + "__NOTE__: all names are converted to lowercase on parse from the originating dss file.\n", + "\n", + "Each bus component has the following properties in the `ENGINEERING` model" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 5 entries:\n", + " \"rg\" => Float64[]\n", + " \"grounded\" => Int64[]\n", + " \"status\" => ENABLED\n", + " \"terminals\" => [1, 2, 3]\n", + " \"xg\" => Float64[]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"bus\"][\"sourcebus\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- `\"terminals\"` indicates which terminals on the bus have active connections\n", + "- `\"grounded\"` indicates which terminals are grounded\n", + "- `\"rg\"` and `\"xg\"` indicate the grounding resistance and reactance of the ground\n", + "- `\"status\"` indicates whether a bus is `ENABLED` or `DISABLED`, and is specified for every component in the engineering model\n", + "\n", + "Next, we look at the `\"line\"` components, which is a generic name for both overhead lines and underground cables, which we do not differentiate between in the nomenclature" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{Any,Any} with 2 entries:\n", + " \"quad\" => Dict{String,Any}(\"cm_ub\"=>[400.0, 400.0, 400.0],\"cm_ub_c\"=>[600.0…\n", + " \"ohline\" => Dict{String,Any}(\"cm_ub\"=>[400.0, 400.0, 400.0],\"cm_ub_c\"=>[600.0…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"line\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 11 entries:\n", + " \"cm_ub\" => [400.0, 400.0, 400.0]\n", + " \"cm_ub_c\" => [600.0, 600.0, 600.0]\n", + " \"f_connections\" => [1, 2, 3]\n", + " \"length\" => 1.0\n", + " \"status\" => ENABLED\n", + " \"source_id\" => \"line.quad\"\n", + " \"t_connections\" => [1, 2, 3]\n", + " \"f_bus\" => \"primary\"\n", + " \"t_bus\" => \"loadbus\"\n", + " \"cm_ub_b\" => [600.0, 600.0, 600.0]\n", + " \"linecode\" => \"4/0quad\"" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"line\"][\"quad\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, we see components identified by their OpenDSS names. A `\"line\"` is an edge object, which will always have the following properties:\n", + "\n", + "- `\"f_bus\"`\n", + "- `\"t_bus\"`\n", + "- `\"f_connections\"` - list of terminals to which the line is connected on the from-side\n", + "- `\"t_connections\"` - list of terminals to which the line is connected on the to-side\n", + "\n", + "Here we are also introduced to two important concepts, the `\"source_id\"`, which is an easy way to identify from where an object originates in the dss file, and a data type element, pointed to by `\"linecode\"` in this case.\n", + "\n", + "A data type element is an element that does not represent a real engineering object, but only contains data that one of those real objects can refer to, in this case a linecode, which contains information like line resistance/reactance and conductance/susceptance." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 6 entries:\n", + " \"b_fr\" => [25.4648 -0.0 -0.0; -0.0 25.4648 -0.0; -0.0 -0.0 25.4648]\n", + " \"rs\" => [0.1167 0.0467 0.0467; 0.0467 0.1167 0.0467; 0.0467 0.0467 0.1167]\n", + " \"xs\" => [0.0667 0.0267 0.0267; 0.0267 0.0667 0.0267; 0.0267 0.0267 0.0667]\n", + " \"b_to\" => [25.4648 -0.0 -0.0; -0.0 25.4648 -0.0; -0.0 -0.0 25.4648]\n", + " \"g_to\" => [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0]\n", + " \"g_fr\" => [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"linecode\"][\"4/0quad\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we introduce a node element, the `\"load\"` object, where we also see the first example of a specification of less than three phases at a time" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 10 entries:\n", + " \"source_id\" => \"load.l1\"\n", + " \"qd_nom\" => [3.0]\n", + " \"status\" => ENABLED\n", + " \"model\" => POWER\n", + " \"connections\" => [1, 4]\n", + " \"vm_nom\" => 0.23094\n", + " \"pd_nom\" => [9.0]\n", + " \"dispatchable\" => NO\n", + " \"bus\" => \"loadbus\"\n", + " \"configuration\" => WYE" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"load\"][\"l1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the length of the Vectors for `\"pd_nom\"` and `\"qd_nom\"` are only one, although the number of terminals listed in `\"connections\"` is two. This is because the connection is WYE, and therefore the final connection is a grounded neutral\n", + "\n", + "Here we are also introduced to two new Enums, `WYE`, which gives the connection configuration, and `NO` under dispatchable, which indicates that if this case were used in an MLD problem, _i.e._ with `run_mc_mld` that this load would not be sheddable.\n", + "\n", + "Finally, we show the generation source for this case, which in opendss is a voltage source named `\"source\"`" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"source_id\" => \"vsource.source\"\n", + " \"rs\" => [4.27691e-8 3.96342e-9 3.96342e-9; 3.96342e-9 4.27691e-8 3.9…\n", + " \"va\" => [0.0, -120.0, 120.0]\n", + " \"status\" => ENABLED\n", + " \"connections\" => [1, 2, 3]\n", + " \"vm\" => [0.229993, 0.229993, 0.229993]\n", + " \"xs\" => [1.54178e-7 -1.04497e-9 -1.04497e-9; -1.04497e-9 1.54178e-7 …\n", + " \"bus\" => \"sourcebus\"" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng[\"voltage_source\"][\"source\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- `\"vm\"` - specifies the fixed voltage magnitudes per phase at the bus\n", + "- `\"va\"` - specifies the fixed reference angles per phases at the bus\n", + "- `\"rs\"` and `\"xs\"` specifies internal impedances of the voltage source" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Importing raw dss properties\n", + "\n", + "In case there are additional properties that you want to use from dss, it is possible to import those directly into the `ENGINEERING` (and `MATHEMATICAL`) data structure with the `import_all` keyword argument" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"case3_unbalanced.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 37 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{Any,Any} with 2 entries:\n", + " \"quad\" => Dict{String,Any}(\"cm_ub\"=>[400.0, 400.0, 400.0],\"cm_ub_c\"=>[600.0…\n", + " \"ohline\" => Dict{String,Any}(\"cm_ub\"=>[400.0, 400.0, 400.0],\"cm_ub_c\"=>[600.0…" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_all = parse_file(\"../test/data/opendss/case3_unbalanced.dss\"; import_all=true)\n", + "\n", + "eng_all[\"line\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You will note the presence of `\"dss\"` dictionaries under components, and `\"dss_options\"` at the root level" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Time Series Parsing Example\n", + "\n", + "In the `ENGINEERING` model, we have included the `time_series` data type, which holds all time series data and can be referred to similar to `\"linecode\"` as demonstrated above.\n", + "\n", + "Below we can see an example of a parse that includes some time_series components" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"case3_balanced.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 36 in \"case3_balanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 38 in \"case3_balanced.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 10 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"source_id\"=>\"vs…\n", + " \"name\" => \"3bus_example\"\n", + " \"line\" => Dict{Any,Any}(\"quad\"=>Dict{String,Any}(\"cm_ub\"=>[400.0, 4…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>100000.0,\"vbases_defaul…\n", + " \"files\" => [\"case3_balanced.dss\"]\n", + " \"load\" => Dict{Any,Any}(\"l2\"=>Dict{String,Any}(\"model\"=>POWER,\"conn…\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"rg\"=>Float64[]…\n", + " \"linecode\" => Dict{Any,Any}(\"556mcm\"=>Dict{String,Any}(\"b_fr\"=>[25.4648…\n", + " \"time_series\" => Dict{Any,Any}(\"ls1\"=>Dict{String,Any}(\"source_id\"=>\"loads…\n", + " \"data_model\" => ENGINEERING" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ts = parse_file(\"../test/data/opendss/case3_balanced.dss\"; time_series=\"daily\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 2 entries:\n", + " \"qd_nom\" => \"ls1\"\n", + " \"pd_nom\" => \"ls1\"" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ts[\"load\"][\"l1\"][\"time_series\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see that under the actual component, in this case a `\"load\"`, that there is a `\"time_series\"` dictionary that contains `ENGINEERING` model variable names and references to the identifiers of a root-level `time_series` object, " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 5 entries:\n", + " \"source_id\" => \"loadshape.ls1\"\n", + " \"time\" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]\n", + " \"replace\" => true\n", + " \"values\" => [0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.035, 0.04, 0.045, 0.…\n", + " \"offset\" => 0.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ts[\"time_series\"][\"ls1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This feature is useful for building multinetwork data structures, which will be described below in the section on the `MATHEMATICAL` model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running Optimal Power Flow\n", + "\n", + "In this section we introduce how to run an optimal power flow (opf) in PowerModelsDistribution on an engineering data model\n", + "\n", + "In order to run an OPF problem you will need\n", + "\n", + "1. a data model\n", + "2. a formulation\n", + "3. a solver\n", + "\n", + "In these examples we will use the `eng` model we worked with above, the `ACPPowerModel`, which is a AC power flow formulation in polar coordinates, and the `ipopt_solver` we already defined above" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "******************************************************************************\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"solve_time\" => 3.23448\n", + " \"optimizer\" => \"Ipopt\"\n", + " \"termination_status\" => LOCALLY_SOLVED\n", + " \"dual_status\" => FEASIBLE_POINT\n", + " \"primal_status\" => FEASIBLE_POINT\n", + " \"objective\" => 0.0214812\n", + " \"solution\" => Dict{String,Any}(\"voltage_source\"=>Dict{Any,Any}(\"sou…\n", + " \"objective_lb\" => -Inf" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_mc_opf(eng, ACPPowerModel, ipopt_solver)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The result of `run_mc_opf` will be very familiar to those who are already familiar with PowerModels and PowerModelsDistribution. The notable difference will be in the `\"solution\"` dictionary" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 6 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"qg_bus\"=>[3.177…\n", + " \"line\" => Dict{Any,Any}(\"quad\"=>Dict{String,Any}(\"qf\"=>[3.09488, 3.…\n", + " \"settings\" => Dict{String,Any}(\"sbase\"=>0.5)\n", + " \"load\" => Dict{Any,Any}(\"l2\"=>Dict{String,Any}(\"qd_bus\"=>[0.0, 3.0,…\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"va\"=>[-0.22425…\n", + " \"per_unit\" => false" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[\"solution\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here you can see that the solution comes back out by default into the same data model as is provided by the user to the `run_` command, as well as being in SI units, as opposed to per unit, which is used during the solve. For example," + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 2 entries:\n", + " \"va\" => [-0.484238, -120.243, 120.274]\n", + " \"vm\" => [0.222521, 0.226727, 0.225577]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[\"solution\"][\"bus\"][\"loadbus\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If for some reason you want to return the result in per-unit rather than SI, you can specify this in the `run_` command by" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 2 entries:\n", + " \"va\" => [-0.484238, -120.243, 120.274]\n", + " \"vm\" => [0.963546, 0.981757, 0.976779]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_pu = run_mc_opf(eng, ACPPowerModel, ipopt_solver; make_si=false)\n", + "\n", + "result_pu[\"solution\"][\"bus\"][\"loadbus\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Branch Flow formulations\n", + "\n", + "Previously, to use a branch flow formulation, such as `SOCNLPUBFPowerModel`, it was required to use a different `run_` command, but now, by using multiple dispatch we have simplified this for the user" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"solve_time\" => 0.172447\n", + " \"optimizer\" => \"Ipopt\"\n", + " \"termination_status\" => LOCALLY_SOLVED\n", + " \"dual_status\" => FEASIBLE_POINT\n", + " \"primal_status\" => FEASIBLE_POINT\n", + " \"objective\" => 0.0211303\n", + " \"solution\" => Dict{String,Any}(\"voltage_source\"=>Dict{Any,Any}(\"sou…\n", + " \"objective_lb\" => -Inf" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_bf = run_mc_opf(eng, SOCNLPUBFPowerModel, ipopt_solver)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running Time Series Models\n", + "\n", + "By default, `time_series` object will be ignored when running a model. To use the time series information you will need to have a multinetwork problem specification\n", + "\n", + "In the example below we use a test case, which is not exported by default, and therefore requires the specification of the PowerModelsDistribution namespace" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"solve_time\" => 0.262751\n", + " \"optimizer\" => \"Ipopt\"\n", + " \"termination_status\" => LOCALLY_SOLVED\n", + " \"dual_status\" => FEASIBLE_POINT\n", + " \"primal_status\" => FEASIBLE_POINT\n", + " \"objective\" => 0.0\n", + " \"solution\" => Dict{String,Any}(\"nw\"=>Dict{String,Any}(\"8\"=>Dict{Any…\n", + " \"objective_lb\" => -Inf" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_mn = PowerModelsDistribution._run_mc_mn_opb(eng_ts, NFAPowerModel, ipopt_solver)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Engineering Model Transformations\n", + "\n", + "One of the power things about the engineering model is that data transformations are much more simple. Here we illustrate two examples that are currently included in PowerModelsDistribution, but writing your own data transformation functions will be trivial, as we will show\n", + "\n", + "First, there are several objects that have loss models by default when parsing from dss files, such as voltage sources, transformers, and switches. To remove these loss models, therefore making these components lossless, we can use the included `make_lossess!` function. Here we use a basic 2-winding wye-wye connected transformer case from `test` to illustrate this" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"ut_trans_2w_yy.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 38 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 16 entries:\n", + " \"polarity\" => [1, 1]\n", + " \"sm_nom\" => [500.0, 500.0]\n", + " \"tm_lb\" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]]\n", + " \"connections\" => [[1, 2, 3, 4], [1, 2, 3, 4]]\n", + " \"tm_set\" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]]\n", + " \"tm_step\" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]]\n", + " \"bus\" => [\"1\", \"2\"]\n", + " \"configuration\" => ConnConfig[WYE, WYE]\n", + " \"noloadloss\" => 0.05\n", + " \"xsc\" => [0.05]\n", + " \"source_id\" => \"transformer.tx1\"\n", + " \"rw\" => [0.01, 0.02]\n", + " \"tm_fix\" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]]\n", + " \"vm_nom\" => [11.0, 4.0]\n", + " \"tm_ub\" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]]\n", + " \"imag\" => 0.11" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ut = parse_file(\"../test/data/opendss/ut_trans_2w_yy.dss\")\n", + "\n", + "eng_ut[\"transformer\"][\"tx1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that `\"noloadloss\"`, `\"rw\"`, and `\"imag\"` are non-zero, but if we apply the `make_lossless!` function we can see these parameters are set to zero, effectively eliminating the losses" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 16 entries:\n", + " \"polarity\" => [1, 1]\n", + " \"sm_nom\" => [500.0, 500.0]\n", + " \"tm_lb\" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]]\n", + " \"connections\" => [[1, 2, 3, 4], [1, 2, 3, 4]]\n", + " \"tm_set\" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]]\n", + " \"tm_step\" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]]\n", + " \"bus\" => [\"1\", \"2\"]\n", + " \"configuration\" => ConnConfig[WYE, WYE]\n", + " \"noloadloss\" => 0.0\n", + " \"xsc\" => [0.0]\n", + " \"source_id\" => \"transformer.tx1\"\n", + " \"rw\" => [0.0, 0.0]\n", + " \"tm_fix\" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]]\n", + " \"vm_nom\" => [11.0, 4.0]\n", + " \"tm_ub\" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]]\n", + " \"imag\" => 0.0" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "make_lossless!(eng_ut)\n", + "\n", + "eng_ut[\"transformer\"][\"tx1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, we can apply this function at parse" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"ut_trans_2w_yy.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 38 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 16 entries:\n", + " \"polarity\" => [1, 1]\n", + " \"sm_nom\" => [500.0, 500.0]\n", + " \"tm_lb\" => [[0.9, 0.9, 0.9], [0.9, 0.9, 0.9]]\n", + " \"connections\" => [[1, 2, 3, 4], [1, 2, 3, 4]]\n", + " \"tm_set\" => [[1.02, 1.02, 1.02], [0.97, 0.97, 0.97]]\n", + " \"tm_step\" => [[0.03125, 0.03125, 0.03125], [0.03125, 0.03125, 0.03125]]\n", + " \"bus\" => [\"1\", \"2\"]\n", + " \"configuration\" => ConnConfig[WYE, WYE]\n", + " \"noloadloss\" => 0.0\n", + " \"xsc\" => [0.0]\n", + " \"source_id\" => \"transformer.tx1\"\n", + " \"rw\" => [0.0, 0.0]\n", + " \"tm_fix\" => Array{Bool,1}[[1, 1, 1], [1, 1, 1]]\n", + " \"vm_nom\" => [11.0, 4.0]\n", + " \"tm_ub\" => [[1.1, 1.1, 1.1], [1.1, 1.1, 1.1]]\n", + " \"imag\" => 0.0" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ut = parse_file(\"../test/data/opendss/ut_trans_2w_yy.dss\"; transformations=[make_lossless!])\n", + "\n", + "eng_ut[\"transformer\"][\"tx1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another transformation function included in PowerModelsDistribution is the `apply_voltage_bounds!` function, which will apply some voltage bounds in SI units, given some percent value, _e.g._ if we want the lower bound on voltage to be `0.9` and upper bound `1.1` after per-unit conversion" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 7 entries:\n", + " \"rg\" => [0.0]\n", + " \"grounded\" => [4]\n", + " \"status\" => ENABLED\n", + " \"terminals\" => [1, 2, 3, 4]\n", + " \"vm_ub\" => [2.54034, 2.54034, 2.54034, 2.54034]\n", + " \"vm_lb\" => [2.07846, 2.07846, 2.07846, 2.07846]\n", + " \"xg\" => [0.0]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "apply_voltage_bounds!(eng_ut; vm_lb=0.9, vm_ub=1.1)\n", + "\n", + "eng_ut[\"bus\"][\"2\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, this can be specified at parse by" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"ut_trans_2w_yy.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 38 in \"ut_trans_2w_yy.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 7 entries:\n", + " \"rg\" => [0.0]\n", + " \"grounded\" => [4]\n", + " \"status\" => ENABLED\n", + " \"terminals\" => [1, 2, 3, 4]\n", + " \"vm_ub\" => [2.54034, 2.54034, 2.54034, 2.54034]\n", + " \"vm_lb\" => [2.07846, 2.07846, 2.07846, 2.07846]\n", + " \"xg\" => [0.0]" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng_ut = parse_file(\"../test/data/opendss/ut_trans_2w_yy.dss\"; transformations=[make_lossless!, (apply_voltage_bounds!, \"vm_lb\"=>0.9, \"vm_ub\"=>1.1)])\n", + "\n", + "eng_ut[\"bus\"][\"2\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Transformations on Multinetworks\n", + "\n", + "Transformations on Multinetworks should happen __before__ the network is converted into a `MATHEMATICAL` data model, so that they can generally follow the same pattern as shown above and can be seen in the `make_lossless!` and `apply_voltage_bounds!` functions already in PowerModelsDistribution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Mathematical Model\n", + "\n", + "In this section we introduce the mathematical model, which was the previous user-facing model in PowerModelsDistribution, explain how conversions between the model happen in practice, and give an example of how to do this conversion manually\n", + "\n", + "In practice, unless the user is interested, the conversion between the `ENGINEERING` and `MATHEMATICAL` models should be seemless and invisible to the user. By providing an `ENGINEERING` model to a `run_` command the `run_mc_model` command will know to convert the model to `MATHEMATICAL`, which will be used to the generate the JuMP model that will actually be optimized. Similarly, the solution generated by this optimization will be automatically converted back to the format of the `ENGINEERING` model.\n", + "\n", + "Let's first take a look at how to convert to the `MATHEMATICAL` model" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 18 entries:\n", + " \"bus\" => Dict{String,Any}(\"4\"=>Dict{String,Any}(\"grounded\"=>Bool[0, 0…\n", + " \"name\" => \"3bus_example\"\n", + " \"dcline\" => Dict{String,Any}()\n", + " \"map\" => Dict{String,Any}[Dict(\"unmap_function\"=>\"_map_math2eng_root!…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>500.0,\"vbases_default\"=>Di…\n", + " \"gen\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"pg\"=>[0.0, 0.0, 0.0]…\n", + " \"branch\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"br_r\"=>[1.09406 0.43…\n", + " \"storage\" => Dict{String,Any}()\n", + " \"switch\" => Dict{String,Any}()\n", + " \"basekv\" => 0.23094\n", + " \"baseMVA\" => 0.5\n", + " \"conductors\" => 3\n", + " \"per_unit\" => true\n", + " \"data_model\" => MATHEMATICAL\n", + " \"shunt\" => Dict{String,Any}()\n", + " \"transformer\" => Dict{String,Any}()\n", + " \"bus_lookup\" => Dict{Any,Int64}(\"primary\"=>1,\"sourcebus\"=>2,\"loadbus\"=>3)\n", + " \"load\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"model\"=>POWER,\"conne…" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math = transform_data_model(eng)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are a couple of things to notice right away. First, the data model transform automatically converts the model to per-unit. Second, there are a lot of empty component sets, whereas in the `ENGINEERING` model, only component types that had components in them were listed. In the `MATHEMATICAL` model certain component dictionaries are always expected to exist, and the `eng2math` conversion functions will automatically populate these.\n", + "\n", + "Next, there are a few unusal fields, such as `\"settings\"`, which previously didn't exist in the `MATHEMATICAL` model. This is used for the per-unit conversion specifically in PowerModelsDistribution. Also, is the `\"map\"` field, which is a `Vector` of Dictionaries that enable the conversion back to `ENGINEERING` from `MATHEMATICAL`. Without this it would be impossible to convert back, and in fact only the solution can be converted, because some properties are combined destructively during the conversion to the `MATHEMATICAL` model, and therefore cannot be reverse engineered. However, since the conversion to `MATHEMATICAL` is not in-place, you will always have a copy of `eng` alongside `math`.\n", + "\n", + "Here is an example of one of the `\"map\"` entries" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 3 entries:\n", + " \"to\" => [\"gen.1\", \"bus.4\", \"branch.3\"]\n", + " \"from\" => \"source\"\n", + " \"unmap_function\" => \"_map_math2eng_voltage_source!\"" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math[\"map\"][end]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, the `MATHEMATICAL` model can be returned directly from the `parse_file` command with the `data_model` keyword argument" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"case3_unbalanced.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 35 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 37 in \"case3_unbalanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 18 entries:\n", + " \"bus\" => Dict{String,Any}(\"4\"=>Dict{String,Any}(\"grounded\"=>Bool[0, 0…\n", + " \"name\" => \"3bus_example\"\n", + " \"dcline\" => Dict{String,Any}()\n", + " \"map\" => Dict{String,Any}[Dict(\"unmap_function\"=>\"_map_math2eng_root!…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>500.0,\"vbases_default\"=>Di…\n", + " \"gen\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"pg\"=>[0.0, 0.0, 0.0]…\n", + " \"branch\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"br_r\"=>[1.09406 0.43…\n", + " \"storage\" => Dict{String,Any}()\n", + " \"switch\" => Dict{String,Any}()\n", + " \"basekv\" => 0.23094\n", + " \"baseMVA\" => 0.5\n", + " \"conductors\" => 3\n", + " \"per_unit\" => true\n", + " \"data_model\" => MATHEMATICAL\n", + " \"shunt\" => Dict{String,Any}()\n", + " \"transformer\" => Dict{String,Any}()\n", + " \"bus_lookup\" => Dict{Any,Int64}(\"primary\"=>1,\"sourcebus\"=>2,\"loadbus\"=>3)\n", + " \"load\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"model\"=>POWER,\"conne…" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math = parse_file(\"../test/data/opendss/case3_unbalanced.dss\"; data_model=MATHEMATICAL)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Multinetworks\n", + "\n", + "In this subsection we cover parsing into a multinetwork data structure, which is a structure that only exists in the `MATHEMATICAL` model\n", + "\n", + "For those unfamiliar, the InfrastructureModels family of packages has a feature called multinetworks, which is useful for, among other things, running optimization problems on time series type problems. \n", + "\n", + "Multinetwork data structures are formatted like so" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "mn = Dict{String,Any}(\n", + " \"multinetwork\" => true,\n", + " \"nw\" => Dict{String,Any}(\n", + " \"1\" => Dict{String,Any}(\n", + " \"bus\" => Dict{String,Any}(),\n", + " ...\n", + " ),\n", + " ...\n", + " ),\n", + " ...\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To automatically create a multinetwork structure from an engineering model that contains `time_series` elements, we can use the `build_multinetwork` keyword argument in `transform_data_model`" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"name\" => \"10 replicates of 3bus_example\"\n", + " \"map\" => Dict{String,Any}[Dict(\"unmap_function\"=>\"_map_math2eng_root…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>100000.0,\"vbases_default\"…\n", + " \"bus_lookup\" => Dict{Any,Int64}(\"primary\"=>1,\"sourcebus\"=>2,\"loadbus\"=>3)\n", + " \"multinetwork\" => true\n", + " \"nw\" => Dict{String,Any}(\"3\"=>Dict{String,Any}(\"bus\"=>Dict{String,A…\n", + " \"per_unit\" => true\n", + " \"data_model\" => MATHEMATICAL" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math_mn = transform_data_model(eng_ts; build_multinetwork=true)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, we can use `parse_file` with the `build_multinetwork` keyword argument combined with `data_model=MATHEMATICAL`" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Circuit has been reset with the \"clear\" on line 1 in \"case3_balanced.dss\"\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"calcvoltagebases\" on line 36 in \"case3_balanced.dss\" is not supported, skipping.\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Command \"solve\" on line 38 in \"case3_balanced.dss\" is not supported, skipping.\u001b[39m\n" + ] + }, + { + "data": { + "text/plain": [ + "Dict{String,Any} with 8 entries:\n", + " \"name\" => \"10 replicates of 3bus_example\"\n", + " \"map\" => Dict{String,Any}[Dict(\"unmap_function\"=>\"_map_math2eng_root…\n", + " \"settings\" => Dict{String,Any}(\"sbase_default\"=>100000.0,\"vbases_default\"…\n", + " \"bus_lookup\" => Dict{Any,Int64}(\"primary\"=>1,\"sourcebus\"=>2,\"loadbus\"=>3)\n", + " \"multinetwork\" => true\n", + " \"nw\" => Dict{String,Any}(\"3\"=>Dict{String,Any}(\"bus\"=>Dict{String,A…\n", + " \"per_unit\" => true\n", + " \"data_model\" => MATHEMATICAL" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math_mn = parse_file(\"../test/data/opendss/case3_balanced.dss\"; build_multinetwork=true, data_model=MATHEMATICAL)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running `MATHEMATICAL` models\n", + "\n", + "There is very little difference from the user point-of-view in running `MATHEMATICAL` models other than the results will not be automatically converted back to the the format of the `ENGINEERING` model" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 7 entries:\n", + " \"baseMVA\" => 0.5\n", + " \"branch\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"qf\"=>[0.00618975, 0.0…\n", + " \"gen\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"qg_bus\"=>[0.00635515,…\n", + " \"load\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"qd_bus\"=>[0.0, 0.006,…\n", + " \"conductors\" => 3\n", + " \"bus\" => Dict{String,Any}(\"4\"=>Dict{String,Any}(\"va\"=>[0.0, -2.0944, 2…\n", + " \"per_unit\" => true" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_math = run_mc_opf(math, ACPPowerModel, ipopt_solver)\n", + "\n", + "result_math[\"solution\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is also possible to manually convert the solution back to the `ENGINEERING` format, provided you have the __map__" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 6 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"qg_bus\"=>[3.177…\n", + " \"line\" => Dict{Any,Any}(\"quad\"=>Dict{String,Any}(\"qf\"=>[3.09488, 3.…\n", + " \"settings\" => Dict{String,Any}(\"sbase\"=>0.5)\n", + " \"load\" => Dict{Any,Any}(\"l2\"=>Dict{String,Any}(\"qd_bus\"=>[0.0, 3.0,…\n", + " \"bus\" => Dict{Any,Any}(\"primary\"=>Dict{String,Any}(\"va\"=>[-0.22425…\n", + " \"per_unit\" => false" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sol_eng = transform_solution(result_math[\"solution\"], math)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Running `MATHEMATICAL` Multinetworks\n", + "\n", + "As with the `ENGINEERING` example of running a multinetwork problem, you will need a multinetwork problem specification, and as with the previous single `MATHEMATICAL` network example above, we only obtain the `MATHEMATICAL` solution, and can transform the solution in the same manner as before" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{String,Any} with 3 entries:\n", + " \"baseMVA\" => 100.0\n", + " \"gen\" => Dict{String,Any}(\"1\"=>Dict{String,Any}(\"pg\"=>[0.0, 0.0, 0.0]))\n", + " \"conductors\" => 3" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_math_mn = PowerModelsDistribution._run_mc_mn_opb(math_mn, NFAPowerModel, ipopt_solver)\n", + "\n", + "result_math_mn[\"solution\"][\"nw\"][\"1\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dict{Any,Any} with 2 entries:\n", + " \"voltage_source\" => Dict{Any,Any}(\"source\"=>Dict{String,Any}(\"pg\"=>[0.0, 0.0,…\n", + " \"settings\" => Dict{String,Any}(\"sbase\"=>100.0)" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sol_eng_mn = transform_solution(result_math_mn[\"solution\"], math_mn)\n", + "\n", + "sol_eng_mn[\"nw\"][\"1\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building the JuMP Model\n", + "\n", + "In some cases the user will want to directly build the JuMP model, which would traditionally be done with `instantiate_model` from PowerModels. In order to facilitate using the `ENGINEERING` model we have introduced `instantiate_mc_model` to aid in the generation of the JuMP model. `instantiate_mc_model` will automatically convert the data model to MATHEMATICAL if necessary (notifying the user of the conversion), and pass the MATHEMATICAL model off to PowerModels' `instantiate_model` with `ref_add_arcs_transformer!` in `ref_extensions`, which is a required ref extension for PowerModelsDistribution." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m[info | PowerModels]: Converting ENGINEERING data model to MATHEMATICAL first to build JuMP model\u001b[39m\n", + "\u001b[35m[warn | PowerModels]: Updated generator 1 cost function with order 3 to a function of order 2: [0.5, 0.0]\u001b[39m\n", + "Min 0.5 0_pg_1[1] + 0.5 0_pg_1[2] + 0.5 0_pg_1[3]\n", + "Subject to\n", + " 0_(3,4,2)_p[1] - 0_pg_1[1] = 0.0\n", + " 0_(3,4,2)_p[2] - 0_pg_1[2] = 0.0\n", + " 0_(3,4,2)_p[3] - 0_pg_1[3] = 0.0\n", + " 0_(2,2,1)_p[1] - 0_(3,4,2)_p[1] = 0.0\n", + " 0_(2,2,1)_p[2] - 0_(3,4,2)_p[2] = 0.0\n", + " 0_(2,2,1)_p[3] - 0_(3,4,2)_p[3] = 0.0\n", + " -0_(1,1,3)_p[1] = -0.018000000000000002\n", + " -0_(1,1,3)_p[2] = -0.012\n", + " -0_(1,1,3)_p[3] = -0.012\n", + " 0_(1,1,3)_p[1] - 0_(2,2,1)_p[1] = 0.0\n", + " 0_(1,1,3)_p[2] - 0_(2,2,1)_p[2] = 0.0\n", + " 0_(1,1,3)_p[3] - 0_(2,2,1)_p[3] = 0.0\n" + ] + } + ], + "source": [ + "pm_eng = instantiate_mc_model(eng, NFAPowerModel, build_mc_opf)\n", + "\n", + "print(pm_eng.model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Min 0.5 0_pg_1[1] + 0.5 0_pg_1[2] + 0.5 0_pg_1[3]\n", + "Subject to\n", + " 0_(3,4,2)_p[1] - 0_pg_1[1] = 0.0\n", + " 0_(3,4,2)_p[2] - 0_pg_1[2] = 0.0\n", + " 0_(3,4,2)_p[3] - 0_pg_1[3] = 0.0\n", + " 0_(2,2,1)_p[1] - 0_(3,4,2)_p[1] = 0.0\n", + " 0_(2,2,1)_p[2] - 0_(3,4,2)_p[2] = 0.0\n", + " 0_(2,2,1)_p[3] - 0_(3,4,2)_p[3] = 0.0\n", + " -0_(1,1,3)_p[1] = -0.018000000000000002\n", + " -0_(1,1,3)_p[2] = -0.012\n", + " -0_(1,1,3)_p[3] = -0.012\n", + " 0_(1,1,3)_p[1] - 0_(2,2,1)_p[1] = 0.0\n", + " 0_(1,1,3)_p[2] - 0_(2,2,1)_p[2] = 0.0\n", + " 0_(1,1,3)_p[3] - 0_(2,2,1)_p[3] = 0.0\n" + ] + } + ], + "source": [ + "import PowerModels\n", + "\n", + "pm_math = PowerModels.instantiate_model(math, NFAPowerModel, build_mc_opf; ref_extensions=[ref_add_arcs_transformer!])\n", + "\n", + "print(pm_math.model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This concludes the introduction to the `ENGINEERING` data model and conversion to the `MATHEMATICAL` model. We hope that you will find this new data model abstraction easy to use and simple to understand" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.4.1", + "language": "julia", + "name": "julia-1.4" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.4.1" + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/PowerModelsDistribution.jl b/src/PowerModelsDistribution.jl index 80ee7ff68..ff6839dce 100644 --- a/src/PowerModelsDistribution.jl +++ b/src/PowerModelsDistribution.jl @@ -9,13 +9,18 @@ module PowerModelsDistribution import LinearAlgebra - const _PMs = PowerModels + const _PM = PowerModels + const _IM = InfrastructureModels + + import PowerModels: ACPPowerModel, ACRPowerModel, DCPPowerModel, IVRPowerModel, NFAPowerModel, conductor_ids, ismulticonductor + import InfrastructureModels: ids, ref, var, con, sol, nw_ids, nws, ismultinetwork function __init__() global _LOGGER = Memento.getlogger(PowerModels) end include("core/types.jl") + include("core/base.jl") include("core/data.jl") include("core/ref.jl") include("core/variable.jl") @@ -39,21 +44,26 @@ module PowerModelsDistribution include("core/constraint_template.jl") include("core/relaxation_scheme.jl") - include("io/common.jl") - include("io/json.jl") + include("io/utils.jl") include("io/dss_parse.jl") include("io/dss_structs.jl") include("io/opendss.jl") + include("io/json.jl") + include("io/common.jl") + + include("data_model/utils.jl") + include("data_model/checks.jl") + include("data_model/components.jl") + include("data_model/eng2math.jl") + include("data_model/math2eng.jl") + include("data_model/transformations.jl") + include("data_model/units.jl") + include("prob/common.jl") include("prob/mld.jl") include("prob/opf.jl") - include("prob/opf_iv.jl") include("prob/opf_oltc.jl") - include("prob/opf_bf.jl") - include("prob/opf_bf_lm.jl") include("prob/pf.jl") - include("prob/pf_bf.jl") - include("prob/pf_iv.jl") include("prob/debug.jl") include("prob/test.jl") diff --git a/src/core/base.jl b/src/core/base.jl new file mode 100644 index 000000000..4807f19fd --- /dev/null +++ b/src/core/base.jl @@ -0,0 +1,9 @@ +"multiconductor version of instantiate model from PowerModels" +function instantiate_mc_model(data::Dict{String,<:Any}, model_type::Type, build_method::Function; ref_extensions::Vector{<:Function}=Vector{Function}([]), kwargs...) + if get(data, "data_model", MATHEMATICAL) == ENGINEERING + Memento.info(_LOGGER, "Converting ENGINEERING data model to MATHEMATICAL first to build JuMP model") + data = transform_data_model(data) + end + + return _PM.instantiate_model(data, model_type, build_method; ref_extensions=[ref_extensions..., ref_add_arcs_transformer!], kwargs...) +end diff --git a/src/core/constraint.jl b/src/core/constraint.jl index bb68b72b5..6a6703dd5 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -1,39 +1,40 @@ "do nothing by default" -function constraint_mc_model_voltage(pm::_PMs.AbstractPowerModel, n::Int) +function constraint_mc_model_voltage(pm::_PM.AbstractPowerModel, n::Int) end -# Generic thermal limit constraint -"" -function constraint_mc_thermal_limit_from(pm::_PMs.AbstractPowerModel, n::Int, f_idx, rate_a) - p_fr = _PMs.var(pm, n, :p, f_idx) - q_fr = _PMs.var(pm, n, :q, f_idx) + +"Generic thermal limit constraint from-side" +function constraint_mc_thermal_limit_from(pm::_PM.AbstractPowerModel, n::Int, f_idx, rate_a) + p_fr = var(pm, n, :p, f_idx) + q_fr = var(pm, n, :q, f_idx) mu_sm_fr = JuMP.@constraint(pm.model, p_fr.^2 + q_fr.^2 .<= rate_a.^2) - if _PMs.report_duals(pm) - _PMs.sol(pm, n, :branch, f_idx[1])[:mu_sm_fr] = mu_sm_fr + if _IM.report_duals(pm) + sol(pm, n, :branch, f_idx[1])[:mu_sm_fr] = mu_sm_fr end end -"" -function constraint_mc_thermal_limit_to(pm::_PMs.AbstractPowerModel, n::Int, t_idx, rate_a) - p_to = _PMs.var(pm, n, :p, t_idx) - q_to = _PMs.var(pm, n, :q, t_idx) + +"Generic thermal limit constraint to-side" +function constraint_mc_thermal_limit_to(pm::_PM.AbstractPowerModel, n::Int, t_idx, rate_a) + p_to = var(pm, n, :p, t_idx) + q_to = var(pm, n, :q, t_idx) mu_sm_to = JuMP.@constraint(pm.model, p_to.^2 + q_to.^2 .<= rate_a.^2) - if _PMs.report_duals(pm) - _PMs.sol(pm, n, :branch, t_idx[1])[:mu_sm_to] = mu_sm_to + if _IM.report_duals(pm) + sol(pm, n, :branch, t_idx[1])[:mu_sm_to] = mu_sm_to end end "on/off bus voltage magnitude constraint" -function constraint_mc_voltage_magnitude_on_off(pm::_PMs.AbstractPowerModel, n::Int, i::Int, vmin, vmax) - vm = _PMs.var(pm, n, :vm, i) - z_voltage = _PMs.var(pm, n, :z_voltage, i) +function constraint_mc_bus_voltage_magnitude_on_off(pm::_PM.AbstractPowerModel, n::Int, i::Int, vmin, vmax) + vm = var(pm, n, :vm, i) + z_voltage = var(pm, n, :z_voltage, i) - for c in _PMs.conductor_ids(pm, n) + for c in conductor_ids(pm, n) if isfinite(vmax[c]) JuMP.@constraint(pm.model, vm[c] <= vmax[c]*z_voltage) end @@ -46,11 +47,11 @@ end "on/off bus voltage magnitude squared constraint for relaxed formulations" -function constraint_mc_voltage_magnitude_sqr_on_off(pm::_PMs.AbstractPowerModel, n::Int, i::Int, vmin, vmax) - w = _PMs.var(pm, n, :w, i) - z_voltage = _PMs.var(pm, n, :z_voltage, i) +function constraint_mc_bus_voltage_magnitude_sqr_on_off(pm::_PM.AbstractPowerModel, n::Int, i::Int, vmin, vmax) + w = var(pm, n, :w, i) + z_voltage = var(pm, n, :z_voltage, i) - for c in _PMs.conductor_ids(pm, n) + for c in conductor_ids(pm, n) if isfinite(vmax[c]) JuMP.@constraint(pm.model, w[c] <= vmax[c]^2*z_voltage) end @@ -62,28 +63,42 @@ function constraint_mc_voltage_magnitude_sqr_on_off(pm::_PMs.AbstractPowerModel, end -function constraint_mc_active_gen_setpoint(pm::_PMs.AbstractPowerModel, n::Int, i, pg) - pg_var = _PMs.var(pm, n, :pg, i) +function constraint_mc_gen_power_setpoint_real(pm::_PM.AbstractPowerModel, n::Int, i, pg) + pg_var = var(pm, n, :pg, i) JuMP.@constraint(pm.model, pg_var .== pg) end "on/off constraint for generators" -function constraint_mc_generation_on_off(pm::_PMs.AbstractPowerModel, n::Int, i::Int, pmin, pmax, qmin, qmax) - pg = _PMs.var(pm, n, :pg, i) - qg = _PMs.var(pm, n, :qg, i) - z = _PMs.var(pm, n, :z_gen, i) - - JuMP.@constraint(pm.model, pg .<= pmax.*z) - JuMP.@constraint(pm.model, pg .>= pmin.*z) - JuMP.@constraint(pm.model, qg .<= qmax.*z) - JuMP.@constraint(pm.model, qg .>= qmin.*z) +function constraint_mc_gen_power_on_off(pm::_PM.AbstractPowerModel, n::Int, i::Int, pmin, pmax, qmin, qmax) + pg = var(pm, n, :pg, i) + qg = var(pm, n, :qg, i) + z = var(pm, n, :z_gen, i) + + for c in conductor_ids(pm, n) + if isfinite(pmax[c]) + JuMP.@constraint(pm.model, pg[c] .<= pmax[c].*z) + end + + if isfinite(pmin[c]) + JuMP.@constraint(pm.model, pg[c] .>= pmin[c].*z) + end + + if isfinite(qmax[c]) + JuMP.@constraint(pm.model, qg[c] .<= qmax[c].*z) + end + + if isfinite(qmin[c]) + JuMP.@constraint(pm.model, qg[c] .>= qmin[c].*z) + end + end end + "" -function constraint_mc_storage_thermal_limit(pm::_PMs.AbstractPowerModel, n::Int, i, rating) - ps = _PMs.var(pm, n, :ps, i) - qs = _PMs.var(pm, n, :qs, i) +function constraint_mc_storage_thermal_limit(pm::_PM.AbstractPowerModel, n::Int, i, rating) + ps = var(pm, n, :ps, i) + qs = var(pm, n, :qs, i) JuMP.@constraint(pm.model, ps.^2 + qs.^2 .<= rating.^2) end diff --git a/src/core/constraint_template.jl b/src/core/constraint_template.jl index 01a1fbbfb..2b2239516 100644 --- a/src/core/constraint_template.jl +++ b/src/core/constraint_template.jl @@ -1,47 +1,47 @@ "reference angle constraints" -function constraint_mc_theta_ref(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - va_ref = _PMs.ref(pm, nw, :bus, i, "va") +function constraint_mc_theta_ref(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + va_ref = ref(pm, nw, :bus, i, "va") constraint_mc_theta_ref(pm, nw, i, va_ref) end "" -function constraint_mc_power_balance_slack(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_sw = _PMs.ref(pm, nw, :bus_arcs_sw, i) - bus_arcs_trans = _PMs.ref(pm, nw, :bus_arcs_trans, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_storage = _PMs.ref(pm, nw, :bus_storage, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) +function constraint_mc_slack_power_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) + bus_arcs = ref(pm, nw, :bus_arcs, i) + bus_arcs_sw = ref(pm, nw, :bus_arcs_sw, i) + bus_arcs_trans = ref(pm, nw, :bus_arcs_trans, i) + bus_gens = ref(pm, nw, :bus_gens, i) + bus_storage = ref(pm, nw, :bus_storage, i) + bus_loads = ref(pm, nw, :bus_loads, i) + bus_shunts = ref(pm, nw, :bus_shunts, i) - bus_pd = Dict(k => _PMs.ref(pm, nw, :load, k, "pd") for k in bus_loads) - bus_qd = Dict(k => _PMs.ref(pm, nw, :load, k, "qd") for k in bus_loads) + bus_pd = Dict(k => ref(pm, nw, :load, k, "pd") for k in bus_loads) + bus_qd = Dict(k => ref(pm, nw, :load, k, "qd") for k in bus_loads) - bus_gs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) - bus_bs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) + bus_gs = Dict(k => ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) + bus_bs = Dict(k => ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) - constraint_mc_power_balance_slack(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + constraint_mc_slack_power_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) end "" -function constraint_mc_model_voltage(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw) +function constraint_mc_model_voltage(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw) constraint_mc_model_voltage(pm, nw) end "ohms constraint for branches on the from-side" -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) t_idx = (i, t_bus, f_bus) - g, b = _PMs.calc_branch_y(branch) - tr, ti = _PMs.calc_branch_t(branch) + g, b = _PM.calc_branch_y(branch) + tr, ti = _PM.calc_branch_t(branch) g_fr = branch["g_fr"] b_fr = branch["b_fr"] tm = branch["tap"] @@ -51,15 +51,15 @@ end "ohms constraint for branches on the to-side" -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) t_idx = (i, t_bus, f_bus) - g, b = _PMs.calc_branch_y(branch) - tr, ti = _PMs.calc_branch_t(branch) + g, b = _PM.calc_branch_y(branch) + tr, ti = _PM.calc_branch_t(branch) g_to = branch["g_to"] b_to = branch["b_to"] tm = branch["tap"] @@ -69,8 +69,8 @@ end "" -function constraint_mc_model_voltage_magnitude_difference(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_model_voltage_magnitude_difference(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) @@ -88,7 +88,7 @@ end "" function constraint_mc_model_current(pm::AbstractUBFModels; nw::Int=pm.cnw) - for (i,branch) in _PMs.ref(pm, nw, :branch) + for (i,branch) in ref(pm, nw, :branch) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) @@ -102,8 +102,8 @@ end "" -function constraint_mc_flow_losses(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_power_losses(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) @@ -118,63 +118,61 @@ function constraint_mc_flow_losses(pm::_PMs.AbstractPowerModel, i::Int; nw::Int= tm = [1, 1, 1] #TODO - constraint_mc_flow_losses(pm, nw, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm) + constraint_mc_power_losses(pm, nw, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm) end "Transformer constraints, considering winding type, conductor order, polarity and tap settings." -function constraint_mc_trans(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw, fix_taps::Bool=true) - if _PMs.ref(pm, pm.cnw, :conductors)!=3 +function constraint_mc_transformer_power(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw, fix_taps::Bool=true) + if ref(pm, pm.cnw, :conductors)!=3 Memento.error(_LOGGER, "Transformers only work with networks with three conductors.") end - trans = _PMs.ref(pm, :transformer, i) - f_bus = trans["f_bus"] - t_bus = trans["t_bus"] + transformer = ref(pm, :transformer, i) + f_bus = transformer["f_bus"] + t_bus = transformer["t_bus"] f_idx = (i, f_bus, t_bus) t_idx = (i, t_bus, f_bus) - config = trans["configuration"] - type = trans["configuration"] - f_cnd = trans["f_connections"][1:3] - t_cnd = trans["t_connections"][1:3] - tm_set = trans["tm"] - tm_fixed = fix_taps ? ones(Bool, length(tm_set)) : trans["fixed"] - tm_scale = calculate_tm_scale(trans, _PMs.ref(pm, nw, :bus, f_bus), _PMs.ref(pm, nw, :bus, t_bus)) + config = transformer["configuration"] + f_cnd = transformer["f_connections"][1:3] + t_cnd = transformer["t_connections"][1:3] + tm_set = transformer["tm_set"] + tm_fixed = fix_taps ? ones(Bool, length(tm_set)) : transformer["tm_fix"] + tm_scale = calculate_tm_scale(transformer, ref(pm, nw, :bus, f_bus), ref(pm, nw, :bus, t_bus)) #TODO change data model # there is redundancy in specifying polarity seperately on from and to side #TODO change this once migrated to new data model - pol = haskey(trans, "poalrity") ? trans["polarity"] : trans["config_fr"]["polarity"] + pol = transformer["polarity"] - if config=="wye" - constraint_mc_trans_yy(pm, nw, i, f_bus, t_bus, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - elseif config=="delta" - constraint_mc_trans_dy(pm, nw, i, f_bus, t_bus, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - end - if type=="zig-zag" + if config == WYE + constraint_mc_transformer_power_yy(pm, nw, i, f_bus, t_bus, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + elseif config == DELTA + constraint_mc_transformer_power_dy(pm, nw, i, f_bus, t_bus, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + elseif config == "zig-zag" Memento.error(_LOGGER, "Zig-zag not yet supported.") end end "KCL including transformer arcs" -function constraint_mc_power_balance(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_sw = _PMs.ref(pm, nw, :bus_arcs_sw, i) - bus_arcs_trans = _PMs.ref(pm, nw, :bus_arcs_trans, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_storage = _PMs.ref(pm, nw, :bus_storage, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) +function constraint_mc_power_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) + bus_arcs = ref(pm, nw, :bus_arcs, i) + bus_arcs_sw = ref(pm, nw, :bus_arcs_sw, i) + bus_arcs_trans = ref(pm, nw, :bus_arcs_trans, i) + bus_gens = ref(pm, nw, :bus_gens, i) + bus_storage = ref(pm, nw, :bus_storage, i) + bus_loads = ref(pm, nw, :bus_loads, i) + bus_shunts = ref(pm, nw, :bus_shunts, i) - bus_pd = Dict(k => _PMs.ref(pm, nw, :load, k, "pd") for k in bus_loads) - bus_qd = Dict(k => _PMs.ref(pm, nw, :load, k, "qd") for k in bus_loads) + bus_pd = Dict(k => ref(pm, nw, :load, k, "pd") for k in bus_loads) + bus_qd = Dict(k => ref(pm, nw, :load, k, "qd") for k in bus_loads) - bus_gs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) - bus_bs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) + bus_gs = Dict(k => ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) + bus_bs = Dict(k => ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) - constraint_mc_power_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + constraint_mc_power_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) end @@ -190,50 +188,50 @@ For a discussion of sequence components and voltage unbalance factor (VUF), see url={https://molzahn.github.io/pubs/girigoudar_molzahn_roald-2019.pdf} } """ -function constraint_mc_voltage_balance(pm::_PMs.AbstractPowerModel, bus_id::Int; nw=pm.cnw) - @assert(_PMs.ref(pm, nw, :conductors)==3) +function constraint_mc_bus_voltage_balance(pm::_PM.AbstractPowerModel, bus_id::Int; nw=pm.cnw) + @assert(ref(pm, nw, :conductors)==3) - bus = _PMs.ref(pm, nw, :bus, bus_id) + bus = ref(pm, nw, :bus, bus_id) if haskey(bus, "vm_vuf_max") - constraint_mc_vm_vuf(pm, nw, bus_id, bus["vm_vuf_max"]) + constraint_mc_bus_voltage_magnitude_vuf(pm, nw, bus_id, bus["vm_vuf_max"]) end if haskey(bus, "vm_seq_neg_max") - constraint_mc_vm_neg_seq(pm, nw, bus_id, bus["vm_seq_neg_max"]) + constraint_mc_bus_voltage_magnitude_negative_sequence(pm, nw, bus_id, bus["vm_seq_neg_max"]) end if haskey(bus, "vm_seq_pos_max") - constraint_mc_vm_pos_seq(pm, nw, bus_id, bus["vm_seq_pos_max"]) + constraint_mc_bus_voltage_magnitude_positive_sequence(pm, nw, bus_id, bus["vm_seq_pos_max"]) end if haskey(bus, "vm_seq_zero_max") - constraint_mc_vm_zero_seq(pm, nw, bus_id, bus["vm_seq_zero_max"]) + constraint_mc_bus_voltage_magnitude_zero_sequence(pm, nw, bus_id, bus["vm_seq_zero_max"]) end if haskey(bus, "vm_ll_min")|| haskey(bus, "vm_ll_max") vm_ll_min = haskey(bus, "vm_ll_min") ? bus["vm_ll_min"] : fill(0, 3) vm_ll_max = haskey(bus, "vm_ll_max") ? bus["vm_ll_max"] : fill(Inf, 3) - constraint_mc_vm_ll(pm, nw, bus_id, vm_ll_min, vm_ll_max) + constraint_mc_bus_voltage_magnitude_ll(pm, nw, bus_id, vm_ll_min, vm_ll_max) end end "KCL including transformer arcs and load variables." -function constraint_mc_power_balance_load(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_sw = _PMs.ref(pm, nw, :bus_arcs_sw, i) - bus_arcs_trans = _PMs.ref(pm, nw, :bus_arcs_trans, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_storage = _PMs.ref(pm, nw, :bus_storage, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) +function constraint_mc_load_power_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) + bus_arcs = ref(pm, nw, :bus_arcs, i) + bus_arcs_sw = ref(pm, nw, :bus_arcs_sw, i) + bus_arcs_trans = ref(pm, nw, :bus_arcs_trans, i) + bus_gens = ref(pm, nw, :bus_gens, i) + bus_storage = ref(pm, nw, :bus_storage, i) + bus_loads = ref(pm, nw, :bus_loads, i) + bus_shunts = ref(pm, nw, :bus_shunts, i) - bus_gs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) - bus_bs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) + bus_gs = Dict(k => ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) + bus_bs = Dict(k => ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) - constraint_mc_power_balance_load(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + constraint_mc_load_power_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) end @@ -271,18 +269,18 @@ sn_a = v_a.conj(i_a) = v_a.(s_ab/(v_a-v_b) - s_ca/(v_c-v_a)) So for delta, sn is constrained indirectly. """ -function constraint_mc_load(pm::_PMs.AbstractPowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) - load = _PMs.ref(pm, nw, :load, id) - bus = _PMs.ref(pm, nw,:bus, load["load_bus"]) +function constraint_mc_load_setpoint(pm::_PM.AbstractPowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) + load = ref(pm, nw, :load, id) + bus = ref(pm, nw,:bus, load["load_bus"]) - conn = haskey(load, "conn") ? load["conn"] : "wye" + conn = haskey(load, "configuration") ? load["configuration"] : WYE a, alpha, b, beta = _load_expmodel_params(load, bus) - if conn=="wye" - constraint_mc_load_wye(pm, nw, id, load["load_bus"], a, alpha, b, beta) + if conn==WYE + constraint_mc_load_setpoint_wye(pm, nw, id, load["load_bus"], a, alpha, b, beta; report=report) else - constraint_mc_load_delta(pm, nw, id, load["load_bus"], a, alpha, b, beta) + constraint_mc_load_setpoint_delta(pm, nw, id, load["load_bus"], a, alpha, b, beta; report=report) end end @@ -299,9 +297,9 @@ sn_a = v_a.conj(i_a) = v_a.(s_ab/(v_a-v_b) - s_ca/(v_c-v_a)) So for delta, sn is constrained indirectly. """ -function constraint_mc_generation(pm::_PMs.AbstractPowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true, bounded::Bool=true) - generator = _PMs.ref(pm, nw, :gen, id) - bus = _PMs.ref(pm, nw,:bus, generator["gen_bus"]) +function constraint_mc_gen_setpoint(pm::_PM.AbstractPowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true, bounded::Bool=true) + generator = ref(pm, nw, :gen, id) + bus = ref(pm, nw,:bus, generator["gen_bus"]) N = 3 pmin = get(generator, "pmin", fill(-Inf, N)) @@ -309,60 +307,60 @@ function constraint_mc_generation(pm::_PMs.AbstractPowerModel, id::Int; nw::Int= qmin = get(generator, "qmin", fill(-Inf, N)) qmax = get(generator, "qmax", fill( Inf, N)) - if generator["conn"]=="wye" - constraint_mc_generation_wye(pm, nw, id, bus["index"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) + if get(generator, "configuration", WYE) == WYE + constraint_mc_gen_setpoint_wye(pm, nw, id, bus["index"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) else - constraint_mc_generation_delta(pm, nw, id, bus["index"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) + constraint_mc_gen_setpoint_delta(pm, nw, id, bus["index"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) end end "KCL for load shed problem with transformers" -function constraint_mc_power_balance_shed(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_sw = _PMs.ref(pm, nw, :bus_arcs_sw, i) - bus_arcs_trans = _PMs.ref(pm, nw, :bus_arcs_trans, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_storage = _PMs.ref(pm, nw, :bus_storage, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) +function constraint_mc_shed_power_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) + bus_arcs = ref(pm, nw, :bus_arcs, i) + bus_arcs_sw = ref(pm, nw, :bus_arcs_sw, i) + bus_arcs_trans = ref(pm, nw, :bus_arcs_trans, i) + bus_gens = ref(pm, nw, :bus_gens, i) + bus_storage = ref(pm, nw, :bus_storage, i) + bus_loads = ref(pm, nw, :bus_loads, i) + bus_shunts = ref(pm, nw, :bus_shunts, i) - bus_pd = Dict(k => _PMs.ref(pm, nw, :load, k, "pd") for k in bus_loads) - bus_qd = Dict(k => _PMs.ref(pm, nw, :load, k, "qd") for k in bus_loads) + bus_pd = Dict(k => ref(pm, nw, :load, k, "pd") for k in bus_loads) + bus_qd = Dict(k => ref(pm, nw, :load, k, "qd") for k in bus_loads) - bus_gs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) - bus_bs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) + bus_gs = Dict(k => ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) + bus_bs = Dict(k => ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) - constraint_mc_power_balance_shed(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + constraint_mc_shed_power_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) end "on/off constraint for bus voltages" -function constraint_mc_bus_voltage_on_off(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) +function constraint_mc_bus_voltage_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) constraint_mc_bus_voltage_on_off(pm, nw; kwargs...) end "on/off voltage magnitude constraint" -function constraint_mc_voltage_magnitude_on_off(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) +function constraint_mc_bus_voltage_magnitude_on_off(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) - constraint_mc_voltage_magnitude_on_off(pm, nw, i, bus["vmin"], bus["vmax"]) + constraint_mc_bus_voltage_magnitude_on_off(pm, nw, i, bus["vmin"], bus["vmax"]) end "on/off voltage magnitude squared constraint for relaxed formulations" -function constraint_mc_voltage_magnitude_sqr_on_off(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) +function constraint_mc_bus_voltage_magnitude_sqr_on_off(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) - constraint_mc_voltage_magnitude_sqr_on_off(pm, nw, i, bus["vmin"], bus["vmax"]) + constraint_mc_bus_voltage_magnitude_sqr_on_off(pm, nw, i, bus["vmin"], bus["vmax"]) end -"This is duplicated at PMD level to correctly handle the indexing of the shunts." -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +"This is duplicated at PowerModelsDistribution level to correctly handle the indexing of the shunts." +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) @@ -373,18 +371,18 @@ end "storage loss constraints, delegate to PowerModels" -function constraint_mc_storage_loss(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) - storage = _PMs.ref(pm, nw, :storage, i) +function constraint_mc_storage_losses(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) + storage = ref(pm, nw, :storage, i) - _PMs.constraint_storage_loss(pm, nw, i, storage["storage_bus"], storage["r"], storage["x"], storage["p_loss"], storage["q_loss"]; - conductors = _PMs.conductor_ids(pm, nw) + _PM.constraint_storage_losses(pm, nw, i, storage["storage_bus"], storage["r"], storage["x"], storage["p_loss"], storage["q_loss"]; + conductors = conductor_ids(pm, nw) ) end "branch thermal constraints from" -function constraint_mc_thermal_limit_from(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_thermal_limit_from(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) @@ -396,8 +394,8 @@ end "branch thermal constraints to" -function constraint_mc_thermal_limit_to(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_thermal_limit_to(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] t_idx = (i, t_bus, f_bus) @@ -409,17 +407,17 @@ end "voltage magnitude setpoint constraint" -function constraint_mc_voltage_magnitude_setpoint(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) - bus = _PMs.ref(pm, nw, :bus, i) +function constraint_mc_voltage_magnitude_only(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) + bus = ref(pm, nw, :bus, i) vmref = bus["vm"] #Not sure why this is needed - constraint_mc_voltage_magnitude_setpoint(pm, nw, i, vmref) + constraint_mc_voltage_magnitude_only(pm, nw, i, vmref) end "generator active power setpoint constraint" -function constraint_mc_active_gen_setpoint(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) - pg_set = _PMs.ref(pm, nw, :gen, i)["pg"] - constraint_mc_active_gen_setpoint(pm, nw, i, pg_set) +function constraint_mc_gen_power_setpoint_real(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw, kwargs...) + pg_set = ref(pm, nw, :gen, i)["pg"] + constraint_mc_gen_power_setpoint_real(pm, nw, i, pg_set) end @@ -428,39 +426,49 @@ This constraint captures problem agnostic constraints that define limits for voltage magnitudes (where variable bounds cannot be used) Notable examples include IVRPowerModel and ACRPowerModel """ -function constraint_mc_voltage_magnitude_bounds(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) +function constraint_mc_voltage_magnitude_bounds(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) vmin = get(bus, "vmin", fill(0.0, 3)) #TODO update for four-wire vmax = get(bus, "vmax", fill(Inf, 3)) #TODO update for four-wire constraint_mc_voltage_magnitude_bounds(pm, nw, i, vmin, vmax) end -function constraint_mc_generation_on_off(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - gen = _PMs.ref(pm, nw, :gen, i) +"" +function constraint_mc_gen_power_on_off(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + gen = ref(pm, nw, :gen, i) + ncnds = length(conductor_ids(pm; nw=nw)) + + pmin = get(gen, "pmin", fill(-Inf, ncnds)) + pmax = get(gen, "pmax", fill( Inf, ncnds)) + qmin = get(gen, "qmin", fill(-Inf, ncnds)) + qmax = get(gen, "qmax", fill( Inf, ncnds)) - constraint_mc_generation_on_off(pm, nw, i, gen["pmin"], gen["pmax"], gen["qmin"], gen["qmax"]) + constraint_mc_gen_power_on_off(pm, nw, i, pmin, pmax, qmin, qmax) end + "" -function constraint_mc_storage_thermal_limit(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - storage = _PMs.ref(pm, nw, :storage, i) +function constraint_mc_storage_thermal_limit(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + storage = ref(pm, nw, :storage, i) constraint_mc_storage_thermal_limit(pm, nw, i, storage["thermal_rating"]) end + "" -function constraint_mc_storage_current_limit(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - storage = _PMs.ref(pm, nw, :storage, i) +function constraint_mc_storage_current_limit(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + storage = ref(pm, nw, :storage, i) constraint_mc_storage_current_limit(pm, nw, i, storage["storage_bus"], storage["current_rating"]) end + "" -function constraint_mc_storage_on_off(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - storage = _PMs.ref(pm, nw, :storage, i) +function constraint_mc_storage_on_off(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + storage = ref(pm, nw, :storage, i) charge_ub = storage["charge_rating"] discharge_ub = storage["discharge_rating"] - cnds = _PMs.conductor_ids(pm, nw) + cnds = conductor_ids(pm, nw) ncnds = length(cnds) pmin = zeros(ncnds) pmax = zeros(ncnds) @@ -468,55 +476,59 @@ function constraint_mc_storage_on_off(pm::_PMs.AbstractPowerModel, i::Int; nw::I qmax = zeros(ncnds) for c in 1:ncnds - inj_lb, inj_ub = _PMs.ref_calc_storage_injection_bounds(_PMs.ref(pm, nw, :storage), _PMs.ref(pm, nw, :bus), c) + inj_lb, inj_ub = _PM.ref_calc_storage_injection_bounds(ref(pm, nw, :storage), ref(pm, nw, :bus), c) pmin[c] = inj_lb[i] pmax[c] = inj_ub[i] - qmin[c] = max(inj_lb[i], _PMs.ref(pm, nw, :storage, i, "qmin")[c]) - qmax[c] = min(inj_ub[i], _PMs.ref(pm, nw, :storage, i, "qmax")[c]) + qmin[c] = max(inj_lb[i], ref(pm, nw, :storage, i, "qmin")[c]) + qmax[c] = min(inj_ub[i], ref(pm, nw, :storage, i, "qmax")[c]) end constraint_mc_storage_on_off(pm, nw, i, pmin, pmax, qmin, qmax, charge_ub, discharge_ub) end + "defines limits on active power output of a generator where bounds can't be used" -function constraint_mc_generation_active_power_limits(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - gen = _PMs.ref(pm, nw, :gen, i) +function constraint_mc_gen_active_bounds(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + gen = ref(pm, nw, :gen, i) bus = gen["gen_bus"] - constraint_mc_generation_active_power_limits(pm, nw, i, bus, gen["pmax"], gen["pmin"]) + constraint_mc_gen_active_bounds(pm, nw, i, bus, gen["pmax"], gen["pmin"]) end + "defines limits on reactive power output of a generator where bounds can't be used" -function constraint_mc_generation_reactive_power_limits(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - gen = _PMs.ref(pm, nw, :gen, i) +function constraint_mc_gen_reactive_bounds(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + gen = ref(pm, nw, :gen, i) bus = gen["gen_bus"] - constraint_mc_generation_reactive_power_limits(pm, nw, i, bus, gen["qmax"], gen["qmin"]) + constraint_mc_gen_reactive_bounds(pm, nw, i, bus, gen["qmax"], gen["qmin"]) end + + "" -function constraint_mc_current_balance_load(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_sw = _PMs.ref(pm, nw, :bus_arcs_sw, i) - bus_arcs_trans = _PMs.ref(pm, nw, :bus_arcs_trans, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_storage = _PMs.ref(pm, nw, :bus_storage, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) +function constraint_mc_load_current_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + bus = ref(pm, nw, :bus, i) + bus_arcs = ref(pm, nw, :bus_arcs, i) + bus_arcs_sw = ref(pm, nw, :bus_arcs_sw, i) + bus_arcs_trans = ref(pm, nw, :bus_arcs_trans, i) + bus_gens = ref(pm, nw, :bus_gens, i) + bus_storage = ref(pm, nw, :bus_storage, i) + bus_loads = ref(pm, nw, :bus_loads, i) + bus_shunts = ref(pm, nw, :bus_shunts, i) - bus_gs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) - bus_bs = Dict(k => _PMs.ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) + bus_gs = Dict(k => ref(pm, nw, :shunt, k, "gs") for k in bus_shunts) + bus_bs = Dict(k => ref(pm, nw, :shunt, k, "bs") for k in bus_shunts) - constraint_mc_current_balance_load(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + constraint_mc_load_current_balance(pm, nw, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) end "" -function constraint_mc_current_from(pm::_PMs.AbstractIVRModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_current_from(pm::_PM.AbstractIVRModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) - tr, ti = _PMs.calc_branch_t(branch) + tr, ti = _PM.calc_branch_t(branch) g_fr = branch["g_fr"] b_fr = branch["b_fr"] tm = branch["tap"] @@ -524,15 +536,16 @@ function constraint_mc_current_from(pm::_PMs.AbstractIVRModel, i::Int; nw::Int=p constraint_mc_current_from(pm, nw, f_bus, f_idx, g_fr, b_fr, tr, ti, tm) end + "" -function constraint_mc_current_to(pm::_PMs.AbstractIVRModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_current_to(pm::_PM.AbstractIVRModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) t_idx = (i, t_bus, f_bus) - tr, ti = _PMs.calc_branch_t(branch) + tr, ti = _PM.calc_branch_t(branch) g_to = branch["g_to"] b_to = branch["b_to"] tm = branch["tap"] @@ -540,17 +553,57 @@ function constraint_mc_current_to(pm::_PMs.AbstractIVRModel, i::Int; nw::Int=pm. constraint_mc_current_to(pm, nw, t_bus, f_idx, t_idx, g_to, b_to) end + "" -function constraint_mc_voltage_drop(pm::_PMs.AbstractPowerModel, i::Int; nw::Int=pm.cnw) - branch = _PMs.ref(pm, nw, :branch, i) +function constraint_mc_bus_voltage_drop(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + branch = ref(pm, nw, :branch, i) f_bus = branch["f_bus"] t_bus = branch["t_bus"] f_idx = (i, f_bus, t_bus) - tr, ti = _PMs.calc_branch_t(branch) + tr, ti = _PM.calc_branch_t(branch) r = branch["br_r"] x = branch["br_x"] tm = branch["tap"] - constraint_mc_voltage_drop(pm, nw, i, f_bus, t_bus, f_idx, r, x, tr, ti, tm) + constraint_mc_bus_voltage_drop(pm, nw, i, f_bus, t_bus, f_idx, r, x, tr, ti, tm) +end + + +"ensures that power generation and demand are balanced" +function constraint_mc_network_power_balance(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) + comp_bus_ids = ref(pm, nw, :components, i) + + comp_gen_ids = Set{Int64}() + for bus_id in comp_bus_ids, gen_id in PowerModels.ref(pm, nw, :bus_gens, bus_id) + push!(comp_gen_ids, gen_id) + end + + comp_loads = Set() + for bus_id in comp_bus_ids, load_id in PowerModels.ref(pm, nw, :bus_loads, bus_id) + push!(comp_loads, PowerModels.ref(pm, nw, :load, load_id)) + end + + comp_shunts = Set() + for bus_id in comp_bus_ids, shunt_id in PowerModels.ref(pm, nw, :bus_shunts, bus_id) + push!(comp_shunts, PowerModels.ref(pm, nw, :shunt, shunt_id)) + end + + comp_branches = Set() + for (branch_id, branch) in PowerModels.ref(pm, nw, :branch) + if in(branch["f_bus"], comp_bus_ids) && in(branch["t_bus"], comp_bus_ids) + push!(comp_branches, branch) + end + end + + comp_pd = Dict(load["index"] => (load["load_bus"], load["pd"]) for load in comp_loads) + comp_qd = Dict(load["index"] => (load["load_bus"], load["qd"]) for load in comp_loads) + + comp_gs = Dict(shunt["index"] => (shunt["shunt_bus"], shunt["gs"]) for shunt in comp_shunts) + comp_bs = Dict(shunt["index"] => (shunt["shunt_bus"], shunt["bs"]) for shunt in comp_shunts) + + comp_branch_g = Dict(branch["index"] => (branch["f_bus"], branch["t_bus"], branch["br_r"], branch["br_x"], branch["tap"], branch["g_fr"], branch["g_to"]) for branch in comp_branches) + comp_branch_b = Dict(branch["index"] => (branch["f_bus"], branch["t_bus"], branch["br_r"], branch["br_x"], branch["tap"], branch["b_fr"], branch["b_to"]) for branch in comp_branches) + + constraint_mc_network_power_balance(pm, nw, i, comp_gen_ids, comp_pd, comp_qd, comp_gs, comp_bs, comp_branch_g, comp_branch_b) end diff --git a/src/core/data.jl b/src/core/data.jl index 55d47a783..5226afe79 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -1,3 +1,9 @@ +import LinearAlgebra: Adjoint + +const _excluded_count_busname_patterns = Vector{Regex}([ + r"^_virtual.*", +]) + "wraps angles in degrees to 180" function _wrap_to_180(degrees) return degrees - 360*floor.((degrees .+ 180)/360) @@ -39,72 +45,77 @@ end _replace_nan(v) = map(x -> isnan(x) ? zero(x) : x, v) -"Counts number of nodes in network" -function count_nodes(dss_data::Dict{String,Array})::Int - sourcebus = get(dss_data["circuit"][1], "bus1", "sourcebus") - all_nodes = Dict() - for comp_type in values(dss_data) - for comp in values(comp_type) - if isa(comp, Dict) && haskey(comp, "buses") - for busname in values(_parse_array(String, comp["buses"])) - name, nodes = _parse_busname(busname) - - if !haskey(all_nodes, name) - all_nodes[name] = Set([]) - end - - for (n, node) in enumerate(nodes[1:3]) - if node - push!(all_nodes[name], n) - end - end - end - elseif isa(comp, Dict) - for (prop, val) in comp - if startswith(prop, "bus") && prop != "buses" - name, nodes = _parse_busname(val) - if !haskey(all_nodes, name) - all_nodes[name] = Set([]) - end +"Counts number of nodes in network" +function count_nodes(data::Dict{String,<:Any})::Int + n_nodes = 0 - for (n, node) in enumerate(nodes[1:3]) - if node - push!(all_nodes[name], n) + if get(data, "data_model", missing) == DSS + all_nodes = Dict() + for obj_type in values(data) + if isa(obj_type, Dict) + for object in values(obj_type) + if isa(object, Dict) + if haskey(object, "buses") + for busname in values(object["buses"]) + name, nodes = _parse_bus_id(busname) + + if !haskey(all_nodes, name) + all_nodes[name] = Set([]) + end + + for (n, node) in enumerate(nodes[1:3]) + if node + push!(all_nodes[name], n) + end + end + end + else + for (prop, val) in object + if startswith(prop, "bus") && prop != "buses" + name, nodes = _parse_bus_id(val) + + if !haskey(all_nodes, name) + all_nodes[name] = Set([]) + end + + for (n, node) in enumerate(nodes[1:3]) + if node + push!(all_nodes[name], n) + end + end + end end end end end end end - end - n_nodes = 0 - for (name, phases) in all_nodes - if name != sourcebus + for (name, phases) in all_nodes n_nodes += length(phases) end - end - - return n_nodes -end - - -"Counts number of nodes in network" -function count_nodes(pmd_data::Dict{String,Any})::Int - if pmd_data["source_type"] == "dss" - Memento.info(_LOGGER, "counting nodes from PowerModelsDistribution structure may not be as accurate as directly from `parse_dss` data due to virtual buses, etc.") - end - - n_nodes = 0 - for bus in values(pmd_data["bus"]) - if pmd_data["source_type"] == "matlab" - n_nodes += sum(bus["vm"] .> 0.0) - elseif pmd_data["source_type"] == "dss" - if !(pmd_data["source_type"] == "dss" && bus["name"] in ["virtual_sourcebus", pmd_data["sourcebus"]]) && !(pmd_data["source_type"] == "dss" && startswith(bus["name"], "tr") && endswith(bus["name"], r"_b\d")) + elseif get(data, "data_model", missing) in [MATHEMATICAL, ENGINEERING] || (haskey(data, "source_type") && data["source_type"] == "matlab") + n_nodes = 0 + for (name, bus) in data["bus"] + if get(data, "data_model", missing) == MATPOWER || (haskey(data, "source_type") && data["source_type"] == "matlab") n_nodes += sum(bus["vm"] .> 0.0) + else + if data["data_model"] == MATHEMATICAL + name = bus["name"] + end + + if all(!occursin(pattern, name) for pattern in [_excluded_count_busname_patterns...]) + if data["data_model"] == MATHEMATICAL + n_nodes += length(bus["terminals"][.!get(bus, "grounded", zeros(length(bus["terminals"])))]) + else + n_nodes += length([n for n in bus["terminals"] if !(n in get(bus, "grounded", []))]) + end + end end end + else + Memento.error(_LOGGER, "Origin of data structure not recognized, cannot count nodes reliably") end return n_nodes @@ -120,7 +131,7 @@ function calculate_tm_scale(trans::Dict{String,Any}, bus_fr::Dict{String,Any}, b config = trans["configuration"] tm_scale = tm_nom*(t_vbase/f_vbase) - if config == "delta" + if config == DELTA #TODO is this still needed? tm_scale *= sqrt(3) elseif config == "zig-zag" @@ -143,19 +154,18 @@ function _calc_bus_vm_ll_bounds(bus::Dict; vdmin_eps=0.1) vmax = bus["vmax"] vmin = bus["vmin"] if haskey(bus, "vm_ll_max") - vdmax = bus["vm_ll_max"]*sqrt(3) + vdmax = bus["vm_ll_max"] else # implied valid upper bound - vdmax = [1 1 0; 0 1 1; 1 0 1]*vmax + vdmax = _mat_mult_rm_nan([1 1 0; 0 1 1; 1 0 1], vmax) id = bus["index"] end if haskey(bus, "vm_ll_min") - vdmin = bus["vm_ll_min"]*sqrt(3) + vdmin = bus["vm_ll_min"] else - vdmin = ones(3)*vdmin_eps*sqrt(3) - id = bus["index"] - Memento.info(_LOGGER, "Bus $id has no phase-to-phase vm upper bound; instead, $vdmin_eps was used as a valid upper bound.") + vdmin = fill(0.0, length(vmin)) end + return (vdmin, vdmax) end @@ -168,10 +178,10 @@ function _calc_load_pq_bounds(load::Dict, bus::Dict) a, alpha, b, beta = _load_expmodel_params(load, bus) vmin, vmax = _calc_load_vbounds(load, bus) # get bounds - pmin = min.(a.*vmin.^alpha, a.*vmax.^alpha) - pmax = max.(a.*vmin.^alpha, a.*vmax.^alpha) - qmin = min.(b.*vmin.^beta, b.*vmax.^beta) - qmax = max.(b.*vmin.^beta, b.*vmax.^beta) + pmin = _nan2zero(min.(a.*vmin.^alpha, a.*vmax.^alpha), a) + pmax = _nan2zero(max.(a.*vmin.^alpha, a.*vmax.^alpha), a) + qmin = _nan2zero(min.(b.*vmin.^beta, b.*vmax.^beta), b) + qmax = _nan2zero(max.(b.*vmin.^beta, b.*vmax.^beta), b) return (pmin, pmax, qmin, qmax) end @@ -195,8 +205,8 @@ Returns magnitude bounds for the current going through the load. function _calc_load_current_magnitude_bounds(load::Dict, bus::Dict) a, alpha, b, beta = _load_expmodel_params(load, bus) vmin, vmax = _calc_load_vbounds(load, bus) - cb1 = sqrt.(a.^(2).*vmin.^(2*alpha.-2) + b.^(2).*vmin.^(2*beta.-2)) - cb2 = sqrt.(a.^(2).*vmax.^(2*alpha.-2) + b.^(2).*vmax.^(2*beta.-2)) + cb1 = sqrt.(_nan2zero(a.^(2).*vmin.^(2*alpha.-2), a) + _nan2zero(b.^(2).*vmin.^(2*beta.-2), b)) + cb2 = sqrt.(_nan2zero(a.^(2).*vmax.^(2*alpha.-2), a) + _nan2zero(b.^(2).*vmax.^(2*beta.-2), b)) cmin = min.(cb1, cb2) cmax = max.(cb1, cb2) return cmin, cmax @@ -213,24 +223,24 @@ function _load_expmodel_params(load::Dict, bus::Dict) pd = load["pd"] qd = load["qd"] ncnds = length(pd) - if load["model"]=="constant_power" + if load["model"]==POWER return (pd, zeros(ncnds), qd, zeros(ncnds)) else # get exponents - if load["model"]=="constant_current" + if load["model"]==CURRENT alpha = ones(ncnds) beta =ones(ncnds) - elseif load["model"]=="constant_impedance" + elseif load["model"]==IMPEDANCE alpha = ones(ncnds)*2 beta =ones(ncnds)*2 - elseif load["model"]=="exponential" + elseif load["model"]==EXPONENTIAL alpha = load["alpha"] @assert(all(alpha.>=0)) beta = load["beta"] @assert(all(beta.>=0)) end # calculate proportionality constants - v0 = load["vnom_kv"]/(bus["base_kv"]/sqrt(3)) + v0 = load["vnom_kv"] a = pd./v0.^alpha b = qd./v0.^beta # get bounds @@ -245,10 +255,10 @@ multiphase load. These are inferred from vmin/vmax for wye loads and from _calc_bus_vm_ll_bounds for delta loads. """ function _calc_load_vbounds(load::Dict, bus::Dict) - if load["conn"]=="wye" + if load["configuration"]==WYE vmin = bus["vmin"] vmax = bus["vmax"] - elseif load["conn"]=="delta" + elseif load["configuration"]==DELTA vmin, vmax = _calc_bus_vm_ll_bounds(bus) end return vmin, vmax @@ -259,9 +269,9 @@ Returns a Bool, indicating whether the convex hull of the voltage-dependent relationship needs a cone inclusion constraint. """ function _check_load_needs_cone(load::Dict) - if load["model"]=="constant_current" + if load["model"]==CURRENT return true - elseif load["model"]=="exponential" + elseif load["model"]==EXPONENTIAL return true else return false @@ -399,6 +409,16 @@ function _calc_branch_power_max(branch::Dict, bus::Dict) end +""" +Returns a total (shunt+series) power magnitude bound for the from and to side +of a branch. The total current rating also implies a current bound through the +upper bound on the voltage magnitude of the connected buses. +""" +function _calc_branch_power_max_frto(branch::Dict, bus_fr::Dict, bus_to::Dict) + return _calc_branch_power_max(branch, bus_fr), _calc_branch_power_max(branch, bus_to) +end + + """ Returns a valid series current magnitude bound for a branch. """ @@ -416,15 +436,12 @@ function _calc_branch_series_current_max(branch::Dict, bus_fr::Dict, bus_to::Dic # get valid bounds on total current c_max_fr_tot = _calc_branch_current_max(branch, bus_fr) c_max_to_tot = _calc_branch_current_max(branch, bus_to) - if ismissing(c_max_fr_tot) || ismissing(c_max_to_tot) - return missing - end # get valid bounds on shunt current y_fr = branch["g_fr"] + im* branch["b_fr"] y_to = branch["g_to"] + im* branch["b_to"] - c_max_fr_sh = abs.(y_fr)*vmax_fr - c_max_to_sh = abs.(y_to)*vmax_to + c_max_fr_sh = _mat_mult_rm_nan(abs.(y_fr), vmax_fr) + c_max_to_sh = _mat_mult_rm_nan(abs.(y_to), vmax_to) # now select element-wise lowest valid bound between fr and to N = 3 #TODO update for 4-wire @@ -490,12 +507,12 @@ function _make_multiconductor!(data::Dict{String,<:Any}, conductors::Real) end for (_, load) in data["load"] - load["model"] = "constant_power" - load["conn"] = "wye" + load["model"] = POWER + load["configuration"] = WYE end for (_, load) in data["gen"] - load["conn"] = "wye" + load["configuration"] = WYE end end @@ -648,16 +665,76 @@ end "Local wrapper method for JuMP.set_lower_bound, which skips NaN and infinite (-Inf only)" -function set_lower_bound(x::JuMP.VariableRef, bound) +function set_lower_bound(x::JuMP.VariableRef, bound; loose_bounds::Bool=false, pm=missing, category=:default) if !(isnan(bound) || bound==-Inf) JuMP.set_lower_bound(x, bound) + elseif loose_bounds + lbs = pm.ext[:loose_bounds] + JuMP.set_lower_bound(x, -lbs.bound_values[category]) + push!(lbs.loose_lb_vars, x) end end "Local wrapper method for JuMP.set_upper_bound, which skips NaN and infinite (+Inf only)" -function set_upper_bound(x::JuMP.VariableRef, bound) +function set_upper_bound(x::JuMP.VariableRef, bound; loose_bounds::Bool=false, pm=missing, category=:default) if !(isnan(bound) || bound==Inf) JuMP.set_upper_bound(x, bound) + elseif loose_bounds + lbs = pm.ext[:loose_bounds] + JuMP.set_upper_bound(x, lbs.bound_values[category]) + push!(lbs.loose_ub_vars, x) + end +end + + +"" +function sol_polar_voltage!(pm::_PM.AbstractPowerModel, solution::Dict) + if haskey(solution, "nw") + nws_data = solution["nw"] + else + nws_data = Dict("0" => solution) + end + + for (n, nw_data) in nws_data + if haskey(nw_data, "bus") + for (i,bus) in nw_data["bus"] + if haskey(bus, "vr") && haskey(bus, "vi") + vr = bus["vr"] + vi = bus["vi"] + if isa(vr, Dict) + bus["vm"] = Dict(t=>abs(vr[t]+im*vi[t]) for t in keys(vr)) + bus["va"] = Dict(t=>_wrap_to_pi(atan(vi[t], vr[t])) for t in keys(vr)) + else + bus["vm"] = abs.(vr .+ im*vi) + bus["va"] = _wrap_to_pi(atan.(vi, vr)) + end + end + end + end end end + +# BOUND manipulation methods (0*Inf->0 is often desired) +_sum_rm_nan(X::Vector) = sum([X[(!).(isnan.(X))]..., 0.0]) + + +"" +function _mat_mult_rm_nan(A::Matrix, B::Union{Matrix, Adjoint}) where T + N, A_ncols = size(A) + B_nrows, M = size(B) + @assert(A_ncols==B_nrows) + return [_sum_rm_nan(A[n,:].*B[:,m]) for n in 1:N, m in 1:M] +end + + +_mat_mult_rm_nan(A::Union{Matrix, Adjoint}, b::Vector) = dropdims(_mat_mult_rm_nan(A, reshape(b, length(b), 1)), dims=2) +_mat_mult_rm_nan(a::Vector, B::Union{Matrix, Adjoint}) = _mat_mult_rm_nan(reshape(a, length(a), 1), B) + + +"" +function _nan2zero(b, a; val=0) + and(x, y) = x && y + b[and.(isnan.(b), a.==val)] .= 0.0 + return b +end diff --git a/src/core/export.jl b/src/core/export.jl index 509983d50..e2048872f 100644 --- a/src/core/export.jl +++ b/src/core/export.jl @@ -22,10 +22,15 @@ end # the follow items are also exported for user-friendlyness when calling # `using PowerModelsDistribution` -# so that users do not need to import JuMP to use a solver with PowerModelsDistribution +# so that users do not need to import JuMP to use a solver with PowerModels import JuMP: with_optimizer export with_optimizer +# so that users do not need to import JuMP to use a solver with PowerModels +# note does appear to be work with JuMP v0.20, but throws "could not import" warning +import JuMP: optimizer_with_attributes +export optimizer_with_attributes + import MathOptInterface: TerminationStatusCode export TerminationStatusCode @@ -39,6 +44,8 @@ for status_code_enum in [TerminationStatusCode, ResultStatusCode] end end -# so that users do not need to import PowerModels -import PowerModels: ACPPowerModel, ACRPowerModel, DCPPowerModel, NFAPowerModel, SOCWRPowerModel -export ACPPowerModel, ACRPowerModel, DCPPowerModel, NFAPowerModel, SOCWRPowerModel +# PowerModels Exports +export ACPPowerModel, ACRPowerModel, DCPPowerModel, IVRPowerModel, NFAPowerModel, conductor_ids, ismulticonductor + +# InfrastructureModels Exports +export ids, ref, var, con, sol, nw_ids, nws, ismultinetwork diff --git a/src/core/objective.jl b/src/core/objective.jl index 25704e2a3..8fcf4bdb5 100644 --- a/src/core/objective.jl +++ b/src/core/objective.jl @@ -1,72 +1,129 @@ "a quadratic penalty for bus power slack variables" -function objective_min_bus_power_slack(pm::_PMs.AbstractPowerModel) +function objective_mc_min_slack_bus_power(pm::_PM.AbstractPowerModel) return JuMP.@objective(pm.model, Min, sum( sum( - sum( _PMs.var(pm, n, :p_slack, i)[c]^2 + _PMs.var(pm, n, :q_slack, i)[c]^2 for (i,bus) in nw_ref[:bus]) - for c in _PMs.conductor_ids(pm, n)) - for (n, nw_ref) in _PMs.nws(pm)) + sum( var(pm, n, :p_slack, i)[c]^2 + var(pm, n, :q_slack, i)[c]^2 for (i,bus) in nw_ref[:bus]) + for c in conductor_ids(pm, n)) + for (n, nw_ref) in nws(pm)) ) end "minimum load delta objective (continuous load shed) with storage" -function objective_mc_min_load_delta(pm::_PMs.AbstractPowerModel) - for (n, nw_ref) in _PMs.nws(pm) - _PMs.var(pm, n)[:delta_pg] = Dict(i => JuMP.@variable(pm.model, - [c in _PMs.conductor_ids(pm, n)], base_name="$(n)_$(i)_delta_pg", - start = 0.0) for i in _PMs.ids(pm, n, :gen)) +function objective_mc_min_load_setpoint_delta(pm::_PM.AbstractPowerModel) + for (n, nw_ref) in nws(pm) + var(pm, n)[:delta_pg] = Dict(i => JuMP.@variable(pm.model, + [c in conductor_ids(pm, n)], base_name="$(n)_$(i)_delta_pg", + start = 0.0) for i in ids(pm, n, :gen)) - _PMs.var(pm, n)[:delta_ps] = Dict(i => JuMP.@variable(pm.model, - [c in _PMs.conductor_ids(pm, n)], base_name="$(n)_$(i)_delta_ps", - start = 0.0) for i in _PMs.ids(pm, n, :storage)) + var(pm, n)[:delta_ps] = Dict(i => JuMP.@variable(pm.model, + [c in conductor_ids(pm, n)], base_name="$(n)_$(i)_delta_ps", + start = 0.0) for i in ids(pm, n, :storage)) - for c in _PMs.conductor_ids(pm, n) + for c in conductor_ids(pm, n) for (i, gen) in nw_ref[:gen] - JuMP.@constraint(pm.model, _PMs.var(pm, n, :delta_pg, i)[c] >= (gen["pg"][c] - _PMs.var(pm, n, :pg, i)[c])) - JuMP.@constraint(pm.model, _PMs.var(pm, n, :delta_pg, i)[c] >= -(gen["pg"][c] - _PMs.var(pm, n, :pg, i)[c])) + JuMP.@constraint(pm.model, var(pm, n, :delta_pg, i)[c] >= (gen["pg"][c] - var(pm, n, :pg, i)[c])) + JuMP.@constraint(pm.model, var(pm, n, :delta_pg, i)[c] >= -(gen["pg"][c] - var(pm, n, :pg, i)[c])) end for (i, strg) in nw_ref[:storage] - JuMP.@constraint(pm.model, _PMs.var(pm, n, :delta_ps, i)[c] >= (strg["ps"][c] - _PMs.var(pm, n, :ps, i)[c])) - JuMP.@constraint(pm.model, _PMs.var(pm, n, :delta_ps, i)[c] >= -(strg["ps"][c] - _PMs.var(pm, n, :ps, i)[c])) + JuMP.@constraint(pm.model, var(pm, n, :delta_ps, i)[c] >= (strg["ps"][c] - var(pm, n, :ps, i)[c])) + JuMP.@constraint(pm.model, var(pm, n, :delta_ps, i)[c] >= -(strg["ps"][c] - var(pm, n, :ps, i)[c])) end end end - load_weight = Dict(n => Dict(i => get(load, "weight", 1.0) for (i,load) in _PMs.ref(pm, n, :load)) for n in _PMs.nw_ids(pm)) - M = Dict(n => Dict(c => 10*maximum([load_weight[n][i]*abs(load["pd"][c]) for (i,load) in _PMs.ref(pm, n, :load)]) for c in _PMs.conductor_ids(pm, n)) for n in _PMs.nw_ids(pm)) + load_weight = Dict(n => Dict(i => get(load, "weight", 1.0) for (i,load) in ref(pm, n, :load)) for n in nw_ids(pm)) + M = Dict(n => Dict(c => 10*maximum([load_weight[n][i]*abs(load["pd"][c]) for (i,load) in ref(pm, n, :load)]) for c in conductor_ids(pm, n)) for n in nw_ids(pm)) JuMP.@objective(pm.model, Min, sum( sum( - sum( ( 10*(1 - _PMs.var(pm, n, :z_voltage, i)) for i in keys(nw_ref[:bus]))) + - sum( ( M[n][c]*(1 - _PMs.var(pm, n, :z_demand, i))) for i in keys(nw_ref[:load])) + - sum( (abs(shunt["gs"][c])*(1 - _PMs.var(pm, n, :z_shunt, i))) for (i,shunt) in nw_ref[:shunt]) + - sum( ( _PMs.var(pm, n, :delta_pg, i)[c] for i in keys(nw_ref[:gen]))) + - sum( ( _PMs.var(pm, n, :delta_ps, i)[c] for i in keys(nw_ref[:storage]))) - for c in _PMs.conductor_ids(pm, n)) - for (n, nw_ref) in _PMs.nws(pm)) + sum( ( 10*(1 - var(pm, n, :z_voltage, i)) for i in keys(nw_ref[:bus]))) + + sum( ( M[n][c]*(1 - var(pm, n, :z_demand, i))) for i in keys(nw_ref[:load])) + + sum( (abs(shunt["gs"][c])*(1 - var(pm, n, :z_shunt, i))) for (i,shunt) in nw_ref[:shunt]) + + sum( ( var(pm, n, :delta_pg, i)[c] for i in keys(nw_ref[:gen]))) + + sum( ( var(pm, n, :delta_ps, i)[c] for i in keys(nw_ref[:storage]))) + for c in conductor_ids(pm, n)) + for (n, nw_ref) in nws(pm)) ) end "maximum loadability objective (continuous load shed) with storage" -function objective_mc_max_loadability(pm::_PMs.AbstractPowerModel) - load_weight = Dict(n => Dict(i => get(load, "weight", 1.0) for (i,load) in _PMs.ref(pm, n, :load)) for n in _PMs.nw_ids(pm)) - M = Dict(n => Dict(c => 10*maximum([load_weight[n][i]*abs(load["pd"][c]) for (i,load) in _PMs.ref(pm, n, :load)]) for c in _PMs.conductor_ids(pm, n)) for n in _PMs.nw_ids(pm)) +function objective_mc_max_load_setpoint(pm::_PM.AbstractPowerModel) + load_weight = Dict(n => Dict(i => get(load, "weight", 1.0) for (i,load) in ref(pm, n, :load)) for n in nw_ids(pm)) + M = Dict(n => Dict(c => 10*maximum([load_weight[n][i]*abs(load["pd"][c]) for (i,load) in ref(pm, n, :load)]) for c in conductor_ids(pm, n)) for n in nw_ids(pm)) JuMP.@objective(pm.model, Max, sum( sum( - sum( ( 10*_PMs.var(pm, n, :z_voltage, i) for (i, bus) in nw_ref[:bus])) + - sum( ( M[n][c]*_PMs.var(pm, n, :z_demand, i)) for (i,load) in nw_ref[:load]) + - sum( (abs(shunt["gs"])*_PMs.var(pm, n, :z_shunt, i)) for (i,shunt) in nw_ref[:shunt]) + - sum( ( _PMs.var(pm, n, :z_gen, i) for (i,gen) in nw_ref[:gen])) + - sum( ( _PMs.var(pm, n, :z_storage, i) for (i,storage) in nw_ref[:storage])) - for c in _PMs.conductor_ids(pm, n)) - for (n, nw_ref) in _PMs.nws(pm)) + sum( ( 10*var(pm, n, :z_voltage, i) for (i, bus) in nw_ref[:bus])) + + sum( ( M[n][c]*var(pm, n, :z_demand, i)) for (i,load) in nw_ref[:load]) + + sum( (abs(shunt["gs"])*var(pm, n, :z_shunt, i)) for (i,shunt) in nw_ref[:shunt]) + + sum( ( var(pm, n, :z_gen, i) for (i,gen) in nw_ref[:gen])) + + sum( ( var(pm, n, :z_storage, i) for (i,storage) in nw_ref[:storage])) + for c in conductor_ids(pm, n)) + for (n, nw_ref) in nws(pm)) ) end + + +"Multiconductor adaptation of min fuel cost polynomial linquad objective" +function _PM._objective_min_fuel_cost_polynomial_linquad(pm::_PM.AbstractIVRModel; report::Bool=true) + gen_cost = Dict() + dcline_cost = Dict() + + for (n, nw_ref) in nws(pm) + for (i,gen) in nw_ref[:gen] + bus = gen["gen_bus"] + + #to avoid function calls inside of @NLconstraint: + pg = var(pm, n, :pg, i) + nc = length(conductor_ids(pm, n)) + if length(gen["cost"]) == 1 + gen_cost[(n,i)] = gen["cost"][1] + elseif length(gen["cost"]) == 2 + gen_cost[(n,i)] = JuMP.@NLexpression(pm.model, gen["cost"][1]*sum(pg[c] for c in 1:nc) + gen["cost"][2]) + elseif length(gen["cost"]) == 3 + gen_cost[(n,i)] = JuMP.@NLexpression(pm.model, gen["cost"][1]*sum(pg[c] for c in 1:nc)^2 + gen["cost"][2]*sum(pg[c] for c in 1:nc) + gen["cost"][3]) + else + gen_cost[(n,i)] = 0.0 + end + end + end + + return JuMP.@NLobjective(pm.model, Min, + sum( + sum( gen_cost[(n,i)] for (i,gen) in nw_ref[:gen] ) + + sum( dcline_cost[(n,i)] for (i,dcline) in nw_ref[:dcline] ) + for (n, nw_ref) in nws(pm)) + ) +end + + +"adds pg_cost variables and constraints" +function objective_variable_pg_cost(pm::_PM.AbstractIVRModel; report::Bool=true) + for (n, nw_ref) in nws(pm) + gen_lines = calc_cost_pwl_lines(nw_ref[:gen]) + + #to avoid function calls inside of @NLconstraint + pg_cost = var(pm, n)[:pg_cost] = JuMP.@variable(pm.model, + [i in ids(pm, n, :gen)], base_name="$(n)_pg_cost", + ) + report && _IM.sol_component_value(pm, n, :gen, :pg_cost, ids(pm, n, :gen), pg_cost) + + nc = length(conductor_ids(pm, n)) + + # gen pwl cost + for (i, gen) in nw_ref[:gen] + pg = var(pm, n, :pg, i) + for line in gen_lines[i] + JuMP.@NLconstraint(pm.model, pg_cost[i] >= line.slope*sum(pg[c] for c in 1:nc) + line.intercept) + end + end + end +end diff --git a/src/core/ref.jl b/src/core/ref.jl index c8ad91a6c..4bc62fd0c 100644 --- a/src/core/ref.jl +++ b/src/core/ref.jl @@ -2,7 +2,7 @@ import LinearAlgebra: diagm "" -function _calc_mc_voltage_product_bounds(pm::_PMs.AbstractPowerModel, buspairs; nw::Int=pm.cnw) +function _calc_mc_voltage_product_bounds(pm::_PM.AbstractPowerModel, buspairs; nw::Int=pm.cnw) wr_min = Dict([(bp, -Inf) for bp in buspairs]) wr_max = Dict([(bp, Inf) for bp in buspairs]) wi_min = Dict([(bp, -Inf) for bp in buspairs]) @@ -10,13 +10,13 @@ function _calc_mc_voltage_product_bounds(pm::_PMs.AbstractPowerModel, buspairs; for (i, j, c, d) in buspairs if i == j - bus = _PMs.ref(pm, nw, :bus)[i] + bus = ref(pm, nw, :bus)[i] vm_fr_max = bus["vmax"][c] vm_to_max = bus["vmax"][d] vm_fr_min = bus["vmin"][c] vm_to_min = bus["vmin"][d] else - buspair = _PMs.ref(pm, nw, :buspairs)[(i, j)] + buspair = ref(pm, nw, :buspairs)[(i, j)] vm_fr_max = buspair["vm_fr_max"][c] vm_to_max = buspair["vm_to_max"][d] vm_fr_min = buspair["vm_fr_min"][c] @@ -34,36 +34,46 @@ end "" -function _find_ref_buses(pm::_PMs.AbstractPowerModel, nw) - buses = _PMs.ref(pm, nw, :bus) +function _find_ref_buses(pm::_PM.AbstractPowerModel, nw) + buses = ref(pm, nw, :bus) return [b for (b,bus) in buses if bus["bus_type"]==3] # return [bus for (b,bus) in buses ] end -"Adds arcs for PMD transformers; for dclines and branches this is done in PMs" -function ref_add_arcs_trans!(pm::_PMs.AbstractPowerModel) - for nw in _PMs.nw_ids(pm) - if !haskey(_PMs.ref(pm, nw), :transformer) +"Adds arcs for PowerModelsDistribution transformers; for dclines and branches this is done in PowerModels" +function ref_add_arcs_transformer!(ref::Dict{Symbol,<:Any}, data::Dict{String,<:Any}) + if _IM.ismultinetwork(data) + nws_data = data["nw"] + else + nws_data = Dict("0" => data) + end + + for (n, nw_data) in nws_data + nw_id = parse(Int, n) + nw_ref = ref[:nw][nw_id] + + if !haskey(nw_ref, :transformer) # this might happen when parsing data from matlab format # the OpenDSS parser always inserts a trans dict - _PMs.ref(pm, nw)[:transformer] = Dict{Int, Any}() + nw_ref[:transformer] = Dict{Int, Any}() end - # dirty fix add arcs_from/to_trans and bus_arcs_trans - pm.ref[:nw][nw][:arcs_from_trans] = [(i, trans["f_bus"], trans["t_bus"]) for (i,trans) in _PMs.ref(pm, nw, :transformer)] - pm.ref[:nw][nw][:arcs_to_trans] = [(i, trans["t_bus"], trans["f_bus"]) for (i,trans) in _PMs.ref(pm, nw, :transformer)] - pm.ref[:nw][nw][:arcs_trans] = [pm.ref[:nw][nw][:arcs_from_trans]..., pm.ref[:nw][nw][:arcs_to_trans]...] - pm.ref[:nw][nw][:bus_arcs_trans] = Dict{Int64, Array{Any, 1}}() - for i in _PMs.ids(pm, nw, :bus) - pm.ref[:nw][nw][:bus_arcs_trans][i] = [e for e in pm.ref[:nw][nw][:arcs_trans] if e[2]==i] + + nw_ref[:arcs_from_trans] = [(i, trans["f_bus"], trans["t_bus"]) for (i,trans) in nw_ref[:transformer]] + nw_ref[:arcs_to_trans] = [(i, trans["t_bus"], trans["f_bus"]) for (i,trans) in nw_ref[:transformer]] + nw_ref[:arcs_trans] = [nw_ref[:arcs_from_trans]..., nw_ref[:arcs_to_trans]...] + nw_ref[:bus_arcs_trans] = Dict{Int64, Array{Any, 1}}() + + for (i,bus) in nw_ref[:bus] + nw_ref[:bus_arcs_trans][i] = [e for e in nw_ref[:arcs_trans] if e[2]==i] end end end "" -function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw) - trans = _PMs.ref(pm, nw, :transformer, i) +function _calc_mc_transformer_Tvi(pm::_PM.AbstractPowerModel, i::Int; nw=pm.cnw) + trans = ref(pm, nw, :transformer, i) # transformation matrices # Tv and Ti will be compositions of these Tbr = [0 0 1; 1 0 0; 0 1 0] # barrel roll @@ -75,7 +85,7 @@ function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw end end # make sure the secondary is y+123 - if trans["config_to"]["type"]!="wye" + if trans["config_to"]["type"]!=WYE Memento.error(_LOGGER, "Secondary should always be of wye type.") end if trans["config_to"]["cnd"]!=[1,2,3] @@ -94,11 +104,11 @@ function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw Memento.error(_LOGGER, "The polarity should be either \'+\' or \'-\', but got \'$polarity\'.") end dyz = trans["config_fr"]["type"] - if !(dyz in ["delta", "wye"]) + if !(dyz in [DELTA, WYE]) Memento.error(_LOGGER, "The winding type should be either delta or wye, but got \'$dyz\'.") end # for now, grounded by default - #grounded = length(trans["conn"])>5 && trans["conn"][6]=='n' + #grounded = length(trans["configuration"])>5 && trans["configuration"][6]=='n' # Tw will contain transformations related to permutation and polarity perm_to_trans = Dict( [1,2,3]=>diagm(0=>ones(Float64, 3)), @@ -109,7 +119,7 @@ function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw Tw = (polarity=='+') ? Tw : -Tw #Tw = diagm(0=>ones(Float64, 3)) vmult = 1.0 # compensate for change in LN - if dyz=="wye" + if dyz==WYE Tv_fr = Tw Tv_im = diagm(0=>ones(Float64, 3)) Ti_fr = Tw @@ -119,7 +129,7 @@ function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw # Ti_fr = [Ti_fr; ones(1,3)] # Ti_im = [Ti_im; zeros(1,3)] # end - elseif dyz=="delta" + elseif dyz==DELTA Tv_fr = Tdelt*Tw Tv_im = diagm(0=>ones(Float64, 3)) Ti_fr = Tw @@ -131,8 +141,8 @@ function _calc_mc_transformer_Tvi(pm::_PMs.AbstractPowerModel, i::Int; nw=pm.cnw # make equations dimensionless # if vbase across a transformer scales according to the ratio of vnom_kv, # this will simplify to 1.0 - bkv_fr = _PMs.ref(pm, nw, :bus, trans["f_bus"], "base_kv") - bkv_to = _PMs.ref(pm, nw, :bus, trans["t_bus"], "base_kv") + bkv_fr = ref(pm, nw, :bus, trans["f_bus"], "base_kv") + bkv_to = ref(pm, nw, :bus, trans["t_bus"], "base_kv") Cv_to = trans["config_fr"]["vm_nom"]/trans["config_to"]["vm_nom"]*bkv_to/bkv_fr # compensate for change of LN voltage of a delta winding Cv_to *= vmult diff --git a/src/core/types.jl b/src/core/types.jl index 422d0c484..131b59b08 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -1,11 +1,38 @@ -"" -abstract type AbstractNLPUBFModel <: _PMs.AbstractBFQPModel end +"Supported data model types" +@enum DataModel ENGINEERING MATHEMATICAL DSS MATPOWER +"Load Models" +@enum LoadModel POWER CURRENT IMPEDANCE EXPONENTIAL ZIP -"" -abstract type AbstractConicUBFModel <: _PMs.AbstractBFConicModel end +"Shunt Models" +@enum ShuntModel GENERIC CAPACITOR REACTOR +"Switch States" +@enum SwitchState OPEN CLOSED +"Generator, Solar, Storage, Wind Control Modes" +@enum ControlMode FREQUENCYDROOP ISOCHRONOUS + +"Configurations" +@enum ConnConfig WYE DELTA + +"Dispatchable" +@enum Dispatchable NO YES + +"Status" +@enum Status DISABLED ENABLED + +PowerModelsDistributionEnums = Union{DataModel,LoadModel,ShuntModel,SwitchState,ControlMode,ConnConfig,Dispatchable,Status} + +"Base Abstract NLP Unbalanced Branch Flow Model" +abstract type AbstractNLPUBFModel <: _PM.AbstractBFQPModel end + + +"Base Abstract Conic Unbalanced Branch Flow Model" +abstract type AbstractConicUBFModel <: _PM.AbstractBFConicModel end + + +"Collection of Unbalanced Branch Flow Models" AbstractUBFModels = Union{AbstractNLPUBFModel, AbstractConicUBFModel} @@ -17,6 +44,7 @@ abstract type SDPUBFModel <: AbstractConicUBFModel end abstract type SDPUBFKCLMXModel <: SDPUBFModel end +"Collection of Semidefinite Models" # TODO Better documentation, name? KCLMXModels = Union{SDPUBFKCLMXModel} @@ -28,6 +56,7 @@ abstract type SOCNLPUBFModel <: AbstractNLPUBFModel end abstract type SOCConicUBFModel <: AbstractConicUBFModel end +"Collection of Second Order Cone Models" SOCUBFModels = Union{SOCNLPUBFModel, SOCConicUBFModel} @@ -35,27 +64,31 @@ SOCUBFModels = Union{SOCNLPUBFModel, SOCConicUBFModel} abstract type AbstractLPUBFModel <: AbstractNLPUBFModel end -"LinDist3Flow per Sankur et al 2016, using vector variables for power, voltage and current" +""" +LinDist3Flow per Arnold et al. (2016), using vector variables for power, voltage and current + +D. B. Arnold, M. Sankur, R. Dobbe, K. Brady, D. S. Callaway and A. Von Meier, "Optimal dispatch of reactive power for voltage regulation and balancing in unbalanced distribution systems," 2016 IEEE Power and Energy Society General Meeting (PESGM), Boston, MA, 2016, pp. 1-5, doi: 10.1109/PESGM.2016.7741261. +""" abstract type LPUBFDiagModel <: AbstractLPUBFModel end const LinDist3FlowModel = LPUBFDiagModel # more popular name for it "default SDP unbalanced DistFlow constructor" -mutable struct SDPUBFPowerModel <: SDPUBFModel _PMs.@pm_fields end +mutable struct SDPUBFPowerModel <: SDPUBFModel _PM.@pm_fields end "default SDP unbalanced DistFlow with matrix KCL constructor" -mutable struct SDPUBFKCLMXPowerModel <: SDPUBFKCLMXModel _PMs.@pm_fields end +mutable struct SDPUBFKCLMXPowerModel <: SDPUBFKCLMXModel _PM.@pm_fields end "default SOC unbalanced DistFlow constructor" -mutable struct SOCNLPUBFPowerModel <: SOCNLPUBFModel _PMs.@pm_fields end +mutable struct SOCNLPUBFPowerModel <: SOCNLPUBFModel _PM.@pm_fields end "default SOC unbalanced DistFlow constructor" -mutable struct SOCConicUBFPowerModel <: SOCConicUBFModel _PMs.@pm_fields end +mutable struct SOCConicUBFPowerModel <: SOCConicUBFModel _PM.@pm_fields end "default LP unbalanced DistFlow constructor" -mutable struct LPUBFDiagPowerModel <: LPUBFDiagModel _PMs.@pm_fields end +mutable struct LPUBFDiagPowerModel <: LPUBFDiagModel _PM.@pm_fields end const LinDist3FlowPowerModel = LPUBFDiagPowerModel # more popular name diff --git a/src/core/variable.jl b/src/core/variable.jl index bde977daa..cf3218e1c 100644 --- a/src/core/variable.jl +++ b/src/core/variable.jl @@ -9,37 +9,37 @@ end function comp_start_value(comp::Dict{String,<:Any}, key::String, default) - return _PMs.comp_start_value(comp, key, default) + return _PM.comp_start_value(comp, key, default) end "" -function variable_mc_voltage_angle(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_angle(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - va = _PMs.var(pm, nw)[:va] = Dict(i => JuMP.@variable(pm.model, + va = var(pm, nw)[:va] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_va_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "va_start", 0.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "va_start", 0.0) + ) for i in ids(pm, nw, :bus) ) - report && _PMs.sol_component_value(pm, nw, :bus, :va, _PMs.ids(pm, nw, :bus), va) + report && _IM.sol_component_value(pm, nw, :bus, :va, ids(pm, nw, :bus), va) end "" -function variable_mc_voltage_magnitude(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_magnitude_only(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - vm = _PMs.var(pm, nw)[:vm] = Dict(i => JuMP.@variable(pm.model, + vm = var(pm, nw)[:vm] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_vm_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "vm_start", c, 1.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "vm_start", c, 1.0) + ) for i in ids(pm, nw, :bus) ) if bounded - for (i,bus) in _PMs.ref(pm, nw, :bus) + for (i,bus) in ref(pm, nw, :bus) if haskey(bus, "vmin") set_lower_bound.(vm[i], bus["vmin"]) end @@ -49,22 +49,22 @@ function variable_mc_voltage_magnitude(pm::_PMs.AbstractPowerModel; nw::Int=pm.c end end - report && _PMs.sol_component_value(pm, nw, :bus, :vm, _PMs.ids(pm, nw, :bus), vm) + report && _IM.sol_component_value(pm, nw, :bus, :vm, ids(pm, nw, :bus), vm) end "" -function variable_mc_voltage_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - vr = _PMs.var(pm, nw)[:vr] = Dict(i => JuMP.@variable(pm.model, + vr = var(pm, nw)[:vr] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_vr_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "vr_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "vr_start", c, 0.0) + ) for i in ids(pm, nw, :bus) ) if bounded - for (i,bus) in _PMs.ref(pm, nw, :bus) + for (i,bus) in ref(pm, nw, :bus) if haskey(bus, "vmax") set_lower_bound.(vr[i], -bus["vmax"]) set_upper_bound.(vr[i], bus["vmax"]) @@ -72,22 +72,22 @@ function variable_mc_voltage_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, b end end - report && _PMs.sol_component_value(pm, nw, :bus, :vr, _PMs.ids(pm, nw, :bus), vr) + report && _IM.sol_component_value(pm, nw, :bus, :vr, ids(pm, nw, :bus), vr) end "" -function variable_mc_voltage_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - vi = _PMs.var(pm, nw)[:vi] = Dict(i => JuMP.@variable(pm.model, + vi = var(pm, nw)[:vi] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_vi_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "vi_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "vi_start", c, 0.0) + ) for i in ids(pm, nw, :bus) ) if bounded - for (i,bus) in _PMs.ref(pm, nw, :bus) + for (i,bus) in ref(pm, nw, :bus) if haskey(bus, "vmax") set_lower_bound.(vi[i], -bus["vmax"]) set_upper_bound.(vi[i], bus["vmax"]) @@ -95,36 +95,36 @@ function variable_mc_voltage_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.c end end - report && _PMs.sol_component_value(pm, nw, :bus, :vi, _PMs.ids(pm, nw, :bus), vi) + report && _IM.sol_component_value(pm, nw, :bus, :vi, ids(pm, nw, :bus), vi) end "branch flow variables, delegated back to PowerModels" -function variable_mc_branch_flow(pm::_PMs.AbstractPowerModel; kwargs...) - variable_mc_branch_flow_active(pm; kwargs...) - variable_mc_branch_flow_reactive(pm; kwargs...) +function variable_mc_branch_power(pm::_PM.AbstractPowerModel; kwargs...) + variable_mc_branch_power_real(pm; kwargs...) + variable_mc_branch_power_imaginary(pm; kwargs...) end "variable: `p[l,i,j]` for `(l,i,j)` in `arcs`" -function variable_mc_branch_flow_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_power_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - p = _PMs.var(pm, nw)[:p] = Dict((l,i,j) => JuMP.@variable(pm.model, + p = var(pm, nw)[:p] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_p_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "p_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs) + start = comp_start_value(ref(pm, nw, :branch, l), "p_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs) - smax = _calc_branch_power_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i)) + for (l,i,j) in ref(pm, nw, :arcs) + smax = _calc_branch_power_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i)) set_upper_bound.(p[(l,i,j)], smax) set_lower_bound.(p[(l,i,j)], -smax) end end - for (l,branch) in _PMs.ref(pm, nw, :branch) + for (l,branch) in ref(pm, nw, :branch) if haskey(branch, "pf_start") f_idx = (l, branch["f_bus"], branch["t_bus"]) set_start_value(p[f_idx], branch["pf_start"]) @@ -135,29 +135,29 @@ function variable_mc_branch_flow_active(pm::_PMs.AbstractPowerModel; nw::Int=pm. end end - report && _PMs.sol_component_value_edge(pm, nw, :branch, :pf, :pt, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), p) + report && _IM.sol_component_value_edge(pm, nw, :branch, :pf, :pt, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), p) end "variable: `q[l,i,j]` for `(l,i,j)` in `arcs`" -function variable_mc_branch_flow_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_power_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - q = _PMs.var(pm, nw)[:q] = Dict((l,i,j) => JuMP.@variable(pm.model, + q = var(pm, nw)[:q] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_q_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "q_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs) + start = comp_start_value(ref(pm, nw, :branch, l), "q_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs) - smax = _calc_branch_power_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i)) + for (l,i,j) in ref(pm, nw, :arcs) + smax = _calc_branch_power_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i)) set_upper_bound.(q[(l,i,j)], smax) set_lower_bound.(q[(l,i,j)], -smax) end end - for (l,branch) in _PMs.ref(pm, nw, :branch) + for (l,branch) in ref(pm, nw, :branch) if haskey(branch, "qf_start") f_idx = (l, branch["f_bus"], branch["t_bus"]) set_start_value(q[f_idx], branch["qf_start"]) @@ -168,128 +168,128 @@ function variable_mc_branch_flow_reactive(pm::_PMs.AbstractPowerModel; nw::Int=p end end - report && _PMs.sol_component_value_edge(pm, nw, :branch, :qf, :qt, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), q) + report && _IM.sol_component_value_edge(pm, nw, :branch, :qf, :qt, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), q) end "variable: `cr[l,i,j]` for `(l,i,j)` in `arcs`" -function variable_mc_branch_current_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - branch = _PMs.ref(pm, nw, :branch) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_current_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + branch = ref(pm, nw, :branch) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - cr = _PMs.var(pm, nw)[:cr] = Dict((l,i,j) => JuMP.@variable(pm.model, + cr = var(pm, nw)[:cr] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_cr_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "cr_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs) + start = comp_start_value(ref(pm, nw, :branch, l), "cr_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs) - cmax = _calc_branch_current_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i)) + for (l,i,j) in ref(pm, nw, :arcs) + cmax = _calc_branch_current_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i)) set_upper_bound.(cr[(l,i,j)], cmax) set_lower_bound.(cr[(l,i,j)], -cmax) end end - report && _PMs.sol_component_value_edge(pm, nw, :branch, :cr_fr, :cr_to, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), cr) + report && _IM.sol_component_value_edge(pm, nw, :branch, :cr_fr, :cr_to, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), cr) end "variable: `ci[l,i,j] ` for `(l,i,j)` in `arcs`" -function variable_mc_branch_current_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - branch = _PMs.ref(pm, nw, :branch) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_current_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + branch = ref(pm, nw, :branch) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - ci = _PMs.var(pm, nw)[:ci] = Dict((l,i,j) => JuMP.@variable(pm.model, + ci = var(pm, nw)[:ci] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_ci_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "ci_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs) + start = comp_start_value(ref(pm, nw, :branch, l), "ci_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs) - cmax = _calc_branch_current_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i)) + for (l,i,j) in ref(pm, nw, :arcs) + cmax = _calc_branch_current_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i)) set_upper_bound.(ci[(l,i,j)], cmax) set_lower_bound.(ci[(l,i,j)], -cmax) end end - report && _PMs.sol_component_value_edge(pm, nw, :branch, :ci_fr, :ci_to, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), ci) + report && _IM.sol_component_value_edge(pm, nw, :branch, :ci_fr, :ci_to, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), ci) end "variable: `csr[l]` for `l` in `branch`" -function variable_mc_branch_series_current_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - branch = _PMs.ref(pm, nw, :branch) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_current_series_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + branch = ref(pm, nw, :branch) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - csr = _PMs.var(pm, nw)[:csr] = Dict(l => JuMP.@variable(pm.model, + csr = var(pm, nw)[:csr] = Dict(l => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_csr_$(l)", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "csr_start", c, 0.0) - ) for l in _PMs.ids(pm, nw, :branch) + start = comp_start_value(ref(pm, nw, :branch, l), "csr_start", c, 0.0) + ) for l in ids(pm, nw, :branch) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from) - cmax = _calc_branch_series_current_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i), _PMs.ref(pm, nw, :bus, j)) + for (l,i,j) in ref(pm, nw, :arcs_from) + cmax = _calc_branch_series_current_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i), ref(pm, nw, :bus, j)) set_upper_bound.(csr[l], cmax) set_lower_bound.(csr[l], -cmax) end end - report && _PMs.sol_component_value(pm, nw, :branch, :csr_fr, _PMs.ids(pm, nw, :branch), csr) + report && _IM.sol_component_value(pm, nw, :branch, :csr_fr, ids(pm, nw, :branch), csr) end "variable: `csi[l]` for `l` in `branch`" -function variable_mc_branch_series_current_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - branch = _PMs.ref(pm, nw, :branch) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_branch_current_series_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + branch = ref(pm, nw, :branch) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - csi = _PMs.var(pm, nw)[:csi] = Dict(l => JuMP.@variable(pm.model, + csi = var(pm, nw)[:csi] = Dict(l => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_csi_$(l)", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "csi_start", c, 0.0) - ) for l in _PMs.ids(pm, nw, :branch) + start = comp_start_value(ref(pm, nw, :branch, l), "csi_start", c, 0.0) + ) for l in ids(pm, nw, :branch) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from) - cmax = _calc_branch_series_current_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i), _PMs.ref(pm, nw, :bus, j)) + for (l,i,j) in ref(pm, nw, :arcs_from) + cmax = _calc_branch_series_current_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i), ref(pm, nw, :bus, j)) set_upper_bound.(csi[l], cmax) set_lower_bound.(csi[l], -cmax) end end - report && _PMs.sol_component_value(pm, nw, :branch, :csi_fr, _PMs.ids(pm, nw, :branch), csi) + report && _IM.sol_component_value(pm, nw, :branch, :csi_fr, ids(pm, nw, :branch), csi) end "variable: `cr[l,i,j]` for `(l,i,j)` in `arcs`" -function variable_mc_transformer_current_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - #trans = _PMs.ref(pm, nw, :transformer) - #bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_transformer_current_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + #trans = ref(pm, nw, :transformer) + #bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - cr = _PMs.var(pm, nw)[:crt] = Dict((l,i,j) => JuMP.@variable(pm.model, + cr = var(pm, nw)[:crt] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_crt_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :transformer, l), "cr_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_trans) + start = comp_start_value(ref(pm, nw, :transformer, l), "cr_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs_trans) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans) - trans = _PMs.ref(pm, nw, :transformer, l) - f_bus = _PMs.ref(pm, nw, :bus, i) - t_bus = _PMs.ref(pm, nw, :bus, j) + for (l,i,j) in ref(pm, nw, :arcs_from_trans) + trans = ref(pm, nw, :transformer, l) + f_bus = ref(pm, nw, :bus, i) + t_bus = ref(pm, nw, :bus, j) cmax_fr, cmax_to = _calc_transformer_current_max_frto(trans, f_bus, t_bus) set_lower_bound(cr[(l,i,j)], -cmax_fr) set_upper_bound(cr[(l,i,j)], cmax_fr) @@ -298,28 +298,28 @@ function variable_mc_transformer_current_real(pm::_PMs.AbstractPowerModel; nw::I end end - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :cr_fr, :cr_to, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), cr) + report && _IM.sol_component_value_edge(pm, nw, :transformer, :cr_fr, :cr_to, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), cr) end "variable: `ci[l,i,j] ` for `(l,i,j)` in `arcs`" -function variable_mc_transformer_current_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - #trans = _PMs.ref(pm, nw, :transformer) - #bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_transformer_current_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + #trans = ref(pm, nw, :transformer) + #bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - ci = _PMs.var(pm, nw)[:cit] = Dict((l,i,j) => JuMP.@variable(pm.model, + ci = var(pm, nw)[:cit] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_cit_$((l,i,j))", - start = comp_start_value(_PMs.ref(pm, nw, :transformer, l), "ci_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_trans) + start = comp_start_value(ref(pm, nw, :transformer, l), "ci_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs_trans) ) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans) - trans = _PMs.ref(pm, nw, :transformer, l) - f_bus = _PMs.ref(pm, nw, :bus, i) - t_bus = _PMs.ref(pm, nw, :bus, j) + for (l,i,j) in ref(pm, nw, :arcs_from_trans) + trans = ref(pm, nw, :transformer, l) + f_bus = ref(pm, nw, :bus, i) + t_bus = ref(pm, nw, :bus, j) cmax_fr, cmax_to = _calc_transformer_current_max_frto(trans, f_bus, t_bus) set_lower_bound(ci[(l,i,j)], -cmax_fr) set_upper_bound(ci[(l,i,j)], cmax_fr) @@ -328,32 +328,24 @@ function variable_mc_transformer_current_imaginary(pm::_PMs.AbstractPowerModel; end end - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :ci_fr, :ci_to, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), ci) + report && _IM.sol_component_value_edge(pm, nw, :transformer, :ci_fr, :ci_to, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), ci) end - -# "voltage variables, relaxed form" -# function variable_mc_voltage(pm::_PMs.AbstractWRModel; kwargs...) -# variable_mc_voltage_magnitude_sqr(pm; kwargs...) -# variable_mc_voltage_product(pm; kwargs...) -# end - - "variable: `w[i] >= 0` for `i` in `buses" -function variable_mc_voltage_magnitude_sqr(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_magnitude_sqr(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - w = _PMs.var(pm, nw)[:w] = Dict(i => JuMP.@variable(pm.model, + w = var(pm, nw)[:w] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_w_$(i)", lower_bound = 0.0, - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "w_start", 1.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "w_start", 1.0) + ) for i in ids(pm, nw, :bus) ) if bounded - for (i,bus) in _PMs.ref(pm, nw, :bus) + for (i,bus) in ref(pm, nw, :bus) if haskey(bus, "vmin") set_lower_bound.(w[i], bus["vmin"].^2) end @@ -363,35 +355,81 @@ function variable_mc_voltage_magnitude_sqr(pm::_PMs.AbstractPowerModel; nw::Int= end end - report && _PMs.sol_component_value(pm, nw, :bus, :w, _PMs.ids(pm, nw, :bus), w) + report && _IM.sol_component_value(pm, nw, :bus, :w, ids(pm, nw, :bus), w) end "variables for modeling storage units, includes grid injection and internal variables" -function variable_mc_storage(pm::_PMs.AbstractPowerModel; kwargs...) - variable_mc_storage_active(pm; kwargs...) - variable_mc_storage_reactive(pm; kwargs...) +function variable_mc_storage_power(pm::_PM.AbstractPowerModel; kwargs...) + variable_mc_storage_power_real(pm; kwargs...) + variable_mc_storage_power_imaginary(pm; kwargs...) + variable_mc_storage_power_control_imaginary(pm; kwargs...) + _PM.variable_storage_current(pm; kwargs...) # TODO storage current variable for multiconductor + _PM.variable_storage_energy(pm; kwargs...) + _PM.variable_storage_charge(pm; kwargs...) + _PM.variable_storage_discharge(pm; kwargs...) +end + + +"" +function variable_mc_storage_power_mi(pm::_PM.AbstractPowerModel; relax::Bool=false, kwargs...) + _PM.variable_storage_current(pm; kwargs...) # TODO storage current variable for multiconductor + _PM.variable_storage_energy(pm; kwargs...) + _PM.variable_storage_charge(pm; kwargs...) + _PM.variable_storage_discharge(pm; kwargs...) + variable_mc_storage_indicator(pm; relax=relax, kwargs...) + variable_mc_storage_power_on_off(pm; kwargs...) + variable_mc_storage_power_control_imaginary(pm; kwargs...) +end + + +""" +a reactive power slack variable that enables the storage device to inject or +consume reactive power at its connecting bus, subject to the injection limits +of the device. +""" +function variable_mc_storage_power_control_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + # TODO properly adapt this new control variable to multiconductor + cnds = conductor_ids(pm; nw=nw) + ncnds = length(cnds) + + qsc = var(pm, nw)[:qsc] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :storage)], base_name="$(nw)_qsc_$(i)", + start = _PM.comp_start_value(ref(pm, nw, :storage, i), "qsc_start") + ) + + if bounded + inj_lb, inj_ub = _PM.ref_calc_storage_injection_bounds(ref(pm, nw, :storage), ref(pm, nw, :bus)) + for (i,storage) in ref(pm, nw, :storage) + if !isinf(sum(inj_lb[i])) || haskey(storage, "qmin") + set_lower_bound(qsc[i], max(sum(inj_lb[i]), sum(get(storage, "qmin", -Inf)))) + end + if !isinf(sum(inj_ub[i])) || haskey(storage, "qmax") + set_upper_bound(qsc[i], min(sum(inj_ub[i]), sum(get(storage, "qmax", Inf)))) + end + end + end - _PMs.variable_storage_energy(pm; kwargs...) - _PMs.variable_storage_charge(pm; kwargs...) - _PMs.variable_storage_discharge(pm; kwargs...) + report && _IM.sol_component_value(pm, nw, :storage, :qsc, ids(pm, nw, :storage), qsc) end -function variable_mc_storage_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) + +"" +function variable_mc_storage_power_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - ps = _PMs.var(pm, nw)[:ps] = Dict(i => JuMP.@variable(pm.model, + ps = var(pm, nw)[:ps] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_ps_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "ps_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :storage) + start = comp_start_value(ref(pm, nw, :storage, i), "ps_start", c, 0.0) + ) for i in ids(pm, nw, :storage) ) if bounded for c in cnds - flow_lb, flow_ub = _PMs.ref_calc_storage_injection_bounds(_PMs.ref(pm, nw, :storage), _PMs.ref(pm, nw, :bus), c) + flow_lb, flow_ub = _PM.ref_calc_storage_injection_bounds(ref(pm, nw, :storage), ref(pm, nw, :bus), c) - for i in _PMs.ids(pm, nw, :storage) + for i in ids(pm, nw, :storage) if !isinf(flow_lb[i]) set_lower_bound(ps[i][c], flow_lb[i]) end @@ -402,24 +440,26 @@ function variable_mc_storage_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, end end - report && _PMs.sol_component_value(pm, nw, :storage, :ps, _PMs.ids(pm, nw, :storage), ps) + report && _IM.sol_component_value(pm, nw, :storage, :ps, ids(pm, nw, :storage), ps) end -function variable_mc_storage_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) + +"" +function variable_mc_storage_power_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - qs = _PMs.var(pm, nw)[:qs] = Dict(i => JuMP.@variable(pm.model, + qs = var(pm, nw)[:qs] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_qs_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "qs_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :storage) + start = comp_start_value(ref(pm, nw, :storage, i), "qs_start", c, 0.0) + ) for i in ids(pm, nw, :storage) ) if bounded for c in cnds - flow_lb, flow_ub = _PMs.ref_calc_storage_injection_bounds(_PMs.ref(pm, nw, :storage), _PMs.ref(pm, nw, :bus), c) + flow_lb, flow_ub = _PM.ref_calc_storage_injection_bounds(ref(pm, nw, :storage), ref(pm, nw, :bus), c) - for i in _PMs.ids(pm, nw, :storage) + for i in ids(pm, nw, :storage) if !isinf(flow_lb[i]) set_lower_bound(qs[i][c], flow_lb[i]) end @@ -430,69 +470,68 @@ function variable_mc_storage_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cn end end - report && _PMs.sol_component_value(pm, nw, :storage, :qs, _PMs.ids(pm, nw, :storage), qs) + report && _IM.sol_component_value(pm, nw, :storage, :qs, ids(pm, nw, :storage), qs) end - "generates variables for both `active` and `reactive` slack at each bus" -function variable_mc_bus_power_slack(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) - variable_mc_active_bus_power_slack(pm; nw=nw, kwargs...) - variable_mc_reactive_bus_power_slack(pm; nw=nw, kwargs...) +function variable_mc_slack_bus_power(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) + variable_mc_slack_bus_power_real(pm; nw=nw, kwargs...) + variable_mc_slack_bus_power_imaginary(pm; nw=nw, kwargs...) end "" -function variable_mc_active_bus_power_slack(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_slack_bus_power_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - p_slack = _PMs.var(pm, nw)[:p_slack] = Dict(i => JuMP.@variable(pm.model, + p_slack = var(pm, nw)[:p_slack] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_p_slack_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "p_slack_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "p_slack_start", cnd, 0.0) + ) for i in ids(pm, nw, :bus) ) - report && _PMs.sol_component_value(pm, nw, :bus, :p_slack, _PMs.ids(pm, nw, :bus), p_slack) + report && _IM.sol_component_value(pm, nw, :bus, :p_slack, ids(pm, nw, :bus), p_slack) end "" -function variable_mc_reactive_bus_power_slack(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_slack_bus_power_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - q_slack = _PMs.var(pm, nw)[:q_slack] = Dict(i => JuMP.@variable(pm.model, + q_slack = var(pm, nw)[:q_slack] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_q_slack_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "q_slack_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :bus) + start = comp_start_value(ref(pm, nw, :bus, i), "q_slack_start", cnd, 0.0) + ) for i in ids(pm, nw, :bus) ) - report && _PMs.sol_component_value(pm, nw, :bus, :q_slack, _PMs.ids(pm, nw, :bus), q_slack) + report && _IM.sol_component_value(pm, nw, :bus, :q_slack, ids(pm, nw, :bus), q_slack) end "Creates variables for both `active` and `reactive` power flow at each transformer." -function variable_mc_transformer_flow(pm::_PMs.AbstractPowerModel; kwargs...) - variable_mc_transformer_flow_active(pm; kwargs...) - variable_mc_transformer_flow_reactive(pm; kwargs...) +function variable_mc_transformer_power(pm::_PM.AbstractPowerModel; kwargs...) + variable_mc_transformer_power_real(pm; kwargs...) + variable_mc_transformer_power_imaginary(pm; kwargs...) end "Create variables for the active power flowing into all transformer windings." -function variable_mc_transformer_flow_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_transformer_power_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - pt = _PMs.var(pm, nw)[:pt] = Dict((l,i,j) => JuMP.@variable(pm.model, + pt = var(pm, nw)[:pt] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_pt_$((l,i,j))", - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_trans) + ) for (l,i,j) in ref(pm, nw, :arcs_trans) ) if bounded - for arc in _PMs.ref(pm, nw, :arcs_from_trans) + for arc in ref(pm, nw, :arcs_from_trans) (t,i,j) = arc - rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(_PMs.ref(pm, nw, :transformer, t), _PMs.ref(pm, nw, :bus, i), _PMs.ref(pm, nw, :bus, j)) + rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(ref(pm, nw, :transformer, t), ref(pm, nw, :bus, i), ref(pm, nw, :bus, j)) set_lower_bound.(pt[(t,i,j)], -rate_a_fr) set_upper_bound.(pt[(t,i,j)], rate_a_fr) set_lower_bound.(pt[(t,j,i)], -rate_a_fr) @@ -500,7 +539,7 @@ function variable_mc_transformer_flow_active(pm::_PMs.AbstractPowerModel; nw::In end end - for (l,transformer) in _PMs.ref(pm, nw, :transformer) + for (l,transformer) in ref(pm, nw, :transformer) if haskey(transformer, "pf_start") f_idx = (l, transformer["f_bus"], transformer["t_bus"]) set_start_value(pt[f_idx], branch["pf_start"]) @@ -511,25 +550,25 @@ function variable_mc_transformer_flow_active(pm::_PMs.AbstractPowerModel; nw::In end end - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :pf, :pt, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), pt) + report && _IM.sol_component_value_edge(pm, nw, :transformer, :pf, :pt, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), pt) end "Create variables for the reactive power flowing into all transformer windings." -function variable_mc_transformer_flow_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_transformer_power_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - qt = _PMs.var(pm, nw)[:qt] = Dict((l,i,j) => JuMP.@variable(pm.model, + qt = var(pm, nw)[:qt] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_qt_$((l,i,j))", start = 0.0 - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_trans) + ) for (l,i,j) in ref(pm, nw, :arcs_trans) ) if bounded - for arc in _PMs.ref(pm, nw, :arcs_from_trans) + for arc in ref(pm, nw, :arcs_from_trans) (t,i,j) = arc - rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(_PMs.ref(pm, nw, :transformer, t), _PMs.ref(pm, nw, :bus, i), _PMs.ref(pm, nw, :bus, j)) + rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(ref(pm, nw, :transformer, t), ref(pm, nw, :bus, i), ref(pm, nw, :bus, j)) set_lower_bound.(qt[(t,i,j)], -rate_a_fr) set_upper_bound.(qt[(t,i,j)], rate_a_fr) @@ -538,7 +577,7 @@ function variable_mc_transformer_flow_reactive(pm::_PMs.AbstractPowerModel; nw:: end end - for (l,transformer) in _PMs.ref(pm, nw, :transformer) + for (l,transformer) in ref(pm, nw, :transformer) if haskey(transformer, "qf_start") f_idx = (l, transformer["f_bus"], transformer["t_bus"]) set_start_value(qt[f_idx], branch["qf_start"]) @@ -549,31 +588,31 @@ function variable_mc_transformer_flow_reactive(pm::_PMs.AbstractPowerModel; nw:: end end - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :qf, :qt, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), qt) + report && _IM.sol_component_value_edge(pm, nw, :transformer, :qf, :qt, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), qt) end "Create tap variables." -function variable_mc_oltc_tap(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) +function variable_mc_oltc_transformer_tap(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) # when extending to 4-wire, this should iterate only over the phase conductors - cnds = _PMs.conductor_ids(pm; nw=nw) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) nph = 3 - p_oltc_ids = [id for (id,trans) in _PMs.ref(pm, nw, :transformer) if !all(trans["fixed"])] - tap = _PMs.var(pm, nw)[:tap] = Dict(i => JuMP.@variable(pm.model, + p_oltc_ids = [id for (id,trans) in ref(pm, nw, :transformer) if !all(trans["tm_fix"])] + tap = var(pm, nw)[:tap] = Dict(i => JuMP.@variable(pm.model, [p in 1:nph], base_name="$(nw)_tm_$(i)", - start=_PMs.ref(pm, nw, :transformer, i, "tm")[p] + start=ref(pm, nw, :transformer, i, "tm_set")[p] ) for i in p_oltc_ids) if bounded for tr_id in p_oltc_ids, p in 1:nph - set_lower_bound(_PMs.var(pm, nw)[:tap][tr_id][p], _PMs.ref(pm, nw, :transformer, tr_id, "tm_min")[p]) - set_upper_bound(_PMs.var(pm, nw)[:tap][tr_id][p], _PMs.ref(pm, nw, :transformer, tr_id, "tm_max")[p]) + set_lower_bound(var(pm, nw)[:tap][tr_id][p], ref(pm, nw, :transformer, tr_id, "tm_lb")[p]) + set_upper_bound(var(pm, nw)[:tap][tr_id][p], ref(pm, nw, :transformer, tr_id, "tm_ub")[p]) end end - report && _PMs.sol_component_value(pm, nw, :transformer, :tap, _PMs.ids(pm, nw, :transformer), tap) + report && _IM.sol_component_value(pm, nw, :transformer, :tap, ids(pm, nw, :transformer), tap) end @@ -582,228 +621,257 @@ Create a dictionary with values of type Any for the load. Depending on the load model, this can be a parameter or a NLexpression. These will be inserted into KCL. """ -function variable_mc_load(pm::_PMs.AbstractPowerModel; nw=pm.cnw, bounded::Bool=true, report::Bool=true) - _PMs.var(pm, nw)[:pd] = Dict{Int, Any}() - _PMs.var(pm, nw)[:qd] = Dict{Int, Any}() - _PMs.var(pm, nw)[:pd_bus] = Dict{Int, Any}() - _PMs.var(pm, nw)[:qd_bus] = Dict{Int, Any}() +function variable_mc_load_setpoint(pm::_PM.AbstractPowerModel; nw=pm.cnw, bounded::Bool=true, report::Bool=true) + var(pm, nw)[:pd] = Dict{Int, Any}() + var(pm, nw)[:qd] = Dict{Int, Any}() + var(pm, nw)[:pd_bus] = Dict{Int, Any}() + var(pm, nw)[:qd_bus] = Dict{Int, Any}() end "Create variables for demand status" -function variable_mc_indicator_demand(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) +function variable_mc_load_indicator(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) # this is not indexedon cnd; why used in start value? cnd = 1 if relax - z_demand = _PMs.var(pm, nw)[:z_demand] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :load)], base_name="$(nw)_z_demand", + z_demand = var(pm, nw)[:z_demand] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :load)], base_name="$(nw)_z_demand", lower_bound = 0, upper_bound = 1, - start = comp_start_value(_PMs.ref(pm, nw, :load, i), "z_demand_on_start", 1.0) + start = comp_start_value(ref(pm, nw, :load, i), "z_demand_on_start", 1.0) ) else - z_demand = _PMs.var(pm, nw)[:z_demand] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :load)], base_name="$(nw)_z_demand", + z_demand = var(pm, nw)[:z_demand] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :load)], base_name="$(nw)_z_demand", binary = true, - start = comp_start_value(_PMs.ref(pm, nw, :load, i), "z_demand_on_start", 1.0) + start = comp_start_value(ref(pm, nw, :load, i), "z_demand_on_start", 1.0) ) end # expressions for pd and qd - pd = _PMs.var(pm, nw)[:pd] = Dict(i => _PMs.var(pm, nw)[:z_demand][i].*_PMs.ref(pm, nw, :load, i)["pd"] - for i in _PMs.ids(pm, nw, :load)) - qd = _PMs.var(pm, nw)[:qd] = Dict(i => _PMs.var(pm, nw)[:z_demand][i].*_PMs.ref(pm, nw, :load, i)["qd"] - for i in _PMs.ids(pm, nw, :load)) + pd = var(pm, nw)[:pd] = Dict(i => var(pm, nw)[:z_demand][i].*ref(pm, nw, :load, i)["pd"] + for i in ids(pm, nw, :load)) + qd = var(pm, nw)[:qd] = Dict(i => var(pm, nw)[:z_demand][i].*ref(pm, nw, :load, i)["qd"] + for i in ids(pm, nw, :load)) - report && _PMs.sol_component_value(pm, nw, :load, :status, _PMs.ids(pm, nw, :load), z_demand) - report && _PMs.sol_component_value(pm, nw, :load, :pd, _PMs.ids(pm, nw, :load), pd) - report && _PMs.sol_component_value(pm, nw, :load, :qd, _PMs.ids(pm, nw, :load), qd) + report && _IM.sol_component_value(pm, nw, :load, :status, ids(pm, nw, :load), z_demand) + report && _IM.sol_component_value(pm, nw, :load, :pd, ids(pm, nw, :load), pd) + report && _IM.sol_component_value(pm, nw, :load, :qd, ids(pm, nw, :load), qd) end "Create variables for shunt status" -function variable_mc_indicator_shunt(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, relax=false, report::Bool=true) +function variable_mc_shunt_indicator(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, relax=false, report::Bool=true) # this is not indexedon cnd; why used in start value? cnd = 1 if relax - z_shunt = _PMs.var(pm, nw)[:z_shunt] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :shunt)], base_name="$(nw)_z_shunt", + z_shunt = var(pm, nw)[:z_shunt] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :shunt)], base_name="$(nw)_z_shunt", lower_bound = 0, upper_bound = 1, - start = comp_start_value(_PMs.ref(pm, nw, :shunt, i), "z_shunt_on_start", 1.0) + start = comp_start_value(ref(pm, nw, :shunt, i), "z_shunt_on_start", 1.0) ) else - z_shunt = _PMs.var(pm, nw)[:z_shunt] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :shunt)], base_name="$(nw)_z_shunt", + z_shunt = var(pm, nw)[:z_shunt] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :shunt)], base_name="$(nw)_z_shunt", binary=true, - start = comp_start_value(_PMs.ref(pm, nw, :shunt, i), "z_shunt_on_start", 1.0) + start = comp_start_value(ref(pm, nw, :shunt, i), "z_shunt_on_start", 1.0) ) end - report && _PMs.sol_component_value(pm, nw, :shunt, :status, _PMs.ids(pm, nw, :shunt), z_shunt) + report && _IM.sol_component_value(pm, nw, :shunt, :status, ids(pm, nw, :shunt), z_shunt) end "Create variables for bus status" -function variable_mc_indicator_bus_voltage(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) +function variable_mc_bus_voltage_indicator(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) if !relax - z_voltage = _PMs.var(pm, nw)[:z_voltage] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :bus)], base_name="$(nw)_z_voltage", + z_voltage = var(pm, nw)[:z_voltage] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :bus)], base_name="$(nw)_z_voltage", binary = true, - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "z_voltage_start", 1.0) + start = comp_start_value(ref(pm, nw, :bus, i), "z_voltage_start", 1.0) ) else - z_voltage =_PMs.var(pm, nw)[:z_voltage] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :bus)], base_name="$(nw)_z_voltage", + z_voltage =var(pm, nw)[:z_voltage] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :bus)], base_name="$(nw)_z_voltage", lower_bound = 0, upper_bound = 1, - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "z_voltage_start", 1.0) + start = comp_start_value(ref(pm, nw, :bus, i), "z_voltage_start", 1.0) ) end - report && _PMs.sol_component_value(pm, nw, :bus, :status, _PMs.ids(pm, nw, :bus), z_voltage) + report && _IM.sol_component_value(pm, nw, :bus, :status, ids(pm, nw, :bus), z_voltage) end "Create variables for generator status" -function variable_mc_indicator_generation(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) +function variable_mc_gen_indicator(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) if !relax - z_gen = _PMs.var(pm, nw)[:z_gen] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :gen)], base_name="$(nw)_z_gen", + z_gen = var(pm, nw)[:z_gen] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :gen)], base_name="$(nw)_z_gen", binary = true, - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "z_gen_start", 1.0) + start = comp_start_value(ref(pm, nw, :gen, i), "z_gen_start", 1.0) ) else - z_gen = _PMs.var(pm, nw)[:z_gen] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :gen)], base_name="$(nw)_z_gen", + z_gen = var(pm, nw)[:z_gen] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :gen)], base_name="$(nw)_z_gen", lower_bound = 0, upper_bound = 1, - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "z_gen_start", 1.0) + start = comp_start_value(ref(pm, nw, :gen, i), "z_gen_start", 1.0) ) end - report && _PMs.sol_component_value(pm, nw, :gen, :gen_status, _PMs.ids(pm, nw, :gen), z_gen) + report && _IM.sol_component_value(pm, nw, :gen, :gen_status, ids(pm, nw, :gen), z_gen) end "Create variables for storage status" -function variable_mc_indicator_storage(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) +function variable_mc_storage_indicator(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, relax::Bool=false, report::Bool=true) if !relax - z_storage = _PMs.var(pm, nw)[:z_storage] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :storage)], base_name="$(nw)-z_storage", + z_storage = var(pm, nw)[:z_storage] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :storage)], base_name="$(nw)-z_storage", binary = true, - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "z_storage_start", 1.0) + start = comp_start_value(ref(pm, nw, :storage, i), "z_storage_start", 1.0) ) else - z_storage = _PMs.var(pm, nw)[:z_storage] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, nw, :storage)], base_name="$(nw)_z_storage", + z_storage = var(pm, nw)[:z_storage] = JuMP.@variable(pm.model, + [i in ids(pm, nw, :storage)], base_name="$(nw)_z_storage", lower_bound = 0, upper_bound = 1, - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "z_storage_start", 1.0) + start = comp_start_value(ref(pm, nw, :storage, i), "z_storage_start", 1.0) ) end - report && _PMs.sol_component_value(pm, nw, :storage, :status, _PMs.ids(pm, nw, :storage), z_storage) + report && _IM.sol_component_value(pm, nw, :storage, :status, ids(pm, nw, :storage), z_storage) end "Create variables for `active` and `reactive` storage injection" -function variable_mc_on_off_storage(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) - variable_mc_on_off_storage_active(pm; nw=nw, kwargs...) - variable_mc_on_off_storage_reactive(pm; nw=nw, kwargs...) +function variable_mc_storage_power_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, kwargs...) + variable_mc_storage_power_real_on_off(pm; nw=nw, kwargs...) + variable_mc_storage_power_imaginary_on_off(pm; nw=nw, kwargs...) end "Create variables for `active` storage injection" -function variable_mc_on_off_storage_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_storage_power_real_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - inj_lb = Dict() - inj_ub = Dict() - for cnd in 1:ncnds - inj_lb[cnd], inj_ub[cnd] = _PMs.ref_calc_storage_injection_bounds(_PMs.ref(pm, nw, :storage), _PMs.ref(pm, nw, :bus), cnd) - end - - ps = _PMs.var(pm, nw)[:ps] = Dict(i => JuMP.@variable(pm.model, + ps = var(pm, nw)[:ps] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_ps_$(i)", - lower_bound = min(0, inj_lb[cnd][i]), - upper_bound = max(0, inj_ub[cnd][i]), - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "ps_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :storage)) + start = comp_start_value(ref(pm, nw, :storage, i), "ps_start", cnd, 0.0) + ) for i in ids(pm, nw, :storage)) + + if bounded + for cnd in 1:ncnds + inj_lb, inj_ub = _PM.ref_calc_storage_injection_bounds(ref(pm, nw, :storage), ref(pm, nw, :bus), cnd) - report && _PMs.sol_component_value(pm, nw, :storage, :ps, _PMs.ids(pm, nw, :storage), ps) + for (i, strg) in ref(pm, nw, :storage) + set_lower_bound.(ps[i], inj_lb[i]) + set_upper_bound.(ps[i], inj_ub[i]) + end + end + end + + report && _IM.sol_component_value(pm, nw, :storage, :ps, ids(pm, nw, :storage), ps) end "Create variables for `reactive` storage injection" -function variable_mc_on_off_storage_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_storage_power_imaginary_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - qs = _PMs.var(pm, nw)[:qs] = Dict(i => JuMP.@variable(pm.model, + qs = var(pm, nw)[:qs] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_qs_$(i)", - lower_bound = min(0, _PMs.ref(pm, nw, :storage, i, "qmin")[cnd]), - upper_bound = max(0, _PMs.ref(pm, nw, :storage, i, "qmax")[cnd]), - start = comp_start_value(_PMs.ref(pm, nw, :storage, i), "qs_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :storage)) + start = comp_start_value(ref(pm, nw, :storage, i), "qs_start", cnd, 0.0) + ) for i in ids(pm, nw, :storage)) + + if bounded + for (i, strg) in ref(pm, nw, :storage) + if haskey(strg, "qmin") + set_lower_bound.(qs[i], strg["qmin"]) + end - report && _PMs.sol_component_value(pm, nw, :storage, :qs, _PMs.ids(pm, nw, :storage), qs) + if haskey(strg, "qmax") + set_upper_bound.(qs[i], strg["qmax"]) + end + end + end + + report && _IM.sol_component_value(pm, nw, :storage, :qs, ids(pm, nw, :storage), qs) end "voltage variable magnitude squared (relaxed form)" -function variable_mc_voltage_magnitude_sqr_on_off(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_magnitude_sqr_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - w = _PMs.var(pm, nw)[:w] = Dict(i => JuMP.@variable(pm.model, + w = var(pm, nw)[:w] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_w_$(i)", - lower_bound = 0, - upper_bound = _PMs.ref(pm, nw, :bus, i, "vmax")[c]^2, - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "w_start", c, 1.001) - ) for i in _PMs.ids(pm, nw, :bus)) + start = comp_start_value(ref(pm, nw, :bus, i), "w_start", c, 1.001) + ) for i in ids(pm, nw, :bus)) + + if bounded + for (i, bus) in ref(pm, nw, :bus) + set_lower_bound.(w[i], 0.0) + + if haskey(bus, "vmax") + set_upper_bound.(w[i], bus["vmax"].^2) + end + end + end - report && _PMs.sol_component_value(pm, nw, :bus, :w, _PMs.ids(pm, nw, :bus), w) + report && _IM.sol_component_value(pm, nw, :bus, :w, ids(pm, nw, :bus), w) end "on/off voltage magnitude variable" -function variable_mc_voltage_magnitude_on_off(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_bus_voltage_magnitude_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - vm = _PMs.var(pm, nw)[:vm] = Dict(i => JuMP.@variable(pm.model, + vm = var(pm, nw)[:vm] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_vm_$(i)", - lower_bound = 0, - upper_bound = _PMs.ref(pm, nw, :bus, i, "vmax")[c], - start = comp_start_value(_PMs.ref(pm, nw, :bus, i), "vm_start", c, 1.0) - ) for i in _PMs.ids(pm, nw, :bus)) + start = comp_start_value(ref(pm, nw, :bus, i), "vm_start", c, 1.0) + ) for i in ids(pm, nw, :bus)) + + if bounded + for (i, bus) in ref(pm, nw, :bus) + set_lower_bound.(vm[i], 0.0) + + if haskey(bus, "vmax") + set_upper_bound.(vm[i], bus["vmax"]) + end + end + end - report && _PMs.sol_component_value(pm, nw, :bus, :vm, _PMs.ids(pm, nw, :bus), vm) + report && _IM.sol_component_value(pm, nw, :bus, :vm, ids(pm, nw, :bus), vm) end "create variables for generators, delegate to PowerModels" -function variable_mc_generation(pm::_PMs.AbstractPowerModel; kwargs...) - variable_mc_generation_active(pm; kwargs...) - variable_mc_generation_reactive(pm; kwargs...) +function variable_mc_gen_power_setpoint(pm::_PM.AbstractPowerModel; kwargs...) + variable_mc_gen_power_setpoint_real(pm; kwargs...) + variable_mc_gen_power_setpoint_imaginary(pm; kwargs...) end -function variable_mc_generation_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_gen_power_setpoint_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - pg = _PMs.var(pm, nw)[:pg] = Dict(i => JuMP.@variable(pm.model, + pg = var(pm, nw)[:pg] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_pg_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "pg_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :gen) + start = comp_start_value(ref(pm, nw, :gen, i), "pg_start", c, 0.0) + ) for i in ids(pm, nw, :gen) ) if bounded - for (i,gen) in _PMs.ref(pm, nw, :gen) + for (i,gen) in ref(pm, nw, :gen) if haskey(gen, "pmin") set_lower_bound.(pg[i], gen["pmin"]) end @@ -813,24 +881,24 @@ function variable_mc_generation_active(pm::_PMs.AbstractPowerModel; nw::Int=pm.c end end - _PMs.var(pm, nw)[:pg_bus] = Dict{Int, Any}() + var(pm, nw)[:pg_bus] = Dict{Int, Any}() - report && _PMs.sol_component_value(pm, nw, :gen, :pg, _PMs.ids(pm, nw, :gen), pg) + report && _IM.sol_component_value(pm, nw, :gen, :pg, ids(pm, nw, :gen), pg) end -function variable_mc_generation_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_gen_power_setpoint_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - qg = _PMs.var(pm, nw)[:qg] = Dict(i => JuMP.@variable(pm.model, + qg = var(pm, nw)[:qg] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_qg_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "qg_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :gen) + start = comp_start_value(ref(pm, nw, :gen, i), "qg_start", c, 0.0) + ) for i in ids(pm, nw, :gen) ) if bounded - for (i,gen) in _PMs.ref(pm, nw, :gen) + for (i,gen) in ref(pm, nw, :gen) if haskey(gen, "qmin") set_lower_bound.(qg[i], gen["qmin"]) end @@ -840,90 +908,110 @@ function variable_mc_generation_reactive(pm::_PMs.AbstractPowerModel; nw::Int=pm end end - _PMs.var(pm, nw)[:qg_bus] = Dict{Int, Any}() + var(pm, nw)[:qg_bus] = Dict{Int, Any}() - report && _PMs.sol_component_value(pm, nw, :gen, :qg, _PMs.ids(pm, nw, :gen), qg) + report && _IM.sol_component_value(pm, nw, :gen, :qg, ids(pm, nw, :gen), qg) end "variable: `crg[j]` for `j` in `gen`" -function variable_mc_generation_current_real(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - gen = _PMs.ref(pm, nw, :gen) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_gen_current_setpoint_real(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + gen = ref(pm, nw, :gen) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - crg = _PMs.var(pm, nw)[:crg] = Dict(i => JuMP.@variable(pm.model, + crg = var(pm, nw)[:crg] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_crg_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "crg_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :gen) + start = comp_start_value(ref(pm, nw, :gen, i), "crg_start", c, 0.0) + ) for i in ids(pm, nw, :gen) ) if bounded for (i, g) in gen - cmax = _calc_gen_current_max(g, _PMs.ref(pm, nw, :bus, g["gen_bus"])) + cmax = _calc_gen_current_max(g, ref(pm, nw, :bus, g["gen_bus"])) set_lower_bound.(crg[i], -cmax) set_upper_bound.(crg[i], cmax) end end - report && _PMs.sol_component_value(pm, nw, :gen, :crg, _PMs.ids(pm, nw, :gen), crg) + report && _IM.sol_component_value(pm, nw, :gen, :crg, ids(pm, nw, :gen), crg) end "variable: `cig[j]` for `j` in `gen`" -function variable_mc_generation_current_imaginary(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - gen = _PMs.ref(pm, nw, :gen) - bus = _PMs.ref(pm, nw, :bus) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_gen_current_setpoint_imaginary(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + gen = ref(pm, nw, :gen) + bus = ref(pm, nw, :bus) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - cig = _PMs.var(pm, nw)[:cig] = Dict(i => JuMP.@variable(pm.model, + cig = var(pm, nw)[:cig] = Dict(i => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_cig_$(i)", - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "cig_start", c, 0.0) - ) for i in _PMs.ids(pm, nw, :gen) + start = comp_start_value(ref(pm, nw, :gen, i), "cig_start", c, 0.0) + ) for i in ids(pm, nw, :gen) ) if bounded for (i, g) in gen - cmax = _calc_gen_current_max(g, _PMs.ref(pm, nw, :bus, g["gen_bus"])) + cmax = _calc_gen_current_max(g, ref(pm, nw, :bus, g["gen_bus"])) set_lower_bound.(cig[i], -cmax) set_upper_bound.(cig[i], cmax) end end - report && _PMs.sol_component_value(pm, nw, :gen, :cig, _PMs.ids(pm, nw, :gen), cig) + report && _IM.sol_component_value(pm, nw, :gen, :cig, ids(pm, nw, :gen), cig) end -function variable_mc_generation_on_off(pm::_PMs.AbstractPowerModel; kwargs...) - variable_mc_active_generation_on_off(pm; kwargs...) - variable_mc_reactive_generation_on_off(pm; kwargs...) +function variable_mc_gen_power_setpoint_on_off(pm::_PM.AbstractPowerModel; kwargs...) + variable_mc_gen_power_setpoint_real_on_off(pm; kwargs...) + variable_mc_gen_power_setpoint_imaginary_on_off(pm; kwargs...) end -function variable_mc_active_generation_on_off(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) - ncnds = length(_PMs.conductor_ids(pm, nw)) +function variable_mc_gen_power_setpoint_real_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) + ncnds = length(conductor_ids(pm, nw)) - pg = _PMs.var(pm, nw)[:pg] = Dict(i => JuMP.@variable(pm.model, + pg = var(pm, nw)[:pg] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_pg_$(i)", - lower_bound = min(0, _PMs.ref(pm, nw, :gen, i, "pmin")[cnd]), - upper_bound = max(0, _PMs.ref(pm, nw, :gen, i, "pmax")[cnd]), - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "pg_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :gen)) + start = comp_start_value(ref(pm, nw, :gen, i), "pg_start", cnd, 0.0) + ) for i in ids(pm, nw, :gen)) - report && _PMs.sol_component_value(pm, nw, :gen, :pg, _PMs.ids(pm, nw, :gen), pg) + if bounded + for (i, gen) in ref(pm, nw, :gen) + if haskey(gen, "pmin") + set_lower_bound.(pg[i], gen["pmin"]) + end + + if haskey(gen, "pmax") + set_upper_bound.(pg[i], gen["pmax"]) + end + end + end + + report && _IM.sol_component_value(pm, nw, :gen, :pg, ids(pm, nw, :gen), pg) end -function variable_mc_reactive_generation_on_off(pm::_PMs.AbstractPowerModel; nw::Int=pm.cnw, report::Bool=true) - cnds = _PMs.conductor_ids(pm; nw=nw) +function variable_mc_gen_power_setpoint_imaginary_on_off(pm::_PM.AbstractPowerModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - qg = _PMs.var(pm, nw)[:qg] = Dict(i => JuMP.@variable(pm.model, + qg = var(pm, nw)[:qg] = Dict(i => JuMP.@variable(pm.model, [cnd in 1:ncnds], base_name="$(nw)_qg_$(i)", - lower_bound = min(0, _PMs.ref(pm, nw, :gen, i, "qmin")[cnd]), - upper_bound = max(0, _PMs.ref(pm, nw, :gen, i, "qmax")[cnd]), - start = comp_start_value(_PMs.ref(pm, nw, :gen, i), "qg_start", cnd, 0.0) - ) for i in _PMs.ids(pm, nw, :gen)) + start = comp_start_value(ref(pm, nw, :gen, i), "qg_start", cnd, 0.0) + ) for i in ids(pm, nw, :gen)) + + if bounded + for (i, gen) in ref(pm, nw, :gen) + if haskey(gen, "qmin") + set_lower_bound.(qg[i], gen["qmin"]) + end + + if haskey(gen, "qmax") + set_upper_bound.(qg[i], gen["qmax"]) + end + end + end - report && _PMs.sol_component_value(pm, nw, :gen, :qg, _PMs.ids(pm, nw, :gen), qg) + report && _IM.sol_component_value(pm, nw, :gen, :qg, ids(pm, nw, :gen), qg) end diff --git a/src/data_model/checks.jl b/src/data_model/checks.jl new file mode 100644 index 000000000..77ec3d711 --- /dev/null +++ b/src/data_model/checks.jl @@ -0,0 +1,572 @@ +"data check functions for the engineering data model" +const _eng_model_checks = Dict{Symbol,Symbol}( + :bus => :_check_bus, + :linecode => :_check_linecode, + # :xfmrcode => :_check_xfmrcode, + :line => :_check_line, + :transformer => :_check_transformer, + # :switch => :_check_switch, + :load => :_check_load, + :shunt => :_check_shunt, + :generator => :_check_generator, + :voltage_source => :_check_voltage_source, + # :solar => :_check_solar, + # :storage => :_check_storage, +) + +"Data types of accepted fields in the engineering data model" +const _eng_model_dtypes = Dict{Symbol,Dict{Symbol,Type}}( + :bus => Dict{Symbol,Type}( + :status => Int, + :terminals => Vector{Any}, + :phases => Vector{Any}, + :neutral => Any, + :grounded => Vector{Any}, + :rg => Vector{<:Real}, + :xg => Vector{<:Real}, + :vm_pn_lb => Real, + :vm_pn_ub => Real, + :vm_pp_lb => Real, + :vm_pp_ub => Real, + :vm_lb => Vector{<:Real}, + :vm_ub => Vector{<:Real}, + :vm => Vector{<:Real}, + :va => Vector{<:Real}, + ), + :line => Dict{Symbol,Type}( + :status => Int, + :f_bus => Any, + :t_bus => Any, + :f_connections => Vector{Any}, + :t_connections => Vector{Any}, + :linecode => String, + :length => Real, + :cm_ub =>Vector{<:Real}, + :sm_ub =>Vector{<:Real}, + :vad_lb=>Vector{<:Real}, + :vad_ub=>Vector{<:Real}, + :rs => Matrix{<:Real}, + :xs => Matrix{<:Real}, + :g_fr => Matrix{<:Real}, + :g_to => Matrix{<:Real}, + :b_fr => Matrix{<:Real}, + :b_to => Matrix{<:Real}, + ), + :transformer => Dict{Symbol,Type}( + :status => Int, + :bus => Vector{Any}, + :connections => Vector{Any}, + :vnom => Vector{<:Real}, + :snom => Vector{<:Real}, + :configuration => Vector{String}, + :polarity => Vector{Int}, + :xsc => Vector{<:Real}, + :rs => Vector{<:Real}, + :noloadloss => Real, + :imag => Real, + :tm_fix => Vector{Union{Vector{Int},Int}}, + :tm => Vector{Union{Vector{<:Real},<:Real}}, + :tm_min => Vector{Union{Vector{<:Real},<:Real}}, + :tm_max => Vector{Union{Vector{<:Real},<:Real}}, + :tm_step => Vector{Union{Vector{<:Real},<:Real}}, + :tm_nom => Union{Vector{<:Real}, Real}, + :f_bus => Any, + :t_bus => Any, + :f_connections => Vector{Any}, + :t_connections => Vector{Any}, + :configuration => String, + :xfmrcode => String, + ), + :switch => Dict{Symbol,Type}( + :status => Int, + :f_bus => Any, + :t_bus => Any, + :f_connections => Vector{Any}, + :t_connections => Vector{Any}, + :cm_ub => Vector{<:Real}, + :sm_ub => Vector{<:Real}, + :rs => Matrix{<:Real}, + :xs => Matrix{<:Real}, + :g_fr => Matrix{<:Real}, + :b_fr => Matrix{<:Real}, + :g_to => Matrix{<:Real}, + :b_to => Matrix{<:Real}, + :state => Int, + ), + :fuse => Dict{Symbol,Type}( + :status => Int, + :f_bus => Any, + :t_bus => Any, + :f_connections => Vector{Any}, + :t_connections => Vector{Any}, + :cm_ub => Vector{<:Real}, + :sm_ub => Vector{<:Real}, + :rs => Matrix{<:Real}, + :xs => Matrix{<:Real}, + :g_fr => Matrix{<:Real}, + :b_fr => Matrix{<:Real}, + :g_to => Matrix{<:Real}, + :b_to => Matrix{<:Real}, + :state => Int, + :fuse_curve => String, + :minimum_melting_curve => String, + ), + :line_reactor => Dict{Symbol,Type}(), + :series_capacitor => Dict{Symbol,Type}(), + :shunt => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :gs => Matrix{<:Real}, + :bs => Matrix{<:Real}, + :vnom => Real, + ), + :shunt_capacitor => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :bs => Matrix{<:Real}, + :vnom => Real, + ), + :shunt_reactor => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :bs => Matrix{<:Real}, + :vnom => Real, + ), + :load => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :model => String, + :pd_nom => Vector{<:Real}, + :qd_nom => Vector{<:Real}, + :vnom => Real, + :pd_exp => Real, + :qd_exp => Real, + :pd_nom_z => Real, + :pd_nom_i => Real, + :pd_nom_p => Real, + :qd_nom_z => Real, + :qd_nom_i => Real, + :qd_nom_p => Real, + ), + :generator => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :model => Int, + :pg => Vector{<:Real}, + :qg => Vector{<:Real}, + :pg_lb => Vector{<:Real}, + :pg_ub => Vector{<:Real}, + :qg_lb => Vector{<:Real}, + :qg_ub => Vector{<:Real}, + :cost_pg_parameters => Vector{<:Real}, + :cost_pg_model => Int, + ), + :solar => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :pg => Vector{<:Real}, + :qg => Vector{<:Real}, + :pg_lb => Vector{<:Real}, + :pg_ub => Vector{<:Real}, + :qg_lb => Vector{<:Real}, + :qg_ub => Vector{<:Real}, + :cost_pg_parameters => Vector{<:Real}, + :cost_pg_model => Int, + ), + :storage => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :energy => Real, + :energy_ub => Real, + :charge_ub => Real, + :sm_ub => Vector{<:Real}, + :cm_ub => Vector{<:Real}, + :charge_efficiency => Real, + :discharge_efficiency => Real, + :qs_lb => Vector{<:Real}, + :qs_ub => Vector{<:Real}, + :rs => Vector{<:Real}, + :xs => Vector{<:Real}, + :pex => Real, + :qex => Real, + ), + :voltage_source => Dict{Symbol,Type}( + :status => Int, + :bus => Any, + :connections => Vector{Any}, + :configuration => String, + :vm => Vector{<:Real}, + :va => Real, + :rs => Matrix{<:Real}, + :xs => Matrix{<:Real}, + ), + :linecode => Dict{Symbol,Type}( + :rs => Matrix{<:Real}, + :xs => Matrix{<:Real}, + :g_fr => Matrix{<:Real}, + :g_to => Matrix{<:Real}, + :b_fr => Matrix{<:Real}, + :b_to => Matrix{<:Real} + ), + :xfmrcode => Dict{Symbol,Type}( + :configurations => Vector{String}, + :xsc => Vector{<:Real}, + :rs => Vector{<:Real}, + :tm_nom => Vector{<:Real}, + :tm_ub => Vector{<:Real}, + :tm_lb => Vector{<:Real}, + :tm_step => Vector{<:Real}, + :tm_set => Vector{<:Real}, + :tm_fix => Vector{<:Real}, + ), + :curve => Dict{Symbol,Type}( + :curve => Function, + ), + :time_series => Dict{Symbol,Type}( + :time => Vector{<:Real}, + :values => Vector{<:Real}, + :replace => Bool, + ), + # Future Components + # :ev => Dict{Symbol,Type}(), + # :wind => Dict{Symbol,Type}(), + # :autotransformer => Dict{Symbol,Type}(), + # :meter => Dict{Symbol,Type}() +) + +"required fields in the engineering data model" +const _eng_model_req_fields= Dict{Symbol,Vector{Symbol}}( + :bus => Vector{Symbol}([ + :status, :terminals, :grounded, :rg, :xg, + ]), + :line => Vector{Symbol}([ + :status, :f_bus, :t_bus, :f_connections, :t_connections, :length, + ]), + :transformer => Vector{Symbol}([ + :status, :configurations, :vnom, :snom, :polarity, :xsc, :rs, + :noloadloss, :imag, :tm_fix, :tm_set, :tm_step, + ]), + :switch => Vector{Symbol}([ + :status, :f_bus, :t_bus, :f_connections, :t_connections, + ]), + :fuse => Vector{Symbol}([ + :status, :f_bus, :t_bus, :f_connections, :t_connections, + ]), + :line_reactor => Vector{Symbol}([ + :status, :f_bus, :t_bus, :f_connections, :t_connections, + ]), + :series_capacitor => Vector{Symbol}([ + :status, :f_bus, :t_bus, :f_connections, :t_connections, + ]), + :shunt => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :gs, :gs, :vnom, + ]), + :shunt_capacitor => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :bs, :vnom, + ]), + :shunt_reactor => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :bs, :vnom, + ]), + :load => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :model, :pd_nom, :qd_nom, + :vnom, + ]), + :generator => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :model, + ]), + :solar => Vector{Symbol}([ + :status, :bus, :connections, :configuration, + ]), + :storage => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :energy, + :charge_efficiency, :discharge_efficiency, :rs, :xs, :pex, :qex, + ]), + :voltage_source => Vector{Symbol}([ + :status, :bus, :connections, :configuration, :vm, :va, + ]), + :linecode => Vector{Symbol}([ + :rs, :xs, :g_fr, :g_to, :b_fr, :b_to, :cm_ub, + ]), + :xfmrcode => Vector{Symbol}([ + :status, :vnom, :snom, :xsc, :rs, :noloadloss, :imag, :tm_fix, :tm, + :tm_min, :tm_max, :tm_step, + ]), + :grounding => Vector{Symbol}([]), + + # Future Components + # :ev => Vector{Symbol}([]), + # :wind => Vector{Symbol}([]), + # :autotransformer => Vector{Symbol}([]), + # :meter => Vector{Symbol}([]) +) + + +"checks the engineering data model for correct data types, required fields and applies default checks" +function check_eng_data_model(data_eng::Dict{String,<:Any}) + for (component_type, components) in data_eng + if isa(components, Dict) + for (name, component) in components + _check_eng_component_dtypes(data_eng, component_type, name) + + for field in get(_eng_model_req_fields, Symbol(component_type), Vector{Symbol}([])) + @assert haskey(component, string(field)) "The property \'$field\' is missing on $component_type $name" + end + + check = get(_eng_model_checks, Symbol(component_type), missing) + if !ismissing(check) + getfield(PowerModelsDistribution, check)(data_eng, name) + end + end + end + end +end + + +"checks that an engineering model component has the correct data types" +function _check_eng_component_dtypes(data_eng::Dict{String,<:Any}, component_type::String, component_name::Any; additional_dtypes=Dict{Symbol,Type}()) + if haskey(_eng_model_dtypes, Symbol(component_type)) + dtypes = merge(_eng_model_dtypes[Symbol(component_type)], additional_dtypes) + else + dtypes = additional_dtypes + end + + if haskey(data_eng, component_type) && haskey(data_eng[component_type], component_name) + component = data_eng[component_type][component_name] + + for (field, dtype) in dtypes + if haskey(component, string(field)) + @assert isa(component[string(field)], dtype) "$component_type $component_name: the property $field should be a $dtype, not a $(typeof(component[string(field)]))" + end + end + else + Memento.warn(_LOGGER, "$component_type $component_name does not exist") + end +end + + + +"check that all data in `fields` have the same size" +function _check_same_size(component::Dict{String,<:Any}, fields::Vector{String}; context::Union{String,Missing}=missing) + @assert length(unique([size(component[string(field)]) for field in fields])) == 1 "$context: not all properties are the same size" +end + + +"check that `fields` has size `data_size`" +function _check_has_size(component::Dict{String,<:Any}, fields::Vector{String}, data_size::Union{Int, Tuple}; context::Union{String,Missing}=missing, allow_missing::Bool=true) + for key in fields + if haskey(component, key) || !allow_missing + @assert all(size(component[string(key)]).==data_size) "$context: the property $key should have as size $data_size" + end + end +end + + +"checks connectivity of object" +function _check_connectivity(data_eng::Dict{String,<:Any}, object::Dict{String,<:Any}; context::Union{String,Missing}=missing) + if haskey(object, "f_bus") + # two-port element + _check_bus_and_terminals(data_eng, object["f_bus"], object["f_connections"], context) + _check_bus_and_terminals(data_eng, object["t_bus"], object["t_connections"], context) + elseif haskey(object, "bus") + if isa(object["bus"], Vector) + for i in 1:length(object["bus"]) + _check_bus_and_terminals(data_eng, object["bus"][i], object["connections"][i], context) + end + else + _check_bus_and_terminals(data_eng, object["bus"], object["connections"], context) + end + end +end + + +"checks `bus_name` exists and has `terminals`" +function _check_bus_and_terminals(data_eng::Dict{String,<:Any}, bus_name::Any, terminals::Vector{Int}, context::Union{String,Missing}=missing) + @assert haskey(data_eng, "bus") && haskey(data_eng["bus"], bus_name) "$context: the bus $bus_name is not defined." + + bus = data_eng["bus"][bus_name] + for t in terminals + @assert t in bus["terminals"] "$context: bus $(bus["obj_name"]) does not have terminal \'$t\'." + end +end + + +"checks that a component has `fields`" +function _check_has_keys(object::Dict{String,<:Any}, fields::Vector{String}; context::Union{String,Missing}=missing) + for key in fields + @assert haskey(object, key) "$context: the property $key is missing." + end +end + + +"checks the connection configuration and infers the dimensions of the connection (number of connected terminals)" +function _check_configuration_infer_dim(object::Dict{String,<:Any}; context::Union{String,Missing}=missing)::Int + conf = object["configuration"] + @assert conf in [DELTA, WYE] "$context: the configuration should be \'delta\' or \'wye\', not \'$conf\'." + + return conf==WYE ? length(object["connections"])-1 : length(object["connections"]) +end + + +"bus data checks" +function _check_bus(data_eng::Dict{String,<:Any}, name::Any) + bus = data_eng["bus"][name] + + _check_same_size(bus, ["grounded", "rg", "xg"], context="bus $name") + + N = length(bus["terminals"]) + _check_has_size(bus, ["vm_max", "vm_min", "vm", "va"], N, context="bus $name") + + if haskey(bus, "neutral") + @assert haskey(bus, "phases") "bus $name: has a neutral, but no phases." + end +end + + +"load data checks" +function _check_load(data_eng::Dict{String,<:Any}, name::Any) + load = data_eng["load"][name] + + N = _check_configuration_infer_dim(load; context="load $name") + + model = load["model"] + @assert model in [POWER, IMPEDANCE, CURRENT, EXPONENTIAL] + + if model==POWER + _check_has_keys(load, ["pd", "qd"], context="load $name, $model:") + _check_has_size(load, ["pd", "qd"], N, context="load $name, $model:") + elseif model==EXPONENTIAL + _check_has_keys(load, ["pd_ref", "qd_ref", "vm_nom", "alpha", "beta"], context="load $name, $model") + _check_has_size(load, ["pd_ref", "qd_ref", "vm_nom", "alpha", "beta"], N, context="load $name, $model:") + else + _check_has_keys(load, ["pd_ref", "qd_ref", "vm_nom"], context="load $name, $model") + _check_has_size(load, ["pd_ref", "qd_ref", "vm_nom"], N, context="load $name, $model:") + end + + _check_connectivity(data_eng, load; context="load $name") +end + + +"linecode data checks" +function _check_linecode(data_eng::Dict{String,<:Any}, name::Any) + _check_same_size(data_eng["linecode"][name], string.([:rs, :xs, :g_fr, :g_to, :b_fr, :b_to])) +end + + +"line data checks" +function _check_line(data_eng::Dict{String,<:Any}, name::Any) + line = data_eng["line"][name] + + # for now, always require a line code + if haskey(line, "linecode") + # line is defined with a linecode + @assert haskey(line, "length") "line $name: a line defined through a linecode, should have a length property." + + linecode_obj_name = line["linecode"] + @assert haskey(data_eng, "linecode") && haskey(data_eng["linecode"], "$linecode_obj_name") "line $name: the linecode $linecode_obj_name is not defined." + linecode = data_eng["linecode"]["$linecode_obj_name"] + + N = size(linecode["rs"])[1] + @assert length(line["f_connections"])==N "line $name: the number of terminals should match the number of conductors in the linecode." + @assert length(line["t_connections"])==N "line $name: the number of terminals should match the number of conductors in the linecode." + else + # normal line + @assert !haskey(line, "length") "line $name: length only makes sense for linees defined through linecodes." + for key in ["n_conductors", "rs", "xs", "g_fr", "g_to", "b_fr", "b_to"] + @assert haskey(line, key) "line $name: a line without linecode, should specify $key." + end + end + + _check_connectivity(data_eng, line, context="line $(name)") +end + + +"generator data checks" +function _check_generator(data_eng::Dict{String,<:Any}, name::Any) + generator = data_eng["generator"][name] + + N = _check_configuration_infer_dim(generator; context="generator $name") + _check_has_size(generator, ["pd", "qd", "pd_min", "pd_max", "qd_min", "qd_max"], N, context="generator $name") + + _check_connectivity(data_eng, generator; context="generator $name") +end + + +"Transformer, n-windings three-phase lossy data checks" +function _check_transformer(data_eng::Dict{String,<:Any}, name::Any) + transformer = data_eng["transformer"][name] + + nrw = length(transformer["bus"]) + _check_has_size(transformer, ["bus", "connections", "vm_nom", "sm_nom", "configuration", "polarity", "rw", "tm_fix", "tm_set", "tm_lb", "tm_ub", "tm_step"], nrw, context="trans $name") + + @assert length(transformer["xsc"])==(nrw^2-nrw)/2 + + nphs = [] + for w in 1:nrw + @assert transformer["configuration"][w] in [WYE, DELTA] + + conf = transformer["configuration"][w] + conns = transformer["connections"][w] + nph = conf==WYE ? length(conns)-1 : length(conns) + @assert all(nph.==nphs) "transformer $name: winding $w has a different number of phases than the previous ones." + + push!(nphs, nph) + #TODO check length other properties + end + + _check_connectivity(data_eng, transformer; context="transformer_nw $name") +end + + +"shunt capacitor data checks" +function _check_shunt_capacitor(data_eng::Dict{String,<:Any}, name::Any) + shunt_capacitor = data_eng["shunt_capacitor"][name] + + N = length(shunt_capacitor["connections"]) + config = shunt_capacitor["configuration"] + if config==WYE + @assert length(shunt_capacitor["qd_ref"])==N-1 "capacitor $name: qd_ref should have $(N-1) elements." + else + @assert length(shunt_capacitor["qd_ref"])==N "capacitor $name: qd_ref should have $N elements." + end + + @assert config in [DELTA, WYE, "wye-grounded", "wye-floating"] + + if config==DELTA + @assert N>=3 "Capacitor $name: delta-connected capacitors should have at least 3 elements." + end + + _check_connectivity(data_eng, shunt_capacitor; context="capacitor $name") +end + + +"shunt data checks" +function _check_shunt(data_eng::Dict{String,<:Any}, name::Any) + shunt = data_eng["shunt"][name] + + _check_connectivity(data_eng, shunt; context="shunt $name") +end + + +"voltage source data checks" +function _check_voltage_source(data_eng::Dict{String,<:Any}, name::Any) + voltage_source = data_eng["voltage_source"][name] + + _check_connectivity(data_eng, voltage_source; context="voltage source $name") + + N = length(voltage_source["connections"]) + _check_has_size(voltage_source, ["vm", "va", "pg_max", "pg_min", "qg_max", "qg_min"], N, context="voltage source $name") +end diff --git a/src/data_model/components.jl b/src/data_model/components.jl new file mode 100644 index 000000000..b73120016 --- /dev/null +++ b/src/data_model/components.jl @@ -0,0 +1,661 @@ +"adds kwargs that were specified but unused by the required defaults to the component" +function _add_unused_kwargs!(object::Dict{String,<:Any}, kwargs) + for (property, value) in kwargs + if !haskey(object, string(property)) + object[string(property)] = value + end + end +end + + +"Generic add function to add components to an engineering data model" +function add_object!(data_eng::Dict{String,<:Any}, obj_type::String, obj_id::Any, object::Dict{String,<:Any}) + if !haskey(data_eng, obj_type) + data_eng[obj_type] = Dict{Any,Any}() + end + + if !haskey(object, "source_id") + object["source_id"] = "$obj_type.$obj_id" + end + + if obj_type == "voltage_source" + if !haskey(data_eng["settings"], "base_bus") + data_eng["settings"]["base_bus"] = object["bus"] + end + end + + for bus_key in ["f_", "t_", ""] + if haskey(object, "$(bus_key)bus") + if !haskey(data_eng, "bus") + data_eng["bus"] = Dict{Any,Any}() + end + + if obj_type == "transformer" + if haskey(object, "f_bus") && haskey(object, "t_bus") + if !haskey(data_eng["bus"], object["f_bus"]) + data_eng["bus"][object["f_bus"]] = create_bus(; terminals=object["f_connections"]) + end + + if !haskey(data_eng["bus"], object["t_bus"]) + data_eng["bus"][object["t_bus"]] = create_bus(; terminals=object["t_connections"]) + end + else + for (wdg, bus_id) in enumerate(object["bus"]) + if !haskey(data_eng["bus"], bus_id) + data_eng["bus"][bus_id] = create_bus(; terminals=object["connections"][wdg]) + end + end + end + else + if !haskey(data_eng["bus"], object["$(bus_key)bus"]) + data_eng["bus"][object["$(bus_key)bus"]] = create_bus(; terminals=object["$(bus_key)connections"]) + end + end + end + end + + data_eng[obj_type][obj_id] = object +end + + +"Instantiates a PowerModelsDistribution data model" +function Model(model_type::DataModel=ENGINEERING; kwargs...)::Dict{String,Any} + if model_type == ENGINEERING + data_model = Dict{String,Any}( + "data_model" => model_type, + "per_unit" => false, + "settings" => Dict{String,Any}( + "voltage_scale_factor" => get(kwargs, :voltage_scale_factor, 1e3), + "power_scale_factor" => get(kwargs, :power_scale_factor, 1e3), + "vbases_default" => get(kwargs, :vbases_default, Dict{Any,Real}()), + "sbase_default" => get(kwargs, :sbase_default, 1.0), + "base_frequency" => get(kwargs, :basefreq, 60.0), + ) + ) + + _add_unused_kwargs!(data_model["settings"], kwargs) + elseif model_type == MATHEMATICAL + Memento.warn(_LOGGER, "There are not currently any helper functions to help build a mathematical model, this will only instantiate required fields.") + data_model = Dict{String,Any}( + "bus" => Dict{String,Any}(), + "load" => Dict{String,Any}(), + "shunt" => Dict{String,Any}(), + "gen" => Dict{String,Any}(), + "storage" => Dict{String,Any}(), + "branch" => Dict{String,Any}(), + "switch" => Dict{String,Any}(), + "dcline" => Dict{String,Any}(), + "per_unit" => false, + "baseMVA" => 100.0, + "basekv" => 1.0, + "data_model" => model_type + ) + + _add_unused_kwargs!(data_model, kwargs) + else + Memento.error(_LOGGER, "Model type '$model_type' not recognized") + end + + return data_model +end + + +"creates a linecode with some defaults" +function create_linecode(rs::Matrix{<:Real}, xs::Matrix{<:Real}; + g_fr::Union{Matrix{<:Real},Missing}=missing, + b_fr::Union{Matrix{<:Real},Missing}=missing, + g_to::Union{Matrix{<:Real},Missing}=missing, + b_to::Union{Matrix{<:Real},Missing}=missing, + cm_ub::Union{Vector{<:Real},Missing}=missing, + kwargs... + )::Dict{String,Any} + + shape = size(rs) + + for v in [rs, xs, g_fr, g_to, b_fr, b_to] + if !ismissing(v) + @assert size(v) == shape "not all of the properties are the same size, aborting linecode creation" + end + end + + linecode = Dict{String,Any}( + "rs" => rs, + "xs" => xs, + "g_fr" => !ismissing(g_fr) ? g_fr : fill(0.0, shape...), + "b_fr" => !ismissing(b_fr) ? b_fr : fill(0.0, shape...), + "g_to" => !ismissing(g_to) ? g_to : fill(0.0, shape...), + "b_to" => !ismissing(b_to) ? b_to : fill(0.0, shape...), + ) + + if !ismissing(cm_ub) + linecode["cm_ub"] = cm_ub + end + + _add_unused_kwargs!(linecode, kwargs) + + return linecode +end + + +"Create a line with some default values" +function create_line(f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; + linecode::Any=missing, + rs::Union{Matrix{<:Real},Missing}=missing, + xs::Union{Matrix{<:Real},Missing}=missing, + g_fr::Union{Matrix{<:Real},Missing}=missing, + b_fr::Union{Matrix{<:Real},Missing}=missing, + g_to::Union{Matrix{<:Real},Missing}=missing, + b_to::Union{Matrix{<:Real},Missing}=missing, + length::Real=1.0, + cm_ub::Union{Vector{<:Real},Missing}=missing, + sm_ub::Union{Vector{<:Real},Missing}=missing, + vad_lb::Union{Vector{<:Real},Missing}=missing, + vad_ub::Union{Vector{<:Real},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = size(f_connections)[1] + shape = (n_conductors, n_conductors) + + for v in [rs, xs, g_fr, b_fr, g_to, b_to, cm_ub, sm_ub, vad_lb, vad_ub] + if !ismissing(v) + if isa(v, Matrix) + @assert size(v) == shape + else + @assert size(v)[1] == n_conductors + end + end + end + + line = Dict{String,Any}( + "f_bus" => f_bus, + "t_bus" => t_bus, + "status" => status, + "f_connections" => f_connections, + "t_connections" => t_connections, + "vad_lb" => !ismissing(vad_lb) ? vad_lb : fill(-60.0, n_conductors), + "vad_ub" => !ismissing(vad_lb) ? vad_lb : fill( 60.0, n_conductors), + "length" => length, + ) + + if ismissing(linecode) + if !ismissing(rs) && !ismissing(xs) + line["rs"] = rs + line["xs"] = xs + + else + Memento.error(_LOGGER, "A linecode or rs & xs must be specified to create a valid line object") + end + + line["g_fr"] = !ismissing(g_fr) ? g_fr : fill(0.0, shape...) + line["b_fr"] = !ismissing(b_fr) ? b_fr : fill(0.0, shape...) + line["g_to"] = !ismissing(g_to) ? g_to : fill(0.0, shape...) + line["b_to"] = !ismissing(b_to) ? b_to : fill(0.0, shape...) + else + line["linecode"] = linecode + for (k,v) in [("rs", rs), ("xs", xs), ("g_fr", g_fr), ("b_fr", b_fr), ("g_to", g_to), ("b_to", b_to)] + if !ismissing(v) + line[k] = v + end + end + end + + for (k,v) in [("cm_ub", cm_ub), ("sm_ub", sm_ub)] + if !ismissing(v) + line[k] = v + end + end + + _add_unused_kwargs!(line, kwargs) + + return line +end + + +"creates a switch object with some defaults" +function create_switch(f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; + cm_ub::Union{Vector{<:Real},Missing}=missing, + sm_ub::Union{Vector{<:Real},Missing}=missing, + linecode::Any=missing, + rs::Union{Matrix{<:Real},Missing}=missing, + xs::Union{Matrix{<:Real},Missing}=missing, + dispatchable::Dispatchable=NO, + state::SwitchState=CLOSED, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + eng_obj = Dict{String,Any}( + "f_bus" => f_bus, + "t_bus" => t_bus, + "f_connections" => f_connections, + "t_connections" => t_connections, + "dispatchable" => dispatchable, + "state" => state, + "status" => status, + ) + + for (k,v) in [("cm_ub", cm_ub), ("sm_ub", sm_ub), ("linecode", linecode), ("rs", rs), ("xs", xs)] + if !ismissing(v) + eng_obj[k] = v + end + end + + _add_unused_kwargs!(eng_obj, kwargs) + + return eng_obj +end + + +"creates a bus object with some defaults" +function create_bus(; + status::Status=ENABLED, + terminals::Union{Vector{Int},Vector{String}}=Vector{Int}([]), + grounded::Union{Vector{Int},Vector{String}}=Vector{Int}([]), + rg::Vector{<:Real}=Vector{Float64}([]), + xg::Vector{<:Real}=Vector{Float64}([]), + kwargs... + )::Dict{String,Any} + + # grounded = Vector{Bool}([terminal in grounded for terminal in terminals]) + + bus = Dict{String,Any}( + "status" => status, + "terminals" => terminals, + "grounded" => grounded, + "rg" => isempty(rg) ? fill(0.0, length(grounded)) : rg, + "xg" => isempty(xg) ? fill(0.0, length(grounded)) : xg, + ) + + _add_unused_kwargs!(bus, kwargs) + + return bus +end + + +"creates a load object with some defaults" +function create_load(bus::Any, connections::Union{Vector{Int},Vector{String}}; + configuration::ConnConfig=WYE, + model::LoadModel=POWER, + pd_nom::Union{Vector{<:Real},Missing}=missing, + qd_nom::Union{Vector{<:Real},Missing}=missing, + vm_nom::Real=1.0, + dispatchable::Dispatchable=NO, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = configuration == WYE ? length(connections)-1 : length(connections) + + for v in [pd_nom, qd_nom] + if !ismissing(v) + @assert length(v) == n_conductors + end + end + + load = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "configuration" => configuration, + "model" => model, + "pd_nom" => !ismissing(pd_nom) ? pd_nom : fill(0.0, n_conductors), + "qd_nom" => !ismissing(qd_nom) ? qd_nom : fill(0.0, n_conductors), + "vm_nom" => vm_nom, + "dispatchable" => dispatchable, + "status" => status, + ) + + _add_unused_kwargs!(load, kwargs) + + return load +end + + +"creates a generator object with some defaults" +function create_generator(bus::Any, connections::Union{Vector{Int},Vector{String}}; + configuration::ConnConfig=WYE, + pg::Union{Vector{<:Real},Missing}=missing, + qg::Union{Vector{<:Real},Missing}=missing, + vg::Union{Vector{<:Real},Missing}=missing, + pg_lb::Union{Vector{<:Real},Missing}=missing, + pg_ub::Union{Vector{<:Real},Missing}=missing, + qg_lb::Union{Vector{<:Real},Missing}=missing, + qg_ub::Union{Vector{<:Real},Missing}=missing, + control_mode::ControlMode=FREQUENCYDROOP, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = length(connections) + + generator = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "configuration" => configuration, + "control_mode" => control_mode, + "status" => status, + ) + + for (k,v) in [("pg", pg), ("qg", qg), ("vg", vg), ("pg_lb", pg_lb), ("pg_ub", pg_ub), ("qg_lb", qg_lb), ("qg_ub", qg_ub)] + if !ismissing(v) + @assert length(v) == n_conductors + generator[k] = v + end + end + + _add_unused_kwargs!(generator, kwargs) + + return generator +end + + +"creates transformer code with some defaults" +function create_xfmrcode(; + configurations::Union{Vector{ConnConfig},Missing}=missing, + xsc::Union{Vector{<:Real},Missing}=missing, + rw::Union{Vector{<:Real},Missing}=missing, + tm_nom::Union{Vector{<:Real},Missing}=missing, + tm_lb::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_ub::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_set::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_fix::Union{Vector{Vector{<:Real}},Missing}=missing, + kwargs... +)::Dict{String,Any} + + n_windings = 0 + for v in [configurations, rw, tm_nom, tm_lb, tm_ub, tm_set, tm_fix] + if !ismissing(v) + n_windings = length(v) + break + end + end + + @assert n_windings >= 2 "Cannot determine valid number of windings" + @assert all(length(v) == n_windings for v in [configurations, rw, tm_nom, tm_lb, tm_ub, tm_set, tm_fix]) "Number of windings inconsistent between parameters" + + n_phases = 0 + for v in [tm_lb, tm_ub, tm_set, tm_fix] + if !ismissing(v) + n_windings = length(v[1]) + break + end + end + + @assert n_phases >= 1 "Cannot determine valid number of phases" + + eng_obj = Dict{String,Any}( + "configurations" => !ismissing(configurations) ? configurations : fill(WYE, n_windings), + "xsc" => !ismissing(xsc) ? xsc : zeros(Int(n_windings * (n_windings-1)//2)), + "rw" => !ismissing(rw) ? rw : zeros(n_windings), + "tm_nom" => !ismissing(tm_nom) ? tm_nom : ones(n_windings), + "tm_set" => !ismissing(tm_set) ? tm_set : fill(fill(1.0, )) + ) + + return eng_obj +end + + +"creates a n-winding transformer object with some defaults" +function create_transformer(buses::Vector{Any}, connections::Vector{Union{Vector{Int},Vector{String}}}; + configurations::Union{Vector{ConnConfig},Missing}=missing, + xfmrcode::Any=missing, + xsc::Union{Vector{<:Real},Missing}=missing, + rw::Union{Vector{<:Real},Missing}=missing, + imag::Real=0.0, + noloadloss::Real=0.0, + tm_nom::Union{Vector{<:Real},Missing}=missing, + tm_lb::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_ub::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_set::Union{Vector{Vector{<:Real}},Missing}=missing, + tm_fix::Union{Vector{Vector{Bool}},Missing}=missing, + polarity::Union{Vector{Int},Missing}=missing, + vm_nom::Union{Vector{<:Real},Missing}=missing, + sm_nom::Union{Vector{<:Real},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_windings = length(buses) + n_conductors = length(connections[1]) + + transformer = Dict{String,Any}( + "buses" => buses, + "connections" => connections, + "configurations" => !ismissing(configurations) ? configurations : fill(WYE, n_windings), + "xsc" => !ismissing(xsc) ? xsc : zeros(Int(n_windings * (n_windings-1)//2)), + "rw" => !ismissing(rw) ? rw : zeros(n_windings), + "cmag" => imag, + "noloadloss" => noloadloss, + "tm_nom" => !ismissing(tm_nom) ? tm_nom : ones(n_windings), + "tm_set" => !ismissing(tm_set) ? tm_set : fill(fill(1.0, n_conductors), n_windings), + "tm_fix" => !ismissing(tm_fix) ? tm_fix : fill(fill(true, n_conductors), n_windings), + "polarity" => !ismissing(polarity) ? polarity : fill(1, n_windings), + "status" => status, + ) + + for (k,v) in [("tm_lb", tm_lb), ("tm_ub", tm_ub), ("vm_nom", vm_nom), ("sm_nom", sm_nom)] + if !ismissing(v) + transformer[k] = v + end + end + + _add_unused_kwargs!(transformer, kwargs) + + return transformer +end + + +"creates a aysmmetric lossless 2-winding transformer object with some defaults" +function create_al2w_transformer(f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; + configuration::ConnConfig=WYE, + tm_nom::Real=1.0, + tm_lb::Union{Vector{<:Real},Missing}=missing, + tm_ub::Union{Vector{<:Real},Missing}=missing, + tm_set::Union{Vector{<:Real},Missing}=missing, + tm_fix::Union{Vector{Bool},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = length(f_connections) + n_conductors = configuration == WYE ? n_conductors-1 : n_conductors + + transformer = Dict{String,Any}( + "f_bus" => f_bus, + "t_bus" => t_bus, + "f_connections" => f_connections, + "t_connections" => t_connections, + "configuration" => configuration, + "tm_nom" => tm_nom, + "tm_set" => !ismissing(tm_set) ? tm_set : fill(1.0, n_conductors), + "tm_fix" => !ismissing(tm_fix) ? tm_fix : fill(true, n_conductors), + "status" => status, + ) + + for (k,v) in [("tm_lb", tm_lb), ("tm_ub", tm_ub)] + if !ismissing(v) + transformer[k] = v + end + end + + _add_unused_kwargs!(transformer, kwargs) + + return transformer +end + + +"creates a generic shunt with some defaults" +function create_shunt(bus, connections; + gs::Union{Matrix{<:Real},Missing}=missing, + bs::Union{Matrix{<:Real},Missing}=missing, + model::ShuntModel=GENERIC, + dispatchable::Dispatchable=NO, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = length(connections) + + shunt = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "gs" => !ismissing(gs) ? gs : fill(0.0, n_conductors, n_conductors), + "bs" => !ismissing(bs) ? bs : fill(0.0, n_conductors, n_conductors), + "model" => model, + "dispatchable" => dispatchable, + "status" => status, + ) + + _add_unused_kwargs!(shunt, kwargs) + + return shunt +end + + +"creates a solar generator with some defaults" +function create_solar(bus::Any, connections::Union{Vector{Int},Vector{String}}; + configuration::ConnConfig=WYE, + pg_lb::Union{Vector{<:Real},Missing}=missing, + pg_ub::Union{Vector{<:Real},Missing}=missing, + qg_lb::Union{Vector{<:Real},Missing}=missing, + qg_ub::Union{Vector{<:Real},Missing}=missing, + pg::Union{Vector{<:Real},Missing}=missing, + qg::Union{Vector{<:Real},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + eng_obj = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "configuration" => configuration, + "status" => status + ) + # TODO + return eng_obj +end + + +"creates energy storage object with some defaults" +function create_storage(bus::Any, connections::Union{Vector{Int},Vector{String}}; + configuration::ConnConfig=WYE, + energy::Real=0.0, + energy_ub::Real=0.0, + charge_ub::Real=0.0, + discharge_ub::Real=0.0, + sm_ub::Union{Vector{<:Real},Missing}=missing, + cm_ub::Union{Vector{<:Real},Missing}=missing, + charge_efficiency::Real=0.9, + discharge_efficiency::Real=0.9, + qs_lb::Union{Vector{<:Real},Missing}=missing, + qs_ub::Union{Vector{<:Real},Missing}=missing, + rs::Union{Vector{<:Real},Missing}=missing, + xs::Union{Vector{<:Real},Missing}=missing, + pex::Real=0.0, + qex::Real=0.0, + ps::Union{Vector{<:Real},Missing}=missing, + qs::Union{Vector{<:Real},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = length(connections) + + eng_obj = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "configuration" => configuration, + "energy" => energy, + "energy_ub" => energy_ub, + "charge_ub" => charge_ub, + "discharge_ub" => discharge_ub, + "charge_efficiency" => charge_efficiency, + "discharge_efficiency" => discharge_efficiency, + "pex" => pex, + "qex" => qex, + "status" => status + ) + + # TODO + + return eng_obj +end + + +"creates a voltage source with some defaults" +function create_voltage_source(bus, connections; + configuration::ConnConfig=WYE, + vm::Union{Vector{<:Real},Missing}=missing, + va::Union{Vector{<:Real},Missing}=missing, + rs::Union{Vector{<:Real},Missing}=missing, + xs::Union{Vector{<:Real},Missing}=missing, + status::Status=ENABLED, + kwargs... + )::Dict{String,Any} + + n_conductors = length(connections) + + voltage_source = Dict{String,Any}( + "bus" => bus, + "connections" => connections, + "configuration" => configuration, + "vm" => !ismissing(vm) ? vm : ones(n_conductors), + "va" => !ismissing(va) ? va : zeros(n_conductors), + "status" => get(kwargs, :status, ENABLED), + ) + + for (k,v) in [("rs", rs), ("xs", xs)] + if !ismissing(v) + voltage_source[k] = v + end + end + + _add_unused_kwargs!(voltage_source, kwargs) + + return voltage_source +end + + +"deletes a component from the engineering data model" +function delete_component!(data_eng::Dict{String,<:Any}, component_type::String, component_id::Any) + delete!(data_eng[component_type], component_id) + if isempty(data_eng[component_type]) + delete!(data_eng, component_type) + end +end + + +"Function to add default vbase for a bus" +function add_vbase_default!(data_eng::Dict{String,<:Any}, bus::Any, vbase::Real) + if !haskey(data_eng, "settings") + data_eng["settings"] = Dict{String,Any}() + end + + if !haskey(data_eng["settings"], "vbases_default") + data_eng["settings"]["vbases_default"] = Dict{Any,Real}() + end + + data_eng["settings"]["vbases_default"][bus] = vbase +end + + +# Data objects +add_bus!(data_eng::Dict{String,<:Any}, id::Any; kwargs...) = add_object!(data_eng, "bus", id, create_bus(; kwargs...)) +add_linecode!(data_eng::Dict{String,<:Any}, id::Any, rs::Matrix{<:Real}, xs::Matrix{<:Real}; kwargs...) = add_object!(data_eng, "linecode", id, create_linecode(rs, xs; kwargs...)) +add_xfmrcode!(data_eng::Dict{String,<:Any}, id::Any; kwargs...) = add_object!(data_eng, "xfmrcode", id, create_xfmrcode(; kwargs...)) +# add_time_series!(data_eng::Dict{String,<:Any}, id::Any; kwargs...) = add_object!(data_eng, "time_series", id, create_timeseries(; kwargs...)) + +# Edge objects +add_line!(data_eng::Dict{String,<:Any}, id::Any, f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "line", id, create_line(f_bus, t_bus, f_connections, t_connections; kwargs...)) +add_transformer!(data_eng::Dict{String,<:Any}, id::Any, buses::Vector{<:Any}, connections::Vector{Union{Vector{Int},Vector{String}}}; kwargs...) = add_object!(data_eng, "transformer", id, create_transformer(buses, connections; kwargs...)) +add_transformer!(data_eng::Dict{String,<:Any}, id::Any, f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "transformer", id, create_al2w_transformer(f_bus, t_bus, f_connections, t_connections; kwargs...)) +add_switch!(data_eng::Dict{String,<:Any}, id::Any, f_bus::Any, t_bus::Any, f_connections::Union{Vector{Int},Vector{String}}, t_connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "switch", id, create_switch(f_bus, t_bus, f_connections, t_connections; kwargs...)) + +# Node objects +add_load!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "load", id, create_load(bus, connections; kwargs...)) +add_shunt!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "shunt", id, create_shunt(bus, connections; kwargs...)) +add_voltage_source!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "voltage_source", id, create_voltage_source(bus, connections; kwargs...)) +add_generator!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "generator", id, create_generator(bus, connections; kwargs...)) +add_storage!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "storage", id, create_storage(bus, connections; kwargs...)) +add_solar!(data_eng::Dict{String,<:Any}, id::Any, bus::Any, connections::Union{Vector{Int},Vector{String}}; kwargs...) = add_object!(data_eng, "solar", id, create_solar(bus, connections; kwargs...)) diff --git a/src/data_model/eng2math.jl b/src/data_model/eng2math.jl new file mode 100644 index 000000000..2c8cd1085 --- /dev/null +++ b/src/data_model/eng2math.jl @@ -0,0 +1,811 @@ +import LinearAlgebra: diagm + +"items that are mapped one-to-one from engineering to math models" +const _1to1_maps = Dict{String,Vector{String}}( + "bus" => ["vm", "va", "terminals", "phases", "neutral", "vm_pn_lb", "vm_pn_ub", "vm_pp_lb", "vm_pp_ub", "vm_ng_ub", "dss"], + "line" => ["f_connections", "t_connections", "source_id", "dss"], + "transformer" => ["f_connections", "t_connections", "source_id", "dss"], + "switch" => ["status", "f_connections", "t_connections", "source_id", "dss"], + "shunt" => ["status", "dispatchable", "gs", "bs", "connections", "source_id", "dss"], + "load" => ["model", "configuration", "connections", "dispatchable", "status", "source_id", "dss"], + "generator" => ["pg", "qg", "vg", "configuration", "connections", "source_id", "dss"], + "solar" => ["pg", "qg", "configuration", "connections", "source_id", "dss"], + "storage" => ["status", "energy", "ps", "qs", "connections", "source_id", "dss"], + "voltage_source" => ["source_id", "dss"], +) + +"list of nodal type elements in the engineering model" +const _eng_node_elements = Vector{String}([ + "load", "shunt", "generator", "solar", "storage", "voltage_source" +]) + +"list of edge type elements in the engineering model" +const _eng_edge_elements = Vector{String}([ + "line", "switch", "transformer" +]) + +"list of nodal type elements in the engineering model" +const _math_node_elements = Vector{String}([ + "load", "shunt", "gen", "storage" +]) + +"list of edge type elements in the engineering model" +const _math_edge_elements = Vector{String}([ + "branch", "switch", "transformer", "dcline" +]) + +"list of multinetwork keys that belong at the root level" +const _pmd_math_global_keys = Set{String}([ + "data_model", "per_unit", "name", "settings", "map", "bus_lookup" +]) + + +"converts a engineering multinetwork to a math multinetwork" +function _map_eng2math_multinetwork(data_eng_mn::Dict{String,Any}; kron_reduced::Bool=kron_reduced)::Dict{String,Any} + data_math_mn = Dict{String,Any}( + "nw" => Dict{String,Any}(), + "multinetwork" => true + ) + for (n, nw) in data_eng_mn["nw"] + for k in _pmd_eng_global_keys + nw[k] = data_eng_mn[k] + end + + data_math_mn["nw"][n] = _map_eng2math(nw; kron_reduced=kron_reduced) + + for k in _pmd_math_global_keys + data_math_mn[k] = data_math_mn["nw"][n][k] + delete!(data_math_mn["nw"][n], k) + end + end + + return data_math_mn +end + + +"base function for converting engineering model to mathematical model" +function _map_eng2math(data_eng; kron_reduced::Bool=true) + @assert get(data_eng, "data_model", MATHEMATICAL) == ENGINEERING + + data_math = Dict{String,Any}( + "name" => get(data_eng, "name", ""), + "per_unit" => get(data_eng, "per_unit", false), + "data_model" => MATHEMATICAL, + "settings" => deepcopy(data_eng["settings"]), + ) + + #TODO the PM tests break for branches which are not of the size indicated by conductors; + # for now, set to 1 to prevent this from breaking when not kron-reduced + data_math["conductors"] = kron_reduced ? 3 : 1 + + data_math["map"] = Vector{Dict{String,Any}}([ + Dict{String,Any}("unmap_function" => "_map_math2eng_root!") + ]) + + _init_base_components!(data_math) + + # convert buses + _map_eng2math_bus!(data_math, data_eng; kron_reduced=kron_reduced) + + # convert edges + _map_eng2math_line!(data_math, data_eng; kron_reduced=kron_reduced) + _map_eng2math_switch!(data_math, data_eng; kron_reduced=kron_reduced) + _map_eng2math_transformer!(data_math, data_eng; kron_reduced=kron_reduced) + + # convert nodes + _map_eng2math_load!(data_math, data_eng; kron_reduced=kron_reduced) + + _map_eng2math_shunt!(data_math, data_eng; kron_reduced=kron_reduced) + + _map_eng2math_generator!(data_math, data_eng; kron_reduced=kron_reduced) + _map_eng2math_solar!(data_math, data_eng; kron_reduced=kron_reduced) + _map_eng2math_storage!(data_math, data_eng; kron_reduced=kron_reduced) + _map_eng2math_voltage_source!(data_math, data_eng; kron_reduced=kron_reduced) + + # post fix + if kron_reduced + #TODO move this out when kron-reducing becomes a transformation + _kron_reduce_buses!(data_math) + else + #TODO fix this in place / throw error instead? IEEE8500 leads to switches + # with 3x3 R matrices but only 1 phase + #NOTE: Don't do this when kron-reducing, it will undo the padding + _slice_branches!(data_math) + end + + return data_math +end + + +"converts engineering bus components into mathematical bus components" +function _map_eng2math_bus!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "bus", Dict{String,Any}()) + terminals = eng_obj["terminals"] + nconductors = data_math["conductors"] + + math_obj = _init_math_obj("bus", name, eng_obj, length(data_math["bus"])+1) + + math_obj["bus_i"] = math_obj["index"] + math_obj["bus_type"] = _bus_type_conversion(data_eng, eng_obj, "status") + + # take care of grounding; convert to shunt if lossy + grounded_perfect, shunts = _convert_grounding(eng_obj["terminals"], eng_obj["grounded"], eng_obj["rg"], eng_obj["xg"]) + + math_obj["grounded"] = grounded_perfect + to_sh = [] + for (sh_connections, sh_y) in shunts + sh_index = length(data_math["shunt"]) + 1 + data_math["shunt"]["$sh_index"] = Dict( + "index" => sh_index, + "shunt_bus" => math_obj["bus_i"], + "connections" => sh_connections, + "gs" => real.(sh_y), + "bs" => real.(sh_y), + ) + push!(to_sh, "shunt.$sh_index") + end + + if haskey(eng_obj, "vm") + math_obj["vm"] = eng_obj["vm"] + end + if haskey(eng_obj, "va") + math_obj["va"] = eng_obj["va"] + end + + math_obj["vmin"] = get(eng_obj, "vm_lb", fill(0.0, length(terminals))) + math_obj["vmax"] = get(eng_obj, "vm_ub", fill(Inf, length(terminals))) + + if kron_reduced + filter = terminals.!=kr_neutral + terminals_kr = terminals[filter] + @assert all(t in kr_phases for t in terminals_kr) "bus $name has terminals $(terminals), outside of $kr_phases, cannot be kron reduced" + + _apply_filter!(math_obj, ["vm", "va", "vmin", "vmax"], filter) + _pad_properties!(math_obj, ["vm", "va", "vmin", "vmax"], terminals_kr, kr_phases) + end + + data_math["bus"]["$(math_obj["index"])"] = math_obj + + if !haskey(data_math, "bus_lookup") + data_math["bus_lookup"] = Dict{Any,Int}() + end + + data_math["bus_lookup"][name] = math_obj["index"] + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "bus.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_bus!", + )) + end +end + + +"converts engineering lines into mathematical branches" +function _map_eng2math_line!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "line", Dict{Any,Dict{String,Any}}()) + _apply_linecode!(eng_obj, data_eng) + + math_obj = _init_math_obj("line", name, eng_obj, length(data_math["branch"])+1) + + nphases = length(eng_obj["f_connections"]) + nconductors = data_math["conductors"] + + math_obj["f_bus"] = data_math["bus_lookup"][eng_obj["f_bus"]] + math_obj["t_bus"] = data_math["bus_lookup"][eng_obj["t_bus"]] + + math_obj["br_r"] = _impedance_conversion(data_eng, eng_obj, "rs") + math_obj["br_x"] = _impedance_conversion(data_eng, eng_obj, "xs") + + math_obj["g_fr"] = _admittance_conversion(data_eng, eng_obj, "g_fr") + math_obj["g_to"] = _admittance_conversion(data_eng, eng_obj, "g_to") + + math_obj["b_fr"] = _admittance_conversion(data_eng, eng_obj, "b_fr") + math_obj["b_to"] = _admittance_conversion(data_eng, eng_obj, "b_to") + + math_obj["angmin"] = get(eng_obj, "vad_lb", fill(-60.0, nphases)) + math_obj["angmax"] = get(eng_obj, "vad_ub", fill( 60.0, nphases)) + + for (f_key, t_key) in [("cm_ub", "c_rating_a"), ("cm_ub_b", "c_rating_b"), ("cm_ub_c", "c_rating_c"), + ("sm_ub", "rate_a"), ("sm_ub_b", "rate_b"), ("sm_ub_c", "rate_c")] + if haskey(eng_obj, f_key) + math_obj[t_key] = eng_obj[f_key] + end + end + + math_obj["transformer"] = false + math_obj["shift"] = zeros(nphases) + math_obj["tap"] = ones(nphases) + + f_bus = data_eng["bus"][eng_obj["f_bus"]] + t_bus = data_eng["bus"][eng_obj["t_bus"]] + + if kron_reduced + @assert all(eng_obj["f_connections"].==eng_obj["t_connections"]) "Kron reduction is only supported if f_connections == t_connections" + filter = _kron_reduce_branch!(math_obj, + ["br_r", "br_x"], ["g_fr", "b_fr", "g_to", "b_to"], + eng_obj["f_connections"], kr_neutral + ) + _apply_filter!(math_obj, ["angmin", "angmax", "tap", "shift"], filter) + connections = eng_obj["f_connections"][filter] + _pad_properties!(math_obj, ["br_r", "br_x", "g_fr", "g_to", "b_fr", "b_to", "shift"], connections, kr_phases) + _pad_properties!(math_obj, ["angmin"], connections, kr_phases; pad_value=-60.0) + _pad_properties!(math_obj, ["angmax"], connections, kr_phases; pad_value=60.0) + _pad_properties!(math_obj, ["tap"], connections, kr_phases; pad_value=1.0) + + for key in ["c_rating_a", "c_rating_b", "c_rating_c", "rate_a", "rate_b", "rate_c"] + if haskey(math_obj, key) + _apply_filter!(math_obj, [key], filter) + _pad_properties!(math_obj, [key], connections, kr_phases) + end + end + else + math_obj["f_connections"] = eng_obj["f_connections"] + math_obj["t_connections"] = eng_obj["t_connections"] + end + + math_obj["switch"] = false + + math_obj["br_status"] = Int(eng_obj["status"]) + + data_math["branch"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "branch.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_line!", + )) + end +end + + +"converts engineering n-winding transformers into mathematical ideal 2-winding lossless transformer branches and impedance branches to represent the loss model" +function _map_eng2math_transformer!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "transformer", Dict{Any,Dict{String,Any}}()) + # Build map first, so we can update it as we decompose the transformer + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => Vector{String}([]), + "unmap_function" => "_map_math2eng_transformer!", + )) + + to_map = data_math["map"][end]["to"] + + _apply_xfmrcode!(eng_obj, data_eng) + + if haskey(eng_obj, "f_bus") && haskey(eng_obj, "t_bus") + @assert all(haskey(eng_obj, k) for k in ["f_bus", "t_bus", "f_connections", "t_connections"]) "Incomplete definition of AL2W tranformer $name, aborting eng2math conversion" + + nphases = length(eng_obj["f_connections"]) + + math_obj = Dict{String,Any}( + "name" => name, + "source_id" => eng_obj["source_id"], + "f_bus" => data_math["bus_lookup"][eng_obj["f_bus"]], + "t_bus" => data_math["bus_lookup"][eng_obj["t_bus"]], + "f_connections" => eng_obj["f_connections"], + "t_connections" => eng_obj["t_connections"], + "configuration" => get(eng_obj, "configuration", WYE), + "tm_nom" => get(eng_obj, "tm_nom", 1.0), + "tm_set" => get(eng_obj, "tm_set", fill(1.0, nphases)), + "tm_fix" => get(eng_obj, "tm_fix", fill(true, nphases)), + "polarity" => fill(1, nphases), + "status" => Int(get(eng_obj, "status", ENABLED)), + "index" => length(data_math["transformer"])+1 + ) + + for k in ["tm_lb", "tm_ub"] + if haskey(eng_obj, k) + math_obj[k] = eng_obj[k] + end + end + + if kron_reduced + # TODO fix how padding works, this is a workaround to get bank working + if eng_obj["configuration"] == WYE + f_connections = math_obj["f_connections"] + _pad_properties!(math_obj, ["tm_lb", "tm_ub", "tm_set"], f_connections[f_connections.!=kr_neutral], kr_phases; pad_value=1.0) + _pad_properties!(math_obj, ["tm_fix"], f_connections[f_connections.!=kr_neutral], kr_phases; pad_value=false) + + math_obj["f_connections"] = math_obj["t_connections"] + end + end + + data_math["transformer"]["$(math_obj["index"])"] = math_obj + + push!(to_map, "transformer.$(math_obj["index"])") + else + vnom = eng_obj["vm_nom"] * data_eng["settings"]["voltage_scale_factor"] + snom = eng_obj["sm_nom"] * data_eng["settings"]["power_scale_factor"] + + nrw = length(eng_obj["bus"]) + + # calculate zbase in which the data is specified, and convert to SI + zbase = (vnom.^2) ./ snom + + # x_sc is specified with respect to first winding + x_sc = eng_obj["xsc"] .* zbase[1] + + # rs is specified with respect to each winding + r_s = eng_obj["rw"] .* zbase + + g_sh = (eng_obj["noloadloss"]*snom[1])/vnom[1]^2 + b_sh = -(eng_obj["cmag"]*snom[1])/vnom[1]^2 + + # data is measured externally, but we now refer it to the internal side + ratios = vnom/data_eng["settings"]["voltage_scale_factor"] + x_sc = x_sc./ratios[1]^2 + r_s = r_s./ratios.^2 + g_sh = g_sh*ratios[1]^2 + b_sh = b_sh*ratios[1]^2 + + # convert x_sc from list of upper triangle elements to an explicit dict + y_sh = g_sh + im*b_sh + z_sc = Dict([(key, im*x_sc[i]) for (i,key) in enumerate([(i,j) for i in 1:nrw for j in i+1:nrw])]) + + #TODO remove once moving out kron-reduction + dims = kron_reduced ? 3 : length(eng_obj["tm_set"][1]) + transformer_t_bus_w = _build_loss_model!(data_math, name, to_map, r_s, z_sc, y_sh, nphases=dims) + + for w in 1:nrw + # 2-WINDING TRANSFORMER + # make virtual bus and mark it for reduction + tm_nom = eng_obj["configuration"][w]==DELTA ? eng_obj["vm_nom"][w]*sqrt(3) : eng_obj["vm_nom"][w] + transformer_2wa_obj = Dict{String,Any}( + "name" => "_virtual_transformer.$name.$w", + "source_id" => "_virtual_transformer.$(eng_obj["source_id"]).$w", + "f_bus" => data_math["bus_lookup"][eng_obj["bus"][w]], + "t_bus" => transformer_t_bus_w[w], + "tm_nom" => tm_nom, + "f_connections" => eng_obj["connections"][w], + "t_connections" => collect(1:dims+1), + "configuration" => eng_obj["configuration"][w], + "polarity" => eng_obj["polarity"][w], + "tm_set" => eng_obj["tm_set"][w], + "tm_fix" => eng_obj["tm_fix"][w], + "status" => Int(get(eng_obj, "status", ENABLED)), + "index" => length(data_math["transformer"])+1 + ) + + for prop in ["tm_lb", "tm_ub", "tm_step"] + if haskey(eng_obj, prop) + transformer_2wa_obj[prop] = eng_obj[prop][w] + end + end + + if kron_reduced + # TODO fix how padding works, this is a workaround to get bank working + if all(conf==WYE for conf in eng_obj["configuration"]) + f_connections = transformer_2wa_obj["f_connections"] + _pad_properties!(transformer_2wa_obj, ["tm_lb", "tm_ub", "tm_set"], f_connections[f_connections.!=kr_neutral], kr_phases; pad_value=1.0) + _pad_properties!(transformer_2wa_obj, ["tm_fix"], f_connections[f_connections.!=kr_neutral], kr_phases; pad_value=false) + + transformer_2wa_obj["f_connections"] = transformer_2wa_obj["t_connections"] + end + end + + data_math["transformer"]["$(transformer_2wa_obj["index"])"] = transformer_2wa_obj + + push!(to_map, "transformer.$(transformer_2wa_obj["index"])") + end + end + end +end + + +"converts engineering switches into mathematical switches and (if neeed) impedance branches to represent loss model" +function _map_eng2math_switch!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + # TODO enable real switches (right now only using vitual lines) + for (name, eng_obj) in get(data_eng, "switch", Dict{Any,Dict{String,Any}}()) + nphases = length(eng_obj["f_connections"]) + nconductors = data_math["conductors"] + + math_obj = _init_math_obj("switch", name, eng_obj, length(data_math["switch"])+1) + + math_obj["f_bus"] = data_math["bus_lookup"][eng_obj["f_bus"]] + math_obj["t_bus"] = data_math["bus_lookup"][eng_obj["t_bus"]] + + math_obj["state"] = get(eng_obj, "state", CLOSED) + + # OPF bounds + for (fr_key, to_key) in [("cm_ub", "c_rating")] + if haskey(eng_obj, fr_key) + math_obj[to_key] = eng_obj[fr_key] + end + end + + map_to = "switch.$(math_obj["index"])" + + if haskey(eng_obj, "linecode") + _apply_linecode!(eng_obj, data_eng) + end + + if true + # if !all(isapprox.(get(eng_obj, "rs", zeros(1, 1)), 0)) && !all(isapprox.(get(eng_obj, "xs", zeros(1, 1)), 0)) # TODO enable real switches + # build virtual bus + + f_bus = data_math["bus"]["$(math_obj["f_bus"])"] + + bus_obj = Dict{String,Any}( + "name" => "_virtual_bus.switch.$name", + "bus_i" => length(data_math["bus"])+1, + "bus_type" => get(eng_obj, "state", CLOSED) == OPEN ? 4 : 1, + "vmin" => f_bus["vmin"], + "vmax" => f_bus["vmax"], + "index" => length(data_math["bus"])+1, + ) + + #= TODO enable real switches + # math_obj["t_bus"] = bus_obj["bus_i"] + # data_math["bus"]["$(bus_obj["index"])"] = bus_obj + =# + + # TODO remove after enabling real switches + if all(isapprox.(get(eng_obj, "rs", zeros(nphases, nphases)), 0)) + eng_obj["rs"] = fill(1e-4, nphases, nphases) + end + + if all(isapprox.(get(eng_obj, "xs", zeros(nphases, nphases)), 0)) + eng_obj["xs"] = fill(1e-3, nphases, nphases) + end + + branch_obj = _init_math_obj("line", name, eng_obj, length(data_math["branch"])+1) + + _branch_obj = Dict{String,Any}( + "name" => "_virtual_branch.switch.$name", + "source_id" => "_virtual_branch.switch.$name", + # "f_bus" => bus_obj["bus_i"], # TODO enable real switches + "f_bus" => data_math["bus_lookup"][eng_obj["f_bus"]], + "t_bus" => data_math["bus_lookup"][eng_obj["t_bus"]], + "br_r" => _impedance_conversion(data_eng, eng_obj, "rs"), + "br_x" => _impedance_conversion(data_eng, eng_obj, "xs"), + "g_fr" => zeros(nphases, nphases), + "g_to" => zeros(nphases, nphases), + "b_fr" => zeros(nphases, nphases), + "b_to" => zeros(nphases, nphases), + "angmin" => fill(-60.0, nphases), + "angmax" => fill( 60.0, nphases), + "transformer" => false, + "shift" => zeros(nphases), + "tap" => ones(nphases), + "switch" => false, + "br_status" => get(eng_obj, "state", CLOSED) == OPEN ? 0 : 1, + ) + + merge!(branch_obj, _branch_obj) + + if kron_reduced + @assert all(eng_obj["f_connections"].==eng_obj["t_connections"]) "Kron reduction is only supported if f_connections == t_connections" + filter = _kron_reduce_branch!(branch_obj, + ["br_r", "br_x"], ["g_fr", "b_fr", "g_to", "b_to"], + eng_obj["f_connections"], kr_neutral + ) + _apply_filter!(branch_obj, ["angmin", "angmax", "tap", "shift"], filter) + connections = eng_obj["f_connections"][filter] + _pad_properties!(branch_obj, ["br_r", "br_x", "g_fr", "g_to", "b_fr", "b_to", "shift"], connections, kr_phases) + _pad_properties!(branch_obj, ["angmin"], connections, kr_phases; pad_value=-60.0) + _pad_properties!(branch_obj, ["angmax"], connections, kr_phases; pad_value=60.0) + _pad_properties!(branch_obj, ["tap"], connections, kr_phases; pad_value=1.0) + else + branch_obj["f_connections"] = eng_obj["f_connections"] + branch_obj["f_connections"] = eng_obj["t_connections"] + end + + data_math["branch"]["$(branch_obj["index"])"] = branch_obj + + map_to = ["branch.$(branch_obj["index"])"] + # map_to = [map_to, "bus.$(bus_obj["index"])", "branch.$(branch_obj["index"])"] # TODO enable real switches + end + + # data_math["switch"]["$(math_obj["index"])"] = math_obj # TODO enable real switches + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => map_to, + "unmap_function" => "_map_math2eng_switch!", + )) + end +end + + +"converts engineering generic shunt components into mathematical shunt components" +function _map_eng2math_shunt!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "shunt", Dict{Any,Dict{String,Any}}()) + math_obj = _init_math_obj("shunt", name, eng_obj, length(data_math["shunt"])+1) + + # TODO change to new capacitor shunt calc logic + math_obj["shunt_bus"] = data_math["bus_lookup"][eng_obj["bus"]] + + math_obj["gs"] = get(eng_obj, "gs", zeros(size(eng_obj["bs"]))) + + if kron_reduced + filter = _kron_reduce_branch!(math_obj, + Vector{String}([]), ["gs", "bs"], + eng_obj["connections"], kr_neutral + ) + connections = eng_obj["connections"][filter] + _pad_properties!(math_obj, ["gs", "bs"], connections, kr_phases) + else + math_obj["connections"] = eng_obj["connections"] + end + + data_math["shunt"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "shunt.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_shunt!", + )) + end +end + + +"converts engineering load components into mathematical load components" +function _map_eng2math_load!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "load", Dict{Any,Dict{String,Any}}()) + math_obj = _init_math_obj("load", name, eng_obj, length(data_math["load"])+1) + + connections = eng_obj["connections"] + + math_obj["load_bus"] = data_math["bus_lookup"][eng_obj["bus"]] + + math_obj["pd"] = eng_obj["pd_nom"] + math_obj["qd"] = eng_obj["qd_nom"] + + if kron_reduced + if math_obj["configuration"]==WYE + @assert connections[end]==kr_neutral "for wye-connected loads, if kron_reduced the connections list should end with a neutral" + _pad_properties!(math_obj, ["pd", "qd"], connections[connections.!=kr_neutral], kr_phases) + else + _pad_properties_delta!(math_obj, ["pd", "qd"], connections, kr_phases) + end + else + math_obj["connections"] = connections + end + + math_obj["vnom_kv"] = eng_obj["vm_nom"] + + data_math["load"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "load.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_load!", + )) + end +end + + +"converts engineering generators into mathematical generators" +function _map_eng2math_generator!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "generator", Dict{String,Any}()) + math_obj = _init_math_obj("generator", name, eng_obj, length(data_math["gen"])+1) + + connections = eng_obj["connections"] + nconductors = data_math["conductors"] + + math_obj["gen_bus"] = data_math["bus_lookup"][eng_obj["bus"]] + math_obj["gen_status"] = Int(eng_obj["status"]) + math_obj["control_mode"] = get(eng_obj, "control_mode", FREQUENCYDROOP) + math_obj["pmax"] = get(eng_obj, "pg_ub", fill(Inf, nconductors)) + + for (f_key, t_key) in [("qg_lb", "qmin"), ("qg_ub", "qmax"), ("pg_lb", "pmin")] + if haskey(eng_obj, f_key) + math_obj[t_key] = eng_obj[f_key] + end + end + + _add_gen_cost_model!(math_obj, eng_obj) + + math_obj["configuration"] = get(eng_obj, "configuration", WYE) + + if kron_reduced + if math_obj["configuration"]==WYE + @assert connections[end]==kr_neutral "For WYE connected generators, if kron_reduced the connections list should end with a neutral conductor" + _pad_properties!(math_obj, ["pg", "qg", "vg", "pmin", "pmax", "qmin", "qmax"], connections[1:end-1], kr_phases) + else + _pad_properties_delta!(math_obj, ["pg", "qg", "vg", "pmin", "pmax", "qmin", "qmax"], connections, kr_phases) + end + else + math_obj["connections"] = connections + end + + # if PV generator mode convert attached bus to PV bus + if math_obj["control_mode"] == ISOCHRONOUS + data_math["bus"]["$(data_math["bus_lookup"][eng_obj["bus"]])"]["bus_type"] = 2 + end + + data_math["gen"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "gen.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_generator!", + )) + end +end + + +"converts engineering solar components into mathematical generators" +function _map_eng2math_solar!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "solar", Dict{Any,Dict{String,Any}}()) + math_obj = _init_math_obj("solar", name, eng_obj, length(data_math["gen"])+1) + + connections = eng_obj["connections"] + nconductors = data_math["conductors"] + + math_obj["gen_bus"] = data_math["bus_lookup"][eng_obj["bus"]] + math_obj["gen_status"] = Int(eng_obj["status"]) + + for (fr_k, to_k) in [("vg", "vg"), ("pg_lb", "pmin"), ("pg_ub", "pmax"), ("qg_lb", "qmin"), ("qg_ub", "qmax")] + if haskey(eng_obj, fr_k) + math_obj[to_k] = eng_obj[fr_k] + end + end + + _add_gen_cost_model!(math_obj, eng_obj) + + if kron_reduced + if math_obj["configuration"]==WYE + @assert(connections[end]==kr_neutral) + _pad_properties!(math_obj, ["pg", "qg", "vg", "pmin", "pmax", "qmin", "qmax"], connections[1:end-1], kr_phases) + else + _pad_properties_delta!(math_obj, ["pg", "qg", "vg", "pmin", "pmax", "qmin", "qmax"], connections, kr_phases) + end + else + math_obj["connections"] = connections + end + + data_math["gen"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "gen.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_solar!", + )) + end +end + + +"converts engineering storage into mathematical storage" +function _map_eng2math_storage!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1, 2, 3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "storage", Dict{Any,Dict{String,Any}}()) + math_obj = _init_math_obj("storage", name, eng_obj, length(data_math["storage"])+1) + + connections = eng_obj["connections"] + nconductors = data_math["conductors"] + + math_obj["storage_bus"] = data_math["bus_lookup"][eng_obj["bus"]] + + # needs to be in units MW + math_obj["energy"] = eng_obj["energy"] * data_eng["settings"]["power_scale_factor"] / 1e6 + #TODO is scale factor correct? + math_obj["energy_rating"] = eng_obj["energy_ub"] * data_eng["settings"]["power_scale_factor"] / 1e6 + math_obj["charge_rating"] = eng_obj["charge_ub"] * data_eng["settings"]["power_scale_factor"] / 1e6 + math_obj["discharge_rating"] = eng_obj["discharge_ub"] * data_eng["settings"]["power_scale_factor"] / 1e6 + math_obj["charge_efficiency"] = eng_obj["charge_efficiency"] / 100.0 + math_obj["discharge_efficiency"] = eng_obj["discharge_efficiency"] / 100.0 + #TODO is scale factor correct? what should be the unit? + math_obj["thermal_rating"] = eng_obj["cm_ub"] .* data_eng["settings"]["power_scale_factor"] ./ 1e6 + math_obj["qmin"] = eng_obj["qs_lb"] .* data_eng["settings"]["power_scale_factor"] ./ 1e6 + math_obj["qmax"] = eng_obj["qs_ub"] .* data_eng["settings"]["power_scale_factor"] ./ 1e6 + math_obj["r"] = eng_obj["rs"] + math_obj["x"] = eng_obj["xs"] + math_obj["p_loss"] = eng_obj["pex"] .* data_eng["settings"]["power_scale_factor"] ./ 1e6 + math_obj["q_loss"] = eng_obj["qex"] .* data_eng["settings"]["power_scale_factor"] ./ 1e6 + + math_obj["ps"] = get(eng_obj, "ps", zeros(size(eng_obj["cm_ub"]))) + math_obj["qs"] = get(eng_obj, "qs", zeros(size(eng_obj["cm_ub"]))) + + if kron_reduced + _pad_properties!(math_obj, ["thermal_rating", "qmin", "qmax", "r", "x", "ps", "qs"], connections, kr_phases) + end + + data_math["storage"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => "storage.$(math_obj["index"])", + "unmap_function" => "_map_math2eng_storage!", + )) + end +end + + +"converts engineering voltage sources into mathematical generators and (if needed) impedance branches to represent the loss model" +function _map_eng2math_voltage_source!(data_math::Dict{String,<:Any}, data_eng::Dict{<:Any,<:Any}; kron_reduced::Bool=true, kr_phases::Vector{Int}=[1,2,3], kr_neutral::Int=4) + for (name, eng_obj) in get(data_eng, "voltage_source", Dict{Any,Any}()) + nconductors = length(eng_obj["vm"]) + + math_obj = _init_math_obj("voltage_source", name, eng_obj, length(data_math["gen"])+1) + + math_obj["name"] = "_virtual_gen.voltage_source.$name" + math_obj["gen_bus"] = gen_bus = data_math["bus_lookup"][eng_obj["bus"]] + math_obj["gen_status"] = Int(eng_obj["status"]) + math_obj["pg"] = fill(0.0, nconductors) + math_obj["qg"] = fill(0.0, nconductors) + math_obj["pmin"] = get(eng_obj, "pg_lb", fill(-Inf, nconductors)) + math_obj["pmax"] = get(eng_obj, "pg_ub", fill( Inf, nconductors)) + math_obj["qmin"] = get(eng_obj, "qg_lb", fill(-Inf, nconductors)) + math_obj["qmax"] = get(eng_obj, "qg_ub", fill( Inf, nconductors)) + math_obj["configuration"] = WYE + math_obj["source_id"] = "_virtual_gen.$(eng_obj["source_id"])" + + _add_gen_cost_model!(math_obj, eng_obj) + + map_to = "gen.$(math_obj["index"])" + + if !all(isapprox.(get(eng_obj, "rs", zeros(1, 1)), 0)) && !all(isapprox.(get(eng_obj, "xs", zeros(1, 1)), 0)) + bus_obj = Dict{String,Any}( + "bus_i" => length(data_math["bus"])+1, + "index" => length(data_math["bus"])+1, + "terminals" => collect(1:nconductors+1), + "grounded" => [fill(false, nconductors)..., true], + "name" => "_virtual_bus.voltage_source.$name", + "bus_type" => 3, + "vm" => [eng_obj["vm"]..., 0.0], + "va" => [eng_obj["va"]..., 0.0], + "vmin" => [eng_obj["vm"]..., 0.0], + "vmax" => [eng_obj["vm"]..., 0.0] + ) + + if kron_reduced + bus_obj["terminals"] = bus_obj["terminals"][1:end-1] + bus_obj["grounded"] = bus_obj["grounded"][1:end-1] + bus_obj["vm"] = bus_obj["vm"][1:end-1] + bus_obj["va"] = bus_obj["va"][1:end-1] + bus_obj["vmin"] = bus_obj["vmin"][1:end-1] + bus_obj["vmax"] = bus_obj["vmax"][1:end-1] + end + + math_obj["gen_bus"] = gen_bus = bus_obj["bus_i"] + + data_math["bus"]["$(bus_obj["index"])"] = bus_obj + + branch_obj = Dict{String,Any}( + "name" => "_virtual_branch.voltage_source.$name", + "source_id" => "_virtual_branch.$(eng_obj["source_id"])", + "f_bus" => bus_obj["bus_i"], + "t_bus" => data_math["bus_lookup"][eng_obj["bus"]], + "f_connections" => collect(1:nconductors), + "t_connections" => eng_obj["connections"], + "angmin" => fill(-60.0, nconductors), + "angmax" => fill( 60.0, nconductors), + "shift" => fill(0.0, nconductors), + "tap" => fill(1.0, nconductors), + "tranformer" => false, + "switch" => false, + "br_status" => 1, + "br_r" => _impedance_conversion(data_eng, eng_obj, "rs"), + "br_x" => _impedance_conversion(data_eng, eng_obj, "xs"), + "g_fr" => zeros(nconductors, nconductors), + "g_to" => zeros(nconductors, nconductors), + "b_fr" => zeros(nconductors, nconductors), + "b_to" => zeros(nconductors, nconductors), + "index" => length(data_math["branch"])+1 + ) + + # finally, we have to set a neutral for the virtual generator + neutral = _get_ground_math!(data_math["bus"]["$gen_bus"], exclude_terminals=[1:nconductors...]) + math_obj["connections"] = [collect(1:nconductors)..., neutral] + + data_math["branch"]["$(branch_obj["index"])"] = branch_obj + + map_to = [map_to, "bus.$(bus_obj["index"])", "branch.$(branch_obj["index"])"] + else + data_math["bus"]["$gen_bus"]["vmin"] = [eng_obj["vm"]..., 0.0] + data_math["bus"]["$gen_bus"]["vmax"] = [eng_obj["vm"]..., 0.0] + data_math["bus"]["$gen_bus"]["vm"] = [eng_obj["vm"]..., 0.0] + data_math["bus"]["$gen_bus"]["va"] = [eng_obj["va"]..., 0.0] + data_math["bus"]["$gen_bus"]["bus_type"] = 3 + end + + data_math["gen"]["$(math_obj["index"])"] = math_obj + + push!(data_math["map"], Dict{String,Any}( + "from" => name, + "to" => map_to, + "unmap_function" => "_map_math2eng_voltage_source!", + )) + end +end diff --git a/src/data_model/math2eng.jl b/src/data_model/math2eng.jl new file mode 100644 index 000000000..55b0b8b1e --- /dev/null +++ b/src/data_model/math2eng.jl @@ -0,0 +1,209 @@ +"" +function transform_solution(solution_math::Dict{String,<:Any}, data_math::Dict{String,<:Any}; map::Union{Vector{Dict{String,<:Any}},Missing}=missing, make_si::Bool=true, convert_rad2deg::Bool=true)::Dict{String,Any} + @assert get(data_math, "data_model", MATHEMATICAL) == MATHEMATICAL "provided solution cannot be converted to an engineering model" + if ismultinetwork(data_math) + solution_eng = Dict{String,Any}( + "nw" => Dict{String,Any}( + k => Dict{Any,Any}() for k in keys(data_math["nw"]) + ) + ) + else + solution_eng = Dict{String,Any}() + end + + solution_math = solution_make_si(solution_math, data_math; mult_vbase=make_si, mult_sbase=make_si, convert_rad2deg=convert_rad2deg) + + map = ismissing(map) ? get(data_math, "map", Vector{Dict{String,Any}}()) : map + @assert !isempty(map) "Map is empty, cannot map solution up to engineering model" + + for map_item in reverse(map) + if ismultinetwork(data_math) && map_item["unmap_function"] != "_map_math2eng_root!" + for (n, nw) in solution_math["nw"] + getfield(PowerModelsDistribution, Symbol(map_item["unmap_function"]))(solution_eng["nw"][n], nw, map_item) + end + else + getfield(PowerModelsDistribution, Symbol(map_item["unmap_function"]))(solution_eng, solution_math, map_item) + end + end + + if ismultinetwork(data_math) + for (n,nw) in solution_eng["nw"] + for (k,v) in nw + if isempty(v) + delete!(nw, k) + end + end + end + else + for (k,v) in solution_eng + if isempty(v) + delete!(solution_eng, k) + end + end + end + + return solution_eng +end + + +"" +function _map_math2eng_voltage_source!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + if !haskey(data_eng, "voltage_source") + data_eng["voltage_source"] = Dict{Any,Any}() + end + + eng_obj = _init_unmap_eng_obj!(data_eng, "voltage_source", map) + + map["to"] = isa(map["to"], Vector) ? map["to"] : [map["to"]] + + for to_id in map["to"] + math_obj = _get_math_obj(data_math, to_id) + if startswith(to_id, "gen") + for property in ["pg", "qg", "pg_bus", "qg_bus"] + if haskey(math_obj, property) + eng_obj[property] = math_obj[property] + end + end + end + end + + if !isempty(eng_obj) + data_eng["voltage_source"][map["from"]] = eng_obj + end +end + + +"" +function _map_math2eng_bus!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "bus", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["bus"][map["from"]] = eng_obj + end +end + + +"" +function _map_math2eng_load!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "load", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["load"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_shunt!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "shunt", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["shunt"][map["from"]] = eng_obj +end +end + + +function _map_math2eng_generator!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "generator", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["generator"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_solar!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "solar", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["solar"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_storage!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "storage", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["storage"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_line!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "line", map) + math_obj = _get_math_obj(data_math, map["to"]) + + merge!(eng_obj, math_obj) + + if !isempty(eng_obj) + data_eng["line"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_switch!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "switch", map) + + + for to_id in map["to"] + if startswith(to_id, "branch") # TODO update math2eng switch for when switches are fully supported + math_obj = _get_math_obj(data_math, to_id) + merge!(eng_obj, math_obj) + end + end + + if !isempty(eng_obj) + data_eng["switch"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_transformer!(data_eng::Dict{<:Any,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + eng_obj = _init_unmap_eng_obj!(data_eng, "transformer", map) + + trans_2wa_ids = [index for (comp_type, index) in split.(map["to"], ".", limit=2) if comp_type=="transformer"] + + prop_map = Dict("pf"=>"p", "qf"=>"q", "crt_fr"=>"crt", "cit_fr"=>"cit") + for (prop_from, prop_to) in prop_map + if haskey(data_math, "transformer") + if any(haskey(data_math["transformer"][id], prop_from) for id in trans_2wa_ids) + eng_obj[prop_to] = [get(data_math["transformer"][id], prop_from, NaN) for id in trans_2wa_ids] + end + end + end + + if !isempty(eng_obj) + data_eng["transformer"][map["from"]] = eng_obj + end +end + + +function _map_math2eng_root!(data_eng::Dict{String,<:Any}, data_math::Dict{String,<:Any}, map::Dict{String,<:Any}) + data_eng["per_unit"] = data_math["per_unit"] + + if !ismultinetwork(data_math) + data_eng["settings"] = Dict{String,Any}("sbase" => data_math["baseMVA"]) + else + for (n,nw) in data_eng["nw"] + nw["settings"] = Dict{String,Any}("sbase" => data_math["nw"][n]["baseMVA"]) + end + end +end diff --git a/src/data_model/transformations.jl b/src/data_model/transformations.jl new file mode 100644 index 000000000..5e8f0004b --- /dev/null +++ b/src/data_model/transformations.jl @@ -0,0 +1,49 @@ +# This file contains useful transformation functions for the engineering data model + +const _loss_model_objects = Dict{String,Vector{String}}( + "switch" => Vector{String}(["linecode", "rs", "xs"]), + "voltage_source" => Vector{String}(["rs", "xs"]), + "transformer" => Vector{String}(["rw", "xsc", "cmag", "noloadloss"]) +) + + +"remove parameters from objects with loss models to make them lossless" +function make_lossless!(data_eng::Dict{String,<:Any}) + @assert data_eng["data_model"] == ENGINEERING "incorrect data model type" + + for (object_type, parameters) in _loss_model_objects + if haskey(data_eng, object_type) + for (id, eng_obj) in data_eng[object_type] + for parameter in parameters + if haskey(eng_obj, parameter) + if parameter == "linecode" + delete!(eng_obj, parameter) + else + eng_obj[parameter] = 0 .* eng_obj[parameter] + end + end + end + end + end + end +end + + +"add voltage bounds" +function apply_voltage_bounds!(data_eng::Dict{String,<:Any}; vm_lb::Union{Real,Missing}=0.9, vm_ub::Union{Real,Missing}=1.1) + @assert data_eng["data_model"] == ENGINEERING "incorrect data model type" + + (bus_vbases, edge_vbases) = calc_voltage_bases(data_eng, data_eng["settings"]["vbases_default"]) + if haskey(data_eng, "bus") + for (id,bus) in data_eng["bus"] + vbase = bus_vbases[id] + if !ismissing(vm_lb) && !haskey(bus, "vm_lb") + bus["vm_lb"] = vbase .* fill(vm_lb, length(bus["terminals"])) + end + + if !ismissing(vm_ub) && !haskey(bus, "vm_ub") + bus["vm_ub"] = vbase .* fill(vm_ub, length(bus["terminals"])) + end + end + end +end diff --git a/src/data_model/units.jl b/src/data_model/units.jl new file mode 100644 index 000000000..f29d17ff1 --- /dev/null +++ b/src/data_model/units.jl @@ -0,0 +1,522 @@ +"lists of scaling factors and what they apply to" +const _dimensionalize_math = Dict{String,Dict{String,Vector{String}}}( + "bus" => Dict{String,Vector{String}}( + "rad2deg"=>Vector{String}(["va", "va_pp", "va_pn"]), + "vbase"=>Vector{String}(["vm", "vr", "vi", "vm_pp", "vm_pn"]) + ), + "gen" => Dict{String,Vector{String}}( + "sbase"=>Vector{String}(["pg", "qg", "pg_bus", "qg_bus"]), + "ibase"=>Vector{String}(["crg", "cig", "crg_bus", "cig_bus"]) + ), + "load" => Dict{String,Vector{String}}( + "sbase"=>Vector{String}(["pd", "qd", "pd_bus", "qd_bus"]), + "ibase"=>Vector{String}(["crd", "cid", "crd_bus", "cid_bus"]) + ), + "branch" => Dict{String,Vector{String}}( + "sbase"=>Vector{String}(["pf", "qf", "pt", "qt"]), + "ibase"=>Vector{String}(["cr_fr", "ci_fr", "cr_to", "cr_to"]) + ), + "transformer" => Dict{String,Vector{String}}( + "ibase_fr"=>Vector{String}(["crt_fr", "cit_fr"]), + "ibase_to"=>Vector{String}(["crt_to", "cit_to"]) + ), +) + + +"converts data model between per-unit and SI units" +function make_per_unit!(data::Dict{String,<:Any}; vbases::Union{Dict{<:Any,<:Real},Missing}=missing, sbase::Union{Real,Missing}=missing) + data_model_type = get(data, "data_model", MATHEMATICAL) + + if data_model_type == MATHEMATICAL + if !get(data, "per_unit", false) + if ismissing(vbases) + vbases = Dict{String,Real}("$(data["bus_lookup"][id])"=>vbase for (id, vbase) in data["settings"]["vbases_default"]) + end + + if ismissing(sbase) + sbase = data["settings"]["sbase_default"] + end + + if ismultinetwork(data) + for (n, nw) in data["nw"] + nw["data_model"] = data["data_model"] + _make_math_per_unit!(nw, data; sbase=sbase, vbases=vbases) + delete!(nw, "data_model") + end + else + _make_math_per_unit!(data, data; sbase=sbase, vbases=vbases) + end + else + # TODO make math model si units + end + else + Memento.warn(_LOGGER, "Data model '$data_model_type' is not recognized, no per-unit transformation performed") + end +end + + +"finds voltage zones" +function discover_voltage_zones(data_model::Dict{String,<:Any})::Dict{Int,Set{Any}} + @assert data_model["data_model"] in [MATHEMATICAL, ENGINEERING] "unsupported data model" + edge_elements = data_model["data_model"] == MATHEMATICAL ? _math_edge_elements : _eng_edge_elements + + unused_components = Set("$comp_type.$id" for comp_type in edge_elements[edge_elements .!= "transformer"] for id in keys(get(data_model, comp_type, Dict()))) + bus_connectors = Dict([(id,Set()) for id in keys(get(data_model, "bus", Dict()))]) + for comp_type in edge_elements[edge_elements .!= "transformer"] + for (id,obj) in get(data_model, comp_type, Dict()) + f_bus = string(obj["f_bus"]) + t_bus = string(obj["t_bus"]) + push!(bus_connectors[f_bus], ("$comp_type.$id",t_bus)) + push!(bus_connectors[t_bus], ("$comp_type.$id",f_bus)) + end + end + + zones = [] + buses = Set(keys(get(data_model, "bus", Dict()))) + while !isempty(buses) + stack = [pop!(buses)] + zone = Set{Any}() + while !isempty(stack) + bus = pop!(stack) + delete!(buses, bus) + push!(zone, bus) + for (id,bus_to) in bus_connectors[bus] + if id in unused_components && bus_to in buses + delete!(unused_components, id) + push!(stack, bus_to) + end + end + end + append!(zones, [zone]) + end + zones = Dict{Int,Set{Any}}(enumerate(zones)) + + return zones +end + + +"calculates voltage bases for each voltage zone" +function calc_voltage_bases(data_model::Dict{String,<:Any}, vbase_sources::Dict{<:Any,<:Real})::Tuple{Dict,Dict} + # find zones of buses connected by lines + zones = discover_voltage_zones(data_model) + bus_to_zone = Dict([(bus,zone) for (zone, buses) in zones for bus in buses]) + + # assign specified vbase to corresponding zones + zone_vbase = Dict{Int, Union{Missing,Real}}([(zone,missing) for zone in keys(zones)]) + for (bus,vbase) in vbase_sources + if !ismissing(zone_vbase[bus_to_zone[bus]]) + Memento.warn(_LOGGER, "You supplied multiple voltage bases for the same zone; ignoring all but the last one.") + end + zone_vbase[bus_to_zone[bus]] = vbase + end + + # transformers form the edges between these zones + zone_edges = Dict{Int,Vector{Tuple{Int,Real}}}([(zone,[]) for zone in keys(zones)]) + for (_,transformer) in get(data_model, "transformer", Dict{Any,Dict{String,Any}}()) + if data_model["data_model"] == MATHEMATICAL + f_zone = bus_to_zone["$(transformer["f_bus"])"] + t_zone = bus_to_zone["$(transformer["t_bus"])"] + tm_nom = transformer["configuration"]==DELTA ? transformer["tm_nom"]/sqrt(3) : transformer["tm_nom"] + push!(zone_edges[f_zone], (t_zone, 1/tm_nom)) + push!(zone_edges[t_zone], (f_zone, tm_nom)) + else + if haskey(transformer, "f_bus") + f_zone = bus_to_zone["$(transformer["f_bus"])"] + t_zone = bus_to_zone["$(transformer["f_bus"])"] + tm_nom = transformer["configuration"] == DELTA ? transformer["tm_nom"]/sqrt(3) : transformer["tm_nom"] + push!(zone_edges[f_zone], (t_zone, 1/tm_nom)) + push!(zone_edges[t_zone], (f_zone, tm_nom)) + else + nrw = length(transformer["bus"]) + f_zone = bus_to_zone[transformer["bus"][1]] + f_vnom = transformer["configuration"][1] == DELTA ? transformer["vm_nom"][1]/sqrt(3) : transformer["vm_nom"][1] + for w in 2:nrw + t_zone = bus_to_zone[transformer["bus"][w]] + t_vnom = transformer["configuration"][1] == DELTA ? transformer["vm_nom"][w]/sqrt(3) : transformer["vm_nom"][w] + tm_nom = f_vnom / t_vnom + push!(zone_edges[f_zone], (t_zone, 1/tm_nom)) + push!(zone_edges[t_zone], (f_zone, tm_nom)) + end + end + end + end + + # initialize the stack with all specified zones + stack = [zone for (zone,vbase) in zone_vbase if !ismissing(vbase)] + while !isempty(stack) + f_zone = pop!(stack) + for (t_zone, scale) in zone_edges[f_zone] + if ismissing(zone_vbase[t_zone]) + zone_vbase[t_zone] = zone_vbase[f_zone]*scale + push!(stack, t_zone) + end + end + end + + edge_elements = data_model["data_model"] == MATHEMATICAL ? _math_edge_elements : _eng_edge_elements + + bus_vbase = Dict([(bus,zone_vbase[zone]) for (bus,zone) in bus_to_zone]) + edge_vbase = Dict([("$edge_type.$id", bus_vbase["$(obj["f_bus"])"]) for edge_type in edge_elements[edge_elements .!= "transformer"] if haskey(data_model, edge_type) for (id,obj) in data_model[edge_type]]) + return (bus_vbase, edge_vbase) +end + + +"converts to per unit from SI" +function _make_math_per_unit!(nw::Dict{String,<:Any}, data_math::Dict{String,<:Any}; sbase::Union{Real,Missing}=missing, vbases::Union{Dict{String,<:Real},Missing}=missing) + if ismissing(sbase) + if haskey(data_math["settings"], "sbase_default") + sbase = data_math["settings"]["sbase_default"] + else + sbase = 1.0 + end + end + + if haskey(data_math["settings"], "sbase") + sbase_old = data_math["settings"]["sbase"] + else + sbase_old = 1.0 + end + + # automatically find a good vbase if not provided + if ismissing(vbases) + if haskey(data_math["settings"], "vbases_default") + vbases = Dict{String,Real}("$(data_math["bus_lookup"][id])" => vbase for (id, vbase) in data_math["settings"]["vbases_default"]) + else + buses_type_3 = [(id, sum(bus["vm"])/length(bus["vm"])) for (id,bus) in nw["bus"] if haskey(bus, "bus_type") && bus["bus_type"]==3] + if !isempty(buses_type_3) + vbases = Dict([buses_type_3[1]]) + else + Memento.error("Please specify vbases manually; cannot make an educated guess for this data model.") + end + end + end + + bus_vbase, line_vbase = calc_voltage_bases(nw, vbases) + voltage_scale_factor = data_math["settings"]["voltage_scale_factor"] + + for (id, bus) in nw["bus"] + _rebase_pu_bus!(bus, bus_vbase[id], sbase, sbase_old, voltage_scale_factor) + end + + for (id, line) in nw["branch"] + vbase = line_vbase["branch.$id"] + _rebase_pu_branch!(line, vbase, sbase, sbase_old, voltage_scale_factor) + end + + for (id, shunt) in nw["shunt"] + _rebase_pu_shunt!(shunt, bus_vbase[string(shunt["shunt_bus"])], sbase, sbase_old, voltage_scale_factor) + end + + for (id, load) in nw["load"] + _rebase_pu_load!(load, bus_vbase[string(load["load_bus"])], sbase, sbase_old, voltage_scale_factor) + end + + for (id, gen) in nw["gen"] + _rebase_pu_generator!(gen, bus_vbase[string(gen["gen_bus"])], sbase, sbase_old, data_math) + end + + for (id, storage) in nw["storage"] + _rebase_pu_storage!(storage, bus_vbase[string(storage["storage_bus"])], sbase, sbase_old) + end + + for (id, switch) in nw["switch"] + vbase = line_vbase["switch.$id"] + _rebase_pu_switch!(switch, vbase, sbase, sbase_old) + end + + if haskey(nw, "transformer") + for (id, trans) in nw["transformer"] + # voltage base across transformer does not have to be consistent with the ratio! + f_vbase = bus_vbase[string(trans["f_bus"])] + t_vbase = bus_vbase[string(trans["t_bus"])] + _rebase_pu_transformer_2w_ideal!(trans, f_vbase, t_vbase, sbase_old, sbase, voltage_scale_factor) + end + end + + data_math["settings"]["sbase"] = sbase + data_math["per_unit"] = true +end + + +"per-unit conversion for buses" +function _rebase_pu_bus!(bus::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, voltage_scale_factor::Real) + # if not in p.u., these are normalized with respect to vnom + prop_vnom = ["vm", "vmax", "vmin", "vm_set", "vm_ln_min", "vm_ln_max", "vm_lg_min", "vm_lg_max", "vm_ng_min", "vm_ng_max", "vm_ll_min", "vm_ll_max"] + + if !haskey(bus, "vbase") + + # if haskey(bus, "vm_nom") + # vnom = bus["vm_nom"] + # _scale_props!(bus, ["vm_nom"], 1/vbase) + # end + _scale_props!(bus, prop_vnom, 1/vbase) + + z_old = 1.0 + else + vbase_old = bus["vbase"] + _scale_props!(bus, [prop_vnom..., "vm_nom"], vbase_old/vbase) + + z_old = vbase_old^2*sbase_old*voltage_scale_factor + end + + # rebase grounding resistance + z_new = vbase^2/sbase*voltage_scale_factor + z_scale = z_old/z_new + _scale_props!(bus, ["rg", "xg"], z_scale) + + if haskey(bus ,"va") + bus["va"] = deg2rad.(bus["va"]) + end + + # save new vbase + bus["vbase"] = vbase +end + + +"per-unit conversion for branches" +function _rebase_pu_branch!(branch::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, voltage_scale_factor::Real) + if !haskey(branch, "vbase") + z_old = 1 + else + z_old = vbase_old^2/sbase_old*voltage_scale_factor + end + + z_new = vbase^2/sbase*voltage_scale_factor + z_scale = z_old/z_new + y_scale = 1/z_scale + sbase_scale = sbase_old/sbase + + _scale_props!(branch, ["br_r", "br_x"], z_scale) + _scale_props!(branch, ["b_fr", "g_fr", "b_to", "g_to"], y_scale) + _scale_props!(branch, ["c_rating_a", "c_rating_b", "c_rating_c", "rate_a", "rate_b", "rate_c"], sbase_scale) + + branch["angmin"] = deg2rad.(branch["angmin"]) + branch["angmax"] = deg2rad.(branch["angmax"]) + + # save new vbase + branch["vbase"] = vbase +end + + +"per-unit conversion for switches" +function _rebase_pu_switch!(switch::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, voltage_scale_factor::Real) + sbase_scale = sbase / sbase_old + + _scale_props!(switch, ["psw", "qsw", "thermal_rating", "current_rating"], sbase_scale) +end + + +"per-unit conversion for shunts" +function _rebase_pu_shunt!(shunt::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, voltage_scale_factor::Real) + if !haskey(shunt, "vbase") + z_old = 1 + else + z_old = vbase_old^2/sbase_old*voltage_scale_factor + end + + # rebase grounding resistance + z_new = vbase^2/sbase*voltage_scale_factor + + z_scale = z_old/z_new + y_scale = 1/z_scale + _scale(shunt, "gs", y_scale) + _scale(shunt, "bs", y_scale) + + # save new vbase + shunt["vbase"] = vbase +end + + +"per-unit conversion for loads" +function _rebase_pu_load!(load::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, voltage_scale_factor::Real) + if !haskey(load, "vbase") + vbase_old = 1 + sbase_old = 1 + else + vbase_old = load["vbase"] + end + + vbase_old = get(load, "vbase", 1.0) + vbase_scale = vbase_old/vbase + _scale(load, "vnom_kv", vbase_scale) + + sbase_scale = sbase_old/sbase + _scale(load, "pd", sbase_scale) + _scale(load, "qd", sbase_scale) + + # save new vbase + load["vbase"] = vbase +end + + +"per-unit conversion for generators" +function _rebase_pu_generator!(gen::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real, data_math::Dict{String,<:Any}) + vbase_old = get(gen, "vbase", 1.0/data_math["settings"]["voltage_scale_factor"]) + vbase_scale = vbase_old/vbase + sbase_scale = sbase_old/sbase + + for key in ["pg", "qg", "pmin", "qmin", "pmax", "qmax"] + _scale(gen, key, sbase_scale) + end + + for key in ["vg"] + _scale(gen, key, vbase_scale) + end + + # if not in per unit yet, the cost has is in $/MWh + if !haskey(data_math["settings"], "sbase") + sbase_old_cost = 1E6/data_math["settings"]["power_scale_factor"] + sbase_scale_cost = sbase_old_cost/sbase + else + sbase_scale_cost = sbase_scale + end + + _PM._rescale_cost_model!(gen, 1/sbase_scale_cost) + + # save new vbase + gen["vbase"] = vbase +end + + +"per-unit conversion for storage" +function _rebase_pu_storage!(gen::Dict{String,<:Any}, vbase::Real, sbase::Real, sbase_old::Real) + sbase_scale = sbase_old/sbase + + for key in ["energy", "energy_rating", "charge_rating", "discharge_rating", "thermal_rating", "current_rating", "qmin", "qmax", "p_loss", "q_loss"] + _scale(gen, key, sbase_scale) + end +end + + +"per-unit conversion for ideal 2-winding transformers" +function _rebase_pu_transformer_2w_ideal!(transformer::Dict{String,<:Any}, f_vbase_new::Real, t_vbase_new::Real, sbase_old::Real, sbase_new::Real, voltage_scale_factor::Real) + f_vbase_old = get(transformer, "f_vbase", 1.0) + t_vbase_old = get(transformer, "t_vbase", 1.0) + f_vbase_scale = f_vbase_old/f_vbase_new + t_vbase_scale = t_vbase_old/t_vbase_new + + _scale(transformer, "tm_nom", f_vbase_scale/t_vbase_scale) + + # save new vbase + transformer["f_vbase"] = f_vbase_new + transformer["t_vbase"] = t_vbase_new +end + + +"helper function to apply a scale factor to given properties" +function _scale_props!(comp::Dict{String,<:Any}, prop_names::Vector{String}, scale::Real) + for name in prop_names + if haskey(comp, name) + comp[name] *= scale + end + end +end + + +_apply_func_vals(x, f) = isa(x, Dict) ? Dict(k=>f(v) for (k,v) in x) : f.(x) + + +"" +function solution_make_si(solution, math_model; mult_sbase=true, mult_vbase=true, mult_ibase=true, convert_rad2deg=true) + solution_si = deepcopy(solution) + + sbase = math_model["settings"]["sbase"] + + if ismultinetwork(math_model) + for (n,nw) in solution_si["nw"] + for (comp_type, comp_dict) in [(x,y) for (x,y) in nw if isa(y, Dict)] + dimensionalize_math_comp = get(_dimensionalize_math, comp_type, Dict()) + vbase_props = mult_vbase ? get(dimensionalize_math_comp, "vbase", []) : [] + sbase_props = mult_sbase ? get(dimensionalize_math_comp, "sbase", []) : [] + ibase_props = mult_ibase ? get(dimensionalize_math_comp, "ibase", []) : [] + rad2deg_props = convert_rad2deg ? get(dimensionalize_math_comp, "rad2deg", []) : [] + + for (id, comp) in comp_dict + if !isempty(vbase_props) || !isempty(ibase_props) + vbase = math_model["nw"][n][comp_type][id]["vbase"] + ibase = sbase/vbase + end + + for (prop, val) in comp + if prop in vbase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*vbase) + elseif prop in sbase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*sbase) + elseif prop in ibase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*ibase) + elseif prop in rad2deg_props + comp[prop] = _apply_func_vals(comp[prop], x->_wrap_to_180(rad2deg(x))) + end + end + + if comp_type=="transformer" + # transformers have different vbase/ibase on each side + f_bus = math_model["nw"][n]["transformer"][id]["f_bus"] + f_vbase = math_model["nw"][n]["bus"]["$f_bus"]["vbase"] + t_bus = math_model["nw"][n]["transformer"][id]["t_bus"] + t_vbase = math_model["nw"][n]["bus"]["$t_bus"]["vbase"] + f_ibase = sbase/f_vbase + t_ibase = sbase/t_vbase + + for (prop, val) in comp + if prop in dimensionalize_math_comp["ibase_fr"] + comp[prop] = _apply_func_vals(comp[prop], x->x*f_ibase) + elseif prop in dimensionalize_math_comp["ibase_to"] + comp[prop] = _apply_func_vals(comp[prop], x->x*t_ibase) + end + end + end + end + end + end + else + for (comp_type, comp_dict) in [(x,y) for (x,y) in solution_si if isa(y, Dict)] + dimensionalize_math_comp = get(_dimensionalize_math, comp_type, Dict()) + vbase_props = mult_vbase ? get(dimensionalize_math_comp, "vbase", []) : [] + sbase_props = mult_sbase ? get(dimensionalize_math_comp, "sbase", []) : [] + ibase_props = mult_ibase ? get(dimensionalize_math_comp, "ibase", []) : [] + rad2deg_props = convert_rad2deg ? get(dimensionalize_math_comp, "rad2deg", []) : [] + + for (id, comp) in comp_dict + if !isempty(vbase_props) || !isempty(ibase_props) + vbase = math_model[comp_type][id]["vbase"] + ibase = sbase/vbase + end + + for (prop, val) in comp + if prop in vbase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*vbase) + elseif prop in sbase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*sbase) + elseif prop in ibase_props + comp[prop] = _apply_func_vals(comp[prop], x->x*ibase) + elseif prop in rad2deg_props + comp[prop] = _apply_func_vals(comp[prop], x->_wrap_to_180(rad2deg(x))) + end + end + + if comp_type=="transformer" + # transformers have different vbase/ibase on each side + f_bus = math_model["transformer"][id]["f_bus"] + f_vbase = math_model["bus"]["$f_bus"]["vbase"] + t_bus = math_model["transformer"][id]["t_bus"] + t_vbase = math_model["bus"]["$t_bus"]["vbase"] + f_ibase = sbase/f_vbase + t_ibase = sbase/t_vbase + + for (prop, val) in comp + if prop in dimensionalize_math_comp["ibase_fr"] + comp[prop] = _apply_func_vals(comp[prop], x->x*f_ibase) + elseif prop in dimensionalize_math_comp["ibase_to"] + comp[prop] = _apply_func_vals(comp[prop], x->x*t_ibase) + end + end + end + end + end + end + + solution_si["per_unit"] = false + + return solution_si +end diff --git a/src/data_model/utils.jl b/src/data_model/utils.jl new file mode 100644 index 000000000..1318d32e0 --- /dev/null +++ b/src/data_model/utils.jl @@ -0,0 +1,674 @@ +const _pmd_eng_global_keys = Set{String}([ + "settings", "files", "name", "data_model" +]) + + +"initializes the base math object of any type, and copies any one-to-one mappings" +function _init_math_obj(obj_type::String, eng_id::Any, eng_obj::Dict{String,<:Any}, index::Int)::Dict{String,Any} + math_obj = Dict{String,Any}( + "name" => "$eng_id", + ) + + for key in _1to1_maps[obj_type] + if haskey(eng_obj, key) + if key in ["status", "dispatchable"] + math_obj[key] = Int(eng_obj[key]) + else + math_obj[key] = eng_obj[key] + end + end + end + + math_obj["index"] = index + + return math_obj +end + + +"initializes the base components that are expected by powermodelsdistribution in the mathematical model" +function _init_base_components!(data_math::Dict{String,<:Any}) + for key in ["bus", "load", "shunt", "gen", "branch", "switch", "transformer", "storage", "dcline"] + if !haskey(data_math, key) + data_math[key] = Dict{String,Any}() + end + end +end + + +"Initializes the lookup table" +function _init_lookup!(data_math::Dict{String,<:Any}) + for key in keys(_1to1_maps) + if !haskey(data_math["lookup"], key) + data_math["lookup"][key] = Dict{Any,Int}() + end + end +end + + +"function for applying a scale to a paramter" +function _scale(dict::Dict{String,<:Any}, key::String, scale::Real) + if haskey(dict, key) + dict[key] *= scale + end +end + + +"_get_ground helper function" +function _get_new_ground(terminals::Vector{<:Any}) + if isa(terminals, Vector{Int}) + return maximum(terminals)+1 + else + nrs = [parse(Int, x[1]) for x in [match(r"n([1-9]{1}[0-9]*)", string(t)) for t in terminals] if x !== nothing] + new = isempty(nrs) ? 1 : maximum(nrs)+1 + if isa(terminals, Vector{Symbol}) + return Symbol("g$new") + else + return "g$new" + end + end +end + + +"gets the grounding information for a bus" +function _get_ground!(bus::Dict{String,<:Any}) + # find perfect groundings (true ground) + grounded_perfect = [] + for i in 1:length(bus["grounded"]) + if bus["rg"][i]==0 && bus["xg"][i]==0 + push!(grounded_perfect, bus["grounded"][i]) + end + end + + if !isempty(grounded_perfect) + return grounded_perfect[1] + else + g = _get_new_ground(bus["terminals"]) + push!(bus["terminals"], g) + push!(bus["rg"], 0.0) + push!(bus["xg"], 0.0) + return g + end +end + + +""" +Converts a set of short-circuit tests to an equivalent reactance network. +Reference: +R. C. Dugan, “A perspective on transformer modeling for distribution system analysis,” +in 2003 IEEE Power Engineering Society General Meeting (IEEE Cat. No.03CH37491), 2003, vol. 1, pp. 114-119 Vol. 1. +""" +function _sc2br_impedance(Zsc::Dict{Tuple{Int,Int},Complex{Float64}})::Dict{Tuple{Int,Int},Complex} + N = maximum([maximum(k) for k in keys(Zsc)]) + # check whether no keys are missing + # Zsc should contain tupples for upper triangle of NxN + for i in 1:N + for j in i+1:N + if !haskey(Zsc, (i,j)) + if haskey(Zsc, (j,i)) + # Zsc is symmetric; use value of lower triangle if defined + Zsc[(i,j)] = Zsc[(j,i)] + else + Memento.error(_LOGGER, "Short-circuit impedance between winding $i and $j is missing.") + end + end + end + end + + # if all zero, return all zeros + if all(values(Zsc).==0.0) + return Zsc + end + + # make Zb + Zb = zeros(Complex{Float64}, N-1,N-1) + for i in 1:N-1 + Zb[i,i] = Zsc[(1,i+1)] + end + for i in 1:N-1 + for j in 1:i-1 + Zb[i,j] = (Zb[i,i]+Zb[j,j]-Zsc[(j+1,i+1)])/2 + Zb[j,i] = Zb[i,j] + end + end + # get Ybus + Y = LinearAlgebra.pinv(Zb) + Y = [-Y*ones(N-1) Y] + Y = [-ones(1,N-1)*Y; Y] + # extract elements + Zbr = Dict{Tuple{Int,Int},Complex}() + for k in keys(Zsc) + Zbr[k] = (abs(Y[k...])==0) ? Inf : -1/Y[k...] + end + return Zbr +end + + +"loss model builder for transformer decomposition" +function _build_loss_model!(data_math::Dict{String,<:Any}, transformer_name::String, to_map::Vector{String}, r_s::Vector{Float64}, zsc::Dict{Tuple{Int,Int},Complex{Float64}}, ysh::Complex{Float64}; nphases::Int=3, kron_reduced=false)::Vector{Int} + # precompute the minimal set of buses and lines + N = length(r_s) + tr_t_bus = collect(1:N) + buses = Set(1:2*N) + + zbr = _sc2br_impedance(zsc) + + edges = [[[i,i+N] for i in 1:N]..., [[i+N,j+N] for (i,j) in keys(zbr)]...] + lines = Dict(enumerate(edges)) + + z = Dict(enumerate([r_s..., values(zbr)...])) + + shunts = Dict(2=>ysh) + + # remove Inf lines + for (l,edge) in lines + if real(z[l])==Inf || imag(z[l])==Inf + delete!(lines, l) + delete!(z, l) + end + end + + # merge short circuits + stack = Set(keys(lines)) + + while !isempty(stack) + l = pop!(stack) + if z[l] == 0 + (i,j) = lines[l] + + # remove line + delete!(lines, l) + + # remove bus j + delete!(buses, j) + + # update lines + for (k,(edge)) in lines + if edge[1] == j + edge[1] = i + end + if edge[2] == j + edge[2] = i + end + if edge[1]==edge[2] + delete!(lines, k) + delete!(stack, k) + end + end + + # move shunts + if haskey(shunts, j) + if haskey(shunts, i) + shunts[i] += shunts[j] + else + shunts[i] = shunts[j] + end + end + + # update transformer buses + for w in 1:N + if tr_t_bus[w] == j + tr_t_bus[w] = i + end + end + end + end + + bus_ids = Dict{Int,Int}() + for bus in buses + bus_obj = Dict{String,Any}( + "name" => "_virtual_bus.transformer.$(transformer_name)_$(bus)", + "bus_i" => length(data_math["bus"])+1, + "vmin" => fill(0.0, nphases), + "vmax" => fill(Inf, nphases), + "grounded" => fill(false, nphases), + "base_kv" => 1.0, + "bus_type" => 1, + "status" => 1, + "index" => length(data_math["bus"])+1, + ) + + if !kron_reduced + if bus in tr_t_bus + bus_obj["terminals"] = collect(1:nphases+1) + bus_obj["vmin"] = fill(0.0, nphases+1) + bus_obj["vmax"] = fill(Inf, nphases+1) + bus_obj["grounded"] = [fill(false, nphases)..., true] + bus_obj["rg"] = [0.0] + bus_obj["xg"] = [0.0] + else + bus_obj["terminals"] = collect(1:nphases) + bus_obj["vmin"] = fill(0.0, nphases) + bus_obj["vmax"] = fill(Inf, nphases) + end + end + + data_math["bus"]["$(bus_obj["index"])"] = bus_obj + + bus_ids[bus] = bus_obj["bus_i"] + + push!(to_map, "bus.$(bus_obj["index"])") + end + + for (l,(i,j)) in lines + # merge the shunts into the shunts of the pi model of the line + g_fr = b_fr = g_to = b_to = 0 + + if haskey(shunts, i) + g_fr = real(shunts[i]) + b_fr = imag(shunts[i]) + delete!(shunts, i) + end + + if haskey(shunts, j) + g_to = real(shunts[j]) + b_to = imag(shunts[j]) + delete!(shunts, j) + end + + branch_obj = Dict{String,Any}( + "name" => "_virtual_branch.transformer.$(transformer_name)_$(l)", + "source_id" => "_virtual_branch.transformer.$(transformer_name)_$(l)", + "index" => length(data_math["branch"])+1, + "br_status"=>1, + "f_bus"=>bus_ids[i], + "t_bus"=>bus_ids[j], + "f_connections"=>collect(1:nphases), + "t_connections"=>collect(1:nphases), + "br_r" => diagm(0=>fill(real(z[l]), nphases)), + "br_x" => diagm(0=>fill(imag(z[l]), nphases)), + "g_fr" => diagm(0=>fill(g_fr, nphases)), + "b_fr" => diagm(0=>fill(b_fr, nphases)), + "g_to" => diagm(0=>fill(g_to, nphases)), + "b_to" => diagm(0=>fill(b_to, nphases)), + "angmin" => fill(-60.0, nphases), + "angmax" => fill( 60.0, nphases), + "shift" => zeros(nphases), + "tap" => ones(nphases), + "switch" => false, + "transformer" => false, + ) + + data_math["branch"]["$(branch_obj["index"])"] = branch_obj + + push!(to_map, "branch.$(branch_obj["index"])") + end + + return Vector{Int}([bus_ids[bus] for bus in tr_t_bus]) +end + + +"performs kron reduction on branch" +function _kron_reduce_branch!(object::Dict{String,<:Any}, Zs_keys::Vector{String}, Ys_keys::Vector{String}, terminals::Vector{Int}, neutral::Int)::Vector{Int} + Zs = Vector{Matrix}([object[k] for k in Zs_keys]) + Ys = Vector{Matrix}([object[k] for k in Ys_keys]) + Zs_kr, Ys_kr, terminals_kr = _kron_reduce_branch(Zs, Ys, terminals, neutral) + + for (i,k) in enumerate(Zs_keys) + object[k] = Zs_kr[i] + end + + for (i,k) in enumerate(Ys_keys) + object[k] = Ys_kr[i] + end + + return _get_idxs(terminals, terminals_kr) +end + + +"get locations of terminal in connections list" +function _get_ilocs(vec::Vector{<:Any}, loc::Any)::Vector{Int} + return collect(1:length(vec))[vec.==loc] +end + + +"performs kron reduction on branch - helper function" +function _kron_reduce_branch(Zs::Vector{Matrix}, Ys::Vector{Matrix}, terminals::Vector{Int}, neutral::Int)::Tuple{Vector{Matrix}, Vector{Matrix}, Vector{Int}} + Zs_kr = Vector{Matrix}([deepcopy(Z) for Z in Zs]) + Ys_kr = Vector{Matrix}([deepcopy(Y) for Y in Ys]) + terminals_kr = deepcopy(terminals) + + while neutral in terminals_kr + n = _get_ilocs(terminals_kr, neutral)[1] + P = setdiff(collect(1:length(terminals_kr)), n) + + if all(size(Z) == (length(terminals_kr), length(terminals_kr)) for Z in Zs_kr) + Zs_kr = Vector{Matrix}([Z[P,P]-(1/Z[n,n])*Z[P,[n]]*Z[[n],P] for Z in Zs_kr]) + Ys_kr = Vector{Matrix}([Y[P,P] for Y in Ys_kr]) + end + + terminals_kr = terminals_kr[P] + end + + return Zs_kr, Ys_kr, terminals_kr +end + + +"pads properties to have the total number of conductors for the whole system" +function _pad_properties!(object::Dict{<:Any,<:Any}, properties::Vector{String}, connections::Vector{Int}, phases::Vector{Int}; pad_value::Real=0.0) + @assert(all(c in phases for c in connections)) + inds = _get_idxs(phases, connections) + + for property in properties + if haskey(object, property) + if isa(object[property], Vector) + tmp = fill(pad_value, length(phases)) + tmp[inds] = object[property] + object[property] = tmp + elseif isa(object[property], Matrix) + tmp = fill(pad_value, length(phases), length(phases)) + tmp[inds, inds] = object[property] + object[property] = tmp + end + end + end +end + + +"pads properties to have the total number of conductors for the whole system - delta connection variant" +function _pad_properties_delta!(object::Dict{<:Any,<:Any}, properties::Vector{String}, connections::Vector{Int}, phases::Vector{Int}; invert::Bool=false) + @assert(all(c in phases for c in connections)) + @assert(length(connections) in [2, 3], "A delta configuration has to have at least 2 or 3 connections!") + @assert(length(phases)==3, "Padding only possible to a |phases|==3!") + + for property in properties + if haskey(object, property) + val = object[property] + val_length = length(connections)==2 ? 1 : length(connections) + @assert(isa(val, Vector) && length(val)==val_length) + + # build tmp + tmp = Dict() + sign = invert ? -1 : 1 + if val_length==1 + tmp[(connections[1], connections[2])] = val[1] + tmp[(connections[2], connections[1])] = sign*val[1] + else + tmp[(connections[1], connections[2])] = val[1] + tmp[(connections[2], connections[3])] = val[2] + tmp[(connections[3], connections[1])] = val[3] + end + merge!(tmp, Dict((k[2], k[1])=>sign*v for (k,v) in tmp)) + get_val(x,y) = haskey(tmp, (x,y)) ? tmp[(x,y)] : 0.0 + + object[property] = [get_val(phases[1], phases[2]), get_val(phases[2], phases[3]), get_val(phases[3], phases[1])] + end + end +end + + +"Filters out values of a vector or matrix for certain properties" +function _apply_filter!(obj::Dict{String,<:Any}, properties::Vector{String}, filter::Union{Array,BitArray}) + for property in properties + if haskey(obj, property) + if isa(obj[property], Vector) + obj[property] = obj[property][filter] + elseif isa(obj[property], Matrix) + obj[property] = obj[property][filter, filter] + else + Memento.error(_LOGGER, "The property $property is not a Vector or a Matrix!") + end + end + end +end + + +""" +Given a set of addmittances 'y' connected from the conductors 'f_cnds' to the +conductors 't_cnds', this method will return a list of conductors 'cnd' and a +matrix 'Y', which will satisfy I[cnds] = Y*V[cnds]. +""" +function _calc_shunt(f_cnds::Vector{Int}, t_cnds::Vector{Int}, y)::Tuple{Vector{Int}, Matrix{Real}} + #TODO fix y::Type + cnds = unique([f_cnds..., t_cnds...]) + e(f,t) = reshape([c==f ? 1 : c==t ? -1 : 0 for c in cnds], length(cnds), 1) + Y = sum([e(f_cnds[i], t_cnds[i])*y[i]*e(f_cnds[i], t_cnds[i])' for i in 1:length(y)]) + return (cnds, Y) +end + + + +""" +Given a set of terminals 'cnds' with associated shunt addmittance 'Y', this +method will calculate the reduced addmittance matrix if terminal 'ground' is +grounded. +""" +function _calc_ground_shunt_admittance_matrix(cnds::Vector{Int}, Y::Matrix{T}, ground::Int)::Tuple{Vector{Int}, Matrix{T}} where T <: Number + if ground in cnds + cndsr = setdiff(cnds, ground) + cndsr_inds = _get_idxs(cnds, cndsr) + Yr = Y[cndsr_inds, cndsr_inds] + return (cndsr, Yr) + else + return cnds, Y + end +end + + +"initialization actions for unmapping" +function _init_unmap_eng_obj!(data_eng::Dict{<:Any,<:Any}, eng_obj_type::String, map::Dict{String,<:Any})::Dict{String,Any} + if !haskey(data_eng, eng_obj_type) + data_eng[eng_obj_type] = Dict{Any,Any}() + end + + eng_obj = Dict{String,Any}() + + return eng_obj +end + + +"returns component from the mathematical data model" +function _get_math_obj(data_math::Dict{String,<:Any}, to_id::String)::Dict{String,Any} + math_type, math_id = split(to_id, '.') + return haskey(data_math, math_type) && haskey(data_math[math_type], math_id) ? data_math[math_type][math_id] : Dict{String,Any}() +end + + +"convert cost model names" +function _add_gen_cost_model!(math_obj::Dict{String,<:Any}, eng_obj::Dict{String,<:Any}) + math_obj["model"] = get(eng_obj, "cost_pg_model", 2) + math_obj["startup"] = 0.0 + math_obj["shutdown"] = 0.0 + math_obj["cost"] = get(eng_obj, "cost_pg_parameters", [0.0, 1.0, 0.0]) + math_obj["ncost"] = length(math_obj["cost"]) +end + + +"applies a xfmrcode to a transformer in preparation for converting to mathematical model" +function _apply_xfmrcode!(eng_obj::Dict{String,<:Any}, data_eng::Dict{String,<:Any}) + if haskey(eng_obj, "xfmrcode") && haskey(data_eng, "xfmrcode") && haskey(data_eng["xfmrcode"], eng_obj["xfmrcode"]) + xfmrcode = data_eng["xfmrcode"][eng_obj["xfmrcode"]] + + for (k, v) in xfmrcode + if !haskey(eng_obj, k) + eng_obj[k] = v + elseif haskey(eng_obj, k) && k in ["vm_nom", "sm_nom", "tm_set", "rw"] + for (w, vw) in enumerate(eng_obj[k]) + if ismissing(vw) + eng_obj[k][w] = v[w] + end + end + end + end + end +end + + +"applies a linecode to a line in preparation for converting to mathematical model" +function _apply_linecode!(eng_obj::Dict{String,<:Any}, data_eng::Dict{String,<:Any}) + if haskey(eng_obj, "linecode") && haskey(data_eng, "linecode") && haskey(data_eng["linecode"], eng_obj["linecode"]) + linecode = data_eng["linecode"][eng_obj["linecode"]] + + for property in ["rs", "xs", "g_fr", "g_to", "b_fr", "b_to"] + if !haskey(eng_obj, property) && haskey(linecode, property) + eng_obj[property] = linecode[property] + end + end + end +end + + +"" +function _impedance_conversion(data_eng::Dict{String,<:Any}, eng_obj::Dict{String,<:Any}, key::String) + eng_obj[key] .* get(eng_obj, "length", 1.0) +end + + +"" +function _admittance_conversion(data_eng::Dict{String,<:Any}, eng_obj::Dict{String,<:Any}, key::String) + 2.0 .* pi .* data_eng["settings"]["base_frequency"] .* eng_obj[key] .* get(eng_obj, "length", 1.0) ./ 1e9 +end + + +"" +function _bus_type_conversion(data_eng::Dict{String,<:Any}, eng_obj::Dict{String,<:Any}, key::String) + eng_obj[key] == 0 ? 4 : 1 +end + + +"lossy grounding to perfect grounding and shunts" +function _convert_grounding(terminals, grounded, rg, xg) + grouped = Dict(t=>[] for t in unique(grounded)) + for (i,t) in enumerate(grounded) + push!(grouped[t], rg[i]+im*xg[i]) + end + t_lookup = Dict(t=>i for (i,t) in enumerate(terminals)) + grounded_lossless = fill(false, length(terminals)) + shunts = [] + for (t, zgs) in grouped + if any(iszero.(zgs)) + grounded_lossless[t_lookup[t]] = true + else + ygs = 1 ./zgs + yg = sum(ygs) + push!(shunts, ([t], [yg])) + end + end + return grounded_lossless, shunts +end + + +"slices branches based on connected terminals" +function _slice_branches!(data_math::Dict{String,<:Any}) + for (_, branch) in data_math["branch"] + if haskey(branch, "f_connections") + N = length(branch["f_connections"]) + for prop in ["br_r", "br_x", "g_fr", "g_to", "b_fr", "b_to"] + branch[prop] = branch[prop][1:N,1:N] + end + end + end +end + + +"transformations might have introduced buses with four-terminals; crop here" +function _kron_reduce_buses!(data_math) + for (_, bus) in data_math["bus"] + for prop in ["vm", "va", "vmax", "vmin"] + if haskey(bus, prop) && length(bus[prop])>3 + bus[prop] = bus[prop][1:3] + end + end + end +end + + +"generate a new, unique terminal" +new_t(terms) = maximum([terms[isa.(terms, Int)]..., 3])+1 + + +"get a grounded terminal from a bus; if not present, create one" +function _get_ground_math!(bus; exclude_terminals=[]) + tgs = setdiff(bus["terminals"][bus["grounded"]], exclude_terminals) + if !isempty(tgs) + return tgs[1] + else + n = new_t([bus["terminals"]]) + push!(bus["terminals"], n) + push!(bus["grounded"], true) + return n + end +end + + +"initializes a time_series data structure in the InfrastructureModels style" +function _init_time_series!(data::Dict{String,<:Any}, component_type::String, component_id::Any) + if !haskey(data, "time_series") + data["time_series"] = Dict{String,Any}() + end + + if !haskey(data["time_series"], component_type) + data["time_series"][component_type] = Dict{String,Any}() + end + + if !haskey(data["time_series"][component_type], "$component_id") + data["time_series"][component_type]["$component_id"] = Dict{String,Any}() + end +end + + +"Builds a Multinetwork" +function _build_eng_multinetwork(data_eng::Dict{String,<:Any})::Dict{String,Any} + _data_eng = Dict{String,Any}( + "time_series" => Dict{String,Any}( + "step_mismatch" => false + ) + ) + + for (eng_obj_type, eng_objs) in data_eng + if !(eng_obj_type in _pmd_eng_global_keys) && isa(eng_objs, Dict{<:Any,<:Any}) + for (id, eng_obj) in eng_objs + if haskey(eng_obj, "time_series") + _init_time_series!(_data_eng, eng_obj_type, id) + + for (k, v) in eng_obj["time_series"] + time_series = data_eng["time_series"][v] + + if !haskey(_data_eng["time_series"], "num_steps") + _data_eng["time_series"]["time"] = time_series["time"] + _data_eng["time_series"]["num_steps"] = length(time_series["time"]) + end + + if length(time_series["time"]) != _data_eng["time_series"]["num_steps"] + _data_eng["time_series"]["step_mismatch"] = true + end + + if time_series["replace"] + _data_eng["time_series"][eng_obj_type][id][k] = [zeros(size(eng_obj[k])) .+ val for val in time_series["values"]] + else + _data_eng["time_series"][eng_obj_type][id][k] = [eng_obj[k] .* val for val in time_series["values"]] + end + end + end + end + end + end + + data_eng_new = Dict{String,Any}() + ### HACK to get make_multinetwork working + for (eng_obj_type, eng_objs) in data_eng + if isa(eng_objs, Dict{Any,Any}) + eng_objs_new = Dict{String,Any}() + for (k, v) in eng_objs + key_new = "$k" + if key_new != k + Memento.warn(_LOGGER, "$eng_obj_type id $k converted to String") + end + eng_objs_new[key_new] = v + end + data_eng_new[eng_obj_type] = eng_objs_new + else + data_eng_new[eng_obj_type] = eng_objs + end + end + + _pre_mn_data = merge(data_eng_new, _data_eng) + + if _pre_mn_data["time_series"]["step_mismatch"] + Memento.warn(_LOGGER, "There is a mismatch between the num_steps of different time_series, cannot automatically build multinetwork structure") + return _pre_mn_data + else + delete!(_data_eng["time_series"], "step_mismatch") + return _IM.make_multinetwork(_pre_mn_data, _pmd_eng_global_keys) + end +end + diff --git a/src/form/acp.jl b/src/form/acp.jl index 12b0ed672..cec2ed9f0 100644 --- a/src/form/acp.jl +++ b/src/form/acp.jl @@ -1,48 +1,48 @@ "" -function variable_mc_voltage(pm::_PMs.AbstractACPModel; nw=pm.cnw, kwargs...) - variable_mc_voltage_angle(pm; nw=nw, kwargs...) - variable_mc_voltage_magnitude(pm; nw=nw, kwargs...) +function variable_mc_bus_voltage(pm::_PM.AbstractACPModel; nw=pm.cnw, kwargs...) + variable_mc_bus_voltage_angle(pm; nw=nw, kwargs...) + variable_mc_bus_voltage_magnitude_only(pm; nw=nw, kwargs...) # This is needed for delta loads, where division occurs by the difference # of voltage phasors. If the voltage phasors at one bus are initialized # in the same point, this would lead to division by zero. - ncnds = length(_PMs.conductor_ids(pm, nw)) + ncnds = length(conductor_ids(pm, nw)) - bus_t1 = [bus for (_, bus) in _PMs.ref(pm, nw, :bus) if bus["bus_type"]==3] + bus_t1 = [bus for (_, bus) in ref(pm, nw, :bus) if bus["bus_type"]==3] if length(bus_t1)>0 theta = bus_t1[1]["va"] else theta = [_wrap_to_pi(2 * pi / ncnds * (1-c)) for c in 1:ncnds] end vm = 1 - for id in _PMs.ids(pm, nw, :bus) - busref = _PMs.ref(pm, nw, :bus, id) + for id in ids(pm, nw, :bus) + busref = ref(pm, nw, :bus, id) if !haskey(busref, "va_start") # if it has this key, it was set at PM level - JuMP.set_start_value.(_PMs.var(pm, nw, :va, id), theta) + JuMP.set_start_value.(var(pm, nw, :va, id), theta) end end end "" -function variable_mc_bus_voltage_on_off(pm::_PMs.AbstractACPModel; kwargs...) - variable_mc_voltage_angle(pm; kwargs...) - variable_mc_voltage_magnitude_on_off(pm; kwargs...) +function variable_mc_bus_voltage_on_off(pm::_PM.AbstractACPModel; kwargs...) + variable_mc_bus_voltage_angle(pm; kwargs...) + variable_mc_bus_voltage_magnitude_on_off(pm; kwargs...) nw = get(kwargs, :nw, pm.cnw) vm = 1 - for id in _PMs.ids(pm, nw, :bus) - busref = _PMs.ref(pm, nw, :bus, id) - ncnd = length(_PMs.conductor_ids(pm, nw)) + for id in ids(pm, nw, :bus) + busref = ref(pm, nw, :bus, id) + ncnd = length(conductor_ids(pm, nw)) theta = [_wrap_to_pi(2 * pi / ncnd * (1-c)) for c in 1:ncnd] if !haskey(busref, "va_start") # if it has this key, it was set at PM level for c in 1:ncnd - JuMP.set_start_value(_PMs.var(pm, nw, :va, id)[c], theta[c]) + JuMP.set_start_value(var(pm, nw, :va, id)[c], theta[c]) end end end @@ -50,23 +50,23 @@ end "" -function constraint_mc_power_balance_slack(pm::_PMs.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - vm = _PMs.var(pm, nw, :vm, i) - va = _PMs.var(pm, nw, :va, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - p_slack = _PMs.var(pm, nw, :p_slack, i) - q_slack = _PMs.var(pm, nw, :q_slack, i) - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_slack_power_balance(pm::_PM.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + vm = var(pm, nw, :vm, i) + va = var(pm, nw, :va, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + p_slack = var(pm, nw, :p_slack, i) + q_slack = var(pm, nw, :q_slack, i) + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -75,7 +75,7 @@ function constraint_mc_power_balance_slack(pm::_PMs.AbstractACPModel, nw::Int, i cstr_p = [] cstr_q = [] - for c in _PMs.conductor_ids(pm; nw=nw) + for c in conductor_ids(pm; nw=nw) cp = JuMP.@NLconstraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) @@ -114,37 +114,43 @@ function constraint_mc_power_balance_slack(pm::_PMs.AbstractACPModel, nw::Int, i end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance_shed(pm::_PMs.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - vm = _PMs.var(pm, nw, :vm, i) - va = _PMs.var(pm, nw, :va, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - z_demand = _PMs.var(pm, nw, :z_demand) - z_shunt = _PMs.var(pm, nw, :z_shunt) +function constraint_mc_shed_power_balance(pm::_PM.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + vm = var(pm, nw, :vm, i) + va = var(pm, nw, :va, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + z_demand = var(pm, nw, :z_demand) + z_shunt = var(pm, nw, :z_shunt) + + cnds = conductor_ids(pm; nw=nw) + ncnds = length(cnds) + + Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) + Bt = isempty(bus_bs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_bs)) cstr_p = [] cstr_q = [] bus_GsBs = [(n,bus_gs[n], bus_bs[n]) for n in keys(bus_gs)] - for c in _PMs.conductor_ids(pm; nw=nw) - cp = JuMP.@constraint(pm.model, + for c in conductor_ids(pm; nw=nw) + cp = JuMP.@NLconstraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) + sum(pt[a_trans][c] for a_trans in bus_arcs_trans) @@ -161,7 +167,7 @@ function constraint_mc_power_balance_shed(pm::_PMs.AbstractACPModel, nw::Int, i: ) push!(cstr_p, cp) - cq = JuMP.@constraint(pm.model, + cq = JuMP.@NLconstraint(pm.model, sum(q[a][c] for a in bus_arcs) + sum(qsw[a_sw][c] for a_sw in bus_arcs_sw) + sum(qt[a_trans][c] for a_trans in bus_arcs_trans) @@ -180,30 +186,30 @@ function constraint_mc_power_balance_shed(pm::_PMs.AbstractACPModel, nw::Int, i: end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance(pm::_PMs.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - vm = _PMs.var(pm, nw, :vm, i) - va = _PMs.var(pm, nw, :va, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_power_balance(pm::_PM.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + vm = var(pm, nw, :vm, i) + va = var(pm, nw, :va, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -248,31 +254,31 @@ function constraint_mc_power_balance(pm::_PMs.AbstractACPModel, nw::Int, i::Int, push!(cstr_q, cq) end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance_load(pm::_PMs.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) - vm = _PMs.var(pm, nw, :vm, i) - va = _PMs.var(pm, nw, :va, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg_bus, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg_bus, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - pd = get(_PMs.var(pm, nw), :pd_bus, Dict()); _PMs._check_var_keys(pd, bus_loads, "active power", "load") - qd = get(_PMs.var(pm, nw), :qd_bus, Dict()); _PMs._check_var_keys(pd, bus_loads, "reactive power", "load") - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_load_power_balance(pm::_PM.AbstractACPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + vm = var(pm, nw, :vm, i) + va = var(pm, nw, :va, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg_bus, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg_bus, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + pd = get(var(pm, nw), :pd_bus, Dict()); _PM._check_var_keys(pd, bus_loads, "active power", "load") + qd = get(var(pm, nw), :qd_bus, Dict()); _PM._check_var_keys(pd, bus_loads, "reactive power", "load") + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -281,7 +287,7 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractACPModel, nw::Int, i: cstr_p = [] cstr_q = [] - for c in _PMs.conductor_ids(pm; nw=nw) + for c in conductor_ids(pm; nw=nw) cp = JuMP.@NLconstraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) @@ -317,9 +323,9 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractACPModel, nw::Int, i: push!(cstr_q, cq) end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end @@ -330,33 +336,33 @@ Creates Ohms constraints (yt post fix indicates that Y and T values are in recta ``` p_fr == g[c,c] * vm_fr[c]^2 + sum( g[c,d]*vm_fr[c]*vm_fr[d]*cos(va_fr[c]-va_fr[d]) + - b[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in _PMs.conductor_ids(pm) if d != c) + + b[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in conductor_ids(pm) if d != c) + sum(-g[c,d]*vm_fr[c]*vm_to[d]*cos(va_fr[c]-va_to[d]) + - -b[c,d]*vm_fr[c]*vm_to[d]*sin(va_fr[c]-va_to[d]) for d in _PMs.conductor_ids(pm)) + -b[c,d]*vm_fr[c]*vm_to[d]*sin(va_fr[c]-va_to[d]) for d in conductor_ids(pm)) + g_fr[c,c] * vm_fr[c]^2 + sum( g_fr[c,d]*vm_fr[c]*vm_fr[d]*cos(va_fr[c]-va_fr[d]) + - b_fr[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in _PMs.conductor_ids(pm) if d != c) + b_fr[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in conductor_ids(pm) if d != c) ) q_fr == -b[c,c] *vm_fr[c]^2 - sum( b[c,d]*vm_fr[c]*vm_fr[d]*cos(va_fr[c]-va_fr[d]) - - g[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in _PMs.conductor_ids(pm) if d != c) - + g[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in conductor_ids(pm) if d != c) - sum(-b[c,d]*vm_fr[c]*vm_to[d]*cos(va_fr[c]-va_to[d]) + - g[c,d]*vm_fr[c]*vm_to[d]*sin(va_fr[c]-va_to[d]) for d in _PMs.conductor_ids(pm)) + g[c,d]*vm_fr[c]*vm_to[d]*sin(va_fr[c]-va_to[d]) for d in conductor_ids(pm)) -b_fr[c,c] *vm_fr[c]^2 - sum( b_fr[c,d]*vm_fr[c]*vm_fr[d]*cos(va_fr[c]-va_fr[d]) - - g_fr[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in _PMs.conductor_ids(pm) if d != c) + g_fr[c,d]*vm_fr[c]*vm_fr[d]*sin(va_fr[c]-va_fr[d]) for d in conductor_ids(pm) if d != c) ) ``` """ -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractACPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) - p_fr = _PMs.var(pm, n, :p, f_idx) - q_fr = _PMs.var(pm, n, :q, f_idx) - vm_fr = _PMs.var(pm, n, :vm, f_bus) - vm_to = _PMs.var(pm, n, :vm, t_bus) - va_fr = _PMs.var(pm, n, :va, f_bus) - va_to = _PMs.var(pm, n, :va, t_bus) - - cnds = _PMs.conductor_ids(pm; nw=n) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractACPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) + p_fr = var(pm, n, :p, f_idx) + q_fr = var(pm, n, :q, f_idx) + vm_fr = var(pm, n, :vm, f_bus) + vm_to = var(pm, n, :vm, t_bus) + va_fr = var(pm, n, :va, f_bus) + va_to = var(pm, n, :va, t_bus) + + cnds = conductor_ids(pm; nw=n) for c in cnds JuMP.@NLconstraint(pm.model, p_fr[c] == (g[c,c]+g_fr[c,c])*vm_fr[c]^2 @@ -387,22 +393,23 @@ p[t_idx] == (g+g_to)*v[t_bus]^2 + (-g*tr-b*ti)/tm*(v[t_bus]*v[f_bus]*cos(t[t_bu q[t_idx] == -(b+b_to)*v[t_bus]^2 - (-b*tr+g*ti)/tm*(v[t_bus]*v[f_bus]*cos(t[f_bus]-t[t_bus])) + (-g*tr-b*ti)/tm*(v[t_bus]*v[f_bus]*sin(t[t_bus]-t[f_bus])) ``` """ -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractACPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractACPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) constraint_mc_ohms_yt_from(pm, n, t_bus, f_bus, t_idx, f_idx, g, b, g_to, b_to, tr, ti, tm) end -function constraint_mc_trans_yy(pm::_PMs.AbstractACPModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - vm_fr = _PMs.var(pm, nw, :vm, f_bus)[f_cnd] - vm_to = _PMs.var(pm, nw, :vm, t_bus)[t_cnd] - va_fr = _PMs.var(pm, nw, :va, f_bus)[f_cnd] - va_to = _PMs.var(pm, nw, :va, t_bus)[t_cnd] +"" +function constraint_mc_transformer_power_yy(pm::_PM.AbstractACPModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + vm_fr = var(pm, nw, :vm, f_bus)[f_cnd] + vm_to = var(pm, nw, :vm, t_bus)[t_cnd] + va_fr = var(pm, nw, :va, f_bus)[f_cnd] + va_to = var(pm, nw, :va, t_bus)[t_cnd] # construct tm as a parameter or scaled variable depending on whether it is fixed or not - tm = [tm_fixed[p] ? tm_set[p] : _PMs.var(pm, nw, :tap, trans_id)[p] for p in _PMs.conductor_ids(pm)] + tm = [tm_fixed[p] ? tm_set[p] : var(pm, nw, :tap, trans_id)[p] for p in conductor_ids(pm)] - for p in _PMs.conductor_ids(pm) + for p in conductor_ids(pm) if tm_fixed[p] JuMP.@constraint(pm.model, vm_fr[p] == tm_scale*tm[p]*vm_to[p]) else @@ -412,27 +419,28 @@ function constraint_mc_trans_yy(pm::_PMs.AbstractACPModel, nw::Int, trans_id::In JuMP.@constraint(pm.model, va_fr[p] == va_to[p] + pol_angle) end - p_fr = _PMs.var(pm, nw, :pt, f_idx)[f_cnd] - p_to = _PMs.var(pm, nw, :pt, t_idx)[t_cnd] - q_fr = _PMs.var(pm, nw, :qt, f_idx)[f_cnd] - q_to = _PMs.var(pm, nw, :qt, t_idx)[t_cnd] + p_fr = var(pm, nw, :pt, f_idx)[f_cnd] + p_to = var(pm, nw, :pt, t_idx)[t_cnd] + q_fr = var(pm, nw, :qt, f_idx)[f_cnd] + q_to = var(pm, nw, :qt, t_idx)[t_cnd] JuMP.@constraint(pm.model, p_fr + p_to .== 0) JuMP.@constraint(pm.model, q_fr + q_to .== 0) end -function constraint_mc_trans_dy(pm::_PMs.AbstractACPModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - vm_fr = [_PMs.var(pm, nw, :vm, f_bus)[p] for p in f_cnd] - vm_to = [_PMs.var(pm, nw, :vm, t_bus)[p] for p in t_cnd] - va_fr = [_PMs.var(pm, nw, :va, f_bus)[p] for p in f_cnd] - va_to = [_PMs.var(pm, nw, :va, t_bus)[p] for p in t_cnd] +"" +function constraint_mc_transformer_power_dy(pm::_PM.AbstractACPModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + vm_fr = [var(pm, nw, :vm, f_bus)[p] for p in f_cnd] + vm_to = [var(pm, nw, :vm, t_bus)[p] for p in t_cnd] + va_fr = [var(pm, nw, :va, f_bus)[p] for p in f_cnd] + va_to = [var(pm, nw, :va, t_bus)[p] for p in t_cnd] # construct tm as a parameter or scaled variable depending on whether it is fixed or not - tm = [tm_fixed[p] ? tm_set[p] : _PMs.var(pm, nw, p, :tap, trans_id) for p in _PMs.conductor_ids(pm)] + tm = [tm_fixed[p] ? tm_set[p] : var(pm, nw, p, :tap, trans_id) for p in conductor_ids(pm)] # introduce auxialiary variable vd = Md*v_fr - nph = length(_PMs.conductor_ids(pm)) + nph = length(conductor_ids(pm)) vd_re = Array{Any,1}(undef, nph) vd_im = Array{Any,1}(undef, nph) for p in 1:nph @@ -445,10 +453,10 @@ function constraint_mc_trans_dy(pm::_PMs.AbstractACPModel, nw::Int, trans_id::In JuMP.@NLconstraint(pm.model, vd_im[p] == pol*tm_scale*tm[p]*vm_to[p]*sin(va_to[p])) end - p_fr = [_PMs.var(pm, nw, :pt, f_idx)[p] for p in f_cnd] - p_to = [_PMs.var(pm, nw, :pt, t_idx)[p] for p in t_cnd] - q_fr = [_PMs.var(pm, nw, :qt, f_idx)[p] for p in f_cnd] - q_to = [_PMs.var(pm, nw, :qt, t_idx)[p] for p in t_cnd] + p_fr = [var(pm, nw, :pt, f_idx)[p] for p in f_cnd] + p_to = [var(pm, nw, :pt, t_idx)[p] for p in t_cnd] + q_fr = [var(pm, nw, :qt, f_idx)[p] for p in f_cnd] + q_to = [var(pm, nw, :qt, t_idx)[p] for p in t_cnd] id_re = Array{Any,1}(undef, nph) id_im = Array{Any,1}(undef, nph) @@ -456,12 +464,12 @@ function constraint_mc_trans_dy(pm::_PMs.AbstractACPModel, nw::Int, trans_id::In # = (p+jq)/|v|*(cos(va)-j*sin(va)) # Re(s/v) = (p*cos(va)+q*sin(va))/|v| # -Im(s/v) = -(q*cos(va)-p*sin(va))/|v| - for p in _PMs.conductor_ids(pm) + for p in conductor_ids(pm) # id = conj(s_to/v_to)./tm id_re[p] = JuMP.@NLexpression(pm.model, (p_to[p]*cos(va_to[p])+q_to[p]*sin(va_to[p]))/vm_to[p]/(tm_scale*tm[p])/pol) id_im[p] = JuMP.@NLexpression(pm.model, -(q_to[p]*cos(va_to[p])-p_to[p]*sin(va_to[p]))/vm_to[p]/(tm_scale*tm[p])/pol) end - for p in _PMs.conductor_ids(pm) + for p in conductor_ids(pm) # rotate by nph-1 to get 'previous' phase # e.g., for nph=3: 1->3, 2->1, 3->2 q = (p-1+nph-1)%nph+1 @@ -488,13 +496,13 @@ vuf = |U-|/|U+| |U-| <= vufmax*|U+| |U-|^2 <= vufmax^2*|U+|^2 """ -function constraint_mc_vm_vuf(pm::_PMs.AbstractACPModel, nw::Int, bus_id::Int, vufmax::Float64) - if !haskey(_PMs.var(pm, pm.cnw), :vmpossqr) - _PMs.var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() - _PMs.var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() +function constraint_mc_bus_voltage_magnitude_vuf(pm::_PM.AbstractACPModel, nw::Int, bus_id::Int, vufmax::Float64) + if !haskey(var(pm, pm.cnw), :vmpossqr) + var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() + var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() end - (vm_a, vm_b, vm_c) = [_PMs.var(pm, nw, i, :vm, bus_id) for i in 1:3] - (va_a, va_b, va_c) = [_PMs.var(pm, nw, i, :va, bus_id) for i in 1:3] + (vm_a, vm_b, vm_c) = [var(pm, nw, i, :vm, bus_id) for i in 1:3] + (va_a, va_b, va_c) = [var(pm, nw, i, :va, bus_id) for i in 1:3] a = exp(im*2*pi/3) # real and imag functions cannot be used in NLexpressions, so precalculate are = real(a) @@ -535,13 +543,13 @@ vuf = |U-|/|U+| |U-| <= vufmax*|U+| |U-|^2 <= vufmax^2*|U+|^2 """ -function constraint_mc_vm_neg_seq(pm::_PMs.AbstractACPModel, nw::Int, bus_id::Int, vmnegmax::Float64) - if !haskey(_PMs.var(pm, pm.cnw), :vmpossqr) - _PMs.var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() - _PMs.var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() +function constraint_mc_bus_voltage_magnitude_negative_sequence(pm::_PM.AbstractACPModel, nw::Int, bus_id::Int, vmnegmax::Float64) + if !haskey(var(pm, pm.cnw), :vmpossqr) + var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() + var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() end - (vm_a, vm_b, vm_c) = [_PMs.var(pm, nw, i, :vm, bus_id) for i in 1:3] - (va_a, va_b, va_c) = [_PMs.var(pm, nw, i, :va, bus_id) for i in 1:3] + (vm_a, vm_b, vm_c) = [var(pm, nw, i, :vm, bus_id) for i in 1:3] + (va_a, va_b, va_c) = [var(pm, nw, i, :va, bus_id) for i in 1:3] a = exp(im*2*pi/3) # real and imag functions cannot be used in NLexpressions, so precalculate are = real(a) @@ -562,15 +570,6 @@ function constraint_mc_vm_neg_seq(pm::_PMs.AbstractACPModel, nw::Int, bus_id::In end -#= TODO unused function, remove? -"Links the power flowing into both windings of a variable tap transformer." -function constraint_mc_transformer_flow_var(pm::_PMs.AbstractPowerModel, i::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, Ti_fr, Ti_im; nw::Int=pm.cnw) - # for ac formulation, indentical to fixed tap - constraint_mc_transformer_flow(pm, i, f_bus, t_bus, f_idx, t_idx, Ti_fr, Ti_im) -end -=# - - """ a = exp(im*2π/3) U+ = (1*Ua + a*Ub a^2*Uc)/3 @@ -579,13 +578,13 @@ vuf = |U-|/|U+| |U-| <= vufmax*|U+| |U-|^2 <= vufmax^2*|U+|^2 """ -function constraint_mc_vm_pos_seq(pm::_PMs.AbstractACPModel, nw::Int, bus_id::Int, vmposmax::Float64) - if !haskey(_PMs.var(pm, pm.cnw), :vmpossqr) - _PMs.var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() - _PMs.var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() +function constraint_mc_bus_voltage_magnitude_positive_sequence(pm::_PM.AbstractACPModel, nw::Int, bus_id::Int, vmposmax::Float64) + if !haskey(var(pm, pm.cnw), :vmpossqr) + var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() + var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() end - (vm_a, vm_b, vm_c) = [_PMs.var(pm, nw, i, :vm, bus_id) for i in 1:3] - (va_a, va_b, va_c) = [_PMs.var(pm, nw, i, :va, bus_id) for i in 1:3] + (vm_a, vm_b, vm_c) = [var(pm, nw, i, :vm, bus_id) for i in 1:3] + (va_a, va_b, va_c) = [var(pm, nw, i, :va, bus_id) for i in 1:3] a = exp(im*2*pi/3) # real and imag functions cannot be used in NLexpressions, so precalculate are = real(a) @@ -614,13 +613,13 @@ vuf = |U-|/|U+| |U-| <= vufmax*|U+| |U-|^2 <= vufmax^2*|U+|^2 """ -function constraint_mc_vm_zero_seq(pm::_PMs.AbstractACPModel, nw::Int, bus_id::Int, vmzeromax::Float64) - if !haskey(_PMs.var(pm, pm.cnw), :vmpossqr) - _PMs.var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() - _PMs.var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() +function constraint_mc_bus_voltage_magnitude_zero_sequence(pm::_PM.AbstractACPModel, nw::Int, bus_id::Int, vmzeromax::Float64) + if !haskey(var(pm, pm.cnw), :vmpossqr) + var(pm, pm.cnw)[:vmpossqr] = Dict{Int, Any}() + var(pm, pm.cnw)[:vmnegsqr] = Dict{Int, Any}() end - (vm_a, vm_b, vm_c) = [_PMs.var(pm, nw, i, :vm, bus_id) for i in 1:3] - (va_a, va_b, va_c) = [_PMs.var(pm, nw, i, :va, bus_id) for i in 1:3] + (vm_a, vm_b, vm_c) = [var(pm, nw, i, :vm, bus_id) for i in 1:3] + (va_a, va_b, va_c) = [var(pm, nw, i, :va, bus_id) for i in 1:3] # real and imaginary components of U+ vrezero = JuMP.@NLexpression(pm.model, (vm_a*cos(va_a) + vm_b*cos(va_b) + vm_c*cos(va_c))/3 @@ -644,11 +643,11 @@ And then s_a = v_a.conj(i_a) = v_a.conj(i_ab-i_ca) idem for s_b and s_c """ -function constraint_mc_load_current_delta(pm::_PMs.AbstractACPModel, nw::Int, load_id::Int, load_bus_id::Int, cp::Vector, cq::Vector) +function constraint_mc_load_current_delta(pm::_PM.AbstractACPModel, nw::Int, load_id::Int, load_bus_id::Int, cp::Vector, cq::Vector) cp_ab, cp_bc, cp_ca = cp cq_ab, cq_bc, cq_ca = cq - vm_a, vm_b, vm_c = _PMs.var(pm, nw, :vm, load_bus_id) - va_a, va_b, va_c = _PMs.var(pm, nw, :va, load_bus_id) + vm_a, vm_b, vm_c = var(pm, nw, :vm, load_bus_id) + va_a, va_b, va_c = var(pm, nw, :va, load_bus_id) # v_xy = v_x - v_y vre_xy(vm_x, va_x, vm_y, va_y) = JuMP.@NLexpression(pm.model, vm_x*cos(va_x)-vm_y*cos(va_y)) vim_xy(vm_x, va_x, vm_y, va_y) = JuMP.@NLexpression(pm.model, vm_x*sin(va_x)-vm_y*sin(va_y)) @@ -673,11 +672,11 @@ function constraint_mc_load_current_delta(pm::_PMs.AbstractACPModel, nw::Int, lo p_x(vm_x, va_x, ire_xy, iim_xy, ire_zx, iim_zx) = JuMP.@NLexpression(pm.model, vm_x*cos(va_x)*(ire_xy-ire_zx) + vm_x*sin(va_x)*(iim_xy-iim_zx)) q_x(vm_x, va_x, ire_xy, iim_xy, ire_zx, iim_zx) = JuMP.@NLexpression(pm.model, vm_x*sin(va_x)*(ire_xy-ire_zx) - vm_x*cos(va_x)*(iim_xy-iim_zx)) # s_x = s_x,ref - _PMs.var(pm, nw, :pd_bus)[load_id] = [ + var(pm, nw, :pd_bus)[load_id] = [ p_x(vm_a, va_a, ire_ab, iim_ab, ire_ca, iim_ca), p_x(vm_b, va_b, ire_bc, iim_bc, ire_ab, iim_ab), p_x(vm_c, va_c, ire_ca, iim_ca, ire_bc, iim_bc)] - _PMs.var(pm, nw, :qd_bus)[load_id] = [ + var(pm, nw, :qd_bus)[load_id] = [ q_x(vm_a, va_a, ire_ab, iim_ab, ire_ca, iim_ca), q_x(vm_b, va_b, ire_bc, iim_bc, ire_ab, iim_ab), q_x(vm_c, va_c, ire_ca, iim_ca, ire_bc, iim_bc)] @@ -685,10 +684,10 @@ end "" -function constraint_mc_vm_ll(pm::_PMs.AbstractACPModel, nw::Int, bus_id::Int, vm_ll_min::Vector, vm_ll_max::Vector) +function constraint_mc_bus_voltage_magnitude_ll(pm::_PM.AbstractACPModel, nw::Int, bus_id::Int, vm_ll_min::Vector, vm_ll_max::Vector) # 3 conductors asserted in template already - vm_ln = [_PMs.var(pm, nw, i, :vm, bus_id) for i in 1:3] - va_ln = [_PMs.var(pm, nw, i, :va, bus_id) for i in 1:3] + vm_ln = [var(pm, nw, i, :vm, bus_id) for i in 1:3] + va_ln = [var(pm, nw, i, :va, bus_id) for i in 1:3] vr_ll = JuMP.@NLexpression(pm.model, [i in 1:3], vm_ln[i]*cos(va_ln[i]) - vm_ln[i%3+1]*cos(va_ln[i%3+1]) ) @@ -709,21 +708,23 @@ end "bus voltage on/off constraint for load shed problem" -function constraint_mc_bus_voltage_on_off(pm::_PMs.AbstractACPModel; nw::Int=pm.cnw, kwargs...) - for (i,bus) in _PMs.ref(pm, nw, :bus) - constraint_mc_voltage_magnitude_on_off(pm, i; nw=nw) +function constraint_mc_bus_voltage_on_off(pm::_PM.AbstractACPModel; nw::Int=pm.cnw, kwargs...) + for (i,bus) in ref(pm, nw, :bus) + constraint_mc_bus_voltage_magnitude_on_off(pm, i; nw=nw) end end + "`vm[i] == vmref`" -function constraint_mc_voltage_magnitude_setpoint(pm::_PMs.AbstractACPModel, n::Int, i::Int, vmref) - vm = _PMs.var(pm, n, :vm, i) +function constraint_mc_voltage_magnitude_only(pm::_PM.AbstractACPModel, n::Int, i::Int, vmref) + vm = var(pm, n, :vm, i) JuMP.@constraint(pm.model, vm .== vmref) end + "" -function constraint_mc_storage_current_limit(pm::_PMs.AbstractACPModel, n::Int, i, bus, rating) +function constraint_mc_storage_current_limit(pm::_PM.AbstractACPModel, n::Int, i, bus, rating) vm = var(pm, n, :vm, bus) ps = var(pm, n, :ps, i) qs = var(pm, n, :qs, i) @@ -733,9 +734,9 @@ end "" -function constraint_mc_load_wye(pm::_PMs.AbstractACPModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vm = _PMs.var(pm, nw, :vm, bus_id) - va = _PMs.var(pm, nw, :va, bus_id) +function constraint_mc_load_setpoint_wye(pm::_PM.AbstractACPModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vm = var(pm, nw, :vm, bus_id) + va = var(pm, nw, :va, bus_id) nph = 3 @@ -757,25 +758,25 @@ function constraint_mc_load_wye(pm::_PMs.AbstractACPModel, nw::Int, id::Int, bus qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vm[i]*cos(va[i])*cid[i]+vm[i]*sin(va[i])*crd[i]) end - _PMs.var(pm, nw, :pd_bus)[id] = pd_bus - _PMs.var(pm, nw, :qd_bus)[id] = qd_bus + var(pm, nw, :pd_bus)[id] = pd_bus + var(pm, nw, :qd_bus)[id] = qd_bus if report - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*vm[i]^alpha[i] ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*vm[i]^beta[i] ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end "" -function constraint_mc_load_delta(pm::_PMs.AbstractACPModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vm = _PMs.var(pm, nw, :vm, bus_id) - va = _PMs.var(pm, nw, :va, bus_id) +function constraint_mc_load_setpoint_delta(pm::_PM.AbstractACPModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vm = var(pm, nw, :vm, bus_id) + va = var(pm, nw, :va, bus_id) nph = 3 prev = Dict(i=>(i+nph-2)%nph+1 for i in 1:nph) @@ -799,27 +800,27 @@ function constraint_mc_load_delta(pm::_PMs.AbstractACPModel, nw::Int, id::Int, b pd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vm[i]*cos(va[i])*crd_bus[i]+vm[i]*sin(va[i])*cid_bus[i]) qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vm[i]*cos(va[i])*cid_bus[i]+vm[i]*sin(va[i])*crd_bus[i]) - _PMs.var(pm, nw, :pd_bus)[id] = pd_bus - _PMs.var(pm, nw, :qd_bus)[id] = qd_bus + var(pm, nw, :pd_bus)[id] = pd_bus + var(pm, nw, :qd_bus)[id] = qd_bus if report - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*(vrd[i]^2+vid[i]^2)^(alpha[i]/2) ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*(vrd[i]^2+vid[i]^2)^(beta[i]/2) ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end "" -function constraint_mc_generation_delta(pm::_PMs.AbstractACPModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) - vm = _PMs.var(pm, nw, :vm, bus_id) - va = _PMs.var(pm, nw, :va, bus_id) - pg = _PMs.var(pm, nw, :pg, id) - qg = _PMs.var(pm, nw, :qg, id) +function constraint_mc_gen_setpoint_delta(pm::_PM.AbstractACPModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + vm = var(pm, nw, :vm, bus_id) + va = var(pm, nw, :va, bus_id) + pg = var(pm, nw, :pg, id) + qg = var(pm, nw, :qg, id) crg = [] cig = [] @@ -840,11 +841,11 @@ function constraint_mc_generation_delta(pm::_PMs.AbstractACPModel, nw::Int, id:: pg_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vm[i]*cos(va[i])*crg_bus[i]+vm[i]*sin(va[i])*cig_bus[i]) qg_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vm[i]*cos(va[i])*cig_bus[i]+vm[i]*sin(va[i])*crg_bus[i]) - _PMs.var(pm, nw, :pg_bus)[id] = pg_bus - _PMs.var(pm, nw, :qg_bus)[id] = qg_bus + var(pm, nw, :pg_bus)[id] = pg_bus + var(pm, nw, :qg_bus)[id] = qg_bus if report - _PMs.sol(pm, nw, :gen, id)[:pg_bus] = pg_bus - _PMs.sol(pm, nw, :gen, id)[:qg_bus] = qg_bus + sol(pm, nw, :gen, id)[:pg_bus] = pg_bus + sol(pm, nw, :gen, id)[:qg_bus] = qg_bus end end diff --git a/src/form/acr.jl b/src/form/acr.jl index 58b933dd9..14e54b3d8 100644 --- a/src/form/acr.jl +++ b/src/form/acr.jl @@ -1,32 +1,32 @@ "" -function variable_mc_voltage(pm::_PMs.AbstractACRModel; nw=pm.cnw, bounded::Bool=true, kwargs...) - variable_mc_voltage_real(pm; nw=nw, bounded=bounded, kwargs...) - variable_mc_voltage_imaginary(pm; nw=nw, bounded=bounded, kwargs...) +function variable_mc_bus_voltage(pm::_PM.AbstractACRModel; nw=pm.cnw, bounded::Bool=true, kwargs...) + variable_mc_bus_voltage_real(pm; nw=nw, bounded=bounded, kwargs...) + variable_mc_bus_voltage_imaginary(pm; nw=nw, bounded=bounded, kwargs...) # local infeasbility issues without proper initialization; # convergence issues start when the equivalent angles of the starting point # are further away than 90 degrees from the solution (as given by ACP) - # this is the default behaviour of _PMs, initialize all phases as (1,0) + # this is the default behaviour of _PM, initialize all phases as (1,0) # the magnitude seems to have little effect on the convergence (>0.05) # updating the starting point to a balanced phasor does the job - ncnd = length(_PMs.conductor_ids(pm)) + ncnd = length(conductor_ids(pm)) theta = [_wrap_to_pi(2 * pi / ncnd * (1-c)) for c in 1:ncnd] vm = 1 - for id in _PMs.ids(pm, nw, :bus) - busref = _PMs.ref(pm, nw, :bus, id) + for id in ids(pm, nw, :bus) + busref = ref(pm, nw, :bus, id) if !haskey(busref, "va_start") for c in 1:ncnd vr = vm*cos(theta[c]) vi = vm*sin(theta[c]) - JuMP.set_start_value(_PMs.var(pm, nw, :vr, id)[c], vr) - JuMP.set_start_value(_PMs.var(pm, nw, :vi, id)[c], vi) + JuMP.set_start_value(var(pm, nw, :vr, id)[c], vr) + JuMP.set_start_value(var(pm, nw, :vi, id)[c], vi) end end end # apply bounds if bounded if bounded - for i in _PMs.ids(pm, nw, :bus) + for i in ids(pm, nw, :bus) constraint_mc_voltage_magnitude_bounds(pm, i, nw=nw) end end @@ -34,10 +34,10 @@ end "`vmin <= vm[i] <= vmax`" -function constraint_mc_voltage_magnitude_bounds(pm::_PMs.AbstractACRModel, n::Int, i, vmin, vmax) +function constraint_mc_voltage_magnitude_bounds(pm::_PM.AbstractACRModel, n::Int, i, vmin, vmax) @assert all(vmin .<= vmax) - vr = _PMs.var(pm, n, :vr, i) - vi = _PMs.var(pm, n, :vi, i) + vr = var(pm, n, :vr, i) + vi = var(pm, n, :vi, i) for t in 1:length(vr) JuMP.@constraint(pm.model, vmin[t]^2 .<= vr[t]^2 + vi[t]^2) @@ -49,10 +49,10 @@ end "Creates phase angle constraints at reference buses" -function constraint_mc_theta_ref(pm::_PMs.AbstractACRModel, n::Int, d::Int, va_ref) - vr = _PMs.var(pm, n, :vr, d) - vi = _PMs.var(pm, n, :vi, d) - cnds = _PMs.conductor_ids(pm; nw=n) +function constraint_mc_theta_ref(pm::_PM.AbstractACRModel, n::Int, d::Int, va_ref) + vr = var(pm, n, :vr, d) + vi = var(pm, n, :vi, d) + cnds = conductor_ids(pm; nw=n) # deal with cases first where tan(theta)==Inf or tan(theta)==0 for c in cnds @@ -80,43 +80,44 @@ function constraint_mc_theta_ref(pm::_PMs.AbstractACRModel, n::Int, d::Int, va_r end end -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractACRModel, n::Int, f_idx, angmin, angmax) + +"" +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractACRModel, n::Int, f_idx, angmin, angmax) i, f_bus, t_bus = f_idx - vr_fr = _PMs.var(pm, n, :vr, f_bus) - vi_fr = _PMs.var(pm, n, :vi, f_bus) - vr_to = _PMs.var(pm, n, :vr, t_bus) - vi_to = _PMs.var(pm, n, :vi, t_bus) + vr_fr = var(pm, n, :vr, f_bus) + vi_fr = var(pm, n, :vi, f_bus) + vr_to = var(pm, n, :vr, t_bus) + vi_to = var(pm, n, :vi, t_bus) - for c in _PMs.conductor_ids(pm; nw=n) + for c in conductor_ids(pm; nw=n) JuMP.@constraint(pm.model, (vi_fr[c]*vr_to[c] - vr_fr[c]*vi_to[c]) <= tan(angmax[c])*(vr_fr[c]*vr_to[c] + vi_fr[c]*vi_to[c])) JuMP.@constraint(pm.model, (vi_fr[c]*vr_to[c] - vr_fr[c]*vi_to[c]) >= tan(angmin[c])*(vr_fr[c]*vr_to[c] + vi_fr[c]*vi_to[c])) end end - "" -function constraint_mc_power_balance_slack(pm::_PMs.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - vr = _PMs.var(pm, nw, :vr, i) - vi = _PMs.var(pm, nw, :vi, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - p_slack = _PMs.var(pm, nw, :p_slack, i) - q_slack = _PMs.var(pm, nw, :q_slack, i) +function constraint_mc_slack_power_balance(pm::_PM.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + vr = var(pm, nw, :vr, i) + vi = var(pm, nw, :vi, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + p_slack = var(pm, nw, :p_slack, i) + q_slack = var(pm, nw, :q_slack, i) cstr_p = [] cstr_q = [] - for c in _PMs.conductor_ids(pm; nw=nw) + for c in conductor_ids(pm; nw=nw) cp = JuMP.@constraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) @@ -144,29 +145,29 @@ function constraint_mc_power_balance_slack(pm::_PMs.AbstractACRModel, nw::Int, i push!(cstr_q, cq) end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance(pm::_PMs.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - vr = _PMs.var(pm, nw, :vr, i) - vi = _PMs.var(pm, nw, :vi, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_power_balance(pm::_PM.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + vr = var(pm, nw, :vr, i) + vi = var(pm, nw, :vi, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -196,31 +197,31 @@ function constraint_mc_power_balance(pm::_PMs.AbstractACRModel, nw::Int, i::Int, - (-vr.*(Gt*vi+Bt*vr) + vi.*(Gt*vr-Bt*vi)) ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance_load(pm::_PMs.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) - vr = _PMs.var(pm, nw, :vr, i) - vi = _PMs.var(pm, nw, :vi, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg_bus, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg_bus, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - pd = get(_PMs.var(pm, nw), :pd_bus, Dict()); _PMs._check_var_keys(pd, bus_loads, "active power", "load") - qd = get(_PMs.var(pm, nw), :qd_bus, Dict()); _PMs._check_var_keys(pd, bus_loads, "reactive power", "load") - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_load_power_balance(pm::_PM.AbstractACRModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + vr = var(pm, nw, :vr, i) + vi = var(pm, nw, :vi, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg_bus, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg_bus, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + pd = get(var(pm, nw), :pd_bus, Dict()); _PM._check_var_keys(pd, bus_loads, "active power", "load") + qd = get(var(pm, nw), :qd_bus, Dict()); _PM._check_var_keys(pd, bus_loads, "reactive power", "load") + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -230,7 +231,7 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractACRModel, nw::Int, i: cstr_q = [] # pd/qd can be NLexpressions, so cannot be vectorized - for c in _PMs.conductor_ids(pm; nw=nw) + for c in conductor_ids(pm; nw=nw) cp = JuMP.@NLconstraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) @@ -262,9 +263,9 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractACRModel, nw::Int, i: push!(cstr_q, cq) end - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end @@ -276,15 +277,15 @@ s_fr = v_fr.*conj(Y*(v_fr-v_to)) s_fr = (vr_fr+im*vi_fr).*(G-im*B)*([vr_fr-vr_to]-im*[vi_fr-vi_to]) s_fr = (vr_fr+im*vi_fr).*([G*vr_fr-G*vr_to-B*vi_fr+B*vi_to]-im*[G*vi_fr-G*vi_to+B*vr_fr-B*vr_to]) """ -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractACRModel, n::Int, f_bus, t_bus, f_idx, t_idx, G, B, G_fr, B_fr, tr, ti, tm) - p_fr = _PMs.var(pm, n, :p, f_idx) - q_fr = _PMs.var(pm, n, :q, f_idx) - vr_fr = _PMs.var(pm, n, :vr, f_bus) - vr_to = _PMs.var(pm, n, :vr, t_bus) - vi_fr = _PMs.var(pm, n, :vi, f_bus) - vi_to = _PMs.var(pm, n, :vi, t_bus) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractACRModel, n::Int, f_bus, t_bus, f_idx, t_idx, G, B, G_fr, B_fr, tr, ti, tm) + p_fr = var(pm, n, :p, f_idx) + q_fr = var(pm, n, :q, f_idx) + vr_fr = var(pm, n, :vr, f_bus) + vr_to = var(pm, n, :vr, t_bus) + vi_fr = var(pm, n, :vi, f_bus) + vi_to = var(pm, n, :vi, t_bus) - cnds = _PMs.conductor_ids(pm; nw=n) + cnds = conductor_ids(pm; nw=n) JuMP.@constraint(pm.model, p_fr .== vr_fr.*(G*vr_fr-G*vr_to-B*vi_fr+B*vi_to) @@ -338,15 +339,15 @@ p[t_idx] == (g+g_to)*v[t_bus]^2 + (-g*tr-b*ti)/tm*(v[t_bus]*v[f_bus]*cos(t[t_bu q[t_idx] == -(b+b_to)*v[t_bus]^2 - (-b*tr+g*ti)/tm*(v[t_bus]*v[f_bus]*cos(t[f_bus]-t[t_bus])) + (-g*tr-b*ti)/tm*(v[t_bus]*v[f_bus]*sin(t[t_bus]-t[f_bus])) ``` """ -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractACRModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractACRModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) constraint_mc_ohms_yt_from(pm, n, t_bus, f_bus, t_idx, f_idx, g, b, g_to, b_to, tr, ti, tm) end "" -function constraint_mc_load_wye(pm::_PMs.AbstractACRModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) +function constraint_mc_load_setpoint_wye(pm::_PM.AbstractACRModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) nph = 3 @@ -368,25 +369,25 @@ function constraint_mc_load_wye(pm::_PMs.AbstractACRModel, nw::Int, id::Int, bus qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vr[i]*cid[i]+vi[i]*crd[i]) end - _PMs.var(pm, nw, :pd_bus)[id] = pd_bus - _PMs.var(pm, nw, :qd_bus)[id] = qd_bus + var(pm, nw, :pd_bus)[id] = pd_bus + var(pm, nw, :qd_bus)[id] = qd_bus if report - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*(vr[i]^2+vi[i]^2)^(alpha[i]/2) ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*(vr[i]^2+vi[i]^2)^(beta[i]/2) ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end "" -function constraint_mc_load_delta(pm::_PMs.AbstractACRModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) +function constraint_mc_load_setpoint_delta(pm::_PM.AbstractACRModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) nph = 3 prev = Dict(i=>(i+nph-2)%nph+1 for i in 1:nph) @@ -410,36 +411,36 @@ function constraint_mc_load_delta(pm::_PMs.AbstractACRModel, nw::Int, id::Int, b pd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vr[i]*crd_bus[i]+vi[i]*cid_bus[i]) qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vr[i]*cid_bus[i]+vi[i]*crd_bus[i]) - _PMs.var(pm, nw, :pd_bus)[id] = pd_bus - _PMs.var(pm, nw, :qd_bus)[id] = qd_bus + var(pm, nw, :pd_bus)[id] = pd_bus + var(pm, nw, :qd_bus)[id] = qd_bus if report - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*(vrd[i]^2+vid[i]^2)^(alpha[i]/2) ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*(vrd[i]^2+vid[i]^2)^(beta[i]/2) ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end "`vm[i] == vmref`" -function constraint_mc_voltage_magnitude_setpoint(pm::_PMs.AbstractACRModel, n::Int, i::Int, vmref) - vr = _PMs.var(pm, n, :vr, i) - vi = _PMs.var(pm, n, :vi, i) +function constraint_mc_voltage_magnitude_only(pm::_PM.AbstractACRModel, n::Int, i::Int, vmref) + vr = var(pm, n, :vr, i) + vi = var(pm, n, :vi, i) JuMP.@constraint(pm.model, vr.^2 + vi.^2 .== vmref.^2) end "" -function constraint_mc_generation_delta(pm::_PMs.AbstractACRModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) - pg = _PMs.var(pm, nw, :pg, id) - qg = _PMs.var(pm, nw, :qg, id) +function constraint_mc_gen_setpoint_delta(pm::_PM.AbstractACRModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) + pg = var(pm, nw, :pg, id) + qg = var(pm, nw, :qg, id) crg = [] cig = [] @@ -460,11 +461,11 @@ function constraint_mc_generation_delta(pm::_PMs.AbstractACRModel, nw::Int, id:: pg_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vr[i]*crg_bus[i]+vi[i]*cig_bus[i]) qg_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vr[i]*cig_bus[i]+vi[i]*crg_bus[i]) - _PMs.var(pm, nw, :pg_bus)[id] = pg_bus - _PMs.var(pm, nw, :qg_bus)[id] = qg_bus + var(pm, nw, :pg_bus)[id] = pg_bus + var(pm, nw, :qg_bus)[id] = qg_bus if report - _PMs.sol(pm, nw, :gen, id)[:pg_bus] = pg_bus - _PMs.sol(pm, nw, :gen, id)[:qg_bus] = qg_bus + sol(pm, nw, :gen, id)[:pg_bus] = pg_bus + sol(pm, nw, :gen, id)[:qg_bus] = qg_bus end end diff --git a/src/form/apo.jl b/src/form/apo.jl index f3c10c334..b23fa6b65 100644 --- a/src/form/apo.jl +++ b/src/form/apo.jl @@ -1,79 +1,79 @@ -### generic features that apply to all active-power-only (apo) approximations import LinearAlgebra: diag + "apo models ignore reactive power flows" -function variable_mc_generation_reactive(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_gen_power_setpoint_imaginary(pm::_PM.AbstractActivePowerModel; kwargs...) end "apo models ignore reactive power flows" -function variable_mc_reactive_generation_on_off(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_gen_power_setpoint_imaginary_on_off(pm::_PM.AbstractActivePowerModel; kwargs...) end "on/off constraint for generators" -function constraint_mc_generation_on_off(pm::_PMs.AbstractActivePowerModel, n::Int, i::Int, pmin, pmax, qmin, qmax) - pg = _PMs.var(pm, n, :pg, i) - z = _PMs.var(pm, n, :z_gen, i) +function constraint_mc_gen_power_on_off(pm::_PM.AbstractActivePowerModel, n::Int, i::Int, pmin, pmax, qmin, qmax) + pg = var(pm, n, :pg, i) + z = var(pm, n, :z_gen, i) + + for c in conductor_ids(pm, n) + if isfinite(pmax[c]) + JuMP.@constraint(pm.model, pg[c] .<= pmax[c].*z) + end - JuMP.@constraint(pm.model, pg .<= pmax.*z) - JuMP.@constraint(pm.model, pg .>= pmin.*z) + if isfinite(pmin[c]) + JuMP.@constraint(pm.model, pg[c] .>= pmin[c].*z) + end + end end "apo models ignore reactive power flows" -function variable_mc_storage_reactive(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_storage_power_imaginary(pm::_PM.AbstractActivePowerModel; kwargs...) end "apo models ignore reactive power flows" -function variable_mc_on_off_storage_reactive(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_storage_power_imaginary_on_off(pm::_PM.AbstractActivePowerModel; kwargs...) end "apo models ignore reactive power flows" -function variable_mc_branch_flow_reactive(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_branch_power_imaginary(pm::_PM.AbstractActivePowerModel; kwargs...) end "apo models ignore reactive power flows" -function variable_mc_branch_flow_ne_reactive(pm::_PMs.AbstractActivePowerModel; kwargs...) +function variable_mc_branch_flow_ne_reactive(pm::_PM.AbstractActivePowerModel; kwargs...) end -# "do nothing, apo models do not have reactive variables" -# function constraint_mc_gen_setpoint_reactive(pm::_PMs.AbstractActivePowerModel, n::Int, c::Int, i, qg) -# end - - "nothing to do, these models do not have complex voltage variables" -function variable_mc_voltage(pm::_PMs.AbstractNFAModel; nw=pm.cnw, kwargs...) +function variable_mc_bus_voltage(pm::_PM.AbstractNFAModel; nw=pm.cnw, kwargs...) end "nothing to do, these models do not have angle difference constraints" -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractNFAModel, n::Int, f_idx, angmin, angmax) +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractNFAModel, n::Int, f_idx, angmin, angmax) end - - "apo models ignore reactive power flows" -function variable_mc_transformer_flow_reactive(pm::_PMs.AbstractActivePowerModel; nw::Int=pm.cnw, bounded=true) +function variable_mc_transformer_power_imaginary(pm::_PM.AbstractActivePowerModel; nw::Int=pm.cnw, bounded=true) end "power balanace constraint with line shunts and transformers, active power only" -function constraint_mc_power_balance_load(pm::_PMs.AbstractActivePowerModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - pg = get(_PMs.var(pm, nw), :pg_bus, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - pd = get(_PMs.var(pm, nw), :pd_bus, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") +function constraint_mc_load_power_balance(pm::_PM.AbstractActivePowerModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + pg = get(var(pm, nw), :pg_bus, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + pd = get(var(pm, nw), :pd_bus, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") cstr_p = [] - for c in _PMs.conductor_ids(pm; nw=nw) + for c in conductor_ids(pm; nw=nw) cp = JuMP.@constraint(pm.model, sum(p[a][c] for a in bus_arcs) + sum(psw[a_sw][c] for a_sw in bus_arcs_sw) @@ -87,40 +87,41 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractActivePowerModel, nw: push!(cstr_p, cp) end # omit reactive constraint - cnds = _PMs.conductor_ids(pm, nw) + cnds = conductor_ids(pm, nw) ncnds = length(cnds) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = [NaN for i in 1:ncnds] + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = [NaN for i in 1:ncnds] end end ######## Lossless Models ######## + "Create variables for the active power flowing into all transformer windings" -function variable_mc_transformer_flow_active(pm::_PMs.AbstractAPLossLessModels; nw::Int=pm.cnw, bounded=true) - ncnds = length(_PMs.conductor_ids(pm)) +function variable_mc_transformer_power_real(pm::_PM.AbstractAPLossLessModels; nw::Int=pm.cnw, bounded=true) + ncnds = length(conductor_ids(pm)) - pt = _PMs.var(pm, nw)[:pt] = Dict((l,i,j) => JuMP.@variable(pm.model, + pt = var(pm, nw)[:pt] = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_pt_$((l,i,j))", start=0 - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans) + ) for (l,i,j) in ref(pm, nw, :arcs_from_trans) ) if bounded - for arc in _PMs.ref(pm, nw, :arcs_from_trans) + for arc in ref(pm, nw, :arcs_from_trans) (t,i,j) = arc - rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(_PMs.ref(pm, nw, :transformer, t), _PMs.ref(pm, nw, :bus, i), _PMs.ref(pm, nw, :bus, j)) + rate_a_fr, rate_a_to = _calc_transformer_power_ub_frto(ref(pm, nw, :transformer, t), ref(pm, nw, :bus, i), ref(pm, nw, :bus, j)) set_lower_bound.(pt[(t,i,j)], -min.(rate_a_fr, rate_a_to)) set_upper_bound.(pt[(t,i,j)], min.(rate_a_fr, rate_a_to)) end end - for cnd in _PMs.conductor_ids(pm) + for cnd in conductor_ids(pm) #TODO what does this dfo, and p does not seem to be defined! - for (l,branch) in _PMs.ref(pm, nw, :branch) + for (l,branch) in ref(pm, nw, :branch) if haskey(branch, "pf_start") f_idx = (l, branch["f_bus"], branch["t_bus"]) JuMP.set_value(p[f_idx], branch["pf_start"]) @@ -130,131 +131,145 @@ function variable_mc_transformer_flow_active(pm::_PMs.AbstractAPLossLessModels; end # this explicit type erasure is necessary - p_expr = Dict{Any,Any}( ((l,i,j), pt[(l,i,j)]) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans) ) - p_expr = merge(p_expr, Dict( ((l,j,i), -1.0*pt[(l,i,j)]) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans))) - _PMs.var(pm, nw)[:pt] = p_expr + p_expr = Dict{Any,Any}( ((l,i,j), pt[(l,i,j)]) for (l,i,j) in ref(pm, nw, :arcs_from_trans) ) + p_expr = merge(p_expr, Dict( ((l,j,i), -1.0*pt[(l,i,j)]) for (l,i,j) in ref(pm, nw, :arcs_from_trans))) + var(pm, nw)[:pt] = p_expr +end + + +"" +function constraint_mc_network_power_balance(pm::_PM.AbstractAPLossLessModels, n::Int, i, comp_gen_ids, comp_pd, comp_qd, comp_gs, comp_bs, comp_branch_g, comp_branch_b) + pg = var(pm, n, :pg) + + for c in conductor_ids(pm) + JuMP.@constraint(pm.model, sum(pg[g][c] for g in comp_gen_ids) == sum(pd[c] for (i,pd) in values(comp_pd)) + sum(gs[c]*1.0^2 for (i,gs) in values(comp_gs))) + # omit reactive constraint + end end "Do nothing, this model is symmetric" -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractAPLossLessModels, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractAPLossLessModels, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) end ### Network Flow Approximation ### + "nothing to do, no voltage angle variables" -function constraint_mc_theta_ref(pm::_PMs.AbstractNFAModel, n::Int, d::Int, va_ref) +function constraint_mc_theta_ref(pm::_PM.AbstractNFAModel, n::Int, d::Int, va_ref) end "nothing to do, no voltage angle variables" -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractNFAModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractNFAModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) end "nothing to do, this model is symmetric" -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractNFAModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractNFAModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) end "nothing to do, this model is symmetric" -function constraint_mc_trans(pm::_PMs.AbstractNFAModel, i::Int; nw::Int=pm.cnw) +function constraint_mc_transformer_power(pm::_PM.AbstractNFAModel, i::Int; nw::Int=pm.cnw) end -## From PowerModels "`-rate_a <= p[f_idx] <= rate_a`" -function constraint_mc_thermal_limit_from(pm::_PMs.AbstractActivePowerModel, n::Int, f_idx, rate_a) - cnds = _PMs.conductor_ids(pm, n) +function constraint_mc_thermal_limit_from(pm::_PM.AbstractActivePowerModel, n::Int, f_idx, rate_a) + cnds = conductor_ids(pm, n) ncnds = length(cnds) mu_sm_fr = [] for c in 1:ncnds - p_fr =_PMs.var(pm, n, :p, f_idx)[c] + p_fr =var(pm, n, :p, f_idx)[c] if isa(p_fr, JuMP.VariableRef) && JuMP.has_lower_bound(p_fr) push!(mu_sm_fr,JuMP.LowerBoundRef(p_fr)) - JuMP.lower_bound(p_fr) < -rate_a[c] && JuMP.set_lower_bound(p_fr, -rate_a[c]) + JuMP.lower_bound(p_fr) < -rate_a[c] && set_lower_bound(p_fr, -rate_a[c]) if JuMP.has_upper_bound(p_fr) - JuMP.upper_bound(p_fr) > rate_a[c] && JuMP.set_upper_bound(p_fr, rate_a[c]) + JuMP.upper_bound(p_fr) > rate_a[c] && set_upper_bound(p_fr, rate_a[c]) end else push!(mu_sm_fr, JuMP.@constraint(pm.model, p_fr <= rate_a[c])) end end - if _PMs.report_duals(pm) - _PMs.sol(pm, n, :branch, f_idx[1])[:mu_sm_fr] = mu_sm_fr + if _IM.report_duals(pm) + sol(pm, n, :branch, f_idx[1])[:mu_sm_fr] = mu_sm_fr end end + "" -function constraint_mc_thermal_limit_to(pm::_PMs.AbstractActivePowerModel, n::Int, t_idx, rate_a) - cnds = _PMs.conductor_ids(pm, n) +function constraint_mc_thermal_limit_to(pm::_PM.AbstractActivePowerModel, n::Int, t_idx, rate_a) + cnds = conductor_ids(pm, n) ncnds = length(cnds) mu_sm_to = [] for c in 1:ncnds - p_to =_PMs.var(pm, n, :p, t_idx)[c] + p_to =var(pm, n, :p, t_idx)[c] if isa(p_to, JuMP.VariableRef) && JuMP.has_lower_bound(p_to) push!(mu_sm_to, JuMP.LowerBoundRef(p_to)) - JuMP.lower_bound(p_to) < -rate_a[c] && JuMP.set_lower_bound(p_to, -rate_a[c]) + JuMP.lower_bound(p_to) < -rate_a[c] && set_lower_bound(p_to, -rate_a[c]) if JuMP.has_upper_bound(p_to) - JuMP.upper_bound(p_to) > rate_a[c] && JuMP.set_upper_bound(p_to, rate_a[c]) + JuMP.upper_bound(p_to) > rate_a[c] && set_upper_bound(p_to, rate_a[c]) end else push!(mu_sm_to, JuMP.@constraint(pm.model, p_to <= rate_a[c])) end end - if _PMs.report_duals(pm) - _PMs.sol(pm, n, :branch, t_idx[1])[:mu_sm_to] = mu_sm_to + if _IM.report_duals(pm) + sol(pm, n, :branch, t_idx[1])[:mu_sm_to] = mu_sm_to end end "" -function constraint_mc_current_limit(pm::_PMs.AbstractActivePowerModel, n::Int, f_idx, c_rating_a) - p_fr =_PMs.var(pm, n, :p, f_idx) +function constraint_mc_current_limit(pm::_PM.AbstractActivePowerModel, n::Int, f_idx, c_rating_a) + p_fr =var(pm, n, :p, f_idx) - cnds = _PMs.conductor_ids(pm, n) + cnds = conductor_ids(pm, n) ncnds = length(cnds) for c in 1:ncnds - JuMP.lower_bound(p_fr[c]) < -c_rating_a[c] && JuMP.set_lower_bound(p_fr[c], -c_rating_a[c]) - JuMP.upper_bound(p_fr[c]) > c_rating_a[c] && JuMP.set_upper_bound(p_fr[c], c_rating_a[c]) + JuMP.lower_bound(p_fr[c]) < -c_rating_a[c] && set_lower_bound(p_fr[c], -c_rating_a[c]) + JuMP.upper_bound(p_fr[c]) > c_rating_a[c] && set_upper_bound(p_fr[c], c_rating_a[c]) end end "" -function constraint_mc_thermal_limit_from_on_off(pm::_PMs.AbstractActivePowerModel, n::Int, i, f_idx, rate_a) - p_fr =_PMs.var(pm, n, :p, f_idx) - z =_PMs.var(pm, n, :z_branch, i) +function constraint_mc_thermal_limit_from_on_off(pm::_PM.AbstractActivePowerModel, n::Int, i, f_idx, rate_a) + p_fr =var(pm, n, :p, f_idx) + z =var(pm, n, :z_branch, i) JuMP.@constraint(pm.model, p_fr .<= rate_a.*z) JuMP.@constraint(pm.model, p_fr .>= -rate_a.*z) end "" -function constraint_mc_thermal_limit_to_on_off(pm::_PMs.AbstractActivePowerModel, n::Int, i, t_idx, rate_a) - p_to =_PMs.var(pm, n, :p, t_idx) - z =_PMs.var(pm, n, :z_branch, i) +function constraint_mc_thermal_limit_to_on_off(pm::_PM.AbstractActivePowerModel, n::Int, i, t_idx, rate_a) + p_to =var(pm, n, :p, t_idx) + z =var(pm, n, :z_branch, i) JuMP.@constraint(pm.model, p_to .<= rate_a.*z) JuMP.@constraint(pm.model, p_to .>= -rate_a.*z) end + "" -function constraint_mc_thermal_limit_from_ne(pm::_PMs.AbstractActivePowerModel, n::Int, i, f_idx, rate_a) - p_fr =_PMs.var(pm, n, :p_ne, f_idx) - z =_PMs.var(pm, n, :branch_ne, i) +function constraint_mc_thermal_limit_from_ne(pm::_PM.AbstractActivePowerModel, n::Int, i, f_idx, rate_a) + p_fr =var(pm, n, :p_ne, f_idx) + z =var(pm, n, :branch_ne, i) JuMP.@constraint(pm.model, p_fr .<= rate_a.*z) JuMP.@constraint(pm.model, p_fr .>= -rate_a.*z) end + "" -function constraint_mc_thermal_limit_to_ne(pm::_PMs.AbstractActivePowerModel, n::Int, i, t_idx, rate_a) - p_to =_PMs.var(pm, n, :p_ne, t_idx) - z =_PMs.var(pm, n, :branch_ne, i) +function constraint_mc_thermal_limit_to_ne(pm::_PM.AbstractActivePowerModel, n::Int, i, t_idx, rate_a) + p_to =var(pm, n, :p_ne, t_idx) + z =var(pm, n, :branch_ne, i) JuMP.@constraint(pm.model, p_to .<= rate_a.*z) JuMP.@constraint(pm.model, p_to .>= -rate_a.*z) @@ -262,50 +277,52 @@ end "" -function constraint_mc_switch_thermal_limit(pm::_PMs.AbstractActivePowerModel, n::Int, f_idx, rating) - psw =_PMs.var(pm, n, :psw, f_idx) +function constraint_mc_switch_thermal_limit(pm::_PM.AbstractActivePowerModel, n::Int, f_idx, rating) + psw =var(pm, n, :psw, f_idx) - cnds = _PMs.conductor_ids(pm, n) + cnds = conductor_ids(pm, n) ncnds = length(cnds) for c in 1:ncnds - JuMP.lower_bound(psw[c]) < -rating[c] && JuMP.set_lower_bound(psw[c], -rating[c]) - JuMP.upper_bound(psw[c]) > rating[c] && JuMP.set_upper_bound(psw[c], rating[c]) + JuMP.lower_bound(psw[c]) < -rating[c] && set_lower_bound(psw[c], -rating[c]) + JuMP.upper_bound(psw[c]) > rating[c] && set_upper_bound(psw[c], rating[c]) end end "" -function constraint_mc_storage_thermal_limit(pm::_PMs.AbstractActivePowerModel, n::Int, i, rating) - ps =_PMs.var(pm, n, :ps, i) +function constraint_mc_storage_thermal_limit(pm::_PM.AbstractActivePowerModel, n::Int, i, rating) + ps =var(pm, n, :ps, i) - cnds = _PMs.conductor_ids(pm, n) + cnds = conductor_ids(pm, n) ncnds = length(cnds) for c in 1:ncnds - JuMP.lower_bound(ps[c]) < -rating[c] && JuMP.set_lower_bound(ps[c], -rating[c]) - JuMP.upper_bound(ps[c]) > rating[c] && JuMP.set_upper_bound(ps[c], rating[c]) + JuMP.lower_bound(ps[c]) < -rating[c] && set_lower_bound(ps[c], -rating[c]) + JuMP.upper_bound(ps[c]) > rating[c] && set_upper_bound(ps[c], rating[c]) end end + "" -function constraint_mc_storage_current_limit(pm::_PMs.AbstractActivePowerModel, n::Int, i, bus, rating) - ps =_PMs.var(pm, n, :ps, i) +function constraint_mc_storage_current_limit(pm::_PM.AbstractActivePowerModel, n::Int, i, bus, rating) + ps =var(pm, n, :ps, i) - cnds = _PMs.conductor_ids(pm, n) + cnds = conductor_ids(pm, n) ncnds = length(cnds) for c in 1:ncnds - JuMP.lower_bound(ps[c]) < -rating[c] && JuMP.set_lower_bound(ps[c], -rating[c]) - JuMP.upper_bound(ps[c]) > rating[c] && JuMP.set_upper_bound(ps[c], rating[c]) + JuMP.lower_bound(ps[c]) < -rating[c] && set_lower_bound(ps[c], -rating[c]) + JuMP.upper_bound(ps[c]) > rating[c] && set_upper_bound(ps[c], rating[c]) end end + "" -function constraint_mc_storage_loss(pm::_PMs.AbstractActivePowerModel, n::Int, i, bus, conductors, r, x, p_loss, q_loss) - ps = _PMs.var(pm, n, :ps, i) - sc = _PMs.var(pm, n, :sc, i) - sd = _PMs.var(pm, n, :sd, i) +function constraint_mc_storage_losses(pm::_PM.AbstractActivePowerModel, n::Int, i, bus, conductors, r, x, p_loss, q_loss) + ps = var(pm, n, :ps, i) + sc = var(pm, n, :sc, i) + sd = var(pm, n, :sd, i) JuMP.@constraint(pm.model, sum(ps[c] for c in conductors) + (sd - sc) @@ -314,45 +331,37 @@ function constraint_mc_storage_loss(pm::_PMs.AbstractActivePowerModel, n::Int, i ) end -function constraint_mc_storage_on_off(pm::_PMs.AbstractActivePowerModel, n::Int, i, pmin, pmax, qmin, qmax, charge_ub, discharge_ub) - z_storage =_PMs.var(pm, n, :z_storage, i) - ps =_PMs.var(pm, n, :ps, i) +"" +function constraint_mc_storage_on_off(pm::_PM.AbstractActivePowerModel, n::Int, i, pmin, pmax, qmin, qmax, charge_ub, discharge_ub) + + z_storage =var(pm, n, :z_storage, i) + ps =var(pm, n, :ps, i) JuMP.@constraint(pm.model, ps .<= pmax.*z_storage) JuMP.@constraint(pm.model, ps .>= pmin.*z_storage) end -# -# "" -# function add_setpoint_switch_flow!(sol, pm::_PMs.AbstractActivePowerModel) -# add_setpoint!(sol, pm, "switch", "psw", :psw, var_key = (idx,item) -> (idx, item["f_bus"], item["t_bus"])) -# add_setpoint_fixed!(sol, pm, "switch", "qsw") -# end - - -""" -Only support wye-connected generators. -""" -function constraint_mc_generation(pm::_PMs.AbstractActivePowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) - _PMs.var(pm, nw, :pg_bus)[id] = _PMs.var(pm, nw, :pg, id) +"Only support wye-connected generators." +function constraint_mc_gen_setpoint(pm::_PM.AbstractActivePowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) + var(pm, nw, :pg_bus)[id] = var(pm, nw, :pg, id) end "Only support wye-connected, constant-power loads." -function constraint_mc_load(pm::_PMs.AbstractActivePowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) - load = _PMs.ref(pm, nw, :load, id) +function constraint_mc_load_setpoint(pm::_PM.AbstractActivePowerModel, id::Int; nw::Int=pm.cnw, report::Bool=true) + load = ref(pm, nw, :load, id) pd = load["pd"] - _PMs.var(pm, nw, :pd)[id] = pd - _PMs.var(pm, nw, :pd_bus)[id] = _PMs.var(pm, nw, :pd, id) + var(pm, nw, :pd)[id] = pd + var(pm, nw, :pd_bus)[id] = var(pm, nw, :pd, id) if report - _PMs.sol(pm, nw, :load, id)[:pd] = _PMs.var(pm, nw, :pd, id) - _PMs.sol(pm, nw, :load, id)[:pd_bus] = _PMs.var(pm, nw, :pd_bus, id) + sol(pm, nw, :load, id)[:pd] = var(pm, nw, :pd, id) + sol(pm, nw, :load, id)[:pd_bus] = var(pm, nw, :pd_bus, id) end end diff --git a/src/form/bf.jl b/src/form/bf.jl index b6b453722..e018052e2 100644 --- a/src/form/bf.jl +++ b/src/form/bf.jl @@ -1,27 +1,27 @@ -"This is duplicated at PMD level to correctly handle the indexing of the shunts." -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractBFModel, n::Int, f_idx, angmin, angmax) +"This is duplicated at PowerModelsDistribution level to correctly handle the indexing of the shunts." +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractBFModel, n::Int, f_idx, angmin, angmax) i, f_bus, t_bus = f_idx t_idx = (i, t_bus, f_bus) - branch = _PMs.ref(pm, n, :branch, i) + branch = ref(pm, n, :branch, i) - for c in _PMs.conductor_ids(pm; nw=n) + for c in conductor_ids(pm; nw=n) tm = branch["tap"][c] g_fr = branch["g_fr"][c,c] g_to = branch["g_to"][c,c] b_fr = branch["b_fr"][c,c] b_to = branch["b_to"][c,c] - tr, ti = _PMs.calc_branch_t(branch) + tr, ti = _PM.calc_branch_t(branch) tr, ti = tr[c], ti[c] r = branch["br_r"][c,c] x = branch["br_x"][c,c] # getting the variables - w_fr = _PMs.var(pm, n, :w, f_bus)[c] - p_fr = _PMs.var(pm, n, :p, f_idx)[c] - q_fr = _PMs.var(pm, n, :q, f_idx)[c] + w_fr = var(pm, n, :w, f_bus)[c] + p_fr = var(pm, n, :p, f_idx)[c] + q_fr = var(pm, n, :q, f_idx)[c] tzr = r*tr + x*ti tzi = r*ti - x*tr @@ -40,10 +40,16 @@ end "Create voltage variables for branch flow model" function variable_mc_bus_voltage_on_off(pm::LPUBFDiagModel; kwargs...) - variable_mc_voltage_magnitude_sqr_on_off(pm; kwargs...) + variable_mc_bus_voltage_magnitude_sqr_on_off(pm; kwargs...) end "nothing to do, this model is symmetric" -function constraint_mc_trans_yy(pm::LPUBFDiagModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) +function constraint_mc_transformer_power_yy(pm::LPUBFDiagModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) +end + + +"TODO" +function constraint_mc_transformer_power_dy(pm::LPUBFDiagModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + Memento.error(_LOGGER, "Delta-Wye transformers are not currently supported in the LPUBFDiagModel type of power flow models") end diff --git a/src/form/bf_mx.jl b/src/form/bf_mx.jl index 5ab758489..73218ad9c 100644 --- a/src/form/bf_mx.jl +++ b/src/form/bf_mx.jl @@ -1,25 +1,34 @@ import LinearAlgebra: diag, diagm + "" function variable_mc_branch_current(pm::AbstractUBFModels; kwargs...) - variable_mc_branch_series_current_prod_hermitian(pm; kwargs...) + constraint_mc_branch_current_series_product_hermitian(pm; kwargs...) end "" -function variable_mc_voltage(pm::AbstractUBFModels; kwargs...) - variable_mc_voltage_prod_hermitian(pm; kwargs...) +function variable_mc_bus_voltage(pm::AbstractUBFModels; kwargs...) + variable_mc_bus_voltage_prod_hermitian(pm; kwargs...) + + nw = get(kwargs, :nw, pm.cnw) + allbuses = Set(ids(pm, nw, :bus)) + startingbuses = Set(i for (l,i,j) in ref(pm, nw, :arcs_from)) + leafnodes = setdiff(allbuses, startingbuses) + for i in leafnodes + constraint_mc_voltage_psd(pm, nw, i) + end end "" -function variable_mc_voltage_prod_hermitian(pm::AbstractUBFModels; n_cond::Int=3, nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) - bus_ids = collect(_PMs.ids(pm, nw, :bus)) +function variable_mc_bus_voltage_prod_hermitian(pm::AbstractUBFModels; n_cond::Int=3, nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) + bus_ids = collect(ids(pm, nw, :bus)) if bounded # get bounds - vmax = Dict([(id, _PMs.ref(pm, nw, :bus, id, "vmax")) for id in bus_ids]) - vmin = Dict([(id, _PMs.ref(pm, nw, :bus, id, "vmin")) for id in bus_ids]) + vmax = Dict([(id, ref(pm, nw, :bus, id, "vmax")) for id in bus_ids]) + vmin = Dict([(id, ref(pm, nw, :bus, id, "vmin")) for id in bus_ids]) # create bounded Hermitian matrix variables (Wr,Wi) = variable_mx_hermitian(pm.model, bus_ids, n_cond; sqrt_upper_bound=vmax, sqrt_lower_bound=vmin, name="W", prefix="$nw") @@ -28,22 +37,35 @@ function variable_mc_voltage_prod_hermitian(pm::AbstractUBFModels; n_cond::Int=3 (Wr,Wi) = variable_mx_hermitian(pm.model, bus_ids, n_cond; set_lower_bound_diag_to_zero=true, name="W", prefix="$nw") end + v_start = exp.((im*2*pi/3).*[0; -1; 1]) #TODO this should be made more generic eventually + W_start = v_start*v_start' + for (id,_) in Wr + for i in 1:3 + for j in 1:i + JuMP.set_start_value(Wr[id][i,j], real.(W_start)[i,j]) + if j JuMP.@variable(pm.model, - [c in 1:ncnds], base_name="$(nw)_pl_$(i)", - lower_bound=pmin[i][c], upper_bound=pmax[i][c] + pd = Dict(i => JuMP.@variable(pm.model, + [c in 1:ncnds], base_name="$(nw)_pd_$(i)" ) for i in load_ids ) - ql = Dict(i => JuMP.@variable(pm.model, - [c in 1:ncnds], base_name="$(nw)_ql_$(i)", - lower_bound=qmin[i][c], upper_bound=qmax[i][c] + qd = Dict(i => JuMP.@variable(pm.model, + [c in 1:ncnds], base_name="$(nw)_qd_$(i)" ) for i in load_ids ) + + if bounded + for i in load_ids + load = ref(pm, nw, :load, i) + bus = ref(pm, nw, :bus, load["load_bus"]) + pmin, pmax, qmin, qmax = _calc_load_pq_bounds(load, bus) + set_lower_bound.(pd[i], pmin) + set_upper_bound.(pd[i], pmax) + set_lower_bound.(qd[i], qmin) + set_upper_bound.(qd[i], qmax) + end + end + #store in dict, but do not overwrite for i in load_ids - _PMs.var(pm, nw)[:pl][i] = pl[i] - _PMs.var(pm, nw)[:ql][i] = ql[i] + var(pm, nw)[:pd][i] = pd[i] + var(pm, nw)[:qd][i] = qd[i] end - report && _PMs.sol_component_value(pm, nw, :load, :pl, load_ids, pl) - report && _PMs.sol_component_value(pm, nw, :load, :ql, load_ids, ql) + report && _IM.sol_component_value(pm, nw, :load, :pd, load_ids, pd) + report && _IM.sol_component_value(pm, nw, :load, :qd, load_ids, qd) end @@ -379,25 +430,25 @@ formulation. """ function variable_mc_load_power_bus(pm::SDPUBFKCLMXModel, load_ids::Array{Int,1}; nw=pm.cnw, bounded::Bool=true, report::Bool=true) @assert(bounded) - ncnds = length(_PMs.conductor_ids(pm, nw)) + ncnds = length(conductor_ids(pm, nw)) # calculate bounds bound = Dict{eltype(load_ids), Array{Real,2}}() for id in load_ids - load = _PMs.ref(pm, nw, :load, id) - @assert(load["conn"]=="wye") - bus = _PMs.ref(pm, nw, :bus, load["load_bus"]) + load = ref(pm, nw, :load, id) + @assert(load["configuration"]==WYE) + bus = ref(pm, nw, :bus, load["load_bus"]) cmax = _calc_load_current_max(load, bus) bound[id] = bus["vmax"]*cmax' end # create matrix variables - (Pd,Qd) = variable_mx_complex_with_diag(pm.model, load_ids, ncnds; symm_bound=bound, name=("Pd", "Qd"), prefix="$nw") + (Pd_bus,Qd_bus) = variable_mx_complex_with_diag(pm.model, load_ids, ncnds; symm_bound=bound, name=("Pd_bus", "Qd_bus"), prefix="$nw") for id in load_ids - _PMs.var(pm, nw, :Pd)[id] = Pd[id] - _PMs.var(pm, nw, :Qd)[id] = Qd[id] + var(pm, nw, :Pd_bus)[id] = Pd_bus[id] + var(pm, nw, :Qd_bus)[id] = Qd_bus[id] end - report && _PMs.sol_component_value(pm, nw, :load, :Pd, load_ids, Pd) - report && _PMs.sol_component_value(pm, nw, :load, :Qd, load_ids, Qd) + report && _IM.sol_component_value(pm, nw, :load, :Pd_bus, load_ids, Pd_bus) + report && _IM.sol_component_value(pm, nw, :load, :Qd_bus, load_ids, Qd_bus) end @@ -418,15 +469,15 @@ See the paper by Zhao et al. for the first convex relaxation of delta transforma See upcoming paper for discussion of bounds. [reference added when accepted] """ -function variable_mc_load_delta_aux(pm::AbstractUBFModels, load_ids::Array{Int,1}; nw=pm.cnw, eps=0.1, bounded::Bool=true, report::Bool=true) +function variable_mc_load_power_delta_aux(pm::AbstractUBFModels, load_ids::Array{Int,1}; nw=pm.cnw, eps=0.1, bounded::Bool=true, report::Bool=true) @assert(bounded) - ncnds = length(_PMs.conductor_ids(pm, nw)) + ncnds = length(conductor_ids(pm, nw)) # calculate bounds bound = Dict{eltype(load_ids), Array{Real,2}}() for id in load_ids - load = _PMs.ref(pm, nw, :load, id) + load = ref(pm, nw, :load, id) bus_id = load["load_bus"] - bus = _PMs.ref(pm, nw, :bus, bus_id) + bus = ref(pm, nw, :bus, bus_id) cmax = _calc_load_current_max(load, bus) bound[id] = bus["vmax"]*cmax' end @@ -434,11 +485,11 @@ function variable_mc_load_delta_aux(pm::AbstractUBFModels, load_ids::Array{Int,1 (Xdr,Xdi) = variable_mx_complex(pm.model, load_ids, ncnds, ncnds; symm_bound=bound, name="Xd", prefix="$nw") # save references - _PMs.var(pm, nw)[:Xdr] = Xdr - _PMs.var(pm, nw)[:Xdi] = Xdi + var(pm, nw)[:Xdr] = Xdr + var(pm, nw)[:Xdi] = Xdi - report && _PMs.sol_component_value(pm, nw, :load, :Xdr, load_ids, Xdr) - report && _PMs.sol_component_value(pm, nw, :load, :Xdi, load_ids, Xdi) + report && _IM.sol_component_value(pm, nw, :load, :Xdr, load_ids, Xdr) + report && _IM.sol_component_value(pm, nw, :load, :Xdi, load_ids, Xdi) end @@ -450,32 +501,24 @@ frame. function variable_mc_load_current(pm::AbstractUBFModels, load_ids::Array{Int,1}; nw=pm.cnw, bounded::Bool=true, report::Bool=true) @assert(bounded) - ncnds = length(_PMs.conductor_ids(pm, nw)) + ncnds = length(conductor_ids(pm, nw)) # calculate bounds cmin = Dict{eltype(load_ids), Array{Real,1}}() cmax = Dict{eltype(load_ids), Array{Real,1}}() - for (id, load) in _PMs.ref(pm, nw, :load) + for (id, load) in ref(pm, nw, :load) bus_id = load["load_bus"] - bus = _PMs.ref(pm, nw, :bus, bus_id) + bus = ref(pm, nw, :bus, bus_id) cmin[id], cmax[id] = _calc_load_current_magnitude_bounds(load, bus) end # create matrix variables (CCdr, CCdi) = variable_mx_hermitian(pm.model, load_ids, ncnds; sqrt_upper_bound=cmax, sqrt_lower_bound=cmin, name="CCd", prefix="$nw") # save references - _PMs.var(pm, nw)[:CCdr] = CCdr - _PMs.var(pm, nw)[:CCdi] = CCdi + var(pm, nw)[:CCdr] = CCdr + var(pm, nw)[:CCdi] = CCdi - report && _PMs.sol_component_value(pm, nw, :load, :CCdr, load_ids, CCdr) - report && _PMs.sol_component_value(pm, nw, :load, :CCdi, load_ids, CCdi) -end - - -""" -Only KCLModels need to further constrain the generator variables. -""" -function constraint_mc_generation(pm::AbstractUBFModels, gen_id::Int; nw::Int=pm.cnw) - # do nothing + report && _IM.sol_component_value(pm, nw, :load, :CCdr, load_ids, CCdr) + report && _IM.sol_component_value(pm, nw, :load, :CCdi, load_ids, CCdi) end @@ -483,14 +526,14 @@ end Link the current and power withdrawn by a generator at the bus through a PSD constraint. The rank-1 constraint is dropped in this formulation. """ -function constraint_mc_generation(pm::SDPUBFKCLMXModel, gen_id::Int; nw::Int=pm.cnw) - Pg = _PMs.var(pm, nw, :Pg, gen_id) - Qg = _PMs.var(pm, nw, :Qg, gen_id) - bus_id = _PMs.ref(pm, nw, :gen, gen_id)["gen_bus"] - Wr = _PMs.var(pm, nw, :Wr, bus_id) - Wi = _PMs.var(pm, nw, :Wi, bus_id) - CCgr = _PMs.var(pm, nw, :CCgr, gen_id) - CCgi = _PMs.var(pm, nw, :CCgi, gen_id) +function constraint_mc_gen_setpoint(pm::SDPUBFKCLMXModel, gen_id::Int; nw::Int=pm.cnw) + Pg = var(pm, nw, :Pg_bus, gen_id) + Qg = var(pm, nw, :Qg_bus, gen_id) + bus_id = ref(pm, nw, :gen, gen_id)["gen_bus"] + Wr = var(pm, nw, :Wr, bus_id) + Wi = var(pm, nw, :Wi, bus_id) + CCgr = var(pm, nw, :CCgr, gen_id) + CCgi = var(pm, nw, :CCgi, gen_id) constraint_SWL_psd(pm.model, Pg, Qg, Wr, Wi, CCgr, CCgi) end @@ -561,13 +604,13 @@ end """ Creates the constraints modelling the (relaxed) voltage-dependent loads. """ -function constraint_mc_load(pm::AbstractUBFModels, load_id::Int; nw::Int=pm.cnw) +function constraint_mc_load_setpoint(pm::AbstractUBFModels, load_id::Int; nw::Int=pm.cnw, report::Bool=true) # shared variables and parameters - load = _PMs.ref(pm, nw, :load, load_id) + load = ref(pm, nw, :load, load_id) pd0 = load["pd"] qd0 = load["qd"] bus_id = load["load_bus"] - bus = _PMs.ref(pm, nw, :bus, bus_id) + bus = ref(pm, nw, :bus, bus_id) ncnds = length(pd0) # calculate load params @@ -578,64 +621,81 @@ function constraint_mc_load(pm::AbstractUBFModels, load_id::Int; nw::Int=pm.cnw) pmin, pmax, qmin, qmax = _calc_load_pq_bounds(load, bus) # take care of connections - if load["conn"]=="wye" - if load["model"]=="constant_power" - _PMs.var(pm, nw, :pl)[load_id] = pd0 - _PMs.var(pm, nw, :ql)[load_id] = qd0 - elseif load["model"]=="constant_impedance" - w = _PMs.var(pm, nw, :w)[bus_id] - _PMs.var(pm, nw, :pl)[load_id] = a.*w - _PMs.var(pm, nw, :ql)[load_id] = b.*w - # in this case, :pl has a JuMP variable + if load["configuration"]==WYE + if load["model"]==POWER + var(pm, nw, :pd)[load_id] = pd0 + var(pm, nw, :qd)[load_id] = qd0 + elseif load["model"]==IMPEDANCE + w = var(pm, nw, :w)[bus_id] + var(pm, nw, :pd)[load_id] = a.*w + var(pm, nw, :qd)[load_id] = b.*w + # in this case, :pd has a JuMP variable else - pl = _PMs.var(pm, nw, :pl)[load_id] - ql = _PMs.var(pm, nw, :ql)[load_id] + Wr = var(pm, nw, :Wr, bus_id) + pd = var(pm, nw, :pd)[load_id] + qd = var(pm, nw, :qd)[load_id] for c in 1:ncnds - constraint_pqw(pm.model, Wr[c,c], pl[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) - constraint_pqw(pm.model, Wr[c,c], ql[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) + constraint_pqw(pm.model, Wr[c,c], pd[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) + constraint_pqw(pm.model, Wr[c,c], qd[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) end end - # :pd is identical to :pl now - _PMs.var(pm, nw, :pd)[load_id] = _PMs.var(pm, nw, :pl)[load_id] - _PMs.var(pm, nw, :qd)[load_id] = _PMs.var(pm, nw, :ql)[load_id] - elseif load["conn"]=="delta" + # :pd_bus is identical to :pd now + var(pm, nw, :pd_bus)[load_id] = var(pm, nw, :pd)[load_id] + var(pm, nw, :qd_bus)[load_id] = var(pm, nw, :qd)[load_id] + + ## reporting + if report + sol(pm, nw, :load, load_id)[:pd] = var(pm, nw, :pd)[load_id] + sol(pm, nw, :load, load_id)[:qd] = var(pm, nw, :qd)[load_id] + sol(pm, nw, :load, load_id)[:pd_bus] = var(pm, nw, :pd_bus)[load_id] + sol(pm, nw, :load, load_id)[:qd_bus] = var(pm, nw, :qd_bus)[load_id] + end + elseif load["configuration"]==DELTA # link Wy, CCd and X - Wr = _PMs.var(pm, nw, :Wr, bus_id) - Wi = _PMs.var(pm, nw, :Wi, bus_id) - CCdr = _PMs.var(pm, nw, :CCdr, load_id) - CCdi = _PMs.var(pm, nw, :CCdi, load_id) - Xdr = _PMs.var(pm, nw, :Xdr, load_id) - Xdi = _PMs.var(pm, nw, :Xdi, load_id) + Wr = var(pm, nw, :Wr, bus_id) + Wi = var(pm, nw, :Wi, bus_id) + CCdr = var(pm, nw, :CCdr, load_id) + CCdi = var(pm, nw, :CCdi, load_id) + Xdr = var(pm, nw, :Xdr, load_id) + Xdi = var(pm, nw, :Xdi, load_id) Td = [1 -1 0; 0 1 -1; -1 0 1] constraint_SWL_psd(pm.model, Xdr, Xdi, Wr, Wi, CCdr, CCdi) - # define pd/qd and pl/ql as affine transformations of X - pd = LinearAlgebra.diag(Xdr*Td) - qd = LinearAlgebra.diag(Xdi*Td) - pl = LinearAlgebra.diag(Td*Xdr) - ql = LinearAlgebra.diag(Td*Xdi) + # define pd/qd and pd_bus/qd_bus as affine transformations of X + pd_bus = LinearAlgebra.diag(Xdr*Td) + qd_bus = LinearAlgebra.diag(Xdi*Td) + pd = LinearAlgebra.diag(Td*Xdr) + qd = LinearAlgebra.diag(Td*Xdi) - _PMs.var(pm, nw, :pd)[load_id] = pd - _PMs.var(pm, nw, :qd)[load_id] = qd - _PMs.var(pm, nw, :pl)[load_id] = pl - _PMs.var(pm, nw, :ql)[load_id] = ql + var(pm, nw, :pd_bus)[load_id] = pd_bus + var(pm, nw, :qd_bus)[load_id] = qd_bus + var(pm, nw, :pd)[load_id] = pd + var(pm, nw, :qd)[load_id] = qd # |Vd|^2 is a linear transformation of Wr wd = LinearAlgebra.diag(Td*Wr*Td') - if load["model"]=="constant_power" + if load["model"]==POWER for c in 1:ncnds - JuMP.@constraint(pm.model, pl[c]==pd0[c]) - JuMP.@constraint(pm.model, ql[c]==qd0[c]) + JuMP.@constraint(pm.model, pd[c]==pd0[c]) + JuMP.@constraint(pm.model, qd[c]==qd0[c]) end - elseif load["model"]=="constant_impedance" + elseif load["model"]==IMPEDANCE for c in 1:ncnds - JuMP.@constraint(pm.model, pl[c]==a[c]*wd[c]) - JuMP.@constraint(pm.model, ql[c]==b[c]*wd[c]) + JuMP.@constraint(pm.model, pd[c]==a[c]*wd[c]) + JuMP.@constraint(pm.model, qd[c]==b[c]*wd[c]) end else for c in 1:ncnds - constraint_pqw(pm.model, wd[c], pl[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) - constraint_pqw(pm.model, wd[c], ql[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) + constraint_pqw(pm.model, wd[c], pd[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) + constraint_pqw(pm.model, wd[c], qd[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) end + end + + ## reporting; for delta these are not available as saved variables! + if report + sol(pm, nw, :load, load_id)[:pd] = pd + sol(pm, nw, :load, load_id)[:qd] = qd + sol(pm, nw, :load, load_id)[:pd_bus] = pd_bus + sol(pm, nw, :load, load_id)[:qd_bus] = qd_bus end end end @@ -645,13 +705,13 @@ end Creates the constraints modelling the (relaxed) voltage-dependent loads for the matrix KCL formulation. """ -function constraint_mc_load(pm::SDPUBFKCLMXModel, load_id::Int; nw::Int=pm.cnw) +function constraint_mc_load_setpoint(pm::SDPUBFKCLMXModel, load_id::Int; nw::Int=pm.cnw, report::Bool=true) # shared variables and parameters - load = _PMs.ref(pm, nw, :load, load_id) + load = ref(pm, nw, :load, load_id) pd0 = load["pd"] qd0 = load["qd"] bus_id = load["load_bus"] - bus = _PMs.ref(pm, nw, :bus, bus_id) + bus = ref(pm, nw, :bus, bus_id) ncnds = length(pd0) # calculate load params @@ -662,71 +722,71 @@ function constraint_mc_load(pm::SDPUBFKCLMXModel, load_id::Int; nw::Int=pm.cnw) pmin, pmax, qmin, qmax = _calc_load_pq_bounds(load, bus) # take care of connections - Wr = _PMs.var(pm, nw, :Wr, bus_id) - Wi = _PMs.var(pm, nw, :Wi, bus_id) - CCdr = _PMs.var(pm, nw, :CCdr, load_id) - CCdi = _PMs.var(pm, nw, :CCdi, load_id) - - if load["conn"]=="wye" - if load["model"]=="constant_power" - _PMs.var(pm, nw, :pl)[load_id] = pd0 - _PMs.var(pm, nw, :ql)[load_id] = qd0 - elseif load["model"]=="constant_impedance" - w = _PMs.var(pm, nw, :w, bus_id) + Wr = var(pm, nw, :Wr, bus_id) + Wi = var(pm, nw, :Wi, bus_id) + CCdr = var(pm, nw, :CCdr, load_id) + CCdi = var(pm, nw, :CCdi, load_id) + + if load["configuration"]==WYE + if load["model"]==POWER + var(pm, nw, :pd)[load_id] = pd0 + var(pm, nw, :qd)[load_id] = qd0 + elseif load["model"]==IMPEDANCE + w = var(pm, nw, :w, bus_id) # for c in 1:ncnds - _PMs.var(pm, nw, :pl)[load_id] = a.*w - _PMs.var(pm, nw, :ql)[load_id] = b.*w + var(pm, nw, :pd)[load_id] = a.*w + var(pm, nw, :qd)[load_id] = b.*w # end - # in this case, :pl has a JuMP variable + # in this case, :pd has a JuMP variable else - pl = _PMs.var(pm, nw, :pl)[load_id] - ql = _PMs.var(pm, nw, :ql)[load_id] + pd = var(pm, nw, :pd)[load_id] + qd = var(pm, nw, :qd)[load_id] for c in 1:ncnds - constraint_pqw(pm.model, Wr[c,c], pl[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) - constraint_pqw(pm.model, Wr[c,c], ql[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) + constraint_pqw(pm.model, Wr[c,c], pd[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) + constraint_pqw(pm.model, Wr[c,c], qd[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) end end - # diagonal of :Pd is identical to :pl now - Pd = _PMs.var(pm, nw, :Pd)[load_id] - Qd = _PMs.var(pm, nw, :Qd)[load_id] + # diagonal of :Pd is identical to :pd now + Pd_bus = var(pm, nw, :Pd_bus)[load_id] + Qd_bus = var(pm, nw, :Qd_bus)[load_id] for c in 1:ncnds - Pd[c,c] = _PMs.var(pm, nw, :pl)[load_id][c] - Qd[c,c] = _PMs.var(pm, nw, :ql)[load_id][c] + Pd_bus[c,c] = var(pm, nw, :pd)[load_id][c] + Qd_bus[c,c] = var(pm, nw, :qd)[load_id][c] end - elseif load["conn"]=="delta" + elseif load["configuration"]==DELTA # link Wy, CCd and X - Xdr = _PMs.var(pm, nw, :Xdr, load_id) - Xdi = _PMs.var(pm, nw, :Xdi, load_id) + Xdr = var(pm, nw, :Xdr, load_id) + Xdi = var(pm, nw, :Xdi, load_id) Td = [1 -1 0; 0 1 -1; -1 0 1] constraint_SWL_psd(pm.model, Xdr, Xdi, Wr, Wi, CCdr, CCdi) - # define pd/qd and pl/ql as affine transformations of X - Pd = Xdr*Td - Qd = Xdi*Td - pl = LinearAlgebra.diag(Td*Xdr) - ql = LinearAlgebra.diag(Td*Xdi) + # define pd_bus/qd_bus and pd/qd as affine transformations of X + Pd_bus = Xdr*Td + Qd_bus = Xdi*Td + pd = LinearAlgebra.diag(Td*Xdr) + qd = LinearAlgebra.diag(Td*Xdi) - _PMs.var(pm, nw, :Pd)[load_id] = Pd - _PMs.var(pm, nw, :Qd)[load_id] = Qd - _PMs.var(pm, nw, :pl)[load_id] = pl - _PMs.var(pm, nw, :ql)[load_id] = ql + var(pm, nw, :Pd_bus)[load_id] = Pd_bus + var(pm, nw, :Qd_bus)[load_id] = Qd_bus + var(pm, nw, :pd)[load_id] = pd + var(pm, nw, :qd)[load_id] = qd # |Vd|^2 is a linear transformation of Wr wd = LinearAlgebra.diag(Td*Wr*Td') - if load["model"]=="constant_power" + if load["model"]==POWER for c in 1:ncnds - JuMP.@constraint(pm.model, pl[c]==pd0[c]) - JuMP.@constraint(pm.model, ql[c]==qd0[c]) + JuMP.@constraint(pm.model, pd[c]==pd0[c]) + JuMP.@constraint(pm.model, qd[c]==qd0[c]) end - elseif load["model"]=="constant_impedance" + elseif load["model"]==IMPEDANCE for c in 1:ncnds - JuMP.@constraint(pm.model, pl[c]==a[c]*wd[c]) - JuMP.@constraint(pm.model, ql[c]==b[c]*wd[c]) + JuMP.@constraint(pm.model, pd[c]==a[c]*wd[c]) + JuMP.@constraint(pm.model, qd[c]==b[c]*wd[c]) end else for c in 1:ncnds - constraint_pqw(pm.model, wd[c], pl[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) - constraint_pqw(pm.model, wd[c], ql[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) + constraint_pqw(pm.model, wd[c], pd[c], a[c], alpha[c], wmin[c], wmax[c], pmin[c], pmax[c]) + constraint_pqw(pm.model, wd[c], qd[c], b[c], beta[c], wmin[c], wmax[c], qmin[c], qmax[c]) end end end @@ -759,24 +819,6 @@ function constraint_M_psd(model::JuMP.Model, M_re, M_im) end -""" -For KCLMXModels, a new power balance constraint is required. -""" -function constraint_mc_power_balance(pm::KCLMXModels, i::Int; nw::Int=pm.cnw) - bus = _PMs.ref(pm, nw, :bus, i) - bus_arcs = _PMs.ref(pm, nw, :bus_arcs, i) - bus_arcs_dc = _PMs.ref(pm, nw, :bus_arcs_dc, i) - bus_gens = _PMs.ref(pm, nw, :bus_gens, i) - bus_loads = _PMs.ref(pm, nw, :bus_loads, i) - bus_shunts = _PMs.ref(pm, nw, :bus_shunts, i) - - bus_Gs = Dict(k => LinearAlgebra.diagm(0=>_PMs.ref(pm, nw, :shunt, k, "gs")) for k in bus_shunts) - bus_Bs = Dict(k => LinearAlgebra.diagm(0=>_PMs.ref(pm, nw, :shunt, k, "bs")) for k in bus_shunts) - - constraint_mc_power_balance(pm, nw, i, bus_arcs, bus_arcs_dc, bus_gens, bus_loads, bus_Gs, bus_Bs) -end - - """ Shunt handling in matrix form: I = Y.U @@ -785,33 +827,47 @@ S = U.I' = U.(Y.U)' = U.U'.Y' = W.Y' P = Wr.G'+Wi.B' Q = -Wr.B'+Wi.G' """ -function constraint_mc_power_balance(pm::KCLMXModels, n::Int, i::Int, bus_arcs, bus_arcs_dc, bus_gens, bus_loads, bus_Gs, bus_Bs) - Wr = _PMs.var(pm, n, :Wr, i) - Wi = _PMs.var(pm, n, :Wi, i) +function constraint_mc_load_power_balance(pm::KCLMXModels, n::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + Wr = var(pm, n, :Wr, i) + Wi = var(pm, n, :Wi, i) + + P = get(var(pm, n), :P, Dict()); _PM._check_var_keys(P, bus_arcs, "active power", "branch") + Q = get(var(pm, n), :Q, Dict()); _PM._check_var_keys(Q, bus_arcs, "reactive power", "branch") + Pg = get(var(pm, n), :Pg_bus, Dict()); _PM._check_var_keys(Pg, bus_gens, "active power", "generator") + Qg = get(var(pm, n), :Qg_bus, Dict()); _PM._check_var_keys(Qg, bus_gens, "reactive power", "generator") + Pd = get(var(pm, n), :Pd_bus, Dict()); _PM._check_var_keys(Pd, bus_loads, "active power", "load") + Qd = get(var(pm, n), :Qd_bus, Dict()); _PM._check_var_keys(Qd, bus_loads, "reactive power", "load") - P = get(_PMs.var(pm, n), :P, Dict()); _PMs._check_var_keys(P, bus_arcs, "active power", "branch") - Q = get(_PMs.var(pm, n), :Q, Dict()); _PMs._check_var_keys(Q, bus_arcs, "reactive power", "branch") - Pg = get(_PMs.var(pm, n), :Pg, Dict()); _PMs._check_var_keys(Pg, bus_gens, "active power", "generator") - Qg = get(_PMs.var(pm, n), :Qg, Dict()); _PMs._check_var_keys(Qg, bus_gens, "reactive power", "generator") - Pd = get(_PMs.var(pm, n), :Pd, Dict()); _PMs._check_var_keys(Pd, bus_loads, "active power", "load") - Qd = get(_PMs.var(pm, n), :Qd, Dict()); _PMs._check_var_keys(Qd, bus_loads, "reactive power", "load") + cnds = conductor_ids(pm; nw=n) + ncnds = length(cnds) - # ignore dc for now - #TODO add DC in matrix version? - ncnds = size(Wr)[1] Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) Bt = isempty(bus_bs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_bs)) - # changed the ordering - # LHS: all variables with generator sign convention - # RHS: all variables with load sign convention - # _PMs.con(pm, n, :kcl_P)[i] = - cp = JuMP.@constraint(pm.model, sum(Pg[g] for g in bus_gens) .== sum(P[a] for a in bus_arcs) + sum(Pd[d] for d in bus_loads) + ( Wr*Gt'+Wi*Bt')) - # _PMs.con(pm, n, :kcl_Q)[i] = - cq = JuMP.@constraint(pm.model, sum(Qg[g] for g in bus_gens) .== sum(Q[a] for a in bus_arcs) + sum(Qd[d] for d in bus_loads) + (-Wr*Bt'+Wi*Gt')) - - if _PMs.report_duals(pm) - _PMs.sol(pm, n, :bus, i)[:lam_kcl_r] = cp - _PMs.sol(pm, n, :bus, i)[:lam_kcl_i] = cq + cstr_p = JuMP.@constraint(pm.model, + sum(P[a] for a in bus_arcs) + # + sum(Psw[a_sw] for a_sw in bus_arcs_sw) + # + sum(Pt[a_trans] for a_trans in bus_arcs_trans) + .== + sum(Pg[g] for g in bus_gens) + # - sum(ps[s] for s in bus_storage) + - sum(Pd[d] for d in bus_loads) + - diag(Wr*Gt'+Wi*Bt') + ) + + cstr_q = JuMP.@constraint(pm.model, + sum(Q[a] for a in bus_arcs) + # + sum(diag(Qsw[a_sw]) for a_sw in bus_arcs_sw) + # + sum(diag(Qt[a_trans]) for a_trans in bus_arcs_trans) + .== + sum(Qg[g] for g in bus_gens) + # - sum(qs[s] for s in bus_storage) + - sum(Qd[d] for d in bus_loads) + - diag(-Wr*Bt'+Wi*Gt') + ) + + if _IM.report_duals(pm) + sol(pm, n, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, n, :bus, i)[:lam_kcl_i] = cstr_q end -end +end \ No newline at end of file diff --git a/src/form/bf_mx_lin.jl b/src/form/bf_mx_lin.jl index 134545b9b..1e1d9baf8 100644 --- a/src/form/bf_mx_lin.jl +++ b/src/form/bf_mx_lin.jl @@ -12,29 +12,29 @@ end "" -function variable_mc_voltage_prod_hermitian(pm::LPUBFDiagModel; n_cond::Int=3, nw::Int=pm.cnw, bounded = true) - variable_mc_voltage_magnitude_sqr(pm, nw=nw) +function variable_mc_bus_voltage(pm::LPUBFDiagModel; n_cond::Int=3, nw::Int=pm.cnw, bounded::Bool=true) + variable_mc_bus_voltage_magnitude_sqr(pm, nw=nw) end "" -function variable_mc_branch_flow(pm::LPUBFDiagModel; n_cond::Int=3, nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) +function variable_mc_branch_power(pm::LPUBFDiagModel; n_cond::Int=3, nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true) @assert(n_cond == 3) - variable_mc_branch_flow_active(pm, nw=nw, bounded=bounded) - variable_mc_branch_flow_reactive(pm, nw=nw, bounded=bounded) + variable_mc_branch_power_real(pm, nw=nw, bounded=bounded) + variable_mc_branch_power_imaginary(pm, nw=nw, bounded=bounded) end "Defines branch flow model power flow equations" -function constraint_mc_flow_losses(pm::LPUBFDiagModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm) - p_fr = _PMs.var(pm, n, :p)[f_idx] - q_fr = _PMs.var(pm, n, :q)[f_idx] +function constraint_mc_power_losses(pm::LPUBFDiagModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm) + p_fr = var(pm, n, :p)[f_idx] + q_fr = var(pm, n, :q)[f_idx] - p_to = _PMs.var(pm, n, :p)[t_idx] - q_to = _PMs.var(pm, n, :q)[t_idx] + p_to = var(pm, n, :p)[t_idx] + q_to = var(pm, n, :q)[t_idx] - w_fr = _PMs.var(pm, n, :w)[f_bus] - w_to = _PMs.var(pm, n, :w)[t_bus] + w_fr = var(pm, n, :w)[f_bus] + w_to = var(pm, n, :w)[t_bus] JuMP.@constraint(pm.model, p_fr + p_to .== diag( g_sh_fr).*w_fr + diag( g_sh_to).*w_to) JuMP.@constraint(pm.model, q_fr + q_to .== diag(-b_sh_fr).*w_fr + diag(-b_sh_to).*w_to) @@ -43,11 +43,11 @@ end "Defines voltage drop over a branch, linking from and to side voltage" function constraint_mc_model_voltage_magnitude_difference(pm::LPUBFDiagModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm) - w_fr = _PMs.var(pm, n, :w)[f_bus] - w_to = _PMs.var(pm, n, :w)[t_bus] + w_fr = var(pm, n, :w)[f_bus] + w_to = var(pm, n, :w)[t_bus] - p_fr = _PMs.var(pm, n, :p)[f_idx] - q_fr = _PMs.var(pm, n, :q)[f_idx] + p_fr = var(pm, n, :p)[f_idx] + q_fr = var(pm, n, :q)[f_idx] p_s_fr = p_fr - diag(g_sh_fr).*w_fr q_s_fr = q_fr + diag(b_sh_fr).*w_to @@ -65,30 +65,32 @@ end "balanced three-phase phasor" function constraint_mc_theta_ref(pm::LPUBFDiagModel, n::Int, i::Int, va_ref) - ncnds = length(_PMs.conductor_ids(pm)) + ncnds = length(conductor_ids(pm)) @assert(ncnds >= 2) - w = _PMs.var(pm, n, :w)[i] + w = var(pm, n, :w)[i] JuMP.@constraint(pm.model, w[2:ncnds] .== w[1]) end "" -function constraint_mc_power_balance(pm::LPUBFDiagModel, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - w = _PMs.var(pm, nw, :w, i) +function constraint_mc_load_power_balance(pm::LPUBFDiagModel, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + w = var(pm, nw, :w, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") + pd = get(var(pm, nw), :pd, Dict()); _PM._check_var_keys(pd, bus_loads, "active power", "load") + qd = get(var(pm, nw), :qd, Dict()); _PM._check_var_keys(qd, bus_loads, "reactive power", "load") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") cstr_p = [] cstr_q = [] @@ -100,7 +102,7 @@ function constraint_mc_power_balance(pm::LPUBFDiagModel, nw::Int, i, bus_arcs, b .== sum(pg[g] for g in bus_gens) - sum(ps[s] for s in bus_storage) - - sum(pd for pd in values(bus_pd)) + - sum(pd[d] for d in bus_loads) - sum(gs.*w for gs in values(bus_gs)) ) @@ -111,12 +113,12 @@ function constraint_mc_power_balance(pm::LPUBFDiagModel, nw::Int, i, bus_arcs, b .== sum(qg[g] for g in bus_gens) - sum(qs[s] for s in bus_storage) - - sum(qd for qd in values(bus_qd)) + - sum(qd[d] for d in bus_loads) + sum(bs.*w for bs in values(bus_bs)) ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end diff --git a/src/form/bf_mx_sdp.jl b/src/form/bf_mx_sdp.jl index 61d0c2737..8a406b111 100644 --- a/src/form/bf_mx_sdp.jl +++ b/src/form/bf_mx_sdp.jl @@ -1,13 +1,13 @@ "Defines relationship between branch (series) power flow, branch (series) current and node voltage magnitude" function constraint_mc_model_current(pm::SDPUBFModel, n::Int, i, f_bus, f_idx, g_sh_fr, b_sh_fr) - p_fr = _PMs.var(pm, n, :P)[f_idx] - q_fr = _PMs.var(pm, n, :Q)[f_idx] + p_fr = var(pm, n, :P)[f_idx] + q_fr = var(pm, n, :Q)[f_idx] - w_fr_re = _PMs.var(pm, n, :Wr)[f_bus] - w_fr_im = _PMs.var(pm, n, :Wi)[f_bus] + w_fr_re = var(pm, n, :Wr)[f_bus] + w_fr_im = var(pm, n, :Wi)[f_bus] - ccm_re = _PMs.var(pm, n, :CCr)[i] - ccm_im = _PMs.var(pm, n, :CCi)[i] + ccm_re = var(pm, n, :CCr)[i] + ccm_im = var(pm, n, :CCi)[i] p_s_fr = p_fr - (w_fr_re*(g_sh_fr)' + w_fr_im*(b_sh_fr)') q_s_fr = q_fr - (w_fr_im*(g_sh_fr)' - w_fr_re*(b_sh_fr)') @@ -28,5 +28,17 @@ function constraint_mc_model_current(pm::SDPUBFModel, n::Int, i, f_bus, f_idx, g mat_real -mat_imag; mat_imag mat_real ] in JuMP.PSDCone()) +end + + +"Add explicit PSD-ness of W for nodes where it is not implied" +function constraint_mc_voltage_psd(pm::SDPUBFModel, n::Int, i) + Wr = var(pm, n, :Wr)[i] + Wi = var(pm, n, :Wi)[i] + JuMP.@constraint(pm.model, + [ + Wr -Wi; + Wi Wr + ] in JuMP.PSDCone()) end diff --git a/src/form/bf_mx_soc.jl b/src/form/bf_mx_soc.jl index a7c300242..4d32ceed1 100644 --- a/src/form/bf_mx_soc.jl +++ b/src/form/bf_mx_soc.jl @@ -1,13 +1,13 @@ "Defines relationship between branch (series) power flow, branch (series) current and node voltage magnitude" function constraint_mc_model_current(pm::SOCUBFModels, n::Int, i, f_bus, f_idx, g_sh_fr, b_sh_fr) - p_fr = _PMs.var(pm, n, :P)[f_idx] - q_fr = _PMs.var(pm, n, :Q)[f_idx] + p_fr = var(pm, n, :P)[f_idx] + q_fr = var(pm, n, :Q)[f_idx] - w_fr_re = _PMs.var(pm, n, :Wr)[f_bus] - w_fr_im = _PMs.var(pm, n, :Wi)[f_bus] + w_fr_re = var(pm, n, :Wr)[f_bus] + w_fr_im = var(pm, n, :Wi)[f_bus] - ccm_re = _PMs.var(pm, n, :CCr)[i] - ccm_im = _PMs.var(pm, n, :CCi)[i] + ccm_re = var(pm, n, :CCr)[i] + ccm_im = var(pm, n, :CCi)[i] p_s_fr = p_fr - g_sh_fr*w_fr_re q_s_fr = q_fr + b_sh_fr*w_fr_re @@ -27,23 +27,32 @@ function constraint_mc_model_current(pm::SOCUBFModels, n::Int, i, f_bus, f_idx, # code below useful for debugging: valid inequality equired to make the SOC-NLP formulation more accurate # (l,i,j) = f_idx # t_idx = (l,j,i) - # p_to = _PMs.var(pm, n, :P)[t_idx] + # p_to = var(pm, n, :P)[t_idx] # total losses are positive when g_fr, g_to and r are positive # not guaranteed for individual phases though when matrix obtained through Kron's reduction # JuMP.@constraint(pm.model, tr(p_fr) + tr(p_to) >= 0) end +"Add explicit PSD-ness of W for nodes where it is not implied" +function constraint_mc_voltage_psd(pm::SOCUBFModels, n::Int, i) + Wr = var(pm, n, :Wr)[i] + Wi = var(pm, n, :Wi)[i] + + relaxation_psd_to_soc(pm.model, Wr, Wi) +end + + "Defines relationship between branch (series) power flow, branch (series) current and node voltage magnitude" function constraint_mc_model_current(pm::SOCConicUBFModel, n::Int, i, f_bus, f_idx, g_sh_fr, b_sh_fr) - p_fr = _PMs.var(pm, n, :P)[f_idx] - q_fr = _PMs.var(pm, n, :Q)[f_idx] + p_fr = var(pm, n, :P)[f_idx] + q_fr = var(pm, n, :Q)[f_idx] - w_fr_re = _PMs.var(pm, n, :Wr)[f_bus] - w_fr_im = _PMs.var(pm, n, :Wi)[f_bus] + w_fr_re = var(pm, n, :Wr)[f_bus] + w_fr_im = var(pm, n, :Wi)[f_bus] - ccm_re = _PMs.var(pm, n, :CCr)[i] - ccm_im = _PMs.var(pm, n, :CCi)[i] + ccm_re = var(pm, n, :CCr)[i] + ccm_im = var(pm, n, :CCi)[i] p_s_fr = p_fr - g_sh_fr*w_fr_re q_s_fr = q_fr + b_sh_fr*w_fr_re @@ -60,3 +69,12 @@ function constraint_mc_model_current(pm::SOCConicUBFModel, n::Int, i, f_bus, f_i relaxation_psd_to_soc_conic(pm.model, mat_real, mat_imag, complex=true) end + + +"Add explicit PSD-ness of W for nodes where it is not implied" +function constraint_mc_voltage_psd(pm::SOCConicUBFModel, n::Int, i) + Wr = var(pm, n, :Wr)[i] + Wi = var(pm, n, :Wi)[i] + + relaxation_psd_to_soc_conic(pm.model, Wr, Wi) +end diff --git a/src/form/dcp.jl b/src/form/dcp.jl index 7073aa7ac..f7ee03c0b 100644 --- a/src/form/dcp.jl +++ b/src/form/dcp.jl @@ -1,25 +1,24 @@ -### simple active power only approximations (e.g. DC Power Flow) - - "" -function variable_mc_voltage(pm::_PMs.AbstractDCPModel; nw=pm.cnw, kwargs...) - variable_mc_voltage_angle(pm; nw=nw, kwargs...) +function variable_mc_bus_voltage(pm::_PM.AbstractDCPModel; nw=pm.cnw, kwargs...) + variable_mc_bus_voltage_angle(pm; nw=nw, kwargs...) end ######## AbstractDCPForm Models (has va but assumes vm is 1.0) ######## + "nothing to do, these models do not have complex voltage constraints" -function constraint_mc_model_voltage(pm::_PMs.AbstractDCPModel, n::Int, c::Int) +function constraint_mc_model_voltage(pm::_PM.AbstractDCPModel, n::Int, c::Int) end "" -function variable_mc_bus_voltage_on_off(pm::_PMs.AbstractDCPModel; kwargs...) - variable_mc_voltage_angle(pm; kwargs...) +function variable_mc_bus_voltage_on_off(pm::_PM.AbstractDCPModel; kwargs...) + variable_mc_bus_voltage_angle(pm; kwargs...) end ### DC Power Flow Approximation ### + """ Creates Ohms constraints (yt post fix indicates that Y and T values are in rectangular form) @@ -27,26 +26,26 @@ Creates Ohms constraints (yt post fix indicates that Y and T values are in recta p[f_idx] == -b*(t[f_bus] - t[t_bus]) ``` """ -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractDCPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) - p_fr = _PMs.var(pm, n, :p, f_idx) - va_fr = _PMs.var(pm, n, :va, f_bus) - va_to = _PMs.var(pm, n, :va, t_bus) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractDCPModel, n::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) + p_fr = var(pm, n, :p, f_idx) + va_fr = var(pm, n, :va, f_bus) + va_to = var(pm, n, :va, t_bus) - for c in _PMs.conductor_ids(pm, n) - JuMP.@constraint(pm.model, p_fr[c] == -sum(b[c,d]*(va_fr[c] - va_to[d]) for d in _PMs.conductor_ids(pm))) + for c in conductor_ids(pm, n) + JuMP.@constraint(pm.model, p_fr[c] == -sum(b[c,d]*(va_fr[c] - va_to[d]) for d in conductor_ids(pm))) end end "power balance constraint with line shunts and transformers for load shed problem, DCP formulation" -function constraint_mc_power_balance_shed(pm::_PMs.AbstractDCPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - z_demand = _PMs.var(pm, nw, :z_demand) - z_shunt = _PMs.var(pm, nw, :z_shunt) +function constraint_mc_shed_power_balance(pm::_PM.AbstractDCPModel, nw::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + z_demand = var(pm, nw, :z_demand) + z_shunt = var(pm, nw, :z_shunt) cp = JuMP.@constraint(pm.model, sum(p[a] for a in bus_arcs) @@ -59,38 +58,38 @@ function constraint_mc_power_balance_shed(pm::_PMs.AbstractDCPModel, nw::Int, i: - sum(diag(gs)*1.0^2 .*z_shunt[n] for (n,gs) in bus_gs) ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cp + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cp end end "on/off bus voltage constraint for DCP formulation, nothing to do" -function constraint_mc_bus_voltage_on_off(pm::_PMs.AbstractDCPModel; nw::Int=pm.cnw, kwargs...) +function constraint_mc_bus_voltage_on_off(pm::_PM.AbstractDCPModel; nw::Int=pm.cnw, kwargs...) end "" -function variable_mc_branch_flow_active(pm::_PMs.AbstractAPLossLessModels; nw::Int=pm.cnw, bounded::Bool = true, report::Bool = true) - cnds = _PMs.conductor_ids(pm) +function variable_mc_branch_power_real(pm::_PM.AbstractAPLossLessModels; nw::Int=pm.cnw, bounded::Bool = true, report::Bool = true) + cnds = conductor_ids(pm) ncnds = length(cnds) p = Dict((l,i,j) => JuMP.@variable(pm.model, [c in 1:ncnds], base_name="$(nw)_($l,$i,$j)_p", - start = comp_start_value(_PMs.ref(pm, nw, :branch, l), "p_start", c, 0.0) - ) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from)) + start = comp_start_value(ref(pm, nw, :branch, l), "p_start", c, 0.0) + ) for (l,i,j) in ref(pm, nw, :arcs_from)) if bounded - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from) - smax = _calc_branch_power_max(_PMs.ref(pm, nw, :branch, l), _PMs.ref(pm, nw, :bus, i)) + for (l,i,j) in ref(pm, nw, :arcs_from) + smax = _calc_branch_power_max(ref(pm, nw, :branch, l), ref(pm, nw, :bus, i)) if !ismissing(smax) - JuMP.set_upper_bound.(p[(l,i,j)], smax) - JuMP.set_lower_bound.(p[(l,i,j)], -smax) + set_upper_bound.(p[(l,i,j)], smax) + set_lower_bound.(p[(l,i,j)], -smax) end end end - for (l,branch) in _PMs.ref(pm, nw, :branch) + for (l,branch) in ref(pm, nw, :branch) if haskey(branch, "pf_start") f_idx = (l, branch["f_bus"], branch["t_bus"]) JuMP.set_start_value(p[f_idx], branch["pf_start"]) @@ -98,7 +97,7 @@ function variable_mc_branch_flow_active(pm::_PMs.AbstractAPLossLessModels; nw::I end # this explicit type erasure is necessary - p_expr = Dict{Any,Any}( ((l,i,j), p[(l,i,j)]) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from) ) - p_expr = merge(p_expr, Dict( ((l,j,i), -1.0*p[(l,i,j)]) for (l,i,j) in _PMs.ref(pm, nw, :arcs_from))) - _PMs.var(pm, nw)[:p] = p_expr + p_expr = Dict{Any,Any}( ((l,i,j), p[(l,i,j)]) for (l,i,j) in ref(pm, nw, :arcs_from) ) + p_expr = merge(p_expr, Dict( ((l,j,i), -1.0*p[(l,i,j)]) for (l,i,j) in ref(pm, nw, :arcs_from))) + var(pm, nw)[:p] = p_expr end diff --git a/src/form/ivr.jl b/src/form/ivr.jl index 021a44de1..8d8f14e7b 100644 --- a/src/form/ivr.jl +++ b/src/form/ivr.jl @@ -3,7 +3,7 @@ # in the context of constant-power loads or generators "" -function variable_mc_branch_current(pm::_PMs.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) +function variable_mc_branch_current(pm::_PM.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) variable_mc_branch_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) variable_mc_branch_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) @@ -11,34 +11,34 @@ function variable_mc_branch_current(pm::_PMs.AbstractIVRModel; nw::Int=pm.cnw, b p = Dict() q = Dict() - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from) - vr_fr = _PMs.var(pm, nw, :vr, i) - vi_fr = _PMs.var(pm, nw, :vi, i) - cr_fr = _PMs.var(pm, nw, :cr, (l,i,j)) - ci_fr = _PMs.var(pm, nw, :ci, (l,i,j)) + for (l,i,j) in ref(pm, nw, :arcs_from) + vr_fr = var(pm, nw, :vr, i) + vi_fr = var(pm, nw, :vi, i) + cr_fr = var(pm, nw, :cr, (l,i,j)) + ci_fr = var(pm, nw, :ci, (l,i,j)) - vr_to = _PMs.var(pm, nw, :vr, j) - vi_to = _PMs.var(pm, nw, :vi, j) - cr_to = _PMs.var(pm, nw, :cr, (l,j,i)) - ci_to = _PMs.var(pm, nw, :ci, (l,j,i)) + vr_to = var(pm, nw, :vr, j) + vi_to = var(pm, nw, :vi, j) + cr_to = var(pm, nw, :cr, (l,j,i)) + ci_to = var(pm, nw, :ci, (l,j,i)) p[(l,i,j)] = vr_fr.*cr_fr + vi_fr.*ci_fr q[(l,i,j)] = vi_fr.*cr_fr - vr_fr.*ci_fr p[(l,j,i)] = vr_to.*cr_to + vi_to.*ci_to q[(l,j,i)] = vi_to.*cr_to - vr_to.*ci_to end - _PMs.var(pm, nw)[:p] = p - _PMs.var(pm, nw)[:q] = q - report && _PMs.sol_component_value_edge(pm, nw, :branch, :pf, :pt, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), p) - report && _PMs.sol_component_value_edge(pm, nw, :branch, :qf, :qt, _PMs.ref(pm, nw, :arcs_from), _PMs.ref(pm, nw, :arcs_to), q) + var(pm, nw)[:p] = p + var(pm, nw)[:q] = q + report && _IM.sol_component_value_edge(pm, nw, :branch, :pf, :pt, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), p) + report && _IM.sol_component_value_edge(pm, nw, :branch, :qf, :qt, ref(pm, nw, :arcs_from), ref(pm, nw, :arcs_to), q) - variable_mc_branch_series_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) - variable_mc_branch_series_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) + variable_mc_branch_current_series_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) + variable_mc_branch_current_series_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) end "" -function variable_mc_transformer_current(pm::_PMs.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) +function variable_mc_transformer_current(pm::_PM.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) variable_mc_transformer_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) variable_mc_transformer_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) @@ -49,101 +49,101 @@ function variable_mc_transformer_current(pm::_PMs.AbstractIVRModel; nw::Int=pm.c f_cnd = [1, 2, 3] #TODO extend to this being passed in constraint template t_cnd = [1, 2, 3] #TODO extend to this being passed in constraint template - for (l,i,j) in _PMs.ref(pm, nw, :arcs_from_trans) - vr_fr = _PMs.var(pm, nw, :vr, i)[f_cnd] - vi_fr = _PMs.var(pm, nw, :vi, i)[f_cnd] - cr_fr = _PMs.var(pm, nw, :crt, (l,i,j)) - ci_fr = _PMs.var(pm, nw, :cit, (l,i,j)) + for (l,i,j) in ref(pm, nw, :arcs_from_trans) + vr_fr = var(pm, nw, :vr, i)[f_cnd] + vi_fr = var(pm, nw, :vi, i)[f_cnd] + cr_fr = var(pm, nw, :crt, (l,i,j)) + ci_fr = var(pm, nw, :cit, (l,i,j)) - vr_to = _PMs.var(pm, nw, :vr, j)[t_cnd] - vi_to = _PMs.var(pm, nw, :vi, j)[t_cnd] - cr_to = _PMs.var(pm, nw, :crt, (l,j,i)) - ci_to = _PMs.var(pm, nw, :cit, (l,j,i)) + vr_to = var(pm, nw, :vr, j)[t_cnd] + vi_to = var(pm, nw, :vi, j)[t_cnd] + cr_to = var(pm, nw, :crt, (l,j,i)) + ci_to = var(pm, nw, :cit, (l,j,i)) p[(l,i,j)] = vr_fr.*cr_fr + vi_fr.*ci_fr q[(l,i,j)] = vi_fr.*cr_fr - vr_fr.*ci_fr p[(l,j,i)] = vr_to.*cr_to + vi_to.*ci_to q[(l,j,i)] = vi_to.*cr_to - vr_to.*ci_to end - _PMs.var(pm, nw)[:p] = p - _PMs.var(pm, nw)[:q] = q - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :pf, :pt, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), p) - report && _PMs.sol_component_value_edge(pm, nw, :transformer, :qf, :qt, _PMs.ref(pm, nw, :arcs_from_trans), _PMs.ref(pm, nw, :arcs_to_trans), q) + var(pm, nw)[:p] = p + var(pm, nw)[:q] = q + report && _IM.sol_component_value_edge(pm, nw, :transformer, :pf, :pt, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), p) + report && _IM.sol_component_value_edge(pm, nw, :transformer, :qf, :qt, ref(pm, nw, :arcs_from_trans), ref(pm, nw, :arcs_to_trans), q) end "" -function variable_mc_load(pm::_PMs.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) - _PMs.var(pm, nw)[:crd] = Dict{Int, Any}() - _PMs.var(pm, nw)[:cid] = Dict{Int, Any}() - _PMs.var(pm, nw)[:crd_bus] = Dict{Int, Any}() - _PMs.var(pm, nw)[:cid_bus] = Dict{Int, Any}() +function variable_mc_load_setpoint(pm::_PM.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) + var(pm, nw)[:crd] = Dict{Int, Any}() + var(pm, nw)[:cid] = Dict{Int, Any}() + var(pm, nw)[:crd_bus] = Dict{Int, Any}() + var(pm, nw)[:cid_bus] = Dict{Int, Any}() end + "" -function variable_mc_generation(pm::_PMs.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) - variable_mc_generation_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) - variable_mc_generation_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) +function variable_mc_gen_power_setpoint(pm::_PM.AbstractIVRModel; nw::Int=pm.cnw, bounded::Bool=true, report::Bool=true, kwargs...) + variable_mc_gen_current_setpoint_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) + variable_mc_gen_current_setpoint_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) - cnds = _PMs.conductor_ids(pm; nw=nw) + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) - _PMs.var(pm, nw)[:crg_bus] = Dict{Int, Any}() - _PMs.var(pm, nw)[:cig_bus] = Dict{Int, Any}() + var(pm, nw)[:crg_bus] = Dict{Int, Any}() + var(pm, nw)[:cig_bus] = Dict{Int, Any}() # store active and reactive power expressions for use in objective + post processing - _PMs.var(pm, nw)[:pg] = Dict{Int, Any}() - _PMs.var(pm, nw)[:qg] = Dict{Int, Any}() + var(pm, nw)[:pg] = Dict{Int, Any}() + var(pm, nw)[:qg] = Dict{Int, Any}() end "" -function variable_mc_voltage(pm::_PMs.AbstractIVRModel; nw=pm.cnw, bounded::Bool=true, kwargs...) - variable_mc_voltage_real(pm; nw=nw, bounded=bounded, kwargs...) - variable_mc_voltage_imaginary(pm; nw=nw, bounded=bounded, kwargs...) +function variable_mc_bus_voltage(pm::_PM.AbstractIVRModel; nw=pm.cnw, bounded::Bool=true, kwargs...) + variable_mc_bus_voltage_real(pm; nw=nw, bounded=bounded, kwargs...) + variable_mc_bus_voltage_imaginary(pm; nw=nw, bounded=bounded, kwargs...) # local infeasbility issues without proper initialization; # convergence issues start when the equivalent angles of the starting point # are further away than 90 degrees from the solution (as given by ACP) - # this is the default behaviour of _PMs, initialize all phases as (1,0) + # this is the default behaviour of _PM, initialize all phases as (1,0) # the magnitude seems to have little effect on the convergence (>0.05) # updating the starting point to a balanced phasor does the job - ncnd = length(_PMs.conductor_ids(pm)) + ncnd = length(conductor_ids(pm)) theta = [_wrap_to_pi(2 * pi / ncnd * (1-c)) for c in 1:ncnd] vm = 1 - for id in _PMs.ids(pm, nw, :bus) - busref = _PMs.ref(pm, nw, :bus, id) + for id in ids(pm, nw, :bus) + busref = ref(pm, nw, :bus, id) if !haskey(busref, "va_start") for c in 1:ncnd vr = vm*cos(theta[c]) vi = vm*sin(theta[c]) - JuMP.set_start_value(_PMs.var(pm, nw, :vr, id)[c], vr) - JuMP.set_start_value(_PMs.var(pm, nw, :vi, id)[c], vi) + JuMP.set_start_value(var(pm, nw, :vr, id)[c], vr) + JuMP.set_start_value(var(pm, nw, :vi, id)[c], vi) end end end # apply bounds if bounded if bounded - for i in _PMs.ids(pm, nw, :bus) + for i in ids(pm, nw, :bus) constraint_mc_voltage_magnitude_bounds(pm, i, nw=nw) end end end -""" -Defines how current distributes over series and shunt impedances of a pi-model branch -""" -function constraint_mc_current_from(pm::_PMs.AbstractIVRModel, n::Int, f_bus, f_idx, g_sh_fr, b_sh_fr, tr, ti, tm) - vr_fr = _PMs.var(pm, n, :vr, f_bus) - vi_fr = _PMs.var(pm, n, :vi, f_bus) - csr_fr = _PMs.var(pm, n, :csr, f_idx[1]) - csi_fr = _PMs.var(pm, n, :csi, f_idx[1]) +"Defines how current distributes over series and shunt impedances of a pi-model branch" +function constraint_mc_current_from(pm::_PM.AbstractIVRModel, n::Int, f_bus, f_idx, g_sh_fr, b_sh_fr, tr, ti, tm) + vr_fr = var(pm, n, :vr, f_bus) + vi_fr = var(pm, n, :vi, f_bus) - cr_fr = _PMs.var(pm, n, :cr, f_idx) - ci_fr = _PMs.var(pm, n, :ci, f_idx) + csr_fr = var(pm, n, :csr, f_idx[1]) + csi_fr = var(pm, n, :csi, f_idx[1]) + + cr_fr = var(pm, n, :cr, f_idx) + ci_fr = var(pm, n, :ci, f_idx) tr = tr ti = ti @@ -155,18 +155,17 @@ function constraint_mc_current_from(pm::_PMs.AbstractIVRModel, n::Int, f_bus, f_ JuMP.@constraint(pm.model, ci_fr .== (tr.*csi_fr + ti.*csr_fr + g_sh_fr*vi_fr + b_sh_fr*vr_fr)./tm.^2) end -""" -Defines how current distributes over series and shunt impedances of a pi-model branch -""" -function constraint_mc_current_to(pm::_PMs.AbstractIVRModel, n::Int, t_bus, f_idx, t_idx, g_sh_to, b_sh_to) - vr_to = _PMs.var(pm, n, :vr, t_bus) - vi_to = _PMs.var(pm, n, :vi, t_bus) - csr_to = -_PMs.var(pm, n, :csr, f_idx[1]) - csi_to = -_PMs.var(pm, n, :csi, f_idx[1]) +"Defines how current distributes over series and shunt impedances of a pi-model branch" +function constraint_mc_current_to(pm::_PM.AbstractIVRModel, n::Int, t_bus, f_idx, t_idx, g_sh_to, b_sh_to) + vr_to = var(pm, n, :vr, t_bus) + vi_to = var(pm, n, :vi, t_bus) - cr_to = _PMs.var(pm, n, :cr, t_idx) - ci_to = _PMs.var(pm, n, :ci, t_idx) + csr_to = -var(pm, n, :csr, f_idx[1]) + csi_to = -var(pm, n, :csi, f_idx[1]) + + cr_to = var(pm, n, :cr, t_idx) + ci_to = var(pm, n, :ci, t_idx) g_sh_to = g_sh_to b_sh_to = b_sh_to @@ -176,18 +175,16 @@ function constraint_mc_current_to(pm::_PMs.AbstractIVRModel, n::Int, t_bus, f_id end -""" -Defines voltage drop over a branch, linking from and to side complex voltage -""" -function constraint_mc_voltage_drop(pm::_PMs.AbstractIVRModel, n::Int, i, f_bus, t_bus, f_idx, r, x, tr, ti, tm) - vr_fr = _PMs.var(pm, n, :vr, f_bus) - vi_fr = _PMs.var(pm, n, :vi, f_bus) +"Defines voltage drop over a branch, linking from and to side complex voltage" +function constraint_mc_bus_voltage_drop(pm::_PM.AbstractIVRModel, n::Int, i, f_bus, t_bus, f_idx, r, x, tr, ti, tm) + vr_fr = var(pm, n, :vr, f_bus) + vi_fr = var(pm, n, :vi, f_bus) - vr_to = _PMs.var(pm, n, :vr, t_bus) - vi_to = _PMs.var(pm, n, :vi, t_bus) + vr_to = var(pm, n, :vr, t_bus) + vi_to = var(pm, n, :vi, t_bus) - csr_fr = _PMs.var(pm, n, :csr, f_idx[1]) - csi_fr = _PMs.var(pm, n, :csi, f_idx[1]) + csr_fr = var(pm, n, :csr, f_idx[1]) + csi_fr = var(pm, n, :csi, f_idx[1]) r = r x = x @@ -196,16 +193,15 @@ function constraint_mc_voltage_drop(pm::_PMs.AbstractIVRModel, n::Int, i, f_bus, JuMP.@constraint(pm.model, vi_to .== (vi_fr.*tr - vr_fr.*ti)./tm.^2 - r*csi_fr - x*csr_fr) end -""" -Bounds the voltage angle difference between bus pairs -""" -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractIVRModel, n::Int, f_idx, angmin, angmax) + +"Bounds the voltage angle difference between bus pairs" +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractIVRModel, n::Int, f_idx, angmin, angmax) i, f_bus, t_bus = f_idx - vr_fr = _PMs.var(pm, n, :vr, f_bus) - vi_fr = _PMs.var(pm, n, :vi, f_bus) - vr_to = _PMs.var(pm, n, :vr, t_bus) - vi_to = _PMs.var(pm, n, :vi, t_bus) + vr_fr = var(pm, n, :vr, f_bus) + vi_fr = var(pm, n, :vi, f_bus) + vr_to = var(pm, n, :vr, t_bus) + vi_to = var(pm, n, :vi, t_bus) vvr = vr_fr.*vr_to + vi_fr.*vi_to vvi = vi_fr.*vr_to - vr_fr.*vi_to @@ -213,28 +209,29 @@ function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractIVRModel, n::In JuMP.@constraint(pm.model, tan.(angmax).*vvr .>= vvi) end + """ Kirchhoff's current law applied to buses `sum(cr + im*ci) = 0` """ -function constraint_mc_current_balance_load(pm::_PMs.AbstractIVRModel, n::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) - vr = _PMs.var(pm, n, :vr, i) - vi = _PMs.var(pm, n, :vi, i) - - cr = get(_PMs.var(pm, n), :cr, Dict()); _PMs._check_var_keys(cr, bus_arcs, "real current", "branch") - ci = get(_PMs.var(pm, n), :ci, Dict()); _PMs._check_var_keys(ci, bus_arcs, "imaginary current", "branch") - crd = get(_PMs.var(pm, n), :crd_bus, Dict()); _PMs._check_var_keys(crd, bus_loads, "real current", "load") - cid = get(_PMs.var(pm, n), :cid_bus, Dict()); _PMs._check_var_keys(cid, bus_loads, "imaginary current", "load") - crg = get(_PMs.var(pm, n), :crg_bus, Dict()); _PMs._check_var_keys(crg, bus_gens, "real current", "generator") - cig = get(_PMs.var(pm, n), :cig_bus, Dict()); _PMs._check_var_keys(cig, bus_gens, "imaginary current", "generator") - crs = get(_PMs.var(pm, n), :crs, Dict()); _PMs._check_var_keys(crs, bus_storage, "real currentr", "storage") - cis = get(_PMs.var(pm, n), :cis, Dict()); _PMs._check_var_keys(cis, bus_storage, "imaginary current", "storage") - crsw = get(_PMs.var(pm, n), :crsw, Dict()); _PMs._check_var_keys(crsw, bus_arcs_sw, "real current", "switch") - cisw = get(_PMs.var(pm, n), :cisw, Dict()); _PMs._check_var_keys(cisw, bus_arcs_sw, "imaginary current", "switch") - crt = get(_PMs.var(pm, n), :crt, Dict()); _PMs._check_var_keys(crt, bus_arcs_trans, "real current", "transformer") - cit = get(_PMs.var(pm, n), :cit, Dict()); _PMs._check_var_keys(cit, bus_arcs_trans, "imaginary current", "transformer") - - cnds = _PMs.conductor_ids(pm; nw=n) +function constraint_mc_load_current_balance(pm::_PM.AbstractIVRModel, n::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + vr = var(pm, n, :vr, i) + vi = var(pm, n, :vi, i) + + cr = get(var(pm, n), :cr, Dict()); _PM._check_var_keys(cr, bus_arcs, "real current", "branch") + ci = get(var(pm, n), :ci, Dict()); _PM._check_var_keys(ci, bus_arcs, "imaginary current", "branch") + crd = get(var(pm, n), :crd_bus, Dict()); _PM._check_var_keys(crd, bus_loads, "real current", "load") + cid = get(var(pm, n), :cid_bus, Dict()); _PM._check_var_keys(cid, bus_loads, "imaginary current", "load") + crg = get(var(pm, n), :crg_bus, Dict()); _PM._check_var_keys(crg, bus_gens, "real current", "generator") + cig = get(var(pm, n), :cig_bus, Dict()); _PM._check_var_keys(cig, bus_gens, "imaginary current", "generator") + crs = get(var(pm, n), :crs, Dict()); _PM._check_var_keys(crs, bus_storage, "real currentr", "storage") + cis = get(var(pm, n), :cis, Dict()); _PM._check_var_keys(cis, bus_storage, "imaginary current", "storage") + crsw = get(var(pm, n), :crsw, Dict()); _PM._check_var_keys(crsw, bus_arcs_sw, "real current", "switch") + cisw = get(var(pm, n), :cisw, Dict()); _PM._check_var_keys(cisw, bus_arcs_sw, "imaginary current", "switch") + crt = get(var(pm, n), :crt, Dict()); _PM._check_var_keys(crt, bus_arcs_trans, "real current", "transformer") + cit = get(var(pm, n), :cit, Dict()); _PM._check_var_keys(cit, bus_arcs_trans, "imaginary current", "transformer") + + cnds = conductor_ids(pm; nw=n) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -262,16 +259,17 @@ function constraint_mc_current_balance_load(pm::_PMs.AbstractIVRModel, n::Int, i end end + "`p[f_idx]^2 + q[f_idx]^2 <= rate_a^2`" -function constraint_mc_thermal_limit_from(pm::_PMs.AbstractIVRModel, n::Int, f_idx, rate_a) +function constraint_mc_thermal_limit_from(pm::_PM.AbstractIVRModel, n::Int, f_idx, rate_a) (l, f_bus, t_bus) = f_idx - vr = _PMs.var(pm, n, :vr, f_bus) - vi = _PMs.var(pm, n, :vi, f_bus) - crf = _PMs.var(pm, n, :cr, f_idx) - cif = _PMs.var(pm, n, :ci, f_idx) + vr = var(pm, n, :vr, f_bus) + vi = var(pm, n, :vi, f_bus) + crf = var(pm, n, :cr, f_idx) + cif = var(pm, n, :ci, f_idx) - cnds = _PMs.conductor_ids(pm; nw=n) + cnds = conductor_ids(pm; nw=n) ncnds = length(cnds) for c in cnds @@ -279,16 +277,17 @@ function constraint_mc_thermal_limit_from(pm::_PMs.AbstractIVRModel, n::Int, f_i end end + "`p[t_idx]^2 + q[t_idx]^2 <= rate_a^2`" -function constraint_mc_thermal_limit_to(pm::_PMs.AbstractIVRModel, n::Int, t_idx, rate_a) +function constraint_mc_thermal_limit_to(pm::_PM.AbstractIVRModel, n::Int, t_idx, rate_a) (l, t_bus, f_bus) = t_idx - vr = _PMs.var(pm, n, :vr, t_bus) - vi = _PMs.var(pm, n, :vi, t_bus) - crt = _PMs.var(pm, n, :cr, t_idx) - cit = _PMs.var(pm, n, :ci, t_idx) + vr = var(pm, n, :vr, t_bus) + vi = var(pm, n, :vi, t_bus) + crt = var(pm, n, :cr, t_idx) + cit = var(pm, n, :ci, t_idx) - cnds = _PMs.conductor_ids(pm; nw=n) + cnds = conductor_ids(pm; nw=n) ncnds = length(cnds) for c in cnds @@ -296,153 +295,103 @@ function constraint_mc_thermal_limit_to(pm::_PMs.AbstractIVRModel, n::Int, t_idx end end + """ Bounds the current magnitude at both from and to side of a branch `cr[f_idx]^2 + ci[f_idx]^2 <= c_rating_a^2` `cr[t_idx]^2 + ci[t_idx]^2 <= c_rating_a^2` """ -function constraint_mc_current_limit(pm::_PMs.AbstractIVRModel, n::Int, f_idx, c_rating_a) +function constraint_mc_current_limit(pm::_PM.AbstractIVRModel, n::Int, f_idx, c_rating_a) (l, f_bus, t_bus) = f_idx t_idx = (l, t_bus, f_bus) - crf = _PMs.var(pm, n, :cr, f_idx) - cif = _PMs.var(pm, n, :ci, f_idx) + crf = var(pm, n, :cr, f_idx) + cif = var(pm, n, :ci, f_idx) - crt = _PMs.var(pm, n, :cr, t_idx) - cit = _PMs.var(pm, n, :ci, t_idx) + crt = var(pm, n, :cr, t_idx) + cit = var(pm, n, :ci, t_idx) JuMP.@constraint(pm.model, crf.^2 + cif.^2 .<= c_rating_a.^2) JuMP.@constraint(pm.model, crt.^2 + cit.^2 .<= c_rating_a.^2) end + """ `pmin <= Re(v*cg') <= pmax` """ -function constraint_mc_generation_active_power_limits(pm::_PMs.AbstractIVRModel, n::Int, i, bus, pmax, pmin) +function constraint_mc_gen_active_bounds(pm::_PM.AbstractIVRModel, n::Int, i, bus, pmax, pmin) @assert pmin <= pmax - vr = _PMs.var(pm, n, :vr, bus) - vi = _PMs.var(pm, n, :vi, bus) - cr = _PMs.var(pm, n, :crg, i) - ci = _PMs.var(pm, n, :cig, i) + vr = var(pm, n, :vr, bus) + vi = var(pm, n, :vi, bus) + cr = var(pm, n, :crg, i) + ci = var(pm, n, :cig, i) JuMP.@constraint(pm.model, pmin .<= vr.*cr + vi.*ci) JuMP.@constraint(pm.model, pmax .>= vr.*cr + vi.*ci) end + """ `qmin <= Im(v*cg') <= qmax` """ -function constraint_mc_generation_reactive_power_limits(pm::_PMs.AbstractIVRModel, n::Int, i, bus, qmax, qmin) +function constraint_mc_gen_reactive_bounds(pm::_PM.AbstractIVRModel, n::Int, i, bus, qmax, qmin) @assert qmin <= qmax - vr = _PMs.var(pm, n, :vr, bus) - vi = _PMs.var(pm, n, :vi, bus) - cr = _PMs.var(pm, n, :crg, i) - ci = _PMs.var(pm, n, :cig, i) + vr = var(pm, n, :vr, bus) + vi = var(pm, n, :vi, bus) + cr = var(pm, n, :crg, i) + ci = var(pm, n, :cig, i) JuMP.@constraint(pm.model, qmin .<= vi.*cr - vr.*ci) JuMP.@constraint(pm.model, qmax .>= vi.*cr - vr.*ci) end + "`pg[i] == pg`" -function constraint_mc_active_gen_setpoint(pm::_PMs.AbstractIVRModel, n::Int, i, pgref) - gen = _PMs.ref(pm, n, :gen, i) +function constraint_mc_gen_power_setpoint_real(pm::_PM.AbstractIVRModel, n::Int, i, pgref) + gen = ref(pm, n, :gen, i) bus = gen["gen_bus"] - vr = _PMs.var(pm, n, :vr, bus) - vi = _PMs.var(pm, n, :vi, bus) - cr = _PMs.var(pm, n, :crg, i) - ci = _PMs.var(pm, n, :cig, i) + vr = var(pm, n, :vr, bus) + vi = var(pm, n, :vi, bus) + cr = var(pm, n, :crg, i) + ci = var(pm, n, :cig, i) JuMP.@constraint(pm.model, pgref .== vr.*cr + vi.*ci) end + "`qq[i] == qq`" -function constraint_mc_reactive_gen_setpoint(pm::_PMs.AbstractIVRModel, n::Int, i, qgref) - gen = _PMs.ref(pm, n, :gen, i) +function constraint_mc_regen_setpoint_active(pm::_PM.AbstractIVRModel, n::Int, i, qgref) + gen = ref(pm, n, :gen, i) bus = gen["gen_bus"] - vr = _PMs.var(pm, n, :vr, bus) - vi = _PMs.var(pm, n, :vi, bus) - cr = _PMs.var(pm, n, :crg, i) - ci = _PMs.var(pm, n, :cig, i) + vr = var(pm, n, :vr, bus) + vi = var(pm, n, :vi, bus) + cr = var(pm, n, :crg, i) + ci = var(pm, n, :cig, i) JuMP.@constraint(pm.model, qgref .== vi.*cr - vr.*ci) end -function _PMs._objective_min_fuel_cost_polynomial_linquad(pm::_PMs.AbstractIVRModel; report::Bool=true) - gen_cost = Dict() - dcline_cost = Dict() - - for (n, nw_ref) in _PMs.nws(pm) - for (i,gen) in nw_ref[:gen] - bus = gen["gen_bus"] - - #to avoid function calls inside of @NLconstraint: - pg = _PMs.var(pm, n, :pg, i) - nc = length(_PMs.conductor_ids(pm, n)) - if length(gen["cost"]) == 1 - gen_cost[(n,i)] = gen["cost"][1] - elseif length(gen["cost"]) == 2 - gen_cost[(n,i)] = JuMP.@NLexpression(pm.model, gen["cost"][1]*sum(pg[c] for c in 1:nc) + gen["cost"][2]) - elseif length(gen["cost"]) == 3 - gen_cost[(n,i)] = JuMP.@NLexpression(pm.model, gen["cost"][1]*sum(pg[c] for c in 1:nc)^2 + gen["cost"][2]*sum(pg[c] for c in 1:nc) + gen["cost"][3]) - else - gen_cost[(n,i)] = 0.0 - end - end - end - - return JuMP.@NLobjective(pm.model, Min, - sum( - sum( gen_cost[(n,i)] for (i,gen) in nw_ref[:gen] ) - + sum( dcline_cost[(n,i)] for (i,dcline) in nw_ref[:dcline] ) - for (n, nw_ref) in _PMs.nws(pm)) - ) -end - - -"adds pg_cost variables and constraints" -function objective_variable_pg_cost(pm::_PMs.AbstractIVRModel; report::Bool=true) - for (n, nw_ref) in _PMs.nws(pm) - gen_lines = calc_cost_pwl_lines(nw_ref[:gen]) - - #to avoid function calls inside of @NLconstraint - pg_cost = _PMs.var(pm, n)[:pg_cost] = JuMP.@variable(pm.model, - [i in _PMs.ids(pm, n, :gen)], base_name="$(n)_pg_cost", - ) - report && _PMs.sol_component_value(pm, n, :gen, :pg_cost, _PMs.ids(pm, n, :gen), pg_cost) - - nc = length(conductor_ids(pm, n)) - - # gen pwl cost - for (i, gen) in nw_ref[:gen] - pg = var(pm, n, :pg, i) - for line in gen_lines[i] - JuMP.@NLconstraint(pm.model, pg_cost[i] >= line.slope*sum(pg[c] for c in 1:nc) + line.intercept) - end - end - end -end - - -function constraint_mc_trans_yy(pm::_PMs.AbstractIVRModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - vr_fr_P = _PMs.var(pm, nw, :vr, f_bus)[f_cnd] - vi_fr_P = _PMs.var(pm, nw, :vi, f_bus)[f_cnd] +"wye-wye transformer power constraint for IVR formulation" +function constraint_mc_transformer_power_yy(pm::_PM.AbstractIVRModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + vr_fr_P = var(pm, nw, :vr, f_bus)[f_cnd] + vi_fr_P = var(pm, nw, :vi, f_bus)[f_cnd] vr_fr_n = 0 vi_fr_n = 0 - vr_to_P = _PMs.var(pm, nw, :vr, t_bus)[t_cnd] - vi_to_P = _PMs.var(pm, nw, :vi, t_bus)[t_cnd] + vr_to_P = var(pm, nw, :vr, t_bus)[t_cnd] + vi_to_P = var(pm, nw, :vi, t_bus)[t_cnd] vr_to_n = 0 vi_to_n = 0 - cr_fr_P = _PMs.var(pm, nw, :crt, f_idx)[f_cnd] - ci_fr_P = _PMs.var(pm, nw, :cit, f_idx)[f_cnd] - cr_to_P = _PMs.var(pm, nw, :crt, t_idx)[t_cnd] - ci_to_P = _PMs.var(pm, nw, :cit, t_idx)[t_cnd] + cr_fr_P = var(pm, nw, :crt, f_idx)[f_cnd] + ci_fr_P = var(pm, nw, :cit, f_idx)[f_cnd] + cr_to_P = var(pm, nw, :crt, t_idx)[t_cnd] + ci_to_P = var(pm, nw, :cit, t_idx)[t_cnd] # construct tm as a parameter or scaled variable depending on whether it is fixed or not - tm = [tm_fixed[p] ? tm_set[p] : _PMs.var(pm, nw, :tap, trans_id)[p] for p in _PMs.conductor_ids(pm)] + tm = [tm_fixed[p] ? tm_set[p] : var(pm, nw, :tap, trans_id)[p] for p in conductor_ids(pm)] scale = (tm_scale*pol).*tm_set JuMP.@constraint(pm.model, (vr_fr_P.-vr_fr_n) .== scale.*(vr_to_P.-vr_to_n)) @@ -452,21 +401,22 @@ function constraint_mc_trans_yy(pm::_PMs.AbstractIVRModel, nw::Int, trans_id::In end -function constraint_mc_trans_dy(pm::_PMs.AbstractIVRModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) - vr_fr_P = _PMs.var(pm, nw, :vr, f_bus)[f_cnd] - vi_fr_P = _PMs.var(pm, nw, :vi, f_bus)[f_cnd] - vr_to_P = _PMs.var(pm, nw, :vr, t_bus)[t_cnd] - vi_to_P = _PMs.var(pm, nw, :vi, t_bus)[t_cnd] +"delta-wye transformer power constraint for IVR formulation" +function constraint_mc_transformer_power_dy(pm::_PM.AbstractIVRModel, nw::Int, trans_id::Int, f_bus::Int, t_bus::Int, f_idx, t_idx, f_cnd, t_cnd, pol, tm_set, tm_fixed, tm_scale) + vr_fr_P = var(pm, nw, :vr, f_bus)[f_cnd] + vi_fr_P = var(pm, nw, :vi, f_bus)[f_cnd] + vr_to_P = var(pm, nw, :vr, t_bus)[t_cnd] + vi_to_P = var(pm, nw, :vi, t_bus)[t_cnd] vr_to_n = 0 vi_to_n = 0 - cr_fr_P = _PMs.var(pm, nw, :crt, f_idx)[f_cnd] - ci_fr_P = _PMs.var(pm, nw, :cit, f_idx)[f_cnd] - cr_to_P = _PMs.var(pm, nw, :crt, t_idx)[t_cnd] - ci_to_P = _PMs.var(pm, nw, :cit, t_idx)[t_cnd] + cr_fr_P = var(pm, nw, :crt, f_idx)[f_cnd] + ci_fr_P = var(pm, nw, :cit, f_idx)[f_cnd] + cr_to_P = var(pm, nw, :crt, t_idx)[t_cnd] + ci_to_P = var(pm, nw, :cit, t_idx)[t_cnd] # construct tm as a parameter or scaled variable depending on whether it is fixed or not - tm = [tm_fixed[p] ? tm_set[p] : _PMs.var(pm, nw, :tap, trans_id)[p] for p in _PMs.conductor_ids(pm)] + tm = [tm_fixed[p] ? tm_set[p] : var(pm, nw, :tap, trans_id)[p] for p in conductor_ids(pm)] scale = (tm_scale*pol).*tm_set n_phases = length(tm) @@ -480,10 +430,10 @@ function constraint_mc_trans_dy(pm::_PMs.AbstractIVRModel, nw::Int, trans_id::In end -"" -function constraint_mc_load_wye(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) +"wye connected load setpoint constraint for IVR formulation" +function constraint_mc_load_setpoint_wye(pm::_PM.IVRPowerModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) nph = 3 @@ -496,31 +446,31 @@ function constraint_mc_load_wye(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_id -b[i]*vr[i]*(vr[i]^2+vi[i]^2)^(beta[i]/2 -1) ) - _PMs.var(pm, nw, :crd_bus)[id] = crd - _PMs.var(pm, nw, :cid_bus)[id] = cid + var(pm, nw, :crd_bus)[id] = crd + var(pm, nw, :cid_bus)[id] = cid if report pd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vr[i]*crd[i]+vi[i]*cid[i]) qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vr[i]*cid[i]+vi[i]*crd[i]) - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus - _PMs.sol(pm, nw, :load, id)[:crd_bus] = crd - _PMs.sol(pm, nw, :load, id)[:cid_bus] = cid + sol(pm, nw, :load, id)[:crd_bus] = crd + sol(pm, nw, :load, id)[:cid_bus] = cid pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*(vr[i]^2+vi[i]^2)^(alpha[i]/2) ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*(vr[i]^2+vi[i]^2)^(beta[i]/2) ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end -"" -function constraint_mc_load_delta(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) +"delta connected load setpoint constraint for IVR formulation" +function constraint_mc_load_setpoint_delta(pm::_PM.IVRPowerModel, nw::Int, id::Int, bus_id::Int, a::Vector{<:Real}, alpha::Vector{<:Real}, b::Vector{<:Real}, beta::Vector{<:Real}; report::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) nph = 3 prev = Dict(i=>(i+nph-2)%nph+1 for i in 1:nph) @@ -541,30 +491,30 @@ function constraint_mc_load_delta(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_ crd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], crd[i]-crd[prev[i]]) cid_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], cid[i]-cid[prev[i]]) - _PMs.var(pm, nw, :crd_bus)[id] = crd_bus - _PMs.var(pm, nw, :cid_bus)[id] = cid_bus + var(pm, nw, :crd_bus)[id] = crd_bus + var(pm, nw, :cid_bus)[id] = cid_bus if report pd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], vr[i]*crd_bus[i]+vi[i]*cid_bus[i]) qd_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], -vr[i]*cid_bus[i]+vi[i]*crd_bus[i]) - _PMs.sol(pm, nw, :load, id)[:pd_bus] = pd_bus - _PMs.sol(pm, nw, :load, id)[:qd_bus] = qd_bus + sol(pm, nw, :load, id)[:pd_bus] = pd_bus + sol(pm, nw, :load, id)[:qd_bus] = qd_bus pd = JuMP.@NLexpression(pm.model, [i in 1:nph], a[i]*(vrd[i]^2+vid[i]^2)^(alpha[i]/2) ) qd = JuMP.@NLexpression(pm.model, [i in 1:nph], b[i]*(vrd[i]^2+vid[i]^2)^(beta[i]/2) ) - _PMs.sol(pm, nw, :load, id)[:pd] = pd - _PMs.sol(pm, nw, :load, id)[:qd] = qd + sol(pm, nw, :load, id)[:pd] = pd + sol(pm, nw, :load, id)[:qd] = qd end end -"" -function constraint_mc_generation_wye(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) - crg = _PMs.var(pm, nw, :crg, id) - cig = _PMs.var(pm, nw, :cig, id) +"wye connected generator setpoint constraint for IVR formulation" +function constraint_mc_gen_setpoint_wye(pm::_PM.IVRPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) + crg = var(pm, nw, :crg, id) + cig = var(pm, nw, :cig, id) nph = 3 @@ -588,27 +538,27 @@ function constraint_mc_generation_wye(pm::_PMs.IVRPowerModel, nw::Int, id::Int, end end - _PMs.var(pm, nw, :crg_bus)[id] = crg - _PMs.var(pm, nw, :cig_bus)[id] = cig - _PMs.var(pm, nw, :pg)[id] = pg - _PMs.var(pm, nw, :qg)[id] = qg + var(pm, nw, :crg_bus)[id] = crg + var(pm, nw, :cig_bus)[id] = cig + var(pm, nw, :pg)[id] = pg + var(pm, nw, :qg)[id] = qg if report - _PMs.sol(pm, nw, :gen, id)[:crg_bus] = _PMs.var(pm, nw, :crg_bus, id) - _PMs.sol(pm, nw, :gen, id)[:cig_bus] = _PMs.var(pm, nw, :crg_bus, id) + sol(pm, nw, :gen, id)[:crg_bus] = var(pm, nw, :crg_bus, id) + sol(pm, nw, :gen, id)[:cig_bus] = var(pm, nw, :crg_bus, id) - _PMs.sol(pm, nw, :gen, id)[:pg] = pg - _PMs.sol(pm, nw, :gen, id)[:qg] = qg + sol(pm, nw, :gen, id)[:pg] = pg + sol(pm, nw, :gen, id)[:qg] = qg end end -"" -function constraint_mc_generation_delta(pm::_PMs.IVRPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) - vr = _PMs.var(pm, nw, :vr, bus_id) - vi = _PMs.var(pm, nw, :vi, bus_id) - crg = _PMs.var(pm, nw, :crg, id) - cig = _PMs.var(pm, nw, :cig, id) +"delta connected generator setpoint constraint for IVR formulation" +function constraint_mc_gen_setpoint_delta(pm::_PM.IVRPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + vr = var(pm, nw, :vr, bus_id) + vi = var(pm, nw, :vi, bus_id) + crg = var(pm, nw, :crg, id) + cig = var(pm, nw, :cig, id) nph = 3 prev = Dict(i=>(i+nph-2)%nph+1 for i in 1:nph) @@ -630,15 +580,15 @@ function constraint_mc_generation_delta(pm::_PMs.IVRPowerModel, nw::Int, id::Int crg_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], crg[i]-crg[prev[i]]) cig_bus = JuMP.@NLexpression(pm.model, [i in 1:nph], cig[i]-cig[prev[i]]) - _PMs.var(pm, nw, :crg_bus)[id] = crg_bus - _PMs.var(pm, nw, :cig_bus)[id] = cig_bus - _PMs.var(pm, nw, :pg)[id] = pg - _PMs.var(pm, nw, :qg)[id] = qg + var(pm, nw, :crg_bus)[id] = crg_bus + var(pm, nw, :cig_bus)[id] = cig_bus + var(pm, nw, :pg)[id] = pg + var(pm, nw, :qg)[id] = qg if report - _PMs.sol(pm, nw, :gen, id)[:crg_bus] = crg_bus - _PMs.sol(pm, nw, :gen, id)[:cig_bus] = cig_bus - _PMs.sol(pm, nw, :gen, id)[:pg] = pg - _PMs.sol(pm, nw, :gen, id)[:qg] = qg + sol(pm, nw, :gen, id)[:crg_bus] = crg_bus + sol(pm, nw, :gen, id)[:cig_bus] = cig_bus + sol(pm, nw, :gen, id)[:pg] = pg + sol(pm, nw, :gen, id)[:qg] = qg end end diff --git a/src/form/shared.jl b/src/form/shared.jl index ea7aa8b7b..bad6c4737 100644 --- a/src/form/shared.jl +++ b/src/form/shared.jl @@ -1,26 +1,28 @@ import LinearAlgebra: diag + "`vm[i] == vmref`" -function constraint_mc_voltage_magnitude_setpoint(pm::_PMs.AbstractWModels, n::Int, i::Int, vmref) - w = _PMs.var(pm, n, :w, i) +function constraint_mc_voltage_magnitude_only(pm::_PM.AbstractWModels, n::Int, i::Int, vmref) + w = var(pm, n, :w, i) JuMP.@constraint(pm.model, w .== vmref.^2) end + "" -function constraint_mc_power_balance_slack(pm::_PMs.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - w = _PMs.var(pm, nw, :w, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - p_slack = _PMs.var(pm, nw, :p_slack, i) - q_slack = _PMs.var(pm, nw, :q_slack, i) +function constraint_mc_slack_power_balance(pm::_PM.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + w = var(pm, nw, :w, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + p_slack = var(pm, nw, :p_slack, i) + q_slack = var(pm, nw, :q_slack, i) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) Bt = isempty(bus_bs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_bs)) @@ -49,24 +51,24 @@ function constraint_mc_power_balance_slack(pm::_PMs.AbstractWModels, nw::Int, i, + q_slack ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "do nothing, no way to represent this in these variables" -function constraint_mc_theta_ref(pm::_PMs.AbstractWModels, n::Int, d::Int, va_ref) +function constraint_mc_theta_ref(pm::_PM.AbstractWModels, n::Int, d::Int, va_ref) end "Creates phase angle constraints at reference buses" -function constraint_mc_theta_ref(pm::_PMs.AbstractPolarModels, n::Int, d::Int, va_ref) - cnds = _PMs.conductor_ids(pm; nw=n) +function constraint_mc_theta_ref(pm::_PM.AbstractPolarModels, n::Int, d::Int, va_ref) + cnds = conductor_ids(pm; nw=n) nconductors = length(cnds) - va = _PMs.var(pm, n, :va, d) + va = var(pm, n, :va, d) JuMP.@constraint(pm.model, va .== va_ref) end @@ -77,30 +79,30 @@ For a variable tap transformer, fix the tap variables which are fixed. For example, an OLTC where the third phase is fixed, will have tap variables for all phases, but the third tap variable should be fixed. """ -function constraint_mc_oltc_tap_fix(pm::_PMs.AbstractPowerModel, i::Int, fixed::Vector, tm::Vector; nw=pm.cnw) +function constraint_mc_oltc_tap_fix(pm::_PM.AbstractPowerModel, i::Int, fixed::Vector, tm::Vector; nw=pm.cnw) for (c,fixed) in enumerate(fixed) if fixed - JuMP.@constraint(pm.model, _PMs.var(pm, nw, c, :tap)[i]==tm[c]) + JuMP.@constraint(pm.model, var(pm, nw, c, :tap)[i]==tm[c]) end end end "KCL for load shed problem with transformers (AbstractWForms)" -function constraint_mc_power_balance_shed(pm::_PMs.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) - w = _PMs.var(pm, nw, :w, i) - p = get(_PMs.var(pm, nw), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") - q = get(_PMs.var(pm, nw), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") - pg = get(_PMs.var(pm, nw), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - psw = get(_PMs.var(pm, nw), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") - qsw = get(_PMs.var(pm, nw), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") - pt = get(_PMs.var(pm, nw), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") - qt = get(_PMs.var(pm, nw), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") - z_demand = _PMs.var(pm, nw, :z_demand) - z_shunt = _PMs.var(pm, nw, :z_shunt) +function constraint_mc_shed_power_balance(pm::_PM.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) + w = var(pm, nw, :w, i) + p = get(var(pm, nw), :p, Dict()); _PM._check_var_keys(p, bus_arcs, "active power", "branch") + q = get(var(pm, nw), :q, Dict()); _PM._check_var_keys(q, bus_arcs, "reactive power", "branch") + pg = get(var(pm, nw), :pg, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + psw = get(var(pm, nw), :psw, Dict()); _PM._check_var_keys(psw, bus_arcs_sw, "active power", "switch") + qsw = get(var(pm, nw), :qsw, Dict()); _PM._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") + pt = get(var(pm, nw), :pt, Dict()); _PM._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") + qt = get(var(pm, nw), :qt, Dict()); _PM._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") + z_demand = var(pm, nw, :z_demand) + z_shunt = var(pm, nw, :z_shunt) bus_GsBs = [(n,bus_gs[n], bus_bs[n]) for n in keys(bus_gs)] @@ -125,32 +127,32 @@ function constraint_mc_power_balance_shed(pm::_PMs.AbstractWModels, nw::Int, i, - sum(z_shunt[n].*(-w.*diag(Bt')) for (n,Gs,Bs) in bus_GsBs) ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "" -function constraint_mc_power_balance_load(pm::_PMs.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) - Wr = _PMs.var(pm, nw, :Wr, i) - Wi = _PMs.var(pm, nw, :Wi, i) - P = get(_PMs.var(pm, nw), :P, Dict()); _PMs._check_var_keys(P, bus_arcs, "active power", "branch") - Q = get(_PMs.var(pm, nw), :Q, Dict()); _PMs._check_var_keys(Q, bus_arcs, "reactive power", "branch") - Psw = get(_PMs.var(pm, nw), :Psw, Dict()); _PMs._check_var_keys(Psw, bus_arcs_sw, "active power", "switch") - Qsw = get(_PMs.var(pm, nw), :Qsw, Dict()); _PMs._check_var_keys(Qsw, bus_arcs_sw, "reactive power", "switch") - Pt = get(_PMs.var(pm, nw), :Pt, Dict()); _PMs._check_var_keys(Pt, bus_arcs_trans, "active power", "transformer") - Qt = get(_PMs.var(pm, nw), :Qt, Dict()); _PMs._check_var_keys(Qt, bus_arcs_trans, "reactive power", "transformer") - - pd = get(_PMs.var(pm, nw), :pd_bus, Dict()); _PMs._check_var_keys(pg, bus_loads, "active power", "load") - qd = get(_PMs.var(pm, nw), :qd_bus, Dict()); _PMs._check_var_keys(qg, bus_loads, "reactive power", "load") - pg = get(_PMs.var(pm, nw), :pg_bus, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") - qg = get(_PMs.var(pm, nw), :qg_bus, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") - ps = get(_PMs.var(pm, nw), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") - qs = get(_PMs.var(pm, nw), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") - - cnds = _PMs.conductor_ids(pm; nw=nw) +function constraint_mc_load_power_balance(pm::_PM.AbstractWModels, nw::Int, i, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_loads, bus_gs, bus_bs) + Wr = var(pm, nw, :Wr, i) + Wi = var(pm, nw, :Wi, i) + P = get(var(pm, nw), :P, Dict()); _PM._check_var_keys(P, bus_arcs, "active power", "branch") + Q = get(var(pm, nw), :Q, Dict()); _PM._check_var_keys(Q, bus_arcs, "reactive power", "branch") + Psw = get(var(pm, nw), :Psw, Dict()); _PM._check_var_keys(Psw, bus_arcs_sw, "active power", "switch") + Qsw = get(var(pm, nw), :Qsw, Dict()); _PM._check_var_keys(Qsw, bus_arcs_sw, "reactive power", "switch") + Pt = get(var(pm, nw), :Pt, Dict()); _PM._check_var_keys(Pt, bus_arcs_trans, "active power", "transformer") + Qt = get(var(pm, nw), :Qt, Dict()); _PM._check_var_keys(Qt, bus_arcs_trans, "reactive power", "transformer") + + pd = get(var(pm, nw), :pd_bus, Dict()); _PM._check_var_keys(pd, bus_loads, "active power", "load") + qd = get(var(pm, nw), :qd_bus, Dict()); _PM._check_var_keys(qd, bus_loads, "reactive power", "load") + pg = get(var(pm, nw), :pg_bus, Dict()); _PM._check_var_keys(pg, bus_gens, "active power", "generator") + qg = get(var(pm, nw), :qg_bus, Dict()); _PM._check_var_keys(qg, bus_gens, "reactive power", "generator") + ps = get(var(pm, nw), :ps, Dict()); _PM._check_var_keys(ps, bus_storage, "active power", "storage") + qs = get(var(pm, nw), :qs, Dict()); _PM._check_var_keys(qs, bus_storage, "reactive power", "storage") + + cnds = conductor_ids(pm; nw=nw) ncnds = length(cnds) Gt = isempty(bus_gs) ? fill(0.0, ncnds, ncnds) : sum(values(bus_gs)) @@ -178,40 +180,41 @@ function constraint_mc_power_balance_load(pm::_PMs.AbstractWModels, nw::Int, i, - diag(-Wr*Bt'+Wi*Gt') ) - if _PMs.report_duals(pm) - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p - _PMs.sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q + if _IM.report_duals(pm) + sol(pm, nw, :bus, i)[:lam_kcl_r] = cstr_p + sol(pm, nw, :bus, i)[:lam_kcl_i] = cstr_q end end "delegate back to PowerModels" -function constraint_mc_ohms_yt_from(pm::_PMs.AbstractWModels, n::Int, c::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) - _PMs.constraint_ohms_yt_from(pm, n, c, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) +function constraint_mc_ohms_yt_from(pm::_PM.AbstractWModels, n::Int, c::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) + _PM.constraint_ohms_yt_from(pm, n, c, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm) end "delegate back to PowerModels" -function constraint_mc_ohms_yt_to(pm::_PMs.AbstractWModels, n::Int, c::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) - _PMs.constraint_ohms_yt_to(pm, n, c, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) +function constraint_mc_ohms_yt_to(pm::_PM.AbstractWModels, n::Int, c::Int, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) + _PM.constraint_ohms_yt_to(pm, n, c, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm) end "on/off bus voltage constraint for relaxed forms" -function constraint_mc_bus_voltage_on_off(pm::_PMs.AbstractWModels, n::Int; kwargs...) - for (i, bus) in _PMs.ref(pm, n, :bus) - constraint_mc_voltage_magnitude_sqr_on_off(pm, i, nw=n) +function constraint_mc_bus_voltage_on_off(pm::_PM.AbstractWModels, n::Int; kwargs...) + for (i, bus) in ref(pm, n, :bus) + constraint_mc_bus_voltage_magnitude_sqr_on_off(pm, i, nw=n) end end -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractPolarModels, n::Int, f_idx, angmin, angmax) +"" +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractPolarModels, n::Int, f_idx, angmin, angmax) i, f_bus, t_bus = f_idx - va_fr = _PMs.var(pm, n, :va, f_bus) - va_to = _PMs.var(pm, n, :va, t_bus) + va_fr = var(pm, n, :va, f_bus) + va_to = var(pm, n, :va, t_bus) - for c in _PMs.conductor_ids(pm; nw=n) + for c in conductor_ids(pm; nw=n) JuMP.@constraint(pm.model, va_fr[c] - va_to[c] <= angmax[c]) JuMP.@constraint(pm.model, va_fr[c] - va_to[c] >= angmin[c]) end @@ -219,29 +222,30 @@ end "" -function constraint_mc_voltage_angle_difference(pm::_PMs.AbstractWModels, n::Int, f_idx, angmin, angmax) +function constraint_mc_voltage_angle_difference(pm::_PM.AbstractWModels, n::Int, f_idx, angmin, angmax) i, f_bus, t_bus = f_idx - ncnds = length(_PMs.conductor_ids(pm, n)) + ncnds = length(conductor_ids(pm, n)) - w_fr = _PMs.var(pm, n, :w, f_bus) - w_to = _PMs.var(pm, n, :w, t_bus) - wr = [_PMs.var(pm, n, :wr)[(f_bus, t_bus, c, c)] for c in 1:ncnds] - wi = [_PMs.var(pm, n, :wi)[(f_bus, t_bus, c, c)] for c in 1:ncnds] + w_fr = var(pm, n, :w, f_bus) + w_to = var(pm, n, :w, t_bus) + wr = [var(pm, n, :wr)[(f_bus, t_bus, c, c)] for c in 1:ncnds] + wi = [var(pm, n, :wi)[(f_bus, t_bus, c, c)] for c in 1:ncnds] JuMP.@constraint(pm.model, wi .<= tan.(angmax).*wr) JuMP.@constraint(pm.model, wi .>= tan.(angmin).*wr) for c in 1:ncnds - _PMs.cut_complex_product_and_angle_difference(pm.model, w_fr[c], w_to[c], wr[c], wi[c], angmin[c], angmax[c]) + _PM.cut_complex_product_and_angle_difference(pm.model, w_fr[c], w_to[c], wr[c], wi[c], angmin[c], angmax[c]) end end -function constraint_mc_storage_on_off(pm::_PMs.AbstractPowerModel, n::Int, i, pmin, pmax, qmin, qmax, charge_ub, discharge_ub) - z_storage =_PMs.var(pm, n, :z_storage, i) - ps =_PMs.var(pm, n, :ps, i) - qs =_PMs.var(pm, n, :qs, i) +"" +function constraint_mc_storage_on_off(pm::_PM.AbstractPowerModel, n::Int, i, pmin, pmax, qmin, qmax, charge_ub, discharge_ub) + z_storage =var(pm, n, :z_storage, i) + ps =var(pm, n, :ps, i) + qs =var(pm, n, :qs, i) JuMP.@constraint(pm.model, ps .<= z_storage.*pmax) JuMP.@constraint(pm.model, ps .>= z_storage.*pmin) @@ -252,12 +256,12 @@ end "" -function constraint_mc_generation_wye(pm::_PMs.AbstractPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) - _PMs.var(pm, nw, :pg_bus)[id] = _PMs.var(pm, nw, :pg, id) - _PMs.var(pm, nw, :qg_bus)[id] = _PMs.var(pm, nw, :qg, id) +function constraint_mc_gen_setpoint_wye(pm::_PM.AbstractPowerModel, nw::Int, id::Int, bus_id::Int, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + var(pm, nw, :pg_bus)[id] = var(pm, nw, :pg, id) + var(pm, nw, :qg_bus)[id] = var(pm, nw, :qg, id) if report - _PMs.sol(pm, nw, :gen, id)[:pg_bus] = _PMs.var(pm, nw, :pg_bus, id) - _PMs.sol(pm, nw, :gen, id)[:qg_bus] = _PMs.var(pm, nw, :qg_bus, id) + sol(pm, nw, :gen, id)[:pg_bus] = var(pm, nw, :pg_bus, id) + sol(pm, nw, :gen, id)[:qg_bus] = var(pm, nw, :qg_bus, id) end end diff --git a/src/form/wr.jl b/src/form/wr.jl index 4c4aaed29..e69de29bb 100644 --- a/src/form/wr.jl +++ b/src/form/wr.jl @@ -1,58 +0,0 @@ -# "" -# function constraint_mc_model_voltage(pm::_PMs.AbstractWRModel, n::Int) -# w = _PMs.var(pm, n, :w) -# wr = _PMs.var(pm, n, :wr) -# wi = _PMs.var(pm, n, :wi) -# -# for c in 1:length(_PMs.conductor_ids(pm, n)) -# for d in c:length(_PMs.conductor_ids(pm)) -# for (i,j) in _PMs.ids(pm, n, :buspairs) -# InfrastructureModels.relaxation_complex_product(pm.model, w[i][d], w[j][c], wr[(i,j,c,d)], wi[(i,j,c,d)]) -# end -# -# if d != c -# for i in _PMs.ids(pm, n, :bus) -# InfrastructureModels.relaxation_complex_product(pm.model, w[i][d], w[i][c], wr[(i,i,c,d)], wi[(i,i,c,d)]) -# end -# end -# end -# end -# end - - -# "power balance constraint with line shunts and transformers for relaxed WR forms" -# function constraint_mc_power_balance(pm::_PMs.AbstractWRModel, nw::Int, c::Int, i::Int, bus_arcs, bus_arcs_sw, bus_arcs_trans, bus_gens, bus_storage, bus_pd, bus_qd, bus_gs, bus_bs) -# w = _PMs.var(pm, nw, c, :w, i) -# p = get(_PMs.var(pm, nw, c), :p, Dict()); _PMs._check_var_keys(p, bus_arcs, "active power", "branch") -# q = get(_PMs.var(pm, nw, c), :q, Dict()); _PMs._check_var_keys(q, bus_arcs, "reactive power", "branch") -# pg = get(_PMs.var(pm, nw, c), :pg, Dict()); _PMs._check_var_keys(pg, bus_gens, "active power", "generator") -# qg = get(_PMs.var(pm, nw, c), :qg, Dict()); _PMs._check_var_keys(qg, bus_gens, "reactive power", "generator") -# ps = get(_PMs.var(pm, nw, c), :ps, Dict()); _PMs._check_var_keys(ps, bus_storage, "active power", "storage") -# qs = get(_PMs.var(pm, nw, c), :qs, Dict()); _PMs._check_var_keys(qs, bus_storage, "reactive power", "storage") -# psw = get(_PMs.var(pm, nw, c), :psw, Dict()); _PMs._check_var_keys(psw, bus_arcs_sw, "active power", "switch") -# qsw = get(_PMs.var(pm, nw, c), :qsw, Dict()); _PMs._check_var_keys(qsw, bus_arcs_sw, "reactive power", "switch") -# pt = get(_PMs.var(pm, nw, c), :pt, Dict()); _PMs._check_var_keys(pt, bus_arcs_trans, "active power", "transformer") -# qt = get(_PMs.var(pm, nw, c), :qt, Dict()); _PMs._check_var_keys(qt, bus_arcs_trans, "reactive power", "transformer") -# -# -# _PMs.con(pm, nw, c, :kcl_p)[i] = JuMP.@constraint(pm.model, -# sum(p[a] for a in bus_arcs) -# + sum(psw[a_sw] for a_sw in bus_arcs_sw) -# + sum(pt[a_trans] for a_trans in bus_arcs_trans) -# == -# sum(pg[g] for g in bus_gens) -# - sum(ps[s] for s in bus_storage) -# - sum(pd for pd in values(bus_pd)) -# - sum(gs for gs in values(bus_gs))*w -# ) -# _PMs.con(pm, nw, c, :kcl_q)[i] = JuMP.@constraint(pm.model, -# sum(q[a] for a in bus_arcs) -# + sum(qsw[a_sw] for a_sw in bus_arcs_sw) -# + sum(qt[a_trans] for a_trans in bus_arcs_trans) -# == -# sum(qg[g] for g in bus_gens) -# - sum(qs[s] for s in bus_storage) -# - sum(qd for qd in values(bus_qd)) -# + sum(bs for bs in values(bus_bs))*w -# ) -# end diff --git a/src/io/common.jl b/src/io/common.jl index 2035e7472..0c5f860e7 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -1,45 +1,134 @@ """ parse_file(io) -Parses the IOStream of a file into a Three-Phase PowerModels data structure. +Parses the IOStream of a file into a PowerModelsDistribution data structure. """ -function parse_file(io::IO; import_all::Bool=false, vmin::Float64=0.9, vmax::Float64=1.1, filetype::AbstractString="json", bank_transformers::Bool=true) +function parse_file( + io::IO, filetype::AbstractString="json"; + data_model::DataModel=ENGINEERING, + import_all::Bool=false, + bank_transformers::Bool=true, + transformations::Vector{<:Any}=[], + build_multinetwork::Bool=false, + kron_reduced::Bool=true, + time_series::String="daily" + )::Dict{String,Any} + if filetype == "dss" - Memento.warn(_LOGGER, "Not all OpenDSS features are supported, currently only minimal support for lines, loads, generators, and capacitors as shunts. Transformers and reactors as transformer branches are included, but value translation is not fully supported.") - pmd_data = PowerModelsDistribution.parse_opendss(io; import_all=import_all, vmin=vmin, vmax=vmax, bank_transformers=bank_transformers) + data_eng = PowerModelsDistribution.parse_opendss(io; + import_all=import_all, + bank_transformers=bank_transformers, + time_series=time_series + ) + + for transform in transformations + @assert isa(transform, Function) || isa(transform, Tuple{<:Function,Vararg{Pair{String,<:Any}}}) + + if isa(transform, Tuple) + transform[1](data_eng; [Symbol(k)=>v for (k,v) in transform[2:end]]...) + else + transform(data_eng) + end + end + + if data_model == MATHEMATICAL + return transform_data_model(data_eng; + make_pu=true, + kron_reduced=kron_reduced, + build_multinetwork=build_multinetwork + ) + else + return data_eng + end elseif filetype == "json" - pmd_data = PowerModelsDistribution.parse_json(io; validate=false) + pmd_data = parse_json(io; validate=false) + + if pmd_data["data_model"] != data_model && data_model == ENGINEERING + return transform_data_model(pmd_data; + kron_reduced=kron_reduced, + build_multinetwork=build_multinetwork + ) + else + return pmd_data + end else - Memento.error(_LOGGER, "only .m and .dss files are supported") + Memento.error(_LOGGER, "only .dss and .json files are supported") end +end - correct_network_data!(pmd_data) - return pmd_data +"" +function parse_file(file::String; kwargs...)::Dict{String,Any} + data = open(file) do io + parse_file(io, split(lowercase(file), '.')[end]; kwargs...) + end + + return data end -"" -function parse_file(file::String; kwargs...) - pmd_data = open(file) do io - parse_file(io; filetype=split(lowercase(file), '.')[end], kwargs...) +"transforms model between engineering (high-level) and mathematical (low-level) models" +function transform_data_model(data::Dict{String,<:Any}; + kron_reduced::Bool=true, + make_pu::Bool=true, + build_multinetwork::Bool=false + )::Dict{String,Any} + + current_data_model = get(data, "data_model", MATHEMATICAL) + + if current_data_model == ENGINEERING + if build_multinetwork && haskey(data, "time_series") + mn_data = _build_eng_multinetwork(data) + if ismultinetwork(mn_data) + data_math = _map_eng2math_multinetwork(mn_data; kron_reduced=kron_reduced) + else + data_math = _map_eng2math(mn_data; kron_reduced=kron_reduced) + end + else + data_math = _map_eng2math(data; kron_reduced=kron_reduced) + end + + correct_network_data!(data_math; make_pu=make_pu) + + return data_math + elseif current_data_model == MATHEMATICAL + Memento.warn(_LOGGER, "A MATHEMATICAL data model cannot be converted back to an ENGINEERING data model, irreversible transformations have been made") + return data + else + Memento.warn(_LOGGER, "Data model '$current_data_model' is not recognized, no model type transformation performed") + return data end - return pmd_data end "" -function correct_network_data!(data::Dict{String,Any}) - _PMs.make_per_unit!(data) - - _PMs.check_connectivity(data) - _PMs.correct_transformer_parameters!(data) - _PMs.correct_voltage_angle_differences!(data) - _PMs.correct_thermal_limits!(data) - _PMs.correct_branch_directions!(data) - _PMs.check_branch_loops(data) - _PMs.correct_bus_types!(data) - _PMs.correct_dcline_limits!(data) - _PMs.correct_cost_functions!(data) - _PMs.standardize_cost_terms!(data) +function correct_network_data!(data::Dict{String,Any}; make_pu::Bool=true) + if get(data, "data_model", MATHEMATICAL) == ENGINEERING + check_eng_data_model(data) + elseif get(data, "data_model", MATHEMATICAL) == MATHEMATICAL + if make_pu + make_per_unit!(data) + #TODO system-wide vbase does not make sense anymore... + #take highest vbase just so it does not break anything for now + if ismultinetwork(data) + for (n,nw) in data["nw"] + nw["basekv"] = maximum(maximum(bus["vbase"] for (_, bus) in nw["bus"]) for nw in values(data["nw"])) + nw["baseMVA"] = data["settings"]["sbase"]*data["settings"]["power_scale_factor"]/1E6 + end + else ismultinetwork(data) + data["baseMVA"] = data["settings"]["sbase"]*data["settings"]["power_scale_factor"]/1E6 + data["basekv"] = maximum(bus["vbase"] for (_, bus) in data["bus"]) + _PM.check_connectivity(data) + _PM.correct_transformer_parameters!(data) + _PM.correct_voltage_angle_differences!(data) + _PM.correct_thermal_limits!(data) + _PM.correct_branch_directions!(data) + _PM.check_branch_loops(data) + _PM.correct_bus_types!(data) + _PM.correct_dcline_limits!(data) + _PM.correct_cost_functions!(data) + _PM.standardize_cost_terms!(data) + end + end + end end diff --git a/src/io/dss_parse.jl b/src/io/dss_parse.jl index f90b40963..b8a9e95b9 100644 --- a/src/io/dss_parse.jl +++ b/src/io/dss_parse.jl @@ -1,456 +1,277 @@ -"Squares `x`, for parsing Reverse Polish Notation" -function _sqr(x::Float64) - return x * x -end - - -_double_operators = Dict("+" => +, "-" => -, "*" => *, "/" => /, "^" => ^, - "atan2" => (x, y) -> rad2deg(atan2(y, x))) - -_single_operators = Dict("sqr" => _sqr, "sqrt" => sqrt, "inv" => inv, "ln" => log, - "exp" => exp, "log10" => log10, "sin" => sind, "cos" => cosd, - "tan" => tand, "asin" => asind, "acos" => acosd, "atan" => atand) - -array_delimiters = ['\"', '\'', '[', '{', '(', ']', '}', ')'] - -"parses Reverse Polish Notation `expr`" -function _parse_rpn(expr::AbstractString, dtype::Type=Float64) - clean_expr = strip(expr, array_delimiters) - - if occursin("rollup", clean_expr) || occursin("rolldn", clean_expr) || occursin("swap", clean_expr) - Memento.warn(_LOGGER, "_parse_rpn does not support \"rollup\", \"rolldn\", or \"swap\", leaving as String") - return expr - end - - stack = [] - split_expr = occursin(",", clean_expr) ? split(clean_expr, ',') : split(clean_expr) - - for item in split_expr - try - if haskey(_double_operators, item) - b = pop!(stack) - a = pop!(stack) - push!(stack, _double_operators[item](a, b)) - elseif haskey(_single_operators, item) - push!(stack, _single_operators[item](pop!(stack))) - else - if item == "pi" - push!(stack, pi) - else - push!(stack, parse(dtype, item)) - end - end - catch error - if isa(error, ArgumentError) - Memento.warn(_LOGGER, "\"$expr\" is not valid Reverse Polish Notation, leaving as String") - return expr - end - end - end - if length(stack) > 1 - Memento.warn(_LOGGER, "\"$expr\" is not valid Reverse Polish Notation, leaving as String") - return expr - else - return stack[1] - end -end - - -"detects if `expr` is Reverse Polish Notation expression" -function _isa_rpn(expr::AbstractString)::Bool - expr = split(strip(expr, array_delimiters)) - opkeys = keys(merge(_double_operators, _single_operators)) - for item in expr - if item in opkeys - return true - end - end - return false -end - - -"parses connection \"conn\" specification reducing to wye or delta" -function _parse_conn(conn::String)::String - if conn in ["wye", "y", "ln"] - return "wye" - elseif conn in ["delta", "ll"] - return "delta" - else - Memento.warn(_LOGGER, "Unsupported connection $conn, defaulting to \"wye\"") - return "wye" - end -end - - -"checks is a string is a connection by checking the values" -function _isa_conn(expr::AbstractString)::Bool - if expr in ["wye", "y", "ln", "delta", "ll"] - return true - else - return false - end -end - - -""" - _get_prop_name(ctype) - -Returns the property names in order for a given component type `ctype`. -""" -function _get_prop_name(ctype::AbstractString)::Array - linecode = ["nphases", "r1", "x1", "r0", "x0", "c1", "c0", "units", - "rmatrix", "xmatrix", "cmatrix", "basefreq", "normamps", - "emergamps", "faultrate", "pctperm", "repair", "kron", - "rg", "xg", "rho", "neutral", "b1", "b0", "like"] - - linegeometry = ["nconds", "nphases", "cond", "wire", "x", "h", "units", - "normamps", "emergamps", "reduce", "spacing", "wires", - "cncable", "tscable", "cncables", "tscables", "like"] - - linespacing = ["nconds", "nphases", "x", "h", "units"] - - loadshape = ["npts", "interval", "minterval", "sinterval", "pmult", - "qmult", "hour", "mean", "stddev", "csvfile", "sngfile", - "pqcsvfile", "action", "useactual", "pmax", "qmax", - "pbase", "like"] - - growthshape = ["npts", "year", "mult", "csvfile", "sngfile", "dblfile", - "like"] - - tcc_curve = ["npts", "c_array", "t_array", "like"] - - cndata = [] - - tsdata = [] - - wiredata = ["rdc", "rac", "runits", "gmrac", "gmrunits", "radius", - "radunits", "normamps", "emergamps", "diam", "like"] - - xfmrcode = [] - - vsource = ["bus1", "bus2", "basekv", "pu", "angle", "frequency", "phases", - "mvasc3", "mvasc1", "x1r1", "x0r0", "isc3", "isc1", "r1", "x1", - "r0", "x0", "scantype", "sequence", "spectrum", "z1", "z2", "z0", - "puz1", "puz2", "puz0", "basemva", "basefreq", "like", "enabled"] - - isource = ["phases", "bus1", "amps", "angle", "frequency", "scantype", - "sequence", "spectrum", "basefreq", "enabled", "like"] - - fault = ["phases", "bus1", "bus2", "r", "gmatrix", "minamps", "ontime", - "pctperm", "temporary", "%stddev", "normamps", "emergamps", - "basefreq", "faultrate", "repair", "enabled", "like"] - - capacitor = ["bus1", "bus2", "phases", "kvar", "kv", "conn", "cmatrix", - "cuf", "r", "xl", "harm", "numsteps", "states", "normamps", - "emergamps", "faultrate", "pctperm", "basefreq", "enabled", - "like"] - - line = ["bus1", "bus2", "linecode", "length", "phases", - "r1", "x1", "r0", "x0", "c1", "c0", "b1", - "b0", "normamps", "emergamps", "faultrate", "pctperm", - "repair", "basefreq", "rmatrix", "xmatrix", "cmatrix", - "switch", "rg", "xg", "rho", "geometry", "earthmodel", - "units", "enabled", "like"] - - reactor = ["phases", "bus1", "bus2", "kv", "kvar", "conn", "parallel", - "r", "rmatrix", "rp", "x", "xmatrix", "z", "z1", "z2", "z0", - "rcurve", "lcurve", "lmh", "normamps", "emergamps", "repair", - "faultrate", "pctperm", "basefreq", "enabled", "like"] - - transformer = ["phases", "windings", "wdg", "bus", "conn", "kv", "kva", - "tap", "%r", "rneut", "xneut", "buses", "conns", "kvs", - "kvas", "taps", "%rs", "xhl", "xlt", "xht", "xscarray", - "thermal", "n", "m", "flrise", "hsrise", "%loadloss", - "%noloadloss", "%imag", "ppm_antifloat", "normhkva", - "emerghkva", "sub", "maxtap", "mintap", "numtaps", - "subname", "bank", "xfmrcode", "xrconst", "leadlag", - "faultrate", "basefreq", "enabled", "like"] - - gictransformer = ["basefreq", "bush", "busnh", "busnx", "busx", - "emergamps", "enabled", "phases", "r1", "r2", "type", - "mva", "kvll1", "kvll2", "%r1", "%r2", "k", "varcurve", - "like", "normamps", "emergamps", "pctperm", "repair"] - - gicline = ["angle", "bus1", "bus2", "c", "ee", "en", "frequency", "lat1", - "lat2", "lon1", "lon2", "phases", "r", "volts", "x", "like", - "basefreq", "enabled", "spectrum"] - - load = ["phases", "bus1", "kv", "kw", "pf", "model", - "yearly", "daily", "duty", "growth", "conn", "kvar", - "rneut", "xneut", "status", "class", "vminpu", "vmaxpu", - "vminnorm", "vminemerg", "xfkva", "allocationfactor", - "kva", "%mean", "%stddev", "cvrwatts", "cvrvars", "kwh", - "kwhdays", "cfactor", "cvrcurve", "numcust", "spectrum", - "zipv", "%seriesrl", "relweight", "vlowpu", "puxharm", - "xrharm", "spectrum", "basefreq", "enabled", "like"] - - generator = ["bus1", "phases", "kv", "kw", "pf", "model", "yearly", - "daily", "duty", "dispvalue", "conn", "kvar", "rneut", - "xneut", "status", "class", "vpu", "maxkvar", "minkvar", - "pvfactor", "debugtrace", "vminpu", "vmaxpu", "forceon", - "kva", "mva", "xd", "xdp", "xdpp", "h", "d", "usermodel", - "userdata", "shaftmodel", "shaftdata", "dutystart", "balanced", - "xrdp", "spectrum", "basefreq", "enabled", "like"] - - indmach012 = ["phases", "bus1", "kv", "kw", "pf", "conn", "kva", "h", - "d", "purs", "puxs", "purr", "puxr", "puxm", "slip", - "maxslip", "slipoption", "spectrum", "enabled"] - - storage = ["phases", "bus1", "%charge", "%discharge", "%effcharge", "%idlingkvar", - "idlingkw", "%r", "%reserve", "%stored", "%x", "basefreq", - "chargetrigger", "class", "conn", "daily", "yearly", "debugtrace", - "dischargetrigger", "dispmode", "duty", "dynadata", "dynadll", "enabled", - "kv", "kva", "kvar", "kw", "kwhrated", "kwhstored", "kwrated", "like", - "model", "pf", "spectrum", "state", "timechargetrig", "userdata", - "usermodel", "vmaxpu", "vminpu", "yearly"] - - capcontrol = ["element", "capacitor", "type", "ctphase", "ctratio", "deadtime", - "delay", "delayoff", "eventlog", "offsetting", "onsetting", - "ptphase", "ptratio", "terminal", "vbus", "vmax", "vmin", - "voltoverride", "enabled"] - - regcontrol = ["transformer", "winding", "vreg", "band", "delay", "ptratio", - "ctprim", "r", "x", "pthase", "tapwinding", "bus", - "remoteptratio", "debugtrace", "eventlog", "inversetime", - "maxtapchange", "revband", "revdelay", "reversible", - "revneutral", "revr", "revthreshold", "revvreg", "revx", - "tapdelay", "tapnum", "vlimit", "ldc_z", "rev_z", "cogen", - "enabled"] - - energymeter = ["element", "terminal", "action", "clear", "save", "take", - "option", "kwnorm", "kwemerg", "peakcurrent", "zonelist", - "zonelist", "zonelist", "localonly", "mask", "losses", - "linelosses", "xfmrlosses", "seqlosses", "3phaselosses", - "vbaselosses", "basefreq", "enabled", "like"] - - pvsystem = ["phases", "bus1", "kv", "irradiance", "pmpp", "temperature", - "pf", "conn", "kvar", "kva", "%cutin", "%cutout", - "effcurve", "p-tcurve", "%r", "%x", "model", "vminpu", - "vmaxpu", "yearly", "daily", "duty", "tyearly", "tduty", - "class", "usermodel", "userdata", "debugtrace", "spectrum"] - - ctypes = Dict{String, Array}("linecode" => linecode, - "linegeometry" => linegeometry, - "linespacing" =>linespacing, - "loadshape" => loadshape, - "growthshape" => growthshape, - "tcc_curve" => tcc_curve, - "cndata" => cndata, - "tsdata" => tsdata, - "wiredata" => wiredata, - "xfmrcode" => xfmrcode, - "vsource" => vsource, - "isource" => isource, - "fault" => fault, - "capacitor" => capacitor, - "line" => line, - "reactor" => reactor, - "transformer" => transformer, - "gictransformer" => gictransformer, - "gicline" => gicline, - "load" => load, - "generator" => generator, - "indmach012" => indmach012, - "storage" => storage, - "capcontrol" => capcontrol, - "regcontrol" => regcontrol, - "energymeter" => energymeter, - "pvsystem" => pvsystem, - "circuit" => vsource - ) - - try - return ctypes[ctype] - catch e - throw(e) - end -end - - -""" - _parse_matrix(dtype, data) - -Parses a OpenDSS style triangular matrix string `data` into a two dimensional -array of type `dtype`. Matrix strings are capped by either parenthesis or -brackets, rows are separated by "|", and columns are separated by spaces. -""" -function _parse_matrix(dtype::Type, data::AbstractString)::Array - rows = [] - for line in split(strip(data, array_delimiters), '|') - cols = [] - for item in split(line) - push!(cols, parse(dtype, item)) - end - push!(rows, cols) - end - - nphases = maximum([length(row) for row in rows]) - - if dtype == AbstractString || dtype == String - matrix = fill("", nphases, nphases) - elseif dtype == Char - matrix = fill(' ', nphases, nphases) - else - matrix = zeros(dtype, nphases, nphases) - end - - if length(rows) == 1 - for i in 1:nphases - matrix[i, i] = rows[1][1] - end - elseif all([length(row) for row in rows] .== [i for i in 1:nphases]) - for (i, row) in enumerate(rows) - for (j, col) in enumerate(row) - matrix[i, j] = matrix[j, i] = col - end - end - elseif all([length(row) for row in rows] .== nphases) - for (i, row) in enumerate(rows) - for (j, col) in enumerate(row) - matrix[i, j] = col - end - end - end - - return matrix -end - - -"parse matrices according to active nodes" -function _parse_matrix(data::Array{T}, nodes::Array{Bool}, nph::Int=3, fill_val=0.0)::Array where T - mat = fill(fill_val, (nph, nph)) - idxs = findall(nodes[1:nph]) - - for i in 1:size(idxs)[1] - mat[idxs[i], idxs[i]] = data[i, i] - for j in 1:i-1 - mat[idxs[i],idxs[j]] = mat[idxs[j],idxs[i]] = data[i, j] - end - end - - return mat -end - - -"checks if `data` is an opendss-style matrix string" -function _isa_matrix(data::AbstractString)::Bool - if occursin("|", data) - return true - else - return false - end -end - - -"Reorders a `matrix` based on the order that phases are listed in on the from- (`pof`) and to-sides (`pot`)" -function _reorder_matrix(matrix, phase_order) - mat = zeros(size(matrix)) - for (i, n) in zip(sort(phase_order), phase_order) - for (j, m) in zip(sort(phase_order), phase_order) - mat[i, j] = matrix[n, m] - end - end - return mat -end - - -""" - _parse_array(dtype, data) - -Parses a OpenDSS style array string `data` into a one dimensional array of type -`dtype`. Array strings are capped by either brackets, single quotes, or double -quotes, and elements are separated by spaces. -""" -function _parse_array(dtype::Type, data::AbstractString) - if occursin(",", data) - split_char = ',' - else - split_char = ' ' - end - - if _isa_rpn(data) - matches = collect((m.match for m = eachmatch(Regex(string("[",join(array_delimiters, '\\'),"]")), data, overlap=false))) - if length(matches) == 2 - if dtype == String - return data - else - return _parse_rpn(data, dtype) - end - - else - elements = _parse_properties(data[2:end-1]) - end - else - elements = split(strip(data, array_delimiters), split_char) - elements = [strip(el) for el in elements if strip(el) != ""] - end - - if dtype == String || dtype == AbstractString || dtype == Char - array = [] - for el in elements - push!(array, el) - end - else - array = zeros(dtype, length(elements)) - for (i, el) in enumerate(elements) - if _isa_rpn(data) - array[i] = _parse_rpn(el, dtype) - else - array[i] = parse(dtype, el) - end - end - end - - return array -end - - -"parse matrices according to active nodes" -function _parse_array(data, nodes::Array{Bool}, nph::Int=3, fill_val=0.0)::Array - mat = fill(fill_val, nph) - idxs = findall(nodes[1:nph]) - - if length(data) == 1 && nph > 1 - for i in idxs - mat[i] = data[1] - end - else - for i in 1:length(idxs) - mat[idxs[i]] = data[i] - end - end - - return mat -end - - -"checks if `data` is an opendss-style array string" -function _isa_array(data::AbstractString)::Bool - clean_data = strip(data) - if !occursin("|", clean_data) - if occursin(",", clean_data) - return true - elseif startswith(clean_data, "[") && endswith(clean_data, "]") - return true - elseif startswith(clean_data, "\"") && endswith(clean_data, "\"") - return true - elseif startswith(clean_data, "\'") && endswith(clean_data, "\'") - return true - elseif startswith(clean_data, "(") && endswith(clean_data, ")") - return true - elseif startswith(clean_data, "{") && endswith(clean_data, "}") - return true - else - return false - end - else - return false - end -end +const _dss_unsupported_commands = Vector{String}([ + "cleanup", "connect", "disconnect", "_docontrolactions", "_initsnap", + "_samplecontrols", "_showcontrolqueue", "_solvedirect", "solvenocontrol", + "_solvepflow", "?", "about", "addbusmarker", "alignfile", "allocateloads", + "batchedit", "buildy", "calcvoltagebases", "capacity", "cd", "cktlosses", + "clearbusmarkers", "close", "closedi", "comparecases", "currents", + "di_plot", "distribute", "doscmd", "dump", "estimate", "export", + "fileedit", "finishtimestep", "formedit", "get", "guids", "help", "init", + "interpolate", "losses", "makebuslist", "makeposseq", "nodelist", + "nodediff", "obfuscate", "open", "phaselosses", "plot", "powers", + "pstcalc", "reconductor", "reduce", "relcalc", "remove", "rephase", + "reprocessbuses", "reset", "rotate", "sample", "save", "select", + "seqcurrents", "seqpowers", "seqvoltages", "setkvbase", "show", "solve", + "summary", "totals", "updatestorage", "var", "variable", "varnames", + "vdiff", "visualize", "voltages", "yearlycurves", "ysc", "zsc", "zsc10", + "zscrefresh" +]) + +const _linecode_properties = Vector{String}([ + "nphases", "r1", "x1", "r0","x0", "c1", "c0", "units", "rmatrix", + "xmatrix", "cmatrix", "basefreq", "normamps", "emergamps", "faultrate", + "pctperm", "repair", "kron", "rg", "xg", "rho", "neutral", "b1", "b0", + "like" +]) + +const _linegeometry_properties = Vector{String}([ + "nconds", "nphases", "cond", "wire", "x", "h", "units", "normamps", + "emergamps", "reduce", "spacing", "wires", "cncable", "tscable", + "cncables", "tscables", "like" +]) + +const _linespacing_properties = Vector{String}([ + "nconds", "nphases", "x", "h", "units", "like" +]) + +const _loadshape_properties = Vector{String}([ + "npts", "interval", "minterval", "sinterval", "pmult", "qmult", "hour", + "mean", "stddev", "csvfile", "sngfile", "pqcsvfile", "action", "useactual", + "pmax", "qmax", "pbase", "like" +]) + +const _xycurve_properties = Vector{String}([ + "npts", "points", "yarray", "xarray", "csvfile", "sngfile", "dblfile", + "x", "y", "xshift", "yshift", "xscale", "yscale", "like" +]) + +const _growthshape_properties = Vector{String}([ + "npts", "year", "mult", "csvfile", "sngfile", "dblfile", "like" +]) + +const _tcc_curve_properties = Vector{String}([ + "npts", "c_array", "t_array", "like" +]) + +const _cndata_properties = Vector{String}([ + "diacable", "diains", "diam", "diastrand", "emergamps", "epsr", "gmrac", + "gmrstrang", "gmrunits", "inslayer", "k", "like", "normamps", "rac", + "radius", "radunits", "rdc", "rstrand", "runits" +]) + +const _tsdata_properties = Vector{String}([ + "diacable", "diains", "diam", "diashield", "emergamps", "epsr", "gmrac", + "gmrunits", "inslayer", "like", "normamps", "rac", "radius", "radunits", + "rdc", "runits", "taplap", "taplayer" +]) + +const _wiredata_properties = Vector{String}([ + "rdc", "rac", "runits", "gmrac", "gmrunits", "radius", "radunits", + "normamps", "emergamps", "diam", "like" +]) + +const _xfmrcode_properties = Vector{String}([ + "phases", "windings", "wdg", "conn", "kv", "kva", "tap", "%r", "rneut", + "xneut", "conns", "kvs", "kvas", "taps", "%rs", "xhl", "xlt", "xht", + "xscarray", "thermal", "n", "m", "flrise", "hsrise", "%loadloss", + "%noloadloss", "%imag", "ppm_antifloat", "normhkva", "emerghkva", "sub", + "maxtap", "mintap", "numtaps", "subname", "xrconst", "leadlag", + "faultrate", "basefreq", "enabled", "like" +]) + +const _vsource_properties = Vector{String}([ + "bus1", "bus2", "basekv", "pu", "angle", "frequency", "phases", "mvasc3", + "mvasc1", "x1r1", "x0r0", "isc3", "isc1", "r1", "x1", "r0", "x0", + "scantype", "sequence", "spectrum", "z1", "z2", "z0", "puz1", "puz2", + "puz0", "basemva", "basefreq", "like", "enabled" +]) + +const _isource_properties = Vector{String}([ + "phases", "bus1", "amps", "angle", "frequency", "scantype", "sequence", + "spectrum", "basefreq", "enabled", "like" +]) + +const _fault_properties = Vector{String}([ + "phases", "bus1", "bus2", "r", "gmatrix", "minamps", "ontime", "pctperm", + "temporary", "%stddev", "normamps", "emergamps", "basefreq", "faultrate", + "repair", "enabled", "like" +]) + +const _capacitor_properties = Vector{String}([ + "bus1", "bus2", "phases", "kvar", "kv", "conn", "cmatrix", "cuf", "r", + "xl", "harm", "numsteps", "states", "normamps", "emergamps", "faultrate", + "pctperm", "basefreq", "enabled", "like" +]) + +const _line_properties = Vector{String}([ + "bus1", "bus2", "linecode", "length", "phases", "r1", "x1", "r0", "x0", + "c1", "c0", "b1", "b0", "normamps", "emergamps", "faultrate", "pctperm", + "repair", "basefreq", "rmatrix", "xmatrix", "cmatrix", "switch", "rg", + "xg", "rho", "geometry", "earthmodel", "units", "enabled", "like" +]) + +const _reactor_properties = Vector{String}([ + "phases", "bus1", "bus2", "kv", "kvar", "conn", "parallel", "r", "rmatrix", + "rp", "x", "xmatrix", "z", "z1", "z2", "z0", "rcurve", "lcurve", "lmh", + "normamps", "emergamps", "repair", "faultrate", "pctperm", "basefreq", + "enabled", "like" +]) + +const _transformer_properties = Vector{String}([ + "phases", "windings", "wdg", "bus", "conn", "kv", "kva", "tap", "%r", + "rneut", "xneut", "buses", "conns", "kvs", "kvas", "taps", "%rs", "xhl", + "xlt", "xht", "xscarray", "thermal", "n", "m", "flrise", "hsrise", + "%loadloss", "%noloadloss", "%imag", "ppm_antifloat", "normhkva", + "emerghkva", "sub", "maxtap", "mintap", "numtaps", "subname", "bank", + "xfmrcode", "xrconst", "leadlag", "faultrate", "basefreq", "enabled", + "like" +]) + +const _gictransformer_properties = Vector{String}([ + "basefreq", "bush", "busnh", "busnx", "busx", "emergamps", "enabled", + "phases", "r1", "r2", "type", "mva", "kvll1", "kvll2", "%r1", "%r2", "k", + "varcurve", "like", "normamps", "emergamps", "pctperm", "repair" +]) + +const _gicline_properties = Vector{String}([ + "angle", "bus1", "bus2", "c", "ee", "en", "frequency", "lat1", "lat2", + "lon1", "lon2", "phases", "r", "volts", "x", "like", "basefreq", + "enabled", "spectrum" +]) + +const _load_properties = Vector{String}([ + "phases", "bus1", "kv", "kw", "pf", "model", "yearly", "daily", "duty", + "growth", "conn", "kvar", "rneut", "xneut", "status", "class", "vminpu", + "vmaxpu", "vminnorm", "vminemerg", "xfkva", "allocationfactor", "kva", + "%mean", "%stddev", "cvrwatts", "cvrvars", "kwh", "kwhdays", "cfactor", + "cvrcurve", "numcust", "spectrum", "zipv", "%seriesrl", "relweight", + "vlowpu", "puxharm", "xrharm", "spectrum", "basefreq", "enabled", "like" +]) + +const _generator_properties = Vector{String}([ + "bus1", "phases", "kv", "kw", "pf", "model", "yearly", "daily", "duty", + "dispvalue", "conn", "kvar", "rneut", "xneut", "status", "class", "vpu", + "maxkvar", "minkvar", "pvfactor", "debugtrace", "vminpu", "vmaxpu", + "forceon", "kva", "mva", "xd", "xdp", "xdpp", "h", "d", "usermodel", + "userdata", "shaftmodel", "shaftdata", "dutystart", "balanced", "xrdp", + "spectrum", "basefreq", "enabled", "like" +]) + +const _indmach012_properties = Vector{String}([ + "phases", "bus1", "kv", "kw", "pf", "conn", "kva", "h", "d", "purs", + "puxs", "purr", "puxr", "puxm", "slip", "maxslip", "slipoption", + "spectrum", "enabled" +]) + +const _storage_properties = Vector{String}([ + "phases", "bus1", "%charge", "%discharge", "%effcharge", "%idlingkvar", + "idlingkw", "%r", "%reserve", "%stored", "%x", "basefreq", "chargetrigger", + "class", "conn", "daily", "yearly", "debugtrace", "dischargetrigger", + "dispmode", "duty", "dynadata", "dynadll", "enabled", "kv", "kva", "kvar", + "kw", "kwhrated", "kwhstored", "kwrated", "like", "model", "pf", + "spectrum", "state", "timechargetrig", "userdata", "usermodel", "vmaxpu", + "vminpu", "yearly" +]) + +const _capcontrol_properties = Vector{String}([ + "element", "capacitor", "type", "ctphase", "ctratio", "deadtime", "delay", + "delayoff", "eventlog", "offsetting", "onsetting", "ptphase", "ptratio", + "terminal", "vbus", "vmax", "vmin", "voltoverride", "enabled" +]) + +const _regcontrol_properties = Vector{String}([ + "transformer", "winding", "vreg", "band", "delay", "ptratio", "ctprim", + "r", "x", "pthase", "tapwinding", "bus", "remoteptratio", "debugtrace", + "eventlog", "inversetime", "maxtapchange", "revband", "revdelay", + "reversible", "revneutral", "revr", "revthreshold", "revvreg", "revx", + "tapdelay", "tapnum", "vlimit", "ldc_z", "rev_z", "cogen", "enabled" +]) + +const _energymeter_properties = Vector{String}([ + "element", "terminal", "action", "clear", "save", "take", "option", + "kwnorm", "kwemerg", "peakcurrent", "zonelist", "zonelist", "zonelist", + "localonly", "mask", "losses", "linelosses", "xfmrlosses", "seqlosses", + "3phaselosses", "vbaselosses", "basefreq", "enabled", "like" +]) + +const _monitor_properties = Vector{String}([ + "element", "terminal", "mode", "action" +]) + +const _pvsystem_properties = Vector{String}([ + "phases", "bus1", "kv", "irradiance", "pmpp", "temperature", "pf", + "conn", "kvar", "kva", "%cutin", "%cutout", "effcurve", "p-tcurve", "%r", + "%x", "model", "vminpu", "vmaxpu", "yearly", "daily", "duty", "tyearly", + "tduty", "class", "usermodel", "userdata", "debugtrace", "spectrum" +]) + +const _recloser_properties = Vector{String}([ + "monitoredobj", "monitoredterm", "switchedobj", "switchedterm", "numfast", + "phasefast", "phasedelayed", "groundfast", "grounddelayed", "phasetrip", + "groundtrip", "phaseinst", "groundinst", "reset", "shots", + "recloseintervals", "delay", "action", "tdphfast", "tdgrfast", + "tdphdelayed", "tdgrdelayed", "basefreq", "enabled", "like" +]) + +const _relay_properties = Vector{String}([ + "monitoredobj", "monitoredterm", "switchedobj", "switchedterm", "type", + "phasecurve", "groundcurve", "phasetrip", "groundtrip", "tdphase", + "tdground", "phaseinst", "groundinst", "reset", "shots", + "recloseintervals", "delay", "overvoltcurve", "undervoltcurve", + "kvbase", "47%pickup", "46baseamps", "46%pickup", "46isqt", + "variable", "overtrip", "undertrip", "breakertime", "action", "basefreq", + "enabled" +]) + +const _fuse_properties = Vector{String}([ + "monitoredobj", "monitoredterm", "switchedobj", "switchedterm", + "fusecurve", "ratedcurrent", "delay", "action", "basefreq", "enabled" +]) + +const _swtcontrol_properties = Vector{String}([ + "action", "basefreq", "delay", "enabled", "like", "lock", "normal", + "reset", "state", "switchedobj", "switchedterm" +]) + +const _dss_object_properties = Dict{String,Vector{String}}( + "linecode" => _linecode_properties, + "linegeometry" => _linegeometry_properties, + "linespacing" => _linespacing_properties, + "loadshape" => _loadshape_properties, + "xycurve" => _xycurve_properties, + "growthshape" => _growthshape_properties, + "tcc_curve" => _tcc_curve_properties, + "cndata" => _cndata_properties, + "tsdata" => _tsdata_properties, + "wiredata" => _wiredata_properties, + "xfmrcode" => _xfmrcode_properties, + "vsource" => _vsource_properties, + "circuit" => _vsource_properties, # alias circuit to vsource + "isource" => _isource_properties, + "fault" => _fault_properties, + "capacitor" => _capacitor_properties, + "line" => _line_properties, + "reactor" => _reactor_properties, + "transformer" => _transformer_properties, + "gictransformer" => _gictransformer_properties, + "gicline" => _gicline_properties, + "load" => _load_properties, + "generator" => _generator_properties, + "indmach012" => _indmach012_properties, + "storage" => _storage_properties, + "capcontrol" => _capacitor_properties, + "regcontrol" => _regcontrol_properties, + "energymeter" => _energymeter_properties, + "monitor" => _monitor_properties, + "pvsystem" => _pvsystem_properties, + "relay" => _relay_properties, + "recloser" => _reactor_properties, + "fuse" => _fuse_properties, + "swtcontrol" => _swtcontrol_properties, +) "parses single column load profile files" -function _parse_loadshape_csv(path::AbstractString, type::AbstractString; header::Bool=false, column::Int=1, interval::Bool=false) +function _parse_csv_file(path::AbstractString, type::AbstractString; header::Bool=false, column::Int=1, interval::Bool=false)::Union{Vector{String}, Tuple{Vector{String}, Vector{String}, Vector{String}}, Tuple{Vector{String}, Vector{String}}} open(path, "r") do f lines = readlines(f) if header @@ -458,36 +279,36 @@ function _parse_loadshape_csv(path::AbstractString, type::AbstractString; header end if type == "mult" - return [split(line, ",")[column] for line in lines] + return Vector{String}([split(line, ",")[column] for line in lines]) elseif type == "csvfile" if interval - hour, mult = [], [] + hour, mult = Vector{String}([]), Vector{String}([]) for line in lines d = split(line, ",") - push!(hour, d[1]) - push!(mult, d[2]) + push!(hour, string(d[1])) + push!(mult, string(d[2])) end return hour, mult else - return [split(line, ",")[1] for line in lines] + return Vector{String}([split(line, ",")[1] for line in lines]) end elseif type == "pqcsvfile" if interval - hour, pmult, qmult = [], [], [] + hour, pmult, qmult = Vector{String}([]), Vector{String}([]), Vector{String}([]) for line in lines d = split(line, ",") - push!(hour, d[1]) - push!(pmult, d[2]) - push!(qmult, d[3]) + push!(hour, string(d[1])) + push!(pmult, string(d[2])) + push!(qmult, string(d[3])) end return hour, pmult, qmult else - pmult, qmult = [], [] + pmult, qmult = Vector{String}([]), Vector{String}([]) for line in lines d = split(line, ",") - push!(pmult, d[1]) - push!(qmult, d[2]) + push!(pmult, string(d[1])) + push!(qmult, string(d[2])) end return pmult, qmult @@ -498,7 +319,7 @@ end "parses sng and dbl precision loadshape binary files" -function _parse_binary(path::AbstractString, precision::Type; npts::Union{Int,Nothing}=nothing, interval::Bool=false) +function _parse_binary_file(path::AbstractString, precision::Type; npts::Union{Int,Nothing}=nothing, interval::Bool=false)::Union{Vector{precision}, Tuple{Vector{precision}, Vector{precision}}} open(path, "r") do f if npts === nothing data = precision[] @@ -510,8 +331,6 @@ function _parse_binary(path::AbstractString, precision::Type; npts::Union{Int,No break end end - - return data else data = Array{precision, 1}(undef, interval ? npts * 2 : npts) @@ -520,30 +339,30 @@ function _parse_binary(path::AbstractString, precision::Type; npts::Union{Int,No catch EOFError error("Error reading binary file: likely npts is wrong") end + end - if interval - data = reshape(data, 2, :) - return data[1, :], data[2, :] - else - return data - end + if interval + data = reshape(data, 2, :) + return data[1, :], data[2, :] + else + return data end end end "parses csv or binary loadshape files" -function _parse_loadshape_file(path::AbstractString, type::AbstractString, npts::Union{Int,Nothing}; header::Bool=false, interval::Bool=false, column::Int=1) +function _parse_data_file(path::AbstractString, type::AbstractString, npts::Union{Int,Nothing}; header::Bool=false, interval::Bool=false, column::Int=1) if type in ["csvfile", "mult", "pqcsvfile"] - return _parse_loadshape_csv(path, type; header=header, column=column, interval=interval) + return _parse_csv_file(path, type; header=header, column=column, interval=interval) elseif type in ["sngfile", "dblfile"] - return _parse_binary(path, Dict("sngfile" => Float32, "dblfile" => Float64)[type]; npts=npts, interval=interval) + return _parse_binary_file(path, Dict("sngfile" => Float32, "dblfile" => Float64)[type]; npts=npts, interval=interval) end end "parses pmult and qmult entries on loadshapes" -function _parse_mult(mult_string::AbstractString; path::AbstractString="", npts::Union{Int,Nothing}=nothing) +function _parse_mult_parameter(mult_string::AbstractString; path::AbstractString="", npts::Union{Int,Nothing}=nothing)::String if !occursin("=", mult_string) return mult_string else @@ -558,60 +377,81 @@ function _parse_mult(mult_string::AbstractString; path::AbstractString="", npts: end file_key = [prop for prop in keys(props) if endswith(prop, "file")][1] - fullpath = path == "" ? props[file_key] : join([path, props[file_key]], '/') + full_path = path == "" ? props[file_key] : join([path, props[file_key]], '/') type = file_key == "file" ? "mult" : file_key - return "($(join(_parse_loadshape_file(fullpath, type, npts; header=get(props, "header", false), column=parse(Int, get(props, "column", "1"))), ",")))" + return "($(join(_parse_data_file(full_path, type, npts; header=get(props, "header", false), column=parse(Int, get(props, "column", "1"))), ",")))" end end "parses loadshape component" -function _parse_loadshape!(curCompDict::Dict{String,Any}; path::AbstractString="") - if any(parse.(Float64, [get(curCompDict, "interval", "1.0"), get(curCompDict, "minterval", "60.0"), get(curCompDict, "sinterval", "3600.0")]) .<= 0.0) +function _parse_loadshape!(current_obj::Dict{String,<:Any}; path::AbstractString="") + if any(parse.(Float64, [get(current_obj, "interval", "1.0"), get(current_obj, "minterval", "60.0"), get(current_obj, "sinterval", "3600.0")]) .<= 0.0) interval = true else interval = false end - npts = parse(Int, get(curCompDict, "npts", "1")) + npts = parse(Int, get(current_obj, "npts", "1")) - for prop in curCompDict["prop_order"] + for prop in current_obj["prop_order"] if prop in ["pmult", "qmult"] - curCompDict[prop] = _parse_mult(curCompDict[prop]; path=path, npts=npts) + current_obj[prop] = _parse_mult_parameter(current_obj[prop]; path=path, npts=npts) elseif prop in ["csvfile", "pqcsvfile", "sngfile", "dblfile"] - fullpath = path == "" ? curCompDict[prop] : join([path, curCompDict[prop]], '/') - data = _parse_loadshape_file(fullpath, prop, parse(Int, get(curCompDict, "npts", "1")); interval=interval, header=false) + full_path = path == "" ? current_obj[prop] : join([path, current_obj[prop]], '/') + data = _parse_data_file(full_path, prop, parse(Int, get(current_obj, "npts", "1")); interval=interval, header=false) if prop == "pqcsvfile" if interval - curCompDict["hour"], curCompDict["pmult"], curCompDict["qmult"] = data + current_obj["hour"], current_obj["pmult"], current_obj["qmult"] = data else - curCompDict["pmult"], curCompDict["qmult"] = data + current_obj["pmult"], current_obj["qmult"] = data end else if interval - curCompDict["hour"], curCompDict["pmult"] = data + current_obj["hour"], current_obj["pmult"] = data else - curCompDict["pmult"] = data + current_obj["pmult"] = data end end end end for prop in ["pmult", "qmult", "hour"] - if haskey(curCompDict, prop) && isa(curCompDict[prop], Array) - curCompDict[prop] = "($(join(curCompDict[prop], ",")))" - elseif haskey(curCompDict, prop) && isa(curCompDict[prop], String) && !_isa_array(curCompDict[prop]) - curCompDict[prop] = "($(curCompDict[prop]))" + if haskey(current_obj, prop) && isa(current_obj[prop], Array) + current_obj[prop] = "($(join(current_obj[prop], ",")))" + elseif haskey(current_obj, prop) && isa(current_obj[prop], String) && !_isa_array(current_obj[prop]) + current_obj[prop] = "($(current_obj[prop]))" + end + end +end + + +"parse xycurve component" +function _parse_xycurve!(current_obj::Dict{String,<:Any}; path::AbstractString="") + for prop in current_obj["prop_order"] + if prop in ["csvfile", "sngfile", "dblfile"] + full_path = isempty(path) ? current_obj[prop] : join([path, current_obj[prop]], '/') + data = _parse_data_file(full_path, prop, nothing; interval=true, header=false) + current_obj["xarray"], current_obj["yarray"] = data + end + + end + + for prop in ["xarray", "yarray", "points"] + if haskey(current_obj, prop) && isa(current_obj[prop], Array) + current_obj[prop] = "($(join(current_obj[prop], ",")))" + elseif haskey(current_obj, prop) && isa(current_obj[prop], String) && !_isa_array(current_obj[prop]) + current_obj[prop] = "($(current_obj[prop]))" end end end "strips lines that are either commented (block or single) or empty" -function _strip_lines(lines::Array)::Array +function _strip_lines(lines::Vector{<:AbstractString})::Vector{String} blockComment = false - stripped_lines = [] + stripped_lines = Vector{String}([]) for line in lines if startswith(strip(line), "/*") || endswith(strip(line), "*/") blockComment = !blockComment @@ -624,13 +464,11 @@ end """ - _parse_buscoords(file) - Parses a Bus Coordinate `file`, in either "dat" or "csv" formats, where in "dat", columns are separated by spaces, and in "csv" by commas. File expected to contain "bus,x,y" on each line. """ -function _parse_buscoords(file::AbstractString)::Array +function _parse_buscoords_file(file::AbstractString)::Dict{String,Any} file_str = read(open(file), String) regex = r",\s*" if endswith(lowercase(file), "csv") || endswith(lowercase(file), "dss") @@ -639,166 +477,183 @@ function _parse_buscoords(file::AbstractString)::Array lines = _strip_lines(split(file_str, '\n')) - coordArray = [] + buscoords = Dict{String,Dict{String,Any}}() for line in lines bus, x, y = split(line, regex; limit=3) - push!(coordArray, Dict{String,Any}("name"=>lowercase(strip(bus, [','])), - "x"=>parse(Float64, strip(x, [','])), - "y"=>parse(Float64, strip(y, [',', '\r'])))) + buscoords[lowercase(strip(bus, [',']))] = Dict{String,Any}("x"=>parse(Float64, strip(x, [','])), "y"=>parse(Float64, strip(y, [',', '\r']))) end - return coordArray + + return buscoords end """ - _parse_properties(properties) - Parses a string of `properties` of a component type, character by character into an array with each element containing (if present) the property name, "=", and the property value. """ -function _parse_properties(properties::AbstractString)::Array - propsOut = [] - endArray = true - endProp = false - endEquality = false - endSQuot = true - endDQuot = true - str_out = "" +function _parse_properties(properties::AbstractString)::Vector{String} + parsed_properties = Vector{SubString{String}}([]) + end_array = true + end_property = false + end_equality = false + end_single_quote = true + end_double_quote = true + string_out = "" properties = replace(properties, r"\s*=\s*" => "=") - nchars = length(properties) + num_chars = length(properties) for (n, char) in enumerate(properties) - sstr_out = split(str_out, "=") - if length(sstr_out) == 2 && sstr_out[2] != "" - endEquality = true - elseif !occursin("=", str_out) && (char == ' ' || n == nchars) - endEquality = true - elseif occursin("=", str_out) && (char == ' ' || n == nchars) && endArray - endEquality = true + substring_out = split(string_out, "=") + if length(substring_out) == 2 && substring_out[2] != "" + end_equality = true + elseif !occursin("=", string_out) && (char == ' ' || n == num_chars) + end_equality = true + elseif occursin("=", string_out) && (char == ' ' || n == num_chars) && end_array + end_equality = true else - endEquality = false + end_equality = false end if char == ' ' - endProp = true + end_property = true else - endProp = false + end_property = false end if char in ['[', '(', '{'] - endArray = false + end_array = false elseif char in [']', ')', '}'] - endArray = true + end_array = true end if char == '\"' - endDQuot = !endDQuot + end_double_quote = !end_double_quote elseif char == '\'' - endSQuot = !endSQuot + end_single_quote = !end_single_quote end - if char != ' ' || !endArray || !endDQuot || !endSQuot - str_out = string(str_out, char) + if char != ' ' || !end_array || !end_double_quote || !end_single_quote + string_out = string(string_out, char) end - if str_out != "" && endArray && endProp && endEquality && endDQuot && endSQuot || n == nchars - push!(propsOut, str_out) - str_out = "" + if string_out != "" && end_array && end_property && end_equality && end_double_quote && end_single_quote || n == num_chars + push!(parsed_properties, string_out) + string_out = "" end end - return propsOut + return parsed_properties end """ - _add_component!(dss_data, ctype_name, compDict) - -Adds a component of type `ctype_name` with properties given by `compDict` to -the existing `dss_data` structure. If a component of the same type has already -been added to `dss_data`, the new component is appeneded to the existing array +Adds a component of type `obj_type_name` with properties given by `object` to +the existing `data_dss` structure. If a component of the same type has already +been added to `data_dss`, the new component is appeneded to the existing array of components of that type, otherwise a new array is created. """ -function _add_component!(dss_data::Dict, ctype_name::AbstractString, compDict::Dict) - ctype = split(ctype_name, '.'; limit=2)[1] - if haskey(dss_data, ctype) - push!(dss_data[ctype], compDict) +function _add_component!(data_dss::Dict{String,<:Any}, obj_type_name::AbstractString, object::Dict{String,<:Any}) + obj_type = split(obj_type_name, '.'; limit=2)[1] + if obj_type == "circuit" + if haskey(data_dss, "circuit") + Memento.error(_LOGGER, "Cannot have two circuits, invalid dss") + else + data_dss[obj_type] = object + end + elseif haskey(data_dss, obj_type) + data_dss[obj_type][object["name"]] = object else - dss_data[ctype] = [compDict] + data_dss[obj_type] = Dict{String,Any}(object["name"] => object) end end -""" - _add_property(compDict, key, value) +function _add_component_edits!(data_dss::Dict{String,<:Any}, obj_type_name::SubString{String}, object::Dict{String,<:Any}) + obj_type = split(obj_type_name, '.'; limit=2)[1] + if !haskey(data_dss, obj_type) + data_dss[obj_type] = Dict{String,Any}( + object["name"] => object + ) + else + if !haskey(data_dss[obj_type], object["name"]) + data_dss[obj_type][object["name"]] = object + else + merge!(data_dss[obj_type][object["name"]], object) + end + end +end + -Adds a property to an existing component properties dictionary `compDict` given +""" +Adds a property to an existing component properties dictionary `object` given the `key` and `value` of the property. If a property of the same name already -exists inside `compDict`, the original value is converted to an array, and the +exists inside `object`, the original value is converted to an array, and the new value is appended to the end. """ -function _add_property(compDict::Dict, key::AbstractString, value::Any)::Dict - if !haskey(compDict, "prop_order") - compDict["prop_order"] = Array{String,1}(["name"]) +function _add_property(object::Dict{String,<:Any}, key::SubString{String}, value::Any)::Dict{String,Any} + if !haskey(object, "prop_order") + object["prop_order"] = Vector{String}(["name"]) end - cur_wdg = "wdg" in compDict["prop_order"] ? string(filter(p->occursin("wdg", p), compDict["prop_order"])[end][end]) : "" - cur_wdg = cur_wdg == "g" ? "" : cur_wdg + current_wdg = "wdg" in object["prop_order"] ? string(filter(p->occursin("wdg", p), object["prop_order"])[end][end]) : "" + current_wdg = current_wdg == "g" ? "" : current_wdg if key in ["wdg", "bus", "conn", "kv", "kva", "tap", "%r", "rneut", "xneut"] - key = join(filter(p->!isempty(p), [key, cur_wdg]), "_") + key = join(filter(p->!isempty(p), [key, current_wdg]), "_") end - if haskey(compDict, lowercase(key)) + if haskey(object, lowercase(key)) rmatch = match(r"_(\d+)$", key) if typeof(rmatch) != Nothing - endNum = parse(Int, rmatch.captures[1]) + 1 - key = replace(key, r"_(\d+)$" => "_$endNum") + end_num = parse(Int, rmatch.captures[1]) + 1 + key = replace(key, r"_(\d+)$" => "_$end_num") else key = string(key, "_2") end end - compDict[lowercase(key)] = value - push!(compDict["prop_order"], lowercase(key)) + object[lowercase(key)] = value + push!(object["prop_order"], string(lowercase(key))) - return compDict + return object end """ - _parse_component(component, properies, compDict=Dict{String,Any}()) - -Parses a `component` with `properties` into a `compDict`. If `compDict` is not +Parses a `component` with `properties` into a `object`. If `object` is not defined, an empty dictionary will be used. Assumes that unnamed properties are given in order, but named properties can be given anywhere. """ -function _parse_component(component::AbstractString, properties::AbstractString, compDict::Dict=Dict{String,Any}(); path::AbstractString="") - Memento.debug(_LOGGER, "Properties: $properties") - ctype, name = split(component, '.'; limit=2) +function _parse_component(component::AbstractString, properties::AbstractString, object::Dict{String,<:Any}=Dict{String,Any}(); path::String="")::Dict{String,Any} + obj_type, name = split(component, '.'; limit=2) + + if !haskey(object, "prop_order") + object["prop_order"] = Vector{String}([]) + end - if !haskey(compDict, "name") - compDict["name"] = name + if !haskey(object, "name") + object["name"] = string(name) end - propArray = _parse_properties(properties) - Memento.debug(_LOGGER, "propArray: $propArray") + push!(object["prop_order"], "name") - propNames = _get_prop_name(ctype) - propIdx = 1 + property_array = _parse_properties(properties) - for (n, property) in enumerate(propArray) + property_names = _dss_object_properties[obj_type] + property_idx = 1 + + for (n, property) in enumerate(property_array) if property == "" continue elseif !occursin("=", property) - property = join([propNames[propIdx], property], '=') - propIdx += 1 + property = join([property_names[property_idx], property], '=') + property_idx += 1 else - if ctype == "loadshape" && startswith(property, "mult") + if obj_type == "loadshape" && startswith(property, "mult") property = replace(property, "mult" => "pmult") - elseif ctype == "transformer" + elseif obj_type == "transformer" prop_name, _ = split(property,'=') if prop_name == "ppm" property = replace(property, prop_name => "ppm_antifloat") @@ -811,36 +666,38 @@ function _parse_component(component::AbstractString, properties::AbstractString, end end - propIdxs = findall(e->e==split(property,'=')[1], propNames) - if length(propIdxs) > 0 - propIdx = findall(e->e==split(property,'=')[1], propNames)[1] + 1 + property_idxs = findall(e->e==split(property,'=')[1], property_names) + if length(property_idxs) > 0 + property_idx = findall(e->e==split(property,'=')[1], property_names)[1] + 1 end end key, value = split(property, '='; limit=2) if occursin(r"\(\s*(sng|dbl)*file=(.+)\)", value) - value = _parse_mult(value; path=path) + value = _parse_mult_parameter(value; path=path) end - _add_property(compDict, key, value) + _add_property(object, key, value) end - return compDict + return object end """ - _merge_dss!(dss_prime, dss_to_add) - Merges two (partially) parsed OpenDSS files to the same dictionary `dss_prime`. Used in cases where files are referenced via the "compile" or "redirect" OpenDSS commands inside the originating file. """ -function _merge_dss!(dss_prime::Dict{String,Array}, dss_to_add::Dict{String,Array}) +function _merge_dss!(dss_prime::Dict{String,<:Any}, dss_to_add::Dict{String,<:Any}) for (k, v) in dss_to_add - if k in keys(dss_prime) - append!(dss_prime[k], v) + if k in keys(dss_prime) && (isa(v, Dict) || isa(v, Set)) + if isa(v, Dict) + merge!(dss_prime[k], v) + elseif isa(v, Set) + union!(dss_prime[k], v) + end else dss_prime[k] = v end @@ -849,16 +706,14 @@ end """ - _parse_line(elements, curCompDict=Dict{String,Any}()) - Parses an already separated line given by `elements` (an array) of an OpenDSS -file into `curCompDict`. If not defined, `curCompDict` is an empty dictionary. +file into `current_obj`. If not defined, `current_obj` is an empty dictionary. """ -function _parse_line(elements::Array, curCompDict::Dict=Dict{String,Any}(); path::AbstractString="") - curCtypeName = strip(elements[2], ['\"', '\'']) - if startswith(curCtypeName, "object") - curCtypeName = split(curCtypeName, '=')[2] - curCompDict["name"] = split(curCtypeName, '.')[2] +function _parse_line(elements::Vector{String}; current_obj::Dict{String,<:Any}=Dict{String,Any}(), path::AbstractString="")::Tuple{SubString{String}, Dict{String,Any}} + current_obj_type = strip(elements[2], ['\"', '\'']) + if startswith(current_obj_type, "object") + current_obj_type = split(current_obj_type, '=')[2] + current_obj["name"] = split(current_obj_type, '.')[2] else if length(elements) != 3 properties = "" @@ -866,10 +721,10 @@ function _parse_line(elements::Array, curCompDict::Dict=Dict{String,Any}(); path properties = elements[3] end - curCompDict = _parse_component(curCtypeName, properties; path=path) + current_obj = _parse_component(current_obj_type, properties; path=path) end - return curCtypeName, curCompDict + return current_obj_type, current_obj end @@ -880,44 +735,43 @@ end """ - _assign_property!(dss_data, cType, cName, propName, propValue) - -Assigns a property with name `propName` and value `propValue` to the component -of type `cType` named `cName` in `dss_data`. +Assigns a property with name `property_name` and value `property_value` to the component +of type `obj_type` named `obj_name` in `data_dss`. """ -function _assign_property!(dss_data::Dict, cType::AbstractString, cName::AbstractString, - propName::AbstractString, propValue::Any) - if haskey(dss_data, cType) - for obj in dss_data[cType] - if obj["name"] == cName - obj[propName] = propValue - end - end +function _assign_property!(data_dss::Dict{String,<:Any}, obj_type::AbstractString, obj_name::AbstractString, property_name::AbstractString, property_value::Any) + if haskey(data_dss, obj_type) && haskey(data_dss[obj_type], obj_name) + data_dss[obj_type][obj_name][property_name] = property_value else - Memento.warn(_LOGGER, "Cannot find $cType object $cName.") + Memento.warn(_LOGGER, "Cannot find $obj_type object $obj_name.") end end -""" - parse_dss(filename) +"" +function parse_dss(filename::AbstractString)::Dict{String,Any} + data_dss = open(filename) do io + parse_dss(io) + end + return data_dss +end + +""" Parses a OpenDSS file given by `filename` into a Dict{Array{Dict}}. Only supports components and options, but not commands, e.g. "plot" or "solve". Will also parse files defined inside of the originating DSS file via the "compile", "redirect" or "buscoords" commands. """ -function parse_dss(io::IOStream)::Dict +function parse_dss(io::IOStream)::Dict{String,Any} filename = match(r"^$", io.name).captures[1] - Memento.info(_LOGGER, "Calling parse_dss on $filename") - currentFile = split(filename, "/")[end] + current_file = split(filename, "/")[end] path = join(split(filename, '/')[1:end-1], '/') - dss_data = Dict{String,Array}() + data_dss = Dict{String,Any}() - dss_data["filename"] = [currentFile] + data_dss["filename"] = Set{String}([string(current_file)]) - curCompDict = Dict{String,Any}() - curCtypeName = "" + current_obj = Dict{String,Any}() + current_obj_type = "" lines = readlines(io) @@ -926,317 +780,130 @@ function parse_dss(io::IOStream)::Dict for (n, line) in enumerate(stripped_lines) real_line_num = findall(lines .== line)[1] - Memento.debug(_LOGGER, "LINE $real_line_num: $line") line = _strip_comments(line) - if startswith(strip(line), '~') - curCompDict = _parse_component(curCtypeName, strip(strip(lowercase(line)), '~'), curCompDict) + if startswith(strip(line), '~') || startswith(strip(lowercase(line)), "more") + if startswith(strip(lowercase(line)), "more") + line = lowercase(strip(line)[5:end]) + end + + current_obj = _parse_component(current_obj_type, strip(strip(lowercase(line)), '~'), current_obj) if n < nlines && startswith(strip(stripped_lines[n + 1]), '~') continue else - _add_component!(dss_data, curCtypeName, curCompDict) + _add_component!(data_dss, current_obj_type, current_obj) end else - curCompDict = Dict{String,Any}() + current_obj = Dict{String,Any}() line_elements = split(line, r"\s+"; limit=3) cmd = lowercase(line_elements[1]) - if cmd == "clear" - Memento.info(_LOGGER, "`dss_data` has been reset with the \"clear\" command.") - dss_data = Dict{String,Array}("filename"=>dss_data["filename"]) - continue + if cmd in _dss_unsupported_commands + Memento.warn(_LOGGER, "Command \"$cmd\" on line $real_line_num in \"$current_file\" is not supported, skipping.") - elseif cmd == "redirect" - file = line_elements[2] - fullpath = path == "" ? file : join([path, file], '/') - Memento.info(_LOGGER, "Redirecting to file \"$file\"") - _merge_dss!(dss_data, parse_dss(fullpath)) + elseif cmd == "clear" + Memento.info(_LOGGER, "Circuit has been reset with the \"clear\" on line $real_line_num in \"$current_file\"") + data_dss = Dict{String,Any}("filename"=>data_dss["filename"]) continue - elseif cmd == "compile" + elseif cmd in ["redirect", "compile"] file = split(strip(line_elements[2], ['(',')']), '\\')[end] - fullpath = path == "" ? file : join([path, file], '/') - Memento.info(_LOGGER, "Compiling file \"$file\"") - _merge_dss!(dss_data, parse_dss(fullpath)) + + if !(file in data_dss["filename"]) + full_path = path == "" ? file : join([path, file], '/') + Memento.info(_LOGGER, "Redirecting to \"$file\" on line $real_line_num in \"$current_file\"") + _merge_dss!(data_dss, parse_dss(full_path)) + end + continue elseif cmd == "set" - Memento.debug(_LOGGER, "set command: $line_elements") + properties = _parse_properties(join(line_elements[2:end], " ")) if length(line_elements) == 2 property, value = split(lowercase(line_elements[2]), '='; limit=2) else - property, value = lowercase(line_elements[2]), strip(strip(lowercase(line_elements[3]), '=')) + property, value = split(lowercase(join(line_elements[2:end], " ")), '='; limit=2) end - if !haskey(dss_data, "options") - dss_data["options"] = [Dict{String,Any}()] + if !haskey(data_dss, "options") + data_dss["options"] = Dict{String,Any}() end - dss_data["options"][1]["$(property)"] = value + data_dss["options"]["$(property)"] = value continue - elseif cmd == "buscoords" - file = line_elements[2] - fullpath = path == "" ? file : join([path, file], '/') - Memento.debug(_LOGGER, "Buscoords path: $fullpath") - dss_data["buscoords"] = _parse_buscoords(fullpath) + elseif cmd == "edit" + current_obj_type, current_obj = _parse_line([lowercase(line_element) for line_element in line_elements]; path=path) - elseif cmd == "new" - curCtypeName, curCompDict = _parse_line([lowercase(line_element) for line_element in line_elements]; path=path) - - if startswith(curCtypeName, "loadshape") - _parse_loadshape!(curCompDict; path=path) - end - else - try - cType, cName, props = split(lowercase(line), '.'; limit=3) - propsOut = _parse_properties(props) - wdg = "" - for prop in propsOut - propName, propValue = split(prop, '=') - if cType == "transformer" - wdg = propName == "wdg" && propValue != "1" ? propValue : propName == "wdg" && propValue == "1" ? "" : wdg - - if propName in ["wdg", "bus", "conn", "kv", "kva", "tap", "%r", "rneut", "xneut"] - propName = join(filter(p->!isempty(p), [propName, wdg]), "_") - end - - _assign_property!(dss_data, cType, cName, propName, propValue) - else - _assign_property!(dss_data, cType, cName, propName, propValue) - end - end - catch - Memento.warn(_LOGGER, "Command \"$cmd\" on line $real_line_num in \"$currentFile\" is not supported, skipping.") - end - end - - Memento.debug(_LOGGER, "size curCompDict: $(length(curCompDict))") - if n < nlines && startswith(strip(stripped_lines[n + 1]), '~') - continue - elseif length(curCompDict) > 0 - _add_component!(dss_data, curCtypeName, curCompDict) - else + _add_component_edits!(data_dss, current_obj_type, current_obj) continue - end - end - end - Memento.info(_LOGGER, "Done parsing $filename") - return dss_data -end + elseif cmd in ["disable", "enable"] + current_obj_type, current_obj_name = split(join(line_elements[2:end], ""), ".") + enabled = cmd == "enable" ? "true" : "false" -"" -function parse_dss(filename::AbstractString)::Dict - dss_data = open(filename) do io - parse_dss(io) - end - return dss_data -end - - -"parses the raw dss values into their expected data types" -function _parse_element_with_dtype(dtype, element) - if _isa_matrix(element) - out = _parse_matrix(eltype(dtype), element) - elseif _isa_array(element) - out = _parse_array(eltype(dtype), element) - elseif dtype <: Bool - if element in ["n", "no"] - element = "false" - elseif element in ["y", "yes"] - element = "true" - end - out = parse(dtype, element) - elseif _isa_rpn(element) - out = _parse_rpn(element) - elseif dtype == String - out = element - else - if _isa_conn(element) - out = _parse_conn(element) - else - try - out = parse(dtype, element) - catch - Memento.warn(_LOGGER, "cannot parse $element as $dtype, leaving as String.") - out = element - end - end - end - - return out -end + _add_component_edits!(data_dss, current_obj_type, Dict{String,Any}("name"=>current_obj_name, "enabled"=>enabled)) + continue + elseif cmd in ["buscoords", "latloncoords"] + file = line_elements[2] + full_path = path == "" ? file : join([path, file], '/') + Memento.info(_LOGGER, "Reading Buscoords in \"$file\" on line $real_line_num in \"$current_file\"") + data_dss["buscoords"] = _parse_buscoords_file(full_path) -""" - parse_dss_with_dtypes!(dss_data, toParse) + elseif cmd == "new" + current_obj_type, current_obj = _parse_line([lowercase(line_element) for line_element in line_elements]; path=path) -Parses the data in keys defined by `toParse` in `dss_data` using types given by -the default properties from the `get_prop_default` function. -""" -function parse_dss_with_dtypes!(dss_data::Dict, toParse::Array{String}=[]) - for compType in toParse - if haskey(dss_data, compType) - Memento.debug(_LOGGER, "type: $compType") - for item in dss_data[compType] - dtypes = _get_dtypes(compType) - for (k, v) in item - if haskey(dtypes, k) - Memento.debug(_LOGGER, "key: $k") - if isa(v, Array) - arrout = [] - for el in v - if isa(v, AbstractString) - push!(arrout, _parse_element_with_dtype(dtypes[k], el)) - else - push!(arrout, el) - end - end - item[k] = arrout - elseif isa(v, AbstractString) - item[k] = _parse_element_with_dtype(dtypes[k], v) - else - Memento.error(_LOGGER, "dtype unknown $compType, $k, $v") - end - end + if startswith(current_obj_type, "loadshape") + _parse_loadshape!(current_obj; path=path) + elseif startswith(current_obj_type, "xycurve") + _parse_xycurve!(current_obj; path=path) end - end - end - end -end - - -""" - _parse_busname(busname) -Parses busnames as defined in OpenDSS, e.g. "primary.1.2.3.0". -""" -function _parse_busname(busname::AbstractString) - parts = split(busname, '.'; limit=2) - name = parts[1] - elements = "1.2.3" - - if length(parts) >= 2 - name, elements = split(busname, '.'; limit=2) - end + if startswith(current_obj_type, "circuit") + current_obj_type = "vsource.source" - nodes = Array{Bool}([0 0 0 0]) + data_dss["circuit"] = current_obj["name"] - for num in 1:3 - if occursin("$num", elements) - nodes[num] = true - end - end - - if occursin("0", elements) || sum(nodes[1:3]) == 1 - nodes[4] = true - end - - return name, nodes -end - - -""" - _get_conductors_ordered(busname; neutral=true) - -Returns an ordered list of defined conductors. If ground=false, will omit any `0` -""" -function _get_conductors_ordered(busname::AbstractString; neutral::Bool=true, nconductors::Int=3)::Array - parts = split(busname, '.'; limit=2) - ret = [] - if length(parts)==2 - conds_str = split(parts[2], '.') - if neutral - ret = [parse(Int, i) for i in conds_str] - else - ret = [parse(Int, i) for i in conds_str if i != "0"] - end - else - ret = collect(1:nconductors) - end - - return ret -end - - -"converts Dict{String,Any} to Dict{Symbol,Any} for passing as kwargs" -function _to_sym_keys(data::Dict{String,Any})::Dict{Symbol,Any} - return Dict{Symbol,Any}((Symbol(k), v) for (k, v) in data) -end - - -"" -function _apply_ordered_properties(defaults::Dict{String,Any}, raw_dss::Dict{String,Any}; code_dict::Dict{String,Any}=Dict{String,Any}()) - _defaults = deepcopy(defaults) - - for prop in filter(p->p!="like", raw_dss["prop_order"]) - if prop in ["linecode", "loadshape"] - merge!(defaults, code_dict) - else - defaults[prop] = _defaults[prop] - end - end - - return defaults -end - - -"applies `like` to component" -function _apply_like!(raw_dss, dss_data, comp_type) - default_exclusions = - links = ["like"] - if any(link in raw_dss["prop_order"] for link in links) - new_prop_order = [] - raw_dss_copy = deepcopy(raw_dss) - - for prop in raw_dss["prop_order"] - push!(new_prop_order, prop) - - if prop in get(_like_exclusions, comp_type, []) || prop in _like_exclusions["all"] - continue - end - - if prop in links - linked_dss = find_component(dss_data, raw_dss[prop], comp_type) - if isempty(linked_dss) - Memento.warn(_LOGGER, "$comp_type.$(raw_dss["name"]): $prop=$(raw_dss[prop]) cannot be found") - else - for linked_prop in linked_dss["prop_order"] - if linked_prop in get(_like_exclusions, comp_type, []) || linked_prop in _like_exclusions["all"] - continue + current_obj["name"] = "source" + end + elseif split(cmd, '.')[1] in keys(_dss_object_properties) + obj_type, obj_name, props = split(lowercase(line), '.'; limit=3) + parsed_properties = _parse_properties(props) + wdg = "" + for prop in parsed_properties + property_name, property_value = split(prop, '=') + if obj_type == "transformer" + wdg = property_name == "wdg" && property_value != "1" ? property_value : property_name == "wdg" && property_value == "1" ? "" : wdg + + if property_name in ["wdg", "bus", "conn", "kv", "kva", "tap", "%r", "rneut", "xneut"] + property_name = join(filter(p->!isempty(p), [property_name, wdg]), "_") end - push!(new_prop_order, linked_prop) - if linked_prop in links - _apply_like!(linked_dss, dss_data, comp_type) - else - raw_dss[linked_prop] = deepcopy(linked_dss[linked_prop]) - end + _assign_property!(data_dss, obj_type, obj_name, property_name, property_value) + else + _assign_property!(data_dss, obj_type, obj_name, property_name, property_value) end end else - raw_dss[prop] = deepcopy(raw_dss_copy[prop]) + Memento.warn(_LOGGER, "Command \"$cmd\" on line $real_line_num in \"$current_file\" is not recognized, skipping.") end - end - final_prop_order = [] - while !isempty(new_prop_order) - prop = popfirst!(new_prop_order) - if !(prop in new_prop_order) - push!(final_prop_order, prop) + if n < nlines && startswith(strip(stripped_lines[n + 1]), '~') + continue + elseif length(current_obj) > 0 + _add_component!(data_dss, current_obj_type, current_obj) + else + continue end end - raw_dss["prop_order"] = final_prop_order end -end + _parse_dss_with_dtypes!(data_dss) -"properties that should be excluded from being overwritten during the application of `like`" -const _like_exclusions = Dict{String,Array}("all" => ["name", "bus1", "bus2", "phases", "nphases", "enabled"], - "line" => ["switch"], - "transformer" => ["bank", "bus", "bus_2", "bus_3", "buses", "windings", "wdg", "wdg_2", "wdg_3"], - "linegeometry" => ["nconds"] - ) + data_dss["data_model"] = DSS + + return data_dss +end diff --git a/src/io/dss_structs.jl b/src/io/dss_structs.jl index 47e07440c..379e9f4a9 100644 --- a/src/io/dss_structs.jl +++ b/src/io/dss_structs.jl @@ -3,29 +3,25 @@ import LinearAlgebra: diagm import Statistics: mean, std -_convert_to_meters = Dict{String,Any}("mi" => 1609.3, - "km" => 1000.0, - "kft" => 304.8, - "m" => 1.0, - "ft" => 0.3048, - "in" => 0.0254, - "cm" => 0.01, - "mm" => 0.001, - "none" => 1.0 - ) +const _convert_to_meters = Dict{String,Float64}( + "mi" => 1609.3, + "km" => 1000.0, + "kft" => 304.8, + "m" => 1.0, + "ft" => 0.3048, + "in" => 0.0254, + "cm" => 0.01, + "mm" => 0.001, + "none" => 1.0 +) """ - _create_linecode(name; kwargs...) - Creates a Dict{String,Any} containing all of the properties of a Linecode. See OpenDSS documentation for valid fields and ways to specify the different -properties. DEPRECIATED: Calculation all done inside of _create_line() due to Rg, -Xg. Merge linecode values into line kwargs values BEFORE calling _create_line(). -This is now mainly used for parsing linecode dicts into correct data types. +properties. """ -function _create_linecode(name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_linecode(name::String=""; kwargs...)::Dict{String,Any} phases = get(kwargs, :nphases, 3) circuit_basefreq = get(kwargs, :circuit_basefreq, 60.0) basefreq = get(kwargs, :basefreq, circuit_basefreq) @@ -79,48 +75,48 @@ function _create_linecode(name::AbstractString=""; kwargs...) xmatrix = get(kwargs, :xmatrix, imag(Z)) cmatrix = get(kwargs, :cmatrix, imag(Yc) / (2 * pi * basefreq)) - # TODO: Rg, Xg cannot be handled at the LineCode level! - units = get(kwargs, :units, "none") - return Dict{String,Any}("name" => name, - "nphases" => phases, - "r1" => r1 / _convert_to_meters[units], - "x1" => x1 / _convert_to_meters[units], - "r0" => r0 / _convert_to_meters[units], - "x0" => x0 / _convert_to_meters[units], - "c1" => c1 / _convert_to_meters[units], - "c0" => c0 / _convert_to_meters[units], - "units" => "m", - "rmatrix" => rmatrix / _convert_to_meters[units], - "xmatrix" => xmatrix / _convert_to_meters[units], - "cmatrix" => cmatrix / _convert_to_meters[units], - "basefreq" => basefreq, - "normamps" => get(kwargs, :normamps, 400.0), - "emergamps" => get(kwargs, :emergamps, 600.0), - "faultrate" => get(kwargs, :faultrate, 0.1), - "pctperm" => get(kwargs, :pctperm, 20.0), - "repair" => get(kwargs, :repair, 3.0), - "kron" => get(kwargs, :kron, false), - "rg" => get(kwargs, :rg, 0.01805), - "xg" => get(kwargs, :xg, 0.155081), - "rho" => get(kwargs, :rho, 100.0), - "neutral" => get(kwargs, :neutral, 3), - "b1" => b1 / _convert_to_meters[units], - "b0" => b0 / _convert_to_meters[units] - ) + Dict{String,Any}( + "name" => name, + "nphases" => phases, + "r1" => r1 / _convert_to_meters[units], + "x1" => x1 / _convert_to_meters[units], + "r0" => r0 / _convert_to_meters[units], + "x0" => x0 / _convert_to_meters[units], + "c1" => c1 / _convert_to_meters[units], + "c0" => c0 / _convert_to_meters[units], + "units" => "m", + "rmatrix" => rmatrix / _convert_to_meters[units], + "xmatrix" => xmatrix / _convert_to_meters[units], + "cmatrix" => cmatrix / _convert_to_meters[units], + "basefreq" => basefreq, + "normamps" => get(kwargs, :normamps, 400.0), + "emergamps" => get(kwargs, :emergamps, 600.0), + "faultrate" => get(kwargs, :faultrate, 0.1), + "pctperm" => get(kwargs, :pctperm, 20.0), + "repair" => get(kwargs, :repair, 3.0), + "kron" => get(kwargs, :kron, false), + "rg" => get(kwargs, :rg, 0.01805), + "xg" => get(kwargs, :xg, 0.155081), + "rho" => get(kwargs, :rho, 100.0), + "neutral" => get(kwargs, :neutral, 3), + "b1" => b1 / _convert_to_meters[units], + "b0" => b0 / _convert_to_meters[units], + "like" => get(kwargs, :like, "") + ) end """ - _create_line(bus1, bus2, name; kwargs...) - Creates a Dict{String,Any} containing all of the properties for a Line. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_line(bus1="", bus2="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_line(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "") + bus2 = get(kwargs, :bus2, "") + phases = get(kwargs, :phases, 3) circuit_basefreq = get(kwargs, :circuit_basefreq, 60.0) basefreq = get(kwargs, :basefreq, circuit_basefreq) @@ -159,8 +155,8 @@ function _create_line(bus1="", bus2="", name::AbstractString=""; kwargs...) Ys = (complex(0.0, 2 * pi * basefreq * c1) * 2.0 + complex(0.0, 2 * pi * basefreq * c0)) / 3.0 Ym = (complex(0.0, 2 * pi * basefreq * c0) - complex(0.0, 2 * pi * basefreq * c1)) / 3.0 - Z = zeros(Complex{Float64}, phases, phases) - Yc = zeros(Complex{Float64}, phases, phases) + Z = Matrix{Complex{Float64}}(undef, phases, phases) + Yc = Matrix{Complex{Float64}}(undef, phases, phases) for i in 1:phases Z[i,i] = Zs Yc[i,i] = Ys @@ -198,55 +194,56 @@ function _create_line(bus1="", bus2="", name::AbstractString=""; kwargs...) xmatrix .-= xgmod xmatrix .*= lenmult * (basefreq / circuit_basefreq) - return Dict{String,Any}("name" => name, - "bus1" => bus1, - "bus2" => bus2, - "linecode" => get(kwargs, :linecode, ""), - "length" => len, - "phases" => phases, - "r1" => r1 / _convert_to_meters[units], - "x1" => x1 / _convert_to_meters[units], - "r0" => r0 / _convert_to_meters[units], - "x0" => x0 / _convert_to_meters[units], - "c1" => c1 / _convert_to_meters[units], - "c0" => c0 / _convert_to_meters[units], - "rmatrix" => rmatrix / _convert_to_meters[units], - "xmatrix" => xmatrix / _convert_to_meters[units], - "cmatrix" => cmatrix / _convert_to_meters[units], - "switch" => get(kwargs, :switch, false), - "rg" => rg / _convert_to_meters[units], - "xg" => xg / _convert_to_meters[units], - "rho" => get(kwargs, :rho, 100), - "geometry" => get(kwargs, :geometry, ""), - "units" => "m", - "spacing" => get(kwargs, :spacing, ""), - "wires" => get(kwargs, :wires, ""), - "earthmodel" => get(kwargs, :earthmodel, ""), - "cncables" => get(kwargs, :cncables, ""), - "tscables" => get(kwargs, :tscables, ""), - "b1" => b1 / _convert_to_meters[units], - "b0" => b0 / _convert_to_meters[units], - # Inherited Properties - "normamps" => get(kwargs, :normamps, 400.0), - "emergamps" => get(kwargs, :emergamps, 600.0), - "faultrate" => get(kwargs, :faultrate, 0.1), - "pctperm" => get(kwargs, :pctperm, 20.0), - "repair" => get(kwargs, :repair, 3.0), - "basefreq" => basefreq, - "enabled" => get(kwargs, :enabled, true), - ) + Dict{String,Any}( + "name" => name, + "bus1" => bus1, + "bus2" => bus2, + "linecode" => get(kwargs, :linecode, ""), + "length" => len, + "phases" => phases, + "r1" => r1 / _convert_to_meters[units], + "x1" => x1 / _convert_to_meters[units], + "r0" => r0 / _convert_to_meters[units], + "x0" => x0 / _convert_to_meters[units], + "c1" => c1 / _convert_to_meters[units], + "c0" => c0 / _convert_to_meters[units], + "rmatrix" => rmatrix / _convert_to_meters[units], + "xmatrix" => xmatrix / _convert_to_meters[units], + "cmatrix" => cmatrix / _convert_to_meters[units], + "switch" => get(kwargs, :switch, false), + "rg" => rg / _convert_to_meters[units], + "xg" => xg / _convert_to_meters[units], + "rho" => get(kwargs, :rho, 100), + "geometry" => get(kwargs, :geometry, ""), + "units" => "m", + "spacing" => get(kwargs, :spacing, ""), + "wires" => get(kwargs, :wires, ""), + "earthmodel" => get(kwargs, :earthmodel, ""), + "cncables" => get(kwargs, :cncables, ""), + "tscables" => get(kwargs, :tscables, ""), + "b1" => b1 / _convert_to_meters[units], + "b0" => b0 / _convert_to_meters[units], + # Inherited Properties + "normamps" => get(kwargs, :normamps, 400.0), + "emergamps" => get(kwargs, :emergamps, 600.0), + "faultrate" => get(kwargs, :faultrate, 0.1), + "pctperm" => get(kwargs, :pctperm, 20.0), + "repair" => get(kwargs, :repair, 3.0), + "basefreq" => basefreq, + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_load(bus1, name; kwargs...) - Creates a Dict{String,Any} containing all of the expected properties for a Load. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_load(bus1="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_load(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "") + kv = get(kwargs, :kv, 12.47) kw = get(kwargs, :kw, 10.0) pf = get(kwargs, :pf, 0.88) @@ -272,68 +269,66 @@ function _create_load(bus1="", name::AbstractString=""; kwargs...) kva = abs(kw) + kvar^2 end - # TODO: yearly, daily, duty, growth, model # TODO: ZIPV (7 coefficient array, depends on model keyword) - load = Dict{String,Any}("name" => name, - "phases" => get(kwargs, :phases, 3), - "bus1" => bus1, - "kv" => kv, - "kw" => kw, - "pf" => pf, - "model" => get(kwargs, :model, 1), - "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [1.0, 1.0])), - "daily" => get(kwargs, :daily, [1.0, 1.0]), - "duty" => get(kwargs, :duty, ""), - "growth" => get(kwargs, :growth, ""), - "conn" => get(kwargs, :conn, "wye"), - "kvar" => kvar, - "rneut" => get(kwargs, :rneut, -1.0), - "xneut" => get(kwargs, :xneut, 0.0), - "status" => get(kwargs, :status, "variable"), - "class" => get(kwargs, :class, 1), - "vminpu" => get(kwargs, :vminpu, 0.95), - "vmaxpu" => get(kwargs, :vmaxpu, 1.05), - "vminnorm" => get(kwargs, :vminnorm, 0.0), - "vminemerg" => get(kwargs, :vminemerg, 0.0), - "xfkva" => get(kwargs, :xfkva, 0.0), - "allocationfactor" => get(kwargs, :allocationfactor, 0.5), - "kva" => kva, - "%mean" => get(kwargs, Symbol("%mean"), 0.5), - "%stddev" => get(kwargs, Symbol("%stddev"), 0.1), - "cvrwatts" => get(kwargs, :cvrwatts, 1.0), - "cvrvars" => get(kwargs, :cvrvars, 2.0), - "kwh" => get(kwargs, :kwh, 0.0), - "kwhdays" => get(kwargs, :kwhdays, 30.0), - "cfactor" => get(kwargs, :cfactor, 4.0), - "cvrcurve" => get(kwargs, :cvrcurve, ""), - "numcust" => get(kwargs, :numcust, 1), - "zipv" => get(kwargs, :zipv, ""), - "%seriesrl" => get(kwargs, "%seriesrl", 0.5), - "relweight" => get(kwargs, :relweight, 1.0), - "vlowpu" => get(kwargs, :vlowpu, 0.5), - "puxharm" => get(kwargs, :puxharm, 0.0), - "xrharm" => get(kwargs, :xrharm, 6.0), - # Inherited Properties - "spectrum" => get(kwargs, :spectrum, "defaultload"), - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true) - ) - - return load + Dict{String,Any}( + "name" => name, + "phases" => get(kwargs, :phases, 3), + "bus1" => bus1, + "kv" => kv, + "kw" => kw, + "pf" => pf, + "model" => get(kwargs, :model, 1), + "yearly" => get(kwargs, :yearly, ""), + "daily" => get(kwargs, :daily, ""), + "duty" => get(kwargs, :duty, ""), + "growth" => get(kwargs, :growth, ""), + "conn" => get(kwargs, :conn, WYE), + "kvar" => kvar, + "rneut" => get(kwargs, :rneut, -1.0), + "xneut" => get(kwargs, :xneut, 0.0), + "status" => get(kwargs, :status, "variable"), + "class" => get(kwargs, :class, 1), + "vminpu" => get(kwargs, :vminpu, 0.95), + "vmaxpu" => get(kwargs, :vmaxpu, 1.05), + "vminnorm" => get(kwargs, :vminnorm, 0.0), + "vminemerg" => get(kwargs, :vminemerg, 0.0), + "xfkva" => get(kwargs, :xfkva, 0.0), + "allocationfactor" => get(kwargs, :allocationfactor, 0.5), + "kva" => kva, + "%mean" => get(kwargs, Symbol("%mean"), 0.5), + "%stddev" => get(kwargs, Symbol("%stddev"), 0.1), + "cvrwatts" => get(kwargs, :cvrwatts, 1.0), + "cvrvars" => get(kwargs, :cvrvars, 2.0), + "kwh" => get(kwargs, :kwh, 0.0), + "kwhdays" => get(kwargs, :kwhdays, 30.0), + "cfactor" => get(kwargs, :cfactor, 4.0), + "cvrcurve" => get(kwargs, :cvrcurve, ""), + "numcust" => get(kwargs, :numcust, 1), + "zipv" => get(kwargs, :zipv, ""), + "%seriesrl" => get(kwargs, Symbol("%seriesrl"), 0.5), + "relweight" => get(kwargs, :relweight, 1.0), + "vlowpu" => get(kwargs, :vlowpu, 0.5), + "puxharm" => get(kwargs, :puxharm, 0.0), + "xrharm" => get(kwargs, :xrharm, 6.0), + # Inherited Properties + "spectrum" => get(kwargs, :spectrum, "defaultload"), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_generator(bus1, name; kwargs...) - Creates a Dict{String,Any} containing all of the expected properties for a Generator. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_generator(bus1="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) - conn = get(kwargs, :conn, "wye") +function _create_generator(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "") + + conn = get(kwargs, :conn, WYE) kw = get(kwargs, :kw, 100.0) kva = get(kwargs, :kva, kw * 1.2) @@ -341,107 +336,110 @@ function _create_generator(bus1="", name::AbstractString=""; kwargs...) kvarmax = get(kwargs, :maxkvar, kvar * 2.0) kvarmin = get(kwargs, :minkvar, -kvarmax) - return Dict{String,Any}("name" => name, - "phases" => get(kwargs, :phases, 3), - "bus1" => bus1, - "kv" => get(kwargs, :kv, 12.47), - "kw" => kw, - "pf" => get(kwargs, :pf, 0.80), - "model" => get(kwargs, :model, 1), - "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [0.0, 0.0])), - "daily" => get(kwargs, :daily, [0.0, 0.0]), - "duty" => get(kwargs, "duty", ""), - "dispmode" => get(kwargs, :dispmode, "default"), - "disvalue" => get(kwargs, :dispvalue, 0.0), - "conn" => conn, - "kvar" => kvar, - "rneut" => get(kwargs, :rneut, 0.0), - "xneut" => get(kwargs, :xneut, 0.0), - "status" => get(kwargs, :status, "variable"), - "class" => get(kwargs, :class, 1), - "vpu" => get(kwargs, :vpu, 1.0), - "maxkvar" => kvarmax, - "minkvar" => kvarmin, - "pvfactor" => get(kwargs, :pvfactor, 0.1), - "debugtrace" => get(kwargs, :debugtrace, false), - "vminpu" => get(kwargs, :vminpu, 0.9), - "vmaxpu" => get(kwargs, :vmaxpu, 1.10), - "forceon" => get(kwargs, :forceon, false), - "kva" => kva, - "mva" => kva * 0.001, - "xd" => get(kwargs, :xd, 1.0), - "xdp" => get(kwargs, :xdp, 0.28), - "xdpp" => get(kwargs, :xdpp, 0.20), - "h" => get(kwargs, :h, 1.0), - "d" => get(kwargs, :d, 1.0), - "usermodel" => get(kwargs, :usermodel, ""), - "userdata" => get(kwargs, :userdata, ""), - "shaftmodel" => get(kwargs, :shaftmodel, ""), - "shaftdata" => get(kwargs, :shaftdata, ""), - "dutystart" => get(kwargs, :dutystart, 0.0), - "balanced" => get(kwargs, :balanced, false), - "xrdp" => get(kwargs, :xrdp, 20.0), - # Inherited Properties - "spectrum" => get(kwargs, :spectrum, "defaultgen"), - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true) - ) + Dict{String,Any}( + "name" => name, + "phases" => get(kwargs, :phases, 3), + "bus1" => bus1, + "kv" => get(kwargs, :kv, 12.47), + "kw" => kw, + "pf" => get(kwargs, :pf, 0.80), + "model" => get(kwargs, :model, 1), + "yearly" => get(kwargs, :yearly, get(kwargs, :daily, Vector{Float64}([0.0, 0.0]))), + "daily" => get(kwargs, :daily, Vector{Float64}([0.0, 0.0])), + "duty" => get(kwargs, :duty, ""), + "dispmode" => get(kwargs, :dispmode, "default"), + "disvalue" => get(kwargs, :dispvalue, 0.0), + "conn" => conn, + "kvar" => kvar, + "rneut" => get(kwargs, :rneut, 0.0), + "xneut" => get(kwargs, :xneut, 0.0), + "status" => get(kwargs, :status, "variable"), + "class" => get(kwargs, :class, 1), + "vpu" => get(kwargs, :vpu, 1.0), + "maxkvar" => kvarmax, + "minkvar" => kvarmin, + "pvfactor" => get(kwargs, :pvfactor, 0.1), + "debugtrace" => get(kwargs, :debugtrace, false), + "vminpu" => get(kwargs, :vminpu, 0.9), + "vmaxpu" => get(kwargs, :vmaxpu, 1.10), + "forceon" => get(kwargs, :forceon, false), + "kva" => kva, + "mva" => kva * 0.001, + "xd" => get(kwargs, :xd, 1.0), + "xdp" => get(kwargs, :xdp, 0.28), + "xdpp" => get(kwargs, :xdpp, 0.20), + "h" => get(kwargs, :h, 1.0), + "d" => get(kwargs, :d, 1.0), + "usermodel" => get(kwargs, :usermodel, ""), + "userdata" => get(kwargs, :userdata, ""), + "shaftmodel" => get(kwargs, :shaftmodel, ""), + "shaftdata" => get(kwargs, :shaftdata, ""), + "dutystart" => get(kwargs, :dutystart, 0.0), + "balanced" => get(kwargs, :balanced, false), + "xrdp" => get(kwargs, :xrdp, 20.0), + # Inherited Properties + "spectrum" => get(kwargs, :spectrum, "defaultgen"), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_capacitor(bus1, name, bus2=0; kwargs) - Creates a Dict{String,Any} containing all of the expected properties for a Capacitor. If `bus2` is not specified, the capacitor will be treated as a shunt. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_capacitor(bus1="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_capacitor(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "") + phases = get(kwargs, :phases, 3) bus2 = get(kwargs, :bus2, string(split(bus1, ".")[1],".",join(fill("0", phases), "."))) - return Dict{String,Any}("name" => name, - "bus1" => bus1, - "bus2" => bus2, - "phases" => phases, - "kvar" => get(kwargs, :kvar, 1200.0), - "kv" => get(kwargs, :kv, 12.47), - "conn" => get(kwargs, :conn, "wye"), - "cmatrix" => get(kwargs, :cmatrix, zeros(phases, phases)), - "cuf" => get(kwargs, :cuf, zeros(phases)), - "r" => get(kwargs, :r, zeros(phases)), - "xl" => get(kwargs, :xl, zeros(phases)), - "harm" => get(kwargs, :harm, zeros(phases)), - "numsteps" => get(kwargs, :numsteps, 1), - "states" => get(kwargs, :states, zeros(Bool, phases)), - # Inherited Properties - "normamps" => get(kwargs, :normamps, 400.0), - "emergamps" => get(kwargs, :emergamps, 600.0), - "faultrate" => get(kwargs, :faultrate, 0.1), - "pctperm" => get(kwargs, :pctperm, 20.0), - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true) - ) + Dict{String,Any}( + "name" => name, + "bus1" => bus1, + "bus2" => bus2, + "phases" => phases, + "kvar" => get(kwargs, :kvar, 1200.0), + "kv" => get(kwargs, :kv, 12.47), + "conn" => get(kwargs, :conn, WYE), + "cmatrix" => get(kwargs, :cmatrix, zeros(phases, phases)), + "cuf" => get(kwargs, :cuf, zeros(phases)), + "r" => get(kwargs, :r, zeros(phases)), + "xl" => get(kwargs, :xl, zeros(phases)), + "harm" => get(kwargs, :harm, zeros(phases)), + "numsteps" => get(kwargs, :numsteps, 1), + "states" => get(kwargs, :states, zeros(Bool, phases)), + # Inherited Properties + "normamps" => get(kwargs, :normamps, 400.0), + "emergamps" => get(kwargs, :emergamps, 600.0), + "faultrate" => get(kwargs, :faultrate, 0.1), + "pctperm" => get(kwargs, :pctperm, 20.0), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_reactor(bus1, name, bus2=0; kwargs...) - Creates a Dict{String,Any} containing all of the expected properties for a Reactor. If `bus2` is not specified Reactor is treated like a shunt. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_reactor(bus1="", name::AbstractString="", bus2=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_reactor(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "") + bus2 = get(kwargs, :bus2, "") + phases = get(kwargs, :phases, 3) kvar = get(kwargs, :kvar, 1200.0) kv = get(kwargs, :kv, 12.47) - conn = get(kwargs, :conn, "wye") + conn = get(kwargs, :conn, WYE) parallel = get(kwargs, :parallel, false) normamps = get(kwargs, :normamps, 400.0) @@ -462,9 +460,9 @@ function _create_reactor(bus1="", name::AbstractString="", bus2=""; kwargs...) # TODO: handle `parallel` if (haskey(kwargs, :kv) && haskey(kwargs, :kvar)) || haskey(kwargs, :x) || haskey(kwargs, :lmh) || haskey(kwargs, :z) - if haskey(kwargs, :kvar) && haskey(:kv) + if haskey(kwargs, :kvar) && haskey(kwargs, :kv) kvarperphase = kvar / phases - if conn == "delta" + if conn == DELTA phasekv = kv else if phases == 2 || phases == 3 @@ -540,49 +538,51 @@ function _create_reactor(bus1="", name::AbstractString="", bus2=""; kwargs...) xmatrix = diagm(0 => fill(x, phases)) end - return Dict{String,Any}("name" => name, - "bus1" => bus1, - "bus2" => bus2, - "phases" => phases, - "kvar" => kvar, - "kv" => kv, - "conn" => conn, - "rmatrix" => rmatrix, - "xmatrix" => xmatrix, - "parallel" => parallel, - "r" => r, - "x" => x, - "rp" => rp, - "z1" => [real(z1), imag(z1)], - "z2" => [real(z2), imag(z2)], - "z0" => [real(z0) imag(z0)], - "z" => [real(z), imag(z)], - "rcurve" => get(kwargs, :rcurve, ""), - "lcurve" => get(kwargs, :lcurve, ""), - "lmh" => lmh, - # Inherited Properties - "normamps" => normamps, - "emergamps" => emergamps, - "repair" => get(kwargs, :repair, 3.0), - "faultrate" => get(kwargs, :faultrate, 0.1), - "pctperm" => get(kwargs, :pctperm, 20.0), - "basefreq" => basefreq, - "enabled" => get(kwargs, :enabled, true) - ) + Dict{String,Any}( + "name" => name, + "bus1" => bus1, + "bus2" => bus2, + "phases" => phases, + "kvar" => kvar, + "kv" => kv, + "conn" => conn, + "rmatrix" => rmatrix, + "xmatrix" => xmatrix, + "parallel" => parallel, + "r" => r, + "x" => x, + "rp" => rp, + "z1" => [real(z1), imag(z1)], + "z2" => [real(z2), imag(z2)], + "z0" => [real(z0) imag(z0)], + "z" => [real(z), imag(z)], + "rcurve" => get(kwargs, :rcurve, ""), + "lcurve" => get(kwargs, :lcurve, ""), + "lmh" => lmh, + # Inherited Properties + "normamps" => normamps, + "emergamps" => emergamps, + "repair" => get(kwargs, :repair, 3.0), + "faultrate" => get(kwargs, :faultrate, 0.1), + "pctperm" => get(kwargs, :pctperm, 20.0), + "basefreq" => basefreq, + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_vsource(bus1, name, bus2=0; kwargs...) - Creates a Dict{String,Any} containing all of the expected properties for a Voltage Source. If `bus2` is not specified, VSource will be treated like a -generator. Mostly used as `sourcebus` which represents the circuit. See +generator. Mostly used as `source` which represents the circuit. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_vsource(bus1="", name::AbstractString="", bus2=0; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_vsource(name::String=""; kwargs...)::Dict{String,Any} + bus1 = get(kwargs, :bus1, "sourcebus") + bus2 = get(kwargs, :bus2, "") + x1r1 = get(kwargs, :x1r1, 4.0) x0r0 = get(kwargs, :x0r0, 3.0) @@ -741,57 +741,60 @@ function _create_vsource(bus1="", name::AbstractString="", bus2=0; kwargs...) puz0 = complex(r0 / Zbase, x0 / Zbase) end - return Dict{String,Any}("name" => name, - "bus1" => bus1, - "basekv" => basekv, - "pu" => pu, - "angle" => get(kwargs, :angle, 0.0), - "frequency" => get(kwargs, :frequency, get(kwargs, :basefreq, 60.0)), - "phases" => phases, - "mvasc3" => mvasc3, - "mvasc1" => mvasc1, - "x1r1" => x1r1, - "x0r0" => x0r0, - "isc3" => isc3, - "isc1" => isc1, - "r1" => r1, - "x1" => x1, - "r0" => r0, - "x0" => x0, - "scantype" => get(kwargs, :scantype, "pos"), - "sequence" => get(kwargs, :sequence, "pos"), - "bus2" => bus2, - "z1" => [real(z1), imag(z1)], - "z0" => [real(z0), imag(z0)], - "z2" => [real(z2), imag(z2)], - "puz1" => [real(puz1), imag(puz1)], - "puz0" => [real(puz0), imag(puz0)], - "puz2" => [real(puz2), imag(puz2)], - "basemva" => basemva, - "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [1.0, 1.0])), - "daily" => get(kwargs, :daily, [1.0, 1.0]), - "duty" => get(kwargs, :duty, ""), - # Inherited Properties - "spectrum" => get(kwargs, :spectrum, "defaultvsource"), - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true), - # Derived Properties - "rmatrix" => real(Z), - "xmatrix" => imag(Z), - "vmag" => Vmag - ) + Dict{String,Any}( + "name" => name, + "bus1" => bus1, + "basekv" => basekv, + "pu" => pu, + "angle" => get(kwargs, :angle, 0.0), + "frequency" => get(kwargs, :frequency, get(kwargs, :basefreq, 60.0)), + "phases" => phases, + "mvasc3" => mvasc3, + "mvasc1" => mvasc1, + "x1r1" => x1r1, + "x0r0" => x0r0, + "isc3" => isc3, + "isc1" => isc1, + "r1" => r1, + "x1" => x1, + "r0" => r0, + "x0" => x0, + "scantype" => get(kwargs, :scantype, "pos"), + "sequence" => get(kwargs, :sequence, "pos"), + "bus2" => bus2, + "z1" => [real(z1), imag(z1)], + "z0" => [real(z0), imag(z0)], + "z2" => [real(z2), imag(z2)], + "puz1" => [real(puz1), imag(puz1)], + "puz0" => [real(puz0), imag(puz0)], + "puz2" => [real(puz2), imag(puz2)], + "basemva" => basemva, + "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [1.0, 1.0])), + "daily" => get(kwargs, :daily, [1.0, 1.0]), + "duty" => get(kwargs, :duty, ""), + # Inherited Properties + "spectrum" => get(kwargs, :spectrum, "defaultvsource"), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + # Derived Properties + "rmatrix" => real(Z), + "xmatrix" => imag(Z), + "vmag" => Vmag, + "like" => get(kwargs, :like, "") + ) end -""" - _create_transformer(name; kwargs...) +"alias _create_circuit to _create_vsource" +_create_circuit = _create_vsource + +""" Creates a Dict{String,Any} containing all of the expected properties for a Transformer. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_transformer(name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_transformer(name::String=""; kwargs...) windings = isempty(name) ? 3 : get(kwargs, :windings, 2) phases = get(kwargs, :phases, 3) @@ -804,7 +807,7 @@ function _create_transformer(name::AbstractString=""; kwargs...) temp = Dict{String,Any}("buss" => get(kwargs, :buses, fill("", windings)), "taps" => get(kwargs, :taps, fill(1.0, windings)), - "conns" => get(kwargs, :conns, fill("wye", windings)), + "conns" => get(kwargs, :conns, fill(WYE, windings)), "kvs" => get(kwargs, :kvs, fill(12.47, windings)), "kvas" => get(kwargs, :kvas, fill(10.0, windings)), "%rs" => prcnt_rs, @@ -824,81 +827,84 @@ function _create_transformer(name::AbstractString=""; kwargs...) end end - trfm = Dict{String,Any}("name" => name, - "phases" => phases, - "windings" => windings, - # Per wdg - "wdg" => 1, - "bus" => temp["buss"][1], - "conn" => temp["conns"][1], - "kv" => temp["kvs"][1], - "kva" => temp["kvas"][1], - "tap" => temp["taps"][1], - "%r" => temp["%rs"][1], - "rneut" => temp["rneuts"][1], - "xneut" => temp["xneuts"][1], - - "wdg_2" => 2, - "bus_2" => temp["buss"][2], - "conn_2" => temp["conns"][2], - "kv_2" => temp["kvs"][2], - "kva_2" => temp["kvas"][2], - "tap_2" => temp["taps"][2], - "%r_2" => temp["%rs"][2], - "rneut_2" => temp["rneuts"][2], - "xneut_2" => temp["xneuts"][2], - - # General - "buses" => temp["buss"], - "conns" => temp["conns"], - "kvs" => temp["kvs"], - "kvas" => temp["kvas"], - "taps" => temp["taps"], - "xhl" => get(kwargs, :xhl, 7.0), - "xht" => get(kwargs, :xht, 35.0), - "xlt" => get(kwargs, :xlt, 30.0), - "xscarray" => get(kwargs, :xscarry, ""), - "thermal" => get(kwargs, :thermal, 2.0), - "n" => get(kwargs, :n, 0.8), - "m" => get(kwargs, :m, 0.8), - "flrise" => get(kwargs, :flrise, 65.0), - "hsrise" => get(kwargs, :hsrise, 15.0), - "%loadloss" => get(kwargs, Symbol("%loadloss"), sum(temp["%rs"][1:2])), - "%noloadloss" => get(kwargs, Symbol("%noloadloss"), 0.0), - "normhkva" => get(kwargs, :normhkva, 1.1 * temp["kvas"][1]), - "emerghkva" => get(kwargs, :emerghkva, 1.5 * temp["kvas"][1]), - "sub" => get(kwargs, :sub, false), - "maxtap" => get(kwargs, :maxtap, 1.10), - "mintap" => get(kwargs, :mintap, 0.90), - "numtaps" => get(kwargs, :numtaps, 32), - "subname" => get(kwargs, :subname, ""), - "%imag" => get(kwargs, Symbol("%imag"), 0.0), - "ppm_antifloat" => get(kwargs, :ppm_antifloat, 1.0), - "%rs" => temp["%rs"], - "bank" => get(kwargs, :bank, ""), - "xfmrcode" => get(kwargs, :xfmrcode, ""), - "xrconst" => get(kwargs, :xrconst, false), - "x12" => get(kwargs, :xhl, 7.0), - "x13" => get(kwargs, :xht, 35.0), - "x23" => get(kwargs, :xlt, 30.0), - "leadlag" => get(kwargs, :leadlag, "lag"), - # Inherited Properties - "faultrate" => get(kwargs, :faultrate, 0.1), - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true) - ) + trfm = Dict{String,Any}( + "name" => name, + "phases" => phases, + "windings" => windings, + # Per wdg + "wdg" => 1, + "bus" => temp["buss"][1], + "conn" => temp["conns"][1], + "kv" => temp["kvs"][1], + "kva" => temp["kvas"][1], + "tap" => temp["taps"][1], + "%r" => temp["%rs"][1], + "rneut" => temp["rneuts"][1], + "xneut" => temp["xneuts"][1], + + "wdg_2" => 2, + "bus_2" => temp["buss"][2], + "conn_2" => temp["conns"][2], + "kv_2" => temp["kvs"][2], + "kva_2" => temp["kvas"][2], + "tap_2" => temp["taps"][2], + "%r_2" => temp["%rs"][2], + "rneut_2" => temp["rneuts"][2], + "xneut_2" => temp["xneuts"][2], + + # General + "buses" => temp["buss"], + "conns" => temp["conns"], + "kvs" => temp["kvs"], + "kvas" => temp["kvas"], + "taps" => temp["taps"], + "xhl" => get(kwargs, :xhl, 7.0), + "xht" => get(kwargs, :xht, 35.0), + "xlt" => get(kwargs, :xlt, 30.0), + "xscarray" => get(kwargs, :xscarry, ""), + "thermal" => get(kwargs, :thermal, 2.0), + "n" => get(kwargs, :n, 0.8), + "m" => get(kwargs, :m, 0.8), + "flrise" => get(kwargs, :flrise, 65.0), + "hsrise" => get(kwargs, :hsrise, 15.0), + "%loadloss" => get(kwargs, Symbol("%loadloss"), sum(temp["%rs"][1:2])), + "%noloadloss" => get(kwargs, Symbol("%noloadloss"), 0.0), + "normhkva" => get(kwargs, :normhkva, 1.1 * temp["kvas"][1]), + "emerghkva" => get(kwargs, :emerghkva, 1.5 * temp["kvas"][1]), + "sub" => get(kwargs, :sub, false), + "maxtap" => get(kwargs, :maxtap, 1.10), + "mintap" => get(kwargs, :mintap, 0.90), + "numtaps" => get(kwargs, :numtaps, 32), + "subname" => get(kwargs, :subname, ""), + "%imag" => get(kwargs, Symbol("%imag"), 0.0), + "ppm_antifloat" => get(kwargs, :ppm_antifloat, 1.0), + "%rs" => temp["%rs"], + "bank" => get(kwargs, :bank, ""), + "xfmrcode" => get(kwargs, :xfmrcode, ""), + "xrconst" => get(kwargs, :xrconst, false), + "x12" => get(kwargs, :xhl, 7.0), + "x13" => get(kwargs, :xht, 35.0), + "x23" => get(kwargs, :xlt, 30.0), + "leadlag" => get(kwargs, :leadlag, "lag"), + # Inherited Properties + "faultrate" => get(kwargs, :faultrate, 0.1), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) if windings == 3 - trfm3 = Dict{String,Any}("wdg_3" => 3, - "bus_3" => temp["buss"][3], - "conn_3" => temp["conns"][3], - "kv_3" => temp["kvs"][3], - "kva_3" => temp["kvas"][3], - "tap_3" => temp["taps"][3], - "%r_3" => temp["%rs"][3], - "rneut_3" => temp["rneuts"][3], - "xneut_3" => temp["xneuts"][3], - ) + trfm3 = Dict{String,Any}( + "wdg_3" => 3, + "bus_3" => temp["buss"][3], + "conn_3" => temp["conns"][3], + "kv_3" => temp["kvs"][3], + "kva_3" => temp["kvas"][3], + "tap_3" => temp["taps"][3], + "%r_3" => temp["%rs"][3], + "rneut_3" => temp["rneuts"][3], + "xneut_3" => temp["xneuts"][3], + ) merge!(trfm, trfm3) end @@ -907,17 +913,129 @@ function _create_transformer(name::AbstractString=""; kwargs...) end +"Transformer codes contain all of the same properties as a transformer except bus, buses, bank, xfmrcode" +function _create_xfmrcode(name::String=""; kwargs...) + windings = isempty(name) ? 3 : get(kwargs, :windings, 2) + phases = get(kwargs, :phases, 3) -""" - _create_pvsystem(bus1, name; kwargs...) + prcnt_rs = fill(0.2, windings) + if haskey(kwargs, Symbol("%rs")) + prcnt_rs = kwargs[Symbol("%rs")] + elseif haskey(kwargs, Symbol("%loadloss")) + prcnt_rs[1] = prcnt_rs[2] = kwargs[Symbol("%loadloss")] / 2.0 + end + + temp = Dict{String,Any}( + "taps" => get(kwargs, :taps, fill(1.0, windings)), + "conns" => get(kwargs, :conns, fill(WYE, windings)), + "kvs" => get(kwargs, :kvs, fill(12.47, windings)), + "kvas" => get(kwargs, :kvas, fill(10.0, windings)), + "%rs" => prcnt_rs, + "rneuts" => fill(0.0, windings), + "xneuts" => fill(0.0, windings) + ) + + for wdg in [:wdg, :wdg_2, :wdg_3] + if haskey(kwargs, wdg) + suffix = kwargs[wdg] == 1 ? "" : "_$(kwargs[wdg])" + for key in [:bus, :tap, :conn, :kv, :kva, Symbol("%r"), :rneut, :xneut] + subkey = Symbol(string(key, suffix)) + if haskey(kwargs, subkey) + temp[string(key, "s")][kwargs[wdg]] = kwargs[subkey] + end + end + end + end + xfmrcode = Dict{String,Any}( + "name" => name, + "phases" => phases, + "windings" => windings, + # Per wdg + "wdg" => 1, + "conn" => temp["conns"][1], + "kv" => temp["kvs"][1], + "kva" => temp["kvas"][1], + "tap" => temp["taps"][1], + "%r" => temp["%rs"][1], + "rneut" => temp["rneuts"][1], + "xneut" => temp["xneuts"][1], + + "wdg_2" => 2, + "conn_2" => temp["conns"][2], + "kv_2" => temp["kvs"][2], + "kva_2" => temp["kvas"][2], + "tap_2" => temp["taps"][2], + "%r_2" => temp["%rs"][2], + "rneut_2" => temp["rneuts"][2], + "xneut_2" => temp["xneuts"][2], + + # General + "conns" => temp["conns"], + "kvs" => temp["kvs"], + "kvas" => temp["kvas"], + "taps" => temp["taps"], + "xhl" => get(kwargs, :xhl, 7.0), + "xht" => get(kwargs, :xht, 35.0), + "xlt" => get(kwargs, :xlt, 30.0), + "xscarray" => get(kwargs, :xscarry, ""), + "thermal" => get(kwargs, :thermal, 2.0), + "n" => get(kwargs, :n, 0.8), + "m" => get(kwargs, :m, 0.8), + "flrise" => get(kwargs, :flrise, 65.0), + "hsrise" => get(kwargs, :hsrise, 15.0), + "%loadloss" => get(kwargs, Symbol("%loadloss"), sum(temp["%rs"][1:2])), + "%noloadloss" => get(kwargs, Symbol("%noloadloss"), 0.0), + "normhkva" => get(kwargs, :normhkva, 1.1 * temp["kvas"][1]), + "emerghkva" => get(kwargs, :emerghkva, 1.5 * temp["kvas"][1]), + "sub" => get(kwargs, :sub, false), + "maxtap" => get(kwargs, :maxtap, 1.10), + "mintap" => get(kwargs, :mintap, 0.90), + "numtaps" => get(kwargs, :numtaps, 32), + "subname" => get(kwargs, :subname, ""), + "%imag" => get(kwargs, Symbol("%imag"), 0.0), + "ppm_antifloat" => get(kwargs, :ppm_antifloat, 1.0), + "%rs" => temp["%rs"], + "xrconst" => get(kwargs, :xrconst, false), + "x12" => get(kwargs, :xhl, 7.0), + "x13" => get(kwargs, :xht, 35.0), + "x23" => get(kwargs, :xlt, 30.0), + "leadlag" => get(kwargs, :leadlag, "lag"), + # Inherited Properties + "faultrate" => get(kwargs, :faultrate, 0.1), + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) + + if windings == 3 + xfmrcode3 = Dict{String,Any}( + "wdg_3" => 3, + "conn_3" => temp["conns"][3], + "kv_3" => temp["kvs"][3], + "kva_3" => temp["kvas"][3], + "tap_3" => temp["taps"][3], + "%r_3" => temp["%rs"][3], + "rneut_3" => temp["rneuts"][3], + "xneut_3" => temp["xneuts"][3], + ) + + merge!(xfmrcode, xfmrcode3) + end + + return xfmrcode +end + + +""" Creates a Dict{String,Any} containing all of the expected properties for a PVSystem. See OpenDSS document https://github.com/tshort/OpenDSS/blob/master/Doc/OpenDSS%20PVSystem%20Model.doc for valid fields and ways to specify the different properties. """ -function _create_pvsystem(bus1="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) +function _create_pvsystem(name::String=""; kwargs...) + bus1 = get(kwargs, :bus1, "") + kv = get(kwargs, :kv, 12.47) kw = get(kwargs, :kw, 10.0) pf = get(kwargs, :pf, 0.88) @@ -948,167 +1066,306 @@ function _create_pvsystem(bus1="", name::AbstractString=""; kwargs...) Memento.warn(_LOGGER, "\"like\" keyword on pvsystem $name is not supported.") end - pvsystem = Dict{String,Any}("name" => name, - "phases" => get(kwargs, :phases, 3), - "bus1" => bus1, - "kv" => kv, - "kw" => kw, - "pf" => pf, - "model" => get(kwargs, :model, 1), - "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [1.0, 1.0])), - "daily" => get(kwargs, :daily, [1.0, 1.0]), - "duty" => get(kwargs, :duty, ""), - "irradiance" => get(kwargs, :irradiance, 0), - "pmpp" => get(kwargs, :pmpp, 0), - "temperature" => get(kwargs, :temperature, 0), - "conn" => get(kwargs, :conn, "wye"), - "kvar" => kvar, - "kva" => kva, - "%cutin" => get(kwargs, :cutin, 0), #TODO not sure what to do with this - "%cutout" => get(kwargs, :cutout, 0), #TODO not sure what to do with this - "effcurve" => get(kwargs, :effcurve, ""), - "p-tcurve" => get(kwargs, :ptcurve, ""), - "%r" => get(kwargs, :r, 0), - "%x" => get(kwargs, :x, 0.50), - "vminpu" => get(kwargs, :vminpu, 0.9), - "vmaxpu" => get(kwargs, :vmaxpu, 1.1), - "tyearly" => get(kwargs, :tyearly, 0), - "tduty" => get(kwargs, :tduty, 0), - "class" => get(kwargs, :class, 0), - "usermodel" => get(kwargs, :usermodel, ""), - "userdata" => get(kwargs, :userdata, ""), - "debugtrace" => get(kwargs, :debugtrace, "no"), - "spectrum" => get(kwargs, :spectrum, "defaultpvsystem"), - # Inherited Properties - "basefreq" => get(kwargs, :basefreq, 60.0), - "enabled" => get(kwargs, :enabled, true) - ) - return pvsystem + Dict{String,Any}( + "name" => name, + "phases" => get(kwargs, :phases, 3), + "bus1" => bus1, + "kv" => kv, + "kw" => kw, + "pf" => pf, + "model" => get(kwargs, :model, 1), + "yearly" => get(kwargs, :yearly, get(kwargs, :daily, [1.0, 1.0])), + "daily" => get(kwargs, :daily, [1.0, 1.0]), + "duty" => get(kwargs, :duty, ""), + "irradiance" => get(kwargs, :irradiance, 0), + "pmpp" => get(kwargs, :pmpp, 0), + "temperature" => get(kwargs, :temperature, 0), + "conn" => get(kwargs, :conn, WYE), + "kvar" => kvar, + "kva" => kva, + "%cutin" => get(kwargs, :cutin, 0), #TODO not sure what to do with this + "%cutout" => get(kwargs, :cutout, 0), #TODO not sure what to do with this + "effcurve" => get(kwargs, :effcurve, ""), + "p-tcurve" => get(kwargs, :ptcurve, ""), + "%r" => get(kwargs, :r, 0), + "%x" => get(kwargs, :x, 0.50), + "vminpu" => get(kwargs, :vminpu, 0.9), + "vmaxpu" => get(kwargs, :vmaxpu, 1.1), + "tyearly" => get(kwargs, :tyearly, 0), + "tduty" => get(kwargs, :tduty, 0), + "class" => get(kwargs, :class, 0), + "usermodel" => get(kwargs, :usermodel, ""), + "userdata" => get(kwargs, :userdata, ""), + "debugtrace" => get(kwargs, :debugtrace, "no"), + "spectrum" => get(kwargs, :spectrum, "defaultpvsystem"), + # Inherited Properties + "basefreq" => get(kwargs, :basefreq, 60.0), + "enabled" => get(kwargs, :enabled, true), + "like" => get(kwargs, :like, "") + ) end """ - _create_storage(bus1, name; kwargs...) - Creates a Dict{String,Any} containing all expected properties for a storage element. See OpenDSS documentation for valid fields and ways to specify the different properties. """ -function _create_storage(bus1="", name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) - - storage = Dict{String,Any}("name" => name, - "%charge" => get(kwargs, :charge, 100.0), - "%discharge" => get(kwargs, :discharge, 100.0), - "%effcharge" => get(kwargs, :effcharge, 90.0), - "%effdischarge" => get(kwargs, :effdischarge, 90.0), - "%idlingkvar" => get(kwargs, :idlingkvar, 0.0), - "%idlingkw" => get(kwargs, :idlingkw, 1.0), - "%r" => get(kwargs, :r, 0.0), - "%reserve" => get(kwargs, :reserve, 20.0), - "%stored" => get(kwargs, :stored, 100.0), - "%x" => get(kwargs, :x, 50.0), - "basefreq" => get(kwargs, :basefreq, 60.0), - "bus1" => bus1, - "chargetrigger" => get(kwargs, :chargetrigger, 0.0), - "class" => get(kwargs, :class, 0), - "conn" => get(kwargs, :conn, "wye"), - "daily" => get(kwargs, :daily, [1.0, 1.0]), - "debugtrace" => get(kwargs, :debugtrace, false), - "dischargetrigger" => get(kwargs, :dischargetrigger, 0.0), - "dispmode" => get(kwargs, :dispmode, "default"), - "duty" => get(kwargs, :duty, ""), - "dynadata" => get(kwargs, :dynadata, ""), - "dynadll" => get(kwargs, :dynadll, "none"), - "enabled" => get(kwargs, :enabled, true), - "kv" => get(kwargs, :kv, 12.47), - "kw" => get(kwargs, :kw, 0.0), - "kva" => get(kwargs, :kva, 25.0), - "kvar" => get(kwargs, :kvar, 0.0), - "kwhrated" => get(kwargs, :kwhrated, 50.0), - "kwhstored" => get(kwargs, :kwhstored, 50.0), - "kwrated" => get(kwargs, :kwrated, 50.0), - "model" => get(kwargs, :model, 1), - "pf" => get(kwargs, :pf, 1.0), - "phases" => get(kwargs, :phases, 3), - "spectrum" => get(kwargs, :spectrum, "default"), - "state" => get(kwargs, :state, "idling"), - "timechargetrig" => get(kwargs, :timechargetrig, 2.0), - "userdata" => get(kwargs, :userdata, ""), - "usermodel" => get(kwargs, :usermodel, "none"), - "vmaxpu" => get(kwargs, :vmaxpu, 1.1), - "vminpu" => get(kwargs, :vimpu, 0.9), - "yearly" => get(kwargs, :yearly, [1.0, 1.0]), - ) - return storage +function _create_storage(name::String=""; kwargs...) + Dict{String,Any}( + "name" => name, + "%charge" => get(kwargs, :charge, 100.0), + "%discharge" => get(kwargs, :discharge, 100.0), + "%effcharge" => get(kwargs, :effcharge, 90.0), + "%effdischarge" => get(kwargs, :effdischarge, 90.0), + "%idlingkvar" => get(kwargs, :idlingkvar, 0.0), + "%idlingkw" => get(kwargs, :idlingkw, 1.0), + "%r" => get(kwargs, :r, 0.0), + "%reserve" => get(kwargs, :reserve, 20.0), + "%stored" => get(kwargs, :stored, 100.0), + "%x" => get(kwargs, :x, 50.0), + "basefreq" => get(kwargs, :basefreq, 60.0), + "bus1" => get(kwargs, :bus1, ""), + "chargetrigger" => get(kwargs, :chargetrigger, 0.0), + "class" => get(kwargs, :class, 0), + "conn" => get(kwargs, :conn, WYE), + "daily" => get(kwargs, :daily, [1.0, 1.0]), + "debugtrace" => get(kwargs, :debugtrace, false), + "dischargetrigger" => get(kwargs, :dischargetrigger, 0.0), + "dispmode" => get(kwargs, :dispmode, "default"), + "duty" => get(kwargs, :duty, ""), + "dynadata" => get(kwargs, :dynadata, ""), + "dynadll" => get(kwargs, :dynadll, "none"), + "enabled" => get(kwargs, :enabled, true), + "kv" => get(kwargs, :kv, 12.47), + "kw" => get(kwargs, :kw, 0.0), + "kva" => get(kwargs, :kva, 25.0), + "kvar" => get(kwargs, :kvar, 0.0), + "kwhrated" => get(kwargs, :kwhrated, 50.0), + "kwhstored" => get(kwargs, :kwhstored, 50.0), + "kwrated" => get(kwargs, :kwrated, 50.0), + "model" => get(kwargs, :model, 1), + "pf" => get(kwargs, :pf, 1.0), + "phases" => get(kwargs, :phases, 3), + "spectrum" => get(kwargs, :spectrum, "default"), + "state" => get(kwargs, :state, "idling"), + "timechargetrig" => get(kwargs, :timechargetrig, 2.0), + "userdata" => get(kwargs, :userdata, ""), + "usermodel" => get(kwargs, :usermodel, "none"), + "vmaxpu" => get(kwargs, :vmaxpu, 1.1), + "vminpu" => get(kwargs, :vimpu, 0.9), + "yearly" => get(kwargs, :yearly, [1.0, 1.0]), + "like" => get(kwargs, :like, "") + ) end """ - _create_loadshape(name; kwargs...) - Creates a Dict{String,Any} containing all expected properties for a LoadShape element. See OpenDSS documentation for valid fields and ways to specify different properties. """ -function _create_loadshape(name::AbstractString=""; kwargs...) - kwargs = Dict{Symbol,Any}(kwargs) - +function _create_loadshape(name::String=""; kwargs...) if haskey(kwargs, :minterval) - kwargs[:interval] = kwargs[:minterval] / 60 + interval = kwargs[:minterval] / 60 elseif haskey(kwargs, :sinterval) - kwargs[:interval] = kwargs[:sinterval] / 60 / 60 + interval = kwargs[:sinterval] / 60 / 60 + else + interval = get(kwargs, :interval, 1.0) end - npts = get(kwargs, :npts, 1) - - pmult = get(kwargs, :pmult, fill(1.0, npts))[1:npts] - qmult = get(kwargs, :qmult, fill(1.0, npts))[1:npts] - - hour = get(kwargs, :hour, collect(range(1.0, step=get(kwargs, :interval, 1.0), length=npts)))[1:npts] - - loadshape = Dict{String,Any}("name" => name, - "npts" => npts, - "interval" => get(kwargs, :interval, 1.0), - "minterval" => get(kwargs, :interval, 1.0) .* 60, - "sinterval" => get(kwargs, :interval, 1.0) .* 3600, - "pmult" => pmult, - "qmult" => qmult, - "hour" => hour, - "mean" => get(kwargs, :mean, mean(pmult)), - "stddev" => get(kwargs, :stddev, std(pmult)), - "csvfile" => get(kwargs, :csvfile, ""), - "sngfile" => get(kwargs, :sngfile, ""), - "dblfile" => get(kwargs, :dblfile, ""), - "pqcsvfile" => get(kwargs, :pqcsvfile, ""), - "action" => get(kwargs, :action, ""), - "useactual" => get(kwargs, :useactual, true), - "pmax" => get(kwargs, :pmax, 1.0), - "qmax" => get(kwargs, :qmax, 1.0), - "pbase" => get(kwargs, :pbase, 0.0), - ) - - return loadshape + pmult = get(kwargs, :pmult, Vector{Float64}([])) + qmult = get(kwargs, :qmult, pmult) + + npts = get(kwargs, :npts, length(pmult) == 0 && length(qmult) == 0 ? 0 : minimum(Int[length(a) for a in [pmult, qmult] if length(a) > 0])) + + pmult = pmult[1:npts] + qmult = qmult[1:npts] + + hour = get(kwargs, :hour, collect(range(1.0, step=interval, length=npts)))[1:npts] + + Dict{String,Any}( + "name" => name, + "npts" => npts, + "interval" => interval, + "minterval" => interval * 60, + "sinterval" => interval * 3600, + "pmult" => pmult, + "qmult" => qmult, + "hour" => hour, + "mean" => get(kwargs, :mean, mean(pmult)), + "stddev" => get(kwargs, :stddev, std(pmult)), + "csvfile" => get(kwargs, :csvfile, ""), + "sngfile" => get(kwargs, :sngfile, ""), + "dblfile" => get(kwargs, :dblfile, ""), + "pqcsvfile" => get(kwargs, :pqcsvfile, ""), + "action" => get(kwargs, :action, ""), + "useactual" => get(kwargs, :useactual, true), + "pmax" => get(kwargs, :pmax, 1.0), + "qmax" => get(kwargs, :qmax, 1.0), + "pbase" => get(kwargs, :pbase, 0.0), + "like" => get(kwargs, :like, "") + ) end -"Returns a Dict{String,Type} for the desired component `comp`, giving all of the expected data types" -function _get_dtypes(comp::AbstractString)::Dict - return Dict{String,Type}((k, typeof(v)) for (k, v) in _constructors[comp]()) +""" +Creates a Dict{String,Any} containing all expected properties for a XYCurve +object. See OpenDSS documentation for valid fields and ways to specify +different properties. +""" +function _create_xycurve(name::String=""; kwargs...) + if haskey(kwargs, :points) + xarray = Vector{Float64}([]) + yarray = Vector{Float64}([]) + + i = 1 + for point in kwargs[:points] + if i % 2 == 1 + push!(xarray, point) + else + push!(yarray, point) + end + i += 1 + end + else + xarray = get(kwargs, :xarray, Vector{Float64}([])) + yarray = get(kwargs, :yarray, Vector{Float64}([])) + end + + npts = min(length(xarray), length(yarray)) + + points = Vector{Float64}([]) + for (x, y) in zip(xarray, yarray) + push!(points, x) + push!(points, y) + end + + Dict{String,Any}( + "name" => name, + "npts" => npts, + "points" => points, + "yarray" => yarray, + "xarray" => xarray, + "csvfile" => get(kwargs, :csvfile, ""), + "sngfile" => get(kwargs, :sngfile, ""), + "dblfile" => get(kwargs, :dblfile, ""), + "x" => get(kwargs, :x, isempty(xarray) ? 0.0 : xarray[1]), + "y" => get(kwargs, :y, isempty(yarray) ? 0.0 : yarray[1]), + "xshift" => get(kwargs, :xshift, 0), + "yshift" => get(kwargs, :yshift, 0), + "xscale" => get(kwargs, :xscale, 1.0), + "yscale" => get(kwargs, :yscale, 1.0), + "like" => get(kwargs, :like, ""), + ) end -"list of constructor functions for easy access" -const _constructors = Dict{String,Any}("line" => _create_line, - "load" => _create_load, - "generator" => _create_generator, - "capacitor" => _create_capacitor, - "reactor" => _create_reactor, - "transformer" => _create_transformer, - "linecode" => _create_linecode, - "circuit" => _create_vsource, - "pvsystem" => _create_pvsystem, - "vsource" => _create_vsource, - "storage" => _create_storage, - "loadshape" => _create_loadshape - ) +"" +function _create_options(; kwargs...) + Dict{String,Any}( + "%growth" => get(kwargs, Symbol("%growth"), 2.5), + "%mean" => get(kwargs, Symbol("%mean"), 65.0), + "%normal" => get(kwargs, Symbol("%normal"), 100.0), + "%stddev" => get(kwargs, Symbol("%stddev"), 9.0), + "addtype" => get(kwargs, :addtype, "generator"), + "algorithm" => get(kwargs, :algorithm, "newton"), + "allocationfactors" => get(kwargs, :allocationfactors, ""), + "allowduplicates" => get(kwargs, :allowduplicates, false), + "autobuslist" => get(kwargs, :autobuslist, Vector{String}([])), + "basefrequency" => get(kwargs, :basefrequency, 60.0), + "bus" => get(kwargs, :bus, ""), + "capkvar" => get(kwargs, :capkvar, 600.0), + "casename" => get(kwargs, :casename, ""), + "capmarkercode" => get(kwargs, :capmarkercode, 37), + "capmarkersize" => get(kwargs, :capmarkersize, 3), + "cfactors" => get(kwargs, :cfactors, 4.0), + "circuit" => get(kwargs, :circuit, ""), + "cktmodel" => get(kwargs, :cktmodel, "multiphase"), + "class" => get(kwargs, :class, ""), + "controlmode" => get(kwargs, :controlmode, "static"), + "datapath" => get(kwargs, :datapath, ""), + "defaultbasefrequency" => get(kwargs, :defaultbasefrequency, 60.0), + "defaultbasefreq" => get(kwargs, :defaultbasefreq, 60.0), # Alias to defaultbasefrequency + "defaultdaily" => get(kwargs, :defaultdaily, "default"), + "defaultyearly" => get(kwargs, :defaultyearly, "default"), + "demandinterval" => get(kwargs, :demandinterval, false), + "diverbose" => get(kwargs, :diverbose, false), + "dssvisualizationtool" => get(kwargs, :dssvisualizationtool, ""), + "earthmodel" => get(kwargs, :earthmodel, "deri"), + "editor" => get(kwargs, :editor, "notepad"), + "element" => get(kwargs, :element, ""), + "emergvmaxpu" => get(kwargs, :emergvmaxpu, 1.08), + "emergvminpu" => get(kwargs, :emergvminpu, 0.90), + "frequency" => get(kwargs, :frequency, 60.0), + "genkw" => get(kwargs, :genkw, 1000.0), + "genmult" => get(kwargs, :genmult, 1.0), + "h" => get(kwargs, :h, ""), + "harmonics" => get(kwargs, :harmonics, "all"), + "hour" => get(kwargs, :hour, 1.0), + "keeplist" => get(kwargs, :keeplist, Vector{String}([])), + "ldcurve" => get(kwargs, :ldcurve, "nil"), + "loadmodel" => get(kwargs, :loadmodel, "admittance"), + "loadmult" => get(kwargs, :loadmult, 1.0), + "log" => get(kwargs, :log, false), + "lossregs" => get(kwargs, :lossregs, 13), + "lossweight" => get(kwargs, :lossweight, 1.0), + "markercode" => get(kwargs, :markercode, 0), + "markswitches" => get(kwargs, :markswitches, false), + "markcapacitors" => get(kwargs, :markcapacitors, false), + "markpvsystems" => get(kwargs, :markpvsystems, false), + "markregulators" => get(kwargs, :markregulators, false), + "markstorage" => get(kwargs, :markstorage, false), + "marktransformers" => get(kwargs, :marktransformers, false), + "maxcontroliter" => get(kwargs, :maxcontroliter, 10), + "maxiter" => get(kwargs, :maxiter, 15), + "miniterations" => get(kwargs, :miniterations, 2), + "mode" => get(kwargs, :mode, "Snap"), + "name" => get(kwargs, :name, ""), + "nodewidth" => get(kwargs, :nodewidth, 1), + "normvmaxpu" => get(kwargs, :normvmaxpu, 1.05), + "normvminpu" => get(kwargs, :normvminpu, 0.95), + "numallociterations" => get(kwargs, :numallociterations, 2), + "number" => get(kwargs, :number, 0), + "object" => get(kwargs, :object, ""), + "overloadreport" => get(kwargs, :overloadreport, false), + "neglectloady" => get(kwargs, :neglectloady, false), + "pricecurve" => get(kwargs, :pricecurve, ""), + "pricesignal" => get(kwargs, :pricesignal, 25), + "pvmarkercode" => get(kwargs, :pvmarkercode, 15), + "pvmarkersize" => get(kwargs, :pvmarkersize, 1), + "random" => get(kwargs, :random, "uniform"), + "recorder" => get(kwargs, :recorder, false), + "reduceoption" => get(kwargs, :reduceoption, "default"), + "registryupdate" => get(kwargs, :registryupdate, true), + "regmarkercode" => get(kwargs, :regmarkercode, 47), + "regmarkersize" => get(kwargs, :regmarkersize, 1), + "sampleenergymeters" => get(kwargs, :sampleenergymeters, false), + "sec" => get(kwargs, :sec, 0.0), + "showexport" => get(kwargs, :showexport, false), + "stepsize" => get(kwargs, :stepsize, "1h"), + "switchmarkercode" => get(kwargs, :switchmarkercode, 4), + "terminal" => get(kwargs, :terminal, ""), + "time" => get(kwargs, :time, Vector{Float64}([0.0, 0.0])), + "tolerance" => get(kwargs, :tolerance, 0.0001), + "totaltime" => get(kwargs, :totaltime, 0.0), + "tracecontrol" => get(kwargs, :tracecontrol, false), + "transmarkercode" => get(kwargs, :transmarkercode, 35), + "transmarkersize" => get(kwargs, :transmarkersize, 1), + "storemarkercode" => get(kwargs, :storemarkercode, 9), + "storemarkersize" => get(kwargs, :storemarkersize, 1), + "trapezoidal" => get(kwargs, :trapezoidal, false), + "type" => get(kwargs, :type, ""), + "ueregs" => get(kwargs, :ueregs, 11), + "ueweight" => get(kwargs, :ueweight, 1.0), + "voltagebases" => get(kwargs, :voltagebases, Vector{Float64}([])), + "voltexceptionreport" => get(kwargs, :voltexceptionreport, false), + "year" => get(kwargs, :year, 0), + "zonelock" => get(kwargs, :zonelock, false), + ) +end + + + +"Returns a Dict{String,Type} for the desired component `comp`, giving all of the expected data types" +const _dss_parameter_data_types = Dict{String,Dict{String,Type}}((comp, Dict{String,Type}((k, typeof(v)) for (k,v) in @eval $(Symbol("_create_$comp"))())) for comp in _dss_supported_components) diff --git a/src/io/json.jl b/src/io/json.jl index ac0c064e4..ff8652d06 100644 --- a/src/io/json.jl +++ b/src/io/json.jl @@ -5,33 +5,37 @@ import JSON.Serializations: CommonSerialization, StandardSerialization import JSON.Writer: StructuralContext, show_json struct PMDSerialization <: CommonSerialization end -function _jsonver2juliaver!(pm_data) - if haskey(pm_data, "source_version") && isa(pm_data["source_version"], Dict) - pm_data["source_version"] = "$(pm_data["source_version"]["major"]).$(pm_data["source_version"]["minor"]).$(pm_data["source_version"]["patch"])" + +"converts julia Version into serializable structure" +function _jsonver2juliaver!(data::Dict{String,<:Any}) + if haskey(data, "source_version") && isa(data["source_version"], Dict) + data["source_version"] = "$(data["source_version"]["major"]).$(data["source_version"]["minor"]).$(data["source_version"]["patch"])" end end -function _parse_mats_recursive!(dict) - parse = haskey(dict, "type") && dict["type"]=="Matrix" && haskey(dict, "eltype") && haskey(dict, "value") +"recursive helper function for parsing serialized matrices" +function _parse_mats_recursive!(data::Dict{String,<:Any})::Dict{String,Any} + parse = haskey(data, "type") && data["type"]=="Matrix" && haskey(data, "eltype") && haskey(data, "value") if parse - return _parse_matrix_value(dict["value"], dict["eltype"]) + return _parse_matrix_value(data["value"], data["eltype"]) else - for (k,v) in dict + for (k,v) in data if isa(v, Dict) - dict[k] = _parse_mats!(v) + data[k] = _parse_mats!(v) elseif isa(v, Vector) && Dict <: eltype(v) - dict[k] = [isa(x, Dict) ? _parse_mats!(x) : x for x in v] + data[k] = [isa(x, Dict) ? _parse_mats!(x) : x for x in v] end end - return dict + return data end end -function _parse_mats!(root_dict) - stack = [(root_dict, k, v) for (k, v) in root_dict] +"parser function for serialized matrices" +function _parse_mats!(data::Dict{String,<:Any}) + stack = Array{Tuple{Any, Any, Any}}([(data, k, v) for (k, v) in data]) while !isempty(stack) (store, k, v) = pop!(stack) @@ -50,52 +54,118 @@ function _parse_mats!(root_dict) end -"" -function parse_json(file::String; kwargs...) - pmd_data = open(file) do io - parse_json(io; filetype=split(lowercase(file), '.')[end], kwargs...) +"parses enums from json" +function _parse_enums!(data::Dict{String,<:Any}) + data["data_model"] = DataModel(get(data, "data_model", 1)) + + for (root_type, root_value) in data + if isa(root_value, Dict) + for (component_id, component) in root_value + if isa(component, Dict) + if haskey(component, "status") + component["status"] = Status(component["status"]) + end + + if haskey(component, "dispatchable") + component["dispatchable"] = Dispatchable(component["dispatchable"]) + end + + if haskey(component, "configuration") + if isa(component["configuration"], Vector) + component["configuration"] = Vector{ConnConfig}([ConnConfig(el) for el in component["configuration"]]) + else + component["configuration"] = ConnConfig(component["configuration"]) + end + end + + if root_type == "switch" && haskey(component, "state") + component["state"] = SwitchState(component["state"]) + end + + if root_type == "generator" && haskey(component, "control_mode") + component["generator"] = ControlMode(component["control_model"]) + end + + if root_type == "load" && haskey(component, "model") + component["model"] = LoadModel(component["model"]) + end + + if root_type == "shunt" && haskey(component, "model") + component["model"] = ShuntModel(component["model"]) + end + end + end + end + end +end + + +"Parses a JSON file into a PowerModelsDistribution data structure" +function parse_json(file::String; validate::Bool=false) + data = open(file) do io + parse_json(io; filetype=split(lowercase(file), '.')[end], validate=validate) end - return pmd_data + return data end -"Parses json from iostream or string" -function parse_json(io::IO; kwargs...)::Dict{String,Any} - pm_data = JSON.parse(io) +"Parses a JSON file into a PowerModelsDistribution data structure" +function parse_json(io::IO; validate::Bool=false)::Dict{String,Any} + data = JSON.parse(io) + + _jsonver2juliaver!(data) - _jsonver2juliaver!(pm_data) + _parse_mats!(data) - _parse_mats!(pm_data) + _parse_enums!(data) - if get(kwargs, :validate, true) - PowerModels.correct_network_data!(pm_data) + if validate + correct_network_data!(data) end - return pm_data + return data end -function print_file(path::String, pmd_data) +"prints a PowerModelsDistribution data structure into a JSON file" +function print_file(path::String, data::Dict{String,<:Any}; indent::Int=2) open(path, "w") do io - print_file(io, pmd_data) + print_file(io, data; indent=indent) end end -function print_file(io::IO, pmd_data) - JSON.print(io, JSON.parse(sprint(show_json, PMDSerialization(), pmd_data))) +"prints a PowerModelsDistribution data structure into a JSON file" +function print_file(io::IO, data::Dict{String,<:Any}; indent::Int=2) + if indent == 0 + JSON.print(io, JSON.parse(sprint(show_json, PMDSerialization(), data))) + else + JSON.print(io, JSON.parse(sprint(show_json, PMDSerialization(), data)), indent) + end end +"turns a matrix into a serializable structure" function show_json(io::StructuralContext, ::PMDSerialization, f::Matrix{<:Any}) N, M = size(f) value = string("[", join([join([f[i,j] for j in 1:M], " ") for i in 1:N], "; "), "]") eltyp = isempty(f) ? eltype(f) : typeof(f[1,1]) - out = Dict(:type=>:Matrix, :eltype=>eltyp, :value=>value) + out = Dict(:type=>:Matrix, :eltype=>eltyp, :value=>value) return show_json(io, StandardSerialization(), out) end +"custom handling for enums output to json" +function show_json(io::StructuralContext, ::CommonSerialization, f::PowerModelsDistributionEnums) + return show_json(io, StandardSerialization(), Int(f)) +end + + +"custom handling for enums output to json" +JSON.lower(p::PowerModelsDistributionEnums) = Int(p) + + +"parses in a serialized matrix" function _parse_matrix_value(value::String, eltyp::String) if value=="[]" eltyp = diff --git a/src/io/opendss.jl b/src/io/opendss.jl index b6172165e..e260e7921 100644 --- a/src/io/opendss.jl +++ b/src/io/opendss.jl @@ -1,1822 +1,903 @@ # OpenDSS parser -import LinearAlgebra: isdiag, diag, pinv +import LinearAlgebra: diagm -"Structure representing OpenDSS `dss_source_id` giving the type of the component `dss_type`, its name `dss_name`, and the active phases `active_phases`" -struct DSSSourceId - dss_type::AbstractString - dss_name::AbstractString - active_phases::Set{Int} -end - - -"Parses a component's OpenDSS source information into the `dss_source_id` struct" -function _parse_dss_source_id(component::Dict)::DSSSourceId - dss_type, dss_name = split(component["source_id"], '.') - return DSSSourceId(dss_type, dss_name, Set(component["active_phases"])) -end - - -"returns the linecode with name `id`" -function _get_linecode(dss_data::Dict, id::AbstractString) - if haskey(dss_data, "linecode") - for item in dss_data["linecode"] - if item["name"] == id - return item - end +"Parses buscoords [lon,lat] (if present) into their respective buses" +function _dss2eng_buscoords!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}) + for (id, coords) in get(data_dss, "buscoords", Dict{String,Any}()) + if haskey(data_eng["bus"], id) + bus = data_eng["bus"][id] + bus["lon"] = coords["x"] + bus["lat"] = coords["y"] end end - return Dict{String,Any}() end -""" - _discover_buses(dss_data) +"Adds nodes as buses to `data_eng` from `data_dss`" +function _dss2eng_bus!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool=false) + buses = _discover_buses(data_dss) -Discovers all of the buses (not separately defined in OpenDSS), from "lines". -""" -function _discover_buses(dss_data::Dict)::Array - bus_names = [] - buses = [] - for compType in ["line", "transformer", "reactor"] - if haskey(dss_data, compType) - compList = dss_data[compType] - for compObj in compList - if compType == "transformer" - compObj = _create_transformer(compObj["name"]; _to_sym_keys(compObj)...) - for bus in compObj["buses"] - name, nodes = _parse_busname(bus) - if !(name in bus_names) - push!(bus_names, name) - push!(buses, (name, nodes)) - end - end - elseif haskey(compObj, "bus2") - for key in ["bus1", "bus2"] - name, nodes = _parse_busname(compObj[key]) - if !(name in bus_names) - push!(bus_names, name) - push!(buses, (name, nodes)) - end - end - end - end - end - end - if length(buses) == 0 - Memento.error(_LOGGER, "dss_data has no branches!") - else - return buses - end -end + for id in buses + @assert !startswith(id, "_virtual") "Bus $id: identifiers should not start with _virtual" + eng_obj = create_bus(status=ENABLED) -""" - _dss2pmd_bus!(pmd_data, dss_data) - -Adds PowerModels-style buses to `pmd_data` from `dss_data`. -""" -function _dss2pmd_bus!(pmd_data::Dict, dss_data::Dict, import_all::Bool=false, vmin::Float64=0.9, vmax::Float64=1.1) - if !haskey(pmd_data, "bus") - pmd_data["bus"] = [] + _add_eng_obj!(data_eng, "bus", id, eng_obj) end +end - nconductors = pmd_data["conductors"] - buses = _discover_buses(dss_data) - for (n, (bus, nodes)) in enumerate(buses) - busDict = Dict{String,Any}() - - busDict["bus_i"] = n - busDict["index"] = n - busDict["name"] = bus - - busDict["bus_type"] = 1 - - busDict["vm"] = _parse_array(1.0, nodes, nconductors) - busDict["va"] = _parse_array([_wrap_to_180(-rad2deg(2*pi/nconductors*(i-1))) for i in 1:nconductors], nodes, nconductors) - - busDict["vmin"] = _parse_array(vmin, nodes, nconductors, vmin) - busDict["vmax"] = _parse_array(vmax, nodes, nconductors, vmax) - - busDict["base_kv"] = pmd_data["basekv"] - - push!(pmd_data["bus"], busDict) - end - - # create virtual sourcebus - circuit = _create_vsource(get(dss_data["circuit"][1], "bus1", "sourcebus"), dss_data["circuit"][1]["name"]; _to_sym_keys(dss_data["circuit"][1])...) - - busDict = Dict{String,Any}() - - nodes = Array{Bool}([1 1 1 0]) - ph1_ang = circuit["angle"] - vm = circuit["pu"] - - busDict["bus_i"] = length(pmd_data["bus"])+1 - busDict["index"] = length(pmd_data["bus"])+1 - busDict["name"] = "virtual_sourcebus" - - busDict["bus_type"] = 3 - - busDict["vm"] = _parse_array(vm, nodes, nconductors) - busDict["va"] = _parse_array([_wrap_to_180(-rad2deg(2*pi/nconductors*(i-1))+ph1_ang) for i in 1:nconductors], nodes, nconductors) - busDict["vmin"] = _parse_array(vm, nodes, nconductors, vm) - busDict["vmax"] = _parse_array(vm, nodes, nconductors, vm) +"Adds loadshapes to `data_eng` from `data_dss`" +function _dss2eng_loadshape!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool=false) + for (id, dss_obj) in get(data_dss, "loadshape", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "loadshape") + defaults = _apply_ordered_properties(_create_loadshape(id; _to_kwargs(dss_obj)...), dss_obj) - busDict["base_kv"] = pmd_data["basekv"] + eng_obj = Dict{String,Any}( + "time" => defaults["hour"], + "offset" => 0.0, + "replace" => defaults["useactual"], + "values" => defaults["pmult"], + "source_id" => "loadshape.$id", + ) - push!(pmd_data["bus"], busDict) -end + if import_all + _import_all!(eng_obj, dss_obj) + end + if _is_loadshape_split(dss_obj) + Memento.warn(_LOGGER, "Loadshape '$id' contains mismatched pmult and qmult, splitting into `time_series` ids '$(id)_p' and '$(id)_q'") + _add_eng_obj!(data_eng, "time_series", "$(id)_p", eng_obj) -""" - find_component(pmd_data, name, compType) + eng_obj["values"] = defaults["qmult"] -Returns the component of `compType` with `name` from `data` of type -Dict{String,Array}. -""" -function find_component(data::Dict, name::AbstractString, compType::AbstractString)::Dict - for comp in values(data[compType]) - if comp["name"] == name - return comp + _add_eng_obj!(data_eng, "time_series", "$(id)_q", eng_obj) + else + _add_eng_obj!(data_eng, "time_series", id, eng_obj) end end - Memento.warn(_LOGGER, "Could not find $compType \"$name\"") - return Dict{String,Any}() end """ - find_bus(busname, pmd_data) - -Finds the index number of the bus in existing data from the given `busname`. -""" -function find_bus(busname::AbstractString, pmd_data::Dict) - bus = find_component(pmd_data, busname, "bus") - if haskey(bus, "bus_i") - return bus["bus_i"] - else - Memento.error(_LOGGER, "cannot find connected bus with id \"$busname\"") - end -end +Adds loads to `data_eng` from `data_dss` +Constant can still be scaled by other settings, fixed cannot +Note that in the current feature set, fixed therefore equals constant +1: Constant P and Q, default +2: Constant Z +3: Constant P and quadratic Q +4: Exponential +5: Constant I +6: Constant P and fixed Q +# 7: Constant P and quadratic Q (i.e., fixed reactance) +# 8: ZIP """ - _dss2pmd_load!(pmd_data, dss_data, import_all) +function _dss2eng_load!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool, time_series::String="daily") + for (id, dss_obj) in get(data_dss, "load", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "load") + defaults = _apply_ordered_properties(_create_load(id; _to_kwargs(dss_obj)...), dss_obj) -Adds PowerModels-style loads to `pmd_data` from `dss_data`. -""" -function _dss2pmd_load!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "load") - pmd_data["load"] = [] - end + nphases = defaults["phases"] + conf = defaults["conn"] - for load in get(dss_data, "load", []) - _apply_like!(load, dss_data, "load") - defaults = _apply_ordered_properties(_create_load(load["bus1"], load["name"]; _to_sym_keys(load)...), load) + if conf==DELTA + @assert(nphases in [1, 3], "$id: only 1 and 3-phase delta loads are supported!") + end - loadDict = Dict{String,Any}() + # connections + bus = _parse_bus_id(defaults["bus1"])[1] + connections_default = conf==WYE ? [collect(1:nphases)..., 0] : nphases==1 ? [1,2] : [1,2,3] + connections = _get_conductors_ordered(defaults["bus1"], default=connections_default, pad_ground=(conf==WYE)) - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) + @assert(length(unique(connections))==length(connections), "$id: connections cannot be made to a terminal more than once.") - kv = defaults["kv"] + # now we can create the load; if you do not have the correct model, + # pd/qd fields will be populated by default (should not happen for constant current/impedance) + eng_obj = Dict{String,Any}( + "bus" => bus, + "model" => defaults["model"], + "configuration" => conf, + "connections" => connections, + "dispatchable" => NO, + "source_id" => "load.$id", + "status" => defaults["enabled"] ? ENABLED : DISABLED + ) - expected_kv = pmd_data["basekv"] / sqrt(pmd_data["conductors"]) - if !isapprox(kv, expected_kv; atol=expected_kv * 0.01) - Memento.warn(_LOGGER, "Load has kv=$kv, not the expected kv=$(expected_kv). Results may not match OpenDSS") - end + _parse_dss_load_model!(eng_obj, id) - loadDict["name"] = defaults["name"] - loadDict["load_bus"] = find_bus(name, pmd_data) - - load_name = defaults["name"] - - cnds = [1, 2, 3, 0][nodes[1,:]] - loadDict["conn"] = defaults["conn"] - nph = defaults["phases"] - delta_map = Dict([1,2]=>1, [2,3]=>2, [1,3]=>3) - if nph==1 - # PMD convention is to specify the voltage across the load - # this what OpenDSS does for 1-phase loads only - loadDict["vnom_kv"] = kv - # default is to connect betwheen L1 and N - cnds = (isempty(cnds)) ? [1, 0] : cnds - # if only one connection specified, implicitly connected to N - # bus1=x.c == bus1=x.c.0 - if length(cnds)==1 - cnds = [cnds..., 0] - end - # if more than two, only first two are considered - if length(cnds)>2 - # this no longer works if order is not preserved - # throw an error instead of behaving like OpenDSS - # cnds = cnds[1:2] - Memento.error(_LOGGER, "A 1-phase load cannot specify more than two terminals.") - end - # conn property has no effect - # delta/wye is determined by whether load connected to ground - # or between two phases - if cnds==[0, 0] - pqd_premul = zeros(3) - elseif cnds[2]==0 || cnds[1]==0 - # this is a wye load in the PMD sense - loadDict["conn"] = "wye" - ph = (cnds[2]==0) ? cnds[1] : cnds[2] - pqd_premul = zeros(3) - pqd_premul[ph] = 1 - else - # this is a delta load in the PMD sense - loadDict["conn"] = "delta" - pqd_premul = zeros(3) - pqd_premul[delta_map[cnds]] = 1 - end - elseif nph==2 - # there are some extremely weird edge cases for this - # the user can enter weird stuff and OpenDSS will still show some result - # for example, take - # nphases=3 bus1=x.1.2.0 - # this looks like a combination of a single-phase PMD delta and wye load - # so throw an error and ask to reformulate as single and three phase loads - Memento.error(_LOGGER, "Two-phase loads (nphases=2) are not supported, as these lead to unexpected behaviour. Reformulate this load as a combination of single-phase loads.") - elseif nph==3 - # for 2 and 3 phase windings, kv is always in LL, also for wye - # whilst PMD model uses actual voltage across load; so LN for wye - if loadDict["conn"]=="wye" - loadDict["vnom_kv"] = kv/sqrt(3) - else - loadDict["vnom_kv"] = kv - end - if cnds==[] - pqd_premul = [1/3, 1/3, 1/3] - else - if (length(cnds)==3 || length(cnds)==4) && cnds==unique(cnds) - #variations of [1, 2, 3] and [1, 2, 3, 0] - pqd_premul = [1/3, 1/3, 1/3] - else - Memento.error(_LOGGER, "Specified connections for three-phase load $name not allowed.") - end - end - else - Memento.error(_LOGGER, "For a load, nphases should be in [1,3].") + # if the ground is used directly, register load + if 0 in connections + _register_awaiting_ground!(data_eng["bus"][bus], connections) end - loadDict["pd"] = pqd_premul.*defaults["kw"]./1e3 - loadDict["qd"] = pqd_premul.*defaults["kvar"]./1e3 - - # parse the model - model = defaults["model"] - # some info on OpenDSS load models - ################################## - # Constant can still be scaled by other settings, fixed cannot - # Note that in the current feature set, fixed therefore equals constant - # 1: Constant P and Q, default - if model == 2 - # 2: Constant Z - elseif model == 3 - # 3: Constant P and quadratic Q - Memento.warn(_LOGGER, "$load_name: load model 3 not supported. Treating as model 1.") - model = 1 - elseif model == 4 - # 4: Exponential - Memento.warn(_LOGGER, "$load_name: load model 4 not supported. Treating as model 1.") - model = 1 - elseif model == 5 - # 5: Constant I - #warn(_LOGGER, "$name: load model 5 not supported. Treating as model 1.") - #model = 1 - elseif model == 6 - # 6: Constant P and fixed Q - Memento.warn(_LOGGER, "$load_name: load model 6 identical to model 1 in current feature set. Treating as model 1.") - model = 1 - elseif model == 7 - # 7: Constant P and quadratic Q (i.e., fixed reactance) - Memento.warn(_LOGGER, "$load_name: load model 7 not supported. Treating as model 1.") - model = 1 - elseif model == 8 - # 8: ZIP - Memento.warn(_LOGGER, "$load_name: load model 8 not supported. Treating as model 1.") - model = 1 + + kv = defaults["kv"] + if conf==WYE && nphases in [2, 3] + kv = kv/sqrt(3) end - # save adjusted model type to dict, human-readable - model_int2str = Dict(1=>"constant_power", 2=>"constant_impedance", 5=>"constant_current") - loadDict["model"] = model_int2str[model] - loadDict["status"] = convert(Int, defaults["enabled"]) + eng_obj["vm_nom"] = kv - loadDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - loadDict["source_id"] = "load.$load_name" + eng_obj["pd_nom"] = fill(defaults["kw"]/nphases, nphases) + eng_obj["qd_nom"] = fill(defaults["kvar"]/nphases, nphases) - loadDict["index"] = length(pmd_data["load"]) + 1 + _build_time_series_reference!(eng_obj, dss_obj, data_dss, defaults, time_series, "pd_nom", "qd_nom") - used = ["phases", "bus1", "name"] - _PMs._import_remaining!(loadDict, defaults, import_all; exclude=used) + if import_all + _import_all!(eng_obj, dss_obj) + end - push!(pmd_data["load"], loadDict) + _add_eng_obj!(data_eng, "load", id, eng_obj) end end -""" - _dss2pmd_shunt!(pmd_data, dss_data, import_all) - -Adds PowerModels-style shunts to `pmd_data` from `dss_data`. -""" -function _dss2pmd_shunt!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "shunt") - pmd_data["shunt"] = [] - end +"Adds capacitors to `data_eng` from `data_dss`" +function _dss2eng_capacitor!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "capacitor", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "capacitor") + defaults = _apply_ordered_properties(_create_capacitor(id; _to_kwargs(dss_obj)...), dss_obj) - for shunt in get(dss_data, "capacitor", []) - _apply_like!(shunt, dss_data, "capacitor") - defaults = _apply_ordered_properties(_create_capacitor(shunt["bus1"], shunt["name"]; _to_sym_keys(shunt)...), shunt) + nphases = defaults["phases"] + conn = defaults["conn"] - shuntDict = Dict{String,Any}() - - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) - - vnom_ln = defaults["kv"] - # 'kv' is specified as phase-to-phase for phases=2/3 (unsure for 4 and more) - if defaults["phases"] > 1 - vnom_ln = vnom_ln/sqrt(3) - end - # 'kvar' is specified for all phases at once; we want the per-phase one, in MVar - qnom = (defaults["kvar"]/1E3)/defaults["phases"] - # indexing qnom[1] is a dirty fix to support both kvar=[x] and kvar=x - # TODO fix this in a clear way, in dss_structs.jl - b_cap = qnom[1]/vnom_ln^2 - # get the base addmittance, with a LN voltage base - Sbase = 1 # not yet pmd_data["baseMVA"] because this is done in _PMs.make_per_unit - Ybase_ln = Sbase/(pmd_data["basekv"]/sqrt(3))^2 - # now convent b_cap to per unit - b_cap_pu = b_cap/Ybase_ln - - b = fill(b_cap_pu, defaults["phases"]) - N = length(b) - if defaults["conn"]=="wye" - B = LinearAlgebra.diagm(0=>b) - else # shunt["conn"]=="delta" - # create delta transformation matrix Md - Md = LinearAlgebra.diagm(0=>ones(N), 1=>-ones(N-1)) - Md[N,1] = -1 - B = Md'*LinearAlgebra.diagm(0=>b)*Md + f_terminals = _get_conductors_ordered(defaults["bus1"], default=collect(1:nphases)) + if conn==WYE + t_terminals = _get_conductors_ordered(defaults["bus2"], default=fill(0,nphases)) + else + # if delta connected, ignore bus2 and generate t_terminals such that + # it corresponds to a delta winding + t_terminals = [f_terminals[2:end]..., f_terminals[1]] end - active_phases = [n for n in 1:nconductors if nodes[n] > 0] + eng_obj = Dict{String,Any}( + "configuration" => conn, + "model" => CAPACITOR, + "dispatchable" => NO, + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "capacitor.$id", + ) - if length(active_phases) < 3 - Bf = zeros(3, 3) - Bf[active_phases, active_phases] = B - B = Bf + if import_all + _import_all!(eng_obj, dss_obj) end - shuntDict["shunt_bus"] = find_bus(name, pmd_data) - shuntDict["name"] = defaults["name"] - shuntDict["gs"] = fill(0.0, 3, 3) - shuntDict["bs"] = B - shuntDict["status"] = convert(Int, defaults["enabled"]) - shuntDict["index"] = length(pmd_data["shunt"]) + 1 - - shuntDict["active_phases"] = active_phases - shuntDict["source_id"] = "capacitor.$(defaults["name"])" + bus_name = _parse_bus_id(defaults["bus1"])[1] + bus2_name = _parse_bus_id(defaults["bus2"])[1] + if bus_name == bus2_name + eng_obj["bus"] = bus_name - used = ["bus1", "phases", "name"] - _PMs._import_remaining!(shuntDict, defaults, import_all; exclude=used) + # 'kv' is specified as phase-to-phase for phases=2/3 (unsure for 4 and more) + #TODO figure out for more than 3 phases + vnom_ln = defaults["kv"] + if defaults["phases"] in [2,3] + vnom_ln = vnom_ln/sqrt(3) + end + defaults["kv"] = fill(vnom_ln, nphases) - push!(pmd_data["shunt"], shuntDict) - end + # 'kvar' is specified for all phases at once; we want the per-phase one + defaults["kvar"] = fill(defaults["kvar"] / nphases, nphases) + # TODO check unit conversion on qnom/b + vnom_ln = defaults["kv"] + qnom = defaults["kvar"] ./ 1e3 + b = qnom ./ vnom_ln.^2 - for shunt in get(dss_data, "reactor", []) - if !haskey(shunt, "bus2") - _apply_like!(shunt, dss_data, "reactor") - defaults = _apply_ordered_properties(_create_reactor(shunt["bus1"], shunt["name"]; _to_sym_keys(shunt)...), shunt) + # convert to a shunt matrix + terminals, B = _calc_shunt(f_terminals, t_terminals, b) - shuntDict = Dict{String,Any}() + # if one terminal is ground (0), reduce shunt addmittance matrix + terminals, B = _calc_ground_shunt_admittance_matrix(terminals, B, 0) - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) + eng_obj["gs"] = zeros(size(B)) + eng_obj["bs"] = B + eng_obj["connections"] = terminals - Zbase = (pmd_data["basekv"] / sqrt(3.0))^2 * nconductors / pmd_data["baseMVA"] # Use single-phase base impedance for each phase - Gcap = Zbase * sum(defaults["kvar"]) / (nconductors * 1e3 * (pmd_data["basekv"] / sqrt(3.0))^2) + _add_eng_obj!(data_eng, "shunt", id, eng_obj) + else + Memento.warn(_LOGGER, "capacitors as constant impedance elements is not yet supported, treating reactor.$id like line") + _eng_obj = Dict{String,Any}( + "f_bus" => _parse_bus_id(defaults["bus1"])[1], + "t_bus" => _parse_bus_id(defaults["bus2"])[1], + "f_connections" => _get_conductors_ordered(defaults["bus1"], default=collect(1:nphases)), + "t_connections" => _get_conductors_ordered(defaults["bus2"], default=collect(1:nphases)), + "length" => 1.0, + "rs" => diagm(0 => fill(0.2, nphases)), + "xs" => zeros(nphases, nphases), + "g_fr" => zeros(nphases, nphases), + "b_fr" => zeros(nphases, nphases), + "g_to" => zeros(nphases, nphases), + "b_to" => zeros(nphases, nphases), + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "capacitor.$id", + ) - shuntDict["shunt_bus"] = find_bus(name, pmd_data) - shuntDict["name"] = defaults["name"] - shuntDict["gs"] = LinearAlgebra.diagm(0=>_parse_array(0.0, nodes, nconductors)) # TODO: - shuntDict["bs"] = LinearAlgebra.diagm(0=>_parse_array(Gcap, nodes, nconductors)) - shuntDict["status"] = convert(Int, defaults["enabled"]) - shuntDict["index"] = length(pmd_data["shunt"]) + 1 + merge!(eng_obj, _eng_obj) - shuntDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - shuntDict["source_id"] = "reactor.$(defaults["name"])" + # if the ground is used directly, register + if 0 in eng_obj["f_connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["f_bus"]], eng_obj["f_connections"]) + end + if 0 in eng_obj["t_connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["t_bus"]], eng_obj["t_connections"]) + end - used = ["bus1", "phases", "name"] - _PMs._import_remaining!(shuntDict, defaults, import_all; exclude=used) + if import_all + _import_all!(eng_obj, dss_obj) + end - push!(pmd_data["shunt"], shuntDict) + _add_eng_obj!(data_eng, "line", id, eng_obj) end end end -""" - _dss2pmd_gen!(pmd_data, dss_data, import_all) - -Adds PowerModels-style generators to `pmd_data` from `dss_data`. -""" -function _dss2pmd_gen!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "gen") - pmd_data["gen"] = [] - end - - # sourcebus generator (created by circuit) - circuit = dss_data["circuit"][1] - defaults = _create_vsource(get(circuit, "bus1", "sourcebus"), "sourcegen"; _to_sym_keys(circuit)...) - - genDict = Dict{String,Any}() +"Adds shunt reactors to `data_eng` from `data_dss`" +function _dss2eng_reactor!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "reactor", Dict{String,Any}()) + if !haskey(dss_obj, "bus2") + _apply_like!(dss_obj, data_dss, "reactor") + defaults = _apply_ordered_properties(_create_reactor(id; _to_kwargs(dss_obj)...), dss_obj) - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) + nphases = defaults["phases"] - genDict["gen_bus"] = find_bus("virtual_sourcebus", pmd_data) - genDict["name"] = defaults["name"] - genDict["gen_status"] = convert(Int, defaults["enabled"]) - - # TODO: populate with VSOURCE properties - genDict["pg"] = _parse_array( 0.0, nodes, nconductors) - genDict["qg"] = _parse_array( 0.0, nodes, nconductors) - - genDict["qmin"] = _parse_array(-NaN, nodes, nconductors) - genDict["qmax"] = _parse_array( NaN, nodes, nconductors) - - genDict["pmin"] = _parse_array(-NaN, nodes, nconductors) - genDict["pmax"] = _parse_array( NaN, nodes, nconductors) - - genDict["model"] = 2 - genDict["startup"] = 0.0 - genDict["shutdown"] = 0.0 - genDict["ncost"] = 3 - genDict["cost"] = [0.0, 1.0, 0.0] - - genDict["index"] = length(pmd_data["gen"]) + 1 - - genDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - genDict["source_id"] = "vsource.$(defaults["name"])" - - genDict["conn"] = "wye" - - used = ["name", "phases", "bus1"] - _PMs._import_remaining!(genDict, defaults, import_all; exclude=used) + eng_obj = Dict{String,Any}( + "bus" => _parse_bus_id(defaults["bus1"])[1], + "configuration" => defaults["conn"], + "model" => REACTOR, + "dispatchable" => NO, + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "reactor.$id", + ) - push!(pmd_data["gen"], genDict) + connections_default = eng_obj["configuration"] == WYE ? [collect(1:nphases)..., 0] : collect(1:nphases) + eng_obj["connections"] = _get_conductors_ordered(defaults["bus1"], default=connections_default, check_length=false) + # if the ground is used directly, register + if 0 in eng_obj["connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"]], eng_obj["connections"]) + end - for gen in get(dss_data, "generator", []) - _apply_like!(gen, dss_data, "generator") - defaults = _apply_ordered_properties(_create_generator(gen["bus1"], gen["name"]; _to_sym_keys(gen)...), gen) + # TODO Check unit conversion on Gcap + Gcap = sum(defaults["kvar"]) / (nphases * 1e3 * (first(data_eng["settings"]["vbases_default"])[2])^2) - genDict = Dict{String,Any}() + eng_obj["bs"] = diagm(0=>fill(Gcap, nphases)) + eng_obj["gs"] = zeros(size(eng_obj["bs"])) - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) + if import_all + _import_all!(eng_obj, dss_obj) + end - genDict["gen_bus"] = find_bus(name, pmd_data) - genDict["name"] = defaults["name"] - genDict["gen_status"] = convert(Int, defaults["enabled"]) - genDict["pg"] = _parse_array(defaults["kw"] / (1e3 * nconductors), nodes, nconductors) - genDict["qg"] = _parse_array(defaults["kvar"] / (1e3 * nconductors), nodes, nconductors) - genDict["vg"] = _parse_array(defaults["kv"] / pmd_data["basekv"], nodes, nconductors) + _add_eng_obj!(data_eng, "shunt", id, eng_obj) + else + Memento.warn(_LOGGER, "reactors as constant impedance elements is not yet supported, treating reactor.$id like line") + _apply_like!(dss_obj, data_dss, "reactor") + defaults = _apply_ordered_properties(_create_reactor(id; _to_kwargs(dss_obj)...), dss_obj) + + nphases = defaults["phases"] + + eng_obj = Dict{String,Any}( + "f_bus" => _parse_bus_id(defaults["bus1"])[1], + "t_bus" => _parse_bus_id(defaults["bus2"])[1], + "f_connections" => _get_conductors_ordered(defaults["bus1"], default=collect(1:nphases)), + "t_connections" => _get_conductors_ordered(defaults["bus2"], default=collect(1:nphases)), + "length" => 1.0, + "rs" => diagm(0 => fill(0.2, nphases)), + "xs" => zeros(nphases, nphases), + "g_fr" => zeros(nphases, nphases), + "b_fr" => zeros(nphases, nphases), + "g_to" => zeros(nphases, nphases), + "b_to" => zeros(nphases, nphases), + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "reactor.$id", + ) - genDict["qmin"] = _parse_array(defaults["minkvar"] / (1e3 * nconductors), nodes, nconductors) - genDict["qmax"] = _parse_array(defaults["maxkvar"] / (1e3 * nconductors), nodes, nconductors) + # if the ground is used directly, register + if 0 in eng_obj["f_connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["f_bus"]], eng_obj["f_connections"]) + end + if 0 in eng_obj["t_connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["t_bus"]], eng_obj["t_connections"]) + end - genDict["apf"] = _parse_array(0.0, nodes, nconductors) + if import_all + _import_all!(eng_obj, dss_obj) + end - genDict["pmax"] = genDict["pg"] # Assumes generator is at rated power - genDict["pmin"] = 0.0 * genDict["pg"] # 0% of pmax + _add_eng_obj!(data_eng, "line", id, eng_obj) + end + end +end - genDict["pc1"] = genDict["pmax"] - genDict["pc2"] = genDict["pmin"] - genDict["qc1min"] = genDict["qmin"] - genDict["qc1max"] = genDict["qmax"] - genDict["qc2min"] = genDict["qmin"] - genDict["qc2max"] = genDict["qmax"] - # For distributed generation ramp rates are not usually an issue - # and they are not supported in OpenDSS - genDict["ramp_agc"] = genDict["pmax"] +"Adds generators to `data_eng` from `data_dss`" +function _dss2eng_generator!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool, time_series::String="daily") + for (id, dss_obj) in get(data_dss, "generator", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "generator") + defaults = _apply_ordered_properties(_create_generator(id; _to_kwargs(dss_obj)...), dss_obj) - genDict["ramp_q"] = _parse_array(max.(abs.(genDict["qmin"]), abs.(genDict["qmax"])), nodes, nconductors) - genDict["ramp_10"] = genDict["pmax"] - genDict["ramp_30"] = genDict["pmax"] + nphases = defaults["phases"] - genDict["control_model"] = defaults["model"] + eng_obj = Dict{String,Any}( + "phases" => nphases, + "connections" => _get_conductors_ordered(defaults["bus1"], pad_ground=true, default=collect(1:defaults["phases"]+1)), + "bus" => _parse_bus_id(defaults["bus1"])[1], + "pg" => fill(defaults["kw"] / nphases, nphases), + "qg" => fill(defaults["kvar"] / nphases, nphases), + "vg" => fill(defaults["kv"], nphases), + "qg_lb" => fill(defaults["minkvar"] / nphases, nphases), + "qg_ub" => fill(defaults["maxkvar"] / nphases, nphases), + "pg_lb" => fill(0.0, nphases), + "pg_ub" => fill(defaults["kw"] / nphases, nphases), + "control_mode" => FREQUENCYDROOP, + "configuration" => WYE, + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "generator.$id" + ) - # if PV generator mode convert attached bus to PV bus - if genDict["control_model"] == 3 - pmd_data["bus"][genDict["gen_bus"]]["bus_type"] = 2 + # if the ground is used directly, register load + if 0 in eng_obj["connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"]], eng_obj["connections"]) end - genDict["model"] = 2 - genDict["startup"] = 0.0 - genDict["shutdown"] = 0.0 - genDict["ncost"] = 3 - genDict["cost"] = [0.0, 1.0, 0.0] + _build_time_series_reference!(eng_obj, dss_obj, data_dss, defaults, time_series, "pg", "qg") - genDict["index"] = length(pmd_data["gen"]) + 1 - - genDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - genDict["source_id"] = "generator.$(defaults["name"])" - - genDict["conn"] = "wye" - - used = ["name", "phases", "bus1"] - _PMs._import_remaining!(genDict, defaults, import_all; exclude=used) + if import_all + _import_all!(eng_obj, dss_obj) + end - push!(pmd_data["gen"], genDict) + _add_eng_obj!(data_eng, "generator", id, eng_obj) end +end - for pv in get(dss_data, "pvsystem", []) - Memento.warn(_LOGGER, "Converting PVSystem \"$(pv["name"])\" into generator with limits determined by OpenDSS property 'kVA'") - - _apply_like!(pv, dss_data, "pvsystem") - defaults = _apply_ordered_properties(_create_pvsystem(pv["bus1"], pv["name"]; _to_sym_keys(pv)...), pv) - - pvDict = Dict{String,Any}() - - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) - - pvDict["name"] = defaults["name"] - pvDict["gen_bus"] = find_bus(name, pmd_data) - pvDict["pg"] = _parse_array(defaults["kva"] / (1e3 * nconductors), nodes, nconductors) - pvDict["qg"] = _parse_array(defaults["kva"] / (1e3 * nconductors), nodes, nconductors) - pvDict["vg"] = _parse_array(defaults["kv"] / pmd_data["basekv"], nodes, nconductors) +"Adds vsources to `data_eng` from `data_dss`" +function _dss2eng_vsource!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool, time_series::String="daily") + for (id, dss_obj) in get(data_dss, "vsource", Dict{<:Any,<:Any}()) + _apply_like!(dss_obj, data_dss, "vsource") + defaults = _create_vsource(id; _to_kwargs(dss_obj)...) - pvDict["pmin"] = _parse_array(0.0, nodes, nconductors) - pvDict["pmax"] = _parse_array(defaults["kva"] / (1e3 * nconductors), nodes, nconductors) - pvDict["qmin"] = -_parse_array(defaults["kva"] / (1e3 * nconductors), nodes, nconductors) - pvDict["qmax"] = _parse_array(defaults["kva"] / (1e3 * nconductors), nodes, nconductors) + ph1_ang = defaults["angle"] + vm_pu = defaults["pu"] - pvDict["gen_status"] = convert(Int, defaults["enabled"]) + phases = defaults["phases"] + vnom = first(data_eng["settings"]["vbases_default"])[2] - pvDict["model"] = 2 - pvDict["startup"] = 0.0 - pvDict["shutdown"] = 0.0 - pvDict["ncost"] = 3 - pvDict["cost"] = [0.0, 1.0, 0.0] + vm = fill(vm_pu, phases)*vnom + va = rad2deg.(_wrap_to_pi.([-2*pi/phases*(i-1)+deg2rad(ph1_ang) for i in 1:phases])) - pvDict["index"] = length(pmd_data["gen"]) + 1 + eng_obj = Dict{String,Any}( + "bus" => _parse_bus_id(defaults["bus1"])[1], + "connections" => collect(1:phases), + "vm" => vm, + "va" => va, + "rs" => defaults["rmatrix"], + "xs" => defaults["xmatrix"], + "source_id" => "vsource.$id", + "status" => defaults["enabled"] ? ENABLED : DISABLED + ) - pvDict["active_phases"] = [nodes[n] > 0 ? 1 : 0 for n in 1:nconductors] - pvDict["source_id"] = "pvsystem.$(defaults["name"])" + # if the ground is used directly, register load + if 0 in eng_obj["connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"]], eng_obj["connections"]) + end - pvDict["conn"] = "wye" + _build_time_series_reference!(eng_obj, dss_obj, data_dss, defaults, time_series, "pg", "qg") - used = ["name", "phases", "bus1"] - _PMs._import_remaining!(pvDict, defaults, import_all; exclude=used) + if import_all + _import_all!(eng_obj, dss_obj) + end - push!(pmd_data["gen"], pvDict) + _add_eng_obj!(data_eng, "voltage_source", id, eng_obj) end end -""" - _dss2pmd_branch!(pmd_data, dss_data, import_all) +"Adds lines to `data_eng` from `data_dss`" +function _dss2eng_linecode!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "linecode", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "linecode") -Adds PowerModels-style branches to `pmd_data` from `dss_data`. -""" -function _dss2pmd_branch!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "branch") - pmd_data["branch"] = [] - end + dss_obj["circuit_basefreq"] = data_eng["settings"]["base_frequency"] - nconductors = pmd_data["conductors"] + defaults = _apply_ordered_properties(_create_linecode(id; _to_kwargs(dss_obj)...), dss_obj) - for line in get(dss_data, "line", []) - _apply_like!(line, dss_data, "line") + eng_obj = Dict{String,Any}() - if haskey(line, "linecode") - linecode = deepcopy(_get_linecode(dss_data, get(line, "linecode", ""))) - if haskey(linecode, "like") - linecode = merge(find_component(dss_data, linecode["like"], "linecode"), linecode) - end + nphases = defaults["nphases"] - linecode["units"] = get(line, "units", "none") == "none" ? "none" : get(linecode, "units", "none") - linecode["circuit_basefreq"] = pmd_data["basefreq"] + eng_obj["rs"] = reshape(defaults["rmatrix"], nphases, nphases) + eng_obj["xs"] = reshape(defaults["xmatrix"], nphases, nphases) - linecode = _create_linecode(get(linecode, "name", ""); _to_sym_keys(linecode)...) - delete!(linecode, "name") - else - linecode = Dict{String,Any}() - end + eng_obj["b_fr"] = reshape(defaults["cmatrix"], nphases, nphases) ./ 2.0 + eng_obj["b_to"] = reshape(defaults["cmatrix"], nphases, nphases) ./ 2.0 + eng_obj["g_fr"] = fill(0.0, nphases, nphases) + eng_obj["g_to"] = fill(0.0, nphases, nphases) - if haskey(line, "basefreq") && line["basefreq"] != pmd_data["basefreq"] - Memento.warn(_LOGGER, "basefreq=$(line["basefreq"]) on line $(line["name"]) does not match circuit basefreq=$(pmd_data["basefreq"])") - line["circuit_basefreq"] = pmd_data["basefreq"] + if import_all + _import_all!(eng_obj, dss_obj) end - defaults = _apply_ordered_properties(_create_line(line["bus1"], line["bus2"], line["name"]; _to_sym_keys(line)...), line; code_dict=linecode) + _add_eng_obj!(data_eng, "linecode", id, eng_obj) + end +end - bf, nodes = _parse_busname(defaults["bus1"]) - phase_order_fr = _get_conductors_ordered(defaults["bus1"]; neutral=false, nconductors=nconductors) - phase_order_to = _get_conductors_ordered(defaults["bus2"]; neutral=false, nconductors=nconductors) +"Adds lines to `data_eng` from `data_dss`" +function _dss2eng_line!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "line", Dict()) + _apply_like!(dss_obj, data_dss, "line") - @assert phase_order_fr == phase_order_to "Order of connections must be identical on either end of a line" + if haskey(dss_obj, "basefreq") && dss_obj["basefreq"] != data_eng["settings"]["base_frequency"] + Memento.warn(_LOGGER, "basefreq=$(dss_obj["basefreq"]) on line.$id does not match circuit basefreq=$(data_eng["settings"]["base_frequency"])") + end - bt = _parse_busname(defaults["bus2"])[1] + defaults = _apply_ordered_properties(_create_line(id; _to_kwargs(dss_obj)...), dss_obj) - branchDict = Dict{String,Any}() + _like_is_switch = haskey(dss_obj, "like") && get(get(data_dss["line"], dss_obj["like"], Dict{String,Any}()), "switch", false) + nphases = defaults["phases"] - branchDict["name"] = defaults["name"] + f_connections = _get_conductors_ordered(defaults["bus1"], default=collect(1:nphases)) + t_connections = _get_conductors_ordered(defaults["bus2"], default=collect(1:nphases)) - branchDict["f_bus"] = find_bus(bf, pmd_data) - branchDict["t_bus"] = find_bus(bt, pmd_data) + ncond = length(f_connections) - branchDict["length"] = defaults["length"] + eng_obj = Dict{String,Any}( + "f_bus" => _parse_bus_id(defaults["bus1"])[1], + "t_bus" => _parse_bus_id(defaults["bus2"])[1], + "length" => defaults["switch"] || _like_is_switch ? 0.001 : defaults["length"], + "f_connections" => f_connections, + "t_connections" => t_connections, + "cm_ub" => fill(defaults["normamps"], ncond), + "cm_ub_b" => fill(defaults["emergamps"], ncond), + "cm_ub_c" => fill(defaults["emergamps"], ncond), + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "line.$id" + ) - rmatrix = _reorder_matrix(_parse_matrix(defaults["rmatrix"], nodes, nconductors), phase_order_fr) - xmatrix = _reorder_matrix(_parse_matrix(defaults["xmatrix"], nodes, nconductors), phase_order_fr) - cmatrix = _reorder_matrix(_parse_matrix(defaults["cmatrix"], nodes, nconductors), phase_order_fr) + if haskey(dss_obj, "linecode") + eng_obj["linecode"] = dss_obj["linecode"] + end - Zbase = (pmd_data["basekv"] / sqrt(3))^2 * nconductors / (pmd_data["baseMVA"]) + if any(haskey(dss_obj, key) && _is_after_linecode(dss_obj["prop_order"], key) for key in ["r0", "r1", "rg", "rmatrix"]) || !haskey(dss_obj, "linecode") + eng_obj["rs"] = reshape(defaults["rmatrix"], ncond, ncond) + end - Zbase = Zbase/3 - # The factor 3 here is needed to convert from a voltage base - # in line-to-line (LL) to a voltage base in line-to-neutral (LN). - # V_LL = √3*V_LN - # Zbase_new = Zbase_old*(Vbase_new/Vbase_old)^2 = Zbase_old*(1/√3)^2 - # In the parser, LL voltage base is used for per unit conversion. - # However, in the mathematical model, the voltage magnitude per phase - # is fixed at 1. So implicitly, we later on state that the voltage base - # is actually in LN. We compensate here for that. + if any(haskey(dss_obj, key) && _is_after_linecode(dss_obj["prop_order"], key) for key in ["x0", "x1", "xg", "xmatrix"]) || !haskey(dss_obj, "linecode") + eng_obj["xs"] = reshape(defaults["xmatrix"], ncond, ncond) + end - branchDict["br_r"] = rmatrix * defaults["length"] / Zbase - branchDict["br_x"] = xmatrix * defaults["length"] / Zbase + if any(haskey(dss_obj, key) && _is_after_linecode(dss_obj["prop_order"], key) for key in ["b0", "b1", "c0", "c1", "cmatrix"]) || !haskey(dss_obj, "linecode") + eng_obj["b_fr"] = reshape(defaults["cmatrix"], ncond, ncond) ./ 2.0 + eng_obj["b_to"] = reshape(defaults["cmatrix"], ncond, ncond) ./ 2.0 + eng_obj["g_fr"] = fill(0.0, ncond, ncond) + eng_obj["g_to"] = fill(0.0, ncond, ncond) + end - branchDict["g_fr"] = LinearAlgebra.diagm(0=>_parse_array(0.0, nodes, nconductors)) - branchDict["g_to"] = LinearAlgebra.diagm(0=>_parse_array(0.0, nodes, nconductors)) + # if the ground is used directly, register + for prefix in ["f_", "t_"] + if 0 in eng_obj["$(prefix)connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["$(prefix)bus"]], eng_obj["$(prefix)connections"]) + end + end - branchDict["b_fr"] = Zbase * (2.0 * pi * pmd_data["basefreq"] * cmatrix * defaults["length"] / 1e9) / 2.0 - branchDict["b_to"] = Zbase * (2.0 * pi * pmd_data["basefreq"] * cmatrix * defaults["length"] / 1e9) / 2.0 + if import_all + _import_all!(eng_obj, dss_obj) + end - branchDict["c_rating_a"] = _parse_array(defaults["normamps"], nodes, nconductors) - branchDict["c_rating_b"] = _parse_array(defaults["emergamps"], nodes, nconductors) - branchDict["c_rating_c"] = _parse_array(defaults["emergamps"], nodes, nconductors) + if defaults["switch"] + eng_obj["state"] = CLOSED + eng_obj["dispatchable"] = NO - branchDict["tap"] = _parse_array(1.0, nodes, nconductors, 1.0) - branchDict["shift"] = _parse_array(0.0, nodes, nconductors) + for key in ["g_fr", "b_fr", "g_to", "b_to"] + delete!(eng_obj, key) + end - branchDict["br_status"] = convert(Int, defaults["enabled"]) + _add_eng_obj!(data_eng, "switch", id, eng_obj) + else + _add_eng_obj!(data_eng, "line", id, eng_obj) + end + end +end - branchDict["angmin"] = _parse_array(-60.0, nodes, nconductors, -60.0) - branchDict["angmax"] = _parse_array( 60.0, nodes, nconductors, 60.0) - branchDict["transformer"] = false - branchDict["switch"] = defaults["switch"] +"Adds transformers to `data_eng` from `data_dss`" +function _dss2eng_xfmrcode!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "xfmrcode", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "xfmrcode") + defaults = _apply_ordered_properties(_create_xfmrcode(id; _to_kwargs(dss_obj)...), dss_obj) - branchDict["index"] = length(pmd_data["branch"]) + 1 + nphases = defaults["phases"] + nrw = defaults["windings"] - nodes = .+([_parse_busname(defaults[n])[2] for n in ["bus1", "bus2"]]...) - branchDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - branchDict["source_id"] = "line.$(defaults["name"])" + eng_obj = Dict{String,Any}( + "tm_set" => Vector{Vector{Float64}}([fill(tap, nphases) for tap in defaults["taps"]]), + "tm_lb" => Vector{Vector{Float64}}(fill(fill(defaults["mintap"], nphases), nrw)), + "tm_ub" => Vector{Vector{Float64}}(fill(fill(defaults["maxtap"], nphases), nrw)), + "tm_fix" => Vector{Vector{Bool}}(fill(ones(Bool, nphases), nrw)), + "tm_step" => Vector{Vector{Float64}}(fill(fill(1/32, nphases), nrw)), + "vm_nom" => Vector{Float64}(defaults["kvs"]), + "sm_nom" => Vector{Float64}(defaults["kvas"]), + "configuration" => Vector{ConnConfig}(defaults["conns"]), + "rw" => Vector{Float64}(defaults["%rs"] ./ 100), + "noloadloss" => defaults["%noloadloss"] / 100, + "cmag" => defaults["%imag"] / 100, + "xsc" => nrw == 2 ? [defaults["xhl"] / 100] : [defaults["xhl"], defaults["xht"], defaults["xlt"]] ./ 100, + "source_id" => "xfmrcode.$id", + ) - used = ["name", "bus1", "bus2", "rmatrix", "xmatrix"] - _PMs._import_remaining!(branchDict, defaults, import_all; exclude=used) + if import_all + _import_all!(eng_obj, dss_obj) + end - push!(pmd_data["branch"], branchDict) + _add_eng_obj!(data_eng, "xfmrcode", id, eng_obj) end end -""" - _dss2pmd_transformer!(pmd_data, dss_data, import_all) +"Adds transformers to `data_eng` from `data_dss`" +function _dss2eng_transformer!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool) + for (id, dss_obj) in get(data_dss, "transformer", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "transformer") + defaults = _apply_ordered_properties(_create_transformer(id; _to_kwargs(dss_obj)...), dss_obj) -Adds ThreePhasePowerModels-style transformers to `pmd_data` from `dss_data`. -""" -function _dss2pmd_transformer!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "transformer_comp") - pmd_data["transformer_comp"] = Array{Any,1}() - end - - for transformer in get(dss_data, "transformer", []) - _apply_like!(transformer, dss_data, "transformer") - defaults = _apply_ordered_properties(_create_transformer(transformer["name"]; _to_sym_keys(transformer)...), transformer) + eng_obj = Dict{String, Any}( + "source_id" => "transformer.$id" + ) - nconductors = pmd_data["conductors"] - nrw = defaults["windings"] - prop_suffix_w = ["", ["_$w" for w in 2:nrw]...] - if nrw>3 - # All of the code is compatible with any number of windings, - # except for the parsing of the loss model (the pair-wise reactance) - Memento.error(_LOGGER, "For now parsing of xscarray is not supported. At most 3 windings are allowed, not $nrw.") + # no way around checking xfmrcode for these properties + _shared = Dict{String,Any}( + "leadlag" => defaults["leadlag"], + "conns" => defaults["conns"], + "phases" => defaults["phases"], + "windings" => defaults["windings"] + ) + if haskey(dss_obj, "xfmrcode") + xfmrcode_dss_obj = deepcopy(data_dss["xfmrcode"][dss_obj["xfmrcode"]]) + _apply_like!(xfmrcode_dss_obj, data_dss, "xfmrcode") + xfmrcode = _apply_ordered_properties(_create_xfmrcode(string(dss_obj["xfmrcode"]); _to_kwargs(xfmrcode_dss_obj)...), xfmrcode_dss_obj) + + for key in ["leadlag", "conns", "phases", "windings"] + if haskey(dss_obj, key) && _is_after_xfmrcode(dss_obj["prop_order"], key) + _shared[key] = defaults[key] + else + _shared[key] = xfmrcode[key] + end + end end - transDict = Dict{String,Any}() - transDict["name"] = defaults["name"] - transDict["source_id"] = "transformer.$(defaults["name"])" - transDict["buses"] = Array{Int, 1}(undef, nrw) - if !isempty(defaults["bank"]) - transDict["bank"] = defaults["bank"] + leadlag = _shared["leadlag"] + confs = _shared["conns"] + nphases = _shared["phases"] + nrw = _shared["windings"] + + # two-phase delta transformers have single coil + if all(conf==DELTA for conf in confs) && nphases==2 + ncoils = 1 + else + ncoils = nphases end - for i in 1:nrw - bnstr = defaults["buses"][i] - bus, nodes = _parse_busname(bnstr) - active_phases = [n for n in 1:nconductors if nodes[n] > 0] - nodes_123 = [true true true] - if !all(nodes[1:3]) && isempty(defaults["bank"]) - Memento.warn(_LOGGER, "Only three-phase transformers are supported. The bus specification $bnstr is treated as $bus instead.") - elseif !isempty(defaults["bank"]) - if haskey(transDict, "active_phases") - if transDict["active_phases"] != active_phases - Memento.error(_LOGGER, "Mismatched phase connections on transformer windings not supported when banking transformers") + + # taps + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, "taps") && _is_after_xfmrcode(dss_obj["prop_order"], "taps")) || all(haskey(dss_obj, k) && _is_after_xfmrcode(dss_obj["prop_order"], k) for k in ["tap", "tap_2", "tap_3"]) + eng_obj["tm_set"] = [fill(defaults["taps"][w], ncoils) for w in 1:nrw] + else + for (w, key_suffix) in enumerate(["", "_2", "_3"]) + if haskey(dss_obj, "tap$(key_suffix)") && _is_after_xfmrcode(dss_obj["prop_order"], "tap$(key_suffix)") + if !haskey(eng_obj, "tm_set") + eng_obj["tm_set"] = Vector{Any}(missing, nrw) end - else - transDict["active_phases"] = active_phases + eng_obj["tm_set"][w] = fill(defaults["taps"][defaults["wdg$(key_suffix)"]], ncoils) end - elseif all(nodes[1:3]) - transDict["active_phases"] = [1, 2, 3] - else - transDict["active_phases"] = [1, 2, 3] - Memento.warn(_LOGGER, "Only three-phase transformers are supported. The bus specification $bnstr is treated as $bus instead.") end - transDict["buses"][i] = find_bus(bus, pmd_data) end - # voltage and power ratings - #transDict["vnom_kv"] = defaults["kvs"] - #transDict["snom_kva"] = defaults["kvas"] - transDict["rate_a"] = [ones(nconductors)*defaults["normhkva"] for i in 1:nrw] - transDict["rate_b"] = [ones(nconductors)*defaults["normhkva"] for i in 1:nrw] - transDict["rate_c"] = [ones(nconductors)*defaults["emerghkva"] for i in 1:nrw] - # convert to 1 MVA base - transDict["rate_a"] *= 1E-3 - transDict["rate_b"] *= 1E-3 - transDict["rate_c"] *= 1E-3 - # connection properties - dyz_map = Dict("wye"=>"wye", "delta"=>"delta", "ll"=>"delta", "ln"=>"wye") - dyz_primary = dyz_map[defaults["conns"][1]] - transDict["conns"] = Array{String,1}(undef, nrw) - - transDict["config"] = Dict{Int,Any}() - transDict["config"][1] = Dict( - "type"=>dyz_primary, - "polarity"=>1, - "cnd"=>[1, 2, 3], - "grounded"=>true, - "vm_nom"=>defaults["kvs"][1] - ) - #transDict["conns"][1] = string("123+", dyz_primary) - for w in 2:nrw - type = dyz_map[defaults["conns"][w]] - if dyz_primary==type - cnd = [1,2,3] - polarity = 1 + # kvs, kvas + for (fr_key, to_key) in zip(["kv", "kva"], ["vm_nom", "sm_nom"]) + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, "$(fr_key)s") && _is_after_xfmrcode(dss_obj["prop_order"], "$(fr_key)s")) || all(haskey(dss_obj, "$(fr_key)$(key_suffix)") && _is_after_xfmrcode(dss_obj["prop_order"], "$(fr_key)$(key_suffix)") for key_suffix in ["", "_2", "_3"]) + eng_obj[to_key] = defaults["$(fr_key)s"] else - if defaults["leadlag"] in ["ansi", "lag"] - #Yd1 => (123+y,123+d) - #Dy1 => (123+d,231-y) - #pp_w = (type=="delta") ? "123+" : "231-" - cnd = (type=="delta") ? [1, 2, 3] : [2, 3, 1] - polarity = (type=="delta") ? 1 : -1 - else # hence defaults["leadlag"] in ["euro", "lead"] - #Yd11 => (123+y,312-d) - #Dy11 => (123+d,123+y) - #pp_w = (type=="delta") ? "312-" : "123+" - cnd = (type=="delta") ? [3, 1, 2] : [1, 2, 3] - polarity = (type=="delta") ? -1 : 1 + for (w, key_suffix) in enumerate(["", "_2", "_3"]) + if haskey(dss_obj, "$(fr_key)$(key_suffix)") && _is_after_xfmrcode(dss_obj["prop_order"], "$(fr_key)$(key_suffix)") + if !haskey(eng_obj, to_key) + eng_obj[to_key] = Vector{Any}(missing, nrw) + end + eng_obj[to_key][w] = defaults["$(fr_key)s"][defaults["wdg$(key_suffix)"]] + end end end - transDict["config"][w] = Dict( - "type"=>type, - "polarity"=>polarity, - "cnd"=>cnd, - "vm_nom"=>defaults["kvs"][w] - ) - if type=="wye" - transDict["config"][w]["grounded"] = true - end end - # tap properties - transDict["tm"] = [ones(Float64,3)*defaults["taps"][i] for i in 1:nrw] - transDict["tm_min"] = [ones(Float64,3)*defaults["mintap"] for i in 1:nrw] - transDict["tm_max"] = [ones(Float64,3)*defaults["maxtap"] for i in 1:nrw] - transDict["tm_step"] = [ones(Int,3)*defaults["numtaps"] for i in 1:nrw] - transDict["fixed"] = [ones(Bool,3) for i in 1:nrw] - - # loss model (converted to SI units, referred to secondary) - function zpn_to_abc(z, p, n; atol=1E-13) - a = exp(im*2*pi/3) - C = 1/sqrt(3)*[1 1 1; 1 a a^2; 1 a^2 a] - res = inv(C)*[z 0 0; 0 p 0; 0 0 n]*C - res = (abs.(res).>atol).*res - return res - end - pos_to_abc(p) = zpn_to_abc(p, p, p) - zbase = 1^2/(defaults["kvas"][1]/1E3) - transDict["rs"] = Array{Matrix{Float64}, 1}(undef, nrw) - transDict["gsh"] = Array{Matrix{Float64}, 1}(undef, nrw) - transDict["bsh"] = Array{Matrix{Float64}, 1}(undef, nrw) - for w in 1:nrw - zs_w_p = defaults["%rs"][w]/100*zbase - Zs_w = pos_to_abc(zs_w_p) - - if haskey(transformer, "rneut") || haskey(transformer, "xneut") - #TODO handle neutral impedance - # neutral impedance is ignored for now; all transformers are - # grounded (that is, those with a wye and zig-zag winding). - Memento.warn(_LOGGER, "The neutral impedance, (rneut and xneut properties), is ignored; the neutral (for wye and zig-zag windings) is connected directly to the ground.") - end - - transDict["rs"][w] = real.(Zs_w) - # shunt elements are added at second winding - if w==2 - ysh_w_p = (defaults["%noloadloss"]-im*defaults["%imag"])/100/zbase - Ysh_w = pos_to_abc(ysh_w_p) - transDict["gsh"][w] = real.(Ysh_w) - transDict["bsh"][w] = imag.(Ysh_w) - else - transDict["gsh"][w] = zeros(Float64, 3, 3) - transDict["bsh"][w] = zeros(Float64, 3, 3) + # mintap, maxtap + for (fr_key, to_key) in zip(["mintap", "maxtap"], ["tm_lb", "tm_ub"]) + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, fr_key) && _is_after_xfmrcode(dss_obj["prop_order"], fr_key)) + eng_obj[to_key] = fill(fill(defaults[fr_key], ncoils), nrw) end end - transDict["xs"] = Dict{String, Matrix{Float64}}() - Zsc = Dict{Tuple{Int,Int}, Complex}() - if nrw==2 - xs_map = Dict("xhl"=>(1,2)) - elseif nrw==3 - xs_map = Dict("xhl"=>(1,2), "xht"=>(1,3), "xlt"=>(2,3)) - end - for (k,v) in xs_map - Zsc[(v)] = im*defaults[k]/100*zbase - end - Zbr = _sc2br_impedance(Zsc) - for (k,zs_ij_p) in Zbr - Zs_ij = pos_to_abc(zs_ij_p) - transDict["xs"]["$(k[1])-$(k[2])"] = imag.(Zs_ij) + # %noloadloss, %imag + for (fr_key, to_key) in zip(["%noloadloss", "%imag"], ["noloadloss", "cmag"]) + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, fr_key) && _is_after_xfmrcode(dss_obj["prop_order"], fr_key)) + eng_obj[to_key] = defaults[fr_key] / 100 + end end - push!(pmd_data["transformer_comp"], transDict) - end -end - - -""" -Converts a set of short-circuit tests to an equivalent reactance network. -Reference: -R. C. Dugan, “A perspective on transformer modeling for distribution system analysis,” -in 2003 IEEE Power Engineering Society General Meeting (IEEE Cat. No.03CH37491), 2003, vol. 1, pp. 114-119 Vol. 1. -""" -function _sc2br_impedance(Zsc) - N = maximum([maximum(k) for k in keys(Zsc)]) - # check whether no keys are missing - # Zsc should contain tupples for upper triangle of NxN - for i in 1:N - for j in i+1:N - if !haskey(Zsc, (i,j)) - if haskey(Zsc, (j,i)) - # Zsc is symmetric; use value of lower triangle if defined - Zsc[(i,j)] = Zsc[(j,i)] - else - Memento.error(_LOGGER, "Short-circuit impedance between winding $i and $j is missing.") + # %rs + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, "%rs") && _is_after_xfmrcode(dss_obj["prop_order"], "%rs")) || all(haskey(dss_obj, k) && _is_after_xfmrcode(dss_obj["prop_order"], k) for k in ["%r", "%r_2", "%r_3"]) + eng_obj["rw"] = defaults["%rs"] / 100 + else + for (w, key_suffix) in enumerate(["", "_2", "_3"]) + if haskey(dss_obj, "%r$(key_suffix)") && _is_after_xfmrcode(dss_obj["prop_order"], "%r$(key_suffix)") + if !haskey(eng_obj, "rw") + eng_obj["rw"] = Vector{Any}(missing, nrw) + end + eng_obj["rw"][w] = defaults["%rs"][defaults["wdg$(key_suffix)"]] / 100 end end end - end - # make Zb - Zb = zeros(Complex{Float64}, N-1,N-1) - for i in 1:N-1 - Zb[i,i] = Zsc[(1,i+1)] - end - for i in 1:N-1 - for j in 1:i-1 - Zb[i,j] = (Zb[i,i]+Zb[j,j]-Zsc[(j+1,i+1)])/2 - Zb[j,i] = Zb[i,j] - end - end - # get Ybus - Y = pinv(Zb) - Y = [-Y*ones(N-1) Y] - Y = [-ones(1,N-1)*Y; Y] - # extract elements - Zbr = Dict() - for k in keys(Zsc) - Zbr[k] = (abs(Y[k...])==0) ? Inf : -1/Y[k...] - end - return Zbr -end - - -""" - _dss2pmd_reactor!(pmd_data, dss_data, import_all) - -Adds PowerModels-style branch components based on DSS reactors to `pmd_data` from `dss_data` -""" -function _dss2pmd_reactor!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "branch") - pmd_data["branch"] = [] - end - - if haskey(dss_data, "reactor") - Memento.warn(_LOGGER, "reactors as constant impedance elements is not yet supported, treating like line") - for reactor in dss_data["reactor"] - if haskey(reactor, "bus2") - _apply_like!(reactor, dss_data, "reactor") - defaults = _apply_ordered_properties(_create_reactor(reactor["bus1"], reactor["name"], reactor["bus2"]; _to_sym_keys(reactor)...), reactor) - - reactDict = Dict{String,Any}() - - nconductors = pmd_data["conductors"] - f_bus, nodes = _parse_busname(defaults["bus1"]) - t_bus = _parse_busname(defaults["bus2"])[1] - - reactDict["name"] = defaults["name"] - reactDict["f_bus"] = find_bus(f_bus, pmd_data) - reactDict["t_bus"] = find_bus(t_bus, pmd_data) - - reactDict["br_r"] = _parse_matrix(diagm(0 => fill(0.2, nconductors)), nodes, nconductors) - reactDict["br_x"] = _parse_matrix(zeros(nconductors, nconductors), nodes, nconductors) - - reactDict["g_fr"] = _parse_array(0.0, nodes, nconductors) - reactDict["g_to"] = _parse_array(0.0, nodes, nconductors) - reactDict["b_fr"] = _parse_array(0.0, nodes, nconductors) - reactDict["b_to"] = _parse_array(0.0, nodes, nconductors) - - for key in ["g_fr", "g_to", "b_fr", "b_to"] - reactDict[key] = LinearAlgebra.diagm(0=>reactDict[key]) + # loss model (converted to SI units, referred to secondary) + if nrw == 2 + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, "xhl") && _is_after_xfmrcode(dss_obj["prop_order"], "xhl")) + eng_obj["xsc"] = [defaults["xhl"]] / 100 + end + elseif nrw == 3 + for (w, key) in enumerate(["xhl", "xht", "xlt"]) + if isempty(defaults["xfmrcode"]) || (haskey(dss_obj, key) && _is_after_xfmrcode(dss_obj["prop_order"], key)) + if !haskey(eng_obj, "xsc") + eng_obj["xsc"] = Vector{Any}(missing, 3) + end + eng_obj["xsc"][w] = defaults[key] / 100 end - - reactDict["c_rating_a"] = _parse_array(defaults["normamps"], nodes, nconductors) - reactDict["c_rating_b"] = _parse_array(defaults["emergamps"], nodes, nconductors) - reactDict["c_rating_c"] = _parse_array(defaults["emergamps"], nodes, nconductors) - - reactDict["tap"] = _parse_array(1.0, nodes, nconductors, NaN) - reactDict["shift"] = _parse_array(0.0, nodes, nconductors) - - reactDict["br_status"] = convert(Int, defaults["enabled"]) - - reactDict["angmin"] = _parse_array(-60.0, nodes, nconductors, -60.0) - reactDict["angmax"] = _parse_array( 60.0, nodes, nconductors, 60.0) - - reactDict["transformer"] = true - - reactDict["index"] = length(pmd_data["branch"]) + 1 - - nodes = .+([_parse_busname(defaults[n])[2] for n in ["bus1", "bus2"]]...) - reactDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - reactDict["source_id"] = "reactor.$(defaults["name"])" - - used = [] - _PMs._import_remaining!(reactDict, defaults, import_all; exclude=used) - - push!(pmd_data["branch"], reactDict) end end - end -end - - -""" - _dss2pmd_pvsystem!(pmd_data, dss_data) - -Adds PowerModels-style pvsystems to `pmd_data` from `dss_data`. -""" -function _dss2pmd_pvsystem!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "pvsystem") - pmd_data["pvsystem"] = [] - end - - for pvsystem in get(dss_data, "pvsystem", []) - _apply_like!(pvsystem, dss_data, "pvsystem") - defaults = _apply_ordered_properties(_create_pvsystem(pvsystem["bus1"], pvsystem["name"]; _to_sym_keys(pvsystem)...), pvsystem) - - pvsystemDict = Dict{String,Any}() - - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) - - pvsystemDict["name"] = defaults["name"] - pvsystemDict["pv_bus"] = find_bus(name, pmd_data) - pvsystemDict["p"] = _parse_array(defaults["kw"] / 1e3, nodes, nconductors) - pvsystemDict["q"] = _parse_array(defaults["kvar"] / 1e3, nodes, nconductors) - pvsystemDict["status"] = convert(Int, defaults["enabled"]) - - pvsystemDict["index"] = length(pmd_data["pvsystem"]) + 1 - - pvsystemDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - pvsystemDict["source_id"] = "pvsystem.$(defaults["name"])" - - used = ["phases", "bus1", "name"] - _PMs._import_remaining!(pvsystemDict, defaults, import_all; exclude=used) - - push!(pmd_data["pvsystem"], pvsystemDict) - end -end - - -""" - _dss2pmd_storage!(pmd_data, dss_data, import_all) - -Adds PowerModels-style storage to `pmd_data` from `dss_data` -""" -function _dss2pmd_storage!(pmd_data::Dict, dss_data::Dict, import_all::Bool) - if !haskey(pmd_data, "storage") - pmd_data["storage"] = [] - end - - for storage in get(dss_data, "storage", []) - _apply_like!(storage, dss_data, "storage") - defaults = _apply_ordered_properties(_create_storage(storage["bus1"], storage["name"]; _to_sym_keys(storage)...), storage) - - storageDict = Dict{String,Any}() - - nconductors = pmd_data["conductors"] - name, nodes = _parse_busname(defaults["bus1"]) - - storageDict["name"] = defaults["name"] - storageDict["storage_bus"] = find_bus(name, pmd_data) - storageDict["energy"] = defaults["kwhstored"] / 1e3 - storageDict["energy_rating"] = defaults["kwhrated"] / 1e3 - storageDict["charge_rating"] = defaults["%charge"] * defaults["kwrated"] / 1e3 / 100.0 - storageDict["discharge_rating"] = defaults["%discharge"] * defaults["kwrated"] / 1e3 / 100.0 - storageDict["charge_efficiency"] = defaults["%effcharge"] / 100.0 - storageDict["discharge_efficiency"] = defaults["%effdischarge"] / 100.0 - storageDict["thermal_rating"] = _parse_array(defaults["kva"] / 1e3 / nconductors, nodes, nconductors) - storageDict["qmin"] = _parse_array(-defaults["kvar"] / 1e3 / nconductors, nodes, nconductors) - storageDict["qmax"] = _parse_array( defaults["kvar"] / 1e3 / nconductors, nodes, nconductors) - storageDict["r"] = _parse_array(defaults["%r"] / 100.0, nodes, nconductors) - storageDict["x"] = _parse_array(defaults["%x"] / 100.0, nodes, nconductors) - storageDict["p_loss"] = defaults["%idlingkw"] * defaults["kwrated"] / 1e3 - storageDict["q_loss"] = defaults["%idlingkvar"] * defaults["kvar"] / 1e3 - - storageDict["status"] = convert(Int, defaults["enabled"]) - - storageDict["ps"] = _parse_array(0.0, nodes, nconductors) - storageDict["qs"] = _parse_array(0.0, nodes, nconductors) - - storageDict["index"] = length(pmd_data["storage"]) + 1 - - storageDict["active_phases"] = [n for n in 1:nconductors if nodes[n] > 0] - storageDict["source_id"] = "storage.$(defaults["name"])" - - used = ["phases", "bus1", "name"] - _PMs._import_remaining!(storageDict, defaults, import_all; exclude=used) - - push!(pmd_data["storage"], storageDict) - end -end - -""" - _adjust_sourcegen_bounds!(pmd_data) - -Changes the bounds for the sourcebus generator by checking the emergamps of all -of the branches attached to the sourcebus and taking the sum of non-infinite -values. Defaults to Inf if all emergamps connected to sourcebus are also Inf. -This method was updated to include connected transformers as well. It know -has to occur after the call to InfrastructureModels.arrays_to_dicts, so the code -was adjusted to accomodate that. -""" -function _adjust_sourcegen_bounds!(pmd_data) - emergamps = Array{Float64,1}([0.0]) - sourcebus_n = find_bus(pmd_data["sourcebus"], pmd_data) - for (_,line) in pmd_data["branch"] - if (line["f_bus"] == sourcebus_n || line["t_bus"] == sourcebus_n) && !startswith(line["source_id"], "virtual") - append!(emergamps, get(line, "c_rating_b", get(line, "rate_b", missing))) + # tm_fix, tm_step don't appear in opendss + if isempty(defaults["xfmrcode"]) + eng_obj["tm_fix"] = fill(ones(Bool, ncoils), nrw) + eng_obj["tm_step"] = fill(fill(1/32, ncoils), nrw) end - end - - if haskey(pmd_data, "transformer") - for (_,trans) in pmd_data["transformer"] - if trans["f_bus"] == sourcebus_n || trans["t_bus"] == sourcebus_n - append!(emergamps, trans["rate_b"]) - end - end - end - bound = sum(emergamps) - - pmd_data["gen"]["1"]["pmin"] = fill(-bound, size(pmd_data["gen"]["1"]["pmin"])) - pmd_data["gen"]["1"]["pmax"] = fill( bound, size(pmd_data["gen"]["1"]["pmin"])) - pmd_data["gen"]["1"]["qmin"] = fill(-bound, size(pmd_data["gen"]["1"]["pmin"])) - pmd_data["gen"]["1"]["qmax"] = fill( bound, size(pmd_data["gen"]["1"]["pmin"])) - - # set current rating of vbranch modelling internal impedance - vbranch = [br for (id, br) in pmd_data["branch"] if br["name"]=="sourcebus_vbranch"][1] - vbranch["rate_a"] = fill(bound, length(vbranch["rate_a"])) -end - - -""" + # always required + eng_obj["bus"] = Array{String, 1}(undef, nrw) + eng_obj["connections"] = Array{Array{Int, 1}, 1}(undef, nrw) + eng_obj["polarity"] = fill(1, nrw) - function _decompose_transformers!(pmd_data) -Replaces complex transformers with a composition of ideal transformers and branches -which model losses. New buses (virtual, no physical meaning) are added. -""" -function _decompose_transformers!(pmd_data; import_all::Bool=false) - if !haskey(pmd_data, "transformer") - pmd_data["transformer"] = Dict{String, Any}() - end - ncnds = pmd_data["conductors"] - for (tr_id, trans) in pmd_data["transformer_comp"] - nrw = length(trans["buses"]) - endnode_id_w = Array{Int, 1}(undef, nrw) - bus_reduce = [] - branch_reduce = [] - # sum ratings for all windings to have internal worst-case ratings - rate_a = sum(trans["rate_a"]) - rate_b = sum(trans["rate_b"]) - rate_c = sum(trans["rate_c"]) - for w in 1:nrw - # 2-WINDING TRANSFORMER - trans_dict = Dict{String, Any}() - trans_dict["name"] = "tr$(tr_id)_w$(w)" - trans_dict["source_id"] = "$(trans["source_id"])_$(w)" - trans_dict["active_phases"] = [1, 2, 3] - _push_dict_ret_key!(pmd_data["transformer"], trans_dict) - # connection settings - trans_dict["config_fr"] = trans["config"][w] - trans_dict["config_to"] = Dict( - "type"=>"wye", - "polarity"=>'+', - "cnd"=>[1, 2, 3], - "grounded"=>true, - "vm_nom"=>1.0 - ) - # temporary fix for prop renaming - trans_dict["configuration"] = trans_dict["config_fr"]["type"] - trans_dict["f_connections"] = trans_dict["config_fr"]["cnd"] - trans_dict["t_connections"] = trans_dict["config_to"]["cnd"] - scale = trans_dict["configuration"]=="delta" ? sqrt(3) : 1.0 - trans_dict["tm_nom"] = trans_dict["config_fr"]["vm_nom"]*scale - - trans_dict["f_bus"] = trans["buses"][w] - # make virtual bus and mark it for reduction - vbus_tr = _create_vbus!(pmd_data, basekv=1.0, name="tr$(tr_id)_w$(w)_b1") - trans_dict["t_bus"] = vbus_tr["index"] - append!(bus_reduce, vbus_tr["index"]) - # convert to baseMVA, because this is not done per_unit now) - trans_dict["rate_a"] = trans["rate_a"][w]/pmd_data["baseMVA"] - trans_dict["rate_b"] = trans["rate_b"][w]/pmd_data["baseMVA"] - trans_dict["rate_c"] = trans["rate_c"][w]/pmd_data["baseMVA"] - # tap settings - trans_dict["tm"] = trans["tm"][w] - trans_dict["fixed"] = trans["fixed"][w] - trans_dict["tm_max"] = trans["tm_max"][w] - trans_dict["tm_min"] = trans["tm_min"][w] - trans_dict["tm_step"] = trans["tm_step"][w] - # WINDING SERIES RESISTANCE - # make virtual bus and mark it for reduction - vbus_br = _create_vbus!(pmd_data, basekv=1.0, name="tr$(tr_id)_w$(w)_b2") - append!(bus_reduce, vbus_br["index"]) - # make virtual branch and mark it for reduction - br = _create_vbranch!( - pmd_data, vbus_tr["index"], vbus_br["index"], - vbase=1.0, - br_r=trans["rs"][w], - g_fr=trans["gsh"][w], - b_fr=trans["bsh"][w], - rate_a=rate_a, - rate_b=rate_b, - rate_c=rate_c, - name="tr$(tr_id)_w$(w)_rs" - ) - append!(branch_reduce, br["index"]) - # save the trailing node for the reactance model - endnode_id_w[w] = vbus_br["index"] + if !haskey(dss_obj, "xfmrcode") + eng_obj["configuration"] = confs end - # now add the fully connected graph for reactances - for w in 1:nrw - for v in w+1:nrw - br = _create_vbranch!( - pmd_data, endnode_id_w[w], endnode_id_w[v], - vbase=1.0, - br_x=trans["xs"][string(w,"-",v)], - rate_a=rate_a, - rate_b=rate_b, - rate_c=rate_c, - name="tr$(tr_id)_xs_$(w)to$(v)" - ) - append!(branch_reduce, br["index"]) - end - end - _rm_redundant_pd_elements!(pmd_data, buses=string.(bus_reduce), branches=string.(branch_reduce)) - end - # remove the transformer_comp dict unless import_all is flagged - if !import_all - delete!(pmd_data, "transformer_comp") - end -end - - -""" -This function adds a new bus to the data model and returns its dictionary. -It is virtual in the sense that it does not correspond to a bus in the network, -but is part of the decomposition of the transformer. -""" -function _create_vbus!(pmd_data; vmin=0, vmax=Inf, basekv=pmd_data["basekv"], name="", source_id="") - vbus = Dict{String, Any}("bus_type"=>"1", "name"=>name) - vbus_id = _push_dict_ret_key!(pmd_data["bus"], vbus) - vbus["bus_i"] = vbus_id - vbus["source_id"] = source_id - ncnds = pmd_data["conductors"] - vbus["vm"] = ones(Float64, ncnds) - vbus["va"] = zeros(Float64, ncnds) - vbus["vmin"] = ones(Float64, ncnds)*vmin - vbus["vmax"] = ones(Float64, ncnds)*vmax - vbus["base_kv"] = basekv - return vbus -end - -""" -This function adds a new branch to the data model and returns its dictionary. -It is virtual in the sense that it does not correspond to a branch in the -network, but is part of the decomposition of the transformer. -""" -function _create_vbranch!(pmd_data, f_bus::Int, t_bus::Int; - name="", source_id="", active_phases=[1, 2, 3], - kwargs...) - ncnd = pmd_data["conductors"] - kwargs = Dict{Symbol,Any}(kwargs) - vbase = haskey(kwargs, :vbase) ? kwargs[:vbase] : pmd_data["basekv"] - # TODO assumes per_unit will be flagged - sbase = haskey(kwargs, :sbase) ? kwargs[:sbase] : pmd_data["baseMVA"] - zbase = vbase^2/sbase - # convert to LN vbase in instead of LL vbase - zbase *= (1/3) - vbranch = Dict{String, Any}("f_bus"=>f_bus, "t_bus"=>t_bus, "name"=>name) - vbranch["active_phases"] = active_phases - vbranch["source_id"] = "virtual_branch.$name" - for k in [:br_r, :br_x, :g_fr, :g_to, :b_fr, :b_to] - if !haskey(kwargs, k) - vbranch[string(k)] = zeros(ncnd, ncnd) - else - if k in [:br_r, :br_x] - vbranch[string(k)] = kwargs[k]./zbase - else - vbranch[string(k)] = kwargs[k].*zbase - end + # test if this transformer conforms with limitations + if nphases<3 && DELTA in confs + # Memento.error(_LOGGER, "Transformers with delta windings should have at least 3 phases to be well-defined: $id.") end - end - vbranch["angmin"] = -ones(ncnd)*60 - vbranch["angmax"] = ones(ncnd)*60 - vbranch["rate_a"] = get(kwargs, :rate_a, fill(Inf, length(active_phases))) - vbranch["shift"] = zeros(ncnd) - vbranch["tap"] = ones(ncnd) - vbranch["transformer"] = false - vbranch["switch"] = false - vbranch["br_status"] = 1 - for k in [:rate_a, :rate_b, :rate_c, :c_rating_a, :c_rating_b, :c_rating_c] - if haskey(kwargs, k) - vbranch[string(k)] = kwargs[k] + if nrw>3 + # All of the code is compatible with any number of windings, + # except for the parsing of the loss model (the pair-wise reactance) + Memento.error(_LOGGER, "For now parsing of xscarray is not supported. At most 3 windings are allowed, not $nrw.") end - end - _push_dict_ret_key!(pmd_data["branch"], vbranch) - return vbranch -end + for w in 1:nrw + eng_obj["bus"][w] = _parse_bus_id(defaults["buses"][w])[1] -"This function appends a component to a component dictionary of a pmd data model" -function _push_dict_ret_key!(dict::Dict{String, Any}, v::Dict{String, Any}; assume_no_gaps=false) - if isempty(dict) - k = 1 - elseif assume_no_gaps - k = length(keys(dict))+1 - else - k = maximum([parse(Int, x) for x in keys(dict)])+1 - end + conf = confs[w] + terminals_default = conf==WYE ? [1:nphases..., 0] : collect(1:nphases) - dict[string(k)] = v - v["index"] = k - return k -end + # append ground if connections one too short + eng_obj["connections"][w] = _get_conductors_ordered(defaults["buses"][w], default=terminals_default, pad_ground=(conf==WYE)) - -""" -This function removes zero impedance branches. Only for transformer loss model! -Branches with zero impedances are deleted, and one of the buses it connects. -For now, the implementation should only be used on the loss model of -transformers. When deleting buses, references at shunts, loads... should -be updated accordingly. In the current implementation, that is only done -for shunts. The other elements, such as loads, do not appear in the -transformer loss model. -""" -function _rm_redundant_pd_elements!(pmd_data; buses=keys(pmd_data["bus"]), branches=keys(pmd_data["branch"])) - # temporary dictionary for pi-model shunt elements - shunts_g = Dict{Int, Any}() - shunts_b = Dict{Int, Any}() - for (br_id, br) in pmd_data["branch"] - f_bus = br["f_bus"] - t_bus = br["t_bus"] - # if branch is flagged - if br_id in branches - # flags for convenience - is_selfloop = f_bus==t_bus # guaranteed to be reducable because branch is flagged - is_shorted = all(br["br_r"] .==0) && all(br["br_x"] .==0) - is_reducable = string(f_bus) in buses || string(t_bus) in buses - if is_shorted && is_reducable - # choose bus to remove - rm_bus = (f_bus in buses) ? f_bus : t_bus - kp_bus = (rm_bus==f_bus) ? t_bus : f_bus - elseif is_selfloop - kp_bus = t_bus - else - # nothing to do, go to next branch - continue - end - # move shunts to the bus that will be left - if !haskey(shunts_g, kp_bus) - shunts_g[kp_bus] = zeros(3, 3) - shunts_b[kp_bus] = zeros(3, 3) - end - - # bus shunts are diagonal, but branch shunts can b e full matrices - # ensure no data is lost by only keeping the diagonal - # this should not be the case for the current transformer parsing - for key in ["g_fr", "g_to", "b_fr", "b_to"] - @assert(all(br[key]-diagm(0=>diag(br[key])).==0)) - end - - shunts_g[kp_bus] .+= br["g_fr"] - shunts_g[kp_bus] .+= br["g_to"] - shunts_b[kp_bus] .+= br["b_fr"] - shunts_b[kp_bus] .+= br["b_to"] - # remove branch from pmd_data - delete!(pmd_data["branch"], string(br_id)) - if is_shorted && is_reducable - # remove bus from pmd_data - delete!(pmd_data["bus"], string(rm_bus)) - # replace bus references in branches - for (br_id, br) in pmd_data["branch"] - if br["f_bus"] == rm_bus - br["f_bus"] = kp_bus - end - if br["t_bus"] == rm_bus - br["t_bus"] = kp_bus - end - end - # replace bus references in transformers - for (_, tr) in pmd_data["transformer"] - if tr["f_bus"] == rm_bus - tr["f_bus"] = kp_bus - end - if tr["t_bus"] == rm_bus - tr["t_bus"] = kp_bus + if w>1 + prim_conf = confs[1] + if leadlag in ["ansi", "lag"] + if prim_conf==DELTA && conf==WYE + eng_obj["polarity"][w] = -1 + eng_obj["connections"][w] = [_barrel_roll(eng_obj["connections"][w][1:end-1], 1)..., eng_obj["connections"][w][end]] end - end - # replace bus references in gens, loads, shunts, storage - for comp_type in ["gen", "load", "shunt", "storage"] - for (_, comp) in pmd_data[comp_type] - if comp["$(comp_type)_bus"] == rm_bus - comp["$(comp_type)_bus"] = kp_bus - end - end - end - # fix new shunt buses - for shunts in [shunts_g, shunts_b] - for (bus, shunt) in shunts - if bus == rm_bus - shunts[kp_bus] .+= shunt - delete!(shunts, bus) - end + else # hence defaults["leadlag"] in ["euro", "lead"] + if prim_conf==WYE && conf==DELTA + eng_obj["polarity"][w] = -1 + eng_obj["connections"][w] = _barrel_roll(eng_obj["connections"][w], -1) end end - # TODO clean up other references to the removed bus - # like for example loads, generators, ... - # skipped for now, not relevant for transformer loss model - # + pvsystem - # ... end - elseif f_bus==t_bus - # this might occur if not all buses and branches are marked for removal - # a branch in parallel with a removed branch can turn into a self-loop - # and if that branch is not marked for removal, we end up here - Memento.error(_LOGGER, "Specified set of buses and branches leads to a self-loop.") - end - end - # create shunts for lumped pi-model shunts - for (bus, shunt_g) in shunts_g - shunt_b = shunts_b[bus] - if !all(shunt_g .==0) || !all(shunt_b .==0) - Memento.warn(_LOGGER, "Pi-model shunt was moved to a bus shunt. Off-diagonals will be discarded in the data model.") - # The shunts are part of PM, and will be scaled later on by make_per_unit, - # unlike PMD level components. The shunts here originate from PMD level - # components which were already scaled. Therefore, we have to undo the - # scaling here to prevent double scaling later on. - gs = shunt_g./1*pmd_data["baseMVA"] - bs = shunt_b./1*pmd_data["baseMVA"] - _add_shunt!(pmd_data, bus, gs=gs, bs=bs) - end - end -end - - -""" -Helper function to add a new shunt. The shunt element is always inserted at the -internal bus of the second winding in OpenDSS. If one of the branches of the -loss model connected to this bus, has zero impedance (for example, if XHL==0 -or XLT==0 or R[3]==0), then this bus might be removed by -_rm_redundant_pd_elements!, in which case a new shunt should be inserted at the -remaining bus of the removed branch. -""" -function _add_shunt!(pmd_data, bus; gs=zeros(3,3), bs=zeros(3,3), vbase_kv=1, sbase_mva=1) - # TODO check whether keys are consistent with the actual data model - shunt_dict = Dict{String, Any}("status"=>1, "shunt_bus"=>bus) - zbase = vbase_kv^2/sbase_mva - shunt_dict["gs"] = gs*zbase - shunt_dict["bs"] = bs*zbase - _push_dict_ret_key!(pmd_data["shunt"], shunt_dict, assume_no_gaps=false) -end - - -""" - function _adjust_base!(pmd_data) -Updates the voltage base at each bus, so that the ratios of the voltage bases -across a transformer are consistent with the ratios of voltage ratings of the -windings. Default behaviour is to start at the primary winding of the first -transformer, and to propagate from there. Branches are updated; the impedances -and addmittances are rescaled to be consistent with the new voltage bases. -""" -function _adjust_base!(pmd_data; start_at_first_tr_prim=false) - # initialize arrays etc. for the recursive part - edges_br = [(br["index"], br["f_bus"], br["t_bus"]) for (br_id_str, br) in pmd_data["branch"]] - edges_tr = [(tr["index"], tr["f_bus"], tr["t_bus"]) for (tr_id_str, tr) in pmd_data["transformer"]] - edges_br_visited = Dict{Int, Bool}([(edge[1], false) for edge in edges_br]) - edges_tr_visited = Dict{Int, Bool}([(edge[1], false) for edge in edges_tr]) - bus_ids = [parse(Int, x) for x in keys(pmd_data["bus"])] - nodes_visited = Dict{Int, Bool}([(bus_id, false) for bus_id in bus_ids]) - # retrieve old voltage bases from connected nodes before starting - br_basekv_old = Dict([(br["index"], pmd_data["bus"][string(br["f_bus"])]["base_kv"]) for (br_id_str, br) in pmd_data["branch"]]) - # start from the primary of the first transformer - if start_at_first_tr_prim && haskey(pmd_data, "transformer") && haskey(pmd_data["transformer"], "1") - trans_first = pmd_data["transformer"]["1"] - source = trans_first["f_bus"] - base_kv_new = trans_first["config_fr"]["vm_nom"] - else - # start at type 3 bus if present - buses_3 = [bus["index"] for (bus_id_str, bus) in pmd_data["bus"] if bus["bus_type"]==3] - buses_2 = [bus["index"] for (bus_id_str, bus) in pmd_data["bus"] if bus["bus_type"]==2] - if length(buses_3)>0 - source = buses_3[1] - elseif length(buses_2)>0 - source = buses_2[1] - else - Memento.warn(_LOGGER, "No bus of type 3 found; selecting random bus instead.") - source = parse(Int, rand(keys(pmd_data["bus"]))) + if 0 in eng_obj["connections"][w] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"][w]], eng_obj["connections"][w]) + end end - base_kv_new = pmd_data["basekv"] - end - _adjust_base_rec!(pmd_data, source, base_kv_new, nodes_visited, edges_br, edges_br_visited, edges_tr, edges_tr_visited, br_basekv_old) - if !all(values(nodes_visited)) - Memento.warn(_LOGGER, "The network contains buses which are not reachable from the start node for the change of voltage base.") - end -end - -""" -This is the recursive code that goes with _adjust_base!; _adjust_base! -initializes arrays and other data that is passed along in the calls to this -recursive function. For very large networks, this might have to be rewritten -to not rely on recursion. -""" -function _adjust_base_rec!(pmd_data, source::Int, base_kv_new::Float64, nodes_visited, edges_br, edges_br_visited, edges_tr, edges_tr_visited, br_basekv_old) - source_dict = pmd_data["bus"][string(source)] - base_kv_prev = source_dict["base_kv"] - if !(base_kv_prev≈base_kv_new) - # only possible when meshed; ensure consistency - if nodes_visited[source] - Memento.error(_LOGGER, "Transformer ratings lead to an inconsistent definition for the voltage base at bus $source.") - end - source_dict["base_kv"] = base_kv_new - # update the connected shunts with the new voltage base - source_shunts = [shunt for (sh_id_str, shunt) in pmd_data["shunt"] if shunt["shunt_bus"]==source] - for shunt in source_shunts - _adjust_base_shunt!(pmd_data, shunt["index"], base_kv_prev, base_kv_new) - end - source_name = haskey(source_dict, "name") ? source_dict["name"] : "" - if source_dict["bus_type"]==3 - #TODO is this the desired behaviour, keep SI units for type 3 bus? - source_dict["vm"] *= base_kv_prev/base_kv_new - source_dict["vmax"] *= base_kv_prev/base_kv_new - source_dict["vmin"] *= base_kv_prev/base_kv_new - Memento.info(_LOGGER, "Rescaling vm, vmin and vmax conform with new base_kv at type 3 bus $source($source_name): $base_kv_prev => $base_kv_new") - else - Memento.info(_LOGGER, "Resetting base_kv at bus $source($source_name): $base_kv_prev => $base_kv_new") - end - # TODO rescale vmin, vmax, vm - # what is the desired behaviour here? - # should the p.u. set point stay the same, or the set point in SI units? - end - nodes_visited[source] = true - # propagate through the connected branches - for (br_id, f_bus, t_bus) in [edge for edge in edges_br if !edges_br_visited[edge[1]]] - # check !edges_br_visited[edge[1]] again, might be visited by now - if (f_bus==source || t_bus==source) && !edges_br_visited[br_id] - # this edge will be visited - edges_br_visited[br_id] = true - source_new = (f_bus==source) ? t_bus : f_bus - # assume the branch was undimensionalised with the basekv of the node - # it is connected to; ideally this will be a property of the branch - # itself in the future to ensure consistency - base_kv_branch_prev = br_basekv_old[br_id] - if base_kv_branch_prev != base_kv_new - br = pmd_data["branch"]["$br_id"] - br_name = haskey(br, "name") ? br["name"] : "" - Memento.info(_LOGGER, "Rescaling impedances at branch $br_id($br_name), conform with change of voltage base: $base_kv_branch_prev => $base_kv_new") - _adjust_base_branch!(pmd_data, br_id, base_kv_branch_prev, base_kv_new) + for key in ["bank", "xfmrcode"] + if !isempty(defaults[key]) + eng_obj[key] = defaults[key] end - # follow the edge to the adjacent node and repeat - _adjust_base_rec!(pmd_data, source_new, base_kv_new, nodes_visited, edges_br, edges_br_visited, edges_tr, edges_tr_visited, br_basekv_old) end - end - # propogate through the connected transformers - for (tr_id, f_bus, t_bus) in [edge for edge in edges_tr if !edges_tr_visited[edge[1]]] - if f_bus==source || t_bus==source - # this edge is now being visited - edges_tr_visited[tr_id] = true - source_new = (f_bus==source) ? t_bus : f_bus - # scale the basekv across the transformer - trans = pmd_data["transformer"][string(tr_id)] - base_kv_new_tr = deepcopy(base_kv_new) - if source_new==t_bus - base_kv_new_tr *= (trans["config_to"]["vm_nom"]/trans["config_fr"]["vm_nom"]) - trans["tm_nom"] *= (base_kv_new_tr/base_kv_prev) - else - base_kv_new_tr *= (trans["config_fr"]["vm_nom"]/trans["config_to"]["vm_nom"]) - trans["tm_nom"] *= (base_kv_prev/base_kv_new_tr) - end - # follow the edge to the adjacent node and repeat - _adjust_base_rec!(pmd_data, source_new, base_kv_new_tr, nodes_visited, edges_br, edges_br_visited, edges_tr, edges_tr_visited, br_basekv_old) + + if !haskey(data_eng, "transformer") + data_eng["transformer"] = Dict{String,Any}() end - end -end + if import_all + _import_all!(eng_obj, dss_obj) + end -"Rescales the parameters of a branch to reflect a change in voltage base" -function _adjust_base_branch!(pmd_data, br_id::Int, base_kv_old::Float64, base_kv_new::Float64) - branch = pmd_data["branch"][string(br_id)] - zmult = (base_kv_old/base_kv_new)^2 - branch["br_r"] *= zmult - branch["br_x"] *= zmult - branch["g_fr"] *= 1/zmult - branch["b_fr"] *= 1/zmult - branch["g_to"] *= 1/zmult - branch["b_to"] *= 1/zmult + _add_eng_obj!(data_eng, "transformer", id, eng_obj) + end end -"Rescales the parameters of a shunt to reflect a change in voltage base" -function _adjust_base_shunt!(pmd_data, sh_id::Int, base_kv_old::Float64, base_kv_new::Float64) - shunt = pmd_data["shunt"][string(sh_id)] - zmult = (base_kv_old/base_kv_new)^2 - shunt["bs"] *= 1/zmult - shunt["gs"] *= 1/zmult -end +"Adds pvsystems to `data_eng` from `data_dss`" +function _dss2eng_pvsystem!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool, time_series::String="daily") + for (id, dss_obj) in get(data_dss, "pvsystem", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "pvsystem") + defaults = _apply_ordered_properties(_create_pvsystem(id; _to_kwargs(dss_obj)...), dss_obj) + # TODO pick parameters for solar objects -""" - _where_is_comp(data, comp_id) + nphases = defaults["phases"] -Finds existing component of id `comp_id` in array of `data` and returns index. -Assumes all components in `data` are unique. -""" -function _where_is_comp(data::Array, comp_id::AbstractString)::Int - for (i, e) in enumerate(data) - if e["name"] == comp_id - return i - end - end - return 0 -end + eng_obj = Dict{String,Any}( + "bus" => _parse_bus_id(defaults["bus1"])[1], + "configuration" => defaults["conn"], + "connections" => _get_conductors_ordered(defaults["bus1"], pad_ground=true, default=collect(1:defaults["phases"]+1)), + "pg" => fill(defaults["kva"] / nphases, nphases), + "qg" => fill(defaults["kvar"] / nphases, nphases), + "vg" => fill(defaults["kv"] / nphases, nphases), + "pg_lb" => fill(0.0, nphases), + "pg_ub" => fill(defaults["kva"], nphases), + "qg_lb" => fill(-defaults["kvar"], nphases), + "qg_ub" => fill( defaults["kvar"], nphases), + # "sm_ub" => fill(defaults["pmpp"] / nphases, nphases), # TODO add irradiance model + # "irradiance" => defaults["irradiance"], + # "temperature" => defaults["temperature"], + # "p-t_curve" => defaults["p-tcurve"], + # "efficiency_curve" => defaults["effcurve"], + # "rs" => diagm(0 => fill(defaults["%r"] / 100., nphases)), + # "xs" => diagm(0 => fill(defaults["%x"] / 100., nphases)), + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "pvsystem.$id", + ) + # if the ground is used directly, register load + if 0 in eng_obj["connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"]], eng_obj["connections"]) + end -""" - _correct_duplicate_components!(dss_data) + _build_time_series_reference!(eng_obj, dss_obj, data_dss, defaults, time_series, "pg", "qg") -Finds duplicate components in `dss_data` and merges up, meaning that older -data (lower indices) is always overwritten by newer data (higher indices). -""" -function _correct_duplicate_components!(dss_data::Dict) - out = Dict{String,Array}() - for (k, v) in dss_data - if !(k in ["options"]) - out[k] = [] - for comp in v - if isa(comp, Dict) - idx = _where_is_comp(out[k], comp["name"]) - if idx > 0 - merge!(out[k][idx], comp) - else - push!(out[k], comp) - end - end - end + if import_all + _import_all!(eng_obj, dss_obj) end + + _add_eng_obj!(data_eng, "solar", id, eng_obj) end - merge!(dss_data, out) end -"Creates a virtual branch between the `virtual_sourcebus` and `sourcebus` with the impedance given by `circuit`" -function _create_sourcebus_vbranch!(pmd_data::Dict, circuit::Dict) - sourcebus = find_bus(pmd_data["sourcebus"], pmd_data) - vsourcebus = find_bus("virtual_sourcebus", pmd_data) - - br_r = circuit["rmatrix"] - br_x = circuit["xmatrix"] - - vbranch = _create_vbranch!(pmd_data, sourcebus, vsourcebus; name="sourcebus_vbranch", br_r=br_r, br_x=br_x) -end +"Adds storage to `data_eng` from `data_dss`" +function _dss2eng_storage!(data_eng::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, import_all::Bool, time_series::String="daily") + for (id, dss_obj) in get(data_dss, "storage", Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, "storage") + defaults = _apply_ordered_properties(_create_storage(id; _to_kwargs(dss_obj)...), dss_obj) + nphases = defaults["phases"] -"Combines transformers with 'bank' keyword into a single transformer" -function _bank_transformers!(pmd_data::Dict) - transformer_names = Dict(trans["name"] => n for (n, trans) in get(pmd_data, "transformer_comp", Dict())) - bankable_transformers = [trans for trans in values(get(pmd_data, "transformer_comp", Dict())) if haskey(trans, "bank")] - banked_transformers = Dict() - for transformer in bankable_transformers - bank = transformer["bank"] - - if !(bank in keys(banked_transformers)) - n = length(pmd_data["transformer_comp"])+length(banked_transformers)+1 - - banked_transformers[bank] = deepcopy(transformer) - banked_transformers[bank]["name"] = deepcopy(transformer["bank"]) - banked_transformers[bank]["source_id"] = "transformer.$(transformer["bank"])" - banked_transformers[bank]["index"] = n - # set impedances / admittances to zero; only the specified phases should be non-zero - for key in ["rs", "xs", "bsh", "gsh"] - inds = key=="xs" ? keys(banked_transformers[bank][key]) : 1:length(banked_transformers[bank][key]) - for w in inds - banked_transformers[bank][key][w] *= 0 - end - end - delete!(banked_transformers[bank], "bank") - end + eng_obj = Dict{String,Any}( + "bus" => _parse_bus_id(defaults["bus1"])[1], + "connections" => _get_conductors_ordered(defaults["bus1"], check_length=false), + "configuration" => WYE, + "energy" => defaults["kwhstored"], + "energy_ub" => defaults["kwrated"], + "charge_ub" => defaults["%charge"] / 100.0 * defaults["kwrated"], + "discharge_ub" => defaults["%discharge"] / 100.0 * defaults["kwrated"], + "cm_ub" => fill(defaults["kva"] / nphases, nphases), + "charge_efficiency" => defaults["%effcharge"], + "discharge_efficiency" => defaults["%effdischarge"], + "qs_lb" => -fill(defaults["kvar"] / nphases, nphases), + "qs_ub" => fill(defaults["kvar"] / nphases, nphases), + "rs" => fill(defaults["%r"] / nphases / 100.0, nphases), + "xs" => fill(defaults["%x"] / nphases / 100.0, nphases), + "pex" => defaults["%idlingkw"] .* defaults["kwrated"], + "qex" => defaults["%idlingkvar"] .* defaults["kvar"], + "status" => defaults["enabled"] ? ENABLED : DISABLED, + "source_id" => "storage.$id", + ) - banked_transformer = banked_transformers[bank] - for phase in transformer["active_phases"] - push!(banked_transformer["active_phases"], phase) - for (k, v) in banked_transformer - if isa(v, Vector) && eltype(v) <: Vector - # most properties are arrays (indexed over the windings) - for w in 1:length(v) - banked_transformer[k][w][phase] = deepcopy(transformer[k][w][phase]) - end - elseif isa(v, Vector) && eltype(v) <: Matrix - # most properties are arrays (indexed over the windings) - for w in 1:length(v) - banked_transformer[k][w][phase, :] .= deepcopy(transformer[k][w][phase, :]) - end - elseif k=="xs" - # xs is a Dictionary indexed over pairs of windings - for w in keys(v) - banked_transformer[k][w][phase, :] .= deepcopy(transformer[k][w][phase, :]) - end - end - end + # if the ground is used directly, register load + if 0 in eng_obj["connections"] + _register_awaiting_ground!(data_eng["bus"][eng_obj["bus"]], eng_obj["connections"]) end - end - - for transformer in bankable_transformers - delete!(pmd_data["transformer_comp"], transformer_names[transformer["name"]]) - end - for transformer in values(banked_transformers) - pmd_data["transformer_comp"]["$(transformer["index"])"] = deepcopy(transformer) - end -end + _build_time_series_reference!(eng_obj, dss_obj, data_dss, defaults, time_series, "ps", "qs") + if import_all + _import_all!(eng_obj, dss_obj) + end -"Parses buscoords [lon,lat] (if present) into their respective buses" -function _dss2pmd_buscoords!(pmd_data::Dict, dss_data::Dict) - for bc in get(dss_data, "buscoords", []) - bus = pmd_data["bus"]["$(find_bus(bc["name"], pmd_data))"] - bus["lon"] = bc["x"] - bus["lat"] = bc["y"] + _add_eng_obj!(data_eng, "storage", id, eng_obj) end end -""" - parse_options(options) - -Parses options defined with the `set` command in OpenDSS. -""" -function parse_options(options) - out = Dict{String,Any}() - if haskey(options, "voltagebases") - out["voltagebases"] = _parse_array(Float64, options["voltagebases"]) - end +"Parses a DSS file into a PowerModels usable format" +function parse_opendss(io::IOStream; + import_all::Bool=false, + bank_transformers::Bool=true, + time_series::String="daily" + )::Dict{String,Any} - if !haskey(options, "defaultbasefreq") - Memento.warn(_LOGGER, "defaultbasefreq is not defined, default for circuit set to 60 Hz") - out["defaultbasefreq"] = 60.0 - else - out["defaultbasefreq"] = parse(Float64, options["defaultbasefreq"]) - end + data_dss = parse_dss(io) - return out + return parse_opendss(data_dss; + import_all=import_all, + bank_transformers=bank_transformers, + time_series=time_series + ) end "Parses a Dict resulting from the parsing of a DSS file into a PowerModels usable format" -function parse_opendss(dss_data::Dict; import_all::Bool=false, vmin::Float64=0.9, vmax::Float64=1.1, bank_transformers::Bool=true)::Dict - pmd_data = Dict{String,Any}() - - _correct_duplicate_components!(dss_data) +function parse_opendss(data_dss::Dict{String,<:Any}; + import_all::Bool=false, + bank_transformers::Bool=true, + time_series::String="daily" + )::Dict{String,Any} - parse_dss_with_dtypes!(dss_data, ["line", "linecode", "load", "generator", "capacitor", - "reactor", "circuit", "transformer", "pvsystem", - "storage"]) + data_eng = Dict{String,Any}( + "data_model" => ENGINEERING, + "settings" => Dict{String,Any}(), + ) - if haskey(dss_data, "options") - condensed_opts = [Dict{String,Any}()] - for opt in dss_data["options"] - merge!(condensed_opts[1], opt) - end - dss_data["options"] = condensed_opts + if import_all + data_eng["dss_options"] = data_dss["options"] end - merge!(pmd_data, parse_options(get(dss_data, "options", [Dict{String,Any}()])[1])) + if haskey(data_dss, "vsource") && haskey(data_dss["vsource"], "source") && haskey(data_dss, "circuit") + defaults = _create_vsource("source"; _to_kwargs(data_dss["vsource"]["source"])...) + source_bus = _parse_bus_id(defaults["bus1"])[1] - pmd_data["per_unit"] = false - pmd_data["source_type"] = "dss" - pmd_data["source_version"] = string(VersionNumber("0")) + data_eng["name"] = data_dss["circuit"] - if haskey(dss_data, "circuit") - circuit = dss_data["circuit"][1] - defaults = _create_vsource(get(circuit, "bus1", "sourcebus"), circuit["name"]; _to_sym_keys(circuit)...) + data_eng["settings"]["voltage_scale_factor"] = 1e3 + data_eng["settings"]["power_scale_factor"] = 1e3 + data_eng["settings"]["vbases_default"] = Dict(source_bus=>defaults["basekv"] / sqrt(3)) + data_eng["settings"]["sbase_default"] = defaults["basemva"] * 1e3 + data_eng["settings"]["base_frequency"] = get(get(data_dss, "options", Dict{String,Any}()), "defaultbasefreq", 60.0) - pmd_data["name"] = defaults["name"] - pmd_data["basekv"] = defaults["basekv"] - pmd_data["baseMVA"] = defaults["basemva"] - pmd_data["basefreq"] = pop!(pmd_data, "defaultbasefreq") - pmd_data["pu"] = defaults["pu"] - pmd_data["conductors"] = defaults["phases"] - pmd_data["sourcebus"] = defaults["bus1"] + # collect turns the Set into Array, making it serializable + data_eng["files"] = collect(data_dss["filename"]) else Memento.error(_LOGGER, "Circuit not defined, not a valid circuit!") end - _dss2pmd_bus!(pmd_data, dss_data, import_all, vmin, vmax) - _dss2pmd_load!(pmd_data, dss_data, import_all) - _dss2pmd_shunt!(pmd_data, dss_data, import_all) - _dss2pmd_branch!(pmd_data, dss_data, import_all) - _dss2pmd_transformer!(pmd_data, dss_data, import_all) - _dss2pmd_reactor!(pmd_data, dss_data, import_all) - _dss2pmd_gen!(pmd_data, dss_data, import_all) - _dss2pmd_pvsystem!(pmd_data, dss_data, import_all) - _dss2pmd_storage!(pmd_data, dss_data, import_all) + _dss2eng_bus!(data_eng, data_dss, import_all) + _dss2eng_buscoords!(data_eng, data_dss) - pmd_data["dcline"] = [] - pmd_data["switch"] = [] + _dss2eng_linecode!(data_eng, data_dss, import_all) + _dss2eng_line!(data_eng, data_dss, import_all) - InfrastructureModels.arrays_to_dicts!(pmd_data) + _dss2eng_xfmrcode!(data_eng, data_dss, import_all) + _dss2eng_transformer!(data_eng, data_dss, import_all) - _dss2pmd_buscoords!(pmd_data, dss_data) + _dss2eng_capacitor!(data_eng, data_dss, import_all) + _dss2eng_reactor!(data_eng, data_dss, import_all) - if bank_transformers - _bank_transformers!(pmd_data) - end + _dss2eng_loadshape!(data_eng, data_dss, import_all) + _dss2eng_load!(data_eng, data_dss, import_all, time_series) - for optional in ["dcline", "load", "shunt", "storage", "pvsystem", "branch"] - if length(pmd_data[optional]) == 0 - pmd_data[optional] = Dict{String,Any}() - end - end + _dss2eng_vsource!(data_eng, data_dss, import_all, time_series) + _dss2eng_generator!(data_eng, data_dss, import_all, time_series) + _dss2eng_pvsystem!(data_eng, data_dss, import_all, time_series) + _dss2eng_storage!(data_eng, data_dss, import_all, time_series) - _create_sourcebus_vbranch!(pmd_data, defaults) + _discover_terminals!(data_eng) - if haskey(pmd_data, "transformer_comp") - # this has to be done before calling _adjust_sourcegen_bounds! - _decompose_transformers!(pmd_data; import_all=import_all) - _adjust_base!(pmd_data) - else - pmd_data["transformer"] = Dict{String, Any}() + if bank_transformers + _bank_transformers!(data_eng) end - _adjust_sourcegen_bounds!(pmd_data) - - pmd_data["files"] = dss_data["filename"] - - return pmd_data -end - - -"Parses a DSS file into a PowerModels usable format" -function parse_opendss(io::IOStream; import_all::Bool=false, vmin::Float64=0.9, vmax::Float64=1.1, bank_transformers::Bool=true)::Dict - dss_data = parse_dss(io) - - return parse_opendss(dss_data; import_all=import_all) + return data_eng end diff --git a/src/io/utils.jl b/src/io/utils.jl new file mode 100644 index 000000000..0c2223740 --- /dev/null +++ b/src/io/utils.jl @@ -0,0 +1,898 @@ +import Base.Iterators: flatten + +"all node types that can help define buses" +const _dss_node_objects = Vector{String}([ + "isource", "load", "generator", "indmach012", "storage", "pvsystem" +]) + +"all edge types that can help define buses" +const _dss_edge_objects = Vector{String}([ + "vsource", "fault", "capacitor", "line", "reactor", "transformer", "gictransformer", "gicline" +]) + +"all data holding objects" +const _dss_data_objects = Vector{String}([ + "options", "xfmrcode", "linecode", "loadshape", "xycurve", "linegeometry", + "linespacing", "growthshape", "tcc_curve", "cndata", "tsdata", "wiredata" +]) + +"all objects that define controls" +const _dss_control_objects = Vector{String}([ + "capcontrol", "regcontrol", "swtcontrol", "relay", "recloser", "fuse" +]) + +"all objects that provide montoring" +const _dss_monitor_objects = Vector{String}([ + "energymeter", "monitor" +]) + +"components currently supported for automatic data type parsing" +const _dss_supported_components = Vector{String}([ + "line", "linecode", "load", "generator", "capacitor", "reactor", + "transformer", "pvsystem", "storage", "loadshape", "options", + "xfmrcode", "vsource", "xycurve" +]) + +"two number operators for reverse polish notation" +_double_operators = Dict{String,Any}( + "+" => +, + "-" => -, + "*" => *, + "/" => /, + "^" => ^, + "atan2" => (x, y) -> rad2deg(atan(y, x)) +) + +"single number operators in reverse polish notation" +_single_operators = Dict{String,Any}( + "sqr" => x -> x * x, + "sqrt" => sqrt, + "inv" => inv, + "ln" => log, + "exp" => exp, + "log10" => log10, + "sin" => sind, + "cos" => cosd, + "tan" => tand, + "asin" => asind, + "acos" => acosd, + "atan" => atand +) + +"different acceptable delimiters for arrays" +const _array_delimiters = Vector{Char}(['\"', '\'', '[', '{', '(', ']', '}', ')']) + +"properties that should be excluded from being overwritten during the application of `like`" +const _like_exclusions = Dict{String,Vector{Regex}}( + "all" => Vector{Regex}([r"name", r"enabled"]), + "line" => [r"switch"], +) + +"Regexes for determining data types" +const _dtype_regex = Dict{Regex, Type}( + r"^[+-]{0,1}\d*\.{0,1}\d*[eE]{0,1}[+-]{0,1}\d*[+-]\d*\.{0,1}\d*[eE]{0,1}[+-]{0,1}\d*[ij]$" => ComplexF64, + r"^[+-]{0,1}\d*\.{0,1}\d*[eE]{0,1}[+-]{0,1}\d*$" => Float64, + r"^\d+$" => Int, +) + + +"dss to pmd load model" +const _dss2pmd_load_model = Dict{Int,LoadModel}( + 1 => POWER, + 2 => IMPEDANCE, + 5 => CURRENT, + 4 => EXPONENTIAL, # TODO add official support for exponential load model + 8 => ZIP, # TODO add official support for ZIP load model +) + + +"detects if `expr` is Reverse Polish Notation expression" +function _isa_rpn(expr::AbstractString)::Bool + expr = split(strip(expr, _array_delimiters)) + op_keys = keys(merge(_double_operators, _single_operators)) + for item in expr + if item in op_keys + return true + end + end + return false +end + + +"parses Reverse Polish Notation `expr`" +function _parse_rpn(expr::AbstractString, dtype::Type=Float64) + clean_expr = strip(expr, _array_delimiters) + + if occursin("rollup", clean_expr) || occursin("rolldn", clean_expr) || occursin("swap", clean_expr) + Memento.warn(_LOGGER, "_parse_rpn does not support \"rollup\", \"rolldn\", or \"swap\", leaving as String") + return expr + end + + stack = [] + split_expr = occursin(",", clean_expr) ? split(clean_expr, ',') : split(clean_expr) + + for item in split_expr + try + if haskey(_double_operators, item) + b = pop!(stack) + a = pop!(stack) + push!(stack, _double_operators[item](a, b)) + elseif haskey(_single_operators, item) + push!(stack, _single_operators[item](pop!(stack))) + else + if item == "pi" + push!(stack, pi) + else + push!(stack, parse(dtype, item)) + end + end + catch error + if isa(error, ArgumentError) + Memento.warn(_LOGGER, "\"$expr\" is not valid Reverse Polish Notation, leaving as String") + return expr + else + throw(error) + end + end + end + if length(stack) > 1 + Memento.warn(_LOGGER, "\"$expr\" is not valid Reverse Polish Notation, leaving as String") + return expr + else + return stack[1] + end +end + + +"checks is a string is a connection by checking the values" +function _isa_conn(expr::AbstractString)::Bool + if expr in ["wye", "y", "ln", "delta", "ll"] + return true + else + return false + end +end + + +"parses connection \"conn\" specification reducing to wye or delta" +function _parse_conn(conn::AbstractString)::ConnConfig + if conn in ["wye", "y", "ln"] + return WYE + elseif conn in ["delta", "ll"] + return DELTA + else + Memento.warn(_LOGGER, "Unsupported connection $conn, defaulting to WYE") + return WYE + end +end + + +"checks if `data` is an opendss-style matrix string" +function _isa_matrix(data::AbstractString)::Bool + if occursin("|", data) + return true + else + return false + end +end + + +""" +Parses a OpenDSS style triangular matrix string `data` into a two dimensional +array of type `dtype`. Matrix strings are capped by either parenthesis or +brackets, rows are separated by "|", and columns are separated by spaces. +""" +function _parse_matrix(dtype::Type, data::AbstractString)::Matrix{dtype} + rows = [] + for line in split(strip(data, _array_delimiters), '|') + cols = [] + for item in split(line) + push!(cols, parse(dtype, item)) + end + push!(rows, cols) + end + + nphases = maximum([length(row) for row in rows]) + + if dtype == AbstractString || dtype == String + matrix = fill("", nphases, nphases) + elseif dtype == Char + matrix = fill(' ', nphases, nphases) + else + matrix = zeros(dtype, nphases, nphases) + end + + if length(rows) == 1 + for i in 1:nphases + matrix[i, i] = rows[1][1] + end + elseif all([length(row) for row in rows] .== [i for i in 1:nphases]) + for (i, row) in enumerate(rows) + for (j, col) in enumerate(row) + matrix[i, j] = matrix[j, i] = col + end + end + elseif all([length(row) for row in rows] .== nphases) + for (i, row) in enumerate(rows) + for (j, col) in enumerate(row) + matrix[i, j] = col + end + end + end + + return matrix +end + + +"checks if `data` is an opendss-style array string" +function _isa_array(data::AbstractString)::Bool + clean_data = strip(data) + if !occursin("|", clean_data) + if occursin(",", clean_data) || + (startswith(clean_data, "[") && endswith(clean_data, "]")) || + (startswith(clean_data, "\"") && endswith(clean_data, "\"")) || + (startswith(clean_data, "\'") && endswith(clean_data, "\'")) || + (startswith(clean_data, "(") && endswith(clean_data, ")")) || + (startswith(clean_data, "{") && endswith(clean_data, "}")) + return true + else + return false + end + else + return false + end +end + + +""" +Parses a OpenDSS style array string `data` into a one dimensional array of type +`dtype`. Array strings are capped by either brackets, single quotes, or double +quotes, and elements are separated by spaces. +""" +function _parse_array(dtype::Type, data::AbstractString)::Vector{dtype} + if occursin(",", data) + split_char = ',' + else + split_char = ' ' + end + + if _isa_rpn(data) + matches = collect((m.match for m = eachmatch(Regex(string("[",join(_array_delimiters, '\\'),"]")), data, overlap=false))) + if length(matches) == 2 + if dtype == String + return data + else + return _parse_rpn(data, dtype) + end + + else + elements = _parse_properties(data[2:end-1]) + end + else + for delim in _array_delimiters + data = replace(data, delim => "") + end + elements = split(data, split_char) + elements = [strip(el) for el in elements if strip(el) != ""] + end + + if all(_isa_conn(el) for el in elements) + array = Vector{ConnConfig}([]) + for el in elements + a = _parse_conn(el) + push!(array, a) + end + elseif dtype == String || dtype == AbstractString || dtype == Char + array = Vector{String}([]) + for el in elements + push!(array, el) + end + else + array = Vector{dtype}(undef, length(elements)) + for (i, el) in enumerate(elements) + if _isa_rpn(data) + array[i] = _parse_rpn(el, dtype) + else + array[i] = parse(dtype, el) + end + end + end + + return array +end + + +"Combines transformers with 'bank' keyword into a single transformer" +function _bank_transformers!(data_eng::Dict{String,<:Any}) + if haskey(data_eng, "transformer") + bankable_transformers = Dict() + for (id, tr) in data_eng["transformer"] + if haskey(tr, "bank") + bank = tr["bank"] + if !haskey(bankable_transformers, bank) + bankable_transformers[bank] = ([], []) + end + push!(bankable_transformers[bank][1], id) + push!(bankable_transformers[bank][2], deepcopy(tr)) + end + end + + for (bank, (ids, trs)) in bankable_transformers + for tr in trs + _apply_xfmrcode!(tr, data_eng) + end + # across-phase properties should be the same to be eligible for banking + props = ["bus", "noloadloss", "xsc", "rw", "cmag", "vm_nom", "sm_nom", "polarity", "configuration"] + btrans = Dict{String, Any}(prop=>trs[1][prop] for prop in props) + if !all(tr[prop]==btrans[prop] for tr in trs, prop in props) + Memento.warn(_LOGGER, "Not all across-phase properties match among transfomers identified by bank='$bank', aborting attempt to bank") + continue + end + nrw = length(btrans["bus"]) + + # only attempt to bank wye-connected transformers + if !all(all(conf==WYE for conf in tr["configuration"]) for tr in trs) + Memento.warn(_LOGGER, "Not all configurations 'wye' on transformers identified by bank='$bank', aborting attempt to bank") + continue + end + neutrals = [conns[end] for conns in trs[1]["connections"]] + # ensure all windings have the same neutral + if !all(all(conns[end]==neutrals[w] for (w, conns) in enumerate(tr["connections"])) for tr in trs) + Memento.warn(_LOGGER, "Not all neutral phases match on transfomers identified by bank='$bank', aborting attempt to bank") + continue + end + + # this will merge the per-phase properties in such a way that the + # f_connections will be sorted from small to large + f_phases_loc = Dict(hcat([[(c,(i,p)) for (p, c) in enumerate(tr["connections"][1][1:end-1])] for (i, tr) in enumerate(trs)]...)) + locs = [f_phases_loc[x] for x in sort(collect(keys(f_phases_loc)))] + props_merge = ["connections", "tm_set", "tm_ub", "tm_lb", "tm_step", "tm_fix"] + for prop in props_merge + btrans[prop] = [[trs[i][prop][w][p] for (i,p) in locs] for w in 1:nrw] + + # for the connections, also prefix the neutral per winding + if prop=="connections" + for w in 1:nrw + push!(btrans[prop][w], neutrals[w]) + end + end + end + + btrans["source_id"] = "transformer.$bank" + + # edit the transformer dict + for id in ids + delete!(data_eng["transformer"], id) + end + data_eng["transformer"][bank] = btrans + end + end +end + + +"discovers all terminals in the network" +function _discover_terminals!(data_eng::Dict{String,<:Any}) + terminals = Dict{String, Set{Int}}([(name, Set{Int}()) for (name,bus) in data_eng["bus"]]) + + if haskey(data_eng, "line") + for (_,eng_obj) in data_eng["line"] + # ignore 0 terminal + push!(terminals[eng_obj["f_bus"]], setdiff(eng_obj["f_connections"], [0])...) + push!(terminals[eng_obj["t_bus"]], setdiff(eng_obj["t_connections"], [0])...) + end + end + + if haskey(data_eng, "switch") + for (_,eng_obj) in data_eng["switch"] + # ignore 0 terminal + push!(terminals[eng_obj["f_bus"]], setdiff(eng_obj["f_connections"], [0])...) + push!(terminals[eng_obj["t_bus"]], setdiff(eng_obj["t_connections"], [0])...) + end + end + + if haskey(data_eng, "transformer") + for (_,tr) in data_eng["transformer"] + for w in 1:length(tr["bus"]) + # ignore 0 terminal + push!(terminals[tr["bus"][w]], setdiff(tr["connections"][w], [0])...) + end + end + end + + for comp_type in [x for x in ["voltage_source", "load", "gen"] if haskey(data_eng, x)] + for comp in values(data_eng[comp_type]) + push!(terminals[comp["bus"]], setdiff(comp["connections"], [0])...) + end + end + + for (id, bus) in data_eng["bus"] + data_eng["bus"][id]["terminals"] = sort(collect(terminals[id])) + end + + for (id,bus) in data_eng["bus"] + if haskey(bus, "awaiting_ground") + neutral = !(4 in bus["terminals"]) ? 4 : maximum(bus["terminals"])+1 + push!(bus["terminals"], neutral) + + bus["grounded"] = [neutral] + bus["rg"] = [0.0] + bus["xg"] = [0.0] + for i in 1:length(bus["awaiting_ground"]) + bus["awaiting_ground"][i][bus["awaiting_ground"][i].==0] .= neutral + end + + delete!(bus, "awaiting_ground") + end + end +end + + +"discovers all phases and neutrals in the network" +function _discover_phases_neutral!(data_eng::Dict{String,<:Any}) + bus_neutral = _find_neutrals(data_eng) + for (id, bus) in data_eng["bus"] + terminals = bus["terminals"] + if haskey(bus_neutral, id) + bus["neutral"] = bus_neutral[id] + phases = setdiff(terminals, bus["neutral"]) + else + phases = terminals + end + @assert(length(phases)<=3, "At bus $id, we found $(length(phases))>3 phases; aborting discovery, requires manual inspection.") + end +end + + +"Discovers all neutrals in the network" +function _find_neutrals(data_eng::Dict{String,<:Any}) + vertices = [(id, t) for (id, bus) in data_eng["bus"] for t in bus["terminals"]] + neutrals = [] + edges = Set([((eng_obj["f_bus"], eng_obj["f_connections"][c]),(eng_obj["t_bus"], eng_obj["t_connections"][c])) for (id, eng_obj) in data_eng["line"] for c in 1:length(eng_obj["f_connections"])]) + + bus_neutrals = [(id,bus["neutral"]) for (id,bus) in data_eng["bus"] if haskey(bus, "neutral")] + trans_neutrals = [] + for (_, tr) in data_eng["transformer"] + for w in 1:length(tr["connections"]) + if tr["configuration"][w] == WYE + push!(trans_neutrals, (tr["bus"][w], tr["connections"][w][end])) + end + end + end + load_neutrals = [(eng_obj["bus"],eng_obj["connections"][end]) for (_,eng_obj) in get(data_eng, "load", Dict{String,Any}()) if eng_obj["configuration"]==WYE] + neutrals = Set(vcat(bus_neutrals, trans_neutrals, load_neutrals)) + neutrals = Set([(bus,t) for (bus,t) in neutrals if t!=0]) + stack = deepcopy(neutrals) + while !isempty(stack) + vertex = pop!(stack) + candidates_t = [((f,t), t) for (f,t) in edges if f==vertex] + candidates_f = [((f,t), f) for (f,t) in edges if t==vertex] + for (edge,next) in [candidates_t..., candidates_f...] + delete!(edges, edge) + push!(stack, next) + push!(neutrals, next) + end + end + bus_neutral = Dict{String, Int}() + for (bus,t) in neutrals + bus_neutral[bus] = t + end + return bus_neutral +end + + +"Returns an ordered list of defined conductors. If ground=false, will omit any `0`" +function _get_conductors_ordered(busname::AbstractString; default::Vector{Int}=Vector{Int}([]), check_length::Bool=true, pad_ground::Bool=false)::Vector{Int} + parts = split(busname, '.'; limit=2) + ret = Vector{Int}([]) + if length(parts)==2 + conds_str = split(parts[2], '.') + ret = [parse(Int, i) for i in conds_str] + else + return default + end + + if pad_ground && length(ret)==length(default)-1 + ret = [ret..., 0] + end + + if check_length && length(default)!=length(ret) + # TODO + Memento.info(_LOGGER, "An inconsistent number of nodes was specified on $(parts[1]); |$(parts[2])|!=$(length(default)).") + end + return ret +end + + +"creates a `dss` dict inside `object` that imports all items in `prop_order` from `dss_obj`" +function _import_all!(object::Dict{String,<:Any}, dss_obj::Dict{String,<:Any}) + object["dss"] = Dict{String,Any}((key, dss_obj[key]) for key in dss_obj["prop_order"]) +end + + +""" +Given a vector and a list of elements to find, this method will return a list +of the positions of the elements in that vector. +""" +function _get_idxs(vec::Vector{<:Any}, els::Vector{<:Any})::Vector{Int} + ret = Array{Int, 1}(undef, length(els)) + for (i,f) in enumerate(els) + for (j,l) in enumerate(vec) + if f==l + ret[i] = j + end + end + end + return ret +end + + +"Discovers all of the buses (not separately defined in OpenDSS), from \"lines\"" +function _discover_buses(data_dss::Dict{String,<:Any})::Set + buses = Set([]) + for obj_type in _dss_node_objects + for (name, dss_obj) in get(data_dss, obj_type, Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, obj_type) + push!(buses, split(dss_obj["bus1"], '.'; limit=2)[1]) + end + end + + for obj_type in _dss_edge_objects + for (name, dss_obj) in get(data_dss, obj_type, Dict{String,Any}()) + _apply_like!(dss_obj, data_dss, obj_type) + if obj_type == "transformer" + transformer = _create_transformer(name; _to_kwargs(dss_obj)...) + for bus in transformer["buses"] + push!(buses, split(bus, '.'; limit=2)[1]) + end + elseif obj_type == "gictransformer" + for key in ["bush", "busx", "busnh", "busnx"] + if haskey(dss_obj, key) + push!(buses, split(dss_obj[key], '.'; limit=2)[1]) + end + end + elseif obj_type == "vsource" + push!(buses, split(get(dss_obj, "bus1", "sourcebus"), '.'; limit=2)[1]) + if haskey(dss_obj, "bus2") + push!(buses, split(dss_obj["bus2"], '.'; limit=2)[1]) + end + else + for key in ["bus1", "bus2"] + if haskey(dss_obj, key) + push!(buses, split(dss_obj[key], '.'; limit=2)[1]) + end + end + end + end + end + + return buses +end + + +"shifts a vector by `shift` spots to the left" +function _barrel_roll(x::Vector{T}, shift::Int)::Vector{T} where T + N = length(x) + if shift < 0 + shift = shift + ceil(Int, shift/N)*N + end + + shift = mod(shift, N) + + return x[[(i-1+shift)%N+1 for i in 1:N]] +end + + +"Parses busnames as defined in OpenDSS, e.g. \"primary.1.2.3.0\"" +function _parse_bus_id(busname::AbstractString)::Tuple{String,Vector{Bool}} + parts = split(busname, '.'; limit=2) + name = parts[1] + elements = "1.2.3" + + if length(parts) >= 2 + name, elements = split(busname, '.'; limit=2) + end + + nodes = Vector{Bool}([0, 0, 0, 0]) + + for num in 1:3 + if occursin("$num", elements) + nodes[num] = true + end + end + + if occursin("0", elements) || sum(nodes[1:3]) == 1 + nodes[4] = true + end + + return name, nodes +end + + +"converts Dict{String,Any} to Dict{Symbol,Any} for passing as kwargs" +function _to_kwargs(data::Dict{String,Any})::Dict{Symbol,Any} + return Dict{Symbol,Any}((Symbol(k), v) for (k, v) in data) +end + + +"apply properties in the order that they are given" +function _apply_ordered_properties(defaults::Dict{String,<:Any}, raw_dss::Dict{String,<:Any}; code_dict::Dict{String,<:Any}=Dict{String,Any}())::Dict{String,Any} + _defaults = deepcopy(defaults) + + for prop in filter(p->p!="like", raw_dss["prop_order"]) + if prop in ["linecode", "loadshape"] + merge!(defaults, code_dict) + else + if haskey(_defaults, prop) + defaults[prop] = _defaults[prop] + end + end + end + + return defaults +end + + +"applies `like` to component" +function _apply_like!(raw_dss::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, comp_type::String) + links = ["like"] + if any(link in raw_dss["prop_order"] for link in links) + new_prop_order = Vector{String}([]) + raw_dss_copy = deepcopy(raw_dss) + + for prop in raw_dss["prop_order"] + push!(new_prop_order, prop) + + if any(match(key, prop) !== nothing for key in [get(_like_exclusions, comp_type, [])..., _like_exclusions["all"]...]) + continue + end + + if prop in links + linked_dss = get(get(data_dss, comp_type, Dict{String,Any}()), raw_dss[prop], Dict{String,Any}()) + if isempty(linked_dss) + Memento.warn(_LOGGER, "$comp_type.$(raw_dss["name"]): $prop=$(raw_dss[prop]) cannot be found") + else + for linked_prop in linked_dss["prop_order"] + if linked_prop in get(_like_exclusions, comp_type, []) || linked_prop in _like_exclusions["all"] + continue + end + + push!(new_prop_order, linked_prop) + if linked_prop in links + _apply_like!(linked_dss, data_dss, comp_type) + else + raw_dss[linked_prop] = deepcopy(linked_dss[linked_prop]) + end + end + end + else + raw_dss[prop] = deepcopy(raw_dss_copy[prop]) + end + end + + final_prop_order = Vector{String}([]) + while !isempty(new_prop_order) + prop = popfirst!(new_prop_order) + if !(prop in new_prop_order) + push!(final_prop_order, prop) + end + end + raw_dss["prop_order"] = final_prop_order + end +end + + +""" +Parses the data in keys defined by `to_parse` in `data_dss` using types given by +the default properties from the `get_prop_default` function. +""" +function _parse_dss_with_dtypes!(data_dss::Dict{String,<:Any}, to_parse::Vector{String}=_dss_supported_components) + for obj_type in to_parse + if haskey(data_dss, obj_type) + dtypes = _dss_parameter_data_types[obj_type] + if obj_type == "options" + _parse_obj_dtypes!(obj_type, data_dss[obj_type], dtypes) + else + for object in values(data_dss[obj_type]) + _parse_obj_dtypes!(obj_type, object, dtypes) + end + end + end + end +end + + +"parses the raw dss values into their expected data types" +function _parse_element_with_dtype(dtype::Type, element::AbstractString) + if _isa_rpn(element) + out = _parse_rpn(element, dtype) + elseif _isa_matrix(element) + out = _parse_matrix(eltype(dtype), element) + elseif _isa_array(element) + out = _parse_array(eltype(dtype), element) + elseif dtype <: Bool + if element in ["n", "no"] + element = "false" + elseif element in ["y", "yes"] + element = "true" + end + out = parse(dtype, element) + elseif _isa_rpn(element) + out = _parse_rpn(element) + elseif dtype == String + out = element + else + if _isa_conn(element) + out = _parse_conn(element) + else + try + out = parse(dtype, element) + catch + Memento.warn(_LOGGER, "cannot parse $element as $dtype, leaving as String.") + out = element + end + end + end + + return out +end + + +"parses data type of properties of objects" +function _parse_obj_dtypes!(obj_type::String, object::Dict{String,Any}, dtypes::Dict{String,Type}) + for (k, v) in object + if isa(v, Vector) && eltype(v) == Any || isa(eltype(v), AbstractString) + _dtype = get(dtypes, k, _guess_dtype("[$(join(v, ","))]")) + for i in 1:length(v) + if isa(v[i], AbstractString) + v[i] = _parse_element_with_dtype(_dtype, v[i]) + end + end + elseif isa(v, Matrix) && eltype(v) == Any || isa(eltype(v), AbstractString) + _dtype = get(dtypes, k, _guess_dtype("$(join(collect(flatten(v)), " "))")) + for i in 1:size(v)[1] + for j in 1:size(v)[2] + if isa(v[i,j], AbstractString) + v[i,j] = _parse_element_with_dtype(_dtype, v[i,j]) + end + end + end + elseif isa(v, AbstractString) + object[k] = _parse_element_with_dtype(get(dtypes, k, _guess_dtype(v)), v) + end + end +end + + +"" +function _register_awaiting_ground!(bus::Dict{String,<:Any}, connections::Vector{Int}) + if !haskey(bus, "awaiting_ground") + bus["awaiting_ground"] = [] + end + + push!(bus["awaiting_ground"], connections) +end + + +"checks to see if a property is after linecode" +function _is_after_linecode(prop_order::Vector{String}, property::String)::Bool + return _is_after(prop_order, property, "linecode") +end + + +"checks to see if a property is after xfmrcode" +function _is_after_xfmrcode(prop_order::Vector{String}, property::String)::Bool + return _is_after(prop_order, property, "xfmrcode") +end + + +"checks to see if property1 is after property2 in the prop_order" +function _is_after(prop_order::Vector{String}, property1::String, property2::String)::Bool + property1_idx = 0 + property2_idx = 0 + + for (i, prop) in enumerate(prop_order) + if prop == property1 + property1_idx = i + elseif prop == property2 + property2_idx = i + end + end + + return property1_idx > property2_idx +end + + +"add engineering data object to engineering data model" +function _add_eng_obj!(data_eng::Dict{String,<:Any}, eng_obj_type::String, eng_obj_id::Any, eng_obj::Dict{String,<:Any}) + if !haskey(data_eng, eng_obj_type) + data_eng[eng_obj_type] = Dict{Any,Any}() + end + + if haskey(data_eng[eng_obj_type], eng_obj_id) + Memento.warn(_LOGGER, "id '$eng_obj_id' already exists in $eng_obj_type, renaming to '$(eng_obj["source_id"])'") + eng_obj_id = eng_obj["source_id"] + end + + data_eng[eng_obj_type][eng_obj_id] = eng_obj +end + + +"guesses the data type of a value using regex, returning Float64, Int, ComplexF64, or String (if number type cannot be determined)" +function _guess_dtype(value::AbstractString)::Type + if _isa_matrix(value) || _isa_array(value) || _isa_rpn(value) + for delim in [keys(_double_operators)..., keys(_single_operators)..., _array_delimiters..., "|", ","] + value = replace(value, delim => " ") + end + _dtypes = unique([_guess_dtype(v) for v in split(value)]) + if length(_dtypes) == 1 + return _dtypes[1] + elseif all(isa(v, Int) for v in _dtypes) + return Int + elseif any(isa(v, Complex) for v in _dtypes) + return ComplexF64 + elseif any(isa(v, Float64) for v in _dtypes) + return Float64 + else + return String + end + else + for (re, typ) in _dtype_regex + if occursin(re, value) + return typ + end + end + + return String + end +end + + +"converts dss load model to supported PowerModelsDistribution LoadModel enum" +function _parse_dss_load_model!(eng_obj::Dict{String,<:Any}, id::Any) + model = eng_obj["model"] + + if model in [3, 4, 7, 8] + Memento.warn(_LOGGER, "$id: dss load model $model not supported. Treating as constant POWER model") + model = 1 + elseif model == 6 + Memento.warn(_LOGGER, "$id: dss load model $model identical to model 1 in current feature set. Treating as constant POWER model") + model = 1 + end + + eng_obj["model"] = _dss2pmd_load_model[model] +end + + +"checks if loadshape has both pmult and qmult" +function _is_loadshape_split(dss_obj::Dict{String,<:Any}) + haskey(dss_obj, "pmult") && haskey(dss_obj, "qmult") && all(dss_obj["pmult"] .!= dss_obj["qmult"]) +end + + +"" +function _parse_dss_xycurve(dss_obj::Dict{String,<:Any}, id::Any, data_dss::Dict{String,<:Any})::Array{Vector{Real},2} + _apply_like!(dss_obj, data_dss, "xycurve") + defaults = _apply_ordered_properties(_create_xycurve(id; _to_kwargs(dss_obj)...), dss_obj) + + xarray = defaults["xarray"] .* defaults["xscale"] .+ defaults["xshift"] + yarray = defaults["yarray"] .* defaults["yscale"] .+ defaults["yshift"] + + @assert length(xarray) >= 2 && length(yarray) >= 2 "XYCurve data must have two or more points" + + return Array{Vector{Real},2}([xarray, yarray]) +end + + +"helper function to properly reference time series variables from opendss" +function _build_time_series_reference!(eng_obj::Dict{String,<:Any}, dss_obj::Dict{String,<:Any}, data_dss::Dict{String,<:Any}, defaults::Dict{String,<:Any}, time_series::String, active::String, reactive::String) + if haskey(dss_obj, time_series) && haskey(data_dss, "loadshape") && haskey(data_dss["loadshape"], defaults[time_series]) + eng_obj["time_series"] = Dict{String,Any}() + if _is_loadshape_split(data_dss["loadshape"][defaults[time_series]]) + eng_obj["time_series"][active] = "$(defaults[time_series])_p" + eng_obj["time_series"][reactive] = "$(defaults[time_series])_q" + else + eng_obj["time_series"][active] = defaults[time_series] + eng_obj["time_series"][reactive] = defaults[time_series] + end + end +end diff --git a/src/prob/common.jl b/src/prob/common.jl new file mode 100644 index 000000000..1f4357722 --- /dev/null +++ b/src/prob/common.jl @@ -0,0 +1,20 @@ +"alias to run_model in PowerModels with multiconductor=true, and transformer ref extensions added by default" +function run_mc_model(data::Dict{String,<:Any}, model_type::Type, solver, build_mc::Function; ref_extensions::Vector{<:Function}=Vector{Function}([]), make_si=!get(data, "per_unit", false), multinetwork::Bool=false, kwargs...)::Dict{String,Any} + if get(data, "data_model", MATHEMATICAL) == ENGINEERING + data_math = transform_data_model(data; build_multinetwork=multinetwork) + + result = _PM.run_model(data_math, model_type, solver, build_mc; ref_extensions=[ref_add_arcs_transformer!, ref_extensions...], multiconductor=true, multinetwork=multinetwork, kwargs...) + + result["solution"] = transform_solution(result["solution"], data_math; make_si=make_si) + elseif get(data, "data_model", MATHEMATICAL) == MATHEMATICAL + result = _PM.run_model(data, model_type, solver, build_mc; ref_extensions=[ref_add_arcs_transformer!, ref_extensions...], multiconductor=true, multinetwork=multinetwork, kwargs...) + end + + return result +end + + +"alias to run_model in PowerModels with multiconductor=true, and transformer ref extensions added by default" +function run_mc_model(file::String, model_type::Type, solver, build_mc::Function; ref_extensions::Vector{<:Function}=Vector{Function}([]), kwargs...)::Dict{String,Any} + return run_mc_model(parse_file(file), model_type, solver, build_mc; ref_extensions=ref_extensions, kwargs...) +end diff --git a/src/prob/debug.jl b/src/prob/debug.jl index 79b8b87b6..ca59db5d7 100644 --- a/src/prob/debug.jl +++ b/src/prob/debug.jl @@ -1,51 +1,39 @@ # These problem formulations are used to debug Distribution datasets # that do not converge using the standard formulations "OPF problem with slack power at every bus" -function run_mc_opf_pbs(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_opf_pbs; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"OPF problem with slack power at every bus" -function run_mc_opf_pbs(file::String, model_type, solver; kwargs...) - return run_mc_opf_pbs(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"PF problem with slack power at every bus" -function run_mc_pf_pbs(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_pf_pbs; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) +function run_mc_opf_pbs(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_opf_pbs; kwargs...) end "PF problem with slack power at every bus" -function run_mc_pf_pbs(file::String, model_type, solver; kwargs...) - return run_mc_pf_pbs(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) +function run_mc_pf_pbs(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_pf_pbs; kwargs...) end "OPF problem with slack power at every bus" -function build_mc_opf_pbs(pm::_PMs.AbstractPowerModel) - variable_mc_voltage(pm) +function build_mc_opf_pbs(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) - variable_mc_generation(pm) + variable_mc_gen_power_setpoint(pm) - variable_mc_bus_power_slack(pm) + variable_mc_slack_bus_power(pm) constraint_mc_model_voltage(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_slack(pm, i) + for i in ids(pm, :bus) + constraint_mc_slack_power_balance(pm, i) end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) @@ -55,49 +43,49 @@ function build_mc_opf_pbs(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - objective_min_bus_power_slack(pm) + objective_mc_min_slack_bus_power(pm) end "PF problem with slack power at every bus" -function build_mc_pf_pbs(pm::_PMs.AbstractPowerModel) - variable_mc_voltage(pm; bounded=false) +function build_mc_pf_pbs(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage(pm; bounded=false) - variable_mc_branch_flow(pm; bounded=false) - variable_mc_transformer_flow(pm; bounded=false) + variable_mc_branch_power(pm; bounded=false) + variable_mc_transformer_power(pm; bounded=false) - variable_mc_generation(pm; bounded=false) + variable_mc_gen_power_setpoint(pm; bounded=false) - variable_mc_bus_power_slack(pm) + variable_mc_slack_bus_power(pm) constraint_mc_model_voltage(pm) - for (i,bus) in _PMs.ref(pm, :ref_buses) + for (i,bus) in ref(pm, :ref_buses) constraint_mc_theta_ref(pm, i) @assert bus["bus_type"] == 3 - constraint_mc_voltage_magnitude_setpoint(pm, i) + constraint_mc_voltage_magnitude_only(pm, i) end - for (i,bus) in _PMs.ref(pm, :bus) - constraint_mc_power_balance_slack(pm, i) + for (i,bus) in ref(pm, :bus) + constraint_mc_slack_power_balance(pm, i) # PV Bus Constraints - if length(_PMs.ref(pm, :bus_gens, i)) > 0 && !(i in _PMs.ids(pm,:ref_buses)) + if length(ref(pm, :bus_gens, i)) > 0 && !(i in ids(pm,:ref_buses)) # this assumes inactive generators are filtered out of bus_gens @assert bus["bus_type"] == 2 - constraint_mc_voltage_magnitude_setpoint(pm, i) - for j in _PMs.ref(pm, :bus_gens, i) - constraint_mc_active_gen_setpoint(pm, j) + constraint_mc_voltage_magnitude_only(pm, i) + for j in ref(pm, :bus_gens, i) + constraint_mc_gen_power_setpoint_real(pm, j) end end end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) end - objective_min_bus_power_slack(pm) + objective_mc_min_slack_bus_power(pm) end diff --git a/src/prob/mld.jl b/src/prob/mld.jl index 9609dbfda..564f85f42 100644 --- a/src/prob/mld.jl +++ b/src/prob/mld.jl @@ -1,84 +1,61 @@ "Run load shedding problem with storage" -function run_mc_mld(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_mld; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"" -function run_mc_mld(file::String, model_type, solver; kwargs...) - return run_mc_mld(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) +function run_mc_mld(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_mld; kwargs...) end "Run Branch Flow Model Load Shedding Problem" -function run_mc_mld_bf(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_mld_bf; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"" -function run_mc_mld_bf(file::String, model_type, solver; kwargs...) - return run_mc_mld_bf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) +function run_mc_mld_bf(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_mld_bf; kwargs...) end "Run unit commitment load shedding problem (!relaxed)" -function run_mc_mld_uc(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_mld_uc; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"" -function run_mc_mld_uc(file::String, model_type, solver; kwargs...) - return run_mc_mld(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) +function run_mc_mld_uc(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_mld_uc; kwargs...) end "Load shedding problem including storage (snap-shot)" -function build_mc_mld(pm::_PMs.AbstractPowerModel) - variable_mc_indicator_bus_voltage(pm; relax=true) +function build_mc_mld(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage_indicator(pm; relax=true) variable_mc_bus_voltage_on_off(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) - variable_mc_indicator_generation(pm; relax=true) - variable_mc_generation_on_off(pm) + variable_mc_gen_indicator(pm; relax=true) + variable_mc_gen_power_setpoint_on_off(pm) - # variable_mc_storage(pm) - _PMs.variable_storage_energy(pm) - _PMs.variable_storage_charge(pm) - _PMs.variable_storage_discharge(pm) - variable_mc_indicator_storage(pm; relax=true) - variable_mc_on_off_storage(pm) + variable_mc_storage_power_mi(pm; relax=true) - variable_mc_indicator_demand(pm; relax=true) - variable_mc_indicator_shunt(pm; relax=true) + variable_mc_load_indicator(pm; relax=true) + variable_mc_shunt_indicator(pm; relax=true) constraint_mc_model_voltage(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end constraint_mc_bus_voltage_on_off(pm) - for i in _PMs.ids(pm, :gen) - constraint_mc_generation_on_off(pm, i) + for i in ids(pm, :gen) + constraint_mc_gen_power_on_off(pm, i) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_shed(pm, i) + for i in ids(pm, :bus) + constraint_mc_shed_power_balance(pm, i) end - for i in _PMs.ids(pm, :storage) - _PMs.constraint_storage_state(pm, i) - _PMs.constraint_storage_complementarity_nl(pm, i) - constraint_mc_storage_loss(pm, i) + for i in ids(pm, :storage) + _PM.constraint_storage_state(pm, i) + _PM.constraint_storage_complementarity_nl(pm, i) + constraint_mc_storage_losses(pm, i) constraint_mc_storage_thermal_limit(pm, i) end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) @@ -88,47 +65,47 @@ function build_mc_mld(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) end - objective_mc_min_load_delta(pm) + objective_mc_min_load_setpoint_delta(pm) end "Load shedding problem for Branch Flow model" -function build_mc_mld_bf(pm::_PMs.AbstractPowerModel) - variable_mc_indicator_bus_voltage(pm; relax=true) +function build_mc_mld_bf(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage_indicator(pm; relax=true) variable_mc_bus_voltage_on_off(pm) variable_mc_branch_current(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) - variable_mc_indicator_generation(pm; relax=true) - variable_mc_generation_on_off(pm) + variable_mc_gen_indicator(pm; relax=true) + variable_mc_gen_power_setpoint_on_off(pm) - variable_mc_indicator_demand(pm; relax=true) - variable_mc_indicator_shunt(pm; relax=true) + variable_mc_load_indicator(pm; relax=true) + variable_mc_shunt_indicator(pm; relax=true) constraint_mc_model_current(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end constraint_mc_bus_voltage_on_off(pm) - for i in _PMs.ids(pm, :gen) - constraint_mc_generation_on_off(pm, i) + for i in ids(pm, :gen) + constraint_mc_gen_power_on_off(pm, i) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_shed(pm, i) + for i in ids(pm, :bus) + constraint_mc_shed_power_balance(pm, i) end - for i in _PMs.ids(pm, :branch) - constraint_mc_flow_losses(pm, i) + for i in ids(pm, :branch) + constraint_mc_power_losses(pm, i) constraint_mc_model_voltage_magnitude_difference(pm, i) constraint_mc_voltage_angle_difference(pm, i) @@ -137,56 +114,56 @@ function build_mc_mld_bf(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) end - objective_mc_min_load_delta(pm) + objective_mc_min_load_setpoint_delta(pm) end "Standard unit commitment (!relaxed) load shedding problem" -function build_mc_mld_uc(pm::_PMs.AbstractPowerModel) - variable_mc_indicator_bus_voltage(pm; relax=false) +function build_mc_mld_uc(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage_indicator(pm; relax=false) variable_mc_bus_voltage_on_off(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) - variable_mc_indicator_generation(pm; relax=false) - variable_mc_generation_on_off(pm) + variable_mc_gen_indicator(pm; relax=false) + variable_mc_gen_power_setpoint_on_off(pm) - variable_mc_storage(pm) - variable_mc_indicator_storage(pm; relax=false) - variable_mc_on_off_storage(pm) + variable_mc_storage_power(pm) + variable_mc_storage_indicator(pm; relax=false) + variable_mc_storage_power_on_off(pm) - variable_mc_indicator_demand(pm; relax=false) - variable_mc_indicator_shunt(pm; relax=false) + variable_mc_load_indicator(pm; relax=false) + variable_mc_shunt_indicator(pm; relax=false) constraint_mc_model_voltage(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end constraint_mc_bus_voltage_on_off(pm) - for i in _PMs.ids(pm, :gen) - constraint_mc_generation_on_off(pm, i) + for i in ids(pm, :gen) + constraint_mc_gen_power_on_off(pm, i) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_shed(pm, i) + for i in ids(pm, :bus) + constraint_mc_shed_power_balance(pm, i) end - for i in _PMs.ids(pm, :storage) - _PMs.constraint_storage_state(pm, i) - _PMs.constraint_storage_complementarity_nl(pm, i) - constraint_mc_storage_loss(pm, i) + for i in ids(pm, :storage) + _PM.constraint_storage_state(pm, i) + _PM.constraint_storage_complementarity_nl(pm, i) + constraint_mc_storage_losses(pm, i) constraint_mc_storage_thermal_limit(pm, i) end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) @@ -196,9 +173,9 @@ function build_mc_mld_uc(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) end - objective_mc_min_load_delta(pm) + objective_mc_min_load_setpoint_delta(pm) end diff --git a/src/prob/opf.jl b/src/prob/opf.jl index 4749b0f50..af966b154 100644 --- a/src/prob/opf.jl +++ b/src/prob/opf.jl @@ -1,58 +1,52 @@ -"" -function run_ac_mc_opf(file, solver; kwargs...) - return run_mc_opf(file, _PMs.ACPPowerModel, solver; kwargs...) +"OPF with ACPPowerModel" +function run_ac_mc_opf(data::Union{Dict{String,<:Any},String}, solver; kwargs...) + return run_mc_opf(data, ACPPowerModel, solver; kwargs...) end -"" -function run_mc_opf(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_opf; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) +"Optimal Power Flow" +function run_mc_opf(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_opf; kwargs...) end -"" -function run_mc_opf(file::String, model_type, solver; kwargs...) - return run_mc_opf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_opf(pm::_PMs.AbstractPowerModel) - variable_mc_voltage(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) - variable_mc_generation(pm) - variable_mc_load(pm) - variable_mc_storage(pm) +"Constructor for Optimal Power Flow" +function build_mc_opf(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) + variable_mc_gen_power_setpoint(pm) + variable_mc_load_setpoint(pm) + variable_mc_storage_power(pm) constraint_mc_model_voltage(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end # generators should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id) + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) end # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id) + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_load(pm, i) + for i in ids(pm, :bus) + constraint_mc_load_power_balance(pm, i) end - for i in _PMs.ids(pm, :storage) - _PMs.constraint_storage_state(pm, i) - _PMs.constraint_storage_complementarity_nl(pm, i) - constraint_mc_storage_loss(pm, i) + for i in ids(pm, :storage) + _PM.constraint_storage_state(pm, i) + _PM.constraint_storage_complementarity_nl(pm, i) + constraint_mc_storage_losses(pm, i) constraint_mc_storage_thermal_limit(pm, i) end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) @@ -62,9 +56,108 @@ function build_mc_opf(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) + end + + _PM.objective_min_fuel_cost(pm) +end + + +"constructor for OPF in current-voltage variable space" +function build_mc_opf(pm::_PM.AbstractIVRModel) + # Variables + variable_mc_bus_voltage(pm) + variable_mc_branch_current(pm) + variable_mc_transformer_current(pm) + variable_mc_gen_power_setpoint(pm) + variable_mc_load_setpoint(pm) + + # Constraints + for i in ids(pm, :ref_buses) + constraint_mc_theta_ref(pm, i) + end + + # gens should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) + end + + # loads should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) + end + + for i in ids(pm, :bus) + constraint_mc_load_current_balance(pm, i) + end + + for i in ids(pm, :branch) + constraint_mc_current_from(pm, i) + constraint_mc_current_to(pm, i) + + constraint_mc_bus_voltage_drop(pm, i) + + constraint_mc_voltage_angle_difference(pm, i) + + constraint_mc_thermal_limit_from(pm, i) + constraint_mc_thermal_limit_to(pm, i) + end + + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) + end + + # Objective + _PM.objective_min_fuel_cost(pm) +end + + +"constructor for branch flow opf" +function build_mc_opf(pm::AbstractUBFModels) + # Variables + variable_mc_bus_voltage(pm) + variable_mc_branch_current(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm) + variable_mc_gen_power_setpoint(pm) + variable_mc_load_setpoint(pm) + + # Constraints + constraint_mc_model_current(pm) + + for i in ids(pm, :ref_buses) + constraint_mc_theta_ref(pm, i) + end + + # gens should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) + end + + # loads should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) + end + + for i in ids(pm, :bus) + constraint_mc_load_power_balance(pm, i) + end + + for i in ids(pm, :branch) + constraint_mc_power_losses(pm, i) + constraint_mc_model_voltage_magnitude_difference(pm, i) + + constraint_mc_voltage_angle_difference(pm, i) + + constraint_mc_thermal_limit_from(pm, i) + constraint_mc_thermal_limit_to(pm, i) + end + + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) end - _PMs.objective_min_fuel_cost(pm) + # Objective + _PM.objective_min_fuel_cost(pm) end diff --git a/src/prob/opf_bf.jl b/src/prob/opf_bf.jl deleted file mode 100644 index 5b2ab7728..000000000 --- a/src/prob/opf_bf.jl +++ /dev/null @@ -1,49 +0,0 @@ -"" -function run_mc_opf_bf(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_opf_bf; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, kwargs...) -end - - -"" -function run_mc_opf_bf(file::String, model_type, solver; kwargs...) - return run_mc_opf_bf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_opf_bf(pm::_PMs.AbstractPowerModel) - # Variables - variable_mc_voltage(pm) - variable_mc_branch_current(pm) - variable_mc_branch_flow(pm) - variable_mc_transformer_flow(pm) - variable_mc_generation(pm) - - # Constraints - constraint_mc_model_current(pm) - - for i in _PMs.ids(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) - end - - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance(pm, i) - end - - for i in _PMs.ids(pm, :branch) - constraint_mc_flow_losses(pm, i) - constraint_mc_model_voltage_magnitude_difference(pm, i) - - constraint_mc_voltage_angle_difference(pm, i) - - constraint_mc_thermal_limit_from(pm, i) - constraint_mc_thermal_limit_to(pm, i) - end - - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) - end - - # Objective - _PMs.objective_min_fuel_cost(pm) -end diff --git a/src/prob/opf_bf_lm.jl b/src/prob/opf_bf_lm.jl deleted file mode 100644 index 716e5f8cd..000000000 --- a/src/prob/opf_bf_lm.jl +++ /dev/null @@ -1,58 +0,0 @@ -# This problem includes load models beyond simple constant power ones; this is -# handled in variable_mc_load and constraint_mc_load. - - -"" -function run_mc_opf_bf_lm(data::Dict{String,Any}, model_constructor, solver; kwargs...) - return _PMs.run_model(data, model_constructor, solver, build_mc_opf_bf_lm; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"" -function run_mc_opf_bf_lm(file::String, model_constructor, solver; kwargs...) - return run_mc_opf_bf_lm(PowerModelsDistribution.parse_file(file), model_constructor, solver; kwargs...) -end - - -"" -function build_mc_opf_bf_lm(pm::_PMs.AbstractPowerModel) - # Variables - variable_mc_voltage(pm) - variable_mc_branch_current(pm) - variable_mc_branch_flow(pm) - - variable_mc_generation(pm) - variable_mc_load(pm) - - # Constraints - constraint_mc_model_current(pm) - - for i in _PMs.ids(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) - end - - for i in _PMs.ids(pm, :branch) - constraint_mc_flow_losses(pm, i) - constraint_mc_model_voltage_magnitude_difference(pm, i) - - constraint_mc_voltage_angle_difference(pm, i) - - constraint_mc_thermal_limit_from(pm, i) - constraint_mc_thermal_limit_to(pm, i) - end - - for i in _PMs.ids(pm, :load) - constraint_mc_load(pm, i) - end - - for i in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, i) - end - - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance(pm, i) - end - - # Objective - _PMs.objective_min_fuel_cost(pm) -end diff --git a/src/prob/opf_iv.jl b/src/prob/opf_iv.jl deleted file mode 100644 index 67fd3a4e8..000000000 --- a/src/prob/opf_iv.jl +++ /dev/null @@ -1,59 +0,0 @@ -"" -function run_mc_opf_iv(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_opf_iv; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, kwargs...) -end - - -"" -function run_mc_opf_iv(file::String, model_type, solver; kwargs...) - return run_mc_opf_iv(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_opf_iv(pm::_PMs.AbstractPowerModel) - # Variables - variable_mc_voltage(pm) - variable_mc_branch_current(pm) - variable_mc_transformer_current(pm) - variable_mc_generation(pm) - variable_mc_load(pm) - - # Constraints - for i in _PMs.ids(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) - end - - # gens should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id) - end - - # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id) - end - - for i in _PMs.ids(pm, :bus) - constraint_mc_current_balance_load(pm, i) - end - - for i in _PMs.ids(pm, :branch) - constraint_mc_current_from(pm, i) - constraint_mc_current_to(pm, i) - - constraint_mc_voltage_drop(pm, i) - - constraint_mc_voltage_angle_difference(pm, i) - - constraint_mc_thermal_limit_from(pm, i) - constraint_mc_thermal_limit_to(pm, i) - end - - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) - end - - # Objective - _PMs.objective_min_fuel_cost(pm) -end diff --git a/src/prob/opf_oltc.jl b/src/prob/opf_oltc.jl index 388ef88db..462da0070 100644 --- a/src/prob/opf_oltc.jl +++ b/src/prob/opf_oltc.jl @@ -1,51 +1,45 @@ -"" -function run_ac_mc_opf_oltc(file, solver; kwargs...) - return run_mc_opf_oltc(file, _PMs.ACPPowerModel, solver; kwargs...) +"on-load tap-changer OPF with ACPPowerModel" +function run_ac_mc_opf_oltc(data::Union{Dict{String,<:Any},String}, solver; kwargs...) + return run_mc_opf_oltc(data, ACPPowerModel, solver; kwargs...) end -"" -function run_mc_opf_oltc(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_opf_oltc; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) +"on-load tap-changer OPF" +function run_mc_opf_oltc(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_opf_oltc; kwargs...) end -"" -function run_mc_opf_oltc(file::String, model_type, solver; kwargs...) - return run_mc_opf_oltc(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_opf_oltc(pm::_PMs.AbstractPowerModel) - variable_mc_voltage(pm) - variable_mc_branch_flow(pm) - variable_mc_generation(pm) - variable_mc_load(pm) - variable_mc_transformer_flow(pm) - variable_mc_oltc_tap(pm) +"constructor for on-load tap-changer OPF" +function build_mc_opf_oltc(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage(pm) + variable_mc_branch_power(pm) + variable_mc_gen_power_setpoint(pm) + variable_mc_load_setpoint(pm) + variable_mc_transformer_power(pm) + variable_mc_oltc_transformer_tap(pm) constraint_mc_model_voltage(pm) - for i in _PMs.ids(pm, :ref_buses) + for i in ids(pm, :ref_buses) constraint_mc_theta_ref(pm, i) end # generators should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id) + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) end # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id) + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) end - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance_load(pm, i) + for i in ids(pm, :bus) + constraint_mc_load_power_balance(pm, i) end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) @@ -55,9 +49,9 @@ function build_mc_opf_oltc(pm::_PMs.AbstractPowerModel) constraint_mc_thermal_limit_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i, fix_taps=false) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i, fix_taps=false) end - _PMs.objective_min_fuel_cost(pm) + _PM.objective_min_fuel_cost(pm) end diff --git a/src/prob/pf.jl b/src/prob/pf.jl index 07176655b..e9ba1f768 100644 --- a/src/prob/pf.jl +++ b/src/prob/pf.jl @@ -1,75 +1,176 @@ -"" -function run_ac_mc_pf(data, solver; kwargs...) - return run_mc_pf(data, _PMs.ACPPowerModel, solver; kwargs...) +"Power Flow problem with ACPPowerModel" +function run_ac_mc_pf(data::Union{Dict{String,<:Any},String}, solver; kwargs...) + return run_mc_pf(data, _PM.ACPPowerModel, solver; kwargs...) end -"" -function run_dc_mc_pf(data, solver; kwargs...) - return run_mc_pf(data, _PMs.DCPPowerModel, solver; kwargs...) +"Power Flow Problem" +function run_mc_pf(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, build_mc_pf; kwargs...) end -"" -function run_mc_pf(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_pf; multiconductor=true, ref_extensions=[ref_add_arcs_trans!], kwargs...) -end - - -"" -function run_mc_pf(file::String, model_type, solver; kwargs...) - return run_mc_pf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_pf(pm::_PMs.AbstractPowerModel) - variable_mc_voltage(pm; bounded=false) - variable_mc_branch_flow(pm; bounded=false) - variable_mc_transformer_flow(pm; bounded=false) - variable_mc_generation(pm; bounded=false) - variable_mc_load(pm; bounded=false) +"Constructor for Power Flow Problem" +function build_mc_pf(pm::_PM.AbstractPowerModel) + variable_mc_bus_voltage(pm; bounded=false) + variable_mc_branch_power(pm; bounded=false) + variable_mc_transformer_power(pm; bounded=false) + variable_mc_gen_power_setpoint(pm; bounded=false) + variable_mc_load_setpoint(pm; bounded=false) constraint_mc_model_voltage(pm) - for (i,bus) in _PMs.ref(pm, :ref_buses) + for (i,bus) in ref(pm, :ref_buses) @assert bus["bus_type"] == 3 constraint_mc_theta_ref(pm, i) - constraint_mc_voltage_magnitude_setpoint(pm, i) + constraint_mc_voltage_magnitude_only(pm, i) end # gens should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id) + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) end # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id) + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) end - for (i,bus) in _PMs.ref(pm, :bus) - constraint_mc_power_balance_load(pm, i) + for (i,bus) in ref(pm, :bus) + constraint_mc_load_power_balance(pm, i) # PV Bus Constraints - if length(_PMs.ref(pm, :bus_gens, i)) > 0 && !(i in _PMs.ids(pm,:ref_buses)) + if length(ref(pm, :bus_gens, i)) > 0 && !(i in ids(pm,:ref_buses)) # this assumes inactive generators are filtered out of bus_gens @assert bus["bus_type"] == 2 - constraint_mc_voltage_magnitude_setpoint(pm, i) - for j in _PMs.ref(pm, :bus_gens, i) - constraint_mc_active_gen_setpoint(pm, j) + constraint_mc_voltage_magnitude_only(pm, i) + for j in ref(pm, :bus_gens, i) + constraint_mc_gen_power_setpoint_real(pm, j) end end end - for i in _PMs.ids(pm, :branch) + for i in ids(pm, :branch) constraint_mc_ohms_yt_from(pm, i) constraint_mc_ohms_yt_to(pm, i) end - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) + end +end + + +"Constructor for Power Flow in current-voltage variable space" +function build_mc_pf(pm::_PM.AbstractIVRModel) + # Variables + variable_mc_bus_voltage(pm, bounded = false) + variable_mc_branch_current(pm, bounded = false) + variable_mc_transformer_current(pm, bounded = false) + variable_mc_gen_power_setpoint(pm, bounded = false) + variable_mc_load_setpoint(pm, bounded = false) + + # Constraints + for (i,bus) in ref(pm, :ref_buses) + @assert bus["bus_type"] == 3 + constraint_mc_theta_ref(pm, i) + constraint_mc_voltage_magnitude_only(pm, i) + end + + # gens should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) + end + + # loads should be constrained before KCL, or Pd/Qd undefined + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) + end + + for (i,bus) in ref(pm, :bus) + constraint_mc_load_current_balance(pm, i) + + # PV Bus Constraints + if length(ref(pm, :bus_gens, i)) > 0 && !(i in ids(pm,:ref_buses)) + # this assumes inactive generators are filtered out of bus_gens + @assert bus["bus_type"] == 2 + constraint_mc_voltage_magnitude_only(pm, i) + for j in ref(pm, :bus_gens, i) + constraint_mc_gen_power_setpoint_real(pm, j) + end + end + end + + for i in ids(pm, :branch) + constraint_mc_current_from(pm, i) + constraint_mc_current_to(pm, i) + + constraint_mc_bus_voltage_drop(pm, i) + end + + for i in ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) + end +end + + +"Constructor for Branch Flow Power Flow" +function build_mc_pf(pm::AbstractUBFModels) + # Variables + variable_mc_bus_voltage(pm; bounded=true) # TODO should be false + variable_mc_branch_current(pm) + variable_mc_branch_power(pm) + variable_mc_transformer_power(pm; bounded=false) + variable_mc_gen_power_setpoint(pm; bounded=false) + variable_mc_load_setpoint(pm) + + # Constraints + constraint_mc_model_current(pm) + + for (i,bus) in ref(pm, :ref_buses) + if !(typeof(pm)<:LPUBFDiagPowerModel) + constraint_mc_theta_ref(pm, i) + end + + @assert bus["bus_type"] == 3 + constraint_mc_voltage_magnitude_only(pm, i) + end + + for id in ids(pm, :gen) + constraint_mc_gen_setpoint(pm, id) + end + + for id in ids(pm, :load) + constraint_mc_load_setpoint(pm, id) + end + + for (i,bus) in ref(pm, :bus) + constraint_mc_load_power_balance(pm, i) + + # PV Bus Constraints + if length(ref(pm, :bus_gens, i)) > 0 && !(i in ids(pm,:ref_buses)) + # this assumes inactive generators are filtered out of bus_gens + @assert bus["bus_type"] == 2 + + constraint_mc_voltage_magnitude_only(pm, i) + for j in ref(pm, :bus_gens, i) + constraint_mc_gen_power_setpoint_real(pm, j) + end + end + end + + for i in ids(pm, :branch) + constraint_mc_power_losses(pm, i) + constraint_mc_model_voltage_magnitude_difference(pm, i) + constraint_mc_voltage_angle_difference(pm, i) + + constraint_mc_thermal_limit_from(pm, i) + constraint_mc_thermal_limit_to(pm, i) + end + + for i in _PM.ids(pm, :transformer) + constraint_mc_transformer_power(pm, i) end end diff --git a/src/prob/pf_bf.jl b/src/prob/pf_bf.jl deleted file mode 100644 index c2a663b9a..000000000 --- a/src/prob/pf_bf.jl +++ /dev/null @@ -1,57 +0,0 @@ -"" -function run_mc_pf_bf(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_pf_bf; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, kwargs...) -end - - -"" -function run_mc_pf_bf(file::String, model_type, solver; kwargs...) - return run_mc_pf_bf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_pf_bf(pm::_PMs.AbstractPowerModel) - # Variables - variable_mc_voltage(pm; bounded=false) - variable_mc_branch_current(pm) - variable_mc_branch_flow(pm) - variable_mc_generation(pm; bounded=false) - - # Constraints - constraint_mc_model_current(pm) - - for (i,bus) in _PMs.ref(pm, :ref_buses) - constraint_mc_theta_ref(pm, i) - - @assert bus["bus_type"] == 3 - constraint_mc_voltage_magnitude_setpoint(pm, i) - end - - for i in _PMs.ids(pm, :bus) - constraint_mc_power_balance(pm, i) - - # PV Bus Constraints - if length(_PMs.ref(pm, :bus_gens, i)) > 0 && !(i in _PMs.ids(pm,:ref_buses)) - # this assumes inactive generators are filtered out of bus_gens - @assert bus["bus_type"] == 2 - - constraint_mc_voltage_magnitude_setpoint(pm, i) - for j in _PMs.ref(pm, :bus_gens, i) - constraint_mc_active_gen_setpoint(pm, j) - end - end - end - - for i in _PMs.ids(pm, :branch) - constraint_mc_flow_losses(pm, i) - constraint_mc_model_voltage_magnitude_difference(pm, i) - constraint_mc_voltage_angle_difference(pm, i) - - constraint_mc_thermal_limit_from(pm, i) - constraint_mc_thermal_limit_to(pm, i) - end - - # Objective - _PMs.objective_min_fuel_cost(pm) -end diff --git a/src/prob/pf_iv.jl b/src/prob/pf_iv.jl deleted file mode 100644 index 150738901..000000000 --- a/src/prob/pf_iv.jl +++ /dev/null @@ -1,63 +0,0 @@ -"" -function run_mc_pf_iv(data::Dict{String,Any}, model_type, solver; kwargs...) - return _PMs.run_model(data, model_type, solver, build_mc_pf_iv; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, kwargs...) -end - - -"" -function run_mc_pf_iv(file::String, model_type, solver; kwargs...) - return run_mc_pf_iv(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -end - - -"" -function build_mc_pf_iv(pm::_PMs.AbstractPowerModel) - # Variables - variable_mc_voltage(pm, bounded = false) - variable_mc_branch_current(pm, bounded = false) - variable_mc_transformer_current(pm, bounded = false) - variable_mc_generation(pm, bounded = false) - variable_mc_load(pm, bounded = false) - - # Constraints - for (i,bus) in _PMs.ref(pm, :ref_buses) - @assert bus["bus_type"] == 3 - constraint_mc_theta_ref(pm, i) - constraint_mc_voltage_magnitude_setpoint(pm, i) - end - - # gens should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id) - end - - # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id) - end - - for (i,bus) in _PMs.ref(pm, :bus) - constraint_mc_current_balance_load(pm, i) - - # PV Bus Constraints - if length(_PMs.ref(pm, :bus_gens, i)) > 0 && !(i in _PMs.ids(pm,:ref_buses)) - # this assumes inactive generators are filtered out of bus_gens - @assert bus["bus_type"] == 2 - constraint_mc_voltage_magnitude_setpoint(pm, i) - for j in ref(pm, :bus_gens, i) - constraint_mc_active_gen_setpoint(pm, j) - end - end - end - - for i in _PMs.ids(pm, :branch) - constraint_mc_current_from(pm, i) - constraint_mc_current_to(pm, i) - - constraint_mc_voltage_drop(pm, i) - end - - for i in _PMs.ids(pm, :transformer) - constraint_mc_trans(pm, i) - end -end diff --git a/src/prob/test.jl b/src/prob/test.jl index 749051fdf..a9773571d 100644 --- a/src/prob/test.jl +++ b/src/prob/test.jl @@ -1,291 +1,18 @@ -###### -# -# These are toy problem formulations used to test advanced features -# such as storage devices -# -###### -# "multi-network opf with storage" -# function _run_mn_mc_opf(data::Dict{String,Any}, model_type, solver; kwargs...) -# return _PMs.run_model(data, model_type, solver, _build_mn_mc_strg_opf; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, multinetwork=true, kwargs...) -# end -# -# -# "multi-network opf with storage" -# function _run_mn_mc_opf(file::String, model_type, solver; kwargs...) -# return run_mn_mc_opf(PowerModelsDistribution.parse_file(file), model_type, solver; kwargs...) -# end -# -# -# "multi-network opf with storage" -# function _build_mn_mc_strg_opf(pm::_PMs.AbstractPowerModel) -# for (n, network) in _PMs.nws(pm) -# variable_mc_voltage(pm; nw=n) -# constraint_mc_model_voltage(pm; nw=n) -# variable_mc_branch_flow(pm; nw=n) -# variable_mc_generation(pm; nw=n) -# variable_mc_storage(pm; nw=n) -# -# for i in _PMs.ids(pm, :ref_buses; nw=n) -# constraint_mc_theta_ref(pm, i; nw=n) -# end -# -# for i in _PMs.ids(pm, :bus; nw=n) -# constraint_mc_power_balance(pm, i; nw=n) -# end -# -# for i in _PMs.ids(pm, :storage; nw=n) -# _PMs.constraint_storage_state(pm, i; nw=n) -# _PMs.constraint_storage_complementarity_nl(pm, i; nw=n) -# constraint_mc_storage_loss(pm, i; nw=n) -# constraint_mc_storage_thermal_limit(pm, i; nw=n) -# end -# -# for i in _PMs.ids(pm, :branch; nw=n) -# constraint_mc_ohms_yt_from(pm, i; nw=n) -# constraint_mc_ohms_yt_to(pm, i; nw=n) -# -# constraint_mc_voltage_angle_difference(pm, i; nw=n) -# -# constraint_mc_thermal_limit_from(pm, i; nw=n) -# constraint_mc_thermal_limit_to(pm, i; nw=n) -# end -# -# for i in _PMs.ids(pm, :transformer; nw=n) -# constraint_mc_trans(pm, i; nw=n) -# end -# end -# -# network_ids = sort(collect(_PMs.nw_ids(pm))) -# -# n_1 = network_ids[1] -# for i in _PMs.ids(pm, :storage; nw=n_1) -# _PMs.constraint_storage_state(pm, i; nw=n_1) -# end -# -# for n_2 in network_ids[2:end] -# for i in _PMs.ids(pm, :storage; nw=n_2) -# _PMs.constraint_storage_state(pm, i, n_1, n_2) -# end -# n_1 = n_2 -# end -# -# _PMs.objective_min_fuel_cost(pm) -# end - - -###### -# -# Formulations from PowerModels -# -###### - -"" -function _run_mc_ucopf(file, model_type::Type, solver; kwargs...) - return _PMs.run_model(file, model_type, solver, _build_mc_ucopf; ref_extensions=[ref_add_arcs_trans!], multiconductor=true, kwargs...) +"test mc mn" +function _run_mc_mn_opb(data::Union{Dict{String,<:Any},String}, model_type::Type, solver; kwargs...) + return run_mc_model(data, model_type, solver, _build_mc_mn_opb; ref_extensions=[_PM.ref_add_connected_components!], multinetwork=true, kwargs...) end -"" -function _build_mc_ucopf(pm::_PMs.AbstractPowerModel) - for (n, network) in _PMs.nws(pm) - variable_mc_voltage(pm, nw=n) - variable_mc_branch_flow(pm, nw=n) - variable_mc_transformer_flow(pm, nw=n) - - variable_mc_indicator_generation(pm, nw=n) - variable_mc_generation_on_off(pm, nw=n) - - - constraint_mc_model_voltage(pm, nw=n) - - - - variable_mc_on_off_storage(pm, nw=n) - _PMs.variable_storage_energy(pm, nw=n) - _PMs.variable_storage_charge(pm, nw=n) - _PMs.variable_storage_discharge(pm, nw=n) - _PMs.variable_storage_indicator(pm, nw=n) - _PMs.variable_storage_complementary_indicator(pm, nw=n) - - - for i in _PMs.ids(pm, :ref_buses, nw=n) - constraint_mc_theta_ref(pm, i, nw=n) - end - - for i in _PMs.ids(pm, :bus, nw=n) - constraint_mc_power_balance(pm, i, nw=n) - - end - - for i in _PMs.ids(pm, :branch, nw=n) - constraint_mc_ohms_yt_from(pm, i, nw=n) - constraint_mc_ohms_yt_to(pm, i, nw=n) - - constraint_mc_voltage_angle_difference(pm, i, nw=n) - - constraint_mc_thermal_limit_from(pm, i, nw=n) - constraint_mc_thermal_limit_to(pm, i, nw=n) - end - - - for i in _PMs.ids(pm, :transformer, nw=n) - constraint_mc_trans(pm, i, nw=n) - end - - # for i in ids(pm, :dcline, nw=n) - # constraint_mc_dcline(pm, i, nw=n) - # end - - for i in _PMs.ids(pm, :storage; nw=n) - # _PMs.constraint_storage_state(pm, i; nw=n) - _PMs.constraint_storage_complementarity_mi(pm, i; nw=n) - constraint_mc_storage_loss(pm, i; nw=n) - constraint_mc_storage_thermal_limit(pm, i; nw=n) - constraint_mc_storage_on_off(pm, i; nw=n) +"Constructor for Optimal Power Flow" +function _build_mc_mn_opb(pm::_PM.AbstractPowerModel) + for (n, network) in nws(pm) + variable_mc_gen_power_setpoint(pm; nw=n) + for i in ids(pm, :components, nw=n) + constraint_mc_network_power_balance(pm, i; nw=n) end end - network_ids = sort(collect(_PMs.nw_ids(pm))) - - n_1 = network_ids[1] - for i in _PMs.ids(pm, :storage, nw=n_1) - _PMs.constraint_storage_state(pm, i, nw=n_1) - end - - for n_2 in network_ids[2:end] - for i in _PMs.ids(pm, :storage, nw=n_2) - _PMs.constraint_storage_state(pm, i, n_1, n_2) - end - n_1 = n_2 - end - _PMs.objective_min_fuel_cost(pm) -end - - -"" -function _run_mn_mc_opf(file, model_type::Type, optimizer; kwargs...) - return _PMs.run_model(file, model_type, optimizer, _build_mn_mc_opf; ref_extensions=[ref_add_arcs_trans!], multinetwork=true, multiconductor=true, kwargs...) -end - -"" -function _build_mn_mc_opf(pm::_PMs.AbstractPowerModel) - for (n, network) in _PMs.nws(pm) - variable_mc_voltage(pm, nw=n) - variable_mc_branch_flow(pm, nw=n) - variable_mc_transformer_flow(pm, nw=n) - variable_mc_generation(pm, nw=n) - - constraint_mc_model_voltage(pm, nw=n) - - for i in _PMs.ids(pm, :ref_buses, nw=n) - constraint_mc_theta_ref(pm, i, nw=n) - end - - for i in _PMs.ids(pm, :bus, nw=n) - constraint_mc_power_balance(pm, i, nw=n) - - end - - for i in _PMs.ids(pm, :branch, nw=n) - constraint_mc_ohms_yt_from(pm, i, nw=n) - constraint_mc_ohms_yt_to(pm, i, nw=n) - - constraint_mc_voltage_angle_difference(pm, i, nw=n) - - constraint_mc_thermal_limit_from(pm, i, nw=n) - constraint_mc_thermal_limit_to(pm, i, nw=n) - end - - - for i in _PMs.ids(pm, :transformer, nw=n) - constraint_mc_trans(pm, i, nw=n) - end - - # for i in ids(pm, :dcline, nw=n) - # constraint_mc_dcline(pm, i, nw=n) - # end - end - _PMs.objective_min_fuel_cost(pm) -end - - -"" -function _run_mn_mc_opf_strg(file, model_type::Type, optimizer; kwargs...) - return _PMs.run_model(file, model_type, optimizer, _build_mn_mc_opf_strg; ref_extensions=[ref_add_arcs_trans!], multinetwork=true, multiconductor=true, kwargs...) -end - -"warning: this model is not realistic or physically reasonable, it is only for test coverage" -function _build_mn_mc_opf_strg(pm::_PMs.AbstractPowerModel) - - for (n, network) in _PMs.nws(pm) - variable_mc_voltage(pm, nw=n) - variable_mc_branch_flow(pm, nw=n) - variable_mc_transformer_flow(pm, nw=n) - variable_mc_generation(pm, nw=n) - variable_mc_load(pm, nw=n) - - variable_mc_storage(pm, nw=n) - - constraint_mc_model_voltage(pm, nw=n) - - for i in _PMs.ids(pm, :ref_buses, nw=n) - constraint_mc_theta_ref(pm, i, nw=n) - end - - # generators should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :gen) - constraint_mc_generation(pm, id, nw=n) - end - - # loads should be constrained before KCL, or Pd/Qd undefined - for id in _PMs.ids(pm, :load) - constraint_mc_load(pm, id, nw=n) - end - - for i in _PMs.ids(pm, :bus, nw=n) - constraint_mc_power_balance_load(pm, i, nw=n) - - end - - for i in _PMs.ids(pm, :branch, nw=n) - constraint_mc_ohms_yt_from(pm, i, nw=n) - constraint_mc_ohms_yt_to(pm, i, nw=n) - - constraint_mc_voltage_angle_difference(pm, i, nw=n) - - constraint_mc_thermal_limit_from(pm, i, nw=n) - constraint_mc_thermal_limit_to(pm, i, nw=n) - end - - - for i in _PMs.ids(pm, :transformer, nw=n) - constraint_mc_trans(pm, i, nw=n) - end - - for i in _PMs.ids(pm, :storage, nw=n) - _PMs.constraint_storage_complementarity_nl(pm, i; nw=n) - constraint_mc_storage_loss(pm, i; nw=n) - constraint_mc_storage_thermal_limit(pm, i, nw=n) - end - - # for i in ids(pm, :dcline, nw=n) - # constraint_mc_dcline(pm, i, nw=n) - # end - - end - network_ids = sort(collect(_PMs.nw_ids(pm))) - - n_1 = network_ids[1] - for i in _PMs.ids(pm, :storage, nw=n_1) - _PMs.constraint_storage_state(pm, i, nw=n_1) - end - - for n_2 in network_ids[2:end] - for i in _PMs.ids(pm, :storage, nw=n_2) - _PMs.constraint_storage_state(pm, i, n_1, n_2) - end - n_1 = n_2 - end - _PMs.objective_min_fuel_cost(pm) + _PM.objective_min_fuel_cost(pm) end diff --git a/test/common.jl b/test/common.jl index a9cd8d2fd..0dfef0c56 100644 --- a/test/common.jl +++ b/test/common.jl @@ -16,17 +16,28 @@ end bus_name2id(pmd_data, name) = [bus["index"] for (_,bus) in pmd_data["bus"] if haskey(bus, "name") && bus["name"]==name][1] va(sol, pmd_data, name) = PMD._wrap_to_pi(sol["solution"]["bus"][string(bus_name2id(pmd_data, name))]["va"][:])*180/pi vm(sol, pmd_data, name) = sol["solution"]["bus"][string(bus_name2id(pmd_data, name))]["vm"] -tap(i, pm) = JuMP.value.(PMs.var(pm, pm.cnw, :tap)[i]) +tap(i, pm) = JuMP.value.(PM.var(pm, pm.cnw, :tap)[i]) vi(sol, pmd_data, name) = sol["solution"]["bus"][string(bus_name2id(pmd_data, name))]["vi"] vr(sol, pmd_data, name) = sol["solution"]["bus"][string(bus_name2id(pmd_data, name))]["vr"] calc_vm_acr(sol, pmd_data, name) = sqrt.(vi(sol, pmd_data, name).^2 .+ vr(sol, pmd_data, name).^2) calc_va_acr(sol, pmd_data, name) = rad2deg.(PMD._wrap_to_pi(atan.(vi(sol, pmd_data, name), vr(sol, pmd_data, name)))) +# Helper functions adjusted for eng model +vi(sol, name) = sol["solution"]["bus"][name]["vi"] +vr(sol, name) = sol["solution"]["bus"][name]["vr"] +calc_vm_acr(sol, name) = sqrt.(vi(sol, name).^2 .+ vr(sol, name).^2) +calc_va_acr(sol, name) = rad2deg.(PMD._wrap_to_pi(atan.(vi(sol, name), vr(sol, name)))) +va(sol, name) = PMD._wrap_to_pi(sol["solution"]["bus"][name]["va"][:])*180/pi +vm(sol, name) = sol["solution"]["bus"][name]["vm"] +pd(sol, name) = sol["solution"]["load"][name]["pd_bus"] +qd(sol, name) = sol["solution"]["load"][name]["qd_bus"] + + # Helper functions for load models tests load_name2id(pmd_data, name) = [load["index"] for (_,load) in pmd_data["load"] if haskey(load, "name") && load["name"]==name][1] -pdvar(pm, pmd_data, name) = [PMs.var(pm, pm.cnw, c, :pd, load_name2id(pmd_data, name)) for c in 1:3] +pdvar(pm, pmd_data, name) = [PM.var(pm, pm.cnw, c, :pd, load_name2id(pmd_data, name)) for c in 1:3] pd(sol, pmd_data, name) = sol["solution"]["load"][string(load_name2id(pmd_data, name))]["pd_bus"] -qdvar(pm, pmd_data, name) = [PMs.var(pm, pm.cnw, c, :qd, load_name2id(pmd_data, name)) for c in 1:3] +qdvar(pm, pmd_data, name) = [PM.var(pm, pm.cnw, c, :qd, load_name2id(pmd_data, name)) for c in 1:3] qd(sol, pmd_data, name) = sol["solution"]["load"][string(load_name2id(pmd_data, name))]["qd_bus"] sd(pm, pmd_data, name) = pd(sol, pmd_data, name)+im*qd(sol, pmd_data, name) @@ -35,14 +46,14 @@ calc_vm_W(result, id) = sqrt.(diag( result["solution"]["bus"][id]["Wr"])) function build_mc_data!(base_data; conductors::Int=3) mp_data = PowerModels.parse_file(base_data) - PMD.make_multiconductor!(mp_data, conductors) + make_multiconductor!(mp_data, conductors) return mp_data end function build_mn_mc_data!(base_data; replicates::Int=3, conductors::Int=3) mp_data = PowerModels.parse_file(base_data) - PMD.make_multiconductor!(mp_data, conductors) + make_multiconductor!(mp_data, conductors) mn_mc_data = PowerModels.replicate(mp_data, replicates) mn_mc_data["conductors"] = mn_mc_data["nw"]["1"]["conductors"] return mn_mc_data @@ -56,11 +67,11 @@ function build_mn_mc_data!(base_data_1, base_data_2; conductors_1::Int=3, conduc @assert mp_data_1["per_unit"] == mp_data_2["per_unit"] if conductors_1 > 0 - PMD.make_multiconductor!(mp_data_1, conductors_1) + make_multiconductor!(mp_data_1, conductors_1) end if conductors_2 > 0 - PMD.make_multiconductor!(mp_data_2, conductors_2) + make_multiconductor!(mp_data_2, conductors_2) end mn_data = Dict( diff --git a/test/data.jl b/test/data.jl index 5285428b7..5b7261022 100644 --- a/test/data.jl +++ b/test/data.jl @@ -4,7 +4,7 @@ branch = Dict{String, Any}() branch["br_r"] = [1 2;3 4] branch["br_x"] = [1 2;3 4] - g,b = PMs.calc_branch_y(branch) + g,b = PM.calc_branch_y(branch) @test typeof(g) <: Matrix @test isapprox(g, [-1.0 0.5; 0.75 -0.25]) @@ -12,7 +12,7 @@ branch["br_r"] = [1 2 0;3 4 0; 0 0 0] branch["br_x"] = [1 2 0;3 4 0; 0 0 0] - g,b = PMs.calc_branch_y(branch) + g,b = PM.calc_branch_y(branch) @test typeof(g) <: Matrix @test isapprox(g, [-1.0 0.5 0; 0.75 -0.25 0; 0 0 0]) @@ -70,13 +70,15 @@ end end @testset "node counting functions" begin - dss = PMD.parse_dss("../test/data/opendss/case5_phase_drop.dss") - pmd = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") + dss = parse_dss("../test/data/opendss/case5_phase_drop.dss") + eng = parse_file("../test/data/opendss/case5_phase_drop.dss") + math = parse_file("../test/data/opendss/case5_phase_drop.dss"; data_model=MATHEMATICAL) - @test count_nodes(dss) == 7 - @test count_nodes(dss) == count_nodes(pmd) + @test count_nodes(dss) == 10 # stopped excluding source from node count + @test count_nodes(dss) == count_nodes(eng) + @test count_nodes(eng) == count_nodes(math) - dss = PMD.parse_dss("../test/data/opendss/ut_trans_2w_yy.dss") - @test count_nodes(dss) == 9 + dss = parse_dss("../test/data/opendss/ut_trans_2w_yy.dss") + @test count_nodes(dss) == 12 # stopped excluding source from node count end end diff --git a/test/data/opendss/case3_balanced.dss b/test/data/opendss/case3_balanced.dss index 4ee15474c..e920f9ac2 100755 --- a/test/data/opendss/case3_balanced.dss +++ b/test/data/opendss/case3_balanced.dss @@ -23,10 +23,11 @@ New Line.OHLine bus1=sourcebus.1.2.3 Primary.1.2.3 linecode = 556MCM length New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=1 ! 100 ft !Loads - single phase +New Loadshape.ls1 pmult=(file=load_profile.csv) -New Load.L1 phases=1 loadbus.1.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 -New Load.L2 phases=1 loadbus.2.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 -New Load.L3 phases=1 loadbus.3.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 +New Load.L1 phases=1 loadbus.1.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 daily=ls1 +New Load.L2 phases=1 loadbus.2.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 daily=ls1 +New Load.L3 phases=1 loadbus.3.0 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 daily=ls1 Set voltagebases=[0.4] diff --git a/test/data/opendss/case3_balanced_prop-order.dss b/test/data/opendss/case3_balanced_prop-order.dss index e08fab186..4d27068bc 100644 --- a/test/data/opendss/case3_balanced_prop-order.dss +++ b/test/data/opendss/case3_balanced_prop-order.dss @@ -19,7 +19,7 @@ New linecode.4/0QUAD nphases=3 basefreq=50 ! ohms per 100ft !Define lines New Line.OHLine bus1=sourcebus.1.2.3 Primary.1.2.3 linecode = 556MCM rmatrix=( 0.1000 | 0.0400 0.1000 | 0.0400 0.0400 0.1000) length=1 ! 5 mile line -New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 like=OHLine linecode = 4/0QUAD length=1 ! 100 ft +New Line.Quad like=OHLine Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=1 ! 100 ft !Loads - single phase diff --git a/test/data/opendss/case3_lm_1230.dss b/test/data/opendss/case3_lm_1230.dss index 3749ffe35..46757e89e 100644 --- a/test/data/opendss/case3_lm_1230.dss +++ b/test/data/opendss/case3_lm_1230.dss @@ -25,30 +25,30 @@ New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=0 !Loads - single phase ! single-phase loads -New Load.d1ph phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d1ph phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 ! if only one is specified, .0 is added -New Load.d1ph2 phases=1 loadbus.2 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d1ph2 phases=1 loadbus.2 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 ! when connected between two phases, acts as delta load in the PMD sense New Load.d1ph23 phases=1 loadbus.2.3 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d1ph0 phases=1 loadbus.0 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d1ph00 phases=1 loadbus.0.0 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d1ph0 phases=1 loadbus.0 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d1ph00 phases=1 loadbus.0.0 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 ! This specification will now lead to an error ! New Load.d1ph123 phases=1 loadbus.0.1.2.3 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 ! This specification will now lead to an error ! New Load.y1ph phases=1 loadbus kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 New Load.y1ph2 phases=1 loadbus.2 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 -New Load.y1ph23 phases=1 loadbus.2.3 kv=0.4 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 -New Load.y1ph0 phases=1 loadbus.0 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 -New Load.y1ph00 phases=1 loadbus.0.0 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 +! New Load.y1ph23 phases=1 loadbus.2.3 kv=0.4 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 +! New Load.y1ph0 phases=1 loadbus.0 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 +! New Load.y1ph00 phases=1 loadbus.0.0 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 ! This specification will now lead to an error ! New Load.y1ph123 phases=1 loadbus.0.1.2.3 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 ! three-phase loads New Load.d3ph phases=3 loadbus kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 New Load.d3ph123 phases=3 loadbus.1.2.3 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d3ph1230 phases=3 loadbus.1.2.3.0 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d3ph1230 phases=3 loadbus.1.2.3.0 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 New Load.d3ph213 phases=3 loadbus.2.1.3 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d3ph3120 phases=3 loadbus.3.1.2.0 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 +! New Load.d3ph3120 phases=3 loadbus.3.1.2.0 kv=0.4 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 ! note change kv New Load.y3ph phases=3 loadbus kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 New Load.y3ph123 phases=3 loadbus.1.2.3 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 diff --git a/test/data/opendss/case3_lm_models.dss b/test/data/opendss/case3_lm_models.dss index de59ebb85..3ada49a21 100644 --- a/test/data/opendss/case3_lm_models.dss +++ b/test/data/opendss/case3_lm_models.dss @@ -24,10 +24,12 @@ New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=0 !Loads - single phase +! kept only to not change the numerical results; single-phase delta-loads should be specified with two connections +New Load.d1phm1 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 +New Load.d1phm2 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=2 conn=wye vminpu=0.7 vmaxpu=1.3 +New Load.d1phm5 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=5 conn=wye vminpu=0.7 vmaxpu=1.3 + ! single-phase loads -New Load.d1phm1 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=1 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d1phm2 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=2 conn=delta vminpu=0.7 vmaxpu=1.3 -New Load.d1phm5 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=5 conn=delta vminpu=0.7 vmaxpu=1.3 New Load.y1phm1 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=1 conn=wye vminpu=0.7 vmaxpu=1.3 New Load.y1phm2 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=2 conn=wye vminpu=0.7 vmaxpu=1.3 New Load.y1phm5 phases=1 loadbus.1 kv=0.23 kW=400 kvar=300 model=5 conn=wye vminpu=0.7 vmaxpu=1.3 diff --git a/test/data/opendss/test2_Linecodes.dss b/test/data/opendss/test2_Linecodes.dss index 92341b39c..31ba114f9 100644 --- a/test/data/opendss/test2_Linecodes.dss +++ b/test/data/opendss/test2_Linecodes.dss @@ -30,4 +30,6 @@ New Linecode.lc8 nphases=3 units=km b1=(3.4 2 pi 60.0 * * *) New Linecode.lc9 nphases=3 b1=(3.4 2 pi 60.0 * * *) b0=(1.6 2 pi 60 * * *) -new linecode.lc10 like=lc/2 \ No newline at end of file +new linecode.lc10 like=lc/2 + +new linecode.300 like=lc/2 \ No newline at end of file diff --git a/test/data/opendss/test2_master.dss b/test/data/opendss/test2_master.dss index 963dbd3aa..969893924 100644 --- a/test/data/opendss/test2_master.dss +++ b/test/data/opendss/test2_master.dss @@ -10,37 +10,37 @@ Redirect test2_Linecodes.dss Compile test2_Loadshape.dss ! Lines -New Line.L1 bus1=b1 bus2=_b2 length=0.001 units=mi r1=0.001 r0=0.001 x1=0.01 x0=0.01 c1=0 c0=0 rg=0.01805 xg=0.155081 like=something +New Line.L1 like=something bus1=b1 bus2=_b2 length=0.001 units=mi r1=0.001 r0=0.001 x1=0.01 x0=0.01 c1=0 c0=0 rg=0.01805 xg=0.155081 New Line.L1-2 bus1=b3-1.2 bus2=b4.2 length=0.032175613 units=km Linecode=lc1 New Line.L2 bus1=b5 bus2=b6_check-chars length=0.013516796 units=none linecode=lc/2 New "Line.L3" phases=3 bus1=b7.1.2.3 bus2=b9.1.2.3 linecode=300 normamps=400 emergamps=600 faultrate=0.1 pctperm=20 repair=0 length=2.58 New "Line._L4" phases=3 bus1=b8.1.2.3 bus2=b10.1.2.3 linecode=300 normamps=400 emergamps=600 faultrate=0.1 pctperm=20 repair=0 length=1.73 switch=y -New line.l5 phases=3 bus1=_b2.1.2.3.0 bus2=b7.1.2.3.0 linecode=lc8 +New line.l5 phases=3 bus1=_b2.1.2.3 bus2=b7.1.2.3 linecode=lc8 New line.l6 phases=3 bus1=b1.1.2.3 bus2=b10.1.2.3 linecode=lc9 -new line.l7 bus1=_b2 bus2=b10 like=L2 linecode=lc10 +new line.l7 like=L2 bus1=_b2 bus2=b10 linecode=lc10 test_param=100.0 ! Loads -New Load.ld1 phases=1 Bus1=b7.1.2 kv=0.208 status=variable model=1 conn=wye kW=3.89 pf=0.97 Vminpu=.88 +New Load.ld1 phases=1 Bus1=b7.1.0 kv=0.208 status=variable model=1 conn=wye kW=3.89 pf=0.97 Vminpu=.88 New "Load.ld2" bus1=b9 phases=3 conn=Wye model=5 kV=24.9 kW=27 kvar=21 Vminpu=.85 -New "Load.ld3" bus1=b10 phases=3 conn=Wye model=2 kV=24.9 kW=405 kvar=315 Vminpu=.85 yearly=(sngfile=load_profile.sng) -new load.ld4 bus1=b1 like=ld2 -New "Load.ld2" bus1=b9 phases=3 conn=Wye model=5 kV=24.9 kW=27 kvar=21 Vminpu=.85 daily=(file=load_profile.csv) +New "Load.ld3" bus1=b10 phases=3 conn=Wye model=2 kV=24.9 kW=405 kvar=315 Vminpu=.85 yearly=ld3_yearly +new load.ld4 like=ld2 bus1=b1 +New "Load.ld2" bus1=b9 phases=3 conn=Wye model=5 kV=24.9 kW=27 kvar=21 Vminpu=.85 daily=ld2_daily ! Capacitors New "Capacitor.c1" bus1=b5 phases=3 kvar=[ 250] kv=20.0 New "Capacitor.c2" bus1=b8 phases=3 kvar=[ 500] kv=25.0 -new capacitor.c3 bus1=b1 like=c2 +new capacitor.c3 like=c2 bus1=b1 ! Reactors -New Reactor.reactor1 bus1=testsource bus2=b1 r=0 x=(1.05 0.75 0.001 5 * - - 125 15.0 / sqr *) normamps=400 emergamps=600 -new reactor.reactor2 bus1=_b2 bus2=b10 like=reactor1 +New Reactor.reactor1 bus1=testsource bus2=b1 r=0 x=(1.05 0.75 0.001 5 * - - 125 15.0 / sqr *) normamps=400 emergamps=600 +new reactor.reactor2 like=reactor1 bus1=_b2 bus2=b10 new reactor.reactor3 bus1=b9 kvar=10.0 -new reactor.reactor4 bus1=b8 like=reactor3 +new reactor.reactor4 like=reactor3 bus1=b8 ! Transformers -New "Transformer.t1" phases=1 windings=2 buses=[testsource, _b2, ] conns=[wye, wye, ] kVs=[15.0, 15.0, ] kVAs=[50000, 50000, ] Xhl=1 +New "Transformer.t1" phases=1 buses=[testsource, _b2, ] xfmrcode=t1 Xhl=1 New "Transformer.t2" phases=3 windings=2 Xhl=0.02 buses=[testsource, b3-1, ] conns=[delta, wye, ] kVs=[69, 24.9, ] kVAs=[25000, 25000, ] taps=[1, 1, ] wdg=1 %R=0.0005 wdg=2 %R=0.0005 -New Transformer.t3a phases=1 windings=2 buses=(b7.1, b10.1) conns=(wye, wye) kvs=(10.0, 10) kvas=(30000, 30000) xhl=0.1 %loadloss=.002 wdg=2 Maxtap=1.05 Mintap=0.95 ppm=0 +New Transformer.t3a phases=1 buses=(b7.1, b10.1) xfmrcode=t1 kvs=(10.0, 10) kvas=(30000, 30000) xhl=0.1 %loadloss=.002 wdg=2 Maxtap=1.05 Mintap=0.95 ppm=0 New Transformer.t4 phases=3 windings=2 buses=(b8, b9.1.2.3.0) rneut=0 xneut=0 ~ conns=(delta wye) ! 14.7->115 for kvs[2], otherwise inconsistent voltage base @@ -48,17 +48,17 @@ New Transformer.t4 phases=3 windings=2 buses=(b8, b9.1.2.3.0) rneut=0 xneut=0 ~ xhl=20.5 sub=y subname=t4_sub ~ wdg=1 %r=0.75000 ~ wdg=2 %r=0.75000 -new transformer.t5 buses=(b3-1, b5) like=t4 +new transformer.t5 like=t4 buses=(b3-1, b5) ! Generators -New Generator.g1 Bus1=b1 kV= 150 kW=1 Model=3 Vpu=1.05 Maxkvar=70000 Minkvar=10000 ! kvar=50000 -New Generator.g2 Bus1=testsource kV= 120 kW=1 Phases=1 Model=3 Vpu=1.05 Maxkvar=70000 Minkvar=10000 ! kvar=60000 -new generator.g3 bus1=b7 like=g2 +New Generator.g1 Bus1=b1 kV= 150 kW=1 Model=3 Vpu=1.05 Maxkvar=70000 Minkvar=10000 ! kvar=50000 +New Generator.g2 Bus1=testsource.1 kV= 120 kW=1 Phases=1 Model=3 Vpu=1.05 Maxkvar=70000 Minkvar=10000 ! kvar=60000 +new generator.g3 like=g2 bus1=b7.1 phases=1 Set voltagebases=[115, 12.47, 0.48, 0.208] -Load.s860.vminpu=.85 -Load.s840.vminpu=.85 +Load.ld1.vminpu=.85 +Load.ld3.vminpu=.85 Load.s844.vminpu=.85 Transformer.t1.wdg=2 Tap=(0.01000 15 * 1 +) ! Tap @@ -70,3 +70,13 @@ Solve show taps new Line.l8 bus1=b10 bus2=b11 linecode=lc/2 units=mi length=(0.013516796 1 1.6093 / *) + +new xycurve.test_curve1 xarray=(file=load_profile.csv) yarray=(sngfile=load_profile.sng) +new xycurve.test_curve2 points=[1 1 2 2 3 3] +new xycurve.test_curve3 csvfile=load_profile_pq.csv +new xycurve.test_curve4 dblfile=load_profile_interval.dbl + +new xfmrcode.t1 phases=1 windings=2 conns=[wye,wye] kvs=[15, 15] kvas=[50000 50000] + +new loadshape.ld3_yearly pmult=(sngfile=load_profile.sng) +new loadshape.ld2_daily pmult=(file=load_profile.csv) \ No newline at end of file diff --git a/test/data/opendss/test_bus_discovery.dss b/test/data/opendss/test_bus_discovery.dss new file mode 100644 index 000000000..7908a74a5 --- /dev/null +++ b/test/data/opendss/test_bus_discovery.dss @@ -0,0 +1,29 @@ +new circuit.test_islands + +new line.1 bus1=1 bus2=2 + +new transformer.1 windings=3 buses=[3 4 5] + +new reactor.1 bus1=6 bus2=7 + +new capacitor.1 bus1=8 bus2=9 + +new gictransformer.1 bush=10 busx=11 + +new gicline.1 bus1=12 bus2=13 + +new vsource.1 bus1=14 bus2=15 + +new fault.1 bus1=16 bus2=17 + +new isource.1 bus1=18 + +new load.1 bus1=19 + +new generator.1 bus1=20 + +new indmach012.1 bus1=21 + +new storage.1 bus1=22 + +new pvsystem.1 bus1=23 diff --git a/test/data/opendss/test_transformer_formatting.dss b/test/data/opendss/test_transformer_formatting.dss index c86bdf9b1..a9c5afbb8 100644 --- a/test/data/opendss/test_transformer_formatting.dss +++ b/test/data/opendss/test_transformer_formatting.dss @@ -13,5 +13,5 @@ transformer.transformer_test.wdg=1 tap=(0.00625 12 * 1 +) transformer.transformer_test.wdg=3 tap=0.9 bus=1.1.2.3 new transformer.reg4a phases=1 windings=2 bank=reg4 buses=[1.1 2.1] conns=[wye wye] kvs=[2.402 2.402] kvas=[2000 2000] XHL=.01 %LoadLoss=0.00001 ppm=0.0 -new transformer.reg4b like=reg4a bank=reg4 buses=[1.2 2.2] ppm=0.0 -new transformer.reg4c like=reg4a bank=reg4 buses=[1.3 2.3] ppm=0.0 +new transformer.reg4b phases=1 like=reg4a bank=reg4 buses=[1.2 2.2] ppm=0.0 +new transformer.reg4c phases=1 like=reg4a bank=reg4 buses=[1.3 2.3] ppm=0.0 diff --git a/test/data/opendss/ut_trans_2w_yy_bank.dss b/test/data/opendss/ut_trans_2w_yy_bank.dss index a2709360c..c9fc535b4 100644 --- a/test/data/opendss/ut_trans_2w_yy_bank.dss +++ b/test/data/opendss/ut_trans_2w_yy_bank.dss @@ -7,41 +7,32 @@ Set DefaultBaseFrequency=60 New circuit.ut_trans ~ BasekV=11 BaseMVA=1 pu=1.0 ISC3=9999999999 ISC1=9999999999 -! Transformers -New Transformer.TX1 windings=2 phases=1 Buses=[1.1 2.1] -~ Conns=[Wye Wye] -~ kVs=[11 4] -~ kVAs=[500 500] +new xfmrcode.tx phases=1 +~ windings=2 +~ conns=[wye wye] +~ kvs=[11 4] +~ kvas=[500 500] ~ xhl=5 +~ leadlag=lead ~ %noloadloss=5 ~ %imag=11 -~ leadlag=lead -~ taps=[1.05 0.95] -~ bank=TX1 ~ wdg=1 %r=1 ~ wdg=2 %r=2 -New Transformer.TX1b windings=2 phases=1 Buses=[1.2 2.2] -~ Conns=[Wye Wye] -~ kVs=[11 4] -~ kVAs=[500 500] -~ %Rs=[1 2] -~ xhl=5 -~ %noloadloss=5 -~ %imag=11 -~ leadlag=lead + +! Transformers +New Transformer.TX1a Buses=[1.1 2.1] +~ xfmrcode=tx +~ taps=[1.05 0.95] +~ bank=TX1 + +New Transformer.TX1b Buses=[1.2 2.2] +~ xfmrcode=tx ~ taps=[1.02 0.97] ~ bank=TX1 -New Transformer.TX1c windings=2 phases=1 Buses=[1.3 2.3] -~ Conns=[Wye Wye] -~ kVs=[11 4] -~ kVAs=[500 500] -~ %Rs=[1 2] -~ xhl=5 -~ %noloadloss=5 -~ %imag=11 -~ leadlag=lead +New Transformer.TX1c Buses=[1.3 2.3] +~ xfmrcode=tx ~ taps=[1.10 0.90] ~ bank=TX1 diff --git a/test/data_model.jl b/test/data_model.jl new file mode 100644 index 000000000..7a7691dfe --- /dev/null +++ b/test/data_model.jl @@ -0,0 +1,79 @@ +@info "running data model creation and conversion tests" + +@testset "engineering data model" begin + @testset "helper functions for building engineering data model" begin + eng = Model() + + add_bus!(eng, "sourcebus"; terminals=[1,2,3,4], grounded=[4]) + add_bus!(eng, "primary"; terminals=[1,2,3]) + add_bus!(eng, "loadbus"; terminals=[1,2,3,4], grounded=[4]) + + add_voltage_source!(eng, "source", "sourcebus", [1,2,3,4]; vm=[1, 1, 1]) + + add_linecode!(eng, "default", diagm(0=>fill(0.01, 3)), diagm(0=>fill(0.2, 3))) + + add_line!(eng, "trunk", "sourcebus", "primary", [1,2,3], [1,2,3]; linecode="default") + add_line!(eng, "primary", "primary", "loadbus", [1,2,3], [1,2,3]; linecode="default") + + add_load!(eng, "balanced", "loadbus", [1,2,3,4]; pd_nom=[5, 5, 5], qd_nom=[1, 1, 1]) + + add_vbase_default!(eng, "sourcebus", 1) + + result = run_mc_opf(eng, ACPPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0.0150; atol=1e-4) + + eng2 = deepcopy(eng) + + add_bus!(eng2, "ttbus"; terminals=[1,2,3,4], grounded=[4]) + + add_transformer!(eng2, "tx1", "sourcebus", "ttbus", [1,2,3,4], [1,2,3,4]) + + add_bus!(eng2, "loadbus2"; terminals=[1,2,3,4], grounded=[4]) + + add_switch!(eng2, "breaker", "ttbus", "loadbus2", [1,2,3], [1,2,3]) + + add_load!(eng2, "tload", "loadbus2", [1,2,3,4]; pd_nom=[5, 5, 5], qd_nom=[1, 1, 1]) + + add_generator!(eng2, "secondary", "loadbus2", [1,2,3,4]; cost_pg_parameters=[0.0, 1.2, 0]) + + add_shunt!(eng2, "cap", "loadbus2", [1,2,3,4]; bs=diagm(0=>fill(1, 3))) + + result2 = run_mc_opf(eng2, ACPPowerModel, ipopt_solver) + + @test result2["termination_status"] == LOCALLY_SOLVED + @test isapprox(result2["objective"], -83.3003; atol=0.2) + end + + @testset "engineering model transformations" begin + eng = parse_file("../test/data/opendss/case3_balanced.dss"; transformations=[(apply_voltage_bounds!, "vm_ub"=>Inf)]) + + @test all(all(isapprox.(bus["vm_lb"], 0.4 / sqrt(3) * 0.9)) && all(isinf.(bus["vm_ub"])) for (id,bus) in eng["bus"] if id != "sourcebus") + + math = transform_data_model(eng) + + @test all(all(isapprox.(bus["vmin"], 0.9)) for (_,bus) in math["bus"] if bus["name"] != "sourcebus" && !startswith(bus["name"], "_virtual")) + + eng = parse_file("../test/data/opendss/ut_trans_2w_yy.dss"; transformations=[make_lossless!, (apply_voltage_bounds!, "vm_lb"=>0.95, "vm_ub"=>1.05)]) + + @test all(all(eng["transformer"]["tx1"][k] .== 0) for k in ["rw", "xsc", "noloadloss", "cmag"]) + + math = transform_data_model(eng) + + @test length(math["bus"]) == 5 + @test all(all(isapprox.(bus["vmin"], 0.95)) && all(isapprox.(bus["vmax"], 1.05)) for (_,bus) in math["bus"] if bus["name"] != "sourcebus" && !startswith(bus["name"], "_virtual")) + end + + @testset "jump model from engineering data model" begin + eng = parse_file("../test/data/opendss/case3_balanced.dss") + + pm_eng = instantiate_mc_model(eng, ACPPowerModel, build_mc_opf) + + math = transform_data_model(eng) + + pm_math = PowerModels.instantiate_model(math, ACPPowerModel, build_mc_opf; ref_extensions=[ref_add_arcs_transformer!]) + + @test sprint(print, pm_eng) == sprint(print, pm_math) + end +end diff --git a/test/debug.jl b/test/debug.jl index e9d964013..2c423d27e 100644 --- a/test/debug.jl +++ b/test/debug.jl @@ -2,10 +2,10 @@ @testset "test pbs" begin @testset "case 3 unbalanced - ac pf pbs" begin - mp_data = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - result = PMD.run_mc_pf_pbs(mp_data, PMs.ACPPowerModel, ipopt_solver) + mp_data = parse_file("../test/data/opendss/case3_unbalanced.dss") + result = run_mc_pf_pbs(mp_data, ACPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0; atol=1e-4) end end diff --git a/test/delta_gens.jl b/test/delta_gens.jl index 31a1ffa47..c0d5544f1 100644 --- a/test/delta_gens.jl +++ b/test/delta_gens.jl @@ -4,53 +4,53 @@ # This test checks the generators are connected properly by comparing them # to equivalent constant-power loads. This is achieved by fixing their bounds. @testset "ACP/ACR tests" begin - pmd_1 = PMD.parse_file("$pmd_path/test/data/opendss/case3_delta_gens.dss") + eng_1 = parse_file("../test/data/opendss/case3_delta_gens.dss") - # convert to constant power loads - for (_, load) in pmd_1["load"] - load["model"] = "constant_power" + for (_,load) in eng_1["load"] + load["model"] = POWER end - # create data model with equivalent generators - pmd_2 = deepcopy(pmd_1) - pmd_2["load"] = Dict() - gen2load = Dict() - for (i,(id, load)) in enumerate(pmd_1["load"]) - load = pmd_1["load"][id] - gen = deepcopy(pmd_1["gen"]["1"]) + for (_,line) in eng_1["line"] + line["cm_ub"] = fill(1e4, size(line["cm_ub"])...) + end + + eng_2 = deepcopy(eng_1) + eng_2["load"] = Dict{Any,Any}() + eng_2["generator"] = Dict{Any,Any}() + for (id,load) in eng_1["load"] + gen = Dict{String,Any}( + "source_id" => load["source_id"], + "configuration" => load["configuration"], + "bus" => load["bus"], + "connections" => load["connections"], + "cost_pg_parameters" => [0, 0, 0], + "control_mode" => FREQUENCYDROOP, + "pg_lb" => -load["pd_nom"], + "pg_ub" => -load["pd_nom"], + "qg_lb" => -load["qd_nom"], + "qg_ub" => -load["qd_nom"], + "status" => ENABLED, + ) - gen["index"] = i+1 - gen["cost"] *= 0 - gen["conn"] = load["conn"] - gen["pmax"] = gen["pmin"] = -load["pd"] - gen["qmin"] = gen["qmax"] = -load["qd"] - gen["gen_bus"] = load["load_bus"] - gen["model"] = 2 - gen2load["$(i+1)"] = id - pmd_2["gen"]["$(i+1)"] = gen + eng_2["generator"][id] = gen end # check ACP and ACR - for (form, build_method) in zip( - [PMs.ACPPowerModel, PMs.ACRPowerModel, PMs.IVRPowerModel], - [PMD.build_mc_opf, PMD.build_mc_opf, PMD.build_mc_opf_iv] - ) - pm_1 = PMs.instantiate_model(pmd_1, form, build_method, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol_1 = PMs.optimize_model!(pm_1, optimizer=ipopt_solver) - @assert(sol_1["termination_status"]==LOCALLY_SOLVED) + for form in [ACPPowerModel, ACRPowerModel, IVRPowerModel] + sol_1 = run_mc_opf(eng_1, form, ipopt_solver) + @test sol_1["termination_status"] == LOCALLY_SOLVED - pm_2 = PMs.instantiate_model(pmd_2, form, build_method, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol_2 = PMs.optimize_model!(pm_2, optimizer=ipopt_solver) - @assert(sol_2["termination_status"]==LOCALLY_SOLVED) + sol_2 = run_mc_opf(eng_2, form, ipopt_solver) + @test sol_2["termination_status"] == LOCALLY_SOLVED # check that gens are equivalent to the loads - for (gen, load) in gen2load - pd_bus = sol_1["solution"]["load"][load]["pd"] - qd_bus = sol_1["solution"]["load"][load]["qd"] - pg_bus = sol_2["solution"]["gen"][gen]["pg"] - qg_bus = sol_2["solution"]["gen"][gen]["qg"] - @test(isapprox(pd_bus, -pg_bus, atol=1E-5)) - @test(isapprox(qd_bus, -qg_bus, atol=1E-5)) + for (id,_) in eng_1["load"] + pd_bus = sol_1["solution"]["load"][id]["pd"] + qd_bus = sol_1["solution"]["load"][id]["qd"] + pg_bus = sol_2["solution"]["generator"][id]["pg"] + qg_bus = sol_2["solution"]["generator"][id]["qg"] + @test isapprox(pd_bus, -pg_bus, atol=1E-5) + @test isapprox(qd_bus, -qg_bus, atol=1E-5) end end end @@ -68,7 +68,7 @@ # # gen["index"] = i+1 # gen["cost"] *= 0.01 - # gen["conn"] = load["conn"] + # gen["configuration"] = load["configuration"] # gen["pmax"] = (-load["pd"])/10 # gen["pmin"] *= 0 # gen["qmin"] = -abs.(gen["pmax"])/10 @@ -77,12 +77,12 @@ # gen["model"] = 2 # end # - # pm_ivr = PMs.instantiate_model(pmd, PMs.IVRPowerModel, PMD.build_mc_opf_iv, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - # sol_ivr = PMs.optimize_model!(pm_ivr, optimizer=ipopt_solver) + # pm_ivr = PowerModels.instantiate_model(pmd, PowerModels.IVRPowerModel, PMD.build_mc_opf, ref_extensions=[PMD.ref_add_arcs_transformer!]) + # sol_ivr = PowerModels.optimize_model!(pm_ivr, optimizer=ipopt_solver) # @assert(sol_1["termination_status"]==LOCALLY_SOLVED) # - # pm_acr = PMs.instantiate_model(pmd, PMs.ACRPowerModel, PMD.build_mc_opf, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - # sol_acr = PMs.optimize_model!(pm_acr, optimizer=ipopt_solver) + # pm_acr = PowerModels.instantiate_model(pmd, PowerModels.ACRPowerModel, PMD.build_mc_opf, ref_extensions=[PMD.ref_add_arcs_transformer!]) + # sol_acr = PowerModels.optimize_model!(pm_acr, optimizer=ipopt_solver) # @assert(sol_2["termination_status"]==LOCALLY_SOLVED) # # end diff --git a/test/loadmodels.jl b/test/loadmodels.jl index c8375dd55..451b46d8d 100644 --- a/test/loadmodels.jl +++ b/test/loadmodels.jl @@ -2,152 +2,103 @@ @testset "test loadmodels pf" begin @testset "loadmodels connection variations" begin - pmd = PMD.parse_file("../test/data/opendss/case3_lm_1230.dss") - pm = PMs.instantiate_model(pmd, PMs.ACPPowerModel, PMD.build_mc_pf, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_lm_1230.dss") + sol = run_mc_pf(pmd, ACPPowerModel, ipopt_solver; make_si=false) # voltage magnitude at load bus - @test isapprox(vm(sol, pmd, "loadbus"), [0.999993, 0.999992, 0.999993], atol=1E-5) + @test isapprox(vm(sol, "loadbus"), [0.999993, 0.999992, 0.999993], atol=1E-5) # single-phase delta loads - @test isapprox(pd(sol, pmd, "d1ph"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1ph"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1ph2"), [0, 0.4000, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1ph2"), [0, 0.3000, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1ph23"), [0, 0.2866, 0.1134], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1ph23"), [0, 0.0345, 0.2655], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1ph0"), [0, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1ph0"), [0, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1ph00"), [0, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1ph00"), [0, 0, 0], atol=1E-4) - # Leads to an error now instead; order of conductors needed for this - # @test isapprox(pd(sol, pmd, "d1ph123"), [0.4000, 0, 0], atol=1E-4) - # @test isapprox(qd(sol, pmd, "d1ph123"), [0.3000, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "d1ph23"), [0, 0.2866, 0.1134], atol=1E-4) + @test isapprox(qd(sol, "d1ph23"), [0, 0.0345, 0.2655], atol=1E-4) # single-phase wye loads - # Leads to an error now instead; order of conductors needed for this - # @test isapprox(pd(sol, pmd, "y1ph"), [0.4000, 0, 0], atol=1E-4) - # @test isapprox(qd(sol, pmd, "y1ph"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1ph2"), [0, 0.4000, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1ph2"), [0, 0.3000, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1ph23"), [0, 0.2866, 0.1134], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1ph23"), [0, 0.0345, 0.2655], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1ph0"), [0, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1ph0"), [0, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1ph00"), [0, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1ph00"), [0, 0, 0], atol=1E-4) - # Leads to an error now instead; order of conductors needed for this - # @test isapprox(pd(sol, pmd, "y1ph123"), [0.4000, 0, 0], atol=1E-4) - # @test isapprox(qd(sol, pmd, "y1ph123"), [0.3000, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1ph2"), [0, 0.4000, 0], atol=1E-4) + @test isapprox(qd(sol, "y1ph2"), [0, 0.3000, 0], atol=1E-4) # three-phase loads - @test isapprox(pd(sol, pmd, "d3ph"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3ph"), [0.100, 0.100, 0.100], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3ph123"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3ph123"), [0.100, 0.100, 0.100], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3ph1230"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3ph1230"), [0.100, 0.100, 0.100], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3ph213"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3ph213"), [0.100, 0.100, 0.100], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3ph3120"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3ph3120"), [0.100, 0.100, 0.100], atol=1E-4) + @test isapprox(pd(sol, "d3ph"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "d3ph"), [0.100, 0.100, 0.100], atol=1E-4) + @test isapprox(pd(sol, "d3ph123"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "d3ph123"), [0.100, 0.100, 0.100], atol=1E-4) + @test isapprox(pd(sol, "d3ph213"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "d3ph213"), [0.100, 0.100, 0.100], atol=1E-4) end @testset "loadmodels 1/2/5 in acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_lm_models.dss") - pm = PMs.instantiate_model(pmd, PMs.ACPPowerModel, PMD.build_mc_pf, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_lm_models.dss") + sol = run_mc_pf(pmd, ACPPowerModel, ipopt_solver; make_si=false) # voltage magnitude at load bus - @test isapprox(vm(sol, pmd, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) + @test isapprox(vm(sol, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) # delta and wye single-phase load models - @test isapprox(pd(sol, pmd, "d1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm5"), [0.2502, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm5"), [0.2502, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm1"), [0.4000, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm1"), [0.3000, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm2"), [0.2783, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm2"), [0.2087, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm5"), [0.3336, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm5"), [0.2502, 0, 0], atol=1E-4) # delta three-phase loads - @test isapprox(pd(sol, pmd, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) + @test isapprox(pd(sol, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) + @test isapprox(qd(sol, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) + @test isapprox(pd(sol, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) + @test isapprox(qd(sol, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) + @test isapprox(pd(sol, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) + @test isapprox(qd(sol, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) # wye three-phase loads - @test isapprox(pd(sol, pmd, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) + @test isapprox(pd(sol, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) + @test isapprox(pd(sol, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) + @test isapprox(qd(sol, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) + @test isapprox(pd(sol, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) + @test isapprox(qd(sol, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) end @testset "loadmodels 1/2/5 in acr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_lm_models.dss") - pm = PMs.instantiate_model(pmd, PMs.ACRPowerModel, PMD.build_mc_pf, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_lm_models.dss") + sol = run_mc_pf(pmd, ACRPowerModel, ipopt_solver; make_si=false) # voltage magnitude at load bus - @test isapprox(calc_vm_acr(sol, pmd, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) + @test isapprox(calc_vm_acr(sol, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) # delta and wye single-phase load models - @test isapprox(pd(sol, pmd, "d1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm5"), [0.2502, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm5"), [0.2502, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm1"), [0.4000, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm1"), [0.3000, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm2"), [0.2783, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm2"), [0.2087, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm5"), [0.3336, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm5"), [0.2502, 0, 0], atol=1E-4) # delta three-phase loads - @test isapprox(pd(sol, pmd, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) + @test isapprox(pd(sol, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) + @test isapprox(qd(sol, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) + @test isapprox(pd(sol, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) + @test isapprox(qd(sol, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) + @test isapprox(pd(sol, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) + @test isapprox(qd(sol, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) # wye three-phase loads - @test isapprox(pd(sol, pmd, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) + @test isapprox(pd(sol, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) + @test isapprox(pd(sol, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) + @test isapprox(qd(sol, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) + @test isapprox(pd(sol, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) + @test isapprox(qd(sol, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) end @testset "loadmodels 1/2/5 in ivr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_lm_models.dss") - pm = PMs.instantiate_model(pmd, PMs.IVRPowerModel, PMD.build_mc_pf_iv, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_lm_models.dss") + sol = run_mc_pf(pmd, IVRPowerModel, ipopt_solver; make_si=false) # voltage magnitude at load bus - @test isapprox(calc_vm_acr(sol, pmd, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) + @test isapprox(calc_vm_acr(sol, "loadbus"), [0.83072, 0.99653, 1.0059], atol=1.5E-4) # delta and wye single-phase load models - @test isapprox(pd(sol, pmd, "d1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "d1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "d1phm5"), [0.2502, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm1"), [0.4000, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm1"), [0.3000, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm2"), [0.2783, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm2"), [0.2087, 0, 0], atol=1E-4) - @test isapprox(pd(sol, pmd, "y1phm5"), [0.3336, 0, 0], atol=1E-4) - @test isapprox(qd(sol, pmd, "y1phm5"), [0.2502, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm1"), [0.4000, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm1"), [0.3000, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm2"), [0.2783, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm2"), [0.2087, 0, 0], atol=1E-4) + @test isapprox(pd(sol, "y1phm5"), [0.3336, 0, 0], atol=1E-4) + @test isapprox(qd(sol, "y1phm5"), [0.2502, 0, 0], atol=1E-4) # delta three-phase loads - @test isapprox(pd(sol, pmd, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) - @test isapprox(pd(sol, pmd, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) - @test isapprox(qd(sol, pmd, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) + @test isapprox(pd(sol, "d3phm1"), [0.1160, 0.1465, 0.1375], atol=1E-4) + @test isapprox(qd(sol, "d3phm1"), [0.0896, 0.0977, 0.1127], atol=1E-4) + @test isapprox(pd(sol, "d3phm2"), [0.1005, 0.1348, 0.1212], atol=1E-4) + @test isapprox(qd(sol, "d3phm2"), [0.0771, 0.0854, 0.1048], atol=1E-4) + @test isapprox(pd(sol, "d3phm5"), [0.1080, 0.1405, 0.1291], atol=1E-4) + @test isapprox(qd(sol, "d3phm5"), [0.0831, 0.0914, 0.1087], atol=1E-4) # wye three-phase loads - @test isapprox(pd(sol, pmd, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) - @test isapprox(pd(sol, pmd, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) - @test isapprox(qd(sol, pmd, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) + @test isapprox(pd(sol, "y3phm1"), [0.1333, 0.1333, 0.1333], atol=1E-4) + @test isapprox(qd(sol, "y3phm1"), [0.1000, 0.1000, 0.1000], atol=1E-4) + @test isapprox(pd(sol, "y3phm2"), [0.0920, 0.1324, 0.1349], atol=1E-4) + @test isapprox(qd(sol, "y3phm2"), [0.0690, 0.0993, 0.1012], atol=1E-4) + @test isapprox(pd(sol, "y3phm5"), [0.1108, 0.1329, 0.1341], atol=1E-4) + @test isapprox(qd(sol, "y3phm5"), [0.0831, 0.0997, 0.1006], atol=1E-4) end end diff --git a/test/mld.jl b/test/mld.jl index 6d3b0133a..83bb2908b 100644 --- a/test/mld.jl +++ b/test/mld.jl @@ -1,11 +1,15 @@ @info "running minimum load delta (mld) tests" @testset "test mld" begin + case5 = PM.parse_file("$(pms_path)/test/data/matpower/case5.m"); make_multiconductor!(case5, 3) + case5_strg = PM.parse_file("$(pms_path)/test/data/matpower/case5_strg.m"); make_multiconductor!(case5_strg, 3) + case3_ml = PM.parse_file("../test/data/matpower/case3_ml.m"); make_multiconductor!(case3_ml, 3) + + ut_trans_2w_yy = PowerModelsDistribution.parse_file("../test/data/opendss/ut_trans_2w_yy.dss") @testset "5-bus acp mld" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld(mp_data, ACPPowerModel, ipopt_solver) + result = run_mc_mld(case5, ACPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.3377; atol = 1e-4) @test all(isapprox(result["solution"]["load"]["1"]["pd"], [3.0, 3.0, 3.0]; atol=1e-4)) @test all(isapprox(result["solution"]["load"]["1"]["qd"], [0.9861, 0.9861, 0.9861]; atol=1e-4)) @@ -14,120 +18,112 @@ end @testset "5-bus storage acp mld" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5_strg.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld(mp_data, ACPPowerModel, ipopt_solver) + result = run_mc_mld(case5_strg, ACPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(result["objective"], 0.3553; atol=1e-2) + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0.3432; atol=1e-2) @test isapprox(result["solution"]["load"]["1"]["status"], 1.000; atol=1e-3) end @testset "5-bus nfa mld" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) + result = run_mc_mld(case5, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.1557; atol = 1e-4) @test isapprox(result["solution"]["load"]["1"]["status"], 1.000; atol = 1e-3) end @testset "5-bus storage nfa mld" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5_strg.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) + result = run_mc_mld(case5_strg, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.1557; atol=1e-4) @test isapprox(result["solution"]["load"]["1"]["status"], 1.000; atol=1e-3) end @testset "3-bus nfa mld" begin - mp_data = PMs.parse_file("../test/data/matpower/case3_ml.m"); PMD.make_multiconductor!(mp_data, 3) + mp_data = PM.parse_file("../test/data/matpower/case3_ml.m"); make_multiconductor!(mp_data, 3) result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 24.9; atol = 1e-1) @test isapprox(result["solution"]["load"]["1"]["status"], 0.689; atol = 1e-3) end @testset "3-bus shunt nfa mld" begin - mp_data = PMs.parse_file("../test/data/matpower/case3_ml_s.m"); PMD.make_multiconductor!(mp_data, 3) + mp_data = PM.parse_file("../test/data/matpower/case3_ml_s.m"); make_multiconductor!(mp_data, 3) result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 22.2; atol = 1e-1) @test isapprox(result["solution"]["load"]["1"]["status"], 0.6; atol = 1e-3) end @testset "3-bus line charge nfa mld" begin - mp_data = PMs.parse_file("../test/data/matpower/case3_ml_lc.m"); PMD.make_multiconductor!(mp_data, 3) + mp_data = PM.parse_file("../test/data/matpower/case3_ml_lc.m"); make_multiconductor!(mp_data, 3) result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 43.8; atol = 1e-1) @test isapprox(result["solution"]["load"]["1"]["status"], 0.544; atol = 1e-3) end @testset "transformer nfa mld" begin - mp_data = PowerModelsDistribution.parse_file("../test/data/opendss/ut_trans_2w_yy.dss") - result = run_mc_mld(mp_data, NFAPowerModel, ipopt_solver) + result = run_mc_mld(ut_trans_2w_yy, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.411, atol = 1e-3) - @test isapprox(result["solution"]["load"]["1"]["status"], 1.0, atol = 1e-3) + @test isapprox(result["solution"]["load"]["load1"]["status"], 1.0, atol = 1e-3) end @testset "5-bus lpubfdiag mld" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld_bf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + result = run_mc_mld_bf(case5, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.1557; atol = 1e-4) @test isapprox(result["solution"]["load"]["1"]["status"], 1.000; atol = 1e-3) - @test_throws(TESTLOG, MethodError, run_mc_mld_bf(mp_data, NFAPowerModel, ipopt_solver)) + @test_throws(TESTLOG, MethodError, run_mc_mld_bf(case5, NFAPowerModel, ipopt_solver)) end @testset "3-bus lpubfdiag mld" begin - mp_data = PMs.parse_file("../test/data/matpower/case3_ml.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld_bf(mp_data, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + result = run_mc_mld_bf(case3_ml, LPUBFDiagPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 24.98; atol = 1e-1) @test isapprox(result["solution"]["load"]["1"]["status"], 0.313; atol = 1e-3) end @testset "transformer case" begin - dss = PowerModelsDistribution.parse_file("../test/data/opendss/ut_trans_2w_yy.dss") - result = run_mc_mld_bf(dss, LPUBFDiagPowerModel, ipopt_solver) + result = run_mc_mld_bf(ut_trans_2w_yy, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0; atol=1e-3) - @test isapprox(result["solution"]["load"]["1"]["status"], 1.0; atol=1e-3) + @test isapprox(result["solution"]["load"]["load1"]["status"], 1.0; atol=1e-3) end @testset "5-bus acp mld_uc" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld_uc(mp_data, ACPPowerModel, juniper_solver) + result = run_mc_mld_uc(case5, ACPPowerModel, juniper_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.338; atol=1e-3) @test all_gens_on(result) @test all_voltages_on(result) end @testset "5-bus nfa mld_uc" begin - mp_data = PMs.parse_file("$(pms_path)/test/data/matpower/case5.m"); PMD.make_multiconductor!(mp_data, 3) - result = run_mc_mld_uc(mp_data, NFAPowerModel, juniper_solver) + result = run_mc_mld_uc(case5, NFAPowerModel, juniper_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED for (i, gen) in result["solution"]["gen"] if i != "4" @test isapprox(gen["gen_status"], 1.0; atol=1e-5) diff --git a/test/multinetwork.jl b/test/multinetwork.jl index 63fa79349..5bdef23a5 100644 --- a/test/multinetwork.jl +++ b/test/multinetwork.jl @@ -1,27 +1,10 @@ @info "running multinetwork tests" @testset "test multinetwork" begin - # TODO: Add new Multinetwork tests after support for LoadShapes -# @testset "5-bus storage multinetwork acp opf_strg" begin -# mp_data = PowerModels.parse_file("../test/data/matpower/case5_strg.m") -# PMD.make_multiconductor!(mp_data, 3) -# mn_mp_data = PowerModels.replicate(mp_data, 5) + @testset "3-bus balanced multinetwork nfa opb" begin + eng_ts = parse_file("../test/data/opendss/case3_balanced.dss"; time_series="daily") + result_mn = PowerModelsDistribution._run_mc_mn_opb(eng_ts, NFAPowerModel, ipopt_solver) -# result = PMD._run_mn_mc_opf_strg(mn_mp_data, PowerModels.ACPPowerModel, ipopt_solver) - -# @test result["termination_status"] == PMs.LOCALLY_SOLVED -# @test isapprox(result["objective"], 2.64596e5; atol = 1e2) - -# for (n, network) in result["solution"]["nw"], c in 1:network["conductors"] -# # @test isapprox(network["storage"]["1"]["ps"][c], -0.012; atol = 1e-3) -# # @test isapprox(network["storage"]["1"]["qs"][c], -0.012; atol = 1e-3) -# @test isapprox(network["storage"]["1"]["ps"][c], -0.012; atol = 1e-1) -# # @test isapprox(network["storage"]["1"]["qs"][c], -0.012; atol = 1e-1) - -# # @test isapprox(network["storage"]["2"]["ps"][c], -0.016; atol = 1e-3) -# # @test isapprox(network["storage"]["2"]["qs"][c], 0.000; atol = 1e-3) -# @test isapprox(network["storage"]["2"]["ps"][c], -0.016; atol = 1e-1) -# @test isapprox(network["storage"]["2"]["qs"][c], 0.000; atol = 1e-1) -# end -# end + @test result_mn["termination_status"] == LOCALLY_SOLVED + end end diff --git a/test/opendss.jl b/test/opendss.jl index 8ed735d83..3881f1153 100644 --- a/test/opendss.jl +++ b/test/opendss.jl @@ -1,14 +1,19 @@ @info "running opendss parser tests" @testset "test opendss parser" begin + @testset "bus discovery parsing" begin + eng = parse_file("../test/data/opendss/test_bus_discovery.dss") + + @test length(eng["bus"]) == 24 + @test all(k in keys(eng["bus"]) for k in [["$i" for i in 1:23]..., "sourcebus"]) + end @testset "loadshape parsing" begin - dss = PMD.parse_dss("../test/data/opendss/loadshapes.dss") - PMD.parse_dss_with_dtypes!(dss, ["loadshape"]) + dss = parse_dss("../test/data/opendss/loadshapes.dss") loadshapes = Dict{String,Any}() - for ls in dss["loadshape"] - loadshapes[ls["name"]] = PMD._create_loadshape(ls["name"]; PMD._to_sym_keys(ls)...) + for (name, ls) in dss["loadshape"] + loadshapes[name] = PMD._create_loadshape(name; PMD._to_kwargs(ls)...) end @test isapprox(loadshapes["1"]["interval"], 1.0/60) @@ -17,16 +22,6 @@ @test all(haskey.([loadshapes["$i"] for i in [4, 6, 8]], "hour")) end - @testset "arrays from files" begin - dss = PMD.parse_file("../test/data/opendss/test2_master.dss"; import_all=true) - - @test isa(dss["load"]["3"]["yearly"], Array) - @test isa(dss["load"]["4"]["daily"], Array) - - @test length(dss["load"]["3"]["yearly"]) == 10 - @test length(dss["load"]["4"]["daily"]) == 10 - end - @testset "reverse polish notation" begin @test isapprox(PMD._parse_rpn("2 pi * 60 * .001 *"), 2 * pi * 60 * .001; atol=1e-12) @test isapprox(PMD._parse_rpn("(14.4 13.8 / sqr 300 *"), (14.4 / 13.8)^2 * 300; atol=1e-12) @@ -50,20 +45,12 @@ Memento.setlevel!(TESTLOG, "error") end - @testset "opendss parse load model errors" begin - for load in 1:5 - dss = PMD.parse_dss("../test/data/opendss/loadparser_error.dss") - dss["load"] = [dss["load"][load]] - @test_throws(TESTLOG, ErrorException, PMD.parse_opendss(dss)) - end - end - @testset "opendss parse load model warnings" begin for model in [3, 4, 7, 8] - dss = PMD.parse_dss("../test/data/opendss/loadparser_warn_model.dss") - dss["load"] = [l for l in dss["load"] if l["name"]=="d1phm$model"] + dss = parse_dss("../test/data/opendss/loadparser_warn_model.dss") + dss["load"] = Dict{String,Any}((n,l) for (n,l) in dss["load"] if n=="d1phm$model") Memento.setlevel!(TESTLOG, "info") - @test_warn(TESTLOG, ": load model $model not supported. Treating as model 1.", PMD.parse_opendss(dss)) + @test_warn(TESTLOG, ": dss load model $model not supported. Treating as constant POWER model", parse_opendss(dss)) Memento.setlevel!(TESTLOG, "error") end end @@ -72,173 +59,115 @@ @testset "opendss parse generic warnings and errors" begin Memento.setlevel!(TESTLOG, "info") - @test_warn(TESTLOG, "Not all OpenDSS features are supported, currently only minimal support for lines, loads, generators, and capacitors as shunts. Transformers and reactors as transformer branches are included, but value translation is not fully supported.", - PMD.parse_file("../test/data/opendss/test_simple.dss")) - - Memento.TestUtils.@test_log(TESTLOG, "info", "Calling parse_dss on ../test/data/opendss/test_simple.dss", - PMD.parse_file("../test/data/opendss/test_simple.dss")) - - Memento.TestUtils.@test_log(TESTLOG, "info", "Done parsing ../test/data/opendss/test_simple.dss", - PMD.parse_file("../test/data/opendss/test_simple.dss")) - @test_throws(TESTLOG, ErrorException, - PMD.parse_file("../test/data/opendss/test_simple2.dss")) - - @test_throws(TESTLOG, ErrorException, - PMD.parse_file("../test/data/opendss/test_simple2.dss")) + parse_file("../test/data/opendss/test_simple2.dss"; data_model=MATHEMATICAL)) @test_warn(TESTLOG, "Command \"solve\" on line 69 in \"test2_master.dss\" is not supported, skipping.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + parse_file("../test/data/opendss/test2_master.dss")) @test_warn(TESTLOG, "Command \"show\" on line 71 in \"test2_master.dss\" is not supported, skipping.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + parse_file("../test/data/opendss/test2_master.dss")) - @test_warn(TESTLOG, "reactors as constant impedance elements is not yet supported, treating like line", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + @test_warn(TESTLOG, "reactors as constant impedance elements is not yet supported, treating reactor.reactor1 like line", + parse_file("../test/data/opendss/test2_master.dss")) @test_warn(TESTLOG, "line.l1: like=something cannot be found", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + parse_file("../test/data/opendss/test2_master.dss")) @test_warn(TESTLOG, "Rg,Xg are not fully supported", - PMD.parse_file("../test/data/opendss/test2_master.dss")) - - @test_warn(TESTLOG, "Could not find line \"something\"", - PMD.parse_file("../test/data/opendss/test2_master.dss")) - - @test_warn(TESTLOG, "The neutral impedance, (rneut and xneut properties), is ignored; the neutral (for wye and zig-zag windings) is connected directly to the ground.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) - - @test_warn(TESTLOG, "Only three-phase transformers are supported. The bus specification b7.1 is treated as b7 instead.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) - - @test_warn(TESTLOG, "Only three-phase transformers are supported. The bus specification b7.1 is treated as b7 instead.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + parse_file("../test/data/opendss/test2_master.dss")) - Memento.TestUtils.@test_log(TESTLOG, "info", "`dss_data` has been reset with the \"clear\" command.", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + Memento.TestUtils.@test_log(TESTLOG, "info", "Circuit has been reset with the \"clear\" on line 2 in \"test2_master.dss\"", + parse_file("../test/data/opendss/test2_master.dss")) - Memento.TestUtils.@test_log(TESTLOG, "info", "Redirecting to file \"test2_Linecodes.dss\"", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + Memento.TestUtils.@test_log(TESTLOG, "info", "Redirecting to \"test2_Linecodes.dss\" on line 9 in \"test2_master.dss\"", + parse_file("../test/data/opendss/test2_master.dss")) - Memento.TestUtils.@test_log(TESTLOG, "info", "Compiling file \"test2_Loadshape.dss\"", - PMD.parse_file("../test/data/opendss/test2_master.dss")) + Memento.TestUtils.@test_log(TESTLOG, "info", "Redirecting to \"test2_Loadshape.dss\" on line 10 in \"test2_master.dss\"", + parse_file("../test/data/opendss/test2_master.dss")) Memento.setlevel!(TESTLOG, "error") end - pmd = PMD.parse_file("../test/data/opendss/test2_master.dss") - - len = 0.013516796 - rmatrix=PMD._parse_matrix(Float64, "[1.5000 |0.200000 1.50000 |0.250000 0.25000 2.00000 ]") * 3 - xmatrix=PMD._parse_matrix(Float64, "[1.0000 |0.500000 0.50000 |0.500000 0.50000 1.000000 ]") * 3 - cmatrix = PMD._parse_matrix(Float64, "[8.0000 |-2.00000 9.000000 |-1.75000 -2.50000 8.00000 ]") / 3 + eng = parse_file("../test/data/opendss/test2_master.dss", import_all=true) + math = parse_file("../test/data/opendss/test2_master.dss"; data_model=MATHEMATICAL, import_all=true) @testset "buscoords automatic parsing" begin - @test all(haskey(bus, "lon") && haskey(bus, "lat") for bus in values(pmd["bus"]) if "bus_i" in 1:10) + @test all(haskey(bus, "lon") && haskey(bus, "lat") for bus in values(math["bus"]) if "bus_i" in 1:10) + end + + @testset "import_all parsing" begin + @test all(haskey(comp, "dss") && isa(comp["dss"], Dict) for (comp_type, comps) in eng if isa(comps, Dict) for (_,comp) in comps if isa(comp, Dict) && comp_type != "bus" && comp_type != "settings") + @test all(haskey(comp, "dss") && isa(comp["dss"], Dict) for (comp_type, comps) in math if isa(comps, Dict) for (id,comp) in comps if isa(comp, Dict) && comp_type != "bus" && comp_type != "settings" && comp_type != "map" && !startswith(comp["name"], "_virtual")) end @testset "opendss parse generic parser verification" begin - dss = PMD.parse_dss("../test/data/opendss/test2_master.dss") + dss = parse_dss("../test/data/opendss/test2_master.dss") - @test pmd["name"] == "test2" + @test dss["line"]["l7"]["test_param"] == 100.0 - @test length(pmd) == 21 - @test length(dss) == 12 + @test math["name"] == "test2" + + @test length(math) == 18 + @test length(dss) == 16 for (key, len) in zip(["bus", "load", "shunt", "branch", "gen", "dcline", "transformer"], [33, 4, 5, 27, 4, 0, 10]) - @test haskey(pmd, key) - @test length(pmd[key]) == len + @test haskey(math, key) + @test length(math[key]) == len end @test all(haskey(dss, key) for key in ["loadshape", "linecode", "buscoords", "options", "filename"]) end - @testset "opendss parse generic branch values verification" begin - basekv_br3 = pmd["bus"][string(pmd["branch"]["3"]["f_bus"])]["base_kv"] - @test all(isapprox.(pmd["branch"]["3"]["br_r"], rmatrix * len / basekv_br3^2 * pmd["baseMVA"]; atol=1e-6)) - @test all(isapprox.(pmd["branch"]["3"]["br_x"], xmatrix * len / basekv_br3^2 * pmd["baseMVA"]; atol=1e-6)) - @test all(isapprox.(pmd["branch"]["3"]["b_fr"], diag(basekv_br3^2 / pmd["baseMVA"] * 2.0 * pi * 60.0 * cmatrix * len / 1e9) / 2.0; atol=1e-6)) - - for i in 6:7 - basekv_bri = pmd["bus"][string(pmd["branch"]["$i"]["f_bus"])]["base_kv"] - @test all(isapprox.(diag(pmd["branch"]["$i"]["b_fr"]), (3.4 * 2.0 + 1.6) / 3.0 * (basekv_bri^2 / pmd["baseMVA"] * 2.0 * pi * 60.0 / 1e9) / 2.0 / 3; atol=1e-6)) + @testset "opendss parse like" begin + for k in ["pd_nom", "qd_nom"] + @test all(isapprox.(eng["load"]["ld2"][k], eng["load"]["ld4"][k]; atol=1e-12)) end - @test all(isapprox.(pmd["branch"]["1"]["br_r"].*(115/69)^2, diagm(0 => fill(6.3012e-8, 3)); atol=1e-12)) - @test all(isapprox.(pmd["branch"]["1"]["br_x"].*(115/69)^2, diagm(0 => fill(6.3012e-7, 3)); atol=1e-12)) - - for k in ["qd", "pd"] - @test all(isapprox.(pmd["load"]["4"][k], pmd["load"]["2"][k]; atol=1e-12)) - end - - for k in ["gs", "bs"] - @test all(isapprox.(pmd["shunt"]["2"][k], pmd["shunt"]["3"][k]; atol=1e-12)) - @test all(isapprox.(pmd["shunt"]["4"][k], pmd["shunt"]["5"][k]; atol=1e-12)) - end - - for k in keys(pmd["gen"]["3"]) - if !(k in ["gen_bus", "index", "name", "source_id", "active_phases"]) - @test all(isapprox.(pmd["gen"]["4"][k], pmd["gen"]["3"][k]; atol=1e-12)) - end - end - - for k in keys(pmd["branch"]["11"]) - if !(k in ["f_bus", "t_bus", "index", "name", "linecode", "source_id", "active_phases"]) - mult = 1.0 - if k in ["br_r", "br_x", "g_fr", "g_to", "b_fr", "b_to"] - # compensation for the different voltage base - basekv_br3 = pmd["bus"][string(pmd["branch"]["3"]["f_bus"])]["base_kv"] - basekv_br8 = pmd["bus"][string(pmd["branch"]["8"]["f_bus"])]["base_kv"] - zmult = (basekv_br3/basekv_br8)^2 - mult = (k in ["br_r", "br_x"]) ? zmult : 1/zmult - end - @test all(isapprox.(pmd["branch"]["3"][k].*mult, pmd["branch"]["8"][k]; atol=1e-12)) + for (k,v) in eng["generator"]["g2"] + if isa(v, Real) + @test all(isapprox.(v, eng["generator"]["g3"][k]; atol=1e-12)) end end end @testset "opendss parse length units" begin - @test pmd["branch"]["9"]["length"] == 1000.0 * len - basekv_br9 = pmd["bus"][string(pmd["branch"]["9"]["f_bus"])]["base_kv"] - @test all(isapprox.(pmd["branch"]["9"]["br_r"], rmatrix * len / basekv_br9^2 * pmd["baseMVA"]; atol=1e-6)) - @test all(isapprox.(pmd["branch"]["9"]["br_x"], xmatrix * len / basekv_br9^2 * pmd["baseMVA"]; atol=1e-6)) - @test all(isapprox.(pmd["branch"]["9"]["b_fr"], diag(basekv_br9^2 / pmd["baseMVA"] * 2.0 * pi * 60.0 * cmatrix * len / 1e9) / 2.0 / 3; atol=1e-6)) + @test eng["line"]["l8"]["length"] == 1000.0 * 0.013516796 end @testset "opendss parse switch length verify" begin @testset "branches with switches" begin - @test pmd["branch"]["5"]["switch"] - @test pmd["branch"]["5"]["length"] == 0.001 - @test all([pmd["branch"]["$i"]["switch"] == false for i in 1:4]) + @test eng["switch"]["_l4"]["length"] == 0.001 + @test !all(get(br, "switch", false) for (_,br) in math["branch"] if !startswith(br["name"],"_virtual_branch.switch")) end end @testset "opendss parse transformer parsing verify" begin - dss_data = PMD.parse_dss("../test/data/opendss/test_transformer_formatting.dss") - transformer = dss_data["transformer"][1] - @test transformer["phases"] == "3" - @test transformer["tap"] == "(0.00625 12 * 1 +)" - @test transformer["tap_2"] == "1.5" - @test transformer["%loadloss"] == "0.01" - @test transformer["xhl"] == "0.02" - @test transformer["kv_2"] == "12.47" - @test transformer["conn_2"] == "wye" - @test transformer["tap_3"] == "0.9" - @test transformer["wdg_3"] == "3" - - PMD._apply_like!(dss_data["transformer"][3], dss_data, "transformer") - @test dss_data["transformer"][3]["%loadloss"] == dss_data["transformer"][2]["%loadloss"] - - pmd_data = PMD.parse_file("../test/data/opendss/test_transformer_formatting.dss") - @test all(all(pmd_data["transformer"]["$n"]["tm"] .== tm) for (n, tm) in zip(1:3, [1.075, 1.5, 0.9])) + dss_data = parse_dss("../test/data/opendss/test_transformer_formatting.dss") + transformer = dss_data["transformer"]["transformer_test"] + @test transformer["phases"] == 3 + @test transformer["tap"] == PMD._parse_rpn("(0.00625 12 * 1 +)") + @test transformer["tap_2"] == 1.5 + @test transformer["%loadloss"] == 0.01 + @test transformer["xhl"] == 0.02 + @test transformer["kv_2"] == 12.47 + @test transformer["conn_2"] == WYE + @test transformer["tap_3"] == 0.9 + @test transformer["wdg_3"] == 3 + + PMD._apply_like!(dss_data["transformer"]["reg4b"], dss_data, "transformer") + @test dss_data["transformer"]["reg4b"]["%loadloss"] == dss_data["transformer"]["reg4a"]["%loadloss"] + + eng_data = parse_file("../test/data/opendss/test_transformer_formatting.dss") + @test all(all(eng_data["transformer"]["$n"]["tm_set"] .== tm) for (n, tm) in zip(["transformer_test", "reg4"], [[fill(1.075, 3), fill(1.5, 3), fill(0.9, 3)], [ones(3), ones(3)]])) end @testset "opendss parse storage" begin - pmd_storage = PMD.parse_file("../test/data/opendss/case3_balanced_battery.dss") - for bat in values(pmd_storage["storage"]) + math_storage = parse_file("../test/data/opendss/case3_balanced_battery.dss"; data_model=MATHEMATICAL) + for bat in values(math_storage["storage"]) for key in ["energy", "storage_bus", "energy_rating", "charge_rating", "discharge_rating", "charge_efficiency", "discharge_efficiency", "thermal_rating", "qmin", "qmax", - "r", "x", "p_loss", "q_loss", "status", "source_id", "active_phases"] + "r", "x", "p_loss", "q_loss", "status", "source_id"] @test haskey(bat, key) if key in ["x", "r", "qmin", "qmax", "thermal_rating"] @test isa(bat[key], Vector) @@ -246,91 +175,63 @@ end end - @test pmd_storage["storage"]["1"]["source_id"] == "storage.s1" && length(pmd_storage["storage"]["1"]["active_phases"]) == 3 - end - - @testset "opendss parse pvsystem" begin - Memento.setlevel!(TESTLOG, "warn") - @test_warn(TESTLOG, "Converting PVSystem \"pv1\" into generator with limits determined by OpenDSS property 'kVA'", - PMD.parse_file("../test/data/opendss/case3_balanced_pv.dss")) - Memento.setlevel!(TESTLOG, "error") + @test math_storage["storage"]["1"]["source_id"] == "storage.s1" end @testset "opendss parse verify source_id" begin - @test pmd["shunt"]["1"]["source_id"] == "capacitor.c1" && length(pmd["shunt"]["1"]["active_phases"]) == 3 - @test pmd["shunt"]["4"]["source_id"] == "reactor.reactor3" && length(pmd["shunt"]["4"]["active_phases"]) == 3 - - @test pmd["branch"]["1"]["source_id"] == "line.l1" && length(pmd["branch"]["1"]["active_phases"]) == 3 - @test pmd["transformer"]["1"]["source_id"] == "transformer.t4_1" # winding indicated by _1 - @test pmd["branch"]["10"]["source_id"] == "reactor.reactor1" && length(pmd["branch"]["10"]["active_phases"]) == 3 - - @test pmd["gen"]["1"]["source_id"] == "vsource.sourcegen" && length(pmd["gen"]["1"]["active_phases"]) == 3 - @test pmd["gen"]["2"]["source_id"] == "generator.g1" && length(pmd["gen"]["2"]["active_phases"]) == 3 - - source_id = PMD._parse_dss_source_id(pmd["load"]["1"]) - @test source_id.dss_type == "load" - @test source_id.dss_name == "ld1" - @test all([n in source_id.active_phases for n in 1:2]) - - @test all(haskey(component, "source_id") && haskey(component, "active_phases") for component_type in ["load", "branch", "shunt", "gen", "storage", "pvsystem"] for component in values(get(pmd, component_type, Dict()))) + @test all(haskey(component, "source_id") for component_type in PMD._dss_supported_components for component in values(get(math, component_type, Dict())) if component_type != "bus") end @testset "opendss parse verify order of properties on line" begin - pmd1 = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - pmd2 = PMD.parse_file("../test/data/opendss/case3_balanced_prop-order.dss") + math1 = parse_file("../test/data/opendss/case3_balanced.dss"; data_model=MATHEMATICAL) + math2 = parse_file("../test/data/opendss/case3_balanced_prop-order.dss"; data_model=MATHEMATICAL) + + delete!(math1, "map") + delete!(math2, "map") - @test pmd1 == pmd2 + @test math1 == math2 - dss1 = PMD.parse_dss("../test/data/opendss/case3_balanced.dss") - dss2 = PMD.parse_dss("../test/data/opendss/case3_balanced_prop-order.dss") + dss1 = parse_dss("../test/data/opendss/case3_balanced.dss") + dss2 = parse_dss("../test/data/opendss/case3_balanced_prop-order.dss") @test dss1 != dss2 - @test all(a == b for (a, b) in zip(dss2["line"][1]["prop_order"],["name", "bus1", "bus2", "linecode", "rmatrix", "length"])) - @test all(a == b for (a, b) in zip(dss2["line"][2]["prop_order"],["name", "bus1", "bus2", "like", "linecode", "length"])) + @test all(a == b for (a, b) in zip(dss2["line"]["ohline"]["prop_order"],["name", "bus1", "bus2", "linecode", "rmatrix", "length"])) + @test all(a == b for (a, b) in zip(dss2["line"]["quad"]["prop_order"],["name", "like", "bus1", "bus2", "linecode", "length"])) end @testset "opendss parse verify mvasc3/mvasc1 circuit parse" begin - dss = PMD.parse_dss("../test/data/opendss/test_simple.dss") - PMD.parse_dss_with_dtypes!(dss, ["circuit"]) - circuit = PMD._create_vsource("sourcebus", "simple"; PMD._to_sym_keys(dss["circuit"][1])...) + dss = parse_dss("../test/data/opendss/test_simple.dss") + circuit = PMD._create_vsource("source"; PMD._to_kwargs(dss["vsource"]["source"])...) @test circuit["mvasc1"] == 2100.0 @test circuit["mvasc3"] == 1900.0 @test isapprox(circuit["isc3"], 9538.8; atol=1e-1) @test isapprox(circuit["isc1"], 10543.0; atol=1e-1) - dss = PMD.parse_dss("../test/data/opendss/test_simple3.dss") - PMD.parse_dss_with_dtypes!(dss, ["circuit"]) - circuit = PMD._create_vsource("sourcebus", "simple"; PMD._to_sym_keys(dss["circuit"][1])...) + dss = parse_dss("../test/data/opendss/test_simple3.dss") + circuit = PMD._create_vsource("source"; PMD._to_kwargs(dss["vsource"]["source"])...) @test circuit["mvasc1"] == 2100.0 @test isapprox(circuit["mvasc3"], 1900.0; atol=1e-1) @test circuit["isc3"] == 9538.8 @test isapprox(circuit["isc1"], 10543.0; atol=1e-1) - dss = PMD.parse_dss("../test/data/opendss/test_simple4.dss") - PMD.parse_dss_with_dtypes!(dss, ["circuit"]) - circuit = PMD._create_vsource("sourcebus", "simple"; PMD._to_sym_keys(dss["circuit"][1])...) + dss = parse_dss("../test/data/opendss/test_simple4.dss") + circuit = PMD._create_vsource("source"; PMD._to_kwargs(dss["vsource"]["source"])...) @test isapprox(circuit["mvasc1"], 2091.5; atol=1e-1) @test circuit["mvasc3"] == 2000.0 @test circuit["isc3"] == 10041.0 @test circuit["isc1"] == 10500.0 end - - @testset "opendss verify sourcebus_vbranch rate_a" begin - data = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - vbranch = [br for (id,br) in data["branch"] if br["name"]=="sourcebus_vbranch"][1] - @test haskey(vbranch, "rate_a") - end end @testset "test json parser" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") + eng = parse_file("../test/data/opendss/case3_balanced.dss") io = PipeBuffer() - PMD.print_file(io, pmd) - pmd_json_file = PMD.parse_file(io) + print_file(io, eng) + eng_json_file = parse_file(io) - @test pmd == pmd_json_file + @test eng == eng_json_file end diff --git a/test/opf.jl b/test/opf.jl index 665b9ee30..e5f805c92 100644 --- a/test/opf.jl +++ b/test/opf.jl @@ -2,238 +2,222 @@ @testset "test opf" begin @testset "test matpower opf" begin + case5 = PM.parse_file("../test/data/matpower/case5.m") + case30 = PM.parse_file("../test/data/matpower/case30.m") + + make_multiconductor!(case5, 3) + make_multiconductor!(case30, 3) + @testset "5-bus matpower acp opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case5.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.ACPPowerModel, ipopt_solver) + result = run_mc_opf(case5, ACPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 45522.096; atol=1e-1) - @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 0.3999999; atol=1e-3) for c in 1:mp_data["conductors"]) - @test all(isapprox(result["solution"]["bus"]["2"]["va"][c], -0.0538204; atol=1e-5) for c in 1:mp_data["conductors"]) + @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 0.3999999; atol=1e-3) for c in 1:case5["conductors"]) + @test all(isapprox(result["solution"]["bus"]["2"]["va"][c], -0.0538204; atol=1e-5) for c in 1:case5["conductors"]) end @testset "5-bus matpower acr opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case5.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.ACRPowerModel, ipopt_solver) + result = run_mc_opf(case5, ACRPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 45522.096; atol=1e-1) calc_va(id) = atan.(result["solution"]["bus"][id]["vi"], result["solution"]["bus"][id]["vr"]) - @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 0.3999999; atol=1e-3) for c in 1:mp_data["conductors"]) - @test all(isapprox(calc_va("2")[c], -0.0538204; atol=1e-5) for c in 1:mp_data["conductors"]) + @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 0.3999999; atol=1e-3) for c in 1:case5["conductors"]) + @test all(isapprox(calc_va("2")[c], -0.0538204; atol=1e-5) for c in 1:case5["conductors"]) end @testset "30-bus matpower acp opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case30.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.ACPPowerModel, ipopt_solver) + result = run_mc_opf(case30, ACPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 614.007; atol=1e-1) - @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 2.192189; atol=1e-3) for c in 1:mp_data["conductors"]) - @test all(isapprox(result["solution"]["bus"]["2"]["va"][c], -0.071853; atol=1e-4) for c in 1:mp_data["conductors"]) + @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 2.192189; atol=1e-3) for c in 1:case30["conductors"]) + @test all(isapprox(result["solution"]["bus"]["2"]["va"][c], -0.071853; atol=1e-4) for c in 1:case30["conductors"]) end @testset "30-bus matpower acr opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case30.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.ACRPowerModel, ipopt_solver) + result = run_mc_opf(case30, ACRPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 614.007; atol=1e-1) calc_va(id) = atan.(result["solution"]["bus"][id]["vi"], result["solution"]["bus"][id]["vr"]) - @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 2.192189; atol=1e-3) for c in 1:mp_data["conductors"]) - @test all(isapprox(calc_va("2")[c], -0.071853; atol=1e-4) for c in 1:mp_data["conductors"]) + @test all(isapprox(result["solution"]["gen"]["1"]["pg"][c], 2.192189; atol=1e-3) for c in 1:case30["conductors"]) + @test all(isapprox(calc_va("2")[c], -0.071853; atol=1e-4) for c in 1:case30["conductors"]) end @testset "30-bus matpower dcp opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case30.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.DCPPowerModel, ipopt_solver) + result = run_mc_opf(case30, DCPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 566.112; atol=1e-1) end @testset "30-bus matpower nfa opf" begin - mp_data = PMs.parse_file("../test/data/matpower/case30.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf(mp_data, PMs.NFAPowerModel, ipopt_solver) + result = run_mc_opf(case30, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 458.006; atol=1e-1) end end @testset "test dropped phases opf" begin + case4_phase_drop = parse_file("../test/data/opendss/case4_phase_drop.dss") + case5_phase_drop = parse_file("../test/data/opendss/case5_phase_drop.dss") + @testset "4-bus phase drop acp opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case4_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.ACPPowerModel, ipopt_solver) + result = run_mc_opf(case4_phase_drop, ACPPowerModel, ipopt_solver, make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0182595; atol=1e-4) - @test all(isapprox.(result["solution"]["gen"]["1"]["pg"], [5.06513e-5, 6.0865e-5, 7.1119e-5]; atol=1e-7)) - @test all(isapprox.(result["solution"]["bus"]["2"]["vm"], [0.990023, 1.000000, 1.000000]; atol=1e-4)) + @test all(isapprox.(result["solution"]["voltage_source"]["source"]["pg"], [5.06513e-5, 6.0865e-5, 7.1119e-5]; atol=1e-7)) + @test isapprox(result["solution"]["bus"]["loadbus1"]["vm"][1], 0.98995; atol=1.5e-4) end @testset "4-bus phase drop acr opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case4_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.ACRPowerModel, ipopt_solver) + result = run_mc_opf(case4_phase_drop, ACRPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0182595; atol=1e-4) - calc_vm(id) = sqrt.(result["solution"]["bus"][id]["vr"].^2+result["solution"]["bus"][id]["vi"].^2) - @test all(isapprox.(result["solution"]["gen"]["1"]["pg"], [5.06513e-5, 6.0865e-5, 7.1119e-5]; atol=1e-7)) - @test isapprox(calc_vm("2")[1], 0.98995; atol=1.5e-4) + @test all(isapprox.(result["solution"]["voltage_source"]["source"]["pg"], [5.06513e-5, 6.0865e-5, 7.1119e-5]; atol=1e-7)) + @test isapprox(calc_vm_acr(result, "loadbus1")[1], 0.98995; atol=1.5e-4) end @testset "5-bus phase drop acp opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.ACPPowerModel, ipopt_solver) + result = run_mc_opf(case5_phase_drop, ACPPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0599389; atol=1e-4) - @test all(isapprox.(result["solution"]["gen"]["1"]["pg"], [0.00015236280779412599, 0.00019836795302238667, 0.0002486642034746673]; atol=1e-7)) - @test all(isapprox.(result["solution"]["bus"]["2"]["vm"], [0.97351, 0.96490, 0.95646]; atol=1e-4)) + @test all(isapprox.(result["solution"]["voltage_source"]["source"]["pg"], [0.00015236280779412599, 0.00019836795302238667, 0.0002486642034746673]; atol=1e-7)) + @test all(isapprox.(result["solution"]["bus"]["midbus"]["vm"], [0.97351, 0.96490, 0.95646]; atol=1e-4)) end @testset "5-bus phase drop acr opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.ACRPowerModel, ipopt_solver) + result = run_mc_opf(case5_phase_drop, ACRPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0599400; atol = 1e-4) - calc_vm(id) = sqrt.(result["solution"]["bus"][id]["vr"].^2+result["solution"]["bus"][id]["vi"].^2) - @test all(isapprox.(result["solution"]["gen"]["1"]["pg"], [0.00015236280779412599, 0.00019836795302238667, 0.0002486688793741932]; atol=1e-7)) - @test all(isapprox.(calc_vm("2"), [0.9735188343958152, 0.9649003198689144, 0.9564593296045091]; atol=1e-4)) + @test all(isapprox.(result["solution"]["voltage_source"]["source"]["pg"], [0.00015236280779412599, 0.00019836795302238667, 0.0002486688793741932]; atol=1e-7)) + @test all(isapprox.(calc_vm_acr(result, "midbus"), [0.9735188343958152, 0.9649003198689144, 0.9564593296045091]; atol=1e-4)) end @testset "5-bus phase drop dcp opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.DCPPowerModel, ipopt_solver) + result = run_mc_opf(case5_phase_drop, DCPPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(result["objective"], 0.0540021; atol=1e-4) + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0.0544220; atol=1e-4) end @testset "5-bus phase drop nfa opf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_opf(mp_data, PMs.NFAPowerModel, ipopt_solver) + result = run_mc_opf(case5_phase_drop, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.054; atol=1e-4) end end @testset "test opendss opf" begin @testset "2-bus diagonal acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case2_diag.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case2_diag.dss") + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - @test all(isapprox.(sol["solution"]["bus"]["2"]["vm"], 0.984377; atol=1e-4)) - @test all(isapprox.(sol["solution"]["bus"]["2"]["va"], PMD._wrap_to_pi.([2 * pi / pmd["conductors"] * (1 - c) - deg2rad(0.79) for c in 1:pmd["conductors"]]); atol=deg2rad(0.2))) + @test all(isapprox.(sol["solution"]["bus"]["primary"]["vm"], 0.984377; atol=1e-4)) + @test all(isapprox.(sol["solution"]["bus"]["primary"]["va"], [0, -120, 120] .- 0.79; atol=0.2)) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0181409; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.0; atol=1e-4) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0181409; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.0; atol=1e-4) end @testset "3-bus balanced acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_balanced.dss") + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - for (bus, va, vm) in zip(["1", "2", "3"], [0.0, deg2rad(-0.03), deg2rad(-0.07)], [0.9959, 0.986973, 0.976605]) - @test all(isapprox.(sol["solution"]["bus"][bus]["va"], PMD._wrap_to_pi.([2 * pi / pmd["conductors"] * (1 - c) + va for c in 1:pmd["conductors"]]); atol=deg2rad(0.01))) + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], [0.0, -0.03, -0.07], [0.9959, 0.986973, 0.976605]) + @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [0, -120, 120] .+ va; atol=0.01)) @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-4)) end - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018276; atol=1e-6) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.008922; atol=1.2e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.018276; atol=1e-6) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.008922; atol=1.2e-5) end @testset "3-bus unbalanced acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_unbalanced.dss") + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - for (bus, va, vm) in zip(["1", "2", "3"], - [0.0, deg2rad.([-0.22, -0.11, 0.12]), deg2rad.([-0.48, -0.24, 0.27])], + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], + [0.0, [-0.22, -0.11, 0.12], [-0.48, -0.24, 0.27]], [0.9959, [0.980937, 0.98936, 0.987039], [0.963546, 0.981757, 0.976779]]) - @test all(isapprox.(sol["solution"]["bus"][bus]["va"], PMD._wrap_to_pi.([2 * pi / pmd["conductors"] * (1 - c) for c in 1:pmd["conductors"]]) .+ va; atol=deg2rad(0.01))) + @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [0, -120, 120] .+ va; atol=0.01)) @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-5)) end - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214812; atol=1e-6) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00927263; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0214812; atol=1e-6) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00927263; atol=1e-5) end @testset "3-bus unbalanced isc acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced_isc.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_balanced_isc.dss") + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED @test isapprox(sol["objective"], 0.0185; atol=1e-4) end @testset "3-bus balanced pv acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced_pv.dss") - - @test length(pmd["gen"]) == 2 - @test all(pmd["gen"]["2"]["qmin"] .== -pmd["gen"]["2"]["qmax"]) - @test all(pmd["gen"]["2"]["pmax"] .== pmd["gen"]["2"]["qmax"]) - @test all(pmd["gen"]["2"]["pmin"] .== 0.0) + pmd = parse_file("../test/data/opendss/case3_balanced_pv.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED - @test sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]) < 0.0 - @test sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]) < 0.0 - @test isapprox(sum(sol["solution"]["gen"]["2"]["pg"] * sol["solution"]["baseMVA"]), 0.0183685; atol=1e-4) - @test isapprox(sum(sol["solution"]["gen"]["2"]["qg"] * sol["solution"]["baseMVA"]), 0.00919404; atol=1e-4) + @test sol["termination_status"] == LOCALLY_SOLVED + @test sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]) < 0.0 + @test sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]) < 0.005 + @test isapprox(sum(sol["solution"]["solar"]["pv1"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0183685; atol=1e-4) + @test isapprox(sum(sol["solution"]["solar"]["pv1"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.0091449; atol=1e-4) end @testset "3-bus unbalanced single-phase pv acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced_1phase-pv.dss") - sol = PMD.run_mc_opf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_unbalanced_1phase-pv.dss") + sol = run_mc_opf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0196116; atol=1e-3) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923107; atol=1e-3) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.01838728; atol=1e-3) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00756634; atol=1e-3) - @test all(sol["solution"]["gen"]["2"]["pg"][2:3] .== 0.0) - @test all(sol["solution"]["gen"]["2"]["qg"][2:3] .== 0.0) + @test all(sol["solution"]["solar"]["pv1"]["pg"][2:3] .== 0.0) + @test all(sol["solution"]["solar"]["pv1"]["qg"][2:3] .== 0.0) end @testset "3-bus balanced capacitor acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced_cap.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_balanced_cap.dss") + sol = run_mc_pf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - @test all(abs(sol["solution"]["bus"]["3"]["vm"][c]-0.98588)<=1E-4 for c in 1:3) - @test all(abs(sol["solution"]["bus"]["2"]["vm"][c]-0.99127)<=1E-4 for c in 1:3) + @test all(abs(sol["solution"]["bus"]["loadbus"]["vm"][c]-0.98588)<=1E-4 for c in 1:3) + @test all(abs(sol["solution"]["bus"]["primary"]["vm"][c]-0.99127)<=1E-4 for c in 1:3) end @testset "3w transformer nfa opf" begin - mp_data = PMD.parse_file("../test/data/opendss/ut_trans_3w_dyy_1.dss") - result = run_mc_opf(mp_data, PMs.NFAPowerModel, ipopt_solver) + mp_data = parse_file("../test/data/opendss/ut_trans_3w_dyy_1.dss") + result = run_mc_opf(mp_data, NFAPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.616; atol=1e-3) end end diff --git a/test/opf_bf.jl b/test/opf_bf.jl index ecdefb8cb..92f82fe21 100644 --- a/test/opf_bf.jl +++ b/test/opf_bf.jl @@ -1,13 +1,14 @@ @info "running branch-flow optimal power flow (opf_bf) tests" -@testset "test distflow formulations" begin +@testset "test distflow formulations in opf" begin + case5 = PowerModels.parse_file("../test/data/matpower/case5.m") + make_multiconductor!(case5, 3) + @testset "test linearised distflow opf_bf" begin @testset "5-bus lpubfdiag opf_bf" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf_bf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + result = run_mc_opf(case5, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 44880; atol = 1e0) # @test isapprox(result["solution"]["bus"]["3"]["vm"], 0.911466*[1,1,1]; atol = 1e-3) vm = calc_vm_w(result, "3") @@ -16,43 +17,112 @@ end @testset "3-bus balanced lpubfdiag opf_bf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_opf_bf(pmd, LPUBFDiagPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_balanced.dss") + sol = run_mc_opf(pmd, LPUBFDiagPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0183456; atol=2e-3) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923328; atol=2e-3) + @test sol["termination_status"] == LOCALLY_SOLVED + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0183456; atol=2e-3) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00923328; atol=2e-3) end @testset "3-bus unbalanced lpubfdiag opf_bf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - sol = PMD.run_mc_opf_bf(pmd, LPUBFDiagPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_unbalanced.dss") + sol = run_mc_opf(pmd, LPUBFDiagPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214812; atol=2e-3) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00927263; atol=2e-3) + @test sol["termination_status"] == LOCALLY_SOLVED + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0214812; atol=2e-3) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00927263; atol=2e-3) end end @testset "test linearised distflow opf_bf in diagonal matrix form" begin @testset "5-bus lpdiagubf opf_bf" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf_bf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + result = run_mc_opf(case5, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 44880; atol = 1e0) end end @testset "test linearised distflow opf_bf in full matrix form" begin @testset "5-bus lpfullubf opf_bf" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") - PMD.make_multiconductor!(mp_data, 3) - result = run_mc_opf_bf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + result = run_mc_opf(case5, LPUBFDiagPowerModel, ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 44880; atol = 1e0) end end + + data = parse_file("../test/data/opendss/case3_unbalanced.dss"; transformations=[make_lossless!]) + data["settings"]["sbase_default"] = 0.001 * 1e3 + data["generator"] = Dict{Any,Any}( + "1" => Dict{String,Any}( + "bus" => "primary", + "connections" => [1, 2, 3, 4], + "cost_pg_parameters" => [0.0, 1200.0, 0.0], + "qg_lb" => fill(0.0, 3), + "qg_ub" => fill(0.0, 3), + "pg_ub" => fill(10, 3), + "pg_lb" => fill(0, 3), + "configuration" => WYE, + "status" => ENABLED + ) + ) + + merge!(data["voltage_source"]["source"], Dict{String,Any}( + "cost_pg_parameters" => [0.0, 1000.0, 0.0], + "pg_lb" => fill( 0.0, 3), + "pg_ub" => fill( 10.0, 3), + "qg_lb" => fill(-10.0, 3), + "qg_ub" => fill( 10.0, 3) + )) + + for (_,line) in data["line"] + line["sm_ub"] = fill(10.0, 3) + end + + data = transform_data_model(data) + + for (_,bus) in data["bus"] + if bus["name"] != "sourcebus" + bus["vmin"] = fill(0.9, 3) + bus["vmax"] = fill(1.1, 3) + bus["vm"] = fill(1.0, 3) + bus["va"] = deg2rad.([0., -120, 120]) + end + end + + @testset "test sdp distflow opf_bf" begin + @testset "3-bus SDPUBF opf_bf" begin + result = run_mc_opf(data, SDPUBFPowerModel, scs_solver) + + @test result["termination_status"] == OPTIMAL + @test isapprox(result["objective"], 21.48; atol = 1e-2) + end + end + + @testset "test sdp distflow opf_bf in full matrix form" begin + @testset "3-bus SDPUBFKCLMX opf_bf" begin + result = run_mc_opf(data, SDPUBFKCLMXPowerModel, scs_solver) + + @test result["termination_status"] == OPTIMAL + @test isapprox(result["objective"], 21.48; atol = 1e-2) + end + end + + + @testset "test soc distflow opf_bf" begin + @testset "3-bus SOCNLPUBF opf_bf" begin + result = run_mc_opf(data, SOCNLPUBFPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 21.179; atol = 1e-1) + end + # @testset "3-bus SOCConicUBF opf_bf" begin + # result = run_mc_opf(data, SOCConicUBFPowerModel, scs_solver) + # + # @test result["termination_status"] == ALMOST_OPTIMAL + # @test isapprox(result["objective"], 21.17; atol = 1e-2) + # end + end end diff --git a/test/opf_iv.jl b/test/opf_iv.jl index c81cb9076..1ce39089f 100644 --- a/test/opf_iv.jl +++ b/test/opf_iv.jl @@ -3,37 +3,37 @@ @testset "test current-voltage formulations" begin @testset "test IVR opf_iv" begin @testset "2-bus diagonal acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case2_diag.dss") - sol = PMD.run_mc_opf_iv(pmd, PMs.IVRPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case2_diag.dss") + sol = run_mc_opf(pmd, IVRPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED @test isapprox(sol["objective"], 0.018208969542066918; atol = 1e-4) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018209; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.000208979; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.018209; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.000208979; atol=1e-5) end @testset "3-bus balanced acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_opf_iv(pmd, PMs.IVRPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_balanced.dss") + sol = run_mc_opf(pmd, IVRPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED @test isapprox(sol["objective"], 0.018345004773175046; atol = 1e-4) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018345; atol=1e-6) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00919404; atol=1.2e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.018345; atol=1e-6) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00919404; atol=1.2e-5) end @testset "3-bus unbalanced acp opf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - sol = PMD.run_mc_opf_iv(pmd, PMs.IVRPowerModel, ipopt_solver) + pmd = parse_file("../test/data/opendss/case3_unbalanced.dss") + sol = run_mc_opf(pmd, IVRPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED @test isapprox(sol["objective"], 0.021481176584287; atol = 1e-4) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214812; atol=1e-6) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00927263; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"] * sol["solution"]["settings"]["sbase"]), 0.0214812; atol=1e-6) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"] * sol["solution"]["settings"]["sbase"]), 0.00927263; atol=1e-5) end end end diff --git a/test/pf.jl b/test/pf.jl index 398ee2628..d83bf583b 100644 --- a/test/pf.jl +++ b/test/pf.jl @@ -1,182 +1,183 @@ @info "running power flow (pf) tests" @testset "test pf" begin + case2_diag = parse_file("../test/data/opendss/case2_diag.dss") + case3_balanced = parse_file("../test/data/opendss/case3_balanced.dss") + case3_unbalanced = parse_file("../test/data/opendss/case3_unbalanced.dss") + case5_phase_drop = parse_file("../test/data/opendss/case5_phase_drop.dss") + case_mxshunt = parse_file("../test/data/opendss/case_mxshunt.dss") - @testset "test opendss pf" begin - @testset "2-bus diagonal acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case2_diag.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + @testset "2-bus diagonal acp pf" begin + sol = run_mc_pf(case2_diag, ACPPowerModel, ipopt_solver) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - @test all(isapprox.(sol["solution"]["bus"]["2"]["vm"], 0.984377; atol=1e-4)) - @test all(isapprox.(sol["solution"]["bus"]["2"]["va"], [PMD._wrap_to_pi(2*pi/pmd["conductors"]*(1-c) - deg2rad(0.79)) for c in 1:pmd["conductors"]]; atol=deg2rad(0.2))) + @test all(isapprox.(sol["solution"]["bus"]["primary"]["vm"], 0.227339; atol=1e-4)) + @test all(isapprox.(sol["solution"]["bus"]["primary"]["va"], [-0.657496, -120.657, 119.343]; atol=0.2)) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018209; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.000208979; atol=1e-5) - end + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.20887; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 0.20887; atol=1e-5) + end + + @testset "2-bus diagonal acr pf" begin + sol = run_mc_pf(case2_diag, ACRPowerModel, ipopt_solver) + + @test sol["termination_status"] == LOCALLY_SOLVED - @testset "2-bus diagonal acr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case2_diag.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACRPowerModel, ipopt_solver) + @test all(isapprox.(calc_vm_acr(sol, "primary"), 0.227339; atol=1e-4)) + @test all(isapprox.(calc_va_acr(sol, "primary"), [-0.657496, -120.657, 119.343]; atol=0.2)) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.20888; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 0.20888; atol=1e-5) + end + + @testset "3-bus balanced acp pf" begin + sol = run_mc_pf(case3_balanced, ACPPowerModel, ipopt_solver) - calc_vm(id) = sqrt.(sol["solution"]["bus"][id]["vr"].^2+sol["solution"]["bus"][id]["vi"].^2) - calc_va(id) = atan.(sol["solution"]["bus"][id]["vi"], sol["solution"]["bus"][id]["vr"]) - @test all(isapprox.(calc_vm("2"), 0.984377; atol=1e-4)) - @test all(isapprox.(calc_va("2"), [PMD._wrap_to_pi(2*pi/pmd["conductors"]*(1-c) - deg2rad(0.79)) for c in 1:pmd["conductors"]]; atol=deg2rad(0.2))) + @test sol["termination_status"] == LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018209; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.000208979; atol=1e-5) + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], [0.0, -0.08, -0.17], [0.229993, 0.227932, 0.225537]) + @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [0, -120, 120] .+ va; atol=0.2)) + @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-3)) end - @testset "3-bus balanced acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.34478; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 9.19392; atol=1e-4) + end - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @testset "3-bus balanced acr pf" begin + sol = run_mc_pf(case3_balanced, ACRPowerModel, ipopt_solver) - for (bus, va, vm) in zip(["1", "2", "3"], [0.0, deg2rad(-0.08), deg2rad(-0.17)], [0.9959, 0.986976, 0.976611]) - @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [PMD._wrap_to_pi(2*pi/pmd["conductors"]*(1-c) .+ va) for c in 1:pmd["conductors"]]; atol=deg2rad(0.2))) - @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-3)) - end + @test sol["termination_status"] == LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0183456; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923328; atol=1e-4) + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], [0.0, -0.08, -0.17], [0.229993, 0.227932, 0.225537]) + @test all(isapprox.(calc_va_acr(sol, bus), [0, -120, 120] .+ va; atol=0.2)) + @test all(isapprox.(calc_vm_acr(sol, bus), vm; atol=1e-3)) end - @testset "3-bus balanced acr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACRPowerModel, ipopt_solver) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.34478; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 9.19392; atol=1e-4) + end - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @testset "3-bus balanced no linecode basefreq defined acp pf" begin + sol = run_mc_pf(case3_balanced, ACPPowerModel, ipopt_solver) - for (bus, va, vm) in zip(["1", "2", "3"], [0.0, deg2rad(-0.08), deg2rad(-0.17)], [0.9959, 0.986976, 0.976611]) - calc_vm(id) = sqrt.(sol["solution"]["bus"][id]["vr"].^2+sol["solution"]["bus"][id]["vi"].^2) - calc_va(id) = atan.(sol["solution"]["bus"][id]["vi"], sol["solution"]["bus"][id]["vr"]) - @test all(isapprox.(calc_va(bus), [PMD._wrap_to_pi(2*pi/pmd["conductors"]*(1-c) .+ va) for c in 1:pmd["conductors"]]; atol=deg2rad(0.2))) - @test all(isapprox.(calc_vm(bus), vm; atol=1e-3)) - end + pmd2 = parse_file("../test/data/opendss/case3_balanced_basefreq.dss") + sol2 = run_mc_pf(pmd2, ACPPowerModel, ipopt_solver) - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0183456; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923328; atol=1e-4) - end + @test all(all(isapprox.(bus["vm"], sol2["solution"]["bus"][i]["vm"]; atol=1e-8)) for (i, bus) in sol["solution"]["bus"]) + @test all(all(isapprox.(bus["va"], sol2["solution"]["bus"][i]["va"]; atol=1e-8)) for (i, bus) in sol["solution"]["bus"]) + @test all(isapprox(sum(sol["solution"]["voltage_source"]["source"][field]), sum(sol2["solution"]["voltage_source"]["source"][field]); atol=1e-8) for field in ["pg", "qg"]) + end - @testset "3-bus balanced no linecode basefreq defined acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + @testset "3-bus balanced w/ switch acp pf" begin + pmd = parse_file("../test/data/opendss/case3_balanced_switch.dss") + sol = run_mc_pf(pmd, ACPPowerModel, ipopt_solver; make_si=false) - pmd2 = PMD.parse_file("../test/data/opendss/case3_balanced_basefreq.dss") - sol2 = PMD.run_mc_pf(pmd2, PMs.ACPPowerModel, ipopt_solver) + @test sol["termination_status"] == LOCALLY_SOLVED - @test all(all(isapprox.(bus["vm"], sol2["solution"]["bus"][i]["vm"]; atol=1e-8)) for (i, bus) in sol["solution"]["bus"]) - @test all(all(isapprox.(bus["va"], sol2["solution"]["bus"][i]["va"]; atol=1e-8)) for (i, bus) in sol["solution"]["bus"]) - @test all(isapprox(sum(sol["solution"]["gen"]["1"][field] * sol["solution"]["baseMVA"]), sum(sol2["solution"]["gen"]["1"][field] * sol2["solution"]["baseMVA"]); atol=1e-8) for field in ["pg", "qg"]) + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], [0.0, 0.0, -0.04], [0.9959, 0.995729, 0.985454]) + @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [0, -120, 120] .+ va; atol=0.2)) + @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-3)) end + end - @testset "3-bus balanced w/ switch acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced_switch.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + @testset "3-bus unbalanced acp pf" begin + sol = run_mc_pf(case3_unbalanced, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test sol["termination_status"] == LOCALLY_SOLVED - for (bus, va, vm) in zip(["1", "2", "3"], [0.0, 0.0, deg2rad(-0.04)], [0.9959, 0.995729, 0.985454]) - @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [PMD._wrap_to_pi(2*pi/pmd["conductors"]*(1-c) .+ va) for c in 1:pmd["conductors"]]; atol=deg2rad(0.2))) - @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=1e-3)) - end + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], + [0.0, [-0.22, -0.11, 0.12], [-0.48, -0.24, 0.27]], + [0.9959, [0.98094, 0.989365, 0.987043], [0.96355, 0.981767, 0.976786]]) + @test all(isapprox.(sol["solution"]["bus"][bus]["va"], [0, -120, 120] .+ va; atol=0.2)) + @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=2e-3)) end - @testset "3-bus unbalanced acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACPPowerModel, ipopt_solver) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 0.04296; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 0.01854; atol=1e-4) + end - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @testset "3-bus unbalanced acr pf" begin + sol = run_mc_pf(case3_unbalanced, ACRPowerModel, ipopt_solver; make_si=false) - for (bus, va, vm) in zip(["1", "2", "3"], - [0.0, deg2rad.([-0.22, -0.11, 0.12]), deg2rad.([-0.48, -0.24, 0.27])], - [0.9959, [0.98094, 0.989365, 0.987043], [0.96355, 0.981767, 0.976786]]) - @test all(isapprox.(sol["solution"]["bus"][bus]["va"], PMD._wrap_to_pi([2*pi/pmd["conductors"]*(1-c) for c in 1:pmd["conductors"]] .+ va); atol=deg2rad(0.2))) - @test all(isapprox.(sol["solution"]["bus"][bus]["vm"], vm; atol=2e-3)) - end + @test sol["termination_status"] == LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214835; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00932693; atol=1e-4) + for (bus, va, vm) in zip(["sourcebus", "primary", "loadbus"], + [0.0, [-0.22, -0.11, 0.12], [-0.48, -0.24, 0.27]], + [0.9959, [0.98094, 0.989365, 0.987043], [0.96355, 0.981767, 0.976786]]) + @test all(isapprox.(calc_va_acr(sol, bus), [0, -120, 120] .+ va; atol=0.2)) + @test all(isapprox.(calc_vm_acr(sol, bus), vm; atol=2e-3)) end - @testset "3-bus unbalanced acr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced.dss") - sol = PMD.run_mc_pf(pmd, PMs.ACRPowerModel, ipopt_solver) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 0.04296; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 0.01854; atol=1e-4) + end - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @testset "3-bus unbalanced w/ asymmetric linecode & phase order swap acp pf" begin + pmd = parse_file("../test/data/opendss/case3_unbalanced_assym_swap.dss") + sol = run_ac_mc_pf(pmd, ipopt_solver; make_si=false) - for (bus, va, vm) in zip(["1", "2", "3"], - [0.0, deg2rad.([-0.22, -0.11, 0.12]), deg2rad.([-0.48, -0.24, 0.27])], - [0.9959, [0.98094, 0.989365, 0.987043], [0.96355, 0.981767, 0.976786]]) - calc_vm(id) = sqrt.(sol["solution"]["bus"][id]["vr"].^2+sol["solution"]["bus"][id]["vi"].^2) - calc_va(id) = atan.(sol["solution"]["bus"][id]["vi"], sol["solution"]["bus"][id]["vr"]) - @test all(isapprox.(calc_va(bus), PMD._wrap_to_pi([2*pi/pmd["conductors"]*(1-c) for c in 1:pmd["conductors"]] .+ va); atol=deg2rad(0.2))) - @test all(isapprox.(calc_vm(bus), vm; atol=2e-3)) - end + @test sol["termination_status"] == LOCALLY_SOLVED - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214835; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00932693; atol=1e-4) - end + @test all(isapprox.(sol["solution"]["bus"]["primary"]["vm"], [0.983453, 0.98718, 0.981602]; atol=1e-5)) + @test all(isapprox.(sol["solution"]["bus"]["primary"]["va"], [-0.07, -120.19, 120.29]; atol=1e-2)) + end - @testset "3-bus unbalanced w/ assymetric linecode & phase order swap acp pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_unbalanced_assym_swap.dss") - sol = PMD.run_ac_mc_pf(pmd, ipopt_solver) + @testset "5-bus phase drop acp pf" begin + result = run_mc_pf(case5_phase_drop, ACPPowerModel, ipopt_solver; make_si=false) - @test sol["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED - @test all(isapprox.(sol["solution"]["bus"]["2"]["vm"], [0.983453, 0.98718, 0.981602]; atol=1e-5)) - @test all(isapprox.(sol["solution"]["bus"]["2"]["va"], deg2rad.([-0.07, -120.19, 120.29]); atol=1e-2)) - end + @test all(isapprox.(result["solution"]["bus"]["midbus"]["vm"], [0.973519, 0.964902, 0.956465]; atol = 1e-3)) + end - @testset "5-bus phase drop acp pf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_pf(mp_data, PMs.ACPPowerModel, ipopt_solver) + @testset "5-bus phase drop acr pf" begin + sol = run_mc_pf(case5_phase_drop, ACRPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(result["objective"], 0.0; atol = 1e-4) + @test sol["termination_status"] == LOCALLY_SOLVED - @test all(isapprox.(result["solution"]["bus"]["2"]["vm"], [0.973519, 0.964902, 0.956465]; atol = 1e-3)) - end + @test all(isapprox.(calc_vm_acr(sol, "midbus"), [0.973519, 0.964902, 0.956465]; atol=1e-4)) + end - @testset "5-bus phase drop acr pf" begin - mp_data = PMD.parse_file("../test/data/opendss/case5_phase_drop.dss") - result = run_mc_pf(mp_data, PMs.ACRPowerModel, ipopt_solver) + @testset "matrix branch shunts acp pf" begin + sol = run_ac_mc_pf(case_mxshunt, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED - @test isapprox(result["objective"], 0.0; atol = 1e-4) + @test all(isapprox.(sol["solution"]["bus"]["loadbus"]["vm"], [0.987399, 0.981300, 1.003536]; atol=1E-6)) + end - calc_vm(id) = sqrt.(result["solution"]["bus"][id]["vr"].^2+result["solution"]["bus"][id]["vi"].^2) - @test isapprox(calc_vm("2")[1], 0.973519; atol = 1e-4) - @test isapprox(calc_vm("2")[2], 0.964902; atol = 1e-4) - @test isapprox(calc_vm("2")[3], 0.956465; atol = 1e-4) - end + @testset "matrix branch shunts acr pf" begin + sol = run_mc_pf(case_mxshunt, ACRPowerModel, ipopt_solver; make_si=false) - @testset "matrix branch shunts acp pf" begin - sol = PMD.run_ac_mc_pf("../test/data/opendss/case_mxshunt.dss", ipopt_solver) + @test all(isapprox.(calc_vm_acr(sol, "loadbus"), [0.987399, 0.981299, 1.003537]; atol=1E-6)) + end - @test all(isapprox.(sol["solution"]["bus"]["2"]["vm"], [0.987399, 0.981300, 1.003536]; atol=1E-6)) - end + @testset "virtual sourcebus creation acp pf" begin + pmd = parse_file("../test/data/opendss/virtual_sourcebus.dss"; data_model=MATHEMATICAL) + result = run_ac_mc_pf(pmd, ipopt_solver) - @testset "matrix branch shunts acr pf" begin - data_pmd = PMD.parse_file("../test/data/opendss/case_mxshunt.dss") - pm = PMs.instantiate_model(data_pmd, PMs.ACRPowerModel, PMD.build_mc_pf, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + @test result["termination_status"] == LOCALLY_SOLVED - calc_vm(id) = sqrt.(sol["solution"]["bus"][id]["vr"].^2+sol["solution"]["bus"][id]["vi"].^2) - @test all(isapprox.(calc_vm("2"), [0.987399, 0.981299, 1.003537]; atol=1E-6)) - end + @test all(all(isapprox.(result["solution"]["bus"]["$n"]["vm"], [0.961352, 0.999418, 1.00113]; atol=1e-6)) for n in [1, 2]) + @test all(all(isapprox.(result["solution"]["bus"]["$n"]["va"], deg2rad.([-1.25, -120.06, 120.0]); atol=1e-1)) for n in [1, 2]) + end - @testset "virtual sourcebus creation acp pf" begin - result = run_ac_mc_pf("../test/data/opendss/virtual_sourcebus.dss", ipopt_solver) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @testset "2-bus diagonal ivr pf" begin + sol = run_mc_pf(case2_diag, IVRPowerModel, ipopt_solver) - @test all(all(isapprox.(result["solution"]["bus"]["$n"]["vm"], [0.961352, 0.999418, 1.00113]; atol=1e-6)) for n in [1, 2]) - @test all(all(isapprox.(rad2deg.(result["solution"]["bus"]["$n"]["va"]), [-1.25, -120.06, 120.0]; atol=1e-1)) for n in [1, 2]) - end + @test sol["termination_status"] == LOCALLY_SOLVED + + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.20896; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 0.20896; atol=1e-5) + end + + @testset "3-bus balanced ivr pf" begin + sol = run_mc_pf(case3_balanced, IVRPowerModel, ipopt_solver) + + @test sol["termination_status"] == LOCALLY_SOLVED + + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["pg"]), 18.34498; atol=1e-5) + @test isapprox(sum(sol["solution"]["voltage_source"]["source"]["qg"]), 9.19404; atol=1e-4) end + end diff --git a/test/pf_bf.jl b/test/pf_bf.jl new file mode 100644 index 000000000..0ecb3459e --- /dev/null +++ b/test/pf_bf.jl @@ -0,0 +1,116 @@ +@info "running branch-flow power flow (pf_bf) tests" + +@testset "test distflow formulations in pf" begin + @testset "test linearised distflow pf_bf" begin + @testset "5-bus lpubfdiag opf_bf" begin + mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") + make_multiconductor!(mp_data, 3) + result = run_mc_pf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0; atol = 1e0) + # @test isapprox(result["solution"]["bus"]["3"]["vm"], 0.911466*[1,1,1]; atol = 1e-3) + vm = calc_vm_w(result, "3") + @test isapprox(vm, [1,1,1]; atol = 1e-3) + + end + + @testset "3-bus balanced lpubfdiag pf_bf" begin + pmd = parse_file("../test/data/opendss/case3_balanced.dss"; data_model=MATHEMATICAL) + sol = run_mc_pf(pmd, LPUBFDiagPowerModel, ipopt_solver) + + @test sol["termination_status"] == LOCALLY_SOLVED + @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0183456; atol=2e-3) + @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923328; atol=2e-3) + end + + @testset "3-bus unbalanced lpubfdiag pf_bf" begin + pmd = parse_file("../test/data/opendss/case3_unbalanced.dss"; data_model=MATHEMATICAL) + sol = run_mc_pf(pmd, LPUBFDiagPowerModel, ipopt_solver) + + @test sol["termination_status"] == LOCALLY_SOLVED + @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0214812; atol=2e-3) + @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00927263; atol=2e-3) + end + end + + @testset "test linearised distflow pf_bf in diagonal matrix form" begin + @testset "5-bus lpdiagubf pf_bf" begin + mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") + make_multiconductor!(mp_data, 3) + result = run_mc_pf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0; atol = 1e0) + end + end + + @testset "test linearised distflow pf_bf in full matrix form" begin + @testset "5-bus lpfullubf pf_bf" begin + mp_data = PowerModels.parse_file("../test/data/matpower/case5.m") + make_multiconductor!(mp_data, 3) + result = run_mc_pf(mp_data, LPUBFDiagPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0; atol = 1e0) + end + end + + data = parse_file("../test/data/opendss/case3_unbalanced.dss"; transformations=[make_lossless!]) + data["settings"]["sbase_default"] = 0.001 * 1e3 + merge!(data["voltage_source"]["source"], Dict{String,Any}( + "cost_pg_parameters" => [0.0, 1000.0, 0.0], + "pg_lb" => fill( 0.0, 3), + "pg_ub" => fill( 10.0, 3), + "qg_lb" => fill(-10.0, 3), + "qg_ub" => fill( 10.0, 3), + ) + ) + + for (_,line) in data["line"] + line["sm_ub"] = fill(10.0, 3) + end + + data = transform_data_model(data) + + for (_,bus) in data["bus"] + if bus["name"] != "sourcebus" + bus["vmin"] = fill(0.9, 3) + bus["vmax"] = fill(1.1, 3) + end + end + + @testset "test sdp distflow pf_bf" begin + @testset "3-bus SDPUBF pf_bf" begin + result = run_mc_pf(data, SDPUBFPowerModel, scs_solver) + + @test result["termination_status"] == OPTIMAL + @test isapprox(result["objective"], 0; atol = 1e-2) + end + end + + # @testset "test sdp distflow pf_bf in full matrix form" begin + # @testset "3-bus SDPUBFKCLMX pf_bf" begin + # result = run_mc_pf(data, SDPUBFKCLMXPowerModel, scs_solver) + # + # @test result["termination_status"] == OPTIMAL + # @test isapprox(result["objective"], 0; atol = 1e-2) + # end + # end + + + @testset "test soc distflow pf_bf" begin + @testset "3-bus SOCNLPUBF pf_bf" begin + result = run_mc_pf(data, SOCNLPUBFPowerModel, ipopt_solver) + + @test result["termination_status"] == LOCALLY_SOLVED + @test isapprox(result["objective"], 0; atol = 1e-1) + end + @testset "3-bus SOCConicUBF pf_bf" begin + result = run_mc_pf(data, SOCConicUBFPowerModel, scs_solver) + + @test result["termination_status"] == OPTIMAL + @test isapprox(result["objective"], 0; atol = 1e-2) + end + end +end diff --git a/test/pf_iv.jl b/test/pf_iv.jl deleted file mode 100644 index e6dbbd8df..000000000 --- a/test/pf_iv.jl +++ /dev/null @@ -1,25 +0,0 @@ -@info "running current-voltage power flow (pf_iv) tests" - -@testset "test pf" begin - @testset "test opendss pf" begin - @testset "2-bus diagonal ivr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case2_diag.dss") - sol = PMD.run_mc_pf_iv(pmd, PMs.IVRPowerModel, ipopt_solver) - - @test sol["termination_status"] == PMs.LOCALLY_SOLVED - - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.018209; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.000208979; atol=1e-5) - end - - @testset "3-bus balanced ivr pf" begin - pmd = PMD.parse_file("../test/data/opendss/case3_balanced.dss") - sol = PMD.run_mc_pf_iv(pmd, PMs.IVRPowerModel, ipopt_solver) - - @test sol["termination_status"] == PMs.LOCALLY_SOLVED - - @test isapprox(sum(sol["solution"]["gen"]["1"]["pg"] * sol["solution"]["baseMVA"]), 0.0183456; atol=1e-5) - @test isapprox(sum(sol["solution"]["gen"]["1"]["qg"] * sol["solution"]["baseMVA"]), 0.00923328; atol=1e-4) - end - end -end diff --git a/test/runtests.jl b/test/runtests.jl index 8cea69752..db20e0fc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,7 @@ import Memento import InfrastructureModels import PowerModels -const PMs = PowerModels +const PM = PowerModels # Suppress warnings during testing. const TESTLOG = Memento.getlogger(PowerModels) @@ -31,37 +31,39 @@ cbc_solver = with_optimizer(Cbc.Optimizer, logLevel=0) scs_solver = with_optimizer(SCS.Optimizer, max_iters=20000, eps=1e-5, alpha=0.4, verbose=0) juniper_solver = with_optimizer(Juniper.Optimizer, nl_solver=with_optimizer(Ipopt.Optimizer, tol=1e-4, print_level=0), mip_solver=cbc_solver, log_levels=[]) -include("common.jl") # all passing +include("common.jl") @testset "PowerModelsDistribution" begin - include("opendss.jl") # all passing + include("opendss.jl") - include("data.jl") # all passing + include("data.jl") - include("pf.jl") # all passing + include("pf.jl") - include("pf_iv.jl") # all passing + include("pf_bf.jl") - include("opf.jl") # all passing + include("opf.jl") - include("opf_bf.jl") # all passing + include("opf_bf.jl") - include("opf_iv.jl") # all passing + include("opf_iv.jl") - include("storage.jl") # all passing + include("storage.jl") - include("debug.jl") # all passing + include("debug.jl") - include("multinetwork.jl") # all passing + include("multinetwork.jl") - include("transformer.jl") # all passing + include("transformer.jl") - include("loadmodels.jl") # all passing + include("loadmodels.jl") - include("delta_gens.jl") # all passing + include("delta_gens.jl") - include("shunt.jl") # all passing + include("shunt.jl") - include("mld.jl") # all passing + include("mld.jl") + + include("data_model.jl") end diff --git a/test/shunt.jl b/test/shunt.jl index 3e8c3210a..fa0b09725 100644 --- a/test/shunt.jl +++ b/test/shunt.jl @@ -1,7 +1,7 @@ @info "running matrix shunt tests" @testset "matrix shunts ACP/ACR/IVR" begin - data = PMD.parse_file("data/opendss/case_mxshunt_2.dss") + data = parse_file("data/opendss/case_mxshunt_2.dss"; data_model=MATHEMATICAL) shunt = data["shunt"]["1"] @test(isa(shunt["gs"], Matrix)) @test(isa(shunt["bs"], Matrix)) @@ -10,10 +10,10 @@ data_diag_shunt = deepcopy(data) data_diag_shunt["shunt"]["1"]["bs"] = shunt["bs"].*[c==d for c in 1:3, d in 1:3] - sol_acp_diag = run_mc_pf(data_diag_shunt, PMs.ACPPowerModel, ipopt_solver) - sol_acp = run_mc_pf(data, PMs.ACPPowerModel, ipopt_solver) - sol_acr = run_mc_pf(data, PMs.ACRPowerModel, ipopt_solver) - sol_iv = run_mc_pf_iv(data, PMs.IVRPowerModel, ipopt_solver) + sol_acp_diag = run_mc_pf(data_diag_shunt, ACPPowerModel, ipopt_solver) + sol_acp = run_mc_pf(data, ACPPowerModel, ipopt_solver) + sol_acr = run_mc_pf(data, ACRPowerModel, ipopt_solver) + sol_iv = run_mc_pf(data, IVRPowerModel, ipopt_solver) # check the results are different with only diagonal elements @test(!isapprox(sol_acp["solution"]["bus"]["2"]["vm"], sol_acp_diag["solution"]["bus"]["2"]["vm"])) diff --git a/test/storage.jl b/test/storage.jl index a999b2e72..820031072 100644 --- a/test/storage.jl +++ b/test/storage.jl @@ -1,56 +1,45 @@ @info "running storage optimal power flow tests" @testset "test storage opf" begin - @testset "5-bus storage acp opf_strg" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5_strg.m") - PMD.make_multiconductor!(mp_data, 3) + mp_data = PM.parse_file("../test/data/matpower/case5_strg.m") + make_multiconductor!(mp_data, 3) - result = PMD.run_mc_opf(mp_data, PowerModels.ACPPowerModel, ipopt_solver) + @testset "5-bus storage acp opf_strg" begin + result = run_mc_opf(mp_data, ACPPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 52299.2; atol = 1e0) - @test isapprox(result["solution"]["storage"]["1"]["se"], 0.0; atol = 1e0) - @test isapprox(result["solution"]["storage"]["2"]["se"], 0.0; atol = 1e0) - for c in 1:mp_data["conductors"] - @test isapprox(result["solution"]["storage"]["1"]["ps"][c], -0.0596928; atol = 1e-3) - @test isapprox(result["solution"]["storage"]["2"]["ps"][c], -0.0794600; atol = 1e-3) - end + @test isapprox(result["solution"]["storage"]["1"]["se"], 0.0; atol=1e0) + @test isapprox(result["solution"]["storage"]["2"]["se"], 0.0; atol=1e0) + + @test all(isapprox.(result["solution"]["storage"]["1"]["ps"], -0.0596928; atol=1e-3)) + @test all(isapprox.(result["solution"]["storage"]["2"]["ps"], -0.0794600; atol=1e-3)) end @testset "5-bus storage dcp opf_strg" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5_strg.m") - PMD.make_multiconductor!(mp_data, 3) - - result = PMD.run_mc_opf(mp_data, PowerModels.DCPPowerModel, ipopt_solver) + result = run_mc_opf(mp_data, DCPPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 52059.6; atol = 1e0) @test isapprox(result["solution"]["storage"]["1"]["se"], 0.0; atol = 1e0) @test isapprox(result["solution"]["storage"]["2"]["se"], 0.0; atol = 1e0) - for c in 1:mp_data["conductors"] - @test isapprox(result["solution"]["storage"]["1"]["ps"][c], -0.0596443; atol = 1e-3) - @test isapprox(result["solution"]["storage"]["2"]["ps"][c], -0.0793700; atol = 1e-3) - end + @test all(isapprox.(result["solution"]["storage"]["1"]["ps"], -0.0596443; atol=1e-3)) + @test all(isapprox.(result["solution"]["storage"]["2"]["ps"], -0.0793700; atol=1e-3)) end @testset "5-bus storage nfa opf_strg" begin - mp_data = PowerModels.parse_file("../test/data/matpower/case5_strg.m") - PMD.make_multiconductor!(mp_data, 3) - - result = PMD.run_mc_opf(mp_data, PowerModels.NFAPowerModel, ipopt_solver) + result = run_mc_opf(mp_data, NFAPowerModel, ipopt_solver; make_si=false) - @test result["termination_status"] == PMs.LOCALLY_SOLVED + @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 43169.9; atol = 1e0) @test isapprox(result["solution"]["storage"]["1"]["se"], 0.0; atol = 1e0) @test isapprox(result["solution"]["storage"]["2"]["se"], 0.0; atol = 1e0) - for c in 1:mp_data["conductors"] - @test isapprox(result["solution"]["storage"]["1"]["ps"][c], -0.0596443; atol = 1e-3) - @test isapprox(result["solution"]["storage"]["2"]["ps"][c], -0.0793700; atol = 1e-3) - end + @test all(isapprox.(result["solution"]["storage"]["1"]["ps"], -0.0596443; atol=1e-3)) + @test all(isapprox.(result["solution"]["storage"]["2"]["ps"], -0.0793700; atol=1e-3)) end end diff --git a/test/transformer.jl b/test/transformer.jl index 0a094d638..8769e0d4c 100644 --- a/test/transformer.jl +++ b/test/transformer.jl @@ -1,131 +1,121 @@ @info "running transformer tests" @testset "transformers" begin - @testset "test transformer acp pf" begin @testset "2w transformer acp pf yy" begin - file = "../test/data/opendss/ut_trans_2w_yy.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test norm(vm(sol, pmd_data, "3")-[0.87451, 0.8613, 0.85348], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[-0.1, -120.4, 119.8], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_2w_yy.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; solution_processors=[sol_polar_voltage!], make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.87451, 0.8613, 0.85348], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[-0.1, -120.4, 119.8], Inf) <= 0.1 end @testset "2w transformer acp pf dy_lead" begin - file = "../test/data/opendss/ut_trans_2w_dy_lead.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test norm(vm(sol, pmd_data, "3")-[0.87391, 0.86055, 0.85486], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[29.8, -90.4, 149.8], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_2w_dy_lead.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; solution_processors=[sol_polar_voltage!], make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.87391, 0.86055, 0.85486], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[29.8, -90.4, 149.8], Inf) <= 0.1 end @testset "2w transformer acp pf dy_lag" begin - file = "../test/data/opendss/ut_trans_2w_dy_lag.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test norm(vm(sol, pmd_data, "3")-[0.92092, 0.91012, 0.90059], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[-30.0, -150.4, 89.8], Inf) <= 0.1 + file = + eng = parse_file("../test/data/opendss/ut_trans_2w_dy_lag.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.92092, 0.91012, 0.90059], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[-30.0, -150.4, 89.8], Inf) <= 0.1 end end @testset "test transformer ivr pf" begin @testset "2w transformer ivr pf yy" begin - file = "../test/data/opendss/ut_trans_2w_yy.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_mc_pf_iv(pmd_data, PMs.IVRPowerModel, ipopt_solver) - @test norm(calc_vm_acr(sol, pmd_data, "3")-[0.87451, 0.8613, 0.85348], Inf) <= 1.5E-5 - @test norm(calc_va_acr(sol, pmd_data, "3")-[-0.1, -120.4, 119.8], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_2w_yy.dss") + sol = run_mc_pf(eng, IVRPowerModel, ipopt_solver; solution_processors=[sol_polar_voltage!], make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.87451, 0.8613, 0.85348], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[-0.1, -120.4, 119.8], Inf) <= 0.1 end @testset "2w transformer ivr pf dy_lead" begin - file = "../test/data/opendss/ut_trans_2w_dy_lead.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_mc_pf_iv(pmd_data, PMs.IVRPowerModel, ipopt_solver) - @test norm(calc_vm_acr(sol, pmd_data, "3")-[0.87391, 0.86055, 0.85486], Inf) <= 1.5E-5 - @test norm(calc_va_acr(sol, pmd_data, "3")-[29.8, -90.4, 149.8], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_2w_dy_lead.dss") + sol = run_mc_pf(eng, IVRPowerModel, ipopt_solver; solution_processors=[sol_polar_voltage!], make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.87391, 0.86055, 0.85486], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[29.8, -90.4, 149.8], Inf) <= 0.1 end @testset "2w transformer ivr pf dy_lag" begin - file = "../test/data/opendss/ut_trans_2w_dy_lag.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_mc_pf_iv(pmd_data, PMs.IVRPowerModel, ipopt_solver) - @test norm(calc_vm_acr(sol, pmd_data, "3")-[0.92092, 0.91012, 0.90059], Inf) <= 1.5E-5 - @test norm(calc_va_acr(sol, pmd_data, "3")-[-30.0, -150.4, 89.8], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_2w_dy_lag.dss") + sol = run_mc_pf(eng, IVRPowerModel, ipopt_solver; solution_processors=[sol_polar_voltage!], make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.92092, 0.91012, 0.90059], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[-30.0, -150.4, 89.8], Inf) <= 0.1 end end - @testset "2w transformer ac pf yy - banked transformers" begin - file = "../test/data/opendss/ut_trans_2w_yy_bank.dss" - pmd1 = PMD.parse_file(file) - pmd2 = PMD.parse_file(file; bank_transformers=false) - result1 = run_ac_mc_pf(pmd1, ipopt_solver) - result2 = run_ac_mc_pf(pmd2, ipopt_solver) - - @test result1["termination_status"] == PMs.LOCALLY_SOLVED - @test result2["termination_status"] == PMs.LOCALLY_SOLVED - @test result1["solution"]["bus"] == result2["solution"]["bus"] - @test result1["solution"]["gen"] == result2["solution"]["gen"] - - dss = PMD.parse_dss(file) - PMD.parse_dss_with_dtypes!(dss, ["line", "load", "transformer"]) - trans = PMD._create_transformer(dss["transformer"][1]["name"]; PMD._to_sym_keys(dss["transformer"][1])...) - @test all(trans["%rs"] .== [1.0, 2.0]) + eng1 = parse_file("../test/data/opendss/ut_trans_2w_yy_bank.dss") + eng2 = parse_file("../test/data/opendss/ut_trans_2w_yy_bank.dss"; bank_transformers=false) + result1 = run_ac_mc_pf(eng1, ipopt_solver) + result2 = run_ac_mc_pf(eng2, ipopt_solver) + + @test result1["termination_status"] == LOCALLY_SOLVED + @test result2["termination_status"] == LOCALLY_SOLVED + + # @test result1["solution"]["bus"] == result2["solution"]["bus"] # TODO need a new test, transformer model changed, use voltages on real bus + # @test result1["solution"]["gen"] == result2["solution"]["gen"] # TODO need a new test, transformer model changed, use voltages on real bus end @testset "three winding transformer pf" begin @testset "3w transformer ac pf dyy - all non-zero" begin - file = "../test/data/opendss/ut_trans_3w_dyy_1.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test norm(vm(sol, pmd_data, "3")-[0.9318, 0.88828, 0.88581], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[30.1, -90.7, 151.2], Inf) <= 0.1 + file = + eng = parse_file("../test/data/opendss/ut_trans_3w_dyy_1.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.9318, 0.88828, 0.88581], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[30.1, -90.7, 151.2], Inf) <= 0.1 end @testset "3w transformer ac pf dyy - some non-zero" begin - file = "../test/data/opendss/ut_trans_3w_dyy_2.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - #@test isapprox(vm(sol, pmd_data, "3"), [0.93876, 0.90227, 0.90454], atol=1E-4) - @test norm(vm(sol, pmd_data, "3")-[0.93876, 0.90227, 0.90454], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[31.6, -88.8, 153.3], Inf) <= 0.1 + file = + eng = parse_file("../test/data/opendss/ut_trans_3w_dyy_2.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; make_si=false) + #@test isapprox(vm(sol, eng, "3"), [0.93876, 0.90227, 0.90454], atol=1E-4) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.93876, 0.90227, 0.90454], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[31.6, -88.8, 153.3], Inf) <= 0.1 end @testset "3w transformer ac pf dyy - all zero" begin - file = "../test/data/opendss/ut_trans_3w_dyy_3.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test norm(vm(sol, pmd_data, "3")-[0.97047, 0.93949, 0.946], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[30.6, -90.0, 151.9], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_3w_dyy_3.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.97047, 0.93949, 0.946], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[30.6, -90.0, 151.9], Inf) <= 0.1 end @testset "3w transformer ac pf dyy - %loadloss=0" begin - file = "../test/data/opendss/ut_trans_3w_dyy_3_loadloss.dss" - pmd_data = PMD.parse_file(file) - sol = PMD.run_ac_mc_pf(pmd_data, ipopt_solver, multiconductor=true) - @test haskey(sol["solution"]["bus"], "10") - @test norm(vm(sol, pmd_data, "3")-[0.969531, 0.938369, 0.944748], Inf) <= 1.5E-5 - @test norm(va(sol, pmd_data, "3")-[30.7, -90.0, 152.0], Inf) <= 0.1 + eng = parse_file("../test/data/opendss/ut_trans_3w_dyy_3_loadloss.dss") + sol = run_ac_mc_pf(eng, ipopt_solver; make_si=false) + @test norm(sol["solution"]["bus"]["3"]["vm"]-[0.969531, 0.938369, 0.944748], Inf) <= 1.5E-5 + @test norm(sol["solution"]["bus"]["3"]["va"]-[30.7, -90.0, 152.0], Inf) <= 0.1 end end @testset "oltc tests" begin @testset "2w transformer acp opf_oltc yy" begin - file = "../test/data/opendss/ut_trans_2w_yy_oltc.dss" - pmd_data = PMD.parse_file(file) + eng = parse_file("../test/data/opendss/ut_trans_2w_yy_oltc.dss") + # free the taps - pmd_data["transformer"]["1"]["fixed"] = zeros(Bool, 3) - pmd_data["transformer"]["2"]["fixed"] = zeros(Bool, 3) - pm = PMs.instantiate_model(pmd_data, PMs.ACPPowerModel, PMD.build_mc_opf_oltc, ref_extensions=[PMD.ref_add_arcs_trans!], multiconductor=true) - sol = PMs.optimize_model!(pm, optimizer=ipopt_solver) + eng["transformer"]["tx1"]["tm_fix"] = fill(zeros(Bool, 3), 2) + + math = transform_data_model(eng) + pm = PM.instantiate_model(math, ACPPowerModel, build_mc_opf_oltc, ref_extensions=[ref_add_arcs_transformer!]) + sol = PM.optimize_model!(pm, optimizer=ipopt_solver) + # check that taps are set as to boost the voltage in the branches as much as possible; # this is trivially optimal if the voltage bounds are not binding # and without significant shunts (both branch and transformer) @test norm(tap(1,pm)-[0.95, 0.95, 0.95], Inf) <= 1E-4 @test norm(tap(2,pm)-[1.05, 1.05, 1.05], Inf) <= 1E-4 + # then check whether voltage is what OpenDSS expects for those values - @test norm(vm(sol, pmd_data, "3")-[1.0352, 1.022, 1.0142], Inf) <= 1E-4 - @test norm(va(sol, pmd_data, "3")-[-0.1, -120.4, 119.8], Inf) <= 0.1 + solution = transform_solution(sol["solution"], math, make_si=false) + + @test norm(solution["bus"]["3"]["vm"]-[1.0352, 1.022, 1.0142], Inf) <= 1E-4 + @test norm(solution["bus"]["3"]["va"]-[-0.1, -120.4, 119.8], Inf) <= 0.1 end end end