2
$\begingroup$

Mason's gain formula (MGF) is a method for finding the transfer function of a linear signal-flow graph (SFG).

I'm trying to port the MATLAB code to Mathematica code, but the Mathematica code outputs false result.

(* ::Package:: *)

(* masonFormula.m
   Translates the mason.m Matlab function to Mathematica.
   Calculates the transfer function (Numerator/Denominator) for a
   signal flow graph described in a .net file, using Mason's Gain Formula.

   Based on:
   mason.m by Rob Walton
   TRLabs and the University of Calgary, Alberta, Canada
   Date: January 25th 1999
   Revised: January 20th 2000

   USAGE:
     {Numerator, Denominator} = masonFormula["Netfile", StartNode, StopNode]

     Netfile     - is a string with the name of the netfile to load
     StartNode   - is the integer number describing the independent input node
     StopNode    - is the integer number describing the dependent output node
     Numerator   - is a symbolic expression for the Numerator
     Denominator - is a symbolic expression for the Denominator
*)


(* --- Helper Function Definitions --- *)

(* Function to check if two lists of nodes have any common elements *)
AreTouchingLoops[nodes1_List, nodes2_List] :=
  Length[Intersection[nodes1, nodes2]] > 0;

(* Function to check if two lists of coefficients represent the same loop OR
   the same combination of non-touching loops.
   This means they contain the same multiset of non-zero coefficient numbers *)
IsSameLoopCombination[coeffs1_List, coeffs2_List] :=
  Sort[Select[coeffs1, # > 0 &]] == Sort[Select[coeffs2, # > 0 &]];

(* Recursive function to find simple paths (no repeated nodes) *)
(* Returns a list of {pathCoefficients, pathNodes} pairs *)
findPaths[curr_, stop_, pathCoeffs_, visitedNodes_, net_] :=
  Module[{newVisitedNodes, outgoingEdges, results},
    (* Check if current node has been visited in this path already *)
    If[MemberQ[visitedNodes, curr], Return[{}]];

    newVisitedNodes = Append[visitedNodes, curr];

    (* Base case: Reached the stop node *)
    If[curr == stop,
      (* A path must have at least one edge *)
      If[Length[pathCoeffs] > 0,
        Return[{{pathCoeffs, newVisitedNodes}}],
        Return[{}] (* Start == Stop with no edges, not a valid path/loop in this context *)
      ]
    ];

    (* Find outgoing edges from the current node *)
    outgoingEdges = Select[net, #[[2]] == curr &]; (* Edges where start node is curr *)

    (* Recursively call for each outgoing edge *)
    results = Flatten[
      Table[
        (* The edge format is {coeffIndex, startNode, endNode} *)
        findPaths[edge[[3]], stop, Append[pathCoeffs, edge[[1]]], newVisitedNodes, net],
        {edge, outgoingEdges}
      ],
      1 (* Flatten by one level *)
    ];

    Return[results];
  ];

(* Function to remove duplicate loop/combination descriptions from a list *)
RemoveDuplicateLoopCombinations[loopList_List] :=
  Module[{uniqueLoops = {}},
    Scan[
      (* Use IsSameLoopCombination for comparison *)
      If[! MemberQ[uniqueLoops, #, (IsSameLoopCombination[#1["Coeff"], #2["Coeff"]] &)],
         AppendTo[uniqueLoops, #]
      ] &,
      loopList
    ];
    Return[uniqueLoops];
  ];

(* Function to convert a list of coefficient numbers into a symbolic product *)
CoeffsToProduct[coefficients_List, coeffSymbols_List] :=
  Module[{validCoeffs},
    validCoeffs = Select[coefficients, # > 0 &]; (* Remove padding/zeros *)
    If[Length[validCoeffs] == 0,
      1 (* Product of empty set is 1. Represents the '1' term in Delta and Delta_k *)
    ,
      (* Map coefficient numbers (indices) to symbols and take the product *)
      Apply[Times, coeffSymbols[[validCoeffs]]]
    ]
  ];

(* Function to calculate the sum of products for loop combinations of a given order
   that do not touch a given set of nodes (pNodes).
   'L' is the Association holding loop combinations grouped by order.
   'order' specifies which order combinations to consider (e.g., 1 for single loops, 2 for pairs, etc.).
   'pNodes' is the list of nodes that the loop combination should NOT touch.
   Returns the sum: Sum[ Product[coeffs] ] for all valid non-touching combinations of the given order.
*)
SumOfProductsNonTouching[L_Association, order_Integer, pNodes_List, coeffSymbols_List] :=
  Module[{orderLoopCombinations, nonTouchingCombinations, products},
    (* Check if combinations of this order exist *)
    If[!KeyExistsQ[L, order] || Length[L[order]] == 0,
      Return[0] (* No combinations of this order *)
    ];

    orderLoopCombinations = L[order];

    (* Find loop combinations of this order that do not touch the path nodes *)
    (* #["Node"] contains ALL nodes from the constituent loops in the combination *)
    nonTouchingCombinations = Select[orderLoopCombinations, ! AreTouchingLoops[#["Node"], pNodes] &];

    (* Calculate the product of coefficients for each non-touching combination *)
    (* #["Coeff"] contains ALL coefficients from the constituent loops *)
    products = CoeffsToProduct[#["Coeff"], coeffSymbols] & /@ nonTouchingCombinations;

    (* Sum the products *)
    Total[products]
  ];


(* --- Main Function --- *)

masonFormula[netFile_String, start_Integer, stop_Integer] :=
  Module[{
    file, line, coeff, net = {}, coeffNames = {}, numCoeffs, numNodes,
    rawPaths, paths = {},
    rawLoops = {}, loopCombinations = <||>, (* Renamed 'loops' to 'loopCombinations' for clarity *)
    coeffSymbols,
    numeratorExpr, denominatorExpr,
    allNodes
    },

    (* --- Load the net description file --- *)
    file = OpenRead[netFile];
    If[file === $Failed,
  Print["*** File, ", netFile, ", not found ***"];
  Return[$Failed]
    ];

    While[True,
      line = ReadList[file, Number, 3];
      If[line === EndOfFile, Break[]];
      coeff = Read[file, Word];
      If[coeff === EndOfFile, Break[]];
      AppendTo[net, line];
      AppendTo[coeffNames, coeff];
      ReadList[file, __, RecordSeparators -> {"\n"}];
    ];
    Close[file];

    numCoeffs = Length[net];
    If[numCoeffs === 0,
      Print["*** No data read from file ", netFile, " ***"];
      Return[$Failed]
    ];

    (* Determine the set of all nodes involved *)
    allNodes = Union[Flatten[net[[All, {2, 3}]]]];
    numNodes = Length[allNodes]; (* Max node number might be different *)

    (* Create symbolic variables from coefficient names *)
    coeffSymbols = Symbol /@ coeffNames;

    (* --- Find all forward paths connecting start and stop nodes --- *)
    rawPaths = findPaths[start, stop, {}, {}, net];

    If[Length[rawPaths] == 0,
      Print["*** There are no paths connecting nodes ", start, " and ", stop, " ***"];
      (* According to Mason's formula, if there are no paths, the gain is 0.
         The denominator (Delta) is usually non-zero if loops exist, or 1 otherwise. *)
      (* We still need to calculate Delta for completeness, or just return 0/1 *)
       (* Let's proceed to calculate Delta anyway, and return {0, Delta} *)
       (* numeratorExpr = 0; (* Skip path processing *) *)
    ];

    (* Structure path data *)
    paths = Map[<|"Coeff" -> #[[1]], "Node" -> #[[2]]|> &, rawPaths];


    (* --- Determine all first-Order loops (L_1 term) --- *)
    (* Find loops starting from *any* node involved in the network *)
    For[i = 1, i <= Length[allNodes], ++i,
        node = allNodes[[i]];
        rawLoops = Join[rawLoops, findPaths[node, node, {}, {}, net]];
    ];


    (* Remove duplicate loops using coefficient comparison *)
    (* findPaths can return the same loop starting from different nodes *)
    rawLoops = RemoveDuplicateLoopCombinations[rawLoops]; (* Use the renamed function *)

    (* Structure loop data for order 1 *)
    If[Length[rawLoops] > 0,
        loopCombinations[1] = Map[<|"Coeff" -> #[[1]], "Node" -> #[[2]]|> &, rawLoops];
    ,
        loopCombinations[1] = {}; (* Ensure key exists even if no loops *)
    ];


    (* --- Determine higher-order loop combination terms (L_m for m > 1) --- *)
    (* These represent combinations of non-touching loops *)
    order = 1;
    While[True,
      order++;
      loopCombinations[order] = {}; (* Initialize list for this order *)

      (* Need loops of order 1 and combinations of order (order-1) to build order 'order' *)
      If[!KeyExistsQ[loopCombinations, 1] || Length[loopCombinations[1]] == 0 ||
         !KeyExistsQ[loopCombinations, order - 1] || Length[loopCombinations[order - 1]] == 0,
         Break[] (* No loops of previous orders, so no higher order combinations *)
      ];

      (* Compare each first-order loop with each (order-1)-th combination *)
      For[i = 1, i <= Length[loopCombinations[1]], ++i,
        For[j = 1, j <= Length[loopCombinations[order - 1]], ++j,
          (* Check if the first-order loop touches the existing combination *)
          If[! AreTouchingLoops[loopCombinations[1, i, "Node"], loopCombinations[order - 1, j, "Node"]],
            (* Non-touching found, combine them *)
            combinedCoeffs = Join[loopCombinations[1, i, "Coeff"], loopCombinations[order - 1, j, "Coeff"]];
            combinedNodes = Join[loopCombinations[1, i, "Node"], loopCombinations[order - 1, j, "Node"]];

            (* Check if this exact combination (based on coeffs) is already in loopCombinations[order] *)
            isDuplicate = MemberQ[loopCombinations[order], <|"Coeff" -> combinedCoeffs|>,
                                 (IsSameLoopCombination[#1["Coeff"], #2["Coeff"]] &)];

            If[! isDuplicate,
              AppendTo[loopCombinations[order], <|"Coeff" -> combinedCoeffs, "Node" -> combinedNodes|>];
            ];
          ]
        ]
      ];

      (* If no combinations of this order were found, stop *)
      If[Length[loopCombinations[order]] == 0,
          KeyDropFrom[loopCombinations, order]; (* Remove empty entry *)
          Break[]
      ];
      (* Important: Remove duplicates *after* generating all possibilities for the order *)
      loopCombinations[order] = RemoveDuplicateLoopCombinations[loopCombinations[order]];
       If[Length[loopCombinations[order]] == 0, Break[]]; (* Check again after deduplication *)
    ];

    (* --- Display Info (Optional) --- *)
    Print["\n-- Network Info --"];
    Print["Net File   : ", netFile];
    Print["Start Node : ", start];
    Print["Stop Node  : ", stop];

    Print["\n----- Paths (P_k) -----"];
    If[Length[paths] == 0,
        Print["(No paths found)"],
        For[i = 1, i <= Length[paths], ++i,
             (* Print symbolic path gain *)
             Print["P", i, " (Nodes: ", paths[[i, "Node"]], ") Gain: ", CoeffsToProduct[paths[[i, "Coeff"]], coeffSymbols]];
        ]
    ];

    Print["\n----- Loop Combinations (Terms for Delta) -----"];
    If[Length[Keys[loopCombinations]] == 0 || Total[Length /@ Values[loopCombinations]] == 0,
        Print["(No loops found)"],
        Scan[
            Function[{orderKey},
                Print["\n- Order ", orderKey, " Combinations (Term L",orderKey,") -"];
                If[Length[loopCombinations[orderKey]] == 0,
                    Print["(None)"],
                    For[i = 1, i <= Length[loopCombinations[orderKey]], ++i,
                         (* Print symbolic product for this combination *)
                         Print["Term ", i, " (Coeffs: ", loopCombinations[orderKey, i, "Coeff"], ") Product: ", CoeffsToProduct[loopCombinations[orderKey, i, "Coeff"] , coeffSymbols] ];
                    ]
                ]
            ],
            Sort[Keys[loopCombinations]] (* Print orders in numerical sequence *)
        ]
    ];
    Print[""]; (* Add a newline *)


    (* --- Generate the final equations (Consistent with Mason's Gain Formula) --- *)

    (* Determine Denominator (Delta) *)
    (* Delta = 1 - L1 + L2 - L3 + ... *)
    denominatorExpr = 1;
    Scan[
      Function[{orderKey},
        sign = If[OddQ[orderKey], -1, 1];
        (* SumOfProductsNonTouching with empty node list gives sum of products for ALL combinations of this order (L_m) *)
        denominatorExpr += sign * SumOfProductsNonTouching[loopCombinations, orderKey, {}, coeffSymbols];
      ],
      Sort[Keys[loopCombinations]]
    ];

    (* Determine Numerator (Sum_k P_k * Delta_k) *)
    numeratorExpr = 0;
    If[Length[paths] > 0, (* Only calculate if paths exist *)
        For[pathn = 1, pathn <= Length[paths], ++pathn,
          pathData = paths[[pathn]];
          pathGain = CoeffsToProduct[pathData["Coeff"], coeffSymbols]; (* P_k *)
          pathNodes = pathData["Node"];

          (* Calculate Delta_k for this path *)
          (* Delta_k = 1 - L1(k) + L2(k) - L3(k) + ... *)
          deltaK = 1;
          Scan[
            Function[{orderKey},
              sign = If[OddQ[orderKey], -1, 1];
              (* SumOfProductsNonTouching with pathNodes gives sum of products for combinations NOT touching the path (L_m(k)) *)
              deltaK += sign * SumOfProductsNonTouching[loopCombinations, orderKey, pathNodes, coeffSymbols];
            ],
            Sort[Keys[loopCombinations]]
          ];

          numeratorExpr += pathGain * deltaK; (* Sum P_k * Delta_k *)
        ];
    ]; (* End If paths exist *)

    Print["The variables returned are symbolic expressions for the"];
    Print["Numerator and Denominator of the transfer function G = Numerator / Denominator."];
    Print["Use Simplify[Numerator/Denominator] to potentially simplify the result.\n"];

    Return[{numeratorExpr, denominatorExpr}];
  ];


(* --- Example Usage --- *)

(*
   1    1   2   s21
   2    4   2   s22
   3    4   3   s12
   4    1   3   s11
   5    2   4   R2
*)
(* Create the example.net file *)
Export["example.net",
"1  1   2   s21
2   4   2   s22
3   4   3   s12
4   1   3   s11
5   2   4   R2", "Text"];


(* Calculate transfer function from node 1 to node 3 *)
 {num, den} = masonFormula["example.net", 1, 3]; 

(* Print the results *)
 Print["Numerator: ", num]; 
 Print["Denominator: ", den]; 

(* Print simplified transfer function *)
 Print["Transfer Function (Simplified): ", Simplify[num/den]]; 

My question is: how to fix the wrong code? Now the finding paths part is correct, whereas the finding loop part is not correct. Thanks in advance.

$\endgroup$
2
  • 3
    $\begingroup$ For those who want little background on using Mason to find TF's, I found this little note on the net. $\endgroup$ Commented Apr 19 at 6:00
  • 3
    $\begingroup$ This question is IMO not a good fit for SE sites, and should be closed. Don't give a large block of code to debug, even if it's as cleanly formatted as here. Do the bulk of the debugging work yourself, localize the issue, and ask about the specific problem you could not solve. You have multiple functions in your code, which you presumably translated from MATLAB one by one. Test them one by one! $\endgroup$ Commented Apr 19 at 13:56

1 Answer 1

2
$\begingroup$

From scratch, using another method, now the code works. My question is: can you accelerate it for large-scale graphs? Should I use IGraph for better performance?

ClearAll[MasonsGainFormula, GetPathGain, GetLoopGain, GetElementNodes, AreElementsTouching, CalculateDeterminantTerm];

(* Helper Function: Calculate the gain of a path (list of nodes) *)
GetPathGain[g_Graph, path_List] := Module[{edges},
   If[Length[path] < 2, Return[1]]; (* A path must have at least 2 nodes for an edge *)
   edges = DirectedEdge @@@ Partition[path, 2, 1];
   Product[PropertyValue[{g, edge}, EdgeWeight], {edge, edges}]
   ];

(* Helper Function: Calculate the gain of a loop (list of edges) *)
GetLoopGain[g_Graph, cycleEdges_List] :=
  Product[PropertyValue[{g, edge}, EdgeWeight], {edge, cycleEdges}];

(* Helper Function: Get unique nodes involved in a path (list of nodes) or a loop (list of edges) *)
GetElementNodes[element_] := DeleteDuplicates[
    If[Head[First[element]] === DirectedEdge, (* List of edges *)
        Flatten[List @@@ element],
        (* List of nodes *)
        element
    ]
];

(* Helper Function: Check if two graph elements (paths/loops) touch *)
AreElementsTouching[nodes1_List, nodes2_List] :=
    Length[Intersection[nodes1, nodes2]] > 0;

(* Helper Function: Calculate one term of the determinant/cofactor sum *)
(* Calculates Sum[ Product[ L_i * L_j * ... ] ] for k non-touching loops *)
CalculateDeterminantTerm[loopData_Association, k_Integer] := Module[
    {loopKeys, subsets, nonTouchingSubsets, gainProducts},
    loopKeys = Keys[loopData];
    If[k == 0, Return[1]]; (* Zeroth term is 1 *)
    If[k > Length[loopKeys], Return[0]]; (* Cannot choose k loops if fewer exist *)
    If[k == 1, Return[Total[#Gain & /@ Values[loopData]]]]; (* Sum of individual loop gains *)

    (* Get all subsets of loops of size k *)
    subsets = Subsets[loopKeys, {k}];

    (* Select only those subsets where all loops are pairwise non-touching *)
    nonTouchingSubsets = Select[subsets,
        Module[{pairs = Subsets[#, {2}]},
            (* Check if all pairs in the subset are non-touching *)
            AllTrue[pairs,
                !AreElementsTouching[loopData[#[[1]]]["Nodes"], loopData[#[[2]]]["Nodes"]] &
            ]
        ] &
    ];

    (* Calculate the product of gains for each valid non-touching subset *)
    gainProducts = Times @@@ Map[loopData[#]["Gain"] &, nonTouchingSubsets, {2}];

    (* Sum up the products *)
    Total[gainProducts]
];


(* Main Function: MasonsGainFormula *)
MasonsGainFormula::usage = "MasonsGainFormula[graph, yIn, yOut] calculates the transfer function (gain) from input node yIn to output node yOut for the signal flow graph represented by the directed graph 'graph' using Mason's Gain Formula. Edge weights represent branch gains.";

MasonsGainFormula[g_Graph, yIn_, yOut_] := Module[
    {
        (* Graph Validation *)
        graph = If[GraphQ[g] && DirectedGraphQ[g], g, Message[MasonsGainFormula::invgraph, g]; Return[$Failed]],
        edgeWeights = PropertyValue[g, EdgeWeight],

        (* Paths *)
        forwardPathsNodes, (* Lists of nodes *)
        forwardPathsData, (* Association: pathNodesList -> <|Gain->g, Nodes->{n..}|> *)

        (* Loops *)
        loopsEdges, (* Lists of edges *)
        allLoopsData, (* Association: loopEdgesList -> <|Gain->g, Nodes->{n..}|> *)

        (* Calculation Variables *)
        maxLoopSetSize, deltaTerms, delta,
        numeratorSum, k, path, pathNodes, pathGain,
        loopsNotTouchingPath, cofactorLoopsData,
        maxCofactorLoopSetSize, cofactorTerms, deltaK
    },

    (* --- Basic Input Checks --- *)
    If[graph === $Failed, Return[$Failed]];
    If[!VertexQ[graph, yIn], Message[MasonsGainFormula::invnode, yIn, Input]; Return[$Failed]];
If[!VertexQ[graph, yOut], Message[MasonsGainFormula::invnode, yOut, Output]; Return[$Failed]];
    (* Check if all edges have weights (allow symbolic or numeric) *)
    If[Length[EdgeList[graph]] > 0 && MemberQ[edgeWeights, _Missing | Automatic],
        Message[MasonsGainFormula::noweights]; Return[$Failed]];

    (* --- Step 1: Find Forward Paths and Gains --- *)
    forwardPathsNodes = FindPath[graph, yIn, yOut, Infinity, All];
    forwardPathsData = Association@Table[
        With[{nodes = pathNodesList, gain = GetPathGain[graph, pathNodesList]},
             pathNodesList -> <|"Gain" -> gain, "Nodes" -> GetElementNodes[nodes]|>
        ],
        {pathNodesList, forwardPathsNodes}
    ];

    (* If no forward paths exist, the gain is 0 *)
    If[Length[forwardPathsData] == 0,
        Return[0]
    ];

    (* --- Step 2: Find All Loops and Gains --- *)
    loopsEdges = FindCycle[graph, Infinity, All];
    allLoopsData = Association@Table[
        With[{nodes = GetElementNodes[loopEdgesList], gain = GetLoopGain[graph, loopEdgesList]},
           loopEdgesList -> <|"Gain" -> gain, "Nodes" -> nodes|>
        ],
        {loopEdgesList, loopsEdges}
    ];

    (* --- Step 3: Compute Determinant Delta --- *)
    maxLoopSetSize = Length[allLoopsData];
    deltaTerms = Table[
        (-1)^i * CalculateDeterminantTerm[allLoopsData, i],
        {i, 0, maxLoopSetSize} (* From i=0 (term is 1) up to max number of loops *)
    ];
    delta = Total[deltaTerms];

    (* Check for zero determinant *)
    If[PossibleZeroQ[delta], (* Use PossibleZeroQ for symbolic robustness *)
       Message[MasonsGainFormula::detzero]; Return[Indeterminate];
    ];

    (* --- Step 4: Compute Cofactors Delta_k and Numerator Sum --- *)
    numeratorSum = 0;
    Do[ (* Iterate through each forward path k *)
        path = pathKey; (* pathKey is the list of nodes *)
        pathGain = forwardPathsData[path]["Gain"];
        pathNodes = forwardPathsData[path]["Nodes"];

        (* Identify loops NOT touching the current path *)
        cofactorLoopsData = Select[allLoopsData,
            !AreElementsTouching[#Nodes, pathNodes] &
        ];

        (* Calculate Delta_k using only these non-touching loops *)
        maxCofactorLoopSetSize = Length[cofactorLoopsData];
        cofactorTerms = Table[
            (-1)^i * CalculateDeterminantTerm[cofactorLoopsData, i],
            {i, 0, maxCofactorLoopSetSize}
        ];
        deltaK = Total[cofactorTerms];

        (* Add to numerator sum: G_k * Delta_k *)
        numeratorSum += pathGain * deltaK;
        ,
        {pathKey, Keys[forwardPathsData]}
    ];

    (* --- Step 5: Apply Formula --- *)
    Simplify[numeratorSum / delta] (* Use Simplify for potentially cleaner symbolic results *)

];

(* --- Message Definitions --- *)
MasonsGainFormula::invgraph = "`1` is not a valid directed Graph object.";
MasonsGainFormula::invnode = "`1` is not a valid vertex in the graph for `2`.";
MasonsGainFormula::noweights = "Graph edges must have explicit EdgeWeight property defined for all edges.";
MasonsGainFormula::detzero = "Determinant Delta is zero or symbolically indeterminate. Gain cannot be calculated.";
g2 = Graph[{1 -> 2, 1 -> 3, 2 -> 4, 4 -> 2, 4 -> 3}, 
   EdgeWeight -> {s21, s11, R2, s22, s12}, 
   VertexLabels -> Automatic];
MasonsGainFormula[g2, 1, 3] //Print
MasonsGainFormula[g2, 1, 2] //Print

enter image description here

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.