Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Procedural Scheduling activity deletion #1610

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures;

import gov.nasa.ammos.aerie.procedural.scheduling.Goal;
import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan;
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 gov.nasa.jpl.aerie.types.ActivityDirectiveId;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.Optional;

/**
* Creates three activities in a chain of anchors, then deletes one.
*/
@SchedulingProcedure
public record ActivityDeletionGoal(int whichToDelete, DeletedAnchorStrategy anchorStrategy) implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {
final var ids = new ActivityDirectiveId[3];

ids[0] = plan.create(
"BiteBanana",
new DirectiveStart.Absolute(Duration.HOUR),
Map.of("biteSize", SerializedValue.of(0))
);
ids[1] = plan.create(
"BiteBanana",
new DirectiveStart.Anchor(ids[0], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.End),
Map.of("biteSize", SerializedValue.of(1))
);
ids[2] = plan.create(
"BiteBanana",
new DirectiveStart.Anchor(ids[1], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.Start),
Map.of("biteSize", SerializedValue.of(2))
);

if (whichToDelete >= 0) {
plan.delete(ids[whichToDelete], anchorStrategy);
}

plan.commit();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ void executeEDSLAndProcedure() throws IOException {
final var args = Json.createObjectBuilder().add("quantity", 4).build();
hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

final String recurrenceGoalDefinition =
"""
export default function myGoal() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}),
interval: Temporal.Duration.from({hours:1})
})}""";

hasura.createSchedulingSpecGoal(
"Recurrence Scheduling Test Goal",
recurrenceGoalDefinition,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling;

import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId;
import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.json.Json;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class DeletionTests extends ProceduralSchedulingSetup {
private GoalInvocationId procedureId;

@BeforeEach
void localBeforeEach() throws IOException {
try (final var gateway = new GatewayRequests(playwright)) {
int procedureJarId = gateway.uploadJarFile("build/libs/ActivityDeletionGoal.jar");
// Add Scheduling Procedure
procedureId = hasura.createSchedulingSpecProcedure(
"Test Scheduling Procedure",
procedureJarId,
specId,
0
);
}
}

@AfterEach
void localAfterEach() throws IOException {
hasura.deleteSchedulingGoal(procedureId.goalId());
}

@Test
void createsThreeActivities() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", -1)
.add("anchorStrategy", "Error")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(3, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
final AtomicReference<Integer> id2 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get());
if (result) id2.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get())
));
}

@Test
void deletesLast() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 2)
.add("anchorStrategy", "Error")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get())
));
}

@Test
void deletesMiddleCascade() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 1)
.add("anchorStrategy", "Cascade")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(1, activities.size());

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null)
));
}

@Test
void deletesMiddleAnchorToParent() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 1)
.add("anchorStrategy", "ReAnchor")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana")
&& Objects.equals(it.anchorId(), id1.get())
&& Objects.equals(it.startOffset(), "02:00:00")
));
}

@Test
void deletesFirstCascade() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 0)
.add("anchorStrategy", "Cascade")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(0, activities.size());
}

@Test
void deletesFirstReAnchorToPlan() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 0)
.add("anchorStrategy", "ReAnchor")
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id2 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana")
&& Objects.equals(it.anchorId(), null)
&& Objects.equals(it.startOffset(), "02:00:00");
if (result) id2.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get())
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@ public abstract class ProceduralSchedulingSetup {

// Cross-Test Constants
protected final String planStartTimestamp = "2023-01-01T00:00:00+00:00";
protected final String recurrenceGoalDefinition =
"""
export default function myGoal() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}),
interval: Temporal.Duration.from({hours:1})
})}""";


@BeforeAll
void beforeAll() {
Expand Down
15 changes: 13 additions & 2 deletions e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ public record Plan(
int revision,
List<ActivityDirective> activityDirectives
) {
public record ActivityDirective(int id, int planId, String type, String startOffset, JsonObject arguments, String name) {
public record ActivityDirective(
int id,
int planId,
String type,
String startOffset,
JsonObject arguments,
String name,
Integer anchorId,
boolean anchoredToStart
) {
public static ActivityDirective fromJSON(JsonObject json){
return new ActivityDirective(
json.getInt("id"),
json.getInt("plan_id"),
json.getString("type"),
json.getString("startOffset"),
json.getJsonObject("arguments"),
json.getString("name")
json.getString("name"),
json.isNull("anchorId") ? null : json.getInt("anchorId"),
json.getBoolean("anchoredToStart")
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,8 @@ query GetPlan($id: Int!) {
startOffset: start_offset
type
name
anchorId: anchor_id
anchoredToStart: anchored_to_start
}
constraint_specification {
constraint_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public boolean validateGrowingDuration() {
@EffectModel
@ControllableDuration(parameterName = "growingDuration")
public void run(final Mission mission) {
final var rate = this.quantity() / (double) this.growingDuration().in(Duration.SECONDS);
final var rate = this.quantity() / (double) this.growingDuration().ratioOver(Duration.SECOND);
mission.fruit.rate.add(rate);
delay(this.growingDuration());
mission.fruit.rate.add(-rate);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gov.nasa.ammos.aerie.procedural.constraints

import gov.nasa.ammos.aerie.procedural.timeline.Interval
import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives
import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances
import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps
import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment
Expand All @@ -15,4 +16,5 @@ open class NotImplementedSimulationResults: SimulationResults {
deserializer: (List<Segment<SerializedValue>>) -> TL
): TL = TODO()
override fun <A : Any> instances(type: String?, deserializer: (SerializedValue) -> A): Instances<A> = TODO()
override fun <A : Any> inputDirectives(deserializer: (SerializedValue) -> A) = TODO()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gov.nasa.ammos.aerie.procedural.scheduling.plan

/**
* How to handle directives anchored to a deleted activity.
*
* If you intend to delete an activity that you believe has nothing anchored to it,
* using [Error] is recommended. This is the default.
*/
enum class DeletedAnchorStrategy {
/** Throw an error. */ Error,
/** Recursively delete everything in the anchor chain. */ Cascade,

/**
* Attempt to delete the activity in-place without changing the start times
* of any activities anchored to it.
*
* Consider the anchor chain `A <- B <- C`, where `A` starts at an absolute time and
* `B` and `C` are anchored.
* - If `A` is deleted with [ReAnchor], `B` will be set to start at the absolute time `A.startTime + B.offset`.
* `C` will be unchanged.
* - If `B` is deleted with [ReAnchor], `C` will be anchored to `A` with a new offset equal to `B.offset + C.offset`.
*
* If an activity is anchored to the end of the deleted activity, the delete activity's duration is assumed to be 0,
* which may change the ultimate start time of the anchored activity.
*/
ReAnchor,
}
Loading
Loading