d4t_formulas/lib/formula_evaluator.dart

463 lines
13 KiB
Dart
Raw Normal View History

import 'dart:math' as Math;
import 'package:d4rt/d4rt.dart';
import 'formula_models.dart';
2026-02-09 15:57:53 +00:00
import 'error_handler.dart';
import 'd4rt_bridge.dart';
/// Exception thrown when formula evaluation fails
class FormulaEvaluationException implements Exception {
final String message;
final Object? cause;
const FormulaEvaluationException(this.message, [this.cause]);
@override
2026-03-04 21:24:37 +00:00
String toString() =>
'FormulaEvaluationException: $message'
'${cause != null ? ' (caused by: $cause)' : ''}';
}
2026-03-04 21:24:37 +00:00
class MyMath {
static Number myLog(Number x) => Math.log(x);
2026-03-04 21:24:37 +00:00
static Number myPow(Number b, Number e) => Math.pow(b, e) as Number;
}
2026-03-04 21:24:37 +00:00
class FormulaResult {
const FormulaResult();
}
2026-03-04 21:24:37 +00:00
class StringResult extends FormulaResult {
final String value;
2026-03-04 21:24:37 +00:00
const StringResult(this.value);
}
2026-03-04 21:24:37 +00:00
class NumberResult extends FormulaResult {
final Number value;
2026-03-04 21:24:37 +00:00
const NumberResult(this.value);
}
class FormulaEvaluator {
final D4rt _interpreter;
static D4rt createDefaultInterpreter() => D4rt();
2026-03-05 17:31:27 +00:00
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter() {
prepareInterpreter(_interpreter);
}
2026-03-04 21:24:37 +00:00
static Number _getNumberValueOf(String s) {
return double.parse(s);
}
2026-03-04 21:24:37 +00:00
static void prepareInterpreter(D4rt interpreter) {
final myMathDefinition = BridgedClass(
nativeType: MyMath,
name: 'MyMath',
staticMethods: {
'myPow': (visitor, positionalArgs, namedArgs) {
2026-03-04 21:24:37 +00:00
final Number base = _getNumberValueOf(positionalArgs[0].toString());
final Number exp = _getNumberValueOf(positionalArgs[1].toString());
return MyMath.myPow(base, exp);
},
'myLog': (visitor, positionalArgs, namedArgs) {
2026-03-04 21:24:37 +00:00
final Number x = _getNumberValueOf(positionalArgs[0].toString());
return MyMath.myLog(x);
2025-09-22 15:00:34 +00:00
},
2026-03-04 21:24:37 +00:00
},
);
2026-03-05 17:31:27 +00:00
interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart");
registerD4rtBridgeBridges(interpreter);
}
static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) {
final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
prepareInterpreter(d4rtInterpreter);
2026-03-04 21:24:37 +00:00
final d4rtCode =
"""
$preamble
main()
{
late var result;
result = $code;
return result;
}""";
print("evaluateExpression:\n$d4rtCode");
final result = d4rtInterpreter.execute(source: d4rtCode);
2026-03-04 21:24:37 +00:00
switch (result) {
case int value:
return NumberResult(value.toDouble());
case Number value:
return NumberResult(value);
case String value:
return StringResult(value);
default:
2026-03-05 17:31:27 +00:00
throw FormulaEvaluationException("Unexpected result type: ${result.runtimeType} -- $result");
}
}
2026-03-04 21:24:37 +00:00
dynamic evaluate(Formula formula, Map<String, dynamic> inputValues) {
_validateInputValues(formula, inputValues);
2025-09-22 15:00:34 +00:00
final completeSource = _buildCompleteSource(formula, inputValues);
2026-01-25 18:03:57 +00:00
try {
final result = _interpreter.execute(source: completeSource);
return result;
2026-03-04 21:24:37 +00:00
} catch (e, stack) {
2026-02-18 10:25:14 +00:00
// SPECIAL CASE: If the error message starts with signalMagicString, treat it as a signal message and return it instead of throwing an exception
// SEE signal() function in the generated d4rt code above for how this is used
2026-03-04 21:24:37 +00:00
if (e.toString().contains(signalMagicString)) {
2026-02-18 10:25:14 +00:00
final signalMessage = e.toString().split(signalMagicString).last.trim();
return signalMessage;
}
errorHandler.notify("$e\n$completeSource", stack);
2026-03-05 17:31:27 +00:00
throw FormulaEvaluationException('Error evaluating formula "${formula.name}": $e', e);
2026-01-25 18:03:57 +00:00
}
}
void _validateInputValues(Formula formula, Map<String, dynamic> inputValues) {
final missingVars = <String>[];
2026-02-07 10:45:25 +00:00
2025-08-24 09:52:34 +00:00
for (final inputVar in formula.inputVarNames()) {
if (!inputValues.containsKey(inputVar)) {
missingVars.add(inputVar);
}
}
2026-02-07 10:45:25 +00:00
if (missingVars.isNotEmpty) {
throw FormulaEvaluationException(
'Missing required input variables for formula "${formula.name}": '
'${missingVars.join(', ')}',
);
}
2026-02-07 10:45:25 +00:00
// Validate that input values are in the allowed values list if specified
for (final vs in formula.input) {
final values = vs.values;
if (values != null && values.isNotEmpty) {
final inputValue = inputValues[vs.name];
if (inputValue != null) {
// Convert input value to string for comparison since allowed values are stored as strings
final inputValueAsString = inputValue.toString();
2026-03-05 17:31:27 +00:00
final containsValue = values.any((allowedValue) => allowedValue.toString() == inputValueAsString);
2026-03-04 21:24:37 +00:00
2026-02-07 10:45:25 +00:00
if (!containsValue) {
throw FormulaEvaluationException(
'Invalid value for variable "${vs.name}" in formula "${formula.name}". '
'Expected one of: [${values.join(', ')}], but got: $inputValue',
);
}
}
}
}
}
List<String> getInputVariableOrder(Formula formula) {
2025-08-24 09:52:34 +00:00
return formula.inputVarNames()..sort();
}
2026-02-18 10:25:14 +00:00
static final String signalMagicString = "###";
2026-03-04 21:24:37 +00:00
static final String preamble =
"""
import 'dart:math';
import "package:d4rt_formulas.dart";
import "package:formulas/runtime_bridge.dart";
void signal( String msg ) => throw Exception("$signalMagicString\$msg");
dynamic fn(String formulaName, Map<String, dynamic> inputValues) => D4rtBridgeImpl.fn(formulaName, inputValues);
2026-02-18 08:46:02 +00:00
""";
2026-03-05 17:31:27 +00:00
static const reservedVariableNames = {"variableValues", "indexOf", "variableAllowedValues"};
2026-02-01 15:16:04 +00:00
2026-03-05 17:31:27 +00:00
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) {
final buffer = StringBuffer();
2025-09-22 15:00:34 +00:00
buffer.writeln("""
$preamble
2025-09-22 15:00:34 +00:00
main()
{
2026-03-04 21:24:37 +00:00
""");
for (final entry in inputValues.entries) {
final varName = entry.key;
final value = entry.value;
2026-03-04 21:24:37 +00:00
if (value is String) {
final escapedValue = value.replaceAll('"', '\\"');
2025-09-22 15:00:34 +00:00
buffer.writeln("""
final $varName = "$escapedValue";
""");
} else {
2025-09-22 15:00:34 +00:00
buffer.writeln("""
final $varName = $value;
""");
}
}
buffer.writeln("""
final variableValues = <String, dynamic>{
""");
for (final entry in inputValues.entries) {
final varName = entry.key;
final value = entry.value;
if (value is String) {
final escapedValue = value.replaceAll('"', '\\"');
buffer.writeln("""
"$varName": "$escapedValue",
""");
} else {
buffer.writeln("""
"$varName": "$value",
""");
}
}
buffer.writeln("""
};
""");
// Build a Map<String, List<String>> named `variableValues` that exposes allowed values
// for each VariableSpec (inputs and output) to the interpreted code. Values are
// converted to strings and quoted in the produced d4rt source.
final variableValuesMap = <String, List<String>>{};
// Include input VariableSpecs when they have allowed values
for (final vs in formula.input) {
final values = vs.values;
if (values != null && values.isNotEmpty) {
2026-03-05 17:31:27 +00:00
variableValuesMap[vs.name] = values.map((v) => v.toString()).toList(growable: false);
}
}
// Explicitly include the output VariableSpec if it has allowed values
final outValues = formula.output.values;
if (outValues != null && outValues.isNotEmpty) {
2026-03-05 17:31:27 +00:00
variableValuesMap[formula.output.name] = outValues.map((v) => v.toString()).toList(growable: false);
}
// Write the variableValues map into the generated source without escaping names/values
buffer.writeln("final variableAllowedValues = {");
variableValuesMap.forEach((name, list) {
final listLiteral = list.map((s) => '"' + s + '"').join(', ');
buffer.writeln(' "' + name + '": [' + listLiteral + '],');
});
buffer.writeln('};');
// Some functions to deal with string values
buffer.writeln("""
2026-02-01 15:16:04 +00:00
// If return type is int, there is an error converting double to int 🤷‍
dynamic indexOf(String inputName) {
String value = variableValues[inputName];
String allowedValues = variableAllowedValues[inputName];
2026-02-01 15:16:04 +00:00
dynamic ret = allowedValues.indexOf(value) as int;
return ret as int;
}
""");
2025-09-22 15:00:34 +00:00
buffer.writeln("""
2025-11-09 19:29:58 +00:00
late var ${formula.output.name};
2025-09-22 15:00:34 +00:00
${formula.d4rtCode}
2025-11-09 19:29:58 +00:00
return ${formula.output.name};
2025-09-22 15:00:34 +00:00
}
""");
2026-03-04 21:24:37 +00:00
return buffer.toString();
}
}
2026-03-05 17:31:27 +00:00
Number formulaSolver(
2026-03-10 15:47:03 +00:00
FormulaInterface formulaInterface,
2026-03-05 17:31:27 +00:00
String variableToSolve,
Map<String, dynamic> fixedInputValues, {
Number hint = 0,
2026-03-09 17:32:39 +00:00
Number step = 100,
2026-03-05 17:31:27 +00:00
Number maxDelta = 0.01,
2026-03-09 17:32:39 +00:00
int maxTries = 1000,
2026-03-05 17:31:27 +00:00
}) {
2026-03-10 15:47:03 +00:00
var formula = FormulaInterface.getRootFormula(formulaInterface);
2026-03-05 17:31:27 +00:00
if (variableToSolve == formula.output.name) {
return FormulaEvaluator().evaluate(formula, fixedInputValues);
}
2026-03-05 17:31:27 +00:00
if (!formula.inputVarNames().contains(variableToSolve)) {
throw ArgumentError(
2026-03-05 17:31:27 +00:00
'Variable "$variableToSolve" is not an input or output variable of the formula "${formula.name}".',
);
}
final modifiedInputValues = Map<String, dynamic>.from(fixedInputValues);
var evaluator = FormulaEvaluator();
Number f(Number x) {
modifiedInputValues[variableToSolve] = x;
final result = evaluator.evaluate(formula, modifiedInputValues);
if (result is Number) {
return result;
} else {
throw FormulaEvaluationException(
'Expected formula evaluation to return a number, but got: $result ${result.runtimeType}',
);
}
}
var fixedFormulaOutput = fixedInputValues[formula.output.name];
return functionSolver(
(Number x) => f(x) - fixedFormulaOutput,
hint: hint,
step: step,
maxDelta: maxDelta,
maxTries: maxTries,
);
}
2026-03-04 21:24:37 +00:00
class NoSolutionException implements Exception {
final String message;
const NoSolutionException(this.message);
@override
String toString() => 'NoSolutionException: $message';
}
Number functionSolver(
Number Function(Number) f, {
Number hint = 0,
Number step = 10,
Number maxDelta = 0.01,
int maxTries = 100,
}) {
Number sign(Number x) => switch (x) {
> 0 => 1,
< 0 => -1,
_ => 0,
};
// Binary search between low and high (expects f(low) and f(high) to have opposite signs)
2026-03-04 21:24:37 +00:00
Number binarySearch(Number low, Number high) {
var yLow = f(low);
var yHigh = f(high);
assert(sign(yLow) != sign(yHigh));
int count = 0;
while ((high - low).abs() > maxDelta) {
count += 1;
if (count > maxTries) {
throw NoSolutionException("Failed to find a root after $maxTries tries.");
}
var mid = (low + high) / 2;
var yMid = f(mid);
if (sign(yMid) == sign(f(low))) {
low = mid;
yLow = yMid;
} else {
high = mid;
yHigh = yMid;
}
}
return (low + high) / 2;
}
// Search for an interval [x1, x2] where f changes sign
2026-03-04 21:24:37 +00:00
List<Number> searchApproximately(Number x1, Number x2) {
var y1 = f(x1);
var y2 = f(x2);
int count = 0;
while (sign(y1) == sign(y2)) {
count += 1;
if (count > maxTries) {
throw NoSolutionException("Failed to find a root after $maxTries tries.");
}
if (y1.abs() < y2.abs()) {
x2 = x1;
x1 = x1 - step;
y2 = y1;
y1 = f(x1);
} else {
x1 = x2;
x2 = x2 + step;
y1 = y2;
y2 = f(x2);
}
}
return [x1, x2];
}
// Numerical derivative using central differences
Number numericalDerivative(Number x) {
final double h = 1e-6;
Number realNumericalDerivative(Number x) {
// Ensure h is not larger than scale of x
final double dx = (x == 0) ? h : (h * x.abs().toDouble());
final Number y1 = f(x + dx);
final Number y2 = f(x - dx);
final Number deriv = (y1 - y2) / (2 * dx);
print( "numericalDerivative $deriv at x=$x: y1=$y1, y2=$y2, dx=$dx");
return deriv;
}
var ret = realNumericalDerivative(x);
if ( ret.abs() < 1e-12) {
// If derivative is too small, try a slightly modified x
ret = realNumericalDerivative(x+h);
}
return ret;
}
Number searchNewton(){
Number x = hint;
final int maxNewtonIters = maxTries;
int iter = 0;
while (iter < maxNewtonIters) {
final Number y = f(x);
2026-04-13 15:13:13 +00:00
print( "iter: $iter x: $x y: $y");
if (y == 0 || y.abs() <= maxDelta) {
return x;
}
final Number dy = numericalDerivative(x);
if (dy == 0 || dy.abs() < 1e-12) {
2026-04-17 10:37:12 +00:00
throw NoSolutionException("Derivative is zero or too small, cannot continue Newton-Raphson: $dy");
}
final Number delta = y / dy;
var xNew = x - delta;
if (xNew.isNaN || xNew.isInfinite) {
throw NoSolutionException("Newton-Raphson diverged to NaN or Infinity.");
}
// If step exploded, cap the step to a reasonable multiple of `step`
final Number maxStepAllowed = step * 1e6;
if ((xNew - x).abs() > maxStepAllowed) {
2026-04-17 10:37:12 +00:00
xNew = x - (delta.isNegative ? -maxStepAllowed : maxStepAllowed);
}
x = xNew;
iter += 1;
}
throw NoSolutionException("Failed to find a root using Newton-Raphson after $maxNewtonIters iterations.");
}
try {
return searchNewton();
2026-04-13 15:13:13 +00:00
} catch (e1) {
try {
var approx = searchApproximately(hint, hint + step);
return binarySearch(approx[0], approx[1]);
}
catch( e2 ){
errorHandler.notify(e1);
errorHandler.notify(e2);
throw NoSolutionException("Failed to find a root using both Newton-Raphson and approximate search: $e1 -- $e2");
}
}
2026-03-04 21:24:37 +00:00
}