From 431891f556fb72d56a5d0d6a237bb68a753a475d Mon Sep 17 00:00:00 2001 From: Markus Mirz <16180422+m-mirz@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:07:07 +0100 Subject: [PATCH 1/3] initial newton raphson Signed-off-by: Markus Mirz <16180422+m-mirz@users.noreply.github.com> --- .gitignore | 1 + Cargo.lock | 313 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 + README.md | 1 + docs/.gitignore | 2 + docs/book.toml | 7 + docs/src/SUMMARY.md | 3 + docs/src/powerflow.md | 212 ++++++++++++++++++++++++++++ src/main.rs | 66 +++++++++ src/network.rs | 48 +++++++ src/solver.rs | 170 +++++++++++++++++++++++ src/types.rs | 27 ++++ 12 files changed, 857 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 docs/.gitignore create mode 100644 docs/book.toml create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/powerflow.md create mode 100644 src/main.rs create mode 100644 src/network.rs create mode 100644 src/solver.rs create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..81ea835 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,313 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "glam" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" + +[[package]] +name = "glam" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b" + +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" + +[[package]] +name = "glam" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34" + +[[package]] +name = "glam" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca" + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" + +[[package]] +name = "gridoxide" +version = "0.1.0" +dependencies = [ + "nalgebra", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "nalgebra" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" +dependencies = [ + "approx", + "glam 0.14.0", + "glam 0.15.2", + "glam 0.16.0", + "glam 0.17.3", + "glam 0.18.0", + "glam 0.19.0", + "glam 0.20.5", + "glam 0.21.3", + "glam 0.22.0", + "glam 0.23.0", + "glam 0.24.2", + "glam 0.25.0", + "glam 0.27.0", + "glam 0.28.0", + "glam 0.29.3", + "glam 0.30.10", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..25489dc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "gridoxide" +version = "0.1.0" +edition = "2024" + +[dependencies] +nalgebra = "0.34.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cd503a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# gridoxide \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..45e076a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +book + diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..198c63b --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,7 @@ +[book] +title = "Power Grid Book" +authors = ["Markus Mirz"] +language = "en" + +[output.html] +mathjax-support = true diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..92b6fc9 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Powerflow](./powerflow.md) diff --git a/docs/src/powerflow.md b/docs/src/powerflow.md new file mode 100644 index 0000000..87b2b2f --- /dev/null +++ b/docs/src/powerflow.md @@ -0,0 +1,212 @@ +# Powerflow + +The powerflow problem is about the calculation of voltage magnitudes and angles for all network nodes. +The solution is obtained from a subset of voltages and power injections. + +## Power System Model +Power systems are modeled as a network of nodes (buses) and branches (lines and transformers). +Power sources (generators) and sinks (loads) can be connected to the nodes. +Each node in the network is fully described by the following four electrical quantities: + +* \\(\vert V_k \vert\\): voltage magnitude +* \\(\theta_k\\): voltage phase angle +* \\(P_k\\): active power +* \\(Q_k\\): reactive power + +There are three types of network nodes: VD, PV and PQ. +Depending on the node type, two of the four electrical quantities are specified. + +| Node Type | Known | Unknown | +| ---| ---| ---| +| \\(VD\\) | \\(\vert V_k \vert, \theta_k\\) | \\(P_k, Q_k\\) | +| \\(PV\\) | \\(P_k, \vert V_k \vert\\) | \\(Q_k, \theta_k\\) | +| \\(PQ\\) | \\(P_k, Q_k\\) | \\(\vert V_k \vert, \theta_k\\) | + + +## Newton Raphson + +The goal is to bring a nonlinear mismatch function \\(f\\) to zero. +The value of the mismatch function depends on a solution vector \\(x\\): + +\\[ f(x) = 0 \\] + +As \\(f(x)\\) is nonlinear, the equation system is solved iteratively using Newton-Raphson: + +\\[ x_{i+1} = x_i + \Delta x_i = x_i - \textbf{J}_f(x_i)^{-1} f(x_i) \\] + +where \\(\Delta x\\) is the correction of the solution vector and \\(\textbf{J}_f\\) is the Jacobian matrix. + +Instead of computing \\(\Delta x_i = - \textbf{J}_f(x_i)^{-1} f(x_i)\\), the linear equation set + +\\[ - \textbf{J}_f(x_i) \Delta x_i = f(x_i) \\] + +is solved for \\(\Delta x_i\\). + +Iterations are stopped when the mismatch is sufficiently small: + +\\[ f(x_i) < \epsilon \\] + + +## Powerflow Solution + +The solution vector \\(x\\) represents the voltage \\(V\\) either in polar coordinates + +\\[ \left [ \begin{array}{c} \delta \\ \vert V \vert \end{array} \right ] \\] + +or rectangular coordinates + +\\[ \left [ \begin{array}{c} V_{real} \\ V_{imag} \end{array} \right ] \\] + +The mismatch function \\(f\\) represents the power mismatch + +\\[ \Delta S = \left [ \begin{array}{c} \Delta P \\ \Delta Q \end{array} \right ] \\] + + +or the current mismatch + +\\[ \Delta I = \left [ \begin{array}{c} \Delta I_{real} \\ \Delta I_{imag} \end{array} \right ] \\] + + +This results in four different formulations of the powerflow problem: + +* power mismatch function and polar coordinates +* power mismatch function and rectangular coordinates +* current mismatch function and polar coordinates +* current mismatch function and rectangular coordinates + +To solve the problem using Newton-Raphson, we need to formulate \\(\textbf{J}_f\\) and \\(f\\) for each powerflow problem formulation. + + +### Powerflow with Power Mismatch Function and Polar Coordinates + +The injected power at a node \\(k\\) is given by + +\\[ S_k = V_k I_k^* \\] + +The current injection into any node \\(k\\) is + +\\[ I_k = \sum_{j=1}^N Y_{kj} V_j \\] + +Substitution yields + +\\[ +\begin{align*} +S_k &= V_k \left ( \sum_{j=1}^N Y_{kj} V_j \right )^* \\\\ + &= V_k \sum_{j=1}^N Y_{kj}^* V_j^* +\end{align*} +\\] + +\\(G_{kj}\\) and \\(B_{kj}\\) are defined as the real and imaginary part of the admittance matrix element \\(Y_{kj}\\), so that \\(Y_{kj} = G_{kj} + jB_{kj}\\). +This results in + +\\[ +\begin{align*} +S_k &= V_k \sum_{j=1}^N Y_{kj}^* V_j^* \\\\ + &= \vert V_k \vert \angle \theta_k \sum_{j=1}^N (G_{kj} + jB_{kj})^* ( \vert V_j \vert \angle \theta_j)^* \\\\ + &= \vert V_k \vert \angle \theta_k \sum_{j=1}^N (G_{kj} - jB_{kj}) ( \vert V_j \vert \angle - \theta_j) \\\\ + &= \sum_{j=1}^N \left \vert V_k \vert \vert V_j \vert \angle (\theta_k - \theta_j) \right (G_{kj} - jB_{kj}) \\\\ + &= \sum_{j=1}^N \vert V_k \vert \vert V_j \vert \left ( cos(\theta_k - \theta_j) + jsin(\theta_k - \theta_j) \right ) (G_{kj} - jB_{kj}) +\end{align*} +\\] + +If we perform the algebraic multiplication of the two terms inside the parentheses, and collect real and imaginary parts, and recall that \\(S_k = P_k + jQ_k\\), we can split this into two equations: one for the real part, and one for the imaginary part. + +\\[ +\theta_{kj} = \theta_k - \theta_j \\\\ +P_k = \sum_{j=1}^N \vert V_k \vert \vert V_j \vert \left ( G_{kj}cos(\theta_{kj}) + B_{kj} sin(\theta_{kj}) \right ) \\\\ +Q_k = \sum_{j=1}^N \vert V_k \vert \vert V_j \vert \left ( G_{kj}sin(\theta_{kj}) - B_{kj} cos(\theta_{kj}) \right ) +\\] + +These are called the power flow equations. + +We consider a power system network having \\(N\\) buses. We assume one VD bus, \\(N_{PV}-1\\) PV buses and \\(N-N_{PV}\\) PQ buses. +We assume that the VD bus is numbered bus \\(1\\), the PV buses are numbered \\(2,...,N_{PV}\\), and the PQ buses are numbered \\(N_{PV}+1,...,N\\). +We define the vector of unknowns as the composite vector of unknown angles \\(\theta\\) and voltage magnitudes \\(\vert V \vert\\): + +\\[ +x = \left[ \begin{array}{c} \theta \\\\ \vert V \vert \\\\ \end{array} \right ] + = \left[ \begin{array}{c} \theta_2 \\\\ \theta_{3} \\\\ \vdots \\\\ \theta_N \\\\ \vert V_{N_{PV+1}} \vert \\\\ \vert V_{N_{PV+2}} \vert \\\\ \vdots \\\\ \vert V_N \vert \end{array} \right] +\\] + +The right-hand sides of the powerflow equations for \\(P_k\\) and \\(Q_k\\) depend on the elements of the unknown vector \\(x\\). + +Expressing this dependency more explicitly, we rewrite these equations as + +\\[ +\begin{align*} +P_k^{spec} = P_k^{calc} (x) \Rightarrow P_k^{calc} (x) - P_k^{spec} &= 0 \quad \quad k = 2,...,N \\\\ +Q_k^{spec} = Q_k^{calc} (x) \Rightarrow Q_k^{calc} (x) - Q_k^{spec} &= 0 \quad \quad k = N_{PV}+1,...,N +\end{align*} +\\] + +We define the mismatch \\({f} (x)\\) as + +\\[ +\begin{align*} +f(x) = \left [ \begin{array}{c} f_1(x) \\\\ \vdots \\\\ f_{N-1}(x) \\\\ ------ \\\\ f_N(x) \\\\ \vdots \\\\ f_{2N-N_{PV} -1}(x) \end{array} \right ] + = \left [ \begin{array}{c} P_2(x) - P_2 \\\\ \vdots \\\\ P_N(x) - P_N \\\\ --------- \\\\ Q_{N_{PV}+1}(x) - Q_{N_{PV}+1} \\\\ \vdots \\\\ Q_N(x) - Q_N \end{array} \right] + = \left [ \begin{array}{c} \Delta P_2 \\\\ \vdots \\\\ \Delta P_N \\\\ ------ \\\\ \Delta Q_{N_{PV}+1} \\\\ \vdots \\\\ \Delta Q_N \end{array} \right ] + = 0 +\end{align*} +\\] + +That is a system of nonlinear equations. +The nonlinearity stems from the fact that \\(P_k\\) and \\(Q_k\\) have terms containing products of unknowns and also terms containing trigonometric functions of unknowns. + + +The Jacobian matrix is obtained by taking all first-order partial derivates of the power mismatch function with respect to the voltage angles \\(\theta_k\\) and magnitudes \\(\vert V_k \vert\\): + +\\[ +\theta_{jk} = \theta_j - \theta_k \\\\ +\begin{align*} +J_{jk}^{P \theta} &= \frac{\partial P_j (x ) } {\partial \theta_k} = \vert V_j \vert \vert V_k \vert \left ( G_{jk} sin(\theta_{jk}) - B_{jk} cos(\theta_{jk} ) \right ) \\\\ +J_{jj}^{P \theta} &= \frac{\partial P_j(x)}{\partial \theta_j} = -Q_j (x ) - B_{jj} \vert V_j \vert ^{2} \\\\ +J_{jk}^{Q \theta} &= \frac{\partial Q_j(x)}{\partial \theta_k} = - \vert V_j \vert \vert V_k \vert \left ( G_{jk} cos(\theta_{jk}) + B_{jk} sin(\theta_{jk}) \right ) \\\\ + J_{jj}^{Q \theta} &= \frac{\partial Q_j(x)}{\partial \theta_k} = P_j (x ) - G_{jj} \vert V_j \vert ^{2} \\\\ + J_{jk}^{PV} &= \frac{\partial P_j (x ) } {\partial \vert V_k \vert } = \vert V_j \vert \left ( G_{jk} cos(\theta_{jk}) + B_{jk} sin(\theta_{jk}) \right ) \\\\ + J_{jj}^{PV} &= \frac{\partial P_j(x)}{\partial \vert V_j \vert } = \frac{P_j (x )}{\vert V_j \vert} + G_{jj} \vert V_j \vert \\\\ + J_{jk}^{QV} &= \frac{\partial Q_j (x ) } {\partial \vert V_k \vert } = \vert V_j \vert \left ( G_{jk} sin(\theta_{jk}) - B_{jk} cos(\theta_{jk}) \right ) \\\\ + J_{jj}^{QV} &= \frac{\partial Q_j(x)}{\partial \vert V_j \vert } = \frac{Q_j (x )}{\vert V_j \vert} - B_{jj} \vert V_j \vert \\\\ +\end{align*} +\\] + +The linear system of equations that is solved in every Newton iteration can be written in matrix form as follows + +\\[ +\begin{align*} +-J(x) \left [ \begin{array}{c} \Delta \theta \\\\ \Delta \vert V \vert \end{array} \right ] &= \left [ \begin{array}{c} \Delta P \\\\ \Delta Q \end{array} \right ] \\\\ +\Rightarrow J(x) \left [ \begin{array}{c} \Delta \theta \\\\ \Delta \vert V \vert \end{array} \right ] &= \left [ \begin{array}{c} -\Delta P \\\\ -\Delta Q \end{array} \right ] +\end{align*} +\\] + +\\[ +\begin{align*} +\left [ \begin{array}{cccccc} + \frac{\partial \Delta P_2 }{\partial \theta_2} & \cdots & \frac{\partial \Delta P_2 }{\partial \theta_N} & + \frac{\partial \Delta P_2 }{\partial \vert V_{N_{G+1}} \vert} & \cdots & \frac{\partial \Delta P_2 }{\partial \vert V_N \vert} \\\\ + \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\\\ + \frac{\partial \Delta P_N }{\partial \theta_2} & \cdots & \frac{\partial \Delta P_N}{\partial \theta_N} & + \frac{\partial \Delta P_N}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta P_N}{\partial \vert V_N \vert} \\\\ + \frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_2} & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_N} & + \frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_N \vert} \\\\ + \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\\\ + \frac{\partial \Delta Q_N}{\partial \theta_2} & \cdots & \frac{\partial \Delta Q_N}{\partial \theta_N} & + \frac{\partial \Delta Q_N}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_N}{\partial \vert V_N \vert} +\end{array} \right ] +\left [ \begin{array}{c} \Delta \theta_2 \\\\ \vdots \\\\ \Delta \theta_N \\\\ \Delta \vert V_{N_{G+1}} \vert \\\\ \vdots \\\\ \Delta \vert V_N \vert \end{array} \right ] += \left [ \begin{array}{c} -\Delta P_2 \\\\ \vdots \\\\ -\Delta P_N \\\\ -\Delta Q_{N_{G+1}} \\\\ \vdots \\\\ -\Delta Q_N \end{array} \right ] +\end{align*} +\\] + +### Solution Steps + +1. Set the iteration counter to \\(i=1\\). Use the initial solution \\(V_{i} = 1 \angle 0^{\circ}\\) +2. Compute the mismatch vector \\(f({x_i})\\) using the power flow equations +3. Check the stopping criterion + * If \\(\vert \Delta P_{i} \vert < \epsilon_{P}\\) for all type PQ and PV buses and + * If \\(\vert \Delta Q_{i} \vert < \epsilon_{Q}\\) for all type PQ + * Then go to step 6 + * Else, go to step 4 +4. Evaluate the Jacobian matrix \\(\textbf{J}_f(x_i)\\) and compute \\(\Delta x_i\\). +5. Compute the new solution vector \\(x_{i+1}\\) and return to step 3. +6. Stop. \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d92364d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,66 @@ +use std::f64::consts::PI; + +mod types; +mod network; +mod solver; + +use types::{Bus, Line, BusType}; +use network::build_ybus; +use solver::newton_raphson; + +fn main() { + // Example 3-bus system: Bus 0 slack, Bus 1 PV, Bus 2 PQ + // Values are illustrative, not from a standard case + let mut buses = vec![ + Bus { + idx: 0, + bus_type: BusType::Slack, + voltage_mag: 1.06, + voltage_ang: 0.0, + p_spec: 0.0, + q_spec: 0.0, + q_min: -999.0, + q_max: 999.0, + }, + Bus { + idx: 1, + bus_type: BusType::PV, + voltage_mag: 1.04, + voltage_ang: 0.0, + p_spec: 0.5, // generation - load + q_spec: 0.0, // for PV we use P specified and Vm specified + q_min: -0.5, + q_max: 0.5, + }, + Bus { + idx: 2, + bus_type: BusType::PQ, + voltage_mag: 1.0, + voltage_ang: 0.0, + p_spec: -0.6, // load of 0.6 p.u. + q_spec: -0.25, + q_min: -999.0, + q_max: 999.0, + }, + ]; + + let lines = vec![ + Line { from: 0, to: 1, r: 0.02, x: 0.06, b_shunt: 0.03 }, + Line { from: 0, to: 2, r: 0.08, x: 0.24, b_shunt: 0.025 }, + Line { from: 1, to: 2, r: 0.06, x: 0.18, b_shunt: 0.02 }, + ]; + + let ybus = build_ybus(buses.len(), &lines); + + newton_raphson(&mut buses, &ybus, 1e-6, 20); + + println!("Final voltages:"); + for b in buses.iter() { + println!( + "Bus {}: |V| = {:.6}, angle = {:.6} deg", + b.idx, + b.voltage_mag, + b.voltage_ang * 180.0 / PI + ); + } +} \ No newline at end of file diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..27c74a2 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,48 @@ +use nalgebra::{DMatrix, DVector}; +use nalgebra::Complex; +use super::types::{Bus, Line}; + +pub fn build_ybus(n: usize, lines: &[Line]) -> DMatrix> { + let mut y = DMatrix::from_element(n, n, Complex::new(0.0, 0.0)); + for ln in lines { + let z = Complex::new(ln.r, ln.x); + // series admittance + let y_line = Complex::new(1.0, 0.0) / z; + // split shunt susceptance equally to both ends of line + let b2 = Complex::new(0.0, ln.b_shunt / 2.0); + // diagonal elements + y[(ln.from, ln.from)] += y_line + b2; + y[(ln.to, ln.to)] += y_line + b2; + // off-diagonal elements + y[(ln.from, ln.to)] -= y_line; + y[(ln.to, ln.from)] -= y_line; + } + y +} + +pub fn power_injections( + buses: &[Bus], + ybus: &DMatrix>, +) -> (Vec, Vec) { + // Calculates the complex power injection into each bus. + // S = V .* conj(I) where I = Ybus * V + // S_k = V_k * I_k^* + let n = buses.len(); + let mut p = vec![0.0; n]; + let mut q = vec![0.0; n]; + + let v = DVector::from_iterator( + n, + buses.iter().map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang)), + ); + + let i = ybus * v.clone(); + let s = v.component_mul(&i.conjugate()); + + for k in 0..n { + p[k] = s[k].re; + q[k] = s[k].im; + } + + (p, q) +} diff --git a/src/solver.rs b/src/solver.rs new file mode 100644 index 0000000..f3d2095 --- /dev/null +++ b/src/solver.rs @@ -0,0 +1,170 @@ +use nalgebra::{DMatrix, DVector}; +use nalgebra::Complex; +use super::types::{Bus, BusType}; +use super::network::power_injections; + +pub fn newton_raphson(buses: &mut [Bus], ybus: &DMatrix>, tol: f64, max_iter: usize) { + + // Identify PV and PQ indices (exclude slack) + let mut pv_idx: Vec = Vec::new(); + let mut pq_idx: Vec = Vec::new(); + for b in buses.iter() { + match b.bus_type { + BusType::Slack => (), + BusType::PV => pv_idx.push(b.idx), + BusType::PQ => pq_idx.push(b.idx), + } + } + + let non_slack_idx: Vec = buses + .iter() + .filter(|b| !matches!(b.bus_type, BusType::Slack)) + .map(|b| b.idx) + .collect(); + + let n_angle = non_slack_idx.len(); + let n_vmag = pq_idx.len(); + let n_unknowns = n_angle + n_vmag; + + for iter in 0..max_iter { + // compute injections + let (p_calc, q_calc) = power_injections(buses, ybus); + + // Build mismatch vector + let mut mismatch = DVector::from_element(n_unknowns, 0.0); + let mut mis_idx = 0; + for &i in &non_slack_idx { + // P mismatch for PV and PQ buses + mismatch[mis_idx] = buses[i].p_spec - p_calc[i]; + mis_idx += 1; + } + for &i in &pq_idx { + // Q mismatch for PQ buses + mismatch[mis_idx] = buses[i].q_spec - q_calc[i]; + mis_idx += 1; + } + + let max_mis = mismatch.iter().fold(0.0f64, |a, &b| a.max(b.abs())); + println!("iter {}: max mismatch = {:.6e}", iter + 1, max_mis); + if max_mis < tol { + println!("Converged in {} iterations", iter + 1); + return; + } + + // Build Jacobian + let j = build_jacobian(buses, ybus, &non_slack_idx, &pq_idx, &p_calc, &q_calc); + + // Solve + let lu = j.lu(); + let dx = match lu.solve(&mismatch) { + Some(sol) => sol, + None => { + println!("Jacobian is singular. Failed to solve."); + return; + } + }; + + // Update state + let mut dx_idx = 0; + for &i in &non_slack_idx { + // update voltage angles + buses[i].voltage_ang += dx[dx_idx]; + dx_idx += 1; + } + for &i in &pq_idx { + // update voltage magnitudes + buses[i].voltage_mag += dx[dx_idx]; + dx_idx += 1; + } + } + + println!("Failed to converge in {} iterations", max_iter); +} + +fn build_jacobian( + buses: &[Bus], + ybus: &DMatrix>, + non_slack_idx: &[usize], + pq_idx: &[usize], + p_calc: &[f64], + q_calc: &[f64], +) -> DMatrix { + // J = [ H N ] + // [ M L ] + // H = dP/d_ang, N = dP/d_vmag + // M = dQ/d_ang, L = dQ/d_vmag + let n_angle = non_slack_idx.len(); + let n_vmag = pq_idx.len(); + let n_unknowns = n_angle + n_vmag; + let mut j = DMatrix::from_element(n_unknowns, n_unknowns, 0.0); + + // Extract voltage magnitudes and angles for each bus + let vm: Vec = buses.iter().map(|b| b.voltage_mag).collect(); + let va: Vec = buses.iter().map(|b| b.voltage_ang).collect(); + + // Jacobian structure: + // J = [ H N ] + // [ M L ] + // H = dP/d_ang, N = dP/d_vmag + // M = dQ/d_ang, L = dQ/d_vmag + + // Loop over non-slack buses for rows + // First n_angle rows: P equations + for (row_idx, &i) in non_slack_idx.iter().enumerate() { + // H block (dP/d_ang) + for (col_idx, &k) in non_slack_idx.iter().enumerate() { + if i == k { // H_ii = dP_i/d_ang_i + // H_ii = -Q_i - V_i^2 * B_ii + j[(row_idx, col_idx)] = -q_calc[i] - vm[i].powi(2) * ybus[(i, i)].im; + } else { // H_ik = dP_i/d_ang_k + // H_ik = V_i * V_k * (G_ik * sin(d_i - d_k) - B_ik * cos(d_i - d_k)) + let y_ik = ybus[(i, k)]; + let angle_ik = va[i] - va[k]; + j[(row_idx, col_idx)] = vm[i] * vm[k] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos()); + } + } + // N block (dP/d_vmag) + for (col_idx, &k) in pq_idx.iter().enumerate() { + if i == k { // N_ii = dP_i/d_vmag_i + // N_ii = P_i/V_i + V_i * G_ii + j[(row_idx, n_angle + col_idx)] = p_calc[i] / vm[i] + vm[i] * ybus[(i, i)].re; + } else { // N_ik = dP_i/d_vmag_k + // N_ik = V_i * (G_ik * cos(d_i - d_k) + B_ik * sin(d_i - d_k)) + let y_ik = ybus[(i, k)]; + let angle_ik = va[i] - va[k]; + j[(row_idx, n_angle + col_idx)] = vm[i] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin()); + } + } + } + + // Loop over PQ buses for rows + // Next n_vmag rows: Q equations + for (row_idx, &i) in pq_idx.iter().enumerate() { + // M block (dQ/d_ang) + for (col_idx, &k) in non_slack_idx.iter().enumerate() { + if i == k { // M_ii = dQ_i/d_ang_i + // M_ii = P_i - V_i^2 * G_ii + j[(n_angle + row_idx, col_idx)] = p_calc[i] - vm[i].powi(2) * ybus[(i, i)].re; + } else { // M_ik = dQ_i/d_ang_k + // M_ik = -V_i * V_k * (G_ik * cos(d_i - d_k) + B_ik * sin(d_i - d_k)) + let y_ik = ybus[(i, k)]; + let angle_ik = va[i] - va[k]; + j[(n_angle + row_idx, col_idx)] = -vm[i] * vm[k] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin()); + } + } + // L block (dQ/d_vmag) + for (col_idx, &k) in pq_idx.iter().enumerate() { + if i == k { // L_ii = dQ_i/d_vmag_i + // L_ii = Q_i/V_i - V_i * B_ii + j[(n_angle + row_idx, n_angle + col_idx)] = q_calc[i] / vm[i] - vm[i] * ybus[(i, i)].im; + } else { // L_ik = dQ_i/d_vmag_k + // L_ik = V_i * (G_ik * sin(d_i - d_k) - B_ik * cos(d_i - d_k)) + let y_ik = ybus[(i, k)]; + let angle_ik = va[i] - va[k]; + j[(n_angle + row_idx, n_angle + col_idx)] = vm[i] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos()); + } + } + } + + j +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..d4885df --- /dev/null +++ b/src/types.rs @@ -0,0 +1,27 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BusType { + Slack, + PV, + PQ, +} + +#[derive(Clone, Debug)] +pub struct Bus { + pub idx: usize, // index in arrays (0-based) + pub bus_type: BusType, + pub voltage_mag: f64, // Vm (p.u.) + pub voltage_ang: f64, // Va (rad) + pub p_spec: f64, // P specified (generation - load) in p.u. + pub q_spec: f64, // Q specified (generation - load) in p.u. + pub q_min: f64, // reactive limits (for PV handling, optional) + pub q_max: f64, +} + +#[derive(Clone, Debug)] +pub struct Line { + pub from: usize, + pub to: usize, + pub r: f64, + pub x: f64, + pub b_shunt: f64, // total line charging +} From b93bc2f3c757b37ea2f32a40ef8e59c5f1f0bf08 Mon Sep 17 00:00:00 2001 From: Markus Mirz <16180422+m-mirz@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:49:19 +0100 Subject: [PATCH 2/3] add json reader and test Signed-off-by: Markus Mirz <16180422+m-mirz@users.noreply.github.com> --- Cargo.lock | 63 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ README.md | 48 +++++++++++++++++++++++++++++- src/json.rs | 8 +++++ src/lib.rs | 29 ++++++++++++++++++ src/main.rs | 65 +++++++---------------------------------- src/types.rs | 8 +++-- tests/data/network.json | 57 ++++++++++++++++++++++++++++++++++++ tests/powerflow_test.rs | 32 ++++++++++++++++++++ 9 files changed, 254 insertions(+), 58 deletions(-) create mode 100644 src/json.rs create mode 100644 src/lib.rs create mode 100644 tests/data/network.json create mode 100644 tests/powerflow_test.rs diff --git a/Cargo.lock b/Cargo.lock index 81ea835..8b9b629 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,8 +124,16 @@ name = "gridoxide" version = "0.1.0" dependencies = [ "nalgebra", + "serde", + "serde_json", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -136,6 +144,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "nalgebra" version = "0.34.1" @@ -266,6 +280,49 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "simba" version = "0.9.1" @@ -311,3 +368,9 @@ dependencies = [ "bytemuck", "safe_arch", ] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/Cargo.toml b/Cargo.toml index 25489dc..d0745d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] nalgebra = "0.34.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/README.md b/README.md index 9cd503a..54c0781 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# gridoxide \ No newline at end of file +# gridoxide + +`gridoxide` is a power flow analysis tool written in Rust. It uses the Newton-Raphson method to solve the power flow equations for an electrical grid defined in a JSON file. + +## Building + +To build the project, you need to have the Rust toolchain installed. You can find instructions on how to install it at [rustup.rs](https://rustup.rs/). + +Once you have Rust installed, you can build the project by running: + +```bash +cargo build +``` + +For an optimized release build, use: + +```bash +cargo build --release +``` + +## Running + +You can run the program using `cargo run`: + +```bash +cargo run +``` + +If you have built the project, you can also run the executable directly. From the project root: + +For a debug build: +```bash +./target/debug/gridoxide +``` + +For a release build: +```bash +./target/release/gridoxide +``` + +## Testing + +To run the tests for the project, use: + +```bash +cargo test +``` diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 0000000..0d7bfdf --- /dev/null +++ b/src/json.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use super::types::{Bus, Line}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NetworkData { + pub buses: Vec, + pub lines: Vec, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7ea4620 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,29 @@ +pub mod types; +pub mod network; +pub mod solver; +pub mod json; + +use network::build_ybus; +use solver::newton_raphson; +use json::NetworkData; +use types::Bus; + +/// Runs a power flow analysis on the given network data. +/// +/// # Arguments +/// +/// * `network_data` - The network data to analyze. +/// +/// # Returns +/// +/// A vector of buses with the calculated voltage magnitudes and angles. +pub fn run_power_flow_analysis(network_data: NetworkData) -> Vec { + let mut buses = network_data.buses; + let lines = network_data.lines; + + let ybus = build_ybus(buses.len(), &lines); + + newton_raphson(&mut buses, &ybus, 1e-6, 20); + + buses +} diff --git a/src/main.rs b/src/main.rs index d92364d..2b5e01e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,58 +1,15 @@ -use std::f64::consts::PI; - -mod types; -mod network; -mod solver; - -use types::{Bus, Line, BusType}; -use network::build_ybus; -use solver::newton_raphson; +use std::fs; +use std::path::PathBuf; +use gridoxide::json::NetworkData; +use gridoxide::run_power_flow_analysis; fn main() { - // Example 3-bus system: Bus 0 slack, Bus 1 PV, Bus 2 PQ - // Values are illustrative, not from a standard case - let mut buses = vec![ - Bus { - idx: 0, - bus_type: BusType::Slack, - voltage_mag: 1.06, - voltage_ang: 0.0, - p_spec: 0.0, - q_spec: 0.0, - q_min: -999.0, - q_max: 999.0, - }, - Bus { - idx: 1, - bus_type: BusType::PV, - voltage_mag: 1.04, - voltage_ang: 0.0, - p_spec: 0.5, // generation - load - q_spec: 0.0, // for PV we use P specified and Vm specified - q_min: -0.5, - q_max: 0.5, - }, - Bus { - idx: 2, - bus_type: BusType::PQ, - voltage_mag: 1.0, - voltage_ang: 0.0, - p_spec: -0.6, // load of 0.6 p.u. - q_spec: -0.25, - q_min: -999.0, - q_max: 999.0, - }, - ]; - - let lines = vec![ - Line { from: 0, to: 1, r: 0.02, x: 0.06, b_shunt: 0.03 }, - Line { from: 0, to: 2, r: 0.08, x: 0.24, b_shunt: 0.025 }, - Line { from: 1, to: 2, r: 0.06, x: 0.18, b_shunt: 0.02 }, - ]; - - let ybus = build_ybus(buses.len(), &lines); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data/network.json"); + let network_json = fs::read_to_string(path).expect("Unable to read network.json"); + let network_data: NetworkData = serde_json::from_str(&network_json).expect("Unable to parse network.json"); - newton_raphson(&mut buses, &ybus, 1e-6, 20); + let buses = run_power_flow_analysis(network_data); println!("Final voltages:"); for b in buses.iter() { @@ -60,7 +17,7 @@ fn main() { "Bus {}: |V| = {:.6}, angle = {:.6} deg", b.idx, b.voltage_mag, - b.voltage_ang * 180.0 / PI + b.voltage_ang.to_degrees() ); } -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs index d4885df..e99f8c3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,11 +1,13 @@ -#[derive(Clone, Copy, Debug, PartialEq)] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum BusType { Slack, PV, PQ, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Bus { pub idx: usize, // index in arrays (0-based) pub bus_type: BusType, @@ -17,7 +19,7 @@ pub struct Bus { pub q_max: f64, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Line { pub from: usize, pub to: usize, diff --git a/tests/data/network.json b/tests/data/network.json new file mode 100644 index 0000000..06259b3 --- /dev/null +++ b/tests/data/network.json @@ -0,0 +1,57 @@ +{ + "buses": [ + { + "idx": 0, + "bus_type": "Slack", + "voltage_mag": 1.06, + "voltage_ang": 0.0, + "p_spec": 0.0, + "q_spec": 0.0, + "q_min": -999.0, + "q_max": 999.0 + }, + { + "idx": 1, + "bus_type": "PV", + "voltage_mag": 1.04, + "voltage_ang": 0.0, + "p_spec": 0.5, + "q_spec": 0.0, + "q_min": -0.5, + "q_max": 0.5 + }, + { + "idx": 2, + "bus_type": "PQ", + "voltage_mag": 1.0, + "voltage_ang": 0.0, + "p_spec": -0.6, + "q_spec": -0.25, + "q_min": -999.0, + "q_max": 999.0 + } + ], + "lines": [ + { + "from": 0, + "to": 1, + "r": 0.02, + "x": 0.06, + "b_shunt": 0.03 + }, + { + "from": 0, + "to": 2, + "r": 0.08, + "x": 0.24, + "b_shunt": 0.025 + }, + { + "from": 1, + "to": 2, + "r": 0.06, + "x": 0.18, + "b_shunt": 0.02 + } + ] +} diff --git a/tests/powerflow_test.rs b/tests/powerflow_test.rs new file mode 100644 index 0000000..209e0c0 --- /dev/null +++ b/tests/powerflow_test.rs @@ -0,0 +1,32 @@ +use std::fs; +use std::path::PathBuf; +use gridoxide::json::NetworkData; +use gridoxide::run_power_flow_analysis; + +#[test] +fn test_power_flow_analysis() { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data/network.json"); + let network_json = fs::read_to_string(path).expect("Unable to read network.json"); + let network_data: NetworkData = serde_json::from_str(&network_json).expect("Unable to parse network.json"); + + let buses = run_power_flow_analysis(network_data); + + // Expected values would be derived from a known correct power flow solution. + // For this test, we are doing a snapshot/regression test. + // These values are from a previous run of the program. + let expected_voltages = vec![ + (1.06, 0.0), + (1.04, 0.014349), + (1.003358, -0.043141) + ]; + + assert_eq!(buses.len(), expected_voltages.len()); + + for i in 0..buses.len() { + assert_eq!(buses[i].idx, i); + let (expected_mag, expected_ang_rad) = expected_voltages[i]; + assert!((buses[i].voltage_mag - expected_mag).abs() < 1e-5); + assert!((buses[i].voltage_ang - expected_ang_rad).abs() < 1e-5); + } +} From 767901ffc86206cbf90f920fdf72355bf58b0c29 Mon Sep 17 00:00:00 2001 From: Markus Mirz <16180422+m-mirz@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:13:45 +0100 Subject: [PATCH 3/3] add ci Signed-off-by: Markus Mirz <16180422+m-mirz@users.noreply.github.com> --- .github/workflows/book.yml | 52 +++++++++++++++++++++++++++++++++++++ .github/workflows/build.yml | 22 ++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .github/workflows/book.yml create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml new file mode 100644 index 0000000..21ad344 --- /dev/null +++ b/.github/workflows/book.yml @@ -0,0 +1,52 @@ +name: Deploy mdBook site to Pages + +on: + push: + branches: ["main"] + + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + env: + MDBOOK_VERSION: 0.4.36 + steps: + - uses: actions/checkout@v4 + - name: Install mdBook + run: | + curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh + rustup update + cargo install --version ${MDBOOK_VERSION} mdbook + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Build with mdBook + run: mdbook build docs + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./book + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8f781fa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +name: Rust build and test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose