From e520755a3655696041092494fa2c6f42a16fb099 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Sep 2023 15:40:19 -0700 Subject: [PATCH 1/6] Implement rolling threshold node --- .../constraints/tree/RollingThreshold.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java new file mode 100644 index 0000000000..dd87190fee --- /dev/null +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java @@ -0,0 +1,84 @@ +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.merlin.protocol.types.Duration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public record RollingThreshold(Expression expression, Expression width, Expression threshold, RollingThresholdAlgorithm algorithm) implements Expression { + + public enum RollingThresholdAlgorithm { + InputSpans, + Hull + } + + @Override + public ConstraintResult evaluate(SimulationResults results, final Interval bounds, EvaluationEnvironment environment) { + final var width = this.width.evaluate(results, bounds, environment); + var spans = this.expression.evaluate(results, bounds, environment); + 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 var violations = new ArrayList(); + 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(); + final var violationActivityIds = new ArrayList(); + for (final var span: spans) { + 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) { + 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.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 names) { + this.expression.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.expression.prettyPrint(prefix + " "), + this.width.prettyPrint(prefix + " "), + this.threshold.prettyPrint(prefix + " "), + this.algorithm + ); + } +} From f772bda860df52fcb85979cac3c695e6e6a2fccb Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Sep 2023 15:40:25 -0700 Subject: [PATCH 2/6] Add ast test --- .../jpl/aerie/constraints/tree/ASTTests.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java index 8a0fdc264c..37193bd08c 100644 --- a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java +++ b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java @@ -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; @@ -1126,6 +1127,43 @@ public void testShiftWindowsEdgesBoundsAdjustment() { assertIterableEquals(expected2, result2); } + @Test + public void testRollingThreshold() { + 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.Hull + ).evaluate(simResults); + + final var expected = 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); + } + /** * An expression that yields the same aliased object every time it is evaluated. */ From c5ab66f40ad8aff52dc925a7efde5d3b8eec711f Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Tue, 5 Sep 2023 14:51:55 -0700 Subject: [PATCH 3/6] Implement RollingThreshold edsl function --- .../constraints/json/ConstraintParsers.java | 19 ++++- .../constraints/tree/RollingThreshold.java | 8 +- .../src/libs/constraints-ast.ts | 13 +++- .../src/libs/constraints-edsl-fluent-api.ts | 77 +++++++++++++++++-- ...ConstraintsDSLCompilationServiceTests.java | 27 +++++++ 5 files changed, 129 insertions(+), 15 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java index fad9655a9b..10017ac713 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java @@ -617,9 +617,26 @@ private static JsonParser> spansExpressionF(JsonParser new ViolationsOfWindows(expression)), $ -> tuple(Unit.UNIT, $.expression)); + public static final JsonParser rollingThresholdAlgorithmP = + enumP(RollingThreshold.RollingThresholdAlgorithm.class, Enum::name); + + static final JsonParser 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> constraintP = recursiveP(selfP -> chooseP( forEachActivityViolationsF(selfP), windowsExpressionP.map(ViolationsOfWindows::new, $ -> $.expression), - violationsOfP)); + violationsOfP, + rollingThresholdP)); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java index dd87190fee..dae1a69eb0 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java @@ -15,7 +15,7 @@ import java.util.List; import java.util.Set; -public record RollingThreshold(Expression expression, Expression width, Expression threshold, RollingThresholdAlgorithm algorithm) implements Expression { +public record RollingThreshold(Expression spans, Expression width, Expression threshold, RollingThresholdAlgorithm algorithm) implements Expression { public enum RollingThresholdAlgorithm { InputSpans, @@ -25,7 +25,7 @@ public enum RollingThresholdAlgorithm { @Override public ConstraintResult evaluate(SimulationResults results, final Interval bounds, EvaluationEnvironment environment) { final var width = this.width.evaluate(results, bounds, environment); - var spans = this.expression.evaluate(results, bounds, environment); + var spans = this.spans.evaluate(results, bounds, environment); final var threshold = this.threshold.evaluate(results, bounds, environment); final var accDuration = spans.accumulatedDuration(threshold); @@ -65,7 +65,7 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound @Override public void extractResources(final Set names) { - this.expression.extractResources(names); + this.spans.extractResources(names); this.width.extractResources(names); this.threshold.extractResources(names); } @@ -75,7 +75,7 @@ public String prettyPrint(final String prefix) { return String.format( "\n%s(rolling-threshold on %s, width %s, threshold %s, algorithm %s)", prefix, - this.expression.prettyPrint(prefix + " "), + this.spans.prettyPrint(prefix + " "), this.width.prettyPrint(prefix + " "), this.threshold.prettyPrint(prefix + " "), this.algorithm diff --git a/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts b/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts index 369aeda9c6..b00da24a60 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts @@ -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; @@ -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

{ kind: NodeKind.AssignGapsExpression, originalProfile: P, diff --git a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts index fbf6348777..50105ef61d 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts @@ -69,6 +69,40 @@ export class Constraint { expression: expression(new ActivityInstance(activityType, alias)).__astNode, }); } + + /** + * Detect when a spans object's cumulative duration exceeds 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. + * + * @param spans spans object to detect threshold events on + * @param width width of the rolling interval + * @param threshold maximum allowable duration within any `width` interval + * @param algorithm algorithm for reporting violations + * @constructor + */ + public static RollingThreshold( + spans: Spans, + width: AST.Duration, + threshold: AST.Duration, + algorithm: RollingThresholdAlgorithm + ): Constraint { + return new Constraint({ + kind: AST.NodeKind.RollingThreshold, + spans: spans.__astNode, + width, + threshold, + algorithm + }); + } +} + +/** Algorithm to use when reporting violations from rolling threshold */ +export enum RollingThresholdAlgorithm { + Spans = 'Spans', + Hull = 'Hull' } /** A boolean profile; a function from time to truth values. */ @@ -105,7 +139,7 @@ export class Windows { public static During(...activityTypes: Gen.ActivityType[]) : Windows { return Windows.Or( ...activityTypes.map((activityType) => - Spans.ForEachActivity(activityType, (activity) => activity.span()).windows()) + Spans.ForEachActivity(activityType).windows()) ); } @@ -464,13 +498,14 @@ export class Spans { * Check a constraint for each instance of an activity type. * * @param activityType activity type to check - * @param expression function of an activity instance that returns a Constraint + * @param expression function of an activity instance that returns a Constraint; default returns the instance's span. * @constructor */ public static ForEachActivity( activityType: A, - expression: (instance: ActivityInstance) => Spans, + expression?: (instance: ActivityInstance) => Spans, ): Spans { + if (expression === undefined) expression = instance => instance.span(); let alias = 'span activity alias ' + Spans.__numGeneratedAliases; Spans.__numGeneratedAliases += 1; return new Spans({ @@ -1039,8 +1074,8 @@ declare global { * @constructor */ public static ForbiddenActivityOverlap( - activityType1: Gen.ActivityType, - activityType2: Gen.ActivityType, + activityType1: Gen.ActivityType, + activityType2: Gen.ActivityType, ): Constraint; /** @@ -1054,6 +1089,32 @@ declare global { activityType: A, expression: (instance: ActivityInstance) => Constraint, ): Constraint; + + /** + * Detect when a spans object's cumulative duration exceeds 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. + * + * @param spans spans object to detect threshold events on + * @param width width of the rolling interval + * @param threshold maximum allowable duration within any `width` interval + * @param algorithm algorithm for reporting violations + * @constructor + */ + public static RollingThreshold( + spans: Spans, + width: AST.Duration, + threshold: AST.Duration, + algorithm: RollingThresholdAlgorithm + ): Constraint; + } + + /** Algorithm to use when reporting violations from rolling threshold */ + export enum RollingThresholdAlgorithm { + Spans = 'Spans', + Hull = 'Hull' } /** A boolean profile; a function from time to truth values. */ @@ -1261,12 +1322,12 @@ declare global { * Applies an expression producing spans for each instance of an activity type and returns the aggregated set of spans. * * @param activityType activity type to check - * @param expression function of an activity instance that returns a Spans + * @param expression function of an activity instance that returns a Spans; default returns the instance's span. * @constructor */ public static ForEachActivity( activityType: A, - expression: (instance: ActivityInstance) => Spans, + expression?: (instance: ActivityInstance) => Spans, ): Spans; /** @@ -1523,4 +1584,4 @@ declare global { } // Make Constraint available on the global object -Object.assign(globalThis, { Constraint, Windows, Spans, Real, Discrete, Inclusivity, Interval }); +Object.assign(globalThis, { Constraint, Windows, Spans, Real, Discrete, Inclusivity, Interval, RollingThresholdAlgorithm }); diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java index 709a400273..2c7765d3f5 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java @@ -31,6 +31,7 @@ import gov.nasa.jpl.aerie.constraints.tree.RealParameter; import gov.nasa.jpl.aerie.constraints.tree.RealResource; import gov.nasa.jpl.aerie.constraints.tree.RealValue; +import gov.nasa.jpl.aerie.constraints.tree.RollingThreshold; import gov.nasa.jpl.aerie.constraints.tree.ShiftBy; import gov.nasa.jpl.aerie.constraints.tree.ShiftWindowsEdges; import gov.nasa.jpl.aerie.constraints.tree.ShorterThan; @@ -1275,4 +1276,30 @@ 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 + ) + ); + } + } From c62ee901b4e4a1bf24ca3b6e337b59a8a65be9f8 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Tue, 5 Sep 2023 16:05:41 -0700 Subject: [PATCH 4/6] Add deficit option for RollingThreshold --- .../constraints/tree/RollingThreshold.java | 58 +++++++++--- .../jpl/aerie/constraints/tree/ASTTests.java | 94 ++++++++++++++++++- .../src/libs/constraints-edsl-fluent-api.ts | 42 ++++++--- ...ConstraintsDSLCompilationServiceTests.java | 57 ++++++----- 4 files changed, 201 insertions(+), 50 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java index dae1a69eb0..f48ba31b7d 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java @@ -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; @@ -18,14 +19,24 @@ public record RollingThreshold(Expression spans, Expression width, Expression threshold, RollingThresholdAlgorithm algorithm) implements Expression { 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); @@ -33,25 +44,51 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound 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(); - 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(); final var violationActivityIds = new ArrayList(); - 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); @@ -59,7 +96,6 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound final var violation = new Violation(violationIntervals, violationActivityIds); violations.add(violation); } - return new ConstraintResult(violations, List.of()); } diff --git a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java index 37193bd08c..e5a49edba2 100644 --- a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java +++ b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java @@ -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(), @@ -1150,10 +1150,10 @@ 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()) @@ -1161,7 +1161,93 @@ public void testRollingThreshold() { 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); } /** diff --git a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts index 50105ef61d..d26800e8ac 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts @@ -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 @@ -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. */ @@ -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 @@ -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 { diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java index 2c7765d3f5..345e7a6215 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java @@ -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; @@ -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() + ) + ); + } } } From ff77b25e0a9500453c3067211dd5658a8765a849 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 13 Sep 2023 13:34:27 -0700 Subject: [PATCH 5/6] Minor fixes --- .../jpl/aerie/constraints/tree/RollingThreshold.java | 10 +++++++--- .../src/libs/constraints-edsl-fluent-api.ts | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java index f48ba31b7d..4dfe965cce 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java @@ -28,13 +28,15 @@ public enum RollingThresholdAlgorithm { @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 var spans = this.spans.evaluate(results, bounds, environment); final Spans reportedSpans; if (algorithm == RollingThresholdAlgorithm.ExcessHull || algorithm == RollingThresholdAlgorithm.ExcessSpans) { reportedSpans = spans; - } else { + } 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); @@ -58,7 +60,7 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound if (algorithm == RollingThresholdAlgorithm.ExcessHull || algorithm == RollingThresholdAlgorithm.ExcessSpans) { leftViolatingBounds = localAccDuration.greaterThan(thresholdEq); - } else { + } else if (algorithm == RollingThresholdAlgorithm.DeficitHull || algorithm == RollingThresholdAlgorithm.DeficitSpans) { leftViolatingBounds = localAccDuration.lessThan(thresholdEq).select( Interval.between( bounds.start, @@ -67,6 +69,8 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound bounds.endInclusivity ) ); + } else { + throw new IllegalArgumentException("Algorithm not supported: " + algorithm); } for (final var leftViolatingBound : leftViolatingBounds.iterateEqualTo(true)) { diff --git a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts index d26800e8ac..4c996b643f 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-edsl-fluent-api.ts @@ -80,7 +80,7 @@ export class Constraint { * 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 + * - `DeficitHull` 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 @@ -1108,7 +1108,7 @@ declare global { * 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 + * - `DeficitHull` 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 From 130e1323e52415d20b0ad5a5b14787f57d7189d7 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 13 Sep 2023 15:24:54 -0700 Subject: [PATCH 6/6] Fix hull operation bug --- .../jpl/aerie/constraints/tree/RollingThreshold.java | 10 ++++------ .../gov/nasa/jpl/aerie/constraints/tree/ASTTests.java | 7 ++++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java index 4dfe965cce..2c01d07d68 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/RollingThreshold.java @@ -88,12 +88,10 @@ public ConstraintResult evaluate(SimulationResults results, final Interval bound } } 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 - ); + var hull = violationIntervals.get(0); + for (final var interval: violationIntervals.subList(1, violationIntervals.size())) { + hull = Interval.unify(hull, interval); + } violationIntervals.clear(); violationIntervals.add(hull); } diff --git a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java index e5a49edba2..c6ac01d3cf 100644 --- a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java +++ b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/tree/ASTTests.java @@ -1137,9 +1137,10 @@ public void testRollingThresholdExcess() { ); 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(4, 5, SECONDS), Interval.between(14, 15, SECONDS), Interval.between(16, 17, SECONDS), @@ -1174,9 +1175,9 @@ public void testRollingThresholdExcess() { List.of( new Violation( List.of( + Interval.between(4, 5, SECONDS), Interval.between(0, 1, SECONDS), - Interval.between(2, 3, SECONDS), - Interval.between(4, 5, SECONDS) + Interval.between(2, 3, SECONDS) ), List.of() ), new Violation(