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 cfb882db49..2d3fd6fff0 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 @@ -74,6 +74,18 @@ static

> JsonParser> assignGapsF(final JsonPa ); } + static

> JsonParser> shiftByF(final JsonParser> profileParser) { + return productP + .field("kind", literalP("ProfileExpressionShiftBy")) + .field("expression", profileParser) + .field("duration", durationExprP) + .map( + untuple((kind, expression, duration) -> new ShiftBy<>(expression, duration)), + $ -> tuple(Unit.UNIT, $.expression(), $.duration()) + ); + } + + static final JsonParser discreteResourceP = productP .field("kind", literalP("DiscreteProfileResource")) @@ -106,6 +118,7 @@ public static JsonParser> discreteProfileExprF(JsonP discreteValueP, discreteParameterP, assignGapsF(selfP), + shiftByF(selfP), valueAtExpressionF(profileExpressionP, spansExpressionP), listExpressionF(profileExpressionP), structExpressionF(profileExpressionP) @@ -206,6 +219,7 @@ private static JsonParser> linearProfileExprF(JsonPars timesF(selfP), rateF(selfP), assignGapsF(selfP), + shiftByF(selfP), accumulatedDurationF(windowsP), accumulatedDurationF(spansP) )); @@ -288,14 +302,14 @@ static JsonParser transitionP(JsonParser> profi $ -> tuple(Unit.UNIT, $.value(), $.interval()) ); - static JsonParser shiftByF(JsonParser> windowsExpressionP) { + static JsonParser shiftWindowsEdgesF(JsonParser> windowsExpressionP) { return productP .field("kind", literalP("WindowsExpressionShiftBy")) .field("windowExpression", windowsExpressionP) .field("fromStart", durationExprP) .field("fromEnd", durationExprP) .map( - untuple((kind, windowsExpression, fromStart, fromEnd) -> new ShiftBy(windowsExpression, fromStart, fromEnd)), + untuple((kind, windowsExpression, fromStart, fromEnd) -> new ShiftWindowsEdges(windowsExpression, fromStart, fromEnd)), $ -> tuple(Unit.UNIT, $.windows, $.fromStart, $.fromEnd)); } static final JsonParser endOfP = @@ -515,12 +529,13 @@ private static JsonParser> windowsExpressionF(JsonParserbuilder(); + + for (final var segment : this.profilePieces) { + final var interval = segment.interval(); + final var shiftedInterval = interval.shiftBy(duration); + + builder.set(Segment.of(shiftedInterval, segment.value())); + } + return new DiscreteProfile(builder.build()); + } + @Override public Optional valueAt(final Duration timepoint) { final var matchPiece = profilePieces diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java index 18325e07d9..e9f9469151 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java @@ -156,6 +156,25 @@ public Optional valueAt(final Duration timepoint) { .map(linearEquationSegment -> SerializedValue.of(linearEquationSegment.value().valueAt(timepoint))); } + @Override + public LinearProfile shiftBy(final Duration duration) { + final var builder = IntervalMap.builder(); + + for (final var segment : this.profilePieces) { + final var interval = segment.interval(); + final var shiftedInterval = interval.shiftBy(duration); + + final var shiftedValue = new LinearEquation( + segment.value().initialTime.saturatingPlus(duration), + segment.value().initialValue, + segment.value().rate + ); + + builder.set(Segment.of(shiftedInterval, shiftedValue)); + } + return new LinearProfile(builder.build()); + } + public static LinearProfile fromSimulatedProfile(final List> simulatedProfile) { return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/Profile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/Profile.java index 2b34ab28fa..6c64987622 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/Profile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/Profile.java @@ -13,6 +13,7 @@ public interface Profile

> { boolean isConstant(); P assignGaps(P def); + P shiftBy(Duration duration); Optional valueAt(Duration timepoint); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java index e215e710ee..0767bd7302 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java @@ -148,6 +148,15 @@ public boolean isPoint() { this.start == this.end; } + public Interval shiftBy(final Duration duration) { + return Interval.between( + this.start.saturatingPlus(duration), + this.startInclusivity, + this.end.saturatingPlus(duration), + this.endInclusivity + ); + } + public Duration duration() { if (this.isEmpty()) return Duration.ZERO; return this.end.minus(this.start); 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 51aa398733..d7ab1ca3ab 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,7 +342,7 @@ public Windows filterByDuration(Duration minDur, Duration maxDur) { * @param fromEnd duration to shift true -> false falling edges * @return a new Windows */ - public Windows shiftBy(Duration fromStart, Duration fromEnd) { + public Windows shiftEdges(Duration fromStart, Duration fromEnd) { final var builder = IntervalMap.builder(); for (final var segment : this.segments) { @@ -363,6 +363,11 @@ public Windows shiftBy(Duration fromStart, Duration fromEnd) { return new Windows(builder.build()); } + @Override + public Windows shiftBy(Duration duration) { + return this.shiftEdges(duration, duration); + } + /** * Converts this into Spans and splits each Span into sub-spans. * diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ActivityWindow.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ActivityWindow.java index 9a566eebfa..1ad673d159 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ActivityWindow.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ActivityWindow.java @@ -20,7 +20,7 @@ public ActivityWindow(final String activityAlias) { public Windows evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final var activity = environment.activityInstances().get(this.activityAlias); return new Windows( - Segment.of(Interval.FOREVER, false), + Segment.of(bounds, false), Segment.of(activity.interval, true) ); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteParameter.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteParameter.java index 10409e5a1b..4c336c4219 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteParameter.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteParameter.java @@ -22,7 +22,7 @@ public DiscreteParameter(final String activityAlias, final String parameterName) public DiscreteProfile evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final var activity = environment.activityInstances().get(this.activityAlias); return new DiscreteProfile( - Segment.of(Interval.FOREVER, activity.parameters.get(this.parameterName)) + Segment.of(bounds, activity.parameters.get(this.parameterName)) ); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteProfileFromDuration.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteProfileFromDuration.java index dce82b04f9..0409afbc3f 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteProfileFromDuration.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/DiscreteProfileFromDuration.java @@ -18,7 +18,7 @@ public record DiscreteProfileFromDuration( @Override public DiscreteProfile evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final Duration duration = this.duration.evaluate(results, bounds, environment); - return new DiscreteProfile(Segment.of(Interval.FOREVER, SerializedValue.of(duration.in(Duration.MICROSECOND)))); + return new DiscreteProfile(Segment.of(bounds, SerializedValue.of(duration.in(Duration.MICROSECOND)))); } @Override diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/EndOf.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/EndOf.java index 77654d0967..1fd684bcd2 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/EndOf.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/EndOf.java @@ -20,7 +20,7 @@ public EndOf(final String activityAlias) { public Windows evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final var activity = environment.activityInstances().get(this.activityAlias); return new Windows( - Segment.of(Interval.FOREVER, false), + Segment.of(bounds, false), Segment.of(Interval.at(activity.interval.end), true) ); } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftBy.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftBy.java index dc62fdd5cb..903e9edf3b 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftBy.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftBy.java @@ -1,59 +1,41 @@ package gov.nasa.jpl.aerie.constraints.tree; import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; +import gov.nasa.jpl.aerie.constraints.model.Profile; 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.merlin.protocol.types.Duration; -import java.util.Objects; import java.util.Set; -public final class ShiftBy implements Expression { - public final Expression windows; - public final Expression fromStart; - public final Expression fromEnd; - - public ShiftBy(final Expression left, final Expression fromStart, final Expression fromEnd) { - this.windows = left; - this.fromStart = fromStart; - this.fromEnd = fromEnd; - } +public record ShiftBy

>( + Expression

expression, + Expression duration) implements Expression

{ @Override - public Windows evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { - final var windows = this.windows.evaluate(results, bounds, environment); - return windows.shiftBy(this.fromStart.evaluate(results, bounds, environment), this.fromEnd.evaluate(results, bounds, environment)); + public P evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { + // bounds aren't shifted here because duration expressions don't care about them; durations don't exist on the timeline. + final var duration = this.duration.evaluate(results, bounds, environment); + + final var shiftedBounds = bounds.shiftBy(Duration.negate(duration)); + final var originalProfile = this.expression.evaluate(results, shiftedBounds, environment); + + return originalProfile.shiftBy(duration); } @Override public void extractResources(final Set names) { - this.windows.extractResources(names); + this.expression.extractResources(names); + this.duration.extractResources(names); } @Override public String prettyPrint(final String prefix) { return String.format( - "\n%s(shift %s by %s %s)", + "\n%s(shiftBy %s %s)", prefix, - this.windows.prettyPrint(prefix + " "), - this.fromStart.toString(), - this.fromEnd.toString() + this.expression.prettyPrint(prefix + " "), + this.duration.prettyPrint(prefix + " ") ); } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ShiftBy)) return false; - final var o = (ShiftBy)obj; - - return Objects.equals(this.windows, o.windows) && - 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); - } } 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/ShiftWindowsEdges.java new file mode 100644 index 0000000000..0db61054bc --- /dev/null +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/ShiftWindowsEdges.java @@ -0,0 +1,69 @@ +package gov.nasa.jpl.aerie.constraints.tree; + +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.merlin.protocol.types.Duration; + +import java.util.Objects; +import java.util.Set; + +public final class ShiftWindowsEdges implements Expression { + public final Expression windows; + public final Expression fromStart; + public final Expression fromEnd; + + public ShiftWindowsEdges(final Expression left, final Expression fromStart, final Expression fromEnd) { + this.windows = left; + this.fromStart = fromStart; + this.fromEnd = fromEnd; + } + + @Override + public Windows 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); + + final var newBounds = Interval.between( + Duration.min(bounds.start.minus(shiftRising), bounds.start.minus(shiftFalling)), + bounds.startInclusivity, + Duration.max(bounds.end.minus(shiftRising), bounds.end.minus(shiftFalling)), + bounds.endInclusivity + ); + + final var windows = this.windows.evaluate(results, newBounds, environment); + return windows.shiftEdges(shiftRising, shiftFalling).select(bounds); + } + + @Override + public void extractResources(final Set names) { + this.windows.extractResources(names); + } + + @Override + public String prettyPrint(final String prefix) { + return String.format( + "\n%s(shiftWindowsEdges %s by %s %s)", + prefix, + this.windows.prettyPrint(prefix + " "), + this.fromStart.toString(), + this.fromEnd.toString() + ); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ShiftWindowsEdges)) return false; + final var o = (ShiftWindowsEdges)obj; + + return Objects.equals(this.windows, o.windows) && + 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); + } +} diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/StartOf.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/StartOf.java index 35946edfef..7f8a4972a3 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/StartOf.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/StartOf.java @@ -20,7 +20,7 @@ public StartOf(final String activityAlias) { public Windows evaluate(final SimulationResults results, final Interval bounds, final EvaluationEnvironment environment) { final var activity = environment.activityInstances().get(this.activityAlias); return new Windows( - Segment.of(Interval.FOREVER, false), + Segment.of(bounds, false), Segment.of(Interval.at(activity.interval.start), true) ); } diff --git a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/time/WindowsTest.java b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/time/WindowsTest.java index 3b4c3fe9b0..eebf3e415d 100644 --- a/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/time/WindowsTest.java +++ b/constraints/src/test/java/gov/nasa/jpl/aerie/constraints/time/WindowsTest.java @@ -347,7 +347,7 @@ public void shiftByStretch() { Segment.of(interval(14, Exclusive, 17, Exclusive, SECONDS), false), Segment.of(at(Duration.MAX_VALUE), true)); //long overflow if at max value - Windows result = orig.shiftBy(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); + Windows result = orig.shiftEdges(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); Windows expected = new Windows( @@ -392,7 +392,7 @@ public void shiftByConnectedIntervals() { .set(interval(0, 2, SECONDS), true) .set(interval(8, 10, SECONDS), true); - var fromStartPosFromEndPos = orig.shiftBy(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); + var fromStartPosFromEndPos = orig.shiftEdges(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); assertIterableEquals( new Windows(interval(0, 10, SECONDS), false) .set(interval(-1, 3, SECONDS), true) @@ -400,7 +400,7 @@ public void shiftByConnectedIntervals() { fromStartPosFromEndPos ); - var fromStartPosFromEndNeg = orig.shiftBy(Duration.of(1, SECONDS), Duration.of(-1, SECONDS)); + var fromStartPosFromEndNeg = orig.shiftEdges(Duration.of(1, SECONDS), Duration.of(-1, SECONDS)); assertEquals( new Windows(interval(1, Exclusive, 9, Exclusive, SECONDS), false) .set(at(1, SECONDS), true) @@ -408,7 +408,7 @@ public void shiftByConnectedIntervals() { fromStartPosFromEndNeg ); - var fromStartNegFromEndPos = orig.shiftBy(Duration.of(1, SECONDS), Duration.of(1, SECONDS)); + var fromStartNegFromEndPos = orig.shiftEdges(Duration.of(1, SECONDS), Duration.of(1, SECONDS)); assertEquals( new Windows(interval(1, 11, SECONDS), false) .set(interval(1, 3, SECONDS), true) @@ -416,7 +416,7 @@ public void shiftByConnectedIntervals() { fromStartNegFromEndPos ); - var fromStartNegFromEndNeg = orig.shiftBy(Duration.of(-1, SECONDS), Duration.of(-1, SECONDS)); + var fromStartNegFromEndNeg = orig.shiftEdges(Duration.of(-1, SECONDS), Duration.of(-1, SECONDS)); assertEquals( new Windows(interval(-1, 9, SECONDS), false) .set(interval(-1, 1, SECONDS), true) @@ -424,7 +424,7 @@ public void shiftByConnectedIntervals() { fromStartNegFromEndNeg ); - var removal = orig.shiftBy(Duration.of(0, SECONDS), Duration.of(-3, SECONDS)); + var removal = orig.shiftEdges(Duration.of(0, SECONDS), Duration.of(-3, SECONDS)); assertEquals( new Windows(interval(-1, Exclusive, 8, Exclusive, SECONDS), false), removal @@ -437,19 +437,19 @@ public void shiftByDisconnectedPoints() { .set(at(0, SECONDS), true) .set(at(2, SECONDS), false); - var fromStartPosFromEndPos = orig.shiftBy(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); + var fromStartPosFromEndPos = orig.shiftEdges(Duration.of(-1, SECONDS), Duration.of(1, SECONDS)); assertIterableEquals( new Windows(interval(-1, 1, SECONDS), true), fromStartPosFromEndPos ); - var fromStartPosFromEndNeg = orig.shiftBy(Duration.of(1, SECONDS), Duration.of(-1, SECONDS)); + var fromStartPosFromEndNeg = orig.shiftEdges(Duration.of(1, SECONDS), Duration.of(-1, SECONDS)); assertIterableEquals( new Windows(interval(1, 3, SECONDS), false), fromStartPosFromEndNeg ); - var fromStartNegFromEndPos = orig.shiftBy(Duration.of(1, SECONDS), Duration.of(1, SECONDS)); + var fromStartNegFromEndPos = orig.shiftEdges(Duration.of(1, SECONDS), Duration.of(1, SECONDS)); assertIterableEquals( new Windows() .set(at(1, SECONDS), true) @@ -457,7 +457,7 @@ public void shiftByDisconnectedPoints() { fromStartNegFromEndPos ); - var fromStartNegFromEndNeg = orig.shiftBy(Duration.of(-1, SECONDS), Duration.of(-1, SECONDS)); + var fromStartNegFromEndNeg = orig.shiftEdges(Duration.of(-1, SECONDS), Duration.of(-1, SECONDS)); assertIterableEquals( new Windows() .set(at(-1, SECONDS), true) 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 05c4eaf98d..b5dc5e5bdd 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 @@ -359,7 +359,7 @@ public void testOr() { @Test public void testExpandBy() { final var simResults = new SimulationResults( - Instant.EPOCH, Interval.between(0, 20, SECONDS), + Instant.EPOCH, Interval.between(-100, 200, SECONDS), List.of(), Map.of(), Map.of() @@ -376,7 +376,7 @@ public void testExpandBy() { final var expandByFromStart = Duration.of(-1, SECONDS); final var expandByFromEnd = Duration.of(0, SECONDS); - final var result = new ShiftBy(Supplier.of(left), Supplier.of(expandByFromStart), Supplier.of(expandByFromEnd)).evaluate(simResults, new EvaluationEnvironment()); + final var result = new ShiftWindowsEdges(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) @@ -391,7 +391,7 @@ public void testExpandBy() { @Test public void testShrink() { final var simResults = new SimulationResults( - Instant.EPOCH, Interval.between(0, 20, SECONDS), + Instant.EPOCH, Interval.between(-100, 200, SECONDS), List.of(), Map.of(), Map.of() @@ -408,7 +408,7 @@ public void testShrink() { final var clampFromStart = Duration.of(1, SECONDS); final var clampFromEnd = Duration.negate(Duration.of(1, SECONDS)); - final var result = new ShiftBy(Supplier.of(left), Supplier.of(clampFromStart), Supplier.of(clampFromEnd)).evaluate(simResults, new EvaluationEnvironment()); + final var result = new ShiftWindowsEdges(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) @@ -505,6 +505,25 @@ public void testDiscreteResource() { assertEquivalent(expected, result); } + + @Test + public void testDiscreteShiftBy() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of(), + Map.of( + "discrete", new DiscreteProfile(Segment.of(Interval.between(1, 2, SECONDS), SerializedValue.of("much value"))) + ) + ); + + final var result = new ShiftBy<>(new DiscreteResource("discrete"), new DurationLiteral(Duration.of(1, SECONDS))).evaluate(simResults, new EvaluationEnvironment()); + + final var expected = new DiscreteProfile(Segment.of(Interval.between(2, 3, SECONDS), SerializedValue.of("much value"))); + + assertEquivalent(expected, result); + } + @Test public void testValueAt(){ final var simResults = new SimulationResults( @@ -621,6 +640,24 @@ public void testRealResourceOnNonexistentResource() { fail("Expected RealResource node to fail on non-existent resource"); } + @Test + public void testRealShiftBy() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of( + "real", new LinearProfile(Segment.of(Interval.between(1, 2, SECONDS), new LinearEquation(Duration.of(1, SECONDS), 1, 1))) + ), + Map.of() + ); + + final var result = new ShiftBy<>(new RealResource("real"), new DurationLiteral(Duration.of(1, SECONDS))).evaluate(simResults, new EvaluationEnvironment()); + + final var expected = new LinearProfile(Segment.of(Interval.between(2, 3, SECONDS), new LinearEquation(Duration.of(2, SECONDS), 1, 1))); + + assertEquivalent(expected, result); + } + @Test public void testForEachActivity() { final var simResults = new SimulationResults( @@ -807,7 +844,7 @@ public void testStartOf() { final var result = new StartOf("act").evaluate(simResults, environment); final var expected = new Windows( - Segment.of(FOREVER, false), + Segment.of(simResults.bounds, false), Segment.of(at(4, SECONDS), true) ); @@ -841,7 +878,7 @@ public void testEndOf() { final var result = new EndOf("act").evaluate(simResults, environment); final var expected = new Windows( - Segment.of(FOREVER, false), + Segment.of(simResults.bounds, false), Segment.of(at(8, SECONDS), true) ); @@ -1017,6 +1054,75 @@ public void testSpansFromInterval() { assertEquals(expected, result); } + @Test + public void testShiftByBoundsAdjustment() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of(), + Map.of() + ); + + final var expression = new ShiftBy<>( + new DiscreteValue(SerializedValue.of("strang")), + Supplier.of(Duration.of(10, SECONDS)) + ); + + final var result = expression.evaluate(simResults); + + final var expected = new DiscreteProfile( + Segment.of(Interval.between(0, 20, SECONDS), SerializedValue.of("strang")) + ); + + assertIterableEquals(expected, result); + } + + @Test + public void testShiftWindowsEdgesBoundsAdjustment() { + final var simResults = new SimulationResults( + Instant.EPOCH, Interval.between(0, 20, SECONDS), + List.of(), + Map.of(), + Map.of() + ); + + final var crossingStartOfPlan = new Windows(false).set(Interval.between(-1, 1, SECONDS), true); + + final var result1 = new ShiftWindowsEdges( + Supplier.of(crossingStartOfPlan), + Supplier.of(Duration.ZERO), + Supplier.of(Duration.of(10, SECONDS)) + ).evaluate(simResults); + final var expected1 = new Windows(false).set(Interval.between(-1, 11, SECONDS), true).select(simResults.bounds); + assertIterableEquals(expected1, result1); + + final var result2 = new ShiftWindowsEdges( + Supplier.of(crossingStartOfPlan), + Supplier.of(Duration.of(-10, SECONDS)), + Supplier.of(Duration.ZERO) + ).evaluate(simResults); + final var expected2 = new Windows(false).set(Interval.between(0, 1, SECONDS), true).select(simResults.bounds); + assertIterableEquals(expected2, result2); + + final var crossingEndOfPlan = new Windows(false).set(Interval.between(19, 21, SECONDS), true); + + final var result3 = new ShiftWindowsEdges( + Supplier.of(crossingEndOfPlan), + Supplier.of(Duration.ZERO), + Supplier.of(Duration.of(10, SECONDS)) + ).evaluate(simResults); + final var expected3 = new Windows(false).set(Interval.between(19, 20, SECONDS), true).select(simResults.bounds); + assertIterableEquals(expected3, result3); + + final var result4 = new ShiftWindowsEdges( + Supplier.of(crossingEndOfPlan), + Supplier.of(Duration.of(-10, SECONDS)), + Supplier.of(Duration.ZERO) + ).evaluate(simResults); + final var expected4 = new Windows(false).set(Interval.between(9, 21, SECONDS), true).select(simResults.bounds); + assertIterableEquals(expected2, result2); + } + private static final class Supplier implements Expression { private final T value; 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 ddc771c33b..369aeda9c6 100644 --- a/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts +++ b/merlin-server/constraints-dsl-compiler/src/libs/constraints-ast.ts @@ -43,6 +43,7 @@ export enum NodeKind { ForEachActivitySpans = 'ForEachActivitySpans', ForEachActivityViolations = 'ForEachActivityViolations', ProfileChanges = 'ProfileChanges', + ProfileExpressionShiftBy = 'ProfileExpressionShiftBy', ViolationsOf = 'ViolationsOf', AbsoluteInterval = 'AbsoluteInterval', IntervalAlias = 'IntervalAlias', @@ -82,6 +83,7 @@ export type WindowsExpression = | WindowsExpressionStartOf | WindowsExpressionEndOf | ProfileChanges + | ProfileExpressionShiftBy | RealProfileLessThan | RealProfileLessThanOrEqual | RealProfileGreaterThan @@ -120,6 +122,12 @@ export interface ProfileChanges { expression: ProfileExpression; } +export interface ProfileExpressionShiftBy

{ + kind: NodeKind.ProfileExpressionShiftBy, + expression: P, + duration: Duration +} + export interface WindowsExpressionValue { kind: NodeKind.WindowsExpressionValue, value: boolean, @@ -266,7 +274,8 @@ export type RealProfileExpression = | RealProfileValue | RealProfileParameter | AssignGapsExpression - | RealProfileAccumulatedDuration; + | RealProfileAccumulatedDuration + | ProfileExpressionShiftBy; export interface StructProfileExpression { kind: NodeKind.StructProfileExpression, @@ -333,7 +342,8 @@ export type DiscreteProfileExpression = | StructProfileExpression | ListProfileExpression | ValueAtExpression - | IntervalDuration; + | IntervalDuration + | ProfileExpressionShiftBy; export interface DiscreteProfileResource { kind: NodeKind.DiscreteProfileResource; 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 48fc9c63b9..472c18c09d 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 @@ -220,19 +220,30 @@ export class Windows { /** * Shifts the start and end of all true segments by a duration. * - * Shifts the start and end of all false segment by the opposite directions (i.e. the start of each false segment + * The second argument is optional: if omitted, `shiftBy(dur)` shifts all segments uniformly by `dur`, which + * is equivalent to `shiftBy(dur, dur)`. + * + * Shifts the start and end of all false segment by the reversed directions (i.e. the start of each false segment * is shifted by `fromEnd`). * * @param fromStart duration to add from the start of each true segment - * @param fromEnd duration to add from the end of each true segment + * @param fromEnd duration to add from the end of each true segment. Default is equal to `fromStart` if omitted. */ - public shiftBy(fromStart: AST.Duration, fromEnd: AST.Duration) : Windows { - return new Windows({ - kind: AST.NodeKind.WindowsExpressionShiftBy, - windowExpression: this.__astNode, - fromStart, - fromEnd - }) + public shiftBy(fromStart: AST.Duration, fromEnd?: AST.Duration | undefined) : Windows { + if (fromEnd === undefined) { + return new Windows({ + kind: AST.NodeKind.ProfileExpressionShiftBy, + expression: this.__astNode, + duration: fromStart + }); + } else { + return new Windows({ + kind: AST.NodeKind.WindowsExpressionShiftBy, + windowExpression: this.__astNode, + fromStart, + fromEnd + }) + } } /** @@ -707,6 +718,19 @@ export class Real { timepoint : timepoint.__astNode }); } + + /** + * Shifts the profile forward or backward in time. + * + * @param duration duration shift each segment (can be negative) + */ + public shiftBy(duration: Temporal.Duration): Real { + return new Real({ + kind: AST.NodeKind.ProfileExpressionShiftBy, + expression: this.__astNode, + duration + }) + } } /** @@ -868,6 +892,19 @@ export class Discrete { defaultProfile: defaultProfile.__astNode }); } + + /** + * Shifts the profile forward or backward in time. + * + * @param duration duration shift each segment (can be negative) + */ + public shiftBy(duration: Temporal.Duration): Discrete { + return new Discrete({ + kind: AST.NodeKind.ProfileExpressionShiftBy, + expression: this.__astNode, + duration + }) + } } /** Represents an instance of an activity in the plan. */ @@ -1065,13 +1102,16 @@ declare global { /** * Shifts the start and end of all true segments by a duration. * - * Shifts the start and end of all false segment by the opposite directions (i.e. the start of each false segment + * The second argument is optional: if omitted, `shiftBy(dur)` shifts all segments uniformly by `dur`, which + * is equivalent to `shiftBy(dur, dur)`. + * + * Shifts the start and end of all false segment by the reversed directions (i.e. the start of each false segment * is shifted by `fromEnd`). * * @param fromStart duration to add from the start of each true segment - * @param fromEnd duration to add from the end of each true segment + * @param fromEnd duration to add from the end of each true segment. Default is equal to `fromStart` if omitted. */ - public shiftBy(fromStart: AST.Duration, fromEnd: AST.Duration): Windows; + public shiftBy(fromStart: AST.Duration, fromEnd?: AST.Duration | undefined): Windows; /** * Returns a new windows object, with all true segments shorter than or equal to the given @@ -1334,6 +1374,13 @@ declare global { * @param timepoint the timepoint, represented by a Spans (must be reduced to a single point) */ public valueAt(timepoint: Spans): Discrete; + + /** + * Shifts the profile forward or backward in time. + * + * @param duration duration shift each segment (can be negative) + */ + public shiftBy(duration: Temporal.Duration): Real; } /** @@ -1422,6 +1469,13 @@ declare global { * @param timepoint the timepoint, represented by a Spans (must be reduced to a single point) */ public valueAt(timepoint: Spans): Discrete; + + /** + * Shifts the profile forward or backward in time. + * + * @param duration duration shift each segment (can be negative) + */ + public shiftBy(duration: Temporal.Duration): Discrete; } /** An enum for whether an interval includes its bounds. */ 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 c667ab35c2..1e008e28f1 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 @@ -32,6 +32,7 @@ import gov.nasa.jpl.aerie.constraints.tree.RealResource; import gov.nasa.jpl.aerie.constraints.tree.RealValue; import gov.nasa.jpl.aerie.constraints.tree.ShiftBy; +import gov.nasa.jpl.aerie.constraints.tree.ShiftWindowsEdges; import gov.nasa.jpl.aerie.constraints.tree.ShorterThan; import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.Split; @@ -486,7 +487,7 @@ export default() => { } @Test - void testShiftBy() { + void testShiftWindowsEdges() { checkSuccessfulCompilation( """ const minute = (m: number) => Temporal.Duration.from({minutes: m}); @@ -494,7 +495,7 @@ export default() => { return Real.Resource("state of charge").rate().equal(Real.Value(4.0)).shiftBy(minute(2), minute(-20)) } """, - new ViolationsOfWindows(new ShiftBy( + new ViolationsOfWindows(new ShiftWindowsEdges( 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))) @@ -1227,4 +1228,32 @@ export default () => { "TypeError: TS2345 Argument of type 'Discrete' is not assignable to parameter of type '\"Option1\" | \"Option2\" | Discrete<\"Option1\" | \"Option2\">'." ); } + + @Test + void testProfileShiftBy() { + checkSuccessfulCompilation( + """ + const minute = (m: number) => Temporal.Duration.from({minutes: m}); + export default() => { + return Real.Resource("state of charge").shiftBy(minute(2)).equal(Real.Value(4.0)) + } + """, + new ViolationsOfWindows( + new Equal<>(new ShiftBy<>(new RealResource("state of charge"), new DurationLiteral(Duration.of(2, Duration.MINUTE))), new RealValue(4.0)) + ) + ); + + checkSuccessfulCompilation( + """ + const minute = (m: number) => Temporal.Duration.from({minutes: m}); + export default() => { + return Discrete.Resource("mode").shiftBy(minute(2)).equal("Option1") + } + """, + new ViolationsOfWindows( + new Equal<>(new ShiftBy<>(new DiscreteResource("mode"), new DurationLiteral(Duration.of(2, Duration.MINUTE))), new DiscreteValue(SerializedValue.of("Option1"))) + ) + ); + } + } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerAfterEach.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerAfterEach.java index 07dad57f5b..2459d7ac53 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerAfterEach.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerAfterEach.java @@ -19,7 +19,7 @@ public Windows transformWindows(final Plan plan, final Windows windows, final Si var retWin = windows; retWin = retWin.not(); retWin = retWin.removeTrueSegment(0); - retWin = retWin.shiftBy(dur, Duration.ZERO); + retWin = retWin.shiftEdges(dur, Duration.ZERO); return retWin; } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerBeforeEach.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerBeforeEach.java index 493f07f25b..cafd1945a7 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerBeforeEach.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/transformers/TransformerBeforeEach.java @@ -19,7 +19,7 @@ public Windows transformWindows(final Plan plan, final Windows windows, final Si var retWin = windows; retWin = retWin.not(); retWin = retWin.removeTrueSegment(-1); - retWin = retWin.shiftBy(Duration.ZERO, Duration.negate(dur)); + retWin = retWin.shiftEdges(Duration.ZERO, Duration.negate(dur)); return retWin; } }