Skip to content

Commit feafbd8

Browse files
committed
feat: Interval evaluator for expressions
1 parent 4ff3f7f commit feafbd8

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

lib/src/evaluator.dart

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

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 evaluateInterval() {
65+
var cases = {IntervalLiteral(Number(1.0), Number(2.0)): Interval(1.0, 2.0)};
66+
parameterized(cases);
67+
}
68+
69+
void evaluateVariable() {
70+
var cases = <Expression, Interval>{
71+
Variable('x'): Interval(12, 12),
72+
Variable('y'): Interval(24, 24),
73+
Variable('∞'): Interval(double.infinity, double.infinity),
74+
};
75+
76+
var evaluator = IntervalEvaluator(ContextModel()
77+
..bindVariableName('x', Number(12))
78+
..bindVariableName('y', two * Variable('x'))
79+
..bindVariableName('∞', Number(double.infinity)));
80+
81+
parameterized(cases, evaluator: evaluator);
82+
}
83+
84+
void evaluateBoundVariable() {
85+
var cases = <Expression, Interval>{
86+
BoundVariable(IntervalLiteral(Number(9), Number(9))): Interval(9.0, 9.0)
87+
};
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)