Exemplo n.º 1
0
        /// <summary>
        ///     Performs Modified Nodal Analysis (MNA) on a given circuit, and
        ///     computes all the values in the circuit.
        /// </summary>
        /// <remarks>
        ///     How it works:
        ///     There are N nodes.
        ///     The equations in the first N rows of A are of the form Sigma(Vn-Vm / Rn) + Sigma(+/- Iv) = Sigma(+/- Ir)
        ///     * For a node numbered "n",
        ///     Rn are resistors connected between this node and others,
        ///     Vn is the voltage at this node,
        ///     Vm is the voltage at the node on the other side of the resistor,
        ///     * So Sigma(Vn-Vm / Rn) is all the current flowing through *resistors* via this node!
        ///     * In each specific case (Vn-Vm1 / Rn1) the result will be negative if the current flows *out* of this node!
        ///     Iv is the current flowing through this node from/to a connected voltage source
        ///     * If Iv flows out of this node we need to subtract it! (If we are connected to the positive side of the voltage
        ///     source it is flowing in)
        ///     Ir is the current applied by a current source (or in out case via a resistor with unknown R)
        ///     * The same rule from Iv applies to Ir (but reversed since we are on the other side of the equation)
        ///     The equations are based on Kirchoff's law: the sum of currents flowing in is equal to the sum of currents flowing
        ///     out,
        ///     So SIi = SIo is the same as SIi - SIo = 0
        ///     As long as we move stuff between sides of the function correctly, the equation remains true.
        ///     MNA takes advantage of this and puts it into a matrix by grouping the variables in a helpful way.
        ///     How can we treat resistors with known I but unknown R as current sources?
        ///     The equation uses Ohm's law: I = (1/R) * V (b=Ax)
        ///     What we did is reversed that. The matrix requires 1/R to be known but we don't have that.
        ///     So instead we said (1/R) * V = I and used I instead on the other side of the equation (vector B)
        ///     And that is why a current source and a resistor are the same thing here: Both follow Ohm's law.
        ///     The rest of the rows in A:
        ///     There are S voltage sources.
        ///     For the last S rows in A: Vp-Vn = Vs
        ///     * For a node numbered "s",
        ///     Vp is the voltage at the node connected to the *positive* side of the voltage source
        ///     Vn is the voltage at the node connected to the *negative* side of the voltage source
        ///     Vs is the voltage of the source "s"
        ///     We need more than N equations since the current through each voltage source is also an unknown
        ///     This equation is much simpler - it is trivial
        ///     The voltage of "s" is the voltage across "s". So the difference between Vp and Vn is ALWAYS Vs.
        ///     Vp and Vn are both unknowns, therefore we can use this equation to figure them out.
        ///     "Wait a minute. Can't we just set Vp to Vs and Vn to 0 ahead of time?"
        ///     - NO. Voltages are relative to the reference point (node) therefore Vp-Vn=Vs is possible even with other values and
        ///     that is perfectly legitimate.
        ///     It is also common IRL: Two AA batteries in series. For one, Vp1=1.5V and Vn1=0V, for the other Vp2=3V and Vn2=1.5V.
        ///     It really just depends where the reference point is.
        ///     "Do we need a reference node?"
        ///     YES. If we have two voltage sources then as with the example above we can't always set Vn=0V unless we pick a
        ///     reference node.
        ///     Ideally the user should choose it, but we can do it for them as well.
        /// </remarks>
        /// <param name="circuit">The circuit which will be analyzed.</param>
        /// <exception cref="ArgumentNullException">
        ///     <para>If <paramref name="circuit" /> is equal to</para>
        ///     <code>null</code>
        ///     <para>.</para>
        /// </exception>
        /// <exception cref="SimulationException">
        ///     If a critical error occured during the analysis.
        /// </exception>
        private static Vector <double> ModifiedNodalAnalysis([NotNull] SimulationCircuit circuit)
        {
            if (circuit == null)
            {
                throw new ArgumentNullException(nameof(circuit));
            }

            /* init structures - non-modified only:
             * var a = Matrix<double>.Build.Dense(numVars, numVars);
             * var b = Vector<double>.Build.Dense(numVars); */
            // init structures - modified:
            var a = Matrix <double> .Build.Dense(circuit.NodeCount + circuit.SourceCount,
                                                 circuit.NodeCount + circuit.SourceCount);

            var b = Vector <double> .Build.Dense(circuit.NodeCount + circuit.SourceCount);

            Log.Information("Starting simulation");

            var ndict = new Dictionary <int, INode>();
            // do BFS
            var q = new Queue <INode>();

            q.Enqueue(circuit.Head);
            while (q.Count > 0)
            {
                var n = q.Dequeue();
                ndict.Add(n.SimulationIndex, n);
                Log.Information("Visiting node {0}", n.ToString());
                // Check for issues
                if (n.SimulationIndex >= circuit.NodeCount)
                {
                    throw new SimulationException("Invalid index for node {0}" + n, n);
                }
                foreach (var c in n.Components)
                {
                    // Get node connected to OTHER side of this component!
                    var o = c.OtherNode(n).OrEquivalent();

                    // Check for issues
                    if (o == null)
                    {
                        Log.Warning("Component {0} is detached!", c.ToString());
                        continue;
                    }
                    if (o.SimulationIndex >= circuit.NodeCount)
                    {
                        throw new SimulationException("Invalid index for node " + o, o);
                    }
                    // we don't want to visit reference node(s)
                    // Add the node - if it hasn't been added yet!
                    if (!o.Mark && !o.IsReferenceNode)
                    {
                        q.Enqueue(o);
                        o.Mark = true;
                    }
                    // For C#7: Should use switch expression patterns here
                    // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
                    if (c is IResistor)
                    {
                        var r = (IResistor)c;
                        Log.Information("Visiting resistor {0} connected to node {1}", r.ToString(), n.ToString());

                        if (r.Resistance > 0 && !c.Mark)
                        {
                            Log.Information("Resistor {0} has known resistance, adding to matrix A", r.ToString());
                            a[n.SimulationIndex, n.SimulationIndex] += r.Conductance; // Conductance = 1/Resistance

                            // we don't want to visit reference node(s)
                            if (!o.IsReferenceNode)
                            {
                                a[o.SimulationIndex, o.SimulationIndex] += r.Conductance;
                                a[o.SimulationIndex, n.SimulationIndex] -= r.Conductance;
                                a[n.SimulationIndex, o.SimulationIndex] -= r.Conductance;
                            }
                        }
                        // Handle a case when the current is known but not the resistance.
                        else if (r.Current != 0 && r.Resistance <= 0)
                        {
                            Log.Information("Resistor {0} has known current, adding to vector B", r.ToString());

                            // Treat the resistor as if it is a current source
                            b[n.SimulationIndex] += c.Node1.OrEquivalent() == n ? -r.Current : r.Current;
                        }
                    }
                    else if (c is IVoltageSource) // A power source with known voltage (V)
                    {
                        var v = (IVoltageSource)c;
                        Log.Information("Visiting voltage source {0} connected to node {1}", v.ToString(),
                                        n.ToString());
                        // Check for issues
                        if (v.SimulationIndex >= circuit.SourceCount)
                        {
                            throw new SimulationException("Invalid index for voltage source " + v, v);
                        }
                        a[circuit.NodeCount + v.SimulationIndex, n.SimulationIndex]     =
                            a[n.SimulationIndex, circuit.NodeCount + v.SimulationIndex] =
                                Equals(v.Node1?.OrEquivalent(), n) ? 1 : -1;
                        // Node1 is the node connected to the plus terminal
                        if (!v.Mark)
                        {
                            b[circuit.NodeCount + v.SimulationIndex] = v.Voltage;
                        }
                    }
                    else if (c is ISwitch && !c.Mark)
                    {
                        Log.Warning("Switch was linked: {0}" + c);
                    }
                    c.Mark = true;
                }
                n.Mark = true;
            }

            Log.Information("The matrix:");

            for (var i = 0; i < a.RowCount; i++)
            {
                Log.Information("{0}", a.Row(i));
            }
            Log.Information("The vector: {0}", b);
            // Solve the linear equation system
            // Compute A+
            var ap = a.PseudoInverse();

            // Check if there actually is a solution (A*A+*b==b)
            // Handle FP precision issue
            if (!b.AlmostEqual(a * ap * b, 1e-13))
            {
                throw new SimulationException("No solution found!");
            }
            // Compute A+*b
            var apb = ap * b;
            // Compute I-A+*A
            var identity = Matrix <double> .Build.DenseIdentity(a.RowCount, a.ColumnCount);

            var             f = identity - ap * a;
            Vector <double> x;

            // If I-A+*A is zero, then there is a single solution
            // Handle FP precision issue
            if (!f.Exists(v => Math.Abs(v) > 1e-13))
            {
                x = apb;
            }
            else
            {
                // Find "optimal" solution for indeterminate linear equation system
                // Vector w can contain any values, it is multiplied by f then added to apb to get x
                // In our case, we want:
                // 1. No resistors with negative resistance values
                // 2. "Average" resistor values (multiples of the same value, use same values where possible, etc)
                // How do we do this?
                // Put identical voltage drops over the unknown resistors, by manipulating the node values.
                // Some node values can't be changed by w (has a corresponding zero row in f).
                // There is always at least one of these, let's call it fn.
                // Some rows of f are identical which means that the two nodes have a constant
                // voltage drop between them, which mens the resistance is known.
                // We will take that voltage drop into account and divide the rest of the drop
                // from fn to 0 (reference) between the other nodes.
                // This algorithm is difficult to understand, but *should* work.

                // Clean up values of f
                for (var i = 0; i < f.RowCount; ++i)
                {
                    for (var j = 0; j < f.ColumnCount; ++j)
                    {
                        f[i, j] = Math.Round(f[i, j], 13);
                    }
                }

                // Get linearly-dependent rows of f and the (constant) voltage drop/rise
                var dep = (from col in f.Kernel()
                           select col.EnumerateIndexed(Zeros.AllowSkip).ToList()
                           into ld
                           where ld.Count >= 2
                           select new Tuple <int, int, double>(ld[0].Item1, ld[1].Item1,
                                                               apb[ld[0].Item1] - apb[ld[1].Item1])).ToList();

                // Should be for each path
                //var r = apb[2] - dep.Sum(t => t.Item3);
                // Why 3? Number of Nodes on path
                //r /= 3;

                // We need to find a desired vector w which will give us the values we want
                var desiredw = Vector <double> .Build.Dense(apb.Count);

                var desiredwDiff = Vector <double> .Build.Dense(apb.Count);

                // This is the number of nodes that have a non-zero row in f
                var div = 0;

                // Start at Node fn which has a zero row in f
                // Usually on the positive side of a voltage source
                // desiredw is zero, so this works to find a zero row
                var si = f.EnumerateRowsIndexed().FirstOrDefault(r => r.Item2.AlmostEqual(desiredw, 1e-13)).Item1;
                if (si >= circuit.NodeCount)
                {
                    Log.Error("Insufficient data to select optimal result! Falling back to result vector {0}", apb);
                    x = apb;
                    return(x);
                }
                var fn = ndict[si];
                // Save the (constant) voltage of fn
                var diff = apb[fn.SimulationIndex];
                // Save voltage of fn. No effect on result, just used in update code.
                desiredw[fn.SimulationIndex] = apb[fn.SimulationIndex];
                // All nodes were marked previously, so treat marked as unmarked and vice versa
                fn.Mark = false;
                // Reuse the old queue
                q.Enqueue(fn);
                while (q.Count > 0)
                {
                    var n = q.Dequeue();
                    // No reference nodes will be here - if any was visited the simulation would have crashed a long time ago!

                    // Traverse through the circuit
                    foreach (var c in n.Components)
                    {
                        // Get node on other side
                        var o = c.OtherNode(n).OrEquivalent();
                        // Visit each node only once!
                        // Also, we can reach reference nodes. Avoid them as usual.
                        if (o.IsReferenceNode || !o.Mark)
                        {
                            continue;
                        }

                        desiredw[o.SimulationIndex]     += desiredw[n.SimulationIndex];
                        desiredwDiff[o.SimulationIndex] += desiredwDiff[n.SimulationIndex];

                        var depInf =
                            dep.FirstOrDefault(d => d.Item1 == n.SimulationIndex && d.Item2 == o.SimulationIndex);
                        var depInf2 =
                            dep.FirstOrDefault(d => d.Item2 == n.SimulationIndex && d.Item1 == o.SimulationIndex);
                        if (depInf != null)
                        {
                            diff -= depInf.Item3;
                            desiredw[o.SimulationIndex] -= depInf.Item3;
                        }
                        else if (depInf2 != null)
                        {
                            diff += depInf2.Item3;
                            desiredw[o.SimulationIndex] += depInf2.Item3;
                        }
                        else
                        {
                            desiredwDiff[o.SimulationIndex] -= 1;
                        }
                        q.Enqueue(o);
                        // Count nodes with non-zero row in f
                        // This line is down here to avoid counting fn
                        ++div;
                        o.Mark = false;
                    }
                }
                diff = diff / div;

                for (var i = 0; i < circuit.SourceCount; ++i)
                {
                    desiredw[circuit.NodeCount + i] = apb[circuit.NodeCount + i];
                }

                // Why 4? Not Node.
                // desiredw[4] = apb[4]; // =0
                // Start at src[0].Node1
                // Why copy from apb? Zero row in f.
                // Add connected to queue (sidx:0, below)
                // desiredw[2] = apb[2]; // =0
                // Why 2? Beginning of path.
                // Search from sidx:2
                // Add connected to queue (nothing)
                // Detect as dependent, add other side to queue with offset dep.Item3
                // var prev = desiredw[dep[0].Item1] = desiredw[2] - div; // -apb[dep[0].Item1]
                // Continue on path
                // Reached here, applied offset since we have one
                // Add connected to queue (sidx:1, below)
                // prev = desiredw[dep[0].Item2] = prev - dep[0].Item3; // -apb[dep[0].Item2]
                // Continue on path
                // Add connected to queue (nothing)
                // prev = desiredw[1] = prev - div; // -apb[dep[0].Item2]

                desiredw = desiredw - apb + desiredwDiff * diff;

                x = apb + f * desiredw;
            }
            // Fix FP precision
            for (var i = 0; i < x.Count; ++i)
            {
                x[i] = Math.Round(x[i], 13);
            }
            Log.Information("The result vector: {0}", x);
            return(x);
        }
Exemplo n.º 2
0
        public static void AnalyzeAndUpdate([NotNull] IEnumerable <INode> nodes,
                                            [NotNull] IEnumerable <IComponent> components)
        {
            // *** Build circuit for simulation ***
            // Some middleware to make the simulation code more flexible

            var nodesList      = nodes.ToList();
            var componentsList = components.ToList();

            // *** Switch handling: ***
            // Closed switch will merge the Nodes that it is connected to. Open ones are ignored.
            // The node that will be used is called the parent
            // The nodes which will not be used are the children
            // Values are copied from each parent to its children after the simulation
            // Algorithm goal: Avoid nested children! (Improves efficiency of code)

            // Reset values to default
            nodesList.ForEach(n => n.EquivalentNode = null);

            // Reverse lookup table: For a parent node, provide the list of children
            var equiv = new Dictionary <INode, HashSet <INode> >();

            foreach (var sw in componentsList.OfType <ISwitch>())
            {
                if (!sw.IsClosed)
                {
                    continue;
                }
                // Choose parent node
                // Use OrEquivalent - in case sw.Node1 is actually a child of another node
                var p = sw.Node1.OrEquivalent();
                // Merge the nodes
                sw.Node2.EquivalentNode = p;
                // Update lookup table
                if (!equiv.ContainsKey(p))
                {
                    equiv.Add(p, new HashSet <INode>());
                }
                equiv[p].Add(sw.Node2);
                // If sw.Node2 is a parent of other nodes, repoint all children of sw.Node2 to p
                if (equiv.ContainsKey(sw.Node2))
                {
                    foreach (var n in equiv[sw.Node2])
                    {
                        n.EquivalentNode = p;
                        equiv[p].Add(n);
                    }
                    // Remove from lookup table (no longer a parent)
                    equiv.Remove(sw.Node2);
                }
            }

            // *** Assign indexes to nodes: ***
            int   vsId = 0, nId = 0;
            INode h = null, refNode = null;

            // Prepare the nodes
            foreach (var n in nodesList)
            {
                // Clear previous links
                n.Components.Clear();
                // Clear mark
                n.Mark = false;
                // Set default simulation index
                n.SimulationIndex = -1;
                // Ignore redundant nodes
                if (n.EquivalentNode != null)
                {
                    continue;
                }
                // Set simulation index if not a reference node
                if (!n.IsReferenceNode)
                {
                    n.SimulationIndex = nId++;
                }
                // Get first non-reference node:
                if (!n.IsReferenceNode && h == null)
                {
                    h = n;
                }
                else if (n.IsReferenceNode && refNode == null)
                {
                    refNode = n;
                }
            }
            if (h == null)
            {
                throw new InvalidOperationException("Missing non-reference node!");
            }
            if (refNode == null)
            {
                throw new InvalidOperationException("Missing reference node!");
            }

            // *** Create reverse links to components: ***
            // Add component to nodes on each side, and assign indexes to voltage sources
            foreach (var c in componentsList.Where(i => !(i is ISwitch)))
            {
                // Clear mark
                c.Mark = false;
                // Create relevant links
                c.Node1.OrEquivalent()?.Components?.Add(c);
                c.Node2.OrEquivalent()?.Components?.Add(c);
                // Assign an index
                if (c is IVoltageSource)
                {
                    c.SimulationIndex = vsId++;
                }
            }

            // Circuit is ready
            var circuit = new SimulationCircuit(h, nId, vsId);

            // *** Do simulation: ***
            var result = ModifiedNodalAnalysis(circuit);

            Log.Information("Updating circuit elements");
            // Input voltages at nodes
            foreach (var n in nodesList)
            {
                if (n.SimulationIndex <= -1)
                {
                    continue;
                }
                n.Voltage = result[n.SimulationIndex];
                Log.Information("Voltage at node {0}: {1}", n.ToString(), n.Voltage);
            }
            // Update child nodes
            foreach (var e in equiv)
            {
                // Copy parent node's voltage to each child node
                foreach (var n in e.Value)
                {
                    n.Voltage = e.Key.Voltage;
                    Log.Information("Voltage at node {0} is the same as at node {1}: {2}", e.Key.ToString(),
                                    n.ToString(), n.Voltage);
                }
            }
            // Update component information
            foreach (var c in componentsList)
            {
                // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
                if (c is IVoltageSource)
                {
                    // Input computed current
                    var v = (IVoltageSource)c;
                    v.Current = -result[circuit.NodeCount + v.SimulationIndex]; // Result is in opposite direction, fix it
                    Log.Information("Current at voltage source {0}: {1}", v.ToString(), v.Current);
                }
                else if (c is IResistor)
                {
                    // Input computed current or resistance
                    var r = (IResistor)c;
                    r.Voltage = (r.Node1?.Voltage ?? 0) - (r.Node2?.Voltage ?? 0);
                    if (r.Resistance > 0)
                    {
                        r.Current = r.Voltage / r.Resistance;
                    }
                    else
                    {
                        r.Resistance = r.Voltage / r.Current;
                        if (r.Resistance < 0)
                        {
                            throw new SimulationException("Invalid current at resistor! " + r, r);
                        }
                    }
                }
            }
        }