systemSolver2.frink

Download or view systemSolver2.frink in plain text format


// This program solves systems of equations.
// See "systemSolverTest.frink" or
//  https://frinklang.org/fsp/solve2.fsp
// for examples of using it.

use allTransforms.frink

showApproximations[false]

class System
{
   // An array of equations, parsed to expressions.  These should use the
   // === operator to separate the sides of the equations.
   var equations

   // A dictionary mapping symbol names (as strings)
   // to an array of the equation numbers
   // in which they occur.  (Or should it be the equation objects?)
   var variableOccurrences

   // A list of symbols in each equation.
   var symbols

   // A set of symbols that we're not going to solve for, because
   // they're intended to be units.  This is a set of strings.
   var ignoreSymbolsStrings = new set

   // A set of symbols that we're not going to solve for, because
   // they're intended to be units.  This is a set of symbols
   var ignoreSymbols = new set
   
   // A dictionary containing solved equations.
   // The key is the variable name (as a string)
   // The value is an array of equations that comprise solutions for that
   // variable.
   var solutions

   // Create a new System with the given equations, passed in as a list.
   new[equationList, ignoreSet=[], debug=false] :=
   {
      if debug
         println["orig ignoreSet is " + ignoreSet]
      for sym = ignoreSet
      {
         if debug
            println["Adding $sym"]
         addIgnoreSymbol[sym]
      }

      if debug
         println["ignoreSymbolsStrings is " + ignoreSymbolsStrings]
      
      equationTemp = deepCopy[equationList]
      equationTemp = solveSimultaneous[equationTemp, debug]
      //      equationTemp = solveSimultaneous[equationTemp, debug]

      equations = new array
      variableOccurrences = new dict
      symbols = new array

      for e = equationTemp
         addEquation[e]

      solutions = new dict
   }

   // Adds a symbol to the ignore list.  This adds it both to the
   // ignoreSymbols and ignoreSymbolsStrings sets.
   addIgnoreSymbol[sym is string] :=
   {
      ignoreSymbolsStrings.put[sym]
      ignoreSymbols.put[constructExpression["Symbol", sym]]
   }

   // Adds a new equation to the system.  Normally we would want all of the
   // equations to be known at construction time, but this may allow us to
   // add more equations to the system later, to provide a "refinement" of
   // the system we're using.  We generally don't want people to add equations
   // after we've partially solved the system, or if we do, we'll have to
   // clear any cache (like the "solutions" dictionary) and re-solve.
   addEquation[e] :=
   {
      i = length[equations]
      equations.push[e]
      
      syms = getSymbols[e]
      symbols@i = syms
      for sym = syms
      {
         if variableOccurrences@sym == undef
            variableOccurrences@sym = new array

         (variableOccurrences@sym).push[i]
      }
   }

   // Find the solutions for the symbol x (specified as a string.)
   // This function sets up for a recursive call.
   solveFor[x, debug=false] :=
   {
      prev = solutions@x
      if prev
         return prev
      
      results = new array       // Array of expressions containing results.
      usedEquations = new set   // Set of numbers of used equations.
      usedSymbols = new set     // Set of names of used symbols.
      results = solveFor[x, results, usedEquations, usedSymbols, debug]
      
      solutions@x = flatten[results]     // Put into dictionary
      return results
   }

   // The private recursive call.
   solveFor[x, results, usedEquations, usedSymbols, debug=false] :=
   {
      usedSymbols.put[x]
      // println["Used symbol $x"]
      for ei = variableOccurrences@x  // For each equation that x is used in...
      {
         if usedEquations.contains[ei]
            next                // Skip equation if it's already been used.
            
         usedEquations.put[ei]
         // println["Used equation $ei"]
         eq = equations@ei

         results.push[solveSingle[eq, x]]
         // println["The equation is $solveEq"]
         // println[result]
      }

      // println["Results before recursion:\n" + results]

      // We count the number of results currently on the list and only
      // substitute into the existing results, as we'll be adding to the
      // results list.
      s = length[results]
      for n = 0 to s-1
      {
         res = results@n
         symbols = array[getSymbols[res]]

         for sym = symbols
            if ! usedSymbols.contains[sym]
            {
               results2 = new array
//               println["Solving for $sym"]
               results2 = solveFor[sym, results2, deepCopy[usedEquations], deepCopy[usedSymbols], debug]
//               println["Solved for $sym, got back " + results2]
               symSymbol = constructExpression["Symbol", sym]

               // TODO:  Only substitute below if the result is a fully solved
               // === expression with the appropriate form.
               for eq2 = flatten[results2]
               {
                  if structureEquals[_a === _b, eq2]
                  {
                     results.push[transformExpression[substituteExpression[res, symSymbol, child[eq2, 1]]]]
//                   println["Used $eq2"]
                  } else
                       if debug
                           println["Discarded unsolved $eq2"]
               }
            }
      }

      return eliminateOverconstrained[results, debug]
   }

   // Find the solutions for the symbol x (specified as a string.)
   // values is an array of [varname, value] pairs.
   solveForValues[x, values, allValues=false, debug=false] :=
   {
      origSolutions = solveFor[x, debug]
      newSolutions = new array
//      if debug
//         println["solveForValues: origSolutions are:" + origSolutions]
      
      for sol = origSolutions
      {
         newSol = sol
         for [symName, value] = values
         {
            if debug
            {
               println["\nReplacing $symName with $value"]
               println["old is:" + newSol]
            }
            
            newSol = replaceVar[newSol, symName, value]
            
            if debug
               println["new is:" + newSol]
         }

         newSolutions.push[transformExpression[newSol]]
      }

      if (allValues == false)
         newSolutions = eliminateOverconstrained[newSolutions, debug]

      solvedVals = new array
      allVals = new array
      for sol = newSolutions
      {
         right = getChild[sol,1]
         allVals.push[right]
         if length[setDifference[getSymbols[right], ignoreSymbolsStrings]] == 0 // Solved?
            solvedVals.push[right]
      }

      if (allValues==false and length[solvedVals] > 0)  // Have fully-solved?
         return solvedVals
      else
         return allVals
   }

   // Find the solutions for the symbol x (specified as a string.)
   // values is an array of [varname, value] pairs.
   // This just sets up arguments for the recursive call.
   solveForValuesOld[x, values, allValues=false, debug=false] :=
   {
      // Create a new set of values we've already substituted for.
      substituted = new set
      // Dictionary<set, equations>
      cachedSolutions = new dict
      return solveForValuesRecursive[x, values, substituted, cachedSolutions, allValues, debug]
   }

   // Puts the specified values into the cache.
   class putCache[cachedSolutions, varName, substituted, vals] :=
   {
      if ! cachedSolutions.containsKey[varName]
         cachedSolutions@varName = new dict

      cachedSolutions@varName@substituted = vals
   }

   // This is the "private" method that does recursive solution of
   // solutions for the symbol x.
   solveForValuesRecursive[x, values, substituted, cachedSolutions, allValues, debug=false] :=
   {
      if cachedSolutions.containsKey[x]
         if (cachedSolutions@x).containsKey[substituted]
            return cachedSolutions@x@substituted

      if debug
         println["Solving for $x, substituted is $substituted"]

      sols = solveFor[x, debug]
      valset = new set
      for [varname, val] = values
      {
         if varname == x        // User passed in solution?
         {
            putCache[cachedSolutions, x, substituted, [val]]
            return [val]          // TODO:  Warn as overspecified?
         }
//         sols = replaceVar[sols, varname, val]  // THINK ABOUT:  Do this later?
         valset.put[varname]
      }

      if debug
         println["Initial solutions for $x are $sols"]
      if (allValues == false)
         sols = eliminateOverconstrained[flatten[sols]]
      sort[sols, {|a,b| length[getSymbols[a]] <=> length[getSymbols[b]]}]

      // Ignore the variable we're solving for and things in the ignore list.
      ignoreVals = new set
      ignoreVals.put[x]
      ignoreVals = union[ignoreVals, ignoreSymbolsStrings]
      res = new array
      for sol = sols
         if length[setDifference[getSymbols[sol], ignoreVals]] == 0  // Solved?
            for s = array[sol]
               res.push[child[s,1]]  // Eval here?

      if length[res] > 0        // Got fully-solved solutions?
      {
         putCache[cachedSolutions, x, substituted, res]
         if debug
            println["Solutions for $x (fully solved) are $res"]
         return res
      } else    // Not fully solved, return symbolic solutions 
      {
          xSym = constructExpression["Symbol", x]
          allIgnores = deepCopy[ignoreSymbolsStrings]
          allIgnores = union[allIgnores, substituted]
          allIgnores = union[allIgnores, valset]
          allIgnores.put[x]

          // Get all symbols in all solutions, minus the things we're
          // ignoring.
          allSyms = setDifference[getSymbols[sols], allIgnores]

          // solsDict is a dictionary <solveFor, [equations]>
          solsDict = new dict
          for u = allSyms
          {
             newsub = deepCopy[substituted]
             newsub.put[x]
             solsDict@u = solveForValuesRecursive[u, values, newsub, cachedSolutions, allValues, debug]
          }

//          println["solsDict is $solsDict"]

          // Now iterate through each solution and substitute in each 
          for s = sols
             if structureEquals[_a === _b, s]
             {
                right = child[s,1]
                unknowns = setDifference[getSymbols[right], allIgnores]
                newsub = deepCopy[substituted]
                newsub.put[x]

                for u = unknowns
                {
                   res2 = solveForValuesRecursive[u, values, newsub, cachedSolutions, allValues, debug]
                   uSym = constructExpression["Symbol", u]
                   for r2 = flatten[res2]
                   {
                      if debug
                         println["Substituting in $s, from $u, to $r2"]
                      s = substituteExpression[s, uSym, r2]
                   }
                }
                
                s = transformExpression[s]
                
                g = getSymbols[child[s, 1]]
                if ! g.contains[x]  // Solution contains x?
                {
                   res.push[s]
                   if debug
                      println["Adding solution $s"]
                } else
                   if debug
                      println["Solution $s rejected because it contains $x"]
             } else
                if debug
                   println["Solution $s rejected because unsolved."]
             

          for [varname, val] = values
             res = replaceVar[res, varname, val]  // THINK ABOUT:  Do this later?

          if (allValues == false)
             res = eliminateOverconstrained[res, debug]
          size = length[res]
          for i=0 to size-1
          {
             child = child[res@i, 1]  // Return only right-hand-sides
             if isSubset[getSymbols[child], ignoreSymbolsStrings]
                res@i = child  // Eval here?
             else
                res@i = child
          }
          if debug
             println["Solutions for $x are $res"]

          putCache[cachedSolutions, x, substituted, res]
          return res  //, false, debug]
      }
   }
   
   // Solve for all unknowns in the system.
   // Setting fullReplace=true makes this work harder at performing full
   // substitutions of all variables in all solutions, but it may be
   // prohibitively slow for a system with a large number of solutions and
   // variables.
   solveAll[fullReplace=false, debug=false] :=
   {
      results = new array
      for [sym, appear] = variableOccurrences
         if ! ignoreSymbolsStrings.contains[sym]
            results.push[solveFor[sym, debug]]

      if debug
         println["Before fullReplace:\n" + join["\n", flatten[results]]]

         // TODO:  Comment back in fullReplace.
         //println["Results are" + results]

      if fullReplace
         return fullReplace[flatten[results], true, debug]
      else
         return flatten[results]
   }

   
   // Once all equations have been solved, this function will redistribute all
   // solutions through all equations.
   fullReplace[equations, allValues=false, debug=false] :=
   {
      // This is a dictionary with variable names as keys and a list of 
      // equations that are the solutions with those variables.  It will
      // replace the solutions member when we're complete.
      newSolutions = new dict
      solArray = new array

      for eq = equations
      {
         if debug
            println["Equation is: $eq"]
         left = getChild[eq, 0]
         right = getChild[eq, 1]
         leftStr = toString[left]
//         println["leftStr is $leftStr, type of leftStr is " + type[leftStr]]
         allSyms = getSymbols[right]
//         println["Symbols on right are " + allSyms]
//         println["ignoreSymbols are " + ignoreSymbols]
//         println["ignoreSymbolsStrings are " + ignoreSymbolsStrings]
         replaceSyms = setDifference[allSyms, ignoreSymbolsStrings]

         symList = toArray[replaceSyms]
         if debug
            println["symList is " + symList]
         stateList = new array
         solList = new array

         for sym = symList
         {
            stateList.push[new range[false, true]]
            
            // Make an array of right-hand-sides of solutions for that symbol
            solList2 = new array
            for sol = solutions@sym
            {
               solRight = child[sol, 1]
//               println["$sym, " + getSymbols[solRight]]
               if ! (getSymbols[solRight].contains[leftStr])
                  solList2.push[solRight]
//               else
//                  println["Rejected $sol because it contains $leftStr"]
            }
            solList.push[solList2]
         }

         numStates = length[symList]

         newEq = eq
         if debug
            println["Before replace: $newEq"]

         multifor states = stateList
         {
            tempSolArray = [eq]
            if debug
            {
               println["states is $states"]
               println["solList is $solList"]
            }

            newSolArray = new array
            for i = 0 to numStates-1
            {
               replaced = false
               if states@i == true
               {
                  for sol = solList@i
                  {
                     replaced = true
                     existingSols = length[tempSolArray]
                     
                     for k = 0 to existingSols-1
                     {
                        curreq = tempSolArray@k
                        curreq = substituteExpression[curreq, constructExpression["Symbol", symList@i], sol]
                        newSolArray.push[transformExpression[curreq]]
                     }
                  }
               }

               if replaced
                  tempSolArray = newSolArray
            }

            tempSolArray = eliminateOverconstrained[tempSolArray]
            if debug
               println["after replace:  $tempSolArray\n"]
            // TODO:  Change to set to avoid dups?
            solArray.pushAll[tempSolArray]
         }
      }
      
      println["Pre-Reduced is $solArray"]
      solArray = eliminateOverconstrained[solArray]
      
      if debug
         println["Reduced is $solArray"]

      for sol = solArray
      {
         key = toString[child[sol,0]]
         if newSolutions.containsKey[key]
            (newSolutions@key).push[sol]
         else
            newSolutions@key = [sol]
      }

      solutions = newSolutions
      
      return solArray
   }

   // Once all equations have been solved, this function will redistribute all
   // solutions through all equations.
   fullReplaceOld[equations, debug=false] :=
   {
      newSolutions = new dict
      res = deepCopy[equations]
      for eq = equations
      {
         if debug
            println["Working on equation $eq"]

         eqLeft = child[eq,0]
         eqRight = child[eq,1]
         lsymbols = getSymbols[eqLeft]
         lsymbolStr = array[lsymbols]@0  // Kludge to get symbol on left
         rsymbols = getSymbols[eqRight]
         syms = getSymbols[eqRight]  // Symbols on right-hand-side of equation
         syms = setDifference[syms, ignoreSymbolsStrings]
         symArray = array[syms]
         len = length[symArray]
         if debug
            println[" symArray is $symArray"]

         if len > 0
         {
            // Now go through and replace all permutations of variables.
            // This essentially counts through all the 2^n binary permutations
            states = new array 
            for i=0 to len-2
               states@i = false

            states@(len-1) = true

            i = len-1
            r = eq
            while i>=0
            {
               if (debug)
                  println["states is $states, $symArray, j is $j, symArray@j is " + symArray@j ]
               for repval = flatten[solveFor[symArray@j]]
               {
                  replaceVal = child[repval,1]
                  for j=0 to len-1
                     if states@j
                        if ! (getSymbols[replaceVal].contains[lsymbolStr])
                        {
                           if debug
                              println["    Replacing " + symArray@j + " with " + replaceVal + " from $repval in " + r]
                           r = transformExpression[replaceVar[r, symArray@j, replaceVal]]
                           if debug
                              println["     Solution is $r"]
                        }
               }

               // Eliminate x === x
               if ! structureEquals[eqLeft, child[r,1]]
                  res.push[r]

               if structureEquals[r, eq]
                  println["Expression $r unchanged."]
               else
                  println["Expression $eq changed to $r"]
  
               // Advance to next binary state
               flipped = false
               i = len-1
               while i>=0 and !flipped
               {
                  // Enter next state
                  if states@i == false
                  {
                     states@i = true
                     flipped = true
                  } else
                  {  // Carry
                     states@i = false
                     i = i - 1
                  }
               }
            }
         }
      }

      res = eliminateOverconstrained[res]
      println["\nAfter fullReplace:\n" + join["\n", res] + "\n"]

      return res
   }

   // Attempts to solve simultaneous equations in the system.
   solveSimultaneous[equationList, debug=true] :=
   {
      // Sort so that shortest equations are first.  This simplifies
      // substitutions and makes resulting equations shorter.
      // We can't do the latter because the sort function doesn't have
      // access to ignoreSymbols!  We have to create a closure to make this
      // happen?
      //      sort[equationList, {|a,b| length[setDifference[getSymbols[a],
      //      ignoreSymbols]] <=> length[setDifference[getSymbols[b],
      //      ignoreSymbols]]}]
      sort[equationList, {|a,b| length[getSymbols[a]] <=> length[getSymbols[b]]}]
      
      s = length[equationList]
      for i = 0 to s-2
      {
         ei = equationList@i
         ui = getSymbols[ei]
         aui = array[ui]
         lui = length[aui]
         
         JLOOP:
         for j = i+1 to s-1
         {
            ej = equationList@j
            uj = getSymbols[ej]

            commonSet = intersection[ui, uj]
            if length[commonSet] >= 1 or lui == 1
            {
               commonMinusIgnore = setDifference[commonSet, ignoreSymbols]
               if length[commonMinusIgnore] > 0
                  solveFor = sort[array[commonMinusIgnore]]@0
               else
                  solveFor = array[ui]@0
               
               eqSolved = solveSingle[ei, solveFor]

               if (debug)
                  println["Solving $ei for $solveFor, result is $eqSolved"]

               for part = flatten[eqSolved]
               {
                  // See if part is fully solved.
                  if structureEquals[_a === _b, part]
                  {
                     // Right-hand-side of solved equation
                     eqSolution = child[part,1]  

                     xSymbol = constructExpression["Symbol", solveFor]

                     // Substitute result into other equations.
                     for k=0 to s-1
                        if k != i     // Don't substitute into this equation.
                        {
                           orig = equationList@k
                           subst = substituteExpression[equationList@k, xSymbol, eqSolution]
                           if (orig != subst)
                           {
                              subst = transformExpression[subst]
                              // Only replace the equation if we actually simplified it.
                              if length[getSymbols[subst]] <= length[getSymbols[orig]]
                              {
                                 if debug
                                    println["Useful substitution: $part TO $subst"]
                                 equationList@k = subst
                              } else
                              if debug
                                 println["Unuseful substitution: $part TO $subst"]
                              }
                           }
                           break JLOOP
                  } else
                      println["Unsolved: $part"]
               }
            }
         }
      }
      return equationList
   }

   // This function eliminates overconstrained equations.  For example, a
   // system containing the solutions a===1/2 c r  and  a===c d^-1 r^2  is
   // overconstrained because a value can always be obtained with the first
   // equation.  The second is not necessary, and could lead to
   // inconsistent results.   This method ignores any symbols listed in the
   // ignoreSymbols list, (these are probably units,) eliminating them from
   // the equations.
   eliminateOverconstrained[eqArray, debug=false] :=
   {
      size = length[eqArray]
      unknowns = new array
      lefts = new array
      for i = 0 to size-1
      {
         lefts@i = child[eqArray@i, 0]
         unknowns@i = setDifference[getSymbols[child[eqArray@i,1]], ignoreSymbols]
      }

      res = new array

      // Check for duplicates.
      for i=0 to size-1
      {
         overconstrained = false
         j = 0
         do
         {
            if i != j and structureEquals[lefts@i, lefts@j]
            {
               // If the unknowns in j are a proper subset of the unknowns in
               // i, then it's probably a simpler form of the equation.  The
               // counterexample is with single solutions or degenerate cases
               // like 0 where we want to see all solutions.  For now, if the
               // right-hand-side is zero, we don't consider it overconstrained
               overconstrained = isProperSubset[unknowns@j, unknowns@i] && child[eqArray@j, 1] != 0 || (i<j and structureEquals[eqArray@i, eqArray@j])
               if (overconstrained)
                  if debug
                     println[eqArray@j + " is a proper subset or match of " + eqArray@i]
            }
            j=j+1
         } while (j < size) and ! overconstrained

         if (! overconstrained)
            res.push[eqArray@i]  // If we got here, no j is a proper subset of i.
      }

      return res
   }
}

// Solve a single equation for the specified symbol.
solveSingle[eq, symbol] :=
{
   xSymbol = constructExpression["Symbol", symbol]
   // We could use this in symbolic mode, otherwise it warns.
   // solveEq = solve[eq, xSymbol]
   solveEq = constructExpression["FunctionCall", ["solve", eq, xSymbol]]
   
   return transformExpression[solveEq]
}

// Replace a variable in the specified equation with the specified value.
// You should probably wrap the "eq" and "value" in a noEval[] block if
// you're not passing them in from a variable.
replaceVar[eq, varName, value] :=
{
   sym = constructExpression["Symbol", varName]
   res = substituteExpression[eq, sym, value]
   return res
}

/** This filters a list, returning only positive results. */
positive[x] :=
{
   result = new array
   for sol = x
   {
      rs = realSignum[sol]
      if rs != undef && rs > 0
         result.push[sol]
   }
   return result      
}

"systemSolver2.frink included OK"


Download or view systemSolver2.frink in plain text format


This is a program written in the programming language Frink.
For more information, view the Frink Documentation or see More Sample Frink Programs.

Alan Eliasen was born 20217 days, 15 hours, 58 minutes ago.