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 19945 days, 8 hours, 16 minutes ago.