All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.phasmidsoftware.number.core.Fuzziness.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023. Phasmid Software
 */

package com.phasmidsoftware.number.core

import com.phasmidsoftware.number.core.Fuzziness.toDecimalPower
import com.phasmidsoftware.number.core.Valuable.ValuableDouble
import java.text.DecimalFormat
import org.apache.commons.math3.special.Erf.erfInv
import scala.math.Numeric.DoubleIsFractional
import scala.util.Try

/**
  * This trait models the behavior of fuzziness.
  *
  * See also related trait Fuzzy[X].
  *
  * @tparam T the underlying type of the fuzziness. Usually Double for fuzzy numerics.
  */
trait Fuzziness[T] {
  /**
    * One of two shapes for the probability density function:
    * Gaussian (normal distribution);
    * Box (uniform distribution).
    */
  val shape: Shape

  /**
    * True if this is relative Fuzziness as opposed to absolute fuzz.
    */
  val style: Boolean

  /**
    * Transform this Fuzziness[T] according to func.
    * Typically, func will be the relative fuzz function arising from a monadic operation.
    *
    * @param func the function to apply to this Fuzziness[T].
    * @return an (optional) transformed version of Fuzziness[T].
    */
  def transform[U: Valuable, V: Valuable](func: T => V)(t: T): Fuzziness[U]

  /**
    * Perform a convolution on this Fuzziness[T] with the given addend.
    * There are two different Fuzziness[T] shapes, and they clearly have different effects.
    * When the shapes are absolute, this operation is suitable for addition of Numbers.
    * When the shapes are relative, this operation is suitable for multiplication of Numbers.
    *
    * Whether or not this is a true convolution, I'm not sure.
    * But is an operation to combine two probability density functions and, as such, f * g = g * f.
    *
    * @param convolute   the convolute, which must have the same shape as this.
    * @param independent true if the fuzz distributions are independent.
    * @return the convolution of this and the convolute.
    */
  def *(convolute: Fuzziness[T], independent: Boolean): Fuzziness[T]

  /**
    * Method to possibly change the style of this Fuzziness[T}.
    *
    * @param t        the magnitude of the relevant Number.
    * @param relative if true then change to Relative (if Absolute).
    * @return the (optional) Fuzziness as a Relative or Absolute Fuzziness, according to relative.
    */
  def normalize(t: T, relative: Boolean): Option[Fuzziness[T]]

  /**
    * Method to convert this Fuzziness[T] into a Fuzziness[T] with Gaussian shape.
    *
    * @return the equivalent Fuzziness[T] with Gaussian shape.
    */
  def normalizeShape: Fuzziness[T]

  /**
    * Method to yield a String to render the given T value.
    *
    * @param t a T value.
    * @return a tuple of a Boolean (indicating if the value is embedded in the result) and a String which is the textual rendering of t with this Fuzziness applied.
    */
  def toString(t: T): (Boolean, String)

  /**
    * A variation on toString where we render this Fuzziness as a percentage.
    *
    * @return a String which ends with the '%' character (provided that this Fuzziness is relative).
    */
  def asPercentage: String

  /**
    * Determine the range +- t within which a deviation is considered within tolerance and where
    * l signifies the extent of the PDF.
    * In other words get the wiggle room.
    *
    * NOTE effectively, this method converts a Gaussian distribution into a Box distribution.
    * CONSIDER refactoring to take advantage of that equivalence.
    *
    * @param p the confidence we wish to have in the result: typical value: 0.5
    * @return the value of t at which the probability density is exactly transitions from likely to not likely.
    */
  def wiggle(p: Double): T
}

/**
  * Relative fuzziness.
  *
  * @param tolerance the error bound.
  * @param shape     the shape (Uniform, Gaussian, etc.)
  * @tparam T the underlying type of the fuzziness. Usually Double for fuzzy numerics [must have Valuable].
  */
case class RelativeFuzz[T: Valuable](tolerance: Double, shape: Shape) extends Fuzziness[T] {

  private val tv: Valuable[T] = implicitly[Valuable[T]]

  /**
    * Transform this Fuzziness[T] according to func.
    * Typically, func will be the derivative of the relevant Number function.
    *
    * @param func the function to apply to this Fuzziness[T].
    * @param t    the value of t (i.e. the input value) which will be passed into func.
    * @tparam U the underlying type of the result.
    * @tparam V the type of the quotient of T/U.
    * @return a transformed version of Fuzziness[U].
    */
  def transform[U: Valuable, V: Valuable](func: T => V)(t: T): Fuzziness[U] = {
    val duByDt: V = func(t)
    val dU: U = implicitly[Valuable[U]].multiply(implicitly[Valuable[T]].fromDouble(tolerance), duByDt)
    RelativeFuzz[U](implicitly[Valuable[U]].toDouble(dU), shape)
  }

  /**
    * This method takes a value of T on which to base a relative fuzz value.
    *
    * @param t the nominal value of the fuzzy number.
    * @return an optional RelativeFuzz[T]
    */
  def absolute(t: T): Option[AbsoluteFuzz[T]] = {
    // CONSIDER using multiply, scale, etc.
    Try(AbsoluteFuzz(tv.normalize(tv.times(tv.fromDouble(tolerance), t)), shape)).toOption
  }

  /**
    * Return either a AbsoluteFuzz equivalent or this, according to relative.
    *
    * @param t        the magnitude of the relevant Number.
    * @param relative if true then convert to RelativeFuzz otherwise wrap this in Some().
    * @return the (optional) Fuzziness as a Relative or Absolute Fuzziness, according to relative.
    */
  def normalize(t: T, relative: Boolean): Option[Fuzziness[T]] = if (relative) Some(this) else absolute(t)

  /**
    * Perform a convolution on this Fuzziness[T] with the given addend.
    * This operation is suitable for multiplication of Numbers.
    *
    * @param convolute   the convolute, which must have the same shape as this.
    * @param independent true if the distributions are independent.
    * @return the convolution of this and the convolute.
    */
  def *(convolute: Fuzziness[T], independent: Boolean): Fuzziness[T] = if (this.shape == convolute.shape)
    convolute match {
      case RelativeFuzz(t, Box) => RelativeFuzz(tolerance + t, shape)
      case RelativeFuzz(t, _) => RelativeFuzz(Gaussian.convolutionProduct(tolerance, t, independent), shape)
      case _ => throw FuzzyNumberException("* operation on different styles")
    }
  else
    throw FuzzyNumberException("* operation on different shapes")

  /**
    * Yield a Fuzziness[T] that is Gaussian (either this or derivative of this).
    */
  lazy val normalizeShape: Fuzziness[T] = shape match {
    case Gaussian => this
    case Box => RelativeFuzz(Box.toGaussianRelative(tolerance), Gaussian)
  }

  /**
    * Render this Fuzziness as with a given T value.
    *
    * @param t the T value.
    * @return a String which is the textual rendering of t with this Fuzziness applied.
    */
  def toString(t: T): (Boolean, String) = false -> asPercentage

  /**
    * A variation on toString where we render this relative Fuzziness as a percentage.
    *
    * @return a String which ends with the '%' character.
    */
  def asPercentage: String = {
    val df = new DecimalFormat("#.#")
    df.setMaximumFractionDigits(100)
    val result = df.format(tolerance * 100)
    val point = result.indexOf(".")
    val decimals = result.substring(point + 1, result.length)
    result.substring(0, point + 1) + decimals.substring(0, decimals.indexWhere(p => p != '0') + 2) + "%"
  }

  /**
    * Determine the range +- t within which a deviation is considered within tolerance and where
    * l signifies the extent of the PDF.
    * In other words get the wiggle room.
    *
    * @param p the confidence we wish to have in the result: typical value: 0.5
    * @return the value of t at which the probability density is exactly transitions from likely to not likely.
    */
  def wiggle(p: Double): T = tv.fromDouble(shape.wiggle(tolerance, p))

  /**
    * True.
    */
  val style: Boolean = true
}

/**
  * Absolute Fuzziness.
  *
  * @param magnitude the magnitude of the fuzz.
  * @param shape     the shape of the fuzz.
  * @tparam T the underlying type of the fuzziness. Usually Double for fuzzy numerics.
  */
case class AbsoluteFuzz[T: Valuable](magnitude: T, shape: Shape) extends Fuzziness[T] {
  private val tv = implicitly[Valuable[T]]

  /**
    * This method takes a value of T on which to base a relative fuzz value.
    * NOTE: if t is zero, we will return None, which corresponds to an Exact number.
    *
    * @param t the nominal value of the fuzzy number.
    * @return an optional RelativeFuzz[T]
    */
  def relative(t: T): Option[RelativeFuzz[T]] =
    Try(RelativeFuzz(tv.toDouble(tv.normalize(tv.div(magnitude, t))), shape)(tv)).toOption

  /**
    * Transform this Fuzziness[T] according to func.
    * Typically, func will be the derivative of the relevant Number function.
    *
    * @param func the function to apply to this Fuzziness[T].
    * @return an (optional) transformed version of Fuzziness[T].
    */
  def transform[U: Valuable, V: Valuable](func: T => V)(t: T): Fuzziness[U] = {
    val duByDt: V = func(t)
    val dU: U = implicitly[Valuable[U]].multiply(magnitude, duByDt)
    AbsoluteFuzz(dU, shape)
  }

  /**
    * Return either a RelativeFuzz equivalent or this, according to relativeStyle.
    *
    * @param t             the magnitude of the relevant Number.
    * @param relativeStyle if true then convert to Absolute otherwise wrap this in Some().
    * @return the (optional) Fuzziness as a Relative or Absolute Fuzziness, according to relative.
    */
  def normalize(t: T, relativeStyle: Boolean): Option[Fuzziness[T]] = if (relativeStyle) relative(t) else Some(this)

  /**
    * Perform a convolution on this Fuzziness[T] with the given addend.
    * There are two different Fuzziness[T] shapes, and they clearly have different effects.
    * This operation is suitable for addition of Numbers.
    *
    * @param convolute   the convolute, which must have the same shape as this.
    * @param independent ignored.
    * @return the convolution of this and the convolute.
    */
  def *(convolute: Fuzziness[T], independent: Boolean): Fuzziness[T] =
    if (this.shape == convolute.shape)
      convolute match {
        case AbsoluteFuzz(m, _) =>
          AbsoluteFuzz(tv.fromDouble(Gaussian.convolutionSum(tv.toDouble(magnitude), tv.toDouble(m))), shape)
        case _ =>
          throw FuzzyNumberException("* operation on different styles")
      }
    else
      throw FuzzyNumberException("* operation on different shapes")

  lazy val normalizeShape: Fuzziness[T] = shape match {
    case Gaussian => this
    case Box => AbsoluteFuzz(Box.toGaussianAbsolute(magnitude), Gaussian)
  }

  /**
    * Method to do accurate rounding of Double.
    *
    * @param x the double value to be rounded.
    * @param n the number of places to be rounded to.
    * @return the rounded value of x.
    */
  def round(x: Double, n: Int): Double = BigDecimal(BigDecimal(math.round(toDecimalPower(x, n)).toInt).bigDecimal.movePointLeft(n)).toDouble

  /**
    * Method to render this Fuzziness according to the nominal value t.
    * NOTE that we are actually embedding the fuzziness into the nominal value and returning that.
    *
    * CONSIDER cleaning this method up a bit.
    *
    * @param t a T value.
    * @return a tuple of a Boolean (indicating if the value is embedded in the result) and a String which is the textual rendering of t with this Fuzziness applied.
    */
  def toString(t: T): (Boolean, String) = {
    val eString = tv.render(t) match {
      case AbsoluteFuzz.numberR(e) => e
      case _ => noExponent
    }
    val exponent = Integer.parseInt(eString)
    val scientificSuffix = eString match {
      case `noExponent` => ""
      case x => s"E$x"
    }
    val scaledM = toDecimalPower(tv.toDouble(magnitude), -exponent)
    val d = math.log10(scaledM).toInt
    val roundedM = round(scaledM, 2 - d)
    //      if (scaledM > 0.01) // TODO let's do this unusual adjustment later
    val scaledT = tv.scale(t, toDecimalPower(1, -exponent))
    val q = f"$roundedM%.99f".substring(2) // XXX drop the "0."
    val (qPrefix, qSuffix) = q.toCharArray.span(_ == '0')
    val (qPreSuffix, _) = qSuffix.span(_ != '0')
    val adjust = qPreSuffix.length - 2
    val mScaledAndRounded = toDecimalPower(round(scaledM, qPrefix.length + 2 + adjust), qPrefix.length)
    val yq = mScaledAndRounded.toString.substring(2).padTo(2 + adjust, '0').substring(0, 2 + adjust)
    val brackets = if (shape == Gaussian) "()" else "[]"
    // CONSIDER changing the padding "0" value to be "5".
    val mask = new String(qPrefix) + "0" * (2 + adjust) + brackets.head + yq + brackets.tail.head
    val (zPrefix, zSuffix) = tv.render(scaledT).toCharArray.span(_ != '.')
    true -> (new String(zPrefix) + "." + Fuzziness.zipStrings(new String(zSuffix).substring(1), mask) + scientificSuffix)
  }

  def asPercentage: String = "absolute fuzz cannot be shown as percentage"

  /**
    * Determine the range +- x within which a deviation is considered within tolerance and where
    * l signifies the extent of the PDF.
    * In other words get the wiggle room.
    *
    * @param p the confidence we wish to have in the result: typical value: 0.5
    * @return the value of x at which the probability density is exactly transitions from likely to not likely.
    */
  def wiggle(p: Double): T = tv.fromDouble(shape.wiggle(tv.toDouble(magnitude), p))

  /**
    * False.
    */
  val style: Boolean = false

  private val noExponent = "+00"
}

object AbsoluteFuzz {
  private val numberR = """-?\d+\.\d+E([\-+]?\d+)""".r
}

object Fuzziness {

  /**
    * Method to yield a transformation (i.e. a Fuzziness[T] => Fuzziness[T]) based on a scale constant k.
    *
    * TESTME
    *
    * @param k the scale constant.
    * @tparam T the underlying type.
    * @return a function which will transform a Fuzziness[T] into a Fuzziness[T].
    */
  def scaleTransform[T: Valuable](k: Double): Fuzziness[T] => Fuzziness[T] =
    tf => tf.transform(_ => implicitly[Valuable[T]].fromDouble(k))(implicitly[Valuable[T]].fromInt(1))

  /**
    * Method to represent an optional Fuzziness as a String.
    *
    * @param fo the fuzziness.
    * @return a String which either shows a percentage (ending in '%') or "".
    */
  def showPercentage(fo: Option[Fuzziness[Double]]): String = fo map (_.asPercentage) getOrElse ""

  /**
    * Scale the fuzz values by the two given coefficients.
    *
    * @param fuzz         the fuzz values (a Tuple), deltaX/X and deltaY/Y.
    * @param coefficients the coefficients (a Tuple): the coefficients of deltaX/X and deltaY/Y respectively.
    * @return the scaled fuzz values (a Tuple).
    */
  def applyCoefficients[T: Valuable](fuzz: (Option[Fuzziness[T]], Option[Fuzziness[T]]), coefficients: Option[(T, T)]): (Option[Fuzziness[T]], Option[Fuzziness[T]]) =
    coefficients match {
      case Some((a, b)) =>
        val f1o = fuzz._1 map scaleTransform(implicitly[Valuable[T]].toDouble(a))
        val f2o = fuzz._2 map scaleTransform(implicitly[Valuable[T]].toDouble(b))
        (f1o, f2o)
      case _ => fuzz
    }

  /**
    * Combine the fuzz values using a convolution.
    * The order of fuzz1 and fuzz2 is not significant.
    * Note that we normalize the style of each fuzz according to the value of relative.
    * Note also that we normalize the shape of each fuzz to ensure Gaussian, since we cannot combine Box shapes into Box
    * (we could combine Box shapes into trapezoids but who needs that?).
    *
    * @param t1       the magnitude of the first operand.
    * @param t2       the magnitude of the second operand.
    * @param relative true if we are multiplying, false if we are adding.
    * @param fuzz     a Tuple of the two optional Fuzziness values.
    * @tparam T the underlying type of the Fuzziness.
    * @return an Option of Fuzziness[T].
    */
  def combine[T](t1: T, t2: T, relative: Boolean, independent: Boolean)(fuzz: (Option[Fuzziness[T]], Option[Fuzziness[T]])): Option[Fuzziness[T]] = {
    val f1o = doNormalize(fuzz._1, t1, relative)
    val f2o = doNormalize(fuzz._2, t2, relative)
    (f1o, f2o) match {
      case (Some(f1), Some(f2)) if relative && f1.shape == Box && f2.shape == Box => Some(f1.*(f2, independent))
      case (Some(f1), Some(f2)) => Some(f1.normalizeShape.*(f2.normalizeShape, independent))
      case (Some(f1), None) => Some(f1)
      case (None, Some(f2)) => Some(f2)
      case _ => None
    }
  }

  /**
    * Map the fuzz value with a function (typically the derivative of the function being applied to the Fuzzy quantity).
    * Note that we normalize the style of each fuzz according to the value of relative.
    * Note also that we normalize the shape of each fuzz to ensure Gaussian, since we cannot combine Box shapes into Box
    * (we could combine Box shapes into trapezoids but who needs that?).
    *
    * @param t        the magnitude of the input Number.
    * @param u        the magnitude of the output Number.
    * @param relative true if we are multiplying, false if we are adding.
    * @param g        the function with which to transform the given Fuzziness value
    * @param fuzz     one of the (optional) Fuzziness values.
    * @tparam T the underlying type of the incoming Fuzziness.
    * @tparam U the underlying type of the resulting Fuzziness.
    * @tparam V the underlying type of the result divided by the input.
    * @return an Option of Fuzziness[T].
    */
  def map[T, U: Valuable, V: Valuable](t: T, u: U, relative: Boolean, g: T => V, fuzz: Option[Fuzziness[T]]): Option[Fuzziness[U]] = {
    val q: Option[Fuzziness[U]] = fuzz match {
      case Some(f1) => Some(f1.transform(g)(t))
      case _ => None
    }
    doNormalize(q, u, relative)
  }

  /**
    * This method creates fuzz based on the practical limitations of representations and functions in double-precision arithmetic.
    * See also the method doublePrecision.
    *
    * @param relativePrecision the approximate number of bits of additional imprecision caused by evaluating a function.
    * @return the approximate precision for a floating point operation, expressed in terms of RelativeFuzz.
    */
  def createFuzz(relativePrecision: Int): Fuzziness[Double] =
    RelativeFuzz[Double](DoublePrecisionTolerance * (1 << relativePrecision), Box)(ValuableDouble)

  /**
    * This is the (approximate) fuzziness caused in general by trying to represent numbers in double precision.
    * Of course, many numbers can be represented exactly by double-precision. But not all.
    *
    * @return a Fuzziness[Double].
    */
  def doublePrecision: Fuzziness[Double] = createFuzz(0)

  /**
    * Normalize the magnitude qualifier of the given fuzz according to relative.
    * If relative is true then the returned value will be a RelativeFuzz.
    * If relative is false then the returned value will be an AbsoluteFuzz.
    *
    * @param fuzz     the optional Fuzziness to work on.
    * @param t        the value of T that the fuzz IS or result SHOULD BE relative to.
    * @param relative if true then return optional relative fuzz, else absolute fuzz.
    * @tparam T the underlying type of the Fuzziness.
    * @return the optional Fuzziness value which is equivalent (or identical) to fuzz, according to the value of relative.
    */
  private def doNormalize[T](fuzz: Option[Fuzziness[T]], t: T, relative: Boolean): Option[Fuzziness[T]] =
    fuzz.flatMap(f => doNormalize(t, relative, f))

  def zipStrings(v: String, t: String): String = {
    val cCs = ((LazyList.from(v.toCharArray.toList) :++ LazyList.continually('0')) zip t.toCharArray.toList).toList
    val r: List[Char] = cCs map {
      case (a, b) => if (b == '0') a else b
    }
    new String(r.toArray)
  }

  /**
    * Apply a decimal exponent of n to x and return the new value.
    * CONSIDER using BigDecimal for more precision (see AbsoluteFuzz.round)
    *
    * @param x a Double.
    * @param n the size of the exponent.
    * @return the result.
    */
  def toDecimalPower(x: Double, n: Int): Double = x * math.pow(10, n)

  private def doNormalize[T](t: T, relative: Boolean, f: Fuzziness[T]) =
    f match {
      case a@AbsoluteFuzz(_, _) => if (relative) a.relative(t) else Some(f)
      case r@RelativeFuzz(_, _) => if (relative) Some(f) else r.absolute(t)
    }

  /**
    * Calculate the fuzziness for the result of a MonadicOperation.
    *
    * NOTE: the parameter x is not actually used in this method. It seems like it ought to be used for functionFuzz
    * but the values produced this way do seem to be correct.
    * It is possible that the values are correct for relative error bounds but maybe not for absolute error bounds.
    *
    * @param op   the monadic operation.
    * @param t    the magnitude of the input to the monadic operation.
    * @param x    the magnitude of the result of the monadic operation.
    * @param fuzz the (optional) fuzziness of input to the monadic operation.
    * @return the optional fuzziness for the result of the monadic operation.
    */
  def monadicFuzziness(op: MonadicOperation, t: Double, x: Double, fuzz: Option[Fuzziness[Double]]): Option[Fuzziness[Double]] = {
    // CONSIDER using map again (which itself uses transform) -- but be careful!
    // First, ensure that the fuzz we are given is relative.
    val relativeFuzz: Option[Fuzziness[Double]] = fuzz flatMap (_.normalize(t, relative = true))
    // Next, calculate the relative fuzziness of the result, according to the function being applied.
    val functionFuzz: Option[Fuzziness[Double]] = relativeFuzz map (_.transform(op.relativeFuzz)(t))
    // Finally, we calculate the precision loss (if any) occasioned by the actual implementation of the operation function itself.
    val operationFuzz = createFuzz(op.fuzz)
    // Combine the functionFuzz with the operationFuzz
    combine(t, t, relative = true, independent = true)((functionFuzz, Some(operationFuzz)))
  }

}

/**
  * Describes a probability density function for a continuous distribution.
  * NOTE: this isn't suitable for discrete distributions, obviously.
  *
  * CONSIDER: implement additional shapes, for example O(f(x)). This would be used for the Basel problem, for example.
  */
trait Shape {

  /**
    * Determine the range +- x within which a deviation is considered within tolerance.
    *
    * @param l the extent of the PDF (for example, the standard deviation, for a Gaussian).
    * @param p the confidence that we wish to place on the likelihood: typical value is 0.5.
    * @return the value of x at which the probability density is exactly transitions from likely to not likely.
    */
  def wiggle(l: Double, p: Double): Double
}

/**
  * Uniform probability density over a specific range, otherwise zero.
  */
case object Box extends Shape {
  /**
    * See, for example, https://www.unf.edu/~cwinton/html/cop4300/s09/class.notes/Distributions1.pdf
    */
  private val uniformToGaussian = 1.0 / math.sqrt(3)

  /**
    * This method is to simulate a uniform distribution
    * with a normal distribution.
    *
    * @param x half the length of the basis of the uniform distribution.
    * @return x/2 which will be used as the standard deviation.
    */
  def toGaussianRelative(x: Double): Double = x * uniformToGaussian

  /**
    * This method is to simulate a uniform distribution
    * with a normal distribution.
    *
    * @param t the magnitude of the Box distribution.
    * @return t/2 which will be used as the standard deviation.
    */
  def toGaussianAbsolute[T: Valuable](t: T): T = implicitly[Valuable[T]].scale(t, uniformToGaussian)

  /**
    * Determine the range +- x within which a deviation is considered within tolerance and where
    * l signifies the extent of the PDF.
    *
    * @param l the half-width of a Box.
    * @param p the confidence that we wish to place on the likelihood: typical value is 0.5.
    *          Unless is is either 0 or 1, the actual p value is ignored.
    * @return the value of x at which the probability density transitions from possible to impossible.
    */
  def wiggle(l: Double, p: Double): Double = p match {
    case 0.0 => Double.PositiveInfinity
    case 1.0 => 0
    case _ => l / 2
  }

}

/**
  * A "normal" probability distribution function.
  */
case object Gaussian extends Shape {
  /**
    * Get the convolution of sum of two Gaussian distributions.
    *
    * This follows the fact that Var(X + Y) = Var(X) + Var(Y)
    *
    * See https://en.wikipedia.org/wiki/Variance
    *
    * @param sigma1 the first standard deviation.
    * @param sigma2 the second standard deviation.
    * @return a double which is the root mean square of the sigmas.
    */
  def convolutionSum(sigma1: Double, sigma2: Double): Double = math.sqrt(sigma1 * sigma1 + sigma2 * sigma2)

  /**
    * For the convolution of the product of two (independent) Gaussian distributions (see https://en.wikipedia.org/wiki/Variance).
    * You must not use this expression when multiplying a fuzzy number by itself, for example, because then they are not independent.
    *
    * Var(X Y) = mux mux Var(Y) + muy muy Var(X) + Var(X) Var(Y)
    *
    * Therefore, (sigma(x*y))**2 = (mux sigma(y))**2 + (muy sigma(x))**2 + (sigma(x) sigma(y))**2
    *
    * Or, dividing by (mux muy)**2 (i.e. using relative variations)
    *
    * sigma(x/mux 8 y/muy)**2 = sigma(x/mux)**2 + sigma(y/muy)**2 + (sigma(x/mux) sigma(y/muy))**2
    *
    * @param sigma1      the first standard deviation.
    * @param sigma2      the second standard deviation.
    * @param independent whether or not the distributions are independent.
    * @return
    */
  def convolutionProduct(sigma1: Double, sigma2: Double, independent: Boolean): Double =
    if (independent) math.sqrt(sigma1 * sigma1 + sigma2 * sigma2 + sigma1 * sigma2)
    else sigma1 + sigma2

  /**
    * Determine the "wiggle room" for a particular probability of confidence,
    * i.e. the range +- x within which a deviation is considered within tolerance and where l signifies the extent of the PDF.
    *
    * This is based on the inverse Error function (see https://en.wikipedia.org/wiki/Normal_distribution#Cumulative_distribution_function).
    *
    * @param l the standard deviation.
    * @param p the confidence desired for the likelihood.
    * @return the value of x such that p iw the probability of a random number x (with mean 0, and variance 1/2) falling between -x and x.
    */
  def wiggle(l: Double, p: Double): Double = l / sigma * erfInv(1 - p)

  /**
    * The standard deviation of a normals distribution whose variance is 1/2.
    * This is the basis of the inverse error function.
    */
  val sigma: Double = math.sqrt(0.5)
}

/**
  * Trait which models the behavior of something with (maybe) fuzziness.
  *
  * See also related trait Fuzzy[X] but note that there, the parametric type X
  * corresponds to the quantity itself, not its fuzziness.
  *
  * @tparam T the type of fuzziness.
  */
trait Fuzz[T] {
  /**
    * The (optional) fuzziness: if None, then there is no fuzziness.
    */
  val fuzz: Option[Fuzziness[T]]
}

/**
  * Type class trait Valuable[T].
  *
  * This is used to represent quantities of different underlying units/dimensions.
  *
  * @tparam T the underlying type of this Valuable.
  */
trait Valuable[T] extends Fractional[T] {
  /**
    * Method to return a String representation of a T value.
    *
    * @param t a T value.
    * @return the corresponding String.
    */
  def render(t: T): String

  /**
    * Method to yield a T from a Double.
    *
    * @param x a Double.
    * @return the corresponding value of T.
    */
  def fromDouble(x: Double): T

  /**
    * Method to scale a T value, according to a constant.
    *
    * This is essentially the inverse of the ratio method.
    *
    * @param t a T value.
    * @param f a factor (a Double, i.e. dimensionless).
    * @return a scaled value of T.
    */
  def scale(t: T, f: Double): T

  /**
    * Method to yield a "normalized" version of x.
    * For a Numeric object, this implies the absolute value, i.e. with no sign.
    *
    * @param x the value.
    * @return the value, without any sign.
    */
  def normalize(x: T): T

  /**
    * Method to yield the ratio of two T values.
    *
    * This is essentially the inverse of the scale method.
    *
    * TESTME
    *
    * @param t1 a T value.
    * @param t2 a T value.
    * @return t1/t2 as a Double.
    */
  def ratio(t1: T, t2: T): Double = toDouble(t1) / toDouble(t2)

  /**
    * Method to multiply a U by a V, resulting in a T.
    *
    * @param u a value of U.
    * @param v a value of V.
    * @tparam U the type of u.
    * @tparam V the type of v.
    * @return a T whose value is u * v.
    */
  def multiply[U: Valuable, V: Valuable](u: U, v: V): T = fromDouble(implicitly[Valuable[U]].toDouble(u) * implicitly[Valuable[V]].toDouble(v))

  /**
    * Method to divide a U by a V, resulting in a T.
    *
    * TESTME
    *
    * @param u a value of U.
    * @param v a value of V.
    * @tparam U the type of u.
    * @tparam V the type of v.
    * @return a T whose value is u / v.
    */
  def divide[U: Valuable, V: Valuable](u: U, v: V): T = fromDouble(implicitly[Valuable[U]].toDouble(u) / implicitly[Valuable[V]].toDouble(v))
}

trait ValuableDouble extends Valuable[Double] with DoubleIsFractional with Ordering.Double.IeeeOrdering {
  def render(t: Double): String = {
    lazy val asScientific: String = f"$t%.20E"
    val z = f"$t%.99f"
    val (prefix, suffix) = z.toCharArray.span(x => x != '.')
    val sevenZeroes = "0000000".toCharArray
    if (prefix.endsWith(sevenZeroes)) asScientific
    else if (suffix.tail.startsWith(sevenZeroes)) asScientific
    else z
  }

  def fromDouble(x: Double): Double = x

  /**
    * Scale the parameter x by the constant factor f.
    *
    * @param t a value.
    * @param f a factor.
    * @return x * f.
    */
  def scale(t: Double, f: Double): Double = t * f

  /**
    * Method to yield a "normalized" version of x.
    * For a Numeric object, this implies the absolute value, i.e. with no sign.
    *
    * @param x the value.
    * @return the value, without any sign.
    */
  def normalize(x: Double): Double = math.abs(x)
}

object Valuable {

  implicit object ValuableDouble extends ValuableDouble

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy