All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lucuma.core.math.WavelengthDelta.scala Maven / Gradle / Ivy

// 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

import algebra.instances.all.given
import cats.Order
import cats.Show
import coulomb.Quantity
import coulomb.syntax.*
import eu.timepit.refined.auto.*
import eu.timepit.refined.cats.*
import eu.timepit.refined.numeric.*
import eu.timepit.refined.types.numeric.PosBigDecimal
import eu.timepit.refined.types.numeric.PosInt
import lucuma.core.math.units.Angstrom
import lucuma.core.math.units.Micrometer
import lucuma.core.math.units.Nanometer
import lucuma.core.math.units.Picometer
import lucuma.core.optics.Format
import monocle.Iso
import monocle.Prism

import java.math.RoundingMode
import scala.annotation.targetName

/**
 * Represents a span of the spectrum, represented as a small positive wavelength
 * delta in picometers. It can be "anchored" by providing a starting, ending, or
 * central `Wavelength`, which results in a wavelength interval (ie: a 
 * `BoundedInterval[Wavelength]`).
 */
opaque type WavelengthDelta = Quantity[PosInt, Picometer]

object WavelengthDelta:
  extension (w: WavelengthDelta) {
    def toPicometers: Quantity[PosInt, Picometer] =
      w

    /**
     * Alias for `toPicometers`.
     */
    def pm: Quantity[PosInt, Picometer] =
      toPicometers

    /**
      * Create a wavelength range centered on the given value. Will be truncated at 
      * Wavelength.Min and Wavelength.Max.
      */
    def centeredAt(λ: Wavelength): BoundedInterval[Wavelength] = 
      val start =  λ.pm.value.value - w.value.value / 2
      BoundedInterval.unsafeClosed(
        Wavelength(PosInt.unsafeFrom(math.max(Wavelength.Min.pm.value.value, start))), 
        Wavelength(PosInt.unsafeFrom(math.min(Wavelength.Max.pm.value.value - pm.value.value, start) + pm.value.value))
      )

    /**
      * Create a wavelength range starting on the given value.  Will be truncated at Wavelength.Max.
      */
    def startingAt(λ: Wavelength): BoundedInterval[Wavelength] = 
      BoundedInterval.unsafeClosed(λ, Wavelength(PosInt.unsafeFrom(
        math.min(Wavelength.Max.pm.value.value - pm.value.value, λ.pm.value.value) + pm.value.value
      )))

    /**
      * Create a wavelength range ending on the given value. Will be truncated at Wavelength.Min.
      */
    def endingAt(λ: Wavelength): BoundedInterval[Wavelength] = 
      BoundedInterval.unsafeClosed(
        Wavelength(PosInt.unsafeFrom(math.max(Wavelength.Min.pm.value.value, λ.pm.value.value - w.value.value))),
        λ
      )
    
    // Conversion between units is guaranteed to be positive since the Wavelength in pm is positive.
    // The value can always be exactly represented as a (Pos)BigDecimal since sub-pm fractions cannot be
    // represented.
    private def to[U](scale: Int): Quantity[PosBigDecimal, U] =
      Quantity[U](PosBigDecimal.unsafeFrom(BigDecimal(toPicometers.value.value, scale)))

    /**
     * Returns the wavelength value in angstroms.
     */
    def toAngstroms: Quantity[PosBigDecimal, Angstrom] =
      to[Angstrom](2)

    /**
     * Alias for `toAngstroms`.
     */
    def Å: Quantity[PosBigDecimal, Angstrom] =
      toAngstroms

    /**
     * Returns the wavelength value in nanometers.
     */
    def toNanometers: Quantity[PosBigDecimal, Nanometer] =
      to[Nanometer](3)

    /**
     * Alias for `toNanometers`.
     */
    def nm: Quantity[PosBigDecimal, Nanometer] =
      toNanometers

    /**
     * Returns the wavelength value in microns.
     */
    def toMicrometers: Quantity[PosBigDecimal, Micrometer] =
      to[Micrometer](6)

    /** Alias for `toMicrometers`. */
    def µm: Quantity[PosBigDecimal, Micrometer] =
      toMicrometers
  }

  def apply(pm: Quantity[PosInt, Picometer]): WavelengthDelta =
    pm

  /**
    * Construct a wavelength range from a positive int
    * @group constructor
    */
  @targetName("applyPicometers") // to distinguish from apply(Quantity[PosInt, Picometer])
  def apply(picometers: PosInt): WavelengthDelta =
    picometers.withUnit[Picometer]

  /** @group Typeclass Instances */
  given Show[WavelengthDelta] =
    Show.fromToString

  /** @group Typeclass Instances */
  given Order[WavelengthDelta] =
    Order.by(_.value)

  val picometers: Iso[PosInt, WavelengthDelta] =
    Iso[PosInt, WavelengthDelta](_.withUnit[Picometer])(_.toPicometers.value)

  def pbdFormat[U](right: Int)(to: WavelengthDelta => Quantity[PosBigDecimal, U]): Format[Quantity[PosBigDecimal, U], WavelengthDelta] = {
    def from(v: Quantity[PosBigDecimal, U]): Option[WavelengthDelta] =
      (scala.util.control.Exception.catching(classOf[ArithmeticException]) opt
        v.value
         .bigDecimal
         .movePointRight(right)
         .setScale(0, RoundingMode.HALF_UP)
         .intValueExact
      ).flatMap(i => PosInt.from(i).toOption).map(Quantity[Picometer](_))

    Format[Quantity[PosBigDecimal, U], WavelengthDelta](from, to)
  }

  val angstroms: Format[Quantity[PosBigDecimal, Angstrom], WavelengthDelta] =
    pbdFormat(2)(_.toAngstroms)

  val nanometers: Format[Quantity[PosBigDecimal, Nanometer], WavelengthDelta] =
    pbdFormat(3)(_.toNanometers)

  val micrometers: Format[Quantity[PosBigDecimal, Micrometer], WavelengthDelta] =
    pbdFormat(6)(_.toMicrometers)

  val intPicometers: Prism[Int, WavelengthDelta] =
    Prism[Int, WavelengthDelta](pm =>
      PosInt.from(pm).toOption.map(_.withUnit[Picometer])
    )(_.value.value)

  /**
   * Try to build a Wavelength from a plain Int. Negatives and Zero will produce a None.
   * @group constructor
   */
  def fromIntPicometers(i: Int): Option[WavelengthDelta] =
    intPicometers.getOption(i)

  def unsafeFromIntPicometers(i: Int): WavelengthDelta =
    fromIntPicometers(i).getOrElse(sys.error(s"Cannot build a WavelengthDelta with value $i"))

  private def fromInt(max: Int, mult: Int): Int => Option[WavelengthDelta] =
    i => Option.when((i > 0) && (i <= max))(i * mult).flatMap(fromIntPicometers)

  def fromIntAngstroms(i: Int): Option[WavelengthDelta] =
    fromInt(Wavelength.MaxAngstrom, 100)(i)

  def fromIntNanometers(i: Int): Option[WavelengthDelta] =
    fromInt(Wavelength.MaxNanometer, 1_000)(i)

  def fromIntMicrometers(i: Int): Option[WavelengthDelta] =
    fromInt(Wavelength.MaxMicrometer, 1_000_000)(i)

  private def scalingFormat(move: Int): Format[BigDecimal, WavelengthDelta] =
    Format[BigDecimal, Int](
      bd => scala.util.control.Exception.catching(classOf[ArithmeticException]) opt
        bd.bigDecimal.movePointRight(move).setScale(0, RoundingMode.HALF_UP).intValueExact,
      i  => BigDecimal(new java.math.BigDecimal(i).movePointLeft(move))
    ).andThen(intPicometers)

  val decimalPicometers: Format[BigDecimal, WavelengthDelta] =
    scalingFormat(0)

  val decimalAngstroms: Format[BigDecimal, WavelengthDelta] =
    scalingFormat(2)

  val decimalNanometers: Format[BigDecimal, WavelengthDelta] =
    scalingFormat(3)

  val decimalMicrometers: Format[BigDecimal, WavelengthDelta] =
    scalingFormat(6)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy