From c6986111c408121c4b62473eca99ece836f2c4b7 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 13 Jul 2023 17:09:23 -0700 Subject: [PATCH 1/3] Implement ShiftEdges for Spans --- .../constraints/json/ConstraintParsers.java | 13 ++++---- .../constraints/time/IntervalContainer.java | 2 ++ .../jpl/aerie/constraints/time/Spans.java | 10 +++++++ .../jpl/aerie/constraints/time/Windows.java | 2 ++ ...ShiftWindowsEdges.java => ShiftEdges.java} | 30 +++++++++---------- .../jpl/aerie/constraints/tree/ASTTests.java | 12 ++++---- .../src/libs/constraints-ast.ts | 11 +++---- .../src/libs/constraints-edsl-fluent-api.ts | 27 +++++++++++++++-- ...ConstraintsDSLCompilationServiceTests.java | 4 +-- 9 files changed, 75 insertions(+), 36 deletions(-) rename constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/{ShiftWindowsEdges.java => ShiftEdges.java} (59%) 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 10017ac713..9397cec074 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 @@ -335,15 +335,15 @@ static JsonParser transitionP(JsonParser> profi $ -> tuple(Unit.UNIT, $.value(), $.interval()) ); - static JsonParser shiftWindowsEdgesF(JsonParser> windowsExpressionP) { + static > JsonParser> shiftEdgesF(final JsonParser> intervalExpressionP) { return productP - .field("kind", literalP("WindowsExpressionShiftBy")) - .field("windowExpression", windowsExpressionP) + .field("kind", literalP("IntervalsExpressionShiftEdges")) + .field("expression", intervalExpressionP) .field("fromStart", durationExprP) .field("fromEnd", durationExprP) .map( - untuple((kind, windowsExpression, fromStart, fromEnd) -> new ShiftWindowsEdges(windowsExpression, fromStart, fromEnd)), - $ -> tuple(Unit.UNIT, $.windows, $.fromStart, $.fromEnd)); + untuple((kind, expr, fromStart, fromEnd) -> new ShiftEdges(expr, fromStart, fromEnd)), + $ -> tuple(Unit.UNIT, $.expression, $.fromStart, $.fromEnd)); } static final JsonParser endOfP = productP @@ -562,7 +562,7 @@ private static JsonParser> windowsExpressionF(JsonParser> spansExpressionF(JsonParser> { LinearProfile accumulatedDuration(final Duration unit); T starts(); T ends(); + T shiftEdges(final Duration fromStart, final Duration fromEnd); + T select(final Interval... intervals); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java index 1d3f89c92d..90596d66b9 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java @@ -215,6 +215,16 @@ public Spans ends() { return this.map($ -> Interval.at($.end)); } + @Override + public Spans shiftEdges(final Duration fromStart, final Duration fromEnd) { + return this.map($ -> Interval.between($.start.plus(fromStart), $.startInclusivity, $.end.plus(fromEnd), $.endInclusivity)); + } + + @Override + public Spans select(final Interval... intervals) { + return this.flatMap($ -> Arrays.stream(intervals).map(selection -> Interval.intersect($, selection))); + } + @Override public boolean equals(final Object obj) { if (!(obj instanceof final Spans spans)) return false; diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java index 651faafd54..a083641eab 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java @@ -342,6 +342,7 @@ public Windows filterByDuration(Duration minDur, Duration maxDur) { * @param fromEnd duration to shift true -> false falling edges * @return a new Windows */ + @Override public Windows shiftEdges(Duration fromStart, Duration fromEnd) { final var builder = IntervalMap.builder(); @@ -622,6 +623,7 @@ public Windows unset(final List intervals) { } /** Delegated to {@link IntervalMap#select(Interval...)} */ + @Override public Windows select(final Interval... intervals) { return new Windows(segments.select(intervals)); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftWindowsEdges.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftEdges.java similarity index 59% rename from constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftWindowsEdges.java rename to constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftEdges.java index 0db61054bc..67f1c1ce99 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftWindowsEdges.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftEdges.java @@ -3,25 +3,25 @@ import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.constraints.time.IntervalContainer; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import java.util.Objects; import java.util.Set; -public final class ShiftWindowsEdges implements Expression { - public final Expression windows; +public final class ShiftEdges> implements Expression { + public final Expression expression; public final Expression fromStart; public final Expression fromEnd; - public ShiftWindowsEdges(final Expression left, final Expression fromStart, final Expression fromEnd) { - this.windows = left; + public ShiftEdges(final Expression left, final Expression fromStart, final Expression fromEnd) { + this.expression = left; this.fromStart = fromStart; this.fromEnd = fromEnd; } @Override - public Windows evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { + public I evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final var shiftRising = this.fromStart.evaluate(results, bounds, environment); final var shiftFalling = this.fromEnd.evaluate(results, bounds, environment); @@ -32,21 +32,21 @@ public Windows evaluate(final SimulationResults results, final Interval bounds, bounds.endInclusivity ); - final var windows = this.windows.evaluate(results, newBounds, environment); - return windows.shiftEdges(shiftRising, shiftFalling).select(bounds); + final var intervals = this.expression.evaluate(results, newBounds, environment); + return intervals.shiftEdges(shiftRising, shiftFalling).select(bounds); } @Override public void extractResources(final Set names) { - this.windows.extractResources(names); + this.expression.extractResources(names); } @Override public String prettyPrint(final String prefix) { return String.format( - "\n%s(shiftWindowsEdges %s by %s %s)", + "\n%s(shiftEdges %s by %s %s)", prefix, - this.windows.prettyPrint(prefix + " "), + this.expression.prettyPrint(prefix + " "), this.fromStart.toString(), this.fromEnd.toString() ); @@ -54,16 +54,16 @@ public String prettyPrint(final String prefix) { @Override public boolean equals(Object obj) { - if (!(obj instanceof ShiftWindowsEdges)) return false; - final var o = (ShiftWindowsEdges)obj; + if (!(obj instanceof ShiftEdges)) return false; + final var o = (ShiftEdges)obj; - return Objects.equals(this.windows, o.windows) && + return Objects.equals(this.expression, o.expression) && Objects.equals(this.fromStart, o.fromStart) && Objects.equals(this.fromEnd, o.fromEnd); } @Override public int hashCode() { - return Objects.hash(this.windows, this.fromStart, this.fromEnd); + return Objects.hash(this.expression, this.fromStart, this.fromEnd); } } 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 c6ac01d3cf..f51289e90c 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 @@ -378,7 +378,7 @@ public void testExpandBy() { final var expandByFromStart = Duration.of(-1, SECONDS); final var expandByFromEnd = Duration.of(0, SECONDS); - final var result = new ShiftWindowsEdges(Supplier.of(left), Supplier.of(expandByFromStart), Supplier.of(expandByFromEnd)).evaluate(simResults, new EvaluationEnvironment()); + final var result = new ShiftEdges<>(Supplier.of(left), Supplier.of(expandByFromStart), Supplier.of(expandByFromEnd)).evaluate(simResults, new EvaluationEnvironment()); final var expected = new Windows() .set(Interval.between(-1, Inclusive, 7, Inclusive, SECONDS), true) @@ -410,7 +410,7 @@ public void testShrink() { final var clampFromStart = Duration.of(1, SECONDS); final var clampFromEnd = Duration.negate(Duration.of(1, SECONDS)); - final var result = new ShiftWindowsEdges(Supplier.of(left), Supplier.of(clampFromStart), Supplier.of(clampFromEnd)).evaluate(simResults, new EvaluationEnvironment()); + final var result = new ShiftEdges<>(Supplier.of(left), Supplier.of(clampFromStart), Supplier.of(clampFromEnd)).evaluate(simResults, new EvaluationEnvironment()); final var expected = new Windows() .set(Interval.between(1, Inclusive, 4, Exclusive, SECONDS), true) @@ -1092,7 +1092,7 @@ public void testShiftWindowsEdgesBoundsAdjustment() { final var crossingStartOfPlan = new Windows(false).set(Interval.between(-1, 1, SECONDS), true); - final var result1 = new ShiftWindowsEdges( + final var result1 = new ShiftEdges<>( Supplier.of(crossingStartOfPlan), Supplier.of(Duration.ZERO), Supplier.of(Duration.of(10, SECONDS)) @@ -1100,7 +1100,7 @@ public void testShiftWindowsEdgesBoundsAdjustment() { final var expected1 = new Windows(false).set(Interval.between(-1, 11, SECONDS), true).select(simResults.bounds); assertIterableEquals(expected1, result1); - final var result2 = new ShiftWindowsEdges( + final var result2 = new ShiftEdges<>( Supplier.of(crossingStartOfPlan), Supplier.of(Duration.of(-10, SECONDS)), Supplier.of(Duration.ZERO) @@ -1110,7 +1110,7 @@ public void testShiftWindowsEdgesBoundsAdjustment() { final var crossingEndOfPlan = new Windows(false).set(Interval.between(19, 21, SECONDS), true); - final var result3 = new ShiftWindowsEdges( + final var result3 = new ShiftEdges<>( Supplier.of(crossingEndOfPlan), Supplier.of(Duration.ZERO), Supplier.of(Duration.of(10, SECONDS)) @@ -1118,7 +1118,7 @@ public void testShiftWindowsEdgesBoundsAdjustment() { final var expected3 = new Windows(false).set(Interval.between(19, 20, SECONDS), true).select(simResults.bounds); assertIterableEquals(expected3, result3); - final var result4 = new ShiftWindowsEdges( + final var result4 = new ShiftEdges<>( Supplier.of(crossingEndOfPlan), Supplier.of(Duration.of(-10, SECONDS)), Supplier.of(Duration.ZERO) 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 b00da24a60..740a033614 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts @@ -24,7 +24,6 @@ export enum NodeKind { WindowsExpressionEndOf = 'WindowsExpressionEndOf', WindowsExpressionLongerThan = 'WindowsExpressionLongerThan', WindowsExpressionShorterThan = 'WindowsExpressionShorterThan', - WindowsExpressionShiftBy = 'WindowsExpressionShiftBy', WindowsExpressionFromSpans = 'WindowsExpressionFromSpans', SpansExpressionFromWindows = 'SpansExpressionFromWindows', SpansExpressionSplit = 'SpansExpressionSplit', @@ -40,6 +39,7 @@ export enum NodeKind { WindowsExpressionNot = 'WindowsExpressionNot', IntervalsExpressionStarts = 'IntervalsExpressionStarts', IntervalsExpressionEnds = 'IntervalsExpressionEnds', + IntervalsExpressionShiftEdges = 'IntervalsExpressionShiftEdges', ForEachActivitySpans = 'ForEachActivitySpans', ForEachActivityViolations = 'ForEachActivityViolations', ProfileChanges = 'ProfileChanges', @@ -107,7 +107,7 @@ export type WindowsExpression = | WindowsExpressionLongerThan | WindowsExpressionShorterThan | WindowsExpressionNot - | WindowsExpressionShiftBy + | IntervalsExpressionShiftEdges | WindowsExpressionFromSpans | IntervalsExpressionStarts | IntervalsExpressionEnds @@ -118,6 +118,7 @@ export type SpansExpression = | SpansExpressionSplit | IntervalsExpressionStarts | IntervalsExpressionEnds + | IntervalsExpressionShiftEdges | SpansExpressionFromWindows | ForEachActivitySpans | SpansExpressionInterval; @@ -148,9 +149,9 @@ export interface WindowsExpressionNot { expression: WindowsExpression; } -export interface WindowsExpressionShiftBy { - kind: NodeKind.WindowsExpressionShiftBy, - windowExpression: WindowsExpression, +export interface IntervalsExpressionShiftEdges { + kind: NodeKind.IntervalsExpressionShiftEdges, + expression: IntervalsExpression, fromStart: Duration, fromEnd: Duration, } 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 4c996b643f..727b4fdef7 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 @@ -280,8 +280,8 @@ export class Windows { }); } else { return new Windows({ - kind: AST.NodeKind.WindowsExpressionShiftBy, - windowExpression: this.__astNode, + kind: AST.NodeKind.IntervalsExpressionShiftEdges, + expression: this.__astNode, fromStart, fromEnd }) @@ -471,6 +471,21 @@ export class Spans { }) } + /** + * Shifts the start and end of each Span by a duration. + * + * @param fromStart duration to shift start by + * @param fromEnd duration to shift end by (defaults is `fromStart` if omitted) + */ + public shiftBy(fromStart: AST.Duration, fromEnd?: AST.Duration | undefined): Spans { + return new Spans({ + kind: AST.NodeKind.IntervalsExpressionShiftEdges, + expression: this.__astNode, + fromStart, + fromEnd: fromEnd !== undefined ? fromEnd : fromStart + }) + } + /** * Convert this into a set of Windows. Each span is a true segment, and everything else is false. * @@ -1314,6 +1329,14 @@ declare global { */ public ends(): Spans; + /** + * Shifts the start and end of each Span by a duration. + * + * @param fromStart duration to shift start by + * @param fromEnd duration to shift end by (defaults is `fromStart` if omitted) + */ + public shiftBy(fromStart: AST.Duration, fromEnd?: AST.Duration | undefined): Spans; + /** * Splits each span into equal sized sub-spans. * 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 345e7a6215..bc5bfc63e6 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 @@ -33,7 +33,7 @@ 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.ShiftEdges; import gov.nasa.jpl.aerie.constraints.tree.ShorterThan; import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.Split; @@ -501,7 +501,7 @@ export default() => { return Real.Resource("state of charge").rate().equal(Real.Value(4.0)).shiftBy(minute(2), minute(-20)) } """, - new ViolationsOfWindows(new ShiftWindowsEdges( + new ViolationsOfWindows(new ShiftEdges( new Equal<>(new Rate(new RealResource("state of charge")), new RealValue(4.0)), new DurationLiteral(Duration.of(2, Duration.MINUTE)), new DurationLiteral(Duration.of(-20, Duration.MINUTE))) From c7ca72f61fadfa3d7f3b3945b6c800fa1726bc5c Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 14 Sep 2023 12:44:27 -0700 Subject: [PATCH 2/3] Implement Spans.selectWhenTrue --- .../constraints/json/ConstraintParsers.java | 15 +++++++- .../constraints/tree/SpansSelectWhenTrue.java | 38 +++++++++++++++++++ .../src/libs/constraints-ast.ts | 10 ++++- .../src/libs/constraints-edsl-fluent-api.ts | 26 +++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/SpansSelectWhenTrue.java 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 9397cec074..80b944c07a 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 @@ -260,6 +260,18 @@ static JsonParser transitionP(JsonParser> profi .map( untuple((kind, alias) -> new ActivitySpan(alias)), $ -> tuple(Unit.UNIT, $.activityAlias)); + + static JsonParser spansSelectWhenTrueF(JsonParser> spansP, JsonParser> windowsP) { + return productP + .field("kind", literalP("SpansSelectWhenTrue")) + .field("spansExpression", spansP) + .field("windowsExpression", windowsP) + .map( + untuple((kind, spans, windows) -> new SpansSelectWhenTrue(spans, windows)), + $ -> tuple(Unit.UNIT, $.spans(), $.windows()) + ); + } + static final JsonParser startOfP = productP .field("kind", literalP("WindowsExpressionStartOf")) @@ -600,7 +612,8 @@ private static JsonParser> spansExpressionF(JsonParser spans, Expression windows) implements Expression { + + @Override + public Spans evaluate(SimulationResults results, final Interval bounds, EvaluationEnvironment environment) { + final var spans = this.spans.evaluate(results, bounds, environment); + final var windows = this.windows.evaluate(results, bounds, environment); + final var trueSegments = new ArrayList(); + for (final var window: windows.iterateEqualTo(true)) trueSegments.add(window); + return spans.select(trueSegments.toArray(new Interval[] {})); + } + + @Override + public void extractResources(final Set names) { + this.spans.extractResources(names); + this.windows.extractResources(names); + } + + @Override + public String prettyPrint(final String prefix) { + return String.format( + "\n%s(spans-select-when-true %s %s)", + prefix, + this.spans.prettyPrint(prefix + " "), + this.windows.prettyPrint(prefix + " ") + ); + } +} 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 740a033614..edda4420fc 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts @@ -28,6 +28,7 @@ export enum NodeKind { SpansExpressionFromWindows = 'SpansExpressionFromWindows', SpansExpressionSplit = 'SpansExpressionSplit', SpansExpressionInterval = 'SpansExpressionInterval', + SpansSelectWhenTrue = 'SpansSelectWhenTrue', ExpressionEqual = 'ExpressionEqual', ExpressionNotEqual = 'ExpressionNotEqual', RealProfileLessThan = 'RealProfileLessThan', @@ -121,7 +122,14 @@ export type SpansExpression = | IntervalsExpressionShiftEdges | SpansExpressionFromWindows | ForEachActivitySpans - | SpansExpressionInterval; + | SpansExpressionInterval + | SpansSelectWhenTrue; + +export interface SpansSelectWhenTrue { + kind: NodeKind.SpansSelectWhenTrue, + spansExpression: SpansExpression + windowsExpression: WindowsExpression +} export type IntervalsExpression = | WindowsExpression 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 727b4fdef7..449eddce06 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 @@ -538,6 +538,22 @@ export class Spans { expression: expression(new ActivityInstance(activityType, alias)).__astNode, }); } + + /** + * Selects only spans that occur during a true segment, removing those that don't. + * + * Spans that only partially overlap with a true segment will be truncated, and spans + * that overlap with multiple true segments will be split. + * + * @param windows + */ + public selectWhenTrue(windows: Windows): Spans { + return new Spans({ + kind: AST.NodeKind.SpansSelectWhenTrue, + spansExpression: this.__astNode, + windowsExpression: windows.__astNode + }); + } } /** @@ -1379,6 +1395,16 @@ declare global { * @param unit unit of time to count. Does not need to be a round unit (i.e. can be 1.5 minutes, if you want). */ public accumulatedDuration(unit: AST.Duration): Real; + + /** + * Selects only spans that occur during a true segment, removing those that don't. + * + * Spans that only partially overlap with a true segment will be truncated, and spans + * that overlap with multiple true segments will be split. + * + * @param windows + */ + public selectWhenTrue(windows: Windows): Spans; } /** From 052e69dd55f093d18fba3d5aaa8a7791a07ee042 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 14 Sep 2023 12:44:31 -0700 Subject: [PATCH 3/3] Add tests --- .../jpl/aerie/constraints/time/Spans.java | 3 +- .../jpl/aerie/constraints/tree/ASTTests.java | 85 +++++++++++++++++++ ...ConstraintsDSLCompilationServiceTests.java | 65 +++++++++++++- 3 files changed, 151 insertions(+), 2 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java index 90596d66b9..0f918e9c58 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Spans.java @@ -217,7 +217,8 @@ public Spans ends() { @Override public Spans shiftEdges(final Duration fromStart, final Duration fromEnd) { - return this.map($ -> Interval.between($.start.plus(fromStart), $.startInclusivity, $.end.plus(fromEnd), $.endInclusivity)); + return this.map($ -> Interval.between($.start.plus(fromStart), $.startInclusivity, $.end.plus(fromEnd), $.endInclusivity)) + .filter($ -> !$.isEmpty()); } @Override 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 f51289e90c..80196ce03e 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 @@ -1251,6 +1251,91 @@ public void tesRollingThresholdDeficit() { assertEquals(expected2, result2); } + @Test + void testSpansShiftEdges() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of(), + Map.of() + ); + + final var result1 = new ShiftEdges<>( + Supplier.of(new Spans( + Interval.between(0, 1, SECONDS), + Interval.between(0, 2, SECONDS), + Interval.between(0, Inclusive, 2, Exclusive, SECONDS), + Interval.between(0, 3, SECONDS) + )), + Supplier.of(Duration.of(1, SECONDS)), + Supplier.of(Duration.of(-1, SECONDS)) + ).evaluate(simResults); + + final var expected1 = new Spans( + Interval.at(1, SECONDS), + Interval.between(1, 2, SECONDS) + ); + + assertIterableEquals(expected1, result1); + + final var result2 = new ShiftEdges<>( + Supplier.of(new Spans( + Interval.between(0, 1, SECONDS), + Interval.between(0, 2, SECONDS), + Interval.between(0, Inclusive, 2, Exclusive, SECONDS), + Interval.between(0, 3, SECONDS) + )), + Supplier.of(Duration.of(3, SECONDS)), + Supplier.of(Duration.of(4, SECONDS)) + ).evaluate(simResults); + + final var expected2 = new Spans( + Interval.between(3, 5, SECONDS), + Interval.between(3, 6, SECONDS), + Interval.between(3, Inclusive, 6, Exclusive, SECONDS), + Interval.between(3, 7, SECONDS) + ); + + assertIterableEquals(expected2, result2); + } + + @Test + void testSpansSelectWhenTrue() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of(), + Map.of() + ); + + final var result = new SpansSelectWhenTrue( + Supplier.of(new Spans( + Interval.between(0, 1, SECONDS), // fully inside + Interval.between(3, 4, SECONDS), // fully outside + Interval.between(5, 7, SECONDS), // half outside + Interval.between(10, 14, SECONDS) // split across multiple + )), + Supplier.of(new Windows(false).set( + List.of( + Interval.between(0, 1, SECONDS), + Interval.between(6, 8, SECONDS), + Interval.between(9, 11, SECONDS), + Interval.between(13, 15, SECONDS) + ), + true + )) + ).evaluate(simResults); + + final var expected = new Spans( + Interval.between(0, 1, SECONDS), + Interval.between(6, 7, SECONDS), + Interval.between(10, 11, SECONDS), + Interval.between(13, 14, SECONDS) + ); + + assertIterableEquals(expected, result); + } + /** * An expression that yields the same aliased object every time it is evaluated. */ 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 bc5bfc63e6..f291fe4ce2 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 @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.server.services; +import gov.nasa.jpl.aerie.constraints.time.Spans; import gov.nasa.jpl.aerie.constraints.tree.AbsoluteInterval; import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.constraints.tree.AccumulatedDuration; @@ -36,6 +37,7 @@ import gov.nasa.jpl.aerie.constraints.tree.ShiftEdges; import gov.nasa.jpl.aerie.constraints.tree.ShorterThan; import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; +import gov.nasa.jpl.aerie.constraints.tree.SpansSelectWhenTrue; import gov.nasa.jpl.aerie.constraints.tree.Split; import gov.nasa.jpl.aerie.constraints.tree.Starts; import gov.nasa.jpl.aerie.constraints.tree.Times; @@ -501,7 +503,7 @@ export default() => { return Real.Resource("state of charge").rate().equal(Real.Value(4.0)).shiftBy(minute(2), minute(-20)) } """, - new ViolationsOfWindows(new ShiftEdges( + new ViolationsOfWindows(new ShiftEdges<>( new Equal<>(new Rate(new RealResource("state of charge")), new RealValue(4.0)), new DurationLiteral(Duration.of(2, Duration.MINUTE)), new DurationLiteral(Duration.of(-20, Duration.MINUTE))) @@ -1315,4 +1317,65 @@ export default () => { } } + @Test + void testSpansShiftBy() { + checkSuccessfulCompilation( + """ + const minute = (m: number) => Temporal.Duration.from({minutes: m}); + export default() => { + return Spans.ForEachActivity(ActivityType.activity, i => i.span()).shiftBy(minute(2)).windows(); + } + """, + new ViolationsOfWindows( + new WindowsFromSpans(new ShiftEdges<>( + new ForEachActivitySpans( + "activity", + "span activity alias 0", + new ActivitySpan("span activity alias 0")), + new DurationLiteral(Duration.of(2, Duration.MINUTE)), + new DurationLiteral(Duration.of(2, Duration.MINUTE)))) + ) + ); + + checkSuccessfulCompilation( + """ + const minute = (m: number) => Temporal.Duration.from({minutes: m}); + export default() => { + return Spans.ForEachActivity(ActivityType.activity, i => i.span()).shiftBy(minute(2), minute(3)).windows(); + } + """, + new ViolationsOfWindows( + new WindowsFromSpans(new ShiftEdges<>( + new ForEachActivitySpans( + "activity", + "span activity alias 0", + new ActivitySpan("span activity alias 0")), + new DurationLiteral(Duration.of(2, Duration.MINUTE)), + new DurationLiteral(Duration.of(3, Duration.MINUTE)) + )) + ) + ); + } + + @Test + void testSpansSelectWhenTrue() { + checkSuccessfulCompilation( + """ + const minute = (m: number) => Temporal.Duration.from({minutes: m}); + export default() => { + return Spans.ForEachActivity(ActivityType.activity, i => i.span()).selectWhenTrue(Windows.Value(true)).windows() + } + """, + new ViolationsOfWindows( + new WindowsFromSpans(new SpansSelectWhenTrue( + new ForEachActivitySpans( + "activity", + "span activity alias 0", + new ActivitySpan("span activity alias 0") + ), + new WindowsValue(true) + )) + ) + ); + } }