Coulomb Unit Analysis Library
repository·scala3·Indexed 41 days ago
https://github.com/erikerlandson/coulombA statically typed unit analysis library for Scala that enables compile-time dimensional analysis. It ensures physical quantities are used correctly in mathematical expressions and is compatible with the Typelevel ecosystem (Cats friendly). The library provides tools for defining BaseUnit, DerivedUnit, and DeltaUnit, as well as type-safe conversion coefficients and offsets for value types including Double, Float, BigDecimal, and Rational.
What's inside Coulomb
- Coulomb is a statically typed unit analysis library for Scala. It allows developers to perform dimensional analysis at compile time, ensuring that physical quantities (like length, mass, or time) are used correctly within their mathematical expressions. It is designed to be compatible with the Typelevel ecosystem (Cats friendly).
What is a DeltaUnit?
scala3A
DeltaUnit[V, U, B]represents an absolute value anchored to a base unitB(e.g.,Kelvinfor temperature orSecondfor time).It is designed specifically to handle the algebra of absolute positions. The only valid operations involving two
DeltaUnits are subtraction, which results in a standardQuantity.Supported Algebra:
DeltaUnit - DeltaUnit => QuantityDeltaUnit + Quantity => DeltaUnitDeltaUnit - Quantity => DeltaUnit
Use arbitrary types as units
scala3In
coulomb, any type can be treated as a unit. If a type is not explicitly defined as a unit, it is treated as aBaseUnit.class Apple val apples = 1.withUnit[Apple] + 2.withUnit[Apple] val s = apples.showUse Quantity[V, U] for unit analysis
scala3A
Quantity[V, U]represents a value of typeVpaired with a unit typeU. This allowscoulombto perform dimensional analysis at compile time, ensuring that operations like addingmeters + secondsresult in a type error, whilemeters / secondscorrectly results in a newQuantitywith the unitMeter / Second.val time = 10.0.withUnit[Second] val dist = 100.0.withUnit[Meter] val v = dist / time v.showUse physical constants as units or values
scala3Physical constants in
coulomb-unitscan be used in two ways:- As a
Quantityobject using theconstant[V, C]function, whereVis the value type andCis the constant type. - As a unit for conversion using
.toUnit[C].
import coulomb.units.constants.{*, given} // the "constant" function gives a physical constant as a Quantity object with a particular value type val c = constant[Double, SpeedOfLight] // use the speed of light as a unit to express a velocity as a fraction of light speed val v = 30e6.withUnit[Meter / Second] v.toUnit[SpeedOfLight]- As a
Handling Integer values and unit conversions
scala3In
coulomb, implicit truncating conversions (e.g., convertingMega * BytetoByteusing anInttype) are generally avoided because they are numerically unsafe.In
coulomb-pureconfigI/O, conversions will only succeed for integer types if:- The unit in the configuration source exactly matches the target type.
- The conversion coefficient between the source unit and the target unit is exactly 1.
Best Practice: To ensure unit conversions always succeed without precision loss, use fractional value types like
FloatorDoublefor yourQuantitydefinitions.// source for a quantity value val qsrc = ConfigSource.string(""" { value: 3 unit: megabyte } """) // succeeds when type matches qsrc.load[Quantity[Int, Mega * Byte]] // succeeds when conversion coefficient is exactly 1 qsrc.load[Quantity[Long, Byte * Mega]] // fails if conversion is not exactly 1 (e.g. requires truncation) qsrc.load[Quantity[Int, Kilo * Byte]]Algebraic support for refined predicates
scala3The
coulomb-refinedpackage enables algebraic operations onrefinedtypes. The availability of operations depends on the value type (Fractional vs Integral) and the predicate used (Positive vs NonNegative).Note that for
Eitherobjects returned byrefineVU, algebraic operations onLeftvalues result in aLeft.| Value Type | Predicate | Add Alg | Mult Alg | `+` | `*` | `/` | `pow` (exponent) | | --- | --- | --- | --- | --- | --- | --- | --- | | Fractional | Positive | semigroup | group | Y | Y | Y | Y (rational) | | Fractional | NonNegative | semigroup | semigroup | Y | Y | N | Y (pos int) | | Integral | Positive | semigroup | semigroup | Y | Y | N | Y (pos int) | | Integral | NonNegative | semigroup | semigroup | Y | Y | N | Y (pos int) |Convert RuntimeQuantity to static Quantity using CoefficientRuntime
scala3To bridge the gap between runtime unit expressions and static unit types (e.g., when loading unit-aware configurations), use a
CoefficientRuntime.- Define a
CoefficientRuntime(e.g., usingMappingCoefficientRuntime.of) and provide the package names containing the unit types you want to support. - Use the
.toQuantity[ValueType, StaticUnitType]method on aRuntimeQuantity.
Because the conversion depends on runtime data, the result is returned as an
Either[_, Quantity[ValueType, StaticUnitType]]to handle cases where units are incompatible.// 1. Setup the coefficient runtime with package mappings given given_CRT: CoefficientRuntime = MappingCoefficientRuntime.of[ "coulomb.units.si" *: "coulomb.units.si.prefixes" *: EmptyTuple ] // 2. Convert from runtime to static // Valid conversion val result1 = rq.toQuantity[Float, Kilo * Meter / Second] // Valid conversion with different value type val result2 = rq.toQuantity[Double, Meter / Second] // This will return a Left (failure) because units are incompatible val result3 = rq.toQuantity[Float, Second]- Define a
How absolute temperatures and timestamps work
scala3In
coulomb, certain units represent absolute positions rather than relative amounts. This applies to:- Temperatures: Absolute temperatures (e.g., Celsius) vs. units of degrees.
- Time: Absolute timestamps (instants) vs. units of duration.
Algebraic Rules for Absolute Values
Because these values are 'anchored' to a reference point (like the Kelvin scale or the Unix epoch), certain operations are invalid:
- Subtraction: Subtracting two absolute temperatures/timestamps yields a
Quantity(a delta/duration). - Addition/Subtraction with Quantity: You can add or subtract a
Quantity(delta) to/from an absolute value to get a new absolute value. - Invalid Operation: Adding two absolute temperatures or two absolute timestamps is a type error (e.g.,
10C + 20Cis undefined).
// Temperature Example import coulomb.units.temperature.{*, given} import coulomb.units.syntax.* val cels1 = 10d.withTemperature[Celsius] val cels2 = 20d.withTemperature[Celsius] val dcels = cels2 - cels1 // Yields a Quantity of degrees cels2 + dcels // Valid // cels1 + cels2 // TYPE ERROR // Time Example import coulomb.units.time.{*, given} import coulomb.units.syntax.* val date1 = 50d.withEpochTime[365 * Day] val date2 = 51d.withEpochTime[365 * Day] val dyear = date2 - date1 // Yields a Quantity of time date2 + dyear // Valid // date1 + date2 // TYPE ERRORDefine algebras for custom Value Types
scala3While
Quantity[V, U]can use any typeVas its value type, operations like addition will only work if an appropriate algebra (e.g.,AdditiveSemigroup) is defined for that type. You can implement these algebras to enable standardcoulomboperations on your custom wrappers.object wrapperalgebra: import algebra.ring.AdditiveSemigroup given add_Wrapper[V](using vadd: AdditiveSemigroup[V]): AdditiveSemigroup[Wrapper[V]] = new AdditiveSemigroup[Wrapper[V]]: def plus(x: Wrapper[V], y: Wrapper[V]): Wrapper[V] = Wrapper(vadd.plus(x.t, y.t)) // import Wrapper algebras into scope import wrapperalgebra.given // adding Wrapper objects works now that we have defined an algebra val wsum = w + wImport coulomb-parser components
scala3To use the parsing API, DSL, and standard unit definitions, use the following imports:
import coulomb.* import coulomb.syntax.* // algebraic definitions import algebra.instances.all.given // unit definitions import coulomb.units.si.{*, given} import coulomb.units.si.prefixes.{*, given} import coulomb.units.info.{*, given} import coulomb.units.time.{*, given} // parsing definitions import coulomb.parser.RuntimeUnitParser import coulomb.parser.dsl.RuntimeUnitDslParserConvert between coulomb and java.time
scala3The library provides conversions between
coulombtime types andjava.timetypes:coulombtime units (e.g.,Second,Minute,Hour) $\leftrightarrow$java.time.Duration.coulombabsolute time (EpochTime) $\leftrightarrow$java.time.Instant.
Explicit Conversions
Use
.toQuantity[V, U]to convert ajava.timeobject to acoulombquantity, and.toDurationor.toInstantto convert back.import java.time.{ Duration, Instant } import coulomb.units.time.{*, given} // extension conversion methods import coulomb.units.javatime.* val dur = Duration.ofSeconds(70, 400000000) // explicit conversion from java.time duration to a coulomb quantity dur.toQuantity[Double, Minute] // convert back to java.time dq.toDuration // the day of the first human moon landing val ins = Instant.parse("1969-07-20T00:00:00Z") // days relative to standard Unix epoch ins.toEpochTime[Double, Day] // convert back to java.time et.toInstant