This project provides exact and fuzzy numeric computation with lazy evaluation in Scala.
- Exact arithmetic wherever possible - including π, e, and √2
- Tracked error bounds - inexact numbers carry their uncertainty
- Lazy evaluation - expressions optimize away precision loss
- Multiple numeric domains - angles, logarithms, roots, complex, and more
- Cats integration - leverages typelevel algebra for abstract algebra
Number is organized into multiple modules:
algebra- Algebraic structures based on Cats typeclassesparse- Parsing facilities for (lazy) expressions and (eager) algebraic structuresexpression- Lazy expression evaluation (being migrated to algebra)core- Legacy numeric types (Number, Field, Rational, Complex, Factor, Fuzz, ****etc.)top- Top level example code
The current version is 1.4.4.
Migration Note: The algebra module is replacing core.Number and core.Field with a cleaner type hierarchy based on algebraic structures.
Migration Note: For version history and more detail regarding migration, see the HISTORY.
import com.phasmidsoftware.number.algebra.eager.Eager
import com.phasmidsoftware.number.algebra.util.LatexRenderer.LatexRendererOps
@main def exampleMainProgram(): Unit =
import expr.*
// Method 1: Start with identity operator
val expr0 = ∅ + 1 + 2 * 3
val expr1 = ∅ * 1 * 2 * 3
// Method 2: String interpolators
val expr2 = math"1 + 2 * 3" // resulting type is Eager
val expr3 = lazymath"$expr1∧2 + 3 * $expr1 - 5" // resulting type is a simplified Expression
val expr4 = puremath"1 + 2 * 3" // resulting type is Expression
// Method 3: Predefined constants
val expr5 = one + 2 * 3
val expr6 = π / 2
// Method 4: Explicit type annotation
val expr7: Expression = 1 + 2
The key import here is import expr.* such that as many Expression methods and constants are available as possible,
including the various interpolators.
For more examples, see the GettingStarted.sc worksheet in the top module.
Wikipedia has been my constant reference for basic mathematical relationships. I'm also indebted to Claude (by Anthropic) for her excellent advice regarding the restructuring in versions 1.3.2 and beyond. Note: While "Claude" is typically a male name in the English-speaking world, in France it is also common as a female name. I'm particularly honoring poor Claude-Emma Debussy ("Chouchou") here.
However, many of the specific ideas and much of the theory behind this project derives from the following book:
- Abramowitz and Stegun, (1970). Handbook of Mathematical Functions with Formulas, Graphs and Mathematical Tables, 9th printing. Dover Publications.
You can also find the 7th printing free online:
All objects that have a "value" in Number are instances of Valuable, which in turn extends Renderable, Numeric,
, Exactitude, Normalizable, and TypeSafe.
Valuable has two subtypes: Eager and Lazy.
Eager values are evaluated and can be rendered as Strings or converted to Double (or even Java Numbers).
Lazy values are not evaluated (they're lazy) and represent numerical expressions.
The purpose of the lazy values is that often, composing a value with another value might not be renderabl exactly. In such a case, eager arithmetic would be forced to evaluate the expression as a fuzzy value.
Yet, sometimes, that loss of precision is premature. For example, in the expression
The algebra module provides a type hierarchy rooted at Eager and based on mathematical structures, with full integration of Cats typeclasses.
Eager is extended by Solution, Nat, and Structure.
The Nat type represents the natural numbers (non-negative integers), based on Peano arithmetic.
The Solution type represents the set of solutions to an equation.
Solution is extended by Algebraic (for non-complex solutions) and Complex (for complex solutions).
All algebraic types extend Structure, which provides:
- Type-safe conversions:
convert[T <: Structure](t: T): Option[T] - Java interop:
asJavaNumber: Option[java.lang.Number]
See docs/DIAGRAMS.md for all project diagrams.
Monotone types have a meaningful total ordering where the order increases monotonically with the underlying value (though not always linearly):
Scalar- Linear relationship (e.g., pure numbers)InversePower- Non-linear monotone (e.g., x^(-n))Transformed- Non-linear monotone (e.g., logarithms)
Non-Monotone types lack total ordering:
Complex- No natural ordering for complex numbersAngle- Circular structure makes ordering meaningless
Some types from the legacy code (core module) but are still used, in particular, Rational, Factor, Fuzziness, and Complex.
The Number class in the legacy code (not to be confused with the class of the same name in the algebra module) represents a number by
specifying its value (as a Value), its factor (as a Factor), and optional fuzziness (as a Fuzziness).
For much more detail on these types, see below.
The main numeric hierarchy supports exact and fuzzy arithmetic:
// WholeNumber - integers/naturals
val n = WholeNumber(42)
// RationalNumber - exact rationals
val third = RationalNumber(r"1/3")
val percent = RationalNumber(r"1/2", isPercentage = true)
percent.render // "50%"
// Real - fuzzy/uncertain numbers
val piApprox = Real("3.14159*")For details of the parsing of fuzzy numbers like for piApprox (above), please see Core Module Parsing below.
Angles support both radians and degrees as display preferences:
val a1 = Angle(Number(1.5), radians = true)
val a2 = Angle(Number(1.5), radians = false)
a1.render // displays in radians
a2.render // displays in degrees
// But they compare equal (normalized values are the same)
a1 === a2 // trueAngles normalize to the range [-1, 1) where a full circle = 2.
Important: Angle has no Order instance because circular ordering is meaningless. It only supports Eq for equality testing.
Transformed values, including Logarithm, store a value which is then rendered as a pure number via a transforming function.
However, this makes the terminology slightly confusing.
For example, the value stored in a Logarithm is the logarithm.
Thus,
val natLog = NatLog(Number(2)) // represents e^2...because 2 is the natural logarithm of e^2.
The algebra module integrates with Cats to provide standard algebraic structures:
| Type | Cats Typeclass(es) | Meaning |
|---|---|---|
RationalNumber |
Field[RationalNumber] |
All operations with inverses (strongest) |
Real |
Ring[Real] |
Addition group + multiplication monoid |
WholeNumber |
CommutativeRing[WholeNumber] |
No additive inverses (can't subtract) |
Angle |
AdditiveCommutativeGroup[Angle] |
Circle group structure |
RationalNumbergetsFieldbecause rationals support all operations: +, -, ×, ÷RealgetsRing(notField) because fuzzy numbers don't have proper multiplicative inversesWholeNumbergetsCommutativeRingbecause you can't subtract and stay in whole numbersAnglegetsAdditiveCommutativeGroupfor the circle group, but multiplication doesn't make geometric sense
The algebra module provides both Cats Eq instances and ScalaTest Equality instances:
import cats.syntax.eq._
import com.phasmidsoftware.number.algebra.StructuralEquality._
// Using Cats Eq in production code
angle1 === angle2
// Using Scalactic Equality in tests
class MySpec extends AnyFlatSpec with Matchers with StructuralEquality {
angle1 should === (angle2)
}For Angle and RationalNumber, equality ignores display flags (radians/degrees, percentage) and compares normalized mathematical values.
Additionally, the algebra module provides both FuzzyEq instances for Eager types:
import com.phasmidsoftware.number.algebra.core.FuzzyEq.~=
val x: Eager = Real(scala.math.Pi)
val y: Eager = Angle(Real(1.0))
x == y // false
x ~= y // trueNumber supports flexible parsing of numeric values from strings, with automatic detection of exact vs. fuzzy numbers.
The algebra module provides limited parsing currently:
import com.phasmidsoftware.number.algebra._
val maybeTheAnswer: Option[RationalNumber] = RationalNumber.parse("42")
val maybeHalf: Option[RationalNumber] = RationalNumber.parse("1/2")
val maybeSevenPercent: Option[RationalNumber] = RationalNumber.parse("7%")For more complex expression parsing with LaTeX-style syntax, see the Expression Module section below, which provides the math, lazymath, puremath, and mathOpt interpolators.
A String representing a number with two or fewer decimal places is considered exact--a number with more than two decimal places is considered fuzzy, unless it ends in two zeroes, in which case it is considered exact. Here are some examples:
- Real("1.00"): exact
- Real("1.0100"): exact
- Real("1.100"): exact
- Real("1.010"): fuzzy
You can always override this behavior by adding "*" or "..." to the end of a number with fewer than two DPs, or by adding two 0s to the end of a number with more than two decimal places.
- Real("1.100*")" fuzzy
See RealWorksheet.sc
The rules are a little different if you define a number using a floating-point literal such as Number(1.23400), the compiler will treat that as a fuzzy number, even though it ends with two zeroes because the compiler essentially ignores them. However, Real(1.23) will be considered exact while Real(1.234) will not. It's best always to use a String if you want to override the default behavior.
In general, the form of a number to be parsed from a String is:
// Grammar for parsing Number strings
number ::= value? factor?
factor ::= "Pi" | "pi" | "PI" | π | ε | √ | ³√
value ::= sign? nominalValue fuzz* exponent*
nominalValue ::= integerPart ( "." fractionalPart )? | rational
rational ::= digits "/" digits
integerPart ::= digits
fractionalPart ::= digits
fuzz ::= "..." | "*" | "(" fuzz ")" | "[" fuzz "]"
exponent ::= E sign? digits
fuzz ::= one or two digitsNote that the e and pi symbols are, respectively,
(in Unicode): \uD835\uDF00 and \uD835\uDED1 (�� and ��)
A number must have at least one of either the value or the factor components.
If no explicit factor is specified, then the number will be a PureNumber (an ordinary number).
If you want to get exact trigonometric values, then it's important to specify the factor as
Parsing, described above, is really the most precise way of specifying numerical values. But, of course, it's a lot easier to write code that uses numerical literals. For Int and Long, these give us no problems, of course. Neither is there any issue with Rational, BigDecimal, and BigInt. BigDecimal values are represented internally by Rational. There are two ways to specify Rational numbers:
- one is to create a String of the form r"n/d" where n and d represent the numerator and the denominator;
- the other way is simply to write n:/d (again n and d are as above).
Either of these methods will require importing the appropriate implicit classes from Rational. It's probably the simplest just to include:
import Rational._Doubles are where the trickiest conversions apply. Writing something like Number(3.1415927) will result in a FuzzyNumber with error bounds of 5 * 10∧-7. To be consistent with the String representation, Number(1.25) will result in an ExactNumber represented internally by a Rational of 5/4. However, if you want to force a number like 3.1415927 to be exact, then you will need to write
Number("3.141592700")
For fuzzy numbers in standard scientific notation, there is an operator "~" which, when following a Double, will add the next one or two integer digits as the standard deviation. For example, the proton-electron mass ratio:
1836.15267343~11
There are three types of rendering available: render, toLatex and toString. The latter method is primarily for debugging purposes and so tends to mirror the actual structure of an object. The render method is defined in the trait Renderable. Its purpose is to render a Valuable object in as natural and appropriate a form as possible. The latex renderer is defined in the trait LatexRenderable. Its purpose is to render a Valuable object in a form suitable for use in a LaTeX document, that's to say as a mathematical expression. For this to work, you will need to import com.phasmidsoftware.number.algebra.util.LatexRenderer._
The rest of this section pertains to the core module.
including Field, Number, Rational, Complex, etc.
For the prettiest output, you should use render rather than toString (which is basically for debugging).
Generally speaking, the output String corresponding to a Number will be the same as the input String, although that is not guaranteed. Numeric quantities followed by "(xx)" show standard scientific notation where xx represents the standard deviation of the error with respect to the last two digits (sometimes there is only one x which corresponds to the last digit). If a number is followed by "[x]" or "[xx]" this corresponds to a "box" (i.e., truncated uniform) probability density function. It's unlikely that you'll need to use this form since box is the default shape when specifying fuzzy numbers with a String.
For Rational numbers, it is most likely that the number will be rendered as exactly as possible. For values which are exactly renderable using decimal notation, that will be the result. For values which have a repeating sequence in decimal notation, the repeating sequence will be enclosed within < and >. If the repeating sequence is too long (or too hard to identify), and if the denominator is less than 100,000, the number will render as a rational, i.e., numerator/denominator. Otherwise, the number will render as many digits as possible, with "..." added to the end.
The Fuzzy[X] trait defines a typeclass which adds fuzziness to any object type. There is exactly one method defined and that is same:
def same(p: Double)(x1: X, x2: X): BooleanGiven a confidence value p (a probability between 0 and 1), this method will determine if any two objects of type X can be considered the same. If p is 0, then all Fuzzy quantities will be considered the same (i.e. same returns true). If p is 1, then Fuzzy quantities will only be considered the same if the numbers actually are exactly the same (in practice, this generally means that passing 1 for p will result in a false return).
The fuzzyCompare method of FuzzyNumber does use the same method.
Note that the Fuzzy trait assumes nothing at all about the representation of X, or even if X is numeric. The spec file shows an example where X represents a color. In the vast majority of cases, the X of Fuzzy will be Double.
Comparison between Numbers is based on their values, providing that they belong to the same domain (see Factor, below). If they are from different domains, one number will be converted to the domain of the other. If, after any conversion is taken into account, the two values compare equal, then the Numbers are equal. For ExactNumber, comparison ends there.
However, for FuzzyNumber, it is then determined whether there is a significant overlap between the fuzz of the two numbers. See Fuzzy, above. The FuzzyNumber object has a method fuzzyCompare, which invokes same for two fuzzy numbers, given a confidence value (p). This, in turn, is invoked by fuzzyCompare of GeneralNumber, which compares this with another Number.
If the overlap is sufficient that there is deemed to be a 50% probability that the numbers are really the same, then the comparison yields 0 (equal). Additionally, each of the comparison methods involved has a signature which includes a p value (the confidence probability). The compare(Number) method of FuzzyNumber (arbitrarily) sets the p value to be 0.5.
The Mill trait allows expressions to be evaluated using RPN (Reverse Polish Notation). For example:
val eo: Option[Expression] = Mill.parseMill("42 37 + 2 *").toOption.flatMap(_.evaluate)yields the optional Expression with a materialized value of 158. See the code for other methods for defining Mill operations.
The Mill.parse method in turn invokes methods of MillParser.
The Mill offers two parsers: one is a pure RPN parser (as described above). The other is an infix parser that uses Dijkstra's Shunting Yard algorithm to build a Mill.
Some of the operators of Mill are as follows:
∧: Power (also ^)
+: Add
-: Subtract
*: Multiply (also ×)
/: Divide (also ÷)
v: Sqrt
<>: Swap
: Noop
(: Open
): Close
Additional operators include clr, chs, inv, ln, exp, sin, cos.
The most general form of mathematical quantity is represented by a Field. See Field. A field supports operations such as addition, subtraction, multiplication, and division. We also support powers because, at least for integer powers, raising to a power is simply iterating over a number of multiplications.
Field extends Numerical which, in turn, extends NumberLike (see definitions below).
The three types of Field supported are Real, Algebraic, and Complex. Real is a wrapper around a Number (see below) while Complex (see below) is a wrapper around two _Number_s (more or less).
Number is a trait that extends Numerical (but not Field).
There are two subtypes of Number: ExactNumber and FuzzyNumber. Each of these types extends an abstract type called GeneralNumber (although this relationship is expected to change at some future point). GeneralNumber has three members:
- value (type Value): the numerical value of the number;
- factor (type Factor): the domain of the value (scalar, radian, log, root, etc.);
- (optional) fuzz (type Fuzz[Double]): the fuzziness of the number (always None for an ExactNumber).
The "value" of a Number is represented by the following type (see com.phasmidsoftware.number.package.scala):
type Value = Either[Either[Option[Double], Rational], Int]Thus, an integer x is represented by Right(x). A Rational x is represented by a Left(Right(x)). A Double x is represented by a Left(Left(Some(x))). There is also an invalid Number case (NaN) which is represented by Left(Left(Left(None))).
This Value is always of the rightmost type possible: given the various possible specializations. Thus, an Int x which is in range will be represented by Right(x). Thus, a Rational with numerator x and unit denominator, where x is in the range of an Int, will be represented by Right(x). It is also possible that a Double x will be represented by a Left(Right(Rational(x))). For this to happen, the value in question must have fewer than three decimal places (similar to the parsing scheme).
Real is a wrapper around a Number and implements Field.
Most of the things you can do with a Number, you can also do with a Real.
In general, a Real will be part of the domain
In addition to the properties of Field, the following methods are defined:
def sqrt: Field
def sin: Field
def cos: Field
def tan: Field
def atan(y: Real): Field
def log(b: Real): Field
def ln: Field
def exp: Field
def toDouble: DoubleFor examples of usage, especially constructing Real objects, please see RealWorksheet.sc.
There are two types of Complex: ComplexCartesian and ComplexPolar. Complex numbers support all the Field operations, as well as modulus, argument, rotate, and conjugate. It is easy to convert between the two types of Complex.
The ComplexPolar object has an additional member (as well as the real and imaginary parts):
the number of branches.
For example, the square root of 2 should have two branches, yielding:
There are two ways to parse a String as a Complex (in each case, the parsing of the String is identical):
- Complex.parse(String): will return a Try[Complex];
- C"...": will return a Complex (or throw an exception).
For example (see also Complex.sc).
C"1i0" : ComplexCartesian(1,0)
C"1-i1" : ComplexCartesian(1,-1)
C"1ipi" : ComplexPolar(1,pi)
Additionally (see below), it is possible to define imaginary values on their own using the following syntax:
val x = Number.i
import SquareRoot.IntToImaginary
val y = 2.i // to give 2iAn Algebraic is a particular root of some polynomial function. It has an equation attribute (of type Equation) and a branch attribute (where the number of branches is the degree of the polynomial). In order to realize an Algebraic as an actual numerical value (or String), you must solve it and thus create s Solution. A Solution typically has two parts: the base value (a PureNumber) and the offset (which depends on the branch). The offset will typically have a different factor such as SquareRoot. An example of an Algebraic is phi, the Golden Ratio. An Algebraic is considered an exact value, although rendering it in decimal form may result in an approximation.
An Algebraic extends Field so can be operated on as any other field. Additionally, there are other operations, unique to Algebraic, that allow exact transformations. Examples include scale, negate, add, multiply, etc.
Algebraic is a parallel concept to Complex (and Real).
The hierarchy of Algebraic is (currently) as follows:
- Algebraic
- Algebraic_Linear (case class representing a Rational number defined as the root (solution) of a monic linear equation)
- Algebraic_Quadratic (case class representing a quantity defined as the root (solution) of a monic quadratic equation)
Factor represents the domain in which a numerical quantity exists. We are most familiar with the pure-number domain, including all the counting numbers, the decimal numbers, and the so-called "real" numbers.
Slightly less familiar are numbers like
The hierarchy of Factor is as follows:
-
Factor (trait: the domain of factors)
-
Scalar (trait: the domain of ordinary numbers)
- PureNumber (object: the domain of pure numbers)
- Radian (object: the domain of radians)
-
Logarithmic (trait: the domain of exponential quantities where the corresponding value is a logarithm)
-
NatLog (object: natural log, i.e.,
$\log_e$ ) -
Log2 (object:
$\log_2$ ) -
Log10 (object:
$\log_{10}$ )
-
NatLog (object: natural log, i.e.,
-
InversePower (trait: all the roots)
-
NthRoot (abstract class)
- SquareRoot (object: the domain of square roots)
- CubeRoot (object: the domain of cube roots)
- AnyRoot (case class: a generalized root based on a Rational)
-
NthRoot (abstract class)
-
Scalar (trait: the domain of ordinary numbers)
As of V 1.0.2, NthRoot is a subclass of InversePower. The inverse power (which root) is a Rational in the case of InversePower but an Int in the case of NthRoot.
These allow certain quantities to be expressed exactly, for example,
Trigonometrical functions are designed to work with Radian quantities.
Such values are limited (modulated) to be in the range
val target = (Number.pi/2).sin
target shouldBe Number.oneSimilarly, if you use the atan method on a Scalar number, the result will be a number (possibly exact) whose factor is Radian.
The 𝜀 factor works quite differently.
It is not a simple matter of scaling.
A Number of the form Number(x, NatLog) actually evaluates to
It would be possible to implement
Negative values associated with SquareRoot are imaginary numbers.
Constant values of fields are defined in the Constants object.
Many of the values are dependent on constants in the Number class which defines values for pi,
The Constants object also contains a number of fundamental (physical and mathematical) constant definitions, in addition to those defined by Number. For example, c (speed of light), alpha (fine structure constant), etc.
NumberLike is a trait that defines behavior which is of the most general number-like nature. The specific methods defined are:
def isExact(maybeFactor: Option[Factor]): Boolean // determines if this object is exact in the domain of the (optional) factor
def isExact: Boolean = isExact(None)
def asNumber: Option[Number]
def render: StringAdditionally, there are two methods relating to the Set of which this NumberLike object is a member, such as
the integers (
def memberOf: Option[NumberSet]
def memberOf(set: NumberSet): BooleanNumerical extends NumberLike. Additional methods include:
def isSame(x: Numerical): Boolean // determines if this and x are equivalent, numerically.
def isInfinite: Boolean
def isZero: Boolean
def isUnity: Boolean
def signum: Int
def unary_- : Field
def invert: Field
def normalize: Field
def asComplex: Complex
def asReal: Option[Real]NumberSet is a trait that recognizes the following sets:
- N:
$\mathbb{N}$ (the counting numbers); - Z:
$\mathbb{Z}$ (the integers); - Q:
$\mathbb{Q}$ (the rationals); - R:
$\mathbb{R}$ (the reals); - C:
$\mathbb{C}$ (the complex numbers);
The most important method is:
def isMember(x: NumberLike): Booleanwhich will yield the most exclusive set that x belongs to.
Number lazy evaluation via a trait called Expression.
The advantage of lazy evaluation is not so much performance.
That's going to be neither here nor there.
But it is in avoiding precision loss in some circumstances.
The simplification mechanism which is invoked when materializing an expression goes to great lengths to cancel out any loss of precision.
An example of this is the expression (√3 + 1)(√3 - 1). It is clear that this should have a value of exactly 2. However, it is not trivial to do the appropriate matching to achieve this simplification. This is why Number uses the Matchers package (https://github.com/rchillyard/Matchers).
The simplification mechanism uses its own ExpressionMatchers, which is an extension of Matchers. The current set of expression optimizations is somewhat limited, but it catches the most important cases.
For example, suppose an expression you are working on involves the square root of, say, 7. However, you don't particularly pay attention to the fact that later on in the calculation, you square everything. If you don't use lazy evaluation, your final result will have an error bound, even though the true value should be proportional to exactly 7.
It's important to realize that, to get the benefit of this behavior, you must use the Expression mechanism (not a pure Number).
it should "give precise result for sqrt(7)∧2" in {
val x: Expression = Number(7)
val y = x.sqrt
val z = y ∧ 2
z.materialize shouldBe Number(7)
}
it should "show ∧2 and sqrt for illustrative purposes" in {
val seven = Number(7)
val x = seven.sqrt
val y = x ∧ 2
y shouldEqual Number(7)
y shouldBe Number(7)
}
The second test fails with "7.000000000000001 was not equal to 7," although if we do a fuzzy comparison, using a custom equality test, we can at least make y shouldEqual 7 work.
NOTE: from V 1.0.12 onwards, there are more special cases implemented in the Number code, and so many of these issues which required the use of Expressions will now work just using Numbers. This is particularly true of the example above involving the square root of 7.
There is an implicit class ExpressionOps which provides methods which allow Number operations to behave as expressions. So, for example, you can write:
val x = Number(1) + 2For this to compile properly, you will need to import the ExpressionOps class.
The one drawback of the Expression mechanism is that, when you want to convert back to a Number, it is a little awkward. You can use the asNumber method (which returns an Option[Number]), or you can use an implicit converter (in which case, you will need to ensure that you have Number._ imported). If you use the latter mechanism, keep in mind that it's possible that an exception will be thrown.
See below for the different types of Expression.
When evaluating an Expression, we need to know what are the acceptable contexts for the evaluation. For instance, if we are going to try to print a number as a decimal representation (of a binary representation), we will need to evaluate the number in the RestrictedContext(PureNumber) context. Often, this will require an approximation (i.e., the generation of a FuzzyNumber).
The hierarchy of Context is as follows:
- Context (trait: closely related to Factor: it is used to determine which domains are acceptable in a particular context)
- RestrictedContext (case class accepts only a specific Factor)
- AnyContext (object: accepts any Factor)
- ImpossibleContext (object: accepts no Factor)
Transcendental numbers are declared as subtypes of Transcendental, although
The error bounds are represented by the Fuzz[Double] class. A Number with None for the fuzz is an ExactNumber, otherwise, FuzzyNumber. There are three major attributes of fuzz: shape, style (either relative or absolute), and the value (called magnitude when absolute, and tolerance when relative). Shape describes the probability density function (PDF) of values compared to the nominal value. There are currently only two types of shape:
- Box: a truncated uniform probability density function--magnitude/tolerance relate to half the width of the non-zero probability section.
- Gaussian: a normal probability density function: the nominal value is at the mean, and magnitude/tolerance is the standard deviation.
It's easy to convert between these four different possibilities. Generally speaking, when doing addition (or when a Number is first defined), it's convenient for the fuzz to be absolute. When performing any other operation, it's most convenient for the fuzz to be relative. It's not possible to combine two Box-shaped fuzzes: it would be possible if we allowed for trapezoids as well as rectangles, but that's far too complicated. So, whenever we combine fuzz (using convolution), we operate on Gaussian PDFs, which can easily be combined.
So, why is relative fuzz usually the best? Well, consider scaling--multiplying by a constant.
The relative fuzz doesn't change at all.
In the following,
Differentiating, we get,
Dividing both sides by y, yields
Thus, the relative fuzz of y is equal to the relative fuzz of x.
When we multiply two fuzzy numbers together, we add the relative fuzzes together:
Therefore, (ignoring the term which is
Dividing both sides by
Thus, the relative fuzz of z is equal to the sum of the relative fuzzes of x and y.
But, when Δx and Δy are taken from a Gaussian probability density function, the convolution of those two PDFs, is given by slightly different expressions depending on whether the PDFs are independent or correlated. See the code (Fuzz) for details.
Things get only slightly more complex when applying monadic (single operand) functions or applying a function such
as
In general, when we apply a monadic operator
Constants cancel, powers survive as is and so on.
For example, if
Again, these formulas can be looked up in the code.
Comparing two fuzzy numbers involves subtracting the two numbers and then determining if the probability at zero is sufficiently high to consider the difference to be zero. If the probability is greater than 50% (the default--although there are method signatures that allow for different values), then we consider that the different is zero (method isZero) or that it has a signum of 0.
Numeric operations (i.e., eager operations) are performed using a set of subtypes of Operation. The common feature of these Operation types is that they provide a set of functions each of which can be applied to a different type of Value (viz., Int, Rational, Double).
The hierarchy of Operation is as follows:
- Operation (trait)
- MonadicOperation (trait)
- MonadicOperationAtan
- MonadicOperationNegate
- MonadicOperationLog
- MonadicOperationModulate
- MonadicOperationScale
- MonadicOperationSin
- MonadicOperationFunc
- MonadicOperationSqrt
- MonadicOperationInvert
- MonadicOperationExp
- DyadicOperation (trait)
- DyadicOperationPlus
- DyadicOperationTimes
- DyadicOperationPower
- MonadicOperation (trait)
The Approximation object provides a method solve which will implement the Newton-Raphson method of approximation and also Halley's method (if you need it). See Newton.sc for examples.
This library includes a facility to create continued fractions which can be used to define (or approximate) constant values. See the worksheet ContinuedFractions.sc.
For example, the golden ratio (
The lazy mechanism (see above) is based on Expressions.
In the following, by "exact," we mean a quantity that is exact (like
The hierarchy of Expression (i.e., lazy) types is as follows (as of version V 1.2.8):
-
Expression (trait: all lazy NumberLike objects)
-
AtomicExpression (trait: a single exact number)
-
FieldExpression (abstract class)
- Literal (case class: defined as a Field and an optional name)
-
NamedConstant (abstract class)
-
ScalarConstant (abstract class)
- Zero
- One
- Two
- Half
- MinusOne
- ConstPi
- Infinity
- ConstE
- ConstI
-
ScalarConstant (abstract class)
- Transcendental (trait) defining exact numbers with no other definition
-
Root
-
phi (object:
$\phi$ , the Golden Ratio) -
psi (object:
$\psi$ , the conjugate of$\phi$ )
-
phi (object:
- Noop (object: not an expression)
-
FieldExpression (abstract class)
-
CompositeExpression (trait for any Expression that is defined by a tree of functions)
- BiFunction (case class: two expressions combined by a dyadic function--see below)
- Function (case class: one expression modified by a function--see below)
- Aggregate (case class: similar to BiFunction but with multiple expressions all combined by the same dyadic function)
-
AtomicExpression (trait: a single exact number)
-
ExpressionFunction (trait)
-
ExpressionBiFunction (trait used in BiFunction (above))
- Atan
- Log
- Sum
- Product
- Power
-
ExpressionMonoFunction (trait used in Function above)
- Cosine
- Sine
- Exp
- Ln
- Negate
- Reciprocal
-
ExpressionBiFunction (trait used in BiFunction (above))
Other types (for reference):
- Factor (see above)
- Context (see above)
- Multivalued
- Equation (trait defining an equation to be used in an Algebraic quantity)
- Quadratic (case class defining a monic quadratic equation)
- LinearEquation (case class defining a monic linear equation)
- Equation (trait defining an equation to be used in an Algebraic quantity)
- Fuzz
- GeneralNumber
- Number
- FuzzyNumber
- Fuzziness
- RelativeFuzz (case class to represent relative values of fuzziness)
- AbsoluteFuzz (case class to represent absolute values of fuzziness)
- Shape
- Box (a probability density function for errors which is in the shape of a box)
- Gaussian (a probability density function for errors which is in the shape of a "normal" (Gaussian) distribution)
- Valuable (type-class trait used in fuzzy arithmetic and which extends Fractional from the Scala library)
- ValuableDouble
- NumberSet
- N (the natural, i.e., counting numbers)
- Z (the integers, i.e., whole numbers)
- Q (the rational numbers)
- R (the real numbers)
- C (the complex numbers)
- Approximation (object with methods for solving functions using the Newton-Raphson method or, more generally, Householder's method)
- Approximatable (supertype of Field and Expression)
- Numerical (super-trait of Field and Number)
- Operation (see above)
- Prime (case class)
- ContinuedFraction (case class)
- Evaluatable (trait)
- ConFrac (class)
Expressions are lazily evaluated and so can be used to avoid any unnecessary computation and, especially, any approximation of what should be an exact value.
Note The expression module has been cloned and restructured from the core module. Although the expression package
still exists in core, it should not be used directly.
The Expression trait supports the following operations:
materialize- Simplifies and evaluates the expression (as anEager) by applying rules of arithmetic.simplify- Simplifies the expression by applying rules of arithmetic, returning a newExpression.approximation- Approximates the expression as anOption[Real]value, but only if the expression is not exact. An exact expression can be approximated by passing the parameterforce=trueinto this method.evaluateAsIs- Evaluates the expression to anOption[Eager]value, which will be defined providing that the expression is exact (i.e., it can be evaluated in the natural context of the expression).evaluate(Context)- Evaluates the expression to anOption[Eager]value, in the given context.
There is additionally, an implicit conversion from Expression to Eager, provided that you have used the following
import, for example:
import Expression._
val expression = ∅ + 6 :* Literal(RationalNumber(2, 3)) :+ One
val eager: Eager = expression // yields 5The best way to define expressions (or the eager values) is to use the LaTeX-like syntax which you can invoke
using one of the following interpolators (defined in com.phasmidsoftware.number.parse.ExpressionParser):
puremath- Parses an infix expression resulting in anExpressionbut without any simplification.lazymath- Equivalent topuremathbut with simplification.mathOpt- Parses, simplifies, and evaluates an expression, returning anOption[Eager]rather than anExpression.math- Parses, simplifies, and evaluates an expression (same asmathOpt), but returns anEagerrather than anOption[Eager]. Note that an exception may be thrown if the expression given cannot be parsed.
The following examples illustrate the use of the math interpolator:
val theAnswer: Eager = math"6*(3+4)"
val seven: Eager = math"""\frac{42}{6}"""
val rootTwo: Eager = math"""\sqrt{2}"""
val rootTwoAlt: Eager = math"√2"
val pi: Eager = math"""\pi"""
val twoPi: Eager = math"""2\pi"""You just need to import the interpolators as follows:
import com.phasmidsoftware.number.parse.ExpressionParser.*Another way to define expressions is to use the empty expression symbol ∅.
For example:
val theAnswer0: Expression = ∅ + 42 // Defines an expression which will evaluate to 42
val theAnswer1: Expression = ∅ * 42 // Also defines an expression which will evaluate to 42
val theAnswer2: Expression = ∅ + 𝛑 + 42 - 𝛑 // Also defines an expression which will evaluate to 42 (exactly)The empty expression ∅ evaluates to the identity for either additive or multiplicative operations.
Note: This section describes the legacy core module (as of version 1.2.11). New code should use the algebra module (see above). The core module is being gradually superseded as we migrate to a cleaner type hierarchy based on mathematical structures.
The core module provides the original implementation of fuzzy, lazy numbers with the following features:
- Exact arithmetic using Value types (Int, Rational, or Double)
- Factor-based domains for different numeric types
- Expression-based lazy evaluation
- Comprehensive fuzzy number support with error tracking
There is no such thing as accidental loss of precision (at least, provided that code follows the recommendations). For example, if you write:
val x = 1 / 2your x will be an Int of value 0, because of the way Java-style operators work (in this case, integer division).
However, if you write the idiomatically correct form:
import com.phasmidsoftware.number.core.Number.NumberOps
val x = 1 :/ 2then x will be a Number with value exactly one half.
Even better is to use the lazy expression mechanism:
val half: Expression = One / 2
half.materializeYou probably want to see some code: so go to the worksheets package and take a look, starting with NumberWorksheet.sc, Foucault1.sc, Newton.sc, and so on.
There are three articles on Medium regarding this library. They are Number (part 1), Number (part 2), and Fuzzy, lazy, functional numeric computing in Scala
The Number project provides mathematical utilities where error bounds are tracked (and not forgotten). All functions handle the transformation or convolution of error bounds appropriately. When the error bound is sufficiently large compared to a number, that number is considered to be zero (see Comparison). This implies that, when comparing numbers, any significant overlap of their error bounds will result in them testing as equal (according to the compare function, but not the equals function).
The values of Numbers are represented internally as either Int, Rational, or Double. Rational is simply a case class with BigInt elements for the numerator and denominator. It is, of course, perfectly possible to use the Rational classes directly, without using the Number (or Expression) classes.
There are four domains of values, each identified by a domain or factor (see Factors below). These allow the exact representation of roots, logarithmic numbers, radians, and pure numbers.
The most important operators are those defined in Expression.ExpressionOps. That's because you should normally be using the (lazy) expressions mechanism for arithmetic expressions. These are the usual operators, except that the power operator is ∧ (not ^ or **).
In addition to the Scala API, version 1.0.14 introduces a Java API where it is harder to invoke the Scala classes directly from Java. These situations involve classes which have similar names (or have no Java equivalent).
Here are the current API specifications:
def bigDecimalToRational(x: java.math.BigDecimal): Rational
def rationalToBigDecimal(r: Rational): java.math.BigDecimal
def bigIntegerToRational(x: BigInteger): Rational
def rationalToBigInteger(r: Rational): BigInteger
def longToRational(l: java.lang.Long): Rational
def rationalToLong(r: Rational): java.lang.Long
def doubleToRational(x: java.lang.Double): Rational
def rationalToDouble(r: Rational): java.lang.Double
def stringToRational(s: String): Rational
def bigDecimalToNumber(x: java.math.BigDecimal): Number
def numberToBigDecimal(x: Number): java.math.BigDecimal
def bigIntegerToNumber(x: BigInteger): Number
def numberToBigInteger(x: Number): BigInteger
def longToNumber(l: java.lang.Long): Number
def numberToLong(x: Number): java.lang.Long
def doubleToNumber(x: java.lang.Double): Number
def stringToNumber(s: String): Number
def add(x: Expression, y: Expression): Expression
def multiply(x: Expression, y: Expression): Expression
The top module contains high-level example code and practical demonstrations of the Number library.
Perhaps most importantly, it houses the worksheets (listed below) and also Specification (unit tests) for high-level constructs.
This module includes:
-
Worksheets - Interactive Scala worksheets demonstrating library features:
Introduction.sc- Introduction to the library.ExpressionWorksheet.sc- Working with expressionsNumberWorksheet.sc- Basic number operations and type conversionsRationalWorksheet.sc- Basics of rational numbersFoucault1.scandFoucault2.sc- Physics calculations (Foucault pendulum) [I'm not sure why we have two]Newton.sc- Numerical approximation methodsRealWorksheet.sc- Working with fuzzy/uncertain numbersContinuedFractions.sc- Continued fraction demonstrationsComplex.sc- Complex number examplesAlgebraic.sc- Algebraic number examples (from thecorepackage)- and others (to be added here).
-
Examples - Practical usage patterns showing how the modules work together:
Foucault.scala- Foucault pendulum exampleNewton.scala- Newton-Raphson approximation method exampleFlog template.sc- Template for how to use functional logging
See the worksheets for hands-on examples of the library in action.
For version history and migration notes, see the HISTORY.
