diff --git a/common/src/main/scala/explore/model/reusability.scala b/common/src/main/scala/explore/model/reusability.scala index 9b187dad7a..955665a6f2 100644 --- a/common/src/main/scala/explore/model/reusability.scala +++ b/common/src/main/scala/explore/model/reusability.scala @@ -13,8 +13,8 @@ import explore.model.enums.AgsState import explore.model.enums.SelectedPanel import explore.model.itc.ItcExposureTime import explore.model.itc.ItcTarget +import explore.modes.InstrumentConfig import explore.modes.InstrumentOverrides -import explore.modes.InstrumentRow import explore.undo.UndoStacks import explore.utils.OdbRestClient import japgolly.scalajs.react.ReactCats.* @@ -101,7 +101,7 @@ object reusability: given Reusability[AngularSize] = Reusability.byEq given Reusability[CatalogTargetResult] = Reusability.byEq given Reusability[BasicConfiguration] = Reusability.byEq - given Reusability[BasicConfigAndItc] = Reusability.byEq + given Reusability[InstrumentConfigAndItcResult] = Reusability.byEq given Reusability[GuideStarCandidate] = Reusability.by(_.name.value) given Reusability[AgsPosition] = Reusability.byEq given Reusability[AgsParams] = Reusability.byEq @@ -110,7 +110,7 @@ object reusability: given Reusability[ObsConfiguration] = Reusability.byEq given Reusability[Existence] = Reusability.byEq given Reusability[ItcExposureTime] = Reusability.byEq - given Reusability[InstrumentRow] = Reusability.byEq + given Reusability[InstrumentConfig] = Reusability.byEq given Reusability[CentralWavelength] = Reusability.byEq given Reusability[ObjectTracking] = Reusability.byEq given Reusability[Asterism] = Reusability.byEq[Asterism] diff --git a/explore/src/main/scala/explore/cache/CacheModifierUpdaters.scala b/explore/src/main/scala/explore/cache/CacheModifierUpdaters.scala index 34208a6de4..331bf70f0f 100644 --- a/explore/src/main/scala/explore/cache/CacheModifierUpdaters.scala +++ b/explore/src/main/scala/explore/cache/CacheModifierUpdaters.scala @@ -4,12 +4,11 @@ package explore.cache import cats.Endo -import cats.Monoid -import cats.MonoidK import cats.Order.given import cats.syntax.all.* import crystal.Pot import eu.timepit.refined.auto.autoUnwrap +import explore.givens.given import explore.model.GroupTree import explore.model.GroupUpdate import explore.model.Observation @@ -30,9 +29,6 @@ import queries.common.TargetQueriesGQL.ProgramTargetsDelta.Data.TargetEdit * Functions to modify cache through subscription updates */ trait CacheModifierUpdaters { - // TODO Move somewhere else - private given [A]: Monoid[Endo[A]] = MonoidK[Endo].algebra[A] - protected def modifyTargets(targetEdit: TargetEdit): ProgramSummaries => ProgramSummaries = ProgramSummaries.targets .modify: targets => diff --git a/explore/src/main/scala/explore/config/AdvancedConfigurationPanel.scala b/explore/src/main/scala/explore/config/AdvancedConfigurationPanel.scala index 3a8cc878b2..51377ff0dc 100644 --- a/explore/src/main/scala/explore/config/AdvancedConfigurationPanel.scala +++ b/explore/src/main/scala/explore/config/AdvancedConfigurationPanel.scala @@ -21,15 +21,13 @@ import explore.components.HelpIcon import explore.components.ui.ExploreStyles import explore.config.ConfigurationFormats.* import explore.model.AppContext -import explore.model.BasicConfigAndItc import explore.model.ExploreModelValidators import explore.model.Observation import explore.model.ScienceRequirements import explore.model.display.* import explore.model.display.given import explore.model.enums.WavelengthUnits -import explore.modes.GmosNorthSpectroscopyRow -import explore.modes.GmosSouthSpectroscopyRow +import explore.modes.InstrumentConfig import explore.modes.ModeCommonWavelengths import explore.modes.ModeSlitSize import explore.modes.ModeWavelength @@ -82,9 +80,8 @@ sealed trait AdvancedConfigurationPanel[T <: ObservingMode, Input]: def calibrationRole: Option[CalibrationRole] def observingMode: Aligner[T, Input] def spectroscopyRequirements: View[ScienceRequirements.Spectroscopy] - def deleteConfig: Callback + def revertConfig: Callback def confMatrix: SpectroscopyModesMatrix - def selectedConfig: View[Option[BasicConfigAndItc]] def sequenceChanged: Callback def readonly: Boolean def units: WavelengthUnits @@ -232,11 +229,11 @@ sealed abstract class AdvancedConfigurationPanelBuilder[ reqsWavelength.flatMap(cw => (mode, row.instrument) match case (m: ObservingMode.GmosNorthLongSlit, - GmosNorthSpectroscopyRow(rGrating, rFpu, rFilter, _) + InstrumentConfig.GmosNorthSpectroscopy(rGrating, rFpu, rFilter, _) ) if m.grating === rGrating && m.filter === rFilter && m.fpu === rFpu => ModeData.build(row, reqsWavelength) case (m: ObservingMode.GmosSouthLongSlit, - GmosSouthSpectroscopyRow(rGrating, rFpu, rFilter, _) + InstrumentConfig.GmosSouthSpectroscopy(rGrating, rFpu, rFilter, _) ) if m.grating === rGrating && m.filter === rFilter && m.fpu === rFpu => ModeData.build(row, reqsWavelength) case _ => none @@ -726,13 +723,7 @@ sealed abstract class AdvancedConfigurationPanelBuilder[ label = "Revert Configuration", icon = Icons.ListIcon, severity = Button.Severity.Secondary, - onClick = props.selectedConfig.mod(c => - BasicConfigAndItc( - props.observingMode.get.toBasicConfiguration, - c.flatMap(_.itcResult.flatMap(_.toOption.map(_.asRight))) - ).some - ) - >> props.deleteConfig + onClick = props.revertConfig ).compact.small .unless(isCustomized(props.observingMode)), Button( @@ -795,9 +786,8 @@ object AdvancedConfigurationPanel { calibrationRole: Option[CalibrationRole], observingMode: Aligner[ObservingMode.GmosNorthLongSlit, GmosNorthLongSlitInput], spectroscopyRequirements: View[ScienceRequirements.Spectroscopy], - deleteConfig: Callback, + revertConfig: Callback, confMatrix: SpectroscopyModesMatrix, - selectedConfig: View[Option[BasicConfigAndItc]], sequenceChanged: Callback, readonly: Boolean, units: WavelengthUnits @@ -998,9 +988,8 @@ object AdvancedConfigurationPanel { calibrationRole: Option[CalibrationRole], observingMode: Aligner[ObservingMode.GmosSouthLongSlit, GmosSouthLongSlitInput], spectroscopyRequirements: View[ScienceRequirements.Spectroscopy], - deleteConfig: Callback, + revertConfig: Callback, confMatrix: SpectroscopyModesMatrix, - selectedConfig: View[Option[BasicConfigAndItc]], sequenceChanged: Callback, readonly: Boolean, units: WavelengthUnits diff --git a/explore/src/main/scala/explore/config/BasicConfigurationPanel.scala b/explore/src/main/scala/explore/config/BasicConfigurationPanel.scala index 2a737b4c9d..9a681b23c9 100644 --- a/explore/src/main/scala/explore/config/BasicConfigurationPanel.scala +++ b/explore/src/main/scala/explore/config/BasicConfigurationPanel.scala @@ -10,8 +10,8 @@ import crystal.react.hooks.* import explore.Icons import explore.components.ui.ExploreStyles import explore.model.AppContext -import explore.model.BasicConfigAndItc import explore.model.ImagingConfigurationOptions +import explore.model.InstrumentConfigAndItcResult import explore.model.Observation import explore.model.ScienceRequirements import explore.model.ScienceRequirements.Spectroscopy @@ -38,7 +38,7 @@ case class BasicConfigurationPanel( userId: Option[User.Id], obsId: Observation.Id, spectroscopyView: ViewOpt[Spectroscopy], - selectedConfig: View[Option[BasicConfigAndItc]], + selectedConfig: View[Option[InstrumentConfigAndItcResult]], constraints: ConstraintSet, itcTargets: List[ItcTarget], baseCoordinates: Option[CoordinatesAtVizTime], @@ -73,7 +73,7 @@ private object BasicConfigurationPanel: .map(_.wavelength) .fold("Wavelength is required for creating a configuration.".some)(_ => props.selectedConfig.get match { - case Some(BasicConfigAndItc(_, itc)) => + case Some(InstrumentConfigAndItcResult(_, itc)) => itc match { case Some(Right(r)) if r.isPending => "Waiting for ITC result...".some case Some(Right(r)) if r.isSuccess => none diff --git a/explore/src/main/scala/explore/config/ConfigurationPanel.scala b/explore/src/main/scala/explore/config/ConfigurationPanel.scala index 1ef483693d..648267937a 100644 --- a/explore/src/main/scala/explore/config/ConfigurationPanel.scala +++ b/explore/src/main/scala/explore/config/ConfigurationPanel.scala @@ -18,13 +18,14 @@ import explore.common.ScienceQueries.ScienceRequirementsUndoView import explore.common.ScienceQueries.UpdateScienceRequirements import explore.components.ui.ExploreStyles import explore.model.AppContext -import explore.model.BasicConfigAndItc +import explore.model.InstrumentConfigAndItcResult import explore.model.ObsConfiguration import explore.model.Observation import explore.model.ScienceRequirements import explore.model.ScienceRequirements.Spectroscopy import explore.model.enums.WavelengthUnits import explore.model.itc.ItcTarget +import explore.modes.InstrumentConfig import explore.modes.SpectroscopyModesMatrix import explore.undo.* import japgolly.scalajs.react.* @@ -44,20 +45,21 @@ import monocle.Iso import queries.common.ObsQueriesGQL case class ConfigurationPanel( - userId: Option[User.Id], - programId: Program.Id, - obsId: Observation.Id, - requirements: UndoSetter[ScienceRequirements], - mode: UndoSetter[Option[ObservingMode]], - posAngle: View[PosAngleConstraint], - obsConf: ObsConfiguration, - itcTargets: List[ItcTarget], - baseCoordinates: Option[CoordinatesAtVizTime], - selectedConfig: View[Option[BasicConfigAndItc]], - modes: SpectroscopyModesMatrix, - sequenceChanged: Callback, - readonly: Boolean, - units: WavelengthUnits + userId: Option[User.Id], + programId: Program.Id, + obsId: Observation.Id, + requirements: UndoSetter[ScienceRequirements], + mode: UndoSetter[Option[ObservingMode]], + posAngle: View[PosAngleConstraint], + obsConf: ObsConfiguration, + itcTargets: List[ItcTarget], + baseCoordinates: Option[CoordinatesAtVizTime], + selectedConfig: View[Option[InstrumentConfigAndItcResult]], + revertedInstrumentConfig: Option[InstrumentConfig], + modes: SpectroscopyModesMatrix, + sequenceChanged: Callback, + readonly: Boolean, + units: WavelengthUnits ) extends ReactFnProps[ConfigurationPanel](ConfigurationPanel.component) object ConfigurationPanel: @@ -151,7 +153,16 @@ object ConfigurationPanel: val optModeView: View[Option[ObservingMode]] = modeAligner.view(_.map(_.toInput).orUnassign) - val deleteConfiguration = optModeView.set(none) + val revertConfiguration: Callback = + optModeView.set(none) >> + props.revertedInstrumentConfig + .map: row => // Select the reverted config + props.selectedConfig.mod: c => + InstrumentConfigAndItcResult( + row, + c.flatMap(_.itcResult.flatMap(_.toOption.map(_.asRight))) + ).some + .orEmpty val optModeAligner = modeAligner.toOption @@ -213,7 +224,7 @@ object ConfigurationPanel: props.obsConf.calibrationRole, createConfiguration( props.obsId, - props.selectedConfig.get.map(_.configuration), + props.selectedConfig.get.flatMap(_.toBasicConfiguration), optModeView ), props.modes, @@ -232,9 +243,8 @@ object ConfigurationPanel: props.obsConf.calibrationRole, northAligner, specView, - deleteConfiguration, + revertConfiguration, props.modes, - props.selectedConfig, props.sequenceChanged, props.readonly, props.units @@ -249,9 +259,8 @@ object ConfigurationPanel: props.obsConf.calibrationRole, southAligner, specView, - deleteConfiguration, + revertConfiguration, props.modes, - props.selectedConfig, props.sequenceChanged, props.readonly, props.units diff --git a/explore/src/main/scala/explore/config/SpectroscopyModesTable.scala b/explore/src/main/scala/explore/config/SpectroscopyModesTable.scala index 0a1eeee399..31cb010190 100644 --- a/explore/src/main/scala/explore/config/SpectroscopyModesTable.scala +++ b/explore/src/main/scala/explore/config/SpectroscopyModesTable.scala @@ -21,7 +21,7 @@ import explore.components.HelpIcon import explore.components.ui.ExploreStyles import explore.events.* import explore.model.AppContext -import explore.model.BasicConfigAndItc +import explore.model.InstrumentConfigAndItcResult import explore.model.Progress import explore.model.ScienceRequirements import explore.model.WorkerClients.* @@ -55,7 +55,6 @@ import lucuma.react.primereact.Button import lucuma.react.syntax.* import lucuma.react.table.* import lucuma.refined.* -import lucuma.schemas.model.BasicConfiguration import lucuma.typed.tanstackVirtualCore as rawVirtual import lucuma.ui.components.ThemeIcons import lucuma.ui.primereact.* @@ -75,7 +74,7 @@ import scalajs.js.JSConverters.* case class SpectroscopyModesTable( userId: Option[User.Id], - selectedConfig: View[Option[BasicConfigAndItc]], + selectedConfig: View[Option[InstrumentConfigAndItcResult]], spectroscopyRequirements: ScienceRequirements.Spectroscopy, constraints: ConstraintSet, targets: Option[List[ItcTarget]], @@ -152,7 +151,7 @@ private object SpectroscopyModesTable: private val formatSlitLength: ModeSlitSize => String = ss => f"${ModeSlitSize.milliarcseconds.get(ss.value).setScale(0, BigDecimal.RoundingMode.DOWN)}%1.0f" - private def formatGrating(grating: InstrumentRow#Grating): String = grating match + private def formatGrating(grating: InstrumentConfig#Grating): String = grating match case f: GmosSouthGrating => f.shortName case f: GmosNorthGrating => f.shortName case f: F2Disperser => f.shortName @@ -160,7 +159,7 @@ private object SpectroscopyModesTable: case f: GnirsDisperser => f.shortName case r => r.toString - private def formatFilter(filter: InstrumentRow#Filter): String = filter match + private def formatFilter(filter: InstrumentConfig#Filter): String = filter match case Some(f: GmosSouthFilter) => f.shortName case Some(f: GmosNorthFilter) => f.shortName case f: F2Filter => f.shortName @@ -170,9 +169,9 @@ private object SpectroscopyModesTable: // I think these are valid Orderings because they should be consistent with == // They could probably be Orders, as well, but only Ordering is actually needed here. - private given Ordering[InstrumentRow#Grating] = Ordering.by(_.toString) - private given Ordering[InstrumentRow#Filter] = Ordering.by(_.toString) - private given Ordering[TimeSpan | Unit] = Ordering.by(_.toOption) + private given Ordering[InstrumentConfig#Grating] = Ordering.by(_.toString) + private given Ordering[InstrumentConfig#Filter] = Ordering.by(_.toString) + private given Ordering[TimeSpan | Unit] = Ordering.by(_.toOption) private def formatInstrument(r: (Instrument, NonEmptyString)): String = r match case (i @ Instrument.Gnirs, m) => s"${i.longName} $m" @@ -294,40 +293,6 @@ private object SpectroscopyModesTable: .sortable ) - extension (row: SpectroscopyModeRow) - private def rowToConf(cw: Option[Wavelength]): Option[BasicConfiguration] = - cw.flatMap(row.intervalCenter) - .flatMap: cc => - row.instrument match - case GmosNorthSpectroscopyRow(grating, fpu, filter, _) - if row.focalPlane === FocalPlane.SingleSlit => - BasicConfiguration - .GmosNorthLongSlit( - grating = grating, - filter = filter, - fpu = fpu, - centralWavelength = cc - ) - .some - case GmosSouthSpectroscopyRow(grating, fpu, filter, _) - if row.focalPlane === FocalPlane.SingleSlit => - BasicConfiguration - .GmosSouthLongSlit( - grating = grating, - filter = filter, - fpu = fpu, - centralWavelength = cc - ) - .some - case _ => none - - extension (row: SpectroscopyModeRowWithResult) - private def rowToConfAndItc(cw: Option[Wavelength]): Option[BasicConfigAndItc] = - row.entry.rowToConf(cw).map(c => BasicConfigAndItc(c, row.result.some)) - - private def equalsConf(conf: BasicConfiguration, cw: Option[Wavelength]): Boolean = - row.entry.rowToConf(cw).contains_(conf) - extension (row: SpectroscopyModeRow) private def enabledRow: Boolean = List(Instrument.GmosNorth, Instrument.GmosSouth).contains_(row.instrument.instrument) && @@ -362,34 +327,44 @@ private object SpectroscopyModesTable: props.constraints ) ): (_, _, _) => - (matrix, s, dec, itc, asterism, constraints) => - val rows = - matrix - .filtered( - focalPlane = s.focalPlane, - capability = s.capability, - wavelength = s.wavelength, - slitLength = s.focalPlaneAngle.map(s => SlitLength(ModeSlitSize(s))), - resolution = s.resolution, - range = s.wavelengthCoverage, - declination = dec + (matrix, s, dec, itcResults, asterism, constraints) => + (s.wavelength, asterism).mapN { (w, a) => + val profiles: NonEmptyList[SourceProfile] = + a.map(_.sourceProfile) + + val rows: List[SpectroscopyModeRow] = + matrix + .filtered( + focalPlane = s.focalPlane, + capability = s.capability, + wavelength = s.wavelength, + slitLength = s.focalPlaneAngle.map(s => SlitLength(ModeSlitSize(s))), + resolution = s.resolution, + range = s.wavelengthCoverage, + declination = dec + ) + val sortedRows: List[SpectroscopyModeRow] = rows.sortBy(_.enabledRow) + // Computes the mode overrides for the current parameters + val fixedModeRows: List[SpectroscopyModeRow] = + sortedRows + .map(_.withModeOverridesFor(w, profiles, constraints.imageQuality)) + .flattenOption + fixedModeRows.map: row => + SpectroscopyModeRowWithResult( + row, + itcResults.forRow( + s.wavelength, + s.signalToNoise, + s.signalToNoiseAt, + constraints, + asterism, + row + ), + s.wavelength.flatMap: w => + ModeCommonWavelengths.wavelengthInterval(w)(row), + row.instrument.configurationSummary ) - val sortedRows = rows.sortBy(_.enabledRow) - sortedRows.map: row => - SpectroscopyModeRowWithResult( - row, - itc.forRow( - s.wavelength, - s.signalToNoise, - s.signalToNoiseAt, - constraints, - asterism, - row - ), - s.wavelength.flatMap: w => - ModeCommonWavelengths.wavelengthInterval(w)(row), - row.rowToConf(s.wavelength).map(_.configurationSummary) - ) + }.orEmpty .useState(none[Progress]) // itcProgress .useMemoBy((props, _, itcResults, rows, _) => // Calculate the common errors (props.spectroscopyRequirements.wavelength, @@ -448,8 +423,8 @@ private object SpectroscopyModesTable: .useStateBy: (props, _, _, rows, _, _, _, _, _) => // selectedRow props.selectedConfig.get .flatMap: c => - rows.value.find: - _.equalsConf(c.configuration, props.spectroscopyRequirements.wavelength) + rows.value.find: row => + c.instrumentConfig === row.entry.instrument .map(_.entry) // selectedIndex // The selected index needs to be the index into the sorted data, because that is what @@ -464,8 +439,11 @@ private object SpectroscopyModesTable: .useEffectWithDepsBy((_, _, _, rows, _, _, _, _, _, _, _) => rows): (props, _, _, _, _, _, _, _, sortedRows, _, selectedIndex) => _ => - val optRow = selectedIndex.value.flatMap(idx => sortedRows.lift(idx)) - val conf = optRow.flatMap(_.rowToConfAndItc(props.spectroscopyRequirements.wavelength)) + val optRow: Option[SpectroscopyModeRowWithResult] = + selectedIndex.value.flatMap(idx => sortedRows.lift(idx)) + val conf: Option[InstrumentConfigAndItcResult] = + optRow.map: row => + InstrumentConfigAndItcResult(row.entry.instrument, row.result.some) if (props.selectedConfig.get =!= conf) props.selectedConfig.set(conf) else Callback.empty @@ -519,12 +497,13 @@ private object SpectroscopyModesTable: row.entry.instrument.instrument match case Instrument.GmosNorth | Instrument.GmosSouth => cache.contains: - ItcRequestParams(w, - sn, - snAt, - constraints, - asterism, - row.entry.instrument + ItcRequestParams( + w, + sn, + snAt, + constraints, + asterism, + row.entry.instrument ) case _ => true @@ -600,10 +579,11 @@ private object SpectroscopyModesTable: def toggleRow( row: SpectroscopyModeRowWithResult - ): Option[explore.model.BasicConfigAndItc] = - row - .rowToConfAndItc(props.spectroscopyRequirements.wavelength) - .filterNot(conf => props.selectedConfig.get.contains_(conf)) + ): Option[InstrumentConfigAndItcResult] = + Option.when( + props.selectedConfig.get.forall(_.instrumentConfig =!= row.entry.instrument) + ): + InstrumentConfigAndItcResult(row.entry.instrument, row.result.some) def scrollButton(content: VdomNode, style: Css, indexCondition: Int => Boolean): TagMod = selectedIndex.value.whenDefined(idx => @@ -675,13 +655,10 @@ private object SpectroscopyModesTable: TagMod( ^.disabled := !row.original.entry.enabledRow, ExploreStyles.TableRowSelected - .when( - props.selectedConfig.get.exists(c => - row.original.equalsConf(c.configuration, - props.spectroscopyRequirements.wavelength - ) - ) - ), + .when: + props.selectedConfig.get + .exists(_.instrumentConfig === row.original.entry.instrument) + , ( ^.onClick --> ( props.selectedConfig.set(toggleRow(row.original)) >> diff --git a/explore/src/main/scala/explore/itc/ItcProps.scala b/explore/src/main/scala/explore/itc/ItcProps.scala index b0dcd267f9..3a2ff74252 100644 --- a/explore/src/main/scala/explore/itc/ItcProps.scala +++ b/explore/src/main/scala/explore/itc/ItcProps.scala @@ -10,7 +10,7 @@ import cats.derived.* import cats.effect.IO import cats.syntax.all.* import explore.events.ItcMessage -import explore.model.BasicConfigAndItc +import explore.model.InstrumentConfigAndItcResult import explore.model.Observation import explore.model.ScienceRequirements import explore.model.TargetList @@ -18,11 +18,8 @@ import explore.model.WorkerClients.ItcClient import explore.model.boopickle.ItcPicklers.given import explore.model.itc.* import explore.model.reusability.given -import explore.modes.GmosNorthSpectroscopyRow -import explore.modes.GmosSouthSpectroscopyRow -import explore.modes.GmosSpectroscopyOverrides +import explore.modes.InstrumentConfig import explore.modes.InstrumentOverrides -import explore.modes.InstrumentRow import japgolly.scalajs.react.Reusability import lucuma.core.enums.Band import lucuma.core.math.BrightnessValue @@ -31,7 +28,6 @@ import lucuma.core.math.Wavelength import lucuma.core.math.dimensional.Units import lucuma.core.model.SourceProfile import lucuma.core.model.Target -import lucuma.schemas.model.BasicConfiguration import lucuma.schemas.model.CentralWavelength import lucuma.ui.reusability.given import queries.schemas.itc.syntax.* @@ -41,17 +37,15 @@ import scala.collection.immutable.SortedMap case class ItcProps( observation: Observation, - selectedConfig: Option[BasicConfigAndItc], // selected row in spectroscopy modes table - at: TargetList, - modeOverrides: Option[InstrumentOverrides] + selectedConfig: Option[InstrumentConfigAndItcResult], // selected row in spectroscopy modes table + at: TargetList ) derives Eq: private val spectroscopyRequirements: Option[ScienceRequirements.Spectroscopy] = ScienceRequirements.spectroscopy.getOption(observation.scienceRequirements) private val allTargets: TargetList = - SortedMap.from( + SortedMap.from: at.view.mapValues(Target.sourceProfile.modify(_.gaiaFree)) - ) private val constraints = observation.constraints private val asterismIds = observation.scienceTargetIds @@ -59,16 +53,15 @@ case class ItcProps( // The remote configuration is read in a different query than the itc results // This will work even in the case the user has overriden some parameters // When we use the remote configuration we don't need the exposure time. - private val remoteConfig = observation.observingMode.map { o => - BasicConfigAndItc( - o.toBasicConfiguration, - none - ) - } + private val remoteConfig: Option[InstrumentConfigAndItcResult] = + observation + .toInstrumentConfig(at) + .map: row => + InstrumentConfigAndItcResult(row, none) // The user may select a configuration on the modes tables, we'd prefer than but if not // we can try with the remote confiiguration provided by the database - val finalConfig: Option[BasicConfigAndItc] = + val finalConfig: Option[InstrumentConfigAndItcResult] = selectedConfig.orElse(remoteConfig) val signalToNoise: Option[SignalToNoise] = @@ -79,31 +72,26 @@ case class ItcProps( private val wavelength: Option[CentralWavelength] = finalConfig - .map(_.configuration) - .map: - case BasicConfiguration.GmosNorthLongSlit(_, _, _, cw) => - modeOverrides match - case Some(GmosSpectroscopyOverrides(overrideCw, _, _)) => overrideCw - case _ => cw - case BasicConfiguration.GmosSouthLongSlit(_, _, _, cw) => - modeOverrides match - case Some(GmosSpectroscopyOverrides(overrideCw, _, _)) => overrideCw - case _ => cw - - private val instrumentRow: Option[InstrumentRow] = - finalConfig - .map(_.configuration) - .map: - case BasicConfiguration.GmosNorthLongSlit(grating, filter, fpu, _) => - val gmosOverride: Option[GmosSpectroscopyOverrides] = modeOverrides match - case Some(o @ GmosSpectroscopyOverrides(_, _, _)) => o.some - case _ => none - GmosNorthSpectroscopyRow(grating, fpu, filter, gmosOverride) - case BasicConfiguration.GmosSouthLongSlit(grating, filter, fpu, _) => - val gmosOverride: Option[GmosSpectroscopyOverrides] = modeOverrides match - case Some(o @ GmosSpectroscopyOverrides(_, _, _)) => o.some - case _ => none - GmosSouthSpectroscopyRow(grating, fpu, filter, gmosOverride) + .map(_.instrumentConfig) + .flatMap: + case InstrumentConfig.GmosNorthSpectroscopy( + _, + _, + _, + Some(InstrumentOverrides.GmosSpectroscopy(cw, _, _)) + ) => + cw.some + case InstrumentConfig.GmosSouthSpectroscopy( + _, + _, + _, + Some(InstrumentOverrides.GmosSpectroscopy(cw, _, _)) + ) => + cw.some + case _ => none + + private val instrumentConfig: Option[InstrumentConfig] = + finalConfig.map(_.instrumentConfig) val itcTargets: Option[NonEmptyList[ItcTarget]] = asterismIds.itcTargets(allTargets).filter(_.canQueryITC).toNel @@ -111,7 +99,7 @@ case class ItcProps( val targets: List[ItcTarget] = itcTargets.foldMap(_.toList) private val queryProps: List[Option[?]] = - List(itcTargets, finalConfig, wavelength, instrumentRow, signalToNoise) + List(itcTargets, finalConfig, wavelength, instrumentConfig, signalToNoise) val isExecutable: Boolean = queryProps.forall(_.isDefined) @@ -132,7 +120,7 @@ case class ItcProps( sn <- signalToNoise snAt <- signalToNoiseAt t <- itcTargets - mode <- instrumentRow + mode <- instrumentConfig yield ItcClient[IO] .requestSingle: ItcMessage.GraphQuery(w, sn, snAt, constraints, t, mode) @@ -153,6 +141,5 @@ object ItcProps: p.observation.wavelength, p.observation.basicConfiguration, p.selectedConfig, - p.at, - p.modeOverrides + p.at ) diff --git a/explore/src/main/scala/explore/itc/ItcResultsCache.scala b/explore/src/main/scala/explore/itc/ItcResultsCache.scala index c8ac7e321b..35bad965c5 100644 --- a/explore/src/main/scala/explore/itc/ItcResultsCache.scala +++ b/explore/src/main/scala/explore/itc/ItcResultsCache.scala @@ -35,7 +35,7 @@ case class ItcResultsCache( def signalToNoiseAt(w: Option[Wavelength]): EitherNec[ItcQueryProblem, Wavelength] = Either.fromOption(w, NonEmptyChain.of(ItcQueryProblem.MissingSignalToNoiseAt)) - def mode(r: SpectroscopyModeRow): EitherNec[ItcQueryProblem, InstrumentRow] = + def mode(r: SpectroscopyModeRow): EitherNec[ItcQueryProblem, InstrumentConfig] = Either.fromOption( ItcResultsCache.enabledRow(r).option(r.instrument), NonEmptyChain.of(ItcQueryProblem.UnsupportedMode) diff --git a/explore/src/main/scala/explore/tabs/ConfigurationTile.scala b/explore/src/main/scala/explore/tabs/ConfigurationTile.scala index f5d16cd211..2a69eba6c4 100644 --- a/explore/src/main/scala/explore/tabs/ConfigurationTile.scala +++ b/explore/src/main/scala/explore/tabs/ConfigurationTile.scala @@ -10,13 +10,14 @@ import explore.components.Tile import explore.components.ui.ExploreStyles import explore.config.ConfigurationPanel import explore.model.AsterismIds -import explore.model.BasicConfigAndItc +import explore.model.InstrumentConfigAndItcResult import explore.model.ObsConfiguration import explore.model.ObsTabTileIds import explore.model.Observation import explore.model.ScienceRequirements import explore.model.TargetList import explore.model.enums.WavelengthUnits +import explore.modes.InstrumentConfig import explore.modes.SpectroscopyModesMatrix import explore.undo.* import japgolly.scalajs.react.Callback @@ -31,21 +32,22 @@ import queries.schemas.itc.syntax.* object ConfigurationTile: def configurationTile( - userId: Option[User.Id], - programId: Program.Id, - obsId: Observation.Id, - requirements: UndoSetter[ScienceRequirements], - mode: UndoSetter[Option[ObservingMode]], - posAngleConstraint: View[PosAngleConstraint], - scienceTargetIds: AsterismIds, - baseCoordinates: Option[CoordinatesAtVizTime], - obsConf: ObsConfiguration, - selectedConfig: View[Option[BasicConfigAndItc]], - modes: SpectroscopyModesMatrix, - allTargets: TargetList, - sequenceChanged: Callback, - readonly: Boolean, - units: WavelengthUnits + userId: Option[User.Id], + programId: Program.Id, + obsId: Observation.Id, + requirements: UndoSetter[ScienceRequirements], + mode: UndoSetter[Option[ObservingMode]], + posAngleConstraint: View[PosAngleConstraint], + scienceTargetIds: AsterismIds, + baseCoordinates: Option[CoordinatesAtVizTime], + obsConf: ObsConfiguration, + selectedConfig: View[Option[InstrumentConfigAndItcResult]], + revertedInstrumentConfig: Option[InstrumentConfig], // configuration selected if reverted + modes: SpectroscopyModesMatrix, + allTargets: TargetList, + sequenceChanged: Callback, + readonly: Boolean, + units: WavelengthUnits )(using Logger[IO]) = Tile( ObsTabTileIds.ConfigurationId.id, @@ -63,6 +65,7 @@ object ConfigurationTile: scienceTargetIds.itcTargets(allTargets), baseCoordinates, selectedConfig, + revertedInstrumentConfig, modes, sequenceChanged, readonly, diff --git a/explore/src/main/scala/explore/tabs/ObsTabTiles.scala b/explore/src/main/scala/explore/tabs/ObsTabTiles.scala index 4b501f84f4..e42dcef0f5 100644 --- a/explore/src/main/scala/explore/tabs/ObsTabTiles.scala +++ b/explore/src/main/scala/explore/tabs/ObsTabTiles.scala @@ -3,6 +3,7 @@ package explore.tabs +import cats.Order.given import cats.data.NonEmptyList import cats.effect.IO import cats.syntax.all.* @@ -35,7 +36,6 @@ import explore.model.enums.AgsState import explore.model.enums.AppTab import explore.model.enums.GridLayoutSection import explore.model.extensions.* -import explore.model.itc.ItcAsterismGraphResults import explore.model.layout.* import explore.modes.SpectroscopyModesMatrix import explore.observationtree.obsEditAttachments @@ -74,10 +74,11 @@ import lucuma.ui.sso.UserVault import lucuma.ui.syntax.all.* import lucuma.ui.syntax.all.given import queries.common.ObsQueriesGQL.* +import queries.schemas.itc.syntax.* import queries.schemas.odb.ObsQueries -import queries.schemas.odb.ObsQueries.* import java.time.Instant +import scala.collection.immutable.SortedMap import scala.collection.immutable.SortedSet case class ObsTabTiles( @@ -104,11 +105,11 @@ case class ObsTabTiles( val targetObservations: Map[Target.Id, SortedSet[Observation.Id]] = programSummaries.targetObservations val obsExecution: Pot[Execution] = programSummaries.obsExecutionPots.getPot(obsId) - val allTargets: TargetList = programSummaries.targets + val obsTargets: TargetList = programSummaries.obsTargets.get(obsId).getOrElse(SortedMap.empty) val obsAttachmentAssignments: ObsAttachmentAssignmentMap = programSummaries.obsAttachmentAssignments val asterismTracking: Option[ObjectTracking] = - observation.get.asterismTracking(allTargets) + observation.get.asterismTracking(obsTargets) object ObsTabTiles: private type Props = ObsTabTiles @@ -135,13 +136,6 @@ object ObsTabTiles: ) ) - private def itcQueryProps( - obs: Observation, - selectedConfig: Option[BasicConfigAndItc], - targetList: TargetList - ): ItcProps = - ItcProps(obs, selectedConfig, targetList, obs.toModeOverride(targetList)) - private case class Offsets( science: Option[NonEmptyList[Offset]], acquisition: Option[NonEmptyList[Offset]] @@ -175,23 +169,14 @@ object ObsTabTiles: // Ags state .useStateView[AgsState](AgsState.Idle) // the configuration the user has selected from the spectroscopy modes table, if any - .useStateView(none[BasicConfigAndItc]) - .useStateWithReuseBy: (props, _, _, _, selectedConfig) => - itcQueryProps(props.observation.get, selectedConfig.get, props.allTargets) - .useState(Pot.pending[ItcAsterismGraphResults]) // itcGraphResults - .useAsyncEffectWithDepsBy((props, _, _, _, selectedConfig, _, _) => - itcQueryProps(props.observation.get, selectedConfig.get, props.allTargets) - ): (props, ctx, _, _, _, oldItcProps, itcGraphResults) => - itcProps => + .useStateView(none[InstrumentConfigAndItcResult]) + .localValBy: (props, _, _, _, selectedConfig) => // itcProps + ItcProps(props.observation.get, selectedConfig.get, props.obsTargets) + .useEffectResultWithDepsBy((_, _, _, _, _, itcProps) => itcProps): (_, ctx, _, _, _, _) => + itcProps => // Compute ITC graph import ctx.given - - oldItcProps.setStateAsync(itcProps) >> - itcGraphResults.setStateAsync(Pot.pending) >> - itcProps.requestGraphs.attemptPot - .flatMap: result => - itcGraphResults.setStateAsync(result) - // Signal that the sequence has changed - .useStateView(().ready) + itcProps.requestGraphs + .useStateView(().ready) // Signal that the sequence has changed .useEffectKeepResultWithDepsBy((p, _, _, _, _, _, _, _) => p.observation.model.get.observationTime ): (_, _, _, _, _, _, _, _) => @@ -274,9 +259,7 @@ object ObsTabTiles: val asterismAsNel: Option[NonEmptyList[TargetWithId]] = NonEmptyList.fromList: - props.observation.get.scienceTargetIds.toList - .map(id => props.allTargets.get(id).map(t => TargetWithId(id, t))) - .flattenOption + props.obsTargets.toList.map((tid, t) => TargetWithId(tid, t)) // asterism base coordinates at viz time or current time val targetCoords: Option[CoordinatesAtVizTime] = @@ -357,9 +340,9 @@ object ObsTabTiles: ItcTile.itcTile( props.vault.userId, props.obsId, - props.allTargets, - itcProps.value, - itcGraphResults.value, + props.obsTargets, + itcProps, + itcGraphResults, props.globalPreferences ) @@ -497,7 +480,7 @@ object ObsTabTiles: val timingWindowsTile = TimingWindowsTile.timingWindowsPanel(timingWindows, props.isDisabled, false) - val configurationTile = + val configurationTile: Tile[?] = ConfigurationTile.configurationTile( props.vault.userId, props.programId, @@ -509,12 +492,13 @@ object ObsTabTiles: targetCoords, obsConf, selectedConfig, + props.observation.get.toInstrumentConfig(props.obsTargets), props.modes, - props.allTargets, - sequenceChanged.mod { + props.obsTargets, + sequenceChanged.mod: case Ready(x) => Pot.pending case x => x - }, + , props.isDisabled, props.globalPreferences.get.wavelengthUnits ) diff --git a/model/shared/src/main/scala/explore/events/ItcMessage.scala b/model/shared/src/main/scala/explore/events/ItcMessage.scala index 380a3fea49..a860bc951e 100644 --- a/model/shared/src/main/scala/explore/events/ItcMessage.scala +++ b/model/shared/src/main/scala/explore/events/ItcMessage.scala @@ -11,7 +11,7 @@ import explore.model.itc.ItcRequestParams import explore.model.itc.ItcResult import explore.model.itc.ItcTarget import explore.model.itc.ItcTargetProblem -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import explore.modes.SpectroscopyModeRow import lucuma.core.math.SignalToNoise import lucuma.core.math.Wavelength @@ -45,7 +45,7 @@ object ItcMessage extends ItcPicklers: signalToNoiseAt: Wavelength, constraints: ConstraintSet, asterism: NonEmptyList[ItcTarget], - modes: InstrumentRow + modes: InstrumentConfig ) extends Request: type ResponseType = ItcAsterismGraphResults diff --git a/model/shared/src/main/scala/explore/givens/package.scala b/model/shared/src/main/scala/explore/givens/package.scala new file mode 100644 index 0000000000..a301f2fb77 --- /dev/null +++ b/model/shared/src/main/scala/explore/givens/package.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.givens + +import cats.Endo +import cats.Monoid +import cats.MonoidK + +given [A]: Monoid[Endo[A]] = MonoidK[Endo].algebra[A] diff --git a/model/shared/src/main/scala/explore/model/BasicConfigAndItc.scala b/model/shared/src/main/scala/explore/model/BasicConfigAndItc.scala deleted file mode 100644 index 4f57c055a2..0000000000 --- a/model/shared/src/main/scala/explore/model/BasicConfigAndItc.scala +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package explore.model - -import cats.Eq -import cats.data.EitherNec -import cats.derived.* -import explore.model.itc.ItcResult -import explore.model.itc.ItcTargetProblem -import lucuma.schemas.model.BasicConfiguration -import monocle.Focus -import monocle.Lens - -case class BasicConfigAndItc( - configuration: BasicConfiguration, - itcResult: Option[EitherNec[ItcTargetProblem, ItcResult]] -) derives Eq - -object BasicConfigAndItc: - val configuration: Lens[BasicConfigAndItc, BasicConfiguration] = - Focus[BasicConfigAndItc](_.configuration) - - val itcResult: Lens[BasicConfigAndItc, Option[EitherNec[ItcTargetProblem, ItcResult]]] = - Focus[BasicConfigAndItc](_.itcResult) diff --git a/model/shared/src/main/scala/explore/model/InstrumentConfigAndItcResult.scala b/model/shared/src/main/scala/explore/model/InstrumentConfigAndItcResult.scala new file mode 100644 index 0000000000..3a0a263cbb --- /dev/null +++ b/model/shared/src/main/scala/explore/model/InstrumentConfigAndItcResult.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.model + +import cats.Eq +import cats.data.EitherNec +import cats.derived.* +import cats.syntax.all.* +import explore.model.itc.ItcResult +import explore.model.itc.ItcTargetProblem +import explore.modes.InstrumentConfig +import lucuma.schemas.model.BasicConfiguration +import monocle.Focus +import monocle.Lens + +case class InstrumentConfigAndItcResult( + instrumentConfig: InstrumentConfig, + itcResult: Option[EitherNec[ItcTargetProblem, ItcResult]] +) derives Eq: + def toBasicConfiguration: Option[BasicConfiguration] = + instrumentConfig match + case InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, filter, Some(cw, _, _)) => + BasicConfiguration.GmosNorthLongSlit(grating, filter, fpu, cw).some + case InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, filter, Some(cw, _, _)) => + BasicConfiguration.GmosSouthLongSlit(grating, filter, fpu, cw).some + case _ => none + +object InstrumentConfigAndItcResult: + val configuration: Lens[InstrumentConfigAndItcResult, InstrumentConfig] = + Focus[InstrumentConfigAndItcResult](_.instrumentConfig) + + val itcResult + : Lens[InstrumentConfigAndItcResult, Option[EitherNec[ItcTargetProblem, ItcResult]]] = + Focus[InstrumentConfigAndItcResult](_.itcResult) diff --git a/model/shared/src/main/scala/explore/model/Observation.scala b/model/shared/src/main/scala/explore/model/Observation.scala index 24754940f2..eaabb4e000 100644 --- a/model/shared/src/main/scala/explore/model/Observation.scala +++ b/model/shared/src/main/scala/explore/model/Observation.scala @@ -10,14 +10,17 @@ import cats.derived.* import cats.syntax.all.* import eu.timepit.refined.cats.* import eu.timepit.refined.types.string.NonEmptyString +import explore.givens.given import explore.model.syntax.all.* -import explore.modes.GmosSpectroscopyOverrides +import explore.modes.InstrumentConfig import explore.modes.InstrumentOverrides +import explore.modes.syntax.* import io.circe.Decoder import io.circe.generic.semiauto.* import io.circe.refined.given import lucuma.core.enums.CalibrationRole -import lucuma.core.enums.GmosAmpCount +import lucuma.core.enums.GmosAmpGain +import lucuma.core.enums.GmosAmpReadMode import lucuma.core.enums.GmosXBinning import lucuma.core.enums.GmosYBinning import lucuma.core.enums.ObservationValidationCode @@ -35,7 +38,6 @@ import lucuma.core.model.SourceProfile import lucuma.core.model.Target import lucuma.core.model.TimingWindow import lucuma.core.model.sequence.gmos.GmosCcdMode -import lucuma.core.model.sequence.gmos.longslit.* import lucuma.core.util.Enumerated import lucuma.core.util.TimeSpan import lucuma.core.util.Timestamp @@ -87,17 +89,25 @@ case class Observation( val needsAGS: Boolean = calibrationRole.forall(_.needsAGS) - // For multiple targets, we take the smallest binning for each axis. - // https://docs.google.com/document/d/1P8_pXLRVomUSvofyVkAniOyGROcAtiJ7EMYt9wWXB0o/edit?disco=AAAA32SmtD4 - private def asterismBinning( - bs: NonEmptyList[(GmosXBinning, GmosYBinning)] - ): (GmosXBinning, GmosYBinning) = - (bs.map(_._1).minimumBy(_.count), bs.map(_._2).minimumBy(_.count)) - private def profiles(targets: TargetList): Option[NonEmptyList[SourceProfile]] = NonEmptyList.fromList: scienceTargetIds.toList.map(targets.get).flattenOption.map(_.sourceProfile) + private def applyGmosCcdModesOverrides( + explicitXBinning: Option[GmosXBinning], + explicitYBinning: Option[GmosYBinning], + explicitAmpReadMode: Option[GmosAmpReadMode], + defaultAmpReadMode: GmosAmpReadMode, + explicitAmpGain: Option[GmosAmpGain], + defaultAmpGain: GmosAmpGain + ): GmosCcdMode => GmosCcdMode = + List( + explicitXBinning.foldMap(GmosCcdMode.xBin.replace), + explicitYBinning.foldMap(GmosCcdMode.yBin.replace), + GmosCcdMode.ampReadMode.replace(explicitAmpReadMode.getOrElse(defaultAmpReadMode)), + GmosCcdMode.ampGain.replace(explicitAmpGain.getOrElse(defaultAmpGain)) + ).reduce(_ >>> _) + def toModeOverride(targets: TargetList): Option[InstrumentOverrides] = observingMode.flatMap: case ObservingMode.GmosNorthLongSlit( @@ -117,7 +127,7 @@ case class Observation( explicitAmpReadMode, defaultAmpGain, explicitAmpGain, - _, + defaultRoi, explicitRoi, _, _, @@ -125,20 +135,23 @@ case class Observation( _ ) => profiles(targets).map: ps => - val (defaultXBinning, defaultYBinning) = - if (fpu.isIFU) (GmosXBinning.One, GmosYBinning.One) - else asterismBinning(ps.map(northBinning(fpu, _, constraints.imageQuality, grating))) + val defaultMode: GmosCcdMode = + GmosCcdMode.defaultGmosNorth(ps, fpu, grating, constraints.imageQuality) - GmosSpectroscopyOverrides( + val mode: GmosCcdMode = + applyGmosCcdModesOverrides( + explicitXBinning, + explicitYBinning, + explicitAmpReadMode, + defaultAmpReadMode, + explicitAmpGain, + defaultAmpGain + )(defaultMode) + + InstrumentOverrides.GmosSpectroscopy( centralWavelength, - GmosCcdMode( - explicitXBinning.getOrElse(defaultXBinning), - explicitYBinning.getOrElse(defaultYBinning), - GmosAmpCount.Twelve, - explicitAmpGain.getOrElse(defaultAmpGain), - explicitAmpReadMode.getOrElse(defaultAmpReadMode) - ).some, - explicitRoi + mode, + explicitRoi.getOrElse(defaultRoi) ) case ObservingMode.GmosSouthLongSlit( _, @@ -157,7 +170,7 @@ case class Observation( explicitAmpReadMode, defaultAmpGain, explicitAmpGain, - _, + defaultRoi, explicitRoi, _, _, @@ -165,22 +178,84 @@ case class Observation( _ ) => profiles(targets).map: ps => - val (defaultXBinning, defaultYBinning) = - if (fpu.isIFU) (GmosXBinning.One, GmosYBinning.One) - else asterismBinning(ps.map(southBinning(fpu, _, constraints.imageQuality, grating))) + val defaultMode: GmosCcdMode = + GmosCcdMode.defaultGmosSouth(ps, fpu, grating, constraints.imageQuality) + + val mode: GmosCcdMode = applyGmosCcdModesOverrides( + explicitXBinning, + explicitYBinning, + explicitAmpReadMode, + defaultAmpReadMode, + explicitAmpGain, + defaultAmpGain + )(defaultMode) - GmosSpectroscopyOverrides( + InstrumentOverrides.GmosSpectroscopy( centralWavelength, - GmosCcdMode( - explicitXBinning.getOrElse(defaultXBinning), - explicitYBinning.getOrElse(defaultYBinning), - GmosAmpCount.Twelve, - explicitAmpGain.getOrElse(defaultAmpGain), - explicitAmpReadMode.getOrElse(defaultAmpReadMode) - ).some, - explicitRoi + mode, + explicitRoi.getOrElse(defaultRoi) ) + def toInstrumentConfig(targets: TargetList): Option[InstrumentConfig] = + (toModeOverride(targets), observingMode) + .mapN: + case (overrides @ InstrumentOverrides.GmosSpectroscopy(_, _, _), + ObservingMode.GmosNorthLongSlit( + _, + grating, + _, + filter, + _, + fpu, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) + ) => + InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, filter, overrides.some).some + case (overrides @ InstrumentOverrides.GmosSpectroscopy(_, _, _), + ObservingMode.GmosSouthLongSlit( + _, + grating, + _, + filter, + _, + fpu, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) + ) => + InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, filter, overrides.some).some + case _ => none + .flatten + lazy val constraintsSummary: String = s"${constraints.imageQuality.label} ${constraints.cloudExtinction.label} ${constraints.skyBackground.label} ${constraints.waterVapor.label}" diff --git a/model/shared/src/main/scala/explore/model/ProgramSummaries.scala b/model/shared/src/main/scala/explore/model/ProgramSummaries.scala index 3e7c10daa3..59db8d25e3 100644 --- a/model/shared/src/main/scala/explore/model/ProgramSummaries.scala +++ b/model/shared/src/main/scala/explore/model/ProgramSummaries.scala @@ -66,6 +66,16 @@ case class ProgramSummaries( .mapValues(obsIds => SortedSet.from(obsIds)) .toMap + lazy val obsTargets: Map[Observation.Id, TargetList] = + observations.toList + .map: obs => + obs.id -> + SortedMap.from: + obs.scienceTargetIds.toList + .map(tid => targets.get(tid).map(t => tid -> t)) + .flattenOption + .toMap + lazy val obsAttachmentAssignments: ObsAttachmentAssignmentMap = observations.toList .flatMap(obs => obs.attachmentIds.map(_ -> obs.id)) diff --git a/model/shared/src/main/scala/explore/model/boopickle/ItcPickler.scala b/model/shared/src/main/scala/explore/model/boopickle/ItcPickler.scala index 0c6e5e0f02..d051cd77a6 100644 --- a/model/shared/src/main/scala/explore/model/boopickle/ItcPickler.scala +++ b/model/shared/src/main/scala/explore/model/boopickle/ItcPickler.scala @@ -21,7 +21,7 @@ import explore.model.itc.ItcTarget import explore.model.itc.ItcTargetProblem import explore.model.itc.OverridenExposureTime import explore.modes.* -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import explore.modes.ModeAO import explore.modes.ModeSlitSize import explore.modes.ModeWavelength @@ -82,28 +82,28 @@ trait ItcPicklers extends CommonPicklers { given Pickler[GmosCcdMode] = generatePickler - given Pickler[GmosSpectroscopyOverrides] = generatePickler + given Pickler[InstrumentOverrides.GmosSpectroscopy] = generatePickler - given Pickler[GmosNorthSpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.GmosNorthSpectroscopy] = generatePickler - given Pickler[GmosSouthSpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.GmosSouthSpectroscopy] = generatePickler - given Pickler[Flamingos2SpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.Flamingos2Spectroscopy] = generatePickler - given Pickler[GpiSpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.GpiSpectroscopy] = generatePickler - given Pickler[GnirsSpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.GnirsSpectroscopy] = generatePickler - given Pickler[GenericSpectroscopyRow] = generatePickler + given Pickler[InstrumentConfig.GenericSpectroscopy] = generatePickler - given Pickler[InstrumentRow] = - compositePickler[InstrumentRow] - .addConcreteType[GmosNorthSpectroscopyRow] - .addConcreteType[GmosSouthSpectroscopyRow] - .addConcreteType[Flamingos2SpectroscopyRow] - .addConcreteType[GpiSpectroscopyRow] - .addConcreteType[GnirsSpectroscopyRow] - .addConcreteType[GenericSpectroscopyRow] + given Pickler[InstrumentConfig] = + compositePickler[InstrumentConfig] + .addConcreteType[InstrumentConfig.GmosNorthSpectroscopy] + .addConcreteType[InstrumentConfig.GmosSouthSpectroscopy] + .addConcreteType[InstrumentConfig.Flamingos2Spectroscopy] + .addConcreteType[InstrumentConfig.GpiSpectroscopy] + .addConcreteType[InstrumentConfig.GnirsSpectroscopy] + .addConcreteType[InstrumentConfig.GenericSpectroscopy] given Pickler[ModeWavelength] = picklerNewType(ModeWavelength) diff --git a/model/shared/src/main/scala/explore/model/display.scala b/model/shared/src/main/scala/explore/model/display.scala index da73d4340f..9de1ea3a68 100644 --- a/model/shared/src/main/scala/explore/model/display.scala +++ b/model/shared/src/main/scala/explore/model/display.scala @@ -7,6 +7,7 @@ import cats.syntax.all.* import eu.timepit.refined.cats.* import explore.model.enums.WavelengthUnits import explore.model.itc.ItcQueryProblem +import explore.modes.InstrumentConfig import lucuma.core.enums.* import lucuma.core.enums.EducationalStatus import lucuma.core.enums.ObservationWorkflowState @@ -238,4 +239,14 @@ trait DisplayImplicits: case BasicConfiguration.GmosSouthLongSlit(grating, _, fpu, _) => s"GMOS-S ${grating.shortName} ${fpu.shortName}" + extension (revertedInstrumentConfig: InstrumentConfig) + def configurationSummary: Option[String] = + revertedInstrumentConfig match + case InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, _, _) => + s"GMOS-N ${grating.shortName} ${fpu.shortName}".some + case InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, _, _) => + s"GMOS-S ${grating.shortName} ${fpu.shortName}".some + case _ => + none + object display extends DisplayImplicits diff --git a/model/shared/src/main/scala/explore/model/itc/ItcRequestParams.scala b/model/shared/src/main/scala/explore/model/itc/ItcRequestParams.scala index 9636dbf764..6d46bcc00a 100644 --- a/model/shared/src/main/scala/explore/model/itc/ItcRequestParams.scala +++ b/model/shared/src/main/scala/explore/model/itc/ItcRequestParams.scala @@ -4,7 +4,7 @@ package explore.model.itc import cats.data.* -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import lucuma.core.math.SignalToNoise import lucuma.core.math.Wavelength import lucuma.core.model.ConstraintSet @@ -16,7 +16,7 @@ case class ItcRequestParams( signalToNoiseAt: Wavelength, constraints: ConstraintSet, asterism: NonEmptyList[ItcTarget], - mode: InstrumentRow + mode: InstrumentConfig ) case class ItcGraphRequestParams( @@ -25,5 +25,5 @@ case class ItcGraphRequestParams( signalToNoiseAt: Wavelength, constraints: ConstraintSet, asterism: NonEmptyList[ItcTarget], - mode: InstrumentRow + mode: InstrumentConfig ) diff --git a/model/shared/src/main/scala/explore/model/package.scala b/model/shared/src/main/scala/explore/model/package.scala index e987290aae..5353a3437e 100644 --- a/model/shared/src/main/scala/explore/model/package.scala +++ b/model/shared/src/main/scala/explore/model/package.scala @@ -55,9 +55,6 @@ object AsterismIds: type AsterismGroupList = SortedMap[ObsIdSet, AsterismIds] type TargetList = SortedMap[Target.Id, Target] type TargetWithObsList = SortedMap[Target.Id, TargetWithObs] -// KeyedIndexedList is only useful is manual order is going to matter. -// For the moment I'm keeping it because it seems it will matter at some point. -// Otherwise, we should change to a SortedMap. type ObservationList = KeyedIndexedList[Observation.Id, Observation] type ConstraintGroupList = SortedMap[ObsIdSet, ConstraintSet] type SchedulingGroupList = SortedMap[ObsIdSet, List[TimingWindow]] diff --git a/model/shared/src/main/scala/explore/modes/InstrumentConfig.scala b/model/shared/src/main/scala/explore/modes/InstrumentConfig.scala new file mode 100644 index 0000000000..69eb64a887 --- /dev/null +++ b/model/shared/src/main/scala/explore/modes/InstrumentConfig.scala @@ -0,0 +1,126 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.modes + +import cats.Eq +import cats.derived.* +import cats.implicits.* +import eu.timepit.refined.* +import eu.timepit.refined.cats.* +import eu.timepit.refined.types.string.* +import lucuma.core.enums.* +import lucuma.core.math.Wavelength +import lucuma.core.math.units.* +import lucuma.core.model.sequence.gmos.GmosCcdMode +import lucuma.core.util.Enumerated +import lucuma.schemas.model.CentralWavelength +import monocle.Getter + +sealed trait InstrumentConfig derives Eq: + def instrument: Instrument + + type Grating + val grating: Grating + + type FPU + val fpu: FPU + + type Filter + val filter: Filter + + val site: Site + + def hasFilter: Boolean + + type Override + def modeOverrides: Option[Override] = None + +object InstrumentConfig: + case class GmosNorthSpectroscopy( + grating: GmosNorthGrating, + fpu: GmosNorthFpu, + filter: Option[GmosNorthFilter], + override val modeOverrides: Option[InstrumentOverrides.GmosSpectroscopy] + ) extends InstrumentConfig derives Eq { + type Grating = GmosNorthGrating + type Filter = Option[GmosNorthFilter] + type FPU = GmosNorthFpu + type Override = InstrumentOverrides.GmosSpectroscopy + val instrument = Instrument.GmosNorth + val site = Site.GN + val hasFilter = filter.isDefined + } + + case class GmosSouthSpectroscopy( + grating: GmosSouthGrating, + fpu: GmosSouthFpu, + filter: Option[GmosSouthFilter], + override val modeOverrides: Option[InstrumentOverrides.GmosSpectroscopy] + ) extends InstrumentConfig derives Eq { + type Grating = GmosSouthGrating + type Filter = Option[GmosSouthFilter] + type FPU = GmosSouthFpu + type Override = InstrumentOverrides.GmosSpectroscopy + val instrument = Instrument.GmosSouth + val site = Site.GS + val hasFilter = filter.isDefined + } + + case class Flamingos2Spectroscopy(grating: F2Disperser, filter: F2Filter) extends InstrumentConfig + derives Eq { + type Grating = F2Disperser + type Filter = F2Filter + type FPU = Unit + type Override = Unit + val fpu = () + val instrument = Instrument.Flamingos2 + val site = Site.GS + val hasFilter = true + } + + case class GpiSpectroscopy(grating: GpiDisperser, filter: GpiFilter) extends InstrumentConfig + derives Eq { + type Grating = GpiDisperser + type Filter = GpiFilter + type FPU = Unit + type Override = Unit + val fpu = () + val instrument = Instrument.Gpi + val site = Site.GN + val hasFilter = true + } + + case class GnirsSpectroscopy(grating: GnirsDisperser, filter: GnirsFilter) + extends InstrumentConfig derives Eq { + type Grating = GnirsDisperser + type Filter = GnirsFilter + type FPU = Unit + type Override = Unit + val fpu = () + val instrument = Instrument.Gnirs + val site = Site.GN + val hasFilter = true + } + + // Used for Instruments not fully defined + case class GenericSpectroscopy(i: Instrument, grating: String, filter: NonEmptyString) + extends InstrumentConfig derives Eq { + type Grating = String + type Filter = NonEmptyString + type FPU = Unit + type Override = Unit + val fpu = () + val instrument = i + val site = Site.GN + val hasFilter = true + } + + val instrument: Getter[InstrumentConfig, Instrument] = + Getter[InstrumentConfig, Instrument](_.instrument) + + def grating: Getter[InstrumentConfig, InstrumentConfig#Grating] = + Getter[InstrumentConfig, InstrumentConfig#Grating](_.grating) + + def filter: Getter[InstrumentConfig, InstrumentConfig#Filter] = + Getter[InstrumentConfig, InstrumentConfig#Filter](_.filter) diff --git a/model/shared/src/main/scala/explore/modes/InstrumentOverrides.scala b/model/shared/src/main/scala/explore/modes/InstrumentOverrides.scala new file mode 100644 index 0000000000..5620226171 --- /dev/null +++ b/model/shared/src/main/scala/explore/modes/InstrumentOverrides.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.modes + +import cats.Eq +import cats.derived.* +import lucuma.core.enums.GmosRoi +import lucuma.core.math.Wavelength +import lucuma.core.math.units.* +import lucuma.core.model.sequence.gmos.GmosCcdMode +import lucuma.core.util.Enumerated +import lucuma.schemas.model.CentralWavelength + +enum InstrumentOverrides derives Eq: + case GmosSpectroscopy(centralWavelength: CentralWavelength, ccdMode: GmosCcdMode, roi: GmosRoi) diff --git a/model/shared/src/main/scala/explore/modes/SpectroscopyModesMatrix.scala b/model/shared/src/main/scala/explore/modes/SpectroscopyModesMatrix.scala index 4fd8dbb752..e5f25d209f 100644 --- a/model/shared/src/main/scala/explore/modes/SpectroscopyModesMatrix.scala +++ b/model/shared/src/main/scala/explore/modes/SpectroscopyModesMatrix.scala @@ -5,6 +5,7 @@ package explore.modes import cats.Eq import cats.Order +import cats.data.NonEmptyList import cats.derived.* import cats.implicits.* import coulomb.* @@ -15,6 +16,7 @@ import eu.timepit.refined.collection.NonEmpty import eu.timepit.refined.types.numeric.* import eu.timepit.refined.types.string.* import explore.model.syntax.all.* +import explore.modes.syntax.* import io.circe.Decoder import io.circe.refined.* import lucuma.core.enums.* @@ -25,6 +27,7 @@ import lucuma.core.math.Declination import lucuma.core.math.Wavelength import lucuma.core.math.WavelengthDelta import lucuma.core.math.units.* +import lucuma.core.model.SourceProfile import lucuma.core.model.sequence.gmos.GmosCcdMode import lucuma.core.util.Enumerated import lucuma.core.util.NewType @@ -36,125 +39,6 @@ import monocle.Lens import monocle.macros.GenLens import spire.math.Rational -sealed trait InstrumentRow derives Eq { - def instrument: Instrument - - type Grating - val grating: Grating - - type FPU - val fpu: FPU - - type Filter - val filter: Filter - - val site: Site - - def hasFilter: Boolean - - type Override - def modeOverrides: Option[Override] = None - - override def toString(): String = s"Mode: ${instrument.shortName}, $grating, $filter, $fpu" -} - -sealed trait InstrumentOverrides derives Eq -case class GmosSpectroscopyOverrides( - centralWavelength: CentralWavelength, - ccdMode: Option[GmosCcdMode], - roi: Option[GmosRoi] -) extends InstrumentOverrides derives Eq - -case class GmosNorthSpectroscopyRow( - grating: GmosNorthGrating, - fpu: GmosNorthFpu, - filter: Option[GmosNorthFilter], - override val modeOverrides: Option[GmosSpectroscopyOverrides] -) extends InstrumentRow { - type Grating = GmosNorthGrating - type Filter = Option[GmosNorthFilter] - type FPU = GmosNorthFpu - type Override = GmosSpectroscopyOverrides - val instrument = Instrument.GmosNorth - val site = Site.GN - val hasFilter = filter.isDefined -} - -case class GmosSouthSpectroscopyRow( - grating: GmosSouthGrating, - fpu: GmosSouthFpu, - filter: Option[GmosSouthFilter], - override val modeOverrides: Option[GmosSpectroscopyOverrides] -) extends InstrumentRow { - type Grating = GmosSouthGrating - type Filter = Option[GmosSouthFilter] - type FPU = GmosSouthFpu - type Override = GmosSpectroscopyOverrides - val instrument = Instrument.GmosSouth - val site = Site.GS - val hasFilter = filter.isDefined -} - -case class Flamingos2SpectroscopyRow(grating: F2Disperser, filter: F2Filter) extends InstrumentRow { - type Grating = F2Disperser - type Filter = F2Filter - type FPU = Unit - type Override = Unit - val fpu = () - val instrument = Instrument.Flamingos2 - val site = Site.GS - val hasFilter = true -} - -case class GpiSpectroscopyRow(grating: GpiDisperser, filter: GpiFilter) extends InstrumentRow { - type Grating = GpiDisperser - type Filter = GpiFilter - type FPU = Unit - type Override = Unit - val fpu = () - val instrument = Instrument.Gpi - val site = Site.GN - val hasFilter = true -} - -case class GnirsSpectroscopyRow(grating: GnirsDisperser, filter: GnirsFilter) - extends InstrumentRow { - type Grating = GnirsDisperser - type Filter = GnirsFilter - type FPU = Unit - type Override = Unit - val fpu = () - val instrument = Instrument.Gnirs - val site = Site.GN - val hasFilter = true -} - -// Used for Instruments not fully defined -case class GenericSpectroscopyRow(i: Instrument, grating: String, filter: NonEmptyString) - extends InstrumentRow { - type Grating = String - type Filter = NonEmptyString - type FPU = Unit - type Override = Unit - val fpu = () - val instrument = i - val site = Site.GN - val hasFilter = true -} - -object InstrumentRow { - - val instrument: Getter[InstrumentRow, Instrument] = - Getter[InstrumentRow, Instrument](_.instrument) - - def grating: Getter[InstrumentRow, InstrumentRow#Grating] = - Getter[InstrumentRow, InstrumentRow#Grating](_.grating) - - def filter: Getter[InstrumentRow, InstrumentRow#Filter] = - Getter[InstrumentRow, InstrumentRow#Filter](_.filter) - -} - trait ModeCommonWavelengths { val λmin: ModeWavelength val λmax: ModeWavelength @@ -195,7 +79,7 @@ type SlitWidth = SlitWidth.Type case class SpectroscopyModeRow( id: Option[Int], // we number the modes for the UI - instrument: InstrumentRow, + instrument: InstrumentConfig, config: NonEmptyString, focalPlane: FocalPlane, capability: Option[SpectroscopyCapabilities], @@ -216,22 +100,60 @@ case class SpectroscopyModeRow( def intervalCenter(cw: Wavelength): Option[CentralWavelength] = ModeCommonWavelengths .wavelengthInterval(cw)(this) - .map(interval => + .map: interval => interval.lower.pm.value.value + (interval.upper.pm.value.value - interval.lower.pm.value.value) / 2 - ) .flatMap(pms => Wavelength.fromIntPicometers(pms)) .map(CentralWavelength(_)) + + import lucuma.core.model.sequence.gmos.longslit.DefaultRoi + + def withModeOverridesFor( + wavelength: Wavelength, + profiles: NonEmptyList[SourceProfile], + imageQuality: ImageQuality + ): Option[SpectroscopyModeRow] = + intervalCenter(wavelength).flatMap: cw => + val instrumentConfig: Option[InstrumentConfig] = + instrument.instrument match + case Instrument.GmosNorth | Instrument.GmosSouth => + instrument match + case i @ InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, _, None) => + i.copy(modeOverrides = + InstrumentOverrides + .GmosSpectroscopy( + cw, + GmosCcdMode.defaultGmosNorth(profiles, fpu, grating, imageQuality), + DefaultRoi + ) + .some + ).some + case i @ InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, _, None) => + i.copy(modeOverrides = + InstrumentOverrides + .GmosSpectroscopy( + cw, + GmosCcdMode.defaultGmosSouth(profiles, fpu, grating, imageQuality), + DefaultRoi + ) + .some + ).some + case i => + i.some + case _ => none + + instrumentConfig.map: i => + copy(instrument = i) } object SpectroscopyModeRow { given ValueConversion[NonNegBigDecimal, BigDecimal] = _.value - val instrumentRow: Lens[SpectroscopyModeRow, InstrumentRow] = + val instrumentConfig: Lens[SpectroscopyModeRow, InstrumentConfig] = GenLens[SpectroscopyModeRow](_.instrument) val instrument: Getter[SpectroscopyModeRow, Instrument] = - instrumentRow.andThen(InstrumentRow.instrument) + instrumentConfig.andThen(InstrumentConfig.instrument) val config: Lens[SpectroscopyModeRow, NonEmptyString] = GenLens[SpectroscopyModeRow](_.config) @@ -245,14 +167,14 @@ object SpectroscopyModeRow { val slitLength: Lens[SpectroscopyModeRow, SlitLength] = GenLens[SpectroscopyModeRow](_.slitLength) - def grating: Getter[SpectroscopyModeRow, InstrumentRow#Grating] = - instrumentRow.andThen(InstrumentRow.grating) + def grating: Getter[SpectroscopyModeRow, InstrumentConfig#Grating] = + instrumentConfig.andThen(InstrumentConfig.grating) def fpu: Lens[SpectroscopyModeRow, FocalPlane] = GenLens[SpectroscopyModeRow](_.focalPlane) - def filter: Getter[SpectroscopyModeRow, InstrumentRow#Filter] = - instrumentRow.andThen(InstrumentRow.filter) + def filter: Getter[SpectroscopyModeRow, InstrumentConfig#Filter] = + instrumentConfig.andThen(InstrumentConfig.filter) import lucuma.core.math.units.* @@ -260,19 +182,19 @@ object SpectroscopyModeRow { Getter(_.resolution) // decoders for instruments are used locally as they are not lawful - private given Decoder[GmosNorthSpectroscopyRow] = c => + private given Decoder[InstrumentConfig.GmosNorthSpectroscopy] = c => for { grating <- c.downField("grating").as[GmosNorthGrating] fpu <- c.downField("fpu").as[GmosNorthFpu] filter <- c.downField("filter").as[Option[GmosNorthFilter]] - } yield GmosNorthSpectroscopyRow(grating, fpu, filter, none) + } yield InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, filter, none) - private given Decoder[GmosSouthSpectroscopyRow] = c => + private given Decoder[InstrumentConfig.GmosSouthSpectroscopy] = c => for { grating <- c.downField("grating").as[GmosSouthGrating] fpu <- c.downField("fpu").as[GmosSouthFpu] filter <- c.downField("filter").as[Option[GmosSouthFilter]] - } yield GmosSouthSpectroscopyRow(grating, fpu, filter, none) + } yield InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, filter, none) given Decoder[SpectroscopyModeRow] = c => for { @@ -288,8 +210,8 @@ object SpectroscopyModeRow { resolution <- c.downField("resolution").as[PosInt] slitWidth <- c.downField("slitWidth").as[Angle] slitLength <- c.downField("slitLength").as[Angle] - gmosNorth <- c.downField("gmosNorth").as[Option[GmosNorthSpectroscopyRow]] - gmosSouth <- c.downField("gmosSouth").as[Option[GmosSouthSpectroscopyRow]] + gmosNorth <- c.downField("gmosNorth").as[Option[InstrumentConfig.GmosNorthSpectroscopy]] + gmosSouth <- c.downField("gmosSouth").as[Option[InstrumentConfig.GmosSouthSpectroscopy]] } yield gmosNorth .orElse(gmosSouth) .map { i => @@ -363,7 +285,7 @@ case class SpectroscopyModesMatrix(matrix: List[SpectroscopyModeRow]) derives Eq if (w >= l && r.hasFilter) ScoreBump else Rational.zero } .getOrElse(Rational.zero) - // Wavelength matche + // Wavelength match val wavelengthScore: BigDecimal = wavelength .map(w => w.toNanometers.value.value / (w.toNanometers.value.value + deltaWave)) .getOrElse(BigDecimal(0)) diff --git a/model/shared/src/main/scala/explore/modes/package.scala b/model/shared/src/main/scala/explore/modes/package.scala index 4dbdccdf9b..b9bc225684 100644 --- a/model/shared/src/main/scala/explore/modes/package.scala +++ b/model/shared/src/main/scala/explore/modes/package.scala @@ -1,7 +1,7 @@ // Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) // For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause -package explore +package explore.modes import cats.Order import eu.timepit.refined.* @@ -11,32 +11,29 @@ import lucuma.core.math.Wavelength import lucuma.core.optics.Wedge import lucuma.core.util.NewType -package modes { - object ModeWavelength extends NewType[Wavelength]: - extension (w: ModeWavelength) - def toString: String = s"${w.value.toMicrometers.value.value.toDouble} μm" - type ModeWavelength = ModeWavelength.Type +object ModeWavelength extends NewType[Wavelength]: + extension (w: ModeWavelength) + def toString: String = s"${w.value.toMicrometers.value.value.toDouble} μm" +type ModeWavelength = ModeWavelength.Type - object ModeSlitSize extends NewType[Angle]: - val milliarcseconds: Wedge[Angle, BigDecimal] = - Angle.milliarcseconds - .imapB(_.underlying.movePointRight(3).intValue, - n => new java.math.BigDecimal(n).movePointLeft(3) - ) +object ModeSlitSize extends NewType[Angle]: + val milliarcseconds: Wedge[Angle, BigDecimal] = + Angle.milliarcseconds + .imapB(_.underlying.movePointRight(3).intValue, + n => new java.math.BigDecimal(n).movePointLeft(3) + ) - given Order[ModeSlitSize] = Order.by(_.value.toMicroarcseconds) + given Order[ModeSlitSize] = Order.by(_.value.toMicroarcseconds) - extension (size: ModeSlitSize) - def toString: String = s"${Angle.milliarcseconds.get(size.value) / 1000.0} arcsec" + extension (size: ModeSlitSize) + def toString: String = s"${Angle.milliarcseconds.get(size.value) / 1000.0} arcsec" - type ModeSlitSize = ModeSlitSize.Type +type ModeSlitSize = ModeSlitSize.Type - object ModeAO extends NewType[Boolean] { - val NoAO = ModeAO(false) - val AO = ModeAO(true) - - def fromBoolean(b: Boolean): ModeAO = if b then AO else NoAO - } - type ModeAO = ModeAO.Type +object ModeAO extends NewType[Boolean] { + val NoAO = ModeAO(false) + val AO = ModeAO(true) + def fromBoolean(b: Boolean): ModeAO = if b then AO else NoAO } +type ModeAO = ModeAO.Type diff --git a/model/shared/src/main/scala/explore/modes/syntax.scala b/model/shared/src/main/scala/explore/modes/syntax.scala new file mode 100644 index 0000000000..7ce84856a3 --- /dev/null +++ b/model/shared/src/main/scala/explore/modes/syntax.scala @@ -0,0 +1,63 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.modes + +import cats.data.NonEmptyList +import cats.syntax.all.* +import lucuma.core.enums.GmosNorthFpu +import lucuma.core.enums.GmosNorthGrating +import lucuma.core.enums.GmosSouthFpu +import lucuma.core.enums.GmosSouthGrating +import lucuma.core.enums.GmosXBinning +import lucuma.core.enums.GmosYBinning +import lucuma.core.enums.ImageQuality +import lucuma.core.model.SourceProfile +import lucuma.core.model.sequence.gmos.GmosCcdMode +import lucuma.core.model.sequence.gmos.longslit.* + +object syntax: + // TODO Actually move to lucuma-core, to GmosCcdMode + extension (self: GmosCcdMode.type) + // For multiple targets, we take the smallest binning for each axis. + // https://docs.google.com/document/d/1P8_pXLRVomUSvofyVkAniOyGROcAtiJ7EMYt9wWXB0o/edit?disco=AAAA32SmtD4 + private def asterismBinning( + bs: NonEmptyList[(GmosXBinning, GmosYBinning)] + ): (GmosXBinning, GmosYBinning) = + (bs.map(_._1).minimumBy(_.count), bs.map(_._2).minimumBy(_.count)) + + def defaultGmosNorth( + profiles: NonEmptyList[SourceProfile], + fpu: GmosNorthFpu, + grating: GmosNorthGrating, + imageQuality: ImageQuality + ): GmosCcdMode = + val (defaultXBinning, defaultYBinning) = + if (fpu.isIFU) (GmosXBinning.One, GmosYBinning.One) + else asterismBinning(profiles.map(northBinning(fpu, _, imageQuality, grating))) + + GmosCcdMode( + defaultXBinning, + defaultYBinning, + DefaultAmpCount, + DefaultAmpGain, + DefaultAmpReadMode + ) + + def defaultGmosSouth( + profiles: NonEmptyList[SourceProfile], + fpu: GmosSouthFpu, + grating: GmosSouthGrating, + imageQuality: ImageQuality + ): GmosCcdMode = + val (defaultXBinning, defaultYBinning) = + if (fpu.isIFU) (GmosXBinning.One, GmosYBinning.One) + else asterismBinning(profiles.map(southBinning(fpu, _, imageQuality, grating))) + + GmosCcdMode( + defaultXBinning, + defaultYBinning, + DefaultAmpCount, + DefaultAmpGain, + DefaultAmpReadMode + ) diff --git a/model/shared/src/main/scala/queries/schemas/itc/syntax.scala b/model/shared/src/main/scala/queries/schemas/itc/syntax.scala index 8f669dc0b3..9f5d8ad71d 100644 --- a/model/shared/src/main/scala/queries/schemas/itc/syntax.scala +++ b/model/shared/src/main/scala/queries/schemas/itc/syntax.scala @@ -8,37 +8,34 @@ import cats.syntax.all.* import explore.model.AsterismIds import explore.model.TargetList import explore.model.itc.ItcTarget -import explore.modes.GmosNorthSpectroscopyRow -import explore.modes.GmosSouthSpectroscopyRow -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import explore.optics.all.* import lucuma.core.enums.GmosRoi import lucuma.core.math.RadialVelocity import lucuma.core.model.* import lucuma.core.model.sequence.gmos.GmosCcdMode -import lucuma.core.model.sequence.gmos.longslit.* import lucuma.itc.client.GmosFpu import lucuma.itc.client.InstrumentMode import lucuma.itc.client.TargetInput trait syntax: - extension (row: InstrumentRow) + extension (row: InstrumentConfig) def toItcClientMode: Option[InstrumentMode] = row match - case GmosNorthSpectroscopyRow(grating, fpu, filter, modeOverrides) => - val roi: Option[GmosRoi] = modeOverrides.flatMap(_.roi).orElse(DefaultRoi.some) - val ccd: Option[GmosCcdMode] = modeOverrides.flatMap(_.ccdMode) + case InstrumentConfig.GmosNorthSpectroscopy(grating, fpu, filter, modeOverrides) => + val roi: Option[GmosRoi] = modeOverrides.map(_.roi) + val ccd: Option[GmosCcdMode] = modeOverrides.map(_.ccdMode) InstrumentMode .GmosNorthSpectroscopy(grating, filter, GmosFpu.North(fpu.asRight), ccd, roi) .some - case GmosSouthSpectroscopyRow(grating, fpu, filter, modeOverrides) => - val roi: Option[GmosRoi] = modeOverrides.flatMap(_.roi).orElse(DefaultRoi.some) - val ccd: Option[GmosCcdMode] = modeOverrides.flatMap(_.ccdMode) + case InstrumentConfig.GmosSouthSpectroscopy(grating, fpu, filter, modeOverrides) => + val roi: Option[GmosRoi] = modeOverrides.map(_.roi) + val ccd: Option[GmosCcdMode] = modeOverrides.map(_.ccdMode) InstrumentMode .GmosSouthSpectroscopy(grating, filter, GmosFpu.South(fpu.asRight), ccd, roi) .some - case _ => None + case _ => None // We may consider adjusting this to consider small variations of RV identical for the // purpose of doing ITC calculations diff --git a/workers/src/main/scala/workers/itc/ITCGraphRequests.scala b/workers/src/main/scala/workers/itc/ITCGraphRequests.scala index efdd03cde1..521bab1987 100644 --- a/workers/src/main/scala/workers/itc/ITCGraphRequests.scala +++ b/workers/src/main/scala/workers/itc/ITCGraphRequests.scala @@ -9,9 +9,7 @@ import cats.effect.* import cats.syntax.all.* import explore.model.boopickle.ItcPicklers.given import explore.model.itc.* -import explore.modes.GmosNorthSpectroscopyRow -import explore.modes.GmosSouthSpectroscopyRow -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import lucuma.core.math.SignalToNoise import lucuma.core.math.Wavelength import lucuma.core.model.ConstraintSet @@ -37,13 +35,13 @@ object ITCGraphRequests: signalToNoiseAt: Wavelength, constraints: ConstraintSet, targets: NonEmptyList[ItcTarget], - mode: InstrumentRow, + mode: InstrumentConfig, cache: Cache[F], callback: ItcAsterismGraphResults => F[Unit] )(using Monoid[F[Unit]], ItcClient[F]): F[Unit] = val itcRowsParams = mode match // Only handle known modes - case m @ GmosNorthSpectroscopyRow(_, _, _, _) => + case m @ InstrumentConfig.GmosNorthSpectroscopy(_, _, _, _) => ItcGraphRequestParams( wavelength, signalToNoise, @@ -52,7 +50,7 @@ object ITCGraphRequests: targets, m ).some - case m @ GmosSouthSpectroscopyRow(_, _, _, _) => + case m @ InstrumentConfig.GmosSouthSpectroscopy(_, _, _, _) => ItcGraphRequestParams( wavelength, signalToNoise, @@ -61,7 +59,7 @@ object ITCGraphRequests: targets, m ).some - case _ => + case _ => none def doRequest(request: ItcGraphRequestParams): F[ItcAsterismGraphResults] = diff --git a/workers/src/main/scala/workers/itc/ITCRequests.scala b/workers/src/main/scala/workers/itc/ITCRequests.scala index 5bb9341793..6b47d9da0d 100644 --- a/workers/src/main/scala/workers/itc/ITCRequests.scala +++ b/workers/src/main/scala/workers/itc/ITCRequests.scala @@ -12,9 +12,7 @@ import cats.syntax.all.* import explore.model.Constants import explore.model.boopickle.ItcPicklers.given import explore.model.itc.* -import explore.modes.GmosNorthSpectroscopyRow -import explore.modes.GmosSouthSpectroscopyRow -import explore.modes.InstrumentRow +import explore.modes.InstrumentConfig import explore.modes.SpectroscopyModeRow import lucuma.core.math.SignalToNoise import lucuma.core.math.Wavelength @@ -112,9 +110,9 @@ object ITCRequests: .map(x => (x.intervalCenter(wavelength), x.instrument)) // Only handle known modes .collect: - case (Some(wavelength), m: GmosNorthSpectroscopyRow) => + case (Some(wavelength), m @ InstrumentConfig.GmosNorthSpectroscopy(_, _, _, _)) => ItcRequestParams(wavelength, signalToNoise, signalToNoiseAt, constraints, asterism, m) - case (Some(wavelength), m: GmosSouthSpectroscopyRow) => + case (Some(wavelength), m @ InstrumentConfig.GmosSouthSpectroscopy(_, _, _, _)) => ItcRequestParams(wavelength, signalToNoise, signalToNoiseAt, constraints, asterism, m) parTraverseN(