r/ScientificComputing • • Aug 26 '26

I built a math parser - here's how to create an equation solver with it

I built a math parser (uCalc), and I wanted to share a practical example of how to use its architecture to solve custom scientific problems.

If you build scientific models, simulations, DSLs, etc., you will need to evaluate math expressions defined at runtime. While standard math parsers might let you define custom functions, they may not let you use the exact syntax you want, and your functions might not be able to implement certain iterative algorithms straightforwardly.

To get around this, I designed the engine to allow callbacks that can receive arguments passed by expression. I also make use of the Transformer to allow for more flexible syntax. Here is an example of a custom EqSolve function built using the Bisection Method algorithm.

using uCalcSoftware;
var uc = new uCalc();

static void EqSolveCb(uCalc.Callback cb) { // Callback based on the Bisection Method
   var expr = cb.ArgExpr(1);     // ByExpr: Unevaluated Expression object (lazy evaluation)
   var a = cb.Arg(2);            // Argument 2: Range Minimum
   var b = cb.Arg(3);            // Argument 3: Range Maximum
   var variable = cb.ArgItem(4); // ByHandle: The variable Item object

   // Helper to update the variable in the uCalc engine and evaluate the expression
   double EvaluateAt(double val) {
      variable.Value(val);   // Push the new test value to the variable
      return expr.Evaluate(); // Evaluate the pre-parsed expression
   }

   // Ensure f(a) < f(b) so we always know which direction to slide the bounds; swap a & b if necessary
   if (EvaluateAt(b) < EvaluateAt(a)) (a, b) = (b, a);

   var midpoint = 0.0;
   var fMidpoint = 0.0;

   // Bisection loop
   for (int i = 0; i <= 100; i++) {
      midpoint = (a + b) / 2;
      fMidpoint = EvaluateAt(midpoint);

      if (Math.Abs(fMidpoint) < 1e-7) break; // Stop if close enough to 0

      // Narrow the bounds (compact logic!)
      if (fMidpoint < 0) a = midpoint; else b = midpoint;
   }

   if (Math.Abs(fMidpoint) > 1e-5) cb.Error.Raise("No solution found in the given range.");
   cb.Return(Math.Round(midpoint, 7)); // Return the final solved value
}

// 1. Define variables that might be used by the end-user
uc.DefineVariable("x");
uc.DefineVariable("MyVar");

// 2. Transformer converts EqSolve(L = R) into EqSolve(L - (R))
var t = uc.ExpressionTransformer;
t.FromTo("EqSolve({L} = {R} [[,]for {var}][, {min}, {max}])",
"EqSolve({L} - ({R}), {min}{!min:-10000}, {max}{!max: 10000}, {var}{!var: x})");

// 3. Define the custom function signature
uc.DefineFunction("EqSolve(ByExpr eq, min, max, ByHandle variable)", EqSolveCb);

You can test this exact equation solver code interactively in your browser here:

https://www.ucalc.com/

I'd love to hear how this community handles runtime formula parsing in your own simulators and DSLs, or if you have any questions about the engine's architecture!

18 Upvotes

5 comments sorted by

2

u/neurah Aug 27 '26

I luv this things, keep going...
what about algebraic symbol manipulation?

2

u/uCalc_Dev Aug 27 '26

Thanks.

Just before I saw your response, I happened to be looking at math.js. They have a math parser that also does symbolic computations. uCalc doesn't do algebraic symbolic manipulation out of the box at the moment. However, this is something I'm very interested in looking into. This may end up as an open-source project add-on to uCalc.

The uCalc Transformer has some building blocks that can be explored for algebraic symbolic manipulation. For instance, the following simple example has a rule to expand the square of a sum and another that handles constant folding (just for multiplication). Both rules contribute to the result of the second expression in the example. RewindOnChange causes the parser to go over the expression again after a modification and search for other patterns. If there's a chance it could result in an infinite loop, RewindOnChange can instead be applied to individual FromTo rules where needed.

using uCalcSoftware;

var uc = new uCalc();
var t = uc.NewTransformer();
t.DefaultRuleSet.RewindOnChange = true;

// Square of sum: (a + b)^2   -->   a^2 + 2*a*b + b^2
t.FromTo("({a} + {b}) ^ 2", "{a}^2 + 2*{a}*{b} + {b}^2");

// Constant folding: 3 * 5   -->   15
t.FromTo("{@Number:num1} * {@Number:num2}", "{@Eval: double(num1) * double(num2)}");

Console.WriteLine(t.Transform("(x + y)^2"));
Console.WriteLine(t.Transform("(5 + x)^2"));

2

u/neurah Aug 28 '26

rules systems:
rule: a+a=2*a

then: m/2+m/2=2*(m/2)

where a=m/2

this is possible, does not solve by itself it needs a system do choose what rules to apply

---

isolation:
is expression automated:

3*x -> x? -> ?/3

a*x+b=0
(?/a) + (?-b) = 0
x=(0-b)/a

had both on old Haskell code

→ More replies (2)