Initial formula solver
This commit is contained in:
parent
fe666fdcd6
commit
bc38acbff6
3 changed files with 162 additions and 47 deletions
|
|
@ -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"},
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
26
test/formula_solver_test.dart
Normal file
26
test/formula_solver_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue