Skip to content

Commit

Permalink
Merge pull request #1478 from gemini-hlsw/add-execution-state-to-digest
Browse files Browse the repository at this point in the history
Adds an `ExecutionState` to `SequenceDigest`
  • Loading branch information
swalker2m authored Nov 15, 2024
2 parents 9874b9a + b1635de commit 9c581b0
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 87 deletions.
8 changes: 4 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ val http4sJdkHttpClientVersion = "0.9.1"
val jwtVersion = "5.0.0"
val logbackVersion = "1.5.11"
val log4catsVersion = "2.7.0"
val lucumaCatalogVersion = "0.48.6"
val lucumaItcVersion = "0.22.6"
val lucumaCoreVersion = "0.107.0"
val lucumaCatalogVersion = "0.48.7"
val lucumaItcVersion = "0.22.7"
val lucumaCoreVersion = "0.108.0"
val lucumaGraphQLRoutesVersion = "0.8.15"
val lucumaSsoVersion = "0.6.27"
val lucumaSsoVersion = "0.6.28"
val munitVersion = "0.7.29" // check test output if you attempt to update this
val munitCatsEffectVersion = "1.0.7" // check test output if you attempt to update this
val munitDisciplineVersion = "1.0.9" // check test output if you attempt to update this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6405,7 +6405,7 @@ type Execution {
"""
Determines the execution state as a whole of this observation.
"""
state: ObservationExecutionState!
executionState: ExecutionState!

"Executed (or at least partially executed) atom records, across all visits."
atomRecords(
Expand Down Expand Up @@ -8619,23 +8619,24 @@ type Observation {

}

enum ObservationExecutionState {
enum ExecutionState {

"""
The observation isn't sufficiently defined, or there is a problem that
must first be resolved.
The sequence or observation isn't sufficiently defined, or there is a problem
that must first be resolved.
"""
NOT_DEFINED

"No execution visit has been recorded for this observation."
"No execution visit has been recorded."
NOT_STARTED

"""
At least one visit was made for this observation, but it is not yet complete.
At least one visit was made, but the sequence or observation is not yet
complete.
"""
ONGOING

"No more science data is expected for this observation."
"No more data is expected."
COMPLETED

}
Expand Down Expand Up @@ -10053,6 +10054,13 @@ type SequenceDigest {
and any remaining atoms not included in 'possibleFuture'.
"""
atomCount: NonNegInt!

"""
Execution state for the sequence. Note, acquisition sequences are never
'COMPLETED'. The execution state for the observation as a whole is that of
the science sequence.
"""
executionState: ExecutionState!
}

"""
Expand Down
21 changes: 11 additions & 10 deletions modules/schema/src/main/scala/lucuma/odb/json/sequence.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.circe.Json
import io.circe.refined.*
import io.circe.syntax.*
import lucuma.core.enums.Breakpoint
import lucuma.core.enums.ExecutionState
import lucuma.core.enums.Instrument
import lucuma.core.enums.ObserveClass
import lucuma.core.math.Offset
Expand Down Expand Up @@ -97,24 +98,24 @@ trait SequenceCodec {
}

given Decoder[SequenceDigest] =
Decoder.instance { c =>
for {
Decoder.instance: c =>
for
o <- c.downField("observeClass").as[ObserveClass]
t <- c.downField("timeEstimate").as[CategorizedTime]
f <- c.downField("offsets").as[List[Offset]].map(SortedSet.from)
n <- c.downField("atomCount").as[NonNegInt]
} yield SequenceDigest(o, t, f, n)
}
e <- c.downField("executionState").as[ExecutionState]
yield SequenceDigest(o, t, f, n, e)

given (using Encoder[Offset], Encoder[TimeSpan]): Encoder[SequenceDigest] =
Encoder.instance { (a: SequenceDigest) =>
Encoder.instance: (a: SequenceDigest) =>
Json.obj(
"observeClass" -> a.observeClass.asJson,
"timeEstimate" -> a.timeEstimate.asJson,
"offsets" -> a.offsets.toList.asJson,
"atomCount" -> a.atomCount.asJson
"observeClass" -> a.observeClass.asJson,
"timeEstimate" -> a.timeEstimate.asJson,
"offsets" -> a.offsets.toList.asJson,
"atomCount" -> a.atomCount.asJson,
"executionState" -> a.executionState.asJson
)
}

given Decoder[ExecutionDigest] =
Decoder.instance { c =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cats.Eq
import cats.effect.Concurrent
import cats.syntax.all.*
import fs2.Stream
import lucuma.core.enums.ExecutionState
import lucuma.core.model.sequence.Atom
import lucuma.core.util.Timestamp
import lucuma.odb.sequence.data.ProtoExecutionConfig
Expand Down Expand Up @@ -39,11 +40,11 @@ case class ExecutionConfigGenerator[S, D](
visits: Stream[F, VisitRecord],
steps: Stream[F, StepRecord[D]],
when: Timestamp
)(using Eq[D]): F[ProtoExecutionConfig[S, Atom[D]]] =
)(using Eq[D]): F[(ProtoExecutionConfig[S, Atom[D]], ExecutionState)] =
mergeByTimestamp(visits, steps)(_.created, _.created)
.fold((acquisition, science)) {
case ((a, s), Left(visit)) => (a.recordVisit(visit), s.recordVisit(visit))
case ((a, s), Right(step)) => (a.recordStep(step), s.recordStep(step))
.fold((acquisition, science, ExecutionState.NotStarted)) {
case ((a, s, _), Left(visit)) => (a.recordVisit(visit), s.recordVisit(visit), ExecutionState.Ongoing)
case ((a, s, _), Right(step)) => (a.recordStep(step), s.recordStep(step), ExecutionState.Ongoing)
}
.compile.onlyOrError.map: (a, s) =>
ProtoExecutionConfig(static, a.generate(when), s.generate(when))
.compile.onlyOrError.map: (a, s, e) =>
(ProtoExecutionConfig(static, a.generate(when), s.generate(when)), e)
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ object Science:
// from 'start' that matches the step.
private def advancePos(start: Int, step: StepRecord[D])(using Eq[D]): Int =
Stream
.iterate(start)(p => (p + 1) % length)
.iterate(start%length)(p => (p + 1) % length)
.take(length)
.dropWhile(p => !records.getUnsafe(p).block.definition.matches(step))
.head
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TYPE e_execution_state AS ENUM(
'not_defined',
'not_started',
'ongoing',
'completed'
);

-- Add the execution state to the sequence digest
--
ALTER TABLE t_execution_digest
ADD COLUMN c_acq_execution_state e_execution_state NOT NULL DEFAULT 'not_defined',
ADD COLUMN c_sci_execution_state e_execution_state NOT NULL DEFAULT 'not_defined';
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ trait BaseMapping[F[_]]
lazy val ExecutionEventType = schema.ref("ExecutionEvent")
lazy val ExecutionEventTypeType = schema.ref("ExecutionEventType")
lazy val ExecutionEventSelectResultType = schema.ref("ExecutionEventSelectResult")
lazy val ExecutionStateType = schema.ref("ExecutionState")
lazy val ExistenceType = schema.ref("Existence")
lazy val ExtinctionType = schema.ref("Extinction")
lazy val FastTurnaroundType = schema.ref("FastTurnaround")
Expand Down Expand Up @@ -195,7 +196,6 @@ trait BaseMapping[F[_]]
lazy val ObsAttachmentTypeMetaType = schema.ref("ObsAttachmentTypeMeta")
lazy val ObsAttachmentTypeType = schema.ref("ObsAttachmentType")
lazy val ObservationEditType = schema.ref("ObservationEdit")
lazy val ObservationExecutionStateType = schema.ref("ObservationExecutionState")
lazy val ObservationIdType = schema.ref("ObservationId")
lazy val ObservationType = schema.ref("Observation")
lazy val ObservationReferenceType = schema.ref("ObservationReference")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ trait ExecutionMapping[F[_]] extends ObservationEffectHandler[F]
SqlField("programId", ObservationView.ProgramId, hidden = true),
EffectField("digest", digestHandler, List("id", "programId")),
EffectField("config", configHandler, List("id", "programId")),
EffectField("state", stateHandler, List("id", "programId")),
EffectField("executionState", executionStateHandler, List("id", "programId")),
SqlObject("atomRecords"),
SqlObject("datasets"),
SqlObject("events"),
Expand Down Expand Up @@ -129,7 +129,7 @@ trait ExecutionMapping[F[_]] extends ObservationEffectHandler[F]
effectHandler(readEnv, calculate)
}

private lazy val stateHandler: EffectHandler[F] =
private lazy val executionStateHandler: EffectHandler[F] =
val calculate: (Program.Id, Observation.Id, Unit) => F[Result[Json]] =
(pid, oid, limit) => {
services.use: s =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import lucuma.core.enums.CalibrationRole
import lucuma.core.enums.CallForProposalsType
import lucuma.core.enums.ConfigurationRequestStatus
import lucuma.core.enums.EducationalStatus
import lucuma.core.enums.ObservationExecutionState
import lucuma.core.enums.ExecutionState
import lucuma.core.enums.ObservingModeType
import lucuma.core.enums.PartnerLinkType
import lucuma.core.enums.ProgramUserRole
Expand Down Expand Up @@ -102,6 +102,7 @@ trait LeafMappings[F[_]] extends BaseMapping[F] {
LeafMapping[Epoch](EpochStringType),
LeafMapping[ExecutionEvent.Id](ExecutionEventIdType),
LeafMapping[ExecutionEventType](ExecutionEventTypeType),
LeafMapping[ExecutionState](ExecutionStateType),
LeafMapping[Existence](ExistenceType),
LeafMapping[Extinction](ExtinctionType),
LeafMapping[Tag](FilterTypeType),
Expand Down Expand Up @@ -146,7 +147,6 @@ trait LeafMappings[F[_]] extends BaseMapping[F] {
LeafMapping[Tag](ObsAttachmentTypeType),
LeafMapping[ObservingModeType](ObservingModeTypeType),
LeafMapping[Observation.Id](ObservationIdType),
LeafMapping[ObservationExecutionState](ObservationExecutionStateType),
LeafMapping[ObservationReference](ObservationReferenceLabelType),
LeafMapping[ObserveClass](ObserveClassType),
LeafMapping[Partner](PartnerType),
Expand Down
58 changes: 24 additions & 34 deletions modules/service/src/main/scala/lucuma/odb/logic/Generator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import eu.timepit.refined.numeric.Interval
import fs2.Pure
import fs2.Stream
import lucuma.core.enums.CalibrationRole
import lucuma.core.enums.ObservationExecutionState
import lucuma.core.enums.ExecutionState
import lucuma.core.enums.SequenceType
import lucuma.core.model.Observation
import lucuma.core.model.Program
Expand Down Expand Up @@ -138,7 +138,7 @@ sealed trait Generator[F[_]] {
def executionState(
programId: Program.Id,
observationId: Observation.Id
)(using NoTransaction[F]): F[ObservationExecutionState]
)(using NoTransaction[F]): F[ExecutionState]

}

Expand Down Expand Up @@ -341,7 +341,7 @@ object Generator {
gen: ExecutionConfigGenerator[S, D],
steps: Stream[F, StepRecord[D]],
when: Option[Timestamp]
)(using Eq[D]): EitherT[F, Error, ProtoExecutionConfig[S, Atom[D]]] =
)(using Eq[D]): EitherT[F, Error, (ProtoExecutionConfig[S, Atom[D]], ExecutionState)] =
val visits = services.visitService.selectAll(oid)
EitherT.liftF(services.transactionally {
for {
Expand All @@ -355,7 +355,7 @@ object Generator {
config: lucuma.odb.sequence.gmos.longslit.Config.GmosNorth,
role: Option[CalibrationRole],
when: Option[Timestamp]
): EitherT[F, Error, ProtoGmosNorth] =
): EitherT[F, Error, (ProtoGmosNorth, ExecutionState)] =
val gen = LongSlit.gmosNorth(calculator.gmosNorth, ctx.namespace, exp.gmosNorth, config, ctx.acquisitionIntegrationTime, ctx.scienceIntegrationTime, role)
val srs = services.gmosSequenceService.selectGmosNorthStepRecords(ctx.oid)
for {
Expand All @@ -368,7 +368,7 @@ object Generator {
config: lucuma.odb.sequence.gmos.longslit.Config.GmosSouth,
role: Option[CalibrationRole],
when: Option[Timestamp]
): EitherT[F, Error, ProtoGmosSouth] =
): EitherT[F, Error, (ProtoGmosSouth, ExecutionState)] =
val gen = LongSlit.gmosSouth(calculator.gmosSouth, ctx.namespace, exp.gmosSouth, config, ctx.acquisitionIntegrationTime, ctx.scienceIntegrationTime, role)
val srs = services.gmosSequenceService.selectGmosSouthStepRecords(ctx.oid)
for {
Expand All @@ -383,17 +383,15 @@ object Generator {
EitherT
.fromEither(Error.sequenceTooLong.asLeft[ExecutionDigest])
.unlessA(ctx.scienceIntegrationTime.toOption.forall(_.exposureCount.value <= SequenceAtomLimit)) *>
(ctx.params match {
(ctx.params match
case GeneratorParams(_, config: gmos.longslit.Config.GmosNorth, role) =>
gmosNorthLongSlit(ctx, config, role, when).flatMap { p =>
EitherT.fromEither[F](executionDigest(p, calculator.gmosNorth.estimateSetup))
}
gmosNorthLongSlit(ctx, config, role, when).flatMap: (p, e) =>
EitherT.fromEither[F](executionDigest(p, e, calculator.gmosNorth.estimateSetup))

case GeneratorParams(_, config: gmos.longslit.Config.GmosSouth, role) =>
gmosSouthLongSlit(ctx, config, role, when).flatMap { p =>
EitherT.fromEither[F](executionDigest(p, calculator.gmosSouth.estimateSetup))
}
})
gmosSouthLongSlit(ctx, config, role, when).flatMap: (p, e) =>
EitherT.fromEither[F](executionDigest(p, e, calculator.gmosSouth.estimateSetup))
)

override def generate(
pid: Program.Id,
Expand All @@ -411,26 +409,24 @@ object Generator {
lim: FutureLimit,
when: Option[Timestamp]
)(using NoTransaction[F]): EitherT[F, Error, InstrumentExecutionConfig] =
ctx.params match {
ctx.params match
case GeneratorParams(_, config: gmos.longslit.Config.GmosNorth, role) =>
gmosNorthLongSlit(ctx, config, role, when).map { p =>
gmosNorthLongSlit(ctx, config, role, when).map: (p, _) =>
InstrumentExecutionConfig.GmosNorth(executionConfig(p, lim))
}

case GeneratorParams(_, config: gmos.longslit.Config.GmosSouth, role) =>
gmosSouthLongSlit(ctx, config, role, when).map { p =>
gmosSouthLongSlit(ctx, config, role, when).map: (p, _) =>
InstrumentExecutionConfig.GmosSouth(executionConfig(p, lim))
}
}

private def executionDigest[S, D](
proto: ProtoExecutionConfig[S, Atom[D]],
execState: ExecutionState,
setupTime: SetupTime
): Either[Error, ExecutionDigest] =

// Compute the sequence digest from the stream by folding over the steps.
def sequenceDigest(s: Stream[Pure, Atom[D]]): Either[Error, SequenceDigest] =
s.fold(SequenceDigest.Zero.asRight[Error]) { case (eDigest, atom) =>
s.fold(SequenceDigest.Zero.copy(executionState = ExecutionState.Completed).asRight[Error]) { case (eDigest, atom) =>
eDigest.flatMap: digest =>
digest
.incrementAtomCount
Expand All @@ -441,6 +437,7 @@ object Generator {
d.add(s.observeClass)
.add(CategorizedTime.fromStep(s.observeClass, s.estimate))
.add(s.telescopeConfig.offset)
.copy(executionState = execState)
}
}.toList.head

Expand Down Expand Up @@ -474,20 +471,13 @@ object Generator {
def executionState(
programId: Program.Id,
observationId: Observation.Id
)(using NoTransaction[F]): F[ObservationExecutionState] =
import ObservationExecutionState.*

def definedStatus(isComplete: Boolean): F[ObservationExecutionState] =
if isComplete then Completed.pure[F]
else services.transactionally {
visitService.selectAll(observationId).head.compile.last
.map(_.fold(NotStarted)(_ => Ongoing))
}

generate(programId, observationId, FutureLimit.Min)
.flatMap(_.fold(
_ => NotDefined.pure[F],
iec => definedStatus(iec.isComplete)
)(using NoTransaction[F]): F[ExecutionState] =
digest(programId, observationId)
.map(_.fold(
_ => ExecutionState.NotDefined,
_.science.executionState
))


}
}
Loading

0 comments on commit 9c581b0

Please sign in to comment.