From 2db49eb0d646367121316b27e0777758a8d49b92 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Tue, 5 Sep 2023 14:51:55 -0700 Subject: [PATCH] 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 + ) + ); + } + }