diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java index 3f87b46791..22fa976077 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java @@ -61,7 +61,7 @@ public static void main(final String[] args) { final var schedulerService = new CachedSchedulerService(stores.results()); final var scheduleAction = new ScheduleAction(specificationService, schedulerService); - final var generateSchedulingLibAction = new GenerateSchedulingLibAction(merlinService); + final var generateSchedulingLibAction = new GenerateSchedulingLibAction(merlinService, merlinService); //establish bindings to the service layers final var bindings = new SchedulerBindings( diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java index 2e1a9b59ec..25ccb01a0b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java @@ -9,6 +9,7 @@ import gov.nasa.jpl.aerie.json.JsonParseResult; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.json.SchemaCache; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; @@ -17,7 +18,7 @@ public class ActivityTemplateJsonParser implements JsonParser { - private final Map activityTypesByName = new HashMap<>(); + private final Map activityTypesByName = new HashMap<>(); public ActivityTemplateJsonParser(MissionModelService.MissionModelTypes activityTypes){ activityTypes.activityTypes().forEach((actType)-> activityTypesByName.put(actType.name(), actType)); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java index 241872d2c7..b919f4e13e 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java @@ -118,8 +118,8 @@ private void getSchedulingDslTypescript(final Context ctx) { try { final var body = parseJson(ctx.body(), hasuraMissionModelIdActionP); final var missionModelId = body.input().missionModelId(); - - final var response = this.generateSchedulingLibAction.run(missionModelId); + final var planId = body.input().planId(); + final var response = this.generateSchedulingLibAction.run(missionModelId, planId); final String resultString; if (response instanceof GenerateSchedulingLibAction.Response.Success r) { var files = Json.createArrayBuilder(); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java index 5383d1d8bc..cd61ec5acf 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java @@ -3,14 +3,17 @@ import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.scheduler.server.models.HasuraAction; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; +import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleFailure; -import org.apache.commons.lang3.tuple.Pair; import java.util.Optional; -import static gov.nasa.jpl.aerie.json.BasicParsers.*; +import static gov.nasa.jpl.aerie.json.BasicParsers.anyP; +import static gov.nasa.jpl.aerie.json.BasicParsers.longP; +import static gov.nasa.jpl.aerie.json.BasicParsers.productP; +import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; import static gov.nasa.jpl.aerie.json.Uncurry.tuple; import static gov.nasa.jpl.aerie.json.Uncurry.untuple; import static gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresParsers.pgTimestampP; @@ -34,6 +37,11 @@ private SchedulerParsers() {} . map( MissionModelId::new, MissionModelId::id); + public static final JsonParser planIdP + = longP + . map( + PlanId::new, + PlanId::id); public static final JsonParser scheduleFailureP = productP .field("type", stringP) @@ -67,29 +75,34 @@ private SchedulerParsers() {} * @return a parser that accepts hasura action / session details along with specified input type into a tuple * of the form (name,input,session) ready for application of a mapping */ - private static JsonParser, HasuraAction.Session>, String>> hasuraActionP(final JsonParser inputP) { + private static JsonParser> hasuraActionF(final JsonParser inputP) { return productP .field("action", productP.field("name", stringP)) .field("input", inputP) .field("session_variables", hasuraActionSessionP) - .field("request_query", stringP); + .field("request_query", stringP) + .map( + untuple((name, input, session, requestQuery) -> new HasuraAction<>(name, input, session)), + $ -> tuple($.name(), $.input(), $.session(), "")); } /** * parser for a hasura action that accepts a plan id as its sole input, along with normal hasura session details */ public static final JsonParser> hasuraSpecificationActionP - = hasuraActionP(productP.field("specificationId", specificationIdP)) + = hasuraActionF(productP.field("specificationId", specificationIdP) .map( - untuple((name, specificationId, session, requestQuery) -> new HasuraAction<>(name, new HasuraAction.SpecificationInput(specificationId), session)), - action -> tuple(action.name(), action.input().specificationId(), action.session(), "")); + untuple(HasuraAction.SpecificationInput::new), + HasuraAction.SpecificationInput::specificationId)); /** * parser for a hasura action that accepts a mission model id as its sole input, along with normal hasura session details */ public static final JsonParser> hasuraMissionModelIdActionP - = hasuraActionP(productP.field("missionModelId", missionModelIdP)) + = hasuraActionF(productP + .field("missionModelId", missionModelIdP) + .optionalField("planId", planIdP) .map( - untuple((name, missionModelId, session, requestQuery) -> new HasuraAction<>(name, new HasuraAction.MissionModelIdInput(missionModelId), session)), - action -> tuple(action.name(), action.input().missionModelId(), action.session(), "")); + untuple(HasuraAction.MissionModelIdInput::new), + input -> tuple(input.missionModelId(), input.planId()))); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java new file mode 100644 index 0000000000..5bd8d78859 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; + +import java.util.Map; + +public record ActivityType(String name, Map parameters, Map> presets) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java new file mode 100644 index 0000000000..0edfddf829 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; + +import java.util.Collection; +import java.util.Map; + +public record ExternalProfiles( + Map realProfiles, + Map discreteProfiles, + Collection resourceTypes) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java index ada8556d80..8c111b7e64 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.models; +import java.util.Optional; + public record HasuraAction(String name, I input, Session session) { public record Session(String hasuraRole, String hasuraUserId) { } @@ -7,5 +9,5 @@ public record Session(String hasuraRole, String hasuraUserId) { } public sealed interface Input permits SpecificationInput, MissionModelIdInput { } public record SpecificationInput(SpecificationId specificationId) implements Input { } - public record MissionModelIdInput(MissionModelId missionModelId) implements Input { } + public record MissionModelIdInput(MissionModelId missionModelId, Optional planId) implements Input { } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java new file mode 100644 index 0000000000..246de7afbf --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java @@ -0,0 +1,5 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; + +public record ResourceType(String name, ValueSchema schema) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java new file mode 100644 index 0000000000..3a5c5deffb --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java @@ -0,0 +1,15 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment; +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; +import java.util.Map; + +public record UnwrappedProfileSet( + Map>>> realProfiles, + Map>>> discreteProfiles +){} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java index 0684bfedd1..06cb4fd5ad 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java @@ -5,15 +5,20 @@ import java.io.InputStreamReader; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; + import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchMissionModelException; +import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; +import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; public record GenerateSchedulingLibAction( - MissionModelService missionModelService + MissionModelService missionModelService, + PlanService.ReaderRole planService ) { public GenerateSchedulingLibAction { - Objects.requireNonNull(missionModelService); + Objects.requireNonNull(planService); } /** @@ -25,12 +30,14 @@ record Success(Map files) implements Response {} } /** - * execute the scheduling operation on the target plan (or retrieve existing scheduling results) + * generates the scheduling typescript files * * @param missionModelId the id of the mission model for which to generate a scheduling library + * @param planId the optional id of the plan concerned by this code generation, if plan id is provided, code will be generated for external resources + * associated with the plan * @return a response object wrapping the results of generating the code (either successful or not) */ - public Response run(final MissionModelId missionModelId) { + public Response run(final MissionModelId missionModelId, final Optional planId) { try { final var schedulingDsl = getTypescriptResource("scheduler-edsl-fluent-api.ts"); final var schedulerAst = getTypescriptResource("scheduler-ast.ts"); @@ -38,7 +45,12 @@ public Response run(final MissionModelId missionModelId) { final var windowsAst = getTypescriptResource("constraints/constraints-ast.ts"); final var temporalPolyfillTypes = getTypescriptResource("constraints/TemporalPolyfillTypes.ts"); - final var missionModelTypes = missionModelService.getMissionModelTypes(missionModelId); + + var missionModelTypes = missionModelService.getMissionModelTypes(missionModelId); + if(planId.isPresent()) { + final var allResourceTypes = planService.getResourceTypes(planId.get()); + missionModelTypes = new MissionModelService.MissionModelTypes(missionModelTypes.activityTypes(), allResourceTypes); + } final var generatedSchedulerCode = TypescriptCodeGenerationService.generateTypescriptTypesFromMissionModel(missionModelTypes); final var generatedConstraintsCode = gov.nasa.jpl.aerie.constraints.TypescriptCodeGenerationService @@ -54,7 +66,8 @@ public Response run(final MissionModelId missionModelId) { "file:///mission-model-generated-code.ts", generatedConstraintsCode, "file:///%s".formatted(temporalPolyfillTypes.basename), temporalPolyfillTypes.source )); - } catch (final NoSuchMissionModelException | IOException | MissionModelService.MissionModelServiceException e) { + } catch (final IOException | MissionModelService.MissionModelServiceException | PlanServiceException | + NoSuchPlanException | NoSuchMissionModelException e) { return new Response.Failure(e.getMessage()); } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java index b8438142dc..105ca752bc 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.services; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; import gov.nasa.jpl.aerie.json.BasicParsers; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; @@ -28,13 +30,17 @@ import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.ActivityAttributesRecord; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; import gov.nasa.jpl.aerie.scheduler.server.models.ProfileSet; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; +import gov.nasa.jpl.aerie.scheduler.server.models.UnwrappedProfileSet; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; @@ -67,7 +73,6 @@ import java.util.stream.Collectors; import static gov.nasa.jpl.aerie.json.BasicParsers.chooseP; -import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; import static gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser.valueSchemaP; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND; @@ -720,8 +725,35 @@ public Collection getResourceTypes(final MissionModelId missionMod return resourceTypes; } + /** + * Gets resource types associated to a plan, those coming from the mission model as well as those coming from external dataset resources + * @param planId the plan id + * @return + * @throws IOException + * @throws MissionModelServiceException + * @throws PlanServiceException + * @throws NoSuchPlanException + */ + @Override + public Collection getResourceTypes(final PlanId planId) + throws IOException, MissionModelServiceException, PlanServiceException, NoSuchPlanException + { + final var missionModelId = this.getPlanMetadata(planId).modelId(); + final var missionModelResourceTypes = getResourceTypes(new MissionModelId(missionModelId)); + final var allResourceTypes = new ArrayList<>(missionModelResourceTypes); + final var associatedDataset = getExternalDatasets(planId); + if(associatedDataset.isPresent()) { + for(final var datasetMetada: associatedDataset.get()) { + final var profileSet = getProfileTypes(datasetMetada.datasetId()); + allResourceTypes.addAll(extractResourceTypes(profileSet)); + } + } + return allResourceTypes; + } -public SimulationId getSimulationId(PlanId planId) throws PlanServiceException, IOException { + + + public SimulationId getSimulationId(PlanId planId) throws PlanServiceException, IOException { final var request = """ query { simulation(where: {plan_id: {_eq: %d}}) { @@ -803,7 +835,22 @@ private Map getSimulatedActivities(Simul return parseSimulatedActivities(data, startSimulation); } -private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, IOException { + private ProfileSet getProfileTypes(DatasetId datasetId) throws PlanServiceException, IOException { + final var request = """ + query{ + profile(where: {dataset_id: {_eq: %d}}){ + type + name + } + } + """.formatted(datasetId.id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("profile"); + return parseProfiles(data); + } + +private ProfileSet getProfilesWithSegments(DatasetId datasetId) throws PlanServiceException, IOException { final var request = """ query{ profile(where: {dataset_id: {_eq: %d}}){ @@ -812,6 +859,7 @@ private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, I profile_segments { start_offset dynamics + is_gap } name } @@ -854,18 +902,20 @@ public Optional getSimulationResults(PlanMetadata planMetadat Future> futureSpans = executorService.submit(() -> getSpans( simulationDatasetId.get().datasetId(), planMetadata.horizon().getStartInstant())); - Future futureProfiles = executorService.submit(() -> getProfiles(simulationDatasetId.get().datasetId())); + Future futureProfiles = executorService.submit(() -> getProfilesWithSegments(simulationDatasetId.get().datasetId())); try { final var simulatedActivities = futureSimulatedActivities.get(); final var unfinishedActivities = futureSpans.get(); final var profiles = futureProfiles.get(); + //verify that there is no gap and convert + final var continuousProfiles = verifyAndConvert(profiles); final var simulationStartTime = planMetadata.horizon().getStartInstant(); final var simulationEndTime = planMetadata.horizon().getEndInstant(); final var micros = java.time.Duration.between(simulationStartTime, simulationEndTime).toNanos() / 1000; final var duration = Duration.of(micros, MICROSECOND); return Optional.of(new SimulationResults( - profiles.realProfiles, - profiles.discreteProfiles, + continuousProfiles.realProfiles(), + continuousProfiles.discreteProfiles(), simulatedActivities, unfinishedActivities, simulationStartTime, @@ -879,6 +929,75 @@ public Optional getSimulationResults(PlanMetadata planMetadat } } + public record DatasetMetadata(DatasetId datasetId, Duration offsetFromPlanStart){}; + + public Optional> getExternalDatasets(final PlanId planId) + throws PlanServiceException, IOException + { + final var datasets = new ArrayList(); + final var request = """ + query { + plan_dataset(where: {plan_id: {_eq: %d}, simulation_dataset_id: {_is_null: true}}) { + dataset_id + offset_from_plan_start + } + } + """.formatted(planId.id()); + final var response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("plan_dataset"); + if (data.size() == 0) { + return Optional.empty(); + } + for(final var dataset:data){ + final var datasetId = new DatasetId(dataset.asJsonObject().getInt("dataset_id")); + final var offsetFromPlanStart = durationFromPGInterval(dataset + .asJsonObject() + .getString("offset_from_plan_start")); + datasets.add(new DatasetMetadata(datasetId, offsetFromPlanStart)); + } + return Optional.of(datasets); + } + + @Override + public ExternalProfiles getExternalProfiles(final PlanId planId) + throws PlanServiceException, IOException + { + final Map realProfiles = new HashMap<>(); + final Map discreteProfiles = new HashMap<>(); + final var resourceTypes = new ArrayList(); + final var datasetMetadatas = getExternalDatasets(planId); + if(datasetMetadatas.isPresent()) { + for(final var datasetMetadata: datasetMetadatas.get()) { + final var profiles = getProfilesWithSegments(datasetMetadata.datasetId()); + profiles.realProfiles().forEach((name, profile) -> { + realProfiles.put(name, + LinearProfile.fromExternalProfile( + datasetMetadata.offsetFromPlanStart, + profile.getRight())); + }); + profiles.discreteProfiles().forEach((name, profile) -> { + discreteProfiles.put(name, + DiscreteProfile.fromExternalProfile( + datasetMetadata.offsetFromPlanStart, + profile.getRight())); + }); + resourceTypes.addAll(extractResourceTypes(profiles)); + } + } + return new ExternalProfiles(realProfiles, discreteProfiles, resourceTypes); +} + + private Collection extractResourceTypes(final ProfileSet profileSet){ + final var resourceTypes = new ArrayList(); + profileSet.realProfiles().forEach((name, profile) -> { + resourceTypes.add(new ResourceType(name, profile.getLeft())); + }); + profileSet.discreteProfiles().forEach((name, profile) -> { + resourceTypes.add(new ResourceType(name, profile.getLeft())); + }); + return resourceTypes; + } + private Map parseUnfinishedActivities(JsonArray unfinishedActivitiesJson, Instant simulationStart){ final var unfinishedActivities = new HashMap(); for(final var unfinishedActivityJson: unfinishedActivitiesJson){ @@ -908,15 +1027,32 @@ private Map parseUnfinishedActivities(J return unfinishedActivities; } - private record Profiles( - Map>>> realProfiles, - Map>>> discreteProfiles - ){} + private UnwrappedProfileSet verifyAndConvert(final ProfileSet profileSet) throws PlanServiceException { + return new UnwrappedProfileSet(verifyAndUnwrapProfiles(profileSet.realProfiles()), verifyAndUnwrapProfiles(profileSet.discreteProfiles())); + } - private Profiles parseProfiles(JsonArray dataset){ - Map>>> realProfiles = new HashMap<>(); - Map>>> discreteProfiles = new HashMap<>(); - for(final var profile:dataset){ + private HashMap>>> verifyAndUnwrapProfiles(Map>>>> profiles) + throws PlanServiceException + { + final var unwrapped = new HashMap>>>(); + for(final var profile: profiles.entrySet()) { + final var unwrappedSegments = new ArrayList>(); + for (final var segment : profile.getValue().getRight()) { + if (segment.dynamics().isEmpty()) { + throw new PlanServiceException("Exception when parsing profiles, profile " + profile.getKey() + " has gaps and cannot be put into simulation results."); + } else { + unwrappedSegments.add(new ProfileSegment<>(segment.extent(), segment.dynamics().get())); + } + } + unwrapped.put(profile.getKey(), Pair.of(profile.getValue().getLeft(), unwrappedSegments)); + } + return unwrapped; + } + + private ProfileSet parseProfiles(JsonArray dataset){ + Map>>>> realProfiles = new HashMap<>(); + Map>>>> discreteProfiles = new HashMap<>(); + for(final var profile :dataset){ final var name = profile.asJsonObject().getString("name"); final var type = profile.asJsonObject().getJsonObject("type"); final var typetype = type.getString("type"); @@ -929,33 +1065,50 @@ private Profiles parseProfiles(JsonArray dataset){ discreteProfiles.put(name, discreteProfile); } } - return new Profiles(realProfiles, discreteProfiles); + return new ProfileSet(realProfiles, discreteProfiles); } - public Pair>> parseProfile(JsonObject profile, JsonParser dynamicsParser){ + public Pair>>> parseProfile(JsonObject profile, JsonParser dynamicsParser){ // Profile segments are stored with their start offset relative to simulation start // We must convert these to durations describing how long each segment lasts - final var profileExtent = durationFromPGInterval(profile.asJsonObject().getString("duration")); final var type = chooseP(discreteValueSchemaTypeP, realValueSchemaTypeP).parse(profile.getJsonObject("type")).getSuccessOrThrow(); - final var resultSet = profile.getJsonArray("profile_segments").iterator(); - JsonValue curProfileSegment = null; - final var segments = new ArrayList>(); - if (resultSet.hasNext()) { - curProfileSegment = resultSet.next(); - var offset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); - var dynamics = dynamicsParser.parse(curProfileSegment.asJsonObject().get("dynamics")).getSuccessOrThrow(); - - while (resultSet.hasNext()) { + final var segments = new ArrayList>>(); + if(profile.containsKey("profile_segments")) { + final var resultSet = profile.getJsonArray("profile_segments").iterator(); + JsonValue curProfileSegment = null; + if (resultSet.hasNext()) { + final var profileExtent = durationFromPGInterval(profile.asJsonObject().getString("duration")); curProfileSegment = resultSet.next(); - final var nextOffset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); - final var duration = nextOffset.minus(offset); + var offset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); + var isGap = curProfileSegment.asJsonObject().getBoolean("is_gap"); + Optional dynamics; + if (!isGap) { + dynamics = Optional.of(dynamicsParser + .parse(curProfileSegment.asJsonObject().get("dynamics")) + .getSuccessOrThrow()); + } else { + dynamics = Optional.empty(); + } + + while (resultSet.hasNext()) { + curProfileSegment = resultSet.next(); + final var nextOffset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); + final var duration = nextOffset.minus(offset); + segments.add(new ProfileSegment<>(duration, dynamics)); + isGap = curProfileSegment.asJsonObject().getBoolean("is_gap"); + offset = nextOffset; + if (!isGap) { + dynamics = Optional.of(dynamicsParser + .parse(curProfileSegment.asJsonObject().get("dynamics")) + .getSuccessOrThrow()); + } else { + dynamics = Optional.empty(); + } + } + + final var duration = profileExtent.minus(offset); segments.add(new ProfileSegment<>(duration, dynamics)); - offset = nextOffset; - dynamics = dynamicsParser.parse(curProfileSegment.asJsonObject().getJsonObject("dynamics")).getSuccessOrThrow(); } - - final var duration = profileExtent.minus(offset); - segments.add(new ProfileSegment(duration, dynamics)); } return Pair.of(type, segments); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java index 9a36a3a209..330067b283 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java @@ -1,14 +1,13 @@ package gov.nasa.jpl.aerie.scheduler.server.services; -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchMissionModelException; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import java.io.IOException; import java.util.Collection; -import java.util.Map; public interface MissionModelService { MissionModelTypes getMissionModelTypes(final PlanId planId) @@ -27,9 +26,5 @@ public MissionModelServiceException(final String message, final Throwable cause) } } - record ActivityType(String name, Map parameters, Map> presets) {} - - record ResourceType(String name, ValueSchema schema) {} - record MissionModelTypes(Collection activityTypes, Collection resourceTypes) {} } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java index b3d7ce07e1..e02a9ce165 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java @@ -12,14 +12,17 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; import java.time.Instant; +import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -73,6 +76,27 @@ void ensurePlanExists(final PlanId planId) * @return simulation results, optionally */ Optional getSimulationResults(PlanMetadata planMetadata) throws PlanServiceException, IOException, InvalidJsonException; + + + /** + * Gets external profiles associated to a plan, including segments + * @param planId the plan id + * @throws PlanServiceException + * @throws IOException + */ + ExternalProfiles getExternalProfiles(final PlanId planId) + throws PlanServiceException, IOException; + + /** + * Gets resource types associated to a plan, those coming from the mission model as well as those coming from external dataset resources + * @param planId the plan id + * @throws IOException + * @throws MissionModelService.MissionModelServiceException + * @throws PlanServiceException + * @throws NoSuchPlanException + */ + Collection getResourceTypes(final PlanId planId) + throws IOException, MissionModelService.MissionModelServiceException, PlanServiceException, NoSuchPlanException; } interface WriterRole { diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java index ef816b1951..f27f80ed24 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java @@ -8,6 +8,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import org.apache.commons.lang3.tuple.Pair; import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; @@ -49,7 +51,7 @@ public static String generateTypescriptTypesFromMissionModel(final MissionModelS return joinLines(result); } - private static String generateResourceTypes(final Collection resourceTypes) { + private static String generateResourceTypes(final Collection resourceTypes) { final var result = new ArrayList(); result.add("export enum Resource {"); for (final var resourceType : resourceTypes) { @@ -216,11 +218,11 @@ static String toString(final TypescriptType type, boolean topLevelStructIsProfil } } - private static ActivityTypeCode getActivityTypeInformation(final MissionModelService.ActivityType activityType) { + private static ActivityTypeCode getActivityTypeInformation(final ActivityType activityType) { return new ActivityTypeCode(activityType.name(), generateActivityParameterTypes(activityType), activityType.presets()); } - private static List generateActivityParameterTypes(final MissionModelService.ActivityType activityType) { + private static List generateActivityParameterTypes(final ActivityType activityType) { return activityType .parameters() .entrySet() diff --git a/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java b/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java index 659b209212..50689f1136 100644 --- a/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java +++ b/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java @@ -5,13 +5,15 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; public final class TypescriptCodeGenerationServiceTestFixtures { public static final MissionModelService.MissionModelTypes MISSION_MODEL_TYPES = new MissionModelService.MissionModelTypes( List.of( - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity1", Map.of( "variant", @@ -35,7 +37,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { ), Map.of() ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity2", Map.of( "quantity", @@ -46,7 +48,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { Map.of("quantity", SerializedValue.of(5)) ) ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity3", Map.of( "variant", @@ -60,15 +62,15 @@ public final class TypescriptCodeGenerationServiceTestFixtures { Map.of("variant", SerializedValue.of("option1")) ) ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivityEmpty", Map.of(), Map.of() ) ), List.of( - new MissionModelService.ResourceType("/sample/resource/1", ValueSchema.REAL), - new MissionModelService.ResourceType("/sample/resource/3", ValueSchema.ofVariant(List.of( + new ResourceType("/sample/resource/1", ValueSchema.REAL), + new ResourceType("/sample/resource/3", ValueSchema.ofVariant(List.of( new ValueSchema.Variant( "option1", "option1" ), @@ -76,7 +78,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { "option2", "option2" ) ))), - new MissionModelService.ResourceType("/sample/resource/2", ValueSchema.ofStruct( + new ResourceType("/sample/resource/2", ValueSchema.ofStruct( Map.of( "field1", ValueSchema.BOOLEAN, "field2", ValueSchema.ofVariant(List.of( diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java index a59af3868f..db877e6ee7 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java @@ -1,26 +1,27 @@ package gov.nasa.jpl.aerie.scheduler.worker.services; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.stream.JsonParsingException; -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.List; -import java.util.Objects; -import static gov.nasa.jpl.aerie.constraints.json.ConstraintParsers.windowsExpressionP; -import gov.nasa.jpl.aerie.constraints.time.Windows; -import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.ConstraintsTypescriptCodeGenerationHelper; import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; import gov.nasa.jpl.aerie.scheduler.server.services.TypescriptCodeGenerationService; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.stream.JsonParsingException; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + public class SchedulingDSLCompilationService { private final Process nodeProcess; @@ -48,23 +49,41 @@ public void close() { this.nodeProcess.destroy(); } - public SchedulingDSLCompilationResult compileGlobalSchedulingCondition(final MissionModelService missionModelService, final PlanId planId, final String conditionTypescript) { + public SchedulingDSLCompilationResult compileGlobalSchedulingCondition(final MissionModelService missionModelService, final PlanId planId, final String conditionTypescript, final + Collection additionalResourceTypes) { try{ final var missionModelTypes = missionModelService.getMissionModelTypes(planId); - return compile(missionModelTypes, conditionTypescript, SchedulingDSL.conditionSpecifierP, "GlobalSchedulingCondition"); + final var aggregatedResourceTypes = new ArrayList<>(missionModelTypes.resourceTypes()); + aggregatedResourceTypes.addAll(additionalResourceTypes); + final var planTypes = new MissionModelService.MissionModelTypes(missionModelTypes.activityTypes(), aggregatedResourceTypes); + return compile(planTypes, conditionTypescript, SchedulingDSL.conditionSpecifierP, "GlobalSchedulingCondition"); } catch (IOException | MissionModelService.MissionModelServiceException e) { throw new Error(e); } } + public SchedulingDSLCompilationResult compileSchedulingGoalDSL( + final MissionModelService missionModelService, + final PlanId planId, + final String goalTypescript){ + return compileSchedulingGoalDSL(missionModelService, planId, goalTypescript, List.of()); + } + /** * NOTE: This method is not re-entrant (assumes only one call to this method is running at any given time) */ - public SchedulingDSLCompilationResult compileSchedulingGoalDSL(final MissionModelService missionModelService, final PlanId planId, final String goalTypescript) + public SchedulingDSLCompilationResult compileSchedulingGoalDSL( + final MissionModelService missionModelService, + final PlanId planId, + final String goalTypescript, + final Collection additionalResourceTypes) { try { final var missionModelTypes = missionModelService.getMissionModelTypes(planId); - return compile(missionModelTypes, goalTypescript, SchedulingDSL.schedulingJsonP(missionModelTypes), "Goal"); + final var aggregatedResourceTypes = new ArrayList<>(missionModelTypes.resourceTypes()); + aggregatedResourceTypes.addAll(additionalResourceTypes); + final var augmentedMissionModelTypes = new MissionModelService.MissionModelTypes(missionModelTypes.activityTypes(), aggregatedResourceTypes); + return compile(augmentedMissionModelTypes, goalTypescript, SchedulingDSL.schedulingJsonP(augmentedMissionModelTypes), "Goal"); } catch (IOException | MissionModelService.MissionModelServiceException e) { throw new Error(e); } 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 d5f84df40d..4f69996dd7 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 @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,12 +47,14 @@ import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.http.ResponseSerializers; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; @@ -130,11 +133,12 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer simulationFacade, schedulerMissionModel.schedulerModel() ); + final var externalProfiles = loadExternalProfiles(planMetadata.planId()); final var initialSimulationResults = loadSimulationResults(planMetadata); //seed the problem with the initial plan contents final var loadedPlanComponents = loadInitialPlan(planMetadata, problem); problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResults); - + problem.setExternalProfile(externalProfiles.realProfiles(), externalProfiles.discreteProfiles()); //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); final var failedGlobalSchedulingConditions = new ArrayList>(); @@ -143,7 +147,8 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer final var result = schedulingDSLCompilationService.compileGlobalSchedulingCondition( missionModelService, planMetadata.planId(), - $.source().source()); + $.source().source(), + externalProfiles.resourceTypes()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { compiledGlobalSchedulingConditions.addAll(conditionBuilder(r.value(), problem)); } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { @@ -177,7 +182,8 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer missionModelService, planMetadata.planId(), goalRecord.definition(), - schedulingDSLCompilationService); + schedulingDSLCompilationService, + externalProfiles.resourceTypes()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { compiledGoals.add(Pair.of(goalRecord, r.value())); } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { @@ -279,6 +285,12 @@ private Optional loadSimulationResults(final PlanMetadata pla } } + private ExternalProfiles loadExternalProfiles(final PlanId planId) + throws PlanServiceException, IOException + { + return planService.getExternalProfiles(planId); + } + private Optional storeSimulationResults(PlanningHorizon planningHorizon, SimulationFacade simulationFacade, PlanMetadata planMetadata, final Map schedDirectiveToMerlinId) throws PlanServiceException, IOException @@ -313,12 +325,14 @@ private static SchedulingDSLCompilationService.SchedulingDSLCompilationResult additionalResourceTypes) { return schedulingDSLCompilationService.compileSchedulingGoalDSL( missionModelService, planId, - goalDefinition.source() + goalDefinition.source(), + additionalResourceTypes ); } diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java index 597751b875..9b059ddda1 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java @@ -12,30 +12,35 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; -import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; import gov.nasa.jpl.aerie.scheduler.server.services.PlanService; -import gov.nasa.jpl.aerie.scheduler.server.services.PlanServiceException; import org.apache.commons.lang3.tuple.Pair; -import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; class MockMerlinService implements MissionModelService, PlanService.OwnerRole { private Optional planningHorizon; + private ExternalProfiles externalProfiles = new ExternalProfiles(Map.of(), Map.of(), List.of()); + + public void setExternalDataset(ExternalProfiles externalProfiles) { + this.externalProfiles = externalProfiles; + } record MissionModelInfo(Path libPath, Path modelPath, String modelName, MissionModelTypes types, Map config) {} @@ -141,6 +146,17 @@ public Optional getSimulationResults(final PlanMetadata planM return Optional.empty(); } + @Override + public ExternalProfiles getExternalProfiles(final PlanId planId) { + return externalProfiles; + } + + @Override + public Collection getResourceTypes(final PlanId planId) + { + return null; + } + @Override public void clearPlanActivityDirectives(final PlanId planId) { diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java index 8ac852ed8b..97c795545b 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java @@ -84,7 +84,8 @@ void testSchedulingDSL_mutex() export default function myCondition() { return GlobalSchedulingCondition.mutex([ActivityTypes.SampleActivity2], [ActivityTypes.SampleActivity1]) } - """); + """, + List.of()); final var expectedGoalDefinition = new SchedulingDSL.ConditionSpecifier.AndCondition(List.of( new SchedulingDSL.ConditionSpecifier.GlobalSchedulingCondition( new Not( @@ -820,7 +821,8 @@ void testWindowsExpression() { export default function() { return GlobalSchedulingCondition.scheduleOnlyWhen([], Real.Resource("/sample/resource/1").lessThan(5.0)); } - """); + """, + List.of()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { assertEquals( new SchedulingDSL.ConditionSpecifier.GlobalSchedulingCondition( diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 4f82be4c57..5f905b758e 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -12,16 +12,23 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static org.junit.jupiter.api.Assertions.*; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; import gov.nasa.jpl.aerie.constraints.time.Interval; +import gov.nasa.jpl.aerie.constraints.time.Segment; +import gov.nasa.jpl.aerie.constraints.tree.DiscreteResource; import gov.nasa.jpl.aerie.foomissionmodel.mappers.FooValueMappers; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; @@ -31,16 +38,20 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.TimeUtility; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.server.config.PlanOutputMode; import gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; @@ -1467,7 +1478,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, List.of(), planningHorizon); + return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty()); } private SchedulingRunResults runScheduler( @@ -1477,23 +1488,33 @@ private SchedulingRunResults runScheduler( final PlanningHorizon planningHorizon ) { - return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon); + return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty()); } - private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, final List globalSchedulingConditions, final PlanningHorizon planningHorizon + ){ + return runScheduler(desc, plannedActivities, goals, globalSchedulingConditions, planningHorizon, Optional.empty()); + } + + private SchedulingRunResults runScheduler( + final MissionModelDescription desc, + final List plannedActivities, + final Iterable goals, + final List globalSchedulingConditions, + final PlanningHorizon planningHorizon, + final Optional externalProfiles ){ final var activities = new HashMap(); long id = 1; for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon); + return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles); } private SchedulingRunResults runScheduler( @@ -1501,12 +1522,16 @@ private SchedulingRunResults runScheduler( final Map plannedActivities, final Iterable goals, final List globalSchedulingConditions, - final PlanningHorizon planningHorizon + final PlanningHorizon planningHorizon, + final Optional externalProfiles ) { final var mockMerlinService = new MockMerlinService(); mockMerlinService.setMissionModel(getMissionModelInfo(desc)); mockMerlinService.setInitialPlan(plannedActivities); mockMerlinService.setPlanningHorizon(planningHorizon); + if(externalProfiles.isPresent()) { + mockMerlinService.setExternalDataset(externalProfiles.get()); + } final var planId = new PlanId(1L); final var goalsByPriority = new ArrayList(); @@ -1557,11 +1582,11 @@ static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( "", ""); final Map> taskSpecTypes = missionModel.getDirectiveTypes().directiveTypes(); - final var activityTypes = new ArrayList(); + final var activityTypes = new ArrayList(); for (final var entry : taskSpecTypes.entrySet()) { final var activityTypeName = entry.getKey(); final var taskSpecType = entry.getValue(); - activityTypes.add(new MissionModelService.ActivityType( + activityTypes.add(new gov.nasa.jpl.aerie.scheduler.server.models.ActivityType( activityTypeName, taskSpecType .getInputType() @@ -1572,11 +1597,11 @@ static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( )); } - final var resourceTypes = new ArrayList(); + final var resourceTypes = new ArrayList(); for (final var entry : missionModel.getResources().entrySet()) { final var name = entry.getKey(); final var resource = entry.getValue(); - resourceTypes.add(new MissionModelService.ResourceType(name, resource.getOutputType().getSchema())); + resourceTypes.add(new ResourceType(name, resource.getOutputType().getSchema())); } return new MissionModelService.MissionModelTypes(activityTypes, resourceTypes);