Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,12 @@ static JsonParser<KeepTrueSegment> keepTrueSegmentP(JsonParser<Expression<Window
productP
.field("windows", listP(intervalP))
.field("activityInstanceIds", listP(longP))
.optionalField("message", stringP)
.map(
untuple(Violation::new),
$ -> tuple($.windows(), $.activityInstanceIds())
untuple((windows, aids, message) -> message
.map(s -> new Violation(windows, aids, s))
.orElseGet(() -> new Violation(windows, aids))),
$ -> tuple($.windows(), $.activityInstanceIds(), $.message())
);

public static final JsonParser<EDSLConstraintResult> edslConstraintResultP =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public record Violation(List<Interval> windows, ArrayList<Long> activityInstanceIds) {
public record Violation(List<Interval> windows, ArrayList<Long> activityInstanceIds, Optional<String> message) {
public Violation(List<Interval> windows, List<Long> activityInstanceIds) {
this(windows, new ArrayList<>(activityInstanceIds));
this(windows, new ArrayList<>(activityInstanceIds), Optional.empty());
}

public Violation(List<Interval> windows, List<Long> activityInstanceIds, String message) {
this(windows, new ArrayList<>(activityInstanceIds), Optional.ofNullable(message));
}

public static List<Violation> fromProceduralViolations(Violations violations, gov.nasa.jpl.aerie.merlin.driver.SimulationResults simResults) {
Expand Down Expand Up @@ -42,7 +47,10 @@ public static List<Violation> fromProceduralViolations(Violations violations, go
}
}

constraintViolations.add(new Violation(List.of(Interval.fromProceduralInterval(v.getInterval())), activityInstanceIds));
constraintViolations.add(new Violation(
List.of(Interval.fromProceduralInterval(v.getInterval())),
activityInstanceIds,
v.getMessage()));
}
return constraintViolations;
}
Expand Down
1 change: 1 addition & 0 deletions deployment/hasura/metadata/actions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ type ConstraintResult {
}

type ConstraintViolation {
message: String,
windows: [Interval!]!,
activityInstanceIds: [Int!]!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults)
return Violations.on(
fruit.lessThan(upperBound).and(fruit.greaterThan(lowerBound)),
false
);
).withDefaultMessage("Fruit count is outside of boundaries: [%d, %d]".formatted(lowerBound, upperBound));
}

@WithDefaults
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures;

import gov.nasa.ammos.aerie.procedural.constraints.Constraint;
import gov.nasa.ammos.aerie.procedural.constraints.Violations;
import gov.nasa.ammos.aerie.procedural.constraints.annotations.ConstraintProcedure;
import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults;
import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real;
import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan;
import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults;
import org.jetbrains.annotations.NotNull;

/**
* Equivalent to FruitThresholdConstraint, except it does not include a message in the violations
*/
@ConstraintProcedure
public record NoMessageConstraint(int lowerBound, int upperBound) implements Constraint {
@NotNull
@Override
public Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults) {
final var fruit = simResults.resource("/fruit", Real.deserializer());

return Violations.on(
fruit.lessThan(upperBound).and(fruit.greaterThan(lowerBound)),
false
);
}

@WithDefaults
public static class Template{
public int lowerBound = 5;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,35 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

public class BasicConstraintTests extends ProceduralSchedulingSetup {
private ConstraintInvocationId fruitTresholdConstraintId;
private ConstraintInvocationId fruitThresholdConstraintId;
private ConstraintInvocationId noMessageConstraintId;

@BeforeEach
void localBeforeEach() throws IOException {
try (final var gateway = new GatewayRequests(playwright)) {
final int fruitTresholdConstraintJarId = gateway.uploadJarFile("build/libs/FruitThresholdConstraint.jar");
// Add Scheduling Procedure
fruitTresholdConstraintId = hasura.createConstraintSpecProcedure(
"Test Constraint Procedure 1",
fruitTresholdConstraintJarId,
final int fruitThresholdConstraintJarId = gateway.uploadJarFile("build/libs/FruitThresholdConstraint.jar");
final int noMessageConstraintJarId = gateway.uploadJarFile("build/libs/NoMessageConstraint.jar");
// Add Constraint Procedures
fruitThresholdConstraintId = hasura.createConstraintSpecProcedure(
"Fruit Threshold Constraint",
fruitThresholdConstraintJarId,
planId
);
noMessageConstraintId = hasura.createConstraintSpecProcedure(
"No Message Constraint",
noMessageConstraintJarId,
planId
);

// Disable the noMessageConstraint by default
hasura.updatePlanConstraintSpecEnabled(noMessageConstraintId.invocationId(), false);
}
}

@AfterEach
void localAfterEach() throws IOException {
hasura.deleteConstraint(fruitTresholdConstraintId.id());
hasura.deleteConstraint(fruitThresholdConstraintId.id());
hasura.deleteConstraint(noMessageConstraintId.id());
}

/**
Expand All @@ -47,8 +58,15 @@ void executeConstraintRunWithoutArguments() throws IOException {
hasura.awaitSimulation(planId);
final var resp = hasura.checkConstraints(planId);
assertEquals(1, resp.constraintsRun().size());
assertEquals(1, resp.constraintsRun().getFirst().errors().size());
assertTrue(resp.constraintsRun().getFirst().errors().getFirst().message().contains("gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException: Invalid arguments for input type \"FruitThresholdConstraint\": extraneous arguments: [], unconstructable arguments: [], missing arguments: [MissingArgument[parameterName=upperBound, schema=IntSchema[]]], valid arguments: [ValidArgument[parameterName=lowerBound, serializedValue=NumericValue[value=5]]]"));
final var constraint = resp.constraintsRun().getFirst();
assertEquals(1, constraint.errors().size());
assertTrue(constraint.errors().getFirst().message().contains(
"gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException: Invalid arguments for input type "
+ "\"FruitThresholdConstraint\": "
+ "extraneous arguments: [], "
+ "unconstructable arguments: [], "
+ "missing arguments: [MissingArgument[parameterName=upperBound, schema=IntSchema[]]], "
+ "valid arguments: [ValidArgument[parameterName=lowerBound, serializedValue=NumericValue[value=5]]]"));
}

/**
Expand All @@ -57,7 +75,7 @@ void executeConstraintRunWithoutArguments() throws IOException {
@Test
void executeConstraintRunWithArguments() throws IOException {
final var args = Json.createObjectBuilder().add("upperBound", 10).build();
hasura.updateConstraintArguments(fruitTresholdConstraintId.invocationId(), args);
hasura.updateConstraintArguments(fruitThresholdConstraintId.invocationId(), args);
hasura.awaitSimulation(planId);
final var resp = hasura.checkConstraints(planId);
assertTrue(resp.constraintsRun().getFirst().success());
Expand All @@ -68,20 +86,55 @@ void executeConstraintRunWithArguments() throws IOException {
assertEquals(violation.windows(), List.of(new ConstraintResult.Interval(0, Duration.hours(48).micros())));
}

/**
* Test that constraints with a violation message include said violation message.
*/
@Test
void messageReturnedIfPresent() throws IOException {
// Enable the NoMessageConstraint
hasura.updatePlanConstraintSpecEnabled(noMessageConstraintId.invocationId(), true);

// Assign args to the constraints
final var args = Json.createObjectBuilder().add("upperBound", 10).build();
hasura.updateConstraintArguments(fruitThresholdConstraintId.invocationId(), args);
hasura.updateConstraintArguments(noMessageConstraintId.invocationId(), args);

// Get Constraint Results
hasura.awaitSimulation(planId);
final var resp = hasura.checkConstraints(planId);

assertEquals(2, resp.constraintsRun().size());

final var firstConstraint = resp.constraintsRun().getFirst();
assertEquals(fruitThresholdConstraintId.invocationId(), firstConstraint.constraintInvocationId());
assertTrue(firstConstraint.success());
assertEquals(1, firstConstraint.result().get().violations().size());
final var firstConstraintViolation = firstConstraint.result().get().violations().getFirst();
assertTrue(firstConstraintViolation.message().isPresent());
assertEquals("Fruit count is outside of boundaries: [5, 10]", firstConstraintViolation.message().get());

final var secondConstraint = resp.constraintsRun().getLast();
assertEquals(noMessageConstraintId.invocationId(), secondConstraint.constraintInvocationId());
assertTrue(secondConstraint.success());
assertEquals(1, secondConstraint.result().get().violations().size());
final var secondConstraintViolation = secondConstraint.result().get().violations().getFirst();
assertTrue(secondConstraintViolation.message().isEmpty());
}

/**
* Queries the procedural constraints arguments.
*/
@Test
void effectiveArgumentsQuery() throws IOException {
final var effectiveArgs = hasura.getEffectiveProceduralConstraintsArgumentsBulk(
List.of(Pair.of(fruitTresholdConstraintId.id(), Json.createObjectBuilder().add("upperBound", 10).build())));
List.of(Pair.of(fruitThresholdConstraintId.id(), Json.createObjectBuilder().add("upperBound", 10).build())));
assertEquals(1, effectiveArgs.size());
assertTrue(effectiveArgs.get(0).success());
assertTrue(effectiveArgs.get(0).arguments().isPresent());
assertTrue(effectiveArgs.get(0).errors().isEmpty());
assertTrue(effectiveArgs.getFirst().success());
assertTrue(effectiveArgs.getFirst().arguments().isPresent());
assertTrue(effectiveArgs.getFirst().errors().isEmpty());

// Check returned Arguments
final var args = effectiveArgs.get(0).arguments().get();
final var args = effectiveArgs.getFirst().arguments().get();
assertEquals(2, args.size());
assertEquals(10, args.getInt("upperBound"));
assertEquals(5, args.getInt("lowerBound"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
import javax.json.JsonObject;
import javax.json.JsonString;
import java.util.List;
import java.util.Optional;

public record ConstraintResult(
List<String> resourceIds,
List<ConstraintResult.ConstraintViolation> violations,
List<ConstraintResult.Interval> gaps
) {
public record ConstraintViolation(List<Integer> activityInstanceIds, List<Interval> windows) {
public record ConstraintViolation(List<Integer> activityInstanceIds, List<Interval> windows, Optional<String> message) {

public static ConstraintViolation fromJSON(JsonObject json) {
return new ConstraintViolation(
json.getJsonArray("activityInstanceIds")
.getValuesAs(JsonNumber::intValue),
json.getJsonArray("windows")
.getValuesAs(Interval::fromJSON));
.getValuesAs(Interval::fromJSON),
Optional.ofNullable(json.getString("message", null))
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ query checkConstraints($planId: Int!, $simulationDatasetId: Int) {
start
}
violations {
message
activityInstanceIds
windows {
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -287,7 +288,7 @@ public static JsonValue serializeResourceSamples(final Map<String, List<Pair<Dur
.build();
}

public static JsonValue serializeConstraintResults(final int requestId, final Map<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>> resultMap) {
public static JsonValue serializeConstraintResults(final int requestId, final SortedMap<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>> resultMap) {
var results = resultMap.entrySet().stream().map(entry -> {

final var constraint = entry.getKey();
Expand Down Expand Up @@ -323,7 +324,7 @@ public static JsonValue serializeConstraintResults(final int requestId, final Ma
}

// successful runs
var constraintResult = (ConstraintResult) fallible.getOptional().get();
var constraintResult = fallible.getOptional().get();
return Json.createObjectBuilder()
.add("success", JsonValue.TRUE)
.add("constraintId", constraint.constraintId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ public record ConstraintRecord(
String description,
ConstraintType type,
Map<String, SerializedValue> arguments
) {}
) implements Comparable<ConstraintRecord> {

@Override
public int compareTo(final ConstraintRecord o) {
return Long.compare(this.priority, o.priority);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public List<BulkConstraintEffectiveArgumentResponse> getConstraintProcedureEffec
* @throws MissionModelService.NoSuchMissionModelException If the plan's mission model does not exist.
* @throws SimulationDatasetMismatchException If the specified simulation is not a simulation of the specified plan.
*/
public Pair<Integer, Map<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>>> getViolations(
public Pair<Integer, SortedMap<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>>> getViolations(
final PlanId planId,
final Optional<SimulationDatasetId> simulationDatasetId,
final boolean force,
Expand Down Expand Up @@ -117,7 +117,7 @@ public Pair<Integer, Map<ConstraintRecord, Fallible<ConstraintResult, List<? ext
final SimulationDatasetId simDatasetId = resultsHandle.getSimulationDatasetId();

final var constraints = new ArrayList<>(this.planService.getConstraintsForPlan(planId));
final var constraintResultMap = new HashMap<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>>();
final var constraintResultMap = new TreeMap<ConstraintRecord, Fallible<ConstraintResult, List<? extends Exception>>>();

// Load cached results if the force rerun flag is not set
final var validConstraintRuns = force ? new HashMap<ConstraintRecord, ConstraintResult>() :
Expand Down
Loading