diff --git a/js/EvaluatorCompanion.scala b/js/EvaluatorCompanion.scala new file mode 100644 index 0000000..ef7edfe --- /dev/null +++ b/js/EvaluatorCompanion.scala @@ -0,0 +1,4 @@ +package genovese + +private[genovese] trait EvaluatorCompanion: + val default = SequentialEvaluator diff --git a/jvm/ParallelCollectionsEvaluator.scala b/jvm/ParallelCollectionsEvaluator.scala new file mode 100644 index 0000000..da4334b --- /dev/null +++ b/jvm/ParallelCollectionsEvaluator.scala @@ -0,0 +1,20 @@ +package genovese + +//> using dep "org.scala-lang.modules::scala-parallel-collections::1.0.4" + +object ParallelCollectionsEvaluator extends Evaluator: + override def evaluate(pop: Population, fitness: Vec => NormalisedFloat): Evaluated = + import scala.collection.parallel.CollectionConverters.* + Evaluated( + IArray.unsafeFromArray( + IArray + .genericWrapArray(pop) + .toArray + .par + .map(vec => vec -> fitness(vec)) + .toArray + ) + ) + +private[genovese] trait EvaluatorCompanion: + val default = ParallelCollectionsEvaluator diff --git a/shared/EnumFeatureful.scala b/shared/EnumFeatureful.scala new file mode 100644 index 0000000..0b514ae --- /dev/null +++ b/shared/EnumFeatureful.scala @@ -0,0 +1,28 @@ +package genovese + +import scala.reflect.ClassTag + +final case class EnumFeatureful[T](values: IArray[T]) extends Featureful[T]: + private val lookup = values.zipWithIndex.toMap + override lazy val features: IArray[Feature[?]] = IArray( + Feature.IntCategory(List.tabulate(values.length)(identity)) + ) + override def toFeatures(value: T)(using + RuntimeChecks + ): IArray[NormalisedFloat] = + features.map: + case f @ Feature.IntCategory(_) => + f.toNormalisedFloat(lookup(value)) + + override def fromFeatures( + fv: IArray[NormalisedFloat] + ): T = + features(0): @unchecked match + case f @ Feature.IntCategory(_) => + val idx = + f.fromNormalisedFloat(NormalisedFloat.applyUnsafe(fv(0))) + values(idx) + + override def toString(): String = s"EnumFeatureful(${values.mkString(",")})" +end EnumFeatureful + diff --git a/shared/Evaluator.scala b/shared/Evaluator.scala new file mode 100644 index 0000000..012e388 --- /dev/null +++ b/shared/Evaluator.scala @@ -0,0 +1,10 @@ +package genovese + +trait Evaluator: + def evaluate(population: Population, fitness: Vec => NormalisedFloat): Evaluated + +object Evaluator extends EvaluatorCompanion + +object SequentialEvaluator extends Evaluator: + override def evaluate(population: Population, fitness: Vec => NormalisedFloat): Evaluated = + Evaluated(population.map(v => v -> fitness(v))) diff --git a/EventHandler.scala b/shared/EventHandler.scala similarity index 100% rename from EventHandler.scala rename to shared/EventHandler.scala diff --git a/Feature.scala b/shared/Feature.scala similarity index 100% rename from Feature.scala rename to shared/Feature.scala diff --git a/Feature.test.scala b/shared/Feature.test.scala similarity index 100% rename from Feature.test.scala rename to shared/Feature.test.scala diff --git a/Featureful.scala b/shared/Featureful.scala similarity index 63% rename from Featureful.scala rename to shared/Featureful.scala index 12d07ca..bd51d0b 100644 --- a/Featureful.scala +++ b/shared/Featureful.scala @@ -2,7 +2,6 @@ package genovese import scala.deriving.Mirror import scala.reflect.ClassTag -import scala.util.boundary trait Featureful[T]: @@ -12,69 +11,6 @@ trait Featureful[T]: def fromFeatures(fv: IArray[NormalisedFloat]): T -final case class SingleFeatureful[T](feature: Feature[T]) extends Featureful[T]: - override val features: IArray[Feature[?]] = IArray(feature) - - override def toFeatures(value: T)(using - RuntimeChecks - ): IArray[NormalisedFloat] = - IArray(feature.toNormalisedFloat(value)) - - override def fromFeatures(fv: IArray[NormalisedFloat]): T = - feature.fromNormalisedFloat(fv(0).asInstanceOf[NormalisedFloat]) -end SingleFeatureful - -final case class EnumFeatureful[T](values: IArray[T]) extends Featureful[T]: - private val lookup = values.zipWithIndex.toMap - override lazy val features: IArray[Feature[?]] = IArray( - Feature.IntCategory(List.tabulate(values.length)(identity)) - ) - override def toFeatures(value: T)(using - RuntimeChecks - ): IArray[NormalisedFloat] = - features.map: - case f @ Feature.IntCategory(_) => - f.toNormalisedFloat(lookup(value)) - - override def fromFeatures( - fv: IArray[NormalisedFloat] - ): T = - features(0) match - case f @ Feature.IntCategory(_) => - val idx = - f.fromNormalisedFloat(NormalisedFloat.applyUnsafe(fv(0))) - values(idx) - - override def toString(): String = s"EnumFeatureful(${values.mkString(",")})" -end EnumFeatureful - -final case class OptionFeatureful[T](original: Featureful[T]) - extends Featureful[Option[T]]: - override lazy val features: IArray[Feature[?]] = - original.features.map(Feature.Optional(_)) - override def fromFeatures(fv: IArray[NormalisedFloat]): Option[T] = - boundary: - val decompressed = fv.map: v => - if v < 0.5f then boundary.break(None) - else NormalisedFloat.applyUnsafe(2 * (v - 0.5f)) - Some(original.fromFeatures(decompressed)) - - override def toFeatures(value: Option[T])(using - RuntimeChecks - ): IArray[NormalisedFloat] = - value match - case None => - IArray.fill(original.features.size)(NormalisedFloat.applyUnsafe(0.25f)) - case Some(value) => - original - .toFeatures(value) - .map(v => NormalisedFloat.applyUnsafe(v / 2 + 0.5f)) -end OptionFeatureful - -case class FieldConfig(overrides: Map[String, Feature[?]]) -object FieldConfig: - inline def None = FieldConfig(Map.empty) - object Featureful: def features[T](using f: Featureful[T]) = f.features @@ -160,8 +96,9 @@ object Featureful: case '{ Feature.IntCategory(${ Expr(alts) }) } => Some(Feature.IntCategory(alts).asInstanceOf[Feature[?]]) - // case '{ Feature.Optional(${ Expr(alts) }) } => - // Some(Feature.Optional(alts).asInstanceOf[Feature[?]]) + // TODO: how do I implement this? + // case '{Feature.Optional(${ Expr(alts) }) } => + // Some(Feature.Optional(alts).asInstanceOf[Feature[?]]) // Match the NormalisedFloatRange case case '{ Feature.NormalisedFloatRange } => @@ -241,27 +178,6 @@ object Featureful: '{ EnumFeatureful[T](IArray.from($values)(using $ct)) - // new Featureful[T]: - // private val ars = IArray.from[T]($values)(using $ct) - // override lazy val features: IArray[Feature[?]] = IArray( - // Feature.IntCategory(List.tabulate($values.length)(identity)) - // ) - // override def toFeatures(value: T)(using - // RuntimeChecks - // ): IArray[NormalisedFloat] = - // features.map: - // case f @ Feature.IntCategory(_) => - // f.toNormalisedFloat($m.ordinal(value)) - - // override def fromFeatures( - // fv: IArray[NormalisedFloat] - // ): T = - // features(0) match - // case f @ Feature.IntCategory(_) => - // val idx = - // f.fromNormalisedFloat(NormalisedFloat.applyUnsafe(fv(0))) - // ars(idx) - } case '{ @@ -353,14 +269,6 @@ object Featureful: )(using Quotes, FieldConfig): Expr[Featureful[E]] = import quotes.reflect.* - // val isCaseClass = Implicits.search(TypeRepr.of[Mirror.ProductOf[E]]) match - // case _: ImplicitSearchSuccess => true - // case _ => false - - // val isEnum = Implicits.search(TypeRepr.of[Mirror.SumOf[E]]) match - // case _: ImplicitSearchSuccess => true - // case _ => false - val hasNested = Implicits.search(TypeRepr.of[Featureful[E]]) match case res: ImplicitSearchSuccess => Some(res.tree.asExprOf[Featureful[E]]) @@ -408,18 +316,6 @@ object Featureful: case _ if hasNested.isDefined => hasNested.get - // ??? - - // Span.Enum('{ $e.features.head }, Type.of[E]) - - // case _ if hasNested.isDefined => - // ??? - // val e = hasNested.get - // Span.Splice(e.asExprOf[Featureful[E]], Type.of[E]) - // '{ $e.features.toList }, - // '{ any => $e.toFeatures(any.asInstanceOf[E]) }, - // '{ (arr, default) => $e.fromFeatures(arr, default.asInstanceOf[E]) } - // ) case '[String] => summon[FieldConfig].overrides.get(name) match @@ -443,82 +339,9 @@ object Featureful: end match end constructFeature - // private enum Span: - // case Primitive[T]( - // feature: Expr[Feature[T]], - // tpe: Type[T] - // ) - - // case Enum( - // feature: Expr[Feature[?]], - // tpe: Type[?] - // ) - - // case Splice( - // delegate: Expr[Featureful[?]], - // tpe: Type[?] - // ) - - // def convert(using - // Quotes - // ): Expr[RuntimeChecks ?=> Any => IArray[NormalisedFloat]] = - // this match - // case p @ Primitive(feature, tpe) => - // val (t, f) = cast(tpe) - // given Type[T] = p.tpe - // '{ rc ?=> any => - // IArray( - // $f($feature).toNormalisedFloat($t(any).asInstanceOf)(using rc) - // ) - // } - // case Enum(feature, tpe) => - // '{ rc ?=> any => - // IArray($feature.toNormalisedFloat(any.asInstanceOf)(using rc)) - // } - // case Splice(delegate, tpe) => - // '{ any => $delegate.toFeatures(any.asInstanceOf) } - - // def back(using Quotes): Expr[(IArray[NormalisedFloat], Any) => Any] = - // this match - // case Primitive(feature, _) => - // '{ (ar, _) => - // $feature.fromNormalisedFloat(ar.apply(0).asInstanceOf) - // } - // case Enum(feature, _) => - // '{ (ar, _) => - // $feature.fromNormalisedFloat(ar.apply(0).asInstanceOf) - // } - // case Splice(delegate, _) => - // '{ (arr, default) => - // $delegate.fromFeatures(arr, default.asInstanceOf) - // } - - // def features(using Quotes) = this match - // case Primitive(feature, _) => '{ List($feature) } - // case Enum(feature, _) => '{ List($feature) } - // case Splice(features, _) => '{ $features.features.toList } - - // def size(using Quotes) = this match - // case Primitive(feature, _) => '{ 1 } - // case Enum(feature, _) => '{ 1 } - // case Splice(delegate, _) => '{ $delegate.features.size } - - // end Span - private def single[T: Type](f: Expr[Feature[T]])(using Quotes ): Expr[SingleFeatureful[T]] = '{ SingleFeatureful($f) } - // Span.Primitive( - // f, - // Type.of[T] - // ) - - // private def cast(t: Type[?])(using Quotes) = - // t match - // case '[t] => - // '{ (any: Any) => any.asInstanceOf[t] } -> '{ (any: Feature[?]) => - // any.asInstanceOf[Feature[t]] - // } end Featureful diff --git a/Featureful.test.scala b/shared/Featureful.test.scala similarity index 98% rename from Featureful.test.scala rename to shared/Featureful.test.scala index e544a9e..a636f4a 100644 --- a/Featureful.test.scala +++ b/shared/Featureful.test.scala @@ -1,8 +1,9 @@ package genovese import munit.* -import org.scalacheck.Prop.* import org.scalacheck.Gen +import org.scalacheck.Prop.* + import Featureful.* class FeaturefulTest extends FunSuite, ScalaCheckSuite: @@ -99,7 +100,7 @@ class FeaturefulTest extends FunSuite, ScalaCheckSuite: property("enums roundtrip"): forAll( - Gen.oneOf(Wrap.values), + Gen.oneOf(Wrap.values.toSeq), Gen.oneOf(Oneline.keep, Oneline.fold, Oneline.unfold) ): (wrap, oneline) => val value = TestEnums(oneline, wrap) diff --git a/shared/FieldConfig.scala b/shared/FieldConfig.scala new file mode 100644 index 0000000..cf41221 --- /dev/null +++ b/shared/FieldConfig.scala @@ -0,0 +1,11 @@ +package genovese + +import scala.deriving.Mirror +import scala.reflect.ClassTag +import scala.util.boundary +import scala.annotation.nowarn + +case class FieldConfig(overrides: Map[String, Feature[?]]) + +object FieldConfig: + inline def None = FieldConfig(Map.empty) diff --git a/Fitness.scala b/shared/Fitness.scala similarity index 100% rename from Fitness.scala rename to shared/Fitness.scala diff --git a/FitnessStats.scala b/shared/FitnessStats.scala similarity index 100% rename from FitnessStats.scala rename to shared/FitnessStats.scala diff --git a/FloatVectorFeatureful.scala b/shared/FloatVectorFeatureful.scala similarity index 100% rename from FloatVectorFeatureful.scala rename to shared/FloatVectorFeatureful.scala diff --git a/Lab.scala b/shared/Lab.scala similarity index 100% rename from Lab.scala rename to shared/Lab.scala diff --git a/Makefile b/shared/Makefile similarity index 100% rename from Makefile rename to shared/Makefile diff --git a/NormalisedFloat.scala b/shared/NormalisedFloat.scala similarity index 100% rename from NormalisedFloat.scala rename to shared/NormalisedFloat.scala diff --git a/shared/OptionFeatureful.scala b/shared/OptionFeatureful.scala new file mode 100644 index 0000000..d4297e4 --- /dev/null +++ b/shared/OptionFeatureful.scala @@ -0,0 +1,30 @@ +package genovese + +import scala.deriving.Mirror +import scala.reflect.ClassTag +import scala.util.boundary +import scala.annotation.nowarn + +final case class OptionFeatureful[T](original: Featureful[T]) + extends Featureful[Option[T]]: + override lazy val features: IArray[Feature[?]] = + original.features.map(Feature.Optional(_)) + override def fromFeatures(fv: IArray[NormalisedFloat]): Option[T] = + boundary: + val decompressed = fv.map: v => + if v < 0.5f then boundary.break(None) + else NormalisedFloat.applyUnsafe(2 * (v - 0.5f)) + Some(original.fromFeatures(decompressed)) + + override def toFeatures(value: Option[T])(using + RuntimeChecks + ): IArray[NormalisedFloat] = + value match + case None => + IArray.fill(original.features.size)(NormalisedFloat.applyUnsafe(0.25f)) + case Some(value) => + original + .toFeatures(value) + .map(v => NormalisedFloat.applyUnsafe(v / 2 + 0.5f)) +end OptionFeatureful + diff --git a/RuntimeChecks.scala b/shared/RuntimeChecks.scala similarity index 100% rename from RuntimeChecks.scala rename to shared/RuntimeChecks.scala diff --git a/Selection.scala b/shared/Selection.scala similarity index 100% rename from Selection.scala rename to shared/Selection.scala diff --git a/shared/SingleFeatureful.scala b/shared/SingleFeatureful.scala new file mode 100644 index 0000000..ddc6f5f --- /dev/null +++ b/shared/SingleFeatureful.scala @@ -0,0 +1,13 @@ +package genovese + +final case class SingleFeatureful[T](feature: Feature[T]) extends Featureful[T]: + override val features: IArray[Feature[?]] = IArray(feature) + + override def toFeatures(value: T)(using + RuntimeChecks + ): IArray[NormalisedFloat] = + IArray(feature.toNormalisedFloat(value)) + + override def fromFeatures(fv: IArray[NormalisedFloat]): T = + feature.fromNormalisedFloat(fv(0).asInstanceOf[NormalisedFloat]) +end SingleFeatureful diff --git a/Train.test.scala b/shared/Train.test.scala similarity index 96% rename from Train.test.scala rename to shared/Train.test.scala index 2e874ab..b87d8a1 100644 --- a/Train.test.scala +++ b/shared/Train.test.scala @@ -70,8 +70,9 @@ class TrainTest extends FunSuite: val top = Train( summon[Featureful[FormattingConfig]], config = trainingConfig, - fitness = fitness - ).train(Handler).maxBy(_._2)._1 + fitness = fitness, + events = Handler, + ).train().maxBy(_._2)._1 assertEquals(fitness(top), 0.6f) diff --git a/TrainingConfig.scala b/shared/TrainingConfig.scala similarity index 100% rename from TrainingConfig.scala rename to shared/TrainingConfig.scala diff --git a/TrainingEvent.scala b/shared/TrainingEvent.scala similarity index 100% rename from TrainingEvent.scala rename to shared/TrainingEvent.scala diff --git a/TrainingInstruction.scala b/shared/TrainingInstruction.scala similarity index 100% rename from TrainingInstruction.scala rename to shared/TrainingInstruction.scala diff --git a/project.scala b/shared/project.scala similarity index 92% rename from project.scala rename to shared/project.scala index e1839d6..cd16b9f 100644 --- a/project.scala +++ b/shared/project.scala @@ -1,5 +1,5 @@ //> using dep "com.lihaoyi::pprint::0.9.0" -//> using dep "org.scala-lang.modules::scala-parallel-collections::1.0.4" +//> using dep "com.indoorvivants::opaque-newtypes::0.1.0" //> using option -Wunused:all -deprecation //> using scala 3.5.0 //> using test.dep "org.scalameta::munit-scalacheck::1.0.0" diff --git a/scalafmt.test.scala b/shared/scalafmt.test.scala similarity index 83% rename from scalafmt.test.scala rename to shared/scalafmt.test.scala index 1bc6e9e..1b10616 100644 --- a/scalafmt.test.scala +++ b/shared/scalafmt.test.scala @@ -4,6 +4,7 @@ import munit.FunSuite import org.scalafmt.Scalafmt import org.scalafmt.config.* import org.scalafmt.config.Docstrings.* +import java.util.concurrent.ConcurrentHashMap given Featureful[Oneline] = Featureful.derive[Oneline](FieldConfig.None) given Featureful[Wrap] = Featureful.derive[Wrap](FieldConfig.None) @@ -18,15 +19,16 @@ class ScalafmtTest extends FunSuite: test("scalafmt"): given RuntimeChecks = RuntimeChecks.Full - import com.github.vickumar1981.stringdistance.* - import com.github.vickumar1981.stringdistance.StringDistance.* - import com.github.vickumar1981.stringdistance.impl.ConstantGap - - val cache = collection.mutable.Map.empty[Docstrings, NormalisedFloat] + // val cache = ConcurrentHashMap[Docstrings, NormalisedFloat]() def fitness(docstrings: Docstrings = Docstrings()) = - cache.getOrElseUpdate( - docstrings, { + import com.github.vickumar1981.stringdistance.* + import com.github.vickumar1981.stringdistance.StringDistance.* + import com.github.vickumar1981.stringdistance.impl.ConstantGap + + // cache.computeIfAbsent( + // docstrings, + // { _ => val formatted = Scalafmt .format(text.trim, style = ScalafmtConfig(docstrings = docstrings)) .get @@ -38,14 +40,14 @@ class ScalafmtTest extends FunSuite: .max(0.0f) .min(1.0f) ) - } - ) + // } + // ) end fitness val trainingConfig = TrainingConfig( populationSize = 100, mutationRate = NormalisedFloat(0.8f), - steps = 1000, + steps = 100000, random = scala.util.Random(80085L), selection = Selection.Top(0.8) ) @@ -71,8 +73,10 @@ class ScalafmtTest extends FunSuite: val top = Train( summon[Featureful[Docstrings]], config = trainingConfig, - fitness = Fitness(fitness) - ).train(Handler).maxBy(_._2)._1 + fitness = Fitness(fitness), + events = Handler, + evaluator = Evaluator.default + ).train().maxBy(_._2)._1 println(top) diff --git a/train.scala b/shared/train.scala similarity index 73% rename from train.scala rename to shared/train.scala index 1cbd5ef..c7e6a4b 100644 --- a/train.scala +++ b/shared/train.scala @@ -5,20 +5,14 @@ import scala.util.boundary class Train[T]( featureful: Featureful[T], config: TrainingConfig, - fitness: Fitness[T] + fitness: Fitness[T], + events: EventHandler = EventHandler.None, + evaluator: Evaluator = SequentialEvaluator )(using RuntimeChecks): import config.* private val VECTOR_SIZE = featureful.features.length - opaque type Vec <: IArray[NormalisedFloat] = IArray[NormalisedFloat] - opaque type Population <: IArray[Vec] = IArray[Vec] - opaque type PopulationSubset <: Population = IArray[Vec] - opaque type Evaluated <: IArray[(Vec, NormalisedFloat)] = - IArray[(Vec, NormalisedFloat)] - - def train( - events: EventHandler = EventHandler.None - ): Map[T, NormalisedFloat] = + def train(): Map[T, NormalisedFloat] = var evaluated: Evaluated | Null = null @@ -63,8 +57,8 @@ class Train[T]( val newPop = newPopulationBuilder.result() if newPop.size < populationSize then - population = newPop ++ seed(populationSize - newPop.size) - else population = newPop.take(populationSize) + population = Population(seed(populationSize - newPop.size) ++ newPop) + else population = Population(newPop.take(populationSize)) runtimeChecks.check( Option.when(population.size != populationSize)( @@ -79,7 +73,10 @@ class Train[T]( events.emit(TrainingFinished, ignore = true) - evaluated.nn.map((vec, score) => materialize(vec) -> score).toMap + evaluated.nn.toSeq + .map: + case (vec, score) => materialize(vec) -> score + .toMap end train private def fitnessStats(evaluated: Evaluated): FitnessStats = @@ -104,44 +101,52 @@ class Train[T]( private def crossover(p1: Vec, p2: Vec): Array[Vec] = val crossoverPoint = random.nextInt(VECTOR_SIZE).max(1) - val child1 = p1.take(crossoverPoint) ++ p2.drop(crossoverPoint) - val child2 = p2.take(crossoverPoint) ++ p1.drop(crossoverPoint) + val child1 = Vec(p1.take(crossoverPoint) ++ p2.drop(crossoverPoint)) + val child2 = Vec(p2.take(crossoverPoint) ++ p1.drop(crossoverPoint)) Array(mutate(child1), mutate(child2)) private def seed(n: Int): Population = - IArray.fill(n)( - IArray.fill(VECTOR_SIZE)(randomNormalisedFloat) + Population( + IArray.fill(n)( + Vec(IArray.fill(VECTOR_SIZE)(randomNormalisedFloat)) + ) ) private inline def evaluate(pop: Population): Evaluated = - import scala.collection.parallel.CollectionConverters.* - IArray.unsafeFromArray( - IArray - .genericWrapArray(pop) - .toArray - .par - .map(vec => vec -> fitness(materialize(vec))) - .toArray - ) - end evaluate - - private def mutate(vec: Vec) = + evaluator.evaluate(pop, v => fitness(materialize(v))) + // import scala.collection.parallel.CollectionConverters.* + // Evaluated( + // IArray.unsafeFromArray( + // IArray + // .genericWrapArray(pop) + // .toArray + // .par + // .map(vec => vec -> fitness(materialize(vec))) + // .toArray + // ) + // ) + + private def mutate(vec: Vec): Vec = if random.nextFloat() >= mutationRate then val idx = random.nextInt(VECTOR_SIZE) - vec.updated( - idx, - NormalisedFloat(random.nextFloat()) + Vec( + vec.updated( + idx, + NormalisedFloat(random.nextFloat()) + ) ) else vec - private def select(pop: Evaluated): PopulationSubset = + private def select(pop: Evaluated): Population = selection match case Selection.Top(portion) => - pop - .sortBy(_._2) - .takeRight((pop.size * portion).toInt) - .map(_._1) + Population( + pop + .sortBy(_._2) + .takeRight((pop.size * portion).toInt) + .map(_._1) + ) private inline def randomNormalisedFloat = NormalisedFloat(random.nextFloat()) diff --git a/shared/type_aliases.scala b/shared/type_aliases.scala new file mode 100644 index 0000000..607996d --- /dev/null +++ b/shared/type_aliases.scala @@ -0,0 +1,27 @@ +package genovese + +import opaque_newtypes.* + +opaque type Vec <: IArray[NormalisedFloat] = IArray[NormalisedFloat] +object Vec extends SubtypeWrapper[Vec, IArray[NormalisedFloat]] + +opaque type Population <: IArray[Vec] = IArray[Vec] +object Population extends SubtypeWrapper[Population, IArray[Vec]] + +opaque type Evaluated <: IArray[(Vec, NormalisedFloat)] = + IArray[(Vec, NormalisedFloat)] +object Evaluated extends SubtypeWrapper[Evaluated, IArray[(Vec, NormalisedFloat)]] + + +trait SubtypeWrapper[Newtype, Impl](using ev: Newtype =:= Impl): + inline def raw(inline a: Newtype): Impl = ev.apply(a) + inline def apply(inline s: Impl): Newtype = ev.flip.apply(s) + + private val flipped = ev.flip + + given SameRuntimeType[Newtype, Impl] = new: + override def apply(a: Newtype): Impl = ev.apply(a) + + given SameRuntimeType[Impl, Newtype] = new: + override def apply(a: Impl): Newtype = flipped.apply(a) +