Initial formula solver

This commit is contained in:
Álvaro González 2026-03-04 22:24:37 +01:00
parent fe666fdcd6
commit bc38acbff6
3 changed files with 162 additions and 47 deletions

View file

@ -7,7 +7,7 @@ Calculates the magnitude of the electrostatic force between two point charges.
Formula: $F = k \dfrac{q_1 q_2}{r^2}$ where $k = 8.9875517923\times10^9\ \mathrm{N\,m^2/C^2}$. Formula: $F = k \dfrac{q_1 q_2}{r^2}$ where $k = 8.9875517923\times10^9\ \mathrm{N\,m^2/C^2}$.
Inputs: `q1`, `q2` in coulombs; `r` in meters. Inputs: $q_1$, $q_2$ in coulombs; $r$ in meters.
Output: Force `F` in newtons (N).""", Output: Force `F` in newtons (N).""",
"input": [ "input": [
{"name": "q1", "unit": "coulomb"}, {"name": "q1", "unit": "coulomb"},

View file

@ -7,11 +7,6 @@ import 'formula_models.dart';
import 'error_handler.dart'; import 'error_handler.dart';
import 'd4rt_bridge.dart'; import 'd4rt_bridge.dart';
/// Exception thrown when formula evaluation fails /// Exception thrown when formula evaluation fails
class FormulaEvaluationException implements Exception { class FormulaEvaluationException implements Exception {
final String message; final String message;
@ -20,26 +15,30 @@ class FormulaEvaluationException implements Exception {
const FormulaEvaluationException(this.message, [this.cause]); const FormulaEvaluationException(this.message, [this.cause]);
@override @override
String toString() => 'FormulaEvaluationException: $message' String toString() =>
'FormulaEvaluationException: $message'
'${cause != null ? ' (caused by: $cause)' : ''}'; '${cause != null ? ' (caused by: $cause)' : ''}';
} }
class MyMath{ class MyMath {
static Number myLog(Number x) => Math.log(x); static Number myLog(Number x) => Math.log(x);
static Number myPow(Number b, Number e) => Math.pow(b,e) as Number;
static Number myPow(Number b, Number e) => Math.pow(b, e) as Number;
} }
class FormulaResult{ class FormulaResult {
const FormulaResult(); const FormulaResult();
} }
class StringResult extends FormulaResult{ class StringResult extends FormulaResult {
final String value; final String value;
const StringResult(this.value); const StringResult(this.value);
} }
class NumberResult extends FormulaResult{ class NumberResult extends FormulaResult {
final Number value; final Number value;
const NumberResult(this.value); const NumberResult(this.value);
} }
@ -48,39 +47,44 @@ class FormulaEvaluator {
static D4rt createDefaultInterpreter() => D4rt(); static D4rt createDefaultInterpreter() => D4rt();
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter(){ FormulaEvaluator([D4rt? interpreter])
: _interpreter = interpreter ?? createDefaultInterpreter() {
prepareInterpreter(_interpreter); prepareInterpreter(_interpreter);
} }
static Number _getNumberValueOf(String s){ static Number _getNumberValueOf(String s) {
return double.parse(s); return double.parse(s);
} }
static void prepareInterpreter(D4rt interpreter){ static void prepareInterpreter(D4rt interpreter) {
final myMathDefinition = BridgedClass( final myMathDefinition = BridgedClass(
nativeType: MyMath, nativeType: MyMath,
name: 'MyMath', name: 'MyMath',
staticMethods: { staticMethods: {
'myPow': (visitor, positionalArgs, namedArgs) { 'myPow': (visitor, positionalArgs, namedArgs) {
final Number base = _getNumberValueOf( positionalArgs[0].toString() ); final Number base = _getNumberValueOf(positionalArgs[0].toString());
final Number exp = _getNumberValueOf( positionalArgs[1].toString() ); final Number exp = _getNumberValueOf(positionalArgs[1].toString());
return MyMath.myPow(base,exp); return MyMath.myPow(base, exp);
}, },
'myLog': (visitor, positionalArgs, namedArgs) { 'myLog': (visitor, positionalArgs, namedArgs) {
final Number x = _getNumberValueOf( positionalArgs[0].toString() ); final Number x = _getNumberValueOf(positionalArgs[0].toString());
return MyMath.myLog(x); return MyMath.myLog(x);
}, },
} },
); );
interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart"); interpreter.registerBridgedClass(
myMathDefinition,
"package:d4rt_formulas.dart",
);
registerD4rtBridgeBridges(interpreter); registerD4rtBridgeBridges(interpreter);
} }
static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) { static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) {
final d4rtInterpreter = interpreter ?? createDefaultInterpreter(); final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
prepareInterpreter(d4rtInterpreter); prepareInterpreter(d4rtInterpreter);
final d4rtCode = """ final d4rtCode =
"""
$preamble $preamble
main() main()
{ {
@ -90,7 +94,7 @@ class FormulaEvaluator {
}"""; }""";
print("evaluateExpression:\n$d4rtCode"); print("evaluateExpression:\n$d4rtCode");
final result = d4rtInterpreter.execute(source: d4rtCode); final result = d4rtInterpreter.execute(source: d4rtCode);
switch ( result ){ switch (result) {
case int value: case int value:
return NumberResult(value.toDouble()); return NumberResult(value.toDouble());
case Number value: case Number value:
@ -98,7 +102,9 @@ class FormulaEvaluator {
case String value: case String value:
return StringResult(value); return StringResult(value);
default: default:
throw FormulaEvaluationException( "Unexpected result type: ${result.runtimeType} -- $result" ); throw FormulaEvaluationException(
"Unexpected result type: ${result.runtimeType} -- $result",
);
} }
} }
@ -108,13 +114,12 @@ class FormulaEvaluator {
try { try {
final result = _interpreter.execute(source: completeSource); final result = _interpreter.execute(source: completeSource);
return result; return result;
} } catch (e, stack) {
catch (e, stack) {
// SPECIAL CASE: If the error message starts with signalMagicString, treat it as a signal message and return it instead of throwing an exception // 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 // SEE signal() function in the generated d4rt code above for how this is used
print( "#######################"); print("#######################");
if(e.toString().contains(signalMagicString)){ if (e.toString().contains(signalMagicString)) {
print( "***********************"); print("***********************");
final signalMessage = e.toString().split(signalMagicString).last.trim(); final signalMessage = e.toString().split(signalMagicString).last.trim();
return signalMessage; return signalMessage;
} }
@ -151,7 +156,9 @@ class FormulaEvaluator {
if (inputValue != null) { if (inputValue != null) {
// Convert input value to string for comparison since allowed values are stored as strings // Convert input value to string for comparison since allowed values are stored as strings
final inputValueAsString = inputValue.toString(); final inputValueAsString = inputValue.toString();
final containsValue = values.any((allowedValue) => allowedValue.toString() == inputValueAsString); final containsValue = values.any(
(allowedValue) => allowedValue.toString() == inputValueAsString,
);
if (!containsValue) { if (!containsValue) {
throw FormulaEvaluationException( throw FormulaEvaluationException(
@ -164,15 +171,14 @@ class FormulaEvaluator {
} }
} }
List<String> getInputVariableOrder(Formula formula) { List<String> getInputVariableOrder(Formula formula) {
return formula.inputVarNames()..sort(); return formula.inputVarNames()..sort();
} }
static final String signalMagicString = "###"; static final String signalMagicString = "###";
static final String preamble = """ static final String preamble =
"""
import 'dart:math'; import 'dart:math';
import "package:d4rt_formulas.dart"; import "package:d4rt_formulas.dart";
import "package:formulas/runtime_bridge.dart"; import "package:formulas/runtime_bridge.dart";
@ -181,18 +187,23 @@ class FormulaEvaluator {
"""; """;
static const reservedVariableNames = { "variableValues", "indexOf", "variableAllowedValues"} ; static const reservedVariableNames = {
"variableValues",
"indexOf",
"variableAllowedValues",
};
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) { String _buildCompleteSource(
Formula formula,
Map<String, dynamic> inputValues,
) {
final buffer = StringBuffer(); final buffer = StringBuffer();
buffer.writeln(""" buffer.writeln("""
$preamble $preamble
main() main()
{ {
""" """);
);
for (final entry in inputValues.entries) { for (final entry in inputValues.entries) {
final varName = entry.key; final varName = entry.key;
@ -241,13 +252,17 @@ class FormulaEvaluator {
for (final vs in formula.input) { for (final vs in formula.input) {
final values = vs.values; final values = vs.values;
if (values != null && values.isNotEmpty) { if (values != null && values.isNotEmpty) {
variableValuesMap[vs.name] = values.map((v) => v.toString()).toList(growable: false); variableValuesMap[vs.name] = values
.map((v) => v.toString())
.toList(growable: false);
} }
} }
// Explicitly include the output VariableSpec if it has allowed values // Explicitly include the output VariableSpec if it has allowed values
final outValues = formula.output.values; final outValues = formula.output.values;
if (outValues != null && outValues.isNotEmpty) { if (outValues != null && outValues.isNotEmpty) {
variableValuesMap[formula.output.name] = outValues.map((v) => v.toString()).toList(growable: false); variableValuesMap[formula.output.name] = outValues
.map((v) => v.toString())
.toList(growable: false);
} }
// Write the variableValues map into the generated source without escaping names/values // Write the variableValues map into the generated source without escaping names/values
@ -278,4 +293,78 @@ class FormulaEvaluator {
return buffer.toString(); return buffer.toString();
} }
}
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,
};
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;
}
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];
}
var approx = searchApproximately(hint, hint + step);
return binarySearch(approx[0], approx[1]);
}

View file

@ -0,0 +1,26 @@
import 'package:d4rt_formulas/formula_evaluator.dart';
import 'package:d4rt_formulas/formula_models.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test("Solve x^2", () {
Number f(Number x) => x*x;
var root = functionSolver(f, hint: 10, step: 1);
expect(root, closeTo(0, 0.1));
});
test("Solve x^2 + 1", () {
Number f(Number x) => x*x+1;
expect(()=> functionSolver(f, hint: 10, step: 1), throwsA(isA<NoSolutionException>()));
});
test("Solve (x-2)(x-10", () {
Number f(Number x) => (x-2)*(x-10);
expect( functionSolver(f, hint: 10, step: 1), closeTo(10, 0.1));
});
}