From 54c92d7c7452542a42bb197aec467b1d7957af69 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 16 Feb 2024 15:27:58 -0800 Subject: [PATCH] Add bounds and time conversion to plan interface --- .../aerie/merlin/protocol/types/Duration.java | 11 ++- ...DatabaseFacade.kt => AeriePostgresPlan.kt} | 72 +++++++++++++++---- .../remote/{DatabaseFacade.kt => Plan.kt} | 19 ++++- 3 files changed, 84 insertions(+), 18 deletions(-) rename timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/{AeriePostgresDatabaseFacade.kt => AeriePostgresPlan.kt} (66%) rename timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/{DatabaseFacade.kt => Plan.kt} (61%) diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/Duration.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/Duration.java index 1dcf377fea..924773ffef 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/Duration.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/Duration.java @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.merlin.protocol.types; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; @@ -372,8 +374,8 @@ public static Duration max(final Duration... durations) { return maximum; } - /** Apply a duration to a {@link java.time.Instant}. */ - public static java.time.Instant addToInstant(final java.time.Instant instant, final Duration duration) { + /** Apply a duration to a {@link Instant}. */ + public static java.time.Instant addToInstant(final Instant instant, final Duration duration) { // Java Instants don't provide capability to add microseconds // Add millis and micros separately to avoid possible overflow return instant @@ -381,6 +383,11 @@ public static java.time.Instant addToInstant(final java.time.Instant instant, fi .plusNanos(1000 * duration.remainderOf(Duration.MILLISECONDS).dividedBy(Duration.MICROSECONDS)); } + /** Find the duration between two {@link Instant}s. */ + public static Duration durationBetweenInstants(final Instant a, final Instant b) { + return microseconds(a.until(b, ChronoUnit.MICROS)); + } + public Duration negate() { return Duration.negate(this); } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresDatabaseFacade.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresPlan.kt similarity index 66% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresDatabaseFacade.kt rename to timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresPlan.kt index 8a2c6293fa..2890fae74c 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresDatabaseFacade.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/AeriePostgresPlan.kt @@ -3,10 +3,10 @@ package gov.nasa.jpl.aerie.timeline.remote import gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser import gov.nasa.jpl.aerie.merlin.protocol.types.Duration import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue -import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* import gov.nasa.jpl.aerie.timeline.Segment import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between import gov.nasa.jpl.aerie.timeline.activities.AnyDirective import gov.nasa.jpl.aerie.timeline.activities.AnyInstance import gov.nasa.jpl.aerie.timeline.activities.Instance @@ -16,29 +16,75 @@ import gov.nasa.jpl.aerie.timeline.collections.Instances import gov.nasa.jpl.aerie.timeline.util.listCollector import java.io.StringReader import java.sql.Connection +import java.sql.PreparedStatement +import java.time.Instant import javax.json.Json import kotlin.jvm.optionals.getOrNull /** A connection to Aerie's database for a particular simulation result. */ -data class AeriePostgresDatabaseFacade( +data class AeriePostgresPlan( /** A connection to Aerie's database. */ val c: Connection, /** The particular simulation dataset to query. */ val simDatasetId: Int -): DatabaseFacade { +): Plan { private val datasetId by lazy { - val statement = c.prepareStatement( - "select dataset_id from simulation_dataset where id = ?;" - ) + val statement = c.prepareStatement("select dataset_id from simulation_dataset where id = ?;") statement.setInt(1, simDatasetId) + getSingleIntQueryResult(statement) + } + + private val simulationId by lazy { + val statement = c.prepareStatement("select simulation_id from simulation_dataset where id = ?;") + statement.setInt(1, simDatasetId) + getSingleIntQueryResult(statement) + } + + private val simulationInfo by lazy { + val statement = c.prepareStatement("select plan_id, simulation_start_time, simulation_end_time from simulation where id = ?;") + statement.setInt(1, simulationId) + val response = statement.executeQuery() + if (!response.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val result = object { + val planId = response.getInt(1) + val startTime = response.getTimestamp(2).toInstant() + val endTime = response.getTimestamp(3).toInstant() + } + if (response.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + result + } + + private fun getSingleIntQueryResult(statement: PreparedStatement): Int { val result = statement.executeQuery() - if (!result.next()) throw DatabaseError("Could not find dataset for simulation dataset $simDatasetId.") - val datasetId = result.getInt(1) - if (result.next()) throw DatabaseError("Multiple datasets found for simulation dataset $simDatasetId") - datasetId + if (!result.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val int = result.getInt(1) + if (result.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + return int + } + + private val planInfo by lazy { + val statement = c.prepareStatement("select start_time, duration from plan where id = ?;") + statement.setInt(1, simulationInfo.planId) + val response = statement.executeQuery() + if (!response.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val result = object { + val startTime = response.getTimestamp(1).toInstant() + val duration = Duration.parseISO8601(response.getString(2)) + } + if (response.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + result } + override fun totalBounds() = between(Duration.ZERO, planInfo.duration) + override fun simBounds() = between( + toRelative(simulationInfo.startTime), + toRelative(simulationInfo.endTime), + ) + + override fun toRelative(abs: Instant): Duration = Duration.durationBetweenInstants(planInfo.startTime, abs) + override fun toAbsolute(rel: Duration): Instant = Duration.addToInstant(planInfo.startTime, rel) + private val intervalStyleStatement = c.prepareStatement("set intervalstyle = 'iso_8601';") private val profileInfoStatement = c.prepareStatement( "select id, duration from profile where dataset_id = ? and name = ?;" @@ -73,7 +119,7 @@ data class AeriePostgresDatabaseFacade( val thisStart = Duration.parseISO8601(response.getString(1)) if (previousStart !== null) { val newSegment = Segment( - Interval.between(previousStart, thisStart, Inclusive, Exclusive), + between(previousStart, thisStart, Inclusive, Exclusive), previousValue!!, ) result.add(newSegment) @@ -90,7 +136,7 @@ data class AeriePostgresDatabaseFacade( if (previousStart !== null) { result.add( Segment( - Interval.between(previousStart, profileInfo.duration, Inclusive, Exclusive), + between(previousStart, profileInfo.duration, Inclusive, Exclusive), previousValue!! ) ) @@ -137,7 +183,7 @@ data class AeriePostgresDatabaseFacade( AnyInstance(arguments, computedAttributes), response.getString(4), directiveId, - Interval.between(start, start.plus(Duration.parseISO8601(response.getString(2)))) + between(start, start.plus(Duration.parseISO8601(response.getString(2)))) )) } return BaseTimeline(::Instances, listCollector(result)).specialize() diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/DatabaseFacade.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/Plan.kt similarity index 61% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/DatabaseFacade.kt rename to timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/Plan.kt index 97ae3f6703..7b2db64dd1 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/DatabaseFacade.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/remote/Plan.kt @@ -1,16 +1,29 @@ package gov.nasa.jpl.aerie.timeline.remote +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.Segment import gov.nasa.jpl.aerie.timeline.activities.AnyDirective import gov.nasa.jpl.aerie.timeline.activities.AnyInstance -import gov.nasa.jpl.aerie.timeline.Timeline import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegments import gov.nasa.jpl.aerie.timeline.collections.Directives import gov.nasa.jpl.aerie.timeline.collections.Instances +import java.time.Instant + +/** An interface for querying plan information and simulation results. */ +interface Plan { + /** Total extent of the plan's bounds, whether it was simulated on the full extent or not. */ + fun totalBounds(): Interval + + /** Bounds on which the plan was most recently simulated. */ + fun simBounds(): Interval + + /** Convert a time instant to a relative duration (relative to plan start). */ + fun toRelative(abs: Instant): Duration + /** Convert a relative duration to a time instant. */ + fun toAbsolute(rel: Duration): Instant -/** An interface for querying timelines of profiles and activities. */ -interface DatabaseFacade { /** * Query a resource profile from the database *