ianohara / squants

The Scala API for Quantities, Units of Measure and Dimensional Analysis

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Squants

The Scala API for Quantities, Units of Measure and Dimensional Analysis

Squants is a framework of data types and a domain specific language (DSL) for representing Quantities, their Units of Measure, and their Dimensional relationships. The API supports typesafe dimensional analysis, improved domain models and more. All types are immutable and thread-safe.

Website | API Documentation | GitHub | User Forum

Current version: 0.2.1-SNAPSHOT (pre-release)

Build Status

Installation

Repository hosting for Squants is provided by Sonatype. To use Squants in your SBT project you will need to add the following dependency to your build.

"com.squants"  %% "squants"  % "0.2.1-SNAPSHOT"

To use Squants interactively in the Scala REPL, clone the git repo and run sbt console

git clone https://github.com/garyKeorkunian/squants
cd squants
sbt console

Better Dimensional Analysis

The Trouble with Doubles

When building programs that perform some type of dimensional analysis, developers are quick to declare quantities using a basic numeric type, usually Double. While this may be perfectly satisfactory in many situations, it can often lead to semantic and other logic issues.

For example, when using a Double to describe a quantity of Energy (kWh) and Power (kW), it is possible to compile a program that adds these two values together. This is not appropriate as kW and kWh measure two different quantities. The unit kWh is used to measure an amount of Energy used or produced. The unit kW is used to measure Power/Load, the rate at which Energy is being used or produced, that is, Power is the first time derivative of Energy.

Power = Energy / Time

Consider the following code

val loadKw: Double = 1.2
val energyMwh: Double = 24.2
val sumKw = loadKw + energyMwh

which not only adds different quantity types (load vs energy), it also fails to convert the scales (Mega vs Kilo). Because this code compiles, detection of these errors is pushed further into the development cycle.

Dimensional Types

The Squants Type Library and DSL helps prevent errors like these by type checking operations at compile time and automatically applying scale and type conversions (see below) at run-time. For example,

val load1: Power = Kilowatts(12)
val load2: Power = Megawatts(0.023)
val sum = load1 + load2
assert(sum == Kilowatts(35))
assert(sum == Megawatts(0.035))

is a valid assertion because Kilowatts and Megawatts are both measures of load. Only the scale is different and the framework applies an appropriate conversion. Also, notice that keeping track of the scale within the value name is no longer needed.

Dimensional Type Safety

The following code highlights the type safety features.

val load: Power = Kilowatts(1.2)
val energy: Energy = KilowattHours(23.0)
val sum = load + energy // Invalid operation - does not compile

The invalid expression prevents the code from compiling, catching the error made when using Double above.

Smart Type Conversions

Dimensionally smart type conversions are a key feature of Squants. Most conversions are implemented by defining relationships between Quantity types using infix operations.

val load: Power = Kilowatts(1.2)
val time: Time = Hours(2)
val energyUsed: Energy = load * time
assert(energyUsed == KilowattHours(2.4))

This code demonstrates use of the Power.* method, defined as an infix operator that takes a Time value and returns an Energy value, conversely

val aveLoad: Power = energyUsed / time
assert(aveLoad == Kilowatts(1.2)

demonstrates use of the Energy./ method that takes a Time and returns a Power

Unit Conversions

If necessary, the value in the desired unit can be extracted with the the to method.

val load: Power = Kilowatts(1200)
val mw: Double = load to Kilowatts // returns 1200.0 (default method)
val gw: Double = load to Gigawatts // returns 0.0012
val my: Double = load to MyPowerUnit // returns ??? Whatever you want
val kw: Double = load toKilowatts // returns 1200.0 (convenience method)

Strings formatted in the desired unit is also supported

val kw: String = load toString Kilowatts // returns “1200.0 kW”
val mw: String = load toString Megawatts // returns “1.2 MW”
val gw: String = load toString Gigawatts // returns “0.0012 GW”

Market Package

Market Types are similar but not quite the same as other quantities in the library. The primary type, Money, is derived from Quantity, and its Units of Measure are Currencies. However, because the conversion multipliers between units can not be predefined, many of the behaviors have been overridden and augmented to realize correct behavior.

Money

A Quantity of purchasing power

val tenBucks: Money = USD(10)
val someYen: Money = JPY(1200)
val goldStash: Money = XAU(50)
val digitalStash: Money = BTC(50)

Price

A Ratio between Money and another Quantity

Price = Money / Quantity

val threeForADollar: Price[Dimensionless] = USD(1) / Each(3)
val energyPrice: Price[Energy] = USD(102.20) / MegawattHours(1)
val milkPrice: Price[Volume] = USD(4) / UsGallons(1)

val costForABunch: Money = threeForADollar * Dozen(10) // returns USD(40)
val energyCost: Money = energyPrice * MegawattHours(4) // returns USD(408.80)
val milkQuota: Volume = milkPrice * USD(20) // returns UsGallons(5)

FX Support

Currency Exchange Rates

val rate = CurrencyExchangeRate(USD(1), JPY(100))
val someYen: Money = JPY(350)
val dollarAmount: Money = rate.convert(someYen) // returns USD(3.5)
val someBucks: Money = USD(23.50)
val yenAmount: Money = rate * someBucks 		// returns JPY(2350)

Money Context

A MoneyContext can be implicitly declared to define default settings and applicable exchange rates. This allows your application to work with a default currency based on an application configuration. It also provides support for dynamically updating exchange rates and using those rates for automatic conversions between currencies. The technique and frequency chosen for exchange rate updates is completely in control of the application.

implicit val moneyContext = MoneyContext(defCur, curList, exchangeRates)
val someMoney = Money(350) // 350 in the default Cur
val usdMoney: Money = someMoney in USD
val usdDouble: Double = someMoney to USD
val yenCost: Money = (energyPrice * MegawattHours(5)) in JPY
val northAmericanSales: Money = (CAD(275) + USD(350) + MXN(290)) in USD

Quantity Ranges

Used to represent a range of Quantity values between an upper and lower bound

val load1: Power = Kilowatts(1000)
val load2: Power = Kilowatts(5000)
val range: QuantityRange[Power] = QuantityRange(load1, load2)

Use multiplication and division to create a Seq of ranges from the original

// Create a Seq of 10 sequential ranges starting with the original and each the same size as the original
val rs1 = range * 10
// Create a Seq of 10 sequential ranges each 1/10th of the original size
val rs2 = range / 10
// Create a Seq of 10 sequential ranges each with a size of 400 kilowatts
val rs3 = range / Kilowatts(400)

Apply foreach, map and foldLeft/foldRight directly to QuantityRanges with a divisor

// foreach over each of the 400 kilometer ranges within the range
range.foreach(Kilometers(400)) {r => ???}
// map over each of 10 even parts of the range
range.map(10) {r => ???}
// fold over each 10 even parts of the range
range.foldLeft(10)(0) {(z, r) => ???}

Natural Language Features

Implicit conversions give the DSL some features that allows client code to express quantities in a more natural way.

// Create Quantities using Unit Of Measure Factory objects (no implicits required)
val load = Kilowatts(100)
val time = Hours(3.75)
val money = USD(112.50)
val price = Price(money, MegawattHours(1))
// Create Quantities using Unit of Measure names and/or symbols (uses implicits)
val load1 = 100 kW 			// Simple expressions don’t need dots
val load2 = 100 megaWatts
val time = 3.hours + 45.minutes // Compound expressions may need dots
// Create Quantities using operations between other Quantities
val energyUsed = 100.kilowatts * (3.hours + 45.minutes)
val price = 112.50.USD / 1.megawattHours
val speed = 55.miles / 1.hours
// Create Quantities using formatted Strings
val load = Power("40 MW")		// 40 MW

The last conversion is useful for automatically interpreting strings from user input, json marshaller and other sources

Type Hierarchy

The type hierarchy includes two root base traits: Quantity and UnitOfMeasure

Quantity

Quantity measures the magnitude or multitude of some thing. Classes extending Quantity represent the various types of quantities that can be measured. These are our alternatives to just using Double. Common 'Base' Quantities include Mass, Temperature, Length, Time, Energy, etc.

Derived Quantities are based on one or more other quantities. Typical examples include Time Derivatives.

Speed is the 1st Time Derivative of Length (Distance), Acceleration is the 2nd Time Derivative.

val distance: Length = Kilometers(100)
val time: Time = Hours(2)
val speed: Speed = distance / time
assert(speed.toKilometersPerHour == 50.0)
val acc: Acceleration = Meters(50) / Second(1) / Second(1)
assert(acc.toMetersPerSecondSquared == 50)

Power is the 1st Time Derivative of Energy, PowerRamp is the 2nd

val energy: Energy = KilowattHours(100)
val time: Time = Hours(2)
val power: Power = energy / time
assert(power.toKilowatts == 50.0)
val ramp: PowerRamp = KilowattHours(50) / Hours(1) / Hours(1)
assert(ramp.toKilowattsPerHour == 50)

Squants currently supports over 50 quantity types.

Unit of Measure

UnitOfMeasure is the scale or multiplier in which the Quantity is being measured.

For each Quantity a series of UOM objects implement a base UOM trait typed to that Quantity. The UOM objects define the unit symbols and conversion settings. Factory methods in each UOM object create instances of the corresponding Quantity.

For example UOM objects extending LengthUnit can be used to create Length quantities

val len1: Length = Inches(5)
val len2: Length = Meters(4.3)
val len3: Length = UsMiles(345.2)

Units of Measure for Time include Milliseconds, Seconds, Minutes, Hours, and Days

Units of Measure for Temperature include Celsius, Kelvin, and Fahrenheit

Units of Measure for Mass include Grams, Kilograms, etc.

Squants currently supports over 150 units of measure

Use Cases

Dimensional Analysis

The primary use case for Squants, as described above, is to produce code that is typesafe with in domains that perform dimensional analysis.

val energyPrice: Price[Energy] = 45.25.money / megawattHour
val energyUsage: Energy = 345.kilowatts * 5.4.hours
val energyCost: Money = energyPrice * energyUsage

val dodgeViper: Acceleration = 60.miles / hour / 3.9.seconds
val speedAfter5Seconds: Velocity = dodgeViper * 5.seconds
val TimeTo100MPH: Time = 100.miles / hour / dodgeViper

val density: Density = 1200.kilograms / cubicMeter
val volFlowRate: VolumeFlowRate = 10.gallons / minute
val flowTime: Time = 30.minutes
val totalMassFlow: Mass = volFlowRate * flowTime * density

Domain Modeling

Another excellent use case for Squants is stronger types for fields in your domain model. This is OK ...

case class Generator(id: String, maxLoadKW: Double, rampRateKWph: Double,
operatingCostPerMWh: Double, currency: String, maintenanceTimeHours: Double)
...
val gen1 = Generator("Gen1", 5000, 7500, 75.4, "USD", 1.5)
val gen2 = Generator("Gen2", 100, 250, 2944.5, "JPY", 0.5)
assetManagementActor ! ManageGenerator(gen1)

… but this is much better

case class Generator(id: String, maxLoad: Power, rampRate: PowerRamp,
operatingCost: Price[Energy], maintenanceTime: Time)
...
val gen1 = Generator("Gen1", 5 MW, 7.5.MW/hour, 75.4.USD/MWh, 1.5 hours)
val gen2 = Generator("Gen2", 100 kW, 250 kWph, 2944.5.JPY/MWh, 30 minutes)
assetManagementActor ! ManageGenerator(gen1)

Anticorruption Layers

Create wrappers around external services that use basic types to represent quantities. Your application code then uses the ACL to communicate with that system thus eliminating the need to deal with type and scale conversions in your application logic.

class ScadaServiceAnticorruption(val service: ScadaService) {
  // ScadaService returns load as Double representing Megawatts
  def getLoad: Power = Megawatts(service.getLoad(meterId))
  }
  // ScadaService.sendTempBias requires a Double representing Fahrenheit
  def sendTempBias(temp: Temperature) =
    service.sendTempBias(temp.to(Fahrenheit))
}

Implement the ACL as a trait and mix in to the application's services where needed.

trait WeatherServiceAntiCorruption {
  val service: WeatherService
  def getTemperature: Temperature = Celsius(service.getTemperature)
  def getIrradiance: Irradiance = WattsPerSquareMeter(service.getIrradiance)
}

Extend the pattern to provide multi-currency support

class MarketServiceAnticorruption(val service: MarketService)
     (implicit val moneyContext: = MoneyContext) {
  // MarketService.getPrice returns a Double representing $/MegawattHour
  def getPrice: Price[Energy] =
    (USD(service.getPrice) in moneyContext.defaultCurrency) / megawattHour
  // MarketService.sendBid requires a Double representing $/MegawattHour
  // and another Double representing the max amount of energy in MegawattHours
  def sendBid(bid: Price[Energy], limit: Energy) =
    service.sendBid((bid * megawattHour) to USD, limit to MegawattHours)
}

Build Anticorruption into Akka routers

// LoadReading message used within the Squant’s enabled application context
case class LoadReading(meterId: String, time: Long, load: Power)
class ScadaLoadListener(router: Router) extends Actor {
  def receive = {
   // ScadaLoadReading, from an external service, types load as a string
   // eg, “10.3 MW”, “345 kW”
   case msg @ ScadaLoadReading(meterId, time, loadString) 
    // This handler converts the string to a Power value and emits the events
    // to the routees which are actors within an app context that uses Squants
    router.route(LoadReading(meterId, time, Power(loadString)), sender())
  }
}

... and REST API's with contracts that require basic types

trait LoadRoute extends HttpService {
  def repo: LoadRepository
  val loadRoute = {
    path("meter-reading") {
      // REST API contract requires load value and units in different fields
      // Units are string values that may be 'kW' or 'MW'
      post {
        parameters(meterId, time, loadDouble, unit) { (meterId, time, loadDouble, unit) =>
          complete {
            val load = unit match {
              case "kW" => Kilowatts(loadDouble)
              case "MW" => Megawatts(loadDouble)
            }
            repo.saveLoad(meterId, time, load)
          }
        }
      } ~
      // REST API contract requires load returned as a number representing megawatts
      get {
        parameters(meterId, time) { (meterId, time) =>
          complete {
            repo.getLoad(meterId, time) to Megawatts
          }
        }
      }
    }
  }
}

About

The Scala API for Quantities, Units of Measure and Dimensional Analysis

License:Apache License 2.0


Languages

Language:Scala 100.0%