Skip to content

Add upload and download simulation dataset feature - #1852

Open
remy-rabideau wants to merge 44 commits into
NASA-AMMOS:developfrom
remy-rabideau:feature/upload-simulation-dataset
Open

Add upload and download simulation dataset feature#1852
remy-rabideau wants to merge 44 commits into
NASA-AMMOS:developfrom
remy-rabideau:feature/upload-simulation-dataset

Conversation

@remy-rabideau

@remy-rabideau remy-rabideau commented Jul 14, 2026

Copy link
Copy Markdown
  • Review: By commit
  • Merge strategy: Merge (no squash)

REQUIRES_GATEWAY_PR="148"
NASA-AMMOS/plandev-gateway#148

Description

Adds a matched pair of Hasura actions for moving simulation datasets in and out of PlanDev:

  • uploadSimulationDataset (mutation) — accepts a simulation results JSON payload in SimulationResultsWriter format and persists it to the database against an existing plan, returning the new simulationDatasetId.
  • downloadSimulationDataset (query) — reads an existing simulation dataset back out and returns it in that same JSON format.

The two are deliberately symmetric: the format downloadSimulationDataset emits is the format uploadSimulationDataset accepts, so a dataset can be downloaded from one plan and re-uploaded to another with round-trip fidelity. Two serializer tests (testSerializeEmptyResultsRoundTrips, testSerializeFullResultsRoundTrips) and one e2e test (validRoundTrip) pin that property.

The primary use case for upload is transferring a simulation dataset generated through procedural stateless scheduling into the PlanDev database for viewing in the UI; ingesting results from external simulators is also supported. Download covers the reverse direction — exporting results for offline analysis, archival, or transfer between environments. The SimulationResultsWriter format is also produced by aerie-cli via plans download-simulation-full-results.

Pipeline (upload): Hasura receives the mutation → forwards the action envelope to merlin-server → SimulationResultsParser parses the JSON → PostgresPlanRepository.uploadSimulationDataset validates activity types, then writes the dataset, profiles, segments, activity spans, topics, and events in a single transaction → returns the new simulationDatasetId.

Pipeline (download): Hasura receives the query → merlin-server loads the dataset record, profiles, spans, topics, events, and simulation arguments → ResponseSerializers.serializeSimulationResultsForDownload emits the SimulationResultsWriter JSON shape.

Reviewer notes

Three things worth attention:

  1. Permissions differ between the two actions, intentionally. uploadSimulationDataset is gated on simulate, because uploading produces a simulation dataset — the same artifact a simulation run produces — rather than an external dataset. downloadSimulationDataset is gated on resource_samples, the permission that already governs reading simulated resource data; the viewer role holds it as NO_CHECK, so viewers can export results from plans they do not own. The e2e tests forbidden and viewerMayDownload cover both sides of this.
  2. The Javalin max request size was raised to 256 MB globally, not scoped to the upload route.
  3. SimulationResults gained a simulationArguments field. The existing 8-argument constructor is retained and delegates with Map.of(), so no existing call site changes.

Changes:

  • deployment/hasura/metadata/actions.graphql — adds the uploadSimulationDataset mutation and downloadSimulationDataset query, their UploadSimulationDatasetResponse / DownloadSimulationDatasetResponse types, and the SimulationResultsInput / SimulationResultsOutput scalars
  • deployment/hasura/metadata/actions.yaml — registers both action handlers against merlin-server with a 300 s timeout; upload permits aerie_admin and user, download additionally permits viewer
  • merlin-server/.../SimulationResultsParser.java (new) — parses the SimulationResultsWriter JSON format: DOY timestamps, real/discrete profiles with named segments, simulated and unfinished activities, simulation arguments, topics, and events. Parse-only — the unparse direction throws UnsupportedOperationException pointing at ResponseSerializers.serializeSimulationResultsForDownload, which is the real serializer
  • merlin-server/.../ResponseSerializers.java — adds serializeSimulationResultsForDownload (profiles, spans, simulation arguments, topics, and events, with event graphs flattened via EventGraphFlattener) and serializeCreatedSimulationDatasetId
  • merlin-server/.../MerlinBindings.java — registers POST /uploadSimulationDataset and POST /downloadSimulationDataset; parses the Hasura action envelope, applies the permission checks described above, and maps failures to status codes
  • merlin-server/.../HasuraParsers.java — adds hasuraUploadSimulationDatasetActionP and hasuraDownloadSimulationDatasetActionP
  • merlin-server/.../HasuraAction.java — adds the UploadSimulationDatasetInput and DownloadSimulationDatasetInput records
  • merlin-server/.../InvalidSimulationDatasetException.java (new) — carries the set of activity types present in an uploaded dataset but absent from the plan's mission model
  • merlin-server/.../MerlinFormattedError.java — maps that exception to an INVALID_SIMULATION_DATASET formatted error
  • merlin-server/.../PlanService.java / LocalPlanService.java — adds uploadSimulationDataset and downloadSimulationDataset to the service interface and implementation
  • merlin-server/.../PlanRepository.java / PostgresPlanRepository.java / InMemoryPlanRepository.java — adds both methods to the repository interface. The PostgreSQL implementation writes the full dataset in a single transaction on upload (validating activity types against the mission model first) and reassembles a SimulationResults on download; the in-memory mock throws UnsupportedOperationException for both
  • merlin-server/.../PostgresResultsCellRepository.java — widens getSimulationTopics, getSimulationEvents, getActivities, insertSimulationTopics, insertSimulationEvents, and postActivities from private to package-private so PostgresPlanRepository can reuse them instead of duplicating the queries
  • merlin-driver/.../SimulationResults.java — adds the simulationArguments field and a 9-argument constructor; the existing 8-argument constructor delegates with an empty map
  • merlin-server/.../AerieAppDriver.java — raises the Javalin max request size to 64 GB per request by Clipper

Verification

Automated coverage added:

  • UploadSimulationDatasetParserTest (9 tests) — real profiles, activities, simulation arguments, topics and events, invalid timestamps, missing fields, and absence defaults
  • DownloadSimulationDatasetParserTest (7 tests) — valid envelopes, missing planId / simulationDatasetId, non-numeric and beyond-int-range dataset ids, and missing session variables
  • DownloadSimulationDatasetSerializerTest (10 tests) — real and discrete profiles, simulated and unfinished activities, simulation arguments, topics and events, concurrent events within one transaction, plus the two round-trip tests
  • UploadSimulationDatasetTests (e2e, 8 tests) — successful upload with empty, discrete, real, and mixed profiles; invalid plan id (404); missing required field (400); unauthorized user (403); unknown activity types rejected (400)
  • DownloadSimulationDatasetTests (e2e, 7 tests) — invalid plan id, missing and nonexistent dataset id, forbidden role, empty profiles, viewer access, and a full upload → download round trip

Documentation

No existing documentation covers these endpoints. The SimulationResultsWriter format they read and write is defined in the aerie-cli repository (see NASA-AMMOS/aerie-cli#187).

Future work

  • Return 404 rather than 500 for a nonexistent simulationDatasetId on download — the lookup currently throws a bare RuntimeException that falls through to the catch-all handler (asserted as-is in nonexistentSimulationDatasetId)
  • Read simulation arguments through a typed repository action instead of the inline select arguments from merlin.simulation_dataset statement in PostgresPlanRepository.downloadSimulationDataset
  • Stream download responses rather than buffering the full dataset in memory
  • plandev-gateway and plandev-ui changes (separate PRs) are required to expose these endpoints to end users

… for creating simulation datasets with profiles
…plement in PostgresPlanRepository and InMemoryPlanRepository
…nstead of individual parameters

Consolidate uploadSimulationDataset parameters into a single SimulationResults object. Update Hasura action definition, parsers, and repository implementations. Add SimulationResultsParser to deserialize JSON format produced by SimulationResultsWriter, including parsing of duration strings, timestamps, profile segments, activities, and events. Update PostgresPlanRepository to extract data from SimulationResults and write
…oadSimulationDataset JSON input

Add simulationArguments Map<String, SerializedValue> field to SimulationResults with constructor overload for backward compatibility. Update SimulationResultsParser to parse optional simulationArguments field from JSON. Modify PostgresPlanRepository to use parsed simulationArguments instead of empty map when creating simulation datasets. Add unit tests verifying parsing with and without simulationArguments fiel
The parser now handles optional "topics" and "events" sections in the
upload JSON. Topics are parsed as a map of name -> schema and converted
to List<Triple<Integer, String, ValueSchema>> with synthesized indices.
Flat events are grouped by (realTime, transactionIndex), then
reconstructed into EventGraph<EventRecord> objects via
EventGraphUnflattener.unflatten() for insertion into the database.

Previously these fields were ignored, resulting in uploaded simulation
datasets always having 0 events.
…on for retrieving simulation datasets with profiles
…retrieving simulation datasets by plan and dataset ID
…trieving simulation datasets with profiles, activities, topics, events, and arguments
…tActivities methods from private to package-private in PostgresResultsCellRepository
…rs for converting SimulationResults to downloadable JSON format with profiles, activities, topics, and events
…ository and StubPlanService throwing UnsupportedOperationException
… and aerie_ui, and remove unnecessary build contexts and volumes for workers
…MerlinBindings

`uploadSimulationDataset`: requires `simulate` permission
`downloadSimulationDataset`: requires `resource_samples` permission
… for creating simulation datasets with profiles
…plement in PostgresPlanRepository and InMemoryPlanRepository
…nstead of individual parameters

Consolidate uploadSimulationDataset parameters into a single SimulationResults object. Update Hasura action definition, parsers, and repository implementations. Add SimulationResultsParser to deserialize JSON format produced by SimulationResultsWriter, including parsing of duration strings, timestamps, profile segments, activities, and events. Update PostgresPlanRepository to extract data from SimulationResults and write
…oadSimulationDataset JSON input

Add simulationArguments Map<String, SerializedValue> field to SimulationResults with constructor overload for backward compatibility. Update SimulationResultsParser to parse optional simulationArguments field from JSON. Modify PostgresPlanRepository to use parsed simulationArguments instead of empty map when creating simulation datasets. Add unit tests verifying parsing with and without simulationArguments fiel
The parser now handles optional "topics" and "events" sections in the
upload JSON. Topics are parsed as a map of name -> schema and converted
to List<Triple<Integer, String, ValueSchema>> with synthesized indices.
Flat events are grouped by (realTime, transactionIndex), then
reconstructed into EventGraph<EventRecord> objects via
EventGraphUnflattener.unflatten() for insertion into the database.

Previously these fields were ignored, resulting in uploaded simulation
datasets always having 0 events.
@remy-rabideau
remy-rabideau force-pushed the feature/upload-simulation-dataset branch from d2501e8 to 9a0d32c Compare July 28, 2026 18:16
…ion directing users to ResponseSerializers.serializeSimulationResultsForDownload()
…es in uploadSimulationDataset to ensure all activity types in the dataset exist in the plan's mission model
Increased maximum request size for uploads from 256 MB to 64 GB (per Clipper request)
final var javalin = Javalin.create(config -> {
config.showJavalinBanner = false;
if (configuration.enableJavalinDevLogging()) config.plugins.enableDevLogging();
config.http.maxRequestSize = 64L * 1024 * 1024 * 1024;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per request by Clipper for large simulation result datasets

@remy-rabideau
remy-rabideau marked this pull request as ready for review July 30, 2026 16:34
@remy-rabideau
remy-rabideau requested a review from a team as a code owner July 30, 2026 16:34
@remy-rabideau
remy-rabideau marked this pull request as draft July 30, 2026 16:35
@adrienmaillard
adrienmaillard removed their request for review July 30, 2026 16:51
@remy-rabideau remy-rabideau changed the title Add upload simulation dataset feature Add upload and download simulation dataset feature Jul 30, 2026
@remy-rabideau
remy-rabideau marked this pull request as ready for review July 30, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant