Skip to content

Commit

Permalink
Algebra in core (#254)
Browse files Browse the repository at this point in the history
* port Add to tl algebra

* port Sub to tl algebra

* port Mul to tl algebra

* Div with TruncatedDivision is failing

* / and tquot

* neg

* pow and tpow

* remove algebra.scala

* explicit truncating conversions

* populate some coulomb.ops.algebra instances

* refactor use of algebras

* scala.math.Ordering -> cats.kernel.Order

* refactor si, si.prefixes, mks, mksa to use export

* tweak directories

* refactor algebra to use export

* factor value-specific optimizations to separate import

* unit test optimized operations
  • Loading branch information
erikerlandson authored Mar 12, 2022
1 parent 57e8b04 commit 201871f
Show file tree
Hide file tree
Showing 33 changed files with 930 additions and 489 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform/*, NativePlatform*/)
.in(file("core"))
.settings(name := "coulomb-core")
.settings(commonSettings :_*)
.settings(libraryDependencies += "org.typelevel" %%% "algebra" % "2.7.0")

lazy val units = crossProject(JVMPlatform, JSPlatform/*, NativePlatform*/)
.crossType(CrossType.Pure)
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/coulomb/conversion/conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import scala.annotation.implicitNotFound
@implicitNotFound("No value conversion in scope for value types {VF} => {VT}")
abstract class ValueConversion[VF, VT] extends (VF => VT)

@implicitNotFound("No truncating value conversion in scope for value types {VF} => {VT}")
abstract class TruncatingValueConversion[VF, VT] extends (VF => VT)

/** Convert a value of type V from implied units UF to UT */
@implicitNotFound("No unit conversion in scope for value type {V}, unit types {UF} => {UT}")
abstract class UnitConversion[V, UF, UT] extends (V => V)

@implicitNotFound("No truncating unit conversion in scope for value type {V}, unit types {UF} => {UT}")
abstract class TruncatingUnitConversion[V, UF, UT] extends (V => V)
20 changes: 9 additions & 11 deletions core/src/main/scala/coulomb/conversion/standard/standard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package coulomb.conversion.standard

import coulomb.conversion.{ValueConversion, UnitConversion}
import coulomb.conversion.*
import coulomb.{coefficient, withUnit, Quantity}
import coulomb.policy.{AllowTruncation, AllowImplicitConversions}

Expand Down Expand Up @@ -50,16 +50,16 @@ inline given ctx_VC_Long[VF](using num: Integral[VF]): ValueConversion[VF, Long]
new ValueConversion[VF, Long]:
def apply(v: VF): Long = num.toLong(v)

inline given ctx_VC_Long_tr[VF](using num: Fractional[VF], at: AllowTruncation): ValueConversion[VF, Long] =
new ValueConversion[VF, Long]:
inline given ctx_TVC_Long[VF](using num: Fractional[VF]): TruncatingValueConversion[VF, Long] =
new TruncatingValueConversion[VF, Long]:
def apply(v: VF): Long = num.toLong(v)

inline given ctx_VC_Int[VF](using num: Integral[VF]): ValueConversion[VF, Int] =
new ValueConversion[VF, Int]:
def apply(v: VF): Int = num.toInt(v)

inline given ctx_VC_Int_tr[VF](using num: Fractional[VF], at: AllowTruncation): ValueConversion[VF, Int] =
new ValueConversion[VF, Int]:
inline given ctx_TVC_Int_tr[VF](using num: Fractional[VF]): TruncatingValueConversion[VF, Int] =
new TruncatingValueConversion[VF, Int]:
def apply(v: VF): Int = num.toInt(v)

// unit conversions that discard fractional values can be imported from
Expand All @@ -76,19 +76,17 @@ inline given ctx_UC_Float[UF, UT]:
new UnitConversion[Float, UF, UT]:
def apply(v: Float): Float = c * v

inline given ctx_UC_Long[UF, UT](using AllowTruncation):
UnitConversion[Long, UF, UT] =
inline given ctx_TUC_Long[UF, UT]: TruncatingUnitConversion[Long, UF, UT] =
val nc = coulomb.conversion.infra.coefficientNumDouble[UF, UT]
val dc = coulomb.conversion.infra.coefficientDenDouble[UF, UT]
// using nc and dc is more efficient than using Rational directly in the conversion function
// but still gives us 53 bits of integer precision for exact rational arithmetic, and also
// graceful loss of precision if nc*v exceeds 53 bits
new UnitConversion[Long, UF, UT]:
new TruncatingUnitConversion[Long, UF, UT]:
def apply(v: Long): Long = ((nc * v) / dc).toLong

inline given ctx_UC_Int[UF, UT](using AllowTruncation):
UnitConversion[Int, UF, UT] =
inline given ctx_TUC_Int[UF, UT]: TruncatingUnitConversion[Int, UF, UT] =
val nc = coulomb.conversion.infra.coefficientNumDouble[UF, UT]
val dc = coulomb.conversion.infra.coefficientDenDouble[UF, UT]
new UnitConversion[Int, UF, UT]:
new TruncatingUnitConversion[Int, UF, UT]:
def apply(v: Int): Int = ((nc * v) / dc).toInt
18 changes: 18 additions & 0 deletions core/src/main/scala/coulomb/infra/meta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ object meta:
report.error(s"type expr ${typestr(TypeRepr.of[E])} is not a non-negative Int")
'{ new coulomb.rational.typeexpr.NonNegInt[E] { val value = 0 } }

def teToPosInt[E](using Quotes, Type[E]): Expr[coulomb.rational.typeexpr.PosInt[E]] =
import quotes.reflect.*
val rationalTE(v) = TypeRepr.of[E]
if ((v.d == 1) && (v.n > 0) && (v.n.isValidInt)) then
'{ new coulomb.rational.typeexpr.PosInt[E] { val value = ${Expr(v.n.toInt)} } }
else
report.error(s"type expr ${typestr(TypeRepr.of[E])} is not a positive Int")
'{ new coulomb.rational.typeexpr.PosInt[E] { val value = 0 } }

def teToInt[E](using Quotes, Type[E]): Expr[coulomb.rational.typeexpr.AllInt[E]] =
import quotes.reflect.*
val rationalTE(v) = TypeRepr.of[E]
if ((v.d == 1) && (v.n.isValidInt)) then
'{ new coulomb.rational.typeexpr.AllInt[E] { val value = ${Expr(v.n.toInt)} } }
else
report.error(s"type expr ${typestr(TypeRepr.of[E])} is not an Int")
'{ new coulomb.rational.typeexpr.AllInt[E] { val value = 0 } }

object rationalTE:
def unapply(using Quotes)(tr: quotes.reflect.TypeRepr): Option[Rational] =
import quotes.reflect.*
Expand Down
35 changes: 35 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/algebra.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

import scala.annotation.implicitNotFound

// there is no typelevel community typeclass that expresses the concept
// "supports raising to fractional powers, without truncation"
// The closest thing is spire NRoot, but it is also defined on truncating integer types,
// so it is not helpful for distinguishing "pow" from "tpow", and in any case requires spire
// https://github.com/typelevel/spire/issues/741

@implicitNotFound("Fractional power not defined for value type ${V}")
abstract class FractionalPower[V]:
/** returns v^e */
def pow(v: V, e: Double): V

@implicitNotFound("Truncating power not defined for value type ${V}")
abstract class TruncatingPower[V]:
/** returns v^e, truncated to integer value (toward zero) */
def tpow(v: V, e: Double): V
23 changes: 23 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/all.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

object all:
export coulomb.ops.algebra.int.{*,given}
export coulomb.ops.algebra.long.{*,given}
export coulomb.ops.algebra.float.{*,given}
export coulomb.ops.algebra.double.{*,given}
37 changes: 37 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/double.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

import algebra.ring.TruncatedDivision

object double:
given ctx_Double_is_FractionalPower: FractionalPower[Double] with
def pow(v: Double, e: Double): Double = math.pow(v, e)

given ctx_Double_is_TruncatingPower: TruncatingPower[Double] with
def tpow(v: Double, e: Double): Double = math.pow(v, e).toLong.toDouble

given ctx_Double_is_TruncatedDivision: TruncatedDivision[Double] with
def tquot(x: Double, y: Double): Double = (x / y).toLong.toDouble
// I don't care about these
def tmod(x: Double, y: Double): Double = ???
def fquot(x: Double, y: Double): Double = ???
def fmod(x: Double, y: Double): Double = ???
def abs(a: Double): Double = ???
def additiveCommutativeMonoid: algebra.ring.AdditiveCommutativeMonoid[Double] = ???
def order: cats.kernel.Order[Double] = ???
def signum(a: Double): Int = ???
37 changes: 37 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/float.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

import algebra.ring.TruncatedDivision

object float:
given ctx_Float_is_FractionalPower: FractionalPower[Float] with
def pow(v: Float, e: Double): Float = math.pow(v.toDouble, e).toFloat

given ctx_Float_is_TruncatingPower: TruncatingPower[Float] with
def tpow(v: Float, e: Double): Float = math.pow(v.toDouble, e).toLong.toFloat

given ctx_Float_is_TruncatedDivision: TruncatedDivision[Float] with
def tquot(x: Float, y: Float): Float = (x / y).toLong.toFloat
// I don't care about these
def tmod(x: Float, y: Float): Float = ???
def fquot(x: Float, y: Float): Float = ???
def fmod(x: Float, y: Float): Float = ???
def abs(a: Float): Float = ???
def additiveCommutativeMonoid: algebra.ring.AdditiveCommutativeMonoid[Float] = ???
def order: cats.kernel.Order[Float] = ???
def signum(a: Float): Int = ???
35 changes: 35 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/int.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

import algebra.ring.TruncatedDivision

object int:
given ctx_Int_is_TruncatingPower: TruncatingPower[Int] with
def tpow(v: Int, e: Double): Int = math.pow(v.toDouble, e).toInt

given ctx_Int_is_TruncatedDivision: TruncatedDivision[Int] with
def tquot(x: Int, y: Int): Int = x / y
// I don't care about these
def tmod(x: Int, y: Int): Int = ???
def fquot(x: Int, y: Int): Int = ???
def fmod(x: Int, y: Int): Int = ???
def abs(a: Int): Int = ???
def additiveCommutativeMonoid: algebra.ring.AdditiveCommutativeMonoid[Int] = ???
def order: cats.kernel.Order[Int] = ???
def signum(a: Int): Int = ???

34 changes: 34 additions & 0 deletions core/src/main/scala/coulomb/ops/algebra/long.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.ops.algebra

import algebra.ring.TruncatedDivision

object long:
given ctx_Long_is_TruncatingPower: TruncatingPower[Long] with
def tpow(v: Long, e: Double): Long = math.pow(v.toDouble, e).toLong

given ctx_Long_is_TruncatedDivision: TruncatedDivision[Long] with
def tquot(x: Long, y: Long): Long = x / y
// I don't care about these
def tmod(x: Long, y: Long): Long = ???
def fquot(x: Long, y: Long): Long = ???
def fmod(x: Long, y: Long): Long = ???
def abs(a: Long): Long = ???
def additiveCommutativeMonoid: algebra.ring.AdditiveCommutativeMonoid[Long] = ???
def order: cats.kernel.Order[Long] = ???
def signum(a: Long): Int = ???
18 changes: 15 additions & 3 deletions core/src/main/scala/coulomb/ops/ops.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import scala.annotation.implicitNotFound

import coulomb.*

@implicitNotFound("Negation not defined in scope for Quantity[${V}, ${U}]")
abstract class Neg[V, U]:
def apply(v: V): V

@implicitNotFound("Addition not defined in scope for Quantity[${VL}, ${UL}] and Quantity[${VR}, ${UR}]")
abstract class Add[VL, UL, VR, UR]:
type VO
Expand All @@ -44,16 +48,24 @@ abstract class Div[VL, UL, VR, UR]:
type UO
def apply(vl: VL, vr: VR): VO

@implicitNotFound("Negation not defined in scope for Quantity[${V}, ${U}]")
abstract class Neg[V, U]:
def apply(v: V): V
@implicitNotFound("Truncating Division not defined in scope for Quantity[${VL}, ${UL}] and Quantity[${VR}, ${UR}]")
abstract class TQuot[VL, UL, VR, UR]:
type VO
type UO
def apply(vl: VL, vr: VR): VO

@implicitNotFound("Power not defined in scope for Quantity[${V}, ${U}] ^ ${P}")
abstract class Pow[V, U, P]:
type VO
type UO
def apply(v: V): VO

@implicitNotFound("Truncating Power not defined in scope for Quantity[${V}, ${U}] ^ ${P}")
abstract class TPow[V, U, P]:
type VO
type UO
def apply(v: V): VO

@implicitNotFound("Ordering not defined in scope for Quantity[${VL}, ${UL}] and Quantity[${VR}, ${UR}]")
abstract class Ord[VL, UL, VR, UR]:
def apply(vl: VL, vr: VR): Int
Expand Down
Loading

0 comments on commit 201871f

Please sign in to comment.