Skip to content

Commit

Permalink
Add deficit option for RollingThreshold
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelCourtney committed Sep 5, 2023
1 parent 2db49eb commit d6e68bd
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand All @@ -18,48 +19,83 @@
public record RollingThreshold(Expression<Spans> spans, Expression<Duration> width, Expression<Duration> threshold, RollingThresholdAlgorithm algorithm) implements Expression<ConstraintResult> {

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

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

final Spans reportedSpans;
if (algorithm == RollingThresholdAlgorithm.ExcessHull || algorithm == RollingThresholdAlgorithm.ExcessSpans) {
reportedSpans = spans;
} else {
reportedSpans = spans.intoWindows().not().intoSpans(bounds);
}

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 var leftViolatingBounds = localAccDuration.greaterThan(new LinearProfile(Segment.of(Interval.FOREVER, new LinearEquation(Duration.ZERO, 1, 0))));

final Windows leftViolatingBounds;
final var violations = new ArrayList<Violation>();
for (final var leftViolatingBound: leftViolatingBounds.iterateEqualTo(true)) {
final var expandedInterval = Interval.between(leftViolatingBound.start, leftViolatingBound.startInclusivity, leftViolatingBound.end.plus(width), leftViolatingBound.endInclusivity);

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 {
leftViolatingBounds = localAccDuration.lessThan(thresholdEq).select(
Interval.between(
bounds.start,
bounds.startInclusivity,
bounds.end.minus(width),
bounds.endInclusivity
)
);
}

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: spans) {
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.Hull) {
if (this.algorithm == RollingThresholdAlgorithm.ExcessHull || this.algorithm == RollingThresholdAlgorithm.DeficitHull) {
final var hull = Interval.between(
violationIntervals.get(0).start,
violationIntervals.get(0).startInclusivity,
violationIntervals.get(violationIntervals.size()-1).end,
violationIntervals.get(violationIntervals.size()-1).endInclusivity
violationIntervals.get(violationIntervals.size() - 1).end,
violationIntervals.get(violationIntervals.size() - 1).endInclusivity
);
violationIntervals.clear();
violationIntervals.add(hull);
}
final var violation = new Violation(violationIntervals, violationActivityIds);
violations.add(violation);
}

return new ConstraintResult(violations, List.of());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,7 @@ public void testShiftWindowsEdgesBoundsAdjustment() {
}

@Test
public void testRollingThreshold() {
public void testRollingThresholdExcess() {
final var simResults = new SimulationResults(
Instant.EPOCH, Interval.between(0, 20, SECONDS),
List.of(),
Expand All @@ -1150,18 +1150,104 @@ public void testRollingThreshold() {
Supplier.of(spans),
Supplier.of(Duration.of(10, SECONDS)),
Supplier.of(Duration.of(2500, MILLISECOND)),
RollingThreshold.RollingThresholdAlgorithm.Hull
RollingThreshold.RollingThresholdAlgorithm.ExcessHull
).evaluate(simResults);

final var expected = new ConstraintResult(
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(expected, result1);
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(0, 1, SECONDS),
Interval.between(2, 3, SECONDS),
Interval.between(4, 5, 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,17 @@ export class Constraint {
}

/**
* Detect when a spans object's cumulative duration exceeds a threshold within any interval of a given width.
* Detect when a spans object's cumulative duration either exceeds or falls short of a threshold within any interval of a given width.
*
* Violations can be reported in two different ways by setting the `algorithm` argument:
* - `RollingThresholdAlgorithm.Spans` highlights the individual spans that contributed to the threshold violation.
* - `RollingThresholdAlgorithm.Hull` highlights the single interval that contains all the violating spans.
* Violations can be reported in various ways by setting the `algorithm` argument:
* - `ExcessSpans` detects times when the duration exceeds the threshold and highlights the individual spans that
* contributed to the threshold violation.
* - `ExcessHull` detects times when the duration exceeds the threshold and highlights the whole group of spans that
* contributed to the threshold violation in one interval.
* - `DeficitSpans` detects times when the duration falls short of the threshold and highlights the individual gaps between spans
* that contributed to the threshold violation.
* - `ExcessHull` detects times when the duration falls short of the threshold and highlights the whole group of gaps between
* spans that contributed to the threshold violation in one interval.
*
* @param spans spans object to detect threshold events on
* @param width width of the rolling interval
Expand All @@ -101,8 +107,10 @@ export class Constraint {

/** Algorithm to use when reporting violations from rolling threshold */
export enum RollingThresholdAlgorithm {
Spans = 'Spans',
Hull = 'Hull'
ExcessSpans = 'ExcessSpans',
ExcessHull = 'ExcessHull',
DeficitSpans = 'DeficitSpans',
DeficitHull = 'DeficitHull'
}

/** A boolean profile; a function from time to truth values. */
Expand Down Expand Up @@ -1091,11 +1099,17 @@ declare global {
): Constraint;

/**
* Detect when a spans object's cumulative duration exceeds a threshold within any interval of a given width.
* Detect when a spans object's cumulative duration either exceeds or falls short of a threshold within any interval of a given width.
*
* Violations can be reported in two different ways by setting the `algorithm` argument:
* - `RollingThresholdAlgorithm.Spans` highlights the individual spans that contributed to the threshold violation.
* - `RollingThresholdAlgorithm.Hull` highlights the single interval that contains all the violating spans.
* Violations can be reported in various ways by setting the `algorithm` argument:
* - `ExcessSpans` detects times when the duration exceeds the threshold and highlights the individual spans that
* contributed to the threshold violation.
* - `ExcessHull` detects times when the duration exceeds the threshold and highlights the whole group of spans that
* contributed to the threshold violation in one interval.
* - `DeficitSpans` detects times when the duration falls short of the threshold and highlights the individual gaps between spans
* that contributed to the threshold violation.
* - `ExcessHull` detects times when the duration falls short of the threshold and highlights the whole group of gaps between
* spans that contributed to the threshold violation in one interval.
*
* @param spans spans object to detect threshold events on
* @param width width of the rolling interval
Expand All @@ -1113,9 +1127,11 @@ declare global {

/** Algorithm to use when reporting violations from rolling threshold */
export enum RollingThresholdAlgorithm {
Spans = 'Spans',
Hull = 'Hull'
}
ExcessSpans = 'ExcessSpans',
ExcessHull = 'ExcessHull',
DeficitSpans = 'DeficitSpans',
DeficitHull = 'DeficitHull'
}

/** A boolean profile; a function from time to truth values. */
export class Windows {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,13 @@

import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;

import static gov.nasa.jpl.aerie.constraints.tree.RollingThreshold.RollingThresholdAlgorithm.DeficitHull;
import static gov.nasa.jpl.aerie.constraints.tree.RollingThreshold.RollingThresholdAlgorithm.DeficitSpans;
import static gov.nasa.jpl.aerie.constraints.tree.RollingThreshold.RollingThresholdAlgorithm.ExcessHull;
import static gov.nasa.jpl.aerie.constraints.tree.RollingThreshold.RollingThresholdAlgorithm.ExcessSpans;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -1278,28 +1283,36 @@ export default() => {

@Test
void testRollingThreshold() {
checkSuccessfulCompilation(
"""
export default () => {
return Constraint.RollingThreshold(
Spans.ForEachActivity(ActivityType.activity),
Temporal.Duration.from({hours: 1}),
Temporal.Duration.from({minutes: 5}),
RollingThresholdAlgorithm.Hull
);
}
""",
new RollingThreshold(
new ForEachActivitySpans(
"activity",
"span activity alias 0",
new ActivitySpan("span activity alias 0")
),
new DurationLiteral(Duration.of(1, Duration.HOUR)),
new DurationLiteral(Duration.of(5, Duration.MINUTE)),
RollingThreshold.RollingThresholdAlgorithm.Hull
)
);
final var algs = Map.of(
"ExcessHull", ExcessHull,
"ExcessSpans", ExcessSpans,
"DeficitHull", DeficitHull,
"DeficitSpans", DeficitSpans
);
for (final var entry: algs.entrySet()) {
checkSuccessfulCompilation(
"""
export default () => {
return Constraint.RollingThreshold(
Spans.ForEachActivity(ActivityType.activity),
Temporal.Duration.from({hours: 1}),
Temporal.Duration.from({minutes: 5}),
RollingThresholdAlgorithm.%s
);
}
""".formatted(entry.getKey()),
new RollingThreshold(
new ForEachActivitySpans(
"activity",
"span activity alias 0",
new ActivitySpan("span activity alias 0")
),
new DurationLiteral(Duration.of(1, Duration.HOUR)),
new DurationLiteral(Duration.of(5, Duration.MINUTE)),
entry.getValue()
)
);
}
}

}

0 comments on commit d6e68bd

Please sign in to comment.