Skip to content

Commit

Permalink
Merge pull request #976 from gemini-hlsw/nearest-emission-line
Browse files Browse the repository at this point in the history
Changes to support emission lines throughout, especially ITC
  • Loading branch information
rpiaggio authored Nov 27, 2024
2 parents f5d61e5 + 7e6f552 commit f126d1b
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 68 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ThisBuild / tlBaseVersion := "0.108"
ThisBuild / tlBaseVersion := "0.109"
ThisBuild / tlCiReleaseBranches := Seq("master")
ThisBuild / githubWorkflowEnv += "MUNIT_FLAKY_OK" -> "true"

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 lucuma.core.math.dimensional

import coulomb.Quantity
import lucuma.core.util.*

object syntax:
extension[N, U](quantity: Quantity[N, U])

/**
* Convert a coulomb `Quantity` to a `Measure` with runtime unit representation.
*/
def toMeasure(using unit: UnitOfMeasure[U]): Measure[N] = Measure(quantity.value, unit)

/**
* Convert a coulomb `Quantity` to a `Measure` with runtime unit representation and tag `Tag`.
*/
def toMeasureTagged[T](using ev: TaggedUnit[U, T]): Measure[N] Of T = {
val tagged: Measure[N] Of T = tag[T](Measure(quantity.value, ev.unit))
tagged
}
10 changes: 9 additions & 1 deletion modules/core/shared/src/main/scala/lucuma/core/math/units.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,16 @@ trait units {
type PicometersPerPixel = Picometer / Pixels

type MetersPerSecond = Meter / Second
given DerivedUnit[MetersPerSecond, Meter / Second, "meters per second", "m/s"] = DerivedUnit()
given TypeString[MetersPerSecond] = TypeString("M_S")

type CentimetersPerSecond = (Centi * Meter) / Second
type KilometersPerSecond = (Kilo * Meter) / Second
given DerivedUnit[CentimetersPerSecond, (Centi * Meter) / Second, "centimeters per second", "cm/s"] = DerivedUnit()
given TypeString[CentimetersPerSecond] = TypeString("CM_S")

type KilometersPerSecond
given DerivedUnit[KilometersPerSecond, (Kilo * Meter) / Second, "kilometers per second", "km/s"] = DerivedUnit()
given TypeString[KilometersPerSecond] = TypeString("KM_S")

type Year
given DerivedUnit[Year, 365 * Day, "year", "y"] = DerivedUnit()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ sealed trait SourceProfile extends Product with Serializable {
* necessary.
*/
def toGaussian: SourceProfile.Gaussian

/**
* Returns the band of a source profile closest to the given wavelength.
* It is only valid for source profiles with integrated or surface brightnesses.
*/
def nearestBand(wavelength: Wavelength): Option[Band] =
SourceProfile.integratedBandNormalizedSpectralDefinition.getOption(this).orElse:
SourceProfile.surfaceBandNormalizedSpectralDefinition.getOption(this)
.flatMap(_.nearestBand(wavelength))

/**
* Returns the emission line of a source profile closest to the given wavelength.
* It is only valid for source profiles with integrated or surface emission lines.
*/
def nearestLine(wavelength: Wavelength): Option[Wavelength] =
SourceProfile.integratedEmissionLinesSpectralDefinition.getOption(this).orElse:
SourceProfile.surfaceEmissionLinesSpectralDefinition.getOption(this)
.flatMap(_.nearestLine(wavelength))

}

object SourceProfile {
Expand Down Expand Up @@ -270,30 +289,4 @@ object SourceProfile {
/** @group Optics */
val surfaceFluxDensityContinuum: Optional[SourceProfile, FluxDensityContinuumMeasure[Surface]] =
surfaceSpectralDefinition.andThen(SpectralDefinition.fluxDensityContinuum[Surface])

/**
* Returns the band and brightness value for the band closest to the given wavelength.
*/
def extractBand[T](w: Wavelength, bMap: SortedMap[Band, BrightnessMeasure[T]]): Option[(Band, BrightnessValue, Units)] =
bMap.minByOption { case (b, _) =>
(w.toPicometers.value.value - b.center.toPicometers.value.value).abs
}.map(x => (x._1, x._2.value, x._2.units))

extension(sp: SourceProfile)
/**
* Returns the band and brightness of a source profile for the band closest to the given wavelength.
* It is only valid for source profiles with integrated or surface brightnesses.
* (Emission lines not supported)
*/
def nearestBand(wavelength: Wavelength): Option[(Band, BrightnessValue, Units)] =
SourceProfile
.integratedBrightnesses
.getOption(sp)
.flatMap(bMap => extractBand[Integrated](wavelength, bMap))
.orElse(
SourceProfile
.surfaceBrightnesses
.getOption(sp)
.flatMap(bMap => extractBand[Surface](wavelength, bMap))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ object SpectralDefinition {
sed,
brightnesses.map { case (band, brightness) => band -> brightness.toTag[Brightness[T0]] }
)

/**
* Returns the defined band closest to the given wavelength.
*/
def nearestBand(wavelength: Wavelength): Option[Band] =
brightnesses.keySet.minByOption: band =>
(wavelength.toPicometers.value.value - band.center.toPicometers.value.value).abs
}

object BandNormalized {
Expand Down Expand Up @@ -102,6 +109,13 @@ object SpectralDefinition {
lines.map { case (wavelength, line) => wavelength -> line.to[T0] },
fluxDensityContinuum.toTag[FluxDensityContinuum[T0]]
)

/**
* Returns the defined line closest to the given wavelength.
*/
def nearestLine(wavelength: Wavelength): Option[Wavelength] =
lines.keySet.minByOption: line =>
(wavelength.toPicometers.value.value - line.toPicometers.value.value).abs
}

object EmissionLines {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package lucuma.core.util

import cats.Contravariant
import cats.syntax.contravariant.*
import eu.timepit.refined.api.*

/**
* Typeclass for things that can be shown in a user interface.
Expand Down Expand Up @@ -50,4 +52,8 @@ object Display {
override def longName(b: B) = fa.longName(f(b))
}
}

given [T, P](using ev: Display[T]): Display[T Refined P] = ev.contramap(_.value)

given Display[BigDecimal] = byShortName(_.toString.toLowerCase) // Use lower case "e" for exponent
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ class MeasureSuite extends munit.DisciplineSuite {

val m = UnitOfMeasure[ABMagnitude].withValue(BigDecimal(1.235))

implicit val displayBigDecimal: Display[BigDecimal] =
Display.byShortName(_.toString)

assertEquals(m.shortName, "1.235 AB mag")
assertEquals(m.withError(BigDecimal(0.005)).shortName, "1.235 ± 0.005 AB mag")
assertEquals(m.withError(BigDecimal(0.005)).displayWithoutError, "1.235 AB mag")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,24 @@ final class SourceProfileSuite extends DisciplineSuite {
assertEquals(gaussian2.toGaussian, gaussian2)
}

test("extractBand") {
test("nearestBand") {
val wv = Wavelength(578000.refined[Positive])
assert(SourceProfile.extractBand(wv, SortedMap.empty).isEmpty)
assert(SourceProfile.extractBand(wv, sd1Brightnesses).exists(_._1 === Band.R))
assert(SourceProfile.extractBand(wv, sd1BrightnessesB).exists(_._1 === Band.SloanR))
assert(point1.nearestBand(wv).contains_(Band.R))
assert(point2.nearestBand(wv).isEmpty)
assert(uniform1.nearestBand(wv).contains_(Band.R))
assert(uniform2.nearestBand(wv).isEmpty)
assert(gaussian1.nearestBand(wv).contains_(Band.R))
assert(gaussian2.nearestBand(wv).isEmpty)
}

test("nearestBand") {
test("nearestLine") {
val wv = Wavelength(578000.refined[Positive])
assert(point1.nearestBand(wv).exists(_._1 === Band.R))
assert(point2.nearestBand(wv).isEmpty) // Emission lines not supported
assert(uniform1.nearestBand(wv).exists(_._1 === Band.R))
assert(uniform2.nearestBand(wv).isEmpty) // Emission lines not supported
assert(gaussian1.nearestBand(wv).exists(_._1 === Band.R))
assert(gaussian2.nearestBand(wv).isEmpty) // Emission lines not supported
assert(point1.nearestLine(wv).isEmpty)
assert(point2.nearestLine(wv).contains_(Wavelength.Min))
assert(uniform1.nearestLine(wv).isEmpty)
assert(uniform2.nearestLine(wv).contains_(Wavelength.Min))
assert(gaussian1.nearestLine(wv).isEmpty)
assert(gaussian2.nearestLine(wv).contains_(Wavelength.Min))
}

// Laws for SourceProfile.Point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cats.syntax.all.*
import coulomb.*
import coulomb.syntax.*
import eu.timepit.refined.cats.*
import eu.timepit.refined.numeric.Positive
import lucuma.core.enums.Band
import lucuma.core.enums.StellarLibrarySpectrum
import lucuma.core.math.BrightnessUnits
Expand All @@ -25,6 +26,7 @@ import lucuma.core.math.units.*
import lucuma.core.model.arb.*
import lucuma.core.util.arb.ArbCollection
import lucuma.core.util.arb.ArbEnumerated
import lucuma.refined.*
import monocle.law.discipline.*
import munit.*

Expand All @@ -44,23 +46,23 @@ final class SpectralDefinitionSuite extends DisciplineSuite {
import BrightnessUnits.*

// Brightness type conversions
val sd1Integrated: SpectralDefinition[Integrated] =
val sd1Integrated: SpectralDefinition.BandNormalized[Integrated] =
SpectralDefinition.BandNormalized(
UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0I).some,
SortedMap(
Band.R -> Band.R.defaultUnits[Integrated].withValueTagged(BrightnessValue.unsafeFrom(10.0))
)
)

val sd1Surface: SpectralDefinition[Surface] =
val sd1Surface: SpectralDefinition.BandNormalized[Surface] =
SpectralDefinition.BandNormalized(
UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0I).some,
SortedMap(
Band.R -> Band.R.defaultUnits[Surface].withValueTagged(BrightnessValue.unsafeFrom(10.0))
)
)

val sd2Integrated: SpectralDefinition[Integrated] =
val sd2Integrated: SpectralDefinition.EmissionLines[Integrated] =
SpectralDefinition.EmissionLines(
SortedMap(
Wavelength.Min -> EmissionLine(
Expand All @@ -73,7 +75,7 @@ final class SpectralDefinitionSuite extends DisciplineSuite {
)
)

val sd2Surface: SpectralDefinition[Surface] =
val sd2Surface: SpectralDefinition.EmissionLines[Surface] =
SpectralDefinition.EmissionLines(
SortedMap(
Wavelength.Min -> EmissionLine(
Expand Down Expand Up @@ -103,6 +105,18 @@ final class SpectralDefinitionSuite extends DisciplineSuite {
assertEquals(sd2Surface.to[Integrated].to[Surface], sd2Surface)
}

test("nearestBand") {
val wv = Wavelength(578000.refined[Positive])
assert(sd1Integrated.nearestBand(wv).contains_(Band.R))
assert(sd1Surface.nearestBand(wv).contains_(Band.R))
}

test("nearestLine") {
val wv = Wavelength(578000.refined[Positive])
assert(sd2Integrated.nearestLine(wv).contains_(Wavelength.Min))
assert(sd2Surface.nearestLine(wv).contains_(Wavelength.Min))
}

// Laws for SpectralDefinition.BandNormalized
checkAll(
"Eq[SpectralDefinition.BandNormalized[Integrated]]",
Expand Down

0 comments on commit f126d1b

Please sign in to comment.