diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e40c81..25bb539 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: version: - - '1.6' + - '1.9' - 'nightly' os: - macOS-latest @@ -19,12 +19,12 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 env: cache-name: cache-artifacts with: @@ -39,6 +39,6 @@ jobs: - uses: julia-actions/julia-runtest@latest continue-on-error: ${{ matrix.version == 'nightly' }} - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: file: lcov.info diff --git a/.gitignore b/.gitignore index fb7254b..8ac8ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ docs/build/ Manifest.toml test/data/enwl/measurements/temp.csv .vscode/ +*.dbl +*.DSV diff --git a/Project.toml b/Project.toml index 12b8016..5ce7ea0 100644 --- a/Project.toml +++ b/Project.toml @@ -32,9 +32,9 @@ JuMP = "1" LoggingExtras = "~0.4" Optim = "~1.2.0, 1.7" Polynomials = "~2.0.17, 3" -PowerModelsDistribution = "~0.11, ~0.12, ~0.13, 0.14" +PowerModelsDistribution = "~0.11, ~0.12, ~0.13, ~0.14, ~0.15" SpecialFunctions = "~1.5.1, ~1.6, ~1.7, ~1.8, ~2.0" -julia = "^1.6" +julia = "1.6 - 1.11" [extras] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/build/assets/search.js b/docs/build/assets/search.js index 42a273f..e30e907 100644 --- a/docs/build/assets/search.js +++ b/docs/build/assets/search.js @@ -46,7 +46,7 @@ $(document).ready(function() { }) // list below is the lunr 2.1.3 list minus the intersect with names(Base) - // (all, any, get, in, is, which) and (do, else, for, let, where, while, with) + // (all, any, get, in, is, only, which) and (do, else, for, let, where, while, with) // ideally we'd just filter the original list but it's not available as a variable lunr.stopWordFilter = lunr.generateStopWordFilter([ 'a', @@ -112,7 +112,6 @@ $(document).ready(function() { 'off', 'often', 'on', - 'only', 'or', 'other', 'our', @@ -196,14 +195,14 @@ $(document).ready(function() { q.term(t.toString(), { fields: ["title"], boost: 100, - usePipeline: false, + usePipeline: true, editDistance: 0, wildcard: lunr.Query.wildcard.NONE }) q.term(t.toString(), { fields: ["title"], boost: 10, - usePipeline: false, + usePipeline: true, editDistance: 2, wildcard: lunr.Query.wildcard.NONE }) diff --git a/docs/build/assets/themes/documenter-dark.css b/docs/build/assets/themes/documenter-dark.css index 5592429..1c370f2 100644 --- a/docs/build/assets/themes/documenter-dark.css +++ b/docs/build/assets/themes/documenter-dark.css @@ -7401,6 +7401,7 @@ html.theme--documenter-dark { padding: 0; flex: 0 0 18rem; z-index: 5; + font-size: 15px; position: fixed; left: -18rem; width: 18rem; @@ -7442,7 +7443,6 @@ html.theme--documenter-dark { html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu { flex-grow: 1; user-select: none; - font-size: 15px; border-top: 1px solid #5e6d6f; padding-bottom: 1.5rem; /* Managing collapsible submenus */ } @@ -7482,16 +7482,21 @@ html.theme--documenter-dark { display: block; padding: 0.5rem 0.5rem; } html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu .tocitem, html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu .tocitem:hover { - color: #fff; } + color: #fff; + background: #282f2f; } html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu a.tocitem:hover, html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu label.tocitem:hover { + color: #fff; background-color: #32393a; } html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active { - border-top: 1px solid #718385; + border-top: 1px solid #5e6d6f; border-bottom: 1px solid #5e6d6f; background-color: #1f2424; } - html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active > .tocitem, html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active > .tocitem:hover { + html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active .tocitem, html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active .tocitem:hover { background-color: #1f2424; color: #fff; } + html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active ul.internal .tocitem:hover { + background-color: #32393a; + color: #fff; } html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu > li.is-active:first-child { border-top: none; } html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu ul.internal { diff --git a/docs/build/assets/themes/documenter-light.css b/docs/build/assets/themes/documenter-light.css index ffcf462..bfb4e9d 100644 --- a/docs/build/assets/themes/documenter-light.css +++ b/docs/build/assets/themes/documenter-light.css @@ -7383,6 +7383,7 @@ html { padding: 0; flex: 0 0 18rem; z-index: 5; + font-size: 1rem; position: fixed; left: -18rem; width: 18rem; @@ -7424,7 +7425,6 @@ html { #documenter .docs-sidebar ul.docs-menu { flex-grow: 1; user-select: none; - font-size: 1rem; border-top: 1px solid #dbdbdb; padding-bottom: 1.5rem; /* Managing collapsible submenus */ } @@ -7464,16 +7464,21 @@ html { display: block; padding: 0.5rem 0.5rem; } #documenter .docs-sidebar ul.docs-menu .tocitem, #documenter .docs-sidebar ul.docs-menu .tocitem:hover { - color: #0a0a0a; } + color: #0a0a0a; + background: whitesmoke; } #documenter .docs-sidebar ul.docs-menu a.tocitem:hover, #documenter .docs-sidebar ul.docs-menu label.tocitem:hover { + color: #0a0a0a; background-color: #ebebeb; } #documenter .docs-sidebar ul.docs-menu li.is-active { - border-top: 1px solid #c7c7c7; + border-top: 1px solid #dbdbdb; border-bottom: 1px solid #dbdbdb; background-color: white; } - #documenter .docs-sidebar ul.docs-menu li.is-active > .tocitem, #documenter .docs-sidebar ul.docs-menu li.is-active > .tocitem:hover { + #documenter .docs-sidebar ul.docs-menu li.is-active .tocitem, #documenter .docs-sidebar ul.docs-menu li.is-active .tocitem:hover { background-color: white; color: #0a0a0a; } + #documenter .docs-sidebar ul.docs-menu li.is-active ul.internal .tocitem:hover { + background-color: #ebebeb; + color: #0a0a0a; } #documenter .docs-sidebar ul.docs-menu > li.is-active:first-child { border-top: none; } #documenter .docs-sidebar ul.docs-menu ul.internal { diff --git a/docs/build/index.html b/docs/build/index.html index a6da1a2..9488fae 100644 --- a/docs/build/index.html +++ b/docs/build/index.html @@ -1,2 +1,2 @@ -Home · PowerModelsSE.jl

PowerModelsSE.jl

Dev Build Status Build Status Codecov

PowerModelsSE.jl is an extention package of PowerModelsDistribution.jl for Static Distribution System State Estimation.

A Distribution System State Estimator determines the most-likely state of distribution system given a set of uncertainties, e.g., measurement errors, pseudo-measurements, etc. These uncertainties may pertain to any quantity of any network component, e.g., :vm of a :bus, :pd of a :load, etc.

Currently, uncertainties may either be described by:

  • a deterministic value Float64, or
  • a continuous univariate distribution ContinuousUnivariateDistribution:
    • a normal distribution, modeled through either WLS or LAV approach, or
    • a non-normal distribution, modeled through -logpdf.

Core Problem Specification

  • State Estimation (SE) as equality constrained optimization problem

Core Network Constraint Formulations

  • AC Polar (exact)
  • AC Rectangular (exact)
  • AC IV Rectangular (exact)
  • SDP (positive semi-definite relaxation)

All the formulations are three-phase unbalanced and feature accurate delta/wye load models. The exact formulations also feature delta/wye transformer models. Network constraint, load and transformer models are taken from PowerModelsDistribution.jl

Network Data Formats

  • OpenDSS ".dss" files in the PowerModelsDistribution format
  • CSV ".csv" file with measurement a statistical information for state estimation

Summary of State Estimation Possibilities

-ACPACRIVRSDP
BI/BFBIBIBFBF
Simple SE[1]AvailableAvailableAvailableAvailable
Advanced SE[2]AvailableAvailableAvailableUnavailable
4-wire[3]v0.2.0v0.2.0v0.2.0v0.2.0

Installation

The latest stable release of PowerModelsSE can be installed using the Julia package manager:

] add https://github.com/timmyfaraday/PowerModelsSE.jl.git

In order to test whether the package works, run:

] test MultiStateSystems

Acknowledgements

This code has been developed at KU Leuven (University of Leuven). The primary developers are Tom Van Acker (@timmyfaraday) and Marta Vanin (@MartaVanin) with support for the following contributors:

  • Frederik Geth (@frederikgeth), CSIRO, General PowerModelsDistribution.jl Advice.
  • Sander Claeys (@sanderclaeys), KU Leuven, General PowerModelsDistribution.jl Advice.

License

This code is provided under a BSD license.

Notes

Currently, bad data detection techniques and observability considerations are out of scope.

  • 1The simple SE does not include transformer models and delta/wye loads.
  • 2The advanced SE includes transformer models and delta/wye loads.
  • 3Awaiting PowerModelsDistribution v0.10.0
+Home · PowerModelsDistributionStateEstimation.jl

PowerModelsDistributionStateEstimation.jl Documentation

Overview

PowerModelsDistributionStateEstimation.jl is an extension package of PowerModelsDistribution.jl for three-phase static Power System State Estimation.

A Power System State Estimator determines the most-likely state of power system given a set of uncertainties, e.g., measurement errors, pseudo-measurements, etc. These uncertainties may pertain to any quantity of any network component, e.g., voltage magnitude (vm) of a bus, power demand (pd) of a load, etc.

Installation

The latest stable release of PowerModelsDistributionStateEstimation can be installed using the Julia package manager:

] add PowerModelsDistributionStateEstimation

To be able to use PowerModelsDistributionStateEstimation, at least one solver is required. For our package tests, we rely on Ipopt and SCS solvers, since they do not have license restrictions. Both solvers can be installed using the package manager:

] add Ipopt
] add SCS

However, it should be noted that, depending on the problem type, these solvers might not be the most appropriate/efficient choice.

In order to test whether the package works, run:

] test PowerModelsDistributionStateEstimation
diff --git a/docs/build/search/index.html b/docs/build/search/index.html index 9dcce5c..7f54c1f 100644 --- a/docs/build/search/index.html +++ b/docs/build/search/index.html @@ -1,2 +1,2 @@ -Search · PowerModelsSE.jl

Loading search...

    +Search · PowerModelsDistributionStateEstimation.jl

    Loading search...

      diff --git a/docs/build/search_index.js b/docs/build/search_index.js index 168c59d..f8ac475 100644 --- a/docs/build/search_index.js +++ b/docs/build/search_index.js @@ -1,3 +1,3 @@ var documenterSearchIndex = {"docs": -[{"location":"quickguide/#Quick-Start-Guide-1","page":"Quick Start Guide","title":"Quick Start Guide","text":"","category":"section"},{"location":"quickguide/#","page":"Quick Start Guide","title":"Quick Start Guide","text":"Once PowerModelsSE is installed, together with its dependencies, install Ipopt and SCS. These are needed to solve non-convex and convex problems, respectively. To run a simulation, a network data file (e.g. \"case3_unbalanced.dss\" in the package folder under /test/data/opendss) needs to be acquired, together with its relative measurement file (e.g. \"case3_input.csv\" in the package folder under /test/data/). Network and measurement data will be merged and a SE example can be run as follow:","category":"page"},{"location":"quickguide/#","page":"Quick Start Guide","title":"Quick Start Guide","text":"using PowerModelsSE, PowerModelsDistribution\nusing Ipopt\n\n_PMD = PowerModelsDistribution\n\ndata = parse_file(\"test/data/opendss/case3_unbalanced.dss\")\npmd_data = _PMD.transform_data_model(data)\nmeas_file = \"test/data/case3_input.csv\"\nadd_measurement_to_pmd_data!(pmd_data, meas_file; actual_meas=false, seed=0)\nadd_measurement_id_to_load!(pmd_data, meas_file)\npmd_data[\"setting\"] = Dict{String,Any}(\"estimation_criterion\" => \"wls\",\n \"rescale_weight\" => 1)\n\nse_result = run_ivr_mc_se(pmd_data, optimizer_with_attributes(Ipopt.Optimizer, \"tol\"=>1e-6, \"print_level\"=>0))","category":"page"},{"location":"quickguide/#","page":"Quick Start Guide","title":"Quick Start Guide","text":"The run commands return detailed results data in the form of a dictionary, following PowerModelsDistribution format, and can be saved for further processing, as above.","category":"page"},{"location":"quickguide/#Accessing-Different-Formulations-1","page":"Quick Start Guide","title":"Accessing Different Formulations","text":"","category":"section"},{"location":"quickguide/#","page":"Quick Start Guide","title":"Quick Start Guide","text":"To different formulations correspond different run functions. The function \"runacpmcse\" uses the AC Polar form, \"runacrmcse\" uses the AC rectangular, etc.","category":"page"},{"location":"quick_start_guide/#Quick-Start-Guide-1","page":"Getting Started","title":"Quick Start Guide","text":"","category":"section"},{"location":"quick_start_guide/#Introduction-1","page":"Getting Started","title":"Introduction","text":"","category":"section"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"Once Ipopt, PowerModelsSE and PowerModelsDistribution are installed, and a network data file (e.g. \"case3_unbalanced.dss\" in the package folder under ./test/data/extra/network) has been acquired as well as a measurement data file (e.g. \"case3_unbalanced.csv\" in the package folder under ./test/data/extra/measurement), an unbalanced AC Static State Estimation can be executed with,","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"using Ipopt\nusing PowerModelsSE\nusing PowerModelsDistribution\n\nrun_mc_se(\"case3_unbalanced.dss\", \"case3_unbalanced.csv\", with_optimizer(Ipopt.Optimizer))","category":"page"},{"location":"quick_start_guide/#Network-Data-Input-1","page":"Getting Started","title":"Network Data Input","text":"","category":"section"},{"location":"quick_start_guide/#Parsing-OpenDSS-files-1","page":"Getting Started","title":"Parsing OpenDSS files","text":"","category":"section"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"To parse an OpenDSS file into PowerModelsDistribution's default ENGINEERING format, use the parse_file command:","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"eng = parse_file(\"case3_unbalanced.dss\")","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"To get the MATHEMATICAL model it is possible to transform the data model using the transform_data_model command.","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"math = transform_data_model(eng)","category":"page"},{"location":"quick_start_guide/#Parsing-ENWL-files-1","page":"Getting Started","title":"Parsing ENWL files","text":"","category":"section"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"To parse a specific feeder fdr of a network ntw of the ENWL data use:","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"data = parse_file(get_enwl_dss_path(ntw,fdr))","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"Parsing ENWL data requires the addition of a load profile before it can be used. This may be accomplished using","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.insert_profiles!(data, season, devices, pfs; t=missing, useactual=true)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.insert_profiles!-NTuple{4,Any}","page":"Getting Started","title":"PowerModelsSE.insert_profiles!","text":"insert_profiles!(data, season, devices, pfs; t=missing, useactual=true)\n\nThis function adds the load profiles to the parsed ENWL ENGINEERING data data.\n\nArguments\n\nseason: \"summer\", \"winter\"\ndevices: \"load\", \"pv\", \"ehp\", \"uchp\"\npfs: power factor of the devices\nt: time-step\n\nExample\n\njulia> data = parse_file(get_enwl_dss_path(10, 1))\njulia> insert_profiles!(data, \"summer\", [\"load\", \"pv\"], [0.95, 0.90], t = 144)\n\n\n\n\n\n","category":"method"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"Additionally, some functions are include specifically for the ENWL data to simplify the data in order to improve tractability.","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.rm_enwl_transformer!(data_eng)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.rm_enwl_transformer!-Tuple{Any}","page":"Getting Started","title":"PowerModelsSE.rm_enwl_transformer!","text":"rm_enwl_transformer!(data_eng)\n\nThis function removes the transformer from a parsed ENWL ENGINEERING data file.\n\n\n\n\n\n","category":"method"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.reduce_enwl_lines_eng!(data_eng)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.reduce_enwl_lines_eng!-Tuple{Any}","page":"Getting Started","title":"PowerModelsSE.reduce_enwl_lines_eng!","text":"reduce_enwl_lines_eng!(data_eng)\n\nThis function removes all trailing lines from a parsed ENWL ENGINEERING data file.\n\n\n\n\n\n","category":"method"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.reduce_enwl_lines_math!(data_math)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.reduce_enwl_lines_math!-Tuple{Any}","page":"Getting Started","title":"PowerModelsSE.reduce_enwl_lines_math!","text":"reduce_enwl_lines_math!(data_math)\n\nThis function removes all trailing lines from a parsed ENWL MATHEMATICAL data file.\n\n\n\n\n\n","category":"method"},{"location":"quick_start_guide/#Measurement-Data-Input-1","page":"Getting Started","title":"Measurement Data Input","text":"","category":"section"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"Adding the measurements to a MATHEMATICAL data dictionary may be accomplished through:","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.add_measurements!(data::Dict, meas_file::String; actual_meas = false)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.add_measurements!-Tuple{Dict,String}","page":"Getting Started","title":"PowerModelsSE.add_measurements!","text":"add_measurements!(data::Dict, meas_file::String; actual_meas::Bool = false, seed::Int=0)\n\nAdd measurement data from separate CSV file to the PowerModelsDistribution data dictionary.\n\nArguments\n\ndata: data dictionary in a format usable with PowerModelsDistribution\nmeas_file: path to and name of file with measurement data\nactual_meas: default is false.\nfalse: the \"val\" in meas_file are not actual measurements, e.g., error-free powerflow results. In this case, a fake measurement is built, extracting a value from the given distribution.\ntrue: the \"val\" column in meas_file are actual measurement values, i.e., with an error. No other processing is required.\nseed: random seed value to make the results reproducible. It is an argument and not a fixed value inside the function to allow, e.g., the exploration of different MonteCarlo scenarios\n\n\n\n\n\n","category":"method"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"Furthermore, functionality is included to write a measurement file, i.e., a csv-file based on powerflow results, using:","category":"page"},{"location":"quick_start_guide/#","page":"Getting Started","title":"Getting Started","text":"PowerModelsSE.write_measurements!(model::Type, data::Dict, pf_results::Dict, path::String)","category":"page"},{"location":"quick_start_guide/#PowerModelsSE.write_measurements!-Tuple{Type,Dict,Dict,String}","page":"Getting Started","title":"PowerModelsSE.write_measurements!","text":"write_measurements!(model::Type, data::Dict, pf_results::Dict, path::String)\n\nFunction to write a measurement file, i.e., a csv-file, to a specific path based on the power flow.\n\n\n\n\n\n","category":"method"},{"location":"#PowerModelsSE.jl-1","page":"Home","title":"PowerModelsSE.jl","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"(Image: Dev) (Image: Build Status) (Image: Build Status) (Image: Codecov)","category":"page"},{"location":"#","page":"Home","title":"Home","text":"PowerModelsSE.jl is an extention package of PowerModelsDistribution.jl for Static Distribution System State Estimation.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"A Distribution System State Estimator determines the most-likely state of distribution system given a set of uncertainties, e.g., measurement errors, pseudo-measurements, etc. These uncertainties may pertain to any quantity of any network component, e.g., :vm of a :bus, :pd of a :load, etc.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"Currently, uncertainties may either be described by:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"a deterministic value Float64, or\na continuous univariate distribution ContinuousUnivariateDistribution:\na normal distribution, modeled through either WLS or LAV approach, or\na non-normal distribution, modeled through -logpdf.","category":"page"},{"location":"#Core-Problem-Specification-1","page":"Home","title":"Core Problem Specification","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"State Estimation (SE) as equality constrained optimization problem","category":"page"},{"location":"#Core-Network-Constraint-Formulations-1","page":"Home","title":"Core Network Constraint Formulations","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"AC Polar (exact)\nAC Rectangular (exact)\nAC IV Rectangular (exact)\nSDP (positive semi-definite relaxation)","category":"page"},{"location":"#","page":"Home","title":"Home","text":"All the formulations are three-phase unbalanced and feature accurate delta/wye load models. The exact formulations also feature delta/wye transformer models. Network constraint, load and transformer models are taken from PowerModelsDistribution.jl","category":"page"},{"location":"#Network-Data-Formats-1","page":"Home","title":"Network Data Formats","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"OpenDSS \".dss\" files in the PowerModelsDistribution format\nCSV \".csv\" file with measurement a statistical information for state estimation","category":"page"},{"location":"#Summary-of-State-Estimation-Possibilities-1","page":"Home","title":"Summary of State Estimation Possibilities","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"- ACP ACR IVR SDP\nBI/BF BI BI BF BF\nSimple SE[1] Available Available Available Available\nAdvanced SE[2] Available Available Available Unavailable\n4-wire[3] v0.2.0 v0.2.0 v0.2.0 v0.2.0","category":"page"},{"location":"#","page":"Home","title":"Home","text":"[1]: The simple SE does not include transformer models and delta/wye loads.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"[2]: The advanced SE includes transformer models and delta/wye loads.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"[3]: Awaiting PowerModelsDistribution v0.10.0","category":"page"},{"location":"#Installation-1","page":"Home","title":"Installation","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"The latest stable release of PowerModelsSE can be installed using the Julia package manager:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] add https://github.com/timmyfaraday/PowerModelsSE.jl.git","category":"page"},{"location":"#","page":"Home","title":"Home","text":"In order to test whether the package works, run:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] test MultiStateSystems","category":"page"},{"location":"#Acknowledgements-1","page":"Home","title":"Acknowledgements","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"This code has been developed at KU Leuven (University of Leuven). The primary developers are Tom Van Acker (@timmyfaraday) and Marta Vanin (@MartaVanin) with support for the following contributors:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"Frederik Geth (@frederikgeth), CSIRO, General PowerModelsDistribution.jl Advice.\nSander Claeys (@sanderclaeys), KU Leuven, General PowerModelsDistribution.jl Advice.","category":"page"},{"location":"#License-1","page":"Home","title":"License","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"This code is provided under a BSD license.","category":"page"},{"location":"#Notes-1","page":"Home","title":"Notes","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"Currently, bad data detection techniques and observability considerations are out of scope.","category":"page"},{"location":"measurements/#Measurement-Conversion-1","page":"Measurement Conversion","title":"Measurement Conversion","text":"","category":"section"},{"location":"measurements/#Introduction-1","page":"Measurement Conversion","title":"Introduction","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"Any network formulation has a specific variable space, e.g., ACP includes vm, va, px and qx[1].","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"[1]: The x in px, qx, cmx, cax, crx and cix indicates that these variables exists for branches (~), generators (g) and loads (-). In order to capture the variable for a specific element it should be rewritten, e.g., \"px\" respectively becomes \"p\", \"pg\" and \"pd\".","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"- vm va cmx cax crx cix px qx vr vi\nACP N N SF X F F N N X X\nACR SF PP SF X MF MF N N N N\nIVR SF PP SF PP N N M M N N","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"where:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"F: conversion of type Fraction\nM: conversion of type Multiplication\nMF: conversion of type MultiplicationFraction\nN: native to the network formulation\nPP: conversion of type PreProcessing\nSF: conversion of type SquareFraction\nX: not provided","category":"page"},{"location":"measurements/#Conversion-1","page":"Measurement Conversion","title":"Conversion","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"Certain measurement variables may not be natively supported in the formulation space. Consequently, it becomes necessary to convert them into that specific space. This is accomplished through the inclusion of an additional constraint(s). The different types of conversion constraints are enumerated in what follows.","category":"page"},{"location":"measurements/#PreProcessing-1","page":"Measurement Conversion","title":"PreProcessing","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"The conversion type PreProcessing allows to include va measurements in the ACR and IVR formulation, and cax measurements in the IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"begineqnarray\n tan(textva) = fractextvitextvr \n tan(textcax) = fractextcixtextcrx\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#Fraction-1","page":"Measurement Conversion","title":"Fraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"The conversion type Fraction allows to include crx and cix measurements in the ACP formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"begineqnarray\n textcrx = fractextpxcdotcos(textva)+textqxcdotsin(textva)textvm \n textcix = fractextpxcdotsin(textva)-textqxcdotcos(textva)textvm\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#Multiplication-1","page":"Measurement Conversion","title":"Multiplication","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"The conversion type Multiplication allows to include px and qx measurements in the IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"begineqnarray\n textpx = textvicdottextcrx + textvicdottextcix \n textqx = textvicdottextcrx - textvicdottextcix\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"These are quadratic equality constraints, modeled using @constraint.","category":"page"},{"location":"measurements/#MultiplicationFraction-1","page":"Measurement Conversion","title":"MultiplicationFraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"The conversion type MultiplicationFraction allows to include crx and cix measurements in the ACR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"begineqnarray\n textpx = fractextpxcdottextvr+textqxcdottextvitextvr^2+textvi^2 \n textpx = fractextpxcdottextvi-textqxcdottextvrtextvr^2+textvi^2 \nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#SquareFraction-1","page":"Measurement Conversion","title":"SquareFraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"The conversion type SquareFraction allows to include vm measurements in the ACR and IVR formulation, and cmx measurements in the ACP, ACR and IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"begineqnarray\n textvm^2 = fractextvi^2 + textvr^21 \n textcmx^2 = fractextpx^2 + textqx^2textvm^2 \n textcmx^2 = fractextcix^2 + textcrx^21\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurement Conversion","title":"Measurement Conversion","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"}] +[{"location":"formulations/#Network-Formulations-1","page":"Power Flow Formulations","title":"Network Formulations","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"This section gives an overview of the three-phase power flow formulations that are available to perform state estimation in PowerModelsDistributionStateEstimation. All formulations except the Reduced ones are imported from PowerModels or PowerModelsDistribution. These are only a subset of the formulations available in these two packages. For further information please refer to their official documentation.","category":"page"},{"location":"formulations/#Type-Hierarchy-1","page":"Power Flow Formulations","title":"Type Hierarchy","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"Formulations (or \"PowerModels\") follow the type hierarchy of PowerModels and PowerModelsDistribution, reported here for convenience for the relevant cases. At the top of the type hierarchy are abstract types. Three exact nonlinear (non-convex) models are available at the top level:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"abstract type PowerModelsDistribution.AbstractUnbalancedACPModel <: PowerModelsDistribution.AbstractUnbalancedPowerModel end\r\nabstract type PowerModelsDistribution.AbstractUnbalancedACRModel <: PowerModelsDistribution.AbstractUnbalancedPowerModel end\r\nabstract type PowerModelsDistribution.AbstractUnbalancedIVRModel <: PowerModelsDistribution.AbstractUnbalancedACRModel end","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"Abstract Models types are then used as the type parameter for PowerModels:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"mutable struct PowerModelsDistribution.ACPUPowerModel <: PowerModelsDistribution.AbstractUnbalancedACPModel PowerModelsDistribution.@pmd_fields end\r\nmutable struct PowerModelsDistribution.ACRUPowerModel <: PowerModelsDistribution.AbstractUnbalancedACRModel PowerModelsDistribution.@pmd_fields end\r\nmutable struct PowerModelsDistribution.IVRUPowerModel <: PowerModelsDistribution.AbstractUnbalancedIVRModel PowerModelsDistribution.@pmd_fields end","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"A \"reduced\" version of each of the three formulations above is derived in PowerModelsDistributionStateEstimation:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"mutable struct PowerModelsDistributionStateEstimation.ReducedACPUPowerModel <: PowerModelsDistribution.AbstractUnbalancedACPModel PowerModelsDistribution.@pmd_fields end\r\nmutable struct PowerModelsDistributionStateEstimation.ReducedACRUPowerModel <: PowerModelsDistribution.AbstractUnbalancedACRModel PowerModelsDistribution.@pmd_fields end\r\nmutable struct PowerModelsDistributionStateEstimation.ReducedIVRUPowerModel <: PowerModelsDistribution.AbstractUnbalancedIVRModel PowerModelsDistribution.@pmd_fields end\r\n\r\nAbstractReducedModel = Union{ReducedACRUPowerModel, ReducedACPUPowerModel}","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"The reduced models are still exact for networks like those made available in the ENWL database, where there are no cable ground admittance, storage elements and active switches. A positive semi-definite (SDP) relaxation is also made available for state estimation in PowerModelsDistributionStateEstimation. The SDP model belongs to the following categories: conic models and branch flow (BF) models, and there relevant type structure is the following:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"abstract type PowerModelsDistribution.AbstractUBFModel <: PowerModelsDistribution.AbstractUnbalancedPowerModel end\r\nabstract type PowerModelsDistribution.AbstractUBFConicModel <: PowerModelsDistribution.AbstractUBFModel end\r\nabstract type PowerModelsDistribution.AbstractConicUBFModel <: PowerModels.AbstractBFConicModel end\r\nabstract type PowerModelsDistribution.SDPUBFModel <: PowerModelsDistribution.AbstractConicUBFModel end\r\n\r\nmutable struct PowerModelsDistribution.SDPUBFPowerModel <: PowerModelsDistribution.SDPUBFModel PowerModelsDistribution.@pmd_fields end","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"where UBF stands for unbalanced branch flow. Finally, a linear unbalanced branch flow model is available for state estimation: the LPUBFDiagModel, better known as LinDist3FlowModel.","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"abstract type PowerModelsDistribution.AbstractLPUBFModel <: PowerModelsDistribution.AbstractNLPUBFModel end\r\nabstract type PowerModelsDistribution.LPUBFDiagModel <: PowerModelsDistribution.AbstractLPUBFModel end\r\nconst PowerModelsDistribution.LinDist3FlowModel = PowerModelsDistribution.LPUBFDiagModel\r\n\r\nmutable struct PowerModelsDistribution.LPUBFDiagPowerModel <: PowerModelsDistribution.LPUBFDiagModel PowerModelsDistribution.@pmd_fields end\r\nconst PowerModelsDistribution.LinDist3FlowPowerModel = PowerModelsDistribution.LPUBFDiagPowerModel","category":"page"},{"location":"formulations/#Details-on-the-Formulations-1","page":"Power Flow Formulations","title":"Details on the Formulations","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"This sub-section reports for convenience the relevant literature for the formulations used in PowerModelsDistributionStateEstimation and is again a reduced version of the official PowerModelsDistribution documentation, available here.","category":"page"},{"location":"formulations/#AbstractACPModel-1","page":"Power Flow Formulations","title":"AbstractACPModel","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"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).","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"See also:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"Carpentier, J. (1962) Contribution to the economic dispatch problem. In Bulletin de la Societe Francoise des Electriciens, vol. 3 no. 8, pp. 431-447.\nCain, M. B., O' Neill, R. P. & Castillo, A. (2012). History of optimal power flow and Models. Available online","category":"page"},{"location":"formulations/#AbstractACRModel-1","page":"Power Flow Formulations","title":"AbstractACRModel","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"See:","category":"page"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"Cain, M. B., O' Neill, R. P. & Castillo, A. (2012). History of optimal power flow and Models. Available online","category":"page"},{"location":"formulations/#AbstractIVRModel-1","page":"Power Flow Formulations","title":"AbstractIVRModel","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"O' Neill, R. P., Castillo, A. & Cain, M. B. (2012). The IV formulation and linear approximations of the ac optimal power flow problem. Available online","category":"page"},{"location":"formulations/#SDPUBFModel-1","page":"Power Flow Formulations","title":"SDPUBFModel","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"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","category":"page"},{"location":"formulations/#LPUBFDiagModel-or-LinDist3FlowModel-1","page":"Power Flow Formulations","title":"LPUBFDiagModel or LinDist3FlowModel","text":"","category":"section"},{"location":"formulations/#","page":"Power Flow Formulations","title":"Power Flow Formulations","text":"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","category":"page"},{"location":"angular_ref/#Angular-Reference-Models-1","page":"Angular Reference Models","title":"Angular Reference Models","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"As of version 0.8.0, PMDSE provides the ability to constraint voltage angles bounds, mostly used to define angular reference models for the reference bus of the network. ","category":"page"},{"location":"angular_ref/#Background-1","page":"Angular Reference Models","title":"Background","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"Traditioanlly, the slack bus voltage phasor is used as the reference phasor for the rest of the network. In single-phase analysis that phasor is kept at 1 pu and 0 degrees. However, in three-phase analysis, the slack bus voltage phasor magnitude is kept at 1 pu and the angles are kept at [0, -120, 120] degrees. This is based on the assumption that the voltage angles at the slack bus are equally spaced by 120 degrees. While this assumption is valid for stiff and balanced networks, it does not hold for unbalanced low voltage networks. ","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"(Image: Phasors-Assumption-LV)","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"The stiffness, indicating how balanced and equally spaced the phasor angles are, can be analytically determined by modeling an equivalent generator, where the generator's short circuit capability represents the upstream network impedance. In PowerModelsDistribution.jl, the slack bus which can be identified by looking up the bus with bus_type = 3 property in the network's mathematical model. PowerModelsDistribution.jl represents that slack bus with a virtual generator connected to a virtual bus via a virtual branch, with branch parameters based on the generator's short circuit capability, as shown in the figure below.","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"(Image: PMD modelling of slack bus)","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"From a state estimation perspective, measurements from the virtual branch (slack bus of power flow) are non-existent. Moreover, Short circuit capabilities and virtual branch parameters often rely on assumptions about the substation transformer and upstream network which might add up into the innacuracy in the estimation results.","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"Therefore, it is important to have the ability to define the voltage angle reference for the network based on the measurements available and free from these assumptions. ","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"Moreover, this feature enhances the estimation accuracy in the case of highly unbalanced distribution networks, where the voltage angles of the slack bus are not necessarily equally spaced by 120 degrees. Furthermore, this angular reference relaxation enables state estimation with respect to any bus as the reference bus, not necessarily the substations bus. This can be useful in case of having a bus with phasor measurement, where all phasors can be referred to that bus.","category":"page"},{"location":"angular_ref/#Angular-Reference-Modeling-Approaches-for-Distribution-System-State-Estimation-(DSSE)-1","page":"Angular Reference Models","title":"Angular Reference Modeling Approaches for Distribution System State Estimation (DSSE)","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"Based on the above discussion, and related literature on the topic, three approaches are proposed for modeling the angular reference for the network in DSSE:","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"(Image: Example for three approaches)","category":"page"},{"location":"angular_ref/#**Approach-A:-Conventional-Symmetrical-Reference-Angle**-1","page":"Angular Reference Models","title":"Approach A: Conventional Symmetrical Reference Angle","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"This approach designates the substation head bus as a stiff slack bus and fixes its voltage phasor angles to [0, -120, 120] degrees, assuming balanced voltage angles.\nThis assumption may lead to inaccuracies in DSSE, especially in LV networks with significant unbalance. The errors propagate to other estimated states, such as voltage magnitudes.","category":"page"},{"location":"angular_ref/#**Approach-B:-Single-Angular-Reference**-1","page":"Angular Reference Models","title":"Approach B: Single Angular Reference","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"This approach fixes only the phase \"a\" voltage angle to 0 degrees (or another arbitrary value), while the angles for phases \"b\" and \"c\" are treated as state variables.\nThis method captures the unbalance at the reference bus without relying on knowledge of upstream network parameters.\nThe voltage angles at the substation bus are relaxed to be estimated by the DSSE framework, which can lead to more accurate state estimates in unbalanced networks.","category":"page"},{"location":"angular_ref/#**Approach-C:-Virtual-Bus-Approach**-1","page":"Angular Reference Models","title":"Approach C: Virtual Bus Approach","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"This approach introduces a virtual bus before the substation bus, allowing for the complete relaxation of voltage phasor angles at the substation bus. This is the typical approach used in the power flow solvers (PowerModelsDistribution.jl). \nThe symmetrical angle assumption is then applied to the virtual bus, and a virtual branch is defined typically using a Thevenin equivalent model of the upstream network or the short circuit capabilities of the equivalent synchronous generator.\nWhile potentially more accurate in scenarios with perfect knowledge of upstream network parameters, this approach suffers from inaccuracies when these parameters are unknown or inaccurately modeled. This is a significant drawback in real-world LV networks where this information is often unavailable.","category":"page"},{"location":"angular_ref/#Usage-1","page":"Angular Reference Models","title":"Usage","text":"","category":"section"},{"location":"angular_ref/#**Approach-C:-Virtual-Bus-Approach**-2","page":"Angular Reference Models","title":"Approach C: Virtual Bus Approach","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"The default behaviour of PowerModelsDistribution.jl is to use the virtual bus to represent the slack bus (Approach C) and to define the angular reference for the network. Hence, performing state estimation with the default MATHEMATICAL model from PowerModelsDistribution.jl will result in assuming measurements at the virtual branch (slack bus) and the results with then be reliant on the assumptions made about the upstream network (virtual branch parameters).","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"# after loading the relevant packages\r\nntw_path = joinpath(PMDSE.BASE_DIR, \"test/data/extra/networks/case3_unbalanced.dss\")\r\nmsr_path = joinpath(mktempdir(),\"temp.csv\")\r\ndata = PowerModelsDistribution.parse_file(ntw_path; data_model=MATHEMATICAL)\r\npf_result = _PMD.solve_mc_pf(data, model, ipopt_solver)\r\nwrite_measurements!(model, data, pf_result, msr_path)\r\nadd_measurements!(data, msr_path, actual_meas = true)\r\ndata[\"se_settings\"] = Dict{String,Any}(\"criterion\" => \"rwlav\", \"rescaler\" => 1)\r\nslv = PMDSE.optimizer_with_attributes(Ipopt.Optimizer, \"tol\"=>1e-8, \"print_level\"=>0)\r\nSE_c = _PMDSE.solve_mc_se(math_c, model, ipopt_solver)","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"However in a realistic case measurement are not available at the virtual branch, but at the substation bus. Hence, the virtual bus is removed from MATHEMATICAL data model. And the substation bus is defined as the slack bus. Additionally the measurement file at msr_path is updated to contain the measurements at the substation bus instead of the virtual bus. ","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"(virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(data) ","category":"page"},{"location":"angular_ref/#**Approach-A:-Conventional-Symmetrical-Reference-Angle**-2","page":"Angular Reference Models","title":"Approach A: Conventional Symmetrical Reference Angle","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"After removing the virtual bus and defining the substation bus as the slack bus, the conventional approach assumes symmetrical reference angle, by fixing the voltage angles at the substation bus to [0, -120, 120] degrees, and the voltage magnitudes to 1.0 p.u. ","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":" math_a = deepcopy(data)\r\n math_a[\"bus\"][\"$(r_new)\"][\"va\"] = deg2rad.([0, -120, 120])\r\n math_a[\"bus\"][\"$(r_new)\"][\"bus_type\"] = 3\r\n SE_a = _PMDSE.solve_mc_se(math_a, model, ipopt_solver)","category":"page"},{"location":"angular_ref/#**Approach-B:-Single-Angular-Reference**-2","page":"Angular Reference Models","title":"Approach B: Single Angular Reference","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"However, for a more flexible approach, PMDSE now offers the ability to bound the voltage angles at the buses between maximum and minimum values instead of fixing them at an exact value. That way the voltage angle at the substation bus can be estimated by the state estimation framework. ","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":" math_b = deepcopy(data)\r\n (virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(math_b) \r\n Angles_bound = Inf \r\n math_b[\"bus\"][\"$(r_new)\"][\"bus_type\"] = 3\r\n math_b[\"bus\"][\"$(r_new)\"][\"vamax\"] = deg2rad.([0.0, -120+Angles_bound, 120+Angles_bound])\r\n math_b[\"bus\"][\"$(r_new)\"][\"vamin\"] = deg2rad.([0.0, -120-Angles_bound, 120-Angles_bound])\r\n SE_b = _PMDSE.solve_mc_se(math_b, model, ipopt_solver)","category":"page"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"Here it can be seen that the angles at the substation bus are relaxed except for the phase \"a\" angle which is fixed to 0 degrees. The angles for phases \"b\" and \"c\" are treated as state variables and are estimated by the state estimation framework. This results in better estimation accuracy for voltage mangitudes in unbalanced networks without relying on upstream network parameters.","category":"page"},{"location":"angular_ref/#Summary-1","page":"Angular Reference Models","title":"Summary","text":"","category":"section"},{"location":"angular_ref/#","page":"Angular Reference Models","title":"Angular Reference Models","text":"In summary when dealing with unbalanced networks, and in order to not lose accuracy when performing the state estimation, PMDSE now offers the ability to constraint, relax or bound the voltage angles at the slack bus (or any arbitrary bus) using the vamin and vamax properties of the bus dictionary in the MATHEMATICAL model.","category":"page"},{"location":"quickguide/#Quick-Start-Guide-1","page":"Getting Started","title":"Quick Start Guide","text":"","category":"section"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"To perform a state estimation (SE), a network data file (e.g. \"case3_unbalanced.dss\" in ../test/data/extra/networks) needs to be acquired, together with its related measurement file (e.g. \"case3_meas.csv\" in ../test/data/extra/measurements). The absolute path to the package is provided through the constant BASE_DIR. Network and measurement data will be merged and a SE can be run as follows:","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"using PowerModelsDistribution, PowerModelsDistributionStateEstimation\r\nusing JuMP, Ipopt\r\n\r\n#full paths to files\r\nntw_path = joinpath(BASE_DIR, \"test/data/extra/networks/case3_unbalanced.dss\")\r\nmsr_path = joinpath(BASE_DIR, \"test/data/extra/measurements/case3_meas.csv\")\r\n\r\n#parse network data file\r\ndata = PowerModelsDistribution.parse_file(ntw_path; data_model=MATHEMATICAL)\r\n\r\n#add measurement data to network data file\r\nadd_measurements!(data, msr_path, actual_meas = true)\r\n\r\n#set state estimation settings\r\ndata[\"se_settings\"] = Dict{String,Any}(\"criterion\" => \"rwlav\", \"rescaler\" => 1)\r\n\r\n#set solver parameters\r\nslv = JuMP.optimizer_with_attributes(Ipopt.Optimizer, \"tol\"=>1e-6, \"print_level\"=>0)\r\n\r\n#run state estimation\r\nse_result = solve_acp_mc_se(data, slv)","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"The run commands return detailed results in the form of a dictionary, following PowerModelsDistribution format, and can be saved for future processing, like in se_result above.","category":"page"},{"location":"quickguide/#Accessing-Different-Formulations-1","page":"Getting Started","title":"Accessing Different Formulations","text":"","category":"section"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"Different run functions correspond to different formulations. The function solve_acp_mc_se uses the AC Polar form, solve_acr_mc_se uses the AC rectangular, etc. Alternatively, the formulation type can directly be passed to the generic solve_mc_se function:","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"solve_mc_se(data, ACPUPowerModel, slv)","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"It should be noted that not all solvers can handle all problem types. For example, to use the SDP formulation, you have to use a SDP-capable solver, such as the open-source solver SCS.","category":"page"},{"location":"quickguide/#Providing-a-Warm-Start-1","page":"Getting Started","title":"Providing a Warm Start","text":"","category":"section"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"Providing a (good) initial value to some or all optimization variables can reduce the number of solver iterations. PowerModelsDistributionStateEstimation provides the assign_start_to_variables! function.","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"assign_start_to_variables!(data)","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"assign_start_to_variables!(data, start_values_source)","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"Alternatively, the user can directly assign a value or vector (depending on the dimensions of the variable) in the data dictionary, under the key variablename_start. The example below shows how to do it for the vm and va variables.","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"data = parse_file(\"case3_unbalanced.dss\"; data_model=MATHEMATICAL)\r\ndata[\"bus\"][\"2\"][\"vm_start\"] = [0.996, 0.996, 0.996]\r\ndata[\"bus\"][\"2\"][\"va_start\"] = [0.00, -2.0944, 2.0944]","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"It should be noted that providing a bad initial value might result in longer calculation times or convergence issues, so the start value assignment should be done cautiously. If no initial value is provided, a flat start is assigned by default. The default initial value of each variable is indicated in the function where the variable is defined, as the last argument of the comp_start_value function (this is valid for both imported PowerModelsDistribution and native PowerModelsDistributionStateEstimation variables).","category":"page"},{"location":"quickguide/#Updating-Variable-Bounds-1","page":"Getting Started","title":"Updating Variable Bounds","text":"","category":"section"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"In constrained optimization, reducing the search space might be an effective way to reduce solver time. Search space reduction can be done by assigning bounds to the variables. This must also be done attentively, though, to make sure that the feasible space is not cut, i.e., that feasible solutions are not removed by this process. This can be avoided if good knowledge of the system is available or if some variable have particularly obvious bounds, e.g., voltage magnitude cannot be negative, so its lower bound can be set to 0 without risk. Similar to providing a warm start, it is to user discretion to assign meaningful and \"safe\" variable bounds. PowerModelsDistributionStateEstimation has functions that allow to define bounds on voltage magnitude, power generation (active and reactive) or power demand (active and reactive):","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"update_voltage_bounds!(data::Dict; v_min=0.0, v_max=Inf)","category":"page"},{"location":"quickguide/#PowerModelsDistributionStateEstimation.update_voltage_bounds!-Tuple{Dict}","page":"Getting Started","title":"PowerModelsDistributionStateEstimation.update_voltage_bounds!","text":"update_voltage_bounds!(data; v_min, v_max)\n\nFunction that allows to automatically set upper (vmax) and lower (vmin) voltage bounds for all buses. It assumes that all bus terminals have the same bounds and that there are at most four active terminals.\n\n\n\n\n\n","category":"method"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"update_generator_bounds!(data::Dict; p_min::Float64=0.0, p_max::Float64=Inf, q_min::Float64=-Inf, q_max::Float64=Inf)","category":"page"},{"location":"quickguide/#PowerModelsDistributionStateEstimation.update_generator_bounds!-Tuple{Dict}","page":"Getting Started","title":"PowerModelsDistributionStateEstimation.update_generator_bounds!","text":"update_generator_bounds!(data; p_min, p_max, q_min, q_max)\n\nFunction that allows to automatically set upper (pmax, qmax) and lower (pmin, qmin) active and reactive power bounds for all generators. It assumes that there are at most four active phases and all have the same bounds.\n\n\n\n\n\n","category":"method"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"update_load_bounds!(data::Dict; p_min::Float64=0.0, p_max::Float64=Inf, q_min::Float64=-Inf, q_max::Float64=Inf)","category":"page"},{"location":"quickguide/#PowerModelsDistributionStateEstimation.update_load_bounds!-Tuple{Dict}","page":"Getting Started","title":"PowerModelsDistributionStateEstimation.update_load_bounds!","text":"update_load_bounds!(data::Dict; p_min::Float64=0.0, p_max::Float64=Inf, q_min::Float64=-Inf, q_max::Float64=Inf)\n\nFunction that allows to automatically set upper (pmax, qmax) and lower (pmin, qmin) active and reactive power bounds for all loads. It assumes that there are at most four active phases and all have the same bounds.\n\n\n\n\n\n","category":"method"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"or, alternatively, all the above at once:","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"update_all_bounds!(data::Dict; v_min::Float64=0.0, v_max::Float64=Inf, pg_min::Float64=0.0, pg_max::Float64=Inf, qg_min::Float64=-Inf, qg_max::Float64=Inf, pd_min::Float64=0.0, pd_max::Float64=Inf, qd_min::Float64=-Inf, qd_max::Float64=Inf)","category":"page"},{"location":"quickguide/#PowerModelsDistributionStateEstimation.update_all_bounds!-Tuple{Dict}","page":"Getting Started","title":"PowerModelsDistributionStateEstimation.update_all_bounds!","text":"update_all_bounds!(data; v_min, v_max, pg_min, pg_max, qg_min, qg_max, pd_min, pd_max, qd_min, qd_max)\n\nFunction that combines update_voltage_bounds!, update_generator_bounds! and update_load_bounds! and assigns bounds to all bus voltages and load and generator power.\n\n\n\n\n\n","category":"method"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"Alternatively, the user can directly assign a value or vector (depending on the dimensions of the variable) in the data dictionary, under the key variablenamemin/variablenamemax. The example below shows how to do it for the active power.","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"data[\"load\"][\"1\"][\"pmax\"] = [1.0, 1.0, 1.0]\r\ndata[\"load\"][\"1\"][\"pmin\"] = [0.0, 0.0, 0.0]","category":"page"},{"location":"quickguide/#Updating-Residual-Bounds-1","page":"Getting Started","title":"Updating Residual Bounds","text":"","category":"section"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"Residuals are a type of variable that is specific to the state estimation problem (and not, e.g., of power flow studies). If you do not know what a residual is, please read the Problem Specifications section of the documentation. While the residuals as defined in the present package are always non-negative (default lower bound is 0), there is no default upper bound. A function is available to add customized upper bounds:","category":"page"},{"location":"quickguide/#","page":"Getting Started","title":"Getting Started","text":"assign_residual_ub!(data::Dict; chosen_upper_bound::Float64=100.0, rescale::Bool=false)","category":"page"},{"location":"quickguide/#PowerModelsDistributionStateEstimation.assign_residual_ub!-Tuple{Dict}","page":"Getting Started","title":"PowerModelsDistributionStateEstimation.assign_residual_ub!","text":"assign_residual_ub!(data::Dict; chosen_upper_bound::Float64=100, rescale::Bool=false)\n\nBasic function that assigns upper bounds to the residual variables, by adding a \"res_max\" entry to the measurement dictionary. The function takes as input either a single measurement dictionary, e.g., data[\"meas\"][\"1\"] or the full measurement dictionary, data[\"meas]. # Arguments - data: ENGINEERING data model of the feeder, or dictionary corresponding to a single measurement - chosenupperbound: finite upper bound, defaults to 100 if no indication provided - rescale: false by default. If true, the chosen_upper_bound is multiplied by the value of data[\"se_settings\"][\"rescaler\"] and their product is used as the upper bound. Otherwise,\n\n\n\n\n\n","category":"method"},{"location":"se_criteria/#Mathematical-Model-of-the-State-Estimation-Criteria-1","page":"State Estimation Criteria","title":"Mathematical Model of the State Estimation Criteria","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Let X be the random variable associated to a measurement m ∈ 𝓜 and x ∈ 𝓧 the related variable, where:","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"𝓜 denotes the set of measurements,\n𝓧 denotes the (extended) variable space of the OPF problem.","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The state of a power system can be determined based on a specific estimation criterion. The state estimator criteria can be classified into two groups based on the random variable X:","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Gaussian\nwlav: weighted least absolute value (exact)\nrwlav: relaxed weighted least absolute value (exact relaxation)\nwls: weighted least square (exact)\nrwls: relaxed weighted least square (exact relaxation)\nNon-Gaussian\nmle: maximum likelihood estimation (exact)","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The user has to specify the criterion for each measurement individually. However, if all measurements can be described by the same criterion, it is sufficient to set the criterion name in the se_settings (Input Data Format), as PMDSE will automatically call assign_unique_individual_criterion!().","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"assign_unique_individual_criterion!(data::Dict)","category":"page"},{"location":"se_criteria/#PowerModelsDistributionStateEstimation.assign_unique_individual_criterion!-Tuple{Dict}","page":"State Estimation Criteria","title":"PowerModelsDistributionStateEstimation.assign_unique_individual_criterion!","text":"assign_unique_individual_criterion!(data::Dict)\n- data: `MATHEMATICAL` data model of the network\nAssigns the criterion in data[\"se_settings\"][\"criterion\"] to all individual measurements.\n\n\n\n\n\n","category":"method"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"To use a combination of different criteria, the user should provide information in data[\"meas\"], under a crit key. For example: data[\"meas\"][\"1\"][\"crit\"] = \"rwlav\" and data[\"meas\"][\"2\"][\"crit\"] = \"mle\". A basic helper function to assign different criteria to different measurement is provided:","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"assign_basic_individual_criteria!(data::Dict; chosen_criterion::String=\"rwlav\")","category":"page"},{"location":"se_criteria/#PowerModelsDistributionStateEstimation.assign_basic_individual_criteria!-Tuple{Dict}","page":"State Estimation Criteria","title":"PowerModelsDistributionStateEstimation.assign_basic_individual_criteria!","text":"assign_basic_individual_criteria!(data::Dict; chosen_criterion::String=\"rwlav\")\n\nBasic function that assigns individual criteria to measurements in data[\"meas\"]. For each measurement, if the distribution type of at least one of the phases is normal, the criterion defaults to the chosen_criterion. Otherwise, it is assigned the 'mle' criterion. The function takes as input either a single measurement dictionary, e.g., data[\"meas\"][\"1\"] or the full MATHEMATICAL data model.\n\n\n\n\n\n","category":"method"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Furthermore, a rescaler can be introduced to improve the convergence of the state estimation. The user has to specify the rescaler through the se_settings (Input Data Format). If no rescaler is specified, it will default to 1.0.","category":"page"},{"location":"se_criteria/#Gaussian-State-Estimation-Criteria-1","page":"State Estimation Criteria","title":"Gaussian State Estimation Criteria","text":"","category":"section"},{"location":"se_criteria/#WLAV-and-rWLAV-1","page":"State Estimation Criteria","title":"WLAV and rWLAV","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The WLAV criterion represents the absolute value norm (p=1) and is given by","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"begineqnarray\r\n rho_m = frac x - mu_m textrsc cdot sigma_mquad m in mathcalM m to x in mathcalX\r\nendeqnarray","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"where:","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"ρ denotes the residual associated with a measurement m,\nx denotes the variable corresponding to a measurement m.\nμ denotes the measured value, i.e., expectation 𝐄(X),\nσ denotes the the measurement error, i.e., standard deviation √(𝐕(X)),\nrsc denotes the rescaler.","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Solving a state estimation using the WLAV criterion is non-trivial as the absolute value function is not continuously differentiable. This drawback is lifted by its exact linear relaxation: rWLAV[1]. The rWLAV criterion is given by","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"begineqnarray\r\n rho_m geq frac x_m - mu_m textrsc cdot sigma_mquad m in mathcalM m to x_m in mathcalX \r\n rho_m geq - frac x_m - mu_m textrsc cdot sigma_mquad m in mathcalM m to x_m in mathcalX \r\nendeqnarray","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"[1]: Note that this relaxation is only exact in the context of minimization problem.","category":"page"},{"location":"se_criteria/#WLS-and-rWLS-1","page":"State Estimation Criteria","title":"WLS and rWLS","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The WLS criterion represents the Eucledian norm (p=2) and is given by","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"begineqnarray\r\n rho_m = frac( x_m - mu_m )^2textrsc cdot sigma_m^2quad m in mathcalM m_m to x in mathcalX\r\nendeqnarray","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The rWLS criterion relaxes the former as a cone and is given by","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"begineqnarray\r\n rsc cdot sigma_m^2 cdot rho_m geq ( x_m - mu_m )^2quad m in mathcalM m to x_m in mathcalX\r\nendeqnarray","category":"page"},{"location":"se_criteria/#Non-Gaussian-State-Estimation-Criteria-1","page":"State Estimation Criteria","title":"Non-Gaussian State Estimation Criteria","text":"","category":"section"},{"location":"se_criteria/#Maximum-Likelihood-Estimation-1","page":"State Estimation Criteria","title":"Maximum Likelihood Estimation","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The maximum likelihood criterion links the measurement residual to the logpdf of the associated distribution and is given by","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"begineqnarray\r\n rho_m = - textrsc cdot textlogpdf_m(x) + textshfquad m in mathcalM m to x in mathcalX\r\nendeqnarray","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"where shf denotes a shift setting the minimum value of the residual to zero.","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"To avoid the use of automatic differentiation, the first derivative (gradlogpdf) is provided by Distributions.jl and the second derivative (heslogpdf) is provided internally.","category":"page"},{"location":"se_criteria/#Supported-Distributions-1","page":"State Estimation Criteria","title":"Supported Distributions","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Currently, the following univariate continuous distributions are supported through the Distributions.jl package:","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Exponential\nWeibull\nNormal\nLog-Normal\nGamma\nBeta\nExtended Beta","category":"page"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"ExtendedBeta","category":"page"},{"location":"se_criteria/#PowerModelsDistributionStateEstimation.ExtendedBeta","page":"State Estimation Criteria","title":"PowerModelsDistributionStateEstimation.ExtendedBeta","text":"ExtendedBeta\n\nThe extended beta distribution with shape parameters α and β, and optional support parameters min and max has a probability density function\n\nf(x α β textmin textmax) =\n begincases\n 0 textifx textmin \n frac(x-textmin)^α-1 (textmax-x)^β-1B(αβ) (textmax-textmin)^α+β-1 textiftextmin x textmax \n 0 textifx max\n endcases\n\nwhere B(α,β) is a Beta function.\n\n\n\n\n\n","category":"type"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"Furthermore, Gaussian Mixture Models (GMM) can also be used in the maximum likelihood estimation. These can be generated via GaussianMixtures.jl, and PMDSE provides logpdf,gradlogpdf and heslogpdf of the resulting GMM. Using GMM are particularly convenient to model non-parametric distributions.","category":"page"},{"location":"se_criteria/#TODO:-add-docs-on-Gaussian-Mixture-Model-1","page":"State Estimation Criteria","title":"TODO: add docs on Gaussian Mixture Model","text":"","category":"section"},{"location":"se_criteria/#","page":"State Estimation Criteria","title":"State Estimation Criteria","text":"The user has to specify the number_of_gaussian through the se_settings (Input Data Format). If no number is specified, it will default to 10.","category":"page"},{"location":"problems/#Problem-Specifications-1","page":"Problem Specifications","title":"Problem Specifications","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"The main purpose of PowerModelsDistributionStateEstimation is to solve state estimation problems. For a number of purposes, it might be useful to perform power flow or OPF calculations, within the context of a state estimation study. For example, power flow calculations can be used to validate the accuracy of the state estimator, or to generate artificial measurement data, if these are not available. Power flow and OPF calculations can be accessed from PowerModelsDistribution. The description of these problems can be found in PowerModelsDistribution's documentation.","category":"page"},{"location":"problems/#State-estimation-problem-implementation-1","page":"Problem Specifications","title":"State estimation problem implementation","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"For a bus injection model, the structure of the state estimation problem is the following. See the implementation at src/prob/se.jl for all the models' implementations and their details. The functions preceded by a \"_PMD.\" are imported from PowerModelsDistributions.jl. Those without prefix are original PowerModelsDistributionStateEstimation functions, those preceded by a \"PowerModelsDistributionStateEstimation.\" are present in both and therefore needed disambiguation.","category":"page"},{"location":"problems/#Variables-1","page":"Problem Specifications","title":"Variables","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"\r\nPowerModelsDistributionStateEstimation.variable_mc_bus_voltage(pm; bounded = true)\r\n_PMD.variable_mc_branch_power(pm; bounded = true)\r\n_PMD.variable_mc_transformer_power(pm; bounded = true)\r\n_PMD.variable_mc_generator_power(pm; bounded = true)\r\nvariable_mc_load(pm; report = true)\r\nvariable_mc_residual(pm, bounded = true)\r\nvariable_mc_measurement(pm, bounded = false)","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"It can be seen that the first variables are bounded. It is up to the user to define reasonable upper/lower bounds that don't cut the feasible space of the problem. They can in principle be set as +/- infinity if no better information on the bounds is available.","category":"page"},{"location":"problems/#Constraints-1","page":"Problem Specifications","title":"Constraints","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"\r\nfor (i,gen) in _PMD.ref(pm, :gen)\r\n _PMD.constraint_mc_gen_setpoint(pm, i)\r\nend\r\nfor (i,bus) in _PMD.ref(pm, :ref_buses)\r\n @assert bus[\"bus_type\"] == 3\r\n _PMD.constraint_mc_theta_ref(pm, i)\r\nend\r\nfor (i,bus) in _PMD.ref(pm, :bus)\r\n PowerModelsDistributionStateEstimation.constraint_power_balance_se(pm, i)\r\nend\r\nfor (i,branch) in _PMD.ref(pm, :branch)\r\n _PMD.constraint_mc_ohms_yt_from(pm, i)\r\n _PMD.constraint_mc_ohms_yt_to(pm,i)\r\nend\r\nfor (i,meas) in _PMD.ref(pm, :meas)\r\n constraint_mc_residual(pm, i)\r\nend\r\n\r\nfor i in _PMD.ids(pm, :transformer)\r\n _PMD.constraint_mc_transformer_power(pm, i)\r\nend","category":"page"},{"location":"problems/#Objective-1","page":"Problem Specifications","title":"Objective","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":" objective_mc_se(pm)","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"For branch flow/linearized/SDP models, the variable space changes and also some of the constraints, while the objective always stays the same.","category":"page"},{"location":"problems/#Mathematical-formulation-1","page":"Problem Specifications","title":"Mathematical formulation","text":"","category":"section"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"For a detailed description of the mathematical model, please refer to the following publication. In the mathematical description below, the following sets are used,","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"beginalign\r\n\r\nmboxsets nonumber \r\n mathcalN mbox - busesnonumber \r\n mathcalR mbox - references busesnonumber \r\n mathcalE mathcalE_i mbox - branches branches to and from bus i nonumber \r\n mathcalG mathcalG_i mbox - generators and generators at bus i nonumber \r\n mathcalL mathcalL_i mbox - loads and loads at bus i nonumber \r\n mathcalS mathcalS_i mbox - shunts and shunts at bus i nonumber \r\n Phi Phi_ij mbox - conductors conductors of branch (ij) nonumber \r\n\r\nendalign","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"Bold characters indicate vectors and matrices\nThe textdiag(cdot) operator takes the diagonal (vector) from a matrix\nThe (cdot)^H indicates the conjugate transpose of a matrix","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"The julia problem above, can be associated to the following mathematical description","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"Variables:","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"\r\nbeginalign\r\n mathbfS^g_k mathbfS^d_k mathbfU_i forall k in mathcalG k in mathcalL i in mathcalN nonumber mathbfS_ij boldsymbolrho_m forall (ij) in mathcalE m in mathcalM nonumber\r\nendalign","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"Constraints:","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"beginalign\r\nmathbfangle U_r = 0 -120 120 deg forall r in mathcalR \r\nsum_substackk in mathcalG_i mathbfS^g_k - sum_substackk in mathcalL_i mathbfS^d_k - sum_substackk in mathcalS_i mathbfU_i mathbfU^H_i (mathbfY^s_k)^H =\r\nsum_substack(ij)in mathcalE_i diag(mathbfS_ij) forall iin mathcalN\r\n mathbfS_ij = mathbfU_i mathbfU_i^H left( mathbfY^sh_ijright)^H + mathbfU_i left(mathbfU_i- mathbfU_j right)^H (mathbfY_ij)^H forall (ij)in mathcalE \r\n mathbfS_ij = mathbfU_i mathbfU_i^H left( mathbfY_ij + mathbfY^sh_ijright)^H - mathbfU_i mathbfU^H_j mathbfY^H_ij forall (ij)in mathcalE \r\n mathbfS_ji = mathbfU_j mathbfU_j^H left( mathbfY_ij + mathbfY^sh_ji right)^H - mathbfU^H_i mathbfU_j mathbfY^H_ij forall (ij)in mathcalE \r\n boldsymbolrho_m = r_m(mathbff_m(mathbfx) mathbfz_m boldsymbolsigma_m)\r\n mathbff_m(mathbfx) - mathbfz_m _pboldsymbolsigma^p_m forall m in mathcalM mathbfx in mathcalX\r\nendalign","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"Objective:","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"beginequation\r\n textminimize sum_substackm in mathcalM boldsymbolrho_m\r\nendequation","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"The residual rho_m phi is a function that allows to represent the uncertainty on a given measurement m, performed on conductor phi. In the mathematical description above, it is identified as the function r, which is depending on the measurement mathbfz, and another function: f.","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"The function f_mphi are used to handle measurements z_mphi that are performed on quantities that do not refer to the problems' variable space. There are the measurements conversions described in the Measurements And Conversions section of the documentation.","category":"page"},{"location":"problems/#","page":"Problem Specifications","title":"Problem Specifications","text":"Function r_mphi, on the other hand, depends on what state estimation criterion is chosen, e.g., WLS, WLAV, MLE. The form that r_mphi takes in the various cases is defined in the section State Estimation Criteria of the documentation.","category":"page"},{"location":"input_data_format/#Input-Data-Format-1","page":"Input Data Format","title":"Input Data Format","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The data input required by PowerModelsDistributionStateEstimation takes the form of a dictionary and can be subdivided in three parts:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Network data\nMeasurement data\nState estimation settings","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The network data contains all the information relative to the physics of the analyzed network: topology, line impedance, location and specs of loads, generators, etc.. The measurement data contains all the information relative to the (pseudo-)measurements available for that network: number and placement of meters, measured quantities (power, voltage...) and measurement accuracy. The state estimation settings allow the user to choose the type of estimation criterion to be used (e.g., WLS, WLAV,..) and add a weight rescaler. More details on each of the three parts can be found in the following sections of this manual.","category":"page"},{"location":"input_data_format/#Network-Data-Input-1","page":"Input Data Format","title":"Network Data Input","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The network data input of PowerModelsDistributionStateEstimation is based on that of PowerModelsDistribution (PMD). In the versions supported by PowerModelsDistributionStateEstimation, PMD allows for two input data formats:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The ENGINEERING model (extensively documented here)\nThe MATHEMATICAL model","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The idea behind offering two options is that the ENGINEERING model is quite intuitive and allows a non-developer to easily generate data and use the PMD package as made available. The MATHEMATICAL model allows developers to explore the details of the PMD package and/or add extra information that can be passed as additional input to go beyond the functionalities that are natively offered in PMD. Ultimately, both PMD and PowerModelsDistributionStateEstimation use a MATHEMATICAL model as input for the calculations, but PMD can be provided directly an ENGINEERING model, which is then transformed at runtime. This is not the case in PowerModelsDistributionStateEstimation, which requires measurement data and state estimation settings to perform state estimation calculations. These two take the form of \"sub-dictionaries\" that need to be appended to a PMD MATHEMATICAL network data model dictionary. If added to the ENGINEERING data model, they will be ignored in the transformation to the MATHEMATICAL model, returning an error. For additional information on the structure of the network data input, the user is referred to the PMD manual. The user can build the network data from scratch, for example writing a native julia parser that builds the dictionary starting from external files. However, to encourage the use and creation of easily reproducible test cases, PMDSE uses the parsing functionalities of PowerModels and PowerModelsDistribution that allow users to directly read network data from the following file formats:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"OpenDSS files (\".dss\"),\nMatpower files (\".m\"),\nJSON files (\".json\") created from PowerModels.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Furthermore, within PMDSE the network and load/generation data and parsers of the ENWL dataset are available and easy-to-use, courtesy of Sander Claeys (@sanderclaeys).","category":"page"},{"location":"input_data_format/#Parsing-OpenDSS-files-1","page":"Input Data Format","title":"Parsing OpenDSS files","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"To parse an OpenDSS file into PowerModelsDistribution's default ENGINEERING format, use the parse_file function:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"eng = PowerModelsDistribution.parse_file(\"path/to/file/file_name.dss\")","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"To obtain the MATHEMATICAL model it is possible to transform the data model using the transform_data_model function.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"math = PowerModelsDistribution.transform_data_model(eng)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"A small example of OpenDSS network data can be found in PowerModelsDistributionStateEstimation/test/data/extra/networks","category":"page"},{"location":"input_data_format/#Parsing-ENWL-files-1","page":"Input Data Format","title":"Parsing ENWL files","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"ENWL files are a collection of 25 real low voltage distribution networks (each of the networks' feeders is also individually accessible) and realistic demand/generation profile data, made available by the Electricity North West and The University of Manchester.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The data is available in OpenDSS-like format in PowerModelsDistributionStateEstimation/test/data/enwl/networks and can be parsed with the PowerModelsDistribution parse_file function. A specific feeder fdr of a network ntw should be parsed to the ENGINEERING model, using:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"eng_data = PowerModelsDistribution.parse_file(PowerModelsDistributionStateEstimation.get_enwl_dss_path(ntw,fdr),data_model=PowerModelsDistribution.ENGINEERING)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"All feeders are featured with a detailed transformer model. It might be convenient or necessary to drop the transformer model and define the source bus as a slack bus: this (slightly) improves tractability and in low voltage power flow and state estimation studies, the exact substation model is often not taken into account. The removal should happen at the ENGINEERING data stage:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.rm_enwl_transformer!(eng_data)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.rm_enwl_transformer!-Tuple{Any}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.rm_enwl_transformer!","text":"rm_enwl_transformer!(data_eng)\n\nThis function removes the transformer from a parsed ENWL ENGINEERING data file.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The ENWL feeders feature a high number of buses that are only used to interpolate the topology layout (i.e., where the cables are) but that host no device. Function reduce_enwl_lines_eng! is included specifically to simplify the data and remove the nodes and lines in excess in order to (considerably) improve tractability. It is highly recommended to use it. The resulting feeder is equivalent to the original one in terms of physical properties, and the calculation results are the same. The function can be applied both to an ENGINEERING and a MATHEMATICAL data model.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.reduce_enwl_lines_eng!(eng_data)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.reduce_enwl_lines_eng!-Tuple{Any}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.reduce_enwl_lines_eng!","text":"reduce_enwl_lines_eng!(data_eng)\n\nThis function removes all trailing lines from a parsed ENWL ENGINEERING data file.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.reduce_enwl_lines_math!(math_data)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.reduce_enwl_lines_math!-Tuple{Any}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.reduce_enwl_lines_math!","text":"reduce_enwl_lines_math!(data_math)\n\nThis function removes all trailing lines from a parsed ENWL MATHEMATICAL data file.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Contrary to \"regular\" OpenDSS files, load profile information needs to be parsed and added to the ENWL feeder ENGINEERING data obtained so far. This is accomplished using the insert_profiles! function:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.insert_profiles!(data, season, devices, pfs; t=missing, useactual=true)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.insert_profiles!-NTuple{4, Any}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.insert_profiles!","text":"insert_profiles!(data, season, devices, pfs; t=missing, useactual=true)\n\nThis function adds the load profiles to the parsed ENWL ENGINEERING data data.\n\nArguments\n\ndata: ENGINEERING data model of the feeder\nseason: \"summer\", \"winter\"\ndevices: \"load\", \"pv\", \"ev\", \"ehp\", \"uchp\"\npfs: power factor of the devices\nt: time-step\n\nExample\n\njulia> data = parse_file(get_enwl_dss_path(10, 1))\njulia> insert_profiles!(data, \"summer\", [\"load\", \"pv\"], [0.95, 0.90], t = 144)\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The ENWL data set features a number of low-carbon technologies profiles: electric vehicles (EV), electric heat pumps (EHP), micro-CHP (uCHP), photovoltaic panels (PV). \"load\" indicates the \"traditional\" residential load.","category":"page"},{"location":"input_data_format/#Parsing-Matpower-files-1","page":"Input Data Format","title":"Parsing Matpower files","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Matpower file can be parsed with PowerModels' function: parse_matpower. However, this will return network data in PowerModels' format, which is not the same as that of PowerModelsDistribution and PowerModelsDistributionStateEstimation. Furthermore, Matpower files are single-conductor networks, which are typically not a good network representation for unbalanced distribution systems, where all phases should be explicitly modelled. To make the parsed matpower file multiconductor and adapted to the PowerModelsDistribution format, one can use the make_multiconductor! function from PowerModelsDistribution. Although this is a \"naive\" way to make the file suitable, it can be a good starting point.","category":"page"},{"location":"input_data_format/#Multiconductor-vs-single-conductor-data-1","page":"Input Data Format","title":"Multiconductor vs single-conductor data","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"While PowerModelsDistributionStateEstimation is designed with three-phase systems in mind, and thus relies on the PowerModelsDistribution data format, the package also allows to perform state estimation on single-conductor-equivalents or networks where some conductors/components are single-phase and other are three-phase. For instance, to perform single-phase state estimation on a Matpower network, it is sufficient to parse it as follows:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"number_of_conductors = 1\r\npm_path = joinpath(dirname(pathof(PowerModels)), \"..\")\r\ncase5 = PowerModels.parse_file(\"$(pm_path)/test/data/matpower/case5.m\") \r\nPowerModelsDistribution.make_multiconductor!(case5, number_of_conductors)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"if number_of_conductors is set to 1, the model will still be a single-phase-equivalent network, but structurally compatible with PowerModelsDistribution. if number_of_conductors is set to 3, the data will be transformed to a naive three-phase model. See examples in test/single_conductor_branches.jl.","category":"page"},{"location":"input_data_format/#Writing-and-parsing-JSON-files-1","page":"Input Data Format","title":"Writing and parsing JSON files","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"You can export/import a PowerModels or PowerModelsDistribution network data dictionary to a JSON file using the following functions:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"For PowerModelsDistribution: \nprint_file to export, \nparse_file to read.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"For PowerModels:\nexport_file\nparse_file","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Clearly, parsing correct network data from a json file with parse_file will work if the file itself was created from a correct data dictionary structure (e.g., with the export functionalities above). Trying to parse an otherwise created json file will probably not work, unless created appropriately.","category":"page"},{"location":"input_data_format/#Measurement-Data-1","page":"Input Data Format","title":"Measurement Data","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Measurement data must be added to a MATHEMATICAL data dictionary, of which they are a \"sub-dictionary\". The user can either create the measurement dictionary from scratch, or it can be imported from a csv file in the right format, with the add_measurements! function. The measurement data in the csv file can be both real measurements (i.e., with an error), or \"fake\"/\"ideal\" measurements with no error. The function provides a functionality to add an error to \"fake\"/\"ideal\" measurements sampling the error from a Normal distribution. See the following:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.add_measurements!(data::Dict, meas_file::String; actual_meas::Bool = false, seed::Int=0)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.add_measurements!-Tuple{Dict, String}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.add_measurements!","text":"add_measurements!(data::Dict, meas_file::String; actual_meas::Bool = false, seed::Int=0)\n\nAdd measurement data from separate CSV file to the PowerModelsDistribution data dictionary. To fully understand how this function works, it is recommended to first read the documentation section that describes the CSV measurement file format.\n\nArguments\n\ndata: MATHEMATICAL data dictionary in a format usable with PowerModelsDistribution\nmeas_file: path to and name of file with measurement data\nactual_meas: default is false. When applied to non-normal distributions, the effect is overruled to that of true. For normal distributions, the following applies:\nfalse: the \"par1\" in measfile are not actual measurements, but, e.g., error-free powerflow results. Then, a fake measurement is built, extracting a value with an error from the given normal distribution.\ntrue: the \"par1\" values in measfile are actual measurement values, and the \"par_2\" are the σs of the measurements' distributions. These are directly used as input of the state estimator without further processing.\nif a \"parse\" column is present in the CSV file, the true or false is associated to each individual row (i.e., measurement), and overrules whatever the actualmeas input of addmeasurements!() itself is.\nseed: random seed value to make the results reproducible and explore different Monte Carlo scenarios when sampling measurement with errors from a probability distribution.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"An example of csv file in the right format can be found in PowerModelsDistributionStateEstimation/test/data/enwl/measurements/measdataexample.csv and refers to network 1, feeder 1 of the ENWL data. The format of the csv input file is explained in the following subsection.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Furthermore, functionality is included to write a measurement file, with the write_measurements! function. This is useful for quick testing or when the user has no actual measurement data, and allows to generate measurement files from the results of powerflow calculations on the same network. It should be noted that this function sets the measurement errors so that they follow a Normal distribution.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.write_measurements!(model::Type, data::Dict, pf_results::Dict, path::String)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.write_measurements!-Tuple{Type, Dict, Dict, String}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.write_measurements!","text":"write_measurements!(model::Type, data::Dict, pf_results::Dict, path::String; exclude::Vector{String}=String[], σ::Float64=0.005)\n\nFunction to write a csv file with measurements, to be used to run state estimation calculations. The file is built starting from power flow results from PowerModelsDistribution.jl (or any dictionary with the same format). The measurements consist of voltage and power/current injections in correspondence of all generators and loads. The exact measurement type depends on the chosen power flow formulation, e.g., with the AC Polar formulation, these are voltage magnitude and active and reactive power.\n\nArguments\n\nmodel: power flow type of the generated measurements, e.g., ACPUPowerModel. If it does not match the power flow model of the pf_results, it might not work. pf_results can be post-processed, e.g., polar results can be converted in rectangular and viceversa, to make the result dictionary compatible.\ndata: MATHEMATICAL data dictionary in a format usable with PowerModelsDistribution\npf_results: PowerModelsDistribution solution dictionary or similar format\npath: path where the csv file will be generated and stored\nexclude: select quantities from the pf_results dictionary to be excluded from the measurement generation. For example, to ignore generator results with ACPUPowerModel, set exclude = [\"pg\", \"qg\"].\nσ: standard deviation of demand/generation measurements. If a float, their stdev of the relative bus voltage measurements is taken with get_sigma(). If a dictionary (recommended option), the individual σ of each measurement quantity is declared.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The measurement \"sub-dictionary\" is now incorporated in the network data dictionary, and can be showed in REPL typing data[\"meas\"].","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"If there are pseudo measurements, or the user wants to explicitly describe measurements as non-Gaussian probability distributions, the same rules apply: either the measurements are provided as a csv file, or they can be created with the write_measurements_and_pseudo! function.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.write_measurements_and_pseudo!(model::Type, data::Dict, pf_results::Dict, path::String; exclude::Vector{String}=String[], distribution_info::String, σ::Float64=0.005)","category":"page"},{"location":"input_data_format/#PowerModelsDistributionStateEstimation.write_measurements_and_pseudo!-Tuple{Type, Dict, Dict, String}","page":"Input Data Format","title":"PowerModelsDistributionStateEstimation.write_measurements_and_pseudo!","text":"write_measurements_and_pseudo!(model::Type, data::Dict, pf_results::Dict, path::String; exclude::Vector{String}=String[], distribution_info::String, σ::Float64=0.005)\n\nHelper function to write a csv file with a combination of measurements and pseudo-measurements. Works similarly to writemeasurements!() with additional support for non-Normal distributions for the pseudo-measurements. In order to use this function, the load data for pseudo measurements data[\"load\"] need to point to an external file where information on the probability distribution is stored. An example of such a file is distrexample.csv in test/extra/measurements. The arguments of the function are the same as writemeasurements!(), with the addition of `distributioninfo`: the path to external csv file for pseudo-measurements distributions.\n\n\n\n\n\n","category":"method"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Please note that this function has only been fully tested with the ExtendedBeta distribution. Some functionalities, such as the per unit conversion of pdfs whose parameters are not in per unit might not work for other distributions. Providing distributions that are already in per unit might allow to use this function directly, but it is not guaranteed. Furthermore, some assumptions need to hold to be able to correctly use this function:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The distributions provided refer to active power pseudo measurements. The same distributions are scaled using the power factor to represent the reactive power of the same load.\nThe format of the external file with distribution information matches the example one: test/data/extra/measurements/distr_example.csv.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"In general, it is advised that users that intend to recur to non-Gaussian distributions build their own measurement creator/parser.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"As state in the function description, the data[\"load\"] dictionary entries of pseudo measurements need to point to the distribution file. A helper function is provided for this purpose as well:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"PowerModelsDistributionStateEstimation.assign_load_pseudo_measurement_info!(data::Dict, pseudo_load_list::Array, cluster_list::Array, csv_path::String; time_step::Int64=1, day::Int64=1)","category":"page"},{"location":"input_data_format/#The-csv-(pseudo)-measurement-data-format-1","page":"Input Data Format","title":"The csv (pseudo) measurement data format","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"In the present section, the term \"component\" refers to buses, branches, loads, generators or any other element present in the network data model. These need to be addressed using the singular term/abbreviation as present in the MATHEMATICAL data model, e.g. gen for generator. In the network data model, each component is identified by a unique index number (NB: there can be both a \"load 1\" and a \"gen 1\", but there can't be two \"load 1\"). The required csv measurement file features the following columns:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"meas_id: unique identifier of the given measurement. Must be an integer.\ncmp_type: indicates which component the measurement refers to: bus, load, gen, branch, etc.\ncmp_id: integer that indicates the index of the above component.\nmeas_type: this is \"G\" if the measured quantity is between phase and neutral, \"P\" if between phases.\nmeas_var: indicates which variable is measured. The entry must correspond to the variable name as defined in PowerModelsDistribution or PowerModelsDistributionStateEstimation, e.g., pg for the injected power from a generator, vm for a bus voltage magnitude, etc.\nphase: phase the measurement refers to, i.e., 1, 2 or 3. If it is a three-phase measurement, this can be indicated with a \"[1, 2, 3]\".\ndst: type of continuous univariate distribution associated to the measurement. In the classic WLAV/WLS estimators, this is a \"Normal\" distribution. In this package, we allow a number of additional distributions. For details, see the manual section on \"Maximum Likelihood Estimation\"\npar_1: is the first of the two parameters that define the measurement error distribution. For the Normal distribution, this is the mean.\npar_2: second parameter of the distribution. For the Normal distribution this is the standard deviation.\npar_3: can be missing or string, if the distribution requires a third parameter.\npar_4: can be missing or string, if the distribution requires a third parameter.\ncrit: can be missing, or it assigns an individual SE criterion to the measurement in a given row (see Mathematical Model of the State Estimation Criteria).\nparse: can be true or false (or missing). It should be true if the measurement provided are real measurements (i.e., with errors). It should be false if the measurements are not real but, e.g., generated with power flow calculations (i.e., they have no errors). In this case, a value with error is sampled from the distribution associated to the measurements, and used in the state estimation process.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Note that the error parsing only works for Normal distributions. It will not return an error for non-Normal distributions, but will default to the same behaviour as setting parse to false.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The last three columns are optional and don't necessarily need to be part of the CSV files. If a row/measurement is characterized by a distribution that requires 4 parameters, while the rest of them only require 2, the par3 and par4 columns of all other measurements need to have \"missing\" values, which will be ignored. Similarly, \"missing\" values in the other optional columns can be set, and they are ignored.","category":"page"},{"location":"input_data_format/#The-csv-distribution-file-format-1","page":"Input Data Format","title":"The csv distribution file format","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"This section explains the format of the csv file that contains information relative to probability distribution for pseudo measurements. An example is test/data/extra/measurements/distr_example.csv. The file has the following columns:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"day: day to which the distribution refers, integer\ntime_step: time step to which the distribution refers, integer\ncluster: load profile group or cluster, integer\npar1,par2,par2,par4: parameters of the distribution, if less than four are required, can be missing. Otherwise, they are floats\ndistr: distribution type, string\nper_unit: boolean, indicates whether the distribution has been rescaled to the unit values used in the SE calculations (true) or not (false). It is advised to used rescaled distributions.\nPF: power factor. This is used to apply the same distribution, which is assumed to refer to the active power, to the reactive power","category":"page"},{"location":"input_data_format/#The-final-dictionary-format-1","page":"Input Data Format","title":"The final dictionary format","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"In general, the measurement information needs to be correctly provided in the data[\"meas\"] sub-dictionary, as ultimately it is this which is used for the calculations. It is to the user to make sure that this is the case, regardless of which helper functions and files are used. Each measurement \"m\" needs to be unique, and should be similar to measurement \"1\" here:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"data[\"meas\"][\"1\"] => Dict{String,Any}(\r\n \"var\" => :pd,\r\n \"cmp\" => :load,\r\n \"cmp_id\" => 4\r\n \"dst\" => Any[ExtendedBeta{Float64}(α=1.18, β=7.1, min=-3.28684e-8, max=1.44621e-5), 0.0, 0.0],\r\n \"crit\" => \"rwlav\"\r\n)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"var is the variable to which the measurement refers. In this case, active power. cmp is the component type to which the measurement refers. In this case, a load. cmp_id is the unique id of this component. dst is a vector that contains the pdf of the measurement, for each phase separately and scaled to the correct units. At the moment, this is always a 3x1 vector, and phases to which loads are not used are assigned a 0.0. This is going to change very soon when upgrading to PowerModelsDistribution v0.10.0, in v0.2.0 of the present package. We will do our best to keep the docs up to date but there might be a delay. crit string that indicates the measurement's SE criterion, see Mathematical Model of the State Estimation Criteria.","category":"page"},{"location":"input_data_format/#State-estimation-settings-1","page":"Input Data Format","title":"State estimation settings","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Finally, an indication on what type of state estimation needs to be performed should be provided using the \"sesettings\" dictionary. The `sesettingsdictionary contains up to three keys:rescaler,criterionandnumberofgaussian. Therescaleris a Float that multiplies the residual constraints. Depending on the specific case and solver, adding a rescaler can improve tractability, even quite significantly. If no entry is provided, this defaults to 1.0. Thecriterionis a String and allows the user to choose a **global** residual definition for all measurements. If different measurements need to have different criteria, this shouldn't be used, but rather a local individual needs to be assigned to each measurement. For details on which criteria are available and how to use them, see [Mathematical Model of the State Estimation Criteria](@ref). If no entry is provided, no action is taken, as individual criterion assignment is assumed. Thenumberofgaussian` is an Int and is only resorted to when the Gaussian Mixture criterion is chosen. In this context, it represents the number of Gaussian components of the model. If no entry is provided, this defaults to 10.","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"\"se_settings\" => Dict{String,Any}(\r\n \"rescaler\" => 10,\r\n \"criterion\" => \"ga\", #only for global criterion assignment\r\n \"number_of_gaussian\" => 6\r\n)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"At this point, the data dictionary should have a structure similar to this:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"Dict{String,Any}(\r\n \"data_model\" => MATHEMATICAL,\r\n \"component_type\" => Dict{Any,Dict{String,Any}}(\r\n id => Dict{String,Any}(\r\n \"parameter\" => value,\r\n ...\r\n ),\r\n ...\r\n ),\r\n \"meas\" => Dict{Any,Dict{String,Any}}(\r\n id => Dict{String,Any}(\r\n \"parameter\" => value,\r\n ...\r\n ),\r\n ...\r\n ),\r\n \"se_settings\" => Dict{String,Any}(\r\n \"rescaler\" => value,\r\n \"criterion\" => \"chosen_criterion\" #only for global criterion assignment\r\n ),\r\n ...\r\n)","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"NB: do not confuse the \"se_settings\" dictionary key with the \"settings\" dictionary key, which is also present in the PowerModelsDistribution network data format.","category":"page"},{"location":"input_data_format/#Putting-everything-together:-complete-input-data-1","page":"Input Data Format","title":"Putting everything together: complete input data","text":"","category":"section"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"The following script allows the user to visualize the various steps to build the data and display final structure:","category":"page"},{"location":"input_data_format/#","page":"Input Data Format","title":"Input Data Format","text":"\r\ndata = parse_file(joinpath(BASE_DIR, \"test/data/extra/networks/case3_unbalanced.dss\"); data_model=MATHEMATICAL) #parses the network data\r\nmsr_path = joinpath(BASE_DIR, \"test/data/extra/measurements/case3_meas.csv\") # indicates the path to measurement data csv file\r\nadd_measurements!(data, msr_path, actual_meas = false) # adds the measurement data to the network data dictionary\r\ndata[\"se_settings\"] = Dict{String,Any}(\"criterion\" => \"rwlav\",\r\n \"rescaler\" => rescaler)# adds the state estimation settings to the data\r\ndisplay(data) # displays the first \"layer\" of the dictionary. The internal structure can be \"navigated\" like any other dictionary\r\n","category":"page"},{"location":"explicit_neutral_models/#Explicit-Neutral-Models-for-DSSE-1","page":"Explicit Neutral Models","title":"Explicit Neutral Models for DSSE","text":"","category":"section"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"As of version 0.8.0, PMDSE support the explicit neutral models of PowerModelsDistribution.jl in the IVRUPowerModel formulation. This feature is useful for the state estimation of low voltage networks where the neutral wire is not grounded at the load side. ","category":"page"},{"location":"explicit_neutral_models/#Background-1","page":"Explicit Neutral Models","title":"Background","text":"","category":"section"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"A generic three-phase low voltage model would have 4 wires: three wires for the three-phase lines (L1, L2, L3), one wire for the neutral (N).","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"In a typicall low voltage installation, the neutral wire is connected to the ground at the transformer secondary, and depending on the load earthing scheme the neutral wire might be connected to the ground at the load side or not. In the first case, where the neutral is grounded at both the transformer secondary and at the load side, then the neutral voltage at both ends is equal at 0 volts (ground voltage) and hence no current flows through the neutral wire. In that case, kron reduction is an exact approximation for the three-phase network analysis. ","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"(Image: Four-wire-grounded-at-load)","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"However, in the case where the neutral is not grounded at the load side, then the neutral voltage at the load side is not equal to the ground voltage and hence a current flows through the neutral wire. And it is necessary for the neutral voltages and current flows to be considered in the network three-phases analysis. In this case, the Kron reduction leads to innacurate results for the analysis and in turn the state estimation.","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"(Image: Four-wire-not-grounded-at-load) [1]","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"[1]: Figures adapted from S. Claeys and G. Deconinck, “Distribution Network Modeling: From Simulation Towards Optimization,” 2021. PhD Thesis, KU Leuven.","category":"page"},{"location":"explicit_neutral_models/#Usage-1","page":"Explicit Neutral Models","title":"Usage","text":"","category":"section"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"The function solve_ivr_en_mc_se() is added to the API to perform state estimation with the explicit neutral models in the IVR formulation. a typical usage for an explicit neutral MATHEMATICAL PowerMdoelsDistribution.jl model is as follows:","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"using PowerModelsDistribution\r\nusing PowerModelsDistributionStateEstimation\r\nPMD = PowerModelsDistribution\r\nPMDSE = PowerModelsDistributionStateEstimation\r\n\r\neng_en = PMD.parse_file(joinpath(PMDSE.BASE_DIR, \"test\", \"data\", \"three-bus-en-models\", \"3bus_4wire.dss\"))\r\nmsr_path = joinpath(mktempdir(),\"temp_msr.csv\")\r\n\r\neng_en = PowerModelsDistribution.parse_file(ntw_path)\r\nPMD.transform_loops!(eng_en)\r\nPMD.remove_all_bounds!(eng_en)\r\n\r\nmath_en = PMD.transform_data_model(eng_en, kron_reduce=false, phase_project=false)\r\n\r\npf_result = PMD.solve_mc_pf(data, model, ipopt_solver)\r\nwrite_measurements!(model, data, pf_result, msr_path)\r\nadd_measurements!(data, msr_path, actual_meas = true)\r\n\r\ndata[\"se_settings\"] = Dict{String,Any}(\"criterion\" => \"rwlav\", \"rescaler\" => 1)\r\nSE = PMDSE.solve_ivr_en_mc_se(data, Ipopt.Optimizer)","category":"page"},{"location":"explicit_neutral_models/#","page":"Explicit Neutral Models","title":"Explicit Neutral Models","text":"as it can be seen other than having a 4-wire network model, the rest of the state estimation process is similar to the one used for the 3-wire network model, just a matter of using the new function.","category":"page"},{"location":"#PowerModelsDistributionStateEstimation.jl-Documentation-1","page":"Home","title":"PowerModelsDistributionStateEstimation.jl Documentation","text":"","category":"section"},{"location":"#Overview-1","page":"Home","title":"Overview","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"PowerModelsDistributionStateEstimation.jl is an extension package of PowerModelsDistribution.jl for three-phase static Power System State Estimation.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"A Power System State Estimator determines the most-likely state of power system given a set of uncertainties, e.g., measurement errors, pseudo-measurements, etc. These uncertainties may pertain to any quantity of any network component, e.g., voltage magnitude (vm) of a bus, power demand (pd) of a load, etc.","category":"page"},{"location":"#Installation-1","page":"Home","title":"Installation","text":"","category":"section"},{"location":"#","page":"Home","title":"Home","text":"The latest stable release of PowerModelsDistributionStateEstimation can be installed using the Julia package manager:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] add PowerModelsDistributionStateEstimation","category":"page"},{"location":"#","page":"Home","title":"Home","text":"To be able to use PowerModelsDistributionStateEstimation, at least one solver is required. For our package tests, we rely on Ipopt and SCS solvers, since they do not have license restrictions. Both solvers can be installed using the package manager:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] add Ipopt","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] add SCS","category":"page"},{"location":"#","page":"Home","title":"Home","text":"However, it should be noted that, depending on the problem type, these solvers might not be the most appropriate/efficient choice.","category":"page"},{"location":"#","page":"Home","title":"Home","text":"In order to test whether the package works, run:","category":"page"},{"location":"#","page":"Home","title":"Home","text":"] test PowerModelsDistributionStateEstimation","category":"page"},{"location":"bad_data/#Bad-Data-Detection-and-Identification-1","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"","category":"section"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"As of version 0.4.0, PMDSE has bad data detection and identification functionalities, namely:","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Chi-square test,\nLargest normalized residuals,\nLeast absolute value (LAV) estimator residual analysis.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The LAV is a robust estimator that presents bad data rejection properties. LAV residuals can be collected and sorted, and the measurements with higher residuals are the ones of the bad data points. The LAV residual analysis can be done with all previous versions of the package too, but is made easier in v0.4.0: in versions up to 0.4.0 the user needs to pass wlav or rwlav as a state estimation criterion, and assign a unitary standard deviation for all weights or all measurements. Now it is sufficient to pass lav as a state estimation criterion.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"All these three techniques are very standard techniques, and a thorough theoretical discussion can be found in the well-known book: \"Power system state estimation - Theory and implementation\" by A. Abur and A. G. Exposito. Furthermore, numerous papers also address in which circumstances the different techniques are more or less effective.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Below, just a functional introduction.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"First of all,","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Bad data detection consists of answering the yes/no question: \"is there bad data\"?\nBad data identification consists of locating which data points are bad (to subsequently correct them or delete them).","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"All the presented techniques require the user to first run a state estimation algorithm, as they are based on the analysis of its residuals.","category":"page"},{"location":"bad_data/#Chi-square-Analysis-1","page":"Bad Data Detection and Identification","title":"Chi-square Analysis","text":"","category":"section"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Chi-squares (Chi^2) analysis is a bad data detection method. If bad data are detected, these still need to be identified.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The method is based on the following assumptions: if all measurement errors follow a Normal distribution, and there are no bad data, then the sum of the weighted squared residuals follows a Chi-square distributions with m-n degrees of freedom, where m is the number of measurements and n of the system variables.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The function exceeds_chi_squares_threshold takes as input the solution of a state estimation calculation and the data dictionary. It calculates the degrees of freedom and the sum of the weighted square residuals (where the weights are the inverse of each measurement's variance). If the state estimation that was run was a wls estimation with no weight rescaler, this sum corresponds to the objective value. However, the function always calculates the sum, to allow the user to use Chi-square calculations in combination with measurement rescalers or other state estimation criteria.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"PowerModelsDistributionStateEstimation.exceeds_chi_squares_threshold(sol_dict::Dict, data::Dict; prob_false::Float64=0.05, suppress_display::Bool=false)","category":"page"},{"location":"bad_data/#PowerModelsDistributionStateEstimation.exceeds_chi_squares_threshold-Tuple{Dict, Dict}","page":"Bad Data Detection and Identification","title":"PowerModelsDistributionStateEstimation.exceeds_chi_squares_threshold","text":"exceeds_chi_squares_threshold(sol_dict::Dict, data::Dict; prob_false::Float64=0.05, suppress_display::Bool=false)\n\nStandard bad data detection method that consists of performing a Chi squares analysis on the objective value, i.e., the sum of the residuals. The outcome of the analysis depends on the degrees of freedom of the problem, which are calculated calling the get_degrees_of_freedom function (see below).\n\nThis function returns:\n\na Boolean. If true, there are probably bad data points, if false there probably are not.\nthe sum of the weighted squares of the residuals (which might be the optimization objective or not, depending on the settings) and the Χ² threshold value. \n\nArguments:\n\nsol_dict: solution dictionary, i.e., the default output dictionary of state estimation calculations,\ndata: data input of the state estimation calculations, used to calculate the degrees of freedom,\nprob_false: probability of errors allowed in the Chi squares test, defaults to 0.05\nsuppress_display: if false, the function also displays the result of the analysis, otherwise this is suppressed.\n\n\n\n\n\n","category":"method"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The function returns a boolean that states whether bad data are suspected, the value of the sum of the residuals and the threshold value above which bad data are suspected. The threshold value depends on the degrees of freedom and the detection confidence probability, that cab=n be set by the user. The default value of the latter is 0.05, as this is often the choice in the literature. ","category":"page"},{"location":"bad_data/#Largest-Normalized-Residuals-1","page":"Bad Data Detection and Identification","title":"Largest Normalized Residuals","text":"","category":"section"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Normalized residuals can be used for both bad data detection and identification. Let the residuals be r_i = h_i(mathbfx) - mathbfz, where h are the measurement functions, mathbfx are the system variables and mathbfz is the measurement vector. This is often the standard notation, e.g., in the book by Abur and Exposito. The normalized residuals r^N_i are:","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"beginalign\r\nr_i^N = fracr_isqrtOmega_ii = fracr_isqrtR_iiS_ii\r\nendalign","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The largest r^N is compared to a threshold, typically 3.0 in the literature. If its value exceeds the threshold, bad data are suspected, and the bad data point is identified as the measurement that corresponds to the largest r^N itself. This package contains different functions that allow to build the measurement matrix (H), the measurement error covariance matrix (R), the gain matrix (G), the hat matrix (K), the sensitivity matrix (S) and the residual covariance matrix (Omega):","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"PowerModelsDistributionStateEstimation.build_H_matrix(functions::Vector, state::Array)::Matrix{Float64}","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_G_matrix(H::Matrix, R::Matrix)::Matrix{Float64}","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_R_matrix(data::Dict)::Matrix{Float64}","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_omega_matrix(R::Matrix{Float64}, H::Matrix{Float64}, G::Matrix{Float64})","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_omega_matrix(S::Matrix{Float64}, R::Matrix{Float64}) ","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_S_matrix(K::Matrix{Float64})","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"build_K_matrix(H::Matrix{Float64}, G::Matrix{Float64}, R::Matrix{Float64})","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Omega","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"can then be used in the function normalized_residuals, which calculates all r_i, returns the highest r^N and indicates whether its value exceeds the threshold or not. Again, the r_i calculation is independent of the chosen state estimation criterion or weight rescaler.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"PowerModelsDistributionStateEstimation.normalized_residuals(data::Dict, se_sol::Dict, Ω::Matrix; t::Float64=3.0)","category":"page"},{"location":"bad_data/#PowerModelsDistributionStateEstimation.normalized_residuals-Tuple{Dict, Dict, Matrix}","page":"Bad Data Detection and Identification","title":"PowerModelsDistributionStateEstimation.normalized_residuals","text":"Adds the normalized residuals to the solution dictionary and returns the largest normalized residual, its index (i.e., the measurement it refers to), and whether it exceeds the given threshold t or not.\n\n\n\n\n\n","category":"method"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Finally, a simplified version of the largest normalized residuals is available: simple_normalized_residuals, that instead of calculating the Omega matrix, calculates the normalized residuals as:","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"beginalign\r\nr_i^N = fracr_isqrtOmega_ii = fracr_isqrtR_ii^2\r\nendalign","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"PowerModelsDistributionStateEstimation.simple_normalized_residuals(data::Dict, se_sol::Dict, R::Matrix)","category":"page"},{"location":"bad_data/#PowerModelsDistributionStateEstimation.simple_normalized_residuals-Tuple{Dict, Dict, Matrix}","page":"Bad Data Detection and Identification","title":"PowerModelsDistributionStateEstimation.simple_normalized_residuals","text":"simple_normalized_residuals(data::Dict, se_sol::Dict, R::Matrix)\n\nIt normalizes the residuals only based on their standard deviation, no sensitivity matrix involved. R^2 replaces Ω, where Ω = S ⋅ R Avoids all the matrix calculations but is a \"simplified\" method\n\n\n\n\n\n","category":"method"},{"location":"bad_data/#LAV-Estimator-Residual-Analysis-1","page":"Bad Data Detection and Identification","title":"LAV Estimator Residual Analysis","text":"","category":"section"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"The LAV estimator is known to be inherently robust to bad data, as it is basically a linear regression. Thus, it is sufficient to run it and then check its residuals as in the piece of code below. The residuals do not even need to be calculated, because in a LAV estimation, they are by default reported as res in the solution dictionary. As such, the user only needs to sort the residuals in descending orders, see what their magnitude is, and whether some residuals are much higher than the others. The latter, in general, points out the bad data.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":" bad_data[\"se_settings\"] = Dict{String,Any}(\"criterion\" => \"lav\",\r\n \"rescaler\" => 1)\r\n\r\n se_result_bd_lav = _PMDSE.solve_acp_red_mc_se(bad_data, solver)\r\n residual_tuples = [(m, maximum(meas[\"res\"])[1]) for (m, meas) in se_result_bd_lav[\"solution\"][\"meas\"]]\r\n sorted_tuples = sort(residual_tuples, by = last, rev = true)\r\n measurement_index_of_largest_residual = first(sorted_tuples[1])\r\n magnitude_of_largest_residual = last(sorted_tuples[1])\r\n ratio12 = (last(sorted_tuples[1])/last(sorted_tuples[2])) # ratio between the first and the second largest residuals.","category":"page"},{"location":"bad_data/#Other-Notes-1","page":"Bad Data Detection and Identification","title":"Other Notes","text":"","category":"section"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"Virtually all bad data detection and identification methods from the literature are done \"a-posteriori\", i.e., after running a state estimation, by performing statistical considerations on the measurement residuals, or \"a priori\" doing some measurement data pre-processing (these are not discussed but could be, e.g., removing missing data or absurd measurements like negative or zero voltage). Thus, it is easy for the user to use this framework to just run the state estimation calculations and then add customized bad data handling methods that take as input the input measurement dictionary and/or the output solution dictionary.","category":"page"},{"location":"bad_data/#","page":"Bad Data Detection and Identification","title":"Bad Data Detection and Identification","text":"An example on how to use this package to perform bad data detection and identification can be found at this link: see both its readme and the file src/scripts/case_study_E.jl.","category":"page"},{"location":"measurements/#Measurement-Conversion-1","page":"Measurements and Conversions","title":"Measurement Conversion","text":"","category":"section"},{"location":"measurements/#Introduction-1","page":"Measurements and Conversions","title":"Introduction","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"PMDSE provides a high degree of modularity in terms of the possible measurement space, the package separates three different variable spaces: the measured variables space, the formulation variable space and the state variables. ","category":"page"},{"location":"measurements/#Examples-of-conversion-from-the-measurement-space-to-the-formulation-space-1","page":"Measurements and Conversions","title":"Examples of conversion from the measurement space to the formulation space","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"In the ACP formulation","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"A typical smart meter measures the voltage magnitude, active and reactive powers, thus the measured variable space is: U_m, P_c and Q_c. However, if the ACP formulation is used then the formulation variable space is: U_m, angle U_a, P_c and Q_c. And the state variable space is: U_m, angle U_a.","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"In that case it can be seen that the state estimator requires an extra conversion step to calculate the voltage angle angle U_a from the measured space variables.","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"(Image: ACP-Conv-gif)","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"### **In the IVREN formulation","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"Similarly, if the IVREN formulation is used, the measured variables space remains the same as: U_m, P_c and Q_c.","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"However, the formulation variable space is the real and imaginary voltages and currents: U_r, U_i, I_r and I_i. So the state estimator requires an extra conversion step to calculate the real and imaginary voltages and currents from the measured space variables. And in that case the state variable space is: U_r, U_i.","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"(Image: IVREN-Conv-gif)","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"Any network formulation has a specific variable space, e.g., ACP includes vm, va, px and qx[1]. w = vm^2 is the lifted voltage variable native to branch flow conic and linear forms. The conversions for the reduced formulations work identically as their non-reduced equivalent.","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"[1]: The x in px, qx, cmx, cax, crx and cix indicates that these variables exists for branches (~), generators (g) and loads (-). In order to capture the variable for a specific element it should be rewritten, e.g., \"px\" respectively becomes \"p\", \"pg\" and \"pd\".","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"- vm va cmx cax crx cix px qx vr vi w Ptot Qtot vll\nACP N N SF X F F N N X X X \nACR S PP SF X MF MF N N N N X \nIVR S PP S PP N N M M N N X \nSDP X X X X X X N* N* X X N \nLD3F S X SF X X X N N X X N \nIVREN S PP S PP N N M M N N X Σ Σ S","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"where:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"F: conversion of type Fraction\nM: conversion of type Multiplication\nMF: conversion of type MultiplicationFraction\nN: native to the network formulation\nPP: conversion of type Tangent\nS: conversion of type Square\nSF: conversion of type SquareFraction\nX: not provided\nΣ: sum of the variables","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The N* in the SDP formulation indicates that those variable are only available for generators, loads and other devices/extensions, but not for measurements that refer to branch flows, yet.","category":"page"},{"location":"measurements/#Conversions-1","page":"Measurements and Conversions","title":"Conversions","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"Certain measurement variables may not be natively supported in the formulation space. Consequently, it becomes necessary to convert them into that specific space. This is accomplished through the inclusion of an additional constraint(s). The different types of conversion constraints are enumerated in what follows.","category":"page"},{"location":"measurements/#Tangent-1","page":"Measurements and Conversions","title":"Tangent","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type Tangent allows to include va measurements in the ACR and IVR formulation, and cax measurements in the IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"begineqnarray\r\n tan(textva) = fractextvitextvr \r\n tan(textcax) = fractextcixtextcrx\r\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#Fraction-1","page":"Measurements and Conversions","title":"Fraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type Fraction allows to include crx and cix measurements in the ACP formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"begineqnarray\r\n textcrx = fractextpxcdotcos(textva)+textqxcdotsin(textva)textvm \r\n textcix = fractextpxcdotsin(textva)-textqxcdotcos(textva)textvm\r\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#Multiplication-1","page":"Measurements and Conversions","title":"Multiplication","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type Multiplication allows to include px and qx measurements in the IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"begineqnarray\r\n textpx = textvrcdottextcrx + textvicdottextcix \r\n textqx = textvicdottextcrx - textvrcdottextcix\r\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"These are quadratic equality constraints, modeled using @constraint.","category":"page"},{"location":"measurements/#MultiplicationFraction-1","page":"Measurements and Conversions","title":"MultiplicationFraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type MultiplicationFraction allows to include crx and cix measurements in the ACR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"begineqnarray\r\n textcrx = fractextpxcdottextvr+textqxcdottextvitextvr^2+textvi^2 \r\n textcix = fractextpxcdottextvi-textqxcdottextvrtextvr^2+textvi^2 \r\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#SquareFraction-1","page":"Measurements and Conversions","title":"SquareFraction","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type SquareFraction allows to include cmx measurements in the ACP and ACR formulation, through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"beginequation\r\n textcmx^2 = fractextpx^2 + textqx^2textvm^2 \r\nendequation","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"If the conversion is applied to the LinDist3Flow formulation, then vm^2 is replaced by w. These are non-linear equality constraints, modeled using @NLconstraint.","category":"page"},{"location":"measurements/#Square-1","page":"Measurements and Conversions","title":"Square","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"The conversion type Square allows to include vm measurements in the ACR and IVR formulation, and cmx measurements in the IVR formulation, respectively through:","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"begineqnarray\r\n textvm^2 = textvi^2 + textvr^2 \r\n textcmx^2 = textcix^2 + textcrx^2 \r\nendeqnarray","category":"page"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"These are quadratic equality constraints, modeled using @constraint.","category":"page"},{"location":"measurements/#No-conversion-provided-1","page":"Measurements and Conversions","title":"No conversion provided","text":"","category":"section"},{"location":"measurements/#","page":"Measurements and Conversions","title":"Measurements and Conversions","text":"As displayed in the Table, some conversions are not provided. This is because the measured quantities are either unlikely to take place in practice, e.g., w, or tend to appear in pairs, e.g., cmx and cax with PMUs. In the latter case, it is more efficient to transform cax and cmx into rectangular variables a priori and then use them, for instance, with IVR.","category":"page"}] } diff --git a/docs/make.jl b/docs/make.jl index dd094be..7156380 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,6 +12,8 @@ makedocs( "Input Data Format" => "input_data_format.md", "Measurements and Conversions" => "measurements.md", "State Estimation Criteria" => "se_criteria.md", + "Angular Reference Models" => "angular_ref.md", + "Explicit Neutral Models" => "explicit_neutral_models.md", "Bad Data Detection and Identification" => "bad_data.md", ], "Library " => [ diff --git a/docs/src/ACP-conv-once.gif b/docs/src/ACP-conv-once.gif new file mode 100644 index 0000000..d4c72e4 Binary files /dev/null and b/docs/src/ACP-conv-once.gif differ diff --git a/docs/src/ACP-conv.gif b/docs/src/ACP-conv.gif new file mode 100644 index 0000000..6d06b83 Binary files /dev/null and b/docs/src/ACP-conv.gif differ diff --git a/docs/src/GroundedKronRed.svg b/docs/src/GroundedKronRed.svg new file mode 100644 index 0000000..ac0c3f9 --- /dev/null +++ b/docs/src/GroundedKronRed.svg @@ -0,0 +1 @@ +Tr. SecondaryGrounded Transformer Secondary and LoadKron Reduced Model (Three Wire) \ No newline at end of file diff --git a/docs/src/IVR-anim-once.gif b/docs/src/IVR-anim-once.gif new file mode 100644 index 0000000..37dd09b Binary files /dev/null and b/docs/src/IVR-anim-once.gif differ diff --git a/docs/src/IVR-anim.gif b/docs/src/IVR-anim.gif new file mode 100644 index 0000000..5c28751 Binary files /dev/null and b/docs/src/IVR-anim.gif differ diff --git a/docs/src/PMDvb.png b/docs/src/PMDvb.png new file mode 100644 index 0000000..46ff3d5 Binary files /dev/null and b/docs/src/PMDvb.png differ diff --git a/docs/src/Phasorspmg.png b/docs/src/Phasorspmg.png new file mode 100644 index 0000000..8ff0ca6 Binary files /dev/null and b/docs/src/Phasorspmg.png differ diff --git a/docs/src/UngroundedNotKron.svg b/docs/src/UngroundedNotKron.svg new file mode 100644 index 0000000..958eb0e --- /dev/null +++ b/docs/src/UngroundedNotKron.svg @@ -0,0 +1 @@ +Tr. SecondaryGrounded Transformer Secondary and Ungrounded LoadKron Reduced Model (Three Wire) \ No newline at end of file diff --git a/docs/src/angular_ref.md b/docs/src/angular_ref.md new file mode 100644 index 0000000..0db2d72 --- /dev/null +++ b/docs/src/angular_ref.md @@ -0,0 +1,121 @@ +# Angular Reference Models +As of version 0.8.0, PMDSE provides the ability to customize bounds of voltage angles variables, mostly used to define angular reference models for the reference bus of the network. + +## Background + +Conventionally, the slack bus voltage phasor is used as reference for the rest of the network. In single-phase calculations, the reference phasor is set to 1 pu (magnitude) and 0 degrees (angle). The three-phase system extension of these assumptions would be to set the slack bus voltage magnitude to [1., 1., 1.] pu and the angles are equally spaced, e.g., [0, -120, 120] degrees. While this is common practice for three-phase network power flows and similar calculations, the assumption of equally spaced phasor angles is only valid for stiff and balanced networks, and do not hold for generic multi-conductor networks, low voltage ones in particular. + +In the case phasor measurements are available at the slackbus, these can integrated in the DSSE by either "fixing" the slackbus variables to the measured values (not recommended), or by treating the phasor measurement as any other measurement. However, phasor measurements are practically absent in present distribution networks, so these solutions are in general not possible, and the standard practice is to rely on the balanced reference slackbus assumptions described above. PMDSE > v0.8.0 features two alternatives to the balanced slackbus model, described below. + +![Phasors-Assumption-LV](slackPhasors.svg) + +The first alternative consists of setting a balanced reference bus to be the **primary** side of the slack bus (instead of the secondary), such that the voltage phasor on the secondary side is, more realistically, unbalanced. The unbalance introduced between the primary and the secondary is dependent on the network's stiffness. +The stiffness can be analytically determined by modeling an equivalent generator, where the generator's short circuit capability represents the upstream network impedance (Thevenin equivalent). This approach is relatively standard in distribution network analysis, and is featured "out-of-the-box", e.g., in OpenDSS and PowerModelsDistribution.jl. +In PowerModelsDistribution.jl, the slack bus can be identified by looking up the bus with `bus_type = 3` property in the network's mathematical model. PowerModelsDistribution.jl represents that slack bus with a virtual generator connected to a virtual bus via a virtual branch, with branch parameters based on the generator's short circuit capability, as shown in the figure below. +Note that PMD can automatically add these virtual elements when parsing network data in OpenDSS format through PMD's built-in parser. + +![PMD modelling of slack bus](PMDvb.png) + +Note that measurements at the ``virtual branch" (slack bus of PowerModelsDistribution.jl) do not exist in practice. Moreover, short circuit capabilities and virtual branch parameters are often calculated through assumptions on the substation transformer and upstream network which are often inaccurate and may compromise the trustworthiness of state estimation results. + +Therefore, there is high value in being able to develop alternative models for voltage angle references in unbalanced networks without phasor measurements. This is usually not possible with conventional (e.g., Gauss-Newton) state estimation algorithms, whereas PMDSE enables such capabilities through its compatibility with more generic solvers, e.g., Ipopt. + +A discussion of one such generic model, as well as its comparison with the two more conventional approaches described above, is presented in our publication[^1]. Hereafter, we provide a succint summary of these three approaches to reference voltage phasor modelling. + +[^1] : M. Numair, M. Vanin, and D. Van Hertem, "Angular Reference Models for Optimization-based Unbalanced Distribution System State Estimation," in Proc. IEEE PES Innovative Smart Grid Technologies Europe (ISGT Europe), Dubrovnik, Croatia, 2024 + +## Angular Reference Modeling Approaches for Distribution System State Estimation (DSSE) +Based on the above discussion and related literature on the topic, three approaches for modeling the reference voltage angles are made available in DSSE: + +![Example for three approaches](threeApproaches.svg) + +### **Approach A: Conventional Symmetrical Reference Angle** + +* This approach designates the substation head bus as a stiff slack bus and **fixes its voltage phasor angles to [0, -120, 120] degrees**, assuming balanced voltage angles. +* This assumption **may lead to inaccuracies in DSSE, especially in LV networks with significant unbalance**. The errors propagate to other estimated states, such as voltage magnitudes. + +### **Approach B: Single Angular Reference** + +* This approach **fixes only the phase "a" voltage angle to 0 degrees (or another arbitrary value), while the angles for phases "b" and "c" are treated as state variables** instead of being assigned determined values, e.g., -120 and 120 degrees. +* This method **captures the unbalance at the reference bus without relying on knowledge of upstream network parameters**, which can lead to more accurate state estimates in generic distribution networks. +* The voltage angles at the reference bus are thus **relaxed**, although they can be bound, e.g., between -100 and -140 for phase b and 100 and 140 for phase c, to facilitate the solving process. Bounds should be loose enough as to not remove improved solutions from the search space. + +### **Approach C: Virtual Bus Approach** + +* This approach introduces **a virtual primary bus**, allowing for the complete relaxation of voltage phasor angles at the secondary side. +* The **symmetrical angle assumption is then applied to the virtual bus**, and a virtual branch is added between primary and secondary bus, whose impedance is determined through a Thevenin equivalent model of the upstream network or the short circuit capabilities of the equivalent synchronous generator. +* While potentially more accurate in scenarios with perfect knowledge of upstream network parameters, this approach **suffers from inaccuracies when these parameters are unknown or inaccurately modeled**. This is a significant drawback in real-world LV networks where this information is often unavailable. + +## Usage of these Models in PMDSE + + +### **Approach C: Virtual Bus Approach** +When importing OpenDSS data, the default behaviour of PowerModelsDistribution.jl is to use the virtual bus to represent the slack bus (Approach C) and to define the angular reference for the network. Hence, performing state estimation with the default MATHEMATICAL model from PowerModelsDistribution.jl will result in **assuming measurements at the virtual branch (slack bus)** and the results with then be reliant on the assumptions made about the upstream network (virtual branch parameters). + + +```julia +# load packages +import Ipopt +import PowerModelsDistribution as _PMD +import PowerModelsDistributionStateEstimation as _PMDSE + +# parse example network data (.dss format), featured in the PMDSE package +ntw_path = joinpath(_PMDSE.BASE_DIR, "test/data/extra/networks/case3_unbalanced.dss") +data = _PMD.parse_file(ntw_path; data_model=_PMD.MATHEMATICAL) + +# solve a power flow to generate synthetic measurements +pf_result = _PMD.solve_mc_pf(data, model, ipopt_solver) + +# write the measurements in a temporary file/folder +msr_path = joinpath(mktempdir(),"temp.csv") +_PMDSE.write_measurements!(_PMDSE.IVRUPowerModel, data, pf_result, msr_path) + +# add measurements to the data dictionary, as well as state estimation settings +_PMDSE.add_measurements!(data, msr_path, actual_meas = true) +data["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1) + +# set solver and attributes, and run state estimation +slv = PMDSE.optimizer_with_attributes(Ipopt.Optimizer, "tol"=>1e-8, "print_level"=>0) +SE_c = _PMDSE.solve_mc_se(math_c, model, slv) +``` + +However in a realistic case measurement are not available at the virtual branch, but at the substation bus. Hence, the virtual bus is removed from the MATHEMATICAL data model. And the substation bus is defined as the slack bus. Additionally the measurement file at `msr_path` is updated to contain the measurements at the substation bus instead of the virtual bus. + +```julia +(virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(data) +``` + +### **Approach A: Conventional Symmetrical Reference Angle** + + +After removing the virtual bus and defining the substation bus as the slack bus, the conventional approach assumes symmetrical reference angle, by fixing the voltage angles at the substation bus to [0, -120, 120] degrees, and the voltage magnitudes to 1.0 p.u. (reference voltage magnitudes can be easily set to any other value if wished, and do not present modelling hurdles like reference angles). To fix the angle values, it is sufficient to add ``va" values to the data dictionary: + +```julia + math_a = deepcopy(data) + math_a["bus"]["$(r_new)"]["va"] = deg2rad.([0, -120, 120]) + math_a["bus"]["$(r_new)"]["bus_type"] = 3 + SE_a = _PMDSE.solve_mc_se(math_a, model, slv) +``` + +### **Approach B: Single Angular Reference** + +For approach B, we bound the voltage angles at the buses between maximum and minimum values instead of fixing them at an exact value, through the "vamin" and "vamax" dictionary entries: + +```julia + math_b = deepcopy(data) + (virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(math_b) + Angles_bound = Inf + math_b["bus"]["$(r_new)"]["bus_type"] = 3 + math_b["bus"]["$(r_new)"]["vamax"] = deg2rad.([0.0, -120+Angles_bound, 120+Angles_bound]) + math_b["bus"]["$(r_new)"]["vamin"] = deg2rad.([0.0, -120-Angles_bound, 120-Angles_bound]) + SE_b = _PMDSE.solve_mc_se(math_b, model, slv) +``` + +Note how the angles at the substation bus are relaxed except for the phase "*a*" angle which is fixed to 0 degrees. The angles for phases "*b*" and "*c*" are treated as state variables and are estimated by PMDSE(+Ipopt). The exact values of these two variables are in practice impossible to retrieve with a standard DSSE set-up (i.e., with limited measurements), and the system is in general **unobservable**. However, as power flows depend on the difference between voltage angles at adjacent buses, rather than on their absolute value, multiple (infinite) angle values result in the same DSSE objective, voltage magnitude and power flow values. As this approach does no rely on upstream network parameters nor balanced angle assumptions, observability of voltage angle values is "sacrificed" in favor of accurate estimation of the other states (notably voltage magnitudes) and their dependent variables (power flows and injections). + +## Summary + +In summary, PMDSE now offers the ability to constrain, relax or bound the voltage angles at the slack bus (or any arbitrary bus) using the `va`, `vamin` and `vamax` dictionary entries in the `MATHEMATICAL` model dictionary, to obtain any of the three approaches above. + + + diff --git a/docs/src/explicit_neutral_models.md b/docs/src/explicit_neutral_models.md new file mode 100644 index 0000000..6a29a79 --- /dev/null +++ b/docs/src/explicit_neutral_models.md @@ -0,0 +1,44 @@ +# Explicit Neutral Models for DSSE + +As of version 0.8.0, PMDSE support the explicit neutral models from PowerModelsDistribution.jl in the `IVRENPowerModel` formulation. This feature is crucial for the state estimation of low voltage networks when the neutral wire is not solidly grounded at all buses, which is usually the case for European low voltage feeders. + +## Background + +A typical European (but this is also true in multiple other parts of the world) low voltage network would have three phases and four wires: three wires for the three-phase lines (L1, L2, L3), one wire for the neutral (N). + +In a typical European low voltage installation, the neutral wire is connected to the ground at the transformer secondary, and depending on the load earthing scheme the neutral wire might be connected to the ground at the load side or not. When the neutral is grounded at both the transformer secondary and at the load side, it is safe to assume that the neutral voltage is consistently zero (the same as ground voltage), and hence no current flows through the neutral wire. In that case, kron reduction is an exact approximation for the three-phase network analysis. + +![Four-wire-grounded-at-load](GroundedKronRed.svg) + +However, in unbalanced networks with sparselt grounded neutral, "neutral shift" occurs in general, i.e., the neutral voltage and currents are nonzero. To capture this phenomenon, mathematical tools that explicitly feature a four-wire model are required, and the commonly used Kron's reduction would result in accuracy loss. + +![Four-wire-not-grounded-at-load](UngroundedNotKron.svg) [^1] + +[^1]: Figures adapted from S. Claeys and G. Deconinck, “Distribution Network Modeling: From Simulation Towards Optimization,” 2021. PhD Thesis, KU Leuven. + + +## Usage +The function `solve_ivr_en_mc_se()` is added to PMDSE to perform state estimation with the explicit neutral models in the IVR formulation. An example (which also relies on PowerModelsDistribution's four-wire capabilities to create synthetic data for the DSSE, is the following: + +```julia +import Ipopt +import PowerModelsDistribution as _PMD +import PowerModelsDistributionStateEstimation as _PMDSE + +eng_en = _PMD.parse_file(joinpath(_PMDSE.BASE_DIR, "test", "data", "three-bus-en-models", "3bus_4wire.dss")) +msr_path = joinpath(mktempdir(),"temp_msr.csv") + +eng_en = _PMD.parse_file(ntw_path) +_PMD.transform_loops!(eng_en) +_PMD.remove_all_bounds!(eng_en) + +math_en = _PMD.transform_data_model(eng_en, kron_reduce=false, phase_project=false) + +pf_result = _PMD.solve_mc_pf(data, model, Ipopt.Optimizer) +_PMDSE.write_measurements!(model, data, pf_result, msr_path) +_PMDSE.add_measurements!(data, msr_path, actual_meas = true) + +data["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1) +se_result = _PMDSE.solve_ivr_en_mc_se(data, Ipopt.Optimizer) +``` +Note that other than using the new function and carefully parsing four-wire data (see PowerModelsDistribution's documentation), the process is similar to that of DSSE for three-wire models. diff --git a/docs/src/measurements.md b/docs/src/measurements.md index 1a2bd64..a03044a 100644 --- a/docs/src/measurements.md +++ b/docs/src/measurements.md @@ -2,6 +2,33 @@ ## Introduction + +PMDSE provides a high degree of modularity in terms of the possible measurement space, the package separates three different variable spaces: the measured variables space, the formulation variable space and the state variables. + +### Examples of conversion from the measurement space to the formulation space + +**In the ACP formulation** + +A typical smart meter measures the voltage magnitude, active and reactive powers, thus the **measured variable space** is: $|U_{m}|$, $P_c$ and $Q_c$. +However, if the ACP formulation is used then the **formulation variable space** is: $|U_{m}|$, $\angle U_{a}$, $P_c$ and $Q_c$. +And the **state variable space** is: $|U_{m}|$, $\angle U_{a}$. + +In that case it can be seen that the state estimator requires an extra conversion step to calculate the voltage angle $\angle U_{a}$ from the measured space variables. + +![ACP-Conv-gif](ACP-Conv-once.gif) + + +### In the IVREN formulation + +Similarly, if the IVREN formulation is used, the **measured variables space** remains the same as: $|U_{m}|$, $P_c$ and $Q_c$. + +However, the **formulation variable space** is the real and imaginary voltages and currents: $U_{r}$, $U_{i}$, $I_{r}$ and $I_{i}$. So the state estimator requires an extra conversion step to calculate the real and imaginary voltages and currents from the measured space variables. And in that case the **state variable space** is: $U_{r}$, $U_{i}$. + +![IVREN-Conv-gif](IVR-anim-once.gif) + + + + Any network formulation has a specific variable space, e.g., ACP includes `vm`, `va`, `px` and `qx`[^1]. `w` = `vm^2` is the lifted voltage variable native to branch flow conic and linear forms. The conversions for the reduced formulations work identically as their non-reduced equivalent. @@ -12,13 +39,16 @@ The conversions for the reduced formulations work identically as their non-reduc should be rewritten, e.g., `"px"` respectively becomes `"p"`, `"pg"` and `"pd"`. -| - | vm | va | cmx | cax | crx | cix | px | qx | vr | vi | w | -| :-------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | -| **ACP** | N | N | SF | X | F | F | N | N | X | X | X | -| **ACR** | S | PP | SF | X | MF | MF | N | N | N | N | X | -| **IVR** | S | PP | S | PP | N | N | M | M | N | N | X | -| **SDP** | X | X | X | X | X | X | N* | N* | X | X | N | -| **LD3F** | S | X | SF | X | X | X | N | N | X | X | N | + + +| - | vm | va | cmx | cax | crx | cix | px | qx | vr | vi | w | Ptot | Qtot | vll | +| :-------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | +| **ACP** | N | N | SF | X | F | F | N | N | X | X | X | Σ | Σ | S | +| **ACR** | S | PP | SF | X | MF | MF | N | N | N | N | X | Σ | Σ | | +| **IVR** | S | PP | S | PP | N | N | M | M | N | N | X | Σ | Σ | S | +| **SDP** | X | X | X | X | X | X | N* | N* | X | X | N | Σ | Σ | | +| **LD3F** | S | X | SF | X | X | X | N | N | X | X | N | Σ | Σ | | +| **IVREN** | S | PP | S | PP | N | N | M | M | N | N | X | Σ | Σ | S | where: - F: conversion of type Fraction @@ -29,6 +59,7 @@ where: - S: conversion of type Square - SF: conversion of type SquareFraction - X: not provided +- Σ: sum of the variables The N* in the SDP formulation indicates that those variable are only available for generators, loads and other devices/extensions, but not for measurements that diff --git a/docs/src/slackPhasors.svg b/docs/src/slackPhasors.svg new file mode 100644 index 0000000..eb977d4 --- /dev/null +++ b/docs/src/slackPhasors.svg @@ -0,0 +1 @@ +Ideally equally spaced phasorsLV network secondary case \ No newline at end of file diff --git a/docs/src/threeApproaches.svg b/docs/src/threeApproaches.svg new file mode 100644 index 0000000..27d4e06 --- /dev/null +++ b/docs/src/threeApproaches.svg @@ -0,0 +1 @@ +Approach (B)Approach (A)Virtual Branch (VB)Approach (C) \ No newline at end of file diff --git a/src/PowerModelsDistributionStateEstimation.jl b/src/PowerModelsDistributionStateEstimation.jl index c94568e..144d61c 100644 --- a/src/PowerModelsDistributionStateEstimation.jl +++ b/src/PowerModelsDistributionStateEstimation.jl @@ -41,6 +41,7 @@ const _RAN = Random const _SF = SpecialFunctions const _STT = Statistics +const _N_IDX = 4 # paths const BASE_DIR = dirname(@__DIR__) @@ -81,6 +82,7 @@ include("io/polynomials.jl") include("io/postprocessing.jl") include("prob/se.jl") +include("prob/se_en.jl") # export include("core/export.jl") diff --git a/src/core/constraint.jl b/src/core/constraint.jl index 658e3dd..556f131 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -19,9 +19,9 @@ function constraint_mc_residual(pm::_PMD.AbstractUnbalancedPowerModel, i::Int; n dst = _PMD.ref(pm, nw, :meas, i, "dst") rsc = _PMD.ref(pm, nw, :se_settings)["rescaler"] crit = _PMD.ref(pm, nw, :meas, i, "crit") - conns = get_active_connections(pm, nw, _PMD.ref(pm, nw, :meas, i, "cmp"), cmp_id) - - for (idx, c) in enumerate(conns) + meas_var = _PMD.ref(pm, nw, :meas, i, "var") + conns = get_active_connections(pm, nw, _PMD.ref(pm, nw, :meas, i, "cmp"), cmp_id, meas_var) + for (idx, c) in enumerate(setdiff(conns,[_N_IDX])) if (occursin("ls", crit) || occursin("lav", crit)) && isa(dst[idx], _DST.Normal) μ, σ = occursin("w", crit) ? (_DST.mean(dst[idx]), _DST.std(dst[idx])) : (_DST.mean(dst[idx]), 1.0) end @@ -37,7 +37,7 @@ function constraint_mc_residual(pm::_PMD.AbstractUnbalancedPowerModel, i::Int; n res[idx] * rsc^2 * σ^2 >= (var[c] - μ)^2 ) elseif crit ∈ ["wlav", "lav"] && isa(dst[idx], _DST.Normal) - JuMP.@NLconstraint(pm.model, + JuMP.@constraint(pm.model, res[idx] * rsc * σ == abs(var[c] - μ) ) elseif crit == "rwlav" && isa(dst[idx], _DST.Normal) @@ -68,3 +68,260 @@ function constraint_mc_residual(pm::_PMD.AbstractUnbalancedPowerModel, i::Int; n end end end + +# Constraints related to the ANGULAR REFERENCE MODELS +######################################################## +## ACP +function variable_mc_bus_voltage(pm::_PMD.AbstractUnbalancedPowerModel; bounded = true) + _PMD.variable_mc_bus_voltage_magnitude_only(pm; bounded = true) + _PMDSE.variable_mc_bus_voltage_angle(pm; bounded = true) +end + + +""" + variable_mc_bus_voltage_angle(pm::_PMD.AbstractUnbalancedPowerModel; nw::Int=_PMD.nw_id_default, bounded::Bool=true, report::Bool=true) + +Defines the bus voltage angle variables for a multi-conductor unbalanced power model. + +# Arguments +- `pm::AbstractUnbalancedPowerModel`: The power model instance. +- `nw::Int`: The network identifier (default is `_PMD.nw_id_default`). +- `bounded::Bool`: If `true`, applies bounds to the voltage angle variables based on the bus data (default is `true`). +- `report::Bool`: If `true`, reports the solution component values (default is `true`). + +# Description +This function initializes the bus voltage angle variables for each bus in the power model. It sets the starting values for the voltage angles based on default values or specified start values in the bus data. If `bounded` is `true`, it applies lower and upper bounds to the voltage angle variables based on the `vamin` and `vamax` fields in the bus data. If `report` is `true`, it reports the solution component values for the voltage angles. + +# Notes +- The starting values for the voltage angles are converted from degrees to radians. +""" +function variable_mc_bus_voltage_angle(pm::_PMD.AbstractUnbalancedPowerModel; nw::Int=_PMD.nw_id_default, bounded::Bool=true, report::Bool=true) + terminals = Dict(i => bus["terminals"] for (i,bus) in _PMD.ref(pm, nw, :bus)) + va_start_defaults = Dict(i => deg2rad.([0.0, -120.0, 120.0, fill(0.0, length(terms))...][terms]) for (i, terms) in terminals) + va = _PMD.var(pm, nw)[:va] = Dict(i => JuMP.@variable(pm.model, + [t in terminals[i]], base_name="$(nw)_va_$(i)", + start = _PMD.comp_start_value(_PMD.ref(pm, nw, :bus, i), ["va_start", "va"], t, va_start_defaults[i][findfirst(isequal(t), terminals[i])]), + + ) for i in _PMD.ids(pm, nw, :bus) + ) + + if bounded + for (i,bus) in _PMD.ref(pm, nw, :bus) + for (idx, t) in enumerate(terminals[i]) + if haskey(bus, "vamin") + _PMD.set_lower_bound(va[i][t], bus["vamin"][idx]) + @warn " va min bounds defined " + end + if haskey(bus, "vamax") + _PMD.set_upper_bound(va[i][t], bus["vamax"][idx]) + @warn " va max bounds defined " + end + end + end + end + + report && _IM.sol_component_value(pm, _PMD.pmd_it_sym, nw, :bus, :va, _PMD.ids(pm, nw, :bus), va) +end + +## ACR + +"" +function variable_mc_bus_voltage(pm::_PMD.AbstractUnbalancedACRModel; nw::Int=_PMD.nw_id_default, bounded::Bool=true, report::Bool=true) + variable_mc_bus_voltage_real(pm; nw=nw, bounded=bounded, report=report) + variable_mc_bus_voltage_imaginary(pm; nw=nw, bounded=bounded, report=report) + + # 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 _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 + for id in _PMD.ids(pm, nw, :bus) + busref = _PMD.ref(pm, nw, :bus, id) + terminals = busref["terminals"] + grounded = busref["grounded"] + + ncnd = length(terminals) + + if haskey(busref, "vr_start") && haskey(busref, "vi_start") + vr = busref["vr_start"] + vi = busref["vi_start"] + else + vm_start = fill(1.0, 3) + for t in 1:3 + if t in terminals + vmax = busref["vmax"][findfirst(isequal(t), terminals)] + vm_start[t] = min(vm_start[t], vmax) + + vmin = busref["vmin"][findfirst(isequal(t), terminals)] + vm_start[t] = max(vm_start[t], vmin) + end + end + + vm = haskey(busref, "vm_start") ? busref["vm_start"] : haskey(busref, "vm") ? busref["vm"] : [vm_start..., fill(0.0, ncnd)...][terminals] + va = haskey(busref, "va_start") ? busref["va_start"] : haskey(busref, "va") ? busref["va"] : [deg2rad.([0, -120, 120])..., zeros(length(terminals))...][terminals] + + vr = vm .* cos.(va) + vi = vm .* sin.(va) + end + + for (idx,t) in enumerate(terminals) + JuMP.set_start_value(_PMD.var(pm, nw, :vr, id)[t], vr[idx]) + JuMP.set_start_value(_PMD.var(pm, nw, :vi, id)[t], vi[idx]) + end + end + + # apply bounds if bounded + if bounded + for i in _PMD.ids(pm, nw, :bus) + _PMD.constraint_mc_voltage_magnitude_bounds(pm, i; nw=nw) + constraint_mc_voltage_angle_bounds(pm, i; nw=nw) + end + end +end + + +"" +function variable_mc_bus_voltage_real(pm::_PMD.AbstractUnbalancedACRModel; nw::Int=_PMD.nw_id_default, bounded::Bool=true, report::Bool=true) + terminals = Dict(i => bus["terminals"] for (i, bus) in _PMD.ref(pm, nw, :bus)) + + vr = _PMD.var(pm, nw)[:vr] = Dict(i => JuMP.@variable(pm.model, + [t in terminals[i]], base_name="$(nw)_vr_$(i)", + start = _PMD.comp_start_value(_PMD.ref(pm, nw, :bus, i), "vr_start", t, 1.0) + ) for i in _PMD.ids(pm, nw, :bus) + ) + report && _IM.sol_component_value(pm, _PMD.pmd_it_sym, nw, :bus, :vr, _PMD.ids(pm, nw, :bus), vr) +end + + +"" +function variable_mc_bus_voltage_imaginary(pm::_PMD.AbstractUnbalancedACRModel; nw::Int=_PMD.nw_id_default, bounded::Bool=true, report::Bool=true) + terminals = Dict(i => bus["terminals"] for (i,bus) in _PMD.ref(pm, nw, :bus)) + vi = _PMD.var(pm, nw)[:vi] = Dict(i => JuMP.@variable(pm.model, + [t in terminals[i]], base_name="$(nw)_vi_$(i)", + start = _PMD.comp_start_value(_PMD.ref(pm, nw, :bus, i), "vi_start", t, 0.0) + ) for i in _PMD.ids(pm, nw, :bus) + ) + report && _IM.sol_component_value(pm, _PMD.pmd_it_sym, nw, :bus, :vi, _PMD.ids(pm, nw, :bus), vi) +end + +function constraint_mc_voltage_angle_bounds(pm::_PMD.AbstractUnbalancedACRModel, i::Int; nw::Int=_PMD.nw_id_default)::Nothing + bus = _PMD.ref(pm, nw, :bus, i) + terminals = length(bus["terminals"]) + if haskey(bus, "vamin") && haskey(bus, "vamax") + va = haskey(bus, "va_start") ? bus["va_start"] : haskey(bus, "va") ? bus["va"] : [deg2rad.([0, -120, 120])..., zeros(length(terminals))...][terminals] + vamin = _PMD.get(bus, "vamin", fill(0.0, length(bus["terminals"]))) + vamax = _PMD.get(bus, "vamax", fill(Inf, length(bus["terminals"]))) + constraint_mc_voltage_angle_bounds(pm, nw, i, vamin, vamax) + end + nothing +end + +"`vamin <= va[i] <= vamax`" +function constraint_mc_voltage_angle_bounds(pm::_PMD.AbstractUnbalancedACRModel, nw::Int, i::Int, vamin::Vector{<:Real}, vamax::Vector{<:Real}) + @assert all(vamin .<= vamax) + vr = _PMD.var(pm, nw, :vr, i) + vi = _PMD.var(pm, nw, :vi, i) + + for (idx,t) in enumerate(_PMD.ref(pm, nw, :bus, i)["terminals"]) + if vamin[idx] > -Inf + JuMP.@NLconstraint(pm.model, tan(vamin[idx]) <= vi[t]/vr[t]) + @warn "consrtained minimum angle vamin at $(vamin[idx]) for bus $i to be less than $(atan(vi[t],vr[t]))" + end + + if vamax[idx] < Inf + JuMP.@NLconstraint(pm.model, tan(vamax[idx]) >= vi[t]/vr[t]) + @warn "consrtained maximum angle vamax at $(vamax[idx]) for bus $i to be greater than $(atan(vi[t],vr[t]))" + end + + end +end + +# Explicit Neutral related Constraints +######################################################## + +function constraint_mc_generator_power_se(pm::_PMD.IVRENPowerModel, id::Int; nw::Int=_IM.nw_id_default, report::Bool=true, bounded::Bool=true) + generator = _PMD.ref(pm, nw, :gen, id) + bus = _PMD.ref(pm, nw,:bus, generator["gen_bus"]) + + N = length(generator["connections"]) + pmin = get(generator, "pmin", fill(-Inf, N)) + pmax = get(generator, "pmax", fill( Inf, N)) + qmin = get(generator, "qmin", fill(-Inf, N)) + qmax = get(generator, "qmax", fill( Inf, N)) + + if get(generator, "configuration", WYE) == _PMD.WYE + constraint_mc_generator_power_wye_se(pm, nw, id, bus["index"], generator["connections"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) + else + constraint_mc_generator_power_delta_se(pm, nw, id, bus["index"], generator["connections"], pmin, pmax, qmin, qmax; report=report, bounded=bounded) + end +end + +"wye connected generator setpoint constraint for IVR formulation - SE adaptation" +function constraint_mc_generator_power_wye_se(pm::_PMD.IVRENPowerModel, nw::Int, id::Int, bus_id::Int, connections::Vector{Int}, pmin::Vector, pmax::Vector, qmin::Vector, qmax::Vector; report::Bool=true, bounded::Bool=true) + vr = _PMD.var(pm, nw, :vr, bus_id) + vi = _PMD.var(pm, nw, :vi, bus_id) + crg = _PMD.var(pm, nw, :crg, id) + cig = _PMD.var(pm, nw, :cig, id) + + if bounded + for (idx, c) in enumerate(connections[1:end-1]) + if pmin[c]> -Inf + JuMP.@constraint(pm.model, pmin[idx] .<= vr[c]*crg[c] + vi[c]*cig[c]) + end + if pmax[c]< Inf + JuMP.@constraint(pm.model, pmax[idx] .>= vr[c]*crg[c] + vi[c]*cig[c]) + end + if qmin[c]>-Inf + JuMP.@constraint(pm.model, qmin[idx] .<= vi[c]*crg[c] - vr[c]*cig[c]) + end + if qmax[c]< Inf + JuMP.@constraint(pm.model, qmax[idx] .>= vi[c]*crg[c] - vr[c]*cig[c]) + end + end + end +end + +function constraint_mc_current_balance_se(pm::_PMD.IVRENPowerModel, nw::Int, i::Int, terminals::Vector{Int}, grounded::Vector{Bool}, bus_arcs::Vector{Tuple{Tuple{Int,Int,Int},Vector{Int}}}, bus_arcs_sw::Vector{Tuple{Tuple{Int,Int,Int},Vector{Int}}}, bus_arcs_trans::Vector{Tuple{Tuple{Int,Int,Int},Vector{Int}}}, bus_gens::Vector{Tuple{Int,Vector{Int}}}, bus_storage::Vector{Tuple{Int,Vector{Int}}}, bus_loads::Vector{Tuple{Int,Vector{Int}}}, bus_shunts::Vector{Tuple{Int,Vector{Int}}}) + #NB only difference with pmd is crd_bus replaced by crd, and same with cid + vr = _PMD.var(pm, nw, :vr, i) + vi = _PMD.var(pm, nw, :vi, i) + + cr = get(_PMD.var(pm, nw), :cr, Dict()); _PMD._check_var_keys(cr, bus_arcs, "real current", "branch") + ci = get(_PMD.var(pm, nw), :ci, Dict()); _PMD._check_var_keys(ci, bus_arcs, "imaginary current", "branch") + crd = get(_PMD.var(pm, nw), :crd, Dict()); _PMD._check_var_keys(crd, bus_loads, "real current", "load") + cid = get(_PMD.var(pm, nw), :cid, Dict()); _PMD._check_var_keys(cid, bus_loads, "imaginary current", "load") + crg = get(_PMD.var(pm, nw), :crg, Dict()); _PMD._check_var_keys(crg, bus_gens, "real current", "generator") + cig = get(_PMD.var(pm, nw), :cig, Dict()); _PMD._check_var_keys(cig, bus_gens, "imaginary current", "generator") + crs = get(_PMD.var(pm, nw), :crs, Dict()); _PMD._check_var_keys(crs, bus_storage, "real currentr", "storage") + cis = get(_PMD.var(pm, nw), :cis, Dict()); _PMD._check_var_keys(cis, bus_storage, "imaginary current", "storage") + crsw = get(_PMD.var(pm, nw), :crsw, Dict()); _PMD._check_var_keys(crsw, bus_arcs_sw, "real current", "switch") + cisw = get(_PMD.var(pm, nw), :cisw, Dict()); _PMD._check_var_keys(cisw, bus_arcs_sw, "imaginary current", "switch") + crt = get(_PMD.var(pm, nw), :crt, Dict()); _PMD._check_var_keys(crt, bus_arcs_trans, "real current", "transformer") + cit = get(_PMD.var(pm, nw), :cit, Dict()); _PMD._check_var_keys(cit, bus_arcs_trans, "imaginary current", "transformer") + + Gs, Bs = _PMD._build_bus_shunt_matrices(pm, nw, terminals, bus_shunts) + + ungrounded_terminals = [(idx,t) for (idx,t) in enumerate(terminals) if !grounded[idx]] + + for (idx, t) in ungrounded_terminals + JuMP.@NLconstraint(pm.model, sum(cr[a][t] for (a, conns) in bus_arcs if t in conns) + + sum(crsw[a_sw][t] for (a_sw, conns) in bus_arcs_sw if t in conns) + + sum(crt[a_trans][t] for (a_trans, conns) in bus_arcs_trans if t in conns) + == + sum(crg[g][t] for (g, conns) in bus_gens if t in conns) + - sum(crs[s][t] for (s, conns) in bus_storage if t in conns) + - sum(crd[d][t] for (d, conns) in bus_loads if t in conns) + - sum( Gs[idx,jdx]*vr[u] -Bs[idx,jdx]*vi[u] for (jdx,u) in ungrounded_terminals) # shunts + ) + JuMP.@NLconstraint(pm.model, sum(ci[a][t] for (a, conns) in bus_arcs if t in conns) + + sum(cisw[a_sw][t] for (a_sw, conns) in bus_arcs_sw if t in conns) + + sum(cit[a_trans][t] for (a_trans, conns) in bus_arcs_trans if t in conns) + == + sum(cig[g][t] for (g, conns) in bus_gens if t in conns) + - sum(cis[s][t] for (s, conns) in bus_storage if t in conns) + - sum(cid[d][t] for (d, conns) in bus_loads if t in conns) + - sum( Gs[idx,jdx]*vi[u] +Bs[idx,jdx]*vr[u] for (jdx,u) in ungrounded_terminals) # shunts + ) + end +end diff --git a/src/core/measurement_conversion.jl b/src/core/measurement_conversion.jl index 092503d..d621042 100644 --- a/src/core/measurement_conversion.jl +++ b/src/core/measurement_conversion.jl @@ -63,6 +63,25 @@ struct MultiplicationFraction<:ConversionType voltage::Array end + +struct LineVoltage<:ConversionType + msr_id::Int64 + cmp_type::Symbol + cmp_id::Int64 + bus_ind::Int64 + elements::Array +end + +struct PowerSum<:ConversionType + msr_sym::Symbol + msr_id::Int64 + cmp_type::Symbol + cmp_id::Int64 + bus_ind::Int64 + arr1::Array + arr2::Array +end + function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedACPModel,i,msr::Symbol;nw=nw) cmp_id = _PMD.ref(pm, nw, :meas, i, "cmp_id") if msr == :cm @@ -83,6 +102,12 @@ function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedACPModel,i,msr msr_type = Fraction(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:pg, :qg, :va], :vm) elseif msr == :cid msr_type = Fraction(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:load,cmp_id)["load_bus"], [:pd, :qd, :va], :vm) + elseif msr ∈ [:qtot, :ptot] + cmp_string = String(_PMD.ref(pm,nw,:meas,i)["cmp"]) # "load" or "gen" + cmp_symb = _PMD.ref(pm,nw,:meas,i)["cmp"] #:load or :gen + msr_type = PowerSum(msr, i, cmp_symb , cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:p], [:q]) + elseif msr == :vll + msr_type = LineVoltage(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"],[:vm, :va]) else error("the chosen measurement $(msr) at $(_PMD.ref(pm, nw, :meas, i, "cmp")) $(_PMD.ref(pm, nw, :meas, i, "cmp_id")) is not supported and should be removed") end @@ -113,6 +138,10 @@ function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedACRModel,i,msr msr_type = MultiplicationFraction(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:pg, :qg], [:vr, :vi]) elseif msr == :cid msr_type = MultiplicationFraction(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:load,cmp_id)["load_bus"], [:pd, :qd], [:vr, :vi]) + elseif msr ∈ [:qtot, :ptot] + cmp_string = String(_PMD.ref(pm,nw,:meas,i)["cmp"]) # "load" or "gen" + cmp_symb = _PMD.ref(pm,nw,:meas,i)["cmp"] #:load or :gen + msr_type = PowerSum(msr, i, cmp_symb , cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:p], [:q]) else error("the chosen measurement $(msr) at $(_PMD.ref(pm, nw, :meas, i, "cmp")) $(_PMD.ref(pm, nw, :meas, i, "cmp_id")) is not supported and will be ignored") end @@ -121,10 +150,13 @@ end function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedIVRModel,i,msr::Symbol;nw=nw) cmp_id = _PMD.ref(pm, nw, :meas, i, "cmp_id") - if msr == :vm + # VOLTAGES + if msr == :vm || msr == :vmn msr_type = Square(i,:bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"], [:vi, :vr]) elseif msr == :va msr_type = Tangent(i, :bus, cmp_id, :vi, :vr) + elseif msr == :vll + msr_type = LineVoltage(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"],[:vi, :vr]) elseif msr == :cm msr_type = Square(i,:branch, cmp_id, _PMD.ref(pm,nw,:branch,cmp_id)["f_bus"], [:cr, :ci]) elseif msr == :cmg @@ -137,6 +169,7 @@ function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedIVRModel,i,msr msr_type = Tangent(i, :gen, cmp_id, :cig, :crg) elseif msr == :cad msr_type = Tangent(i, :load, cmp_id, :cid, :crd) + # POWERS elseif msr == :p msr_type = Multiplication(msr, i,:branch, cmp_id, _PMD.ref(pm,nw,:branch,cmp_id)["f_bus"], [:cr, :ci], [:vr, :vi]) elseif msr == :pg @@ -149,6 +182,10 @@ function assign_conversion_type_to_msr(pm::_PMD.AbstractUnbalancedIVRModel,i,msr msr_type = Multiplication(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:crg, :cig], [:vr, :vi]) elseif msr == :qd msr_type = Multiplication(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:load,cmp_id)["load_bus"], [:crd, :cid], [:vr, :vi]) + elseif msr ∈ [:qtot, :ptot] + cmp_string = String(_PMD.ref(pm,nw,:meas,i)["cmp"]) # "load" or "gen" + cmp_symb = _PMD.ref(pm,nw,:meas,i)["cmp"] #:load or :gen + msr_type = PowerSum(msr, i, cmp_symb, cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:cr, :ci], [:vr, :vi]) # _PMD.ref(pm,nw,:meas,i)["cmp"] -> :gen or :bus else error("the chosen measurement $(msr) at $(_PMD.ref(pm, nw, :meas, i, "cmp")) $(_PMD.ref(pm, nw, :meas, i, "cmp_id")) is not supported and should be removed") end @@ -165,6 +202,10 @@ function assign_conversion_type_to_msr(pm::_PMD.LinDist3FlowPowerModel,i,msr::Sy msr_type = SquareFraction(i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:pg, :qg], [:w]) elseif msr == :cmd msr_type = SquareFraction(i,:load, cmp_id, _PMD.ref(pm,nw,:load,cmp_id)["load_bus"], [:pd, :qd], [:w]) + elseif msr ∈ [:qtot, :ptot] + cmp_string = String(_PMD.ref(pm,nw,:meas,i)["cmp"]) # "load" or "gen" + cmp_symb = _PMD.ref(pm,nw,:meas,i)["cmp"] #:load or :gen + msr_type = PowerSum(msr, i, cmp_symb , cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:p], [:q]) else error("the chosen measurement $(msr) at $(_PMD.ref(pm, nw, :meas, i, "cmp")) $(_PMD.ref(pm, nw, :meas, i, "cmp_id")) is not supported and should be removed") end @@ -317,7 +358,7 @@ function create_conversion_constraint(pm::_PMD.AbstractUnbalancedPowerModel, ori den = _PMD.var(pm, nw, msr.denominator, (msr.cmp_id, _PMD.ref(pm, nw, :branch,msr.cmp_id)["f_bus"], _PMD.ref(pm, nw, :branch,msr.cmp_id)["t_bus"])) else num = _PMD.var(pm, nw, msr.numerator, msr.cmp_id) - den = _PMD.var(pm, nw, msr.numerator, msr.cmp_id) + den = _PMD.var(pm, nw, msr.denominator, msr.cmp_id) end msr.cmp_type == :branch ? id = (msr.cmp_id, _PMD.ref(pm,nw,:branch,msr.cmp_id)["f_bus"], _PMD.ref(pm,nw,:branch,msr.cmp_id)["t_bus"]) : id = msr.cmp_id JuMP.@NLconstraint(pm.model, @@ -385,3 +426,197 @@ function create_conversion_constraint(pm::_PMD.AbstractUnbalancedPowerModel, ori ) end end + +# msr_type = PowerSum(msr, i, cmp_symb , cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:p], [:q]) +# ↗ PowerSum(msr, i,:load, cmp_id, load_bus_id, [:pd], [:qd]) +# ↗ :qtot || :ptot ↗ PowerSum(msr, i,:gen, cmp_id, gen_bus_id, [:pg], [:qg]) +function create_conversion_constraint(pm::_PMD.AbstractUnbalancedPowerModel, original_var, msr::PowerSum, nw=nw) + + cmp_indication = msr.cmp_type == :load ? "d" : msr.cmp_type == :gen ? "g" : "" + + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + + if occursin("p", string(msr.msr_sym)) # then it is :ptot + + ps = Symbol.(String.(msr.arr1).*cmp_indication) + px = _PMD.var(pm, nw, ps[1], msr.cmp_id) + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(px[c] for c in conn) == 0 + ) + + elseif occursin("q", string(msr.msr_sym)) # then it is :qtot + qs = Symbol.(String.(msr.arr2).*cmp_indication) + qx = _PMD.var(pm, nw, qs[1], msr.cmp_id) + + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(qx[c] for c in conn) == 0 + ) + end +end + + + +# msr_type = PowerSum(msr, i, cmp_symb , cmp_id, _PMD.ref(pm,nw,cmp_symb,cmp_id)["$(cmp_string)_bus"], [:p], [:q]) +# ↗ :ptot || :qtot ↗ PowerSum(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["load_bus"], [:crd, :cid], [:vr, :vi]) +# ↗ :ptot || :qtot ↗ PowerSum(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:crg, :cig], [:vr, :vi]) +function create_conversion_constraint(pm::_PMD.AbstractUnbalancedIVRModel, original_var, msr::PowerSum; nw=nw) + #This works for both four and three wire IVR (might be overriding to the funtion at se_en.jl) + + cmp_indication = msr.cmp_type == :load ? "d" : msr.cmp_type == :gen ? "g" : "" + cs = Symbol.(String.(msr.arr1).*cmp_indication) + vs = msr.arr2 + vr = _PMD.var(pm,nw, vs[1],msr.bus_ind) + vi = _PMD.var(pm,nw, vs[2],msr.bus_ind) + cr = _PMD.var(pm,nw, cs[1],msr.cmp_id) + ci = _PMD.var(pm,nw, cs[2],msr.cmp_id) + + allconn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + conn = setdiff(allconn,_N_IDX) + + threeWireIVR = conn == allconn ? true : false + + if occursin("p", String(msr.msr_sym)) + + if threeWireIVR + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(cr[c]*(vr[c])+ci[c]*(vi[c]) for c in conn) == 0 + ) + else + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(cr[c]*(vr[c]-vr[_N_IDX])+ci[c]*(vi[c]-vi[_N_IDX]) for c in conn) == 0 + ) + end + + elseif occursin("q", String(msr.msr_sym)) + + if threeWireIVR + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(-ci[c]*(vr[c])+cr[c]*(vi[c]) for c in conn) == 0 + ) + else + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(-ci[c]*(vr[c]-vr[_N_IDX])+cr[c]*(vi[c]-vi[_N_IDX]) for c in conn) == 0 + ) + end + end +end + +# msr_type = LineVoltage(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"],[:vm, :va]) +#↗ :vll ↗ LineVoltage(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"], [:vi, :vr]) +function create_conversion_constraint(pm::_PMD.AbstractUnbalancedIVRModel, original_var, msr::LineVoltage; nw=nw) + # msr.elements = [:vi, :vr] + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + vr = _PMD.var(pm, nw, msr.elements[2], msr.cmp_id) # msr.elements[2] = :vr 0_vr_3[] + vi = _PMD.var(pm, nw, msr.elements[1], msr.cmp_id) # msr.elements[1] = :vi + index_pairs = length(conn) > 2 ? [(1,2), (2,3), (3,1)] : [tuple(setdiff(conn, _N_IDX)...)] # checks if :vll is for three phase or single line-to-line load + for (idx, (i, j)) in enumerate(index_pairs) + JuMP.@constraint(pm.model, original_var[msr.cmp_id][idx]^2 == vr[i]^2 + vr[j]^2 - 2*vr[i]*vr[j] + vi[i]^2 + vi[j]^2 - 2*vi[i]*vi[j]) + end +end + +# Explicit Neutral related conversion functions + +# ↗ :vmn ↗ Square(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"], [:vi, :vr]) +function create_conversion_constraint(pm::_PMD.IVRENPowerModel, original_var, msr::Square; nw=nw) + new_var = [] + # prepare the :vi and :vr inside pm.model + for nvn in msr.elements + if msr.cmp_type == :branch + push!(new_var, _PMD.var(pm, nw, nvn, (msr.cmp_id, _PMD.ref(pm,nw,:branch,msr.cmp_id)["f_bus"], _PMD.ref(pm,nw,:branch,msr.cmp_id)["t_bus"]))) + else + push!(new_var, _PMD.var(pm, nw, nvn, msr.cmp_id)) + end + end + + + msr.cmp_type == :branch ? id = (msr.cmp_id, _PMD.ref(pm,nw,:branch,msr.cmp_id)["f_bus"], _PMD.ref(pm,nw,:branch,msr.cmp_id)["t_bus"]) : id = msr.cmp_id + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) # 1:4 for buses and branches while 1:3 for loads and gens by nature + + JuMP.@constraint(pm.model, [c in setdiff(conn,_N_IDX)], + original_var[id][c]^2 == (sum( (n[c]- n[_N_IDX])^2 for n in new_var )) + + ) + +end + +# ↗ :vll ↗ LineVoltage(i, :bus, cmp_id, _PMD.ref(pm,nw,:bus,cmp_id)["index"], [:vi, :vr]) +function create_conversion_constraint(pm::_PMD.IVRENPowerModel, original_var, msr::LineVoltage; nw=nw) + # msr.elements = [:vi, :vr] + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + vr = _PMD.var(pm, nw, msr.elements[2], msr.cmp_id) # msr.elements[2] = :vr 0_vr_3[] + vi = _PMD.var(pm, nw, msr.elements[1], msr.cmp_id) # msr.elements[1] = :vi + index_pairs = length(conn) > 2 ? [(1,2), (2,3), (3,1)] : [tuple(setdiff(conn, _N_IDX)...)] # checks if :vll is for three phase or single line-to-line load + for (idx, (i, j)) in enumerate(index_pairs) + JuMP.@constraint(pm.model, original_var[msr.cmp_id][idx]^2 == vr[i]^2 + vr[j]^2 - 2*vr[i]*vr[j] + vi[i]^2 + vi[j]^2 - 2*vi[i]*vi[j]) + end +end + +# ↗ :pd || :qd ↗ Multiplication(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["load_bus"], [:crd, :cid], [:vr, :vi]) +# ↗ :pg || :qg ↗ Multiplication(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:crg, :cig], [:vr, :vi]) +function create_conversion_constraint(pm::_PMD.IVRENPowerModel, original_var, msr::Multiplication; nw=nw) + + m1 = [] + m2 = [] + + for m in msr.mult1 + if occursin("v", String(m)) && msr.cmp_type != :bus + push!(m1, _PMD.var(pm, nw, m, msr.bus_ind)) + elseif msr.cmp_type == :branch + push!(m1, _PMD.var(pm, nw, m, (msr.cmp_id, msr.bus_ind, _PMD.ref(pm, nw, :branch,msr.cmp_id)["t_bus"]))) + else + push!(m1, _PMD.var(pm, nw, m, msr.cmp_id)) + end + end + + for mm in msr.mult2 + if occursin("v", String(mm)) && msr.cmp_type != :bus + push!(m2, _PMD.var(pm, nw, mm, msr.bus_ind)) + elseif msr.cmp_type == :branch + push!(m2, _PMD.var(pm, nw, mm, (msr.cmp_id, msr.bus_ind, _PMD.ref(pm, nw, :branch,msr.cmp_id)["t_bus"]))) + else + push!(m2, _PMD.var(pm, nw, mm, msr.cmp_id)) + end + end + msr.cmp_type == :branch ? id = (msr.cmp_id, msr.bus_ind, _PMD.ref(pm,nw,:branch,msr.cmp_id)["t_bus"]) : id = msr.cmp_id + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + + conn = setdiff(conn,_N_IDX) + if occursin("p", String(msr.msr_sym)) + pcons = JuMP.@constraint(pm.model, [c in conn], + original_var[id][c] == m1[1][c]*(m2[1][c]-m2[1][_N_IDX])+m1[2][c]*(m2[2][c]-m2[2][_N_IDX]) + ) + display(pcons) + elseif occursin("q", String(msr.msr_sym)) + qcons= JuMP.@constraint(pm.model, [c in conn], + original_var[id][c] == -m1[2][c]*(m2[1][c]-m2[1][_N_IDX])+m1[1][c]*(m2[2][c]-m2[2][_N_IDX]) + ) + display(qcons) + end +end + + +# ↗ :ptot || :qtot ↗ PowerSum(msr, i,:load, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["load_bus"], [:crd, :cid], [:vr, :vi]) +# ↗ :ptot || :qtot ↗ PowerSum(msr, i,:gen, cmp_id, _PMD.ref(pm,nw,:gen,cmp_id)["gen_bus"], [:crg, :cig], [:vr, :vi]) +function create_conversion_constraint(pm::_PMD.IVRENPowerModel, original_var, msr::PowerSum; nw=nw) + + # decide if its a load or a generator + cmp_indication = msr.cmp_type == :load ? "d" : msr.cmp_type == :gen ? "g" : "" + cs = Symbol.(String.(msr.arr1).*cmp_indication) + vs = msr.arr2 + vr = _PMD.var(pm,nw, vs[1],msr.bus_ind) + vi = _PMD.var(pm,nw, vs[2],msr.bus_ind) + cr = _PMD.var(pm,nw, cs[1],msr.cmp_id) + ci = _PMD.var(pm,nw, cs[2],msr.cmp_id) + + conn = get_active_connections(pm, nw, msr.cmp_type, msr.cmp_id) + conn = setdiff(conn,_N_IDX) + if occursin("p", String(msr.msr_sym)) + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(cr[c]*(vr[c]-vr[_N_IDX])+ci[c]*(vi[c]-vi[_N_IDX]) for c in conn) == 0 + ) + elseif occursin("q", String(msr.msr_sym)) + JuMP.@constraint(pm.model, + original_var[msr.cmp_id] .- sum(-ci[c]*(vr[c]-vr[_N_IDX])+cr[c]*(vi[c]-vi[_N_IDX]) for c in conn) == 0 + ) + end +end \ No newline at end of file diff --git a/src/core/utils.jl b/src/core/utils.jl index 4b145d0..8334095 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -245,17 +245,40 @@ end Returns the list of terminals, connections or t_ and f_connections, depending on the type of the component. """ function get_active_connections(pm::_PMD.AbstractUnbalancedPowerModel, nw::Int, cmp_type::Symbol, cmp_id::Int) - if cmp_type == :bus - active_conn = _PMD.ref(pm, nw, :bus, cmp_id)["terminals"] - elseif cmp_type ∈ [:gen, :load] - active_conn = _PMD.ref(pm, nw, cmp_type, cmp_id)["connections"] - elseif cmp_type == :branch - active_conn = intersect(_PMD.ref(pm, nw, :branch, cmp_id)["f_connections"], _PMD.ref(pm, nw, :branch, cmp_id)["t_connections"]) - else - error("Measurements for component of type $cmp_type are not supported") - end - return active_conn + if cmp_type == :bus + active_conn = _PMD.ref(pm, nw, :bus, cmp_id)["terminals"] + elseif cmp_type ∈ [:gen, :load] + active_conn = _PMD.ref(pm, nw, cmp_type, cmp_id)["connections"] + elseif cmp_type == :branch + active_conn = intersect(_PMD.ref(pm, nw, :branch, cmp_id)["f_connections"], _PMD.ref(pm, nw, :branch, cmp_id)["t_connections"]) + else + error("Measurements for component of type $cmp_type are not supported") + end + return active_conn +end + + + +function get_active_connections(pm::_PMD.AbstractUnbalancedPowerModel, nw::Int, cmp_type::Symbol, cmp_id::Int, msr_var::Symbol ) + if msr_var == :qtot || msr_var == :ptot + active_conn = 1 + else + if cmp_type == :bus + active_conn = _PMD.ref(pm, nw, :bus, cmp_id)["terminals"] + elseif cmp_type ∈ [:gen, :load] + active_conn = _PMD.ref(pm, nw, cmp_type, cmp_id)["connections"] + elseif cmp_type == :branch + active_conn = intersect(_PMD.ref(pm, nw, :branch, cmp_id)["f_connections"], _PMD.ref(pm, nw, :branch, cmp_id)["t_connections"]) + else + error("Measurements for component of type $cmp_type are not supported") + end + end + return active_conn end + + + + """ function add_zib_virtual_meas!(data::Dict, σ::Float64; exclude::Array=[]) Adds zero-injection buses measurements to the "meas" dictionary if they are not present. @@ -312,4 +335,30 @@ function add_measurement!(data::Dict, var::Symbol, cmp::Symbol, cmp_id::Int64, m "dst" => [_DST.Normal(mu, sigma) for(mu, sigma) in zip(measured_value, σ)], "cmp_id"=> cmp_id ) +end + +""" + remove_virtual_bus!(math_b) + +Remove virtual bus, branch, and generator from the given `math` dictionary. + +# Arguments +- `math_b::Dict`: A dictionary containing information about buses, branches, and generators. + +# Example +""" +function remove_virtual_bus!(math) + virtual_bus = findfirst(bus -> contains(bus["name"], "virtual_bus.voltage_source.source"), math["bus"]) + virtual_branch = findfirst(branch -> contains(branch["name"], "_virtual_branch.voltage_source.source"), math["branch"]) + virtual_gen = findfirst(gen -> contains(gen["name"], "_virtual_gen"), math["gen"]) + + r_new = math["branch"][virtual_branch]["t_bus"] + + math["bus"][string(r_new)]["bus_type"] = 3 + math["gen"][string(virtual_gen)]["gen_bus"] = r_new + + delete!(math["bus"], string(virtual_bus)) + delete!(math["branch"], string(virtual_branch)) + + return virtual_bus, r_new end \ No newline at end of file diff --git a/src/core/variable.jl b/src/core/variable.jl index 2f0bfef..639569f 100644 --- a/src/core/variable.jl +++ b/src/core/variable.jl @@ -149,7 +149,7 @@ function variable_mc_measurement(pm::_PMD.AbstractUnbalancedPowerModel; nw::Int= msr_var = _PMD.ref(pm, nw, :meas, i, "var") cmp_id = _PMD.ref(pm, nw, :meas, i, "cmp_id") cmp_type = _PMD.ref(pm, nw, :meas, i, "cmp") - connections = get_active_connections(pm, nw, cmp_type, cmp_id) + connections = get_active_connections(pm, nw, cmp_type, cmp_id, msr_var) if no_conversion_needed(pm, msr_var) #no additional variable is created, it is already by default in the formulation else @@ -172,3 +172,75 @@ function variable_mc_generator_current_se(pm::_PMD.AbstractUnbalancedIVRModel; n _PMD.variable_mc_generator_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) _PMD.variable_mc_generator_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) end + +# Explicit Neutral related variables + + +"only total current variables defined over the bus_arcs in PMD are considered: with no shunt admittance, these are +equivalent to the series current defined over the branches." +function variable_mc_branch_current(pm::_PMD.IVRENPowerModel; nw::Int=_IM.nw_id_default, bounded::Bool=true, report::Bool=true, kwargs...) + _PMD.variable_mc_branch_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) + _PMD.variable_mc_branch_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) + + # ADD MISSING SERIES CURRENT VARIABLES +end + +function variable_mc_generator_current_se(pm::_PMD.IVRENPowerModel; nw::Int=_IM.nw_id_default, bounded::Bool=true, report::Bool=true, kwargs...) + #NB: the difference with PowerModelsDistributions is that pg and qg expressions are not created + _PMD.variable_mc_generator_current_real(pm, nw=nw, bounded=bounded, report=report; kwargs...) + _PMD.variable_mc_generator_current_imaginary(pm, nw=nw, bounded=bounded, report=report; kwargs...) +end + +""" + variable_mc_load_current, IVR current equivalent of variable_mc_load +""" +function variable_mc_load_current(pm::_PMD.IVRENPowerModel; kwargs...) + variable_mc_load_current_real(pm; kwargs...) + variable_mc_load_current_imag(pm; kwargs...) +end + + +function variable_mc_load_current_real(pm::_PMD.IVRENPowerModel; + nw::Int=_IM.nw_id_default, bounded::Bool=true, report::Bool=true) + + int_dim = Dict(i => _PMD._infer_int_dim_unit(load, false) for (i,load) in _PMD.ref(pm, nw, :load)) + + crd_phases = _PMD.var(pm, nw)[:crd_phases] = Dict(i => JuMP.@variable(pm.model, + [c in 1: int_dim[i]], base_name="$(nw)_crd_$(i)" + #,start = _PMD.comp_start_value(_PMD.ref(pm, nw, :load, i), "crd_start", c, 0.0) + ) for i in _PMD.ids(pm, nw, :load) + ) + + _PMD.var(pm, nw)[:crd] = Dict() + + for i in _PMD.ids(pm, nw, :load) + _PMD.var(pm, nw, :crd)[i] = _PMD._merge_bus_flows(pm, [crd_phases[i]..., -sum(crd_phases[i])], _PMD.ref(pm, nw, :load, i)["connections"]) + end + + crd = _PMD.var(pm, nw, :crd) + + report && _IM.sol_component_value(pm, :pmd, nw, :load, :crd, _PMD.ids(pm, nw, :load), crd) + +end + +function variable_mc_load_current_imag(pm::_PMD.IVRENPowerModel; nw::Int=_IM.nw_id_default, bounded::Bool=true, report::Bool=true, meas_start::Bool=false) + + + int_dim = Dict(i => _PMD._infer_int_dim_unit(load, false) for (i,load) in _PMD.ref(pm, nw, :load)) + + # Note: `cid_phases` is a Dict of variable reference for phases (no neutral) current variables + cid_phases = _PMD.var(pm, nw)[:cid_phases] = Dict(i => JuMP.@variable(pm.model, + [c in 1: int_dim[i]], base_name="$(nw)_cid_$(i)" + #,start = _PMD.comp_start_value(_PMD.ref(pm, nw, :load, i), "cid_start", c, 0.0) + ) for i in _PMD.ids(pm, nw, :load) + ) + _PMD.var(pm, nw)[:cid] = Dict() + + for i in _PMD.ids(pm, nw, :load) + _PMD.var(pm, nw, :cid)[i] = _PMD._merge_bus_flows(pm, [cid_phases[i]..., -sum(cid_phases[i])], _PMD.ref(pm, nw, :load, i)["connections"]) + end + + cid = _PMD.var(pm, nw, :cid) +report && _IM.sol_component_value(pm, :pmd, nw, :load, :cid, _PMD.ids(pm, nw, :load), cid) + +end diff --git a/src/form/reduced_ac.jl b/src/form/reduced_ac.jl index 20f78da..c08c71a 100644 --- a/src/form/reduced_ac.jl +++ b/src/form/reduced_ac.jl @@ -69,8 +69,3 @@ end function variable_mc_bus_voltage(pm::ReducedACRUPowerModel; bounded = true) _PMD.variable_mc_bus_voltage(pm; bounded = bounded) end - -"If the formulation is not reduced, delegates back to PowerModelsDistribution" -function variable_mc_bus_voltage(pm::_PMD.AbstractUnbalancedPowerModel; bounded = true) - _PMD.variable_mc_bus_voltage(pm; bounded = bounded) -end diff --git a/src/io/measurement_parser.jl b/src/io/measurement_parser.jl index 6ca1292..19099ba 100644 --- a/src/io/measurement_parser.jl +++ b/src/io/measurement_parser.jl @@ -6,9 +6,12 @@ # State Estimation. # ################################################################################ +abstract type IndustrialENMeasurementsModel end + + ## CSV to measurement parser function dataString_to_array(input::AbstractString)::Array - if size(split(input, ","))[1] > 1 + if occursin("[", input) && occursin("]", input) rm_brackets = input[2:(end-1)] raw_string = split(rm_brackets, ",") float_array = [parse(Float64, ss) for ss in raw_string] @@ -105,6 +108,11 @@ function get_measures(model::DataType, cmp_type::String) if cmp_type == "bus" return ["vm"] end if cmp_type == "gen" return ["pg","qg"] end if cmp_type == "load" return ["pd","qd"] end + elseif model <: _PMD.IVRENPowerModel + # NB: EN IVR AbstractExplicitNeutralIVRModel is a subtype of ACR, therefore it should preceed ACR and superceeds IVRENPowerModel (maybe and IVRReducedENPowerModel) + if cmp_type == "bus" return ["vr","vi"] end + if cmp_type == "gen" return ["crg","cig"] end + if cmp_type == "load" return ["crd","cid"] end #no cxd_bus elseif model <: _PMD.AbstractUnbalancedIVRModel # NB: IVR is a subtype of ACR, therefore it should preceed ACR if cmp_type == "bus" return ["vr","vi"] end @@ -122,6 +130,17 @@ function get_measures(model::DataType, cmp_type::String) if cmp_type == "bus" return ["w"] end if cmp_type == "gen" return ["pg","qg"] end if cmp_type == "load" return ["pd","qd"] end + elseif model <: IndustrialENMeasurementsModel + if cmp_type == "bus" return ["vmn"] end + #if cmp_type == "bus" return ["vr","vi"] end + if cmp_type == "bus-Δ" return ["vll"] end + #if cmp_type == "bus-Δ" return ["vr","vi"] end + #if cmp_type == "bus-Δ" return ["vmn"] end + if cmp_type == "load-Δ" return ["ptot","qtot"] end + #if cmp_type == "load-Δ" return ["pd","qd"] end + #if cmp_type == "load" return ["pd","qd"] end + if cmp_type == "gen" return ["pg","qg"] end + #if cmp_type == "gen-Δ" return ["pg","qg"] end #doesn't happen -for now- but for completeness end return [] end @@ -146,29 +165,30 @@ init_measurements() = function write_cmp_measurement!(df::_DFS.DataFrame, model::Type, cmp_id::String, cmp_type::String, cmp_data::Dict{String,Any}, cmp_res::Dict{String,Any}, phases, very_basic_case::Bool; exclude::Vector{String}=String[], σ::Float64) - if length(phases) == 1 - phases = phases[1] - ph = 1 - else - ph = [1,2,3] - end + ph = [1:length(phases)...] if !repeated_measurement(df, cmp_id, cmp_type, phases) config = get_configuration(cmp_type, cmp_data) - for meas_var in get_measures(model, cmp_type) if !(meas_var in exclude) - par_2 = very_basic_case ? string(get_sigma(σ, meas_var,phases)) : string(length(phases) == 1 ? σ : σ.*ones(length(phases))) + for meas_var in get_measures(model, cmp_type) + if !(meas_var in exclude) + par_1 = meas_var == "ptot" || meas_var == "qtot" ? string(cmp_res[meas_var][1]) : string(cmp_res[meas_var][ph]) + par_2 = meas_var == "ptot" || meas_var == "qtot" ? string(get_sigma(σ, meas_var,1)) : string(get_sigma(σ, meas_var,phases)) push!(df, [length(df.meas_id)+1, # meas_id - cmp_type, # cmp_type + split(cmp_type,"-")[1], # cmp_type cmp_id, # cmp_id config, # config reduce_name(meas_var), # meas_var string(phases), # phase "Normal", # dst - string(cmp_res[meas_var][ph]), # par_1 + par_1, # par_1 par_2, # par_2 missing, missing, missing # par_3,4,crit ]) - end end end + end + end + else + @warn "Measurement for $cmp_type[$cmp_id] already present in the dataframe" + end end function write_cmp_measurements!(df::_DFS.DataFrame, model::Type, cmp_type::String, data::Dict{String,Any}, pf_results::Dict{String,Any}, very_basic_case::Bool; @@ -176,15 +196,28 @@ function write_cmp_measurements!(df::_DFS.DataFrame, model::Type, cmp_type::Stri for (cmp_id, cmp_res) in pf_results["solution"][cmp_type] # write the properties for the component cmp_data = data[cmp_type][cmp_id] - phases = cmp_data["connections"] + phases = setdiff(cmp_data["connections"],[_N_IDX]) σ_cmp = isa(σ, Dict) ? σ[cmp_type] : σ - write_cmp_measurement!(df, model, cmp_id, cmp_type, cmp_data, cmp_res, phases, very_basic_case, exclude = exclude, σ = σ_cmp) - # write the properties for its bus - cmp_id = string(cmp_data["$(cmp_type)_bus"]) - cmp_data = data["bus"][cmp_id] - cmp_res = pf_results["solution"]["bus"][cmp_id] - σ_cmp = isa(σ, Dict) ? σ["bus"] : σ - write_cmp_measurement!(df, model, cmp_id, "bus", cmp_data, cmp_res, cmp_data["terminals"], very_basic_case, exclude = exclude, σ = σ_cmp) + + if cmp_data["configuration"] == _PMD.WYE + write_cmp_measurement!(df, model, cmp_id, cmp_type, cmp_data, cmp_res, phases, very_basic_case, exclude = exclude, σ = σ_cmp) + # write the properties for its bus + cmp_id = string(cmp_data["$(cmp_type)_bus"]) + cmp_data = data["bus"][cmp_id] + cmp_res = pf_results["solution"]["bus"][cmp_id] + σ_cmp = isa(σ, Dict) ? σ["bus"] : σ + write_cmp_measurement!(df, model, cmp_id, "bus", cmp_data, cmp_res, cmp_data["terminals"], very_basic_case, exclude = exclude, σ = σ_cmp) + elseif cmp_data["configuration"] == _PMD.DELTA + write_cmp_measurement!(df, model, cmp_id, "$cmp_type-Δ", cmp_data, cmp_res, phases, very_basic_case, exclude = exclude, σ = σ_cmp) + # write the properties for its bus + cmp_id = string(cmp_data["$(cmp_type)_bus"]) + cmp_data = data["bus"][cmp_id] + cmp_res = pf_results["solution"]["bus"][cmp_id] + σ_cmp = isa(σ, Dict) ? σ["bus"] : σ + write_cmp_measurement!(df, model, cmp_id, "bus-Δ", cmp_data, cmp_res, cmp_data["terminals"], very_basic_case, exclude = exclude, σ = σ_cmp) + else + error("Configuration of $cmp_data[$cmp_id] not recognized") + end end end """ diff --git a/src/prob/se.jl b/src/prob/se.jl index 59b187b..4777bf3 100644 --- a/src/prob/se.jl +++ b/src/prob/se.jl @@ -65,7 +65,7 @@ end function build_mc_se(pm::_PMD.AbstractUnbalancedPowerModel) # Variables - _PMDSE.variable_mc_bus_voltage(pm; bounded = true) + _PMDSE.variable_mc_bus_voltage(pm, bounded = true) _PMD.variable_mc_branch_power(pm; bounded = true) _PMD.variable_mc_transformer_power(pm; bounded = true) _PMD.variable_mc_generator_power(pm; bounded = true) @@ -105,7 +105,7 @@ end function build_mc_se(pm::_PMD.AbstractUnbalancedIVRModel) # Variables - _PMD.variable_mc_bus_voltage(pm, bounded = true) + _PMDSE.variable_mc_bus_voltage(pm, bounded = true) _PMDSE.variable_mc_branch_current(pm, bounded = true) variable_mc_generator_current_se(pm, bounded = true) _PMD.variable_mc_transformer_current(pm, bounded = false) diff --git a/src/prob/se_en.jl b/src/prob/se_en.jl new file mode 100644 index 0000000..5a634b1 --- /dev/null +++ b/src/prob/se_en.jl @@ -0,0 +1,54 @@ +"solves state estimation in current and voltage rectangular coordinates for an explicit neutral model (IVREN formulation)" +function solve_ivr_en_mc_se(data::Union{Dict{String,<:Any},String}, solver; kwargs...) + return solve_mc_se(data, _PMD.IVRENPowerModel, solver; kwargs...) +end + + +##################################################################### +###################### Optimization Problem Formulation ############## +##################################################################### + +"specification of the state estimation problem for the IVR Flow formulation" +function build_mc_se(pm::_PMD.IVRENPowerModel) + # Variables + _PMD.variable_mc_bus_voltage(pm, bounded = true) + _PMD.variable_mc_branch_current(pm, bounded = true) + variable_mc_load_current(pm, bounded = true) + _PMD.variable_mc_generator_current(pm, bounded = true) + _PMD.variable_mc_transformer_current(pm, bounded = true) + variable_mc_residual(pm, bounded = true) + variable_mc_measurement(pm, bounded = false) + + # Constraints + + for i in _PMD.ids(pm, :bus) + if i in _PMD.ids(pm, :ref_buses) + _PMD.constraint_mc_voltage_reference(pm, i) # vm is not fixed + end + _PMD.constraint_mc_voltage_absolute(pm, i) + _PMD.constraint_mc_voltage_pairwise(pm, i) + end + + for i in _PMD.ids(pm, :transformer) + _PMD.constraint_mc_transformer_voltage(pm, i) + _PMD.constraint_mc_transformer_current(pm, i) + end + + + for i in _PMD.ids(pm, :branch) + _PMD.constraint_mc_current_from(pm, i) + _PMD.constraint_mc_current_to(pm, i) + _PMD.constraint_mc_bus_voltage_drop(pm, i) + end + + for (i,bus) in _PMD.ref(pm, :bus) + constraint_mc_current_balance_se(pm, i) + end + + for (i,meas) in _PMD.ref(pm, :meas) + constraint_mc_residual(pm, i) + end + + + objective_mc_se(pm) +end \ No newline at end of file diff --git a/test/bad_data.jl b/test/bad_data.jl index 4e7c47c..7b0bd88 100644 --- a/test/bad_data.jl +++ b/test/bad_data.jl @@ -3,7 +3,7 @@ msr_path = joinpath(mktempdir(),"temp.csv") custom_solver = _PMDSE.optimizer_with_attributes(Ipopt.Optimizer,"max_cpu_time"=>300.0, - "tol"=>1e-12, + "tol"=>1e-5, "print_level"=>0, "mu_strategy"=>"adaptive") data = _PMD.parse_file(_PMDSE.get_enwl_dss_path(ntw, fdr)) @@ -48,14 +48,14 @@ chi_result = _PMDSE.exceeds_chi_squares_threshold(se_result, data) @test _PMDSE.get_degrees_of_freedom(data) == 34 @test chi_result[1] == false - @test isapprox(chi_result[2], 0.0, atol = 1e-7) + @test isapprox(chi_result[2],0.3017354, atol = 1e-4) @test isapprox(chi_result[3], 48.60, atol = 1e-2) _PMDSE.add_measurements!(data, msr_path, actual_meas = false) se_result = _PMDSE.solve_acp_red_mc_se(data, custom_solver) chi_result = _PMDSE.exceeds_chi_squares_threshold(se_result, data) @test chi_result[1] == true #TODO: there's no real bad data, check whether there is a problem with write_meas - @test isapprox(chi_result[2], 963.32, atol = 1e-2) + @test isapprox(chi_result[2],64.9021184, atol = 1e-2) @test isapprox(chi_result[3], 48.60, atol = 1e-2) end diff --git a/test/data/three-bus-en-models/3bus_3wire.dss b/test/data/three-bus-en-models/3bus_3wire.dss new file mode 100644 index 0000000..c9681c2 --- /dev/null +++ b/test/data/three-bus-en-models/3bus_3wire.dss @@ -0,0 +1,39 @@ +Clear +Set DefaultBaseFreq=50 +New Circuit.3Bus_example +! define a really not stiff source +~ basekv=0.4 pu=1.0 MVAsc1=1e6 MVAsc3=1e6 basemva=0.5 + +!Define Linecodes + + +New linecode.556MCM nphases=3 basefreq=50 ! ohms per 5 mile +~ rmatrix = ( 0.1000 | 0.0400 0.1000 | 0.0400 0.0400 0.1000) +~ xmatrix = ( 0.0583 | 0.0233 0.0583 | 0.0233 0.0233 0.0583) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 ) ! small capacitance + + +New linecode.4/0QUAD nphases=3 basefreq=50 ! ohms per 100ft +~ rmatrix = ( 0.1167 | 0.0467 0.1167 | 0.0467 0.0467 0.1167) +~ xmatrix = (0.0667 | 0.0267 0.0667 | 0.0267 0.0267 0.0667 ) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 ) ! small capacitance + +!Define lines + +New Line.OHLine bus1=sourcebus.1.2.3 Primary.1.2.3 linecode = 556MCM length=1 ! 5 mile line +New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=1 ! 100 ft + +!Loads - single phase + + +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=12 kvar=3 model=1 +New Load.L3 phases=1 loadbus.3.0 ( 0.4 3 sqrt / ) kW=9 kvar=3 model=1 + + +Set voltagebases=[0.4] +Set tolerance=0.000001 +set defaultbasefreq=50 +Calcvoltagebases + +Solve \ No newline at end of file diff --git a/test/data/three-bus-en-models/3bus_3wire_delta_single.dss b/test/data/three-bus-en-models/3bus_3wire_delta_single.dss new file mode 100644 index 0000000..a167f77 --- /dev/null +++ b/test/data/three-bus-en-models/3bus_3wire_delta_single.dss @@ -0,0 +1,36 @@ +Clear +Set DefaultBaseFreq=50 +New Circuit.3Bus_example +! define a really not stiff source +~ basekv=0.4 pu=1.0 MVAsc1=1e6 MVAsc3=1e6 basemva=0.5 + +!Define Linecodes + + +New linecode.556MCM nphases=3 basefreq=50 ! ohms per 5 mile +~ rmatrix = ( 0.1000 | 0.0400 0.1000 | 0.0400 0.0400 0.1000) +~ xmatrix = ( 0.0583 | 0.0233 0.0583 | 0.0233 0.0233 0.0583) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 ) ! small capacitance + + +New linecode.4/0QUAD nphases=3 basefreq=50 ! ohms per 100ft +~ rmatrix = ( 0.1167 | 0.0467 0.1167 | 0.0467 0.0467 0.1167) +~ xmatrix = (0.0667 | 0.0267 0.0667 | 0.0267 0.0267 0.0667 ) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 ) ! small capacitance + +!Define lines + +New Line.OHLine bus1=sourcebus.1.2.3 Primary.1.2.3 linecode = 556MCM length=1 ! 5 mile line +New Line.Quad Bus1=Primary.1.2.3 loadbus.1.2.3 linecode = 4/0QUAD length=1 ! 100 ft + +!Loads - single phase + +New Load.L12 phases=1 conn=delta bus1=loadbus.1.2 kv=0.4 model=1 kVA=10 pf=0.9 vminpu=0.1 vmaxpu=2 + + +Set voltagebases=[0.4] +Set tolerance=0.000001 +set defaultbasefreq=50 +Calcvoltagebases + +Solve \ No newline at end of file diff --git a/test/data/three-bus-en-models/3bus_4wire.dss b/test/data/three-bus-en-models/3bus_4wire.dss new file mode 100644 index 0000000..8e12f46 --- /dev/null +++ b/test/data/three-bus-en-models/3bus_4wire.dss @@ -0,0 +1,43 @@ +Clear +Set DefaultBaseFreq=50 +New Circuit.3Bus_4wire +! define a really stiff source +~ basekv=0.4 pu=1.0 MVAsc1=1e6 MVAsc3=1e6 basemva=0.5 + +!Define Linecodes + +New linecode.556MCM nphases=4 basefreq=50 ! ohms per 5 mile +~ rmatrix = ( 0.1000 | 0.0400 0.1000 | 0.0400 0.0400 0.1000 | 0.0400 0.0400 0.0400 0.1000) +~ xmatrix = ( 0.0583 | 0.0233 0.0583 | 0.0233 0.0233 0.0583 | 0.0233 0.0233 0.0233 0.0583) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 | -0 -0 -0 50.92958178940651 ) ! small capacitance + +New linecode.4/0QUAD nphases=4 basefreq=50 ! ohms per 100ft +~ rmatrix = ( 0.1167 | 0.0467 0.1167 | 0.0467 0.0467 0.1167 | 0.0467 0.0467 0.0467 0.1167) +~ xmatrix = (0.0667 | 0.0267 0.0667 | 0.0267 0.0267 0.0667 | 0.0267 0.0267 0.0267 0.0667 ) +~ cmatrix = (50.92958178940651 | -0 50.92958178940651 | -0 -0 50.92958178940651 | -0 -0 -0 50.92958178940651 ) ! small capacitance + + +!Define lines + +New Line.OHLine bus1=sourcebus.1.2.3.0 Primary.1.2.3.4 linecode = 556MCM length=1 ! 5 mile line +New Line.Quad Bus1=Primary.1.2.3.4 loadbus.1.2.3.4 linecode = 4/0QUAD length=1 ! 100 ft + +! New Load.L4 phases=1 Primary.2.4 ( 0.4 3 sqrt / ) kW=3 kvar=1 model=1 + + +// !Loads - single phase +! New Load.ThreeL phases=3 loadbus.1.2.3.4 kW=18 kvar=0 model=1 conn= wye +New Load.ThreeL2 phases=3 loadbus.1.2.3.4 kW=9 kvar=0 model=1 conn= wye + +New Load.L1 phases=1 loadbus.1.4 ( 0.4 3 sqrt / ) kW=6 kvar=3 model=1 +New Load.L2 phases=1 loadbus.2.4 ( 0.4 3 sqrt / ) kW=12 kvar=3 model=1 +New Load.L3 phases=1 loadbus.3.4 ( 0.4 3 sqrt / ) kW=9 kvar=3 model=1 + + + +Set voltagebases=[0.4] +Set tolerance=0.000001 +set defaultbasefreq=50 +Calcvoltagebases + +Solve \ No newline at end of file diff --git a/test/estimation_criteria.jl b/test/estimation_criteria.jl index 2fbb0c1..33a617a 100644 --- a/test/estimation_criteria.jl +++ b/test/estimation_criteria.jl @@ -86,14 +86,14 @@ se_result_mle = _PMDSE.solve_acr_red_mc_se(data, custom_solver) delta, max_err_mle, avg_mle = _PMDSE.calculate_voltage_magnitude_error(se_result_mle, pf_result) - @test se_result_mle["termination_status"] == _PMDSE.LOCALLY_SOLVED - @test isapprox(abs(max_err-max_err_mle), 0.0; atol = 1e-4) - @test isapprox(abs(avg-avg_mle), 0.0; atol = 1e-5) + @test se_result_mle["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + @test isapprox(abs(max_err-max_err_mle), 0.0; atol = 1e-3) + @test isapprox(abs(avg-avg_mle), 0.0; atol = 1e-3) end @testset "Mixed mle/rwlav criterion - with error" begin - data["se_settings"] = Dict{String,Any}("criterion" => "rwls", "rescaler" => rescaler) + data["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => rescaler) se_result_rwls = _PMDSE.solve_acp_red_mc_se(data, custom_solver) delta, max_err, avg = _PMDSE.calculate_voltage_magnitude_error(se_result_rwls, pf_result) @@ -105,11 +105,12 @@ _PMDSE.assign_basic_individual_criteria!(data["meas"][m]; chosen_criterion="rwls") end end + se_result_mixed = _PMDSE.solve_acp_red_mc_se(data, custom_solver) delta, max_err_mixed, avg_mixed = _PMDSE.calculate_voltage_magnitude_error(se_result_mixed, pf_result) - @test se_result_mixed["termination_status"] == _PMDSE.LOCALLY_SOLVED - @test isapprox(abs(max_err-max_err_mixed), 0.0; atol = 3e-4) - @test isapprox(abs(avg-avg_mixed), 0.0; atol = 6e-5) + @test se_result_mixed["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + @test isapprox(abs(max_err-max_err_mixed), 0.0; atol =1e-3) + @test isapprox(abs(avg-avg_mixed), 0.0; atol = 1e-4) end end diff --git a/test/ivren.jl b/test/ivren.jl new file mode 100644 index 0000000..f7d961e --- /dev/null +++ b/test/ivren.jl @@ -0,0 +1,122 @@ +@testset "IVR three-wire model" begin + #3 WIRES MODEL to check nothing broke there when implementing four-wire models + PF_msr_path = joinpath(mktempdir(),"PF_msr_path.csv") + σ_3w = 0.05 + dss_file_name = "3bus_3wire.dss" + + eng = _PMD.parse_file(joinpath(_PMDSE.BASE_DIR, "test", "data", "three-bus-en-models", dss_file_name)) + _PMD.remove_all_bounds!(eng) + math = _PMD.transform_data_model(eng) + _PMD.add_start_vrvi!(math) + pf_res = _PMD.solve_mc_opf(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + pf_sol = pf_res["solution"] + + + _PMDSE.write_measurements!(_PMD.IVRUPowerModel, math, pf_res, PF_msr_path, σ= σ_3w) + _PMDSE.add_measurements!(math, PF_msr_path, actual_meas = true) + + math["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1); + SE= _PMDSE.solve_mc_se(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + se_sol = SE["solution"] + @test isapprox(SE["objective"], 0.0; atol = 1e-4) +end + + +@testset "IVR three-wire - SM_ind measurements " begin + #3 WIRES MODEL to check nothing broke there when implementing four-wire models + PF_msr_path = joinpath(mktempdir(),"PF_msr_path.csv") + σ_3w = 0.05 + dss_file_name = "3bus_3wire_delta_single.dss" + eng = _PMD.parse_file(joinpath(_PMDSE.BASE_DIR, "test", "data", "three-bus-en-models", dss_file_name)) + _PMD.remove_all_bounds!(eng) + math = _PMD.transform_data_model(eng) + _PMD.add_start_vrvi!(math) + + pf_res = _PMD.solve_mc_opf(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + pf_sol = pf_res["solution"] + + for (b,bus) in pf_sol["bus"] + bus["vmn2"] = [] + for t in math["bus"][b]["terminals"] + push!(bus["vmn2"], (bus["vr"][t])^2 + (bus["vi"][t])^2) + end + bus["vmn"] = sqrt.(bus["vmn2"]) + end + + for (l, load) in math["load"] + if load["configuration"] == _PMD.DELTA + pf_sol["load"][l]["ptot"] = [sum(pf_sol["load"][l]["pd"])] + pf_sol["load"][l]["qtot"] = [sum(pf_sol["load"][l]["qd"])] + + load_bus = pf_sol["bus"][string(load["load_bus"])] + load_bus["vll"] = sqrt.([ (load_bus["vr"][x] - load_bus["vr"][y] )^2 + (load_bus["vi"][x] - load_bus["vi"][y] )^2 for (x,y) in [(1,2), (2,3), (3,1)]]) + end + end + + PF_msr_path_SM_ind_Models = joinpath(mktempdir(),"PF_msr_path_IndustrialENMeasurementsModel.csv") + _PMDSE.write_measurements!(_PMDSE.IndustrialENMeasurementsModel, math, pf_res, PF_msr_path_SM_ind_Models, σ=0.005) + _PMDSE.add_measurements!(math, PF_msr_path_SM_ind_Models, actual_meas = true) + + math["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1); + SE= _PMDSE.solve_mc_se(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + se_sol = SE["solution"] + + @test isapprox(SE["objective"], 0.0; atol = 1e-4) + +end + + + + +@testset "Explicit Neutral Models" begin + + PF_msr_path_en = joinpath(mktempdir(),"PF_msr_path_en.csv") + + @testset "IVR measurements - Single Phase Y Load - Constant Power" begin + # dss_file_name = "3bus_4wire.dss" + dss_file_name = "test_load_3ph_wye_cp.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMD.IVRENPowerModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + @testset "IVR measurements - Single Phase Δ Load - Constant Power" begin + dss_file_name = "test_load_1ph_delta_cp.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMD.IVRENPowerModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + @testset "IVR measurements - Single Phase Y Load - Constant Impedance" begin + dss_file_name = "test_load_3ph_delta_cz.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMD.IVRENPowerModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + @testset "SM_ind_en measurements - Single Phase Y Load - Constant Power" begin + dss_file_name = "test_load_1ph_wye_cp.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMDSE.IndustrialENMeasurementsModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + @testset "SM_ind_en measurements - Single Phase Δ Load - Constant Power" begin + dss_file_name = "test_load_1ph_delta_cp.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMDSE.IndustrialENMeasurementsModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + @testset "SM_ind_en measurements - Three Phase Δ Load - Constant Power" begin + dss_file_name = "test_load_3ph_delta_cp.dss" + SE_en = run_en_model(dss_file_name, PF_msr_path_en,_PMDSE.IndustrialENMeasurementsModel) + @test isapprox(SE_en["objective"], 0.0; atol = 1e-4) + @test SE_en["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + + +end + + diff --git a/test/reference_angles.jl b/test/reference_angles.jl new file mode 100644 index 0000000..2393da4 --- /dev/null +++ b/test/reference_angles.jl @@ -0,0 +1,150 @@ +@testset "Angular Reference Models" begin + + # set measurement path + msr_path = joinpath(mktempdir(),"temp.csv") + + @testset "ACP-rwlav" begin + + # set model + crit = "rwlav" + model = _PMD.ACPUPowerModel + + # load data + data = _PMD.parse_file(_PMDSE.get_enwl_dss_path(ntw, fdr)) + if rm_transfo _PMDSE.rm_enwl_transformer!(data) end + + + # insert the load profiles + #_PMDSE.insert_profiles!(data, season, elm, pfs, t = time_step) + + # transform data model + data = _PMD.transform_data_model(data); + pf_result = _PMD.solve_mc_pf(data, model, ipopt_solver) + _PMDSE.write_measurements!(model, data, pf_result, msr_path) + _PMDSE.add_measurements!(data, msr_path, actual_meas = true) + + datarm = deepcopy(data) + (virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(datarm) # doesn't remove, but just returns the ids needed later + + + for (m, meas) in data["meas"] + if meas["cmp"] == :bus && meas["cmp_id"] == parse(Int, virtual_bus) + meas["cmp_id"] = r_new + if meas["var"] == :vm + meas["dst"] = [_DST.Normal(vm_i, 0.00166667) for vm_i in pf_result["solution"]["bus"]["$(r_new)"]["vm"]] + end + end + end + + + _PMDSE.assign_start_to_variables!(data) + #_PMDSE.update_all_bounds!(data; v_min = 0.8, v_max = 1.2, pg_min=-1.0, pg_max = 1.0, qg_min=-1.0, qg_max=1.0, pd_min=-1.0, pd_max=1.0, qd_min=-1.0, qd_max=1.0 ) + + # set se settings + data["se_settings"] = Dict{String,Any}("criterion" => crit, "rescaler" => 100) + + + # Test 1: State Estimation Approaches Feasibility and Low Objective + # Approach A + + math_a = deepcopy(data) + (virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(math_a) + math_a["bus"]["$(r_new)"]["va"] = deg2rad.([0, -120, 120]) + math_a["bus"]["$(r_new)"]["bus_type"] = 3 + SE_a = _PMDSE.solve_mc_se(math_a, model, ipopt_solver) + + @testset "State Estimation Approach A" begin + @test isapprox(SE_a["objective"], 0.0; atol = 1e-4) + @test SE_a["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + # Approach B + math_b = deepcopy(data) + (virtual_bus, r_new) = _PMDSE.remove_virtual_bus!(math_b) + Angles_bound = Inf + math_b["bus"]["$(r_new)"]["bus_type"] = 3 + math_b["bus"]["$(r_new)"]["vamax"] = deg2rad.([0.0, -120+Angles_bound, 120+Angles_bound]) + math_b["bus"]["$(r_new)"]["vamin"] = deg2rad.([0.0, -120-Angles_bound, 120-Angles_bound]) + SE_b = _PMDSE.solve_mc_se(math_b, model, ipopt_solver) + + @testset "State Estimation Approach B" begin + @test isapprox(SE_b["objective"], 0.0; atol = 1e-6) + @test SE_b["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + # Approach C + math_c = deepcopy(data) + SE_c = _PMDSE.solve_mc_se(math_c, model, ipopt_solver) + + @testset "State Estimation Approach C" begin + @test isapprox(SE_c["objective"], 0.0; atol = 1e-6) + @test SE_c["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + delta, max, avg = _PMDSE.calculate_voltage_magnitude_error(SE_c, pf_result) + @test isapprox(max, 0.0; atol = 1e-6) + @test isapprox(avg, 0.0; atol = 1e-8) + end + end + + + @testset "IVR-rwlav" begin + + #3 WIRES + PF_msr_path = joinpath(mktempdir(),"PF_msr_path.csv") + σ_3w = 0.05 + dss_file_name = "3bus_3wire.dss" + + eng = _PMD.parse_file(joinpath(_PMDSE.BASE_DIR, "test", "data", "three-bus-en-models", dss_file_name)) + _PMD.remove_all_bounds!(eng) + math = _PMD.transform_data_model(eng) + _PMD.add_start_vrvi!(math) + + #PF_RES = solve_mc_pf(math, PowerModelsDistribution.IVRUPowerModel, Ipopt.Optimizer) + PF_RES = _PMD.solve_mc_opf(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + pf_sol = PF_RES["solution"] + + _PMDSE.write_measurements!(_PMD.IVRUPowerModel, math, PF_RES, PF_msr_path, σ= σ_3w) + _PMDSE.add_measurements!(math, PF_msr_path, actual_meas = true) + + math["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1); + SE= _PMDSE.solve_mc_se(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + se_sol = SE["solution"] + + + @test SE["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + @test isapprox( atan.(pf_sol["bus"]["3"]["vi"],pf_sol["bus"]["3"]["vr"]), atan.(se_sol["bus"]["3"]["vi"],se_sol["bus"]["3"]["vr"]) ;atol= 1e-4) + + + + eng = _PMD.parse_file(joinpath(_PMDSE.BASE_DIR, "test", "data", "three-bus-en-models", dss_file_name)) + _PMD.remove_all_bounds!(eng) + math = _PMD.transform_data_model(eng) + _PMD.add_start_vrvi!(math) + + math["bus"]["3"]["vamax"] = deg2rad.([0.0, -119, 119.15]) + math["bus"]["3"]["vamin"] = deg2rad.([0.0, -120.3833, 119.15]) + + #PF_RES = solve_mc_pf(math, PowerModelsDistribution.IVRUPowerModel, Ipopt.Optimizer) + PF_RES = _PMD.solve_mc_opf(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + pf_sol = PF_RES["solution"] + + _PMDSE.write_measurements!(_PMD.IVRUPowerModel, math, PF_RES, PF_msr_path, σ= σ_3w) + _PMDSE.add_measurements!(math, PF_msr_path, actual_meas = true) + + math["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1); + SE= _PMDSE.solve_mc_se(math, _PMD.IVRUPowerModel, Ipopt.Optimizer) + se_sol = SE["solution"] + + atan.(pf_sol["bus"]["3"]["vi"],pf_sol["bus"]["3"]["vr"]) + + + @test isapprox(atan.(se_sol["bus"]["3"]["vi"],se_sol["bus"]["3"]["vr"]), deg2rad.([0.0, -120.3833, 119.15]) ;atol= 1e-4) + + @test SE["termination_status"] ∈ [_PMDSE.LOCALLY_SOLVED, _PMDSE.ALMOST_LOCALLY_SOLVED] + end + + + + +end + + diff --git a/test/runtests.jl b/test/runtests.jl index 5c17680..9d00b72 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,7 +16,6 @@ import Polynomials as _Poly import PowerModels import PowerModelsDistribution as _PMD import Statistics -#import SCS #removed while SDP tests are not active using Test #network and feeder from ENWL for tests @@ -36,23 +35,22 @@ ipopt_solver = _PMDSE.optimizer_with_attributes(Ipopt.Optimizer,"max_cpu_time" = "print_level" => 0, "mu_strategy" => "adaptive") -# scs_solver = _PMDSE.optimizer_with_attributes(SCS.Optimizer, "max_iters"=>20000, "eps"=>1e-5, -# "alpha"=>0.4, "verbose"=>0) #deactivated while SDP tests not active - @testset "PowerModelsDistributionStateEstimation" begin - + include("test_utils.jl") include("bad_data.jl") include("distributions.jl") include("estimation_criteria.jl") + include("ivren.jl") include("mixed_measurements.jl") include("non_exact_forms.jl") include("power_flow.jl") include("pseudo_measurements.jl") + include("reference_angles.jl") include("single_conductor_branches.jl") include("utils_and_start_val.jl") include("with_errors.jl") - end + ambiguities = Test.detect_ambiguities(_PMDSE); if !isempty(ambiguities) println("ambiguities detected: $ambiguities") diff --git a/test/test_utils.jl b/test/test_utils.jl new file mode 100644 index 0000000..1f247db --- /dev/null +++ b/test/test_utils.jl @@ -0,0 +1,51 @@ +function run_en_model(dss_file_name, PF_msr_path_en, measures_type; σ_en=0.005) + + eng_en = _PMD.parse_file(joinpath(dirname(pathof(_PMD)),"..", "test", "data", "en_validation_case_data", dss_file_name)) + + + + _PMD.transform_loops!(eng_en) + _PMD.remove_all_bounds!(eng_en) + math_en = _PMD.transform_data_model(eng_en, kron_reduce=false, phase_project=false) + _PMD.add_start_vrvi!(math_en) + PF_RES_en = _PMD.solve_mc_opf(math_en, _PMD.IVRENPowerModel, Ipopt.Optimizer) + pf_sol_en = PF_RES_en["solution"] + + if measures_type == _PMD.IVRENPowerModel + _PMDSE.write_measurements!(measures_type, math_en, PF_RES_en, PF_msr_path_en, σ=σ_en) + _PMDSE.add_measurements!(math_en, PF_msr_path_en, actual_meas=true) + elseif measures_type == _PMDSE.IndustrialENMeasurementsModel + math_meas = deepcopy(math_en) + + for (b,bus) in pf_sol_en["bus"] + bus["vmn2"] = [] + for t in setdiff(math_en["bus"][b]["terminals"],[4]) + push!(bus["vmn2"], (bus["vr"][t] - bus["vr"][4])^2 + (bus["vi"][t] - bus["vi"][4])^2) + end + bus["vmn"] = sqrt.(bus["vmn2"]) + math_meas["bus"][b]["terminals"] = setdiff(math_en["bus"][b]["terminals"], 4) + end + + for (l, load) in math_en["load"] + if load["configuration"] == _PMD.DELTA + pf_sol_en["load"][l]["ptot"] = [sum(pf_sol_en["load"][l]["pd"])] + pf_sol_en["load"][l]["qtot"] = [sum(pf_sol_en["load"][l]["qd"])] + + load_bus = pf_sol_en["bus"][string(load["load_bus"])] + load_bus["vll"] = sqrt.([ (load_bus["vr"][x] - load_bus["vr"][y] )^2 + (load_bus["vi"][x] - load_bus["vi"][y] )^2 for (x,y) in [(1,2), (2,3), (3,1)]]) + end + end + + PF_msr_path_IndustrialENMeasurementsModel = joinpath(mktempdir(),"PF_msr_path_IndustrialENMeasurementsModel.csv") + _PMDSE.write_measurements!(measures_type, math_meas, PF_RES_en, PF_msr_path_IndustrialENMeasurementsModel, σ=σ_en) + _PMDSE.add_measurements!(math_en, PF_msr_path_IndustrialENMeasurementsModel, actual_meas = true) + else + error("Measures type $measures_type not recognized") + end + + math_en["se_settings"] = Dict{String,Any}("criterion" => "rwlav", "rescaler" => 1) + slv = _PMDSE.optimizer_with_attributes(Ipopt.Optimizer, "tol"=>1e-8, "print_level"=>5) + SE_en = _PMDSE.solve_ivr_en_mc_se(math_en, slv) + + return SE_en +end \ No newline at end of file diff --git a/test/with_errors.jl b/test/with_errors.jl index 1e01da2..4c809f5 100644 --- a/test/with_errors.jl +++ b/test/with_errors.jl @@ -107,16 +107,23 @@ # solve the state estimation original_se_result = _PMDSE.solve_mc_se(data, model, ipopt_solver) delta_ref, max_ref, avg_ref = _PMDSE.calculate_voltage_magnitude_error(original_se_result, pf_result) - for data_model in [_PMD.ACPUPowerModel,_PMD.IVRUPowerModel] + + # solve the state estimation _PMD.ACPUPowerModel + se_result = _PMDSE.solve_mc_se(data, _PMD.ACPUPowerModel, ipopt_solver) - # solve the state estimation - se_result = _PMDSE.solve_mc_se(data, data_model, ipopt_solver) + # tests + delta, max, avg = _PMDSE.calculate_voltage_magnitude_error(se_result, pf_result) + @test isapprox(max-max_ref, 0.0; atol = 3e-2) + @test isapprox(avg-avg_ref, 0.0; atol = 3e-2) + + # solve the state estimation _PMD.IVRUPowerModel + se_result = _PMDSE.solve_mc_se(data, _PMD.IVRUPowerModel, ipopt_solver) + + # tests + delta, max, avg = _PMDSE.calculate_voltage_magnitude_error(se_result, pf_result) + @test isapprox(max-max_ref, 0.0; atol = 3e-2) + @test isapprox(avg-avg_ref, 0.0; atol = 3e-2) - # tests - delta, max, avg = _PMDSE.calculate_voltage_magnitude_error(se_result, pf_result) - @test isapprox(max-max_ref, 0.0; atol = 7.8e-3) - @test isapprox(avg-avg_ref, 0.0; atol = 1e-3) - end end @testset "ACR input-WLS" begin # set model