diff --git a/modules/benchmarks/src/test/scala/lucuma/itc/LegacyITCSimulation.scala b/modules/benchmarks/src/test/scala/lucuma/itc/LegacyITCSimulation.scala
deleted file mode 100644
index 376a41fe..00000000
--- a/modules/benchmarks/src/test/scala/lucuma/itc/LegacyITCSimulation.scala
+++ /dev/null
@@ -1,618 +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 lucuma.itc
-
-import cats.implicits.*
-import coulomb.*
-import coulomb.syntax.*
-import coulomb.units.si.*
-import eu.timepit.refined.types.numeric.PosInt
-import io.circe.syntax.*
-import io.gatling.core.Predef.*
-import io.gatling.http.Predef.*
-import io.gatling.http.funspec.GatlingHttpFunSpec
-import lucuma.core.enums.*
-import lucuma.core.math.Angle
-import lucuma.core.math.BrightnessUnits.*
-import lucuma.core.math.BrightnessValue
-import lucuma.core.math.Redshift
-import lucuma.core.math.Wavelength
-import lucuma.core.math.dimensional.*
-import lucuma.core.math.dimensional.syntax.*
-import lucuma.core.math.units.*
-import lucuma.core.model.SourceProfile
-import lucuma.core.model.SpectralDefinition
-import lucuma.core.model.UnnormalizedSED
-import lucuma.core.model.sequence.gmos.GmosCcdMode
-import lucuma.core.util.Enumerated
-import lucuma.itc.legacy.*
-import lucuma.itc.legacy.given
-import lucuma.itc.search.GmosNorthFpuParam
-import lucuma.itc.search.GmosSouthFpuParam
-import lucuma.itc.search.ItcObservationDetails
-import lucuma.itc.search.ObservingMode
-import lucuma.itc.search.TargetData
-import lucuma.itc.search.syntax.*
-import lucuma.refined.*
-
-import scala.collection.immutable.SortedMap
-
-/**
- * This is a unit test mostly to ensure all possible combination of params can be parsed by the
- * legacy ITC (Note that the ITC may still return an error but we want to ensure it can parse the
- * values
- */
-class LegacyITCSimulation extends GatlingHttpFunSpec {
-  val headers_10 = Map("Content-Type" -> """application/json""")
-  val baseUrl    = "https://gemini-new-itc.herokuapp.com"
-
-  val sourceDefinition = ItcSourceDefinition(
-    TargetData(
-      SourceProfile.Point(
-        SpectralDefinition.BandNormalized(
-          UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
-          SortedMap(
-            Band.R -> BrightnessValue
-              .unsafeFrom(5)
-              .withUnit[VegaMagnitude]
-              .toMeasureTagged
-          )
-        )
-      ),
-      Redshift(0.03)
-    ),
-    Band.R.asLeft
-  )
-
-  val lsAnalysisMethod  = ItcObservationDetails.AnalysisMethod.Aperture.Auto(5)
-  val ifuAnalysisMethod =
-    ItcObservationDetails.AnalysisMethod.Ifu.Single(skyFibres = 250, offset = 5.0)
-
-  val obs = ItcObservationDetails(
-    calculationMethod = ItcObservationDetails.CalculationMethod.SignalToNoise.Spectroscopy(
-      exposureCount = 1,
-      coadds = None,
-      exposureDuration = 1,
-      sourceFraction = 1.0,
-      ditherOffset = Angle.Angle0
-    ),
-    analysisMethod = lsAnalysisMethod
-  )
-
-  val telescope  = ItcTelescopeDetails(
-    wfs = ItcWavefrontSensor.OIWFS
-  )
-  val instrument = ItcInstrumentDetails.fromObservingMode(
-    ObservingMode.SpectroscopyMode.GmosNorth(
-      Wavelength.decimalNanometers.getOption(600).get,
-      GmosNorthGrating.B1200_G5301,
-      GmosNorthFpuParam(GmosNorthFpu.LongSlit_5_00),
-      none,
-      GmosCcdMode(GmosXBinning.Two,
-                  GmosYBinning.Two,
-                  GmosAmpCount.Twelve,
-                  GmosAmpGain.High,
-                  GmosAmpReadMode.Fast
-      ).some,
-      GmosRoi.FullFrame.some
-    )
-  )
-
-  val conditions = ItcObservingConditions(ImageQuality.PointEight,
-                                          CloudExtinction.OnePointFive,
-                                          WaterVapor.Median,
-                                          SkyBackground.Bright,
-                                          2
-  )
-
-  def bodyCond(c: ItcObservingConditions) =
-    ItcParameters(
-      sourceDefinition,
-      obs,
-      c,
-      telescope,
-      instrument
-    )
-
-  Enumerated[ImageQuality].all.map { iq =>
-    spec {
-      http("sanity_cond_iq")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyCond(conditions.copy(iq = iq)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[CloudExtinction].all.map { ce =>
-    spec {
-      http("sanity_cond_ce")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyCond(conditions.copy(cc = ce)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[WaterVapor].all.map { wv =>
-    spec {
-      http("sanity_cond_wv")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyCond(conditions.copy(wv = wv)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[SkyBackground].all.map { sb =>
-    spec {
-      http("sanity_cond_sb")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("ItcSpectroscopyResult").exists)
-        .check(substring("decode").notExists)
-        .body(StringBody(bodyCond(conditions.copy(sb = sb)).asJson.noSpaces))
-    }
-  }
-
-  val gnConf = ObservingMode.SpectroscopyMode.GmosNorth(
-    Wavelength.decimalNanometers.getOption(600).get,
-    GmosNorthGrating.B1200_G5301,
-    GmosNorthFpuParam(GmosNorthFpu.LongSlit_1_00),
-    none,
-    GmosCcdMode(GmosXBinning.Two,
-                GmosYBinning.Two,
-                GmosAmpCount.Twelve,
-                GmosAmpGain.High,
-                GmosAmpReadMode.Fast
-    ).some,
-    GmosRoi.FullFrame.some
-  )
-
-  val gsConf = ObservingMode.SpectroscopyMode.GmosSouth(
-    Wavelength.decimalNanometers.getOption(600).get,
-    GmosSouthGrating.B1200_G5321,
-    GmosSouthFpuParam(GmosSouthFpu.LongSlit_1_00),
-    none,
-    GmosCcdMode(GmosXBinning.Two,
-                GmosYBinning.Two,
-                GmosAmpCount.Twelve,
-                GmosAmpGain.High,
-                GmosAmpReadMode.Fast
-    ).some,
-    GmosRoi.FullFrame.some
-  )
-
-  def bodyConf(
-    c:        ObservingMode.SpectroscopyMode,
-    analysis: ItcObservationDetails.AnalysisMethod = lsAnalysisMethod
-  ) =
-    ItcParameters(
-      sourceDefinition,
-      obs.copy(analysisMethod = analysis),
-      ItcObservingConditions(ImageQuality.PointEight,
-                             CloudExtinction.OnePointFive,
-                             WaterVapor.Median,
-                             SkyBackground.Dark,
-                             2
-      ),
-      telescope,
-      ItcInstrumentDetails.fromObservingMode(c)
-    )
-
-  Enumerated[GmosNorthGrating].all.map { d =>
-    spec {
-      http("sanity_gn_disperser")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyConf(gnConf.copy(disperser = d)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[GmosNorthFpu].all.map { f =>
-    spec {
-      http("sanity_gn_fpu")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200, 400))
-        .check(substring("decode").notExists)
-        .body(StringBody(bodyConf(gnConf.copy(fpu = GmosNorthFpuParam(f))).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[GmosNorthFilter].all.map { f =>
-    spec {
-      http("sanity_gn_filter")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200, 400))
-        .check(substring("decode").notExists)
-        // .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyConf(gnConf.copy(filter = f.some)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[GmosSouthGrating].all.map { d =>
-    spec {
-      http("sanity_gs_disperser")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyConf(gsConf.copy(disperser = d)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[GmosSouthFpu].all
-    .filter(f => f =!= GmosSouthFpu.Bhros)
-    .map { f =>
-      val conf =
-        if (f.isGSIfu)
-          bodyConf(gsConf.copy(fpu = GmosSouthFpuParam(f)), ifuAnalysisMethod)
-        else
-          bodyConf(gsConf.copy(fpu = GmosSouthFpuParam(f)))
-      spec {
-        http("sanity_gs_fpu")
-          .post("/json")
-          .headers(headers_10)
-          .check(status.in(200))
-          .check(substring("decode").notExists)
-          .check(substring("ItcSpectroscopyResult").exists)
-          .body(StringBody(conf.asJson.noSpaces))
-      }
-    }
-
-  Enumerated[GmosSouthFilter].all.map { f =>
-    spec {
-      http("sanity_gn_filter")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200, 400))
-        .check(substring("decode").notExists)
-        // .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodyConf(gsConf.copy(filter = f.some)).asJson.noSpaces))
-    }
-  }
-
-  def bodySED(c: UnnormalizedSED) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile.unnormalizedSED
-            .modifyOption(_ => c.some)(sourceDefinition.sourceProfile)
-            .getOrElse(sourceDefinition.sourceProfile)
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  Enumerated[StellarLibrarySpectrum].all.map { f =>
-    spec {
-      http("stellar_library")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.StellarLibrary(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[CoolStarTemperature].all.map { f =>
-    spec {
-      http("cool_star")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.CoolStarModel(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[GalaxySpectrum].all.map { f =>
-    spec {
-      http("galaxy")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.Galaxy(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[PlanetSpectrum].all.map { f =>
-    spec {
-      http("planet")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.Planet(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[QuasarSpectrum].all.map { f =>
-    spec {
-      http("quasar")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.Quasar(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[HIIRegionSpectrum].all.map { f =>
-    spec {
-      http("hiiregion")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.HIIRegion(f)).asJson.noSpaces))
-    }
-  }
-
-  Enumerated[PlanetaryNebulaSpectrum].all.map { f =>
-    spec {
-      http("quasar")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(StringBody(bodySED(UnnormalizedSED.PlanetaryNebula(f)).asJson.noSpaces))
-    }
-  }
-
-  def bodyIntMagUnits(c: BrightnessMeasure[Integrated]) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile
-            .integratedBrightnessIn(Band.R)
-            .replace(c)(sourceDefinition.sourceProfile)
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  Brightness.Integrated.all.map { f =>
-    spec {
-      http("integrated units")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(
-          StringBody(
-            bodyIntMagUnits(
-              f.withValueTagged(BrightnessValue.unsafeFrom(5))
-            ).asJson.noSpaces
-          )
-        )
-    }
-  }
-
-  def bodySurfaceMagUnits(c: BrightnessMeasure[Surface]) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile.Uniform(
-            SpectralDefinition.BandNormalized(
-              UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
-              SortedMap(Band.R -> c)
-            )
-          )
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  Brightness.Surface.all.map { f =>
-    spec {
-      http("surface units")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(
-          StringBody(
-            bodySurfaceMagUnits(
-              f.withValueTagged(BrightnessValue.unsafeFrom(5))
-            ).asJson.noSpaces
-          )
-        )
-    }
-  }
-
-  def bodyIntGaussianMagUnits(c: BrightnessMeasure[Integrated]) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile.Gaussian(
-            Angle.fromDoubleArcseconds(10),
-            SpectralDefinition.BandNormalized(
-              UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
-              SortedMap(Band.R -> c)
-            )
-          )
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  Brightness.Integrated.all.map { f =>
-    spec {
-      http("gaussian integrated units")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(
-          StringBody(
-            bodyIntGaussianMagUnits(
-              f.withValueTagged(BrightnessValue.unsafeFrom(5))
-            ).asJson.noSpaces
-          )
-        )
-    }
-  }
-
-  def bodyPowerLaw(c: Int) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile.Gaussian(
-            Angle.fromDoubleArcseconds(10),
-            SpectralDefinition.BandNormalized(
-              UnnormalizedSED.PowerLaw(c).some,
-              SortedMap(
-                Band.R ->
-                  BrightnessValue
-                    .unsafeFrom(5)
-                    .withUnit[VegaMagnitude]
-                    .toMeasureTagged
-              )
-            )
-          )
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  List(-10, 0, 10, 100).map { f =>
-    spec {
-      http("power law")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(
-          StringBody(
-            bodyPowerLaw(f).asJson.noSpaces
-          )
-        )
-    }
-  }
-
-  def bodyBlackBody(c: PosInt) =
-    ItcParameters(
-      sourceDefinition.copy(
-        target = sourceDefinition.target.copy(
-          sourceProfile = SourceProfile.Gaussian(
-            Angle.fromDoubleArcseconds(10),
-            SpectralDefinition.BandNormalized(
-              UnnormalizedSED.BlackBody(c.withUnit[Kelvin]).some,
-              SortedMap(
-                Band.R ->
-                  BrightnessValue
-                    .unsafeFrom(5)
-                    .withUnit[VegaMagnitude]
-                    .toMeasureTagged
-              )
-            )
-          )
-        )
-      ),
-      obs,
-      conditions,
-      telescope,
-      instrument
-    )
-
-  List[PosInt](10.refined, 100.refined).map { f =>
-    spec {
-      http("black body")
-        .post("/json")
-        .headers(headers_10)
-        .check(status.in(200))
-        .check(substring("decode").notExists)
-        .check(substring("ItcSpectroscopyResult").exists)
-        .body(
-          StringBody(
-            bodyBlackBody(f).asJson.noSpaces
-          )
-        )
-    }
-  }
-
-  // def bodyEmissionLine(c: PosBigDecimal) =
-  //   ItcParameters(
-  //     sourceDefinition.copy(profile =
-  //       SourceProfile.Point(
-  //         SpectralDefinition.EmissionLines(
-  //           SortedMap(
-  //             Wavelength.decimalNanometers.getOption(600).get -> EmissionLine(
-  //               c.withUnit[KilometersPerSecond],
-  //               c
-  //                 .withUnit[WattsPerMeter2]
-  //                 .toMeasureTagged
-  //             )
-  //           ),
-  //           BigDecimal(5)
-  //             .withRefinedUnit[Positive, WattsPerMeter2Micrometer]
-  //             .toMeasureTagged
-  //         )
-  //       )
-  //     ),
-  //     obs,
-  //     conditions,
-  //     telescope,
-  //     instrument
-  //   )
-  //
-  // List[PosBigDecimal](BigDecimal(0.1), BigDecimal(10), BigDecimal(100)).map { f =>
-  //   println(bodyEmissionLine(f).asJson)
-  //   spec {
-  //     http("emission line")
-  //       .post("/json")
-  //       .headers(headers_10)
-  //       .check(status.in(200))
-  //       .check(substring("decode").notExists)
-  //       .check(substring("ItcSpectroscopyResult").exists)
-  //       .body(
-  //         StringBody(
-  //           bodyEmissionLine(f).asJson.noSpaces
-  //         )
-  //       )
-  //   }
-  // }
-
-}
diff --git a/modules/service/src/main/scala/lucuma/itc/legacy/package.scala b/modules/service/src/main/scala/lucuma/itc/legacy/package.scala
index 9e8e81af..c0511404 100644
--- a/modules/service/src/main/scala/lucuma/itc/legacy/package.scala
+++ b/modules/service/src/main/scala/lucuma/itc/legacy/package.scala
@@ -38,10 +38,6 @@ case class ItcParameters(
 
 case class ItcInstrumentDetails(mode: ObservingMode)
 
-object ItcInstrumentDetails:
-  def fromObservingMode(mode: ObservingMode): ItcInstrumentDetails =
-    apply(mode)
-
 private def buildSourceDefinition(
   target:       TargetData,
   atWavelength: Wavelength
@@ -77,7 +73,7 @@ def spectroscopyGraphParams(
       telescope = ItcTelescopeDetails(
         wfs = ItcWavefrontSensor.OIWFS
       ),
-      instrument = ItcInstrumentDetails.fromObservingMode(observingMode)
+      instrument = ItcInstrumentDetails(observingMode)
     )
   (parameters, bandOrLine)
 
@@ -108,7 +104,7 @@ def spectroscopyExposureTimeParams(
       telescope = ItcTelescopeDetails(
         wfs = ItcWavefrontSensor.OIWFS
       ),
-      instrument = ItcInstrumentDetails.fromObservingMode(observingMode)
+      instrument = ItcInstrumentDetails(observingMode)
     )
   (parameters, bandOrLine)
 
@@ -137,6 +133,6 @@ def imagingParams(
       telescope = ItcTelescopeDetails(
         wfs = ItcWavefrontSensor.OIWFS
       ),
-      instrument = ItcInstrumentDetails.fromObservingMode(observingMode)
+      instrument = ItcInstrumentDetails(observingMode)
     )
   (parameters, bandOrLine)
diff --git a/modules/service/src/main/scala/lucuma/itc/legacy/syntax/InstrumentSyntax.scala b/modules/service/src/main/scala/lucuma/itc/legacy/syntax/InstrumentSyntax.scala
index 15f7aa84..e54a7892 100644
--- a/modules/service/src/main/scala/lucuma/itc/legacy/syntax/InstrumentSyntax.scala
+++ b/modules/service/src/main/scala/lucuma/itc/legacy/syntax/InstrumentSyntax.scala
@@ -173,7 +173,7 @@ trait GmosSouthFpuSyntax:
         case LongSlit_1_50       => "LONGSLIT_5"
         case LongSlit_2_00       => "LONGSLIT_6"
         case LongSlit_5_00       => "LONGSLIT_7"
-        case Bhros               => "BHROs"
+        case Bhros               => "BHROS"
       }
 
 object gmossouthfpu extends GmosSouthFpuSyntax
diff --git a/modules/service/src/main/scala/lucuma/itc/search/syntax/GmosSouthFpu.scala b/modules/service/src/main/scala/lucuma/itc/search/syntax/GmosSouthFpu.scala
index 165639c1..952b1acb 100644
--- a/modules/service/src/main/scala/lucuma/itc/search/syntax/GmosSouthFpu.scala
+++ b/modules/service/src/main/scala/lucuma/itc/search/syntax/GmosSouthFpu.scala
@@ -26,4 +26,4 @@ extension (self: GmosSouthFpu)
       case LongSlit_1_50                                                     => false
       case LongSlit_2_00                                                     => false
       case LongSlit_5_00                                                     => false
-      case Bhros                                                             => sys.error("obsolete")
+      case Bhros                                                             => false
diff --git a/modules/tests/src/test/scala/lucuma/itc/tests/LegacyITCSuite.scala b/modules/tests/src/test/scala/lucuma/itc/tests/LegacyITCSuite.scala
new file mode 100644
index 00000000..23e44528
--- /dev/null
+++ b/modules/tests/src/test/scala/lucuma/itc/tests/LegacyITCSuite.scala
@@ -0,0 +1,540 @@
+// 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.itc
+
+import cats.implicits.*
+import coulomb.*
+import coulomb.syntax.*
+import coulomb.units.si.*
+import eu.timepit.refined.types.numeric.PosInt
+import io.circe.syntax.*
+import lucuma.core.enums.*
+import lucuma.core.math.Angle
+import lucuma.core.math.BrightnessUnits.*
+import lucuma.core.math.BrightnessValue
+import lucuma.core.math.Redshift
+import lucuma.core.math.Wavelength
+import lucuma.core.math.dimensional.*
+import lucuma.core.math.dimensional.Units
+import lucuma.core.math.dimensional.syntax.*
+import lucuma.core.math.units.*
+import lucuma.core.model.SourceProfile
+import lucuma.core.model.SpectralDefinition
+import lucuma.core.model.UnnormalizedSED
+import lucuma.core.model.sequence.gmos.GmosCcdMode
+import lucuma.core.util.*
+import lucuma.itc.legacy.*
+import lucuma.itc.legacy.given
+import lucuma.itc.search.GmosNorthFpuParam
+import lucuma.itc.search.GmosSouthFpuParam
+import lucuma.itc.search.ItcObservationDetails
+import lucuma.itc.search.ObservingMode
+import lucuma.itc.search.TargetData
+import lucuma.itc.service.Main.ReverseClassLoader
+import munit.FunSuite
+
+import java.io.File
+import java.io.FileFilter
+import scala.collection.immutable.SortedMap
+
+/**
+ * This is a unit test mostly to ensure all possible combination of params can be parsed by the
+ * legacy ITC (Note that the ITC may still return an error but we want to ensure it can parse the
+ * values
+ */
+class LegacyITCSuite extends FunSuite {
+
+  val sourceDefinition = ItcSourceDefinition(
+    TargetData(
+      SourceProfile.Point(
+        SpectralDefinition.BandNormalized(
+          UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
+          SortedMap(
+            Band.R -> BrightnessValue
+              .unsafeFrom(9)
+              .withUnit[VegaMagnitude]
+              .toMeasureTagged
+          )
+        )
+      ),
+      Redshift(0.03)
+    ),
+    Band.R.asLeft
+  )
+
+  val lsAnalysisMethod  = ItcObservationDetails.AnalysisMethod.Aperture.Auto(5)
+  val ifuAnalysisMethod =
+    ItcObservationDetails.AnalysisMethod.Ifu.Single(skyFibres = 250, offset = 5.0)
+
+  val obs = ItcObservationDetails(
+    calculationMethod = ItcObservationDetails.CalculationMethod.SignalToNoise.SpectroscopyWithSNAt(
+      sigma = 1,
+      wavelength = Wavelength.decimalNanometers.getOption(600).get,
+      coadds = None,
+      sourceFraction = 1.0,
+      ditherOffset = Angle.Angle0
+    ),
+    analysisMethod = lsAnalysisMethod
+  )
+
+  val telescope  = ItcTelescopeDetails(
+    wfs = ItcWavefrontSensor.OIWFS
+  )
+  val instrument = ItcInstrumentDetails(
+    ObservingMode.SpectroscopyMode.GmosNorth(
+      Wavelength.decimalNanometers.getOption(600).get,
+      GmosNorthGrating.B1200_G5301,
+      GmosNorthFpuParam(GmosNorthFpu.LongSlit_5_00),
+      none,
+      GmosCcdMode(GmosXBinning.One,
+                  GmosYBinning.One,
+                  GmosAmpCount.Twelve,
+                  GmosAmpGain.High,
+                  GmosAmpReadMode.Fast
+      ).some,
+      GmosRoi.FullFrame.some
+    )
+  )
+
+  val conditions = ItcObservingConditions(ImageQuality.PointEight,
+                                          CloudExtinction.OnePointFive,
+                                          WaterVapor.Median,
+                                          SkyBackground.Bright,
+                                          1
+  )
+
+  def bodyCond(c: ItcObservingConditions) =
+    ItcParameters(
+      sourceDefinition,
+      obs,
+      c,
+      telescope,
+      instrument
+    )
+
+  def allowedErrors(err: List[String]) =
+    err.exists(_.contains("Invalid S/N")) || err.exists(_.contains("do not overlap")) ||
+      err.exists(_.contains("Unsupported configuration")) ||
+      err.exists(_.contains("Unsupported calculation method")) ||
+      err.exists(_.contains("target is too bright"))
+
+  val localItc = {
+    val jarFiles =
+      new File("modules/service/ocslib").listFiles(new FileFilter() {
+        override def accept(file: File): Boolean =
+          file.getName().endsWith(".jar");
+      })
+    LocalItc(
+      new ReverseClassLoader(jarFiles.map(_.toURI.toURL), ClassLoader.getSystemClassLoader())
+    )
+  }
+
+  test("image quality") {
+    Enumerated[ImageQuality].all.foreach { iq =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyCond(conditions.copy(iq = iq)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  test("cloud extinction") {
+    Enumerated[CloudExtinction].all.foreach { ce =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyCond(conditions.copy(cc = ce)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  test("water vapor") {
+    Enumerated[WaterVapor].all.foreach { wv =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyCond(conditions.copy(wv = wv)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  test("sky background") {
+    Enumerated[SkyBackground].all.foreach { sb =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyCond(conditions.copy(sb = sb)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  val gnConf = ObservingMode.SpectroscopyMode.GmosNorth(
+    Wavelength.decimalNanometers.getOption(600).get,
+    GmosNorthGrating.B1200_G5301,
+    GmosNorthFpuParam(GmosNorthFpu.LongSlit_1_00),
+    none,
+    GmosCcdMode(GmosXBinning.One,
+                GmosYBinning.One,
+                GmosAmpCount.Twelve,
+                GmosAmpGain.High,
+                GmosAmpReadMode.Fast
+    ).some,
+    GmosRoi.FullFrame.some
+  )
+
+  def bodyConf(
+    c:        ObservingMode.SpectroscopyMode,
+    analysis: ItcObservationDetails.AnalysisMethod = lsAnalysisMethod
+  ) =
+    ItcParameters(
+      sourceDefinition,
+      obs.copy(analysisMethod = analysis),
+      ItcObservingConditions(ImageQuality.PointEight,
+                             CloudExtinction.OnePointFive,
+                             WaterVapor.Median,
+                             SkyBackground.Dark,
+                             2
+      ),
+      telescope,
+      ItcInstrumentDetails(c)
+    )
+
+  test("gmos north grating") {
+    Enumerated[GmosNorthGrating].all.map { d =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyConf(gnConf.copy(disperser = d)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  test("gmos north filter") {
+    Enumerated[GmosNorthFilter].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodyConf(gnConf.copy(filter = f.some)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("gmos north fpu") {
+    Enumerated[GmosNorthFpu].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(
+          bodyConf(gnConf.copy(fpu = GmosNorthFpuParam(f)),
+                   analysis = if (f.isIFU) ifuAnalysisMethod else lsAnalysisMethod
+          ).asJson.noSpaces
+        )
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  val gsConf = ObservingMode.SpectroscopyMode.GmosSouth(
+    Wavelength.decimalNanometers.getOption(600).get,
+    GmosSouthGrating.B1200_G5321,
+    GmosSouthFpuParam(GmosSouthFpu.LongSlit_1_00),
+    none,
+    GmosCcdMode(GmosXBinning.One,
+                GmosYBinning.One,
+                GmosAmpCount.Twelve,
+                GmosAmpGain.High,
+                GmosAmpReadMode.Fast
+    ).some,
+    GmosRoi.FullFrame.some
+  )
+
+  test("gmos south grating") {
+    Enumerated[GmosSouthGrating].all.map { d =>
+      assert(
+        localItc
+          .calculateIntegrationTime(bodyConf(gsConf.copy(disperser = d)).asJson.noSpaces)
+          .isRight
+      )
+    }
+  }
+
+  test("gmos south filter") {
+    Enumerated[GmosSouthFilter].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodyConf(gsConf.copy(filter = f.some)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("gmos south fpu") {
+    Enumerated[GmosSouthFpu].all.foreach { f =>
+      val result = localItc
+        .calculateIntegrationTime(
+          bodyConf(gsConf.copy(fpu = GmosSouthFpuParam(f)),
+                   analysis = if (f.isIFU) ifuAnalysisMethod else lsAnalysisMethod
+          ).asJson.noSpaces
+        )
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodySED(c: UnnormalizedSED) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile.unnormalizedSED
+            .modifyOption(_ => c.some)(sourceDefinition.sourceProfile)
+            .getOrElse(sourceDefinition.sourceProfile)
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  test("stellar library spectrum") {
+    Enumerated[StellarLibrarySpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.StellarLibrary(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("cool star") {
+    Enumerated[CoolStarTemperature].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.CoolStarModel(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("galaxy spectrum") {
+    Enumerated[GalaxySpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.Galaxy(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("planet spectrum") {
+    Enumerated[PlanetSpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.Planet(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("quasar spectrum") {
+    Enumerated[QuasarSpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.Quasar(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("hii region spectrum") {
+    Enumerated[HIIRegionSpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.HIIRegion(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  test("planetary nebula spectrum") {
+    Enumerated[PlanetaryNebulaSpectrum].all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodySED(UnnormalizedSED.PlanetaryNebula(f)).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodyIntMagUnits(c: BrightnessMeasure[Integrated]) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile
+            .integratedBrightnessIn(Band.R)
+            .replace(c)(sourceDefinition.sourceProfile)
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  test("brightness integrated units") {
+    Brightness.Integrated.all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(
+          bodyIntMagUnits(f.withValueTagged(BrightnessValue.unsafeFrom(5))).asJson.noSpaces
+        )
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodySurfaceMagUnits(c: BrightnessMeasure[Surface]) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile.Uniform(
+            SpectralDefinition.BandNormalized(
+              UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
+              SortedMap(Band.R -> c)
+            )
+          )
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  test("surface units") {
+    Brightness.Surface.all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(
+          bodySurfaceMagUnits(f.withValueTagged(BrightnessValue.unsafeFrom(5))).asJson.noSpaces
+        )
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodyIntGaussianMagUnits(c: BrightnessMeasure[Integrated]) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile.Gaussian(
+            Angle.fromDoubleArcseconds(10),
+            SpectralDefinition.BandNormalized(
+              UnnormalizedSED.StellarLibrary(StellarLibrarySpectrum.A0V).some,
+              SortedMap(Band.R -> c)
+            )
+          )
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  test("gaussian units") {
+    Brightness.Integrated.all.map { f =>
+      val result = localItc
+        .calculateIntegrationTime(
+          bodyIntGaussianMagUnits(f.withValueTagged(BrightnessValue.unsafeFrom(5))).asJson.noSpaces
+        )
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodyPowerLaw(c: Int) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile.Gaussian(
+            Angle.fromDoubleArcseconds(10),
+            SpectralDefinition.BandNormalized(
+              UnnormalizedSED.PowerLaw(c).some,
+              SortedMap(
+                Band.R ->
+                  BrightnessValue
+                    .unsafeFrom(5)
+                    .withUnit[VegaMagnitude]
+                    .toMeasureTagged
+              )
+            )
+          )
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  test("power law") {
+    List(-10, 0, 10, 100).map { f =>
+      val result = localItc
+        .calculateIntegrationTime(bodyPowerLaw(f).asJson.noSpaces)
+      assert(result.fold(allowedErrors, _ => true))
+    }
+  }
+
+  def bodyBlackBody(c: PosInt) =
+    ItcParameters(
+      sourceDefinition.copy(
+        target = sourceDefinition.target.copy(
+          sourceProfile = SourceProfile.Gaussian(
+            Angle.fromDoubleArcseconds(10),
+            SpectralDefinition.BandNormalized(
+              UnnormalizedSED.BlackBody(c.withUnit[Kelvin]).some,
+              SortedMap(
+                Band.R ->
+                  BrightnessValue
+                    .unsafeFrom(5)
+                    .withUnit[VegaMagnitude]
+                    .toMeasureTagged
+              )
+            )
+          )
+        )
+      ),
+      obs,
+      conditions,
+      telescope,
+      instrument
+    )
+
+  // test("black body") {
+  //   List[PosInt](10.refined, 100.refined).map { f =>
+  //     val result = localItc
+  //       .calculateIntegrationTime(bodyBlackBody(f).asJson.noSpaces)
+  //     println(result)
+  //     assert(result.fold(allowedErrors, _ => true))
+  //   }
+  // }
+
+  // def bodyEmissionLine(c: PosBigDecimal) =
+  //   ItcParameters(
+  //     sourceDefinition.copy(profile =
+  //       SourceProfile.Point(
+  //         SpectralDefinition.EmissionLines(
+  //           SortedMap(
+  //             Wavelength.decimalNanometers.getOption(600).get -> EmissionLine(
+  //               c.withUnit[KilometersPerSecond],
+  //               c
+  //                 .withUnit[WattsPerMeter2]
+  //                 .toMeasureTagged
+  //             )
+  //           ),
+  //           BigDecimal(5)
+  //             .withRefinedUnit[Positive, WattsPerMeter2Micrometer]
+  //             .toMeasureTagged
+  //         )
+  //       )
+  //     ),
+  //     obs,
+  //     conditions,
+  //     telescope,
+  //     instrument
+  //   )
+  //
+  // List[PosBigDecimal](BigDecimal(0.1), BigDecimal(10), BigDecimal(100)).map { f =>
+  //   println(bodyEmissionLine(f).asJson)
+  //   spec {
+  //     http("emission line")
+  //       .post("/json")
+  //       .headers(headers_10)
+  //       .check(status.in(200))
+  //       .check(substring("decode").notExists)
+  //       .check(substring("ItcSpectroscopyResult").exists)
+  //       .body(
+  //         StringBody(
+  //           bodyEmissionLine(f).asJson.noSpaces
+  //         )
+  //       )
+  //   }
+  // }
+
+}