Skip to content

Commit b91a928

Browse files
authored
Merge branch 'main' into feature/document-high-level-design
2 parents 9b16de3 + 22d88bc commit b91a928

File tree

10 files changed

+378
-100
lines changed

10 files changed

+378
-100
lines changed

.github/workflows/build-test-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ jobs:
208208
xcode-version: latest-stable
209209

210210
- name: Build wheels
211-
uses: pypa/cibuildwheel@v2.23.2
211+
uses: pypa/cibuildwheel@v2.23.3
212212
# GitHub Actions specific build parameters
213213
env:
214214
# pass GitHub runner info into Linux container

.github/workflows/ci.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,48 @@ jobs:
6161

6262
steps:
6363
- run: echo "ci passed"
64+
65+
publish:
66+
name: Publish to PyPI
67+
runs-on: ubuntu-latest
68+
permissions:
69+
contents: write
70+
id-token: write # Required for Trusted Publishing
71+
needs: build-test-release
72+
if: (github.event_name == 'workflow_dispatch') || github.event_name == 'push'
73+
74+
steps:
75+
- name: Download assets from GitHub release
76+
uses: robinraju/release-downloader@v1
77+
with:
78+
repository: ${{ github.repository }}
79+
# download the latest release
80+
latest: true
81+
# don't download pre-releases
82+
preRelease: false
83+
fileName: "*"
84+
# don't download GitHub-generated source tar and zip files
85+
tarBall: false
86+
zipBall: false
87+
# create a directory to store the downloaded assets
88+
out-file-path: assets-to-publish
89+
# don't extract downloaded files
90+
extract: false
91+
92+
- name: List downloaded assets
93+
run: ls -la assets-to-publish
94+
95+
- name: Upload assets to PyPI
96+
uses: pypa/gh-action-pypi-publish@release/v1
97+
with:
98+
# To test, use the TestPyPI:
99+
# repository-url: https://test.pypi.org/legacy/
100+
# You must also create an account and project on TestPyPI,
101+
# as well as set the trusted-publisher in the project settings:
102+
# https://docs.pypi.org/trusted-publishers/adding-a-publisher/
103+
# To publish to the official PyPI repository, just keep
104+
# repository-url commented out.
105+
packages-dir: assets-to-publish
106+
skip-existing: true
107+
print-hash: true
108+
verbose: true

.github/workflows/publish-pypi.yml

Lines changed: 0 additions & 55 deletions
This file was deleted.

docs/user_manual/calculations.md

Lines changed: 117 additions & 17 deletions
Large diffs are not rendered by default.

docs/user_manual/components.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,117 @@ $$
721721
\end{eqnarray}
722722
$$
723723

724+
### Generic Current Sensor
725+
726+
```{warning}
727+
At the time of writing, this feature is still experimental and is not yet publicly available.
728+
```
729+
730+
```{warning}
731+
At the time of writing, state estimation with current sensors is not supported by the Newton-Raphson calculation method.
732+
```
733+
734+
* type name: `generic_current_sensor`
735+
736+
`current_sensor` is an abstract class for symmetric and asymmetric current sensor and is derived from
737+
{hoverxreftooltip}`user_manual/components:sensor`. It measures the magnitude and angle of the current flow of a terminal.
738+
The terminal is connecting the from/to end of a `branch` (except `link`) and a `node`.
739+
740+
```{note}
741+
Due to the high admittance of a `link` it is chosen that a current sensor cannot be coupled to a `link`, even though a link is a `branch`
742+
```
743+
744+
##### Input
745+
746+
| name | data type | unit | description | required | update | valid values |
747+
| ------------------------ | ----------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- | :------: | :------: | :--------------------------------------------------: |
748+
| `measured_terminal_type` | {py:class}`MeasuredTerminalType <power_grid_model.enum.MeasuredTerminalType>` | - | indicate the side of the `branch` | &#10004; | &#10060; | the terminal type should match the `measured_object` |
749+
| `angle_measurement_type` | {py:class}`AngleMeasurementType <power_grid_model.enum.AngleMeasurementType>` | - | indicate whether the measured angle is a global angle or a local angle; (see the [electric model](#local-angle-current-sensors) below) | &#10004; | &#10060; | |
750+
| `i_sigma` | `double` | ampere (A) | standard deviation of the current (`i`) measurement error. Usually this is the absolute measurement error range divided by 3. | &#10004; | &#10004; | `> 0` |
751+
| `i_angle_sigma` | `double` | rad | standard deviation of the current (`i`) phase angle measurement error. Usually this is the absolute measurement error range divided by 3. | &#10004; | &#10004; | `> 0` |
752+
753+
#### Current Sensor Concrete Types
754+
755+
```{warning}
756+
At the time of writing, this feature is still experimental and is not yet publicly available.
757+
```
758+
759+
```{warning}
760+
At the time of writing, state estimation with current sensors is not supported by the Newton-Raphson calculation method.
761+
```
762+
763+
There are two concrete types of current sensor. They share similar attributes:
764+
the meaning of `RealValueInput` is different, as shown in the table below.
765+
766+
| type name | meaning of `RealValueInput` |
767+
| --------------------- | --------------------------- |
768+
| `sym_current_sensor` | `double` |
769+
| `asym_current_sensor` | `double[3]` |
770+
771+
##### Input
772+
773+
| name | data type | unit | description | required | update |
774+
| ------------------ | ---------------- | ---------- | ----------------------------------------- | :--------------------------------: | :------: |
775+
| `i_measured` | `RealValueInput` | ampere (A) | measured current (`i`) magnitude | &#10024; only for state estimation | &#10004; |
776+
| `i_angle_measured` | `RealValueInput` | rad | measured phase angle of the current (`i`; see the [electric model](#local-angle-current-sensors) below) | &#10024; only for state estimation | &#10004; |
777+
778+
See the documentation on [state estimation calculation methods](calculations.md#state-estimation-algorithms) for details per method on how the variances are taken into account for both the global and local angle measurement types and for the individual phases.
779+
780+
##### Steady state output
781+
782+
```{note}
783+
A sensor only has output for state estimation. For other calculation types, sensor output is undefined.
784+
```
785+
786+
| name | data type | unit | description |
787+
| ------------------ | ----------------- | ---------- | ------------------------------------------------------------------------------------------- |
788+
| `i_residual` | `RealValueOutput` | ampere (A) | residual value between measured current (`i`) and calculated current (`i`) |
789+
| `i_angle_residual` | `RealValueOutput` | rad | residual value between measured phase angle and calculated phase angle of the current (`i`) |
790+
791+
#### Electric Model
792+
793+
`Generic Current Sensor` is modeled by following equations:
794+
795+
##### Global angle current sensors
796+
797+
Current sensors with `angle_measurement_type` equal to `AngleMeasurementType.global_angle` measure the phase of the current relative to
798+
some reference angle that is the same across the entire grid. This reference angle must be the same one as for [voltage phasor measurements](#generic-voltage-sensor). Because the reference point may be ambiguous in the case of current sensor measurements, the power-grid-model imposes the following requirement:
799+
800+
```{note}
801+
Global angle current measurements require at least one voltage angle measurement to make sense.
802+
```
803+
804+
As a sign convention, the angle is the phase shift of the current relative to the reference angle, i.e.,
805+
806+
$$
807+
\underline{I} = \text{i_measured} \cdot e^{j \text{i_angle_measured}} \text{ .}
808+
$$
809+
810+
##### Local angle current sensors
811+
812+
Current sensors with `angle_measurement_type` equal to `AngleMeasurementType.local_angle` measure the phase shift between
813+
the voltage and the current phasor, i.e., $\text{i_angle_measured} = \text{voltage_phase} - \text{current_phase}$.
814+
As a result, the global current phasor depends on the local voltage phase offset and is obtained using the following formula.
815+
816+
$$
817+
\underline{I} = \underline{I}_{\text{local}}^{*} \frac{\underline{U}}{|\underline{U}|} = \text{i_measured} \cdot e^{\mathrm{j} \left(\theta_{U} - \text{i_angle_measured}\right)}
818+
$$
819+
820+
```{note}
821+
As a result, the local angle current sensors have a different sign convention from the global angle current sensors.
822+
```
823+
824+
##### Residuals
825+
826+
$$
827+
\begin{eqnarray}
828+
& i_{\text{residual}} = i_{\text{measured}} - i_{\text{state}} && \\
829+
& i_{\text{angle},\text{residual}} = i_{\text{angle},\text{measured}} - i_{\text{angle},\text{state}} \pmod 2 \pi
830+
\end{eqnarray}
831+
$$
832+
833+
The $\pmod 2\pi$ is handled such that $-\pi \lt i_{\text{angle},\text{residual}} \leq \pi$.
834+
724835
## Fault
725836

726837
* type name: `fault`

power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ class IterationDiverge : public PowerGridError {
137137

138138
class MaxIterationReached : public IterationDiverge {
139139
public:
140-
MaxIterationReached(std::string_view msg = "") {
141-
append_msg(std::format("Maximum number of iterations reached{}\n", msg));
140+
MaxIterationReached(std::string const& msg = "") {
141+
append_msg(std::format("Maximum number of iterations reached {}\n", msg));
142142
}
143143
};
144144

power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ using EdgeWeight = int64_t;
4141
using RankedTransformerGroups = std::vector<std::vector<Idx2D>>;
4242

4343
constexpr auto infty = std::numeric_limits<Idx>::max();
44-
constexpr Idx2D unregulated_idx = {.group = -1, .pos = -1};
44+
constexpr auto last_rank = infty - 1;
45+
constexpr Idx2D unregulated_idx = {-1, -1};
4546
struct TrafoGraphVertex {
4647
bool is_source{};
4748
};
@@ -260,11 +261,12 @@ inline auto build_transformer_graph(State const& state) -> TransformerGraph {
260261
return trafo_graph;
261262
}
262263

263-
inline void process_edges_dijkstra(Idx v, std::vector<EdgeWeight>& vertex_distances, TransformerGraph const& graph) {
264+
inline void process_edges_dijkstra(Idx search_start_vertex, std::vector<EdgeWeight>& vertex_distances,
265+
TransformerGraph const& graph) {
264266
using TrafoGraphElement = std::pair<EdgeWeight, TrafoGraphIdx>;
265267
std::priority_queue<TrafoGraphElement, std::vector<TrafoGraphElement>, std::greater<>> pq;
266-
vertex_distances[v] = 0;
267-
pq.emplace(0, v);
268+
vertex_distances[search_start_vertex] = 0;
269+
pq.emplace(0, search_start_vertex);
268270

269271
while (!pq.empty()) {
270272
auto [dist, u] = pq.top();
@@ -274,19 +276,14 @@ inline void process_edges_dijkstra(Idx v, std::vector<EdgeWeight>& vertex_distan
274276
continue;
275277
}
276278

277-
BGL_FORALL_EDGES(e, graph, TransformerGraph) {
279+
BGL_FORALL_OUTEDGES(u, e, graph, TransformerGraph) {
278280
auto s = boost::source(e, graph);
279281
auto t = boost::target(e, graph);
280282
const EdgeWeight weight = graph[e].weight;
281283

282-
// We can not use BGL_FORALL_OUTEDGES here because we need information
283-
// regardless of edge direction
284-
if (u == s && vertex_distances[s] + weight < vertex_distances[t]) {
284+
if (vertex_distances[s] + weight < vertex_distances[t]) {
285285
vertex_distances[t] = vertex_distances[s] + weight;
286286
pq.emplace(vertex_distances[t], t);
287-
} else if (u == t && vertex_distances[t] + weight < vertex_distances[s]) {
288-
vertex_distances[s] = vertex_distances[t] + weight;
289-
pq.emplace(vertex_distances[s], s);
290287
}
291288
}
292289
}
@@ -328,11 +325,16 @@ inline auto get_edge_weights(TransformerGraph const& graph) -> TrafoGraphEdgePro
328325
// situations can happen.
329326
// The logic still holds in meshed grids, albeit operating a more complex graph.
330327
if (!is_unreachable(edge_src_rank) || !is_unreachable(edge_tgt_rank)) {
331-
if (edge_src_rank != edge_tgt_rank - 1) {
332-
throw AutomaticTapInputError("The control side of a transformer regulator should be relatively further "
333-
"away from the source than the tap side.\n");
328+
if ((edge_src_rank == infty) || (edge_tgt_rank == infty)) {
329+
throw AutomaticTapInputError("The transformer is being controlled from non source side towards source "
330+
"side.\n");
331+
} else if (edge_src_rank != edge_tgt_rank - 1) {
332+
// Control side is also controlled by a closer regulated transformer.
333+
// Make this transformer have the lowest possible priority.
334+
result.emplace_back(graph[e].regulated_idx, last_rank);
335+
} else {
336+
result.emplace_back(graph[e].regulated_idx, edge_tgt_rank);
334337
}
335-
result.emplace_back(graph[e].regulated_idx, edge_tgt_rank);
336338
}
337339
}
338340

@@ -663,7 +665,7 @@ template <symmetry_tag sym> struct NodeState {
663665

664666
class RankIteration {
665667
public:
666-
RankIteration(std::vector<IntS> iterations_per_rank, Idx rank_index)
668+
RankIteration(std::vector<uint64_t> iterations_per_rank, Idx rank_index)
667669
: iterations_per_rank_{std::move(iterations_per_rank)}, rank_index_{rank_index} {}
668670

669671
// Getters
@@ -692,7 +694,7 @@ class RankIteration {
692694
};
693695

694696
private:
695-
std::vector<IntS> iterations_per_rank_;
697+
std::vector<uint64_t> iterations_per_rank_;
696698
Idx rank_index_{};
697699
};
698700

@@ -1001,7 +1003,7 @@ class TapPositionOptimizerImpl<std::tuple<TransformerTypes...>, StateCalculator,
10011003
strategy_ == OptimizerStrategy::global_maximum || strategy_ == OptimizerStrategy::local_maximum;
10021004
bool tap_changed = true;
10031005
Idx rank_index = 0;
1004-
RankIteration rank_iterator(std::vector<IntS>(regulator_order.size()), rank_index);
1006+
RankIteration rank_iterator(std::vector<uint64_t>(regulator_order.size()), rank_index);
10051007

10061008
while (tap_changed) {
10071009
tap_changed = false;
@@ -1021,8 +1023,10 @@ class TapPositionOptimizerImpl<std::tuple<TransformerTypes...>, StateCalculator,
10211023
rank_index = rank_iterator.rank_index();
10221024

10231025
if (tap_changed) {
1024-
if (static_cast<uint64_t>(iterations_per_rank[rank_index]) > 2 * max_tap_ranges_per_rank[rank_index]) {
1025-
throw MaxIterationReached{"TapPositionOptimizer::iterate"};
1026+
if (iterations_per_rank[rank_index] > 2 * max_tap_ranges_per_rank[rank_index]) {
1027+
throw MaxIterationReached{
1028+
std::format("TapPositionOptimizer::iterate {} iterations reached: {}x2 iterations in rank {}",
1029+
iterations_per_rank[rank_index], max_tap_ranges_per_rank[rank_index], rank_index)};
10261030
}
10271031
update_state(update_data);
10281032
result = calculate_(state, method);

0 commit comments

Comments
 (0)