Browse Source

Initial commit

Henrik Kunzelmann 3 years ago
parent
commit
cd5ec4fd73
44 changed files with 1967 additions and 32 deletions
  1. 11 12
      build.gradle
  2. 1 2
      gradle/wrapper/gradle-wrapper.properties
  3. BIN
      jars/Jama-1.0.3.jar
  4. BIN
      res/Button_Images/Thumbs.db
  5. 5 0
      src/holeg/GridSolverResult.java
  6. 80 0
      src/holeg/HolegGateway.java
  7. 282 0
      src/holeg/HolegPowerFlow.java
  8. 47 0
      src/holeg/model/Grid.java
  9. 14 0
      src/holeg/model/GridEdge.java
  10. 15 0
      src/holeg/model/GridNode.java
  11. 52 0
      src/holeg/power_flow/AdmittanceMatrix.java
  12. 30 0
      src/holeg/power_flow/Bus.java
  13. 7 0
      src/holeg/power_flow/BusType.java
  14. 157 0
      src/holeg/power_flow/ComplexMatrix.java
  15. 79 0
      src/holeg/power_flow/ComplexNumber.java
  16. 54 0
      src/holeg/power_flow/FlowCalculation.java
  17. 10 0
      src/holeg/power_flow/FlowData.java
  18. 26 0
      src/holeg/power_flow/Line.java
  19. 235 0
      src/holeg/power_flow/NewtonRaphsonSolver.java
  20. 67 0
      src/holeg/power_flow/PowerFlowProblem.java
  21. 19 0
      src/holeg/power_flow/PowerFlowProgram.java
  22. 5 0
      src/holeg/power_flow/Solver.java
  23. 8 0
      src/holeg/power_flow/SolverError.java
  24. 7 0
      src/holeg/power_flow/SolverPerformance.java
  25. 79 0
      src/holeg/power_flow/SolverResult.java
  26. 15 0
      src/holeg/power_flow/SolverSettings.java
  27. 127 0
      src/holeg/power_flow/TestData.java
  28. 4 0
      src/holeg/power_flow/TransmissionLineModel.java
  29. 78 0
      src/holeg/power_flow/Util.java
  30. 59 0
      src/holeg/simple_grid/SimpleGridBuilder.java
  31. 51 0
      src/holeg/simple_grid/SimpleGridEdge.java
  32. 47 0
      src/holeg/simple_grid/SimpleGridNode.java
  33. 43 0
      src/holeg/test_headless/TestHeadlessProgram.java
  34. 92 0
      src/holeg/test_sensitivity/TestSensitivityProgram.java
  35. 4 0
      src/holeg/ui/FlowTableWindow.java
  36. 38 0
      src/holeg/ui/PowerFlowAnalysisMenu.java
  37. 4 0
      src/holeg/ui/SettingsWindow.java
  38. 21 0
      src/holeg/ui/SolveResultMessageBox.java
  39. 3 0
      src/ui/model/DecoratedCable.java
  40. 7 2
      src/ui/model/DecoratedNetwork.java
  41. 6 2
      src/ui/model/Model.java
  42. 37 8
      src/ui/view/GUI.java
  43. 4 1
      src/ui/view/Main.java
  44. 37 5
      src/ui/view/MyCanvas.java

+ 11 - 12
build.gradle

@@ -10,14 +10,11 @@ buildscript {
     repositories {
         jcenter()
     }
-    dependencies {
-        classpath 'gradle.plugin.edu.sc.seis.gradle:launch4j:2.3.0'
-    }
 }
 
 
 plugins {
-    id 'edu.sc.seis.launch4j' version '2.3.0'
+    id "edu.sc.seis.launch4j" version "2.4.9"
 }
 
 // Apply the java plugin to add support for Java
@@ -53,14 +50,14 @@ jar {
 }
 
 sourceSets {
-    test {
-        java {
-            srcDir 'tests'
-        }
-        resources {
-            srcDir 'res'
-        }
-    }
+    //test {
+    //    java {
+    //        srcDir 'tests'
+    //    }
+    //    resources {
+    //        srcDir 'res'
+    //    }
+    //}
     main {
         java {
             srcDir 'src'
@@ -80,6 +77,8 @@ dependencies {
 	// download jars on the fly
 	// https://mvnrepository.com/artifact/org.apache.commons/commons-compress
 	compile group: 'org.apache.commons', name: 'commons-compress', version: '1.13'
+	// https://mvnrepository.com/artifact/org.apache.commons/commons-email
+    compile group: 'org.apache.commons', name: 'commons-email', version: '1.5'
     // https://mvnrepository.com/artifact/com.google.code.gson/gson
     compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'
 	// https://mvnrepository.com/artifact/junit/junit

+ 1 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,5 @@
-#Tue Apr 04 16:44:00 CEST 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

BIN
jars/Jama-1.0.3.jar


BIN
res/Button_Images/Thumbs.db


+ 5 - 0
src/holeg/GridSolverResult.java

@@ -0,0 +1,5 @@
+package holeg;
+
+public enum GridSolverResult {
+    Error, PartialFailure, Solved
+}

+ 80 - 0
src/holeg/HolegGateway.java

@@ -0,0 +1,80 @@
+package holeg;
+
+import classes.AbstractCanvasObject;
+import classes.HolonObject;
+import holeg.model.Grid;
+import holeg.model.GridEdge;
+import holeg.model.GridNode;
+import holeg.power_flow.ComplexNumber;
+import holeg.power_flow.SolverSettings;
+import holeg.simple_grid.SimpleGridBuilder;
+import holeg.simple_grid.SimpleGridEdge;
+import holeg.simple_grid.SimpleGridNode;
+import holeg.ui.PowerFlowAnalysisMenu;
+import holeg.ui.SolveResultMessageBox;
+import ui.controller.FlexManager;
+import ui.model.*;
+
+import java.util.HashMap;
+
+public class HolegGateway {
+    private static Grid convert(MinimumNetwork network, int iteration, FlexManager flexManager) {
+        SimpleGridBuilder gridBuilder = new SimpleGridBuilder();
+        HashMap<AbstractCanvasObject, SimpleGridNode> canvasToGrid = new HashMap<>();
+        for (HolonObject object : network.getHolonObjectList()) {
+            double power = object.getEnergyAtTimeStep(iteration);
+            SimpleGridNode node;
+            if (power <= 0)
+                node = gridBuilder.addHouse(new ComplexNumber(-power, 0));
+            else
+                node = gridBuilder.addGenerator(new ComplexNumber(power, 0));
+            node.tag = object;
+            canvasToGrid.put(object, node);
+        }
+        for (AbstractCanvasObject object : network.getNodeAndSwitches()) {
+            SimpleGridNode node = gridBuilder.addHouse(new ComplexNumber(0, 0));
+            node.tag = object;
+            canvasToGrid.put(object, node);
+        }
+        for (IntermediateCableWithState cable : network.getEdgeList()) {
+            SimpleGridNode a = canvasToGrid.get(cable.getModel().getA());
+            SimpleGridNode b = canvasToGrid.get(cable.getModel().getB());
+            if (a == null || b == null)
+                continue;
+            SimpleGridEdge edge = gridBuilder.connect(a, b, 1);
+            edge.tag = cable;
+        }
+        return gridBuilder.getGrid();
+    }
+
+    private static void decorateNetwork(MinimumNetwork network, int iteration, FlexManager flexManager, DecoratedNetwork decoratedNetwork, Grid grid) {
+        for (GridNode node : grid.getNodes()) {
+            SimpleGridNode simpleNode = (SimpleGridNode) node;
+            if (node.getPowerGeneration().lenSquared() > 0)
+                decoratedNetwork.getSupplierList().add(new Supplier((HolonObject) simpleNode.tag, (float) node.getPowerGeneration().real, 0));
+            else if (node.getPowerConsumption().lenSquared() < 0)
+                decoratedNetwork.getConsumerList().add(new Consumer((HolonObject) simpleNode.tag));
+            else
+                decoratedNetwork.getPassivNoEnergyList().add(new Passiv((HolonObject) simpleNode.tag));
+        }
+        for (GridEdge edge : grid.getEdges()) {
+            SimpleGridEdge simpleGridEdge = (SimpleGridEdge) edge;
+            decoratedNetwork.getDecoratedCableList().add(
+                    new DecoratedCable(
+                            ((IntermediateCableWithState)simpleGridEdge.tag).getModel(),
+                            DecoratedCable.CableState.Working,
+                            (float)simpleGridEdge.power.real));
+        }
+    }
+
+    public static void solve(MinimumNetwork minimumNetwork, int iteration, FlexManager flexManager, DecoratedNetwork network) {
+        Grid grid = convert(minimumNetwork, iteration, flexManager);
+
+        HolegPowerFlow powerFlow = new HolegPowerFlow();
+        GridSolverResult result = powerFlow.solve(grid);
+        decorateNetwork(minimumNetwork, iteration, flexManager, network, grid);
+
+        if (PowerFlowAnalysisMenu.getInstance().shouldShowResult())
+            SolveResultMessageBox.show(result);
+    }
+}

+ 282 - 0
src/holeg/HolegPowerFlow.java

@@ -0,0 +1,282 @@
+package holeg;
+
+import holeg.model.Grid;
+import holeg.model.GridEdge;
+import holeg.model.GridNode;
+import holeg.power_flow.*;
+import holeg.ui.PowerFlowAnalysisMenu;
+
+import javax.swing.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class HolegPowerFlow {
+    private Random random = new Random();
+
+    public GridSolverResult solve(Grid grid) {
+        List<Grid> islands = findIslands(grid);
+
+        // Use default settings
+        SolverSettings settings = new SolverSettings();
+
+        boolean anySolved = false;
+        GridSolverResult sumResult = GridSolverResult.Solved;
+
+        // Try to solve each island
+        for (Grid island : islands) {
+            boolean solved = false;
+
+            // Try to solve problem 6 times -> each time the slack node has a new position (random)
+            for (int i = 0; i < 6; i++) {
+                // Build problem and solve it
+                PowerFlowProblem problem = buildProblem(island);
+                Solver solver = new NewtonRaphsonSolver();
+                SolverResult result = solver.solve(problem, settings);
+
+                // Debug message
+                if (PowerFlowAnalysisMenu.getInstance().shouldShowDebug() && (result.solved || i == 3))
+                    debugShowResultAsMessageBox(problem, result);
+
+                // Update grid if solved
+                if (result.solved) {
+                    updateGrid(island, problem, result);
+                    solved = true;
+                    break;
+                }
+            }
+
+            if (!solved)
+                sumResult = GridSolverResult.Error;
+            else
+                anySolved = true;
+        }
+
+        // Check if some islands failed, but not all
+        if (anySolved && sumResult == GridSolverResult.Error)
+            return GridSolverResult.PartialFailure;
+        return sumResult;
+    }
+
+    private List<Grid> findIslands(Grid grid) {
+        List<Grid> islands = new ArrayList<>();
+        List<GridNode> nodes = grid.getNodes();
+
+        // Keeps track if the node at any index was already visited
+        boolean[] visited = new boolean[nodes.size()];
+
+        for (int i = 0; i < nodes.size(); i++) {
+            if (visited[i])
+                continue;
+
+            // First node or node that is not connected to any other node -> new island
+            Grid island = new Grid();
+            Stack<Integer> nodesToVisit = new Stack<>();
+
+            // Add first node
+            nodesToVisit.push(i);
+
+            // Depth first search algorithm
+            while (!nodesToVisit.empty()) {
+                int index = nodesToVisit.pop();
+
+                // Check if already visited
+                if (visited[index])
+                    continue;
+                visited[index] = true;
+
+                // Add node to island
+                GridNode node = nodes.get(index);
+                island.addNode(node);
+
+                // Iterate over all edges
+                List<GridEdge> edges = grid.getEdges(node);
+                for (GridEdge edge : edges) {
+                    island.addEdge(edge);
+
+                    // Select other end of the edge for next visit
+                    if (edge.getFrom() == node)
+                        nodesToVisit.push(nodes.indexOf(edge.getTo()));
+                    else
+                        nodesToVisit.push(nodes.indexOf(edge.getFrom()));
+                }
+            }
+            islands.add(island);
+        }
+        return islands;
+    }
+
+    private PowerFlowProblem buildProblem(Grid grid) {
+        List<Bus> buses = new ArrayList<>();
+        List<Line> lines = new ArrayList<>();
+
+        // Convert from grid to power flow objects
+        for (GridNode node : grid.getNodes())
+            createGridNode(buses, node);
+        for (GridEdge edge : grid.getEdges())
+            createGridLine(grid, lines, edge);
+
+        // Scale voltages and scale power
+        double scaleVoltage = scaleVoltages(buses);
+        double scalePower = scalePower(buses);
+
+        // Find position for slack node
+        addSlackNode(buses, lines);
+
+        // Sort lines
+        lines.sort(Comparator.comparingInt(a -> a.from));
+
+        // Create problem
+        return new PowerFlowProblem(buses.toArray(new Bus[0]), lines.toArray(new Line[0]), scaleVoltage, scalePower);
+    }
+
+    private void createGridNode(List<Bus> buses, GridNode node) {
+        Bus bus = new Bus();
+        bus.delta = 0;
+        bus.voltage = 1;
+        bus.Pl = node.getPowerConsumption().real;
+        bus.Ql = node.getPowerConsumption().imaginary;
+        bus.tag = node;
+
+        if (node.getPowerGeneration().isZero()) {
+            bus.Pg = 0;
+            bus.Qg = 0;
+            bus.type = BusType.PQ;
+        }
+        else {
+            bus.Pg = node.getPowerGeneration().real;
+            bus.Qg = node.getPowerGeneration().imaginary;
+            bus.type = BusType.PV;
+        }
+
+        buses.add(bus);
+    }
+
+    private void createGridLine(Grid grid, List<Line> lines, GridEdge edge) {
+        Line line = new Line();
+        line.tag = edge;
+        // Calculate values for line based on sample datasheet
+        line.R = 0.01938; //0.791 * edge.getLengthKilometers();
+        line.X = 0.1; //0.11 * edge.getLengthKilometers();
+        line.B_2 = 0.1; //0.5 * 0.350141 * edge.getLengthKilometers();
+        line.a = 1;
+        line.from = grid.getNodes().indexOf(edge.getFrom());
+        line.to = grid.getNodes().indexOf(edge.getTo());
+        lines.add(line);
+    }
+
+    private double scaleVoltages(List<Bus> buses) {
+        double decimal = 1;
+        while(true) {
+            boolean nextDecimal = false;
+            for (Bus bus : buses) {
+                if (bus.voltage > decimal) {
+                    nextDecimal = true;
+                    break;
+                }
+            }
+            if (!nextDecimal)
+                break;
+            decimal *= 10;
+        }
+        for (Bus bus : buses)
+            bus.voltage /= decimal;
+        return 1.0 / decimal;
+    }
+
+    private double scalePower(List<Bus> buses) {
+        double decimal = 1;
+        while(true) {
+            boolean nextDecimal = false;
+            for (Bus bus : buses) {
+                if (Math.abs(bus.Pl) > decimal || Math.abs(bus.Pg) > decimal || Math.abs(bus.Ql) > decimal || Math.abs(bus.Qg) > decimal) {
+                    nextDecimal = true;
+                    break;
+                }
+            }
+            if (!nextDecimal)
+                break;
+            decimal *= 10;
+        }
+        for (Bus bus : buses) {
+            bus.Pl /= decimal;
+            bus.Pg /= decimal;
+            bus.Ql /= decimal;
+            bus.Qg /= decimal;
+        }
+        return 1.0 / decimal;
+    }
+
+    private void addSlackNode(List<Bus> buses, List<Line> lines) {
+        // Find possible positions
+        List<Integer> possiblePositions = new ArrayList<>();
+        for (int i = 0; i < buses.size(); i++)
+            if (buses.get(i).Pg > 0)
+                possiblePositions.add(i);
+
+        // No generators? Add all positions
+        if (possiblePositions.size() == 0) {
+            for (int i = 0; i < buses.size(); i++)
+                possiblePositions.add(i);
+        }
+
+        // Choose position in grid
+        int position = possiblePositions.get(random.nextInt(possiblePositions.size()));
+
+        // Create slack node
+        Bus slack = new Bus();
+        slack.voltage = 1;
+        slack.delta = 0;
+        slack.type = BusType.Slack;
+
+        buses.add(slack);
+
+        // Create short line
+        Line line = new Line();
+        line.from = position;
+        line.to = buses.indexOf(slack);
+        line.R = 0.01938;
+        line.X = 0.1;
+        line.B_2 = 0.1;
+        line.a = 1;
+
+        lines.add(line);
+    }
+
+    private void debugShowResultAsMessageBox(PowerFlowProblem problem, SolverResult result) {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            final String utf8 = StandardCharsets.UTF_8.name();
+            try (PrintStream ps = new PrintStream(baos, true, utf8)) {
+                result.printTo(problem, ps);
+            }
+            String data = baos.toString(utf8);
+            JOptionPane.showMessageDialog(null, data,"Debug output", JOptionPane.INFORMATION_MESSAGE);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void updateGrid(Grid grid, PowerFlowProblem problem, SolverResult result) {
+        double currentScale = 1;
+        for (int i = 0; i < problem.buses.length; i++) {
+            GridNode node = (GridNode)problem.buses[i].tag;
+            if (node == null)
+                continue;
+
+            node.setVoltage(result.voltages[i].real / problem.scaleVoltage);
+            node.setCurrent(result.flowData.busCurrent[i] / currentScale);
+        }
+
+        for (int i = 0; i < problem.lines.length; i++) {
+            GridEdge edge = (GridEdge)problem.lines[i].tag;
+            if (edge == null)
+                continue;
+
+            edge.setPowerFlow(result.flowData.linePower[i].multiply(1 / problem.scalePower));
+            edge.setLineLoss(result.flowData.linePowerLoss[i].multiply(1 / problem.scalePower));
+            edge.setCurrent(result.flowData.lineCurrent[i] / currentScale);
+        }
+    }
+}

+ 47 - 0
src/holeg/model/Grid.java

@@ -0,0 +1,47 @@
+package holeg.model;
+
+import java.util.*;
+
+public class Grid {
+    private List<GridNode> nodes = new ArrayList<>();
+    private Set<GridEdge> edges = new HashSet<>();
+    private HashMap<GridNode, List<GridEdge>> edgesPerNode = new HashMap<>();
+
+    public void addNode(GridNode node) {
+        if (node == null)
+            throw new IllegalArgumentException("node is null");
+        nodes.add(node);
+    }
+
+    private void addEdgeToMap(GridNode node, GridEdge edge) {
+        List<GridEdge> edges = edgesPerNode.getOrDefault(node, new ArrayList<>());
+        edges.add(edge);
+        edgesPerNode.put(node, edges);
+    }
+
+    public void addEdge(GridEdge edge) {
+        if (edge == null)
+            throw new IllegalArgumentException("edge is null");
+
+        if (edges.contains(edge))
+            return;
+        edges.add(edge);
+
+        addEdgeToMap(edge.getFrom(), edge);
+        addEdgeToMap(edge.getTo(), edge);
+    }
+
+    public List<GridNode> getNodes() {
+        return Collections.unmodifiableList(nodes);
+    }
+
+    public Set<GridEdge> getEdges() {
+        return Collections.unmodifiableSet(edges);
+    }
+
+    public List<GridEdge> getEdges(GridNode node) {
+        if (edgesPerNode.containsKey(node))
+            return Collections.unmodifiableList(edgesPerNode.get(node));
+        return Collections.emptyList();
+    }
+}

+ 14 - 0
src/holeg/model/GridEdge.java

@@ -0,0 +1,14 @@
+package holeg.model;
+
+import holeg.power_flow.ComplexNumber;
+
+public interface GridEdge {
+    GridNode getFrom();
+    GridNode getTo();
+
+    double getLengthKilometers();
+
+    void setCurrent(double current);
+    void setPowerFlow(ComplexNumber power);
+    void setLineLoss(ComplexNumber loss);
+}

+ 15 - 0
src/holeg/model/GridNode.java

@@ -0,0 +1,15 @@
+package holeg.model;
+
+import holeg.power_flow.ComplexNumber;
+
+import java.util.List;
+
+public interface GridNode {
+    List<GridEdge> getEdges();
+
+    ComplexNumber getPowerConsumption();
+    ComplexNumber getPowerGeneration();
+
+    void setVoltage(double voltage);
+    void setCurrent(double current);
+}

+ 52 - 0
src/holeg/power_flow/AdmittanceMatrix.java

@@ -0,0 +1,52 @@
+package holeg.power_flow;
+
+import Jama.Matrix;
+
+public class AdmittanceMatrix extends ComplexMatrix {
+    public AdmittanceMatrix(int size) {
+        super(size);
+    }
+
+    public static AdmittanceMatrix build(Bus[] buses, Line[] lines) {
+        AdmittanceMatrix matrix = new AdmittanceMatrix(buses.length);
+
+        // Convert line admittances to complex numbers
+        ComplexNumber[] y = new ComplexNumber[lines.length];
+        for (int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+
+            y[i] = (new ComplexNumber(line.R, line.X)).reciprocal();
+        }
+
+        for (int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+            ComplexNumber value = matrix.get(line.from, line.to).subtract(y[i].multiply(1 / line.a));
+            matrix.set(line.from, line.to, value);
+            matrix.set(line.to, line.from, value);
+        }
+
+        for (int i = 0; i < buses.length; i++) {
+            for (int j = 0; j < lines.length; j++) {
+                Line line = lines[j];
+                ComplexNumber yLine = y[j];
+                ComplexNumber b = new ComplexNumber(0, line.B_2);
+
+                ComplexNumber Y = matrix.get(i, i);
+                if (line.from == i)
+                    Y = Y.add(yLine.multiply(1 / (line.a * line.a))).add(b);
+                else if (line.to == i)
+                    Y = Y.add(yLine).add(b);
+                matrix.set(i, i, Y);
+            }
+        }
+        return matrix;
+    }
+
+    public Matrix getG() {
+        return getRealMatrix();
+    }
+
+    public Matrix getB() {
+        return getImaginaryMatrix();
+    }
+}

+ 30 - 0
src/holeg/power_flow/Bus.java

@@ -0,0 +1,30 @@
+package holeg.power_flow;
+
+public class Bus {
+    public double voltage;
+    public double delta;
+    public double Pg;
+    public double Qg;
+    public double Pl;
+    public double Ql;
+    public double Qmin;
+    public double Qmax;
+    public BusType type;
+    public Object tag;
+
+    public Bus() {
+
+    }
+
+    public Bus(double voltage, double delta, double pg, double qg, double pl, double ql, double qmin, double qmax, BusType type) {
+        this.voltage = voltage;
+        this.delta = delta;
+        this.Pg = pg / 100;
+        this.Qg = qg / 100;
+        this.Pl = pl / 100;
+        this.Ql = ql / 100;
+        this.Qmin = qmin / 100;
+        this.Qmax = qmax / 100;
+        this.type = type;
+    }
+}

+ 7 - 0
src/holeg/power_flow/BusType.java

@@ -0,0 +1,7 @@
+package holeg.power_flow;
+
+public enum BusType {
+    Slack,
+    PV,
+    PQ,
+}

+ 157 - 0
src/holeg/power_flow/ComplexMatrix.java

@@ -0,0 +1,157 @@
+package holeg.power_flow;
+
+import Jama.Matrix;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+
+public class ComplexMatrix {
+    private final int width;
+    private final int height;
+    private final ComplexNumber[] data;
+
+    public ComplexMatrix(int size) {
+        this(size, size);
+    }
+
+    public ComplexMatrix(int width, int height) {
+        if (width <= 0)
+            throw new IllegalArgumentException("width is invalid");
+        if (height <= 0)
+            throw new IllegalArgumentException("height is invalid");
+
+        this.width = width;
+        this.height = height;
+        this.data = new ComplexNumber[width * height];
+        Arrays.fill(this.data, ComplexNumber.Zero);
+    }
+
+    public static ComplexMatrix sameSizeAs(ComplexMatrix other) {
+        return new ComplexMatrix(other.getWidth(), other.getHeight());
+    }
+
+    public static ComplexMatrix fromArray(ComplexNumber[] numbers) {
+        if (numbers == null)
+            throw new IllegalArgumentException("number is null");
+        if (numbers.length == 0)
+            throw new IllegalArgumentException("number is empty");
+
+        ComplexMatrix out = new ComplexMatrix(numbers.length, 1);
+        for (int i = 0; i < numbers.length; i++)
+            out.uncheckedSet(i, 0, numbers[i]);
+        return out;
+    }
+
+    public int getWidth() {
+        return width;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    private ComplexNumber uncheckedGet(int row, int column) {
+        return data[column * width + row];
+    }
+
+    public ComplexNumber get(int row, int column) {
+        if (row < 0 || row >= width)
+            throw new IllegalArgumentException("row is invalid");
+        if (column < 0 || column >= height)
+            throw new IllegalArgumentException("column is invalid");
+        return uncheckedGet(row, column);
+    }
+
+    public void set(int row, int column, double realValue) {
+        set(row, column, new ComplexNumber(realValue, 0));
+    }
+
+    private void uncheckedSet(int row, int column, ComplexNumber value) {
+        data[column * width + row] = value;
+    }
+
+    public void set(int row, int column, ComplexNumber value) {
+        if (row < 0 || row >= width)
+            throw new IllegalArgumentException("row is invalid");
+        if (column < 0 || column >= height)
+            throw new IllegalArgumentException("column is invalid");
+        if (value == null)
+            throw new IllegalArgumentException("value is null");
+        uncheckedSet(row, column, value);
+    }
+
+    public Matrix getRealMatrix() {
+        Matrix m = new Matrix(getWidth(), getHeight());
+        for (int i = 0; i < getWidth(); i++)
+            for (int j = 0; j < getHeight(); j++)
+                m.set(i, j, uncheckedGet(i, j).real);
+        return m;
+    }
+
+    public Matrix getImaginaryMatrix() {
+        Matrix m = new Matrix(getWidth(), getHeight());
+        for (int i = 0; i < getWidth(); i++)
+            for (int j = 0; j < getHeight(); j++)
+                m.set(i, j, uncheckedGet(i, j).imaginary);
+        return m;
+    }
+
+    public ComplexMatrix add(ComplexMatrix other) {
+        if (getWidth() != other.getWidth())
+            throw new IllegalArgumentException("width does not match");
+        if (getHeight() != other.getHeight())
+            throw new IllegalArgumentException("height does not match");
+        ComplexMatrix out = new ComplexMatrix(getWidth(), getHeight());
+        for (int i = 0; i < getWidth(); i++)
+            for (int j = 0; j < getHeight(); j++)
+                out.uncheckedSet(i, j, uncheckedGet(i, j).add(other.uncheckedGet(i, j)));
+        return out;
+    }
+
+    public ComplexMatrix subtract(ComplexMatrix other) {
+        if (getWidth() != other.getWidth())
+            throw new IllegalArgumentException("width does not match");
+        if (getHeight() != other.getHeight())
+            throw new IllegalArgumentException("height does not match");
+        ComplexMatrix out = new ComplexMatrix(getWidth(), getHeight());
+        for (int i = 0; i < getWidth(); i++)
+            for (int j = 0; j < getHeight(); j++)
+                out.uncheckedSet(i, j, uncheckedGet(i, j).subtract(other.uncheckedGet(i, j)));
+        return out;
+    }
+
+    public ComplexMatrix multiply(ComplexNumber[] numbers) {
+        return multiply(ComplexMatrix.fromArray(numbers));
+    }
+
+    public ComplexMatrix multiply(ComplexMatrix other) {
+        if (getHeight() != other.getWidth())
+            throw new IllegalArgumentException("height does not match other width");
+
+        ComplexMatrix out = new ComplexMatrix(getWidth(), other.getHeight());
+        for (int i = 0; i < getWidth(); i++) {
+            for (int j = 0; j < other.getHeight(); j++) {
+                ComplexNumber sum = ComplexNumber.Zero;
+
+                for (int k = 0; k < getHeight(); k++) {
+                    ComplexNumber a = uncheckedGet(i, k);
+                    ComplexNumber b = other.uncheckedGet(k, j);
+                    sum = sum.add(a.multiply(b));
+                }
+                out.uncheckedSet(i, j, sum);
+            }
+        }
+        return out;
+    }
+
+    public void printTo(PrintStream stream) {
+        if (stream == null)
+            throw new IllegalArgumentException("stream is null");
+
+        for (int row = 0; row < getWidth(); row++) {
+            for (int col = 0; col < getHeight(); col++)
+                stream.printf("%s, ", uncheckedGet(row, col).toString());
+            stream.println();
+        }
+    }
+}

+ 79 - 0
src/holeg/power_flow/ComplexNumber.java

@@ -0,0 +1,79 @@
+package holeg.power_flow;
+
+public class ComplexNumber {
+    public final double real;
+    public final double imaginary;
+
+    public final static ComplexNumber Zero = new ComplexNumber(0, 0);
+    public final static ComplexNumber One = new ComplexNumber(1, 0);
+    public final static ComplexNumber j = new ComplexNumber(0, -1);
+
+    public ComplexNumber(double real) {
+        this.real = real;
+        this.imaginary = 0;
+    }
+
+    public ComplexNumber(double real, double imaginary) {
+        this.real = real;
+        this.imaginary = imaginary;
+    }
+
+    public static ComplexNumber fromPolar(double magnitude, double angle) {
+        return new ComplexNumber(magnitude * Math.cos(angle), magnitude * Math.sin(angle));
+    }
+
+    public double lenSquared() {
+        return real * real + imaginary * imaginary;
+    }
+
+    public double len() {
+        return Math.sqrt(lenSquared());
+    }
+
+    public double angle() {
+        return Math.atan2(imaginary, real);
+    }
+
+    public ComplexNumber add(ComplexNumber other) {
+        return new ComplexNumber(real + other.real, imaginary + other.imaginary);
+    }
+
+    public ComplexNumber subtract(ComplexNumber other) {
+        return new ComplexNumber(real - other.real, imaginary - other.imaginary);
+    }
+
+    public ComplexNumber multiply(double scalar) {
+        return new ComplexNumber(real * scalar, imaginary * scalar);
+    }
+
+    public ComplexNumber multiply(ComplexNumber other) {
+        return new ComplexNumber(real * other.real - imaginary * other.imaginary, imaginary * other.real + real * other.imaginary);
+    }
+
+    public ComplexNumber conjugate() {
+        return new ComplexNumber(real, -imaginary);
+    }
+
+    public ComplexNumber reciprocal() {
+        return new ComplexNumber(real / lenSquared(), -imaginary / lenSquared());
+    }
+
+    public ComplexNumber divide(ComplexNumber other) {
+        return multiply(other.reciprocal());
+    }
+
+    public ComplexNumber negate() {
+        return new ComplexNumber(-real, -imaginary);
+    }
+
+    public boolean isZero() {
+        return Math.abs(real) < 0.0001 && Math.abs(imaginary) < 0.0001;
+    }
+
+    @Override
+    public String toString() {
+        if (imaginary >= 0)
+            return String.format("%.4f + %.4fj", real, imaginary);
+        return String.format("%.4f - %.4fj", real, Math.abs(imaginary));
+    }
+}

+ 54 - 0
src/holeg/power_flow/FlowCalculation.java

@@ -0,0 +1,54 @@
+package holeg.power_flow;
+
+public class FlowCalculation {
+    public static FlowData calculate(AdmittanceMatrix admittanceMatrix, Bus[] buses, Line[] lines, ComplexNumber[] voltages) {
+        FlowData result = new FlowData();
+
+        // Current calculation I = U / R; R = 1 / Y => I = Y * U
+        ComplexMatrix busCurrent = admittanceMatrix.multiply(voltages);
+
+        // Calculate line currents
+        ComplexMatrix lineCurrent = ComplexMatrix.sameSizeAs(admittanceMatrix);
+        for (int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+
+            // Calculate current by multiplying the voltage dropped between the line and the admittance
+            ComplexNumber voltageDrop = voltages[line.from].subtract(voltages[line.to]);
+            ComplexNumber current = voltageDrop.multiply(admittanceMatrix.get(line.to, line.from));
+
+            // Symmetrical
+            lineCurrent.set(line.to, line.from, current.negate());
+            lineCurrent.set(line.from, line.to, current);
+        }
+
+        // Power calculation (P = U * I)
+        ComplexMatrix linePower = ComplexMatrix.sameSizeAs(lineCurrent);
+        for (int i = 0; i < linePower.getWidth(); i++)
+            for (int j = 0; j < linePower.getHeight(); j++)
+                if (i != j)
+                    linePower.set(i, j, voltages[i].multiply(lineCurrent.get(i, j).conjugate()));
+
+        // Create result and calculate losses
+        result.busCurrent = new double[buses.length];
+        result.lineCurrent = new double[lines.length];
+        result.busInjection = new ComplexNumber[buses.length];
+        result.linePower = new ComplexNumber[lines.length];
+        result.linePowerLoss = new ComplexNumber[lines.length];
+
+        for (int i = 0; i < buses.length; i++) {
+            result.busCurrent[i] = busCurrent.get(i, 0).len();
+
+            // Power = Current * Voltage
+            result.busInjection[i] = busCurrent.get(i, 0).conjugate().multiply(voltages[i]);
+        }
+        for (int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+
+            result.lineCurrent[i] = lineCurrent.get(line.from, line.to).len();
+            result.linePower[i] = linePower.get(line.from, line.to).negate();
+            result.linePowerLoss[i] = linePower.get(line.from, line.to).add(linePower.get(line.to, line.from));
+        }
+
+        return result;
+    }
+}

+ 10 - 0
src/holeg/power_flow/FlowData.java

@@ -0,0 +1,10 @@
+package holeg.power_flow;
+
+public class FlowData {
+    public double[] busCurrent;
+    public double[] lineCurrent;
+
+    public ComplexNumber[] busInjection;
+    public ComplexNumber[] linePower;
+    public ComplexNumber[] linePowerLoss;
+}

+ 26 - 0
src/holeg/power_flow/Line.java

@@ -0,0 +1,26 @@
+package holeg.power_flow;
+
+public class Line {
+    public int from;
+    public int to;
+
+    public double R;
+    public double X;
+    public double B_2;
+    public double a;
+
+    public Object tag;
+
+    public Line() {
+
+    }
+
+    public Line(int from, int to, double r, double x, double b_2, double a) {
+        this.from = from - 1;
+        this.to = to - 1;
+        this.R = r;
+        this.X = x;
+        this.B_2 = b_2;
+        this.a = a;
+    }
+}

+ 235 - 0
src/holeg/power_flow/NewtonRaphsonSolver.java

@@ -0,0 +1,235 @@
+package holeg.power_flow;
+
+import Jama.LUDecomposition;
+import Jama.Matrix;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+
+public class NewtonRaphsonSolver implements Solver {
+    @Override
+    public SolverResult solve(PowerFlowProblem problem, SolverSettings settings) {
+        if (problem == null)
+            throw new IllegalArgumentException("problem is null");
+        if (settings == null)
+            throw new IllegalArgumentException("settings is null");
+
+        try {
+            long timeStart = System.nanoTime();
+
+            // Check data first
+            if (!problem.checkForValidData())
+                return SolverResult.error(SolverError.InvalidData);
+
+            // Create performance monitor
+            SolverPerformance performance = new SolverPerformance();
+
+            // Build admittance matrix
+            AdmittanceMatrix admittanceMatrix = AdmittanceMatrix.build(problem.buses, problem.lines);
+            Matrix G_bus = admittanceMatrix.getG();
+            Matrix B_bus = admittanceMatrix.getB();
+
+            // Copy data over to do math with it
+            int busCount = problem.buses.length;
+            double[] V = new double[busCount];
+            double[] delta = new double[busCount];
+            double[] Pset = new double[busCount];
+            double[] Qset = new double[busCount];
+            for (int i = 0; i < busCount; i++) {
+                V[i] = problem.buses[i].voltage;
+                delta[i] = problem.buses[i].delta;
+                Pset[i] = problem.buses[i].Pg - problem.buses[i].Pl; // generation - load
+                Qset[i] = problem.buses[i].Qg - problem.buses[i].Ql;
+            }
+
+            // Find PQ/PV nodes
+            int[] pq = Util.indicesOf(problem.buses, BusType.PQ);
+            int[] pv = Util.indicesOf(problem.buses, BusType.PV);
+
+            // Main solver iteration
+            double tolerance = Double.MAX_VALUE;
+
+            // Jacobian caching
+            Matrix inverseJ = Matrix.identity(1, 1);
+            double factor = 1;
+            int jacobianRecalculation = 0;
+
+            for (int iter = 1; iter <= settings.maxIterations; iter++) {
+                // Calculate current P/Q values
+                double[] P = new double[busCount];
+                double[] Q = new double[busCount];
+
+                for (int i = 0; i < busCount; i++) {
+                    for (int j = 0; j < busCount; j++) {
+                        P[i] += V[i] * V[j] * (G_bus.get(i, j) * Math.cos(delta[i] - delta[j]) + B_bus.get(i, j) * Math.sin(delta[i] - delta[j]));
+                        Q[i] += V[i] * V[j] * (G_bus.get(i, j) * Math.sin(delta[i] - delta[j]) - B_bus.get(i, j) * Math.cos(delta[i] - delta[j]));
+                    }
+                }
+
+                // Reactive power limit
+                if (iter > 2 && iter < 20) {
+                    for (int i = 0; i < pv.length; i++) {
+                        Bus bus = problem.buses[i];
+                        double q = Q[i] + bus.Ql;
+                        if (q < bus.Qmin)
+                            V[i] += 0.01;
+                        else if (q > bus.Qmax)
+                            V[i] -= 0.01;
+                    }
+                }
+
+                // Calculate error given by delta between set values and current values
+                double[] dP = Util.subtract(Pset, P);
+                double[] dQ = Util.subtract(Qset, Q);
+
+                // Create error vector
+                double[] error = new double[busCount - 1 + pq.length];
+                for (int i = 0; i < busCount - 1; i++)
+                    error[i] = dP[i + 1];
+                for (int i = 0; i < pq.length; i++)
+                    error[i + busCount - 1] = dQ[pq[i]];
+
+                // Update performance
+                performance.iterations = iter;
+                performance.error = tolerance;
+
+                // Calculate mismatch between values excepted and current values
+                tolerance = Arrays.stream(error).map(Math::abs).max().orElse(0);
+                if (tolerance < settings.minError)
+                    break;
+
+                if (jacobianRecalculation <= 0) {
+                    // Jacobian matrix J1
+                    Matrix J1 = new Matrix(busCount - 1, busCount - 1);
+                    for (int i = 0; i < busCount - 1; i++) {
+                        int m = i + 1;
+                        for (int k = 0; k < busCount - 1; k++) {
+                            int n = k + 1;
+                            if (n == m) {
+                                for (int l = 0; l < busCount - 1; l++)
+                                    J1.set(i, k, J1.get(i, k) + V[m] * V[l] * (-G_bus.get(m, l) * Math.sin(delta[m] - delta[l]) + B_bus.get(m, l) * Math.cos(delta[m] - delta[l])));
+                                J1.set(i, k, J1.get(i, k) - Math.pow(V[m], 2) * B_bus.get(m, m));
+                            } else
+                                J1.set(i, k, V[m] * V[n] * (G_bus.get(m, n) * Math.sin(delta[m] - delta[n]) - B_bus.get(m, n) * Math.cos(delta[m] - delta[n])));
+                        }
+                    }
+
+                    // Jacobian matrix J2
+                    Matrix J2 = new Matrix(busCount - 1, pq.length);
+                    for (int i = 0; i < busCount - 1; i++) {
+                        int m = i + 1;
+                        for (int k = 0; k < pq.length; k++) {
+                            int n = pq[k];
+                            if (n == m) {
+                                for (int l = 0; l < busCount; l++)
+                                    J2.set(i, k, J2.get(i, k) + V[l] * (G_bus.get(m, l) * Math.cos(delta[m] - delta[l]) + B_bus.get(m, l) * Math.sin(delta[m] - delta[l])));
+                                J2.set(i, k, J2.get(i, k) + V[m] * G_bus.get(m, m));
+                            } else
+                                J2.set(i, k, V[m] * (G_bus.get(m, n) * Math.cos(delta[m] - delta[n]) + B_bus.get(m, n) * Math.sin(delta[m] - delta[n])));
+                        }
+                    }
+
+                    // Jacobian matrix J3
+                    Matrix J3 = new Matrix(pq.length, busCount - 1);
+                    for (int i = 0; i < pq.length; i++) {
+                        int m = pq[i];
+                        for (int k = 0; k < busCount - 1; k++) {
+                            int n = k + 1;
+                            if (n == m) {
+                                for (int l = 0; l < busCount; l++)
+                                    J3.set(i, k, J3.get(i, k) + V[m] * V[l] * (G_bus.get(m, l) * Math.cos(delta[m] - delta[l]) + B_bus.get(m, l) * Math.sin(delta[m] - delta[l])));
+                                J3.set(i, k, J3.get(i, k) - Math.pow(V[m], 2) * G_bus.get(m, m));
+                            } else
+                                J3.set(i, k, V[m] * V[n] * (-G_bus.get(m, n) * Math.cos(delta[m] - delta[n]) - B_bus.get(m, n) * Math.sin(delta[m] - delta[n])));
+                        }
+                    }
+
+                    // Jacobian matrix J4
+                    Matrix J4 = new Matrix(pq.length, pq.length);
+                    for (int i = 0; i < pq.length; i++) {
+                        int m = pq[i];
+                        for (int k = 0; k < pq.length; k++) {
+                            int n = pq[k];
+                            if (n == m) {
+                                for (int l = 0; l < busCount; l++)
+                                    J4.set(i, k, J4.get(i, k) + V[l] * (G_bus.get(m, l) * Math.sin(delta[m] - delta[l]) - B_bus.get(m, l) * Math.cos(delta[m] - delta[l])));
+                                J4.set(i, k, J4.get(i, k) - V[m] * B_bus.get(m, m));
+                            } else
+                                J4.set(i, k, V[m] * (G_bus.get(m, n) * Math.sin(delta[m] - delta[n]) - B_bus.get(m, n) * Math.cos(delta[m] - delta[n])));
+                        }
+                    }
+
+                    // Construct matrix like this:
+                    // J1 | J2
+                    // -------
+                    // J3 | J4
+                    Matrix J12 = Util.concatColumns(J1, J2);
+                    Matrix J34 = Util.concatColumns(J3, J4);
+                    Matrix J = Util.concatRows(J12, J34);
+
+                    // Inverse of matrix
+                    try {
+                        LUDecomposition lu = new LUDecomposition(J);
+                        inverseJ = lu.solve(Matrix.identity(J.getRowDimension(), J.getColumnDimension()));
+                    }
+                    catch (Exception e) {
+                        for (int row = 0; row < J.getRowDimension(); row++) {
+                            for (int col = 0; col < J.getColumnDimension(); col++)
+                                System.out.printf("%f ", J.get(col, row));
+                            System.out.println(";");
+                        }
+                        throw e;
+                    }
+
+                    // Try to find good factor to slow down convergence
+                    factor = 0.1;
+                    try {
+                        double[] eigenvalues = inverseJ.eig().getRealEigenvalues();
+                        double maxEigenvalue = Arrays.stream(eigenvalues).map(Math::abs).max().orElse(0);
+
+                        // Will only convergence when eigenvalues of matrix are < 1
+                        if (maxEigenvalue > 1)
+                            factor = 1 / maxEigenvalue;
+                    } catch (Exception unused) {
+                        // ignored
+                    }
+
+                    jacobianRecalculation = settings.jacobianRecalculationInterval;
+                }
+                else
+                    jacobianRecalculation--;
+
+                // Newton step
+                Matrix x = inverseJ.times(Util.fromArray(error));
+
+                // Update current state (delta, voltage)
+                for (int i = 0; i < busCount - 1; i++)
+                    delta[i + 1] += x.get(i, 0) * factor;
+                for (int i = 0; i < pq.length; i++)
+                    V[pq[i]] += x.get(i + busCount - 1, 0) * factor;
+            }
+
+            // Tolerance is to high
+            if (tolerance >= settings.maxError)
+                return SolverResult.error(SolverError.ConvergenceError);
+
+            // Complex voltages
+            ComplexNumber[] voltages = new ComplexNumber[V.length];
+            for (int i = 0; i < V.length; i++)
+                voltages[i] = ComplexNumber.fromPolar(V[i], delta[i]);
+
+            // Calculate flows
+            FlowData flowData = FlowCalculation.calculate(admittanceMatrix, problem.buses, problem.lines, voltages);
+
+            // Save finished time
+            performance.timeInMilliseconds = (System.nanoTime() - timeStart) / 1e6;
+
+            // Return success
+            return SolverResult.success(performance, voltages, flowData);
+        }
+        catch(Exception e) {
+            e.printStackTrace();
+        }
+        return SolverResult.error(SolverError.Internal);
+    }
+}

+ 67 - 0
src/holeg/power_flow/PowerFlowProblem.java

@@ -0,0 +1,67 @@
+package holeg.power_flow;
+
+public class PowerFlowProblem {
+    public final Bus[] buses;
+    public final Line[] lines;
+    public final double scaleVoltage;
+    public final double scalePower;
+
+    public PowerFlowProblem(Bus[] buses, Line[] lines, double scaleVoltage, double scalePower) {
+        if (buses == null)
+            throw new IllegalArgumentException("buses is null");
+        if (lines == null)
+            throw new IllegalArgumentException("lines is null");
+        this.buses = buses;
+        this.lines = lines;
+        this.scaleVoltage = scaleVoltage;
+        this.scalePower = scalePower;
+    }
+
+    public boolean checkForValidData() {
+        // Check per unit scale
+        if (scaleVoltage <= 0)
+            return false;
+        if (scalePower <= 0)
+            return false;
+
+        // Check buses
+        boolean slackBusFound = false;
+        for (int i = 0; i < buses.length; i++) {
+            Bus bus = buses[i];
+            if (bus == null)
+                return false;
+
+            // Invalid voltage
+            if (bus.voltage <= 0)
+                return false;
+
+            // Check for multiple slack buses
+            if (bus.type == BusType.Slack) {
+                if (slackBusFound)
+                    return false;
+                slackBusFound = true;
+            }
+        }
+
+        // No slack bus
+        if (!slackBusFound)
+            return false;
+
+        // Check lines
+        for (int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+            if (line == null)
+                return false;
+
+            // Invalid index of to/from
+            if (line.to < 0 || line.to >= buses.length)
+                return false;
+            if (line.from < 0 || line.from >= buses.length)
+                return false;
+            // Invalid values for R and X
+            if (line.R < 0 || line.X < 0)
+                return false;
+        }
+        return true;
+    }
+}

+ 19 - 0
src/holeg/power_flow/PowerFlowProgram.java

@@ -0,0 +1,19 @@
+package holeg.power_flow;
+
+public class PowerFlowProgram {
+    public static void main(String[] args) {
+        // Load data
+        Bus[] buses = TestData.getIEEE14BussesHugeGeneration();
+        Line[] lines = TestData.getIEEE14Lines();
+
+        // Create problem and solver
+        PowerFlowProblem problem = new PowerFlowProblem(buses, lines, 1, 1 / 100.0);
+        Solver solver = new NewtonRaphsonSolver();
+
+        // Solve the problem with default settings
+        SolverResult result = solver.solve(problem, new SolverSettings());
+
+        // Print result
+        result.printTo(problem, System.out);
+    }
+}

+ 5 - 0
src/holeg/power_flow/Solver.java

@@ -0,0 +1,5 @@
+package holeg.power_flow;
+
+public interface Solver {
+    SolverResult solve(PowerFlowProblem problem, SolverSettings settings);
+}

+ 8 - 0
src/holeg/power_flow/SolverError.java

@@ -0,0 +1,8 @@
+package holeg.power_flow;
+
+public enum SolverError {
+    None,
+    Internal,
+    InvalidData,
+    ConvergenceError
+}

+ 7 - 0
src/holeg/power_flow/SolverPerformance.java

@@ -0,0 +1,7 @@
+package holeg.power_flow;
+
+public class SolverPerformance {
+    public int iterations;
+    public double error;
+    public double timeInMilliseconds;
+}

+ 79 - 0
src/holeg/power_flow/SolverResult.java

@@ -0,0 +1,79 @@
+package holeg.power_flow;
+
+import java.io.PrintStream;
+
+public class SolverResult {
+    public final boolean solved;
+    public final SolverError error;
+    public final SolverPerformance performance;
+    public final ComplexNumber[] voltages;
+    public final FlowData flowData;
+
+    private SolverResult(boolean solved, SolverError error, SolverPerformance performance, ComplexNumber[] voltages, FlowData flowData) {
+        this.solved = solved;
+        this.error = error;
+        this.performance = performance;
+        this.voltages = voltages;
+        this.flowData = flowData;
+    }
+
+    public static SolverResult success(SolverPerformance performance, ComplexNumber[] voltages, FlowData flowData) {
+        if (performance == null)
+            throw new IllegalArgumentException("performance is null");
+        if (voltages == null)
+            throw new IllegalArgumentException("voltages is null");
+        if (flowData == null)
+            throw new IllegalArgumentException("flowData is null");
+        return new SolverResult(true, SolverError.None, performance, voltages, flowData);
+    }
+
+    public static SolverResult error(SolverError error) {
+        if (error == SolverError.None)
+            return error(SolverError.Internal);
+        return new SolverResult(false, error, null,null, null);
+    }
+
+    public void printTo(PowerFlowProblem problem, PrintStream stream) {
+        if (problem == null)
+            throw new IllegalArgumentException("problem is null");
+        if (stream == null)
+            throw new IllegalArgumentException("stream is null");
+
+        // Print error message
+        if (!solved) {
+            stream.println("Solver could not solve problem: " + error.name());
+            return;
+        }
+
+        // Print data
+        stream.printf("Solver solved problem! Took %d iterations in %.2f ms", performance.iterations, performance.timeInMilliseconds);
+        stream.println();
+
+        stream.printf("Error: %f", performance.error);
+        stream.println();
+
+        // Buses
+        for (int i = 0; i < voltages.length; i++) {
+            stream.printf("[Bus %2d %s] Voltage: %5.4f Vpu (%4.2f°), Power: %4.2f MW",
+                    i,
+                    problem.buses[i].type.name(),
+                    voltages[i].len(),
+                    Math.toDegrees(voltages[i].angle()),
+                    flowData.busInjection[i].real / problem.scalePower);
+            stream.println();
+        }
+
+        // Lines
+        for (int i = 0; i < problem.lines.length; i++) {
+            Line line = problem.lines[i];
+            stream.printf("[Line %2d -> %2d] Power: %5.3f MW, %5.3f Mvar, Loss: %5.3f MW, %5.3f Mvar",
+                    line.from,
+                    line.to,
+                    flowData.linePower[i].real / problem.scalePower,
+                    flowData.linePower[i].imaginary / problem.scalePower,
+                    flowData.linePowerLoss[i].real / problem.scalePower,
+                    flowData.linePowerLoss[i].imaginary / problem.scalePower);
+            stream.println();
+        }
+    }
+}

+ 15 - 0
src/holeg/power_flow/SolverSettings.java

@@ -0,0 +1,15 @@
+package holeg.power_flow;
+
+public class SolverSettings {
+    public int maxIterations;
+    public double minError;
+    public double maxError;
+    public int jacobianRecalculationInterval;
+
+    public SolverSettings() {
+        maxIterations = 1000;
+        minError = 1e-4;
+        maxError = minError * 10;
+        jacobianRecalculationInterval = 4;
+    }
+}

+ 127 - 0
src/holeg/power_flow/TestData.java

@@ -0,0 +1,127 @@
+package holeg.power_flow;
+
+public class TestData {
+    public static Bus[] getIEEE14Busses() {
+        return new Bus[]{
+                new Bus(1.06, 0, 0, 0, 0, 0, 0, 0, BusType.Slack),
+                new Bus(1.045, 0, 40, 42.4, 21.7, 12.7, -40, 50, BusType.PV),
+                new Bus(1.010, 0, 0, 23.4, 94.2, 19.0, 0, 40, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 47.8, -3.9, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 7.6, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.070, 0, 0, 12.2, 11.2, 7.5, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 0.0, 0.0, 0, 0, BusType.PQ),
+                new Bus(1.090, 0, 0, 17.4, 0.0, 0.0, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 29.5, 16.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 9.0, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 3.5, 1.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 6.1, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 13.5, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 14.9, 5.0, 0, 0, BusType.PQ)
+        };
+    }
+
+    public static Bus[] getIEEE14BussesSameVoltages() {
+        return new Bus[]{
+                new Bus(1.0, 0, 0, 0, 0, 0, 0, 0, BusType.Slack),
+                new Bus(1.0, 0, 40, 42.4, 21.7, 12.7, -40, 50, BusType.PV),
+                new Bus(1.0, 0, 0, 23.4, 94.2, 19.0, 0, 40, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 47.8, -3.9, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 7.6, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 12.2, 11.2, 7.5, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 0.0, 0.0, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 17.4, 0.0, 0.0, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 29.5, 16.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 9.0, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 3.5, 1.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 6.1, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 13.5, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 14.9, 5.0, 0, 0, BusType.PQ)
+        };
+    }
+
+    public static Bus[] getIEEE14BussesNoGeneration() {
+        return new Bus[]{
+                new Bus(1.0, 0, 0, 0, 0, 0, 0, 0, BusType.Slack),
+                new Bus(1.0, 0, 0, 0, 21.7, 12.7, -40, 50, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 94.2, 19.0, 0, 40, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 47.8, -3.9, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 7.6, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 12.2, 11.2, 7.5, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 0.0, 0.0, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 17.4, 0.0, 0.0, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 29.5, 16.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 9.0, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 3.5, 1.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 6.1, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 13.5, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 14.9, 5.0, 0, 0, BusType.PQ)
+        };
+    }
+
+    public static Bus[] getIEEE14BussesHugeGeneration() {
+        return new Bus[]{
+                new Bus(1.06, 0, 0, 0, 0, 0, 0, 0, BusType.Slack),
+                new Bus(1.045, 0, 40, 42.4, 21.7, 12.7, -40, 50, BusType.PV),
+                new Bus(1.010, 0, 100, 23.4, 94.2, 19.0, 0, 40, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 47.8, -3.9, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 7.6, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.070, 0, 0, 12.2, 11.2, 7.5, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 0.0, 0.0, 0, 0, BusType.PQ),
+                new Bus(1.090, 0, 500, 17.4, 0.0, 0.0, -6, 24, BusType.PV),
+                new Bus(1.0, 0, 0, 0, 29.5, 16.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 9.0, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 3.5, 1.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 6.1, 1.6, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 13.5, 5.8, 0, 0, BusType.PQ),
+                new Bus(1.0, 0, 0, 0, 14.9, 5.0, 0, 0, BusType.PQ)
+        };
+    }
+
+    public static Line[] getIEEE14Lines() {
+        return new Line[]{
+                new Line(1, 2, 0.01938, 0.05917, 0.0264, 1),
+                new Line(1, 5, 0.05403, 0.22304, 0.0246, 1),
+                new Line(2, 3, 0.04699, 0.19797, 0.0219, 1),
+                new Line(2, 4, 0.05811, 0.17632, 0.0170, 1),
+                new Line(2, 5, 0.05695, 0.17388, 0.0173, 1),
+                new Line(3, 4, 0.06701, 0.17103, 0.0064, 1),
+                new Line(4, 5, 0.01335, 0.04211, 0.0, 1),
+                new Line(4, 7, 0.0, 0.20912, 0.0, 0.978),
+                new Line(4, 9, 0.0, 0.55618, 0.0, 0.969),
+                new Line(5, 6, 0.0, 0.25202, 0.0, 0.932),
+                new Line(6, 11, 0.09498, 0.19890, 0.0, 1),
+                new Line(6, 12, 0.12291, 0.25581, 0.0, 1),
+                new Line(6, 13, 0.06615, 0.13027, 0.0, 1),
+                new Line(7, 8, 0.0, 0.17615, 0.0, 1),
+                new Line(7, 9, 0.0, 0.11001, 0.0, 1),
+                new Line(9, 10, 0.03181, 0.08450, 0.0, 1),
+                new Line(9, 14, 0.12711, 0.27038, 0.0, 1),
+                new Line(10, 11, 0.08205, 0.19207, 0.0, 1),
+                new Line(12, 13, 0.22092, 0.19988, 0.0, 1),
+                new Line(13, 14, 0.17093, 0.34802, 0.0, 1)
+        };
+    }
+
+    public static Line[] getIEEE14LinesNoConnectionToSlack() {
+        return new Line[]{
+                new Line(2, 3, 0.04699, 0.19797, 0.0219, 1),
+                new Line(2, 4, 0.05811, 0.17632, 0.0170, 1),
+                new Line(2, 5, 0.05695, 0.17388, 0.0173, 1),
+                new Line(3, 4, 0.06701, 0.17103, 0.0064, 1),
+                new Line(4, 5, 0.01335, 0.04211, 0.0, 1),
+                new Line(4, 7, 0.0, 0.20912, 0.0, 0.978),
+                new Line(4, 9, 0.0, 0.55618, 0.0, 0.969),
+                new Line(5, 6, 0.0, 0.25202, 0.0, 0.932),
+                new Line(6, 11, 0.09498, 0.19890, 0.0, 1),
+                new Line(6, 12, 0.12291, 0.25581, 0.0, 1),
+                new Line(6, 13, 0.06615, 0.13027, 0.0, 1),
+                new Line(7, 8, 0.0, 0.17615, 0.0, 1),
+                new Line(7, 9, 0.0, 0.11001, 0.0, 1),
+                new Line(9, 10, 0.03181, 0.08450, 0.0, 1),
+                new Line(9, 14, 0.12711, 0.27038, 0.0, 1),
+                new Line(10, 11, 0.08205, 0.19207, 0.0, 1),
+                new Line(12, 13, 0.22092, 0.19988, 0.0, 1),
+                new Line(13, 14, 0.17093, 0.34802, 0.0, 1)
+        };
+    }
+}

+ 4 - 0
src/holeg/power_flow/TransmissionLineModel.java

@@ -0,0 +1,4 @@
+package holeg.power_flow;
+
+public class TransmissionLineModel {
+}

+ 78 - 0
src/holeg/power_flow/Util.java

@@ -0,0 +1,78 @@
+package holeg.power_flow;
+
+import Jama.Matrix;
+
+import java.util.ArrayList;
+
+public class Util {
+    public static double[] subtract(double[] a, double[] b) {
+        if (a == null)
+            throw new IllegalArgumentException("a is null");
+        if (b == null)
+            throw new IllegalArgumentException("b is null");
+        if (a.length != b.length)
+            throw new IllegalArgumentException("a size does not match b size");
+        double[] r = new double[a.length];
+        for (int i = 0; i < r.length; i++)
+            r[i] = a[i] - b[i];
+        return r;
+    }
+
+    public static int[] indicesOf(Bus[] array, BusType value) {
+        if (array == null)
+            throw new IllegalArgumentException("array is null");
+        if (array.length == 0)
+            return new int[0];
+
+        ArrayList<Integer> indices = new ArrayList<Integer>(array.length);
+        for (int i = 0; i < array.length; i++)
+            if (array[i].type.equals(value))
+                indices.add(i);
+        return indices.stream().mapToInt(i -> i).toArray();
+    }
+
+    public static Matrix fromArray(double[] array) {
+        if (array == null)
+            throw new IllegalArgumentException("array is null");
+        Matrix m = new Matrix(array.length, 1);
+        for (int i = 0; i < array.length; i++)
+            m.set(i, 0, array[i]);
+        return m;
+    }
+
+    public static Matrix concatColumns(Matrix a, Matrix b) {
+        if (a == null)
+            throw new IllegalArgumentException("a is null");
+        if (b == null)
+            throw new IllegalArgumentException("b is null");
+        if (a.getRowDimension() != b.getRowDimension())
+            throw new IllegalArgumentException("rows count do not match");
+
+        Matrix c = new Matrix(a.getRowDimension(), a.getColumnDimension() + b.getColumnDimension());
+        for (int i = 0; i < a.getRowDimension(); i++)
+            for (int j = 0; j < a.getColumnDimension(); j++)
+                c.set(i, j, a.get(i, j));
+        for (int i = 0; i < b.getRowDimension(); i++)
+            for (int j = 0; j < b.getColumnDimension(); j++)
+                c.set(i, j + a.getColumnDimension(), b.get(i, j));
+        return c;
+    }
+
+    public static Matrix concatRows(Matrix a, Matrix b) {
+        if (a == null)
+            throw new IllegalArgumentException("a is null");
+        if (b == null)
+            throw new IllegalArgumentException("b is null");
+        if (a.getColumnDimension() != b.getColumnDimension())
+            throw new IllegalArgumentException("columns count do not match");
+
+        Matrix c = new Matrix(a.getRowDimension() + b.getRowDimension(), a.getColumnDimension());
+        for (int i = 0; i < a.getRowDimension(); i++)
+            for (int j = 0; j < a.getColumnDimension(); j++)
+                c.set(i, j, a.get(i, j));
+        for (int i = 0; i < b.getRowDimension(); i++)
+            for (int j = 0; j < b.getColumnDimension(); j++)
+                c.set(i + a.getRowDimension(), j, b.get(i, j));
+        return c;
+    }
+}

+ 59 - 0
src/holeg/simple_grid/SimpleGridBuilder.java

@@ -0,0 +1,59 @@
+package holeg.simple_grid;
+
+import holeg.model.Grid;
+import holeg.power_flow.ComplexNumber;
+
+import java.util.ArrayList;
+
+public class SimpleGridBuilder {
+    private Grid grid = new Grid();
+
+    public Grid getGrid() {
+        return grid;
+    }
+
+    public SimpleGridNode addGenerator(ComplexNumber power) {
+        if (power == null)
+            throw new IllegalArgumentException("power is null");
+
+        SimpleGridNode node = new SimpleGridNode();
+        node.edges = new ArrayList<>();
+        node.voltage = 1;
+        node.powerGeneration = power;
+        node.powerConsumption = ComplexNumber.Zero;
+        grid.addNode(node);
+        return node;
+    }
+
+    public SimpleGridNode addHouse(ComplexNumber power) {
+        if (power == null)
+            throw new IllegalArgumentException("power is null");
+        SimpleGridNode node = new SimpleGridNode();
+        node.edges = new ArrayList<>();
+        node.voltage = 1;
+        node.powerGeneration = ComplexNumber.Zero;
+        node.powerConsumption = power;
+        grid.addNode(node);
+        return node;
+    }
+
+    public SimpleGridEdge connect(SimpleGridNode a, SimpleGridNode b, double length) {
+        if (a == null)
+            throw new IllegalArgumentException("a is null");
+        if (b == null)
+            throw new IllegalArgumentException("b is null");
+        if (length <= 0)
+            throw new IllegalArgumentException("length <= 0");
+
+        SimpleGridEdge edge = new SimpleGridEdge();
+        edge.from = a;
+        edge.to = b;
+        edge.lengthKilometers = length;
+
+        // Register edge
+        grid.addEdge(edge);
+        a.edges.add(edge);
+        b.edges.add(edge);
+        return edge;
+    }
+}

+ 51 - 0
src/holeg/simple_grid/SimpleGridEdge.java

@@ -0,0 +1,51 @@
+package holeg.simple_grid;
+
+import holeg.model.GridEdge;
+import holeg.power_flow.ComplexNumber;
+
+import java.io.PrintStream;
+
+public class SimpleGridEdge implements GridEdge {
+    public SimpleGridNode from;
+    public SimpleGridNode to;
+    public double lengthKilometers = 0;
+    public double current = 0;
+    public ComplexNumber power = ComplexNumber.Zero;
+    public ComplexNumber loss = ComplexNumber.Zero;
+    public Object tag;
+
+    @Override
+    public SimpleGridNode getFrom() {
+        return from;
+    }
+
+    @Override
+    public SimpleGridNode getTo() {
+        return to;
+    }
+
+    @Override
+    public double getLengthKilometers() {
+        return lengthKilometers;
+    }
+
+    @Override
+    public void setCurrent(double current) {
+        this.current = current;
+    }
+
+    @Override
+    public void setPowerFlow(ComplexNumber power) {
+        this.power = power;
+    }
+
+    @Override
+    public void setLineLoss(ComplexNumber loss) {
+        this.loss = loss;
+    }
+
+    public void print(PrintStream stream) {
+        stream.printf("%.0f km, current: %.2f A, power flow: %s, line loss: %s", lengthKilometers, current, power.toString(), loss.toString());
+        stream.println();
+    }
+}

+ 47 - 0
src/holeg/simple_grid/SimpleGridNode.java

@@ -0,0 +1,47 @@
+package holeg.simple_grid;
+
+import holeg.model.GridEdge;
+import holeg.model.GridNode;
+import holeg.power_flow.ComplexNumber;
+
+import java.io.PrintStream;
+import java.util.List;
+
+public class SimpleGridNode implements GridNode {
+    public List<GridEdge> edges;
+    public ComplexNumber powerConsumption = ComplexNumber.Zero;
+    public ComplexNumber powerGeneration = ComplexNumber.Zero;
+    public double voltage;
+    public double current;
+    public Object tag;
+
+    @Override
+    public List<GridEdge> getEdges() {
+        return edges;
+    }
+
+    @Override
+    public ComplexNumber getPowerConsumption() {
+        return powerConsumption;
+    }
+
+    @Override
+    public ComplexNumber getPowerGeneration() {
+        return powerGeneration;
+    }
+
+    @Override
+    public void setVoltage(double voltage) {
+        this.voltage = voltage;
+    }
+
+    @Override
+    public void setCurrent(double current) {
+        this.current = current;
+    }
+
+    public void print(PrintStream stream) {
+        stream.printf("consumption: %s, generation: %s, voltage: %.3f V, current: %.3f A", powerConsumption.toString(), powerGeneration.toString(), voltage, current);
+        stream.println();
+    }
+}

+ 43 - 0
src/holeg/test_headless/TestHeadlessProgram.java

@@ -0,0 +1,43 @@
+package holeg.test_headless;
+
+
+import holeg.HolegPowerFlow;
+import holeg.model.Grid;
+import holeg.model.GridEdge;
+import holeg.model.GridNode;
+import holeg.power_flow.*;
+import holeg.simple_grid.SimpleGridBuilder;
+import holeg.simple_grid.SimpleGridEdge;
+import holeg.simple_grid.SimpleGridNode;
+
+public class TestHeadlessProgram {
+    public static void test() {
+        // Build grid
+        SimpleGridBuilder builder = new SimpleGridBuilder();
+        SimpleGridNode generatorA = builder.addGenerator(new ComplexNumber(4.4));
+        SimpleGridNode houseA = builder.addHouse(new ComplexNumber(1));
+        SimpleGridNode houseB = builder.addHouse(new ComplexNumber(2));
+        SimpleGridNode houseC = builder.addHouse(new ComplexNumber(2));
+        SimpleGridNode generatorB = builder.addGenerator(new ComplexNumber(1));
+
+        // generatorA -- houseA
+        // |
+        // |---- houseB -- houseC --- generatorB
+
+        builder.connect(generatorA, houseA, 11);
+        builder.connect(generatorA, houseB, 10);
+        builder.connect(houseB, houseC, 10);
+        builder.connect(houseC, generatorB, 10);
+
+        // Grid
+        Grid grid = builder.getGrid();
+        HolegPowerFlow flow = new HolegPowerFlow();
+        flow.solve(grid);
+
+        // Show results
+        for (GridNode node : grid.getNodes())
+            ((SimpleGridNode)node).print(System.out);
+        for (GridEdge edge : grid.getEdges())
+            ((SimpleGridEdge)edge).print(System.out);
+    }
+}

+ 92 - 0
src/holeg/test_sensitivity/TestSensitivityProgram.java

@@ -0,0 +1,92 @@
+package holeg.test_sensitivity;
+
+import holeg.power_flow.*;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestSensitivityProgram {
+    public static void test() {
+        // Load data
+        Bus[] buses = TestData.getIEEE14Busses();
+        Line[] lines = TestData.getIEEE14Lines();
+
+        float xStart = -0.8f;
+        float xEnd = 10.0f;
+        float yStart = -0.8f;
+        float yEnd = 10.0f;
+        float h = 0.5f;
+        int lineIndex = 7;
+        int busIndex = 5;
+
+        List<String> csvOutput = new ArrayList<>();
+
+        StringBuilder xLine = new StringBuilder();
+        xLine.append(";");
+        for (float x = xStart; x <= xEnd; x += h) {
+            xLine.append(String.format("%.3f", x));
+            xLine.append(";");
+        }
+        csvOutput.add(xLine.toString());
+
+        double R = lines[lineIndex].R;
+        double X = lines[lineIndex].X;
+
+        double Pg = buses[busIndex].Pg;
+        double Pl = buses[busIndex].Pl;
+
+        for (float y = yStart; y <= yEnd; y += h) {
+            System.out.printf("%f / %f\n", y, yEnd);
+
+            StringBuilder csvLine = new StringBuilder();
+            csvLine.append(String.format("%.3f", y));
+            csvLine.append(";");
+            for (float x = xStart; x <= xEnd; x += h) {
+
+                Line line = lines[lineIndex];
+                line.R = R * (1 + x);
+                line.X = X * (1 + y);
+
+                /*Bus bus = buses[busIndex];
+                bus.Pg = Pg * (1 + x);
+                bus.Pl = Pl * (1 + y);*/
+
+                // Create problem and solver
+                PowerFlowProblem problem = new PowerFlowProblem(buses, lines, 1, 1 / 100.0);
+                Solver solver = new NewtonRaphsonSolver();
+
+                // Solve the problem with default settings
+                SolverResult result = solver.solve(problem, new SolverSettings());
+
+                if (result.solved)
+                    csvLine.append(1);
+                else
+                    csvLine.append(0);
+                csvLine.append(";");
+            }
+            csvOutput.add(csvLine.toString());
+        }
+
+        writeToFile("C:\\Users\\Henrik\\Desktop\\test.csv", csvOutput);
+        System.out.println("Done!");
+    }
+
+    private static void writeToFile(String fileName, List<String> data) {
+        PrintWriter out = null;
+        try {
+            out = new PrintWriter(new OutputStreamWriter(
+                    new BufferedOutputStream(new FileOutputStream(fileName)), StandardCharsets.UTF_8));
+            for (String line : data)
+                out.println(line);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } finally {
+            if (out != null) {
+                out.flush();
+                out.close();
+            }
+        }
+    }
+}

+ 4 - 0
src/holeg/ui/FlowTableWindow.java

@@ -0,0 +1,4 @@
+package holeg.ui;
+
+public class FlowTableWindow {
+}

+ 38 - 0
src/holeg/ui/PowerFlowAnalysisMenu.java

@@ -0,0 +1,38 @@
+package holeg.ui;
+
+import ui.model.MinimumModel;
+import ui.model.Model;
+
+import javax.swing.*;
+
+public class PowerFlowAnalysisMenu extends JMenu {
+    private JMenuItem settingsMenu;
+    private JMenuItem showFlow;
+    private JCheckBoxMenuItem showResultMessageBox;
+    private JCheckBoxMenuItem showDebugMessageBox;
+
+    private static PowerFlowAnalysisMenu instance;
+
+    public PowerFlowAnalysisMenu(Model model) {
+        super("Power flow");
+
+        settingsMenu = add(new JMenuItem("Settings"));
+        showFlow = add(new JMenuItem("Show flow table"));
+        showResultMessageBox = (JCheckBoxMenuItem) add(new JCheckBoxMenuItem("Show result message"));
+        showDebugMessageBox = (JCheckBoxMenuItem) add(new JCheckBoxMenuItem("Show debug message"));
+
+        instance = this;
+    }
+
+    public boolean shouldShowResult() {
+        return showResultMessageBox.getState();
+    }
+
+    public boolean shouldShowDebug() {
+        return showDebugMessageBox.getState();
+    }
+
+    public static PowerFlowAnalysisMenu getInstance() {
+        return instance;
+    }
+}

+ 4 - 0
src/holeg/ui/SettingsWindow.java

@@ -0,0 +1,4 @@
+package holeg.ui;
+
+public class SettingsWindow {
+}

+ 21 - 0
src/holeg/ui/SolveResultMessageBox.java

@@ -0,0 +1,21 @@
+package holeg.ui;
+
+import holeg.GridSolverResult;
+
+import javax.swing.*;
+
+public class SolveResultMessageBox {
+    public static void show(GridSolverResult result) {
+        switch(result) {
+            case Error:
+                JOptionPane.showMessageDialog(null,"Could not solve grid.","Error while solving.", JOptionPane.ERROR_MESSAGE);
+                break;
+            case PartialFailure:
+                JOptionPane.showMessageDialog(null,"Could not solve some part of the grid.","Error while solving.", JOptionPane.ERROR_MESSAGE);
+                break;
+            case Solved:
+                JOptionPane.showMessageDialog(null,"Successfully solved!","Success", JOptionPane.INFORMATION_MESSAGE);
+                break;
+        }
+    }
+}

+ 3 - 0
src/ui/model/DecoratedCable.java

@@ -23,4 +23,7 @@ public class DecoratedCable {
 	public float getFlowEnergy() {
 		return flowEnergy;
 	}
+	public void setFlowEnergy(float flowEnergy) {
+		this.flowEnergy = flowEnergy;
+	}
 }

+ 7 - 2
src/ui/model/DecoratedNetwork.java

@@ -5,6 +5,7 @@ import java.util.List;
 import java.util.stream.Collectors;
 import classes.HolonElement;
 import classes.HolonObject;
+import holeg.HolegGateway;
 import ui.controller.FlexManager;
 import ui.model.DecoratedCable.CableState;
 import ui.model.DecoratedHolonObject.HolonObjectState;
@@ -25,9 +26,13 @@ public class DecoratedNetwork {
 			calculateAllEqualNetwork(minimumNetwork, Iteration, flexManager);
 			break;
 		case MininumDemandFirst:
-		default:
 			calculateMinimumDemandFirstNetwork(minimumNetwork, Iteration, flexManager);
-			break;		
+			break;
+		case PowerFlowAnalysis:
+			// TODO: (Henrik)
+			HolegGateway.solve(minimumNetwork, Iteration, flexManager, this);
+			calculateStates();
+			break;
 		}
 	}
 

+ 6 - 2
src/ui/model/Model.java

@@ -94,10 +94,14 @@ public class Model {
 		/**
 		 * All HolonObjects will receive the same amount of energy.
 		 */
-		AllEqual
+		AllEqual,
+        /**
+         * Uses power flow analysis to check the flow of power between HolonObjects.
+         */
+        PowerFlowAnalysis
 	}
     /** the Fairness model in use */
-    private FairnessModel fairnessModel = FairnessModel.MininumDemandFirst;
+    private FairnessModel fairnessModel = FairnessModel.PowerFlowAnalysis;
     
     /*
      * Array of all categories in the model. It is set by default with the

+ 37 - 8
src/ui/view/GUI.java

@@ -6,6 +6,7 @@ import com.google.gson.JsonNull;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
 
+import holeg.ui.PowerFlowAnalysisMenu;
 import interfaces.CategoryListener;
 import interfaces.GraphEditable;
 import interfaces.LocalMode;
@@ -108,11 +109,14 @@ public class GUI implements CategoryListener {
 	/** menu for the different fairness Models */
 	private final JMenu mnFairnessModel = new JMenu("Fairness Model");
 	/** press to supply minimum demand first */
-	private final JMenuItem mntmFairMinFirst = new JMenuItem(
+	private final JRadioButtonMenuItem mntmFairMinFirst = new JRadioButtonMenuItem(
 			"Minimum demand first");
 	/** press to give everyone the same energy */
-	private final JMenuItem mntmFairAlleEqual = new JMenuItem(
+	private final JRadioButtonMenuItem mntmFairAlleEqual = new JRadioButtonMenuItem(
 			"Equal supply for everyone");
+	/** press to give everyone the same energy */
+	private final JRadioButtonMenuItem mntmPowerFlowAnalysisMode = new JRadioButtonMenuItem(
+			"Power flow analysis");
 	private final JMenuItem mntmOpen = new JMenuItem("Open");
 	private final JMenuItem mntmNew = new JMenuItem("New");
 	private final JMenuItem mntmSave = new JMenuItem("Save");
@@ -208,6 +212,9 @@ public class GUI implements CategoryListener {
 	private final Model model;
 	private final Control controller;
 
+	// Power flow analysis
+	private PowerFlowAnalysisMenu powerFlowMenu;
+
 	// In this section are all the Holonelements that correspond to the clicked
 	// HolonObject with consumption/production, name and amount.
 	private final JPanel panel = new JPanel();
@@ -794,7 +801,6 @@ public class GUI implements CategoryListener {
 		mnNewMenuOptions.add(mnFairnessModel);
 
 		mnFairnessModel.add(mntmFairMinFirst);
-		mntmFairMinFirst.setForeground(Color.BLUE);
 		mntmFairMinFirst
 				.setToolTipText("HolonObjects with the smallest mininum Demand will be partially supplied first.\n"
 						+ "After that as many HolonObjects as possible will get fully supplied.");
@@ -802,9 +808,6 @@ public class GUI implements CategoryListener {
 		mntmFairMinFirst
 				.addActionListener(arg0 -> {
 					controller.setFairnessModel(FairnessModel.MininumDemandFirst);
-					mntmFairMinFirst.setForeground(Color.BLUE);
-					mntmFairAlleEqual.setForeground(mnFairnessModel
-							.getForeground());
 					controller.calculateStateAndVisualForCurrentTimeStep();
 					// Update UpperNodes
 					Component canvasOrUpperNodeCanvas = getScrollPaneFromTabbedPane()
@@ -821,8 +824,6 @@ public class GUI implements CategoryListener {
 
 		mntmFairAlleEqual.addActionListener(arg0 -> {
 			controller.setFairnessModel(FairnessModel.AllEqual);
-			mntmFairAlleEqual.setForeground(Color.BLUE);
-			mntmFairMinFirst.setForeground(mnFairnessModel.getForeground());
 			controller.calculateStateAndVisualForCurrentTimeStep();
 			// Update UpperNodes
 				Component canvasOrUpperNodeCanvas = getScrollPaneFromTabbedPane()
@@ -833,6 +834,30 @@ public class GUI implements CategoryListener {
 				}
 			});
 
+		mnFairnessModel.add(mntmPowerFlowAnalysisMode);
+		mntmPowerFlowAnalysisMode
+				.setToolTipText("Use power flow analysis");
+
+		mntmPowerFlowAnalysisMode.addActionListener(arg0 -> {
+			controller.setFairnessModel(FairnessModel.PowerFlowAnalysis);
+			controller.calculateStateAndVisualForCurrentTimeStep();
+
+			// Update UpperNodes
+			Component canvasOrUpperNodeCanvas = getScrollPaneFromTabbedPane()
+					.getViewport().getComponent(0);
+			if (canvasOrUpperNodeCanvas != null
+					&& canvasOrUpperNodeCanvas instanceof GroupNodeCanvas) {
+				((GroupNodeCanvas) canvasOrUpperNodeCanvas).repaint();
+			}
+		});
+
+		ButtonGroup fairnessModelGroup = new ButtonGroup();
+		fairnessModelGroup.add(mntmFairMinFirst);
+		fairnessModelGroup.add(mntmFairAlleEqual);
+		fairnessModelGroup.add(mntmPowerFlowAnalysisMode);
+
+		mntmPowerFlowAnalysisMode.setSelected(true);
+
 		menuBar.add(mnNewMenuView);
 
 		mnNewMenuView.add(mntmCanvasSize);
@@ -986,6 +1011,10 @@ public class GUI implements CategoryListener {
 		mnHelp.add(mntmCodeDoc);
 		mnHelp.add(mntmAboutUs);
 
+		// Power flow
+		powerFlowMenu = new PowerFlowAnalysisMenu(model);
+		menuBar.add(powerFlowMenu);
+
 		tabbedPaneOriginal.addChangeListener(changeEvent -> {
 			if (tabbedPaneOriginal.getSelectedComponent() == null) {
 				Component tempC = tabbedPaneSplit.getSelectedComponent();

+ 4 - 1
src/ui/view/Main.java

@@ -1,5 +1,6 @@
 package ui.view;
 
+import holeg.test_sensitivity.TestSensitivityProgram;
 import ui.controller.Control;
 import ui.controller.SingletonControl;
 import ui.model.Model;
@@ -8,6 +9,8 @@ import javax.swing.*;
 
 import java.awt.*;
 
+import holeg.test_headless.*;
+
 /**
  * The main Class in this Program. The GUI is created in this Class.
  * 
@@ -26,7 +29,7 @@ public class Main {
 		if (!System.getProperty("os.name").startsWith("Linux")) {
 			loadNotLinuxLookAndFeel();	
 		}
-		
+
         EventQueue.invokeLater(() -> {
             try {
                 Model model = new Model();

+ 37 - 5
src/ui/view/MyCanvas.java

@@ -32,6 +32,7 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.font.LineMetrics;
+import java.awt.geom.AffineTransform;
 import java.awt.geom.Line2D;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
@@ -390,7 +391,7 @@ public class MyCanvas extends AbstractCanvas implements MouseListener,
 	{
 		Position start = cable.getModel().getA().getPosition();
 		Position end =  cable.getModel().getB().getPosition();
-		float currentEnergy = cable.getFlowEnergy();
+		float currentEnergy = Math.abs(cable.getFlowEnergy());
 		float capacity = cable.getModel().getCapacity();
 		boolean unlimited = cable.getModel().isUnlimitedCapacity();
 		switch(cable.getState()) {
@@ -406,13 +407,44 @@ public class MyCanvas extends AbstractCanvas implements MouseListener,
 		if(isSelected){
 			g.setColor(Color.lightGray);
 		}
-		g.drawLine(start.x, start.y, end.x, end.y);
-		if(showConnectionInformation) {			
-			Position middle = new Position((start.x + end.x) / 2, (start.y + end.y) / 2);
+		if (cable.getFlowEnergy() > 2)
+			drawArrow(g, start.x, start.y, end.x, end.y);
+		else if (cable.getFlowEnergy() < -2)
+			drawArrow(g, end.x, end.y, start.x, start.y);
+		else
+			g.drawLine(start.x, start.y, end.x, end.y);
+
+		if(showConnectionInformation) {
+			double dx = start.x - end.x, dy = start.y - end.y;
+			double angle = Math.atan2(dy, dx);
+			int offsetX = (int)(Math.sin(angle - Math.PI) * 15);
+			int offsetY = (int)(Math.cos(angle - Math.PI) * 15);
+
+			Position middle = new Position((start.x + end.x) / 2 + offsetX, (start.y + end.y) / 2 + offsetY);
 			g.setFont(new Font("TimesRoman", Font.PLAIN, Math.max((int) (controller.getScale() / 3.5f), 10) )); 
-			g.drawString(currentEnergy + "/" + (unlimited?"\u221E":capacity) , middle.x, middle.y);
+			g.drawString(String.format("%.1f / %s", currentEnergy, (unlimited?"\u221E":capacity)) , middle.x, middle.y);
 		}
 	}
+
+	private void drawArrow(Graphics g1, int x1, int y1, int x2, int y2) {
+		Graphics2D g = (Graphics2D) g1.create();
+
+		double dx = x2 - x1, dy = y2 - y1;
+		double angle = Math.atan2(dy, dx);
+		int len = (int) Math.sqrt(dx * dx + dy * dy);
+		AffineTransform at = AffineTransform.getTranslateInstance(x1, y1);
+		at.concatenate(AffineTransform.getRotateInstance(angle));
+		g.transform(at);
+
+		final int ARR_SIZE = 15;
+		final int offset = 30;
+
+		len -= offset;
+
+		g.drawLine(0, 0, len, 0);
+		g.fillPolygon(new int[]{len, len - ARR_SIZE, len - ARR_SIZE, len}, new int[]{0, -ARR_SIZE / 2, ARR_SIZE / 2, 0}, 4);
+	}
+
 	private void paintSwitch(Graphics2D g, DecoratedSwitch dSwitch)
 	{
 		drawCanvasObject(g, dSwitch.getState() == SwitchState.Open ? HolonSwitch.getSwitchOpenImage(): HolonSwitch.getSwitchClosedImage() , dSwitch.getModel().getPosition());