gem.config.GmosConfig.scala Maven / Gradle / Ivy
The newest version!
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause
package gem.config
import gem.enum._
import gem.instances.time._
import gsp.math.{ Offset, Wavelength }
import cats.{ Eq, Order }
import cats.implicits._
import java.time.Duration
import monocle._
/**
* Additional type hierarchy over the low-level GMOS enums.
* @group Instrument-Specific Models
*/
object GmosConfig {
/** Nod-and-shuffle offset in detector rows, which must be positive, non-zero.
* This class essentially provides a newtype for Int.
*/
sealed abstract case class GmosShuffleOffset(detectorRows: Int) {
// Enforced by fromRowCount constructor
assert(detectorRows > 0, s"detectorRows must be > 0, not $detectorRows")
}
object GmosShuffleOffset extends GmosShuffleOffsetOptics {
/** Constructs the shuffle offset with the given number of detector rows,
* provided it is a positive number.
*
* @return `Some(GmosShuffleOffset(rows))` if `rows` is positive,
* `None` otherwise
*/
def fromRowCount(rows: Int): Option[GmosShuffleOffset] =
if (rows > 0) Some(new GmosShuffleOffset(rows) {}) else None
/** Constructs the shuffle offset with the given number of detector rows
* provided `rows` is positive, or throws an exception if zero or negative.
*/
def unsafeFromRowCount(rows: Int): GmosShuffleOffset =
fromRowCount(rows).getOrElse(sys.error(s"Expecting positive detector row count, not $rows"))
/** Constructs a shuffle offset using the default number of detector rows
* associated with the detector.
*/
def defaultFromDetector(detector: GmosDetector): GmosShuffleOffset =
fromRowCount(detector.shuffleOffset).getOrElse(sys.error(s"Misconfigured GmosDetector $detector"))
implicit val EqualGmosShuffleOffset: Eq[GmosShuffleOffset] =
Eq.fromUniversalEquals
}
trait GmosShuffleOffsetOptics {
/** @group Optics */
val detectorRows: Getter[GmosShuffleOffset, Int] =
Getter(_.detectorRows)
}
/** The number of nod-and-shuffle cycles, which must be at least 1. This class
* essentially provides a newtype for Int.
*/
sealed abstract case class GmosShuffleCycles(toInt: Int) {
// Enforced by fromCycleCount constructor
assert(toInt > 0, s"toInt must be > 0, not $toInt")
}
object GmosShuffleCycles extends GmosShuffleCyclesOptics {
/** Default non-and-shuffle cycles, which is 1. */
val Default: GmosShuffleCycles =
unsafeFromCycleCount(1)
/** Constructs the shuffle cycles from a count if `cycles` is positive.
*
* @return `Some(GmosShuffleCycles(cycles))` if `cycles` is positive,
* `None` otherwise
*/
def fromCycleCount(cycles: Int): Option[GmosShuffleCycles] =
if (cycles > 0) Some(new GmosShuffleCycles(cycles) {}) else None
/** Constructs the shuffle cycles with the given `cycles` count provided it
* is positive, or else throws an exception if 0 or negative.
*/
def unsafeFromCycleCount(cycles: Int): GmosShuffleCycles =
fromCycleCount(cycles).getOrElse(sys.error(s"Expecting positive shuffle cycles, not $cycles"))
implicit val EqualGmosShuffleCycles: Eq[GmosShuffleCycles] =
Eq.fromUniversalEquals
}
trait GmosShuffleCyclesOptics {
/** @group Optics */
val shuffleCycles: Getter[GmosShuffleCycles, Int] =
Getter(_.toInt)
}
// TODO: there are many ways to misconfigure Nod And Shuffle. Some of these
// ways can be caught in a companion object constructor, and some cannot, or
// at least cannot easily. In the first category, we should definitly check
// whether posA and posB are close enough together (< 2 arcsecs) to allow
// e-offsetting. In the second category, shuffle offset in detector rows has
// to be a multiple of y-binning. Worse, y-binning is part of the dynamic
// configuration so it isn't clear what happens if the shuffle offset isn't
// always a multiple of y-binning.
/** GMOS nod-and-shuffle configuration. */
final case class GmosNodAndShuffle(
posA: Offset,
posB: Offset,
eOffset: GmosEOffsetting,
shuffle: GmosShuffleOffset,
cycles: GmosShuffleCycles
)
object GmosNodAndShuffle extends GmosNodAndShuffleOptics {
val Default: GmosNodAndShuffle =
GmosNodAndShuffle(
Offset.Zero,
Offset.Zero,
GmosEOffsetting.Off,
GmosShuffleOffset.defaultFromDetector(GmosDetector.HAMAMATSU),
GmosShuffleCycles.Default
)
implicit val EqualGmosNodAndShuffle: Eq[GmosNodAndShuffle] =
Eq.fromUniversalEquals
}
trait GmosNodAndShuffleOptics {
/** @group Optics */
val posA: Lens[GmosNodAndShuffle, Offset] =
Lens[GmosNodAndShuffle, Offset](_.posA)(a => _.copy(posA = a))
/** @group Optics */
val posB: Lens[GmosNodAndShuffle, Offset] =
Lens[GmosNodAndShuffle, Offset](_.posB)(a => _.copy(posB = a))
/** @group Optics */
val eOffset: Lens[GmosNodAndShuffle, GmosEOffsetting] =
Lens[GmosNodAndShuffle, GmosEOffsetting](_.eOffset)(a => _.copy(eOffset = a))
/** @group Optics */
val shuffle: Lens[GmosNodAndShuffle, GmosShuffleOffset] =
Lens[GmosNodAndShuffle, GmosShuffleOffset](_.shuffle)(a => _.copy(shuffle = a))
/** @group Optics */
val cycles: Lens[GmosNodAndShuffle, GmosShuffleCycles] =
Lens[GmosNodAndShuffle, GmosShuffleCycles](_.cycles)(a => _.copy(cycles = a))
}
/** GMOS custom ROI entry definition. */
sealed abstract case class GmosCustomRoiEntry(
xMin: Short,
yMin: Short,
xRange: Short,
yRange: Short
) {
// Enforced by fromDescription constructor
assert(xMin > 0, s"xMin must be > 0, not $xMin")
assert(yMin > 0, s"yMin must be > 0, not $yMin")
assert(xRange > 0, s"xRange must be > 0, not $xRange")
assert(yRange > 0, s"yRange must be > 0, not $yRange")
/** Columns included in this ROI entry (start, end]. */
def columns: (Int, Int) =
(xMin.toInt, xMin + xRange)
/** Rows included in this ROI entry (start, end]. */
def rows: (Int, Int) =
(yMin.toInt, yMin + yRange)
/** Returns `true` if the pixels specified by this custom ROI entry overlap
* with the pixels specified by `that` entry.
*/
def overlaps(that: GmosCustomRoiEntry): Boolean =
columnsOverlap(that) && rowsOverlap(that)
/** Returns `true` if the columns spanned this custom ROI entry overlap with
* the columns spanned by `that` entry.
*/
def columnsOverlap(that: GmosCustomRoiEntry): Boolean =
overlapCheck(that, _.columns)
/** Returns `true` if the rows spanned this custom ROI entry overlap with
* the rows spanned by `that` entry.
*/
def rowsOverlap(that: GmosCustomRoiEntry): Boolean =
overlapCheck(that, _.rows)
private def overlapCheck(that: GmosCustomRoiEntry, f: GmosCustomRoiEntry => (Int, Int)): Boolean = {
val List((_, end), (start, _)) = List(f(this), f(that)).sortBy(_._1)
end > start
}
}
object GmosCustomRoiEntry extends GmosCustomRoiEntryOptics {
def fromDescription(xMin: Short, yMin: Short, xRange: Short, yRange: Short): Option[GmosCustomRoiEntry] =
if ((xMin > 0) && (yMin > 0) && (xRange > 0) && (yRange > 0))
Some(new GmosCustomRoiEntry(xMin, yMin, xRange, yRange) {})
else
None
def unsafeFromDescription(xMin: Short, yMin: Short, xRange: Short, yRange: Short): GmosCustomRoiEntry =
fromDescription(xMin, yMin, xRange, yRange)
.getOrElse(sys.error(s"All custom ROI fields must be > 0 in GmosCustomRoi.unsafeFromDefinition($xMin, $yMin, $xRange, $yRange)"))
implicit val OrderGmosCustomRoiEntry: Order[GmosCustomRoiEntry] =
Order.by(c => (c.xMin, c.yMin, c.xRange, c.yRange))
}
trait GmosCustomRoiEntryOptics {
/** @group Optics */
val xMin: Getter[GmosCustomRoiEntry, Short] =
Getter(_.xMin)
/** @group Optics */
val yMin: Getter[GmosCustomRoiEntry, Short] =
Getter(_.yMin)
/** @group Optics */
val xRange: Getter[GmosCustomRoiEntry, Short] =
Getter(_.xRange)
/** @group Optics */
val yRange: Getter[GmosCustomRoiEntry, Short] =
Getter(_.yRange)
}
/** Shared static configuration for both GMOS-N and GMOS-S.
*/
final case class GmosCommonStaticConfig(
detector: GmosDetector,
mosPreImaging: MosPreImaging,
nodAndShuffle: Option[GmosNodAndShuffle],
customRois: Set[GmosCustomRoiEntry]
)
object GmosCommonStaticConfig extends GmosCommonStaticConfigOptics {
val Default: GmosCommonStaticConfig =
GmosCommonStaticConfig(
GmosDetector.HAMAMATSU,
MosPreImaging.IsNotMosPreImaging,
None,
Set.empty[GmosCustomRoiEntry]
)
implicit val EqGmosCommonStaticConfig: Eq[GmosCommonStaticConfig] =
Eq.by(c => (c.detector, c.mosPreImaging, c.nodAndShuffle, c.customRois))
}
trait GmosCommonStaticConfigOptics {
/** @group Optics */
val detector: Lens[GmosCommonStaticConfig, GmosDetector] =
Lens[GmosCommonStaticConfig, GmosDetector](_.detector)(a => _.copy(detector = a))
/** @group Optics */
val mosPreImaging: Lens[GmosCommonStaticConfig, MosPreImaging] =
Lens[GmosCommonStaticConfig, MosPreImaging](_.mosPreImaging)(a => _.copy(mosPreImaging = a))
/** @group Optics */
val nodAndShuffle: Lens[GmosCommonStaticConfig, Option[GmosNodAndShuffle]] =
Lens[GmosCommonStaticConfig, Option[GmosNodAndShuffle]](_.nodAndShuffle)(a => _.copy(nodAndShuffle = a))
/** @group Optics */
val customRois: Lens[GmosCommonStaticConfig, Set[GmosCustomRoiEntry]] =
Lens[GmosCommonStaticConfig, Set[GmosCustomRoiEntry]](_.customRois)(a => _.copy(customRois = a))
}
/** Parameters that determine GMOS CCD readout.
*/
final case class GmosCcdReadout(
xBinning: GmosXBinning,
yBinning: GmosYBinning,
ampCount: GmosAmpCount,
ampGain: GmosAmpGain,
ampReadMode: GmosAmpReadMode
)
object GmosCcdReadout extends GmosCcdReadoutOptics {
val Default: GmosCcdReadout =
GmosCcdReadout(
GmosXBinning.One,
GmosYBinning.One,
GmosAmpCount.Twelve,
GmosAmpGain.Low,
GmosAmpReadMode.Slow
)
implicit val EqualGmosCcdReadout: Eq[GmosCcdReadout] =
Eq.by(c => (c.xBinning, c.yBinning, c.ampCount, c.ampGain, c.ampReadMode))
}
trait GmosCcdReadoutOptics {
/** @group Optics */
val xBinning: Lens[GmosCcdReadout, GmosXBinning] =
Lens[GmosCcdReadout, GmosXBinning](_.xBinning)(a => _.copy(xBinning = a))
/** @group Optics */
val yBinning: Lens[GmosCcdReadout, GmosYBinning] =
Lens[GmosCcdReadout, GmosYBinning](_.yBinning)(a => _.copy(yBinning = a))
/** @group Optics */
val ampCount: Lens[GmosCcdReadout, GmosAmpCount] =
Lens[GmosCcdReadout, GmosAmpCount](_.ampCount)(a => _.copy(ampCount = a))
/** @group Optics */
val ampGain: Lens[GmosCcdReadout, GmosAmpGain] =
Lens[GmosCcdReadout, GmosAmpGain](_.ampGain)(a => _.copy(ampGain = a))
/** @group Optics */
val ampReadMode: Lens[GmosCcdReadout, GmosAmpReadMode] =
Lens[GmosCcdReadout, GmosAmpReadMode](_.ampReadMode)(a => _.copy(ampReadMode = a))
}
/** Shared dynamic configuration for both GMOS-N and GMOS-S.
*/
final case class GmosCommonDynamicConfig(
ccdReadout: GmosCcdReadout,
dtaxOffset: GmosDtax,
exposureTime: Duration,
roi: GmosRoi
)
object GmosCommonDynamicConfig extends GmosCommonDynamicConfigOptics {
val Default: GmosCommonDynamicConfig =
GmosCommonDynamicConfig(
GmosCcdReadout.Default,
GmosDtax.Zero,
Duration.ofSeconds(300),
GmosRoi.FullFrame
)
implicit val EqualGmosCommonDynamicConfig: Eq[GmosCommonDynamicConfig] =
Eq.by(c => (c.ccdReadout, c.dtaxOffset, c.exposureTime, c.roi))
}
trait GmosCommonDynamicConfigOptics {
/** @group Optics */
val ccdReadout: Lens[GmosCommonDynamicConfig, GmosCcdReadout] =
Lens[GmosCommonDynamicConfig, GmosCcdReadout](_.ccdReadout)(a => _.copy(ccdReadout = a))
/** @group Optics */
val dtaxOffset: Lens[GmosCommonDynamicConfig, GmosDtax] =
Lens[GmosCommonDynamicConfig, GmosDtax](_.dtaxOffset)(a => _.copy(dtaxOffset = a))
/** @group Optics */
val exposureTime: Lens[GmosCommonDynamicConfig, Duration] =
Lens[GmosCommonDynamicConfig, Duration](_.exposureTime)(a => _.copy(exposureTime = a))
/** @group Optics */
val roi: Lens[GmosCommonDynamicConfig, GmosRoi] =
Lens[GmosCommonDynamicConfig, GmosRoi](_.roi)(a => _.copy(roi = a))
}
/** Custom mask definition, which is available as an alternative to using a
* builtin FPU. Either both these parameters are set or neither are set in a
* GMOS observation
*/
final case class GmosCustomMask(
maskDefinitionFilename: String,
slitWidth: GmosCustomSlitWidth
)
object GmosCustomMask extends GmosCustomMaskOptics {
implicit val EqualGmosCustomMask: Eq[GmosCustomMask] =
Eq.by(c => (c.maskDefinitionFilename, c.slitWidth))
}
trait GmosCustomMaskOptics {
/** @group Optics */
val maskDefinitionFilename: Lens[GmosCustomMask, String] =
Lens[GmosCustomMask, String](_.maskDefinitionFilename)(a => _.copy(maskDefinitionFilename = a))
/** @gropu Optics */
val slitWidth: Lens[GmosCustomMask, GmosCustomSlitWidth] =
Lens[GmosCustomMask, GmosCustomSlitWidth](_.slitWidth)(a => _.copy(slitWidth = a))
}
/** GMOS grating configuration, parameterized on the disperser type. These
* are grouped because they only apply when using a grating. That is, all
* are defined or none or defined in the dynamic config.
*
* @tparam D disperser type, expected to be `GmosNorthDisperser` or
* `GmosSouthDisperser`
*/
final case class GmosGrating[D](
disperser: D,
order: GmosDisperserOrder,
wavelength: Wavelength
)
object GmosGrating extends GmosGratingOptics {
implicit def EqualGmosGrating[D: Eq]: Eq[GmosGrating[D]] =
Eq.by(g => (g.disperser, g.order, g.wavelength))
}
trait GmosGratingOptics {
/** @group Optics */
def disperser[D]: Lens[GmosGrating[D], D] =
Lens[GmosGrating[D], D](_.disperser)(a => _.copy(disperser = a))
/** @group Optics */
def order[D]: Lens[GmosGrating[D], GmosDisperserOrder] =
Lens[GmosGrating[D], GmosDisperserOrder](_.order)(a => _.copy(order = a))
/** @group Optics */
def wavelength[D]: Lens[GmosGrating[D], Wavelength] =
Lens[GmosGrating[D], Wavelength](_.wavelength)(a => _.copy(wavelength = a))
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy