Skip to content

Commit 868a98c

Browse files
committed
feat: Interval evaluator for expressions
1 parent 2f41f11 commit 868a98c

File tree

3 files changed

+263
-0
lines changed

3 files changed

+263
-0
lines changed

lib/src/evaluator.dart

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,3 +469,133 @@ class RealEvaluator extends ExpressionEvaluator<num> {
469469
push1(product);
470470
}
471471
}
472+
473+
class IntervalEvaluator extends ExpressionEvaluator<Interval> {
474+
/// Create a new evaluator with the given context.
475+
IntervalEvaluator([ContextModel? context])
476+
: super(EvaluationType.INTERVAL, context ?? ContextModel());
477+
478+
@override
479+
void visitNumber(Number literal) {
480+
if (literal.value is num) {
481+
push1(Interval(literal.value, literal.value));
482+
} else if (literal.value is Interval) {
483+
push1(literal.value);
484+
} else {
485+
throw UnsupportedError(
486+
'Number $literal with type ${literal.value.runtimeType} can not be interpreted as: $type');
487+
}
488+
}
489+
490+
@override
491+
void visitInterval(IntervalLiteral literal) {
492+
var (max, min) = pop2();
493+
// Expect min and max expressions to evaluate to real numbers,
494+
// i.e. an interval with min == max.
495+
assert(min.min == min.max);
496+
assert(max.min == max.max);
497+
push1(Interval(min.min, max.min));
498+
}
499+
500+
@override
501+
void visitVariable(Variable literal) {
502+
// Resolve variable and evaluate its expression
503+
this.context.getExpression(literal.name).accept(this);
504+
}
505+
506+
@override
507+
void visitUnaryPlus(UnaryPlus op) {
508+
// no-op
509+
}
510+
511+
@override
512+
void visitUnaryMinus(UnaryMinus op) {
513+
var (val,) = pop1();
514+
push1(-val);
515+
}
516+
517+
@override
518+
void visitPlus(Plus op) {
519+
var (addend, augend) = pop2();
520+
push1(augend + addend);
521+
}
522+
523+
@override
524+
void visitMinus(Minus op) {
525+
var (subtrahend, minuend) = pop2();
526+
push1(minuend - subtrahend);
527+
}
528+
529+
@override
530+
void visitTimes(Times op) {
531+
var (multiplicand, multiplier) = pop2();
532+
push1(multiplier * multiplicand);
533+
}
534+
535+
@override
536+
void visitDivide(Divide op) {
537+
var (divisor, dividend) = pop2();
538+
push1(dividend / divisor);
539+
}
540+
541+
@override
542+
void visitModulo(Modulo op) {
543+
throw UnimplementedError(
544+
'Evaluate Modulo with type $type not supported yet.');
545+
}
546+
547+
@override
548+
void visitPower(Power op) {
549+
// Expect base to be interval.
550+
var (exp, base) = pop2();
551+
552+
// Expect exponent to be a natural number.
553+
int exponent = exp.min.toInt();
554+
num evalMin, evalMax;
555+
556+
// Distinction of cases depending on oddity of exponent.
557+
if (exponent.isOdd) {
558+
// [x, y]^n = [x^n, y^n] for n = odd
559+
evalMin = math.pow(base.min, exponent);
560+
evalMax = math.pow(base.max, exponent);
561+
} else {
562+
// [x, y]^n = [x^n, y^n] for x >= 0
563+
if (base.min >= 0) {
564+
// Positive interval.
565+
evalMin = math.pow(base.min, exponent);
566+
evalMax = math.pow(base.max, exponent);
567+
}
568+
569+
// [x, y]^n = [y^n, x^n] for y < 0
570+
if (base.min >= 0) {
571+
// Positive interval.
572+
evalMin = math.pow(base.max, exponent);
573+
evalMax = math.pow(base.min, exponent);
574+
}
575+
576+
// [x, y]^n = [0, max(x^n, y^n)] otherwise
577+
evalMin = 0;
578+
evalMax =
579+
math.max(math.pow(base.min, exponent), math.pow(base.min, exponent));
580+
}
581+
582+
assert(evalMin <= evalMax);
583+
push1(Interval(evalMin, evalMax));
584+
}
585+
586+
@override
587+
void visitFunction(MathFunction func) {
588+
if (func is! Exponential) {
589+
throw UnimplementedError();
590+
}
591+
}
592+
593+
@override
594+
void visitExponential(Exponential func) {
595+
var (val,) = pop1();
596+
597+
// Special case of a^[x, y] = [a^x, a^y] for a > 1 (with a = e)
598+
// Expect exponent to be interval.
599+
push1(Interval(math.exp(val.min), math.exp(val.max)));
600+
}
601+
}

test/evaluator_interval_test_set.dart

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
part of 'math_expressions_test.dart';
2+
3+
class IntervalEvaluatorTests extends TestSet {
4+
@override
5+
String get name => 'Interval evaluation';
6+
7+
@override
8+
String get tags => 'evaluator';
9+
10+
@override
11+
Map<String, Function> get testGroups => {
12+
// Literals
13+
'Number': evaluateNumber,
14+
'Vector': evaluateVector,
15+
'Interval': evaluateInterval,
16+
'Variable': evaluateVariable,
17+
'BoundVariable': evaluateBoundVariable,
18+
19+
// Operators: generic cases
20+
'UnaryOperator': evaluateUnaryOperator,
21+
'BinaryOperator': evaluateBinaryOperator,
22+
23+
// Default functions
24+
'Exponential': evaluateExponential,
25+
};
26+
27+
final evaluator = IntervalEvaluator();
28+
29+
final zero = Number(0);
30+
final one = Number(1);
31+
final two = Number(2);
32+
33+
void parameterized(Map<Expression, dynamic> cases,
34+
{ExpressionEvaluator? evaluator}) {
35+
evaluator ??= this.evaluator;
36+
cases.forEach((key, value) {
37+
if (value is Throws) {
38+
test('$key -> $value',
39+
() => expect(() => evaluator!.evaluate(key), value));
40+
} else {
41+
test('$key -> $value', () => expect(evaluator!.evaluate(key), value));
42+
}
43+
});
44+
}
45+
46+
void evaluateNumber() {
47+
var cases = {
48+
zero: Interval(0.0, 0.0),
49+
one: Interval(1.0, 1.0),
50+
Number(0.5): Interval(0.5, 0.5),
51+
// max precision 15 digits
52+
Number(999999999999999): Interval(999999999999999, 999999999999999)
53+
};
54+
parameterized(cases);
55+
}
56+
57+
void evaluateVector() {
58+
var cases = {
59+
Vector([Number(1.0), Number(2.0)]): throwsA(isUnsupportedError),
60+
};
61+
parameterized(cases);
62+
}
63+
64+
void evaluateVariable() {
65+
var cases = <Expression, Interval>{
66+
Variable('x'): Interval(12, 12),
67+
Variable('y'): Interval(24, 24),
68+
Variable('∞'): Interval(double.infinity, double.infinity),
69+
};
70+
71+
var evaluator = IntervalEvaluator(ContextModel()
72+
..bindVariableName('x', Number(12))
73+
..bindVariableName('y', two * Variable('x'))
74+
..bindVariableName('∞', Number(double.infinity)));
75+
76+
parameterized(cases, evaluator: evaluator);
77+
}
78+
79+
void evaluateBoundVariable() {
80+
var cases = <Expression, Interval>{
81+
BoundVariable(IntervalLiteral(Number(9), Number(9))): Interval(9.0, 9.0)
82+
};
83+
parameterized(cases);
84+
}
85+
86+
void evaluateInterval() {
87+
var cases = {IntervalLiteral(Number(1.0), Number(2.0)): Interval(1.0, 2.0)};
88+
parameterized(cases);
89+
}
90+
91+
void evaluateUnaryOperator() {
92+
final num1 = 2.25;
93+
final num2 = 5.0;
94+
var interval = IntervalLiteral(Number(num1), Number(num2));
95+
96+
var cases = {
97+
UnaryPlus(interval): Interval(num1, num2),
98+
UnaryMinus(interval): -Interval(num1, num2),
99+
UnaryMinus(UnaryMinus(interval)): Interval(num1, num2),
100+
};
101+
parameterized(cases);
102+
}
103+
104+
void evaluateBinaryOperator() {
105+
final num1 = 2.25;
106+
final num2 = 5.0;
107+
final num3 = 199.9999999;
108+
var int1 = Interval(num1, num2), int2 = Interval(num2, num3);
109+
110+
var n1 = Number(num1), n2 = Number(num2), n3 = Number(num3);
111+
var i1 = IntervalLiteral(n1, n2), i2 = IntervalLiteral(n2, n3);
112+
113+
var cases = {
114+
i1 + i2: int1 + int2,
115+
i1 - i2: int1 - int2,
116+
i1 * i2: int1 * int2,
117+
i1 / i2: int1 / int2,
118+
i1 % i2: throwsA(isUnimplementedError),
119+
i1 ^ i2: Interval(57.6650390625, 3125.0),
120+
};
121+
parameterized(cases);
122+
}
123+
124+
void evaluateExponential() {
125+
var cases = {
126+
// e(0) -> 1
127+
Exponential(IntervalLiteral(zero, zero)): Interval(1.0, 1.0),
128+
};
129+
parameterized(cases);
130+
}
131+
}

test/math_expressions_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ part 'lexer_test_set.dart';
1515
part 'parser_test_set.dart';
1616
part 'parser_petit_test_set.dart';
1717
part 'evaluator_test_set.dart';
18+
part 'evaluator_interval_test_set.dart';
1819

1920
/// relative accuracy for floating-point calculations
2021
const num EPS = 0.00001;
@@ -28,6 +29,7 @@ void main() {
2829
PetitParserTests(),
2930
ExpressionTests(),
3031
RealEvaluatorTests(),
32+
IntervalEvaluatorTests(),
3133
];
3234

3335
TestExecutor.initWith(testSets).runTests();

0 commit comments

Comments
 (0)