From b7e6513f0d648dce639c4e9cbe1d1fbfc0ffabc0 Mon Sep 17 00:00:00 2001 From: joelco Date: Thu, 24 Jul 2025 13:04:53 -0700 Subject: [PATCH 1/7] Allow querying resources by register --- .../jpl/aerie/contrib/models/Accumulator.java | 2 +- .../aerie/contrib/models/NamedResource.java | 17 ++++++++++ .../jpl/aerie/contrib/models/Pointing.java | 2 +- .../jpl/aerie/contrib/models/Register.java | 2 +- .../aerie/contrib/models/SampledResource.java | 2 +- .../contrib/models/ValidationResult.java | 2 -- .../contrib/models/counters/Counter.java | 3 +- .../jpl/aerie/merlin/framework/Registrar.java | 9 +++++ .../framework/resources/NameableResource.java | 8 +++++ merlin-server/build.gradle | 1 + procedural/timeline/build.gradle | 1 + .../timeline/collections/profiles/Numbers.kt | 33 +++++++++++++++++++ .../timeline/plan/SimulationResults.kt | 10 ++++++ .../utils/PerishableSimResultsWrapper.kt | 24 +++++++++++++- stateless-aerie/build.gradle | 1 + 15 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java create mode 100644 merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Accumulator.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Accumulator.java index cd3a6b9ee5..4bf3c5e917 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Accumulator.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Accumulator.java @@ -7,7 +7,7 @@ import gov.nasa.jpl.aerie.merlin.framework.resources.real.RealResource; import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; -public final class Accumulator implements RealResource { +public final class Accumulator extends NamedResource implements RealResource { private final CellRef ref; public final Rate rate = new Rate(); diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java new file mode 100644 index 0000000000..6057318877 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java @@ -0,0 +1,17 @@ +package gov.nasa.jpl.aerie.contrib.models; + +import gov.nasa.jpl.aerie.merlin.framework.resources.NameableResource; + +public abstract class NamedResource implements NameableResource { + private String name = ""; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(final String name) { + this.name = name; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Pointing.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Pointing.java index 8a86ca03a1..7676f030a7 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Pointing.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Pointing.java @@ -54,7 +54,7 @@ public void slew(final Vector3D target, final Duration duration) { addRate(previousRate); // Reset rate to previous rate } - public static final class Component implements RealResource { + public static final class Component extends NamedResource implements RealResource { private final Accumulator acc; public final Accumulator.Rate rate; diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Register.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Register.java index d24573fbde..1436dc5d3a 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Register.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/Register.java @@ -7,7 +7,7 @@ import java.util.function.UnaryOperator; -public final class Register implements DiscreteResource { +public final class Register extends NamedResource implements DiscreteResource { public final CellRef> ref; private Register(final UnaryOperator duplicator, final Value initialValue) { diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/SampledResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/SampledResource.java index 271db4e2a8..13c86dd990 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/SampledResource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/SampledResource.java @@ -13,7 +13,7 @@ * Simple resource that samples arbitrarily many existing resources/values at a specified period (default period is once * per second). */ -public class SampledResource implements DiscreteResource { +public class SampledResource extends NamedResource implements DiscreteResource { private final Register result; private final Supplier sampler; private final Register period; diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/ValidationResult.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/ValidationResult.java index 0a40521d4e..8816788122 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/ValidationResult.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/ValidationResult.java @@ -1,5 +1,3 @@ package gov.nasa.jpl.aerie.contrib.models; -import java.util.Optional; - public record ValidationResult(boolean success, String subject, String message) {} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/counters/Counter.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/counters/Counter.java index b3514bc061..3452fc892c 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/counters/Counter.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/counters/Counter.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.contrib.models.counters; import gov.nasa.jpl.aerie.contrib.cells.counters.CounterCell; +import gov.nasa.jpl.aerie.contrib.models.NamedResource; import gov.nasa.jpl.aerie.merlin.framework.CellRef; import gov.nasa.jpl.aerie.merlin.framework.resources.discrete.DiscreteResource; @@ -8,7 +9,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; -public final class Counter implements DiscreteResource { +public final class Counter extends NamedResource implements DiscreteResource { private final CellRef> ref; public Counter(final T initialValue, final T zero, final BinaryOperator adder, final UnaryOperator duplicator) { diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java index f9287e34a5..3bdff54c3e 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.framework; +import gov.nasa.jpl.aerie.merlin.framework.resources.NameableResource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; import gov.nasa.jpl.aerie.merlin.protocol.driver.Querier; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; @@ -24,6 +25,10 @@ public boolean isInitializationComplete() { } public void discrete(final String name, final Resource resource, final ValueMapper mapper) { + if (resource instanceof NameableResource) { + ((NameableResource) resource).setName(name); + } + this.builder.resource(name, makeResource("discrete", resource, mapper.getValueSchema(), mapper::serializeValue)); } @@ -36,6 +41,10 @@ public void realWithMetadata(final String name, final Resource } private void real(final String name, final Resource resource, UnaryOperator schemaModifier) { + if (resource instanceof NameableResource) { + ((NameableResource) resource).setName(name); + } + this.builder.resource( name, makeResource( diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java new file mode 100644 index 0000000000..c97392c096 --- /dev/null +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.merlin.framework.resources; + +import gov.nasa.jpl.aerie.merlin.framework.Resource; + +public interface NameableResource extends Resource { + String getName(); + void setName(String name); +} diff --git a/merlin-server/build.gradle b/merlin-server/build.gradle index 680df08cc1..5dfcc0632c 100644 --- a/merlin-server/build.gradle +++ b/merlin-server/build.gradle @@ -82,6 +82,7 @@ application { dependencies { implementation project(':type-utils') implementation project(':merlin-driver') + implementation project(':merlin-framework') implementation project(':parsing-utilities') implementation project(':constraints') implementation project(':permissions') diff --git a/procedural/timeline/build.gradle b/procedural/timeline/build.gradle index 81eafb8dfc..d217985d4a 100644 --- a/procedural/timeline/build.gradle +++ b/procedural/timeline/build.gradle @@ -14,6 +14,7 @@ repositories { dependencies { implementation project(':merlin-driver') + implementation project(':merlin-framework') implementation project(':type-utils') testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/profiles/Numbers.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/profiles/Numbers.kt index 128aed4569..adfbd92aee 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/profiles/Numbers.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/profiles/Numbers.kt @@ -223,5 +223,38 @@ data class Numbers(private val timeline: Timeline, Numbers } seg.withNewValue(number) }) } + + /** + * Converts a list of serialized value segments into an integer [Numbers] profile; + * for use with [gov.nasa.ammos.aerie.procedural.timeline.plan.Plan.resource]. + * + * @throws ArithmeticException if any of the segment values are not integers + */ + @JvmStatic fun intDeserializer() = { list: List> -> Numbers(list.map { seg -> + val bigDecimal = seg.value.asNumeric().orElseThrow { Exception("value was not numeric: $seg") } + seg.withNewValue(bigDecimal.intValueExact()) + }) } + + /** + * Converts a list of serialized value segments into a double [Numbers] profile; + * for use with [gov.nasa.ammos.aerie.procedural.timeline.plan.Plan.resource]. + * + * @throws ArithmeticException if any of the segment values are not doubles + */ + @JvmStatic fun doubleDeserializer() = { list: List> -> Numbers(list.map { seg -> + val bigDecimal = seg.value.asNumeric().orElseThrow { Exception("value was not numeric: $seg") } + seg.withNewValue(bigDecimal.toDouble()) + }) } + + /** + * Converts a list of serialized value segments into a long [Numbers] profile; + * for use with [gov.nasa.ammos.aerie.procedural.timeline.plan.Plan.resource]. + * + * @throws ArithmeticException if any of the segment values are not longs + */ + @JvmStatic fun longDeserializer() = { list: List> -> Numbers(list.map { seg -> + val bigDecimal = seg.value.asNumeric().orElseThrow { Exception("value was not numeric: $seg") } + seg.withNewValue(bigDecimal.longValueExact()) + }) } } } diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt index fdf86f5c7d..56f7a1c391 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt @@ -6,8 +6,11 @@ import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyInstance import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances +import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.* import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective +import gov.nasa.jpl.aerie.merlin.framework.resources.NameableResource +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics /** An interface for querying plan information and simulation results. */ interface SimulationResults { @@ -25,6 +28,13 @@ interface SimulationResults { */ fun > resource(name: String, deserializer: (List>) -> TL): TL + fun resource(instance: NameableResource) = resource(instance.name, Real.deserializer()) + fun resource(instance: NameableResource) = resource(instance.name, Booleans.deserializer()) + fun resource(instance: NameableResource) = resource(instance.name, Strings.deserializer()) + fun resource(instance: NameableResource) = resource(instance.name, Numbers.deserializer()) + fun resource(instance: NameableResource, mapper: (Segment) -> V) + = resource(instance.name, Constants.deserializer(mapper)) + /** * Query activity instances. * diff --git a/procedural/utils/src/main/java/gov/nasa/ammos/aerie/procedural/utils/PerishableSimResultsWrapper.kt b/procedural/utils/src/main/java/gov/nasa/ammos/aerie/procedural/utils/PerishableSimResultsWrapper.kt index 8aa9471ef1..f085a68a28 100644 --- a/procedural/utils/src/main/java/gov/nasa/ammos/aerie/procedural/utils/PerishableSimResultsWrapper.kt +++ b/procedural/utils/src/main/java/gov/nasa/ammos/aerie/procedural/utils/PerishableSimResultsWrapper.kt @@ -1,7 +1,15 @@ package gov.nasa.ammos.aerie.procedural.utils import gov.nasa.ammos.aerie.procedural.scheduling.utils.PerishableSimulationResults +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.collections.profiles.* +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps +import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue /** * A wrapper around [SimulationResults] objects to make them implement @@ -10,10 +18,24 @@ import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults class PerishableSimResultsWrapper( private val simulationResults: SimulationResults, private var stale: Boolean = false -): PerishableSimulationResults, SimulationResults by simulationResults { +): PerishableSimulationResults { override fun setStale(stale: Boolean) { this.stale = stale } override fun isStale() = this.stale + + + // Delegation for SimulationResults + // Cannot use the `by` keyword due to multiple inheritance of the default overloaded resource methods + + override fun simBounds() = simulationResults.simBounds() + + override fun > resource(name: String, deserializer: (List>) -> TL) + = simulationResults.resource(name, deserializer) + + override fun instances(type: String?, deserializer: (SerializedValue) -> A) = simulationResults.instances(type, deserializer) + + override fun inputDirectives(deserializer: (SerializedValue) -> A) = simulationResults.inputDirectives(deserializer) + } diff --git a/stateless-aerie/build.gradle b/stateless-aerie/build.gradle index 886b701662..a6f6441eeb 100644 --- a/stateless-aerie/build.gradle +++ b/stateless-aerie/build.gradle @@ -20,6 +20,7 @@ jar { dependsOn(':orchestration-utils:jar') dependsOn(':constraints:jar') dependsOn(':merlin-driver:jar') + dependsOn(':merlin-framework:jar') dependsOn(':merlin-sdk:jar') dependsOn(':procedural:timeline:jar') dependsOn(':procedural:constraints:jar') From 92c4f31a671060935a7e3e3d7cc8e8f5a24640d8 Mon Sep 17 00:00:00 2001 From: joelco Date: Thu, 24 Jul 2025 14:23:36 -0700 Subject: [PATCH 2/7] Expose mission model to procedures --- .../aerie/contrib/models/NamedResource.java | 2 +- e2e-tests/build.gradle | 1 + .../procedures/ModelIntegrationGoal.java | 27 +++++++++ .../scheduling/ModelIntegrationTests.java | 57 +++++++++++++++++++ .../merlin/driver/MissionModelLoader.java | 3 +- .../merlin/server/models/ProcedureLoader.java | 3 +- .../examples/banana-procedures/build.gradle | 2 + .../procedures/SimulationDemo.java | 7 ++- .../procedural/timeline/util/WithModel.kt | 16 ++++++ .../jpl/aerie/scheduler/ProcedureLoader.java | 3 +- .../services/SynchronousSchedulerAgent.java | 2 + 11 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ModelIntegrationGoal.java create mode 100644 e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java create mode 100644 procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java index 6057318877..dc71b1ef37 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java @@ -3,7 +3,7 @@ import gov.nasa.jpl.aerie.merlin.framework.resources.NameableResource; public abstract class NamedResource implements NameableResource { - private String name = ""; + private String name = "ERROR: Name was not set during model construction"; @Override public String getName() { diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 0068fe7917..95891a8d05 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -64,6 +64,7 @@ dependencies { implementation project(':merlin-sdk') implementation project(':type-utils') implementation project(':contrib') + compileOnly project(':examples:banananation') testImplementation "com.zaxxer:HikariCP:5.1.0" testImplementation("org.postgresql:postgresql:42.6.0") diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ModelIntegrationGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ModelIntegrationGoal.java new file mode 100644 index 0000000000..bb937a30df --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ModelIntegrationGoal.java @@ -0,0 +1,27 @@ +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.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.ammos.aerie.procedural.timeline.util.WithModel; +import gov.nasa.jpl.aerie.banananation.Mission; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Creates a bite banana every time /producer changes + */ +@SchedulingProcedure +public record ModelIntegrationGoal() implements Goal, WithModel { + @Override + public void run(@NotNull final EditablePlan plan) { + final var changes = plan.simulate().resource(model().producer).changes().highlightTrue(); + for (final var interval: changes) { + plan.create("BiteBanana", new DirectiveStart.Absolute(interval.start), Map.of("biteSize", SerializedValue.of(1))); + } + plan.commit(); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java new file mode 100644 index 0000000000..fdd7780260 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java @@ -0,0 +1,57 @@ +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 javax.json.JsonValue; +import java.io.IOException; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ModelIntegrationTests extends ProceduralSchedulingSetup { + private int procedureJarId; + private GoalInvocationId procedureId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + procedureJarId = gateway.uploadJarFile("build/libs/ModelIntegrationGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + } + + @Test + void testModelIntegration() throws IOException { + hasura.insertActivityDirective( + planId, + "ChangeProducer", + "1h", + Json.createObjectBuilder().add("producer", Json.createValue("p")).build() + ); + hasura.updatePlanRevisionSchedulingSpec(planId); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java index dc455b4c7c..8e835e54df 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java @@ -66,7 +66,8 @@ public static MerlinPlugin loadMissionModelProvider(final Path path, final Strin final var className = getImplementingClassName(path, name, version); // Construct a ClassLoader with access to classes in the mission model location. - final var classLoader = new URLClassLoader(new URL[] {missionModelPathToUrl(path)}); + final var parentClassLoader = Thread.currentThread().getContextClassLoader(); + final var classLoader = new URLClassLoader(new URL[] {missionModelPathToUrl(path)}, parentClassLoader); try { final var pluginClass$ = classLoader.loadClass(className); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java index 10b2a0c172..8aedde0e49 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java @@ -15,7 +15,8 @@ public static ProcedureMapper loadProcedure(final Path path) throws ProcedureLoadException { final var className = getImplementingClassName(path); - final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}); + final var parentClassLoader = Thread.currentThread().getContextClassLoader(); + final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}, parentClassLoader); try { final var pluginClass$ = classLoader.loadClass(className); diff --git a/procedural/examples/banana-procedures/build.gradle b/procedural/examples/banana-procedures/build.gradle index 586865484a..8e1ef8e32c 100644 --- a/procedural/examples/banana-procedures/build.gradle +++ b/procedural/examples/banana-procedures/build.gradle @@ -24,6 +24,8 @@ dependencies { implementation project(':type-utils') implementation project(':contrib') + implementation project(":examples:banananation") + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation project(':procedural:utils') testImplementation project(':orchestration-utils') diff --git a/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/procedures/SimulationDemo.java b/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/procedures/SimulationDemo.java index d620755d07..b78abed888 100644 --- a/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/procedures/SimulationDemo.java +++ b/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/procedures/SimulationDemo.java @@ -1,25 +1,26 @@ package gov.nasa.ammos.aerie.procedural.examples.bananaprocedures.procedures; +import gov.nasa.ammos.aerie.procedural.timeline.util.WithModel; +import gov.nasa.jpl.aerie.banananation.Mission; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; 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.EditablePlan; -import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real; import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; import org.jetbrains.annotations.NotNull; import java.util.Map; @SchedulingProcedure -public record SimulationDemo(int quantity) implements Goal { +public record SimulationDemo(int quantity) implements Goal, WithModel { @Override public void run(@NotNull final EditablePlan plan) { var simResults = plan.latestResults(); if (simResults == null) simResults = plan.simulate(); - final var lowFruit = simResults.resource("/fruit", Real.deserializer()).lessThan(3.5).isolateTrue(); + final var lowFruit = simResults.resource(model().fruit).lessThan(3.5).isolateTrue(); final var bites = simResults.instances("BiteBanana"); final var connections = lowFruit.starts().shift(Duration.MINUTE.negate()) diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt new file mode 100644 index 0000000000..c0e7480fa9 --- /dev/null +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt @@ -0,0 +1,16 @@ +package gov.nasa.ammos.aerie.procedural.timeline.util + +interface WithModel { + @Suppress("unchecked_cast") + fun model(): M { + if (modelSingleton == null) { + throw IllegalStateException("modelSingleton was not initialized.") + } + + return modelSingleton as M + } + + companion object { + @JvmStatic var modelSingleton: Any? = null + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java index c0ebef784e..45f19956ee 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java @@ -15,7 +15,8 @@ public static ProcedureMapper loadProcedure(final Path path) throws ProcedureLoadException { final var className = getImplementingClassName(path); - final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}); + final var parentClassLoader = Thread.currentThread().getContextClassLoader(); + final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}, parentClassLoader); try { final var pluginClass$ = classLoader.loadClass(className); diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index a180d46741..cae4df5593 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import gov.nasa.ammos.aerie.procedural.timeline.payloads.ExternalEvent; +import gov.nasa.ammos.aerie.procedural.timeline.util.WithModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; @@ -129,6 +130,7 @@ public void schedule( ensureRequestIsCurrent(specification, request); //create scheduler problem seeded with initial plan final var schedulerMissionModel = loadMissionModel(planMetadata); + WithModel.setModelSingleton(schedulerMissionModel.missionModel.getModel()); final var planningHorizon = new PlanningHorizon( specification.horizonStartTimestamp().toInstant(), specification.horizonEndTimestamp().toInstant() From 0d748ac1973c5798310ede11cbb98401fe1a5c71 Mon Sep 17 00:00:00 2001 From: joelco Date: Mon, 28 Jul 2025 17:35:44 -0700 Subject: [PATCH 3/7] Fix class loader mismatch in scheduling --- .../gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java | 7 ++++++- .../gov/nasa/jpl/aerie/scheduler/goals/Procedure.java | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java index 45f19956ee..bc261b1a46 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/ProcedureLoader.java @@ -13,9 +13,14 @@ public final class ProcedureLoader { public static ProcedureMapper loadProcedure(final Path path) throws ProcedureLoadException + { + return loadProcedure(path, Thread.currentThread().getContextClassLoader()); + } + + public static ProcedureMapper loadProcedure(final Path path, final ClassLoader parentClassLoader) + throws ProcedureLoadException { final var className = getImplementingClassName(path); - final var parentClassLoader = Thread.currentThread().getContextClassLoader(); final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}, parentClassLoader); try { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java index 19d76ad089..533a8264b0 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java @@ -82,7 +82,7 @@ public boolean deleteAtBeginning( final var simResults = editablePlan.latestResults(); - instantiateGoal(); + instantiateGoal(missionModel.getModel().getClass().getClassLoader()); this.shouldDelete = this.goal.shouldDeletePastCreations(editablePlan, simResults); @@ -103,7 +103,7 @@ public void run( final DirectiveIdGenerator idGenerator, Map> eventsByDerivationGroup ) { - instantiateGoal(); + instantiateGoal(missionModel.getModel().getClass().getClassLoader()); List newActivities = new ArrayList<>(); @@ -150,10 +150,14 @@ public void run( } private void instantiateGoal() { + instantiateGoal(Thread.currentThread().getContextClassLoader()); + } + + private void instantiateGoal(final ClassLoader missionModelClassLoader) { if (this.goal == null) { final ProcedureMapper procedureMapper; try { - procedureMapper = ProcedureLoader.loadProcedure(jarPath); + procedureMapper = ProcedureLoader.loadProcedure(jarPath, missionModelClassLoader); } catch (ProcedureLoader.ProcedureLoadException e) { throw new RuntimeException(e); } From a64da13f576c2f3b59dba0e02fa2184fcdb9b658 Mon Sep 17 00:00:00 2001 From: joelco Date: Tue, 29 Jul 2025 15:16:33 -0700 Subject: [PATCH 4/7] Expose mission model to constraints --- e2e-tests/build.gradle | 1 + .../ModelIntegrationConstraint.java | 30 +++++++++ .../{scheduling => }/package-info.java | 2 +- ...edulingSetup.java => ProceduralSetup.java} | 4 +- .../ModelIntegrationConstraintTests.java | 64 +++++++++++++++++++ .../scheduling/AutoDeletionTests.java | 4 +- .../e2e/procedural/scheduling/BasicTests.java | 3 +- .../scheduling/DatabaseDeletionTests.java | 3 +- .../procedural/scheduling/DeletionTests.java | 3 +- .../ExternalEventsSchedulingTests.java | 3 +- .../scheduling/ExternalProfilesTests.java | 3 +- .../scheduling/ModelIntegrationTests.java | 3 +- .../jpl/aerie/e2e/types/ConstraintError.java | 3 +- .../jpl/aerie/e2e/utils/HasuraRequests.java | 26 ++++++++ .../aerie/merlin/server/AerieAppDriver.java | 3 +- .../server/models/ExecutableConstraint.java | 11 +++- .../merlin/server/models/ProcedureLoader.java | 7 +- .../server/services/ConstraintAction.java | 11 +++- .../services/LocalMissionModelService.java | 6 +- .../server/services/MissionModelService.java | 8 +++ .../server/mocks/StubMissionModelService.java | 11 ++++ 21 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/procedures/ModelIntegrationConstraint.java rename e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/{scheduling => }/package-info.java (77%) rename e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/{scheduling/ProceduralSchedulingSetup.java => ProceduralSetup.java} (94%) create mode 100644 e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/ModelIntegrationConstraintTests.java diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 95891a8d05..07a19d6411 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -60,6 +60,7 @@ dependencies { annotationProcessor project(':procedural:processor') implementation project(":procedural:scheduling") + implementation project(":procedural:constraints") implementation project(":procedural:timeline") implementation project(':merlin-sdk') implementation project(':type-utils') diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/procedures/ModelIntegrationConstraint.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/procedures/ModelIntegrationConstraint.java new file mode 100644 index 0000000000..db7be06d8c --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/procedures/ModelIntegrationConstraint.java @@ -0,0 +1,30 @@ +package gov.nasa.jpl.aerie.e2e.procedural.constraints.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.timeline.plan.Plan; +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults; +import gov.nasa.ammos.aerie.procedural.timeline.util.WithModel; +import gov.nasa.jpl.aerie.banananation.Mission; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A simple constraint that verifies access to the mission model through the WithModel interface + */ +@ConstraintProcedure +public record ModelIntegrationConstraint() implements Constraint, WithModel { + @Override + public @NotNull Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults) { + // Access the mission model through the WithModel interface + // This should work without ClassCastException if the class loader fix is working + final var mission = model(); + + // Simple constraint: fruit should never go below 2.0 + final var lowFruit = simResults.resource(mission.fruit).lessThan(2.0); + + return Violations.inside(lowFruit.highlightTrue()); + } +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/package-info.java similarity index 77% rename from e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java rename to e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/package-info.java index c77565fa77..3730d17025 100644 --- a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/package-info.java @@ -1,5 +1,5 @@ @WithMappers(BasicValueMappers.class) -package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +package gov.nasa.jpl.aerie.e2e.procedural; import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithMappers; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/ProceduralSetup.java similarity index 94% rename from e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java rename to e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/ProceduralSetup.java index 341b445299..cb78f2c223 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/ProceduralSetup.java @@ -1,4 +1,4 @@ -package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +package gov.nasa.jpl.aerie.e2e.procedural; import com.microsoft.playwright.Playwright; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; @@ -12,7 +12,7 @@ import java.io.IOException; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public abstract class ProceduralSchedulingSetup { +public abstract class ProceduralSetup { // Requests protected Playwright playwright; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/ModelIntegrationConstraintTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/ModelIntegrationConstraintTests.java new file mode 100644 index 0000000000..ed3b07d47e --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/ModelIntegrationConstraintTests.java @@ -0,0 +1,64 @@ +package gov.nasa.jpl.aerie.e2e.procedural.constraints; + +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; +import gov.nasa.jpl.aerie.e2e.types.ConstraintInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ModelIntegrationConstraintTests extends ProceduralSetup { + private int constraintJarId; + private ConstraintInvocationId constraintId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + constraintJarId = gateway.uploadJarFile("build/libs/ModelIntegrationConstraint.jar"); + // Add Constraint Procedure + constraintId = hasura.insertPlanConstraintJar( + "Test Model Integration Constraint", + planId, + constraintJarId + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteConstraint(constraintId.id()); + } + + @Test + void testModelIntegrationConstraint() throws IOException { + // Add an activity that will cause fruit to go below 2.0 + hasura.insertActivityDirective( + planId, + "BiteBanana", + "1h", + Json.createObjectBuilder().add("biteSize", Json.createValue(4)).build() + ); + + // Simulate the plan + hasura.awaitSimulation(planId); + + // Run constraints and check that our constraint can access the mission model + final var results = hasura.checkConstraints(planId); + final var run = results.constraintsRun().getFirst(); + + assertEquals("Test Model Integration Constraint", run.constraintName()); + + // The constraint should run without ClassCastException and detect violations + assertTrue(run.success()); + assertEquals(1, run.result().get().violations().size()); + + assertEquals(Duration.hours(1), Duration.microseconds(run.result().get().violations().getFirst().windows().getFirst().start())); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java index 262bd5001d..88e1d6f6a5 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -10,13 +11,12 @@ import java.io.IOException; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class AutoDeletionTests extends ProceduralSchedulingSetup { +public class AutoDeletionTests extends ProceduralSetup { private GoalInvocationId edslId; private GoalInvocationId procedureId; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java index 6a8b8d7f04..683e87f807 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -13,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class BasicTests extends ProceduralSchedulingSetup { +public class BasicTests extends ProceduralSetup { private int procedureJarId; private GoalInvocationId procedureId; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DatabaseDeletionTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DatabaseDeletionTests.java index 7b8d464f7d..b9b2bc0ede 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DatabaseDeletionTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DatabaseDeletionTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -16,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -public class DatabaseDeletionTests extends ProceduralSchedulingSetup { +public class DatabaseDeletionTests extends ProceduralSetup { private GoalInvocationId procedureId; @BeforeEach diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java index 9cd51f98f5..6385d40348 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -14,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class DeletionTests extends ProceduralSchedulingSetup { +public class DeletionTests extends ProceduralSetup { private GoalInvocationId procedureId; @BeforeEach diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsSchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsSchedulingTests.java index ec63afb558..cfe12676a7 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsSchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalEventsSchedulingTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.types.Plan; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; @@ -17,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class ExternalEventsSchedulingTests extends ProceduralSchedulingSetup { +public class ExternalEventsSchedulingTests extends ProceduralSetup { private GoalInvocationId procedureId; private final static String SOURCE_TYPE = "TestType"; private final static String EVENT_TYPE = "TestType"; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java index 7f8061b653..6031db2e73 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; import gov.nasa.jpl.aerie.e2e.ExternalDatasetsTest; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -14,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ExternalProfilesTests extends ProceduralSchedulingSetup { +public class ExternalProfilesTests extends ProceduralSetup { private GoalInvocationId procedureId; private int datasetId; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java index fdd7780260..731c0528fe 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ModelIntegrationTests.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling; +import gov.nasa.jpl.aerie.e2e.procedural.ProceduralSetup; import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import org.junit.jupiter.api.AfterEach; @@ -14,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ModelIntegrationTests extends ProceduralSchedulingSetup { +public class ModelIntegrationTests extends ProceduralSetup { private int procedureJarId; private GoalInvocationId procedureId; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java index b8077f6670..40470909f7 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java @@ -5,7 +5,8 @@ public record ConstraintError(String message, String stack, Location location ){ record Location(int column, int line){ public static Location fromJSON(JsonObject json){ - return new Location(json.getJsonNumber("column").intValue(), json.getJsonNumber("line").intValue()); + if (!json.containsKey("column") || !json.containsKey("line") || json.isNull("column") || json.isNull("line")) return null; + else return new Location(json.getJsonNumber("column").intValue(), json.getJsonNumber("line").intValue()); } }; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index 9b6f27c4fd..8070d542df 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -1272,6 +1272,32 @@ public ConstraintInvocationId insertPlanConstraint(String name, int planId, Stri ); } + public ConstraintInvocationId insertPlanConstraintJar( + String name, + int planId, + int jarId + ) throws IOException { + final var constraintInsertBuilder = Json.createObjectBuilder() + .add("plan_id", planId) + .add("constraint_metadata", + Json.createObjectBuilder() + .add("data", + Json.createObjectBuilder() + .add("name", name) + .add("versions", + Json.createObjectBuilder() + .add("data", + Json.createObjectBuilder() + .add("type", "JAR") + .add("uploaded_jar_id", jarId))))); + final var variables = Json.createObjectBuilder().add("constraint", constraintInsertBuilder).build(); + final var resp = makeRequest(GQL.INSERT_PLAN_SPEC_CONSTRAINT, variables).getJsonObject("constraint"); + return new ConstraintInvocationId( + resp.getInt("constraint_id"), + resp.getInt("invocation_id") + ); + } + public void updatePlanConstraintSpecVersion(int invocationId, int constraintRevision) throws IOException { final var variables = Json.createObjectBuilder() .add("invocation_id", invocationId) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java index 1549f10367..59e049943b 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java @@ -90,7 +90,8 @@ public static void main(final String[] args) { constraintsDSLCompilationService, constraintService, planController, - simulationController + simulationController, + missionModelController ); final var generateConstraintsLibAction = new GenerateConstraintsLibAction(typescriptCodeGenerationService); final var permissionsService = new PermissionsService( diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java index 15977f4f52..68a3e1736a 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java @@ -75,11 +75,20 @@ public ProceduralConstraintResult run( ReadonlyPlan plan, ReadonlyProceduralSimResults simResults, gov.nasa.jpl.aerie.merlin.driver.SimulationResults merlinResults + ) { + return run(plan, simResults, merlinResults, Thread.currentThread().getContextClassLoader()); + } + + public ProceduralConstraintResult run( + ReadonlyPlan plan, + ReadonlyProceduralSimResults simResults, + gov.nasa.jpl.aerie.merlin.driver.SimulationResults merlinResults, + ClassLoader missionModelClassLoader ) { final ProcedureMapper procedureMapper; try { final var jar = (ConstraintType.JAR) record.type(); - procedureMapper = ProcedureLoader.loadProcedure(jar.path()); + procedureMapper = ProcedureLoader.loadProcedure(jar.path(), missionModelClassLoader); } catch (ProcedureLoader.ProcedureLoadException e) { throw new RuntimeException(e); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java index 8aedde0e49..a1aeb23615 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ProcedureLoader.java @@ -11,11 +11,14 @@ import java.util.jar.JarFile; public final class ProcedureLoader { - public static ProcedureMapper loadProcedure(final Path path) + public static ProcedureMapper loadProcedure(final Path path) throws ProcedureLoadException { + return loadProcedure(path, Thread.currentThread().getContextClassLoader()); + } + + public static ProcedureMapper loadProcedure(final Path path, final ClassLoader parentClassLoader) throws ProcedureLoadException { final var className = getImplementingClassName(path); - final var parentClassLoader = Thread.currentThread().getContextClassLoader(); final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}, parentClassLoader); try { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index e1e90c313c..5151fc8889 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -10,6 +10,7 @@ import gov.nasa.jpl.aerie.merlin.server.models.*; import gov.nasa.jpl.aerie.types.MissionModelId; import org.apache.commons.lang3.tuple.Pair; +import gov.nasa.ammos.aerie.procedural.timeline.util.WithModel; import java.util.*; @@ -18,17 +19,20 @@ public class ConstraintAction { private final ConstraintService constraintService; private final PlanService planService; private final SimulationService simulationService; + private final MissionModelService missionModelService; public ConstraintAction( final ConstraintsDSLCompilationService constraintsDSLCompilationService, final ConstraintService constraintService, final PlanService planService, - final SimulationService simulationService + final SimulationService simulationService, + final MissionModelService missionModelService ) { this.constraintsDSLCompilationService = constraintsDSLCompilationService; this.constraintService = constraintService; this.planService = planService; this.simulationService = simulationService; + this.missionModelService = missionModelService; } /** @@ -160,6 +164,9 @@ public Pair loadAndInstantiateMissionModel(final MissionModelId missionModelId) + @Override + public MissionModel loadAndInstantiateMissionModel(final MissionModelId missionModelId) throws NoSuchMissionModelException, MissionModelLoadException { return loadAndInstantiateMissionModel(missionModelId, untruePlanStart, SerializedValue.of(Map.of())); @@ -418,7 +419,4 @@ private MissionModel loadAndInstantiateMissionModel( } } - public static class MissionModelLoadException extends RuntimeException { - public MissionModelLoadException(final Throwable cause) { super(cause); } - } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java index cb3bda30e5..75ee6fa1c1 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.server.services; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import gov.nasa.jpl.aerie.types.MissionModelId; @@ -71,6 +72,9 @@ SimulationResults runSimulation( final SimulationResourceManager resourceManager ) throws NoSuchMissionModelException, MissionModelService.NoSuchActivityTypeException; + MissionModel loadAndInstantiateMissionModel(final MissionModelId missionModelId) + throws NoSuchMissionModelException, MissionModelLoadException; + void refreshModelParameters(MissionModelId missionModelId) throws NoSuchMissionModelException; void refreshActivityTypes(MissionModelId missionModelId) throws NoSuchMissionModelException; void refreshResourceTypes(MissionModelId missionModelId) throws NoSuchMissionModelException; @@ -115,4 +119,8 @@ record NoSuchMissionModelError(NoSuchMissionModelException ex) implements BulkAr record NoSuchActivityError(NoSuchActivityTypeException ex) implements BulkArgumentValidationResponse { } record InstantiationError(InstantiationException ex) implements BulkArgumentValidationResponse { } } + + class MissionModelLoadException extends RuntimeException { + public MissionModelLoadException(final Throwable cause) { super(cause); } + } } diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java index 391dea2529..273574400c 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.server.mocks; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import gov.nasa.jpl.aerie.types.MissionModelId; import gov.nasa.jpl.aerie.types.Plan; @@ -207,6 +208,16 @@ public SimulationResults runSimulation( return SUCCESSFUL_SIMULATION_RESULTS; } + @Override + public MissionModel loadAndInstantiateMissionModel(final MissionModelId missionModelId) + throws NoSuchMissionModelException, MissionModelLoadException + { + if (!Objects.equals(missionModelId, EXISTENT_MISSION_MODEL_ID)) { + throw new NoSuchMissionModelException(missionModelId); + } + return null; + } + @Override public void refreshModelParameters(final MissionModelId missionModelId) {} From 0aeecf9a9f00b6ba9b344911936bea7a8b234fd9 Mon Sep 17 00:00:00 2001 From: joelco Date: Tue, 29 Jul 2025 15:49:30 -0700 Subject: [PATCH 5/7] Exclude mission model from jar --- e2e-tests/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 07a19d6411..fd3d774d80 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation project(':merlin-sdk') implementation project(':type-utils') implementation project(':contrib') - compileOnly project(':examples:banananation') + implementation project(':examples:banananation') testImplementation "com.zaxxer:HikariCP:5.1.0" testImplementation("org.postgresql:postgresql:42.6.0") @@ -120,6 +120,9 @@ tasks.create("generateProcedureJarTasks") { manifest { attributes 'Main-Class': getMainClassFromGeneratedFile(file) } + dependencies { + exclude(project(":examples:banananation")) + } minimize() } } From 78e9c481bf82bc93806dc6e8a08861246ef303f9 Mon Sep 17 00:00:00 2001 From: joelco Date: Tue, 29 Jul 2025 15:49:36 -0700 Subject: [PATCH 6/7] Add doc comments --- .../jpl/aerie/contrib/models/NamedResource.java | 3 +++ .../framework/resources/NameableResource.java | 4 ++++ .../aerie/procedural/timeline/util/WithModel.kt | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java index dc71b1ef37..ef2ddaec2f 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/models/NamedResource.java @@ -2,6 +2,9 @@ import gov.nasa.jpl.aerie.merlin.framework.resources.NameableResource; +/** + * Provides a field for setting the string name of the resource. + */ public abstract class NamedResource implements NameableResource { private String name = "ERROR: Name was not set during model construction"; diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java index c97392c096..e340d6b8df 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/resources/NameableResource.java @@ -2,6 +2,10 @@ import gov.nasa.jpl.aerie.merlin.framework.Resource; +/** + * A resource that can be given a string name. Used by scheduling and constraints + * to access resources through the mission model rather than with name+deserializer. + */ public interface NameableResource extends Resource { String getName(); void setName(String name); diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt index c0e7480fa9..9385cd1149 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/util/WithModel.kt @@ -1,5 +1,18 @@ package gov.nasa.ammos.aerie.procedural.timeline.util +/** + * A mixin for accessing a singleton instance of the mission model object. + * + * Just make your goal or constraint `implements WithModel`. You can + * then access the mission model with `this.model()`. This interface makes no + * guarantees about the simulation configuration the model was instantiated with. + * It will most likely be the default config, but that is not a formal + * requirement. The only guarantee is that it will be a valid instance of the class. + * + * The type `M` provided to `WithModel` is not checked at compile time. + * Giving the wrong type will result in a runtime class cast exception when + * `model()` is called. + */ interface WithModel { @Suppress("unchecked_cast") fun model(): M { From 98ced54463345dda4dbb55e3c9ca161c714155f5 Mon Sep 17 00:00:00 2001 From: joelco Date: Tue, 29 Jul 2025 15:59:56 -0700 Subject: [PATCH 7/7] Fix raw cast --- .../java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java index 3bdff54c3e..1a7dd74403 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java @@ -26,7 +26,7 @@ public boolean isInitializationComplete() { public void discrete(final String name, final Resource resource, final ValueMapper mapper) { if (resource instanceof NameableResource) { - ((NameableResource) resource).setName(name); + ((NameableResource) resource).setName(name); } this.builder.resource(name, makeResource("discrete", resource, mapper.getValueSchema(), mapper::serializeValue)); @@ -42,7 +42,7 @@ public void realWithMetadata(final String name, final Resource private void real(final String name, final Resource resource, UnaryOperator schemaModifier) { if (resource instanceof NameableResource) { - ((NameableResource) resource).setName(name); + ((NameableResource) resource).setName(name); } this.builder.resource(