Skip to content

Commit

Permalink
Merge pull request #1128 from NASA-AMMOS/1015-implement-dedicated-rol…
Browse files Browse the repository at this point in the history
…lingwindow-constraint-node

Implement dedicated rollingwindow constraint node
  • Loading branch information
JoelCourtney authored Sep 18, 2023
2 parents 6a77a90 + 130e132 commit 0c7891d
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -617,9 +617,26 @@ private static JsonParser<Expression<Spans>> spansExpressionF(JsonParser<Express
untuple((kind, expression) -> new ViolationsOfWindows(expression)),
$ -> tuple(Unit.UNIT, $.expression));

public static final JsonParser<RollingThreshold.RollingThresholdAlgorithm> rollingThresholdAlgorithmP =
enumP(RollingThreshold.RollingThresholdAlgorithm.class, Enum::name);

static final JsonParser<RollingThreshold> rollingThresholdP =
productP
.field("kind", literalP("RollingThreshold"))
.field("spans", spansExpressionP)
.field("width", durationExprP)
.field("threshold", durationExprP)
.field("algorithm", rollingThresholdAlgorithmP)
.map(
untuple((kind, spans, width, threshold, alg) -> new RollingThreshold(spans, width, threshold, alg)),
$ -> tuple(Unit.UNIT, $.spans(), $.width(), $.threshold(), $.algorithm())
);


public static final JsonParser<Expression<ConstraintResult>> constraintP =
recursiveP(selfP -> chooseP(
forEachActivityViolationsF(selfP),
windowsExpressionP.map(ViolationsOfWindows::new, $ -> $.expression),
violationsOfP));
violationsOfP,
rollingThresholdP));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package gov.nasa.jpl.aerie.constraints.tree;

import gov.nasa.jpl.aerie.constraints.model.ConstraintResult;
import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment;
import gov.nasa.jpl.aerie.constraints.model.LinearEquation;
import gov.nasa.jpl.aerie.constraints.model.LinearProfile;
import gov.nasa.jpl.aerie.constraints.model.SimulationResults;
import gov.nasa.jpl.aerie.constraints.model.Violation;
import gov.nasa.jpl.aerie.constraints.time.Interval;
import gov.nasa.jpl.aerie.constraints.time.Segment;
import gov.nasa.jpl.aerie.constraints.time.Spans;
import gov.nasa.jpl.aerie.constraints.time.Windows;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public record RollingThreshold(Expression<Spans> spans, Expression<Duration> width, Expression<Duration> threshold, RollingThresholdAlgorithm algorithm) implements Expression<ConstraintResult> {

public enum RollingThresholdAlgorithm {
ExcessSpans,
ExcessHull,
DeficitSpans,
DeficitHull
}

@Override
public ConstraintResult evaluate(SimulationResults results, final Interval bounds, EvaluationEnvironment environment) {
final var width = this.width.evaluate(results, bounds, environment);
final var spans = this.spans.evaluate(results, bounds, environment);

final Spans reportedSpans;
if (algorithm == RollingThresholdAlgorithm.ExcessHull || algorithm == RollingThresholdAlgorithm.ExcessSpans) {
reportedSpans = spans;
} else if (algorithm == RollingThresholdAlgorithm.DeficitHull || algorithm == RollingThresholdAlgorithm.DeficitSpans) {
reportedSpans = spans.intoWindows().not().intoSpans(bounds);
} else {
throw new IllegalArgumentException("Algorithm not supported: " + algorithm);
}

final var threshold = this.threshold.evaluate(results, bounds, environment);

final var accDuration = spans.accumulatedDuration(threshold);
final var shiftedBack = accDuration.shiftBy(Duration.negate(width));

final var localAccDuration = shiftedBack.plus(accDuration.times(-1));

final Windows leftViolatingBounds;
final var violations = new ArrayList<Violation>();

final var thresholdEq = new LinearProfile(Segment.of(
Interval.FOREVER,
new LinearEquation(
Duration.ZERO,
1,
0
)
));

if (algorithm == RollingThresholdAlgorithm.ExcessHull || algorithm == RollingThresholdAlgorithm.ExcessSpans) {
leftViolatingBounds = localAccDuration.greaterThan(thresholdEq);
} else if (algorithm == RollingThresholdAlgorithm.DeficitHull || algorithm == RollingThresholdAlgorithm.DeficitSpans) {
leftViolatingBounds = localAccDuration.lessThan(thresholdEq).select(
Interval.between(
bounds.start,
bounds.startInclusivity,
bounds.end.minus(width),
bounds.endInclusivity
)
);
} else {
throw new IllegalArgumentException("Algorithm not supported: " + algorithm);
}

for (final var leftViolatingBound : leftViolatingBounds.iterateEqualTo(true)) {
final var expandedInterval = Interval.between(
leftViolatingBound.start,
leftViolatingBound.startInclusivity,
leftViolatingBound.end.plus(width),
leftViolatingBound.endInclusivity);
final var violationIntervals = new ArrayList<Interval>();
final var violationActivityIds = new ArrayList<Long>();
for (final var span : reportedSpans) {
if (!Interval.intersect(span.interval(), expandedInterval).isEmpty()) {
violationIntervals.add(span.interval());
span.value().ifPresent(m -> violationActivityIds.add(m.activityInstance().id));
}
}
if (this.algorithm == RollingThresholdAlgorithm.ExcessHull || this.algorithm == RollingThresholdAlgorithm.DeficitHull) {
var hull = violationIntervals.get(0);
for (final var interval: violationIntervals.subList(1, violationIntervals.size())) {
hull = Interval.unify(hull, interval);
}
violationIntervals.clear();
violationIntervals.add(hull);
}
final var violation = new Violation(violationIntervals, violationActivityIds);
violations.add(violation);
}
return new ConstraintResult(violations, List.of());
}

@Override
public void extractResources(final Set<String> names) {
this.spans.extractResources(names);
this.width.extractResources(names);
this.threshold.extractResources(names);
}

@Override
public String prettyPrint(final String prefix) {
return String.format(
"\n%s(rolling-threshold on %s, width %s, threshold %s, algorithm %s)",
prefix,
this.spans.prettyPrint(prefix + " "),
this.width.prettyPrint(prefix + " "),
this.threshold.prettyPrint(prefix + " "),
this.algorithm
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import static gov.nasa.jpl.aerie.constraints.time.Interval.at;
import static gov.nasa.jpl.aerie.constraints.time.Interval.interval;
import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS;
import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MILLISECOND;
import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
Expand Down Expand Up @@ -1126,6 +1127,130 @@ public void testShiftWindowsEdgesBoundsAdjustment() {
assertIterableEquals(expected2, result2);
}

@Test
public void testRollingThresholdExcess() {
final var simResults = new SimulationResults(
Instant.EPOCH, Interval.between(0, 20, SECONDS),
List.of(),
Map.of(),
Map.of()
);

final var spans = new Spans(
// These two are out of order to make sure RollingThreshold's hull operation correctly handles unsorted spans.
Interval.between(4, 5, SECONDS),
Interval.between(0, 1, SECONDS),
Interval.between(2, 3, SECONDS),

Interval.between(14, 15, SECONDS),
Interval.between(16, 17, SECONDS),
Interval.between(18, 19, SECONDS)
);

final var result1 = new RollingThreshold(
Supplier.of(spans),
Supplier.of(Duration.of(10, SECONDS)),
Supplier.of(Duration.of(2500, MILLISECOND)),
RollingThreshold.RollingThresholdAlgorithm.ExcessHull
).evaluate(simResults);

final var expected1 = new ConstraintResult(
List.of(
new Violation(List.of(Interval.between(0, 5, SECONDS)), List.of()),
new Violation(List.of(Interval.between(14, 19, SECONDS)), List.of())
),
List.of()
);

assertEquals(expected1, result1);

final var result2 = new RollingThreshold(
Supplier.of(spans),
Supplier.of(Duration.of(10, SECONDS)),
Supplier.of(Duration.of(2500, MILLISECOND)),
RollingThreshold.RollingThresholdAlgorithm.ExcessSpans
).evaluate(simResults);

final var expected2 = new ConstraintResult(
List.of(
new Violation(
List.of(
Interval.between(4, 5, SECONDS),
Interval.between(0, 1, SECONDS),
Interval.between(2, 3, SECONDS)
), List.of()
),
new Violation(
List.of(
Interval.between(14, 15, SECONDS),
Interval.between(16, 17, SECONDS),
Interval.between(18, 19, SECONDS)
), List.of()
)
), List.of()
);

assertEquals(expected2, result2);
}

@Test
public void tesRollingThresholdDeficit() {
final var simResults = new SimulationResults(
Instant.EPOCH, Interval.between(0, 20, SECONDS),
List.of(),
Map.of(),
Map.of()
);

final var spans = new Spans(
Interval.between(0, 1, SECONDS),
Interval.between(2, 3, SECONDS),
Interval.between(4, 5, SECONDS),

Interval.between(14, 15, SECONDS),
Interval.between(16, 17, SECONDS),
Interval.between(18, 19, SECONDS)
);

final var result1 = new RollingThreshold(
Supplier.of(spans),
Supplier.of(Duration.of(10, SECONDS)),
Supplier.of(Duration.of(2500, MILLISECOND)),
RollingThreshold.RollingThresholdAlgorithm.DeficitHull
).evaluate(simResults);

final var expected1 = new ConstraintResult(
List.of(
new Violation(List.of(Interval.between(1, Exclusive, 18, Exclusive, SECONDS)), List.of())
),
List.of()
);

assertEquals(expected1, result1);

final var result2 = new RollingThreshold(
Supplier.of(spans),
Supplier.of(Duration.of(10, SECONDS)),
Supplier.of(Duration.of(2500, MILLISECOND)),
RollingThreshold.RollingThresholdAlgorithm.DeficitSpans
).evaluate(simResults);

final var expected2 = new ConstraintResult(
List.of(
new Violation(List.of(
Interval.between(1, Exclusive, 2, Exclusive, SECONDS),
Interval.between(3, Exclusive, 4, Exclusive, SECONDS),
Interval.between(5, Exclusive, 14, Exclusive, SECONDS),
Interval.between(15, Exclusive, 16, Exclusive, SECONDS),
Interval.between(17, Exclusive, 18, Exclusive, SECONDS)
), List.of())
),
List.of()
);

assertEquals(expected2, result2);
}

/**
* An expression that yields the same aliased object every time it is evaluated.
*/
Expand Down
13 changes: 11 additions & 2 deletions merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ export enum NodeKind {
ViolationsOf = 'ViolationsOf',
AbsoluteInterval = 'AbsoluteInterval',
IntervalAlias = 'IntervalAlias',
IntervalDuration = 'IntervalDuration'
IntervalDuration = 'IntervalDuration',
RollingThreshold = 'RollingThreshold'
}

export type Constraint = ViolationsOf | WindowsExpression | SpansExpression | ForEachActivityConstraints;
export type Constraint = ViolationsOf | WindowsExpression | SpansExpression | ForEachActivityConstraints | RollingThreshold;

export interface ViolationsOf {
kind: NodeKind.ViolationsOf;
Expand All @@ -71,6 +72,14 @@ export interface ForEachActivitySpans {
expression: SpansExpression;
}

export interface RollingThreshold {
kind: NodeKind.RollingThreshold;
spans: SpansExpression,
width: Duration,
threshold: Duration,
algorithm: API.RollingThresholdAlgorithm
}

export interface AssignGapsExpression<P extends ProfileExpression> {
kind: NodeKind.AssignGapsExpression,
originalProfile: P,
Expand Down
Loading

0 comments on commit 0c7891d

Please sign in to comment.