Skip to content

Commit

Permalink
Updated validation to allow for missing power_sigma when both `p_si…
Browse files Browse the repository at this point in the history
…gma` and `q_sigma` exist and are valid

updated test case
updated documentation

Signed-off-by: Jerry Guo <[email protected]>
  • Loading branch information
Jerry-Jinfeng-Guo committed Feb 20, 2024
1 parent c3fb744 commit c9fda67
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 2 deletions.
14 changes: 13 additions & 1 deletion docs/user_manual/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ Because of this distribution, at least one appliance is required to be connected
| name | data type | unit | description | required | update | valid values |
| ------------------------ | ----------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------: | :------: | :--------------------------------------------------: |
| `measured_terminal_type` | {py:class}`MeasuredTerminalType <power_grid_model.enum.MeasuredTerminalType>` | - | indicate if it measures an `appliance` or a `branch` | &#10004; | &#10060; | the terminal type should match the `measured_object` |
| `power_sigma` | `double` | volt-ampere (VA) | standard deviation of the measurement error. Usually this is the absolute measurement error range divided by 3. See {hoverxreftooltip}`user_manual/components:Power Sensor Concrete Types`. | &#10024; only for state estimation. | &#10004; | `> 0` |
| `power_sigma` | `double` | volt-ampere (VA) | standard deviation of the measurement error. Usually this is the absolute measurement error range divided by 3. See {hoverxreftooltip}`user_manual/components:Power Sensor Concrete Types`. | &#10024; only for state estimation. See the explanation below. | &#10004; | `> 0` |

#### Power Sensor Concrete Types

Expand All @@ -615,6 +615,18 @@ the meaning of `RealValueInput` is different, as shown in the table below.
| `p_sigma` | `RealValueInput` | watt (W) | standard deviation of the active power measurement error. Usually this is the absolute measurement error range divided by 3. | &#10060; see the explanation below. | &#10004; | `> 0` |
| `q_sigma` | `RealValueInput` | volt-ampere-reactive (var) | standard deviation of the reactive power measurement error. Usually this is the absolute measurement error range divided by 3. | &#10060; see the explanation below. | &#10004; | `> 0` |

Valid combinations of `power_sigma`, `p_sigma` and `q_sigma` are:
| `power_sigma` | `p_sigma` | `q_sigma` | result |
|:-------------:|:---------:|:---------:|:--------:|
| &#10004; | &#10004; | &#10004; | valid |
| &#10004; | &#10004; | &#10060; | invalid |
| &#10004; | &#10060; | &#10004; | invalid |
| &#10004; | &#10060; | &#10060; | valid |
| &#10060; | &#10004; | &#10004; | valid |
| &#10060; | &#10004; | &#10060; | invalid |
| &#10060; | &#10060; | &#10004; | invalid |
| &#10060; | &#10060; | &#10060; | invalid |

```{note}
1. If both `p_sigma` and `q_sigma` are provided, they represent the standard deviation of the active and reactive power, respectively, and the value of `power_sigma` is ignored. Any infinite component invalidates the entire measurement.
Expand Down
18 changes: 18 additions & 0 deletions src/power_grid_model/validation/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ def validate_ids_exist(update_data: Dict[str, np.ndarray], input_data: SingleDat
return list(chain(*errors))


def process_power_sigma_and_p_q_sigma(data: SingleDataset, sensor: str, required_list: Dict[str, List[str]]) -> None:
"""
Helper function to process the required list when both `p_sigma` and `q_sigma` exist
and valid but `power_sigma` is missing.
"""
if sensor in data and isinstance(data[sensor], np.ndarray):
sensor_data = data[sensor]
if "p_sigma" in sensor_data.dtype.names and "q_sigma" in sensor_data.dtype.names:
p_sigma = sensor_data["p_sigma"]
q_sigma = sensor_data["q_sigma"]
if not (np.isnan(p_sigma).any() or np.isnan(q_sigma).any()):
required_list[sensor].remove("power_sigma")


def validate_required_values(
data: SingleDataset, calculation_type: Optional[CalculationType] = None, symmetric: bool = True
) -> List[MissingValueError]:
Expand Down Expand Up @@ -331,6 +345,10 @@ def validate_required_values(
required["line"] += ["r0", "x0", "c0", "tan0"]
required["shunt"] += ["g0", "b0"]

# Allow missing `power_sigma` of both `p_sigma` and `q_sigma` are present
process_power_sigma_and_p_q_sigma(data, "sym_power_sensor", required)
process_power_sigma_and_p_q_sigma(data, "asym_power_sensor", required)

return list(chain(*(none_missing(data, component, required.get(component, [])) for component in data)))


Expand Down
69 changes: 68 additions & 1 deletion tests/unit/validation/test_validation_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import numpy as np
import pytest

from power_grid_model import MeasuredTerminalType, initialize_array, power_grid_meta_data
# from power_grid_model import PowerGridModel, LoadGenType, MeasuredTerminalType, initialize_array, CalculationType, CalculationMethod
from power_grid_model import CalculationType, LoadGenType, MeasuredTerminalType, initialize_array, power_grid_meta_data
from power_grid_model.enum import CalculationType, FaultPhase, FaultType
from power_grid_model.validation import assert_valid_input_data
from power_grid_model.validation.errors import (
IdNotInDatasetError,
InfinityError,
Expand Down Expand Up @@ -646,3 +648,68 @@ def test_validate_generic_power_sensor__terminal_types(
all_valid_ids.assert_any_call(
ANY, ANY, field=ANY, ref_components=ref_component, measured_terminal_type=measured_terminal_type
)


def test_power_sigma_or_p_q_sigma():
# node
node = initialize_array("input", "node", 2)
node["id"] = np.array([0, 3])
node["u_rated"] = [10.5e3, 10.5e3]

# line
line = initialize_array("input", "line", 1)
line["id"] = [2]
line["from_node"] = [0]
line["to_node"] = [3]
line["from_status"] = [1]
line["to_status"] = [1]
line["r1"] = [0.001]
line["x1"] = [0.02]
line["c1"] = [0.0]
line["tan1"] = [0.0]
line["i_n"] = [1000.0]

# load
sym_load = initialize_array("input", "sym_load", 1)
sym_load["id"] = [4]
sym_load["node"] = [3]
sym_load["status"] = [1]
sym_load["type"] = [LoadGenType.const_power]
sym_load["p_specified"] = [1e6]
sym_load["q_specified"] = [-1e6]

# source
source = initialize_array("input", "source", 1)
source["id"] = [1]
source["node"] = [0]
source["status"] = [1]
source["u_ref"] = [1.0]

# voltage sensor
voltage_sensor = initialize_array("input", "sym_voltage_sensor", 1)
voltage_sensor["id"] = 5
voltage_sensor["measured_object"] = 0
voltage_sensor["u_sigma"] = [100.0]
voltage_sensor["u_measured"] = [10.5e3]

# power sensor
power_sensor = initialize_array("input", "sym_power_sensor", 2)
power_sensor["id"] = [6, 7]
power_sensor["measured_object"] = [2, 4]
power_sensor["measured_terminal_type"] = [MeasuredTerminalType.branch_from, MeasuredTerminalType.load]
power_sensor["p_measured"] = [1e6, -1e6]
power_sensor["q_measured"] = [1e6, -1e6]
power_sensor["p_sigma"] = [1e4, 1e9] # trust P for sensor 6
power_sensor["q_sigma"] = [1e9, 1e4] # trust Q for sensor 7

# all
input_data = {
"node": node,
"line": line,
"sym_load": sym_load,
"source": source,
"sym_voltage_sensor": voltage_sensor,
"sym_power_sensor": power_sensor,
}

assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.state_estimation)

0 comments on commit c9fda67

Please sign in to comment.