Skip to content

Commit

Permalink
Add complex test case
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelCourtney committed Aug 29, 2024
1 parent 3ec3162 commit 4a16914
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.procedures;

import gov.nasa.ammos.aerie.procedural.scheduling.Rule;
import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan;
import gov.nasa.ammos.aerie.procedural.timeline.Interval;
import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real;
import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Strings;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.LinearEquation;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.Map;

@SchedulingProcedure
public record StayWellFed(double bitePeriodHours) implements Rule {
@Override
public void run(@NotNull final EditablePlan plan) {
final var bitePeriod = Duration.hours(bitePeriodHours);
var simResults = plan.latestResults();
if (simResults == null) simResults = plan.simulate();

// I'm using producer as a substitute for a mission phase variable.
// This goal will only apply during the Dole mission phase. :)
final var dolePhase = simResults
.resource("/producer", Strings::deserialize)
.highlightEqualTo("Dole");
dolePhase.cache();

// Manual recurrence goal: during Dole phase, require a bitebanana every `bitePeriod`.
final var bites = plan.directives("BiteBanana")
.filterByWindows(dolePhase, false)
.collect();

var currentTime = Duration.MIN_VALUE;
for (final var phase: dolePhase.collect()) {
currentTime = Duration.max(currentTime, phase.start);

while (currentTime.plus(bitePeriod).shorterThan(phase.end)) {
var nextExistingBiteTime = bites.isEmpty() ? Duration.MAX_VALUE : bites.getFirst().getStartTime();
while (nextExistingBiteTime.minus(currentTime).noLongerThan(bitePeriod)) {
currentTime = Duration.max(currentTime, nextExistingBiteTime);
bites.removeFirst();

nextExistingBiteTime = bites.isEmpty() ? Duration.MAX_VALUE : bites.getFirst().getStartTime();
}
while (nextExistingBiteTime.minus(currentTime).longerThan(bitePeriod) && phase.contains(currentTime.plus(bitePeriod))) {
currentTime = currentTime.plus(bitePeriod);
plan.create(
"BiteBanana",
new DirectiveStart.Absolute(currentTime),
Map.of("biteSize", SerializedValue.of(1))
);
}
}
}

plan.commit();
simResults = plan.simulate();

final var newBites = plan.directives("BiteBanana")
.filterByWindows(dolePhase, false);

// All this banana biting made us run out of bananas.
// So we iteratively find the first time /fruit drops below zero
// and add a grow banana fix it. We then mock the effect of grow banana
// by adding one to /fruit, rather than resimulating, and do it again.
var fruit = simResults.resource("/fruit", Real::deserialize);
fruit.cache();

var ranOutAt = fruit.lessThan(0).filterByWindows(dolePhase, true).risingEdges().highlightTrue().collect();
while (!ranOutAt.isEmpty()) {
final var problemStart = ranOutAt.getFirst().start;
final var growStart = problemStart.minus(Duration.HOUR);
final var activeBites = newBites.collect(Interval.at(problemStart));

final var currentFruit = fruit.sample(problemStart);
final var pastFruit = fruit.sample(growStart);

plan.create(
"GrowBanana",
// activeBites.isEmpty() ? new DirectiveStart.Absolute(growStart)
// : new DirectiveStart.Anchor(activeBites.getFirst().id, Duration.HOUR.negate(), DirectiveStart.Anchor.AnchorPoint.Start),
new DirectiveStart.Absolute(growStart),
Map.of(
"growingDuration", SerializedValue.of(Duration.HOUR.micros()),
"quantity", SerializedValue.of(pastFruit - currentFruit)
)
);

fruit = fruit.plus(
Real.step(growStart, pastFruit - currentFruit)
.set(new Real(List.of(
new Segment<>(Interval.between(growStart, problemStart), new LinearEquation(growStart, 0.0, (pastFruit - currentFruit) / (Duration.HOUR.in(Duration.SECONDS))))
)))
);

ranOutAt = fruit.lessThan(0).filterByWindows(dolePhase, true).risingEdges().highlightTrue().collect();
}

plan.commit();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import gov.nasa.ammos.aerie.procedural.timeline.util.truncateList
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.unaryMinus
import kotlin.jvm.optionals.getOrNull
import kotlin.math.pow
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.*;

/** A profile of [LinearEquations][LinearEquation]; a piece-wise linear real-number profile. */
data class Real(private val timeline: Timeline<Segment<LinearEquation>, Real>):
Expand Down Expand Up @@ -217,5 +218,10 @@ data class Real(private val timeline: Timeline<Segment<LinearEquation>, Real>):
}

/***/ class RealDeserializeException(message: String): Exception(message)

@JvmStatic @JvmOverloads fun step(at: Duration = Duration.ZERO, value: Double = 1.0) = Real(
Segment(Duration.MIN_VALUE ..< at, LinearEquation(0.0)),
Segment(at .. Duration.MAX_VALUE, LinearEquation(value))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface SerialSegmentOps<V : Any, THIS: SerialSegmentOps<V, THIS>>: SerialOps<
/** [(DOC)][assignGaps] Fills in gaps in this profile with another profile. */
// While this is logically the converse of [set], they can't delegate to each other because it would mess up the return type.
infix fun assignGaps(other: SerialSegmentOps<V, *>) =
map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { l, _, _, -> l })
map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { l, _, _ -> l })
/** [(DOC)][assignGaps] Fills in gaps in this profile with a constant value. */
infix fun assignGaps(v: V) = assignGaps(Constants(v))

Expand Down

0 comments on commit 4a16914

Please sign in to comment.