diff --git a/.gitignore b/.gitignore index 350e960c..a14af547 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ project/plugins/project/ .cache .classpath .project +/bin/ diff --git a/Figaro/META-INF/MANIFEST.MF b/Figaro/META-INF/MANIFEST.MF index d0965edd..d36215ec 100644 --- a/Figaro/META-INF/MANIFEST.MF +++ b/Figaro/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Figaro Bundle-SymbolicName: com.cra.figaro -Bundle-Version: 2.5.0 +Bundle-Version: 3.0.0 Export-Package: com.cra.figaro.algorithm, com.cra.figaro.algorithm.decision, com.cra.figaro.algorithm.decision.index, diff --git a/Figaro/figaro_build.properties b/Figaro/figaro_build.properties index f3516d50..a0bb9ee9 100644 --- a/Figaro/figaro_build.properties +++ b/Figaro/figaro_build.properties @@ -1 +1 @@ -version=2.5.0.0 +version=3.0.0.0 diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/Abstraction.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/Abstraction.scala index b8220834..08395683 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/Abstraction.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/Abstraction.scala @@ -1,5 +1,5 @@ /* - * Abstraction + * Abstraction.scala * Abstractions of elements to a smaller set of values. * * Created By: Avi Pfeffer (apfeffer@cra.com) @@ -29,7 +29,7 @@ import scala.language.postfixOps * argument is used to generate the sample concrete points from which the abstract points are selected, in * case the full set of concrete points cannot be generated (e.g., for continuous elements). The value of * this argument is multiplied by the number of abstract points to determine the total number of concrete - * points. The default value for this argument is 10.This is indicated by attaching an Abstraction to the + * points. The default value for this argument is 10. This is indicated by attaching an Abstraction to the * element. */ diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala index 84b14270..e885a1bb 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala @@ -1,13 +1,13 @@ /* * Anytime.scala * Anytime algorithms - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -56,7 +56,7 @@ trait Anytime extends Algorithm { def runStep(): Unit /** - * Optional function to run when the algorithm is stopped (not killed). Used in samplers to update lazy values + * Optional function to run when the algorithm is stopped (not killed). Used in samplers to update lazy values. */ def stopUpdate(): Unit = { } @@ -65,17 +65,17 @@ trait Anytime extends Algorithm { */ class Runner extends Actor { import context._ - + def active: Receive = { case Handle(service) => sender ! handle(service) - case "stop" => + case "stop" => stopUpdate() become (inactive) - case "next" => + case "next" => runStep() self ! "next" - case _ => + case _ => sender ! ExceptionResponse("Algorithm is still running") } @@ -86,23 +86,23 @@ trait Anytime extends Algorithm { runStep() become(active) self ! "next" - case "resume" => + case "resume" => resume() become(active) self ! "next" - case "kill" => + case "kill" => become(shuttingDown) - case _ => + case _ => sender ! ExceptionResponse("Algorithm is stopped") } - + def shuttingDown: Receive = { - case _ => + case _ => sender ! ExceptionResponse("Anytime algorithm has terminated") } - + def receive = inactive - + } @@ -116,7 +116,7 @@ trait Anytime extends Algorithm { } """) - + var system: ActorSystem = null var runner: ActorRef = null var running = false; @@ -126,16 +126,15 @@ trait Anytime extends Algorithm { */ def handle(service: Service): Response - + protected def doStart() = { if (!running) { system = ActorSystem("Anytime", ConfigFactory.load(customConf)) runner = system.actorOf(Props(new Runner)) initialize() -// println("Using ANYTIME") running = true } - + runner ! "start" } @@ -146,7 +145,10 @@ trait Anytime extends Algorithm { protected def doKill() = { shutdown } - + + /** + * Release all resources from this anytime algorithm. + */ def shutdown { cleanUp() if (running) @@ -155,6 +157,5 @@ trait Anytime extends Algorithm { system.stop(runner) system.shutdown } -// println("Shutdown ANYTIME") } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala index 177cc64b..29339a0f 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala @@ -35,7 +35,7 @@ trait AnytimeProbQuery extends ProbQueryAlgorithm with Anytime { */ case class Distribution[T](distribution: Stream[(Double, T)]) extends Response /** - * A message instructing the handler to compute the expectation of the target element under the given function + * A message instructing the handler to compute the expectation of the target element under the given function. */ case class ComputeExpectation[T](target: Element[T], function: T => Double) extends Service /** diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/LazyAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/LazyAlgorithm.scala index 644755a9..202488cf 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/LazyAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/LazyAlgorithm.scala @@ -1,30 +1,54 @@ /* * LazyAlgorithm.scala * Lazy algorithms. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Dec 28, 2013 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ package com.cra.figaro.algorithm +/** + * A lazy algorithm is an algorithm that can be run to increasing depths. + */ trait LazyAlgorithm extends Algorithm { + /** + * The current depth to which the algorithm should be run. + */ var depth = 0 - + + /** + * Run the algorithm to the given depth. + */ def run(depth: Int): Unit - + + /** + * Start the algorithm. This will run the algorithm to one depth. + */ def doStart() { pump() } - + + /** + * Increase the depth and run the algorithm again. + */ def pump() { depth += 1; run(depth) } - + + /** + * Stop the algorithm. + */ def doStop() { } - + + /** + * Resume the algorithm by increasing the depth and running again. + */ def doResume() { pump() } - + + /** + * Kill the algorithm. + */ def doKill() { depth = -1 } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/ParameterLearner.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/ParameterLearner.scala index 3cb41e4b..431e856a 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/ParameterLearner.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/ParameterLearner.scala @@ -14,7 +14,7 @@ package com.cra.figaro.algorithm /** - * Trait of algorithms that learn parameters + * Trait of algorithms that learn parameters. */ trait ParameterLearner { diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbEvidenceAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbEvidenceAlgorithm.scala index e8cd646a..2d9575ff 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbEvidenceAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbEvidenceAlgorithm.scala @@ -50,7 +50,7 @@ trait ProbEvidenceAlgorithm extends Algorithm { } /** - * The computed probability of evidence + * The computed probability of evidence. */ def probEvidence: Double = { if (!active) throw new AlgorithmInactiveException @@ -58,7 +58,7 @@ trait ProbEvidenceAlgorithm extends Algorithm { } /** - * The computed log probability of evidence + * The computed log probability of evidence. */ def logProbEvidence: Double = { if (!active) throw new AlgorithmInactiveException diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala index 8eb81494..68c302c0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala @@ -94,14 +94,14 @@ trait ProbQueryAlgorithm } /** - * Return the mean of the probability density function for the given continuous element + * Return the mean of the probability density function for the given continuous element. */ def mean(target: Element[Double]): Double = { expectation(target, (d: Double) => d) } /** - * Return the variance of the probability density function for the given continuous element + * Return the variance of the probability density function for the given continuous element. */ def variance(target: Element[Double]): Double = { val m = mean(target) @@ -110,7 +110,7 @@ trait ProbQueryAlgorithm } /** - * Return an element representing the posterior probability distribution of the given element + * Return an element representing the posterior probability distribution of the given element. */ def posteriorElement[T](target: Element[T], universe: Universe = Universe.universe): Element[T] = { Select(distribution(target).toList:_*)("", universe) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionAlgorithm.scala index 4156bd28..8d48ab65 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionAlgorithm.scala @@ -21,7 +21,7 @@ import scala.collection.immutable.Map /** * Trait that defines some common interface functions for decision algorithms. - * Every decision algorithm must define the function computeUtility() + * Every decision algorithm must define the function computeUtility(). */ trait DecisionAlgorithm[T, U] extends Algorithm { @@ -38,24 +38,24 @@ trait DecisionAlgorithm[T, U] extends Algorithm { private lazy val util = computeUtility() /** - * Get the total utility and weight for all sampled values of the parent and decision + * Get the total utility and weight for all sampled values of the parent and decision. */ def getUtility() = util /** - * Get the total utility and weight for a specific value of a parent and decision + * Get the total utility and weight for a specific value of a parent and decision. */ def getUtility(p: T, d: U) = util((p, d)) /** * Sets the policy for the given decision. This will get the computed utility of the algorithm * and call setPolicy on the decision. Note there is no error checking here, so the decision in - * the argument must match the target decision in the algorithm + * the argument must match the target decision in the algorithm. */ def setPolicy(e: Decision[T, U]): Unit = e.setPolicy(getUtility()) } /** - * Trait for one time Decision Algorithms + * Trait for one time Decision Algorithms. */ trait OneTimeProbQueryDecision[T, U] extends OneTimeProbQuery with DecisionAlgorithm[T, U] diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionImportance.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionImportance.scala index cdfb09f8..da6e6c44 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionImportance.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionImportance.scala @@ -29,7 +29,7 @@ import scala.collection.mutable.{ Set, Map } */ /** * Importance sampling for decisions. Almost the exact same as normal importance sampling except that it keeps - * track of utilities and probabilities (to compute expected utility) and it implements DecisionAlgorithm trait + * track of utilities and probabilities (to compute expected utility) and it implements DecisionAlgorithm trait. */ abstract class DecisionImportance[T, U] private (override val universe: Universe, utilityNodes: List[Element[_]], decisionTarget: Decision[T, U], @@ -45,7 +45,7 @@ abstract class DecisionImportance[T, U] private (override val universe: Universe private def utilitySum = (0.0 /: utilityNodes)((s: Double, n: Element[_]) => s + n.value.asInstanceOf[Double]) /** - * Cleans up the temporary elements created during sampling + * Cleans up the temporary elements created during sampling. */ def cleanup() = universe.deactivate(queryTargets) @@ -86,7 +86,7 @@ abstract class DecisionImportance[T, U] private (override val universe: Universe * * For decisions, our weight is actually the weight of the sampled state times the sum of the utility nodes. This will be * used as the "weight" in the weighted sampler, ie, we are accumulating the expected utility of each state. Note that the weights - * will not be normalized, but that is ok since strategies are an optimization and everything will be divided by a constant + * will not be normalized, but that is ok since strategies are an optimization and everything will be divided by a constant. * */ @tailrec final def sample(): Sample = { diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionMetropolisHastings.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionMetropolisHastings.scala index 3fa2fe55..87ed965f 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionMetropolisHastings.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionMetropolisHastings.scala @@ -88,32 +88,29 @@ abstract class DecisionMetropolisHastings[T, U] private (universe: Universe, pro * We keep track of the improvement in the constraints for the new proposal compared to the original value. * We keep track of which elements do not have their condition satisfied by the new proposal. */ -private def attemptChange[T](state: State, elem: Element[T]): State = { -val newValue = elem.generateValue(elem.randomness) -// if an old value is already stored, don't overwrite it -val newOldValues = -if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) -if (elem.value != newValue) { -//val newProb = -// state.modelProb * elem.score(elem.value, newValue) -val newProb = state.modelProb -val newDissatisfied = -if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem -elem.value = newValue -State(newOldValues, state.oldRandomness, state.proposalProb, newProb, newDissatisfied) -} else { -// We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, -// even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set -// when deciding whether to accept the proposal. -val newDissatisfied = -if (elem.condition(newValue)) { -state.dissatisfied - elem -} else { -state.dissatisfied + elem -} -State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) -} -} + private def attemptChange[T](state: State, elem: Element[T]): State = { + val newValue = elem.generateValue(elem.randomness) + // if an old value is already stored, don't overwrite it + val newOldValues = + if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) + if (elem.value != newValue) { + val newDissatisfied = + if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem + elem.value = newValue + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } else { + // We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, + // even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set + // when deciding whether to accept the proposal. + val newDissatisfied = + if (elem.condition(newValue)) { + state.dissatisfied - elem + } else { + state.dissatisfied + elem + } + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } + } private def propose[T](state: State, elem: Element[T]): State = { if (debug) println("Proposing " + elem.name.string) @@ -227,30 +224,45 @@ State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, ne result } -def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { + /* + * Update a set of elements after a proposal given the current state. + * Since elements must be updated in generative order, we have to ensure the arguments + * of an element are updated being an element itself. To do that, we check the intersection + * of the an element's arguments with the elements that still need to be updated. If the intersection + * is not empty, we recursively update the intersecting elements. Once those updates are completed, + * we update an element and move on to the next element in the set. + */ + private def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { var returnState = state var updatesLeft = toUpdate - while (!updatesLeft.isEmpty){ - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - while (!argsRemaining.isEmpty) { - returnState = updateManyHelper(returnState, argsRemaining.toSet) - argsRemaining = argsRemaining.tail + while (!updatesLeft.isEmpty) { + // Check the intersection of an element's arguments with the updates that still need to occur + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + while (!argsRemaining.isEmpty) { + // update the element's arguments first + returnState = updateManyHelper(returnState, argsRemaining.toSet) + argsRemaining = argsRemaining.tail } - returnState = updateOne(returnState, updatesLeft.head) + // once the args are updated, update this element + returnState = updateOne(returnState, updatesLeft.head) updatesLeft = updatesLeft.tail - } - returnState + } + returnState } - + + /* + * A recursive function to work in conjuction with updateMany to check the order of the element + * updates. + */ @tailrec private def updateManyHelper(state: State, toUpdate: Set[Element[_]]): State = { - var returnState = state - var updatesLeft = toUpdate - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - if (argsRemaining.isEmpty){ - returnState = updateOne(returnState, updatesLeft.head) - returnState } - else { updateManyHelper(returnState, argsRemaining.toSet) } + var returnState = state + var updatesLeft = toUpdate + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + if (argsRemaining.isEmpty) { + returnState = updateOne(returnState, updatesLeft.head) + returnState + } else { updateManyHelper(returnState, argsRemaining.toSet) } } /* @@ -352,7 +364,7 @@ def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { protected override def update(): Unit = { super.update sampleCount += 1 - allUtilitiesSeen foreach (updateWeightSeenForTarget((utilitySum, Map[Element[_], Any](dummyTarget -> dummyTarget.value)), _)) + allUtilitiesSeen foreach (updateWeightSeenForTarget((utilitySum, Map[Element[_], Any](dummyTarget -> dummyTarget.value)), _)) sampleCount -= 1 } @@ -454,7 +466,6 @@ object DecisionMetropolisHastings { u.value match { case d: Double => 1 case _ => { - println(u.value) throw new IllegalArgumentException("Only double utilities are allowed") } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionPolicy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionPolicy.scala index c15d064f..47d44369 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionPolicy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionPolicy.scala @@ -27,7 +27,7 @@ import com.cra.figaro.algorithm.lazyfactored.Regular /** * Abstract base class for all Decision Policies. Must define two functions: * toFcn: T => Element[U] - this is the function that is called to - * compute the decision for a parent value + * compute the decision for a parent value. * * toUtility: T => Element[Double] - this returns the expected utility of the * decision for a parent value. Used in backward induction algorithm. @@ -39,12 +39,12 @@ trait DecisionPolicy[T, U] { /* All implemented Decision policies must define these two functions */ /** - * The function that returns a decision (Element[U]) given the value of the parent T + * The function that returns a decision (Element[U]) given the value of the parent T. */ def toFcn(): T => Element[U] /** - * The function that returns the expected utility (Element[Double]) given the value of the parent T + * The function that returns the expected utility (Element[Double]) given the value of the parent T. */ def toUtility(): T => Element[Double] } @@ -56,7 +56,7 @@ object DecisionPolicy { /** * Computes the maximal decision from a set of (decision, utility) samples, * returned from a nearest neighbor algorithm. It is unweighted because the - * samples are NOT weighted by their distance from the parent value + * samples are NOT weighted by their distance from the parent value. */ def UWMAX[U](nn: List[(Double, U, DecisionSample)]): (U, Double) = { val decisions = scala.collection.mutable.Map[U, DecisionSample]() @@ -73,7 +73,7 @@ object DecisionPolicy { * Computes the maximal decision from a set of (decision, utility) samples, * returned from the nearest neighbor algorithm. This method IS weighted. Samples * who's parent value closer to the query parent value are weighted more when - * computing the expected value + * computing the expected value. */ def WMAX[U](nn: List[(Double, U, DecisionSample)]): (U, Double) = { val decisions = scala.collection.mutable.Map[U, DecisionSample]() @@ -128,7 +128,7 @@ class DecisionPolicyNN[T <% Distance[T], U](D: Index[T, U], combineFcn: (List[(D /** * Returns the number of nearest neighbors to use. If kNN is greater than 1, then return kNN. If kNN is less than - * 1, then return kNN* Number of Samples + * 1, then return kNN* Number of Samples. */ def getNumNNSamples = if (numNNSamples >= 1) { numNNSamples.toInt @@ -167,14 +167,14 @@ object DecisionPolicyExact { } /** - * Create an exact decision policy from a Map of (parent, decision) tuples to a DecisionSample + * Create an exact decision policy from a Map of (parent, decision) tuples to a DecisionSample. */ def apply[T, U](policy: Map[(T, U), DecisionSample]) = { new DecisionPolicyExact(maxPolicy(policy)) } /** - * Create an exact decision policy from a DecisionAlgorithm + * Create an exact decision policy from a DecisionAlgorithm. */ def apply[T, U](Alg: DecisionAlgorithm[T, U]) = { val policy_t = Alg.getUtility() @@ -207,7 +207,7 @@ object DecisionPolicyNN { /** * Create an approximate decision policy from a DecisionAlgorithm, using the supplied combination function - * and kNN. This uses the default index (VP-Tree) + * and kNN. This uses the default index (VP-Tree). * */ def apply[T <% Distance[T], U](Alg: DecisionAlgorithm[T, U], @@ -219,7 +219,7 @@ object DecisionPolicyNN { /** * Create an approximate decision policy from a Map of (parent, decision) tuples to a DecisionSample, - * using the supplied combination function and kNN. This uses the default index (VP-Tree) + * using the supplied combination function and kNN. This uses the default index (VP-Tree). * */ def apply[T <% Distance[T], U](policy: Map[(T, U), DecisionSample], diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionVariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionVariableElimination.scala index c5b4be50..7aef3d98 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionVariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/DecisionVariableElimination.scala @@ -1,272 +1,274 @@ -/* - * DecisionVariableElimination.scala - * Variable elimination for Decisions algorithm. - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Oct 1, 2012 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.decision - -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.sampling._ -import com.cra.figaro.language._ -import com.cra.figaro.library.decision._ -import com.cra.figaro.util._ -import com.cra.figaro.algorithm.lazyfactored.Extended -import annotation.tailrec -import scala.collection.mutable.{ Map, Set } -import scala.language.existentials - -/* Trait only extends for double utilities. User needs to provide another trait or convert utilities to double - * in order to use - * - */ -/** - * Trait for Decision based Variable Elimination. This implementation is hardcoded to use - * Double utilities - */ -trait ProbabilisticVariableEliminationDecision extends VariableElimination[(Double, Double)] { - /** Retrieve utility nodes in the model - */ - /* Implementations must define this */ - def getUtilityNodes: List[Element[_]] - - /** - * Semiring for Decisions uses a sum-product-utility semiring - */ - override val semiring = SumProductUtilitySemiring - - /** - * Makes a utility factor an element designated as a utility. This is factor of a tuple (Double, Double) - * where the first value is 1.0 and the second is a possible utility of the element - */ - def makeUtilFactor(e: Element[_]): Factor[(Double, Double)] = { - val f = Factory.make[(Double, Double)](List(Variable(e))) - f.fillByRule((l: List[Any]) => (1.0, l.asInstanceOf[List[Extended[Double]]](0).value)) - f - } - - - /* Even though utility nodes are eliminated, we need to create factors for them and anything they use. */ - override def starterElements = getUtilityNodes ::: targetElements - - /** - * Create the factors for decision factors. Each factor is hardcoded as a tuple of (Double, Double), - * where the first value is the probability and the second is the utility. - */ - def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[(Double, Double)]] = { - if (debug) { - println("Elements (other than utilities) appearing in factors and their ranges:") - for { element <- neededElements } { - println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) - } - } - - Factory.removeFactors() - val thisUniverseFactorsExceptUtil = neededElements flatMap (Factory.make(_)) - // Make special utility factors for utility elements - val thisUniverseFactorsUtil = getUtilityNodes map (makeUtilFactor(_)) - - val dependentUniverseFactors = - for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) - - // Convert all non-utility factors from standard factors to decision factors, ie, factors are now tuples of (Double, _) - val thisUniverseFactorsExceptUtil_conv = thisUniverseFactorsExceptUtil.map(s => convert(s, false)) - val thisUniverseFactorsUtil_conv = thisUniverseFactorsUtil - val dependentUniverseFactors_conv = dependentUniverseFactors.map(s => convert(s, false)) - - dependentUniverseFactors_conv ::: thisUniverseFactorsExceptUtil_conv ::: thisUniverseFactorsUtil_conv - } - - /* - * Converts a factor created by ProbFactor into a tuple of (Prob, E[Utility]), where E[Utility] is zero for - * all non-utility nodes, and Prob is 1 for all utility nodes - */ - private def convert(f: Factor[Double], utility: Boolean): Factor[(Double, Double)] = { - val factor = Factory.make[(Double, Double)](f.variables) - val allIndices = f.allIndices - - allIndices.foreach { k: List[Int] => - val p = f.get(k) - val v = if (utility) { - if (f.variables.length > 1) throw new IllegalUtilityNodeException - f.variables(0).range(k(0)).asInstanceOf[Double] - } else { - 0.0 - } - factor.set(k, (p, v)) - } - factor - } - -} - -/** - * Decision VariableElimination algorithm that computes the expected utility of decision elements using the default - * elimination order. - */ -class ProbQueryVariableEliminationDecision[T, U](override val universe: Universe, utilityNodes: List[Element[_]], target: Element[_])( - val showTiming: Boolean, - val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) - extends OneTimeProbQuery - with ProbabilisticVariableEliminationDecision - with DecisionAlgorithm[T, U] { - lazy val queryTargets = List(target) - - /** - * The variable elimination eliminates all variables except on all decision nodes and their parents. - * Thus the target elements is both the decision element and the parent element - */ - val targetElements = List(target, target.args(0)) - - def getUtilityNodes = utilityNodes - - private var finalFactors: Factor[(Double, Double)] = Factory.make[(Double, Double)](List[Variable[_]]()) - - /* Marginalizes the final factor using the semiring for decisions - * - */ - private def marginalizeToTarget(factor: Factor[(Double, Double)], target: Element[_]): Unit = { - val unnormalizedTargetFactor = factor.marginalizeTo(semiring, Variable(target)) - val z = unnormalizedTargetFactor.foldLeft(semiring.zero, (x: (Double, Double), y: (Double, Double)) => (x._1 + y._1, 0.0)) - //val targetFactor = Factory.make[(Double, Double)](unnormalizedTargetFactor.variables) - val targetFactor = unnormalizedTargetFactor.mapTo((d: (Double, Double)) => (d._1 / z._1, d._2), unnormalizedTargetFactor.variables) - targetFactors += target -> targetFactor - } - - private def marginalize(resultFactor: Factor[(Double, Double)]) = - queryTargets foreach (marginalizeToTarget(resultFactor, _)) - - private def makeResultFactor(factorsAfterElimination: Set[Factor[(Double, Double)]]): Factor[(Double, Double)] = { - // It is possible that there are no factors (this will happen if there are no decisions or utilities). - // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. - factorsAfterElimination.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - } - - def finish(factorsAfterElimination: Set[Factor[(Double, Double)]], eliminationOrder: List[Variable[_]]) = - finalFactors = makeResultFactor(factorsAfterElimination) - - /** - * Returns distribution of the target, ignoring utilities - */ - def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = { - val factor = targetFactors(target) - val targetVar = Variable(target) - val dist = targetVar.range.filter(_.isRegular).map(_.value).zipWithIndex map (pair => (factor.get(List(pair._2))._1, pair._1)) - // normalization is unnecessary here because it is done in marginalizeTo - dist.toStream - } - - /** - * Returns expectation of the target, ignoring utilities - */ - def computeExpectation[T](target: Element[T], function: T => Double): Double = { - def get(pair: (Double, T)) = pair._1 * function(pair._2) - (0.0 /: computeDistribution(target))(_ + get(_)) - } - - /** - * Returns the computed utility of all parent/decision tuple values. For VE, these are not samples - * but the actual computed expected utility for all combinations of the parent and decision - */ - def computeUtility(): scala.collection.immutable.Map[(T, U), DecisionSample] = computeStrategy(finalFactors) - - /* - * Converts the final factor into a map of parent/decision values and expected utility - */ - private def computeStrategy(factor: Factor[(Double, Double)]) = { - val strat = Map[(T, U), DecisionSample]() - - //find the variable associated with the decision - val decisionVariable = factor.variables.filter(_.asInstanceOf[ElementVariable[_]].element == target)(0) - - // find the variables of the parents. - val parentVariable = factor.variables.filterNot(_ == decisionVariable)(0) - - // index of the decision variable - - val indexOfDecision = indices(factor.variables, decisionVariable) - val indexOParent = indices(factor.variables, parentVariable) - - for { indices <- factor.allIndices } { - - /* for each index in the list of indices, strip out the decision variable index, - * and retrieve the map entry for the parents. If the factor value is greater than - * what is currently stored in the strategy map, replace the decision with the new one from the factor - */ - val parent = parentVariable.range(indices(indexOParent(0))).value.asInstanceOf[T] - val decision = decisionVariable.range(indices(indexOfDecision(0))).value.asInstanceOf[U] - val utility = factor.get(indices)._2 - strat += (parent, decision) -> DecisionSample(utility, 1.0) - - } - strat.toMap - - } - -} - -object DecisionVariableElimination { - - /* Checks conditions of Decision Usage - * 1. Double utilities - */ - private[decision] def usageCheck(utilityNodes: List[Element[_]], target: Decision[_, _]): Unit = { - utilityNodes.foreach { u => - u.value match { - case d: Double => 1 - case _ => throw new IllegalArgumentException("Only double utilities are allowed") - } - } - } - - /** - * Create a decision variable elimination instance with the given decision variables and indicated utility - * nodes - */ - def apply[T, U](utilityNodes: List[Element[_]], target: Decision[T, U])(implicit universe: Universe) = { - utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check - usageCheck(utilityNodes, target) - new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( - false, - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) - } - /** - * Create a decision variable elimination algorithm with the given decision variables and indicated utility - * nodes and using the given dependent universes in the current default universe. - */ - def apply[T, U](dependentUniverses: List[(Universe, List[NamedEvidence[_]])], utilityNodes: List[Element[_]], target: Decision[T, U])(implicit universe: Universe) = { - utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check - usageCheck(utilityNodes, target) - new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( - false, - dependentUniverses, - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) - } - /** - * Create a decision variable elimination algorithm with the given decision variables and indicated utility - * nodes and using the given dependent universes in the current default universe. Use the given dependent - * algorithm function to determine the algorithm to use to compute probability of evidence in each dependent universe. - */ - def apply[T, U]( - dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, - utilityNodes: List[Element[_]], - target: Decision[T, U])(implicit universe: Universe) = { - utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check - usageCheck(utilityNodes, target) - new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( - false, - dependentUniverses, - dependentAlgorithm) - } -} +/* + * DecisionVariableElimination.scala + * Variable elimination for Decisions algorithm. + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 1, 2012 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.decision + +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.factory._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.language._ +import com.cra.figaro.library.decision._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.lazyfactored.Extended +import annotation.tailrec +import scala.collection.mutable.{ Map, Set } +import scala.language.existentials + +/* Trait only extends for double utilities. User needs to provide another trait or convert utilities to double + * in order to use + * + */ +/** + * Trait for Decision based Variable Elimination. This implementation is hardcoded to use. + * Double utilities. + */ +trait ProbabilisticVariableEliminationDecision extends VariableElimination[(Double, Double)] { + /** Retrieve utility nodes in the model + */ + /* Implementations must define this */ + def getUtilityNodes: List[Element[_]] + + /** + * Semiring for Decisions uses a sum-product-utility semiring. + */ + override val semiring = SumProductUtilitySemiring + + /** + * Makes a utility factor an element designated as a utility. This is factor of a tuple (Double, Double) + * where the first value is 1.0 and the second is a possible utility of the element. + */ + def makeUtilFactor(e: Element[_]): Factor[(Double, Double)] = { + val f = Factory.defaultFactor[(Double, Double)](List(), List(Variable(e))) + f.fillByRule((l: List[Any]) => (1.0, l.asInstanceOf[List[Extended[Double]]](0).value)) + f + } + + + /* Even though utility nodes are eliminated, we need to create factors for them and anything they use. */ + override def starterElements = getUtilityNodes ::: targetElements + + /** + * Create the factors for decision factors. Each factor is hardcoded as a tuple of (Double, Double), + * where the first value is the probability and the second is the utility. + */ + def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[(Double, Double)]] = { + if (debug) { + println("Elements (other than utilities) appearing in factors and their ranges:") + for { element <- neededElements } { + println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) + } + } + + Factory.removeFactors() + val thisUniverseFactorsExceptUtil = neededElements flatMap (Factory.make(_)) + // Make special utility factors for utility elements + val thisUniverseFactorsUtil = getUtilityNodes map (makeUtilFactor(_)) + + val dependentUniverseFactors = + for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) + + // Convert all non-utility factors from standard factors to decision factors, ie, factors are now tuples of (Double, _) + val thisUniverseFactorsExceptUtil_conv = thisUniverseFactorsExceptUtil.map(s => convert(s, false)) + val thisUniverseFactorsUtil_conv = thisUniverseFactorsUtil + val dependentUniverseFactors_conv = dependentUniverseFactors.map(s => convert(s, false)) + + dependentUniverseFactors_conv ::: thisUniverseFactorsExceptUtil_conv ::: thisUniverseFactorsUtil_conv + } + + /* + * Converts a factor created by ProbFactor into a tuple of (Prob, E[Utility]), where E[Utility] is zero for + * all non-utility nodes, and Prob is 1 for all utility nodes + */ + private def convert(f: Factor[Double], utility: Boolean): Factor[(Double, Double)] = { + val factor = Factory.defaultFactor[(Double, Double)](f.parents, f.output) + val allIndices = f.allIndices + + allIndices.foreach { k: List[Int] => + val p = f.get(k) + val v = if (utility) { + if (f.variables.length > 1) throw new IllegalUtilityNodeException + f.variables(0).range(k(0)).asInstanceOf[Double] + } else { + 0.0 + } + factor.set(k, (p, v)) + } + factor + } + +} + +/** + * Decision VariableElimination algorithm that computes the expected utility of decision elements using the default + * elimination order. + */ +class ProbQueryVariableEliminationDecision[T, U](override val universe: Universe, utilityNodes: List[Element[_]], target: Element[_])( + val showTiming: Boolean, + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) + extends OneTimeProbQuery + with ProbabilisticVariableEliminationDecision + with DecisionAlgorithm[T, U] { + lazy val queryTargets = List(target) + + /** + * The variable elimination eliminates all variables except on all decision nodes and their parents. + * Thus the target elements is both the decision element and the parent element. + */ + val targetElements = List(target, target.args(0)) + + def getUtilityNodes = utilityNodes + + private var finalFactors: Factor[(Double, Double)] = Factory.defaultFactor[(Double, Double)](List(), List()) + + /* Marginalizes the final factor using the semiring for decisions + * + */ + private def marginalizeToTarget(factor: Factor[(Double, Double)], target: Element[_]): Unit = { + val unnormalizedTargetFactor = factor.marginalizeTo(semiring, Variable(target)) + val z = unnormalizedTargetFactor.foldLeft(semiring.zero, (x: (Double, Double), y: (Double, Double)) => (x._1 + y._1, 0.0)) + //val targetFactor = Factory.make[(Double, Double)](unnormalizedTargetFactor.variables) + val targetFactor = unnormalizedTargetFactor.mapTo((d: (Double, Double)) => (d._1 / z._1, d._2)) + targetFactors += target -> targetFactor + } + + private def marginalize(resultFactor: Factor[(Double, Double)]) = + queryTargets foreach (marginalizeToTarget(resultFactor, _)) + + private def makeResultFactor(factorsAfterElimination: Set[Factor[(Double, Double)]]): Factor[(Double, Double)] = { + // It is possible that there are no factors (this will happen if there are no decisions or utilities). + // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. + factorsAfterElimination.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + } + + def finish(factorsAfterElimination: Set[Factor[(Double, Double)]], eliminationOrder: List[Variable[_]]) = + finalFactors = makeResultFactor(factorsAfterElimination) + + /** + * Returns distribution of the target, ignoring utilities. + */ + def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = { + val factor = targetFactors(target) + val targetVar = Variable(target) + val dist = targetVar.range.filter(_.isRegular).map(_.value).zipWithIndex map (pair => (factor.get(List(pair._2))._1, pair._1)) + // normalization is unnecessary here because it is done in marginalizeTo + dist.toStream + } + + /** + * Returns expectation of the target, ignoring utilities + */ + def computeExpectation[T](target: Element[T], function: T => Double): Double = { + def get(pair: (Double, T)) = pair._1 * function(pair._2) + (0.0 /: computeDistribution(target))(_ + get(_)) + } + + /** + * Returns the computed utility of all parent/decision tuple values. For VE, these are not samples + * but the actual computed expected utility for all combinations of the parent and decision. + */ + def computeUtility(): scala.collection.immutable.Map[(T, U), DecisionSample] = computeStrategy(finalFactors) + + /* + * Converts the final factor into a map of parent/decision values and expected utility + */ + private def computeStrategy(factor: Factor[(Double, Double)]) = { + val strat = Map[(T, U), DecisionSample]() + + //find the variable associated with the decision + val decisionVariable = factor.variables.filter(_.asInstanceOf[ElementVariable[_]].element == target)(0) + + // find the variables of the parents. + val parentVariable = factor.variables.filterNot(_ == decisionVariable)(0) + + // index of the decision variable + + val indexOfDecision = indices(factor.variables, decisionVariable) + val indexOParent = indices(factor.variables, parentVariable) + + for { indices <- factor.allIndices } { + + /* for each index in the list of indices, strip out the decision variable index, + * and retrieve the map entry for the parents. If the factor value is greater than + * what is currently stored in the strategy map, replace the decision with the new one from the factor + */ + val parent = parentVariable.range(indices(indexOParent(0))).value.asInstanceOf[T] + val decision = decisionVariable.range(indices(indexOfDecision(0))).value.asInstanceOf[U] + val utility = factor.get(indices)._2 + strat += (parent, decision) -> DecisionSample(utility, 1.0) + + } + strat.toMap + + } + +} + +object DecisionVariableElimination { + + /* Checks conditions of Decision Usage + * 1. Double utilities + */ + private[decision] def usageCheck(utilityNodes: List[Element[_]], target: Decision[_, _]): Unit = { + utilityNodes.foreach { u => + u.value match { + case d: Double => 1 + case _ => throw new IllegalArgumentException("Only double utilities are allowed") + } + } + } + + /** + * Create a decision variable elimination instance with the given decision variables and indicated utility + * nodes. + */ + def apply[T, U](utilityNodes: List[Element[_]], target: Decision[T, U])(implicit universe: Universe) = { + utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check + usageCheck(utilityNodes, target) + new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( + false, + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + } + /** + * Create a decision variable elimination algorithm with the given decision variables and indicated utility + * nodes and using the given dependent universes in the current default universe. + */ + def apply[T, U](dependentUniverses: List[(Universe, List[NamedEvidence[_]])], utilityNodes: List[Element[_]], target: Decision[T, U])(implicit universe: Universe) = { + utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check + usageCheck(utilityNodes, target) + new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( + false, + dependentUniverses, + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + } + /** + * Create a decision variable elimination algorithm with the given decision variables and indicated utility + * nodes and using the given dependent universes in the current default universe. Use the given dependent + * algorithm function to determine the algorithm to use to compute probability of evidence in each dependent universe. + */ + def apply[T, U]( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + utilityNodes: List[Element[_]], + target: Decision[T, U])(implicit universe: Universe) = { + utilityNodes.foreach(_.generate()) // need initial values for the utility nodes before the usage check + usageCheck(utilityNodes, target) + new ProbQueryVariableEliminationDecision[T, U](universe, utilityNodes, target)( + false, + dependentUniverses, + dependentAlgorithm) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionAlgorithm.scala index 0ea0c7c5..376c64bb 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionAlgorithm.scala @@ -28,13 +28,13 @@ import scala.language.existentials * by 1) determining the order in which decisions can be computed 2) Implementing a single decision algorithm on * each decision (in the proper order). * - * Note: Only OneTime algorithms are supported in multi-decision algorithms + * Note: Only OneTime algorithms are supported in multi-decision algorithms. */ abstract class MultiDecisionAlgorithm(universe: Universe, utilityNodes: List[Element[_]], targets: List[Element[_]]) extends OneTime { /** - * List of the single decision algorithms implemented in the multi-decision algorithm + * List of the single decision algorithms implemented in the multi-decision algorithm. */ var algList: Map[Decision[_, _], OneTimeProbQueryDecision[_, _]] = Map() @@ -48,7 +48,7 @@ abstract class MultiDecisionAlgorithm(universe: Universe, utilityNodes: List[Ele } } /** - * Get the utility for a specific parent and decision in the multi-decision algorithm + * Get the utility for a specific parent and decision in the multi-decision algorithm. */ def getUtility[T, U](D: Decision[T, U], p: T, d: U) = { algList.get(D) match { @@ -58,7 +58,7 @@ abstract class MultiDecisionAlgorithm(universe: Universe, utilityNodes: List[Ele } /** - * Computes the order in which decisions should be computed. Decision order goes from independent->dependent + * Computes the order in which decisions should be computed. Decision order goes from independent->dependent. * */ val decisionOrder = getDecisionOrder(targets, universe) @@ -116,7 +116,7 @@ abstract class MultiDecisionAlgorithm(universe: Universe, utilityNodes: List[Ele override def cleanUp() = algList.foreach(_._2.kill) /** - * Run in a debug mode where only a single decision is run each time + * Run in a debug mode where only a single decision is run each time. * */ def run(oneStep: Boolean): Unit = runMulti(decisionOrder, oneStep) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionImportance.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionImportance.scala index 6707872b..1bf50569 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionImportance.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/MultiDecisionImportance.scala @@ -20,7 +20,7 @@ import com.cra.figaro.library.decision._ import scala.collection.mutable.Map /** - * A OneTime multi-decision algorithm that uses Importance sampling for each decision + * A OneTime multi-decision algorithm that uses Importance sampling for each decision. */ class OneTimeMultiDecisionImportance(universe: Universe, myNumSamples: Int, utilityNodes: List[Element[_]], targets: Decision[_, _]*) extends MultiDecisionAlgorithm(universe, utilityNodes, targets.toList) { diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Distance.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Distance.scala index 4724be88..15895c02 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Distance.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Distance.scala @@ -36,23 +36,23 @@ import scala.language.implicitConversions */ trait Distance[T] { /** - * Return the distance from this value to another + * Return the distance from this value to another. */ def distance(that: T): Double } /** - * Trait to compute the L-2 Norm + * Trait to compute the L-2 Norm. */ trait L2Norm { /** - * Reduce a variable list of doubles using the L2 norm + * Reduce a variable list of doubles using the L2 norm. */ def reduce(v: Double*) = math.pow((0.0 /: v)((c, n) => c + math.pow(math.abs(n), 2.0)), 0.5) } /** - * Abstract class to define the distance between tuples of classes that implement the Distance[T] trait + * Abstract class to define the distance between tuples of classes that implement the Distance[T] trait. */ abstract class TupleDistance { /** @@ -63,21 +63,21 @@ abstract class TupleDistance { } /** - * Extension of Ints to the Distance[T] trait. Computes the L1 distance between two Ints + * Extension of Ints to the Distance[T] trait. Computes the L1 distance between two Ints. */ private[index] case class IntDistance(value: Int) extends Distance[Int] { def distance(that: Int) = math.abs(value - that) } /** - * Extension of Doubles to the Distance[T] trait. Computes the L1 distance between two Doubles + * Extension of Doubles to the Distance[T] trait. Computes the L1 distance between two Doubles. */ private[index] case class DoubleDistance(value: Double) extends Distance[Double] { def distance(that: Double) = math.abs(value - that) } /** - * Extension of Boolean to the Distance[T] trait. Returns the (this xor that) + * Extension of Boolean to the Distance[T] trait. Returns the (this xor that). */ private[index] case class BooleanDistance(value: Boolean) extends Distance[Boolean] { def distance(that: Boolean) = if (value ^ that) 1.0 else 0.0 @@ -104,7 +104,7 @@ case class TupleDistance2[T1 <% Distance[T1], T2 <% Distance[T2]](value: (T1, T2 * Contains implicit conversions from Int, Double and Boolean to classes that extend the Distance[T] trait, * as well as 2-tuples of Int, Double and Boolean. * - * Uses L1 distance for single values and L2 distance for tuples + * Uses L1 distance for single values and L2 distance for tuples. */ object Distance { implicit def int2Dist(x: Int) = IntDistance(x) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/FlatIndex.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/FlatIndex.scala index e9417f8d..c1743d54 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/FlatIndex.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/FlatIndex.scala @@ -21,7 +21,7 @@ import scala.collection.immutable.Map import scala.collection.mutable.{ HashMap, MultiMap, Set } /** - * Node class used in the flat index + * Node class used in the flat index. */ private[index] class FlatNode[T, U] extends Node[T, U](null, true) with LNode[T, U] @@ -63,7 +63,7 @@ class FlatIndex[T <% Distance[T], U](stratMap: Map[(T, U), DecisionSample]) exte object FlatIndex { /** - * Create a flat index from a map of (parent, decision) -> Decision values + * Create a flat index from a map of (parent, decision) -> Decision values. */ def apply[T <% Distance[T], U](strat: Map[(T, U), DecisionSample]) = { new FlatIndex(strat) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Index.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Index.scala index 67e491e0..6447f409 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Index.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/decision/index/Index.scala @@ -38,7 +38,7 @@ abstract class Index[T <% Distance[T], U](stratMap: Map[(T, U), DecisionSample]) /* Implementors must define this class */ /** * Get the specified number of nearest neighbors of the parent value. Returns a tuple list of - * (distance to parent, decision value, weighted utility) + * (distance to parent, decision value, weighted utility). */ def getNN(parent: T, num: Int): List[(Double, U, DecisionSample)] @@ -48,14 +48,14 @@ abstract class Index[T <% Distance[T], U](stratMap: Map[(T, U), DecisionSample]) protected def getNNDebug(parent: T, num: Int): Iterable[(Double, Set[(U, DecisionSample)])] = null /** - * Get the size of the index + * Get the size of the index. */ def size = stratMap.size } /** * Helper class to create indices. Defines a node in a tree, with a parent node and a boolean - * indication if it is a leaf node + * indication if it is a leaf node. * * @param leaf Indicates if this is a leaf node */ @@ -64,43 +64,43 @@ abstract class Node[T, U](parent: Node[_, _], val leaf: Boolean) { protected type ObjectMapType = HashMap[Distance[T], Set[U]] with MultiMap[Distance[T], U] /** - * Defines the distance between a query and the internal nodes of an index + * Defines the distance between a query and the internal nodes of an index. */ def iDist(v: T): Iterable[(Double, Node[T, U])] /** - * Defines the distance between a query and the leaf nodes of an index + * Defines the distance between a query and the leaf nodes of an index. */ def oDist(v: T): Iterable[(Double, Set[U])] /** - * The children of a node + * The children of a node. */ var children: List[Node[T, U]] = List() /** - * Adds a child node to this node + * Adds a child node to this node. */ def addChild(n: Node[T, U]) = children = children :+ n } -/** Convenience trait for internal node of a tree */ +/** Convenience trait for internal node of a tree. */ trait INode[T, U] extends Node[T, U] { def oDist(o: T) = Map[Double, Set[U]]() } -/** Convenience trait for leaf node of a tree */ +/** Convenience trait for leaf node of a tree. */ trait LNode[T, U] extends Node[T, U] { val objects: ObjectMapType = new HashMap[Distance[T], Set[U]] with MultiMap[Distance[T], U] /** - * Adds an object of Distance[T] to the node that contains a value v of type U + * Adds an object of Distance[T] to the node that contains a value v of type U. */ def addObject(k: Distance[T], v: U) = objects.addBinding(k, v) /** * Compute the distance between each object stored in the leaf node - * and a query value of type Distance[T]. Will return the distances in sorted order + * and a query value of type Distance[T]. Will return the distances in sorted order. */ def oDist(o: T): Iterable[(Double, Set[U])] = { val m = PriorityQueue[(Double, Set[U])]()(Ordering.by(-1.0 * _._1)) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/DensityEstimator.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/DensityEstimator.scala index 19a3b73a..294533f1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/DensityEstimator.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/DensityEstimator.scala @@ -1,19 +1,52 @@ +/* + * DensityEstimator.scala + * Class to estimate densities of marginal distributions for resampling + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 9, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.algorithm.factored + class DensityException(s: String) extends RuntimeException(s) +/** + * The density estimator class is an abstract class to estimate the density of a point + * in some space given a list of weighted samples on that space. This class is principally + * used in Particle BP, where new samples for BP are generated from the estimate of the posterior. + * The density estimator is used in PBP to estimate densities during a Metropolis-Hastings sampler + */ abstract class DensityEstimator { + /** + * Get's the density of a point from a list of weighted samples + */ def getDensity(pt: Any, samples: List[(Double, Any)]): Double } +/** + * Trait for Density estimators on the space of doubles + */ trait DoubleDensityEstimator extends DensityEstimator { def getDensity(pt: Double, samples: List[(Double, Double)]): Double } +/** + * Trait for Density estimators on the space of ints + */ trait IntDensityEstimator extends DensityEstimator { def getDensity(pt: Int, samples: List[(Double, Int)]): Double } +/** + * A constant density estimator that always returns 1.0 + */ class ConstantDensityEstimator extends DensityEstimator { def getDensity(pt: Any, samples: List[(Double, Any)]): Double = 1.0 -} \ No newline at end of file +} + diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/FactoredAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/FactoredAlgorithm.scala index 73af6f64..0e2f327a 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/FactoredAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/FactoredAlgorithm.scala @@ -16,6 +16,7 @@ package com.cra.figaro.algorithm.factored import com.cra.figaro.algorithm._ import com.cra.figaro.language._ import com.cra.figaro.library.decision._ +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.util._ import annotation.tailrec import scala.collection._ diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factory.scala deleted file mode 100644 index 3bc82c44..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factory.scala +++ /dev/null @@ -1,783 +0,0 @@ -/* - * ProbFactor.scala - * Factors over variables. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -import com.cra.figaro.algorithm._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import annotation.tailrec -import scala.language.existentials -import com.cra.figaro.algorithm.lazyfactored._ -import scala.collection.mutable.ListBuffer -import scala.collection.mutable.SetBuilder - -/** - * Methods for creating probabilistic factors associated with elements. - */ -object Factory { - def makeStarFactor[T](elem: Element[T]): List[Factor[Double]] = { - val elemVar = Variable(elem) - require(elemVar.range.size == 1 && elemVar.range(0) == Star[T], "Trying to create a star factor from a value set that is not only star") - val factor = new BasicFactor[Double](List(elemVar)) - factor.set(List(0), 1.0) - List(factor) - } - - private def makeFactors[T](const: Constant[T]): List[Factor[Double]] = { - val factor = new BasicFactor[Double](List(Variable(const))) - factor.set(List(0), 1.0) - List(factor) - } - - private def makeFactors(flip: AtomicFlip): List[Factor[Double]] = { - val flipVar = Variable(flip) - if (flipVar.range.exists(!_.isRegular)) { - assert(flipVar.range.size == 1) // Flip's range must either be {T,F} or {*} - makeStarFactor(flip) - } else { - val factor = new BasicFactor[Double](List(flipVar)) - val i = flipVar.range.indexOf(Regular(true)) - factor.set(List(i), flip.prob) - factor.set(List(1 - i), 1.0 - flip.prob) - List(factor) - } - } - - private def makeFactors(flip: CompoundFlip): List[Factor[Double]] = { - val flipVar = Variable(flip) - if (flipVar.range.exists(!_.isRegular)) { - assert(flipVar.range.size == 1) // Flip's range must either be {T,F} or {*} - makeStarFactor(flip) - } else { - val probVar = Variable(flip.prob) - val factor = new BasicFactor[Double](List(probVar, flipVar)) - val parentVals = probVar.range - val i = flipVar.range.indexOf(Regular(true)) - for { j <- 0 until parentVals.size } { - if (parentVals(j).isRegular) { - val value = parentVals(j).value - factor.set(List(j, i), value) - factor.set(List(j, 1 - i), 1.0 - value) - } else { - factor.set(List(j, 0), 0.0) - factor.set(List(j, 1), 0.0) - } - } - List(factor) - } - } - - private def makeSimpleDistribution[T](target: Variable[T], probs: List[Double]): Factor[Double] = { - val factor = new BasicFactor[Double](List(target)) - for { (prob, index) <- probs.zipWithIndex } { - factor.set(List(index), prob) - } - factor - } - - private def makeComplexDistribution[T](target: Variable[T], probElems: List[Element[Double]]): Factor[Double] = { - val probVars: List[Variable[Double]] = probElems map (Variable(_)) - val factor = new BasicFactor[Double](List((target :: probVars): _*)) - val probVals: List[List[Extended[Double]]] = probVars map (_.range) - for { indices <- factor.allIndices } { - // unnormalized is a list, one for each probability element, of the value of that element under these indices - val unnormalized = - for { (probIndex, position) <- indices.toList.tail.zipWithIndex } yield { - val xprob = probVals(position)(probIndex) // The probability of the particular value of the probability element in this position - if (xprob.isRegular) xprob.value; else 0.0 - } - val normalized = normalize(unnormalized).toArray - // The first variable specifies the position of the remaining variables, so indices(0) is the correct probability - factor.set(indices, normalized(indices(0))) - } - factor - } - - private def getProbs[U, T](select: Select[U, T]): List[U] = getProbs(select, select.clauses) - - private def getProbs[U, T](elem: Element[T], clauses: List[(U, T)]): List[U] = { - val selectVar = Variable(elem) - def getProb(xvalue: Extended[T]): U = { - clauses.find(_._2 == xvalue.value).get._1 // * cannot be a value of a Select - } - val probs = - for { xvalue <- selectVar.range } yield getProb(xvalue) - probs - } - - private def parameterizedGetProbs[T](select: ParameterizedSelect[T]): List[Double] = { - val outcomes = select.outcomes - val map = select.parameter.MAPValue - for { - xvalue <- Variable(select).range - index = outcomes.indexOf(xvalue.value) - } yield map(index) - } - - private def makeFactors[T](select: AtomicSelect[T]): List[Factor[Double]] = { - val selectVar = Variable(select) - if (selectVar.range.exists(!_.isRegular)) { - assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} - makeStarFactor(select) - } else { - val probs = getProbs(select) - List(makeSimpleDistribution(selectVar, probs)) - } - } - - private def makeFactors[T](select: CompoundSelect[T]): List[Factor[Double]] = { - val selectVar = Variable(select) - if (selectVar.range.exists(!_.isRegular)) { - assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} - makeStarFactor(select) - } else { - val probs = getProbs(select) - List(makeComplexDistribution(selectVar, probs)) - } - } - - private def makeFactors[T](select: ParameterizedSelect[T]): List[Factor[Double]] = { - val selectVar = Variable(select) - if (selectVar.range.exists(!_.isRegular)) { - assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} - makeStarFactor(select) - } else { - val probs = parameterizedGetProbs(select) - List(makeSimpleDistribution(selectVar, probs)) - } - } - - private def makeDontCares[U](factor: Factor[Double], - intermedIndex: Int, - overallVar: Variable[U], - outcomeVar: Variable[U]): Unit = { - // If we don't care, we assign 1.0 to all combinations of the distVar and outcomeVar - for { - j <- 0 until overallVar.size - k <- 0 until outcomeVar.size - } { - factor.set(List(intermedIndex, j, k), 1.0) - } - } - - private def makeCares[U](factor: Factor[Double], intermedIndex: Int, - overallVar: Variable[U], outcomeVar: Variable[U], choices: Set[U])(implicit mapper: PointMapper[U]): Unit = { - // We care to match up overallVar with outcomeVar - for { - (overallVal, j) <- overallVar.range.zipWithIndex - (outcomeVal, k) <- outcomeVar.range.zipWithIndex - } { - // Star stands for "something". If outcomeVal is Star and overallVal is Star, we know something will match something, so the entry is (1,1). - // If outcomeVal is Star and overallVal is a regular value, then maybe there will be a match, so the entry is (0,1). - // If outcomeVal is regular, all the probability mass associated with that outcome should be on regular values of overallVal, so the entry is (0,0). - val entry = - if (overallVal.isRegular && outcomeVal.isRegular) { - if (overallVal.value == mapper.map(outcomeVal.value, choices)) 1.0 - else 0.0 - } else if (!overallVal.isRegular && !outcomeVal.isRegular) 1.0 - else 0.0 - factor.set(List(intermedIndex, j, k), entry) - } - } - - /* - * The conditional selector creates a factor in which, when the selector's value is such that the result - * element is relevant to the final result, the result element and overall element must have the same - * value (handled by makeCares). Otherwise, the result element and overall element can take on any - * value (handled by makeDontCares) - */ - /** - * Make a conditional selector factor used in the decomposition of chain and other elements. - * A chain defines a factor over the parent element, each of the possible result elements of the chain, - * and the overall chain element. This can produce a very large factor when there are many result elements. - * This is solved by decomposing the chain factor into a product of factors, each of which contains the - * parent element, one of the result elements, and the overall chain element. - */ - def makeConditionalSelector[T, U](overallElem: Element[U], selector: Variable[T], - outcomeIndex: Int, outcomeVar: Variable[U])(implicit mapper: PointMapper[U]): Factor[Double] = { - val overallVar = Variable(overallElem) - //val outcomeVar = Variable(outcomeElem) - val overallValues = LazyValues(overallElem.universe).storedValues(overallElem) - val factor = new BasicFactor[Double](List(selector, overallVar, outcomeVar)) - for { i <- 0 until outcomeIndex } { makeDontCares(factor, i, overallVar, outcomeVar) } - makeCares(factor, outcomeIndex, overallVar, outcomeVar, overallValues.regularValues)(mapper) - for { i <- outcomeIndex + 1 until selector.size } { makeDontCares(factor, i, overallVar, outcomeVar) } - factor - } - - private def intermedAndClauseFactors[U, T](dist: Dist[U, T]): (Variable[Int], List[Factor[Double]]) = { - val intermed = new Variable(ValueSet.withoutStar((0 until dist.clauses.size).toSet)) - val clauseFactors = dist.outcomes.zipWithIndex map (pair => - makeConditionalSelector(dist, intermed, pair._2, Variable(pair._1))) - (intermed, clauseFactors) - } - - private def makeFactors[T](dist: AtomicDist[T]): List[Factor[Double]] = { - val (intermed, clauseFactors) = intermedAndClauseFactors(dist) - val intermedFactor = makeSimpleDistribution(intermed, dist.probs) - intermedFactor :: clauseFactors - } - - private def makeFactors[T](dist: CompoundDist[T]): List[Factor[Double]] = { - val (intermed, clauseFactors) = intermedAndClauseFactors(dist) - val intermedFactor = makeComplexDistribution(intermed, dist.probs) - intermedFactor :: clauseFactors - } - - private def makeFactors[T, U](chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val chainMap: scala.collection.mutable.Map[T, Element[U]] = LazyValues(chain.universe).getMap(chain) - val parentVar = Variable(chain.parent) - var tempFactors = parentVar.range.zipWithIndex flatMap (pair => { - val parentVal = pair._1 - // parentVal.value should have been placed in applyMap at the time the values of this apply were computed. - // By using chainMap, we can make sure that the result element is the same now as they were when values were computed. - if (parentVal.isRegular) List(makeConditionalSelector(chain, parentVar, pair._2, Variable(chainMap(parentVal.value)))(mapper)) - else { - // We create a dummy variable for the outcome variable whose value is always star. - // We create a dummy factor for that variable. - // Then we use makeConditionalSelector with the dummy variable - val dummy = new Variable(ValueSet.withStar[U](Set())) - val dummyFactor = new BasicFactor[Double](List(dummy)) - dummyFactor.set(List(0), 1.0) - List(makeConditionalSelector(chain, parentVar, pair._2, dummy), dummyFactor) - } - }) - tempFactors - } - - val maxElementCount = 6 - val maxSize = 500 - val newFactors = ListBuffer[Factor[Double]]() - val tempFactors = ListBuffer[Factor[Double]]() - - def combineFactors(oldFactors: List[Factor[Double]], semiring: Semiring[Double], removeTemporaries: Boolean): List[Factor[Double]] = { - newFactors.clear - tempFactors.clear - - for (factor <- oldFactors) - { - if (factor.hasStar) - { - newFactors += factor - } - else - { - tempFactors += factor - } - } - - var nextFactor = tempFactors.head - - for (factor <- tempFactors.tail) - { - val commonVariables = factor.variables.toSet & nextFactor.variables.toSet - - if (commonVariables.size > 0) - { - val newVariables = factor.variables.toSet -- nextFactor.variables.toSet - val potentialSize = calculateSize(nextFactor.size, newVariables) - if ((nextFactor.numVars + newVariables.size) < maxElementCount - && potentialSize < maxSize) - { - nextFactor = nextFactor.product(factor, semiring) - } - else - { - if (removeTemporaries) - { - newFactors ++= reduceFactor(nextFactor, semiring, maxElementCount) - } - else - { - newFactors += nextFactor - } - nextFactor = factor - } - } - else - { - newFactors += nextFactor - nextFactor = factor - } - } - - if (nextFactor.numVars > 0) - { - if (removeTemporaries) - { - newFactors ++= reduceFactor(nextFactor, semiring, maxElementCount) - } - else - { - newFactors += nextFactor - } - } - newFactors.toList - } - - val variableSet = scala.collection.mutable.Set[ElementVariable[_]]() - val nextFactors = ListBuffer[Factor[Double]]() - - private def reduceFactor(factor: Factor[Double], semiring: Semiring[Double], maxElementCount: Int):List[Factor[Double]] = { - variableSet.clear - - (variableSet /: List(factor))(_ ++= _.variables.asInstanceOf[List[ElementVariable[_]]]) - var elementCount = variableSet count (v => !isTemporary(v)) - var resultFactor = Factor.unit[Double](semiring).product(factor, semiring) - - var tempCount = 0; - - for {variable <- variableSet} - { - if (isTemporary(variable) && elementCount <= maxElementCount) - { - nextFactors.clear - nextFactors ++= concreteFactors(variable.asInstanceOf[ElementVariable[_]].element) - (variableSet /: nextFactors)(_ ++= _.variables.asInstanceOf[List[ElementVariable[_]]]) - elementCount = variableSet count (v => !isTemporary(v)) - - for (nextFactor <- nextFactors) - { - resultFactor = resultFactor.product(nextFactor, semiring) - } - tempCount += 1 - } - } - - if (tempCount > 0 && elementCount <= maxElementCount) - { - for {variable <- variableSet} - { - if (isTemporary(variable)) - { - resultFactor = resultFactor.sumOver(variable, semiring) - } - } - - } - List(resultFactor) - } - - private def calculateSize(currentSize: Int, variables: Set[Variable[_]]) = { - (currentSize /: variables)(_ * _.size) - } - - private def isTemporary[_T](variable: Variable[_]): Boolean = { - variable match { - case e: ElementVariable[_] => e.element.isTemporary - case _ => false - } - } - - private def makeFactors[T, U](apply: Apply1[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val applyMap: scala.collection.mutable.Map[T, U] = LazyValues(apply.universe).getMap(apply) - val arg1Var = Variable(apply.arg1) - val resultVar = Variable(apply) - val applyValues = LazyValues(apply.universe).storedValues(apply) - val factor = new BasicFactor[Double](List(arg1Var, resultVar)) - val arg1Indices = arg1Var.range.zipWithIndex - val resultIndices = resultVar.range.zipWithIndex - for { - (arg1Val, arg1Index) <- arg1Indices - (resultVal, resultIndex) <- resultIndices - } { - // See logic in makeCares - val entry = - if (arg1Val.isRegular && resultVal.isRegular) { - // arg1Val.value should have been placed in applyMap at the time the values of this apply were computed. - // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. - if (resultVal.value == mapper.map(applyMap(arg1Val.value), applyValues.regularValues)) 1.0 - else 0.0 - } else if (!arg1Val.isRegular && !resultVal.isRegular) 1.0 - else if (!arg1Val.isRegular && resultVal.isRegular) 0.0 - else 0.0 - factor.set(List(arg1Index, resultIndex), entry) - } - List(factor) - } - - private def makeFactors[T1, T2, U](apply: Apply2[T1, T2, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val applyMap: scala.collection.mutable.Map[(T1, T2), U] = LazyValues(apply.universe).getMap(apply) - val arg1Var = Variable(apply.arg1) - val arg2Var = Variable(apply.arg2) - val resultVar = Variable(apply) - val applyValues = LazyValues(apply.universe).storedValues(apply) - val factor = new BasicFactor[Double](List(arg1Var, arg2Var, resultVar)) - val arg1Indices = arg1Var.range.zipWithIndex - val arg2Indices = arg2Var.range.zipWithIndex - val resultIndices = resultVar.range.zipWithIndex - for { - (arg1Val, arg1Index) <- arg1Indices - (arg2Val, arg2Index) <- arg2Indices - (resultVal, resultIndex) <- resultIndices - } { - val entry = - if (arg1Val.isRegular && arg2Val.isRegular && resultVal.isRegular) { - // The argument values should have been placed in applyMap at the time the values of this apply were computed. - // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. - if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value)), applyValues.regularValues)) 1.0 - else 0.0 - } else if ((!arg1Val.isRegular || !arg2Val.isRegular) && !resultVal.isRegular) 1.0 - else if ((!arg1Val.isRegular || !arg2Val.isRegular) && resultVal.isRegular) 0.0 - else 0.0 - factor.set(List(arg1Index, arg2Index, resultIndex), entry) - } - List(factor) - } - - private def makeFactors[T1, T2, T3, U](apply: Apply3[T1, T2, T3, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val applyMap: scala.collection.mutable.Map[(T1, T2, T3), U] = LazyValues(apply.universe).getMap(apply) - val arg1Var = Variable(apply.arg1) - val arg2Var = Variable(apply.arg2) - val arg3Var = Variable(apply.arg3) - val resultVar = Variable(apply) - val applyValues = LazyValues(apply.universe).storedValues(apply) - val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var, resultVar)) - val arg1Indices = arg1Var.range.zipWithIndex - val arg2Indices = arg2Var.range.zipWithIndex - val arg3Indices = arg3Var.range.zipWithIndex - val resultIndices = resultVar.range.zipWithIndex - for { - (arg1Val, arg1Index) <- arg1Indices - (arg2Val, arg2Index) <- arg2Indices - (arg3Val, arg3Index) <- arg3Indices - (resultVal, resultIndex) <- resultIndices - } { - val entry = - if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && resultVal.isRegular) { - // The argument values should have been placed in applyMap at the time the values of this apply were computed. - // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. - if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value)), applyValues.regularValues)) 1.0 - else 0.0 - } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular) && !resultVal.isRegular) 1.0 - else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular) && resultVal.isRegular) 0.0 - else 0.0 - factor.set(List(arg1Index, arg2Index, arg3Index, resultIndex), entry) - } - List(factor) - } - - private def makeFactors[T1, T2, T3, T4, U](apply: Apply4[T1, T2, T3, T4, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val applyMap: scala.collection.mutable.Map[(T1, T2, T3, T4), U] = LazyValues(apply.universe).getMap(apply) - val arg1Var = Variable(apply.arg1) - val arg2Var = Variable(apply.arg2) - val arg3Var = Variable(apply.arg3) - val arg4Var = Variable(apply.arg4) - val resultVar = Variable(apply) - val applyValues = LazyValues(apply.universe).storedValues(apply) - val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var, arg4Var, resultVar)) - val arg1Indices = arg1Var.range.zipWithIndex - val arg2Indices = arg2Var.range.zipWithIndex - val arg3Indices = arg3Var.range.zipWithIndex - val arg4Indices = arg4Var.range.zipWithIndex - val resultIndices = resultVar.range.zipWithIndex - for { - (arg1Val, arg1Index) <- arg1Indices - (arg2Val, arg2Index) <- arg2Indices - (arg3Val, arg3Index) <- arg3Indices - (arg4Val, arg4Index) <- arg4Indices - (resultVal, resultIndex) <- resultIndices - } { - val entry = - if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && arg4Val.isRegular && resultVal.isRegular) { - // The argument values should have been placed in applyMap at the time the values of this apply were computed. - // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. - if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value, arg4Val.value)), applyValues.regularValues)) 1.0 - else 0.0 - } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular) && !resultVal.isRegular) 1.0 - else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular) && resultVal.isRegular) 0.0 - else 0.0 - factor.set(List(arg1Index, arg2Index, arg3Index, arg4Index, resultIndex), entry) - } - List(factor) - } - - private def makeFactors[T1, T2, T3, T4, T5, U](apply: Apply5[T1, T2, T3, T4, T5, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { - val applyMap: scala.collection.mutable.Map[(T1, T2, T3, T4, T5), U] = LazyValues(apply.universe).getMap(apply) - val arg1Var = Variable(apply.arg1) - val arg2Var = Variable(apply.arg2) - val arg3Var = Variable(apply.arg3) - val arg4Var = Variable(apply.arg4) - val arg5Var = Variable(apply.arg5) - val resultVar = Variable(apply) - val applyValues = LazyValues(apply.universe).storedValues(apply) - val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var, arg4Var, arg5Var, resultVar)) - val arg1Indices = arg1Var.range.zipWithIndex - val arg2Indices = arg2Var.range.zipWithIndex - val arg3Indices = arg3Var.range.zipWithIndex - val arg4Indices = arg4Var.range.zipWithIndex - val arg5Indices = arg5Var.range.zipWithIndex - val resultIndices = resultVar.range.zipWithIndex - for { - (arg1Val, arg1Index) <- arg1Indices - (arg2Val, arg2Index) <- arg2Indices - (arg3Val, arg3Index) <- arg3Indices - (arg4Val, arg4Index) <- arg4Indices - (arg5Val, arg5Index) <- arg5Indices - (resultVal, resultIndex) <- resultIndices - } { - val entry = - if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && arg4Val.isRegular && arg5Val.isRegular && resultVal.isRegular) { - // The argument values should have been placed in applyMap at the time the values of this apply were computed. - // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. - if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value, arg4Val.value, arg5Val.value)), applyValues.regularValues)) 1.0 - else 0.0 - } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular || !arg5Val.isRegular) && !resultVal.isRegular) 1.0 - else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular || !arg5Val.isRegular) && resultVal.isRegular) 0.0 - else 0.0 - factor.set(List(arg1Index, arg2Index, arg3Index, arg4Index, arg5Index, resultIndex), entry) - } - List(factor) - } - - private def makeFactors[T](inject: Inject[T]): List[Factor[Double]] = { - def rule(values: List[Extended[_]]) = { - val resultXvalue :: inputXvalues = values - // See logic under makeCares - if (inputXvalues.exists(!_.isRegular)) { - if (!resultXvalue.isRegular) 1.0; else 0.0 - } - else if (resultXvalue.isRegular) { - if (resultXvalue.value.asInstanceOf[List[T]] == inputXvalues.map(_.value.asInstanceOf[T])) 1.0; else 0.0 - } else 0.0 - } - val inputVariables = inject.args map (Variable(_)) - val resultVariable = Variable(inject) - val variables = resultVariable :: inputVariables - val factor = new BasicFactor[Double](variables) - factor.fillByRule(rule _) - List(factor) - } - - // When we're using a parameter to compute expected sufficient statistics, we just use its expected value - private def makeParameterFactors(param: Parameter[_]): List[Factor[Double]] = { - // The parameter should have one possible value, which is its expected value - assert(Variable(param).range.size == 1) - val factor = new BasicFactor[Double](List(Variable(param))) - factor.set(List(0), 1.0) - List(factor) - } - - private def makeFactors(flip: ParameterizedFlip): List[Factor[Double]] = { - val flipVar = Variable(flip) - val factor = new BasicFactor[Double](List(flipVar)) - val prob = flip.parameter.MAPValue - val i = flipVar.range.indexOf(Regular(true)) - factor.set(List(i), prob) - factor.set(List(1 - i), 1.0 - prob) - List(factor) - } - - private def makeFactors[T](atomic: Atomic[T]): List[Factor[Double]] = { - val atomicVar = Variable(atomic) - val pbpSampler = ParticleGenerator(atomic.universe) - // Note, don't need number of samples because values should have already been expanded on it - // (and values will initiate the sampling) - val samples = pbpSampler(atomic) - if (atomicVar.range.exists(!_.isRegular)) { - assert(atomicVar.range.size == 1) // Select's range must either be a list of regular values or {*} - makeStarFactor(atomic) - } else { - val probs = getProbs(atomic, samples) - List(makeSimpleDistribution(atomicVar, probs)) - } - } - - def concreteFactors[T](elem: Element[T]): List[Factor[Double]] = - elem match { - case f: ParameterizedFlip => makeFactors(f) - case s: ParameterizedSelect[_] => makeFactors(s) - case p: DoubleParameter => makeParameterFactors(p) - case p: ArrayParameter => makeParameterFactors(p) - case c: Constant[_] => makeFactors(c) - case f: AtomicFlip => makeFactors(f) - case f: CompoundFlip => makeFactors(f) - case s: AtomicSelect[_] => makeFactors(s) - case s: CompoundSelect[_] => makeFactors(s) - case d: AtomicDist[_] => makeFactors(d) - case d: CompoundDist[_] => makeFactors(d) - case c: Chain[_, _] => makeFactors(c) - case a: Apply1[_, _] => makeFactors(a) - case a: Apply2[_, _, _] => makeFactors(a) - case a: Apply3[_, _, _, _] => makeFactors(a) - case a: Apply4[_, _, _, _, _] => makeFactors(a) - case a: Apply5[_, _, _, _, _, _] => makeFactors(a) - case i: Inject[_] => makeFactors(i) - case f: ProbFactorMaker => f.makeFactors - case a: Atomic[_] => makeFactors(a) - - case _ => throw new UnsupportedAlgorithmException(elem) - } - - private def makeAbstract[T](atomic: Atomic[T], abstraction: Abstraction[T]): List[Factor[Double]] = { - val variable = Variable(atomic) - val values = variable.range.map(_.value) - val densityMap = scala.collection.mutable.Map[T, Double]() - for { v <- values } { - val currentDensity = densityMap.getOrElse(v, 0.0) - densityMap.update(v, currentDensity + atomic.density(v)) - } - val factor = new BasicFactor[Double](List(variable)) - for { (v, i) <- values.zipWithIndex } { - factor.set(List(i), densityMap(v)) - } - List(factor) - } - - private def makeAbstract[T](elem: Element[T], abstraction: Abstraction[T]): List[Factor[Double]] = - elem match { - case atomic: Atomic[_] => makeAbstract(atomic, abstraction) - case apply: Apply1[_, _] => makeFactors(apply)(abstraction.scheme) - case apply: Apply2[_, _, _] => makeFactors(apply)(abstraction.scheme) - case apply: Apply3[_, _, _, _] => makeFactors(apply)(abstraction.scheme) - // In the case of a Chain, its pragmas are inherited by the expanded result elements. The abstraction will be - // taken into account when we generate factors for the result elements. - case chain: Chain[_, _] => makeFactors(chain)(abstraction.scheme) - case _ => throw new UnsupportedAlgorithmException(elem) - } - - private def makeNonConstraintFactorsUncached[T](elem: Element[T]): List[Factor[Double]] = { - /* - * Don't make non-constraint factors for an element that is expanded to depth -1. - * The element must take on the value *, so the factor is the unit. - * Attempting to create a factor can result in problems where the element's arguments are not star, - * leading to the factor being zero everywhere. - * For example, consider an Apply. If the Apply is expanded to depth -1, but its argument has already - * been expanded and produced a set of values without *, the Apply factor would have probability zero - * for cases where the Apply result is *. But since the Apply has only been expanded to depth -1, - * its only possible result is *, so the factor is zero everywhere. - */ - if (LazyValues(elem.universe).expandedDepth(elem).getOrElse(-1) != -1) { - Abstraction.fromPragmas(elem.pragmas) match { - case None => concreteFactors(elem) - case Some(abstraction) => makeAbstract(elem, abstraction) - } - } - else List() - } - - private def makeConditionAndConstraintFactors[T](elem: Element[T]): List[Factor[Double]] = - elem.allConditions.map(makeConditionFactor(elem, _)) ::: elem.allConstraints.map(makeConstraintFactor(elem, _)) - - private def makeConditionFactor[T](elem: Element[T], cc: (T => Boolean, Element.Contingency)): Factor[Double] = - makeConstraintFactor(elem, (ProbConstraintType((t: T) => if (cc._1(t)) 1.0; else 0.0), cc._2)) - - private def makeConstraintFactor[T](elem: Element[T], cc: (T => Double, Element.Contingency)): Factor[Double] = { - val (constraint, contingency) = cc - contingency match { - case List() => makeUncontingentConstraintFactor(elem, constraint) - case first :: rest => makeContingentConstraintFactor(elem, constraint, first, rest) - } - } - - private def makeUncontingentConstraintFactor[T](elem: Element[T], constraint: T => Double): Factor[Double] = { - val elemVar = Variable(elem) - val factor = new BasicFactor[Double](List(elemVar)) - for { (elemVal, index) <- elemVar.range.zipWithIndex } { - val entry = if (elemVal.isRegular) math.exp(constraint(elemVal.value)); else 0.0 - factor.set(List(index), entry) - } - factor - } - - private def makeContingentConstraintFactor[T](elem: Element[T], constraint: T => Double, firstConting: Element.ElemVal[_], restContinges: Element.Contingency): Factor[Double] = { - val restFactor = makeConstraintFactor(elem, (constraint, restContinges)) - extendConstraintFactor(restFactor, firstConting) - } - - private def extendConstraintFactor(restFactor: Factor[Double], firstConting: Element.ElemVal[_]): Factor[Double] = { - // The extended factor is obtained by getting the underlying factor and expanding each row so that the row only provides its entry if the contingent variable takes - // on the appropriate value, otherwise the entry is 1 - val Element.ElemVal(firstElem, firstValue) = firstConting - val firstVar = Variable(firstElem) - val firstValues = firstVar.range - val numFirstValues = firstValues.size - val matchingIndex: Int = firstValues.indexOf(Regular(firstValue)) - val resultFactor = new BasicFactor[Double](firstVar :: restFactor.variables) - for { restIndices <- restFactor.allIndices } { - val restEntry = restFactor.get(restIndices) - for { firstIndex <- 0 until numFirstValues } { - val resultEntry = if (firstIndex == matchingIndex) restEntry; else 1.0 - resultFactor.set(firstIndex :: restIndices, resultEntry) - } - } - resultFactor - } - - private val factorCache = scala.collection.mutable.Map[Element[_], List[Factor[Double]]]() - - def makeNonConstraintFactors(elem: Element[_]): List[Factor[Double]] = { - factorCache.get(elem) match { - case Some(l) => l - case None => - val result = makeNonConstraintFactorsUncached(elem) - factorCache += elem -> result - elem.universe.register(factorCache) - result - } - } - - - /** - * Create the probabilistic factors associated with an element. This method is memoized. - */ - def make(elem: Element[_]): List[Factor[Double]] = { - makeConditionAndConstraintFactors(elem) ::: makeNonConstraintFactors(elem) - } - - /** - * Create the probabilistic factors associated with a list. - */ - def make[T](variables: List[Variable[_]]): Factor[T] = { - new BasicFactor[T](variables) - } - - /** - * Remove an element from the factor cache, ensuring that factors for the element - * are regenerated. This is important, for example, if evidence on the variable has changed. - * - */ - def removeFactors(elem: Element[_]) { factorCache -= elem } - - /** - * Clear the factor cache. - */ - def removeFactors() { factorCache.clear } - - /** - * Update the factor cache. - */ - def updateFactor[T](elem: Element[_], f: List[Factor[Double]]) { factorCache.update(elem, f) } - - /** - * Create the probabilistic factor encoding the probability of evidence in the dependent universe as a function of the - * values of variables in the parent universe. The third argument is the the function to use for computing - * probability of evidence in the dependent universe. It is assumed that the definition of this function will already contain the - * right evidence. - */ - def makeDependentFactor(parentUniverse: Universe, - dependentUniverse: Universe, - probEvidenceComputer: () => Double): Factor[Double] = { - val uses = dependentUniverse.parentElements filter (_.universe == parentUniverse) - def rule(values: List[Any]) = { - for { (elem, value) <- uses zip values } { elem.value = value.asInstanceOf[Regular[elem.Value]].value } - val result = probEvidenceComputer() - result - } - val variables = uses map (Variable(_)) - val factor = new BasicFactor[Double](variables) - factor.fillByRule(rule _) - factor - } -} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/MPEVariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/MPEVariableElimination.scala index e6b2b3e0..ca1ec409 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/MPEVariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/MPEVariableElimination.scala @@ -16,6 +16,7 @@ package com.cra.figaro.algorithm.factored import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.util import scala.collection.mutable.{ Set, Map } import com.cra.figaro.algorithm.lazyfactored._ @@ -39,7 +40,7 @@ class MPEVariableElimination(override val universe: Universe)( override val starterElements = universe.activeElements /** - * Empty for MPE Algorithms + * Empty for MPE Algorithms. */ val targetElements = List[Element[_]]() diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ParticleGenerator.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ParticleGenerator.scala index bb8ec1e6..14d829e6 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ParticleGenerator.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ParticleGenerator.scala @@ -1,140 +1,153 @@ -package com.cra.figaro.algorithm.factored - -import scala.collection.mutable.Map -import com.cra.figaro.algorithm.sampling.ElementSampler -import com.cra.figaro.language.Element -import com.cra.figaro.util._ -import com.cra.figaro.language.Universe -import com.cra.figaro.language.Atomic -import com.cra.figaro.library.atomic.discrete.OneShifter - -/** - * Class to handle sampling from continuous elements in PBP - * @param argSamples Maximum number of samples to take from atomic elements - * @param totalSamples Maximum number of samples on the output of chains - * @param de An instance to compute the density estimate of point during resampling - */ -class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTotalSamples: Int) { - - // Caches the samples for an element - private val sampleMap = Map[Element[_], List[(Double, _)]]() - - /** - * Returns the set of sampled elements contained in this sampler - */ - def sampledElements(): Set[Element[_]] = sampleMap.keySet.toSet - - /** - * Clears all of the samples for elements in this sampler - */ - def clear() = sampleMap.clear - - /** - * Updates the samples for an element - */ - def update(elem: Element[_], samples: List[(Double, _)]) = sampleMap.update(elem, samples) - - /** - * Retrieves the samples for an element using the default number of samples. - */ - def apply[T](elem: Element[T]): List[(Double, T)] = apply(elem, numArgSamples) - - /** - * Retrieves the samples for an element using the indicated number of samples - */ - def apply[T](elem: Element[T], numSamples: Int): List[(Double, T)] = { - sampleMap.get(elem) match { - case Some(e) => { - e.asInstanceOf[List[(Double, T)]] - } - case None => { - val sampler = ElementSampler(elem, numSamples) - sampler.start - val result = sampler.computeDistribution(elem).toList - sampleMap += elem -> result - elem.universe.register(sampleMap) - sampler.kill - result - } - } - } - - /** - * Resample and update the element from the indicated beliefs - * beliefs = (Probability, Value) - */ - def resample(elem: Element[_], beliefs: List[(Double, _)], proposalVariance: Double): Unit = { - - def nextInt(i: Int) = if (random.nextBoolean) i + 1 else i - 1 - def nextDouble(d: Double) = random.nextGaussian() * proposalVariance + d - - val sampleDensity: Double = 1.0 / beliefs.size - - val newSamples = elem match { - case o: OneShifter => { - beliefs.map(b => { - val oldValue = b._2.asInstanceOf[Int] - val newValue = nextInt(oldValue) - val nextValue = if (o.density(newValue) > 0.0) { - accept(oldValue, newValue, beliefs.asInstanceOf[List[(Double, Int)]]) - } else oldValue - (sampleDensity, nextValue) - }) - } - case a: Atomic[Double] => { // The double is unchecked, bad stuff if the atomic is not double - beliefs.map(b => { - val oldValue = b._2.asInstanceOf[Double] - val newValue = nextDouble(oldValue) - val nextValue = if (a.density(newValue) > 0.0) { - accept(oldValue, newValue, beliefs.asInstanceOf[List[(Double, Double)]]) - } else oldValue - (sampleDensity, nextValue) - }) - } - case _ => { // Not an atomic element, we don't know how to resample - beliefs - } - } - update(elem, newSamples) - } - - def accept[T](oldValue: T, newValue: T, beliefs: List[(Double, T)]): T = { - val oldDensity = de.getDensity(oldValue, beliefs) - val newDensity = de.getDensity(newValue, beliefs) - val ratio = newDensity / oldDensity - - val nextValue = if (ratio > 1) { - newValue - } else { - if (random.nextDouble < ratio) newValue else oldValue - } - nextValue - } -} - -object ParticleGenerator { - var defaultArgSamples = 20 - var defaultTotalSamples = 50 - - private val samplerMap: Map[Universe, ParticleGenerator] = Map() - - def clear(univ: Universe) = samplerMap -= univ - - def clear() = samplerMap.clear - - def apply(univ: Universe, de: DensityEstimator, numArgSamples: Int, numTotalSamples: Int): ParticleGenerator = - samplerMap.get(univ) match { - case Some(e) => e - case None => { - samplerMap += (univ -> new ParticleGenerator(de, numArgSamples, numTotalSamples)) - univ.registerUniverse(samplerMap) - samplerMap(univ) - } - } - - def apply(univ: Universe): ParticleGenerator = apply(univ, new ConstantDensityEstimator, - defaultArgSamples, defaultTotalSamples) - - def exists(univ: Universe): Boolean = samplerMap.contains(univ) - -} \ No newline at end of file +/* + * ParticleGenerator.scala + * Class to handle sampling from continuous elements in PBP + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 8, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored + +import scala.collection.mutable.Map +import com.cra.figaro.algorithm.sampling.ElementSampler +import com.cra.figaro.language.Element +import com.cra.figaro.util._ +import com.cra.figaro.language.Universe +import com.cra.figaro.language.Atomic +import com.cra.figaro.library.atomic.discrete.OneShifter + +/** + * Class to handle sampling from continuous elements in PBP. + * @param argSamples Maximum number of samples to take from atomic elements + * @param totalSamples Maximum number of samples on the output of chains + * @param de An instance to compute the density estimate of point during resampling + */ +class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTotalSamples: Int) { + + // Caches the samples for an element + private val sampleMap = Map[Element[_], List[(Double, _)]]() + + /** + * Returns the set of sampled elements contained in this sampler. + */ + def sampledElements(): Set[Element[_]] = sampleMap.keySet.toSet + + /** + * Clears all of the samples for elements in this sampler. + */ + def clear() = sampleMap.clear + + /** + * Updates the samples for an element. + */ + def update(elem: Element[_], samples: List[(Double, _)]) = sampleMap.update(elem, samples) + + /** + * Retrieves the samples for an element using the default number of samples. + */ + def apply[T](elem: Element[T]): List[(Double, T)] = apply(elem, numArgSamples) + + /** + * Retrieves the samples for an element using the indicated number of samples. + */ + def apply[T](elem: Element[T], numSamples: Int): List[(Double, T)] = { + sampleMap.get(elem) match { + case Some(e) => { + e.asInstanceOf[List[(Double, T)]] + } + case None => { + val sampler = ElementSampler(elem, numSamples) + sampler.start + val result = sampler.computeDistribution(elem).toList + sampleMap += elem -> result + elem.universe.register(sampleMap) + sampler.kill + result + } + } + } + + /** + * Resample and update the element from the indicated beliefs. + * beliefs = (Probability, Value) + */ + def resample(elem: Element[_], beliefs: List[(Double, _)], proposalVariance: Double): Unit = { + + def nextInt(i: Int) = if (random.nextBoolean) i + 1 else i - 1 + def nextDouble(d: Double) = random.nextGaussian() * proposalVariance + d + + val sampleDensity: Double = 1.0 / beliefs.size + + val newSamples = elem match { + case o: OneShifter => { + beliefs.map(b => { + val oldValue = b._2.asInstanceOf[Int] + val newValue = nextInt(oldValue) + val nextValue = if (o.density(newValue) > 0.0) { + accept(oldValue, newValue, beliefs.asInstanceOf[List[(Double, Int)]]) + } else oldValue + (sampleDensity, nextValue) + }) + } + case a: Atomic[_] => { // The double is unchecked, bad stuff if the atomic is not double + beliefs.map(b => { + val oldValue = b._2.asInstanceOf[Double] + val newValue = nextDouble(oldValue) + val nextValue = if (a.asInstanceOf[Atomic[Double]].density(newValue) > 0.0) { + accept(oldValue, newValue, beliefs.asInstanceOf[List[(Double, Double)]]) + } else oldValue + (sampleDensity, nextValue) + }) + } + case _ => { // Not an atomic element, we don't know how to resample + beliefs + } + } + update(elem, newSamples) + } + + private def accept[T](oldValue: T, newValue: T, beliefs: List[(Double, T)]): T = { + val oldDensity = de.getDensity(oldValue, beliefs) + val newDensity = de.getDensity(newValue, beliefs) + val ratio = newDensity / oldDensity + + val nextValue = if (ratio > 1) { + newValue + } else { + if (random.nextDouble < ratio) newValue else oldValue + } + nextValue + } +} + +object ParticleGenerator { + var defaultArgSamples = 20 + var defaultTotalSamples = 50 + + private val samplerMap: Map[Universe, ParticleGenerator] = Map() + + def clear(univ: Universe) = samplerMap -= univ + + def clear() = samplerMap.clear + + def apply(univ: Universe, de: DensityEstimator, numArgSamples: Int, numTotalSamples: Int): ParticleGenerator = + samplerMap.get(univ) match { + case Some(e) => e + case None => { + samplerMap += (univ -> new ParticleGenerator(de, numArgSamples, numTotalSamples)) + univ.registerUniverse(samplerMap) + samplerMap(univ) + } + } + + def apply(univ: Universe): ParticleGenerator = apply(univ, new ConstantDensityEstimator, + defaultArgSamples, defaultTotalSamples) + + def exists(univ: Universe): Boolean = samplerMap.contains(univ) + +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactorMaker.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactorMaker.scala deleted file mode 100644 index 47e3a9e7..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactorMaker.scala +++ /dev/null @@ -1,25 +0,0 @@ -/* - * ProbFactorMaker.scala - * Trait of elements for which factors can be created. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -/** - * Trait of elements for which probabilistic factors can be created. Elements that implement this trait must - * implement the makeFactors method. - */ -trait ProbFactorMaker { - /** - * Returns the factors corresponding to this element. - */ - def makeFactors: List[Factor[Double]] -} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala index fcf46283..267b2c04 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala @@ -1,107 +1,108 @@ -/* - * SufficientStatisticsVariableElimination.scala - * Variable elimination algorithm for sufficient statistics factors - * - * Created By: Michael Howard (mhoward@cra.com) - * Creation Date: Jun 1, 2013 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.learning._ -import com.cra.figaro.language._ -import scala.collection._ -import scala.collection.mutable.{ Map, Set } - -/** - * Variable elimination for sufficient statistics factors. - * The final factor resulting from variable elimination contains a mapping of parameters to sufficient statistics vectors - * which can be used to maximize parameter values. - * - * @param parameterMap A map of parameters to their sufficient statistics. - */ -class SufficientStatisticsVariableElimination( - parameterMap: immutable.Map[Parameter[_], Seq[Double]], - val universe: Universe) - extends VariableElimination[(Double, Map[Parameter[_], Seq[Double]])] { - - /** - * No timing information enabled for this algorithm. - */ - val showTiming = false - - protected val statFactor = new SufficientStatisticsFactor(parameterMap) - - /** - * Clear the sufficient statistics factors used by this algorithm. - */ - private def removeFactors() { - statFactor.removeFactors - } - - /** - * Particular implementations of probability of evidence algorithms must define the following method. - */ - def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[(Double, mutable.Map[Parameter[_], Seq[Double]])]] = { - val allElements = neededElements.filter(p => p.isInstanceOf[Parameter[_]] == false) - if (debug) { - println("Elements appearing in factors and their ranges:") - for { element <- allElements } { - println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) - } - } - - Factory.removeFactors() - val thisUniverseFactors = allElements flatMap (statFactor.make(_)) - val dependentUniverseFactors = - for { (dependentUniverse, evidence) <- dependentUniverses } yield statFactor.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) - - dependentUniverseFactors ::: thisUniverseFactors - } - - /** - * Empty for this algorithm. - */ - val targetElements = List[Element[_]]() - - override def starterElements = universe.conditionedElements ++ universe.constrainedElements - - private var result: (Double, Map[Parameter[_], Seq[Double]]) = _ - - def finish(factorsAfterElimination: Set[Factor[(Double, Map[Parameter[_], Seq[Double]])]], eliminationOrder: List[Variable[_]]): Unit = { - // It is possible that there are no factors (this will happen if there is no evidence). - // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. - val finalFactor = factorsAfterElimination.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - finalFactor.variables.size match { - case 0 => result = finalFactor.get(List()) - case _ => throw new RuntimeException("Final factor has variables") - } - } - - /** - * Returns a mapping of parameters to sufficient statistics resulting from - * elimination of the factors. - */ - def getSufficientStatisticsForAllParameters = { result._2.toMap } - - val semiring = SufficientStatisticsSemiring(parameterMap) - - override def cleanUp() = { - statFactor.removeFactors - super.cleanUp() - } - - val dependentUniverses: List[(Universe, List[NamedEvidence[_]])] = List() - val dependentAlgorithm = (u: Universe, e: List[NamedEvidence[_]]) => () => 1.0 - -} - -object SufficientStatisticsVariableElimination { - def apply(parameterMap : immutable.Map[Parameter[_], Seq[Double]])(implicit universe: Universe) = new SufficientStatisticsVariableElimination(parameterMap,universe) -} +/* + * SufficientStatisticsVariableElimination.scala + * Variable elimination algorithm for sufficient statistics factors + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 1, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored + +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.learning._ +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.factors._ +import scala.collection._ +import scala.collection.mutable.{ Map, Set } + +/** + * Variable elimination for sufficient statistics factors. + * The final factor resulting from variable elimination contains a mapping of parameters to sufficient statistics vectors + * which can be used to maximize parameter values. + * + * @param parameterMap A map of parameters to their sufficient statistics. + */ +class SufficientStatisticsVariableElimination( + parameterMap: immutable.Map[Parameter[_], Seq[Double]], + val universe: Universe) + extends VariableElimination[(Double, Map[Parameter[_], Seq[Double]])] { + + /** + * No timing information enabled for this algorithm. + */ + val showTiming = false + + protected val statFactor = new SufficientStatisticsFactor(parameterMap) + + /** + * Clear the sufficient statistics factors used by this algorithm. + */ + private def removeFactors() { + statFactor.removeFactors + } + + /** + * Particular implementations of probability of evidence algorithms must define the following method. + */ + def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[(Double, mutable.Map[Parameter[_], Seq[Double]])]] = { + val allElements = neededElements.filter(p => p.isInstanceOf[Parameter[_]] == false) + if (debug) { + println("Elements appearing in factors and their ranges:") + for { element <- allElements } { + println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) + } + } + + Factory.removeFactors() + val thisUniverseFactors = allElements flatMap (statFactor.make(_)) + val dependentUniverseFactors = + for { (dependentUniverse, evidence) <- dependentUniverses } yield statFactor.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) + + dependentUniverseFactors ::: thisUniverseFactors + } + + /** + * Empty for this algorithm. + */ + val targetElements = List[Element[_]]() + + override def starterElements = universe.conditionedElements ++ universe.constrainedElements + + private var result: (Double, Map[Parameter[_], Seq[Double]]) = _ + + def finish(factorsAfterElimination: Set[Factor[(Double, Map[Parameter[_], Seq[Double]])]], eliminationOrder: List[Variable[_]]): Unit = { + // It is possible that there are no factors (this will happen if there is no evidence). + // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. + val finalFactor = factorsAfterElimination.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + finalFactor.variables.size match { + case 0 => result = finalFactor.get(List()) + case _ => throw new RuntimeException("Final factor has variables") + } + } + + /** + * Returns a mapping of parameters to sufficient statistics resulting from + * elimination of the factors. + */ + def getSufficientStatisticsForAllParameters = { result._2.toMap } + + val semiring = SufficientStatisticsSemiring(parameterMap) + + override def cleanUp() = { + statFactor.removeFactors + super.cleanUp() + } + + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])] = List() + val dependentAlgorithm = (u: Universe, e: List[NamedEvidence[_]]) => () => 1.0 + +} + +object SufficientStatisticsVariableElimination { + def apply(parameterMap : immutable.Map[Parameter[_], Seq[Double]])(implicit universe: Universe) = new SufficientStatisticsVariableElimination(parameterMap,universe) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VEGraph.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VEGraph.scala index 523db327..89656f78 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VEGraph.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VEGraph.scala @@ -14,6 +14,7 @@ package com.cra.figaro.algorithm.factored import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored.factors._ import scala.collection.mutable.PriorityQueue /** @@ -25,7 +26,7 @@ case class AbstractFactor(variables: List[Variable[_]]) * Information associated with a variable during variable elimination, including the factors to which it * belongs and variables with which it shares a factor. * - * @param factors The abstract factors to which this variable belongs + * @param factors The abstract factors to which this variable belongs. * @param neighbors The variables that share a factor in common with this variable. */ case class VariableInfo(factors: Set[AbstractFactor], neighbors: Set[Variable[_]]) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VariableElimination.scala index 11229981..8b338c92 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/VariableElimination.scala @@ -1,334 +1,341 @@ -/* - * VariableElimination.scala - * Variable elimination algorithm. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.sampling._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import annotation.tailrec -import scala.collection.mutable.{ Map, Set } -import scala.language.postfixOps - -/** - * Trait of algorithms that perform variable elimination. - * - * @tparam T The type of entries in the factors. - */ -trait VariableElimination[T] extends FactoredAlgorithm[T] with OneTime { - - /** - * By default, implementations that inherit this trait have no debug information. - * Override this if you want a debugging option. - */ - var debug: Boolean = false - - /** - * The universe on which this variable elimination algorithm should be applied. - */ - val universe: Universe - - /** - * Target elements that should not be eliminated but should be available for querying. - */ - val targetElements: List[Element[_]] - - /** - * Elements towards which queries are directed. By default, these are the target elements. - * This is overridden by DecisionVariableElimination, where it also includes utility variables. - */ - def starterElements: List[Element[_]] = targetElements - - /** - * Flag indicating whether the run time of each step should be displayed. - */ - val showTiming: Boolean - - private def optionallyShowTiming[T](op: => T, name: String) = - if (showTiming) timed(op, name); else op - - /* - private def expand(): Unit = - optionallyShowTiming(Expand(universe), "Expansion") -*/ - - // The first element of FactorMap is the complete set of factors. - // The second element maps variables to the factors mentioning that variable. - private type FactorMap[T] = Map[Variable[_], Set[Factor[T]]] - - private def addFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = - factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) + factor)) - - private def removeFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = - factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) - factor)) - - private def initialFactorMap(factors: Traversable[Factor[T]]): FactorMap[T] = { - val map: FactorMap[T] = Map() - factors foreach (addFactor(_, map)) - map - } - - protected var recordingFactors: List[Factor[_]] = List() - - /** - * Some variable elimination algorithms, such as computing the most probable explanation, record values of - * variables as they are eliminated. Such values are stored in a factor that maps values of the other variables - * to a value of the eliminated variable. This factor is produced by finding the value of the variable that - * "maximizes" the entry associated with the value in the product factor resulting from eliminating this - * variable, for some maximization function. The recordingFunction determines which of two entries is greater - * according to the maximization function. It returns true iff the second entry is greater. The recording - * function is an option so that variable elimination algorithms that do not use it can ignore it. - */ - val comparator: Option[(T, T) => Boolean] = None - - private def eliminate( - variable: Variable[_], - factors: Set[Factor[T]], - map: FactorMap[T]): Unit = { - val varFactors = map(variable) - if (debug) { - println("*****************\nEliminating " + variable.id) - println("Input factors:") - for { factor <- varFactors } { println(factor.toReadableString) } - } - if (varFactors nonEmpty) { - val productFactor = varFactors reduceLeft (_.product(_, semiring)) - val resultFactor = productFactor.sumOver(variable, semiring) - varFactors foreach (removeFactor(_, map)) - addFactor(resultFactor, map) - comparator match { - case None => () - case Some(recorder) => recordingFactors ::= productFactor.recordArgMax(variable, recorder) - } - map -= variable - factors --= varFactors - if (debug) println("Result factor\n" + resultFactor.toReadableString) - factors += resultFactor - } - } - - private def eliminateInOrder( - order: List[Variable[_]], - factors: Set[Factor[T]], - map: FactorMap[T]): Set[Factor[T]] = - order match { - case Nil => - factors - case first :: rest => - eliminate(first, factors, map) - eliminateInOrder(rest, factors, map) - } - - /** - * Method for choosing the elimination order. - * The default order chooses first the variable that - * minimizes the number of extra factor entries that would be created when it is eliminated. - * Override this method if you want a different rule. - */ - def eliminationOrder(factors: Traversable[Factor[T]], toPreserve: Traversable[Variable[_]]): List[Variable[_]] = { - val eliminableVars = (Set[Variable[_]]() /: factors)(_ ++ _.variables) -- toPreserve - var initialGraph = new VEGraph(factors) - val candidates = new HeapPriorityMap[Variable[_], Double] - eliminableVars foreach (v => candidates += v -> initialGraph.score(v)) - eliminationOrderHelper(candidates, toPreserve, initialGraph, List()) - } - - @tailrec private def eliminationOrderHelper(candidates: PriorityMap[Variable[_], Double], - toPreserve: Traversable[Variable[_]], - graph: VEGraph, - accum: List[Variable[_]]): List[Variable[_]] = { - if (candidates.isEmpty) accum.reverse - else { - val best = candidates.extractMin()._1 - // do not read the best variable after it has been removed, and do not add the preserved variables - val touched = graph.info(best).neighbors - best -- toPreserve - val nextGraph = graph.eliminate(best) - touched foreach (v => candidates += v -> graph.score(v)) - eliminationOrderHelper(candidates, toPreserve, nextGraph, best :: accum) - } - } - - private[figaro] def ve(): Unit = { - //expand() - val (neededElements, _) = getNeededElements(starterElements, Int.MaxValue) - val allFactors = optionallyShowTiming(getFactors(neededElements, targetElements), "Getting factors") - val targetVariables = targetElements.map(Variable(_)) - doElimination(allFactors, targetVariables) - } - - protected def doElimination(allFactors: List[Factor[T]], targetVariables: Seq[Variable[_]]) { - recordingFactors = List() - if (debug) { - println("*****************\nStarting factors\n") - allFactors.foreach((f: Factor[_]) => println(f.toReadableString)) - } - val order = optionallyShowTiming(eliminationOrder(allFactors, targetVariables), "Computing elimination order") - val factorsAfterElimination = - optionallyShowTiming(eliminateInOrder(order, Set(allFactors: _*), initialFactorMap(allFactors)), "Elimination") - if (debug) println("*****************") - if (debug) factorsAfterElimination foreach (f => println(f.toReadableString)) - optionallyShowTiming(finish(factorsAfterElimination, order), "Finalizing") - if (debug) targetFactors.values foreach (f => println(f.toReadableString)) - } - - protected[figaro] var targetFactors: Map[Element[_], Factor[T]] = Map() - - /** - * All implementation of variable elimination must specify what to do after variables have been eliminated. - */ - def finish(factorsAfterElimination: Set[Factor[T]], eliminationOrder: List[Variable[_]]): Unit - - def run() = ve() - -} - -/** - * Variable elimination over probabilistic factors. - */ -trait ProbabilisticVariableElimination extends VariableElimination[Double] { - def getFactors(allElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[Double]] = { - if (debug) { - println("Elements appearing in factors and their ranges:") - for { element <- allElements } { - println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) - } - } - Factory.removeFactors() - val thisUniverseFactors = allElements flatMap (Factory.make(_)) - val dependentUniverseFactors = - for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) - dependentUniverseFactors ::: thisUniverseFactors - } - -} - -/** - * Variable elimination algorithm that computes the conditional probability of query elements. - * - */ -class ProbQueryVariableElimination(override val universe: Universe, targets: Element[_]*)( - val showTiming: Boolean, - val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) - extends OneTimeProbQuery - with ProbabilisticVariableElimination { - val targetElements = targets.toList - lazy val queryTargets = targets.toList - - val semiring = SumProductSemiring - private def marginalizeToTarget(factor: Factor[Double], target: Element[_]): Unit = { - val unnormalizedTargetFactor = factor.marginalizeTo(semiring, Variable(target)) - val z = unnormalizedTargetFactor.foldLeft(semiring.zero, _ + _) - //val targetFactor = Factory.make[Double](unnormalizedTargetFactor.variables) - val targetFactor = unnormalizedTargetFactor.mapTo((d: Double) => d / z, unnormalizedTargetFactor.variables) - targetFactors += target -> targetFactor - } - - private def marginalize(resultFactor: Factor[Double]) = - targets foreach (marginalizeToTarget(resultFactor, _)) - - private def makeResultFactor(factorsAfterElimination: Set[Factor[Double]]): Factor[Double] = { - // It is possible that there are no factors (this will happen if there are no queries or evidence). - // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. - factorsAfterElimination.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - } - - def finish(factorsAfterElimination: Set[Factor[Double]], eliminationOrder: List[Variable[_]]) = - marginalize(makeResultFactor(factorsAfterElimination)) - - def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = { - val factor = targetFactors(target) - val targetVar = Variable(target) - val dist = targetVar.range.filter(_.isRegular).map(_.value).zipWithIndex map (pair => (factor.get(List(pair._2)), pair._1)) - // normalization is unnecessary here because it is done in marginalizeTo - dist.toStream - } - - def computeExpectation[T](target: Element[T], function: T => Double): Double = { - def get(pair: (Double, T)) = pair._1 * function(pair._2) - (0.0 /: computeDistribution(target))(_ + get(_)) - } -} - -object VariableElimination { - /** - * Create a variable elimination computer with the given target query variables in the current default - * universe. - */ - def apply(targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryVariableElimination(universe, targets: _*)( - false, - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) - - /** - * Create a variable elimination computer with the given target query variables in the current default - * universe, with debug information enabled. - */ - def debugged(targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryVariableElimination(universe, targets: _*)( - true, - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) { debug = true } - /** - * Create a variable elimination computer with the given target query variables in the current default - * universe, with timing information enabled. - */ - def timed(targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryVariableElimination(universe, targets: _*)( - true, - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) - - /** - * Create a variable elimination computer with the given target query variables and using the given - * dependent universes in the current default universe. - */ - def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryVariableElimination(universe, targets: _*)( - false, - dependentUniverses, - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) - - /** - * Create a variable elimination computer with the given target query variables and using the given - * dependent universes in the current default universe. Use the given dependent algorithm function to - * determine the algorithm to use to compute probability of evidence in each dependent universe. - */ - def apply( - dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, - targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryVariableElimination(universe, targets: _*)( - false, - dependentUniverses, - dependentAlgorithm) - - /** - * Use VE to compute the probability that the given element satisfies the given predicate. - */ - def probability[T](target: Element[T], predicate: T => Boolean): Double = { - val alg = VariableElimination(target) - alg.start() - val result = alg.probability(target, predicate) - alg.kill() - result - } - - /** +/* + * VariableElimination.scala + * Variable elimination algorithm. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored + +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.util._ +import annotation.tailrec +import scala.collection.mutable.{ Map, Set } +import scala.language.postfixOps + +/** + * Trait of algorithms that perform variable elimination. + * + * @tparam T The type of entries in the factors. + */ +trait VariableElimination[T] extends FactoredAlgorithm[T] with OneTime { + + /** + * By default, implementations that inherit this trait have no debug information. + * Override this if you want a debugging option. + */ + var debug: Boolean = false + + /** + * The universe on which this variable elimination algorithm should be applied. + */ + val universe: Universe + + /** + * Target elements that should not be eliminated but should be available for querying. + */ + val targetElements: List[Element[_]] + + /** + * Elements towards which queries are directed. By default, these are the target elements. + * This is overridden by DecisionVariableElimination, where it also includes utility variables. + */ + def starterElements: List[Element[_]] = targetElements + + /** + * Flag indicating whether the run time of each step should be displayed. + */ + val showTiming: Boolean + + private def optionallyShowTiming[T](op: => T, name: String) = + if (showTiming) timed(op, name); else op + + /* + private def expand(): Unit = + optionallyShowTiming(Expand(universe), "Expansion") +*/ + + // The first element of FactorMap is the complete set of factors. + // The second element maps variables to the factors mentioning that variable. + private type FactorMap[T] = Map[Variable[_], Set[Factor[T]]] + + private def addFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) + factor)) + + private def removeFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) - factor)) + + private def initialFactorMap(factors: Traversable[Factor[T]]): FactorMap[T] = { + val map: FactorMap[T] = Map() + factors foreach (addFactor(_, map)) + map + } + + protected var recordingFactors: List[Factor[_]] = List() + + /** + * Some variable elimination algorithms, such as computing the most probable explanation, record values of + * variables as they are eliminated. Such values are stored in a factor that maps values of the other variables + * to a value of the eliminated variable. This factor is produced by finding the value of the variable that + * "maximizes" the entry associated with the value in the product factor resulting from eliminating this + * variable, for some maximization function. The recordingFunction determines which of two entries is greater + * according to the maximization function. It returns true iff the second entry is greater. The recording + * function is an option so that variable elimination algorithms that do not use it can ignore it. + */ + val comparator: Option[(T, T) => Boolean] = None + + private def eliminate( + variable: Variable[_], + factors: Set[Factor[T]], + map: FactorMap[T]): Unit = { + val varFactors = map(variable) + if (debug) { + println("*****************\nEliminating " + variable.id) + println("Input factors:") + for { factor <- varFactors } { println(factor.toReadableString) } + } + if (varFactors nonEmpty) { + val productFactor = varFactors reduceLeft (_.product(_, semiring)) + val resultFactor = productFactor.sumOver(variable, semiring) + varFactors foreach (removeFactor(_, map)) + addFactor(resultFactor, map) + comparator match { + case None => () + case Some(recorder) => recordingFactors ::= productFactor.recordArgMax(variable, recorder) + } + map -= variable + factors --= varFactors + if (debug) println("Result factor\n" + resultFactor.toReadableString) + factors += resultFactor + } + } + + private def eliminateInOrder( + order: List[Variable[_]], + factors: Set[Factor[T]], + map: FactorMap[T]): Set[Factor[T]] = + order match { + case Nil => + factors + case first :: rest => + eliminate(first, factors, map) + eliminateInOrder(rest, factors, map) + } + + /** + * Method for choosing the elimination order. + * The default order chooses first the variable that + * minimizes the number of extra factor entries that would be created when it is eliminated. + * Override this method if you want a different rule. + */ + def eliminationOrder(factors: Traversable[Factor[T]], toPreserve: Traversable[Variable[_]]): List[Variable[_]] = { + val eliminableVars = (Set[Variable[_]]() /: factors)(_ ++ _.variables) -- toPreserve + var initialGraph = new VEGraph(factors) + val candidates = new HeapPriorityMap[Variable[_], Double] + eliminableVars foreach (v => candidates += v -> initialGraph.score(v)) + eliminationOrderHelper(candidates, toPreserve, initialGraph, List()) + } + + @tailrec private def eliminationOrderHelper(candidates: PriorityMap[Variable[_], Double], + toPreserve: Traversable[Variable[_]], + graph: VEGraph, + accum: List[Variable[_]]): List[Variable[_]] = { + if (candidates.isEmpty) accum.reverse + else { + val best = candidates.extractMin()._1 + // do not read the best variable after it has been removed, and do not add the preserved variables + val touched = graph.info(best).neighbors - best -- toPreserve + val nextGraph = graph.eliminate(best) + touched foreach (v => candidates += v -> graph.score(v)) + eliminationOrderHelper(candidates, toPreserve, nextGraph, best :: accum) + } + } + + private[figaro] def ve(): Unit = { + //expand() + val (neededElements, _) = getNeededElements(starterElements, Int.MaxValue) + val allFactors = optionallyShowTiming(getFactors(neededElements, targetElements), "Getting factors") + val targetVariables = targetElements.map(Variable(_)) + doElimination(allFactors, targetVariables) + } + + protected def doElimination(allFactors: List[Factor[T]], targetVariables: Seq[Variable[_]]) { + recordingFactors = List() + if (debug) { + println("*****************\nStarting factors\n") + allFactors.foreach((f: Factor[_]) => println(f.toReadableString)) + } + val order = optionallyShowTiming(eliminationOrder(allFactors, targetVariables), "Computing elimination order") + val factorsAfterElimination = + optionallyShowTiming(eliminateInOrder(order, Set(allFactors: _*), initialFactorMap(allFactors)), "Elimination") + if (debug) println("*****************") + if (debug) factorsAfterElimination foreach (f => println(f.toReadableString)) + optionallyShowTiming(finish(factorsAfterElimination, order), "Finalizing") + if (debug) targetFactors.values foreach (f => println(f.toReadableString)) + } + + protected[figaro] var targetFactors: Map[Element[_], Factor[T]] = Map() + + /** + * All implementation of variable elimination must specify what to do after variables have been eliminated. + */ + def finish(factorsAfterElimination: Set[Factor[T]], eliminationOrder: List[Variable[_]]): Unit + + def run() = ve() + +} + +/** + * Variable elimination over probabilistic factors. + */ +trait ProbabilisticVariableElimination extends VariableElimination[Double] { + def getFactors(allElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[Double]] = { + if (debug) { + println("Elements appearing in factors and their ranges:") + for { element <- allElements } { + println(Variable(element).id + "(" + element.name.string + "@" + element.hashCode + ")" + ": " + element + ": " + Variable(element).range.mkString(",")) + } + } + Factory.removeFactors() + val thisUniverseFactors = allElements flatMap (Factory.make(_)) + val dependentUniverseFactors = + for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) + dependentUniverseFactors ::: thisUniverseFactors + } + +} + +/** + * Variable elimination algorithm that computes the conditional probability of query elements. + * + */ +class ProbQueryVariableElimination(override val universe: Universe, targets: Element[_]*)( + val showTiming: Boolean, + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) + extends OneTimeProbQuery + with ProbabilisticVariableElimination { + val targetElements = targets.toList + lazy val queryTargets = targets.toList + + val semiring = SumProductSemiring + private def marginalizeToTarget(factor: Factor[Double], target: Element[_]): Unit = { + val unnormalizedTargetFactor = factor.marginalizeTo(semiring, Variable(target)) + val z = unnormalizedTargetFactor.foldLeft(semiring.zero, _ + _) + //val targetFactor = Factory.make[Double](unnormalizedTargetFactor.variables) + val targetFactor = unnormalizedTargetFactor.mapTo((d: Double) => d / z) + targetFactors += target -> targetFactor + } + + private def marginalize(resultFactor: Factor[Double]) = + targets foreach (marginalizeToTarget(resultFactor, _)) + + private def makeResultFactor(factorsAfterElimination: Set[Factor[Double]]): Factor[Double] = { + // It is possible that there are no factors (this will happen if there are no queries or evidence). + // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. + factorsAfterElimination.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + } + + def finish(factorsAfterElimination: Set[Factor[Double]], eliminationOrder: List[Variable[_]]) = + marginalize(makeResultFactor(factorsAfterElimination)) + + /** + * Computes the normalized distribution over a single target element. + */ + def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = { + val factor = targetFactors(target) + val targetVar = Variable(target) + val dist = targetVar.range.filter(_.isRegular).map(_.value).zipWithIndex map (pair => (factor.get(List(pair._2)), pair._1)) + // normalization is unnecessary here because it is done in marginalizeTo + dist.toStream + } + + /** + * Computes the expectation of a given function for single target element. + */ + def computeExpectation[T](target: Element[T], function: T => Double): Double = { + def get(pair: (Double, T)) = pair._1 * function(pair._2) + (0.0 /: computeDistribution(target))(_ + get(_)) + } +} + +object VariableElimination { + /** + * Create a variable elimination computer with the given target query variables in the current default + * universe. + */ + def apply(targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryVariableElimination(universe, targets: _*)( + false, + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + + /** + * Create a variable elimination computer with the given target query variables in the current default + * universe, with debug information enabled. + */ + def debugged(targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryVariableElimination(universe, targets: _*)( + true, + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) { debug = true } + /** + * Create a variable elimination computer with the given target query variables in the current default + * universe, with timing information enabled. + */ + def timed(targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryVariableElimination(universe, targets: _*)( + true, + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + + /** + * Create a variable elimination computer with the given target query variables and using the given + * dependent universes in the current default universe. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryVariableElimination(universe, targets: _*)( + false, + dependentUniverses, + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + + /** + * Create a variable elimination computer with the given target query variables and using the given + * dependent universes in the current default universe. Use the given dependent algorithm function to + * determine the algorithm to use to compute probability of evidence in each dependent universe. + */ + def apply( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryVariableElimination(universe, targets: _*)( + false, + dependentUniverses, + dependentAlgorithm) + + /** + * Use VE to compute the probability that the given element satisfies the given predicate. + */ + def probability[T](target: Element[T], predicate: T => Boolean): Double = { + val alg = VariableElimination(target) + alg.start() + val result = alg.probability(target, predicate) + alg.kill() + result + } + + /** * Use VE to compute the probability that the given element has the given value. - */ - def probability[T](target: Element[T], value: T): Double = - probability(target, (t: T) => t == value) -} + */ + def probability[T](target: Element[T], value: T): Double = + probability(target, (t: T) => t == value) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BPNode.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BPNode.scala index a1052840..cd46949e 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BPNode.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BPNode.scala @@ -13,23 +13,23 @@ package com.cra.figaro.algorithm.factored.beliefpropagation -import com.cra.figaro.algorithm.factored.Variable +import com.cra.figaro.algorithm.factored.factors.Variable /** - * Trait for Nodes used in a Factor Graph + * Trait for Nodes used in a Factor Graph. */ trait Node /** - * Class for FactorNodes in a FactorGraph + * Class for FactorNodes in a FactorGraph. */ final case class FactorNode(val variables: Set[Variable[_]]) extends Node { override def toString() = "F(" + variables.map(_.id).mkString(",") + ")" } /** - * Class for VariableNodes in a FactorGraph + * Class for VariableNodes in a FactorGraph. */ case class VariableNode(val variable: Variable[_]) extends Node { override def toString() = "V(" + variable.id + ")" diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BasicFactorGraph.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BasicFactorGraph.scala index f375a219..a93a47e7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BasicFactorGraph.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BasicFactorGraph.scala @@ -1,85 +1,107 @@ -/* - * BasicFactorGraph.scala - * A basic factor graph for double factors - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Jan 15, 2014 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored.beliefpropagation - -import scala.collection.Iterable -import scala.collection.immutable.List -import com.cra.figaro.algorithm.factored._ - -/** - * The basic implementation of FactorGraph for Probabilistic factors in BP - */ -class BasicFactorGraph(factors: List[Factor[Double]], semiring: Semiring[Double]) - extends FactorGraph[Double] { - - def uniformFactor(v: List[Variable[_]]): Factor[Double] = { - val f = new BasicFactor[Double](v) - f.fillByRule((l: List[Any]) => semiring.one) - f - } - - // Combine all factors of the same variables into a single factor - private def combineFactors() = factors.groupBy(_.variables.map(_.id)).values.map(_.reduceLeft(_.product(_, semiring))).toList - - /* - * Create Nodes for factors - */ - private def adjacencyListFactors(): Map[Node, Map[Node, Factor[Double]]] = { - factorsByNode.map { factors => - (factors._1 -> { - Map[Node, Factor[Double]]() ++ factors._2.variables.map(v => VariableNode(v) -> uniformFactor(List(v))) - }) - } - } - - /* - * Create Nodes for Variables - */ - private def adjacencyListVariables(): Map[Node, Map[Node, Factor[Double]]] = { - val adjacencyListVariables = factorsByNode.map(f => f._2.variables.map(v => VariableNode(v) -> f._1)).flatten - - // Group them by common variables - val adjacencyListGrouped = adjacencyListVariables.groupBy(_._1) - - Map[Node, Map[Node, Factor[Double]]]() ++ adjacencyListGrouped.map(e => { - e._1 -> (Map[Node, Factor[Double]]() ++ e._2.map(f => f._2 -> uniformFactor(List(f._1.variable)))) - }) - } - - def toMutableMap(m: Map[Node, Factor[Double]]): scala.collection.mutable.Map[Node, Factor[Double]] = - scala.collection.mutable.Map[Node, Factor[Double]]() ++ m - - private[figaro] val factorsByNode = combineFactors.map(factor => (new FactorNode(factor.variables.toSet) -> (factor))).toMap - - private[figaro] val adjacencyList = (adjacencyListFactors() ++ adjacencyListVariables()).map(m => m._1 -> toMutableMap(m._2)) - - def getNodes(): Iterable[Node] = adjacencyList.keys - - def getNeighbors(source: Node): Iterable[Node] = adjacencyList(source).keys - - def getNeighbors(source: Node, excluding: Node): Iterable[Node] = getNeighbors(source).filterNot(_ == excluding) - - def getFactorForNode(fn: FactorNode): Factor[Double] = factorsByNode(fn) - - def getMessagesForNode(node: Node): Iterable[(Node, Factor[Double])] = adjacencyList(node) - - def getLastMessage(from: Node, to: Node): Factor[Double] = adjacencyList(from)(to) - - def update(from: Node, to: Node, f: Factor[Double]): FactorGraph[Double] = { - adjacencyList(from) += (to -> f) - this - } - - def contains(v: Node): Boolean = adjacencyList.contains(v) +/* + * BasicFactorGraph.scala + * A basic factor graph for double factors + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Jan 15, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.beliefpropagation + +import scala.collection.Iterable +import scala.collection.immutable.List +import com.cra.figaro.algorithm.factored.factors._ + +/** + * The basic implementation of FactorGraph for Probabilistic factors in BP. + */ +class BasicFactorGraph(factors: List[Factor[Double]], semiring: Semiring[Double]) + extends FactorGraph[Double] { + + def uniformFactor(v: List[Variable[_]]): Factor[Double] = { + val f = new BasicFactor[Double](List(), v) + f.fillByRule((l: List[Any]) => semiring.one) + f + } + + // Combine all factors of the same variables into a single factor + private def combineFactors() = factors.groupBy(_.variables.map(_.id)).values.map(_.reduceLeft(_.product(_, semiring))).toList + + /* + * Create Nodes for factors + */ + private def adjacencyListFactors(): Map[Node, Map[Node, Factor[Double]]] = { + factorsByNode.map { factors => + (factors._1 -> { + Map[Node, Factor[Double]]() ++ factors._2.variables.map(v => VariableNode(v) -> uniformFactor(List(v))) + }) + } + } + + /* + * Create Nodes for Variables + */ + private def adjacencyListVariables(): Map[Node, Map[Node, Factor[Double]]] = { + val adjacencyListVariables = factorsByNode.map(f => f._2.variables.map(v => VariableNode(v) -> f._1)).flatten + + // Group them by common variables + val adjacencyListGrouped = adjacencyListVariables.groupBy(_._1) + + Map[Node, Map[Node, Factor[Double]]]() ++ adjacencyListGrouped.map(e => { + e._1 -> (Map[Node, Factor[Double]]() ++ e._2.map(f => f._2 -> uniformFactor(List(f._1.variable)))) + }) + } + + def toMutableMap(m: Map[Node, Factor[Double]]): scala.collection.mutable.Map[Node, Factor[Double]] = + scala.collection.mutable.Map[Node, Factor[Double]]() ++ m + + private[figaro] val factorsByNode = combineFactors.map(factor => (new FactorNode(factor.variables.toSet) -> (factor))).toMap + + private[figaro] val adjacencyList = (adjacencyListFactors() ++ adjacencyListVariables()).map(m => m._1 -> toMutableMap(m._2)) + + /** + * Returns all nodes in the factor graph. + */ + def getNodes(): Iterable[Node] = adjacencyList.keys + + /** + * Returns all neighbors of a given node. + */ + def getNeighbors(source: Node): Iterable[Node] = adjacencyList(source).keys + + /** + * Returns all neighbors of a given node excluding the node of the second argument. + */ + def getNeighbors(source: Node, excluding: Node): Iterable[Node] = getNeighbors(source).filterNot(_ == excluding) + + /** + * Gets the factor for a particular factor node. + */ + def getFactorForNode(fn: FactorNode): Factor[Double] = factorsByNode(fn) + + /** + * Get a list of messages to the node. + */ + def getMessagesForNode(node: Node): Iterable[(Node, Factor[Double])] = adjacencyList(node) + + /** + * Gets the last message to a node from another. + */ + def getLastMessage(from: Node, to: Node): Factor[Double] = adjacencyList(from)(to) + + /** + * Updates the factor graph with a message from a node to another. + * Returns a new factor graph, which can be the same as this one. + */ + def update(from: Node, to: Node, f: Factor[Double]): FactorGraph[Double] = { + adjacencyList(from) += (to -> f) + this + } + + def contains(v: Node): Boolean = adjacencyList.contains(v) } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BeliefPropagation.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BeliefPropagation.scala index dd0fa53c..ed766ca1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BeliefPropagation.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/BeliefPropagation.scala @@ -1,521 +1,507 @@ -/* - * BeliefPropagation.scala - * A belief propagation algorithm. - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Jan 15, 2014 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored.beliefpropagation - -import scala.Option.option2Iterable -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.sampling._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import annotation.tailrec -import com.cra.figaro.algorithm.OneTimeProbQuery -import com.cra.figaro.algorithm.ProbQueryAlgorithm -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.factored.Variable -import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler -import com.cra.figaro.language.Element -import com.cra.figaro.language.Universe -import com.cra.figaro.algorithm.lazyfactored.LazyValues -import com.cra.figaro.algorithm.lazyfactored.BoundedProbFactor -import scala.collection.mutable.Map - -/** - * Trait for performing belief propagation. - * - * @tparam T The type of entries in the factors. - */ -trait BeliefPropagation[T] extends FactoredAlgorithm[T] { - - /** - * By default, implementations that inherit this trait have no debug information. - * Override this if you want a debugging option. - */ - val debug: Boolean = false - - /** - * The universe on which this belief propagation algorithm should be applied. - */ - val universe: Universe - - /** - * Target elements that should not be eliminated but should be available for querying. - */ - val targetElements: List[Element[_]] - - /** - * Since BP uses division to compute messages, the semiring has to have a division function defined - */ - override val semiring: DivideableSemiRing[T] - - /** - * Elements towards which queries are directed. By default, these are the target elements. - * This is overridden by DecisionVariableElimination, where it also includes utility variables. - */ - def starterElements: List[Element[_]] = targetElements - - /* The factor graph for this BP object */ - protected[figaro] var factorGraph: FactorGraph[T] = _ - - /* The beliefs associated with each node in the factor graph. The belief is the product - * of all messages to the node times any factor at the node - */ - private[figaro] val beliefMap: Map[Node, Factor[T]] = Map() - - /* - * Returns a new message from a source node to a target node. - */ - protected[figaro] def newMessage(source: Node, target: Node): Factor[T] = { - val message: Factor[T] = (source, target) match { - case (f: FactorNode, v: VariableNode) => getNewMessageFactorToVar(f, v) - case (v: VariableNode, f: FactorNode) => getNewMessageVarToFactor(v, f) - case _ => throw new UnsupportedOperationException() - } - - if (debug) { - println("message: " + source + " to " + target) - println(message.toReadableString) - } - message - } - - /* - * A message from a factor Node to a variable Node is the product of the factor with - * messages from all other Nodes (except the destination node), - * marginalized over all variables except the variable: - */ - private def getNewMessageFactorToVar(fn: FactorNode, vn: VariableNode) = { - val vnFactor = factorGraph.getLastMessage(vn, fn) - - val total = beliefMap(fn).combination(vnFactor, semiring.divide) - total.marginalizeTo(semiring, vn.variable) - } - - /* - * A message from a variable Node to a factor Node is the product of the messages from - * all other neighboring factor Nodes (except the recipient; alternatively one can say the - * recipient sends the message "1"): - */ - private def getNewMessageVarToFactor(vn: VariableNode, fn: FactorNode) = { - val fnFactor = factorGraph.getLastMessage(fn, vn) - - val total = beliefMap(vn).combination(fnFactor, semiring.divide) - total - } - - /** - * Returns the product of all messages from a source node's neighbors to itself. - */ - def belief(source: Node) = { - val messageList = factorGraph.getNeighbors(source) map (factorGraph.getLastMessage(_, source)) - - val f = if (messageList.isEmpty) { - source match { - case fn: FactorNode => factorGraph.uniformFactor(fn.variables.toList) - case vn: VariableNode => factorGraph.uniformFactor(List(vn.variable)) - } - } else { - val messageBelief = messageList.reduceLeft(_.product(_, semiring)) - source match { - case fn: FactorNode => messageBelief.product(factorGraph.getFactorForNode(fn), semiring) - case vn: VariableNode => messageBelief - } - } - f - } - - /* - * This is intended to perform an asynchronous update of the factor graph. - * It is unclear if this is the correct implementation since messages - * are updating in the factor graph immediately - */ - private def asynchronousUpdate(): Unit = { - factorGraph.getNodes.foreach { node1 => - factorGraph.getNeighbors(node1).foreach { node2 => - factorGraph.update(node1, node2, newMessage(node1, node2)) - } - } - // Update the beliefs of each node - factorGraph.getNodes.foreach(n => beliefMap.update(n, belief(n))) - } - - /* - * Propagates one set of synchronous message in the graph - */ - private def synchronousUpdate(): Unit = { - val updates = factorGraph.getNodes.flatMap { node1 => - factorGraph.getNeighbors(node1).map { node2 => - (node1, node2, newMessage(node1, node2)) - } - } - updates.foreach { u => factorGraph.update(u._1, u._2, u._3) } - // Update the beliefs of each node - factorGraph.getNodes.foreach(n => beliefMap.update(n, belief(n))) - } - - /** - * Runs this belief propagation algorithm for one iteration. An iteration - * consists of each node of the factor graph sending a message to each of its neighbors. - */ - def runStep() { - if (debug) { - println("Factor graph: ") - println(factorGraph.getNodes.map(n => n -> factorGraph.getNeighbors(n)).toMap.mkString("\n")) - println() - } - synchronousUpdate() - if (debug) { - beliefMap.foreach(a => println(a._1 + " => " + a._2)); println - println("Factor Messages:") - factorGraph.getNodes.foreach{n => - println(n + ": ") - println(factorGraph.getMessagesForNode(n)) - } - } - } - - override def initialize() = { - factorGraph.getNodes.foreach(n => beliefMap.update(n, belief(n))) - } - -} - -/** - * Trait for probabilistic BP algorithms - */ -trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { - - /** - * Normalize a factor - */ - def normalize(factor: Factor[Double]): Factor[Double] = { - val z = semiring.sumMany(factor.contents.values) - // Since we're in log space, d - z = log(exp(d)/exp(z)) - factor.mapTo((d: Double) => if (z != semiring.zero) d - z else semiring.zero, factor.variables) - } - - /* - * Overrides newMessage in the BP with normalization at the end - */ - override protected[figaro] def newMessage(source: Node, target: Node): Factor[Double] = { - val newMessage = super.newMessage(source, target) - normalize(newMessage) - } - - /** - * Returns the factors needed for BP. Since BP operates on a complete factor graph, factors are created - * for all elements in the universe. - */ - def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = { - - val thisUniverseFactors = (neededElements flatMap (BoundedProbFactor.make(_, upperBounds))).filterNot(_.isEmpty) - val dependentUniverseFactors = - for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) - val factors = dependentUniverseFactors ::: thisUniverseFactors - // To prevent underflow, we do all computation in log space - factors.map(makeLogarithmic(_)) - } - - private[figaro] def makeLogarithmic(factor: Factor[Double]): Factor[Double] = { - factor.mapTo((d: Double) => Math.log(d), factor.variables) - } - - private[figaro] def unmakeLogarithmic(factor: Factor[Double]): Factor[Double] = { - factor.mapTo((d: Double) => Math.exp(d), factor.variables) - } - - /** - * Get the belief for an element - */ - protected[figaro] def getBeliefsForElement[T](target: Element[T]): List[(Double, T)] = { - val finalFactor = getFinalFactorForElement(target) - if (finalFactor.isEmpty) { - List[(Double, T)]() - } else { - val factor = normalize(finalFactor) - val factorVariable = Variable(target) - // Since all computations have been in log space, we get out of log space here to provide the final beliefs - factorVariable.range.zipWithIndex.map(pair => (Math.exp(factor.get(List(pair._2))), pair._1.value)) - } - } - - /** - * Get the final factor for an element - */ - def getFinalFactorForElement[T](target: Element[T]): Factor[Double] = { - val targetVar = Variable(target) - val targetNode = factorGraph.getNodes.find { node => - node match { - case vn: VariableNode => vn.variable == targetVar - case _ => false - } - } - beliefMap(targetNode.get) - } - -} - -/** - * Trait for One Time BP algorithms - */ -trait OneTimeProbabilisticBeliefPropagation extends ProbabilisticBeliefPropagation with OneTime { - val iterations: Int - def run() = { - if (debug) { - val varNodes = factorGraph.getNodes.filter(_.isInstanceOf[VariableNode]) - val allVars = (Set[Variable[_]]() /: factorGraph.getNodes)((s: Set[Variable[_]], n: Node) => { - val a = (n match { - case vn: VariableNode => Set(vn.variable) - case fn: FactorNode => fn.variables - }) - s ++ a - }) - println("*****************\nElement ids:") - for { variable <- allVars } { - variable match { - case elemVar: /*Extended*/ ElementVariable[_] => - println(variable.id + "(" + elemVar.element.name.string + ")" + "@" + elemVar.element.hashCode + ": " + elemVar.element) - case _ => - println(variable.id + ": not an element variable") - } - } - println("*****************\nOriginal Factors:") - factorGraph.getNodes.foreach { n => - n match { - case fn: FactorNode => println(factorGraph.getFactorForNode(fn).toReadableString) - case _ => - } - } - println("*****************") - } - - for { i <- 1 to iterations } { runStep() } - } -} - -/** - * Trait for Anytime BP algorithms - */ -trait AnytimeProbabilisticBeliefPropagation extends ProbabilisticBeliefPropagation with Anytime - -/** - * Class to implement a probability query BP algorithm - */ -abstract class ProbQueryBeliefPropagation(override val universe: Universe, targets: Element[_]*)( - val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, - depth: Int = Int.MaxValue, upperBounds: Boolean = false) - extends ProbQueryAlgorithm - with ProbabilisticBeliefPropagation with ProbEvidenceBeliefPropagation { - - val targetElements = targets.toList - - val queryTargets = targetElements - - val semiring = LogSumProductSemiring - - var neededElements: List[Element[_]] = _ - var needsBounds: Boolean = _ - - def generateGraph() = { - val needs = getNeededElements(starterElements, depth) - neededElements = needs._1 - needsBounds = needs._2 - - // Depth < MaxValue implies we are using bounds - val factors = if (depth < Int.MaxValue && needsBounds) { - getFactors(neededElements, targetElements, upperBounds) - } else { - getFactors(neededElements, targetElements) - } - - factorGraph = new BasicFactorGraph(factors, semiring): FactorGraph[Double] - } - - override def initialize() = { - if (factorGraph == null) generateGraph() - super.initialize - } - - def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = getBeliefsForElement(target).toStream - - def computeExpectation[T](target: Element[T], function: T => Double): Double = { - computeDistribution(target).map((pair: (Double, T)) => pair._1 * function(pair._2)).sum - } -} - -trait ProbEvidenceBeliefPropagation extends ProbabilisticBeliefPropagation { - - def logFcn: (Double => Double) = semiring match { - case LogSumProductSemiring => (d: Double) => d - case SumProductSemiring => (d: Double) => if (d == semiring.zero) Double.NegativeInfinity else math.log(d) - } - def probFcn: (Double => Double) = semiring match { - case LogSumProductSemiring => (d: Double) => if (d == semiring.zero) 0 else math.exp(d) - case SumProductSemiring => (d: Double) => d - } - - def entropy(probFactor: Factor[Double], logFactor: Factor[Double]): Double = { - //println("probfactor: " + probFactor.toReadableString) - //println("logfactor: " + logFactor.toReadableString) - - // Even though the variables in each factor are the same, the order of the vars might be different - val logFactorMapping = probFactor.variables.map(v => logFactor.variables.indexOf(v)) - def remap(l: List[Int]) = l.zipWithIndex.map(s => (s._1, logFactorMapping(s._2))).sortBy(_._2).unzip._1 - - val e = (0.0 /: probFactor.allIndices)((c: Double, i: List[Int]) => { - val p = probFcn(probFactor.get(i)) - if (p == 0) c else c + p * logFcn(logFactor.get(remap(i))) - }) - e - } - - /* Not true mutual information for > 2 factors, but standard for computing Bethe approximation */ - def mutualInformation(joint: Factor[Double], marginals: Iterable[Factor[Double]]) = { - println(joint.toReadableString) - marginals foreach (f => println(f.toReadableString)) - val newFactor = (joint /: marginals)((c: Factor[Double], n: Factor[Double]) => c.combination(n, semiring.divide)) - val mi = (0.0 /: newFactor.allIndices)((c: Double, i: List[Int]) => { - val p = probFcn(joint.get(i)) - if (p == 0) c else c + p * logFcn(newFactor.get(i)) - }) - mi - } - - def computeEvidence(): Double = { - - //println("Computing P(Evidence)") - val factorNodes = factorGraph.getNodes.filter(_.isInstanceOf[FactorNode]).toList - val varNodes = factorGraph.getNodes.filter(_.isInstanceOf[VariableNode]).toList - - val nonZeroEvidence = factorNodes.exists(p => beliefMap(p).contents.exists(_._2 != Double.NegativeInfinity)) - - if (nonZeroEvidence) { - //println("Computing energy") - val betheEnergy = -1 * factorNodes.map(f => { - entropy(normalize(beliefMap(f)), factorGraph.getFactorForNode(f.asInstanceOf[FactorNode])) - }).sum - //println("Computing entropy") - val betheEntropy = { - val factorEntropy = -1 * factorNodes.map(f => { - entropy(normalize(beliefMap(f)), normalize(beliefMap(f))) - }).sum - val varEntropy = varNodes.map(v => { - (factorGraph.getNeighbors(v).size - 1) * entropy(normalize(beliefMap(v)), normalize(beliefMap(v))) - }).sum - factorEntropy + varEntropy - } - //println("energy: " + betheEnergy + ", entropy: " + betheEntropy) - math.exp(-1 * (betheEnergy - betheEntropy)) - } else { - 0.0 - } - } - -} - -object BeliefPropagation { - /** - * Creates a One Time belief propagation computer in the current default universe. - */ - def apply(myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } - - /** - * Creates a Anytime belief propagation computer in the current default universe. - */ - def apply(targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery - - /** - * Create a One Time belief propagation computer current default universe, with debug information enabled. - */ - def debugged(myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - List(), - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations; override val debug = true } - - /** - * Create a Anytime belief propagation computer using the given dependent universes in the current default universe. - */ - def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - dependentUniverses, - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } - - /** - * Create a One Time belief propagation computer using the given dependent universes in the current default universe. - */ - def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - dependentUniverses, - (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery - - /** - * Create a One Time belief propagation computer using the given dependent universes in the current - * default universe. Use the given dependent algorithm function to determine the algorithm to use - * to compute probability of evidence in each dependent universe. - */ - def apply( - dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, - myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - dependentUniverses, - dependentAlgorithm) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } - - /** - * Create a Anytime belief propagation computer using the given dependent universes in the current - * default universe. Use the given dependent algorithm function to determine the algorithm to use - * to compute probability of evidence in each dependent universe. - */ - def apply( - dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - dependentUniverses, - dependentAlgorithm) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery - - /** - * Use BP to compute the probability that the given element satisfies the given predicate. - */ - def probability[T](target: Element[T], predicate: T => Boolean): Double = { - val alg = BeliefPropagation(10, target) - alg.start() - val result = alg.probability(target, predicate) - alg.kill() - result - } - - /** - * Use BP to compute the probability that the given element has the given value. - */ - def probability[T](target: Element[T], value: T): Double = - probability(target, (t: T) => t == value) - - /** - * Lazy version of BP that operates only on bounds - */ - def lazyBP(myIterations: Int, depth: Int, upperBounds: Boolean, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryBeliefPropagation(universe, targets: _*)( - List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), - depth, upperBounds) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations; override val debug = false } - -} - - - +/* + * BeliefPropagation.scala + * A belief propagation algorithm. + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Jan 15, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.beliefpropagation + +import scala.Option.option2Iterable +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import annotation.tailrec +import com.cra.figaro.algorithm.OneTimeProbQuery +import com.cra.figaro.algorithm.ProbQueryAlgorithm +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.factory._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler +import com.cra.figaro.language.Element +import com.cra.figaro.language.Universe +import com.cra.figaro.algorithm.lazyfactored.LazyValues +import com.cra.figaro.algorithm.lazyfactored.BoundedProbFactor +import scala.collection.mutable.Map + +/** + * Trait for performing belief propagation. + * + * @tparam T The type of entries in the factors. + */ +trait BeliefPropagation[T] extends FactoredAlgorithm[T] { + + /** + * By default, implementations that inherit this trait have no debug information. + * Override this if you want a debugging option. + */ + val debug: Boolean = false + + /** + * The universe on which this belief propagation algorithm should be applied. + */ + val universe: Universe + + /** + * Target elements that should not be eliminated but should be available for querying. + */ + val targetElements: List[Element[_]] + + /** + * Since BP uses division to compute messages, the semiring has to have a division function defined + */ + override val semiring: DivideableSemiRing[T] + + /** + * Elements towards which queries are directed. By default, these are the target elements. + * This is overridden by DecisionVariableElimination, where it also includes utility variables. + */ + def starterElements: List[Element[_]] = targetElements + + /* The factor graph for this BP object */ + protected[figaro] var factorGraph: FactorGraph[T] = _ + + /* The beliefs associated with each node in the factor graph. The belief is the product + * of all messages to the node times any factor at the node + */ + private[figaro] val beliefMap: Map[Node, Factor[T]] = Map() + + /* + * Returns a new message from a source node to a target node. + */ + protected[figaro] def newMessage(source: Node, target: Node): Factor[T] = { + val message: Factor[T] = (source, target) match { + case (f: FactorNode, v: VariableNode) => getNewMessageFactorToVar(f, v) + case (v: VariableNode, f: FactorNode) => getNewMessageVarToFactor(v, f) + case _ => throw new UnsupportedOperationException() + } + + if (debug) { + println("message: " + source + " to " + target) + println(message.toReadableString) + } + message + } + + /* + * A message from a factor Node to a variable Node is the product of the factor with + * messages from all other Nodes (except the destination node), + * marginalized over all variables except the variable: + */ + private def getNewMessageFactorToVar(fn: FactorNode, vn: VariableNode) = { + val vnFactor = factorGraph.getLastMessage(vn, fn) + + val total = beliefMap(fn).combination(vnFactor, semiring.divide) + total.marginalizeTo(semiring, vn.variable) + } + + /* + * A message from a variable Node to a factor Node is the product of the messages from + * all other neighboring factor Nodes (except the recipient; alternatively one can say the + * recipient sends the message "1"): + */ + private def getNewMessageVarToFactor(vn: VariableNode, fn: FactorNode) = { + val fnFactor = factorGraph.getLastMessage(fn, vn) + + val total = beliefMap(vn).combination(fnFactor, semiring.divide) + total + } + + /** + * Returns the product of all messages from a source node's neighbors to itself. + */ + def belief(source: Node) = { + val messageList = factorGraph.getNeighbors(source) map (factorGraph.getLastMessage(_, source)) + + val f = if (messageList.isEmpty) { + source match { + case fn: FactorNode => factorGraph.uniformFactor(fn.variables.toList) + case vn: VariableNode => factorGraph.uniformFactor(List(vn.variable)) + } + } else { + val messageBelief = messageList.reduceLeft(_.product(_, semiring)) + source match { + case fn: FactorNode => messageBelief.product(factorGraph.getFactorForNode(fn), semiring) + case vn: VariableNode => messageBelief + } + } + f + } + + + /* + * Propagates one set of synchronous message in the graph + */ + private def synchronousUpdate(): Unit = { + val updates = factorGraph.getNodes.par.flatMap { node1 => + factorGraph.getNeighbors(node1).map { node2 => + (node1, node2, newMessage(node1, node2)) + } + } + updates.foreach { u => factorGraph.update(u._1, u._2, u._3) } + // Update the beliefs of each node + factorGraph.getNodes.foreach(n => beliefMap.update(n, belief(n))) + } + + /** + * Runs this belief propagation algorithm for one iteration. An iteration + * consists of each node of the factor graph sending a message to each of its neighbors. + */ + def runStep() { + if (debug) { + println("Factor graph: ") + println(factorGraph.getNodes.map(n => n -> factorGraph.getNeighbors(n)).toMap.mkString("\n")) + println() + } + synchronousUpdate() + if (debug) { + beliefMap.foreach(a => println(a._1 + " => " + a._2)); println + println("Factor Messages:") + factorGraph.getNodes.foreach{n => + println(n + ": ") + println(factorGraph.getMessagesForNode(n)) + } + } + } + + override def initialize() = { + factorGraph.getNodes.foreach(n => beliefMap.update(n, belief(n))) + } + +} + +/** + * Trait for probabilistic BP algorithms. + */ +trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { + + /** + * Normalize a factor. + */ + def normalize(factor: Factor[Double]): Factor[Double] = { + val z = semiring.sumMany(factor.contents.values) + // Since we're in log space, d - z = log(exp(d)/exp(z)) + factor.mapTo((d: Double) => if (z != semiring.zero) d - z else semiring.zero) + } + + /* + * Overrides newMessage in the BP with normalization at the end + */ + override protected[figaro] def newMessage(source: Node, target: Node): Factor[Double] = { + val newMessage = super.newMessage(source, target) + normalize(newMessage) + } + + /** + * Returns the factors needed for BP. Since BP operates on a complete factor graph, factors are created + * for all elements in the universe. + */ + def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = { + + val thisUniverseFactors = (neededElements flatMap (BoundedProbFactor.make(_, upperBounds))).filterNot(_.isEmpty) + val dependentUniverseFactors = + for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) + val factors = dependentUniverseFactors ::: thisUniverseFactors + // To prevent underflow, we do all computation in log space + factors.map(makeLogarithmic(_)) + } + + private[figaro] def makeLogarithmic(factor: Factor[Double]): Factor[Double] = { + factor.mapTo((d: Double) => Math.log(d)) + } + + private[figaro] def unmakeLogarithmic(factor: Factor[Double]): Factor[Double] = { + factor.mapTo((d: Double) => Math.exp(d)) + } + + /** + * Get the belief for an element. + */ + protected[figaro] def getBeliefsForElement[T](target: Element[T]): List[(Double, T)] = { + val finalFactor = getFinalFactorForElement(target) + if (finalFactor.isEmpty) { + List[(Double, T)]() + } else { + val factor = normalize(finalFactor) + val factorVariable = Variable(target) + // Since all computations have been in log space, we get out of log space here to provide the final beliefs + factorVariable.range.zipWithIndex.map(pair => (Math.exp(factor.get(List(pair._2))), pair._1.value)) + } + } + + /** + * Get the final factor for an element. + */ + def getFinalFactorForElement[T](target: Element[T]): Factor[Double] = { + val targetVar = Variable(target) + val targetNode = factorGraph.getNodes.find { node => + node match { + case vn: VariableNode => vn.variable == targetVar + case _ => false + } + } + beliefMap(targetNode.get) + } + +} + +/** + * Trait for One Time BP algorithms. + */ +trait OneTimeProbabilisticBeliefPropagation extends ProbabilisticBeliefPropagation with OneTime { + val iterations: Int + def run() = { + if (debug) { + val varNodes = factorGraph.getNodes.filter(_.isInstanceOf[VariableNode]) + val allVars = (Set[Variable[_]]() /: factorGraph.getNodes)((s: Set[Variable[_]], n: Node) => { + val a = (n match { + case vn: VariableNode => Set(vn.variable) + case fn: FactorNode => fn.variables + }) + s ++ a + }) + println("*****************\nElement ids:") + for { variable <- allVars } { + variable match { + case elemVar: /*Extended*/ ElementVariable[_] => + println(variable.id + "(" + elemVar.element.name.string + ")" + "@" + elemVar.element.hashCode + ": " + elemVar.element) + case _ => + println(variable.id + ": not an element variable") + } + } + println("*****************\nOriginal Factors:") + factorGraph.getNodes.foreach { n => + n match { + case fn: FactorNode => println(factorGraph.getFactorForNode(fn).toReadableString) + case _ => + } + } + println("*****************") + } + + for { i <- 1 to iterations } { runStep() } + } +} + +/** + * Trait for Anytime BP algorithms. + */ +trait AnytimeProbabilisticBeliefPropagation extends ProbabilisticBeliefPropagation with Anytime + +/** + * Class to implement a probability query BP algorithm. + */ +abstract class ProbQueryBeliefPropagation(override val universe: Universe, targets: Element[_]*)( + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + depth: Int = Int.MaxValue, upperBounds: Boolean = false) + extends ProbQueryAlgorithm + with ProbabilisticBeliefPropagation with ProbEvidenceBeliefPropagation { + + val targetElements = targets.toList + + val queryTargets = targetElements + + val semiring = LogSumProductSemiring + + var neededElements: List[Element[_]] = _ + var needsBounds: Boolean = _ + + def generateGraph() = { + val needs = getNeededElements(starterElements, depth) + neededElements = needs._1 + needsBounds = needs._2 + + // Depth < MaxValue implies we are using bounds + val factors = if (depth < Int.MaxValue && needsBounds) { + getFactors(neededElements, targetElements, upperBounds) + } else { + getFactors(neededElements, targetElements) + } + + factorGraph = new BasicFactorGraph(factors, semiring): FactorGraph[Double] + } + + override def initialize() = { + if (factorGraph == null) generateGraph() + super.initialize + } + + def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = getBeliefsForElement(target).toStream + + def computeExpectation[T](target: Element[T], function: T => Double): Double = { + computeDistribution(target).map((pair: (Double, T)) => pair._1 * function(pair._2)).sum + } +} + +trait ProbEvidenceBeliefPropagation extends ProbabilisticBeliefPropagation { + + private def logFcn: (Double => Double) = semiring match { + case LogSumProductSemiring => (d: Double) => d + case SumProductSemiring => (d: Double) => if (d == semiring.zero) Double.NegativeInfinity else math.log(d) + } + private def probFcn: (Double => Double) = semiring match { + case LogSumProductSemiring => (d: Double) => if (d == semiring.zero) 0 else math.exp(d) + case SumProductSemiring => (d: Double) => d + } + + private def entropy(probFactor: Factor[Double], logFactor: Factor[Double]): Double = { + // Even though the variables in each factor are the same, the order of the vars might be different + val logFactorMapping = probFactor.variables.map(v => logFactor.variables.indexOf(v)) + def remap(l: List[Int]) = l.zipWithIndex.map(s => (s._1, logFactorMapping(s._2))).sortBy(_._2).unzip._1 + + val e = (0.0 /: probFactor.allIndices)((c: Double, i: List[Int]) => { + val p = probFcn(probFactor.get(i)) + if (p == 0) c else c + p * logFcn(logFactor.get(remap(i))) + }) + e + } + + /* Not true mutual information for > 2 factors, but standard for computing Bethe approximation */ + private def mutualInformation(joint: Factor[Double], marginals: Iterable[Factor[Double]]) = { + if (debug) { + println(joint.toReadableString) + marginals foreach (f => println(f.toReadableString)) + } + val newFactor = (joint /: marginals)((c: Factor[Double], n: Factor[Double]) => c.combination(n, semiring.divide)) + val mi = (0.0 /: newFactor.allIndices)((c: Double, i: List[Int]) => { + val p = probFcn(joint.get(i)) + if (p == 0) c else c + p * logFcn(newFactor.get(i)) + }) + mi + } + + /** + * Compute the evidence of the model. Returns the probability of evidence on the model. This assumes that BP + * has already been run on this algorithm instance. + */ + def computeEvidence(): Double = { + + val factorNodes = factorGraph.getNodes.filter(_.isInstanceOf[FactorNode]).toList + val varNodes = factorGraph.getNodes.filter(_.isInstanceOf[VariableNode]).toList + + val nonZeroEvidence = factorNodes.exists(p => beliefMap(p).contents.exists(_._2 != Double.NegativeInfinity)) + + if (nonZeroEvidence) { + val betheEnergy = -1 * factorNodes.map(f => { + entropy(normalize(beliefMap(f)), factorGraph.getFactorForNode(f.asInstanceOf[FactorNode])) + }).sum + val betheEntropy = { + val factorEntropy = -1 * factorNodes.map(f => { + entropy(normalize(beliefMap(f)), normalize(beliefMap(f))) + }).sum + val varEntropy = varNodes.map(v => { + (factorGraph.getNeighbors(v).size - 1) * entropy(normalize(beliefMap(v)), normalize(beliefMap(v))) + }).sum + factorEntropy + varEntropy + } + math.exp(-1 * (betheEnergy - betheEntropy)) + } else { + 0.0 + } + } + +} + +object BeliefPropagation { + /** + * Creates a One Time belief propagation computer in the current default universe. + */ + def apply(myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } + + /** + * Creates a Anytime belief propagation computer in the current default universe. + */ + def apply(targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery + + /** + * Create a One Time belief propagation computer current default universe, with debug information enabled. + */ + def debugged(myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations; override val debug = true } + + /** + * Create a Anytime belief propagation computer using the given dependent universes in the current default universe. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + dependentUniverses, + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } + + /** + * Create a One Time belief propagation computer using the given dependent universes in the current default universe. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + dependentUniverses, + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery + + /** + * Create a One Time belief propagation computer using the given dependent universes in the current + * default universe. Use the given dependent algorithm function to determine the algorithm to use + * to compute probability of evidence in each dependent universe. + */ + def apply( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations } + + /** + * Create a Anytime belief propagation computer using the given dependent universes in the current + * default universe. Use the given dependent algorithm function to determine the algorithm to use + * to compute probability of evidence in each dependent universe. + */ + def apply( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery + + /** + * Use BP to compute the probability that the given element satisfies the given predicate. + */ + def probability[T](target: Element[T], predicate: T => Boolean): Double = { + val alg = BeliefPropagation(10, target) + alg.start() + val result = alg.probability(target, predicate) + alg.kill() + result + } + + /** + * Use BP to compute the probability that the given element has the given value. + */ + def probability[T](target: Element[T], value: T): Double = + probability(target, (t: T) => t == value) + + /** + * Lazy version of BP that operates only on bounds. + */ + def lazyBP(myIterations: Int, depth: Int, upperBounds: Boolean, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryBeliefPropagation(universe, targets: _*)( + List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + depth, upperBounds) with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery { val iterations = myIterations; override val debug = false } + +} + + + diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/FactorGraph.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/FactorGraph.scala index 14647f7c..c61613ad 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/FactorGraph.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/FactorGraph.scala @@ -13,50 +13,50 @@ package com.cra.figaro.algorithm.factored.beliefpropagation -import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ /** - * Trait for Factor graphs used by Belief Propagation algorithms + * Trait for Factor graphs used by Belief Propagation algorithms. */ trait FactorGraph[T] { /** - * Returns a uniform factor + * Returns a uniform factor. */ def uniformFactor(v: List[Variable[_]]): Factor[T] /** - * Returns true if the graph contains a node for a (single) variable + * Returns true if the graph contains a node for a (single) variable. */ def contains(v: Node): Boolean /** - * Returns all nodes in the factor graph + * Returns all nodes in the factor graph. */ def getNodes(): Iterable[Node] /** - * Returns all neighbors of a given node + * Returns all neighbors of a given node. */ def getNeighbors(source: Node): Iterable[Node] /** - * Returns all neighbors of a given node excluding the node of the second argument + * Returns all neighbors of a given node excluding the node of the second argument. */ def getNeighbors(source: Node, excluding: Node): Iterable[Node] /** - * Gets the factor for a particular factor node + * Gets the factor for a particular factor node. */ def getFactorForNode(fn: FactorNode): Factor[T] /** - * Get a list of messages to the node + * Get a list of messages to the node. */ def getMessagesForNode(node: Node): Iterable[(Node, Factor[T])] /** - * Gets the last message to a node from another + * Gets the last message to a node from another. */ def getLastMessage(from: Node, to: Node): Factor[T] diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/InnerBPHandler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/InnerBPHandler.scala index 0308edcc..62fc8320 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/InnerBPHandler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/InnerBPHandler.scala @@ -1,17 +1,31 @@ +/* + * InnerBPHandler.scala + * Trait for creating and running Belief Propagation within another algorithm + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 20, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.algorithm.factored.beliefpropagation import com.cra.figaro.language._ import com.cra.figaro.algorithm.OneTimeProbQuery import com.cra.figaro.algorithm.AnytimeProbQuery import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler +import com.cra.figaro.algorithm.Anytime /** - * Trait for creating and running Belief Propagation within another algorithm + * Trait for creating and running Belief Propagation within another algorithm. */ trait InnerBPHandler { /** - * Universe associated with this algorithm + * Universe associated with this algorithm. */ protected var currentUniverse: Universe = _ @@ -63,6 +77,7 @@ trait AnytimeInnerBPHandler extends InnerBPHandler { val myStepTimeMillis: Long protected def createBP(targets: List[Element[_]], depth: Int = Int.MaxValue, upperBounds: Boolean = false): Unit = { + if (bp != null) bp.kill bp = new ProbQueryBeliefPropagation(currentUniverse, targets: _*)(List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), depth, upperBounds) with AnytimeProbabilisticBeliefPropagation with AnytimeProbQuery } @@ -70,6 +85,6 @@ trait AnytimeInnerBPHandler extends InnerBPHandler { protected def runBP() { bp.start() Thread.sleep(myStepTimeMillis) - bp.stop() + bp.stop() } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/MPEBeliefPropagation.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/MPEBeliefPropagation.scala index 34a91689..a939bee7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/MPEBeliefPropagation.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/MPEBeliefPropagation.scala @@ -17,7 +17,7 @@ import com.cra.figaro.algorithm._ import com.cra.figaro.language._ import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.sampling._ -import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.util import scala.collection.mutable.{ Set, Map } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/BasicFactor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala similarity index 68% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/factored/BasicFactor.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala index 22e1370e..0cc45711 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/BasicFactor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala @@ -1,284 +1,267 @@ -/* - * Factor.scala - * General class of factors over values. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -import com.cra.figaro.util._ -import scala.annotation.tailrec -import scala.collection.mutable.Map -import com.cra.figaro.language.Element -import com.cra.figaro.algorithm.lazyfactored.Extended - -/** - * General class of factors. A factor is associated with a set of variables and specifies a value for every - * combination of assignments to those variables. Factors are parameterized by the types of values they contain. - */ -class BasicFactor[T](val variables: List[Variable[_]]) extends Factor[T] { - - /** - * Fill the contents of the target by applying the given function to all elements of this factor. - */ - override def mapTo[U](fn: T => U, variables: List[Variable[_]]): Factor[U] = { - val newFactor = Factory.make[U](variables) - for { (key, value) <- contents } { - newFactor.contents += key -> fn(value) } - newFactor - } - - /** - * Fill the contents of this factor by applying a rule to every combination of values. - */ - override def fillByRule(rule: List[Extended[_]] => T):Factor[T] = { - val ranges: List[List[(Extended[_], Int)]] = variables map (_.range.zipWithIndex) - val cases: List[List[(Extended[_], Int)]] = homogeneousCartesianProduct(ranges: _*) - for { cas <- cases } { - val (values, indices) = cas.unzip - contents += indices -> rule(values) - } - this - } - - // unionVars takes the variables in two factors and produces their union. In addition, it produces two lists that - // map indexMap in each input factor into indexMap in the union. - private def unionVars[U](that: Factor[U]): (List[Variable[_]], List[Int], List[Int]) = { - val resultVars = variables ++ (that.variables filter ((v: Variable[_]) => !(variables contains v))) - val indexMap1 = variables.zipWithIndex map (_._2) - val indexMap2 = that.variables map (resultVars.indexOf(_)) - (resultVars, indexMap1, indexMap2) - } - - /** - * Returns the product of this factor with another factor according to a given multiplication function. - * The product is associated with all variables in the two inputs, and the value associated with an assignment - * is the product of the values in the two inputs. - */ - override def product( - that: Factor[T], - semiring: Semiring[T]): Factor[T] = { - combination(that, semiring.product) - } - - /** - * Generic combination function for factors. By default, this is product, but other operations - * (such as divide that is a valid operation for some semirings) can use this - */ - override def combination( - that: Factor[T], - op: (T, T) => T): Factor[T] = { - val (allVars, indexMap1, indexMap2) = unionVars(that) - val result = new BasicFactor[T](allVars) - for { indices <- result.allIndices } { - val indexIntoThis = indexMap1 map (indices(_)) - val indexIntoThat = indexMap2 map (indices(_)) - val value = op(get(indexIntoThis), that.get(indexIntoThat)) - result.set(indices, value) - } - result - } - - private def computeSum( - resultIndices: List[Int], - summedVariable: Variable[_], - summedVariableIndices: List[Int], - semiring: Semiring[T]): T = { - var value = semiring.zero - val values = - for { i <- 0 until summedVariable.size } yield { - val sourceIndices = insertAtIndices(resultIndices, summedVariableIndices, i) - get(sourceIndices) - } - semiring.sumMany(values) - } - - /** - * Returns the summation of the factor over a variable according to an addition function. - * The result is associated with all the variables in the - * input except for the summed over variable and the value for a set of assignments is the - * sum of the values of the corresponding assignments in the input. - */ - override def sumOver( - variable: Variable[_], - semiring: Semiring[T]): BasicFactor[T] = { - if (variables contains variable) { - // The summed over variable does not necessarily appear exactly once in the factor. - val indicesOfSummedVariable = indices(variables, variable) - val resultVars = variables.toList.filterNot(_ == variable) - val result = new BasicFactor[T](resultVars) - for { indices <- result.allIndices } { - result.set(indices, computeSum(indices, variable, indicesOfSummedVariable, semiring)) - } - result - } else this - } - - private def computeArgMax[U]( - resultIndices: List[Int], - summedVariable: Variable[U], - summedVariableIndices: List[Int], - comparator: (T, T) => Boolean): U = { - def getEntry(i: Int) = - get(insertAtIndices(resultIndices, summedVariableIndices, i)) - val valuesWithEntries = - for { - i <- 0 until summedVariable.size - xvalue = summedVariable.range(i) - if xvalue.isRegular - } yield (summedVariable.range(i).value, getEntry(i)) - def process(best: (U, T), next: (U, T)) = - if (comparator(best._2, next._2)) next; else best - valuesWithEntries.reduceLeft(process(_, _))._1 - } - - /** - * Returns a factor that maps values of the other variables to the value of the given variable that - * maximizes the entry associated with that value, according to some maximization function. - * comparator defines the maximization. It returns true iff its second argument is greater than its first. - * - * @tparam U The type of element whose value is being recorded. The resulting factor maps values of - * other variables in this factor to this type. - * @tparam T The type of entries of this factor. - */ - override def recordArgMax[U](variable: Variable[U], comparator: (T, T) => Boolean): Factor[U] = { - if (!(variables contains variable)) throw new IllegalArgumentException("Recording value of a variable not present") - val indicesOfSummedVariable = indices(variables, variable) - val resultVars = variables.toList.filterNot(_ == variable) - val result = new BasicFactor[U](resultVars) - for { indices <- result.allIndices } yield { - result.set(indices, computeArgMax(indices, variable, indicesOfSummedVariable, comparator)) - } - result - } - - /** - * Returns the marginalization of the factor to a variable according to the given addition function. - * This involves summing out all other variables. - */ - override def marginalizeTo( - semiring: Semiring[T], - targets: Variable[_]*): Factor[T] = { - val marginalized = - (this /: variables)((factor: BasicFactor[T], variable: Variable[_]) => - if (targets contains variable) factor - else factor.sumOver(variable, semiring)) - // It's possible that the target variable appears more than once in this factor. If so, we need to reduce it to - // one column by eliminating any rows in which the target variable values do not agree. - deDuplicate(marginalized) - } - - override def deDuplicate(): Factor[T] = - { - deDuplicate(this) - } - - private def deDuplicate(factor: Factor[T]): Factor[T] = - { - val repeats = findRepeats(factor.variables) - val hasRepeats = (false /: repeats.values)(_ || _.size > 1) - if (hasRepeats) { - val reduced = new BasicFactor[T](repeats.keySet.toList) - val newVariableLocations = repeats.values.map(_(0)) - - val repeatedVariables = repeats.values.filter(_.size > 1) - for (row <- factor.allIndices) { - if (checkRow(row, repeatedVariables)) { - var newRow = List[Int]() - for (pos <- newVariableLocations) { - newRow = newRow :+ row(pos) - } - reduced.set(newRow, factor.get(row)) - } - } - reduced - } else { - factor - } - } - - private def checkRow(row: List[Int], repeatedVariables: Iterable[List[Int]]): Boolean = { - var ok = true - - for (repeats <- repeatedVariables) { - val checkVal = row(repeats(0)) - for (pos <- repeats) { - if (checkVal != row(pos)) { - ok = false - } - } - } - ok - } - - private def findRepeats(varList: List[Variable[_]]): Map[Variable[_], List[Int]] = - { - var repeats = Map[Variable[_], List[Int]]() - - for (variable <- varList) { - if (!repeats.keySet.contains(variable)) { - var indices = List[Int]() - var start = varList.indexOf(variable) - while (start > -1) { - indices = indices :+ start - start = varList.indexOf(variable, start + 1) - } - repeats = repeats + (variable -> indices) - } - } - repeats - } - - /** - * Produce a readable string representation of the factor - */ - override def toReadableString: String = { - val result = new StringBuffer - // layout has one column for each of the variables followed by a column for the result - val valueWidths = - for { variable <- variables } yield { - val valueLengths = variable.range.map(_.toString.length) - val maxValueLength = valueLengths.foldLeft(4)(_ max _) - (maxValueLength max variable.id.toString.length) + 2 // add 2 for spaces - } - val resultWidth = contents.values.map(_.toString.length).foldLeft(4)(_ max _) + 2 - def addBorderRow() { - for { width <- valueWidths } { result.append("|" + "-" * width) } - result.append("|" + "-" * resultWidth + "|\n") // - } - def addCentered(string: String, width: Int) { - val buffer = (width - string.length) / 2 - val bufferRemainder = (width - string.length) % 2 - result.append(" " * buffer + string + " " * (buffer + bufferRemainder)) - } - addBorderRow() - // Header row - for { (variable, width) <- variables zip valueWidths } { - result.append("|") - addCentered(variable.id.toString, width) - } - result.append("|" + " " * resultWidth + "|\n") - addBorderRow() - // Data rows - for { indices <- allIndices } { - val values = for { (variable, index) <- variables zip indices } yield { variable.range(index) } - for { (value, width) <- values zip valueWidths } { - result.append("|") - addCentered(value.toString, width) - } - result.append("|") - addCentered(contents(indices).toString, resultWidth) - result.append("|\n") - } - addBorderRow() - result.toString - } - -} +/* + * BasicFactor.scala + * Default implementation of factors over values. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors + +import com.cra.figaro.util._ +import scala.annotation.tailrec +import scala.collection.mutable.Map +import com.cra.figaro.language.Element +import com.cra.figaro.algorithm.lazyfactored.Extended + +/** + * Default implementation of Factor. A factor is associated with a set of variables and specifies a value for every + * combination of assignments to those variables. Factors are parameterized by the types of values they contain. + */ +class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_]]) extends Factor[T] { + + override def convert[U](): Factor[U] = { + new BasicFactor[U](parents, output) + } + + override def mapTo[U](fn: T => U): Factor[U] = { + val newFactor = new BasicFactor[U](parents, output) + for { (key, value) <- contents } { + newFactor.set(key, fn(value)) + } + newFactor + } + + override def fillByRule(rule: List[Extended[_]] => T): Factor[T] = { + val ranges: List[List[(Extended[_], Int)]] = variables map (_.range.zipWithIndex) + val cases: List[List[(Extended[_], Int)]] = homogeneousCartesianProduct(ranges: _*) + for { cas <- cases } { + val (values, indices) = cas.unzip + set(indices, rule(values)) + } + this + } + + // unionVars takes the variables in two factors and produces their union. + private def unionVars[U](that: Factor[U]): (List[Variable[_]], List[Variable[_]], List[Int], List[Int]) = { + val allParents = parents.union(that.parents).distinct + val allOutputs = output.union(that.output).distinct diff (allParents) + + val resultVars = allParents ::: allOutputs + val indexMap1 = variables map (resultVars.indexOf(_)) + val indexMap2 = that.variables map (resultVars.indexOf(_)) + (allParents, allOutputs, indexMap1, indexMap2) + } + + override def product( + that: Factor[T], + semiring: Semiring[T]): Factor[T] = { + combination(that, semiring.product) + } + + override def combination( + that: Factor[T], + op: (T, T) => T): Factor[T] = { + val (allParents, allChildren, indexMap1, indexMap2) = unionVars(that) + val result = new BasicFactor[T](allParents, allChildren) + + // val indexMap1 = variables map (result.variables.indexOf(_)) + // val indexMap2 = that.variables map (result.variables.indexOf(_)) + + for { indices <- result.allIndices } { + val indexIntoThis = indexMap1 map (indices(_)) + val indexIntoThat = indexMap2 map (indices(_)) + val value = op(get(indexIntoThis), that.get(indexIntoThat)) + result.set(indices, value) + } + result + } + + private def computeSum( + resultIndices: List[Int], + summedVariable: Variable[_], + summedVariableIndices: List[Int], + semiring: Semiring[T]): T = { + var value = semiring.zero + val values = + for { i <- 0 until summedVariable.size } yield { + val sourceIndices = insertAtIndices(resultIndices, summedVariableIndices, i) + get(sourceIndices) + } + semiring.sumMany(values) + } + + override def sumOver( + variable: Variable[_], + semiring: Semiring[T]): BasicFactor[T] = { + if (variables contains variable) { + // The summed over variable does not necessarily appear exactly once in the factor. + val indicesOfSummedVariable = indices(variables, variable) + + val newParents = parents.filterNot(_ == variable) + val newOutput = output.filterNot(_ == variable) + + val result = new BasicFactor[T](newParents, newOutput) + for { indices <- result.allIndices } { + result.set(indices, computeSum(indices, variable, indicesOfSummedVariable, semiring)) + } + result + } else this + } + + private def computeArgMax[U]( + resultIndices: List[Int], + summedVariable: Variable[U], + summedVariableIndices: List[Int], + comparator: (T, T) => Boolean): U = { + def getEntry(i: Int) = + get(insertAtIndices(resultIndices, summedVariableIndices, i)) + val valuesWithEntries = + for { + i <- 0 until summedVariable.size + xvalue = summedVariable.range(i) + if xvalue.isRegular + } yield (summedVariable.range(i).value, getEntry(i)) + def process(best: (U, T), next: (U, T)) = + if (comparator(best._2, next._2)) next; else best + valuesWithEntries.reduceLeft(process(_, _))._1 + } + + override def recordArgMax[U](variable: Variable[U], comparator: (T, T) => Boolean): Factor[U] = { + if (!(variables contains variable)) throw new IllegalArgumentException("Recording value of a variable not present") + val indicesOfSummedVariable = indices(variables, variable) + + val newParents = parents.filterNot(_ == variable) + val newOutput = output.filterNot(_ == variable) + + val result = new BasicFactor[U](newParents, newOutput) + for { indices <- result.allIndices } yield { + result.set(indices, computeArgMax(indices, variable, indicesOfSummedVariable, comparator)) + } + result + } + + override def marginalizeTo( + semiring: Semiring[T], + targets: Variable[_]*): Factor[T] = { + val marginalized = + (this /: variables)((factor: BasicFactor[T], variable: Variable[_]) => + if (targets contains variable) factor + else factor.sumOver(variable, semiring)) + // It's possible that the target variable appears more than once in this factor. If so, we need to reduce it to + // one column by eliminating any rows in which the target variable values do not agree. + deDuplicate(marginalized) + } + + override def deDuplicate(): Factor[T] = + { + deDuplicate(this) + } + + private def deDuplicate(factor: Factor[T]): Factor[T] = + { + val repeats = findRepeats(factor.variables) + val hasRepeats = (false /: repeats.values)(_ || _.size > 1) + if (hasRepeats) { + val reducedVariables = repeats.keySet.toList + val reducedParents = reducedVariables.intersect(parents) + val reducedChildren = reducedVariables.diff(reducedParents) + val reduced = new BasicFactor[T](reducedParents, reducedChildren) + val newVariableLocations = repeats.values.map(_(0)) + + val repeatedVariables = repeats.values.filter(_.size > 1) + for (row <- factor.allIndices) { + if (checkRow(row, repeatedVariables)) { + var newRow = List[Int]() + for (pos <- newVariableLocations) { + newRow = newRow :+ row(pos) + } + reduced.set(newRow, factor.get(row)) + } + } + reduced + } else { + factor + } + } + + private def checkRow(row: List[Int], repeatedVariables: Iterable[List[Int]]): Boolean = { + var ok = true + + for (repeats <- repeatedVariables) { + val checkVal = row(repeats(0)) + for (pos <- repeats) { + if (checkVal != row(pos)) { + ok = false + } + } + } + ok + } + + private def findRepeats(varList: List[Variable[_]]): Map[Variable[_], List[Int]] = + { + var repeats = Map[Variable[_], List[Int]]() + + for (variable <- varList) { + if (!repeats.keySet.contains(variable)) { + var indices = List[Int]() + var start = varList.indexOf(variable) + while (start > -1) { + indices = indices :+ start + start = varList.indexOf(variable, start + 1) + } + repeats = repeats + (variable -> indices) + } + } + repeats + } + + override def toReadableString: String = { + val result = new StringBuffer + // layout has one column for each of the variables followed by a column for the result + val valueWidths = + for { variable <- variables } yield { + val valueLengths = variable.range.map(_.toString.length) + val maxValueLength = valueLengths.foldLeft(4)(_ max _) + (maxValueLength max variable.id.toString.length) + 2 // add 2 for spaces + } + val resultWidth = contents.values.map(_.toString.length).foldLeft(4)(_ max _) + 2 + def addBorderRow() { + for { width <- valueWidths } { result.append("|" + "-" * width) } + result.append("|" + "-" * resultWidth + "|\n") // + } + def addCentered(string: String, width: Int) { + val buffer = (width - string.length) / 2 + val bufferRemainder = (width - string.length) % 2 + result.append(" " * buffer + string + " " * (buffer + bufferRemainder)) + } + addBorderRow() + // Header row + for { (variable, width) <- variables zip valueWidths } { + result.append("|") + addCentered(variable.id.toString, width) + } + result.append("|" + " " * resultWidth + "|\n") + addBorderRow() + // Data rows + for { indices <- allIndices } { + val values = for { (variable, index) <- variables zip indices } yield { variable.range(index) } + for { (value, width) <- values zip valueWidths } { + result.append("|") + addCentered(value.toString, width) + } + result.append("|") + addCentered(contents(indices).toString, resultWidth) + result.append("|\n") + } + addBorderRow() + result.toString + } + +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala similarity index 51% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factor.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala index 82074f0c..a1fc1cd5 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Factor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala @@ -1,129 +1,183 @@ -/* - * Factor.scala - * General trait for factors over values. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ -package com.cra.figaro.algorithm.factored - -import scala.annotation.tailrec -import com.cra.figaro.algorithm.lazyfactored.Extended - -/** - * Refactored by - * - * @author Glenn Takata Sep 11, 2014 - * - */ -trait Factor[T] { - def variables: List[Variable[_]] - lazy val numVars = variables.size - - protected[figaro] var contents: Map[List[Int], T] = Map() - - val size = (1 /: variables)(_ * _.size) - - override def toString = "Factor(" + variables.map(_.id).mkString(",") + " " + contents.mkString(",") + ")" - - def hasStar = (false /: variables)(_ || _.valueSet.hasStar) - - def isEmpty = size == 0 - - /** - * Fold the given function through the contents of the factor, beginning with the given initial values - */ - def foldLeft(initial: T, fn: (T, T) => T): T = { - (initial /: contents.values)(fn(_, _)) - } - - /** - * Returns the indices lists corresponding to all the rows in order. - */ - def allIndices: List[List[Int]] = { - @tailrec def helper(current: List[Int], accum: List[List[Int]]): List[List[Int]] = - nextIndices(current) match { - case Some(next) => helper(next, current :: accum) - case None => (current :: accum).reverse - } - if (isEmpty) List() - else helper(firstIndices, List()) - } - - /** - * Returns the list of indices into the variable ranges associated with the first row in the factor. - */ - def firstIndices: List[Int] = List.fill(numVars)(0) - - /** - * Set the value associated with a row. The row is identified by an list of indices - * into the ranges of the variables over which the factor is defined. - */ - def set(indices: List[Int], value: T): Factor[T] = { - contents += indices -> value - this - } - - /** - * Get the value associated with a row. The row is identified by an list of indices - * into the ranges of the variables over which the factor is defined. - */ - def get(indices: List[Int]): T = contents(indices) - - /** - * Given a list of indices corresponding to a row in the factor, returns the list of indices - * corresponding to the next row. - * Returns None if the last index list has been reached. - */ - def nextIndices(indices: List[Int]): Option[List[Int]] = { - def makeNext(position: Int) = - for { i <- 0 until numVars } yield if (i > position) indices(i) - else if (i < position) 0 - else indices(position) + 1 - def helper(position: Int): Option[List[Int]] = - if (position == numVars) None - else if (indices(position) < variables(position).size - 1) Some(makeNext(position).toList) - else helper(position + 1) - helper(0) - } - - def fillByRule(rule: List[Extended[_]] => T):Factor[T] - - def mapTo[U](fn: T => U, variables: List[Variable[_]]): Factor[U] - - def product( - that: Factor[T], - semiring: Semiring[T]): Factor[T] - - def combination( - that: Factor[T], - op: (T, T) => T): Factor[T] - - def sumOver(variable: Variable[_], semiring: Semiring[T]): Factor[T] - - def recordArgMax[U](variable: Variable[U], comparator: (T, T) => Boolean): Factor[U] - - def marginalizeTo( - semiring: Semiring[T], - targets: Variable[_]*): Factor[T] - - def deDuplicate(): Factor[T] - - def toReadableString: String -} - -object Factor { - /** - * The mutliplicative identity factor. - */ - def unit[T](semiring: Semiring[T]): Factor[T] = { - val result = new BasicFactor[T](List()) - result.set(List(), semiring.one) - result - } +/* + * Factor.scala + * General trait for factors over values. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ +package com.cra.figaro.algorithm.factored.factors + +import scala.annotation.tailrec +import com.cra.figaro.language.Element +import com.cra.figaro.algorithm.lazyfactored.Extended + +/** + * Definition of Factor

+ * + * A factor is associated with a set of variables and specifies a value for every + * combination of assignments to those variables. Factors are parameterized by the + * Variables they contain. Parent variables are distinguished from the output variable. + * + * Refactored by + * + * @author Glenn Takata Sep 11, 2014 + * + */ +trait Factor[T] { + def parents: List[Variable[_]] + def output: List[Variable[_]] + def variables = parents ::: output + + lazy val numVars = variables.size + + protected[figaro] var contents: Map[List[Int], T] = Map() + + val size = (1 /: variables)(_ * _.size) + + /** + * Description that includes the variable list and conditional probabilites + */ + override def toString = "Factor(" + variables.map(_.id).mkString(",") + " " + contents.mkString(",") + ")" + + /** + * Indicates if any of this Factor's variables has Star + */ + def hasStar = (false /: variables)(_ || _.valueSet.hasStar) + + /** + * Indicates if this Factor has any variables + */ + def isEmpty = size == 0 + + /** + * Fold the given function through the contents of the factor, beginning with the given initial values + */ + def foldLeft(initial: T, fn: (T, T) => T): T = { + (initial /: contents.values)(fn(_, _)) + } + + /** + * Returns the indices lists corresponding to all the rows in order. + */ + def allIndices: List[List[Int]] = { + @tailrec def helper(current: List[Int], accum: List[List[Int]]): List[List[Int]] = + nextIndices(current) match { + case Some(next) => helper(next, current :: accum) + case None => (current :: accum).reverse + } + if (isEmpty) List() + else helper(firstIndices, List()) + } + + /** + * Returns the list of indices into the variable ranges associated with the first row in the factor. + */ + def firstIndices: List[Int] = List.fill(numVars)(0) + + /** + * Set the value associated with a row. The row is identified by an list of indices + * into the ranges of the variables over which the factor is defined. + */ + def set(indices: List[Int], value: T): Factor[T] = { + contents += indices -> value + this + } + + /** + * Get the value associated with a row. The row is identified by an list of indices + * into the ranges of the variables over which the factor is defined. + */ + def get(indices: List[Int]): T = contents(indices) + + /** + * Given a list of indices corresponding to a row in the factor, returns the list of indices + * corresponding to the next row. + * Returns None if the last index list has been reached. + */ + def nextIndices(indices: List[Int]): Option[List[Int]] = { + def makeNext(position: Int) = + for { i <- 0 until numVars } yield if (i < position) indices(i) + else if (i > position) 0 + else indices(position) + 1 + def helper(position: Int): Option[List[Int]] = + if (position < 0) None + else if (indices(position) < variables(position).size - 1) Some(makeNext(position).toList) + else helper(position - 1) + helper(numVars - 1) + } + + /** + * Fill the contents of this factor by applying a rule to every combination of values. + */ + def fillByRule(rule: List[Extended[_]] => T): Factor[T] + + /** + * Fill the contents of the target by applying the given function to all elements of this factor. + */ + def mapTo[U](fn: T => U): Factor[U] + + /** + * Returns the product of this factor with another factor according to a given multiplication function. + * The product is associated with all variables in the two inputs, and the value associated with an assignment + * is the product of the values in the two inputs. + */ + def product( + that: Factor[T], + semiring: Semiring[T]): Factor[T] + + /** + * Generic combination function for factors. By default, this is product, but other operations + * (such as divide that is a valid operation for some semirings) can use this + */ + def combination( + that: Factor[T], + op: (T, T) => T): Factor[T] + + /** + * Returns the summation of the factor over a variable according to an addition function. + * The result is associated with all the variables in the + * input except for the summed over variable and the value for a set of assignments is the + * sum of the values of the corresponding assignments in the input. + */ + def sumOver(variable: Variable[_], semiring: Semiring[T]): Factor[T] + + /** + * Returns a factor that maps values of the other variables to the value of the given variable that + * maximizes the entry associated with that value, according to some maximization function. + * comparator defines the maximization. It returns true iff its second argument is greater than its first. + * + * @tparam U The type of element whose value is being recorded. The resulting factor maps values of + * other variables in this factor to this type. + * @tparam T The type of entries of this factor. + */ + def recordArgMax[U](variable: Variable[U], comparator: (T, T) => Boolean): Factor[U] + + /** + * Returns the marginalization of the factor to a variable according to the given addition function. + * This involves summing out all other variables. + */ + def marginalizeTo( + semiring: Semiring[T], + targets: Variable[_]*): Factor[T] + + /** + * Returns a new Factor with duplicate variable(s) removed + */ + def deDuplicate(): Factor[T] + + /** + * Creates a new Factor of the same class with a different type + */ + def convert[U](): Factor[U] + + /** + * Produce a readable string representation of the factor + */ + def toReadableString: String + } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factory.scala new file mode 100644 index 00000000..b9283016 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factory.scala @@ -0,0 +1,521 @@ +/* + * Factory.scala + * Factors over variables. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors + +import com.cra.figaro.algorithm._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.algorithm.factored._ +import scala.collection.mutable.ListBuffer +import com.cra.figaro.algorithm.factored.factors.factory._ +import com.cra.figaro.library.compound._ +import com.cra.figaro.library.collection._ +import com.cra.figaro.library.atomic.discrete._ + +/** + * A trait for elements that are able to construct their own Factor. + */ +trait FactorMaker[T] { + def makeFactors[T]: List[Factor[Double]] +} + +/** + * Methods for creating probabilistic factors associated with elements. + */ +object Factory { + + /** + * The mutliplicative identity factor. + */ + def unit[T](semiring: Semiring[T]): Factor[T] = { + val result = new BasicFactor[T](List(), List()) + result.set(List(), semiring.one) + result + } + + /** + * Create a BasicFactor from the supplied parent and children variables + */ + def defaultFactor[T](parents: List[Variable[_]], children: List[Variable[_]]) = new BasicFactor[T](parents, children) + + private def makeFactors[T](const: Constant[T]): List[Factor[Double]] = { + val factor = new BasicFactor[Double](List(), List(Variable(const))) + factor.set(List(0), 1.0) + List(factor) + } + + private def makeDontCares[U](factor: Factor[Double], + intermedIndex: Int, + outcomeVar: Variable[U], + overallVar: Variable[U] + ): Unit = { + // If we don't care, we assign 1.0 to all combinations of the distVar and outcomeVar + for { + j <- 0 until outcomeVar.size + k <- 0 until overallVar.size + } { + factor.set(List(intermedIndex, j, k), 1.0) + } + } + + private def makeCares[U](factor: Factor[Double], intermedIndex: Int, + outcomeVar: Variable[U], overallVar: Variable[U], choices: Set[U])(implicit mapper: PointMapper[U]): Unit = { + // We care to match up overallVar with outcomeVar + for { + (outcomeVal, j) <- outcomeVar.range.zipWithIndex + (overallVal, k) <- overallVar.range.zipWithIndex + } { + // Star stands for "something". If outcomeVal is Star and overallVal is Star, we know something will match something, so the entry is (1,1). + // If outcomeVal is Star and overallVal is a regular value, then maybe there will be a match, so the entry is (0,1). + // If outcomeVal is regular, all the probability mass associated with that outcome should be on regular values of overallVal, so the entry is (0,0). + val entry = + if (overallVal.isRegular && outcomeVal.isRegular) { + if (overallVal.value == mapper.map(outcomeVal.value, choices)) 1.0 + else 0.0 + } else if (!overallVal.isRegular && !outcomeVal.isRegular) 1.0 + else 0.0 + factor.set(List(intermedIndex, j, k), entry) + } + } + + /* + * The conditional selector creates a factor in which, when the selector's value is such that the result + * element is relevant to the final result, the result element and overall element must have the same + * value (handled by makeCares). Otherwise, the result element and overall element can take on any + * value (handled by makeDontCares) + */ + /** + * Make a conditional selector factor used in the decomposition of chain and other elements. + * A chain defines a factor over the parent element, each of the possible result elements of the chain, + * and the overall chain element. This can produce a very large factor when there are many result elements. + * This is solved by decomposing the chain factor into a product of factors, each of which contains the + * parent element, one of the result elements, and the overall chain element. + */ + def makeConditionalSelector[T, U](overallElem: Element[U], selector: Variable[T], + outcomeIndex: Int, outcomeVar: Variable[U])(implicit mapper: PointMapper[U]): Factor[Double] = { + val overallVar = Variable(overallElem) + //val outcomeVar = Variable(outcomeElem) + val overallValues = LazyValues(overallElem.universe).storedValues(overallElem) + val factor = new BasicFactor[Double](List(selector, outcomeVar), List(overallVar)) + for { i <- 0 until selector.size} { + if (i == outcomeIndex) { + makeCares(factor, outcomeIndex, outcomeVar, overallVar, overallValues.regularValues)(mapper) + } + else { + makeDontCares(factor, i, outcomeVar, overallVar) + } + } + + factor + } + + private def makeFactors[T, U](chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val chainMap: scala.collection.mutable.Map[T, Element[U]] = LazyValues(chain.universe).getMap(chain) + val parentVar = Variable(chain.parent) + var tempFactors = parentVar.range.zipWithIndex flatMap (pair => { + val parentVal = pair._1 + // parentVal.value should have been placed in applyMap at the time the values of this apply were computed. + // By using chainMap, we can make sure that the result element is the same now as they were when values were computed. + if (parentVal.isRegular) List(makeConditionalSelector(chain, parentVar, pair._2, Variable(chainMap(parentVal.value)))(mapper)) + else { + // We create a dummy variable for the outcome variable whose value is always star. + // We create a dummy factor for that variable. + // Then we use makeConditionalSelector with the dummy variable + val dummy = new Variable(ValueSet.withStar[U](Set())) + val dummyFactor = new BasicFactor[Double](List(), List(dummy)) + dummyFactor.set(List(0), 1.0) + List(makeConditionalSelector(chain, parentVar, pair._2, dummy), dummyFactor) + } + }) + tempFactors + } + + val maxElementCount = 6 + val maxSize = 500 + val newFactors = ListBuffer[Factor[Double]]() + val tempFactors = ListBuffer[Factor[Double]]() + + /** + * Combines a set of factors into a single larger factor. This method is used when a factor has + * been decomposed into may dependent Factors and a single Factor is required. + */ + def combineFactors(oldFactors: List[Factor[Double]], semiring: Semiring[Double], removeTemporaries: Boolean): List[Factor[Double]] = { + newFactors.clear + tempFactors.clear + + for (factor <- oldFactors) + { + if (factor.hasStar) + { + newFactors += factor + } + else + { + tempFactors += factor + } + } + + var nextFactor = tempFactors.head + + for (factor <- tempFactors.tail) + { + val commonVariables = factor.variables.toSet & nextFactor.variables.toSet + + if (commonVariables.size > 0) + { + val newVariables = factor.variables.toSet -- nextFactor.variables.toSet + val potentialSize = calculateSize(nextFactor.size, newVariables) + if ((nextFactor.numVars + newVariables.size) < maxElementCount + && potentialSize < maxSize) + { + nextFactor = nextFactor.product(factor, semiring) + } + else + { + if (removeTemporaries) + { + newFactors ++= reduceFactor(nextFactor, semiring, maxElementCount) + } + else + { + newFactors += nextFactor + } + nextFactor = factor + } + } + else + { + newFactors += nextFactor + nextFactor = factor + } + } + + if (nextFactor.numVars > 0) + { + if (removeTemporaries) + { + newFactors ++= reduceFactor(nextFactor, semiring, maxElementCount) + } + else + { + newFactors += nextFactor + } + } + newFactors.toList + } + + val variableSet = scala.collection.mutable.Set[ElementVariable[_]]() + val nextFactors = ListBuffer[Factor[Double]]() + + private def reduceFactor(factor: Factor[Double], semiring: Semiring[Double], maxElementCount: Int):List[Factor[Double]] = { + variableSet.clear + + (variableSet /: List(factor))(_ ++= _.variables.asInstanceOf[List[ElementVariable[_]]]) + var elementCount = variableSet count (v => !isTemporary(v)) + var resultFactor = unit[Double](semiring).product(factor, semiring) + + var tempCount = 0; + + for {variable <- variableSet} + { + if (isTemporary(variable) && elementCount <= maxElementCount) + { + nextFactors.clear + nextFactors ++= concreteFactors(variable.asInstanceOf[ElementVariable[_]].element) + (variableSet /: nextFactors)(_ ++= _.variables.asInstanceOf[List[ElementVariable[_]]]) + elementCount = variableSet count (v => !isTemporary(v)) + + for (nextFactor <- nextFactors) + { + resultFactor = resultFactor.product(nextFactor, semiring) + } + tempCount += 1 + } + } + + if (tempCount > 0 && elementCount <= maxElementCount) + { + for {variable <- variableSet} + { + if (isTemporary(variable)) + { + resultFactor = resultFactor.sumOver(variable, semiring) + } + } + + } + List(resultFactor) + } + + private def calculateSize(currentSize: Int, variables: Set[Variable[_]]) = { + (currentSize /: variables)(_ * _.size) + } + + private def isTemporary[_T](variable: Variable[_]): Boolean = { + variable match { + case e: ElementVariable[_] => e.element.isTemporary + case _ => false + } + } + + private def makeFactors[T](inject: Inject[T]): List[Factor[Double]] = { + def rule(values: List[Extended[_]]) = { + val inputXvalues :+ resultXvalue = values + // See logic under makeCares + if (inputXvalues.exists(!_.isRegular)) { + if (!resultXvalue.isRegular) 1.0; else 0.0 + } + else if (resultXvalue.isRegular) { + if (resultXvalue.value.asInstanceOf[List[T]] == inputXvalues.map(_.value.asInstanceOf[T])) 1.0; else 0.0 + } else 0.0 + } + val inputVariables = inject.args map (Variable(_)) + val resultVariable = Variable(inject) +// val variables = resultVariable :: inputVariables + val factor = new BasicFactor[Double](inputVariables, List(resultVariable)) + factor.fillByRule(rule _) + List(factor) + } + + // When we're using a parameter to compute expected sufficient statistics, we just use its expected value + private def makeParameterFactors(param: Parameter[_]): List[Factor[Double]] = { + // The parameter should have one possible value, which is its expected value + assert(Variable(param).range.size == 1) + val factor = new BasicFactor[Double](List(), List(Variable(param))) + factor.set(List(0), 1.0) + List(factor) + } + + private def makeFactors[T](atomic: Atomic[T]): List[Factor[Double]] = { + val atomicVar = Variable(atomic) + val pbpSampler = ParticleGenerator(atomic.universe) + // Note, don't need number of samples because values should have already been expanded on it + // (and values will initiate the sampling) + val samples = pbpSampler(atomic) + if (atomicVar.range.exists(!_.isRegular)) { + assert(atomicVar.range.size == 1) // Select's range must either be a list of regular values or {*} + StarFactory.makeStarFactor(atomic) + } else { + val probs = SelectFactory.getProbs(atomic, samples) + List(SelectFactory.makeSimpleDistribution(atomicVar, probs)) + } + } + + /** + * Invokes Factor constructors for a standard set of Elements. This method uses various + * secondary factories. + */ + def concreteFactors[T](elem: Element[T]): List[Factor[Double]] = + elem match { + case flip: ParameterizedFlip => DistributionFactory.makeFactors(flip) + case pSelect: ParameterizedSelect[_] => SelectFactory.makeFactors(pSelect) + case parameter: DoubleParameter => makeParameterFactors(parameter) + case array: ArrayParameter => makeParameterFactors(array) + case constant: Constant[_] => makeFactors(constant) + case f: AtomicFlip => DistributionFactory.makeFactors(f) + case f: CompoundFlip => DistributionFactory.makeFactors(f) + case ab: AtomicBinomial => DistributionFactory.makeFactors(ab) + case s: AtomicSelect[_] => SelectFactory.makeFactors(s) + case s: CompoundSelect[_] => SelectFactory.makeFactors(s) + case d: AtomicDist[_] => SelectFactory.makeFactors(d) + case d: CompoundDist[_] => SelectFactory.makeFactors(d) + case s: IntSelector => SelectFactory.makeFactors(s) + case c: Chain[_, _] => makeFactors(c) + case a: Apply1[_, _] => ApplyFactory.makeFactors(a) + case a: Apply2[_, _, _] => ApplyFactory.makeFactors(a) + case a: Apply3[_, _, _, _] => ApplyFactory.makeFactors(a) + case a: Apply4[_, _, _, _, _] => ApplyFactory.makeFactors(a) + case a: Apply5[_, _, _, _, _, _] => ApplyFactory.makeFactors(a) + case i: Inject[_] => makeFactors(i) + case r: SingleValuedReferenceElement[_] => ComplexFactory.makeFactors(r) + case r: MultiValuedReferenceElement[_] => ComplexFactory.makeFactors(r) + case r: Aggregate[_, _] => ComplexFactory.makeFactors(r) + case m: MakeList[_] => ComplexFactory.makeFactors(m) + case m: MakeArray[_] => ComplexFactory.makeFactors(m) + case f: FoldLeft[_,_] => ComplexFactory.makeFactors(f) + case f: FactorMaker[_] => f.makeFactors + case a: Atomic[_] => makeFactors(a) + + case _ => throw new UnsupportedAlgorithmException(elem) + } + + private def makeAbstract[T](atomic: Atomic[T], abstraction: Abstraction[T]): List[Factor[Double]] = { + val variable = Variable(atomic) + val values = variable.range.map(_.value) + val densityMap = scala.collection.mutable.Map[T, Double]() + for { v <- values } { + val currentDensity = densityMap.getOrElse(v, 0.0) + densityMap.update(v, currentDensity + atomic.density(v)) + } + val factor = new BasicFactor[Double](List(), List(variable)) + for { (v, i) <- values.zipWithIndex } { + factor.set(List(i), densityMap(v)) + } + List(factor) + } + + private def makeAbstract[T](elem: Element[T], abstraction: Abstraction[T]): List[Factor[Double]] = + elem match { + case apply: Apply1[_, _] => ApplyFactory.makeFactors(apply)(abstraction.scheme) + case apply: Apply2[_, _, _] => ApplyFactory.makeFactors(apply)(abstraction.scheme) + case apply: Apply3[_, _, _, _] => ApplyFactory.makeFactors(apply)(abstraction.scheme) + // In the case of a Chain, its pragmas are inherited by the expanded result elements. The abstraction will be + // taken into account when we generate factors for the result elements. + case chain: Chain[_, _] => makeFactors(chain)(abstraction.scheme) + case atomic: Atomic[_] => makeAbstract(atomic, abstraction) + case _ => throw new UnsupportedAlgorithmException(elem) + } + + private def makeNonConstraintFactorsUncached[T](elem: Element[T]): List[Factor[Double]] = { + /* + * Don't make non-constraint factors for an element that is expanded to depth -1. + * The element must take on the value *, so the factor is the unit. + * Attempting to create a factor can result in problems where the element's arguments are not star, + * leading to the factor being zero everywhere. + * For example, consider an Apply. If the Apply is expanded to depth -1, but its argument has already + * been expanded and produced a set of values without *, the Apply factor would have probability zero + * for cases where the Apply result is *. But since the Apply has only been expanded to depth -1, + * its only possible result is *, so the factor is zero everywhere. + */ + if (LazyValues(elem.universe).expandedDepth(elem).getOrElse(-1) != -1) { + Abstraction.fromPragmas(elem.pragmas) match { + case None => concreteFactors(elem) + case Some(abstraction) => makeAbstract(elem, abstraction) + } + } + else List() + } + + private def makeConditionAndConstraintFactors[T](elem: Element[T]): List[Factor[Double]] = + elem.allConditions.map(makeConditionFactor(elem, _)) ::: elem.allConstraints.map(makeConstraintFactor(elem, _)) + + private def makeConditionFactor[T](elem: Element[T], cc: (T => Boolean, Element.Contingency)): Factor[Double] = + makeConstraintFactor(elem, (ProbConstraintType((t: T) => if (cc._1(t)) 1.0; else 0.0), cc._2)) + + private def makeConstraintFactor[T](elem: Element[T], cc: (T => Double, Element.Contingency)): Factor[Double] = { + val (constraint, contingency) = cc + contingency match { + case List() => makeUncontingentConstraintFactor(elem, constraint, false) + case first :: rest => makeContingentConstraintFactor(elem, constraint, first, rest) + } + } + + private def makeUncontingentConstraintFactor[T](elem: Element[T], constraint: T => Double, upper: Boolean): Factor[Double] = { + val elemVar = Variable(elem) + val factor = new BasicFactor[Double](List(), List(elemVar)) + for { (elemVal, index) <- elemVar.range.zipWithIndex } { + val entry = if (elemVal.isRegular) { + math.exp(constraint(elemVal.value)) + } else if (upper) { + 1.0 + } else { + 0.0 + } + factor.set(List(index), entry) + } + factor + } + + private def makeContingentConstraintFactor[T](elem: Element[T], constraint: T => Double, firstConting: Element.ElemVal[_], restContinges: Element.Contingency): Factor[Double] = { + val restFactor = makeConstraintFactor(elem, (constraint, restContinges)) + extendConstraintFactor(restFactor, firstConting) + } + + private def extendConstraintFactor(restFactor: Factor[Double], firstConting: Element.ElemVal[_]): Factor[Double] = { + // The extended factor is obtained by getting the underlying factor and expanding each row so that the row only provides its entry if the contingent variable takes + // on the appropriate value, otherwise the entry is 1 + val Element.ElemVal(firstElem, firstValue) = firstConting + val firstVar = Variable(firstElem) + val firstValues = firstVar.range + val numFirstValues = firstValues.size + val matchingIndex: Int = firstValues.indexOf(Regular(firstValue)) + val resultFactor = new BasicFactor[Double](firstVar :: restFactor.parents, restFactor.output) + for { restIndices <- restFactor.allIndices } { + val restEntry = restFactor.get(restIndices) + for { firstIndex <- 0 until numFirstValues } { + val resultEntry = if (firstIndex == matchingIndex) restEntry; else 1.0 + resultFactor.set(firstIndex :: restIndices, resultEntry) + } + } + resultFactor + } + + private val factorCache = scala.collection.mutable.Map[Element[_], List[Factor[Double]]]() + + /** + * Construct a Factor without constraints. + */ + def makeNonConstraintFactors(elem: Element[_]): List[Factor[Double]] = { + factorCache.get(elem) match { + case Some(l) => l + case None => + val result = makeNonConstraintFactorsUncached(elem) + factorCache += elem -> result + elem.universe.register(factorCache) + result + } + } + + /** + * Create the probabilistic factors associated with an element. This method is memoized. + */ + def make(elem: Element[_]): List[Factor[Double]] = { + makeConditionAndConstraintFactors(elem) ::: makeNonConstraintFactors(elem) + } + + /** + * Creates a BasicFactor from the supplied variables + */ + def simpleMake[T](variables: List[Variable[_]]) = + new BasicFactor[T](variables, List()) + + /** + * Remove an element from the factor cache, ensuring that factors for the element + * are regenerated. This is important, for example, if evidence on the variable has changed. + * + */ + def removeFactors(elem: Element[_]) { factorCache -= elem } + + /** + * Clear the factor cache. + */ + def removeFactors() { factorCache.clear } + + /** + * Update the factor cache. + */ + def updateFactor[T](elem: Element[_], f: List[Factor[Double]]) { factorCache.update(elem, f) } + + /** + * Create the probabilistic factor encoding the probability of evidence in the dependent universe as a function of the + * values of variables in the parent universe. The third argument is the the function to use for computing + * probability of evidence in the dependent universe. It is assumed that the definition of this function will already contain the + * right evidence. + */ + def makeDependentFactor(parentUniverse: Universe, + dependentUniverse: Universe, + probEvidenceComputer: () => Double): Factor[Double] = { + val uses = dependentUniverse.parentElements filter (_.universe == parentUniverse) + def rule(values: List[Any]) = { + for { (elem, value) <- uses zip values } { elem.value = value.asInstanceOf[Regular[elem.Value]].value } + val result = probEvidenceComputer() + result + } + val variables = uses map (Variable(_)) + val factor = new BasicFactor[Double](variables, List()) + factor.fillByRule(rule _) + factor + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Semiring.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala similarity index 96% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Semiring.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala index c3f8dc87..73e9ebfb 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Semiring.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala @@ -11,12 +11,12 @@ * See http://www.github.com/p2t2/figaro for a copy of the software license. */ -package com.cra.figaro.algorithm.factored +package com.cra.figaro.algorithm.factored.factors /** * Operations in factored algorithms are defined by a semiring algebraic structure. * Each semiring defines a product and sum operation, and a value for zero and one which satisfy a set of properties. - * Different semirings are appropriate for certain algorithms and data types + * Different semirings are appropriate for certain algorithms and data types. */ trait Semiring[T] { /** @@ -68,16 +68,16 @@ object SumProductUtilitySemiring extends DivideableSemiRing[(Double, Double)] { def divide(x: (Double, Double), y: (Double, Double)) = if (y._1 == zero._1) (zero._1, x._2 - y._2) else (x._1 / y._1, x._2 - y._2) /** - * Decision joint factor marginalization + * Decision joint factor marginalization. */ def sum(x: (Double, Double), y: (Double, Double)) = if (x._1 + y._1 != 0.0) (x._1 + y._1, (x._1 * x._2 + y._1 * y._2) / (x._1 + y._1)); else (0.0, 0.0) /** - * 0 probability and 0 utility + * 0 probability and 0 utility. */ val zero = (0.0, 0.0) /** - * 1 probability and 0 utility + * 1 probability and 0 utility. */ val one = (1.0, 0.0) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsSemiring.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/SufficientStatisticsSemiring.scala similarity index 98% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsSemiring.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/SufficientStatisticsSemiring.scala index b4f87b85..b3d5791d 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsSemiring.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/SufficientStatisticsSemiring.scala @@ -11,7 +11,7 @@ * See http://www.github.com/p2t2/figaro for a copy of the software license. */ -package com.cra.figaro.algorithm.factored +package com.cra.figaro.algorithm.factored.factors import com.cra.figaro.language._ import scala.collection._ @@ -29,13 +29,13 @@ class SufficientStatisticsSemiring(parameterMap: immutable.Map[Parameter[_], Seq /** * 0 probability and a vector of zeros for all parameters. The vector for a parameter - * must be of length equal to number of possible observations of the parameter + * must be of length equal to number of possible observations of the parameter. */ val zero = (0.0, mutable.Map(parameterMap.toSeq: _*)) /** * 1 probability and a vector of zeros for all parameters. The vector for a parameter - * must be of length equal to number of possible observations of the parameter + * must be of length equal to number of possible observations of the parameter. */ val one = (1.0, mutable.Map(parameterMap.toSeq: _*)) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Variable.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Variable.scala similarity index 94% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Variable.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Variable.scala index 1abccb14..1beeff9b 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/Variable.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Variable.scala @@ -1,135 +1,135 @@ -/* - * Variable.scala - * Variables that appear in factors. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.factored - -import com.cra.figaro.algorithm._ -import com.cra.figaro.language._ -import scala.collection.mutable.Map -import com.cra.figaro.algorithm.lazyfactored.{ LazyValues, Extended, ValueSet } - -/** - * Variables that appear in factors. - * - * @param range The range of values of the variable - */ -class Variable[T](val valueSet: ValueSet[T]) { - type Value = T - - val range: List[Extended[T]] = valueSet.xvalues.toList - - /** - * The unique identifier of the variable. - */ - val id = Variable.nextId() - - /** - * Size of the range of the variable. - */ - val size = range.size - - /** - * Override equality of Variable. Variables are the same if their id's are the same - */ - override def equals(o: Any) = o match { - case that: Variable[T] => that.id == id - case _ => false - } - override def hashCode = id - - override def toString = range.toString -} - -/** - * Variables generated from elements. - * - * @param range The range of values of the variable - */ -class ElementVariable[T](val element: Element[T]) extends Variable(LazyValues(element.universe).storedValues(element)) { - - override val id = Variable.nextId(element) - - override def toString = element.toString -} - -/** - * Variables generated from parameterized elements - * - * @param range The range of values of the variable - */ -class ParameterizedVariable[T](override val element: Parameterized[T]) extends ElementVariable(element) { - override def toString = "Parameterized variable:" + element.toString -} - -/* Variables generated from sufficient statistics of parameters */ -object Variable { - // An element should always map to the same variable - private val memoMake: Map[Element[_], Variable[_]] = Map() - - // Make sure to register this map (or replace the memoMake) - private val idCache: Map[Element[_], Int] = Map() - - private var id: Int = 0 - - def nextId(elem: Element[_]): Int = { - idCache.get(elem) match { - case Some(id) => id - case None => - val id = nextId() - idCache += (elem -> id) - id - } - } - - private def nextId(): Int = { - id += 1 - id - } - - private def make[T](elem: Element[T]): Variable[T] = { - // Make sure that the element will be removed from the memoMake map when it is inactivated - elem.universe.register(memoMake) - elem.universe.register(idCache) - new ElementVariable(elem) - } - - private def make[T](p: Parameterized[T]): Variable[T] = { - // Make sure that the element will be removed from the memoMake map when it is inactivated - p.universe.register(memoMake) - p.universe.register(idCache) - new ParameterizedVariable(p) - } - - /** - * Create the variable associated with an element. This method is memoized. - */ - def apply[T](elem: Element[T]): Variable[T] = - memoMake.get(elem) match { - case Some(v) => v.asInstanceOf[Variable[T]] - case None => - val result = make(elem) - memoMake += elem -> result - result - } - - def apply[T](elem: Parameterized[T]): Variable[T] = - memoMake.get(elem) match { - case Some(v) => v.asInstanceOf[Variable[T]] - case None => - val result = make(elem) - memoMake += elem -> result - result - } - - def clearCache() { memoMake.clear() } -} +/* + * Variable.scala + * Variables that appear in factors. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors + +import com.cra.figaro.algorithm._ +import com.cra.figaro.language._ +import scala.collection.mutable.Map +import com.cra.figaro.algorithm.lazyfactored.{ LazyValues, Extended, ValueSet } + +/** + * Variables that appear in factors. + * + * @param range The range of values of the variable + */ +class Variable[T](val valueSet: ValueSet[T]) { + type Value = T + + val range: List[Extended[T]] = valueSet.xvalues.toList + + /** + * The unique identifier of the variable. + */ + val id = Variable.nextId() + + /** + * Size of the range of the variable. + */ + val size = range.size + + /** + * Override equality of Variable. Variables are the same if their id's are the same + */ + override def equals(o: Any) = o match { + case that: Variable[T] => that.id == id + case _ => false + } + override def hashCode = id + + override def toString = range.toString +} + +/** + * Variables generated from elements. + * + * @param range The range of values of the variable + */ +class ElementVariable[T](val element: Element[T]) extends Variable(LazyValues(element.universe).storedValues(element)) { + + override val id = Variable.nextId(element) + + override def toString = element.toString +} + +/** + * Variables generated from parameterized elements + * + * @param range The range of values of the variable + */ +class ParameterizedVariable[T](override val element: Parameterized[T]) extends ElementVariable(element) { + override def toString = "Parameterized variable:" + element.toString +} + +/* Variables generated from sufficient statistics of parameters */ +object Variable { + // An element should always map to the same variable + private val memoMake: Map[Element[_], Variable[_]] = Map() + + // Make sure to register this map (or replace the memoMake) + private val idCache: Map[Element[_], Int] = Map() + + private var idState: Int = 0 + + def nextId(elem: Element[_]): Int = { + idCache.get(elem) match { + case Some(id) => id + case None => + val id = nextId() + idCache += (elem -> id) + id + } + } + + private def nextId(): Int = { + idState += 1 + idState + } + + private def make[T](elem: Element[T]): Variable[T] = { + // Make sure that the element will be removed from the memoMake map when it is inactivated + elem.universe.register(memoMake) + elem.universe.register(idCache) + new ElementVariable(elem) + } + + private def make[T](p: Parameterized[T]): Variable[T] = { + // Make sure that the element will be removed from the memoMake map when it is inactivated + p.universe.register(memoMake) + p.universe.register(idCache) + new ParameterizedVariable(p) + } + + /** + * Create the variable associated with an element. This method is memoized. + */ + def apply[T](elem: Element[T]): Variable[T] = + memoMake.get(elem) match { + case Some(v) => v.asInstanceOf[Variable[T]] + case None => + val result = make(elem) + memoMake += elem -> result + result + } + + def apply[T](elem: Parameterized[T]): Variable[T] = + memoMake.get(elem) match { + case Some(v) => v.asInstanceOf[Variable[T]] + case None => + val result = make(elem) + memoMake += elem -> result + result + } + + def clearCache() { memoMake.clear() } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ApplyFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ApplyFactory.scala new file mode 100644 index 00000000..ffb8678e --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ApplyFactory.scala @@ -0,0 +1,202 @@ +/* + * ApplyFactory.scala + * Description needed + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Dec 15, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors.factory + +import com.cra.figaro.algorithm.PointMapper +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.language._ + +/** + * A Sub-Factory for Apply Elements + */ +object ApplyFactory { + + /** + * Factor constructor for an Apply Element that has one input + */ + def makeFactors[T, U](apply: Apply1[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val applyMap: scala.collection.mutable.Map[T, U] = LazyValues(apply.universe).getMap(apply) + val arg1Var = Variable(apply.arg1) + val resultVar = Variable(apply) + val applyValues = LazyValues(apply.universe).storedValues(apply) + val factor = new BasicFactor[Double](List(arg1Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (resultVal, resultIndex) <- resultIndices + } { + // See logic in makeCares + val entry = + if (arg1Val.isRegular && resultVal.isRegular) { + // arg1Val.value should have been placed in applyMap at the time the values of this apply were computed. + // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. + if (resultVal.value == mapper.map(applyMap(arg1Val.value), applyValues.regularValues)) 1.0 + else 0.0 + } else if (!arg1Val.isRegular && !resultVal.isRegular) 1.0 + else if (!arg1Val.isRegular && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, resultIndex), entry) + } + List(factor) + } + + /** + * Factor constructor for an Apply Element that has two inputs + */ + def makeFactors[T1, T2, U](apply: Apply2[T1, T2, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val applyMap: scala.collection.mutable.Map[(T1, T2), U] = LazyValues(apply.universe).getMap(apply) + val arg1Var = Variable(apply.arg1) + val arg2Var = Variable(apply.arg2) + val resultVar = Variable(apply) + val applyValues = LazyValues(apply.universe).storedValues(apply) + val factor = new BasicFactor[Double](List(arg1Var, arg2Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val arg2Indices = arg2Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (arg2Val, arg2Index) <- arg2Indices + (resultVal, resultIndex) <- resultIndices + } { + val entry = + if (arg1Val.isRegular && arg2Val.isRegular && resultVal.isRegular) { + // The argument values should have been placed in applyMap at the time the values of this apply were computed. + // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. + if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value)), applyValues.regularValues)) 1.0 + else 0.0 + } else if ((!arg1Val.isRegular || !arg2Val.isRegular) && !resultVal.isRegular) 1.0 + else if ((!arg1Val.isRegular || !arg2Val.isRegular) && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, arg2Index, resultIndex), entry) + } + List(factor) + } + + /** + * Factor constructor for an Apply Element that has three inputs + */ + def makeFactors[T1, T2, T3, U](apply: Apply3[T1, T2, T3, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val applyMap: scala.collection.mutable.Map[(T1, T2, T3), U] = LazyValues(apply.universe).getMap(apply) + val arg1Var = Variable(apply.arg1) + val arg2Var = Variable(apply.arg2) + val arg3Var = Variable(apply.arg3) + val resultVar = Variable(apply) + val applyValues = LazyValues(apply.universe).storedValues(apply) + val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val arg2Indices = arg2Var.range.zipWithIndex + val arg3Indices = arg3Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (arg2Val, arg2Index) <- arg2Indices + (arg3Val, arg3Index) <- arg3Indices + (resultVal, resultIndex) <- resultIndices + } { + val entry = + if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && resultVal.isRegular) { + // The argument values should have been placed in applyMap at the time the values of this apply were computed. + // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. + if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value)), applyValues.regularValues)) 1.0 + else 0.0 + } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular) && !resultVal.isRegular) 1.0 + else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular) && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, arg2Index, arg3Index, resultIndex), entry) + } + List(factor) + } + + /** + * Factor constructor for an Apply Element that has four inputs + */ + def makeFactors[T1, T2, T3, T4, U](apply: Apply4[T1, T2, T3, T4, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val applyMap: scala.collection.mutable.Map[(T1, T2, T3, T4), U] = LazyValues(apply.universe).getMap(apply) + val arg1Var = Variable(apply.arg1) + val arg2Var = Variable(apply.arg2) + val arg3Var = Variable(apply.arg3) + val arg4Var = Variable(apply.arg4) + val resultVar = Variable(apply) + val applyValues = LazyValues(apply.universe).storedValues(apply) + val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var, arg4Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val arg2Indices = arg2Var.range.zipWithIndex + val arg3Indices = arg3Var.range.zipWithIndex + val arg4Indices = arg4Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (arg2Val, arg2Index) <- arg2Indices + (arg3Val, arg3Index) <- arg3Indices + (arg4Val, arg4Index) <- arg4Indices + (resultVal, resultIndex) <- resultIndices + } { + val entry = + if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && arg4Val.isRegular && resultVal.isRegular) { + // The argument values should have been placed in applyMap at the time the values of this apply were computed. + // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. + if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value, arg4Val.value)), applyValues.regularValues)) 1.0 + else 0.0 + } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular) && !resultVal.isRegular) 1.0 + else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular) && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, arg2Index, arg3Index, arg4Index, resultIndex), entry) + } + List(factor) + } + + /** + * Factor constructor for an Apply Element that has five inputs + */ + def makeFactors[T1, T2, T3, T4, T5, U](apply: Apply5[T1, T2, T3, T4, T5, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val applyMap: scala.collection.mutable.Map[(T1, T2, T3, T4, T5), U] = LazyValues(apply.universe).getMap(apply) + val arg1Var = Variable(apply.arg1) + val arg2Var = Variable(apply.arg2) + val arg3Var = Variable(apply.arg3) + val arg4Var = Variable(apply.arg4) + val arg5Var = Variable(apply.arg5) + val resultVar = Variable(apply) + val applyValues = LazyValues(apply.universe).storedValues(apply) + val factor = new BasicFactor[Double](List(arg1Var, arg2Var, arg3Var, arg4Var, arg5Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val arg2Indices = arg2Var.range.zipWithIndex + val arg3Indices = arg3Var.range.zipWithIndex + val arg4Indices = arg4Var.range.zipWithIndex + val arg5Indices = arg5Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (arg2Val, arg2Index) <- arg2Indices + (arg3Val, arg3Index) <- arg3Indices + (arg4Val, arg4Index) <- arg4Indices + (arg5Val, arg5Index) <- arg5Indices + (resultVal, resultIndex) <- resultIndices + } { + val entry = + if (arg1Val.isRegular && arg2Val.isRegular && arg3Val.isRegular && arg4Val.isRegular && arg5Val.isRegular && resultVal.isRegular) { + // The argument values should have been placed in applyMap at the time the values of this apply were computed. + // By using applyMap, we can make sure that any contained elements in the result of the apply are the same now as they were when values were computed. + if (resultVal.value == mapper.map(applyMap((arg1Val.value, arg2Val.value, arg3Val.value, arg4Val.value, arg5Val.value)), applyValues.regularValues)) 1.0 + else 0.0 + } else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular || !arg5Val.isRegular) && !resultVal.isRegular) 1.0 + else if ((!arg1Val.isRegular || !arg2Val.isRegular || !arg3Val.isRegular || !arg4Val.isRegular || !arg5Val.isRegular) && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, arg2Index, arg3Index, arg4Index, arg5Index, resultIndex), entry) + } + List(factor) + } + +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ComplexFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ComplexFactory.scala new file mode 100644 index 00000000..679b8b35 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ComplexFactory.scala @@ -0,0 +1,226 @@ +/* + * ComplexFactory.scala + * Description needed + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Dec 15, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors.factory + +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.language._ +import com.cra.figaro.library.compound._ +import com.cra.figaro.algorithm.factored.factors._ + +/** + * A Sub-Factory for Complex Elements + */ +object ComplexFactory { + + /** + * Factor constructor for a SingleValuedReferenceElement + */ + def makeFactors[T](element: SingleValuedReferenceElement[T]): List[Factor[Double]] = { + val (first, rest) = element.collection.getFirst(element.reference) + rest match { + case None => + val elementVar = Variable(element) + val firstVar = Variable(first) + val factor = new BasicFactor[Double](List(firstVar), List(elementVar)) + for { + i <- 0 until firstVar.range.size + j <- 0 until elementVar.range.size + } { + factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) + } + List(factor) + case Some(restRef) => + val firstVar = Variable(first) + val selectedFactors = + for { + (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex + firstCollection = firstXvalue.value.asInstanceOf[ElementCollection] + restElement = element.embeddedElements(firstCollection) + } yield { + Factory.makeConditionalSelector(element, firstVar, firstIndex, Variable(restElement)) :: makeFactors(restElement) + } + selectedFactors.flatten + } + } + + /** + * Factor constructor for a MultiValuedReferenceElement + */ + def makeFactors[T](element: MultiValuedReferenceElement[T]): List[Factor[Double]] = { + val (first, rest) = element.collection.getFirst(element.reference) + val selectionFactors: List[List[Factor[Double]]] = { + rest match { + case None => + val elementVar = Variable(element) + val firstVar = Variable(first) + val factor = new BasicFactor[Double](List(firstVar), List(elementVar)) + for { + i <- 0 until firstVar.range.size + j <- 0 until elementVar.range.size + } { + factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) + } + List(List(factor)) + case Some(restRef) => + val firstVar = Variable(first) + for { + (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex + } yield { + if (firstXvalue.isRegular) { + firstXvalue.value match { + case firstCollection: ElementCollection => + val restElement = element.embeddedElements(firstCollection) + val result: List[Factor[Double]] = + Factory.makeConditionalSelector(element, firstVar, firstIndex, Variable(restElement)) :: Factory.make(restElement) + result + case cs: Traversable[_] => + // Create a multi-valued reference element (MVRE) for each collection in the value of the first name. + // Since the first name is multi-valued, its value is the union of the values of all these MVREs. + val collections = cs.asInstanceOf[Traversable[ElementCollection]].toList.distinct // Set semantics + val multis: List[MultiValuedReferenceElement[T]] = collections.map(element.embeddedElements(_)).toList + // Create the element that takes the union of the values of the all the MVREs. + // The combination and setMaker elements are encapsulated within this object and are created now, so we need to create factors for them. + // Finally, we create a conditional selector (see ProbFactor) to select the appropriate result value when the first + // name's value is these MVREs. + val combination = element.embeddedInject(collections) + val setMaker = element.embeddedApply(collections) + val result: List[Factor[Double]] = + Factory.makeConditionalSelector(element, firstVar, firstIndex, Variable(setMaker)) :: Factory.make(combination) ::: + Factory.make(setMaker) + result + } + } else StarFactory.makeStarFactor(element) + } + } + } + selectionFactors.flatten + } + + /** + * Factor constructor for an Aggregate Element + */ + def makeFactors[T, U](element: Aggregate[T, U]): List[Factor[Double]] = { + val elementVar = Variable(element) + val mvreVar = Variable(element.mvre) + val factor = new BasicFactor[Double](List(mvreVar), List(elementVar)) + for { + (mvreXvalue, mvreIndex) <- mvreVar.range.zipWithIndex + (elementXvalue, elementIndex) <- elementVar.range.zipWithIndex + } { + if (elementXvalue.isRegular && mvreXvalue.isRegular) factor.set(List(mvreIndex, elementIndex), if (element.aggregate(mvreXvalue.value) == elementXvalue.value) 1.0; else 0.0) + } + // The MultiValuedReferenceElement for this aggregate is generated when values is called. + // Therefore, it will be included in the expansion and have factors made for it automatically, so we do not create factors for it here. + List(factor) + } + + /** + * Constructor for a MakeList Element + */ + def makeFactors[T](element: MakeList[T]): List[Factor[Double]] = { + val parentVar = Variable(element.numItems) + // We need to create factors for the items and the lists themselves, which are encapsulated in this MakeList + val regularParents = parentVar.range.filter(_.isRegular).map(_.value) + val maxItem = regularParents.reduce(_ max _) + val itemFactors = List.tabulate(maxItem)((i: Int) => Factory.make(element.items(i))) + val indexedResultElemsAndFactors = + for { i <- regularParents } yield { + val elem = element.embeddedInject(i) + val factors = Factory.make(elem) + (Regular(i), elem, factors) + } + val conditionalFactors = + parentVar.range.zipWithIndex map (pair => + Factory.makeConditionalSelector(element, parentVar, pair._2, Variable(indexedResultElemsAndFactors.find(_._1 == pair._1).get._2))) + conditionalFactors ::: itemFactors.flatten ::: indexedResultElemsAndFactors.flatMap(_._3) + } + + // adapted from Apply1 + /** + * Factor constructor for a MakeArray Element + */ + def makeFactors[T](element: com.cra.figaro.library.collection.MakeArray[T]): List[Factor[Double]] = { + val arg1Var = Variable(element.numItems) + val resultVar = Variable(element) + val factor = new BasicFactor[Double](List(arg1Var), List(resultVar)) + val arg1Indices = arg1Var.range.zipWithIndex + val resultIndices = resultVar.range.zipWithIndex + for { + (arg1Val, arg1Index) <- arg1Indices + (resultVal, resultIndex) <- resultIndices + } { + val entry = + if (arg1Val.isRegular && resultVal.isRegular) { + if (resultVal.value == element.arrays(arg1Val.value)) 1.0 + else 0.0 + } else if (!arg1Val.isRegular && !resultVal.isRegular) 1.0 + else if (!arg1Val.isRegular && resultVal.isRegular) 0.0 + else 0.0 + factor.set(List(arg1Index, resultIndex), entry) + } + List(factor) + } + + /** + * Factor constructor for a FoldLeft Element + */ + def makeFactors[T,U](fold: FoldLeft[T,U]): List[Factor[Double]] = { + def makeOneFactor(currentAccumVar: Variable[U], elemVar: Variable[T], nextAccumVar: Variable[U]): Factor[Double] = { + val result = new BasicFactor[Double](List(currentAccumVar, elemVar), List(nextAccumVar)) + val currentAccumIndices = currentAccumVar.range.zipWithIndex + val elemIndices = elemVar.range.zipWithIndex + val nextAccumIndices = nextAccumVar.range.zipWithIndex + for { + (currentAccumVal, currentAccumIndex) <- currentAccumIndices + (elemVal, elemIndex) <- elemIndices + (nextAccumVal, nextAccumIndex) <- nextAccumIndices + } { + val entry = + if (currentAccumVal.isRegular && elemVal.isRegular && nextAccumVal.isRegular) { + if (nextAccumVal.value == fold.function(currentAccumVal.value, elemVal.value)) 1.0 + else 0.0 + } else if ((!currentAccumVal.isRegular || !elemVal.isRegular) && !nextAccumVal.isRegular) 1.0 + else 0.0 + result.set(List(currentAccumIndex, elemIndex, nextAccumIndex), entry) + } + result + } + + def makeFactorSequence(currentAccumVar: Variable[U], remaining: Seq[Element[T]]): List[Factor[Double]] = { + if (remaining.isEmpty) List() + else { + val firstVar = Variable(remaining.head) + val rest = remaining.tail + val nextAccumVar = + if (rest.isEmpty) Variable(fold) + else { + val currentAccumRegular = currentAccumVar.range.filter(_.isRegular).map(_.value) + val firstRegular = firstVar.range.filter(_.isRegular).map(_.value) + val nextVals = + for { + accum <- currentAccumRegular + first <- firstRegular + } yield fold.function(accum, first) + val nextHasStar = currentAccumVar.range.exists(!_.isRegular) || firstVar.range.exists(!_.isRegular) + val nextVS = if (nextHasStar) ValueSet.withStar(nextVals.toSet) else ValueSet.withoutStar(nextVals.toSet) + new Variable(nextVS) + } + val nextFactor = makeOneFactor(currentAccumVar, firstVar, nextAccumVar) + nextFactor :: makeFactorSequence(nextAccumVar, rest) + } + } + val startVar = new Variable(ValueSet.withoutStar(Set(fold.start))) + makeFactorSequence(startVar, fold.elements) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/DistributionFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/DistributionFactory.scala new file mode 100644 index 00000000..26bd68af --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/DistributionFactory.scala @@ -0,0 +1,95 @@ +/* + * DistributionFactory.scala + * Description needed + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Dec 15, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors.factory + +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.discrete._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ + +/** + * A Sub-Factory for simple probability distribution Elements + */ +object DistributionFactory { + + /** + * Factor constructor for an AtomicFlip + */ + def makeFactors(flip: AtomicFlip): List[Factor[Double]] = { + val flipVar = Variable(flip) + if (flipVar.range.exists(!_.isRegular)) { + assert(flipVar.range.size == 1) // Flip's range must either be {T,F} or {*} + StarFactory.makeStarFactor(flip) + } else { + val factor = new BasicFactor[Double](List(), List(flipVar)) + val i = flipVar.range.indexOf(Regular(true)) + factor.set(List(i), flip.prob) + factor.set(List(1 - i), 1.0 - flip.prob) + List(factor) + } + } + + /** + * Factor constructor for a CompoundFlip + */ + def makeFactors(flip: CompoundFlip): List[Factor[Double]] = { + val flipVar = Variable(flip) + if (flipVar.range.exists(!_.isRegular)) { + assert(flipVar.range.size == 1) // Flip's range must either be {T,F} or {*} + StarFactory.makeStarFactor(flip) + } else { + val probVar = Variable(flip.prob) + val factor = new BasicFactor[Double](List(probVar), List(flipVar)) + val parentVals = probVar.range + val i = flipVar.range.indexOf(Regular(true)) + for { j <- 0 until parentVals.size } { + if (parentVals(j).isRegular) { + val value = parentVals(j).value + factor.set(List(j, i), value) + factor.set(List(j, 1 - i), 1.0 - value) + } else { + factor.set(List(j, 0), 0.0) + factor.set(List(j, 1), 0.0) + } + } + List(factor) + } + } + + /** + * Factor constructor for a ParameterizedFlip + */ + def makeFactors(flip: ParameterizedFlip): List[Factor[Double]] = { + val flipVar = Variable(flip) + val factor = new BasicFactor[Double](List(),List(flipVar)) + val prob = flip.parameter.MAPValue + val i = flipVar.range.indexOf(Regular(true)) + factor.set(List(i), prob) + factor.set(List(1 - i), 1.0 - prob) + List(factor) + } + + /** + * Factor constructor for an AtomicBinomial + */ + def makeFactors(binomial: AtomicBinomial): List[Factor[Double]] = { + val binVar = Variable(binomial) + val factor = new BasicFactor[Double](List(), List(binVar)) + for { (xvalue, index) <- binVar.range.zipWithIndex } { + factor.set(List(index), binomial.density(xvalue.value)) + } + List(factor) + } + +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/SelectFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/SelectFactory.scala new file mode 100644 index 00000000..7a1b6182 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/SelectFactory.scala @@ -0,0 +1,167 @@ +/* + * SelectFactory.scala + * Description needed + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Dec 15, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors.factory + +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.library.compound._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.factors.Factory + +/** + * A Sub-Factory for Select or Dist Elements + */ +object SelectFactory { + + /** + * Factor constructor for an AtomicDistFlip + */ + def makeFactors[T](dist: AtomicDist[T]): List[Factor[Double]] = { + val (intermed, clauseFactors) = intermedAndClauseFactors(dist) + val intermedFactor = makeSimpleDistribution(intermed, dist.probs) + intermedFactor :: clauseFactors + } + + /** + * Factor constructor for a CompoundDist + */ + def makeFactors[T](dist: CompoundDist[T]): List[Factor[Double]] = { + val (intermed, clauseFactors) = intermedAndClauseFactors(dist) + val intermedFactor = makeComplexDistribution(intermed, dist.probs) + intermedFactor :: clauseFactors + } + + /** + * Factor constructor for an AtomicSelect + */ + def makeFactors[T](select: AtomicSelect[T]): List[Factor[Double]] = { + val selectVar = Variable(select) + if (selectVar.range.exists(!_.isRegular)) { + assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} + StarFactory.makeStarFactor(select) + } else { + val probs = getProbs(select) + List(makeSimpleDistribution(selectVar, probs)) + } + } + + /** + * Factor constructor for a CompoundSelect + */ + def makeFactors[T](select: CompoundSelect[T]): List[Factor[Double]] = { + val selectVar = Variable(select) + if (selectVar.range.exists(!_.isRegular)) { + assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} + StarFactory.makeStarFactor(select) + } else { + val probs = getProbs(select) + List(makeComplexDistribution(selectVar, probs)) + } + } + + /** + * Factor constructor for a ParameterizedSelect + */ + def makeFactors[T](select: ParameterizedSelect[T]): List[Factor[Double]] = { + val selectVar = Variable(select) + if (selectVar.range.exists(!_.isRegular)) { + assert(selectVar.range.size == 1) // Select's range must either be a list of regular values or {*} + StarFactory.makeStarFactor(select) + } else { + val probs = parameterizedGetProbs(select) + List(makeSimpleDistribution(selectVar, probs)) + } + } + + /** + * Factor constructor for an IntSelector + */ + def makeFactors[T](select: IntSelector): List[Factor[Double]] = { + val elementVar = Variable(select) + val counterVar = Variable(select.counter) + val comb = new BasicFactor[Double](List(counterVar), List(elementVar)) + comb.fillByRule((l: List[Any]) => { + val counterValue :: elementValue :: _ = l.asInstanceOf[List[Extended[Int]]] + if (counterValue.isRegular && elementValue.isRegular) { + if (elementValue.value < counterValue.value) 1.0 / counterValue.value; else 0.0 + } else 1.0 + + }) + List(comb) + } + + private def getProbs[U, T](select: Select[U, T]): List[U] = getProbs(select, select.clauses) + + /** + * Get the potential (probability) for each value of an element, based on supplied rules + */ + def getProbs[U, T](elem: Element[T], clauses: List[(U, T)]): List[U] = { + val selectVar = Variable(elem) + def getProb(xvalue: Extended[T]): U = { + clauses.find(_._2 == xvalue.value).get._1 // * cannot be a value of a Select + } + val probs = + for { xvalue <- selectVar.range } yield getProb(xvalue) + probs + } + + private def parameterizedGetProbs[T](select: ParameterizedSelect[T]): List[Double] = { + val outcomes = select.outcomes + val map = select.parameter.MAPValue + for { + xvalue <- Variable(select).range + index = outcomes.indexOf(xvalue.value) + } yield map(index) + } + + private def intermedAndClauseFactors[U, T](dist: Dist[U, T]): (Variable[Int], List[Factor[Double]]) = { + val intermed = new Variable(ValueSet.withoutStar((0 until dist.clauses.size).toSet)) + val clauseFactors = dist.outcomes.zipWithIndex map (pair => + Factory.makeConditionalSelector(dist, intermed, pair._2, Variable(pair._1))) + (intermed, clauseFactors) + } + + /** + * Constructs a BasicFactor from a probability distribution. It assumes that the probabilities + * are assigned to the Variable in the same order as it's values. + */ + def makeSimpleDistribution[T](target: Variable[T], probs: List[Double]): Factor[Double] = { + val factor = new BasicFactor[Double](List(), List(target)) + for { (prob, index) <- probs.zipWithIndex } { + factor.set(List(index), prob) + } + factor + } + + private def makeComplexDistribution[T](target: Variable[T], probElems: List[Element[Double]]): Factor[Double] = { + val probVars: List[Variable[Double]] = probElems map (Variable(_)) + val nVars = probVars.size + val factor = new BasicFactor[Double](probVars, List(target)) + val probVals: List[List[Extended[Double]]] = probVars map (_.range) + for { indices <- factor.allIndices } { + // unnormalized is a list, one for each probability element, of the value of that element under these indices + val unnormalized = + // expects outcome to be first, but isn't + for { (probIndex, position) <- indices.toList.take(nVars).zipWithIndex } yield { + val xprob = probVals(position)(probIndex) // The probability of the particular value of the probability element in this position + if (xprob.isRegular) xprob.value; else 0.0 + } + val normalized = normalize(unnormalized).toArray + // The first variable specifies the position of the remaining variables, so indices(0) is the correct probability + factor.set(indices, normalized(indices.last)) + } + factor + } +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/StarFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/StarFactory.scala new file mode 100644 index 00000000..767a745f --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/StarFactory.scala @@ -0,0 +1,37 @@ +/* + * StarFactory.scala + * Description needed + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Dec 15, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.factored.factors.factory + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ + +/** + * A Sub-Factory to make Star Factors from arbitrary elements + */ +object StarFactory { + + /** + * Make a StarFactor from an Element Boolean, Element.Contingency), upper: Boolean): Factor[Double] = - makeConstraintFactor(elem, (ProbConstraintType((t: T) => if (cc._1(t)) 1.0; else 0.0), cc._2), upper) - - private def makeConstraintFactor[T](elem: Element[T], cc: (T => Double, Element.Contingency), upper: Boolean): Factor[Double] = { - val (constraint, contingency) = cc - contingency match { - case List() => makeUncontingentConstraintFactor(elem, constraint, upper) - case first :: rest => makeContingentConstraintFactor(elem, constraint, first, rest, upper) - } - } - - private def makeUncontingentConstraintFactor[T](elem: Element[T], constraint: T => Double, upper: Boolean): Factor[Double] = { - val elemVar = Variable(elem) - val factor = Factory.make[Double](List(elemVar)) - for { (elemVal, index) <- elemVar.range.zipWithIndex } { - val entry = if (elemVal.isRegular) { - val c = math.exp(constraint(elemVal.value)) - c - } else { - // The (0,1) Double assume the constraint is always bounded between 0 and 1. This is always correct for conditions. - // For constraints that could be greater than 1, the resulting Double on the answer could be wrong. - if (upper) 1.0; else 0.0 - } - factor.set(List(index), entry) - } - factor - } - - private def makeContingentConstraintFactor[T](elem: Element[T], constraint: T => Double, firstConting: Element.ElemVal[_], restContinges: Element.Contingency, upper: Boolean): Factor[Double] = { - val restFactor = makeConstraintFactor(elem, (constraint, restContinges), upper) - extendConstraintFactor(restFactor, firstConting) - } - - private def extendConstraintFactor(restFactor: Factor[Double], firstConting: Element.ElemVal[_]): Factor[Double] = { - // The extended factor is obtained by getting the underlying factor and expanding each row so that the row only provides its entry if the contingent variable takes - // on the appropriate value, otherwise the entry is 1 - val Element.ElemVal(firstElem, firstValue) = firstConting - val firstVar = Variable(firstElem) - val firstValues = firstVar.range - val numFirstValues = firstValues.size - val matchingIndex: Int = firstValues.indexOf(Regular(firstValue)) - val resultFactor = Factory.make[Double](firstVar :: restFactor.variables) - for { restIndices <- restFactor.allIndices } { - val restEntry = restFactor.get(restIndices) - for { firstIndex <- 0 until numFirstValues } { - // constraint doesn't apply if the index is not the required one, so we use a value of 1 - val resultEntry = if (firstIndex == matchingIndex) restEntry; else 1.0 - resultFactor.set(firstIndex :: restIndices, resultEntry) - } - } - resultFactor - } - - /** - * Create the probabilistic factors associated with an element. - */ - def make(elem: Element[_], upper: Boolean): List[Factor[Double]] = { - val constraintFactors = makeConditionAndConstraintFactors(elem, upper) - constraintFactors ::: Factory.makeNonConstraintFactors(elem) - } - - /** - * Create the probabilistic factor encoding the probability of evidence in the dependent universe as a function of the - * values of variables in the parent universe. The third argument is the the function to use for computing - * probability of evidence in the dependent universe. It is assumed that the definition of this function will already contain the - * right evidence. - */ - def makeDependentFactor(parentUniverse: Universe, - dependentUniverse: Universe, - probEvidenceComputer: () => Double): (Factor[Double], Factor[Double]) = { - val uses = dependentUniverse.parentElements filter (_.universe == parentUniverse) - def rule(upper: Boolean)(values: List[Extended[_]]) = { - if (values.exists(!_.isRegular)) { if (upper) 1.0; else 0.0 } - else { - for { (elem, value) <- uses zip values } { elem.value = value.asInstanceOf[Regular[elem.Value]].value } - val result = probEvidenceComputer() - result - } - } - val variables = uses map (Variable(_)) - val lb = Factory.make[Double](variables) - lb.fillByRule(rule(false)) - val ub = Factory.make[Double](variables) - ub.fillByRule(rule(true)) - (lb, ub) - } -} +/* + * BoundedProbFactor.scala + * Factors over variables. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 15, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.lazyfactored + +import com.cra.figaro.algorithm._ +import com.cra.figaro.language.{Element, ProbConstraintType, Universe} +import com.cra.figaro.util._ +import annotation.tailrec +import scala.language.existentials +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.Factory + +/** + * Methods for creating lower and upper bound probability factors. + */ +object BoundedProbFactor { + private def makeConditionAndConstraintFactors[T](elem: Element[T], upper: Boolean): List[Factor[Double]] = + elem.allConditions.map(makeConditionFactor(elem, _, upper)) ::: elem.allConstraints.map(makeConstraintFactor(elem, _, upper)) + + private def makeConditionFactor[T](elem: Element[T], cc: (T => Boolean, Element.Contingency), upper: Boolean): Factor[Double] = + makeConstraintFactor(elem, (ProbConstraintType((t: T) => if (cc._1(t)) 1.0; else 0.0), cc._2), upper) + + private def makeConstraintFactor[T](elem: Element[T], cc: (T => Double, Element.Contingency), upper: Boolean): Factor[Double] = { + val (constraint, contingency) = cc + contingency match { + case List() => makeUncontingentConstraintFactor(elem, constraint, upper) + case first :: rest => makeContingentConstraintFactor(elem, constraint, first, rest, upper) + } + } + + private def makeUncontingentConstraintFactor[T](elem: Element[T], constraint: T => Double, upper: Boolean): Factor[Double] = { + val elemVar = Variable(elem) + val factor = Factory.make(elem)(0) + for { (elemVal, index) <- elemVar.range.zipWithIndex } { + val entry = if (elemVal.isRegular) { + val c = math.exp(constraint(elemVal.value)) + c + } else { + // The (0,1) Double assume the constraint is always bounded between 0 and 1. This is always correct for conditions. + // For constraints that could be greater than 1, the resulting Double on the answer could be wrong. + if (upper) 1.0; else 0.0 + } + factor.set(List(index), entry) + } + factor + } + + private def makeContingentConstraintFactor[T](elem: Element[T], constraint: T => Double, firstConting: Element.ElemVal[_], restContinges: Element.Contingency, upper: Boolean): Factor[Double] = { + val restFactor = makeConstraintFactor(elem, (constraint, restContinges), upper) + extendConstraintFactor(restFactor, firstConting) + } + + private def extendConstraintFactor(baseFactor: Factor[Double], firstConting: Element.ElemVal[_]): Factor[Double] = { + // The extended factor is obtained by getting the underlying factor and expanding each row so that the row only provides its entry if the contingent variable takes + // on the appropriate value, otherwise the entry is 1 + val Element.ElemVal(firstElem, firstValue) = firstConting + val firstVar = Variable(firstElem) + val firstValues = firstVar.range + val numFirstValues = firstValues.size + val matchingIndex: Int = firstValues.indexOf(Regular(firstValue)) + val resultFactor = Factory.defaultFactor[Double](firstVar :: baseFactor.parents, baseFactor.output) + for { factorIndices <- baseFactor.allIndices } { + val factorValue = baseFactor.get(factorIndices) + for { firstIndex <- 0 until numFirstValues } { + // constraint doesn't apply if the index is not the required one, so we use a value of 1 + val resultEntry = if (firstIndex == matchingIndex) factorValue; else 1.0 + resultFactor.set(firstIndex :: factorIndices, resultEntry) + } + } + resultFactor + } + + /** + * Create the probabilistic factors associated with an element. + */ + def make(elem: Element[_], upper: Boolean): List[Factor[Double]] = { + val constraintFactors = makeConditionAndConstraintFactors(elem, upper) + constraintFactors ::: Factory.makeNonConstraintFactors(elem) + } + + /** + * Create the probabilistic factor encoding the probability of evidence in the dependent universe as a function of the + * values of variables in the parent universe. The third argument is the the function to use for computing + * probability of evidence in the dependent universe. It is assumed that the definition of this function will already contain the + * right evidence. + */ + def makeDependentFactor(parentUniverse: Universe, + dependentUniverse: Universe, + probEvidenceComputer: () => Double): (Factor[Double], Factor[Double]) = { + val uses = dependentUniverse.parentElements filter (_.universe == parentUniverse) + def rule(upper: Boolean)(values: List[Extended[_]]) = { + if (values.exists(!_.isRegular)) { if (upper) 1.0; else 0.0 } + else { + for { (elem, value) <- uses zip values } { elem.value = value.asInstanceOf[Regular[elem.Value]].value } + val result = probEvidenceComputer() + result + } + } + val variables = uses map (Variable(_)) + val lb = Factory.defaultFactor[Double](variables, List()) + lb.fillByRule(rule(false)) + val ub = Factory.defaultFactor[Double](variables, List()) + ub.fillByRule(rule(true)) + (lb, ub) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyValues.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyValues.scala index 8d413265..3b5b2803 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyValues.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyValues.scala @@ -1,13 +1,13 @@ /* * Values.scala * Lazily compute the range of values of elements. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Dec 27, 2013 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -23,25 +23,29 @@ import ValueSet._ import com.cra.figaro.algorithm.factored.ParticleGenerator /* - * The Values class takes a universe and a depth and provides a + * The Values class takes a universe and a depth and provides a * Computing values in a lazy way requires a depth to be specified. Every time a Chain is expanded, the depth is decreased by 1. If the depth is 0, the chain is * not expanded, and its value set contains only Star, which means unspecified values. - * + * */ +/** + * Object for lazily computing the range of values of elements in a universe. Given a universe, you can compute the values + * of elements in the universe to any desired depth. + */ class LazyValues(universe: Universe) { private def values[T](element: Element[T], depth: Int, numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = { // In some cases (e.g. CompoundFlip), we might know the value of an element without getting the values of its arguments. // However, future algorithms rely on the values of the arguments having been gotten already. - // Therefore, we compute the values of the arguments (to a lesser depth) first, + // Therefore, we compute the values of the arguments (to a lesser depth) first, // and use the stored values of the arguments when we actually compute the values of the element. - + // Override the argument list for chains since the resultElement of the chain will be processed as the chain is processed val elementArgs = element match { - case c: Chain[_,_] => List(c.parent) + case c: Chain[_,_] => List(c.parent) case _ => element.args } - + for { arg <- elementArgs } { LazyValues(arg.universe)(arg, depth - 1, numArgSamples, numTotalSamples) usedBy(arg) = usedBy.getOrElse(arg, Set()) + element @@ -150,7 +154,7 @@ class LazyValues(universe: Universe) { def findChainValues[T, U](chain: Chain[T, U], cmap: Map[T, Element[U]], pVals: ValueSet[T], samples: Int): Set[ValueSet[U]] = { val chainVals = pVals.regularValues.map { parentVal => val resultElem = getOrElseInsert(cmap, parentVal, chain.getUncached(parentVal)) - val result = LazyValues(resultElem.universe)(resultElem, depth - 1, samples, samples) + val result = LazyValues(resultElem.universe)(resultElem, depth - 1, samples, samples) usedBy(resultElem) = usedBy.getOrElse(resultElem, Set()) + element result } @@ -163,9 +167,9 @@ class LazyValues(universe: Universe) { } val chainMap = getMap(c) - val parentVS = LazyValues(c.parent.universe).storedValues(c.parent) + val parentVS = LazyValues(c.parent.universe).storedValues(c.parent) val samplesPerValue = math.max(1, (numTotalSamples.toDouble/parentVS.regularValues.size).toInt) - + val resultVSs = findChainValues(c, chainMap, parentVS, samplesPerValue) val startVS: ValueSet[c.Value] = @@ -185,7 +189,7 @@ class LazyValues(universe: Universe) { println("Warning: Sampling element " + a + " even though no sampler defined for this universe") } val thisSampler = ParticleGenerator(universe) - val samples = thisSampler(a, numArgSamples) + val samples = thisSampler(a, numArgSamples) withoutStar(samples.unzip._2.toSet) } case _ => @@ -211,13 +215,13 @@ class LazyValues(universe: Universe) { if (hasStar) withStar(results); else withoutStar(results) } - /* + /* * The memoized values for an element contains the value set that was found so far for the element, together with * the depth of expansion that was performed. */ private val memoValues: Map[Element[_], (ValueSet[_], Int)] = Map() universe.register(memoValues) - + private val requiredDepths: Map[Element[_], Int] = Map() universe.register(requiredDepths) @@ -228,7 +232,7 @@ class LazyValues(universe: Universe) { */ private val usedBy: Map[Element[_], Set[Element[_]]] = Map() universe.register(usedBy) - + /** * Returns the range of values of an element. This method is memoized. * If it has previously been called on the same element with a depth at least as great as this one, @@ -243,7 +247,7 @@ class LazyValues(universe: Universe) { } apply(element, depth, numArgSamples, numTotalSamples) } - + def apply[T](element: Element[T], depth: Int, numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = { val myDepth = requiredDepths.getOrElse(element, -1).max(depth) if (LazyValues.debug) { @@ -325,7 +329,7 @@ class LazyValues(universe: Universe) { * even for non-caching chains. First, it is necessary for correctness of factored inference. Second, if we are computing the range of values * of the chain, we are already computing all the values of the parent, so in most cases the number of parent values will not be so large as to * cause memory problems. - * + * * New development: We have discovered a design pattern where the result of an Apply is a container of elements. For example, see the lazy list example * where the result of Apply is a Cons containing a head element and a tail element. In these cases, we also need to make sure that these elements are * the same. Therefore, we also have to maintain a map from Apply arguments to their resulting values. This cache is contained in applyMap. @@ -392,13 +396,13 @@ object LazyValues { e.applyMaps.clear() e.usedBy.clear() e.requiredDepths.clear() - + universe.deregister(e.memoValues) universe.deregister(e.chainMaps) universe.deregister(e.applyMaps) universe.deregister(e.usedBy) universe.deregister(e.requiredDepths) - + universe.deregisterUniverse(expansions) expansions -= universe } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyVariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyVariableElimination.scala index bd65e959..73a844c2 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyVariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/LazyVariableElimination.scala @@ -1,334 +1,363 @@ -/* - * LazyVE.scala - * Lazy variable elimination algorithm. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Dec 28, 2013 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.lazyfactored - -import com.cra.figaro.algorithm.{LazyAlgorithm, ProbQueryAlgorithm} -import com.cra.figaro.language.{Element, Universe} -import com.cra.figaro.algorithm.factored._ -import scala.annotation.tailrec -import com.cra.figaro.util._ -import scala.collection.mutable.{Map, Set} - -class LazyVariableElimination(targetElements: Element[_]*)(implicit val universe: Universe) extends FactoredAlgorithm[Double] -with LazyAlgorithm { - var debug = false - var showTiming = false - - val dependentAlgorithm = null - - val dependentUniverses = List() - - val semiring = SumProductSemiring - - var currentResult: Factor[(Double, Double)] = _ - - def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = { - for { - elem <- neededElements - factor <- BoundedProbFactor.make(elem, upperBounds) - if !factor.isEmpty - } yield factor - } - - private def optionallyShowTiming[T](op: => T, name: String) = - if (showTiming) timed(op, name); else op - - def run(depth: Int) { - Variable.clearCache() - val (includedElements, needsBounds) = getNeededElements(targetElements.toList, depth) - val targetVariables = targetElements.map(Variable(_)) - if (needsBounds) { - Factory.removeFactors() - val lowerFactors = getFactors(includedElements, targetElements.toList, false) - Factory.removeFactors() - val upperFactors = getFactors(includedElements, targetElements.toList, true) - val lowerFactorsAfterElimination = doElimination(lowerFactors, targetVariables) - val upperFactorsAfterElimination = doElimination(upperFactors, targetVariables) - currentResult = finishWithBounds(lowerFactorsAfterElimination, upperFactorsAfterElimination) - } else { - Factory.removeFactors() - val allFactors = getFactors(includedElements, targetElements.toList) - val factorsAfterElimination = doElimination(allFactors, targetVariables) - currentResult = finishNoBounds(factorsAfterElimination) - } - marginalize(currentResult) - } - - protected def doElimination(allFactors: List[Factor[Double]], targetVariables: Seq[Variable[_]]): Set[Factor[Double]] = { - val allVars = (Set[/*Extended*/Variable[_]]() /: allFactors)(_ ++ _./*extended*/variables) - if (debug) { - println("*****************\nElement ids:") - for { variable <- allVars } { - variable match { - case elemVar: /*Extended*/ElementVariable[_] => - println(variable.id + "(" + elemVar.element.name.string + ")" + "@" + elemVar.element.hashCode + ": " + elemVar.element) - case _ => - println(variable.id + ": not an element variable") - } - } - } - recordingFactors = List() - if (debug) { - println("*****************\nStarting factors\n") - allFactors.foreach((f: /*Extended*/Factor[_]) => println(f.toReadableString)) - } - val order = optionallyShowTiming(eliminationOrder(allVars, allFactors, targetVariables), "Computing elimination order") - val factorsAfterElimination = - optionallyShowTiming(eliminateInOrder(order, Set(allFactors: _*), initialFactorMap(allFactors)), "Elimination") - if (debug) println("*****************") - if (debug) factorsAfterElimination foreach (f => println(f.toReadableString)) - factorsAfterElimination - } - - /* - * Determine if the second row can be absorbed into the first. This is true iff, for each position, either the second value is * - * or they are equal. - */ - private def pairConsistent(range: Array[Extended[_]], index1: Int, index2: Int): Boolean = { - val x1 = range(index1) - val x2 = range(index2) - x1 == x2 || !x2.isRegular - } - - private def consistent(ranges: List[Array[Extended[_]]], indices1: List[Int], indices2: List[Int]): Boolean = { - (ranges.zip(indices1).zip(indices2)).forall{ case ((range, indices1), indices2) => pairConsistent(range, indices1, indices2) } - } - - /* - * We use the mathematician's solution. Finalizing with no bounds is the same as finalizing with equal lower and upper bounds, - * so we just delegate to normalizeAndAbsorbWithBounds. - */ - private def normalizeAndAbsorbNoBounds(factor: Factor[Double]): Factor[(Double, Double)] = { - normalizeAndAbsorbWithBounds(factor, factor) - } - - /* - * The job of normalizeAndAbsorbWithBounds is to take a factor consisting of unnormalized lower bounds for regular values and *, - * and another factor containing unnormalized upper bounds for regular values and *, and to compute normalized lower and upper - * bounds for each of the regular values. These bounds should satisfy that the true probability of the regular values lies - * between the bounds. - * - * The process of normalizeAndAbsorbWithBounds is as follows: - * (1) Compute the sums of the unnormalized lower and upper bounds. These constitute lower and upper bounds on the normalizing factor, - * respectively. - * (2) Compute the normalized lower bounds, which are the unnormalized lower bounds divided by the upper bound on the normalizing factor. - * The normalized lower bound on a regular value represents definitely allocated probability to that value, that cannot possibly be - * allocated to any other regular value. This fact will be used in calculating one of the upper bounds. - * (3) Compute the first upper bound. This consists of all the probability mass that has not definitely been allocated to other - * values. Since the resulting factor might have more than one variable, we need to determine which rows probability mass can definitely - * not be allocated to a particular row. This is determined by the consistent subroutine below. The first upper bound on the normalized - * probability of a given row is equal to 1 - the sum of normalized lower bounds of rows that are not consistent with this row. - * (4) For the second upper bound, we use the opposite approach. We add together all the unnormalized upper bounds of rows that are - * consistent with this row, and then divide by the lower bound on the normalizing factor. - * (5) Finally, we set the lower and upper bounds of the row in the result to be the computed normalized lower bound and the lesser - * of the two computed upper bounds. - */ - private def normalizeAndAbsorbWithBounds(lowerFactor: Factor[Double], upperFactor: Factor[Double]): Factor[(Double, Double)] = { - assert(lowerFactor.variables == upperFactor.variables) - - val ranges: List[Array[Extended[_]]] = lowerFactor.variables.map(_.range.toArray[Extended[_]]) - - /* - * First, we compute the sum of unnormalized lower and upper bounds. - */ - val result = Factory.make[(Double, Double)](lowerFactor.variables) - var lowerTotal = 0.0 - var upperTotal = 0.0 - var starTotal = 0.0 - val allIndicesIndexed = lowerFactor.allIndices.zipWithIndex - for { (indices, i) <- allIndicesIndexed } { - val lower = lowerFactor.get(indices) - val upper = upperFactor.get(indices) - lowerTotal += lower - upperTotal += upper - } - - /* - * Get the definitely allocated probability mass: upperTotal is an upper bound on the normalizing factor. - */ - var lowerBounds: Array[Double] = Array.fill(allIndicesIndexed.size)(0) - for { (indices, i) <- allIndicesIndexed } { lowerBounds(i) = lowerFactor.get(indices) / upperTotal } - - /* - * There are two possible upper bounds of a row. - * The first is 1 - the definite lower bounds of incompatible rows. - * The second is the sum of all compatible rows' unnormalized upper bounds - * divided by the lower bound on the normalizing factor. - * We use the lesser of these two bounds. - */ - var upperBounds: Array[Double] = Array.fill(allIndicesIndexed.size)(0) - for { (indices, i) <- allIndicesIndexed } { - var inconsistentLowerTotal = 0.0 - var consistentUpperTotal = 0.0 - for { (otherIndices, j) <- allIndicesIndexed } { - if (!consistent(ranges, indices, otherIndices)) inconsistentLowerTotal += lowerBounds(j) - else consistentUpperTotal += upperFactor.get(otherIndices) - } - upperBounds(i) = (1.0 - inconsistentLowerTotal).min(consistentUpperTotal / lowerTotal) - } - - /* - * Finally, create the result factor with lower and upper bounds. - */ - for { (indices, i) <- allIndicesIndexed } { - result.set(indices, (lowerBounds(i), upperBounds(i))) - } - result - } - - var targetFactors: Map[Element[_], Factor[(Double, Double)]] = Map() - - private def marginalizeToTarget(factor: Factor[(Double, Double)], target: Element[_]): Unit = { - val targetFactor = factor.marginalizeTo(BoundsSumProductSemiring, Variable(target)) - targetFactors += target -> targetFactor - } - - private def marginalize(resultFactor: Factor[(Double, Double)]) = { - targetElements foreach (marginalizeToTarget(resultFactor, _)) - } - - def probabilityBounds[T](target: Element[_], value: T): (Double, Double) = { - require(targetElements contains target) - val index = Variable(target).range.indexOf(Regular(value)) - if (index == -1) (0, 1) - else targetFactors(target).get(List(index)) - } - - def finishNoBounds(factorsAfterElimination: Set[Factor[Double]]): Factor[(Double, Double)] = { - // It is possible that there are no factors (this will happen if there is no evidence). - // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. - val multiplied = factorsAfterElimination.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - val normalized = normalizeAndAbsorbNoBounds(multiplied) - normalized - } - - def finishWithBounds(lowerFactors: Set[Factor[Double]], upperFactors: Set[Factor[Double]]): Factor[(Double, Double)] = { - // It is possible that there are no factors (this will happen if there is no evidence). - // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. - val lowerMultiplied = lowerFactors.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - val upperMultiplied = upperFactors.foldLeft(Factor.unit(semiring))(_.product(_, semiring)) - val normalized = normalizeAndAbsorbWithBounds(lowerMultiplied, upperMultiplied) - normalized - } - - /* This code is copied straight from VariableElimination. It should be refactored to avoid duplication. */ - // The first element of FactorMap is the complete set of factors. - // The second element maps variables to the factors mentioning that variable. - private type FactorMap[T] = Map[Variable[_], Set[Factor[T]]] - - private def addFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = - factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) + factor)) - - private def removeFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = - factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) - factor)) - - private def initialFactorMap(factors: Traversable[Factor[Double]]): FactorMap[Double] = { - val map: FactorMap[Double] = Map() - factors foreach (addFactor(_, map)) - map - } - - protected var recordingFactors: List[Factor[_]] = List() - - /** - * Some variable elimination algorithms, such as computing the most probable explanation, record values of - * variables as they are eliminated. Such values are stored in a factor that maps values of the other variables - * to a value of the eliminated variable. This factor is produced by finding the value of the variable that - * "maximizes" the entry associated with the value in the product factor resulting from eliminating this - * variable, for some maximization function. The recordingFunction determines which of two entries is greater - * according to the maximization function. It returns true iff the second entry is greater. The recording - * function is an option so that variable elimination algorithms that do not use it can ignore it. - */ - val comparator: Option[(Double, Double) => Boolean] = None - - private def eliminate( - variable: Variable[_], - factors: Set[Factor[Double]], - map: FactorMap[Double]): Unit = { - val varFactors: Set[Factor[Double]] = map(variable) - if (debug) { - println("*****************\nEliminating " + variable.id) - println("Input factors:") - for { factor <- varFactors } { println(factor.toReadableString) } - } - if (varFactors.nonEmpty) { - def multiply(f1: Factor[Double], f2: Factor[Double]): Factor[Double] = f1.product(f2, semiring) - val productFactor: Factor[Double] = varFactors.reduceLeft(_.product(_, semiring)) - val resultFactor: Factor[Double] = productFactor.sumOver(variable, semiring) - varFactors foreach (removeFactor(_, map)) - addFactor(resultFactor, map) - comparator match { - case None => () - case Some(recorder) => recordingFactors ::= productFactor.recordArgMax(variable, recorder) - } - map -= variable - factors --= varFactors - if (debug) println("Result factor\n" + resultFactor.toReadableString) - factors += resultFactor - } - } - - private def eliminateInOrder( - order: List[Variable[_]], - factors: Set[Factor[Double]], - map: FactorMap[Double]): Set[Factor[Double]] = - order match { - case Nil => - factors - case first :: rest => - eliminate(first, factors, map) - eliminateInOrder(rest, factors, map) - } - - // for debugging - private def printVariables(variables: Traversable[/*Extended*/Variable[_]]) { - for { variable <- variables } { - print(" ") - variable match { - case ev: ElementVariable[_] => - println(ev.element.toNameString) - case _ => println(variable) - } - } - } - /** - * Method for choosing the elimination order. - * The default order chooses first the variable that - * minimizes the number of extra factor entries that would be created when it is eliminated. - * Override this method if you want a different rule. - */ - def eliminationOrder(allVars: Set[Variable[_]], factors: Traversable[Factor[Double]], toPreserve: Traversable[Variable[_]]): List[Variable[_]] = { - val eliminableVars = allVars -- toPreserve - var initialGraph = new VEGraph(factors) - val candidates = new HeapPriorityMap[Variable[_], Double] - eliminableVars foreach (v => candidates += v.asInstanceOf[Variable[_]] -> initialGraph.score(v)) - eliminationOrderHelper(candidates, toPreserve, initialGraph, List()) - } - - @tailrec private def eliminationOrderHelper(candidates: PriorityMap[Variable[_], Double], - toPreserve: Traversable[Variable[_]], - graph: VEGraph, - accum: List[Variable[_]]): List[Variable[_]] = { - if (candidates.isEmpty) accum.reverse - else { - val best = candidates.extractMin()._1 - // do not read the best variable after it has been removed, and do not add the preserved variables - val touched = graph.info(best).neighbors - best -- toPreserve - val nextGraph = graph.eliminate(best) - touched foreach (v => candidates += v.asInstanceOf[Variable[_]] -> graph.score(v)) - eliminationOrderHelper(candidates, toPreserve, nextGraph, best :: accum) - } - } - -} +/* + * LazyVE.scala + * Lazy variable elimination algorithm. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Dec 28, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.lazyfactored + +import com.cra.figaro.algorithm.{LazyAlgorithm, ProbQueryAlgorithm} +import com.cra.figaro.algorithm.factored.{FactoredAlgorithm, VEGraph} +import com.cra.figaro.language.{Element, Universe} +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.Factory +import scala.annotation.tailrec +import com.cra.figaro.util._ +import scala.collection.mutable.{Map, Set} + +/** + * Algorithm that lazily performs variable elimination. This algorithm is a lazy algorithm that can be run to any depth. + * Given a depth, it expands the model up to that depth and creates factors for the expanded elements. + * It also creates factors that capture the effect of parts of the model that have not been expanded on the query targets. + * These factors are used to compute lower or upper bounds on the queries. + * Then it uses ordinary variable elimination to solve these factors. + */ +class LazyVariableElimination(targetElements: Element[_]*)(implicit val universe: Universe) extends FactoredAlgorithm[Double] +with LazyAlgorithm { + var debug = false + var showTiming = false + + val dependentAlgorithm = null + + val dependentUniverses = List() + + val semiring = SumProductSemiring + + var currentResult: Factor[(Double, Double)] = _ + + /** + * Create the necessary factors. + * + * @param neededElements elements that have been expanded that need factors created + * @param targetElements query targets + * @param upperBounds flag indicating whether lower (false) or upper (true) bounds should be computed for unexpanded parts of the model + */ + def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = { + for { + elem <- neededElements + factor <- BoundedProbFactor.make(elem, upperBounds) + if !factor.isEmpty + } yield factor + } + + private def optionallyShowTiming[T](op: => T, name: String) = + if (showTiming) timed(op, name); else op + + def run(depth: Int) { + Variable.clearCache() + val (includedElements, needsBounds) = getNeededElements(targetElements.toList, depth) + val targetVariables = targetElements.map(Variable(_)) + if (needsBounds) { + Factory.removeFactors() + val lowerFactors = getFactors(includedElements, targetElements.toList, false) + Factory.removeFactors() + val upperFactors = getFactors(includedElements, targetElements.toList, true) + val lowerFactorsAfterElimination = doElimination(lowerFactors, targetVariables) + val upperFactorsAfterElimination = doElimination(upperFactors, targetVariables) + currentResult = finishWithBounds(lowerFactorsAfterElimination, upperFactorsAfterElimination) + } else { + Factory.removeFactors() + val allFactors = getFactors(includedElements, targetElements.toList) + val factorsAfterElimination = doElimination(allFactors, targetVariables) + currentResult = finishNoBounds(factorsAfterElimination) + } + marginalize(currentResult) + } + + protected def doElimination(allFactors: List[Factor[Double]], targetVariables: Seq[Variable[_]]): Set[Factor[Double]] = { + val allVars = (Set[/*Extended*/Variable[_]]() /: allFactors)(_ ++ _./*extended*/variables) + if (debug) { + println("*****************\nElement ids:") + for { variable <- allVars } { + variable match { + case elemVar: /*Extended*/ElementVariable[_] => + println(variable.id + "(" + elemVar.element.name.string + ")" + "@" + elemVar.element.hashCode + ": " + elemVar.element) + case _ => + println(variable.id + ": not an element variable") + } + } + } + recordingFactors = List() + if (debug) { + println("*****************\nStarting factors\n") + allFactors.foreach((f: /*Extended*/Factor[_]) => println(f.toReadableString)) + } + val order = optionallyShowTiming(eliminationOrder(allVars, allFactors, targetVariables), "Computing elimination order") + val factorsAfterElimination = + optionallyShowTiming(eliminateInOrder(order, Set(allFactors: _*), initialFactorMap(allFactors)), "Elimination") + if (debug) println("*****************") + if (debug) factorsAfterElimination foreach (f => println(f.toReadableString)) + factorsAfterElimination + } + + /* + * Determine if the second row can be absorbed into the first. This is true iff, for each position, either the second value is * + * or they are equal. + */ + private def pairConsistent(range: Array[Extended[_]], index1: Int, index2: Int): Boolean = { + val x1 = range(index1) + val x2 = range(index2) + x1 == x2 || !x2.isRegular + } + + private def consistent(ranges: List[Array[Extended[_]]], indices1: List[Int], indices2: List[Int]): Boolean = { + (ranges.zip(indices1).zip(indices2)).forall{ case ((range, indices1), indices2) => pairConsistent(range, indices1, indices2) } + } + + /* + * We use the mathematician's solution. Finalizing with no bounds is the same as finalizing with equal lower and upper bounds, + * so we just delegate to normalizeAndAbsorbWithBounds. + */ + private def normalizeAndAbsorbNoBounds(factor: Factor[Double]): Factor[(Double, Double)] = { + normalizeAndAbsorbWithBounds(factor, factor) + } + + /* + * The job of normalizeAndAbsorbWithBounds is to take a factor consisting of unnormalized lower bounds for regular values and *, + * and another factor containing unnormalized upper bounds for regular values and *, and to compute normalized lower and upper + * bounds for each of the regular values. These bounds should satisfy that the true probability of the regular values lies + * between the bounds. + * + * The process of normalizeAndAbsorbWithBounds is as follows: + * (1) Compute the sums of the unnormalized lower and upper bounds. These constitute lower and upper bounds on the normalizing factor, + * respectively. + * (2) Compute the normalized lower bounds, which are the unnormalized lower bounds divided by the upper bound on the normalizing factor. + * The normalized lower bound on a regular value represents definitely allocated probability to that value, that cannot possibly be + * allocated to any other regular value. This fact will be used in calculating one of the upper bounds. + * (3) Compute the first upper bound. This consists of all the probability mass that has not definitely been allocated to other + * values. Since the resulting factor might have more than one variable, we need to determine which rows probability mass can definitely + * not be allocated to a particular row. This is determined by the consistent subroutine below. The first upper bound on the normalized + * probability of a given row is equal to 1 - the sum of normalized lower bounds of rows that are not consistent with this row. + * (4) For the second upper bound, we use the opposite approach. We add together all the unnormalized upper bounds of rows that are + * consistent with this row, and then divide by the lower bound on the normalizing factor. + * (5) Finally, we set the lower and upper bounds of the row in the result to be the computed normalized lower bound and the lesser + * of the two computed upper bounds. + */ + private def normalizeAndAbsorbWithBounds(lowerFactor: Factor[Double], upperFactor: Factor[Double]): Factor[(Double, Double)] = { + assert(lowerFactor.variables == upperFactor.variables) + + val ranges: List[Array[Extended[_]]] = lowerFactor.variables.map(_.range.toArray[Extended[_]]) + + /* + * First, we compute the sum of unnormalized lower and upper bounds. + */ + val result = Factory.defaultFactor[(Double, Double)](lowerFactor.parents, lowerFactor.output) + var lowerTotal = 0.0 + var upperTotal = 0.0 + var starTotal = 0.0 + val allIndicesIndexed = lowerFactor.allIndices.zipWithIndex + for { (indices, i) <- allIndicesIndexed } { + val lower = lowerFactor.get(indices) + val upper = upperFactor.get(indices) + lowerTotal += lower + upperTotal += upper + } + + /* + * Get the definitely allocated probability mass: upperTotal is an upper bound on the normalizing factor. + */ + var lowerBounds: Array[Double] = Array.fill(allIndicesIndexed.size)(0) + for { (indices, i) <- allIndicesIndexed } { lowerBounds(i) = lowerFactor.get(indices) / upperTotal } + + /* + * There are two possible upper bounds of a row. + * The first is 1 - the definite lower bounds of incompatible rows. + * The second is the sum of all compatible rows' unnormalized upper bounds + * divided by the lower bound on the normalizing factor. + * We use the lesser of these two bounds. + */ + var upperBounds: Array[Double] = Array.fill(allIndicesIndexed.size)(0) + for { (indices, i) <- allIndicesIndexed } { + var inconsistentLowerTotal = 0.0 + var consistentUpperTotal = 0.0 + for { (otherIndices, j) <- allIndicesIndexed } { + if (!consistent(ranges, indices, otherIndices)) inconsistentLowerTotal += lowerBounds(j) + else consistentUpperTotal += upperFactor.get(otherIndices) + } + upperBounds(i) = (1.0 - inconsistentLowerTotal).min(consistentUpperTotal / lowerTotal) + } + + /* + * Finally, create the result factor with lower and upper bounds. + */ + for { (indices, i) <- allIndicesIndexed } { + result.set(indices, (lowerBounds(i), upperBounds(i))) + } + result + } + + var targetFactors: Map[Element[_], Factor[(Double, Double)]] = Map() + + private def marginalizeToTarget(factor: Factor[(Double, Double)], target: Element[_]): Unit = { + val targetFactor = factor.marginalizeTo(BoundsSumProductSemiring, Variable(target)) + targetFactors += target -> targetFactor + } + + private def marginalize(resultFactor: Factor[(Double, Double)]) = { + targetElements foreach (marginalizeToTarget(resultFactor, _)) + } + + /** + * Returns the lower and upper bounds of the probability of the target. + */ + def probabilityBounds[T](target: Element[_], value: T): (Double, Double) = { + require(targetElements contains target) + val index = Variable(target).range.indexOf(Regular(value)) + if (index == -1) (0, 1) + else targetFactors(target).get(List(index)) + } + + /** + * Postprocess the factors produced by eliminating variables, assuming the entire model has been expanded so + * the lower and upper bounds are the same. + */ + def finishNoBounds(factorsAfterElimination: Set[Factor[Double]]): Factor[(Double, Double)] = { + // It is possible that there are no factors (this will happen if there is no evidence). + // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. + val multiplied = factorsAfterElimination.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + val normalized = normalizeAndAbsorbNoBounds(multiplied) + normalized + } + + /** + * Postprocess the factors produced by eliminating variables, when the lower and upper bounds may be different. + * + * @param lowerFactors the factors produced with the upperBounds flag = false + * @param upperFactors the factors produced with the upperBounds flag = true + */ + def finishWithBounds(lowerFactors: Set[Factor[Double]], upperFactors: Set[Factor[Double]]): Factor[(Double, Double)] = { + // It is possible that there are no factors (this will happen if there is no evidence). + // Therefore, we start with the unit factor and use foldLeft, instead of simply reducing the factorsAfterElimination. + val lowerMultiplied = lowerFactors.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + val upperMultiplied = upperFactors.foldLeft(Factory.unit(semiring))(_.product(_, semiring)) + val normalized = normalizeAndAbsorbWithBounds(lowerMultiplied, upperMultiplied) + normalized + } + + /* This code is copied straight from VariableElimination. It should be refactored to avoid duplication. */ + // The first element of FactorMap is the complete set of factors. + // The second element maps variables to the factors mentioning that variable. + private type FactorMap[T] = Map[Variable[_], Set[Factor[T]]] + + private def addFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) + factor)) + + private def removeFactor[T](factor: Factor[T], map: FactorMap[T]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, Set()) - factor)) + + private def initialFactorMap(factors: Traversable[Factor[Double]]): FactorMap[Double] = { + val map: FactorMap[Double] = Map() + factors foreach (addFactor(_, map)) + map + } + + protected var recordingFactors: List[Factor[_]] = List() + + /** + * Some variable elimination algorithms, such as computing the most probable explanation, record values of + * variables as they are eliminated. Such values are stored in a factor that maps values of the other variables + * to a value of the eliminated variable. This factor is produced by finding the value of the variable that + * "maximizes" the entry associated with the value in the product factor resulting from eliminating this + * variable, for some maximization function. The recordingFunction determines which of two entries is greater + * according to the maximization function. It returns true iff the second entry is greater. The recording + * function is an option so that variable elimination algorithms that do not use it can ignore it. + */ + val comparator: Option[(Double, Double) => Boolean] = None + + private def eliminate( + variable: Variable[_], + factors: Set[Factor[Double]], + map: FactorMap[Double]): Unit = { + val varFactors: Set[Factor[Double]] = map(variable) + if (debug) { + println("*****************\nEliminating " + variable.id) + println("Input factors:") + for { factor <- varFactors } { println(factor.toReadableString) } + } + if (varFactors.nonEmpty) { + def multiply(f1: Factor[Double], f2: Factor[Double]): Factor[Double] = f1.product(f2, semiring) + val productFactor: Factor[Double] = varFactors.reduceLeft(_.product(_, semiring)) + val resultFactor: Factor[Double] = productFactor.sumOver(variable, semiring) + varFactors foreach (removeFactor(_, map)) + addFactor(resultFactor, map) + comparator match { + case None => () + case Some(recorder) => recordingFactors ::= productFactor.recordArgMax(variable, recorder) + } + map -= variable + factors --= varFactors + if (debug) println("Result factor\n" + resultFactor.toReadableString) + factors += resultFactor + } + } + + private def eliminateInOrder( + order: List[Variable[_]], + factors: Set[Factor[Double]], + map: FactorMap[Double]): Set[Factor[Double]] = + order match { + case Nil => + factors + case first :: rest => + eliminate(first, factors, map) + eliminateInOrder(rest, factors, map) + } + + // for debugging + private def printVariables(variables: Traversable[/*Extended*/Variable[_]]) { + for { variable <- variables } { + print(" ") + variable match { + case ev: ElementVariable[_] => + println(ev.element.toNameString) + case _ => println(variable) + } + } + } + /** + * Method for choosing the elimination order. + * The default order chooses first the variable that + * minimizes the number of extra factor entries that would be created when it is eliminated. + * Override this method if you want a different rule. + */ + def eliminationOrder(allVars: Set[Variable[_]], factors: Traversable[Factor[Double]], toPreserve: Traversable[Variable[_]]): List[Variable[_]] = { + val eliminableVars = allVars -- toPreserve + var initialGraph = new VEGraph(factors) + val candidates = new HeapPriorityMap[Variable[_], Double] + eliminableVars foreach (v => candidates += v.asInstanceOf[Variable[_]] -> initialGraph.score(v)) + eliminationOrderHelper(candidates, toPreserve, initialGraph, List()) + } + + @tailrec private def eliminationOrderHelper(candidates: PriorityMap[Variable[_], Double], + toPreserve: Traversable[Variable[_]], + graph: VEGraph, + accum: List[Variable[_]]): List[Variable[_]] = { + if (candidates.isEmpty) accum.reverse + else { + val best = candidates.extractMin()._1 + // do not read the best variable after it has been removed, and do not add the preserved variables + val touched = graph.info(best).neighbors - best -- toPreserve + val nextGraph = graph.eliminate(best) + touched foreach (v => candidates += v.asInstanceOf[Variable[_]] -> graph.score(v)) + eliminationOrderHelper(candidates, toPreserve, nextGraph, best :: accum) + } + } + +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/ValueSet.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/ValueSet.scala index 95faa419..9c72cdaa 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/ValueSet.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/lazyfactored/ValueSet.scala @@ -28,7 +28,7 @@ class ValueSet[T](val xvalues: Set[Extended[T]]) { /** * Returns the union of this value set with that value set. - * If both have a Star, only one Star is kept in the result + * If both have a Star, only one Star is kept in the result. */ def ++(that: ValueSet[T]): ValueSet[T] = { if (hasStar && that.hasStar) new ValueSet(xvalues ++ that.xvalues.filter(_.isRegular)) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/EMTerminationCriteria.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/EMTerminationCriteria.scala index c4952187..e8e9c8b9 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/EMTerminationCriteria.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/EMTerminationCriteria.scala @@ -1,36 +1,51 @@ +/* + * EMTerminationCriteria.scala + * Class for defining termination of EM algorithms. + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Nov 7, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.algorithm.learning import com.cra.figaro.language.Parameter -abstract class EMTerminationCriteria(val alg: ExpectationMaximization) { +/** + * Termination criteria for EM algorithms. A termination criteria can be passed as an argument to the EM apply method. + */ +abstract class EMTerminationCriteria { type SufficientStatistics = Map[Parameter[_], Seq[Double]] def apply(s: SufficientStatistics): Boolean } -class LikelihoodTermination(val tolerance: Double, alg: ExpectationMaximization) extends EMTerminationCriteria(alg) { - var previousLikelihood = Double.NegativeInfinity - override def apply(s: SufficientStatistics): Boolean = { - false - } -} - -class MaxIterations(val iterations: Int, alg: ExpectationMaximization) extends EMTerminationCriteria(alg) { +/** + * Terminate when the maximum number of iterations has been reached + */ +class MaxIterations(val max: Int) extends EMTerminationCriteria { var currentIterations = 0 override def apply(s: SufficientStatistics): Boolean = { currentIterations += 1 - if (currentIterations < iterations) false else true + if (currentIterations < max) false else true } } -class SufficientStatisticsMagnitudes(val tolerance: Double, alg: ExpectationMaximization) extends EMTerminationCriteria(alg) { +/** + * Terminate when the magnitude of sufficient statistics does not exhibit a change greater than the specified tolerance. + */ +class SufficientStatisticsMagnitudes(val tolerance: Double) extends EMTerminationCriteria { var previousSufficientStatistics = Map.empty[Parameter[_], Seq[Double]] - + def difference(x: Seq[Double], y: Seq[Double]): Double = { - require(x.size == y.size) - val sum = (for ((a, b) <- x zip y) yield Math.abs(a - b).toDouble) - sum.sum/(x.size.toDouble) + require(x.size == y.size) + val sum = (for ((a, b) <- x zip y) yield Math.abs(a - b).toDouble) + sum.sum / (x.size.toDouble) } - + override def apply(s: SufficientStatistics): Boolean = { if (previousSufficientStatistics.isEmpty) { previousSufficientStatistics = s @@ -38,9 +53,9 @@ class SufficientStatisticsMagnitudes(val tolerance: Double, alg: ExpectationMax } val delta = for (k <- s.keys) yield { - difference(s(k),previousSufficientStatistics(k)) + difference(s(k), previousSufficientStatistics(k)) } - val totalDelta = delta.sum/(delta.size.toDouble) + val totalDelta = delta.sum / (delta.size.toDouble) previousSufficientStatistics = s if (totalDelta < tolerance) { return true @@ -49,8 +64,13 @@ class SufficientStatisticsMagnitudes(val tolerance: Double, alg: ExpectationMax } } -class BICTermination(val tolerance: Double, alg: ExpectationMaximization) extends EMTerminationCriteria(alg) { - override def apply(s: SufficientStatistics): Boolean = { - false - } +object EMTerminationCriteria { + /** + * Terminate when the maximum number of iterations has been reached + */ + def maxIterations(max: Int): () => EMTerminationCriteria = () => new MaxIterations(max) + /** + * Terminate when the magnitude of sufficient statistics does not exhibit a change greater than the specified tolerance. + */ + def sufficientStatisticsMagnitude(tolerance: Double): () => EMTerminationCriteria = () => new SufficientStatisticsMagnitudes(tolerance) } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala deleted file mode 100644 index e69de29b..00000000 diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala index 2355edce..80d7b6e0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala @@ -1,267 +1,342 @@ -/* - * GeneralizedEM.scala - * Expectation maximization algorithm using any ProbQueryAlgorithm as the inference algorithm. - * - * Created By: Michael Howard (mhoward@cra.com) - * Creation Date: Jun 1, 2013 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.algorithm.learning - -import com.cra.figaro.language._ -import com.cra.figaro.algorithm.{ Algorithm, ParameterLearner, ProbQueryAlgorithm, OneTime } -import com.cra.figaro.algorithm.factored.beliefpropagation.BeliefPropagation -import com.cra.figaro.algorithm.sampling.{ Importance, MetropolisHastings, ProposalScheme } -import com.cra.figaro.algorithm.factored.Factory -import com.cra.figaro.patterns.learning.ModelParameters -import com.cra.figaro.algorithm.factored.SufficientStatisticsVariableElimination - -/* - * @param inferenceAlgorithmConstructor - */ - -abstract class ExpectationMaximization(universe: Universe, targetParameters: Parameter[_]*) extends Algorithm with ParameterLearner { - - protected def doStart(): Unit = { - em() - } - - protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] - - var sufficientStatistics: Map[Parameter[_], Seq[Double]] = Map.empty[Parameter[_],Seq[Double]] - val debug = false - protected def em(): Unit = { - if (debug) println("Entering EM loop") - while (shouldTerminate(sufficientStatistics) == false) { - iteration() - } - } - - protected def doMaximizationStep(parameterMapping: Map[Parameter[_], Seq[Double]]): Unit = { - - for (p <- targetParameters) yield { - p.maximize(parameterMapping(p)) - } - } - - def iteration() : Unit = { - sufficientStatistics = doExpectationStep() - doMaximizationStep(sufficientStatistics) - if (debug) println("Completed iteration") - } - - private[learning] val shouldTerminate: EMTerminationCriteria - - /* - * Stop the algorithm from computing. The algorithm is still ready to provide answers after it returns. - */ - protected def doStop(): Unit = {} - - /* - * Resume the computation of the algorithm, if it has been stopped. - */ - - protected def doResume(): Unit = {} - - /* - * Kill the algorithm so that it is inactive. It will no longer be able to provide answers. - */ - - protected def doKill(): Unit = {} - -} - -/** - * Expectation maximization iteratively produces an estimate of sufficient statistics for learnable parameters, - * then maximizes the parameters according to the estimate. It uses an factored inference algorithm, SufficientStatisticsVariableElimination, - * to produce the estimate of the sufficient statistics. This class can be extended with a different expectation - * or maximization algorithm; see the code for details. - */ -class ExpectationMaximizationWithFactors(universe: Universe, targetParameters: Parameter[_]*)(val numberOfIterations: Int) extends ExpectationMaximization(universe,targetParameters:_*) { - val shouldTerminate = new MaxIterations(numberOfIterations,this) - //Defines the length of the sequences corresponding to different parameters - protected val paramMap : Map[Parameter[_],Seq[Double]]= Map(targetParameters.map(p => p -> p.zeroSufficientStatistics):_*) - - protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { - val algorithm = SufficientStatisticsVariableElimination(paramMap)(universe) - algorithm.start - val result = algorithm.getSufficientStatisticsForAllParameters - algorithm.kill - result - } - -} - -class GeneralizedEM(inferenceAlgorithmConstructor: Seq[Element[_]] => Universe => ProbQueryAlgorithm with OneTime, universe: Universe, targetParameters: Parameter[_]*)(val numberOfIterations: Int) extends ExpectationMaximization(universe,targetParameters:_*){ - /* - * Start the algorithm. After it returns, the algorithm must be ready to provide answers. - */ - val shouldTerminate = new MaxIterations(numberOfIterations,this) - - protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { - val inferenceTargets = - universe.activeElements.filter(_.isInstanceOf[Parameterized[_]]).map(_.asInstanceOf[Parameterized[_]]) - - val algorithm = inferenceAlgorithmConstructor(inferenceTargets)(universe) - algorithm.start() - - var result: Map[Parameter[_], Seq[Double]] = Map() - - - - for { parameter <- targetParameters } { - var stats = parameter.zeroSufficientStatistics - for { - target <- universe.directlyUsedBy(parameter) - } { - val t: Parameterized[target.Value] = target.asInstanceOf[Parameterized[target.Value]] - val distribution: Stream[(Double, target.Value)] = algorithm.distribution(t) - val newStats = t.distributionToStatistics(parameter, distribution) - stats = (stats.zip(newStats)).map(pair => pair._1 + pair._2) - } - result += parameter -> stats - } - algorithm.kill() - result - } - -} - -object EM { - - //If we are keeping sufficient statistics factors, why doesn't this implementation use them? - def withBP(p: ModelParameters)(implicit universe: Universe) = EMWithBP(p)(universe) - def withBP(emIterations: Int, bpIterations: Int, p: ModelParameters)(implicit universe: Universe) = EMWithBP(emIterations, bpIterations, p: ModelParameters)(universe) - def withBP(p: Parameter[_]*)(implicit universe: Universe) = EMWithBP(p: _*)(universe) - def witHBP(emIterations: Int, bpIterations: Int, p: Parameter[_]*)(implicit universe: Universe) = EMWithBP(emIterations, bpIterations, p: _*)(universe) - - def withVE(p: ModelParameters)(implicit universe: Universe) = EMWithVE(p)(universe) - def withVE(emIterations: Int, p: ModelParameters)(implicit universe: Universe) = EMWithVE(emIterations, p: ModelParameters)(universe) - def withVE(p: Parameter[_]*)(implicit universe: Universe) = EMWithVE(p: _*)(universe) - def withVE(emIterations: Int, p: Parameter[_]*)(implicit universe: Universe) = EMWithVE(emIterations, p: _*)(universe) - - def withImportance(emIterations: Int, importanceParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = EMWithImportance(emIterations, importanceParticles, p: _*)(universe) - def withImportance(p: ModelParameters)(implicit universe: Universe) = EMWithImportance(p)(universe) - def withImportance(emIterations: Int, importanceParticles: Int, p: ModelParameters)(implicit universe: Universe) = EMWithImportance(emIterations, importanceParticles, p)(universe) - - def withMH(emIterations: Int, mhParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = EMWithMH(emIterations, mhParticles, p: _*)(universe) - def withMH(p: ModelParameters)(implicit universe: Universe) = EMWithMH(p)(universe) - def withMH(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: ModelParameters)(implicit universe: Universe) = EMWithMH(emIterations, mhParticles, proposalScheme, p)(universe) - def withMH(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: Parameter[_]*)(implicit universe: Universe) = EMWithMH(emIterations, mhParticles, proposalScheme, p: _*)(universe) - - -} - -object EMWithBP { - private def makeBP(numIterations: Int, targets: Seq[Element[_]])(universe: Universe) = { - Factory.removeFactors() - BeliefPropagation(numIterations, targets: _*)(universe) - } - - def apply(p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(10, targets)(universe), universe, parameters: _*)(10) - } - - def apply(emIterations: Int, bpIterations: Int, p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, parameters: _*)(emIterations) - } - - /** - * An expectation maximization algorithm which will run for the default of 10 iterations - */ - def apply(p: Parameter[_]*)(implicit universe: Universe) = - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(10, targets)(universe), universe, p: _*)(10) - - /** - * An expectation maximization algorithm which will run for the number of iterations specified - */ - def apply(emIterations: Int, bpIterations: Int, p: Parameter[_]*)(implicit universe: Universe) = - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, p: _*)(emIterations) -} - -object EMWithImportance { - private def makeImportance(numParticles: Int, targets: Seq[Element[_]])(universe: Universe) = { - Importance(numParticles, targets: _*)(universe) - } - - /** - * An expectation maximization algorithm using importance sampling for inference - * - * @param emIterations number of iterations of the EM algorithm - * @param importanceParticles number of particles of the importance sampling algorithm - */ - def apply(emIterations: Int, importanceParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(importanceParticles, targets)(universe), universe, p: _*)(emIterations) - - def apply(p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(100000, targets)(universe), universe, parameters: _*)(10) - } - - def apply(emIterations: Int, importanceParticles: Int, p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(100000, targets)(universe), universe, parameters: _*)(emIterations) - } - -} - -object EMWithMH { - private def makeMH(numParticles: Int, proposalScheme: ProposalScheme, targets: Seq[Element[_]])(universe: Universe) = { - MetropolisHastings(numParticles, proposalScheme, targets: _*)(universe) - } - - /** - * An expectation maximization algorithm using Metropolis Hastings for inference. - * - * @param emIterations number of iterations of the EM algorithm - * @param mhParticles number of particles of the MH algorithm - */ - def apply(emIterations: Int, mhParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, ProposalScheme.default(universe), targets)(universe), universe, p: _*)(emIterations) - - /** - * An expectation maximization algorithm using Metropolis Hastings for inference. - * - * @param emIterations number of iterations of the EM algorithm - * @param mhParticles number of particles of the MH algorithm - */ - def apply(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: Parameter[_]*)(implicit universe: Universe) = - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, p: _*)(emIterations) - - def apply(p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(100000, ProposalScheme.default(universe), targets)(universe), universe, parameters: _*)(10) - } - - def apply(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: ModelParameters)(implicit universe: Universe) = { - val parameters = p.convertToParameterList - new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, parameters: _*)(emIterations) - } - -} - -object EMWithVE { - /** - * An expectation maximization algorithm which will run for the default of 10 iterations - */ - def apply(p: Parameter[_]*)(implicit universe: Universe) = - new ExpectationMaximizationWithFactors(universe, p: _*)(10) - - def apply(p: ModelParameters)(implicit universe: Universe) = - new ExpectationMaximizationWithFactors(universe, p.convertToParameterList:_*)(10) - - def apply(iterations: Int, p: ModelParameters)(implicit universe: Universe) = - new ExpectationMaximizationWithFactors(universe, p.convertToParameterList:_*)(iterations) - /** - * An expectation maximization algorithm which will run for the number of iterations specified - */ - def apply(iterations: Int, p: Parameter[_]*)(implicit universe: Universe) = - new ExpectationMaximizationWithFactors(universe, p: _*)(iterations) - } +/* + * GeneralizedEM.scala + * Expectation maximization algorithm using any ProbQueryAlgorithm as the inference algorithm. + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 1, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.learning + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.{ Algorithm, ParameterLearner, ProbQueryAlgorithm, OneTime } +import com.cra.figaro.algorithm.factored.beliefpropagation.BeliefPropagation +import com.cra.figaro.algorithm.sampling.{ Importance, MetropolisHastings, ProposalScheme } +import com.cra.figaro.algorithm.factored.factors.Factory +import com.cra.figaro.patterns.learning.ModelParameters +import com.cra.figaro.algorithm.factored.SufficientStatisticsVariableElimination + + +/** + * Base class of Expectation Maximization algorithms. This class also implements the outer EM loop and checks against termination criteria. + */ +abstract class ExpectationMaximization(universe: Universe, val terminationCriteria: () => EMTerminationCriteria, targetParameters: Parameter[_]*) extends Algorithm with ParameterLearner { + + protected def doStart(): Unit = { + em() + } + + protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] + + var sufficientStatistics: Map[Parameter[_], Seq[Double]] = Map.empty[Parameter[_], Seq[Double]] + val debug = false + protected def em(): Unit = { + //Instantiate termination criteria here. + val shouldTerminate = terminationCriteria() + if (debug) println("Entering EM loop") + while (shouldTerminate(sufficientStatistics) == false) { + iteration() + } + } + + protected def doMaximizationStep(parameterMapping: Map[Parameter[_], Seq[Double]]): Unit = { + + for (p <- targetParameters) yield { + p.maximize(parameterMapping(p)) + } + } + + def iteration(): Unit = { + sufficientStatistics = doExpectationStep() + doMaximizationStep(sufficientStatistics) + if (debug) println("Completed iteration") + } + + /* + * Stop the algorithm from computing. The algorithm is still ready to provide answers after it returns. + */ + protected def doStop(): Unit = {} + + /* + * Resume the computation of the algorithm, if it has been stopped. + */ + + protected def doResume(): Unit = {} + + /* + * Kill the algorithm so that it is inactive. It will no longer be able to provide answers. + */ + + protected def doKill(): Unit = {} + +} + +/** + * Expectation maximization iteratively produces an estimate of sufficient statistics for learnable parameters, + * then maximizes the parameters according to the estimate. It uses an factored inference algorithm, SufficientStatisticsVariableElimination, + * to produce the estimate of the sufficient statistics. This class can be extended with a different expectation + * or maximization algorithm; see the code for details. + */ +class ExpectationMaximizationWithFactors(universe: Universe, targetParameters: Parameter[_]*)(terminationCriteria: () => EMTerminationCriteria) extends ExpectationMaximization(universe, terminationCriteria, targetParameters: _*) { + + //Defines the length of the sequences corresponding to different parameters + protected val paramMap: Map[Parameter[_], Seq[Double]] = Map(targetParameters.map(p => p -> p.zeroSufficientStatistics): _*) + + protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { + val algorithm = SufficientStatisticsVariableElimination(paramMap)(universe) + algorithm.start + val result = algorithm.getSufficientStatisticsForAllParameters + algorithm.kill + result + } + +} + +class GeneralizedEM(inferenceAlgorithmConstructor: Seq[Element[_]] => Universe => ProbQueryAlgorithm with OneTime, universe: Universe, targetParameters: Parameter[_]*)(terminationCriteria: () => EMTerminationCriteria) extends ExpectationMaximization(universe, terminationCriteria, targetParameters: _*) { + /* + * Start the algorithm. After it returns, the algorithm must be ready to provide answers. + */ + + protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { + val inferenceTargets = + universe.activeElements.filter(_.isInstanceOf[Parameterized[_]]).map(_.asInstanceOf[Parameterized[_]]) + + val algorithm = inferenceAlgorithmConstructor(inferenceTargets)(universe) + algorithm.start() + + var result: Map[Parameter[_], Seq[Double]] = Map() + + for { parameter <- targetParameters } { + var stats = parameter.zeroSufficientStatistics + for { + target <- universe.directlyUsedBy(parameter) + } { + val t: Parameterized[target.Value] = target.asInstanceOf[Parameterized[target.Value]] + val distribution: Stream[(Double, target.Value)] = algorithm.distribution(t) + val newStats = t.distributionToStatistics(parameter, distribution) + stats = (stats.zip(newStats)).map(pair => pair._1 + pair._2) + } + result += parameter -> stats + } + algorithm.kill() + result + } + +} + +object EMWithBP { + private def makeBP(numIterations: Int, targets: Seq[Element[_]])(universe: Universe) = { + Factory.removeFactors() + BeliefPropagation(numIterations, targets: _*)(universe) + } + /** + * An expectation maximization algorithm using Belief Propagation sampling for inference. + * + * @param params parameters to target with EM algorithm + */ + def apply(params: ModelParameters)(implicit universe: Universe) = { + val parameters = params.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(10, targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(10)) + } + /** + * An expectation maximization algorithm using Belief Propagation sampling for inference. + * @param emIterations number of iterations of the EM algorithm + * @param bpIterations number of iterations of the BP algorithm + * @param params parameters to target with EM algorithm + */ + def apply(emIterations: Int, bpIterations: Int, p: ModelParameters)(implicit universe: Universe) = { + val parameters = p.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(emIterations)) + } + + /** + * An expectation maximization algorithm using Belief Propagation sampling for inference. + * @param params parameters to target with EM algorithm + */ + def apply(params: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(10, targets)(universe), universe, params: _*)(EMTerminationCriteria.maxIterations(10)) + + /** + * An expectation maximization algorithm using importance sampling for inference. + * @param emIterations number of iterations of the EM algorithm + * @param bpIterations number of iterations of the BP algorithm + * @param params parameters to target with EM algorithm + */ + def apply(emIterations: Int, bpIterations: Int, params: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, params: _*)(EMTerminationCriteria.maxIterations(emIterations)) + + /** + * An expectation maximization algorithm using importance sampling for inference. + * @param terminationCriteria criteria for stopping the EM algorithm + * @param bpIterations number of iterations of the BP algorithm + * @param params parameters to target with EM algorithm + */ + def apply(terminationCriteria: () => EMTerminationCriteria, bpIterations: Int, params: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, params: _*)(terminationCriteria) +} + +object EMWithImportance { + private def makeImportance(numParticles: Int, targets: Seq[Element[_]])(universe: Universe) = { + Importance(numParticles, targets: _*)(universe) + } + + /** + * An expectation maximization algorithm using importance sampling for inference. + * + * @param emIterations number of iterations of the EM algorithm + * @param importanceParticles number of particles of the importance sampling algorithm + */ + def apply(emIterations: Int, importanceParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(importanceParticles, targets)(universe), universe, p: _*)(EMTerminationCriteria.maxIterations(emIterations)) + + /** + * An expectation maximization algorithm using importance sampling for inference. + * + * @param terminationCriteria criteria for stopping the EM algorithm + * @param importanceParticles number of particles of the importance sampling algorithm + */ + def apply(terminationCriteria: () => EMTerminationCriteria, importanceParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(importanceParticles, targets)(universe), universe, p: _*)(terminationCriteria) + + /** + * An expectation maximization algorithm using importance sampling for inference. + * + * @param params parameters to target with EM algorithm + */ + def apply(params: ModelParameters)(implicit universe: Universe) = { + val parameters = params.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(100000, targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(10)) + } + + /** + * An expectation maximization algorithm using importance sampling for inference. + * + * @param emIterations number of iterations of the EM algorithm + * @param importanceParticles number of particles of the importance sampling algorithm + * @param params parameters to target with EM algorithm + */ + def apply(emIterations: Int, importanceParticles: Int, params: ModelParameters)(implicit universe: Universe) = { + val parameters = params.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(100000, targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(emIterations)) + } + + /** + * An expectation maximization algorithm using importance sampling for inference. + * + * @param terminationCriteria criteria for stopping the EM algorithm + * @param importanceParticles number of particles of the importance sampling algorithm + * @param params parameters to target with EM algorithm + */ + def apply(terminationCriteria: () => EMTerminationCriteria, importanceParticles: Int, params: ModelParameters)(implicit universe: Universe) = { + val parameters = params.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(100000, targets)(universe), universe, parameters: _*)(terminationCriteria) + } + +} + +object EMWithMH { + private def makeMH(numParticles: Int, proposalScheme: ProposalScheme, targets: Seq[Element[_]])(universe: Universe) = { + MetropolisHastings(numParticles, proposalScheme, targets: _*)(universe) + } + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param emIterations number of iterations of the EM algorithm + * @param mhParticles number of particles of the MH algorithm + */ + def apply(emIterations: Int, mhParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, ProposalScheme.default(universe), targets)(universe), universe, p: _*)(EMTerminationCriteria.maxIterations(emIterations)) + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * @param terminationCriteria criteria for stopping the EM algorithm + * @param mhParticles number of particles of the MH algorithm + * @param params parameters to target in EM algorithm + */ + def apply(terminationCriteria: () => EMTerminationCriteria, mhParticles: Int, params: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, ProposalScheme.default(universe), targets)(universe), universe, params: _*)(terminationCriteria) + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param iterations number of iterations of the EM algorithm + * @param mhParticles number of particles of the MH algorithm + * @param proposalScheme proposal scheme for MH algorithm + * @param params parameters to target in EM algorithm + */ + def apply(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, params: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, params: _*)(EMTerminationCriteria.maxIterations(emIterations)) + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * @param params parameters to target in EM algorithm + */ + def apply(p: ModelParameters)(implicit universe: Universe) = { + val parameters = p.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(100000, ProposalScheme.default(universe), targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(10)) + } + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param iterations number of iterations of the EM algorithm + * @param mhParticles number of particles of the MH algorithm + * @param proposalScheme proposal scheme for MH algorithm + * @param params parameters to target in EM algorithm + */ + def apply(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: ModelParameters)(implicit universe: Universe) = { + val parameters = p.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, parameters: _*)(EMTerminationCriteria.maxIterations(emIterations)) + } + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param terminationCriteria criteria for stopping the EM algorithm + * @param mhParticles number of particles of the MH algorithm + * @param proposalScheme proposal scheme for MH algorithm + * @param params parameters to target in EM algorithm + */ + def apply(terminationCriteria: () => EMTerminationCriteria, mhParticles: Int, proposalScheme: ProposalScheme, params: ModelParameters)(implicit universe: Universe) = { + val parameters = params.convertToParameterList + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, parameters: _*)(terminationCriteria) + } + +} + +object EMWithVE { + /** + * An expectation maximization algorithm which will run for the default of 10 iterations. + */ + def apply(p: Parameter[_]*)(implicit universe: Universe) = + new ExpectationMaximizationWithFactors(universe, p: _*)(EMTerminationCriteria.maxIterations(10)) + /** + * An expectation maximization algorithm which will run for the default of 10 iterations. + */ + def apply(p: ModelParameters)(implicit universe: Universe) = + new ExpectationMaximizationWithFactors(universe, p.convertToParameterList: _*)(EMTerminationCriteria.maxIterations(10)) + + /** + * An expectation maximization algorithm which will run for the number of iterations specified. + */ + def apply(iterations: Int, p: ModelParameters)(implicit universe: Universe) = + new ExpectationMaximizationWithFactors(universe, p.convertToParameterList: _*)(EMTerminationCriteria.maxIterations(iterations)) + /** + * An expectation maximization algorithm which will run for the number of iterations specified. + */ + def apply(iterations: Int, p: Parameter[_]*)(implicit universe: Universe) = + new ExpectationMaximizationWithFactors(universe, p: _*)(EMTerminationCriteria.maxIterations(iterations)) + + /** + * An expectation maximization algorithm which will stop according to a user specified termination criteria. + */ + def apply(terminationCriteria: () => EMTerminationCriteria, p: Parameter[_]*)(implicit universe: Universe) = + new ExpectationMaximizationWithFactors(universe, p: _*)(terminationCriteria) + +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala index abea68e1..561e0420 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala @@ -15,7 +15,9 @@ package com.cra.figaro.algorithm.learning import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.sampling._ -import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.library.atomic.discrete._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.Factory import com.cra.figaro.algorithm.lazyfactored._ import com.cra.figaro.library.decision._ import com.cra.figaro.language._ @@ -29,15 +31,14 @@ import JSci.maths.ExtraMath.binomial /** * Methods for creating probabilistic factors associated with elements and their sufficient statistics. - * + * * @param parameterMap Map of parameters to their sufficient statistics. Expectation */ -class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[Double]]) -{ +class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[Double]]) { private def makeFactors[T](const: Constant[T]): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(Variable(const))) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(Variable(const))) val mapping = mutable.Map(parameterMap.toSeq: _*) factor.set(List(0), (1.0, mapping)) List(factor) @@ -45,7 +46,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeFactors(flip: AtomicFlip): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { val flipVar = Variable(flip) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(flipVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(flipVar)) val i = flipVar.range.indexOf(Regular(true)) val trueMapping = mutable.Map(parameterMap.toSeq: _*) val falseMapping = mutable.Map(parameterMap.toSeq: _*) @@ -57,7 +58,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeFactors(flip: CompoundFlip): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { val flipVar = Variable(flip) val probVar = Variable(flip.prob) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(probVar, flipVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(probVar), List(flipVar)) val parentVals = probVar.range val i = flipVar.range.indexOf(Regular(true)) @@ -71,7 +72,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeFactors(flip: ParameterizedFlip): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { val flipVar = Variable(flip) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(flipVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(flipVar)) val prob = flip.parameter.MAPValue val i = flipVar.range.indexOf(Regular(true)) @@ -90,13 +91,13 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeFactors(bin: ParameterizedBinomialFixedNumTrials): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { val binVar = Variable(bin) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(binVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(binVar)) val prob = bin.parameter.MAPValue.asInstanceOf[Double] val mappings = binVar.range.map(i => (i, mutable.Map(parameterMap.toSeq: _*))) - for { + for { (ext, map) <- mappings if (ext.isRegular) - } { + } { val i = ext.value map.remove(bin.parameter) map.put(bin.parameter, Seq(i, bin.numTrials - i)) @@ -109,7 +110,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeSimpleDistribution[T](target: Variable[T], probs: List[Double]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(target)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(target)) for { (prob, index) <- probs.zipWithIndex } { val rowMapping = mutable.Map(parameterMap.toSeq: _*) factor.set(List(index), (prob, rowMapping)) @@ -118,7 +119,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D } private def makeSimpleDistributionForParameterized[T](target: Variable[T], probs: List[Double], select: ParameterizedSelect[T]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(target)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(target)) //For each outcome val unzippedClauses = select.clauses.unzip @@ -141,15 +142,15 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeComplexDistribution[T](target: Variable[T], probElems: List[Element[Double]]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { val probVars = probElems map (Variable(_)) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List((target :: probVars): _*)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](probVars, List(target)) val probVals = probVars map (_.range) for { indices <- factor.allIndices } { - val probIndices = indices.toList.tail.zipWithIndex + val probIndices = indices.take(probElems.size).zipWithIndex val unnormalized = probIndices map (pair => probVals(pair._2)(pair._1).value) val normalized = normalize(unnormalized).toArray val rowMapping = mutable.Map(parameterMap.toSeq: _*) - factor.set(indices, (normalized(indices(0)), rowMapping)) + factor.set(indices, (normalized(indices.last), rowMapping)) } factor } @@ -214,7 +215,6 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D } } - /** * Make a conditional selector factor used in the decomposition of chain and other elements. * A chain defines a factor over the parent element, each of the possible result elements of the chain, @@ -228,10 +228,14 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val outcomeVar = Variable(resultElem) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(selector, overallVar, outcomeVar)) - for { i <- 0 until outcomeIndex } { makeDontCares(factor, i, overallVar, outcomeVar) } - makeCares(factor, outcomeIndex, overallVar, outcomeVar, Values(overallElem.universe)(overallElem))(mapper) - for { i <- outcomeIndex + 1 until selector.size } { makeDontCares(factor, i, overallVar, outcomeVar) } + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(selector, outcomeVar), List(overallVar)) + for { i <- 0 to selector.size } { + if (outcomeIndex == i) { + makeCares(factor, outcomeIndex, outcomeVar, overallVar, Values(overallElem.universe)(overallElem))(mapper) + } else { + makeDontCares(factor, i, outcomeVar, overallVar) + } + } factor } @@ -268,7 +272,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val arg1Var = Variable(apply.arg1) val resultVar = Variable(apply) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, resultVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var), List(resultVar)) val arg1Indices = arg1Var.range.zipWithIndex val resultIndices = resultVar.range.zipWithIndex for { @@ -288,7 +292,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val arg1Var = Variable(apply.arg1) val arg2Var = Variable(apply.arg2) val resultVar = Variable(apply) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, resultVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var), List(resultVar)) val arg1Indices = arg1Var.range.zipWithIndex val arg2Indices = arg2Var.range.zipWithIndex val resultIndices = resultVar.range.zipWithIndex @@ -311,7 +315,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val arg2Var = Variable(apply.arg2) val arg3Var = Variable(apply.arg3) val resultVar = Variable(apply) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var, resultVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var), List(resultVar)) val arg1Indices = arg1Var.range.zipWithIndex val arg2Indices = arg2Var.range.zipWithIndex val arg3Indices = arg3Var.range.zipWithIndex @@ -336,7 +340,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val arg3Var = Variable(apply.arg3) val arg4Var = Variable(apply.arg4) val resultVar = Variable(apply) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var, arg4Var, resultVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var, arg4Var), List(resultVar)) val arg1Indices = arg1Var.range.zipWithIndex val arg2Indices = arg2Var.range.zipWithIndex val arg3Indices = arg3Var.range.zipWithIndex @@ -364,7 +368,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val arg4Var = Variable(apply.arg4) val arg5Var = Variable(apply.arg5) val resultVar = Variable(apply) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var, arg4Var, arg5Var, resultVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(arg1Var, arg2Var, arg3Var, arg4Var, arg5Var), List(resultVar)) val arg1Indices = arg1Var.range.zipWithIndex val arg2Indices = arg2Var.range.zipWithIndex val arg3Indices = arg3Var.range.zipWithIndex @@ -396,7 +400,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D if (resultValue.asInstanceOf[List[T]].toList == inputValues) (1.0, rowMapping) else (0.0, rowMapping) } - + def parameterRule(values: List[T], p: ParameterizedVariable[T]) = { val resultValue :: inputValues = values @@ -405,11 +409,11 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D if (resultValue.asInstanceOf[List[T]].toList == inputValues) { for (pr <- p.element.parameters) { - rowMapping.put(pr, pr.sufficientStatistics(resultValue)) + rowMapping.put(pr, pr.sufficientStatistics(resultValue)) } } else { for (pr <- p.element.parameters) { - rowMapping.put(pr, pr.sufficientStatistics(resultValue)) + rowMapping.put(pr, pr.sufficientStatistics(resultValue)) } } if (resultValue.asInstanceOf[List[T]].toList == inputValues) (1.0, rowMapping) @@ -418,9 +422,10 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val inputVariables = inject.args map (Variable(_)) val resultVariable = Variable(inject) - val variables = resultVariable :: inputVariables - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](variables) - + //val variables = inputresultVariable :: inputVariables + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](inputVariables, List(resultVariable)) + val variables = factor.variables + val ranges: List[(List[(Any, Int)], Int)] = List() val mapping = Map.empty[Int, Variable[_]] @@ -434,7 +439,6 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D newRanges :: l._1.map(a => (a._1, a._2, l._2)) } - val cases: List[List[Any]] = cartesianProduct(newRanges: _*) for { cas <- cases } { //Unzip splits into a list of values and a list of indices @@ -459,7 +463,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D } private def convertProbFactor(probFactor: Factor[Double]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { - val result = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](probFactor.variables) + val result = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](probFactor.parents, probFactor.output) for { indices <- result.allIndices } { result.set(indices, (probFactor.get(indices), mutable.Map(parameterMap.toSeq: _*))) } @@ -473,6 +477,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D case f: CompoundFlip => makeFactors(f) case f: ParameterizedFlip => makeFactors(f) case s: ParameterizedSelect[_] => makeFactors(s) + case ab: AtomicBinomial => Factory.concreteFactors(ab).map(convertProbFactor(_)) case b: ParameterizedBinomialFixedNumTrials => makeFactors(b) case s: AtomicSelect[_] => makeFactors(s) case s: CompoundSelect[_] => makeFactors(s) @@ -485,8 +490,8 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D case a: Apply4[_, _, _, _, _] => makeFactors(a) case a: Apply5[_, _, _, _, _, _] => makeFactors(a) case i: Inject[_] => makeFactors(i) - case f: ProbFactorMaker => - Factory.concreteFactors(f).map(convertProbFactor(_)) + // case f: ProbFactorMaker => + // Factory.concreteFactors(f).map(convertProbFactor(_)) /*case p: Parameter[_] => makeFactors(p)*/ case _ => throw new UnsupportedAlgorithmException(elem) } @@ -499,7 +504,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val currentDensity = densityMap.getOrElse(v.value, 0.0) densityMap.update(v.value, currentDensity + atomic.density(v.value)) } - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(variable)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(variable)) for { (v, i) <- values.zipWithIndex } { val rowMapping = Map.empty[Parameter[_], Seq[Double]] ++ parameterMap factor.set(List(i), (densityMap(v.value), rowMapping)) @@ -541,7 +546,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeUncontingentConstraintFactor[T](elem: Element[T], constraint: T => Double): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { val elemVar = Variable(elem) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](List(elemVar)) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](List(), List(elemVar)) for { (elemVal, index) <- elemVar.range.zipWithIndex } { val rowMapping = mutable.Map(parameterMap.toSeq: _*) val entry = (math.exp(constraint(elemVal.value)), rowMapping) @@ -563,7 +568,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D val firstValues = firstVar.range val numFirstValues = firstValues.size val matchingIndex: Int = firstValues.indexOf(Regular(firstValue)) - val resultFactor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](firstVar :: restFactor.variables) + val resultFactor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](firstVar :: restFactor.parents, restFactor.output) for { restIndices <- restFactor.allIndices } { val restEntry = restFactor.get(restIndices)._1 for { firstIndex <- 0 until numFirstValues } { @@ -598,7 +603,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D */ def removeFactors(elem: Element[_]) { factorCache -= elem } /** - * Removes the factors for all elements from the cache. + * Removes the factors for all elements from the cache. */ def removeFactors() { factorCache.clear } @@ -619,7 +624,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D (result, rowMapping) } val variables = uses map (Variable(_)) - val factor = Factory.make[(Double, Map[Parameter[_], Seq[Double]])](variables) + val factor = Factory.defaultFactor[(Double, Map[Parameter[_], Seq[Double]])](variables, List()) factor.fillByRule(rule _) factor } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ElementSampler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ElementSampler.scala index 71b4b742..244fa6e5 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ElementSampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ElementSampler.scala @@ -18,6 +18,10 @@ import com.cra.figaro.language._ import com.cra.figaro.util._ import scala.collection.mutable.Map +/** + * An abstract class to generates samples from the marginal distribution of an element. + * @param target The element to generate samples from + */ abstract class ElementSampler(target: Element[_]) extends UnweightedSampler(target.universe, target) { def sample(): (Boolean, Sample) = { @@ -33,10 +37,7 @@ abstract class ElementSampler(target: Element[_]) extends UnweightedSampler(targ } /** - * Anytime Metropolis-Hastings sampler. - * @param burnIn The number of iterations to run before samples are collected - * @param interval The number of iterations to perform between collecting samples - * + * Anytime Element sampler. */ class AnytimeElementSampler(target: Element[_]) extends ElementSampler(target) @@ -59,11 +60,9 @@ class AnytimeElementSampler(target: Element[_]) } /** - * One-time Metropolis-Hastings sampler. - * - * @param burnIn The number of iterations to run before samples are collected - * @param interval The number of iterations to perform between collecting samples + * One-time Element sampler. * + * @param myNumSamples The number samples to take from the element */ class OneTimeElementSampler(target: Element[_], myNumSamples: Int) extends ElementSampler(target) @@ -85,14 +84,12 @@ class OneTimeElementSampler(target: Element[_], myNumSamples: Int) object ElementSampler { /** - * Create an anytime Metropolis-Hastings sampler using the given proposal scheme with the given target - * query elements. + * Create an anytime Element sampler with the given target element */ def apply(target: Element[_]) = new AnytimeElementSampler(target) /** - * Create a one-time Metropolis-Hastings sampler using the given number of samples and proposal - * scheme with the given target query elements. + * Create an one time Element sampler with the given target element using the number of samples */ def apply(target: Element[_], numSamples: Int) = new OneTimeElementSampler(target, numSamples) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Forward.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Forward.scala index 376adc7f..566ddb95 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Forward.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Forward.scala @@ -74,10 +74,13 @@ object Forward { case _ => // avoiding recursion var state1 = state - var argsRemaining = (element.args ::: element.elementsIAmContingentOn.toList) + var initialArgs = (element.args ::: element.elementsIAmContingentOn.toList).toSet + var argsRemaining = initialArgs while (!argsRemaining.isEmpty) { state1 = sampleInState(argsRemaining.head, state1, universe) - argsRemaining = argsRemaining.tail + val newArgs = element.args.toSet -- initialArgs + initialArgs = initialArgs ++ newArgs + argsRemaining = argsRemaining.tail ++ newArgs } element.generate (state1, element.value) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala index bd17c768..d5760dd1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala @@ -1,13 +1,13 @@ /* * Importance.scala * Importance sampler. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -18,6 +18,8 @@ import com.cra.figaro.language._ import com.cra.figaro.util._ import scala.annotation.tailrec import scala.collection.mutable.{ Set, Map } +import com.cra.figaro.experimental.particlebp.AutomaticDensityEstimator +import com.cra.figaro.algorithm.factored.ParticleGenerator /** * Importance samplers. @@ -33,22 +35,37 @@ abstract class Importance(universe: Universe, targets: Element[_]*) * observations. To avoid this, we keep track of all these dependencies. * The dependencies map contains all the elements that could propagate an * observation to any given element. - * Note: the dependencies map is only concerned with active elements that are present + * Note: the dependencies map is only concerned with active elements that are present * at the beginning of sampling (even though we get a new active elements list each sample). * Temporary elements will always be created after the element that could propagate - * an observation to them, because that propagation has to go through a permanent + * an observation to them, because that propagation has to go through a permanent * element. * Therefore, we can generate the dependencies map once before all the samples are generated. - */ + */ + + // Calling Values on a continuous element requires that a ParticleGenerator exist in the universe. + // For a continuous element, we don't actually want to generate any dependencies, so we have the + // ParticleGenerator return zero sample. + ParticleGenerator(universe, new AutomaticDensityEstimator, 1, 0) private val dependencies = scala.collection.mutable.Map[Element[_], Set[Element[_]]]() private def makeDependencies() = { for { element <- universe.activeElements } { element match { - case d: Dist[_,_] => + case d: Dist[_,_] => for { o <- d.outcomes } { dependencies += o -> (dependencies.getOrElse(o, Set()) + d) } - case c: CachingChain[_,_] => + //In principle, we should create a dependency from the result element of a chain to + //the chain. If the result element is a permanent element, this could matter. + //However, in most relevant cases (e.g., compound versions of atomic elements), the + //outcome element will be temporary and automatically generated after the chain, + //so we don't need the dependency. On the other hand, creating the dependency requires + //a call to values which is slow and dangerous. + // + //Unfortunately, the above intuition doesn't hold. The EM with importance tests fail + //to terminate unless we add dependencies to chains. We're still searching for a better + //way to determine the possible outcome elements than to call values on the parent. + case c: CachingChain[_,_] => val outcomes = Values(universe)(c.parent).map(c.get(_)) for { o <- outcomes } { dependencies += o -> (dependencies.getOrElse(o, Set()) + c) } case _ => () @@ -56,28 +73,27 @@ abstract class Importance(universe: Universe, targets: Element[_]*) } } makeDependencies() - + private var numRejections = 0 private var logSuccessWeight = 0.0 private var numSamples = 0 - + override protected def resetCounts() { super.resetCounts() numRejections = 0 logSuccessWeight = 0.0 numSamples = 0 } - - + /* * Produce one weighted sample of the given element. weightedSample takes into account conditions and constraints * on all elements in the Universe, including those that depend on this element. */ @tailrec final def sample(): Sample = { - /* + /* * We need to recreate the activeElements each sample, because non-temporary elements may have been made active * in a previous iteration. See the relevant test in ImportanceTest. - */ + */ val activeElements = universe.activeElements val resultOpt: Option[Sample] = try { @@ -137,7 +153,7 @@ abstract class Importance(universe: Universe, targets: Element[_]*) case (Some(obs1), Some(obs2)) if obs1 == obs2 => Some(obs1) case _ => throw Importance.Reject // incompatible observations } - val value: T = + val value: T = if (fullObservation.isEmpty || !element.isInstanceOf[HasDensity[_]]) { val result = sampleValue(state, element, fullObservation) if (!element.condition(result)) throw Importance.Reject @@ -147,15 +163,16 @@ abstract class Importance(universe: Universe, targets: Element[_]*) // This partially implements likelihood weighting by clamping the element to its // desired value and multiplying the weight by the density of the value. // This can dramatically reduce the number of rejections. - element.args.foreach(sampleOne(state, _, None)) val obs = fullObservation.get - // Subtle issue taken care of by the following line - // A parameterized element may or may not be a chain to an atomic element - // If it's not, we have to make sure to set its value to the observation here - // If it is, we have to make sure to propagate the observation through the chain - sampleValue(state, element, Some(obs)) - state.weight += math.log(element.asInstanceOf[HasDensity[T]].density(obs)) + sampleArgs(element, state: State, Set[Element[_]](element.args:_*)) + + // I'm not quite sure why we have to call sampleValue here when we're about to set the value of this element to obs. + // If I remove this call, the test "should correctly resample an element's arguments when the arguments change during samples" + // fails. + sampleValue(state, element, Some(obs)) + val density = element.asInstanceOf[HasDensity[T]].density(obs) + state.weight += math.log(density) obs } element.value = value @@ -172,7 +189,7 @@ abstract class Importance(universe: Universe, targets: Element[_]*) * element. Dist is an exception, because not all the outcomes need to be generated, but we only know which one * after we have sampled the randomness of the Dist. For this reason, we write special code to handle Dists. * For Chain, we also write special code to avoid calling get twice. - * + * * We propagate observations on Chains and Dists to their possible outcome elements. This ensures that instead of * sampling these elements and then checking whether the observation is satisfied, we set their values to the * required ones. This implements likelihood weighting and leads to faster convergence of the algorithm. @@ -203,29 +220,54 @@ abstract class Importance(universe: Universe, targets: Element[_]*) case Some(false) => state.weight += math.log(1 - probValue) false - case _ => + case _ => val result = random.nextDouble() < probValue f.value = result result } + case f: ParameterizedFlip => + val probValue = sampleOne(state, f.parameter, None) + + observation match { + case Some(true) => + true + case Some(false) => + false + case _ => + val result = random.nextDouble() < probValue + f.value = result + result + } + case _ => - (element.args ::: element.elementsIAmContingentOn.toList) foreach (sampleOne(state, _, None)) + val args = (element.args ::: element.elementsIAmContingentOn.toList) + sampleArgs(element, state: State, Set[Element[_]](args:_*)) element.randomness = element.generateRandomness() element.value = element.generateValue(element.randomness) element.value } } + @tailrec + private def sampleArgs(element: Element[_], state: State, args: Set[Element[_]]): Unit = { + args foreach (sampleOne(state, _, None)) + val newArgs = Set[Element[_]](element.args:_*) -- state.assigned + if (newArgs.nonEmpty) sampleArgs(element, state, newArgs) else return + } + + /** + * The computed probability of evidence. + */ def logProbEvidence: Double = { logSuccessWeight - Math.log(numSamples + numRejections) } - + } object Importance { /* - * An element cannot be assigned more than once during importance sampling. If an element has been assigned, - * its assigned value will be held in its value field. A state consists of the set of variables that have + * An element cannot be assigned more than once during importance sampling. If an element has been assigned, + * its assigned value will be held in its value field. A state consists of the set of variables that have * been assigned, together with the accumulated weight so far. */ /** * Convenience class to store the set of sampled elements, along with the current sampling weight. @@ -245,8 +287,8 @@ object Importance { * using the given number of samples. */ def apply(myNumSamples: Int, targets: Element[_]*)(implicit universe: Universe) = - new Importance(universe, targets: _*) with OneTimeProbQuerySampler { - val numSamples = myNumSamples + new Importance(universe, targets: _*) with OneTimeProbQuerySampler { + val numSamples = myNumSamples /** * Use one-time sampling to compute the probability of the given named evidence. @@ -260,20 +302,20 @@ object Importance { start() Math.exp(logProbEvidence - logPartition) } - + } - + /** * Use IS to compute the probability that the given element satisfies the given predicate. - */ + */ def probability[T](target: Element[T], predicate: T => Boolean): Double = { val alg = Importance(10000, target) alg.start() val result = alg.probability(target, predicate) alg.kill() result - } - + } + /** * Use IS to compute the probability that the given element has the given value. */ diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala index 337b7d03..21c3b438 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala @@ -1,13 +1,13 @@ /* * MetropolisHastings.scala * Metropolis-Hastings sampler. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -48,12 +48,12 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc universe.register(currentConstraintValues) /** - * Get the acceptance ratio for the sampler + * Get the acceptance ratio for the sampler. */ def acceptRejectRatio = accepts.toDouble / (accepts.toDouble + rejects.toDouble) /** - * Set this flag to true to obtain debugging information + * Set this flag to true to obtain debugging information. */ var debug = false @@ -67,32 +67,35 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc * We keep track of the improvement in the constraints for the new proposal compared to the original value. * We keep track of which elements do not have their condition satisfied by the new proposal. */ -private def attemptChange[T](state: State, elem: Element[T]): State = { -val newValue = elem.generateValue(elem.randomness) -// if an old value is already stored, don't overwrite it -val newOldValues = -if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) -if (elem.value != newValue) { -//val newProb = -// state.modelProb * elem.score(elem.value, newValue) -val newProb = state.modelProb -val newDissatisfied = -if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem -elem.value = newValue -State(newOldValues, state.oldRandomness, state.proposalProb, newProb, newDissatisfied) -} else { -// We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, -// even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set -// when deciding whether to accept the proposal. -val newDissatisfied = -if (elem.condition(newValue)) { -state.dissatisfied - elem -} else { -state.dissatisfied + elem -} -State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) -} -} + private def attemptChange[T](state: State, elem: Element[T]): State = { + val newValue = { + // Don't generate a new value for an observed element because it won't agree with the observation + // For a compound element we can't do this because we have to condition the arguments by the + // probability of generating the correct value. + if (elem.observation.isEmpty || !elem.isInstanceOf[Atomic[_]]) elem.generateValue(elem.randomness) + else elem.observation.get + } + // if an old value is already stored, don't overwrite it + val newOldValues = + if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) + if (elem.value != newValue) { + val newDissatisfied = + if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem + elem.value = newValue + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } else { + // We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, + // even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set + // when deciding whether to accept the proposal. + val newDissatisfied = + if (elem.condition(newValue)) { + state.dissatisfied - elem + } else { + state.dissatisfied + elem + } + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } + } private def propose[T](state: State, elem: Element[T]): State = { if (debug) println("Proposing " + elem.name.string) @@ -206,30 +209,45 @@ State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, ne result } -def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { + /* + * Update a set of elements after a proposal given the current state. + * Since elements must be updated in generative order, we have to ensure the arguments + * of an element are updated being an element itself. To do that, we check the intersection + * of the an element's arguments with the elements that still need to be updated. If the intersection + * is not empty, we recursively update the intersecting elements. Once those updates are completed, + * we update an element and move on to the next element in the set. + */ + private def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { var returnState = state var updatesLeft = toUpdate - while (!updatesLeft.isEmpty){ - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - while (!argsRemaining.isEmpty) { - returnState = updateManyHelper(returnState, argsRemaining.toSet) - argsRemaining = argsRemaining.tail + while (!updatesLeft.isEmpty) { + // Check the intersection of an element's arguments with the updates that still need to occur + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + while (!argsRemaining.isEmpty) { + // update the element's arguments first + returnState = updateManyHelper(returnState, argsRemaining.toSet) + argsRemaining = argsRemaining.tail } - returnState = updateOne(returnState, updatesLeft.head) + // once the args are updated, update this element + returnState = updateOne(returnState, updatesLeft.head) updatesLeft = updatesLeft.tail - } - returnState + } + returnState } - + + /* + * A recursive function to work in conjunction with updateMany to check the order of the element + * updates. + */ @tailrec private def updateManyHelper(state: State, toUpdate: Set[Element[_]]): State = { - var returnState = state - var updatesLeft = toUpdate - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - if (argsRemaining.isEmpty){ - returnState = updateOne(returnState, updatesLeft.head) - returnState } - else { updateManyHelper(returnState, argsRemaining.toSet) } + var returnState = state + var updatesLeft = toUpdate + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + if (argsRemaining.isEmpty) { + returnState = updateOne(returnState, updatesLeft.head) + returnState + } else { updateManyHelper(returnState, argsRemaining.toSet) } } /* @@ -346,7 +364,7 @@ def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { } protected def doInitialize(): Unit = { - // Need to prime the universe to make sure all elements have a generated value + // Need to prime the universe to make sure all elements have a generated value Forward(universe) initConstrainedValues() for { i <- 1 to burnIn } mhStep() @@ -496,14 +514,14 @@ object MetropolisHastings { /** * Use MH to compute the probability that the given element has the given value. - */ + */ def probability[T](target: Element[T], predicate: T => Boolean): Double = { val alg = MetropolisHastings(1000000, ProposalScheme.default, target) alg.start() val result = alg.probability(target, predicate) alg.kill() result - } + } /** * Use MH to compute the probability that the given element has the given value. diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastingsAnnealer.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastingsAnnealer.scala index f4a276cc..c3af5854 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastingsAnnealer.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastingsAnnealer.scala @@ -22,7 +22,7 @@ import scala.math.log import scala.annotation.tailrec /** - * Metropolis-Hastings based Annealer + * Metropolis-Hastings based Annealer. * * @param annealSchedule The schedule that determines how to anneal the model * @param burnIn The number of iterations to run before annealing starts @@ -50,7 +50,7 @@ abstract class MetropolisHastingsAnnealer(universe: Universe, proposalScheme: Pr universe.register(nextConstraintValues) /** - * Set this flag to true to obtain debugging information + * Set this flag to true to obtain debugging information. */ var debug = false @@ -75,32 +75,29 @@ abstract class MetropolisHastingsAnnealer(universe: Universe, proposalScheme: Pr * We keep track of the improvement in the constraints for the new proposal compared to the original value. * We keep track of which elements do not have their condition satisfied by the new proposal. */ -private def attemptChange[T](state: State, elem: Element[T]): State = { -val newValue = elem.generateValue(elem.randomness) -// if an old value is already stored, don't overwrite it -val newOldValues = -if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) -if (elem.value != newValue) { -//val newProb = -// state.modelProb * elem.score(elem.value, newValue) -val newProb = state.modelProb -val newDissatisfied = -if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem -elem.value = newValue -State(newOldValues, state.oldRandomness, state.proposalProb, newProb, newDissatisfied) -} else { -// We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, -// even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set -// when deciding whether to accept the proposal. -val newDissatisfied = -if (elem.condition(newValue)) { -state.dissatisfied - elem -} else { -state.dissatisfied + elem -} -State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) -} -} + private def attemptChange[T](state: State, elem: Element[T]): State = { + val newValue = elem.generateValue(elem.randomness) + // if an old value is already stored, don't overwrite it + val newOldValues = + if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) + if (elem.value != newValue) { + val newDissatisfied = + if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem + elem.value = newValue + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } else { + // We need to make sure to add the element to the dissatisfied set if its condition is not satisfied, + // even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set + // when deciding whether to accept the proposal. + val newDissatisfied = + if (elem.condition(newValue)) { + state.dissatisfied - elem + } else { + state.dissatisfied + elem + } + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + } + } private def propose[T](state: State, elem: Element[T]): State = { if (debug) println("Proposing " + elem.name.string) @@ -213,30 +210,45 @@ State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, ne result } -def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { + /* + * Update a set of elements after a proposal given the current state. + * Since elements must be updated in generative order, we have to ensure the arguments + * of an element are updated being an element itself. To do that, we check the intersection + * of the an element's arguments with the elements that still need to be updated. If the intersection + * is not empty, we recursively update the intersecting elements. Once those updates are completed, + * we update an element and move on to the next element in the set. + */ + private def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { var returnState = state var updatesLeft = toUpdate - while (!updatesLeft.isEmpty){ - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - while (!argsRemaining.isEmpty) { - returnState = updateManyHelper(returnState, argsRemaining.toSet) - argsRemaining = argsRemaining.tail + while (!updatesLeft.isEmpty) { + // Check the intersection of an element's arguments with the updates that still need to occur + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + while (!argsRemaining.isEmpty) { + // update the element's arguments first + returnState = updateManyHelper(returnState, argsRemaining.toSet) + argsRemaining = argsRemaining.tail } - returnState = updateOne(returnState, updatesLeft.head) + // once the args are updated, update this element + returnState = updateOne(returnState, updatesLeft.head) updatesLeft = updatesLeft.tail - } - returnState + } + returnState } - + + /* + * A recursive function to work in conjunction with updateMany to check the order of the element + * updates. + */ @tailrec private def updateManyHelper(state: State, toUpdate: Set[Element[_]]): State = { - var returnState = state - var updatesLeft = toUpdate - var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) - if (argsRemaining.isEmpty){ - returnState = updateOne(returnState, updatesLeft.head) - returnState } - else { updateManyHelper(returnState, argsRemaining.toSet) } + var returnState = state + var updatesLeft = toUpdate + var argsRemaining = universe.uses(updatesLeft.head).intersect(updatesLeft) + if (argsRemaining.isEmpty) { + returnState = updateOne(returnState, updatesLeft.head) + returnState + } else { updateManyHelper(returnState, argsRemaining.toSet) } } /* * A single step of MetropolisHastings consists of proposing according to the scheme and updating any elements that depend on those @@ -368,7 +380,7 @@ def updateMany[T](state: State, toUpdate: Set[Element[_]]): State = { for { i <- 1 to numSamples } { val newStateUnconstrained = proposeAndUpdate() val state1 = State(newStateUnconstrained.oldValues, newStateUnconstrained.oldRandomness, - newStateUnconstrained.proposalProb, newStateUnconstrained.modelProb + computeScores, newStateUnconstrained.dissatisfied) + newStateUnconstrained.proposalProb, newStateUnconstrained.modelProb + computeScores, newStateUnconstrained.dissatisfied) if (decideToAccept(state1, Schedule.schedule, 1)) { accepts += 1 // collect results for the new state and restore the original state diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ProbEvidenceSampler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ProbEvidenceSampler.scala index ba42f5a1..99b8ddb1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ProbEvidenceSampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/ProbEvidenceSampler.scala @@ -86,7 +86,7 @@ object ProbEvidenceSampler { * It also uses an anytime sampler for computing the baseline probability of conditions and constraints in the * program. * - * @param baselineWaitingTime The amount of time to allow the algorithm for computing the baseling probability to run. + * @param baselineWaitingTime The amount of time to allow the algorithm for computing the baseline probability to run. */ def apply(baselineWaitingTime: Long, evidence: List[NamedEvidence[_]])(implicit universe: Universe): ProbEvidenceAlgorithm = { val baseline = new ProbEvidenceSampler(universe) with AnytimeProbEvidenceSampler diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Schedule.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Schedule.scala index 2c7fb3ad..98dc700a 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Schedule.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Schedule.scala @@ -18,7 +18,7 @@ package com.cra.figaro.algorithm.sampling */ class Schedule(sch: (Double, Int) => Double) { /** - * Return the temperature given the current temperature and sampler iteration + * Return the temperature given the current temperature and sampler iteration. */ def temperature(current: Double, iter: Int) = sch(current, iter) } @@ -27,13 +27,13 @@ object Schedule { // the no annealing schedule, used for burn-in /** - * A schedule that performs no annealing (always returns 1.0) + * A schedule that performs no annealing (always returns 1.0). */ val schedule = new Schedule((c: Double, i: Int) => 1.0) /** * The default schedule used the standard logarithmic schedule, where calling default with a - * double will divide the log score by the parameter k + * double will divide the log score by the parameter k. */ def default(k: Double = 1.0) = new Schedule((c: Double, i: Int) => math.log(i.toDouble + 1.0) / k) diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/AutomaticDensityEstimator.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/AutomaticDensityEstimator.scala index 552ffd04..47fff956 100644 --- a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/AutomaticDensityEstimator.scala +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/AutomaticDensityEstimator.scala @@ -1,8 +1,21 @@ +/* + * AutomaticDensityEstimator.scala + * Class to compute the normal kernel density estimation of a set of samples + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 20, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.experimental.particlebp /** * Class to compute the normal kernel density estimation of a set of samples - * using Silverman's rule of thumb to automatically compute the bandwidth + * using Silverman's rule of thumb to automatically compute the bandwidth. */ class AutomaticDensityEstimator extends NormalKernelDensityEstimator { diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/NormalKernelDensityEstimator.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/NormalKernelDensityEstimator.scala index 5fe06eb3..366f975d 100644 --- a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/NormalKernelDensityEstimator.scala +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/NormalKernelDensityEstimator.scala @@ -1,3 +1,16 @@ +/* + * NormalKernelDensityEstimator.scala + * Trait to TBD + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 20, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.experimental.particlebp import scala.math._ @@ -7,6 +20,9 @@ import com.cra.figaro.algorithm.factored.DoubleDensityEstimator import com.cra.figaro.algorithm.factored.DensityException import com.cra.figaro.algorithm.factored.DensityEstimator +/** + * Doc needed + */ trait NormalKernelDensityEstimator extends DensityEstimator with DoubleDensityEstimator with IntDensityEstimator { def getBandwidth(samples: List[(Double, Double)]): Double diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/ParticleBeliefPropagation.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/ParticleBeliefPropagation.scala index 5aa5eeed..30755b32 100644 --- a/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/ParticleBeliefPropagation.scala +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/particlebp/ParticleBeliefPropagation.scala @@ -1,303 +1,317 @@ -package com.cra.figaro.experimental.particlebp - -import com.cra.figaro.language._ -import com.cra.figaro.algorithm.factored.FactoredAlgorithm -import com.cra.figaro.algorithm.factored.DivideableSemiRing -import com.cra.figaro.algorithm.lazyfactored.LazyValues -import com.cra.figaro.algorithm.factored.Factory -import com.cra.figaro.algorithm.factored.Variable -import com.cra.figaro.algorithm.OneTime -import com.cra.figaro.algorithm.Anytime -import com.cra.figaro.algorithm.factored.LogSumProductSemiring -import com.cra.figaro.algorithm.ProbQueryAlgorithm -import com.cra.figaro.algorithm.OneTimeProbQuery -import com.cra.figaro.algorithm.factored.Factor -import scala.collection.immutable.Set -import scala.collection.mutable.Map -import com.cra.figaro.algorithm.factored.beliefpropagation.InnerBPHandler -import com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeInnerBPHandler -import com.cra.figaro.algorithm.factored.beliefpropagation.VariableNode -import com.cra.figaro.algorithm.factored.ParticleGenerator -import com.cra.figaro.algorithm.factored.DensityEstimator -import com.cra.figaro.algorithm.AnytimeProbQuery -import com.cra.figaro.algorithm.factored.beliefpropagation.AnytimeInnerBPHandler -import com.cra.figaro.algorithm.factored.beliefpropagation.FactorNode -import com.cra.figaro.algorithm.factored.beliefpropagation.Node - -trait ParticleBeliefPropagation extends FactoredAlgorithm[Double] with InnerBPHandler { - - /** - * By default, implementations that inherit this trait have no debug information. - * Override this if you want a debugging option. - */ - val debug: Boolean = false - - /** - * The universe on which this belief propagation algorithm should be applied. - */ - val universe: Universe - - /** - * Target elements that should not be eliminated but should be available for querying. - */ - val targetElements: List[Element[_]] - - /** - * Since BP uses division to compute messages, the semiring has to have a division function defined - */ - override val semiring: DivideableSemiRing[Double] - - /** - * The density estimator that will estimate the density of a particle. used for resampling. - */ - val densityEstimator: DensityEstimator - - /** - * A particle generator to generate particles and do resampling - */ - val pbpSampler: ParticleGenerator - - /** - * Variable that if set to true, will preserve parts of the factor graph that cannot - * change during resampling. This will preserve the messages in those parts of the factor graph. - * This feature is experimental and not guaranteed to work currently. Default is false. - */ - val preserveUnchangedGraph: Boolean = false - - /** - * Elements towards which queries are directed. By default, these are the target elements. - */ - def starterElements: List[Element[_]] = targetElements - - /* - * Updates the posterior factors for the specified elements after each inner loop of BP - * For each of the prior factors, it finds the belief on the corresponding factor in the BP factor +/* + * NormalKernelDensityEstimator.scala + * Trait to TBD + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 20, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.experimental.particlebp + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.factored.FactoredAlgorithm +import com.cra.figaro.algorithm.factored.factors.{Factory, DivideableSemiRing, Factor, LogSumProductSemiring, Variable} +import com.cra.figaro.algorithm.lazyfactored.LazyValues +import com.cra.figaro.algorithm.OneTime +import com.cra.figaro.algorithm.Anytime +import com.cra.figaro.algorithm.ProbQueryAlgorithm +import com.cra.figaro.algorithm.OneTimeProbQuery +import scala.collection.immutable.Set +import scala.collection.mutable.Map +import com.cra.figaro.algorithm.factored.beliefpropagation.InnerBPHandler +import com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeInnerBPHandler +import com.cra.figaro.algorithm.factored.beliefpropagation.VariableNode +import com.cra.figaro.algorithm.factored.ParticleGenerator +import com.cra.figaro.algorithm.factored.DensityEstimator +import com.cra.figaro.algorithm.AnytimeProbQuery +import com.cra.figaro.algorithm.factored.beliefpropagation.AnytimeInnerBPHandler +import com.cra.figaro.algorithm.factored.beliefpropagation.FactorNode +import com.cra.figaro.algorithm.factored.beliefpropagation.Node + +/** + * Doc needed + */ +trait ParticleBeliefPropagation extends FactoredAlgorithm[Double] with InnerBPHandler { + + /** + * By default, implementations that inherit this trait have no debug information. + * Override this if you want a debugging option. + */ + val debug: Boolean = false + + /** + * The universe on which this belief propagation algorithm should be applied. + */ + val universe: Universe + + /** + * Target elements that should not be eliminated but should be available for querying. + */ + val targetElements: List[Element[_]] + + /** + * Since BP uses division to compute messages, the semiring has to have a division function defined + */ + override val semiring: DivideableSemiRing[Double] + + /** + * The density estimator that will estimate the density of a particle. used for resampling. + */ + val densityEstimator: DensityEstimator + + /** + * A particle generator to generate particles and do resampling. + */ + val pbpSampler: ParticleGenerator + + /** + * Variable that if set to true, will preserve parts of the factor graph that cannot + * change during resampling. This will preserve the messages in those parts of the factor graph. + * This feature is experimental and not guaranteed to work currently. Default is false. + */ + val preserveUnchangedGraph: Boolean = false + + /** + * Elements towards which queries are directed. By default, these are the target elements. + */ + def starterElements: List[Element[_]] = targetElements + + /* + * Updates the posterior factors for the specified elements after each inner loop of BP + * For each of the prior factors, it finds the belief on the corresponding factor in the BP factor * graph, and updates the cached version of the factors in the Factory - */ - /* - private[figaro] def updatePosteriorFactors(elems: Set[Element[_]]) = { - for { elem <- elems } { - val priorFactors = Factory.make(elem).toSet - val posteriorFactors = bp.factorGraph.getNeighbors(VariableNode(Variable(elem))).map(n => bp.unmakeLogarithmic(bp.belief(n))).toSet - val newFactors = priorFactors.map(f => { - val existInNew = posteriorFactors.find(p => p.variables.toSet == f.variables.toSet) - if (existInNew.nonEmpty) existInNew.get else f - }) - Factory.updateFactor(elem, newFactors.toList) - } - } + */ + /* + private[figaro] def updatePosteriorFactors(elems: Set[Element[_]]) = { + for { elem <- elems } { + val priorFactors = Factory.make(elem).toSet + val posteriorFactors = bp.factorGraph.getNeighbors(VariableNode(Variable(elem))).map(n => bp.unmakeLogarithmic(bp.belief(n))).toSet + val newFactors = priorFactors.map(f => { + val existInNew = posteriorFactors.find(p => p.variables.toSet == f.variables.toSet) + if (existInNew.nonEmpty) existInNew.get else f + }) + Factory.updateFactor(elem, newFactors.toList) + } + } * - */ - - /* + */ + + /* * Saves the posterior messages from a factor graph into a map - */ - private[figaro] def savePosteriorMessages(elems: Set[Element[_]]): Map[Node, Map[Node, Factor[Double]]] = { - val oldMsgs = Map[Node, Map[Node, Factor[Double]]]() - - elems.foreach { elem => - val priorFactors = Factory.make(elem).toSet - - priorFactors.foreach { pf => - val fn: Node = FactorNode(pf.variables.toSet) - bp.factorGraph.getNeighbors(fn).map(n => { - val msg = bp.factorGraph.getLastMessage(n, fn) - oldMsgs.getOrElseUpdate(n, Map[Node, Factor[Double]]()) += fn -> msg - }) - } - val vn: Node = VariableNode(Variable(elem)) - bp.factorGraph.getNeighbors(vn).map(n => { - val msg = bp.factorGraph.getLastMessage(n, vn) - oldMsgs.getOrElseUpdate(n, Map[Node, Factor[Double]]()) += vn -> msg - }) - } - oldMsgs - } - - /* + */ + private[figaro] def savePosteriorMessages(elems: Set[Element[_]]): Map[Node, Map[Node, Factor[Double]]] = { + val oldMsgs = Map[Node, Map[Node, Factor[Double]]]() + + elems.foreach { elem => + val priorFactors = Factory.make(elem).toSet + + priorFactors.foreach { pf => + val fn: Node = FactorNode(pf.variables.toSet) + bp.factorGraph.getNeighbors(fn).map(n => { + val msg = bp.factorGraph.getLastMessage(n, fn) + oldMsgs.getOrElseUpdate(n, Map[Node, Factor[Double]]()) += fn -> msg + }) + } + val vn: Node = VariableNode(Variable(elem)) + bp.factorGraph.getNeighbors(vn).map(n => { + val msg = bp.factorGraph.getLastMessage(n, vn) + oldMsgs.getOrElseUpdate(n, Map[Node, Factor[Double]]()) += vn -> msg + }) + } + oldMsgs + } + + /* * Updates the messages in the bp factor graph with oldMsgs saved in a map - */ - private[figaro] def updatePosteriorMessages(oldMsgs: Map[Node, Map[Node, Factor[Double]]]) = { - oldMsgs.foreach { node => - node._2.foreach(to => - if (bp.factorGraph.contains(node._1) && bp.factorGraph.contains(to._1)) bp.factorGraph.update(node._1, to._1, to._2)) - } - } - - /* - * Runs the inner loop of PBP. + */ + private[figaro] def updatePosteriorMessages(oldMsgs: Map[Node, Map[Node, Factor[Double]]]) = { + oldMsgs.foreach { node => + node._2.foreach(to => + if (bp.factorGraph.contains(node._1) && bp.factorGraph.contains(to._1)) bp.factorGraph.update(node._1, to._1, to._2)) + } + } + + /* + * Runs the inner loop of PBP. * - */ - private[figaro] def runInnerLoop(elemsWithPosteriors: Set[Element[_]], dependentElems: Set[Element[_]]) = { - currentUniverse = universe - - // Save old messages if we are preserving the factor graph - val oldMsgs: Map[Node, Map[Node, Factor[Double]]] = if (preserveUnchangedGraph) savePosteriorMessages(elemsWithPosteriors) else Map() - - // Remove factors on all elements that can possibly change during resamples - dependentElems.foreach(Factory.removeFactors(_)) - - // Clear the variable and values caches - Variable.clearCache - LazyValues.clear(universe) - - // Create BP. - createBP(targetElements) - - // If we are updating old messages, do it now - if (oldMsgs.nonEmpty) { - bp.generateGraph() - updatePosteriorMessages(oldMsgs) - } - - // run BP - runBP() - } - - /* - * The resample function. All sampled elements are resampled. For each element that is resampled, - * we record the dependent elements on those elemens since that portion the factor graph will + */ + private[figaro] def runInnerLoop(elemsWithPosteriors: Set[Element[_]], dependentElems: Set[Element[_]]) = { + currentUniverse = universe + + // Save old messages if we are preserving the factor graph + val oldMsgs: Map[Node, Map[Node, Factor[Double]]] = if (preserveUnchangedGraph) savePosteriorMessages(elemsWithPosteriors) else Map() + + // Remove factors on all elements that can possibly change during resamples + dependentElems.foreach(Factory.removeFactors(_)) + + // Clear the variable and values caches + Variable.clearCache + LazyValues.clear(universe) + + // Create BP. + createBP(targetElements) + + // If we are updating old messages, do it now + if (oldMsgs.nonEmpty) { + bp.generateGraph() + updatePosteriorMessages(oldMsgs) + } + + // run BP + runBP() + } + + /* + * The resample function. All sampled elements are resampled. For each element that is resampled, + * we record the dependent elements on those elemens since that portion the factor graph will * have to be removed (since resampling can change the structure). - */ - private[figaro] def resample(): (Set[Element[_]], Set[Element[_]]) = { - val needsToBeResampled = pbpSampler.sampledElements.filter(e => bp.factorGraph.contains(VariableNode(Variable(e)))) - val dependentElems = needsToBeResampled.flatMap { elem => - val oldBeliefs = bp.getBeliefsForElement(elem) - val bw = proposalEstimator(oldBeliefs) - val newSamples = pbpSampler.resample(elem, oldBeliefs, bw) - universe.usedBy(elem) - } - (needsToBeResampled, dependentElems) - } - - /* + */ + private[figaro] def resample(): (Set[Element[_]], Set[Element[_]]) = { + val needsToBeResampled = pbpSampler.sampledElements.filter(e => bp.factorGraph.contains(VariableNode(Variable(e)))) + val dependentElems = needsToBeResampled.flatMap { elem => + val oldBeliefs = bp.getBeliefsForElement(elem) + val bw = proposalEstimator(oldBeliefs) + val newSamples = pbpSampler.resample(elem, oldBeliefs, bw) + universe.usedBy(elem) + } + (needsToBeResampled, dependentElems) + } + + /* * Runs the outer loop of PBP. - */ - private[figaro] def runOuterLoop() = { - - val (needsToBeResampled, dependentElems): (Set[Element[_]], Set[Element[_]]) = if (bp != null) resample() else (Set(), Set()) - val elemsWithPosteriors: Set[Element[_]] = if (bp != null) bp.neededElements.toSet -- dependentElems -- needsToBeResampled else Set() - - runInnerLoop(elemsWithPosteriors, dependentElems) - //println("Inner loop complete") - } - - /* + */ + private[figaro] def runOuterLoop() = { + + val (needsToBeResampled, dependentElems): (Set[Element[_]], Set[Element[_]]) = if (bp != null) resample() else (Set(), Set()) + val elemsWithPosteriors: Set[Element[_]] = if (bp != null) bp.neededElements.toSet -- dependentElems -- needsToBeResampled else Set() + + runInnerLoop(elemsWithPosteriors, dependentElems) + //println("Inner loop complete") + } + + /* * Estimates the proposal distribution using the variance of the samples - */ - def proposalEstimator(beliefs: List[(Double, _)]): Double = { - val percentOfStd = .1 - - beliefs.head._2 match { - case i: Int => 1.0 - case d: Double => { - val bd = beliefs.asInstanceOf[List[(Double, Double)]] - val mean = (0.0 /: bd)((c: Double, n: (Double, Double)) => c + n._1 * n._2) - val std = math.sqrt((0.0 /: bd)((c: Double, n: (Double, Double)) => c + (n._1 - mean) * (n._1 - mean) * n._2)) - std * .1 - } - } - - } - - /** - * Runs this particle belief propagation algorithm for one iteration. An iteration here is - * one iteration of the outer loop. This means that the inner BP loop may run several iterations. - */ - def runStep() { - runOuterLoop() - } - -} - -trait OneTimeParticleBeliefPropagation extends ParticleBeliefPropagation with OneTime with OneTimeInnerBPHandler { - val outerIterations: Int - - def run() = { - for { i <- 1 to outerIterations } { runStep() } - } -} - -/** - * Trait for Anytime BP algorithms - */ -trait AnytimeParticleBeliefPropagation extends ParticleBeliefPropagation with Anytime with AnytimeInnerBPHandler - -/** - * Class to implement a probability query BP algorithm - */ -abstract class ProbQueryParticleBeliefPropagation(numArgSamples: Int, numTotalSamples: Int, - override val universe: Universe, targets: Element[_]*)( - //val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], - //val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, - depth: Int = Int.MaxValue, upperBounds: Boolean = false) - extends ProbQueryAlgorithm - with ParticleBeliefPropagation { //with ProbEvidenceBeliefPropagation { - - val targetElements = targets.toList - - val queryTargets = targetElements - - val semiring = LogSumProductSemiring - - // Dependent stuff not currently implemented - val dependentUniverses: List[(Universe, List[NamedEvidence[_]])] = List() - - val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double = { - (u: Universe, e: List[NamedEvidence[_]]) => () => 1.0 - } - - val densityEstimator = new AutomaticDensityEstimator - - val pbpSampler = ParticleGenerator(universe, densityEstimator, numArgSamples, numTotalSamples) - - /** - * Getting factors for PBP returns an empty list, since all of the factor creation is handled inside of - * the BP instances - */ - def getFactors(neededElements: List[Element[_]], - targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = List() - - def createBP(targets: List[Element[_]]): Unit = createBP(targets, depth, upperBounds) - - def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = bp.getBeliefsForElement(target).toStream - - def computeExpectation[T](target: Element[T], function: T => Double): Double = { - computeDistribution(target).map((pair: (Double, T)) => pair._1 * function(pair._2)).sum - } - - def computeEvidence(): Double = bp.computeEvidence -} - -object ParticleBeliefPropagation { - - /** - * Creates a One Time belief propagation computer in the current default universe. - */ - def apply(myOuterIterations: Int, myInnerIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, - universe, targets: _*)() with OneTimeParticleBeliefPropagation with OneTimeProbQuery { - val outerIterations = myOuterIterations - val innerIterations = myInnerIterations - } - - def apply(myOuterIterations: Int, myInnerIterations: Int, argSamples: Int, totalSamples: Int, - targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(argSamples, totalSamples, universe, targets: _*)() with OneTimeParticleBeliefPropagation with OneTimeProbQuery { - val outerIterations = myOuterIterations - val innerIterations = myInnerIterations - } - - def apply(stepTimeMillis: Long, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, - universe, targets: _*)() with AnytimeParticleBeliefPropagation with AnytimeProbQuery { - val myStepTimeMillis = stepTimeMillis - } - - def apply(stepTimeMillis: Long, argSamples: Int, totalSamples: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(argSamples, totalSamples, universe, targets: _*)() with AnytimeParticleBeliefPropagation with AnytimeProbQuery { - val myStepTimeMillis = stepTimeMillis - } -} - - + */ + def proposalEstimator(beliefs: List[(Double, _)]): Double = { + val percentOfStd = .1 + + beliefs.head._2 match { + case i: Int => 1.0 + case d: Double => { + val bd = beliefs.asInstanceOf[List[(Double, Double)]] + val mean = (0.0 /: bd)((c: Double, n: (Double, Double)) => c + n._1 * n._2) + val std = math.sqrt((0.0 /: bd)((c: Double, n: (Double, Double)) => c + (n._1 - mean) * (n._1 - mean) * n._2)) + std * .1 + } + } + + } + + /** + * Runs this particle belief propagation algorithm for one iteration. An iteration here is + * one iteration of the outer loop. This means that the inner BP loop may run several iterations. + */ + def runStep() { + runOuterLoop() + } + +} + +trait OneTimeParticleBeliefPropagation extends ParticleBeliefPropagation with OneTime with OneTimeInnerBPHandler { + val outerIterations: Int + + def run() = { + for { i <- 1 to outerIterations } { runStep() } + } +} + +/** + * Trait for Anytime BP algorithms + */ +trait AnytimeParticleBeliefPropagation extends ParticleBeliefPropagation with Anytime with AnytimeInnerBPHandler { + override def cleanUp() = if (bp != null) bp.kill +} + +/** + * Class to implement a probability query BP algorithm + */ +abstract class ProbQueryParticleBeliefPropagation(numArgSamples: Int, numTotalSamples: Int, + override val universe: Universe, targets: Element[_]*)( + //val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + //val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + depth: Int = Int.MaxValue, upperBounds: Boolean = false) + extends ProbQueryAlgorithm + with ParticleBeliefPropagation { //with ProbEvidenceBeliefPropagation { + + val targetElements = targets.toList + + val queryTargets = targetElements + + val semiring = LogSumProductSemiring + + // Dependent stuff not currently implemented + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])] = List() + + val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double = { + (u: Universe, e: List[NamedEvidence[_]]) => () => 1.0 + } + + val densityEstimator = new AutomaticDensityEstimator + + val pbpSampler = ParticleGenerator(universe, densityEstimator, numArgSamples, numTotalSamples) + + /** + * Getting factors for PBP returns an empty list, since all of the factor creation is handled inside of + * the BP instances + */ + def getFactors(neededElements: List[Element[_]], + targetElements: List[Element[_]], upperBounds: Boolean = false): List[Factor[Double]] = List() + + def createBP(targets: List[Element[_]]): Unit = createBP(targets, depth, upperBounds) + + def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = bp.getBeliefsForElement(target).toStream + + def computeExpectation[T](target: Element[T], function: T => Double): Double = { + computeDistribution(target).map((pair: (Double, T)) => pair._1 * function(pair._2)).sum + } + + def computeEvidence(): Double = bp.computeEvidence +} + +object ParticleBeliefPropagation { + + /** + * Creates a One Time belief propagation computer in the current default universe. + */ + def apply(myOuterIterations: Int, myInnerIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, + universe, targets: _*)() with OneTimeParticleBeliefPropagation with OneTimeProbQuery { + val outerIterations = myOuterIterations + val innerIterations = myInnerIterations + } + + def apply(myOuterIterations: Int, myInnerIterations: Int, argSamples: Int, totalSamples: Int, + targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryParticleBeliefPropagation(argSamples, totalSamples, universe, targets: _*)() with OneTimeParticleBeliefPropagation with OneTimeProbQuery { + val outerIterations = myOuterIterations + val innerIterations = myInnerIterations + } + + def apply(stepTimeMillis: Long, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, + universe, targets: _*)() with AnytimeParticleBeliefPropagation with AnytimeProbQuery { + val myStepTimeMillis = stepTimeMillis + } + + def apply(stepTimeMillis: Long, argSamples: Int, totalSamples: Int, targets: Element[_]*)(implicit universe: Universe) = + new ProbQueryParticleBeliefPropagation(argSamples, totalSamples, universe, targets: _*)() with AnytimeParticleBeliefPropagation with AnytimeProbQuery { + val myStepTimeMillis = stepTimeMillis + } +} + + diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Chain.scala b/Figaro/src/main/scala/com/cra/figaro/language/Chain.scala index aeb66e40..a825ae7e 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Chain.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Chain.scala @@ -1,13 +1,13 @@ /* * Chain.scala * Element Chains - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -20,13 +20,13 @@ import scala.collection.mutable.Set /** * A Chain(parent, fcn) represents the process that first generates a value for the parent, then - * applies fcn to get a new Element, and finally generates a value from that new Element. + * applies fcn to get a new Element, and finally generates a value from that new Element. * * Chain is the common base class for caching and non-caching chains. * All chains have a cache, whose size is specified by the cacheSize argument. * When a parent value is encountered, first the cache is checked to see if the result element is known. * If it is not, the resulting element is generated from scratch by calling fcn. - * + * * @param parent The parent element of the chain */ @@ -46,7 +46,7 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c */ var resultElement: Element[U] = _ - /* Data structures for the Chain. The cache stores previously generated result elements. The Context data + /* Data structures for the Chain. The cache stores previously generated result elements. The Context data * structures store the elements that were created in this context. We also stored newly created elements * in a subContext, which is based on the value of the parent. * Because Elements might be stored in sets or maps by algorithms, we need a way to allow the elements to be removed @@ -60,7 +60,7 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c private var lastParentValue: T = _ - /* Must override clear temporary for Chains. We can never leave the chain in an uninitialized state. That is, + /* Must override clear temporary for Chains. We can never leave the chain in an uninitialized state. That is, * the chain MUST ALWAYS have a valid element to return. So when clearing temporaries we clear everything * except the current context. */ @@ -91,11 +91,11 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c } /* Computes the new result. If the cache contains a VALID element for this parent value, then return the - * the element. Otherwise, we need to create one. First, we create an entry in the ContextContents since + * the element. Otherwise, we need to create one. First, we create an entry in the ContextContents since * any elements created in this context will be stored in the subContext of parentValue. Then, if the cache * is full, we resize the cache to make room for a new result element. Apply the function of the chain, and * stored the new result in the cache. If the element created is a new element, then registers it uses - * in the Universe. + * in the Universe. */ /** @@ -123,11 +123,11 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c resultElement = newResult newResult } - + /** - * Get the distribution over the result corresponding to the given parent value. This call is UNCACHED, + * Get the distribution over the result corresponding to the given parent value. This call is UNCACHED, * meaning it will not be stored in the Chain's cache, and subsequent calls using the same parentValue - * could return different elements + * could return different elements. */ def getUncached(parentValue: T): Element[U] = { if (lastParentValue == null || lastParentValue != parentValue) { @@ -138,7 +138,7 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c } resultElement } - + // All elements created in cpd will be created in this Chain's context with a subContext of parentValue private def getResult(parentValue: T): Element[U] = { universe.pushContext(this) @@ -152,22 +152,24 @@ class Chain[T, U](name: Name[U], val parent: Element[T], fcn: T => Element[U], c */ protected def resizeCache(dropValue: T) = { cache -= dropValue - universe.deactivate(myMappedContextContents(dropValue)) - elemInContext --= myMappedContextContents(dropValue) - myMappedContextContents -= dropValue + if (myMappedContextContents.contains(dropValue)) { + universe.deactivate(myMappedContextContents(dropValue)) + elemInContext --= myMappedContextContents(dropValue) + myMappedContextContents -= dropValue + } } override def toString = "Chain(" + parent + ", " + cpd + ")" } /** - * A NonCachingChain is an implementation of Chain with a single element cache + * A NonCachingChain is an implementation of Chain with a single element cache. */ class NonCachingChain[T, U](name: Name[U], parent: Element[T], cpd: T => Element[U], collection: ElementCollection) extends Chain(name, parent, cpd, 2, collection) /** - * A CachingChain is an implementation of Chain with a 1000 element cache + * A CachingChain is an implementation of Chain with a 1000 element cache. */ class CachingChain[T, U](name: Name[U], parent: Element[T], cpd: T => Element[U], collection: ElementCollection) extends Chain(name, parent, cpd, Int.MaxValue, collection) @@ -189,7 +191,7 @@ object NonCachingChain { object Chain { /** * Create a chain of 1 argument. - * The Chain factory constructor chooses either a CachingChain or NonCachingChain depending on the cacheability of the parent. + * The Chain factory constructor chooses either a CachingChain or NonCachingChain depending on the cacheability of the parent. * The parent's cacheability is determined by the parent.isCachable field. This is normally determined by the type of the element. */ def apply[T, U](parent: Element[T], cpd: T => Element[U])(implicit name: Name[U], collection: ElementCollection): Chain[T, U] = { @@ -197,7 +199,7 @@ object Chain { else new NonCachingChain(name, parent, cpd, collection) } - /** + /** * Create a chain of 2 arguments. This is implemented as a chain on the single argument consisting of the pair of the two arguments. * If both arguments are cachable, a CachingChain will be produced, otherwise a NonCaching chain. */ @@ -215,7 +217,7 @@ object CachingChain { def apply[T, U](parent: Element[T], cpd: T => Element[U])(implicit name: Name[U], collection: ElementCollection): CachingChain[T, U] = new CachingChain(name, parent, cpd, collection) - /** + /** * Create a CachingChain of 2 arguments. This is implemented as a chain on the single argument consisting of the pair of the two arguments. */ def apply[T1, T2, U](parent1: Element[T1], parent2: Element[T2], cpd: (T1, T2) => Element[U])(implicit name: Name[U], collection: ElementCollection): CachingChain[(T1, T2), U] = { diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Constant.scala b/Figaro/src/main/scala/com/cra/figaro/language/Constant.scala index f44daf28..8b61b1e7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Constant.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Constant.scala @@ -1,13 +1,13 @@ /* * Constant.scala * Constant elements. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -18,10 +18,15 @@ package com.cra.figaro.language */ class Constant[T](name: Name[T], val constant: T, collection: ElementCollection) - extends Deterministic[T](name, collection) with Atomic[T] with Cacheable[T] { + /* Avi: We don't handle point mass (infinite) densities properly, so I'm removing density. */ + /* Stuart and his students have a paper on propertly handling point masses - we should look at it. */ + //extends Deterministic[T](name, collection) with Atomic[T] with Cacheable[T] { + extends Deterministic[T](name, collection) with Cacheable[T] { + def args = List() + def generateValue() = constant - def density(t: T) = if (t == constant) Double.PositiveInfinity; else 0.0 + //def density(t: T) = if (t == constant) Double.PositiveInfinity; else 0.0 override def toString = "Constant(" + constant.toString + ")" } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/ConstraintType.scala b/Figaro/src/main/scala/com/cra/figaro/language/ConstraintType.scala index 8adf9e4d..7de6d629 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/ConstraintType.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/ConstraintType.scala @@ -1,3 +1,16 @@ +/* + * ConstraintType.scala + * Convenience class to place all constraints in logarithmic form + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: May 7, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.language import scala.math.log @@ -9,20 +22,20 @@ import scala.math.exp * * These classes don't need to be instantiated by the user, and are handled automatically in Figaro */ -abstract class ConstraintType[T] extends Function1[T, Double] { +private[figaro] abstract class ConstraintType[T] extends Function1[T, Double] { def apply(d: T): Double } /* - * Case class for user defined constraints that are already in logarithimc form + * Case class for user defined constraints that are already in logarithmic form */ -case class LogConstraintType[T](fcn: T => Double) extends ConstraintType[T] { +private[figaro] case class LogConstraintType[T](fcn: T => Double) extends ConstraintType[T] { def apply(d: T) = fcn(d) } /* * Case class for user defined constraints that are already in double form, converts to logs */ -case class ProbConstraintType[T](fcn: T => Double) extends ConstraintType[T] { +private[figaro] case class ProbConstraintType[T](fcn: T => Double) extends ConstraintType[T] { def apply(d: T) = log(fcn(d)) } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Continuous.scala b/Figaro/src/main/scala/com/cra/figaro/language/Continuous.scala index c364fa7b..73c4a245 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Continuous.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Continuous.scala @@ -1,5 +1,21 @@ +/* + * Continuous.scala + * Trait for continuous elements + * + * Created By: Synapski (no e-mail) + * Creation Date: Oct 6, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.language +/** + * Trait of elements representing continuous probability distributions + */ trait Continuous[T] extends Element[T] { /** @@ -7,12 +23,30 @@ trait Continuous[T] extends Element[T] { */ def logp(value: T): Double + //This constraint is not actually applied, but we have to define it so it can be set later. + private var observationConstraint : T => Double = (t: T) => 1.0 + + /** + * Condition the element by observing a particular value. + * Propagates the effect to dependent elements and ensures that no other value for the element can be generated. + * For continuous elements, a constraint is added whose value is the log likelihood of the observation. + */ override def observe(value: T) { - addLogConstraint { _ => logp(value) } + //We have to remove old observation first, or repeatedly observing will add on lots of constraints + //Should conditions be removed as well, as they are in regular element.observe? + removeConditions() + this.removeConstraint(observationConstraint) + observationConstraint = (t: T) => logp(value) + addLogConstraint( observationConstraint ) set(value) + this.observation = Some(value) } - + + /** + * Removes conditions on the element and allows different values of the element to be generated. + */ override def unobserve() { + this.removeConstraint(observationConstraint) unset() } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Element.scala b/Figaro/src/main/scala/com/cra/figaro/language/Element.scala index 65cc82e7..0daed4d9 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Element.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Element.scala @@ -1,13 +1,13 @@ /* * Element.scala * Elements of Figaro models. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -53,7 +53,7 @@ import scala.language.implicitConversions * An element has a name and belongs to an element collection that is used to find the element the name. * * Elements can be cacheable or non-cacheable, which determines what type of Chain will be created for them. - * If you create a new Element class that you want to be cached, you should declare it to implement the Cachable or IfArgsCachable traits. + * If you create a new Element class that you want to be cached, you should declare it to implement the Cacheable or IfArgsCacheable traits. * * @param name The name of the element * @param collection The element collection to which this element belongs @@ -74,7 +74,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * The type of soft constraints on the element. A constraint is a function from a value to a Double. */ type Constraint = T => Double - + /** * The type of randomness content of the element. */ @@ -89,7 +89,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) /** * The cacheability of the element. Chains create caches of their parent values, and it is useful to know when these values can be effectively cached and reused. - * In general, continuous distributions are not cacheable + * In general, continuous distributions are not cacheable. */ def isCachable(): Boolean = false @@ -101,11 +101,11 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) /** * Generate the next randomness given the current randomness. * Returns three values: The next randomness, the Metropolis-Hastings proposal probability - * ratio, which is + * ratio, which is: * * P(new -> old) / P(old -> new) * - * and the model probability ratio, which is + * and the model probability ratio, which is: * * P(new) / P(old) * @@ -121,7 +121,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) var randomness: Randomness = _ /** - * Generate the value of the element determinstically given its randomness and the values of + * Generate the value of the element deterministically given its randomness and the values of * its arguments. */ def generateValue(rand: Randomness): Value @@ -146,18 +146,18 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) /* Complete context of this element */ private[language] var myContext: List[Element[_]] = List() - /** The elements on which the existence of this element depends */ + /** The elements on which the existence of this element depends. */ def context = if (!active) { throw new NoSuchElementException } else myContext /* Stores the elements that were created in this element's context. Note this is not used - * for chains, since they maintain their own context control. + * for chains, since they maintain their own context control. */ private val myDirectContextContents: Set[Element[_]] = Set() /** - * Returns the set of elements directly created in the context of this element + * Returns the set of elements directly created in the context of this element. */ def directContextContents: Set[Element[_]] = if (!active) { throw new NoSuchElementException @@ -168,12 +168,12 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) private[language] def removeContextContents(e: Element[_]): Unit = myDirectContextContents -= e /** - * Returns true if this element is temporary, that is, was created in the context of another element + * Returns true if this element is temporary, that is, was created in the context of another element. */ def isTemporary = !myContext.isEmpty /** - * Clears all the temporary elements associated with this element (all elements created in it's context) + * Clears all the temporary elements associated with this element (all elements created in it's context). */ def clearContext() = universe.deactivate(directContextContents) @@ -188,6 +188,16 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) private[figaro]type Contingency = Element.Contingency private[figaro]type ElemVal[T] = Element.ElemVal[T] + /** + * Returns the elements that this element is contingent on. These are elements that are required to have a certain value for a condition or constraint + * to be relevant to this element. The contingency is required because conditions and constraints can be applied to references that are + * uncertain. Every possible element that could be pointed to by a reference must be given the condition or constraint, but the condition + * or constraint only applies if the elements earlier in the reference have the required value. + * + * Figaro takes care of handling all this under the + * hood. However, some algorithms may need to know which elements an element is contingent on. For example, sampling algorithms may need to sample + * those other elements first. This method is supplied to support this use case. + */ def elementsIAmContingentOn: Set[Element[_]] = { val conditionElements = for { @@ -223,12 +233,12 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * captured in a condition. However, for some algorithms, such as importance sampling, * it is useful to know that a condition is actually an observation of a specific value. * This is a common case, and to optimize it, we store the observation. - * + * * If an element has any other condition besides this observation, we cannot use the * observation. However, it can have a constraint. */ private[figaro] var observation: Option[T] = None - + /* * Testing whether a condition is satisfied can use any type of value. The condition can only be satisfied if the value has the right type and the condition returns true. */ @@ -288,11 +298,24 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) */ def allConstraints = myConstraints + // Avoid issuing a warning every time this method is called, e.g. for every sample. + private var constraintWarningIssued = false + /* - * Testing whether a condition is satisfied can use any type of value. The condition can only be satisfied if the value has the right type and the condition returns true. + * Computes the result of the element's constraint on a given value. + * A value of any type can be passed, but if the value is of an inappropriate type, the constraint result is negative infinity. + * This method also issues a warning if the constraint is greater than log(1) = 0. */ private def checkedConstraint(constraint: Constraint, value: Any): Double = - try { constraint(value.asInstanceOf[Value]) } catch { case _: ClassCastException => Double.NegativeInfinity } + try { + val result = constraint(value.asInstanceOf[Value]) + if (result > 0 && !constraintWarningIssued) { + + //println("Warning: constraint value " + result + " is greater than 1. Algorithms that use an upper bound of 1 will be incorrect.") + constraintWarningIssued = true + } + result + } catch { case _: ClassCastException => Double.NegativeInfinity } /* * Determines the result of a contingent constraint for a given value of this element. If any of the contingent elements does not have its appropriate value, the result is 1, @@ -330,7 +353,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) contingency.foreach(ev => ensureContingency(ev.elem)) myConstraints ::= (ProbConstraintType(constraint), contingency) } - + /** * Add a log contingent constraint to the element. By default, the contingency is empty. */ @@ -339,7 +362,6 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) contingency.foreach(ev => ensureContingency(ev.elem)) myConstraints ::= (LogConstraintType(constraint), contingency) } - /** * Remove all constraints associated with the given contingency. By default, the contingency is empty. @@ -349,6 +371,12 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) if (myConstraints.isEmpty) universe.makeUnconstrained(this) } + protected def removeConstraint(constraint: Constraint, contingency: Contingency = List()): Unit = { + myConstraints = myConstraints.filterNot((c: (Constraint,Contingency)) => c._2 == contingency && c._1 == constraint) + if (myConstraints.isEmpty) universe.makeUnconstrained(this) + } + + /** * Set the constraint associated with the contingency. Removes previous constraints associated with the contingency. By default, the contingency is empty. */ @@ -356,7 +384,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) removeConstraints(contingency) addConstraint(newConstraint, contingency) } - + /** * Set the log constraint associated with the contingency. Removes previous constraints associated with the contingency. By default, the contingency is empty. */ @@ -377,7 +405,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) } /** - * Removes conditions on the element and allows different values of the element to be generated + * Removes conditions on the element and allows different values of the element to be generated. */ def unobserve(): Unit = { unset() @@ -484,7 +512,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) def !==(that: Element[Value])(implicit universe: Universe) = new Neq("", this, that, universe) /** - * A string that is the element's name, if it has a non-empty one, otherwise the result of the element's toString + * A string that is the element's name, if it has a non-empty one, otherwise the result of the element's toString. */ def toNameString = if (name.string != "") name.string; else toString @@ -548,7 +576,7 @@ object Element { /** * Returns the given elements and all elements on which they are contingent, closed recursively. - * Only elements with condition + * Only elements with condition. */ def closeUnderContingencies(elements: scala.collection.Set[Element[_]]): scala.collection.Set[Element[_]] = { def findContingent(elements: scala.collection.Set[Element[_]]): scala.collection.Set[Element[_]] = { @@ -582,5 +610,3 @@ trait Cacheable[V] extends Element[V] { trait IfArgsCacheable[V] extends Element[V] { override def isCachable = args forall (_.isCachable) } - - diff --git a/Figaro/src/main/scala/com/cra/figaro/language/ElementCollection.scala b/Figaro/src/main/scala/com/cra/figaro/language/ElementCollection.scala index 69e77a5a..f9cff830 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/ElementCollection.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/ElementCollection.scala @@ -1,13 +1,13 @@ /* * ElementCollection.scala * Element collections. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -17,6 +17,7 @@ import com.cra.figaro.algorithm.Values import com.cra.figaro.algorithm.lazyfactored.{ValueSet, LazyValues} import com.cra.figaro.util._ import scala.collection.mutable.Map +import com.cra.figaro.library.collection.Container /** * An element collection contains elements. It can be used to find the elements in it by reference. @@ -27,7 +28,7 @@ trait ElementCollection { * Override this if you want a different universe. */ val universe: Universe = Universe.universe - + /* Elements with the same name may belong to the same collection. The most recently added on is used to resolve references. (That is why a List is used to hold the elements * with a given name, rather than a Set, so we always know which is the most recent.) When evidence is applied to a name, all the elements with that name have the evidence * applied to them. @@ -37,12 +38,13 @@ trait ElementCollection { /** All named elements in this collection. */ @deprecated("Use namedElements instead", "2.3.0.0") def allElements: List[Element[_]] = myElementMap.values.flatten.toList - + /** All named elements in this collection. */ def namedElements: List[Element[_]] = myElementMap.values.flatten.toList /** * Returns a reference element representing the single-valued reference. + * This method produces a new reference element every time it is called. */ def get[T](reference: Reference[T]): SingleValuedReferenceElement[T] = new SingleValuedReferenceElement(this, reference) @@ -154,7 +156,7 @@ trait ElementCollection { /* * Evidence in the element collection needs to be asserted not only on existing elements but also on future created elements that have the right reference. - * Therefore, we keep track of + * Therefore, we keep track of */ private case class RefEv[T](ref: Reference[T], ev: Evidence[T], contingency: Element.Contingency) private var collectedEvidence: Map[Reference[_], RefEv[_]] = Map() @@ -196,7 +198,7 @@ trait ElementCollection { else Set((None, List())) case Indirect(head, tail) => - val headCheck = myElementMap.getOrElse(head, List()) // use most recent element with the name + val headCheck = myElementMap.getOrElse(head, List()) // use most recent element with the name if (headCheck.nonEmpty) { val headElem = myElementMap(head).head // use most recent element with the name for { @@ -268,8 +270,12 @@ trait ElementCollection { /* * Follow the single-valued reference in the current state to get the element referred to. - * Need to clarify: This gets the element *currently* referred to by the reference. Provide example. - * + * In other words, this method produces the actual element pointed to by this reference, + * considering the values of elements along this reference. + * This method should be contrasted with the get method, which produces a reference element. + * If there is uncertainty in the reference, the reference element produces by get captures + * this uncertainty, whereas getElementByReference only produces one possible element, as + * determined by the current values of elements in the reference. */ def getElementByReference[T](reference: Reference[T]): Element[T] = reference match { @@ -301,7 +307,7 @@ trait ElementCollection { case Name(name) => try { val elems = myElementMap(name) - Set(elems.head.asInstanceOf[Element[T]]) // use most recent element with the name + Set(elems.head.asInstanceOf[Element[T]]) // use most recent element with the name } catch { case e: ClassCastException => throw new IllegalArgumentException("Reference has wrong type") @@ -326,8 +332,8 @@ object ElementCollection { */ implicit def default: ElementCollection = Universe.universe - /* - * Turns either a single element collection or a traversable of element collections into an element collection set + /* + * Turns either a single element collection or a traversable of element collections into an element collection set */ private[language] def makeElementCollectionSet(value: Any): Set[ElementCollection] = { def makeEC[T](s: Set[T]) = s map (_.asInstanceOf[ElementCollection]) diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Evidence.scala b/Figaro/src/main/scala/com/cra/figaro/language/Evidence.scala index 32127aff..18d2729c 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Evidence.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Evidence.scala @@ -29,7 +29,7 @@ sealed abstract class Evidence[T] { /** * Evidence representing a condition on an element. * - * @param predicate The predicate that must be satisfied by the element + * @param predicate The predicate that must be satisfied by the element. */ case class Condition[T](predicate: T => Boolean) extends Evidence[T] { /** Assert this evidence on the given element. The second argument is an optional contingency. */ diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala b/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala index 2ed4e65d..ade53b89 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala @@ -64,7 +64,9 @@ class ParameterizedFlip(name: Name[Boolean], override val parameter: AtomicBeta, def args: List[Element[_]] = List(parameter) protected def probValue = parameter.value - +/** + * Convert a distribution from this Flip into sufficient statistics + */ def distributionToStatistics(distribution: Stream[(Double, Boolean)]): Seq[Double] = { val distList = distribution.toList val trueProb = diff --git a/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala b/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala index ba79dbcc..65a7f4bd 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala @@ -1,5 +1,21 @@ +/* + * HasDensity.scala + * Trait for TBD + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jun 10, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.language +/** + * Trait of elements for which a density method is defined. + */ trait HasDensity[T] extends Element[T] { /** The probability density of a value. */ def density(t: T): Double diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Inject.scala b/Figaro/src/main/scala/com/cra/figaro/language/Inject.scala index 77ed895d..597ad201 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Inject.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Inject.scala @@ -1,13 +1,13 @@ /* * Inject.scala * Element that converts a sequence of elements into an element over sequences. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -18,7 +18,7 @@ import com.cra.figaro.util._ /** * Element that converts a sequence of elements into an element over sequences. - * + * * @param xs The sequence of elements to be converted. */ class Inject[T](name: Name[List[T]], val xs: Seq[Element[T]], collection: ElementCollection) diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Parameter.scala b/Figaro/src/main/scala/com/cra/figaro/language/Parameter.scala index e28c2381..e70c9a8a 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Parameter.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Parameter.scala @@ -31,7 +31,7 @@ abstract trait Parameter[T] extends Atomic[T] { */ def maximize(sufficientStatistics: Seq[Double]) /** - * Returns a zero vector of sufficient statistics + * Returns a zero vector of sufficient statistics. */ def zeroSufficientStatistics: Seq[Double] /** @@ -50,7 +50,7 @@ abstract trait Parameter[T] extends Atomic[T] { def expectedValue: T /** - * The most likely value of the parameter (maximum a posteriori) given the evidence + * The most likely value of the parameter (maximum a posteriori) given the evidence. */ def MAPValue: T } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala b/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala index c0f2f477..cf9e5f69 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala @@ -17,20 +17,24 @@ package com.cra.figaro.language * Trait of elements which accept learnable parameters. * Parameterized elements are compound elements whose outcome is determined by a learnable parameter. */ - trait Parameterized[T] extends Element[T] with HasDensity[T] { /** * The parameter for this element. */ val parameters: Set[Parameter[_]] + /** + * Convert a distribution from this element into sufficient statistics for the specified parameter + */ def distributionToStatistics(p: Parameter[_], distribution: Stream[(Double, T)]): Seq[Double] } trait SingleParameterized[T] extends Parameterized[T] { val parameter: Parameter[_] override val parameters: Set[Parameter[_]] = Set(parameter) - + /** + * Convert a distribution from this element into sufficient statistics for the specified parameter + */ override def distributionToStatistics(p: Parameter[_], distribution: Stream[(Double, T)]): Seq[Double] = { if (p == parameter) { distributionToStatistics(distribution) @@ -40,5 +44,8 @@ trait SingleParameterized[T] extends Parameterized[T] { } } + /** + * Convert a distribution from this element into sufficient statistics + */ def distributionToStatistics(distribution: Stream[(Double, T)]): Seq[Double] } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/ReferenceElement.scala b/Figaro/src/main/scala/com/cra/figaro/language/ReferenceElement.scala index ad876b51..47352975 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/ReferenceElement.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/ReferenceElement.scala @@ -1,307 +1,316 @@ -/* - * ReferenceElement.scala - * Elements representing references and aggregates over references. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.language - -import com.cra.figaro.algorithm.ValuesMaker -import com.cra.figaro.algorithm.lazyfactored._ -import ValueSet._ -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.util._ -import scala.collection.mutable.Map -import scala.language.existentials - -/** - * Element representing the value of a reference. - * - * @param collection The collection to use to resolve the reference. - * @param reference The reference whose value is represented by this element. - * - */ - -abstract class ReferenceElement[T, U](coll: ElementCollection, val reference: Reference[T]) - extends Deterministic[U]("", coll) with IfArgsCacheable[U] { - lazy val args = collection.makeArgs(reference).toList -} - -/** - * Element representing a single-valued reference. Its value in a state is generated by following the reference - * in the state and taking the value of the resulting element. - */ -class SingleValuedReferenceElement[T](collection: ElementCollection, reference: Reference[T]) - extends ReferenceElement[T, T](collection, reference) with ValuesMaker[T] with ProbFactorMaker { - def generateValue() = { - val referredToElement = collection.getElementByReference(reference) - referredToElement.generateValue(referredToElement.randomness) - referredToElement.value - } - - /* - * We need to make sure that if the reference is indirect, each of the reference elements embedded in this element have their values - * determined, and then the same embedded elements are used when making factors. This is achieved by storing the embedded elements - * in a map. - * Note that we could have achieved a similar effect by making SingleValuedReferenceElement a case class, but that would be incorrect - * since with reference uncertainty, the same reference may exist twice in a collection with two different embedded references. - */ - private val embeddedElements: Map[ElementCollection, SingleValuedReferenceElement[T]] = Map() - - /** - * Returns all possible values of the given reference. - */ - private def referenceValues(reference: Reference[T], depth: Int): ValueSet[T] = { - collection.getFirst(reference) match { - case (elem, None) => LazyValues(universe)(elem.asInstanceOf[Element[T]], depth) - case (elem, Some(rest)) => - val firstValues = LazyValues(universe)(elem.asInstanceOf[Element[ElementCollection]], depth) - val restValues = - for { first <- firstValues.regularValues } yield { - val embedded = new SingleValuedReferenceElement(first, rest) - embeddedElements += first -> embedded - LazyValues(universe)(embedded, depth - 1) - } - val startValues: ValueSet[T] = if (!firstValues.hasStar) ValueSet.withoutStar(Set()); else ValueSet.withStar(Set()) - restValues.foldLeft(startValues)(_ ++ _) - } - } - def makeValues(depth: Int): ValueSet[T] = { - referenceValues(reference, depth) - } - - def makeFactors: List[Factor[Double]] = { - val (first, rest) = collection.getFirst(reference) - rest match { - case None => - val thisVar = Variable(this) - val refVar = Variable(first) - val factor = Factory.make[Double](List(thisVar, refVar)) - for { - i <- 0 until refVar.range.size - j <- 0 until refVar.range.size - } { - factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) - } - List(factor) - case Some(restRef) => - val firstVar = Variable(first) - val selectedFactors = - for { - (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex - firstCollection = firstXvalue.value.asInstanceOf[ElementCollection] - restElement = embeddedElements(firstCollection) - } yield { - Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(restElement)) :: restElement.makeFactors - } - selectedFactors.flatten - } - } -} - -/** - * Aggregate elements based on multi-valued references. Note that the values aggregated over are all the values of - * all elements that are referred to by the reference. If the same element is reachable by more than one path in the - * reference, its value is only included once. However, if two different referred to elements have the same value, - * the value is included multiple times. Since the order of these values is immaterial, we use a multiset to represent them. - * - * @param aggregate A function to aggregate elements referred to by the reference into a value of this aggregate element. - */ -class Aggregate[T, U](collection: ElementCollection, reference: Reference[T], aggregate: MultiSet[T] => U) - extends ReferenceElement[T, U](collection, reference) with ValuesMaker[U] with ProbFactorMaker { - - private class MultiValuedReferenceElement[T](coll: ElementCollection, ref: Reference[T]) extends ReferenceElement[T, MultiSet[T]](coll, ref) - with ValuesMaker[MultiSet[T]] with ProbFactorMaker { - - /* - * We need to make sure that if the reference is indirect, each of the reference elements embedded in this element have their values - * determined, and then the same embedded elements are used when making factors. This is achieved by storing the embedded elements - * in a map. - * Note that we could have achieved a similar effect by making MultiValuedReferenceElement a case class, but that would be incorrect - * since with reference uncertainty, the same reference may exist twice in a collection with two different embedded references. - * - * Since the MVRE factor maker uses embedded Inject and Apply elements, we need to make sure they are expanded and have their - * values computed when makeValues is called. Therefore, we store them in embeddedInject and embeddedApply. The key to these maps - * is the list of element collections that represents a possible value of the head of the reference. - */ - private val embeddedElements: Map[ElementCollection, MultiValuedReferenceElement[T]] = Map() - private val embeddedInject: Map[List[ElementCollection], Element[List[MultiSet[T]]]] = Map() - private val embeddedApply: Map[List[ElementCollection], Element[MultiSet[T]]] = Map() - - // collection.getElements is a set of elements, because if the same element is reachable by more than one path, it is only counted once. - // We convert it to a list so we get all the values of these elements, even if some of the elements have the same values. - def generateValue(): MultiSet[T] = { - val referredToElements = collection.getManyElementsByReference(reference).toList - referredToElements.foreach(elem => elem.generateValue(elem.randomness)) - HashMultiSet(referredToElements:_*) map ((e: Element[T]) => e.value) - } - - private def allUnions(setset1: ValueSet[MultiSet[T]], setset2: ValueSet[MultiSet[T]]): ValueSet[MultiSet[T]] = { - val multiSets = for { set1 <- setset1.regularValues; set2 <- setset2.regularValues } yield set1 union set2 - if (setset1.hasStar || setset2.hasStar) withStar(multiSets); else withoutStar(multiSets) - } - - /* - * The value of a MultiValuedReferenceElement is a multiset. - * To find all possible values, we proceed as follows: - * If the reference is simple (i.e., just a name), the element referred to by that name has some set of values. - * Each of those values becomes a singleton multiset in the values of the MVRE. - * We get a value set of multisets for each possible value of the name. - * The final step is to take the value set union of these value sets and return it as the value set for this MVRE. - * - * If the reference is compound, the each of the first name's possible values may either be a single EC or a traversable of ECs. - * If it's just an EC, we get the values of the rest of the reference (which are multisets) - * and those become the possible values of the MVRE associated with this first value. - * If it's a traversable of ECs, we get the possible values of the rest of the reference for each of the ECs in the traversable. - * Each such value is a multiset. - * So, at this point, we have a traversable T of value sets of multisets. We need to convert it into a single values set - * of multisets such that each multiset in the resulting value set is the multiset union of one multiset chosen for - * each of the value sets in T. This is accomplished by allUnions. - * As a result, we get a value set of multisets for each possible value of the name. - * The final step is to take the value set union of these value sets and return it as the value set for this MVRE. - */ - def makeValues(depth: Int): ValueSet[MultiSet[T]] = { - val (first, rest) = collection.getFirst(reference) - rest match { - case None => - val firstValues = LazyValues(universe)(first.asInstanceOf[Element[T]], depth) - firstValues.map((t: T) => HashMultiSet(List(t): _*)) - case Some(restRef) => - val results: Set[ValueSet[MultiSet[T]]] = { - val firstValues: ValueSet[_] = LazyValues(universe)(first, depth) - for { firstXvalue <- firstValues.xvalues } yield { - if (firstXvalue.isRegular) { - firstXvalue.value match { - case firstColl: ElementCollection => - val embedded = new MultiValuedReferenceElement(firstColl, restRef) - embeddedElements += firstColl -> embedded - LazyValues(universe)(embedded, depth - 1) - case ecs: Traversable[_] => - val collections: List[ElementCollection] = ecs.map((x: Any) => x.asInstanceOf[ElementCollection]).toList.distinct - val multis = { - for { - firstColl <- collections // Aggregates use set semantics for the elements they use. If the same element appears more than once, it is only counted once. - } yield { - val restMulti = new MultiValuedReferenceElement(firstColl, restRef) - embeddedElements += firstColl -> restMulti - restMulti - } - } - val combination: Element[List[MultiSet[T]]] = Inject(multis: _*) - LazyValues(universe)(combination, depth - 1) - embeddedInject += collections -> combination - val applyStarter: MultiSet[T] = HashMultiSet[T]() - val setMaker = Apply(combination, (sets: List[MultiSet[T]]) => (applyStarter /: sets)(_ union _)) - LazyValues(universe)(setMaker, depth - 1) - embeddedApply += collections -> setMaker - val resultSets: List[ValueSet[MultiSet[T]]] = - for { - (firstColl, multi) <- collections.zip(multis) // Aggregates use set semantics for the elements they use. If the same element appears more than once, it is only counted once. - } yield { - LazyValues(universe)(multi, depth - 1) - } - /* - * Here, we are creating the value set of multisets in which each multiset is the multiset union of - * multisets in the result sets. Since we are taking the multiset union, we start with the empty - * multiset, which is why the start is the value set containing the single empty multiset. - */ - val starter: ValueSet[MultiSet[T]] = withoutStar(Set(HashMultiSet())) - (starter /: resultSets)(allUnions(_, _)) - } - } else withStar[MultiSet[T]](Set()) - } - } - /* - * Here we are taking the value set union of the value sets produced for each of the possible first values, - * so that starter is the empty value set. - */ - val starter: ValueSet[MultiSet[T]] = withoutStar(Set()) - (starter /: results)(_ ++ _) - } - } - - def makeFactors: List[Factor[Double]] = { - val (first, rest) = collection.getFirst(reference) - val selectionFactors: List[List[Factor[Double]]] = { - rest match { - case None => - val thisVar = Variable(this) - val refVar = Variable(first) - val factor = Factory.make[Double](List(thisVar, refVar)) - for { - i <- 0 until refVar.range.size - j <- 0 until refVar.range.size - } { - factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) - } - List(List(factor)) - case Some(restRef) => - val firstVar = Variable(first) - for { - (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex - } yield { - if (firstXvalue.isRegular) { - firstXvalue.value match { - case firstCollection: ElementCollection => - val restElement = embeddedElements(firstCollection) - val result: List[Factor[Double]] = - Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(restElement)) :: Factory.make(restElement) - result - case cs: Traversable[_] => - // Create a multi-valued reference element (MVRE) for each collection in the value of the first name. - // Since the first name is multi-valued, its value is the union of the values of all these MVREs. - val collections = cs.asInstanceOf[Traversable[ElementCollection]].toList.distinct // Set semantics - val multis: List[MultiValuedReferenceElement[T]] = collections.map(embeddedElements(_)).toList - // Create the element that takes the union of the values of the all the MVREs. - // The combination and setMaker elements are encapsulated within this object and are created now, so we need to create factors for them. - // Finally, we create a conditional selector (see ProbFactor) to select the appropriate result value when the first - // name's value is these MVREs. - val combination = embeddedInject(collections) - val setMaker = embeddedApply(collections) - val result: List[Factor[Double]] = - Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(setMaker)) :: Factory.make(combination) ::: - Factory.make(setMaker) - result - } - } else Factory.makeStarFactor(this) - } - } - } - selectionFactors.flatten - } - } - - private val mvre = new MultiValuedReferenceElement(collection, reference) - private def possibleInputs(depth: Int): ValueSet[MultiSet[T]] = - LazyValues(universe)(mvre, depth) - - def generateValue(): U = aggregate(HashMultiSet(collection.getManyElementsByReference(reference).toList.map(_.value): _*)) - - def makeValues(depth: Int): ValueSet[U] = { - val inputs = possibleInputs(depth) - val resultValues = inputs.regularValues.map(aggregate(_)) - if (inputs.hasStar) withStar(resultValues); else withoutStar(resultValues) - } - - def makeFactors = { - val thisVar = Variable(this) - val mvreVar = Variable(mvre) - val factor = Factory.make[Double](List(thisVar, mvreVar)) - for { - (thisXvalue, thisIndex) <- thisVar.range.zipWithIndex - (mvreXvalue, mvreIndex) <- mvreVar.range.zipWithIndex - } { - if (thisXvalue.isRegular && mvreXvalue.isRegular) factor.set(List(thisIndex, mvreIndex), if (aggregate(mvreXvalue.value) == thisXvalue.value) 1.0; else 0.0) - } - // The MultiValuedReferenceElement for this aggregate is generated when values is called. - // Therefore, it will be included in the expansion and have factors made for it automatically, so we do not create factors for it here. - List(factor) - } -} +/* + * ReferenceElement.scala + * Elements representing references and aggregates over references. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.language + +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored._ +import ValueSet._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.factors.Factory +import com.cra.figaro.util._ +import scala.collection.mutable.Map +import scala.language.existentials + +/** + * Element representing the value of a reference. + * + * @param collection The collection to use to resolve the reference. + * @param reference The reference whose value is represented by this element. + * + */ + +abstract class ReferenceElement[T, U](coll: ElementCollection, val reference: Reference[T]) + extends Deterministic[U]("", coll) with IfArgsCacheable[U] { + lazy val args = collection.makeArgs(reference).toList +} + +/** + * Element representing a single-valued reference. Its value in a state is generated by following the reference + * in the state and taking the value of the resulting element. + */ +class SingleValuedReferenceElement[T](collection: ElementCollection, reference: Reference[T]) + extends ReferenceElement[T, T](collection, reference) with ValuesMaker[T] { + def generateValue() = { + val referredToElement = collection.getElementByReference(reference) + referredToElement.generateValue(referredToElement.randomness) + referredToElement.value + } + + /* + * We need to make sure that if the reference is indirect, each of the reference elements embedded in this element have their values + * determined, and then the same embedded elements are used when making factors. This is achieved by storing the embedded elements + * in a map. + * Note that we could have achieved a similar effect by making SingleValuedReferenceElement a case class, but that would be incorrect + * since with reference uncertainty, the same reference may exist twice in a collection with two different embedded references. + */ + val embeddedElements: Map[ElementCollection, SingleValuedReferenceElement[T]] = Map() + + /** + * Returns all possible values of the given reference. + */ + private def referenceValues(reference: Reference[T], depth: Int): ValueSet[T] = { + collection.getFirst(reference) match { + case (elem, None) => LazyValues(universe)(elem.asInstanceOf[Element[T]], depth) + case (elem, Some(rest)) => + val firstValues = LazyValues(universe)(elem.asInstanceOf[Element[ElementCollection]], depth) + val restValues = + for { first <- firstValues.regularValues } yield { + val embedded = new SingleValuedReferenceElement(first, rest) + embeddedElements += first -> embedded + LazyValues(universe)(embedded, depth - 1) + } + val startValues: ValueSet[T] = if (!firstValues.hasStar) ValueSet.withoutStar(Set()); else ValueSet.withStar(Set()) + restValues.foldLeft(startValues)(_ ++ _) + } + } + def makeValues(depth: Int): ValueSet[T] = { + referenceValues(reference, depth) + } + + // def makeFactors: List[Factor[Double]] = { + // val (first, rest) = collection.getFirst(reference) + // rest match { + // case None => + // val thisVar = Variable(this) + // val refVar = Variable(first) + // val factor = Factory.make[Double](List(thisVar, refVar)) + // for { + // i <- 0 until refVar.range.size + // j <- 0 until refVar.range.size + // } { + // factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) + // } + // List(factor) + // case Some(restRef) => + // val firstVar = Variable(first) + // val selectedFactors = + // for { + // (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex + // firstCollection = firstXvalue.value.asInstanceOf[ElementCollection] + // restElement = embeddedElements(firstCollection) + // } yield { + // Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(restElement)) :: restElement.makeFactors + // } + // selectedFactors.flatten + // } + // } +} + +/** + * Aggregate elements based on multi-valued references. Note that the values aggregated over are all the values of + * all elements that are referred to by the reference. If the same element is reachable by more than one path in the + * reference, its value is only included once. However, if two different referred to elements have the same value, + * the value is included multiple times. Since the order of these values is immaterial, we use a multiset to represent them. + * + * @param aggregate A function to aggregate elements referred to by the reference into a value of this aggregate element. + */ +class Aggregate[T, U](collection: ElementCollection, reference: Reference[T], val aggregate: MultiSet[T] => U) + extends ReferenceElement[T, U](collection, reference) with ValuesMaker[U] { + + val mvre = new MultiValuedReferenceElement(collection, reference) + private def possibleInputs(depth: Int): ValueSet[MultiSet[T]] = + LazyValues(universe)(mvre, depth) + + def generateValue(): U = aggregate(HashMultiSet(collection.getManyElementsByReference(reference).toList.map(_.value): _*)) + + def makeValues(depth: Int): ValueSet[U] = { + val inputs = possibleInputs(depth) + val resultValues = inputs.regularValues.map(aggregate(_)) + if (inputs.hasStar) withStar(resultValues); else withoutStar(resultValues) + } + +// def makeFactors = { +// val thisVar = Variable(this) +// val mvreVar = Variable(mvre) +// val factor = Factory.make[Double](List(thisVar, mvreVar)) +// for { +// (thisXvalue, thisIndex) <- thisVar.range.zipWithIndex +// (mvreXvalue, mvreIndex) <- mvreVar.range.zipWithIndex +// } { +// if (thisXvalue.isRegular && mvreXvalue.isRegular) factor.set(List(thisIndex, mvreIndex), if (aggregate(mvreXvalue.value) == thisXvalue.value) 1.0; else 0.0) +// } +// // The MultiValuedReferenceElement for this aggregate is generated when values is called. +// // Therefore, it will be included in the expansion and have factors made for it automatically, so we do not create factors for it here. +// List(factor) +// } +} + +/** + * Element representing the values of a reference that can have multiple values. + * + * @param collection The collection to use to resolve the reference. + * @param reference The reference whose value is represented by this element. + * + */ + +class MultiValuedReferenceElement[T](coll: ElementCollection, ref: Reference[T]) extends ReferenceElement[T, MultiSet[T]](coll, ref) + with ValuesMaker[MultiSet[T]] { + + /* + * We need to make sure that if the reference is indirect, each of the reference elements embedded in this element have their values + * determined, and then the same embedded elements are used when making factors. This is achieved by storing the embedded elements + * in a map. + * Note that we could have achieved a similar effect by making MultiValuedReferenceElement a case class, but that would be incorrect + * since with reference uncertainty, the same reference may exist twice in a collection with two different embedded references. + * + * Since the MVRE factor maker uses embedded Inject and Apply elements, we need to make sure they are expanded and have their + * values computed when makeValues is called. Therefore, we store them in embeddedInject and embeddedApply. The key to these maps + * is the list of element collections that represents a possible value of the head of the reference. + */ + val embeddedElements: Map[ElementCollection, MultiValuedReferenceElement[T]] = Map() + val embeddedInject: Map[List[ElementCollection], Element[List[MultiSet[T]]]] = Map() + val embeddedApply: Map[List[ElementCollection], Element[MultiSet[T]]] = Map() + + // collection.getElements is a set of elements, because if the same element is reachable by more than one path, it is only counted once. + // We convert it to a list so we get all the values of these elements, even if some of the elements have the same values. + def generateValue(): MultiSet[T] = { + val referredToElements = collection.getManyElementsByReference(reference).toList + referredToElements.foreach(elem => elem.generateValue(elem.randomness)) + HashMultiSet(referredToElements: _*) map ((e: Element[T]) => e.value) + } + + private def allUnions(setset1: ValueSet[MultiSet[T]], setset2: ValueSet[MultiSet[T]]): ValueSet[MultiSet[T]] = { + val multiSets = for { set1 <- setset1.regularValues; set2 <- setset2.regularValues } yield set1 union set2 + if (setset1.hasStar || setset2.hasStar) withStar(multiSets); else withoutStar(multiSets) + } + + /* + * The value of a MultiValuedReferenceElement is a multiset. + * To find all possible values, we proceed as follows: + * If the reference is simple (i.e., just a name), the element referred to by that name has some set of values. + * Each of those values becomes a singleton multiset in the values of the MVRE. + * We get a value set of multisets for each possible value of the name. + * The final step is to take the value set union of these value sets and return it as the value set for this MVRE. + * + * If the reference is compound, the each of the first name's possible values may either be a single EC or a traversable of ECs. + * If it's just an EC, we get the values of the rest of the reference (which are multisets) + * and those become the possible values of the MVRE associated with this first value. + * If it's a traversable of ECs, we get the possible values of the rest of the reference for each of the ECs in the traversable. + * Each such value is a multiset. + * So, at this point, we have a traversable T of value sets of multisets. We need to convert it into a single values set + * of multisets such that each multiset in the resulting value set is the multiset union of one multiset chosen for + * each of the value sets in T. This is accomplished by allUnions. + * As a result, we get a value set of multisets for each possible value of the name. + * The final step is to take the value set union of these value sets and return it as the value set for this MVRE. + */ + def makeValues(depth: Int): ValueSet[MultiSet[T]] = { + val (first, rest) = collection.getFirst(reference) + rest match { + case None => + val firstValues = LazyValues(universe)(first.asInstanceOf[Element[T]], depth) + firstValues.map((t: T) => HashMultiSet(List(t): _*)) + case Some(restRef) => + val results: Set[ValueSet[MultiSet[T]]] = { + val firstValues: ValueSet[_] = LazyValues(universe)(first, depth) + for { firstXvalue <- firstValues.xvalues } yield { + if (firstXvalue.isRegular) { + firstXvalue.value match { + case firstColl: ElementCollection => + val embedded = new MultiValuedReferenceElement(firstColl, restRef) + embeddedElements += firstColl -> embedded + LazyValues(universe)(embedded, depth - 1) + case ecs: Traversable[_] => + val collections: List[ElementCollection] = ecs.map((x: Any) => x.asInstanceOf[ElementCollection]).toList.distinct + val multis = { + for { + firstColl <- collections // Aggregates use set semantics for the elements they use. If the same element appears more than once, it is only counted once. + } yield { + val restMulti = new MultiValuedReferenceElement(firstColl, restRef) + embeddedElements += firstColl -> restMulti + restMulti + } + } + val combination: Element[List[MultiSet[T]]] = Inject(multis: _*) + LazyValues(universe)(combination, depth - 1) + embeddedInject += collections -> combination + val applyStarter: MultiSet[T] = HashMultiSet[T]() + val setMaker = Apply(combination, (sets: List[MultiSet[T]]) => (applyStarter /: sets)(_ union _)) + LazyValues(universe)(setMaker, depth - 1) + embeddedApply += collections -> setMaker + val resultSets: List[ValueSet[MultiSet[T]]] = + for { + (firstColl, multi) <- collections.zip(multis) // Aggregates use set semantics for the elements they use. If the same element appears more than once, it is only counted once. + } yield { + LazyValues(universe)(multi, depth - 1) + } + /* + * Here, we are creating the value set of multisets in which each multiset is the multiset union of + * multisets in the result sets. Since we are taking the multiset union, we start with the empty + * multiset, which is why the start is the value set containing the single empty multiset. + */ + val starter: ValueSet[MultiSet[T]] = withoutStar(Set(HashMultiSet())) + (starter /: resultSets)(allUnions(_, _)) + } + } else withStar[MultiSet[T]](Set()) + } + } + /* + * Here we are taking the value set union of the value sets produced for each of the possible first values, + * so that starter is the empty value set. + */ + val starter: ValueSet[MultiSet[T]] = withoutStar(Set()) + (starter /: results)(_ ++ _) + } + } + + // def makeFactors: List[Factor[Double]] = { + // val (first, rest) = collection.getFirst(reference) + // val selectionFactors: List[List[Factor[Double]]] = { + // rest match { + // case None => + // val thisVar = Variable(this) + // val refVar = Variable(first) + // val factor = Factory.make[Double](List(thisVar, refVar)) + // for { + // i <- 0 until refVar.range.size + // j <- 0 until refVar.range.size + // } { + // factor.set(List(i, j), (if (i == j) 1.0; else 0.0)) + // } + // List(List(factor)) + // case Some(restRef) => + // val firstVar = Variable(first) + // for { + // (firstXvalue, firstIndex) <- firstVar.range.zipWithIndex + // } yield { + // if (firstXvalue.isRegular) { + // firstXvalue.value match { + // case firstCollection: ElementCollection => + // val restElement = embeddedElements(firstCollection) + // val result: List[Factor[Double]] = + // Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(restElement)) :: Factory.make(restElement) + // result + // case cs: Traversable[_] => + // // Create a multi-valued reference element (MVRE) for each collection in the value of the first name. + // // Since the first name is multi-valued, its value is the union of the values of all these MVREs. + // val collections = cs.asInstanceOf[Traversable[ElementCollection]].toList.distinct // Set semantics + // val multis: List[MultiValuedReferenceElement[T]] = collections.map(embeddedElements(_)).toList + // // Create the element that takes the union of the values of the all the MVREs. + // // The combination and setMaker elements are encapsulated within this object and are created now, so we need to create factors for them. + // // Finally, we create a conditional selector (see ProbFactor) to select the appropriate result value when the first + // // name's value is these MVREs. + // val combination = embeddedInject(collections) + // val setMaker = embeddedApply(collections) + // val result: List[Factor[Double]] = + // Factory.makeConditionalSelector(this, firstVar, firstIndex, Variable(setMaker)) :: Factory.make(combination) ::: + // Factory.make(setMaker) + // result + // } + // } else Factory.makeStarFactor(this) + // } + // } + // } + // selectionFactors.flatten + // } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala b/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala index 45d82be8..f86cec35 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala @@ -1,13 +1,13 @@ /* * Universe.scala * Universes of elements. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -27,7 +27,7 @@ import scala.collection.generic.Shrinkable * * Ordinarily, the arguments of elements in a universe must also belong to the universe. You can * optionally supply a list of parent elements that contains other elements that can be arguments. - * + * * @param parentElements The parent elements on which this universe depends. */ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCollection { @@ -46,7 +46,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol /** Elements in the universe that are not defined in the context of another element. */ def permanentElements: List[Element[_]] = myActiveElements.toList filterNot (_.isTemporary) - private val myConditionedElements: Set[Element[_]] = Set() + private val myConditionedElements: Set[Element[_]] = Set() /** Elements in the universe that have had a condition applied to them. */ def conditionedElements: List[Element[_]] = myConditionedElements.toList @@ -141,7 +141,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol elemGraphBuilder(List[(Element[_], Set[Element[_]])]() :+ (elem, Set[Element[_]]()), myUsedBy, myRecursiveUsedBy) myRecursiveUsedBy.getOrElse(elem, Set()) } - + /** * Returns the set of elements that are directly used by the given element, without recursing. */ @@ -170,8 +170,9 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol private[language] def activate(element: Element[_]): Unit = { if (element.active) throw new IllegalArgumentException("Activating active element") - if (element.args exists (!_.active)) - throw new IllegalArgumentException("Attempting to activate element with inactive argument") +// if (element.args exists (!_.active)) +// throw new IllegalArgumentException("Attempting to activate element with inactive argument") + element.args.filter(!_.active).foreach(activate(_)) myActiveElements.add(element) if (!element.isInstanceOf[Deterministic[_]]) myStochasticElements.add(element) @@ -181,6 +182,8 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol } element.args foreach (registerUses(element, _)) element.active = true +// myRecursiveUsedBy.clear +// myRecursiveUses.clear } private[language] def deactivate(element: Element[_]): Unit = { @@ -198,7 +201,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol element.active = false element.args foreach (deregisterUses(element, _)) - //Make sure that if another element uses this element, that we remove it from the UsedBy + //Make sure that if another element uses this element, that we remove it from the UsedBy if (myUsedBy.contains(element) && !myUsedBy(element).isEmpty) { myUsedBy(element) foreach (deregisterUses(_, element)) } @@ -265,27 +268,27 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol * This avoids memory management problems. */ def register(collection: Shrinkable[Element[_]]): Unit = registeredMaps += collection - + /** Deregister a map of elements. */ def deregister(collection: Shrinkable[Element[_]]): Unit = registeredMaps -= collection // Immediately register the constrained and conditioned elements register(myConditionedElements) register(myConstrainedElements) - + /** * Register the maps that this universe is used as a key. - * Needed to make sure Universe is garbage collected when cleared and dereferenced + * Needed to make sure Universe is garbage collected when cleared and dereferenced. */ def registerUniverse(map: Map[Universe, _]): Unit = registeredUniverseMaps += map /** Deregister a map that uses this universe as a key. */ def deregisterUniverse(map: Map[Universe, _]): Unit = registeredUniverseMaps -= map - + /** * Register algorithms that use this universe. * When the Universe is cleared, all previous algorithms are no longer valid, - * so they must be killed (if still running) + * so they must be killed (if still running). */ def registerAlgorithm(alg: Algorithm): Unit = registeredAlgorithms += alg @@ -322,7 +325,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol /* * Recursively (depth-first) builds a usedBy/uses graph and will re-use previously cached paths - * base is a data structure that just contains the direct edges. existing contains + * base is a data structure that just contains the direct edges. existing contains * any previous recursive paths that have already been cached. */ @tailrec @@ -361,10 +364,9 @@ object Universe { } } - //This could go in Evidence instead of here. object AssertEvidence { - + /** * Assert the given evidence associated with references to elements in the collection. */ @@ -375,7 +377,7 @@ object AssertEvidence { def apply[T](evidence: NamedEvidence[T]): Unit = { apply(evidence.reference, evidence.evidence) } - + /** * Assert the given evidence on the given reference. The third argument is an optional contingency. * This method makes sure to assert the evidence on all possible resolutions of the reference. diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/InverseGamma.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/InverseGamma.scala new file mode 100644 index 00000000..bb960242 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/InverseGamma.scala @@ -0,0 +1,70 @@ +/* + * InverseGamma.scala + * Class for a Gamma distribution in which both the k and theta parameters are constants + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Dec 4, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.atomic.continuous + +import com.cra.figaro.language.Atomic +import com.cra.figaro.language.ElementCollection +import com.cra.figaro.language.Element +import com.cra.figaro.language.Name +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import annotation.tailrec +import scala.math.{ exp, log, pow } +import JSci.maths.SpecialMath.{ gamma, logGamma } + +/** + * A Gamma distribution in which both the k and theta parameters are constants. + * Theta defaults to 1. + */ +class AtomicInverseGamma(name: Name[Double], shape: Double, scale: Double = 1.0, collection: ElementCollection) + extends Element[Double](name, collection) with Atomic[Double] { + + + + + type Randomness = Double + + def generateRandomness() = Util.generateGamma(shape) + + def generateValue(rand: Randomness) = 1.0 / (rand * scale) // due to scaling property of Gamma + + /** + * The normalizing factor. + */ + private val normalizer = pow(scale, shape) / (gamma(shape)) + + /** + * Density of a value. + */ + def density(x: Double) = { + if (x < 0.0) 0.0 else { + //Convert to logarithms if this is too large. + val numer = pow(x, -1.0 * shape - 1) * exp(-1.0 * scale / x) + numer * normalizer + } + } + + override def toString = + if (scale == 1.0) "InverseGamma(" + shape + ")" + else "InverseGamma(" + shape + ", " + scale + ")" +} + +object InverseGamma { + /** + * Create an InverseGamma element. + */ + def apply(shape: Double, scale: Double)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicInverseGamma(name, shape, scale, collection) + +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala index 3c898b1c..57d3f981 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala @@ -1,6 +1,5 @@ /* * MultivariateNormal.scala - * * Elements representing a multivariate normal distributions * * Created By: Glenn Takata (gtakata@cra.com) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Normal.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Normal.scala index 45b4ed8b..34179007 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Normal.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Normal.scala @@ -66,6 +66,24 @@ class NormalCompoundMean(name: Name[Double], val mean: Element[Double], val vari override def toString = "Normal(" + mean + ", " + variance + ")" } + +/** + * A normal distribution in which the mean is constant and the variance is an element. + */ +class NormalCompoundVariance(name: Name[Double], val mean: Double, val variance: Element[Double], collection: ElementCollection) + extends NonCachingChain( + name, + variance, + (v: Double) => new AtomicNormal("", mean, v, collection), + collection) + with Normal { + + def varianceValue = variance.value + lazy val meanValue = mean + + override def toString = "Normal(" + mean + ", " + variance + ")" +} + /** * A normal distribution in which the mean and variance are both elements. */ @@ -127,8 +145,15 @@ object Normal extends Creatable { def apply(mean: Element[Double], variance: Double)(implicit name: Name[Double], collection: ElementCollection) = new NormalCompoundMean(name, mean, variance, collection) + /** + * Create a normal distribution in which the mean is an constant and the variance is an element. + */ + def apply(mean: Double, variance: Element[Double])(implicit name: Name[Double], collection: ElementCollection) = + new NormalCompoundVariance(name, mean, variance, collection) + + /** - * Create a normal distribution in both the mean and the variance are constants. + * Create a normal distribution in both the mean and the variance are elements. */ def apply(mean: Element[Double], variance: Element[Double])(implicit name: Name[Double], collection: ElementCollection) = new CompoundNormal(name, mean, variance, collection) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala index 9e85e031..5351e060 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala @@ -1,165 +1,165 @@ -/* - * Binomial.scala - * Elements representing binomial distributions. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Feb 25, 2011 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.library.atomic.discrete - -import com.cra.figaro.algorithm.ValuesMaker -import com.cra.figaro.algorithm.lazyfactored.ValueSet -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.language._ -import com.cra.figaro.library.atomic.continuous._ -import annotation.tailrec - -/** - * A binomial distribution in which the parameters are constants. - */ -class AtomicBinomial(name: Name[Int], val numTrials: Int, val probSuccess: Double, collection: ElementCollection) - extends Element[Int](name, collection) with Atomic[Int] with ValuesMaker[Int] with ProbFactorMaker with Cacheable[Int] - with OneShifter { - protected lazy val lowerBound = 0 - protected lazy val upperBound = numTrials - - private lazy val q = 1 - probSuccess - - // Devroye, p. 525 - @tailrec - private def generateHelper(x: Int, sum: Int): Int = { - val g = Util.generateGeometric(1 - probSuccess) - val newSum = sum + g - val newX = x + 1 - if (newSum <= numTrials) generateHelper(newX, newSum) - else newX - } - - def generateRandomness() = if (probSuccess <= 0) 0; else if (probSuccess < 1) generateHelper(-1, 0); else numTrials - - /** - * The Metropolis-Hastings proposal is to increase or decrease the value of by 1. - */ - override def nextRandomness(rand: Randomness) = shiftOne(rand) - - def generateValue(rand: Int) = rand - - /** - * Probability of a value. - */ - def density(k: Int) = { - if (k < 0 || k > numTrials) 0.0 else - Util.binomialDensity(numTrials, probSuccess, k) - } - - /** - * Return the range of values of the element. - */ - def makeValues(depth: Int) = ValueSet.withoutStar((for { i <- 0 to numTrials } yield i).toSet) - - /** - * Convert an element into a list of factors. - */ - def makeFactors = { - val binVar = Variable(this) - val factor = Factory.make[Double](List(binVar)) - for { (xvalue, index) <- binVar.range.zipWithIndex } { - factor.set(List(index), density(xvalue.value)) - } - List(factor) - } - - override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" -} - -/** - * A binomial distribution in which the number of trials is fixed and the success probability is an element. - */ -class BinomialFixedNumTrials(name: Name[Int], val numTrials: Int, val probSuccess: Element[Double], collection: ElementCollection) - extends NonCachingChain[Double, Int](name, probSuccess, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) { - override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" -} - - /** - * A binomial with a fixed number of trials parameterized by a beta distribution. - */ -class ParameterizedBinomialFixedNumTrials(name: Name[Int], val numTrials: Int, override val parameter: AtomicBeta, collection: ElementCollection) - extends CachingChain[Double, Int](name, parameter, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) - with SingleParameterized[Int] { - - - override def distributionToStatistics(distribution: Stream[(Double, Int)]): Seq[Double] = { - val distList = distribution.toList - var totalPos = 0.0 - var totalNeg = 0.0 - for { i <- 0 to numTrials } { - distList.find(_._2 == i) match { - case Some((prob, _)) => - totalPos += prob * i - totalNeg += prob * (numTrials - i) - case None => () - } - } - List(totalPos, totalNeg) - } - - def density(value: Int): Double = { - val probSuccess = parameter.value - if (value < 0 || value > numTrials) 0.0 - else Util.binomialDensity(numTrials, probSuccess, value) - } - - override def toString = "ParameterizedBinomial(" + numTrials + ", " + parameter + ")" -} - -/** - * A binomial distribution in which the parameters are elements. - */ -class CompoundBinomial(name: Name[Int], val numTrials: Element[Int], val probSuccess: Element[Double], collection: ElementCollection) - extends CachingChain[Int, Int]( - name, - numTrials, - (n: Int) => new NonCachingChain( - "", - probSuccess, - (p: Double) => new AtomicBinomial("", n, p, collection), - collection), - collection) { - override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" -} - -object Binomial extends Creatable { - /** - * Create a binomial distribution in which the parameters are constants. - */ - def apply(n: Int, p: Double)(implicit name: Name[Int], collection: ElementCollection) = - new AtomicBinomial(name, n, p, collection) - - /** - * Create a binomial distribution in which the number of trials is fixed and the success probability is an element. - * - * If the element is an atomic beta element, the flip uses that element - * as a learnable parameter. - */ - def apply(n: Int, p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = { - if (p.isInstanceOf[AtomicBeta]) - new ParameterizedBinomialFixedNumTrials(name, n, p.asInstanceOf[AtomicBeta], collection) - else new BinomialFixedNumTrials(name, n, p, collection) - } - - /** - * Create a binomial distribution in which the parameters are elements. - */ - def apply(n: Element[Int], p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = - new CompoundBinomial(name, n, p, collection) - - type ResultType = Int - - def create(args: List[Element[_]]) = apply(args(0).asInstanceOf[Element[Int]], args(1).asInstanceOf[Element[Double]]) -} +/* + * Binomial.scala + * Elements representing binomial distributions. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Feb 25, 2011 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.atomic.discrete + +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored.ValueSet +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous._ +import annotation.tailrec + +/** + * A binomial distribution in which the parameters are constants. + */ +class AtomicBinomial(name: Name[Int], val numTrials: Int, val probSuccess: Double, collection: ElementCollection) + extends Element[Int](name, collection) with Atomic[Int] with ValuesMaker[Int] with Cacheable[Int] + with OneShifter { + protected lazy val lowerBound = 0 + protected lazy val upperBound = numTrials + + private lazy val q = 1 - probSuccess + + // Devroye, p. 525 + @tailrec + private def generateHelper(x: Int, sum: Int): Int = { + val g = Util.generateGeometric(1 - probSuccess) + val newSum = sum + g + val newX = x + 1 + if (newSum <= numTrials) generateHelper(newX, newSum) + else newX + } + + def generateRandomness() = if (probSuccess <= 0) 0; else if (probSuccess < 1) generateHelper(-1, 0); else numTrials + + /** + * The Metropolis-Hastings proposal is to increase or decrease the value of by 1. + */ + override def nextRandomness(rand: Randomness) = shiftOne(rand) + + def generateValue(rand: Int) = rand + + /** + * Probability of a value. + */ + def density(k: Int) = { + if (k < 0 || k > numTrials) 0.0 else + Util.binomialDensity(numTrials, probSuccess, k) + } + + /** + * Return the range of values of the element. + */ + def makeValues(depth: Int) = ValueSet.withoutStar((for { i <- 0 to numTrials } yield i).toSet) + + /** + * Convert an element into a list of factors. + */ +// def makeFactors = { +// val binVar = Variable(this) +// val factor = Factory.make[Double](List(binVar)) +// for { (xvalue, index) <- binVar.range.zipWithIndex } { +// factor.set(List(index), density(xvalue.value)) +// } +// List(factor) +// } + + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" +} + +/** + * A binomial distribution in which the number of trials is fixed and the success probability is an element. + */ +class BinomialFixedNumTrials(name: Name[Int], val numTrials: Int, val probSuccess: Element[Double], collection: ElementCollection) + extends NonCachingChain[Double, Int](name, probSuccess, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) { + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" +} + + /** + * A binomial with a fixed number of trials parameterized by a beta distribution. + */ +class ParameterizedBinomialFixedNumTrials(name: Name[Int], val numTrials: Int, override val parameter: AtomicBeta, collection: ElementCollection) + extends CachingChain[Double, Int](name, parameter, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) + with SingleParameterized[Int] { + + + override def distributionToStatistics(distribution: Stream[(Double, Int)]): Seq[Double] = { + val distList = distribution.toList + var totalPos = 0.0 + var totalNeg = 0.0 + for { i <- 0 to numTrials } { + distList.find(_._2 == i) match { + case Some((prob, _)) => + totalPos += prob * i + totalNeg += prob * (numTrials - i) + case None => () + } + } + List(totalPos, totalNeg) + } + + def density(value: Int): Double = { + val probSuccess = parameter.value + if (value < 0 || value > numTrials) 0.0 + else Util.binomialDensity(numTrials, probSuccess, value) + } + + override def toString = "ParameterizedBinomial(" + numTrials + ", " + parameter + ")" +} + +/** + * A binomial distribution in which the parameters are elements. + */ +class CompoundBinomial(name: Name[Int], val numTrials: Element[Int], val probSuccess: Element[Double], collection: ElementCollection) + extends CachingChain[Int, Int]( + name, + numTrials, + (n: Int) => new NonCachingChain( + "", + probSuccess, + (p: Double) => new AtomicBinomial("", n, p, collection), + collection), + collection) { + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" +} + +object Binomial extends Creatable { + /** + * Create a binomial distribution in which the parameters are constants. + */ + def apply(n: Int, p: Double)(implicit name: Name[Int], collection: ElementCollection) = + new AtomicBinomial(name, n, p, collection) + + /** + * Create a binomial distribution in which the number of trials is fixed and the success probability is an element. + * + * If the element is an atomic beta element, the flip uses that element + * as a learnable parameter. + */ + def apply(n: Int, p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = { + if (p.isInstanceOf[AtomicBeta]) + new ParameterizedBinomialFixedNumTrials(name, n, p.asInstanceOf[AtomicBeta], collection) + else new BinomialFixedNumTrials(name, n, p, collection) + } + + /** + * Create a binomial distribution in which the parameters are elements. + */ + def apply(n: Element[Int], p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = + new CompoundBinomial(name, n, p, collection) + + type ResultType = Int + + def create(args: List[Element[_]]) = apply(args(0).asInstanceOf[Element[Int]], args(1).asInstanceOf[Element[Double]]) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/Container.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/Container.scala new file mode 100644 index 00000000..8062c826 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/Container.scala @@ -0,0 +1,195 @@ +/* + * Container.scala + * Trait for a Process with a defined sequence of indices + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ +import scala.language.implicitConversions +import com.cra.figaro.library.compound.{FoldLeft, FoldRight, Reduce} +import com.cra.figaro.algorithm.{Values, ValuesMaker} +import com.cra.figaro.algorithm.factored.factors.FactorMaker +import com.cra.figaro.algorithm.factored.factors.Variable +import com.cra.figaro.algorithm.factored.factors.Factor +import com.cra.figaro.algorithm.lazyfactored.ValueSet +import com.cra.figaro.library.atomic.discrete.FromRange + +/** + * A Container is a Process with a defined sequence of indices. + */ +trait Container[Index, Value] +extends Process[Index, Value] { + val indices: Seq[Index] + + def rangeCheck(index: Index) = indices.contains(index) + + /** + * Return all the elements in this container as an ordinary Scala Seq. + * This method caches the elements, like apply, so the same elements will be returned every time. + */ + def elements: Seq[Element[Value]] = indices.map(generateCached(_)) + + /** + * Convert this container into an ordinary Scala map. + */ + def toMap: Map[Index, Element[Value]] = + Map(indices.map(i => i -> apply(i)):_*) + + /** + * Apply the given function to every value in this container, returning a new container. + */ + override def map[Value2](f: Value => Value2): Container[Index, Value2] = { + val thisContainer = this + new Container[Index, Value2] { + val indices = thisContainer.indices + def generate(i: Index) = { + val elem1 = thisContainer(i) + Apply(elem1, f)("", elem1.universe) + } + def generate(indices: List[Index]) = + thisContainer.generate(indices).mapValues((e: Element[Value]) => Apply(e, f)("", e.universe)) + } + } + + /** + * Chain every value in this container through the given function, returning a new container. + */ + override def chain[Value2](f: Value => Element[Value2]): Container[Index, Value2] = { + val thisContainer = this + new Container[Index, Value2] { + val indices = thisContainer.indices + def generate(i: Index) = { + val elem1 = thisContainer(i) + Chain(elem1, f)("", elem1.universe) + } + def generate(indices: List[Index]) = + thisContainer.generate(indices).mapValues((e: Element[Value]) => Chain(e, f)("", e.universe)) + } + } + + /** + * Fold the values in this container through the given function. + */ + def foldLeft[Value2](start: Value2)(f: (Value2, Value) => Value2)(implicit name: Name[Value2], collection: ElementCollection): Element[Value2] = { + val myArgs = indices.map(apply(_)).toList +// myArgs.foreach { arg => if (!arg.active) arg.activate } + FoldLeft(start, f)(myArgs:_*)(name, collection) + } + + /** + * Fold the values in this container through the given function. + */ + def foldRight[Value2](start: Value2)(f: (Value, Value2) => Value2)(implicit name: Name[Value2], collection: ElementCollection): Element[Value2] = { + val myArgs = indices.map(apply(_)).toList +// myArgs.foreach { arg => if (!arg.active) arg.activate } + FoldRight(start, f)(myArgs:_*)(name, collection) + } + + /** + * Reduce the values in this container through the given function. + */ + def reduce(f: (Value, Value) => Value)(implicit name: Name[Value], collection: ElementCollection): Element[Value] = { + val myArgs = indices.map(apply(_)).toList +// myArgs.foreach { arg => if (!arg.active) arg.activate } + Reduce(f)(myArgs:_*)(name, collection) + } + + /** + * Aggregate the results of applying an operator to each element. + */ + def aggregate[Value2](start: => Value2)(seqop: (Value2, Value) => Value2, combop: (Value2, Value2) => Value2) + (implicit name: Name[Value2], collection: ElementCollection): Element[Value2] = { + foldLeft(start)((v1: Value2, v2: Value) => combop(v1, seqop(v1, v2)))(name, collection) + } + + /** + * Returns an element representing the number of elements in the container whose values satisfy the predicate. + */ + def count(f: (Value) => Boolean)(implicit name: Name[Int], collection: ElementCollection): Element[Int] = { + foldLeft(0)((i: Int, v: Value) => if (f(v)) i + 1 else i)(name, collection) + } + + /** + * Returns an element representing whether the value of any element in the container satisfies the predicate. + */ + def exists(pred: Value => Boolean)(implicit name: Name[Boolean], collection: ElementCollection): Element[Boolean] = { + foldLeft(false)((b: Boolean, v: Value) => pred(v) || b)(name, collection) + } + + /** + * Returns an element representing whether the values of all elements in the container satisfy the predicate. + */ + def forall(pred: Value => Boolean)(implicit name: Name[Boolean], collection: ElementCollection): Element[Boolean] = { + foldLeft(true)((b: Boolean, v: Value) => pred(v) && b)(name, collection) + } + + /** + * Returns an element representing the optional index of the first element in the container whose value satisfies the predicate. + * The result has value None if no element is found. + */ + def findIndex(pred: Value => Boolean)(implicit name: Name[Option[Index]], collection: ElementCollection): Element[Option[Index]] = { + val argMap = indices.map(arg => (arg, apply(arg))).toMap + def step(oi: Option[Index], index: Index) = oi match { + case Some(_) => oi + case None => if (pred(argMap(index).value)) Some(index) else None + } + val myArgs = argMap.values.toList +// myArgs.foreach { arg => if (!arg.active) arg.activate } + new Deterministic[Option[Index]](name, collection) { + def args = myArgs + def generateValue(): Option[Index] = { + indices.foldLeft(None: Option[Index])(step) + } + } + } + + /** + * Returns a new container containing the elements of this container and the argument. + * If an index is defined in both container, the element of the argument is used. + */ + def ++(that: Container[Index, Value]): Container[Index, Value] = { + val thisContainer = this + new Container[Index, Value] { + val indices = { + val fromThis = thisContainer.indices.filterNot(that.indices.contains(_)) + fromThis ++ that.indices + } + def generate(i: Index) = + if (that.rangeCheck(i)) that.generate(i); else thisContainer.generate(i) + def generate(indices: List[Index]) = { + val (fromThatIndices, fromThisIndices) = indices.partition(that.rangeCheck(_)) + val fromThis = thisContainer.generate(fromThisIndices) + val fromThat = that.generate(indices) + fromThis ++ fromThat + } + } + } + + /** + * Choose a random element from this container. + */ + def randomElement() = { + val selector = FromRange(0, indices.length) + Chain(selector, (i: Int) => apply(indices(i))) + } +} + object Container { + def apply[T](elements: Element[T]*): Container[Int, T] = { + new FixedSizeArray(elements.size, (i: Int) => elements(i)) + } + + def apply[T](numItems: Element[Int], generator: Int => Element[T])(implicit name: Name[FixedSizeArray[T]], collection: ElementCollection): FixedSizeArrayElement[T] = { + VariableSizeArray(numItems, generator)(name, collection) + } + + implicit def toContainerElement[I, T](container: Container[I, T]): ContainerElement[I, T] = new ContainerElement(Constant(container)) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/ContainerElement.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/ContainerElement.scala new file mode 100644 index 00000000..d8d6d39f --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/ContainerElement.scala @@ -0,0 +1,124 @@ +/* + * ContainerElement.scala + * Class for a Container element + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ + +/** + * Represents an element whose value is a container. + * + * Elements that are created by operations are put in the same universe as this element. + */ +class ContainerElement[Index, Value](val element: Element[Container[Index, Value]]) +{ + /** + * Creates an element whose value is the value at the corresponding index of the value of the process element. + */ + def apply(i: Index): Element[Value] = { + if (!element.active) element.activate() + CachingChain(element, (c: Container[Index, Value]) => c(i))("", element.universe) + } + + /** + * Creates an element whose value is the value at the corresponding index in the value of the container element, + * if the index is in range, None otherwise. + */ + def get(i: Index): Element[Option[Value]] = { + if (!element.active) element.activate() + CachingChain(element, (c: Container[Index, Value]) => c.get(i))("", element.universe) + } + + /** + * Map the given function pointwise through the value of the container element. + */ + def map[Value2](f: Value => Value2): ContainerElement[Index, Value2] = { + if (!element.active) element.activate() + new ContainerElement(Apply(element, (c: Container[Index, Value]) => c.map(f))("", element.universe)) + } + + /** + * Chain the given function pointwise through the value of the container element. + */ + def chain[Value2](f: Value => Element[Value2]): ContainerElement[Index, Value2] = { + if (!element.active) element.activate() + new ContainerElement(Apply(element, (c: Container[Index, Value]) => c.chain(f))("", element.universe)) + } + + /** + * Produce the element over values obtained by selecting a particular container and folding through its values. + */ + def foldLeft[Value2](start: Value2)(f: (Value2, Value) => Value2): Element[Value2] = { + if (!element.active) element.activate() + CachingChain(element, (c: Container[Index, Value]) => c.foldLeft(start)(f))("", element.universe) + } + + /** + * Produce the element over values obtained by selecting a particular container and folding through its values. + */ + def foldRight[Value2](start: Value2)(f: (Value, Value2) => Value2): Element[Value2] = { + if (!element.active) element.activate() + CachingChain(element, (c: Container[Index, Value]) => c.foldRight(start)(f))("", element.universe) + } + + /** + * Produce the element over values obtained by selecting a particular container and reducing through its values. + */ + def reduce(f: (Value, Value) => Value): Element[Value] = { + if (!element.active) element.activate() + CachingChain(element, (c: Container[Index, Value]) => c.reduce(f))("", element.universe) + } + + /** + * Aggregate the results of applying an operator to each element. + */ + def aggregate[Value2](start: => Value2)(seqop: (Value2, Value) => Value2, combop: (Value2, Value2) => Value2): Element[Value2] = { + if (!element.active) element.activate() + foldLeft(start)((v1: Value2, v2: Value) => combop(v1, seqop(v1, v2))) + } + + /** + * Returns an element representing the number of elements in the container whose values satisfy the predicate. + */ + def count(f: (Value) => Boolean): Element[Int] = { + foldLeft(0)((i: Int, v: Value) => if (f(v)) i + 1 else i) + } + + /** + * Returns an element representing whether the value of any element in the container satisfies the predicate. + */ + def exists(pred: Value => Boolean): Element[Boolean] = { + foldLeft(false)((b: Boolean, v: Value) => pred(v) || b) + } + + /** + * Returns an element representing whether the values of all elements in the container satisfy the predicate. + */ + def forall(pred: Value => Boolean): Element[Boolean] = { + foldLeft(true)((b: Boolean, v: Value) => pred(v) && b) + } + + /** + * Returns an element representing the length of the container. + */ + def length: Element[Int] = { + foldLeft(0)((i: Int, v: Value) => i + 1) + } + + /** + * Select a random element from the container. Ensures that no IndexOutOfRange exception should be thrown. + */ + def randomElement(): Element[Value] = { + element.flatMap(_.randomElement) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/FixedSizeArray.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/FixedSizeArray.scala new file mode 100644 index 00000000..36aaac01 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/FixedSizeArray.scala @@ -0,0 +1,49 @@ +/* + * FixedSizeArray.scala + * Class for a fixed size array + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ + +/** + * A Figaro collection representing a fixed number of elements. The indices into the collection are the integers + * from 0 to the size - 1. + * + * @param size the number of elements in the collection + * @param generator a function to generate the elements in the collection, given the index + */ +class FixedSizeArray[Value]( + val size: Int, + val generator: Int => Element[Value] +) extends Container[Int, Value] { + val indices = 0 until size + + override def rangeCheck(index: Int) = index >= 0 && index < size + + def generate(index: Int) = generator(index) + + def generate(indices: List[Int]) = { + Map(indices.map(index => (index, generator(index))):_*) + } + + /** + * Concatenate this container with another one. In the result, all the elements in this container will precede all + * the elements in the other container. The result is an array containing all the elements of both containers. + */ + def concat[Index2](that: FixedSizeArray[Value]) = { + new FixedSizeArray( + this.indices.size + that.indices.size, + (i: Int) => if (i < indices.size) this(i) else that(i - indices.size) + ) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/IncrementalProcess.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/IncrementalProcess.scala new file mode 100644 index 00000000..b8df7764 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/IncrementalProcess.scala @@ -0,0 +1,35 @@ +/* + * IncrementalProcess.scala + * Trait for an incremental process + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ + +/** + * A process in which you can incrementally get the elements at a set of indices, while producing a new process. + * The new process should take into account any dependencies with elements that have already been generated. + */ +trait IncrementalProcess[Index, Value] extends Process[Index, Value] { + /** + * Produce the elements representing the value of the process at the given indices. + * Ensures that any dependencies between the elements are represented. + * Also return a new container such that when elements for future indices are produced, + * any dependencies between those elements and the ones for these indices are represented. + * + * The first return value maps each provided index to the corresponding element. + * The second return values is the new container. + * + * This method does not assume that the indices have already been range checked. + */ + protected def generateIncremental(indices: List[Index]): (Map[Index, Element[Value]], Process[Index, Value]) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/IndependentProcess.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/IndependentProcess.scala new file mode 100644 index 00000000..a4078399 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/IndependentProcess.scala @@ -0,0 +1,29 @@ +/* + * IncrementalProcess.scala + * Trait for an independent process + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ + +/** + * A process in which all elements are independent. + */ +trait IndependentProcess[Index, Value] extends Process[Index, Value] { + val generator: Index => Element[Value] + + def generate(index: Index) = generator(index) + + def generate(indices: List[Index]) = { + Map(indices.map(index => (index, generator(index))):_*) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/Process.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/Process.scala similarity index 61% rename from Figaro/src/main/scala/com/cra/figaro/library/process/Process.scala rename to Figaro/src/main/scala/com/cra/figaro/library/collection/Process.scala index e53930a4..7f2302a5 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/Process.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/Process.scala @@ -1,16 +1,31 @@ -package com.cra.figaro.library.process +/* + * Process.scala + * Trait to map indices to elements over values + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection import com.cra.figaro.language._ import com.cra.figaro.util.memo - /** * A Process maps indices to elements over values. + * + * Elements, of course, have a universe. Wherever possible, operations on processes, such as getting elements, or mapping or chaining through + * a function, produce an element in the same universe as the original element. + * An exception is an element over an optional value created for an index out of range, which goes in the default universe, + * because there is no source element. + * Another exception is a fold operation on a container, which is given a universe (defaults to the current Universe.universe). */ trait Process[Index, Value] { - val name: Name[_] = "" - val collection: ElementCollection = Universe.universe - /** * Thrown if the index does not have an element. */ @@ -20,7 +35,7 @@ trait Process[Index, Value] { * Check whether the given index has an element. */ def rangeCheck(index: Index): Boolean - + /** * Produce the elements representing the value of the process at the given indices. * Ensures that any dependencies between the elements are represented. @@ -28,23 +43,31 @@ trait Process[Index, Value] { * The return value maps each provided index to the corresponding element. * This method can assume that the indices has already been range checked. */ - def generate(indices: List[Index]): Map[Index, Element[Value]] + def generate(indices: List[Index]): Map[Index, Element[Value]] /** * Produce the element corresponding to the value of the process at the given index. * This method can assume that the indices has already been range checked. */ def generate(index: Index): Element[Value] - + + private val cachedElements = scala.collection.mutable.Map[Index, Element[Value]]() + + private[collection] def generateCached(index: Index): Element[Value] = { + cachedElements.getOrElseUpdate(index, generate(index)) + } + /** * Get an element representing the value of the process at the given index. * Throws IndexOutOfRangeException if the index has no value. + * + * This apply method is cached, so calling process(index) always returns the same element. */ def apply(index: Index): Element[Value] = { if (!rangeCheck(index)) throw IndexOutOfRangeException(index) - generate(index) + generateCached(index) } - + /** * Get the elements representing the value of the process at the given indices. * Throws IndexOutOfRangeException if any index has no value. @@ -55,72 +78,76 @@ trait Process[Index, Value] { } generate(indices.toList) } - + /** - * Safely get an element over an optional value at the index. + * Safely get an element over an optional value at the index. * If the index is in range, the value of the element will be Some(something). * If the index is out of range, the value of the element will be None. */ def get(index: Index): Element[Option[Value]] = { try { - Apply(apply(index), (v: Value) => Some(v)) + val elem = apply(index) + new Apply1("", apply(index), (v: Value) => Some(v), elem.universe) } catch { case _: IndexOutOfRangeException => Constant(None) } } - + /** * Safely get the elements over optional values at all of the indices. * Any index that is not in range will always have value None. * Dependencies between elements for indices in range will be produced. */ def get(indices: Traversable[Index]): Map[Index, Element[Option[Value]]] = { - val (inRange, outOfRange) = indices.partition(rangeCheck(_)) + val (inRange, outOfRange) = indices.partition(rangeCheck(_)) val inRangeElems: Map[Index, Element[Value]] = generate(inRange.toList) - val inRangeOpts: Map[Index, Element[Option[Value]]] = - inRangeElems.mapValues(elem => { - val optElem: Element[Option[Value]] = Apply(elem, (v: Value) => Some(v)) - optElem - }) - val pairs: Seq[(Index, Element[Option[Value]])] = { + val inRangeOptPairs: List[(Index, Element[Option[Value]])] = + for { (index, elem) <- inRangeElems.toList } yield { + val elem2: Element[Option[Value]] = new Apply1("", elem, (v: Value) => Some(v), elem.universe) + (index, elem2) + } + + val outOfRangeOptPairs: List[(Index, Element[Option[Value]])] = { for { i <- outOfRange.toList } yield { - val elem: Element[Option[Value]] = Constant(None) + val elem: Element[Option[Value]] = new Constant("", None, Universe.universe) (i, elem) } } - inRangeOpts ++ Map(pairs:_*) + Map((inRangeOptPairs ::: outOfRangeOptPairs):_*) } - + /** * Apply the given function to every value in this process, returning a new process. */ def map[Value2](f: Value => Value2): Process[Index, Value2] = { - val thisProcess = this + val thisProcess = this new Process[Index, Value2] { - override val name: Name[_] = thisProcess.name + ".map" - override val collection = thisProcess.collection - def generate(i: Index) = thisProcess(i).map(f) + def generate(i: Index) = { + val elem1 = thisProcess(i) + Apply(elem1, f)("", elem1.universe) + } def generate(indices: List[Index]) = - thisProcess.generate(indices).mapValues(_.map(f)) + thisProcess.generate(indices).mapValues((e: Element[Value]) => Apply(e, f)("", e.universe)) def rangeCheck(i: Index) = thisProcess.rangeCheck(i) } } - + /** * Chain every value in this process through the given function, returning a new process. */ def chain[Value2](f: Value => Element[Value2]): Process[Index, Value2] = { - val thisProcess = this + val thisProcess = this new Process[Index, Value2] { - override val name: Name[_] = thisProcess.name + ".map" - override val collection = thisProcess.collection - def generate(i: Index) = thisProcess(i).flatMap(f) + def generate(i: Index) = { + val elem1 = thisProcess(i) + Chain(elem1, f)("", elem1.universe) + } def generate(indices: List[Index]) = - thisProcess.generate(indices).mapValues(_.flatMap(f)) + thisProcess.generate(indices).mapValues((e: Element[Value]) => Chain(e, f)("", e.universe)) def rangeCheck(i: Index) = thisProcess.rangeCheck(i) } } - + /** * Returns a new process containing the elements of this process and the argument. * If an index is defined in both processes, the element of the argument is used. @@ -128,9 +155,7 @@ trait Process[Index, Value] { def ++(that: Process[Index, Value]): Process[Index, Value] = { val thisProcess = this new Process[Index, Value] { - override val name: Name[_] = thisProcess.name + "++" + that.name - override val collection = that.collection - def generate(i: Index) = + def generate(i: Index) = if (that.rangeCheck(i)) that.generate(i); else thisProcess.generate(i) def generate(indices: List[Index]) = { val (fromThatIndices, fromThisIndices) = indices.partition(that.rangeCheck(_)) @@ -141,4 +166,4 @@ trait Process[Index, Value] { def rangeCheck(i: Index) = that.rangeCheck(i) || thisProcess.rangeCheck(i) } } -} \ No newline at end of file +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/ProcessElement.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/ProcessElement.scala new file mode 100644 index 00000000..15ea0977 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/ProcessElement.scala @@ -0,0 +1,54 @@ +/* + * ProcessElement.scala + * Class for an element whose value is a process + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ + +/** + * Represents an element whose value is a process. + * + * Elements that are created by operations are put in the same universe as this element. + */ +class ProcessElement[Index, Value](val element: Element[Process[Index, Value]]) { + /** + * Creates an element whose value is the value at the corresponding index of the value of the process element. + */ + def apply(i: Index): Element[Value] = { + Chain(element, (p: Process[Index, Value]) => p(i))("", element.universe) + } + + /** + * Safely creates an element whose value is the optional value at the corresponding index of the value + * of the process element. If the value of the process element does not have the corresponding index, the value + * of this element is None. + */ + def get(i: Index): Element[Option[Value]] = { + Chain(element, (p: Process[Index, Value]) => p.get(i))("", element.universe) + } + + /** + * Map the given function pointwise through the value of the process element. + */ + def map[Value2](f: Value => Value2): ProcessElement[Index, Value2] = { + new ProcessElement(Apply(element, (p: Process[Index, Value]) => p.map(f))("", element.universe)) + } + + /** + * Chain the given function pointwise through the value of the process element. + */ + def chain[Value2](f: Value => Element[Value2]): ProcessElement[Index, Value2] = { + new ProcessElement(Apply(element, (p: Process[Index, Value]) => p.chain(f))("", element.universe)) + } +} + diff --git a/Figaro/src/main/scala/com/cra/figaro/library/collection/VariableSizeArray.scala b/Figaro/src/main/scala/com/cra/figaro/library/collection/VariableSizeArray.scala new file mode 100644 index 00000000..1aebab8e --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/collection/VariableSizeArray.scala @@ -0,0 +1,121 @@ +/* + * VariableSizeArray.scala + * Class for a variable size array + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 14, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.collection + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.algorithm.factored._ + +/** + * Holder for an element whose value is a fixed size array. + */ +class FixedSizeArrayElement[Value](private val fsa: Element[FixedSizeArray[Value]]) +extends ContainerElement[Int, Value](fsa.asInstanceOf[Element[Container[Int, Value]]]) { + + /** + * Concatenate the value of this fixed size array element with the value of another fixed size array element. + * This method produces an element whose possible values are all the possible concatenations of this with that. + */ + def concat(that: FixedSizeArrayElement[Value]): FixedSizeArrayElement[Value] = { + val resultElem: Element[FixedSizeArray[Value]] = + new Apply2("", this.fsa, that.fsa, (c1: FixedSizeArray[Value], c2: FixedSizeArray[Value]) => c1.concat(c2), this.element.universe) + new FixedSizeArrayElement(resultElem) + } +} + +/** + * An element representing a random process that generates an array of a variable number of items, in which each item is generated by an + * element defined by a given function. MakeArray uses two streams: a stream of items in a potentially infinite array, and a stream of + * FixedSizeArray prefixes of this stream of increasing lengths. The value of the MakeArray element is one of these FixedSizeArrays, depending + * on the value of numItems. + * + * @param numItems element representing the number of items in the array + * @param itemMaker a function that creates the element for generating each item, given the index into the array + */ +class MakeArray[T](name: Name[FixedSizeArray[T]], val numItems: Element[Int], val itemMaker: Int => Element[T], collection: ElementCollection) + extends Deterministic[FixedSizeArray[T]](name, collection) + with ValuesMaker[FixedSizeArray[T]] { + /* MakeArray is basically an Apply, with a few differences: + * 1. We have to register the embedded elements in the containers (i.e., the items) with the MakeArray. + * 2. When generating values, we have to go in and generate values for the items. + * 3. LazyValues doesn't need to store a map from the numItems value to the possible containers, because the arrays stream + * takes care of making sure they are always the same. Therefore, we don't need to worry about it in makeFactors. + */ + + /** + * An infinite stream of items in the array. + */ + lazy val items = makeItems(0) + private def makeItems(i: Int): Stream[Element[T]] = { + val item = itemMaker(i) + //item.makePermanent() // Since the same item is used again and again, we don't want to deactivate it + item #:: makeItems(i + 1) + } + + /** + * An infinite stream of array prefixes. At any point in time, the value of the MakeArray element + * is the array prefix specified by the value of numItems. + */ + lazy val arrays = makeArrays(0) + private def makeArrays(i: Int): Stream[FixedSizeArray[T]] = { + val array: FixedSizeArray[T] = new FixedSizeArray(i, (j: Int) => items(j)) + array #:: makeArrays(i + 1) + } + + override def args = numItems :: (items take numItems.value).toList + + override def generateValue = arrays(numItems.value) + + /** + * Return the i-th item in the list. Throws IllegalArgumentException if i is greater + * than the current value of numItems + */ + def apply(i: Int) = i < numItems.value match { + case true => items(i) + case _ => throw new IllegalArgumentException("Invalid indices to MakeList") + } + + def values = LazyValues(universe) + + def makeValues(depth: Int): ValueSet[FixedSizeArray[T]] = { + // This code is subtle. + // If we used itemMaker here, it would create bugs, as the items that appeared in the values would be different from the ones actually used by the Makelist. + // On the other hand, if we used Values(items(0)) as a template for the values of all items, it would create other bugs, as different indices would have the same values, + // even when the values include "new C", so they should all be different. + // Therefore, we use Values()(items(i)) to get the possible value for each item in the stream. + val possibleLengthValues = values(numItems, depth - 1) + val possibleLengths = possibleLengthValues.regularValues + val resultValues = possibleLengths.map(arrays(_)) + // Make sure to generate values for the possible items + for { item <- items.take(possibleLengths.max) } { values(item, depth - 1) } + val incomplete = possibleLengthValues.hasStar + if (incomplete) ValueSet.withStar(resultValues); else ValueSet.withoutStar(resultValues) + } + +} + +object VariableSizeArray { + /** + * Create a FixedSizeArrayElement that holds a MakeArray representing a random process that generates an array of a variable + * number of items, in which each item is generated by an element defined by a given function. + * + * @param numItems element representing the number of items in the array + * @param itemMaker a function that creates the element for generating each item, given the index into the array + */ + def apply[Value](numItems: Element[Int], generator: Int => Element[Value]) + (implicit name: Name[FixedSizeArray[Value]], collection: ElementCollection): FixedSizeArrayElement[Value] = { + new FixedSizeArrayElement[Value](new MakeArray(name, numItems, generator, collection)) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/compound/Fold.scala b/Figaro/src/main/scala/com/cra/figaro/library/compound/Fold.scala new file mode 100644 index 00000000..b2ddd449 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/compound/Fold.scala @@ -0,0 +1,91 @@ +/* + * Fold.scala + * Class for TBD + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.compound + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored.{LazyValues, ValueSet} + +/** + * Element representing the folding of a function through a sequence of elements. Elements are processed left to right. + * Factored algorithms use the decomposition of the fold to avoid creating huge factors. + * + * @param start the initial value for the fold + */ +class FoldLeft[T,U](name: Name[U], val start: U, val function: (U, T) => U, val elements: Seq[Element[T]], collection: ElementCollection) +extends Deterministic[U](name, collection) with ValuesMaker[U] { + /* + * FoldLeft has two alternative implementations. + * For factored algorithms, we use a decomposition into a chain series. + * For other algorithms, we use a simple generateValue. + */ + def args = elements.toList + override def generateValue = elements.map(_.value).foldLeft(start)(function) + + def makeValues(depth: Int): ValueSet[U] = { + val values = LazyValues(universe) + + def helper(currentAccum: ValueSet[U], remainingElements: Seq[Element[T]]): ValueSet[U] = { + if (remainingElements.isEmpty) currentAccum + else { + val firstVS = values(remainingElements.head, depth - 1) + val nextRegular = + for { + currentAccumVal <- currentAccum.regularValues + firstVal <- firstVS.regularValues + } yield function(currentAccumVal, firstVal) + val nextHasStar = currentAccum.hasStar || firstVS.hasStar + val nextAccum = if (nextHasStar) ValueSet.withStar(nextRegular) else ValueSet.withoutStar(nextRegular) + helper(nextAccum, remainingElements.tail) + } + } + + helper(ValueSet.withoutStar(Set(start)), elements) + } +} + +object FoldLeft { + def apply[T,U](start: U, function: (U, T) => U)(elements: Element[T]*)(implicit name: Name[U], collection: ElementCollection): Element[U] = { + if (elements.isEmpty) Constant(start) + else { + val elem = elements.head + if (!elem.active) elem.activate + new FoldLeft(name, start, function, elements, collection) + } + } +} + +/** + * Element representing the folding of a function through a sequence of elements. Elements are processed right to left. + * Factored algorithms use the decomposition of the fold to avoid creating huge factors. + * + * @param start the initial value for the fold + */ +object FoldRight { + def apply[T,U](start: U, function: (T, U) => U)(elements: Element[T]*)(implicit name: Name[U], collection: ElementCollection): Element[U] = { + FoldLeft(start, (u: U, t: T) => function(t, u))(elements.reverse:_*)(name, collection) + } +} + +/** + * Element representing the reducing of a function through a sequence of elements, with no initial value. Elements are processed left to right. + * Factored algorithms use the decomposition of the fold to avoid creating huge factors. + */ +object Reduce { + def apply[T](function: (T, T) => T)(elements: Element[T]*)(implicit name: Name[T], collection: ElementCollection): Element[T] = { + val elem = elements.head + if (!elem.active) elem.activate + Chain(elem, (t: T) => FoldLeft(t, function)(elements.tail:_*))(name, collection) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/compound/IntSelector.scala b/Figaro/src/main/scala/com/cra/figaro/library/compound/IntSelector.scala index 2c9a8be5..eb1189fd 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/compound/IntSelector.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/compound/IntSelector.scala @@ -1,73 +1,73 @@ -/* - * IntSelector.scala - * Selection of an integer uniformly from 0 to a variable upper bound. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Oct 17, 2011 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.library.compound - -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.lazyfactored._ - -/** - * An IntSelector represents the selection of an integer from 0 (inclusive) to a variable upper bound (exclusive). The - * upper bound is an element represented by the bound argument. The IntSelector class has been defined so that - * (1) the value is always uniformly distributed in the range, no matter how the upper bound changes, and - * (2) it attempts to avoid changing the value as much as it possibly can as the upper bound changes. - * This latter property makes the class useful in an algorithm like Metropolis-Hastings, where we would like to - * change as little as possible as we make a proposal. - */ -class IntSelector(name: Name[Int], counter: Element[Int], collection: ElementCollection) - extends Element[Int](name, collection) with IfArgsCacheable[Int] with ValuesMaker[Int] with ProbFactorMaker { - // We achieve the two properties by making the randomness a random stream of doubles and selecting the index - // within range that has the highest randomness. If the bound changes, the double associated with the index - // does not change, so quite often the highest index will stay the same. - type Randomness = Stream[Double] - - def args = List(counter) - - def generateRandomness(): Randomness = Stream.continually(random.nextDouble()) - - def generateValue(rand: Randomness): Int = argmax(rand take counter.value) - - def makeValues(depth: Int): ValueSet[Int] = { - val counterValues = LazyValues(universe)(counter, depth - 1) - if (counterValues.regularValues.nonEmpty) { - val maxCounter = counterValues.regularValues.max - val all = List.tabulate(maxCounter)(i => i).toSet - if (counterValues.hasStar) ValueSet.withStar(all); else ValueSet.withoutStar(all) - } else { ValueSet.withStar(Set()) } - } - - def makeFactors: List[Factor[Double]] = { - val thisVar = Variable(this) - val counterVar = Variable(counter) - val comb = Factory.make[Double](List(thisVar, counterVar)) - comb.fillByRule((l: List[Any]) => { - val xvalue0 :: xvalue1 :: _ = l.asInstanceOf[List[Extended[Int]]] - if (xvalue0.isRegular && xvalue1.isRegular) { - if (xvalue0.value < xvalue1.value) 1.0/xvalue1.value; else 0.0 - } else 1.0 - - }) - List(comb) - } -} - -object IntSelector { - /** - * Create an IntSelector using the counter element as the upper bound - */ - def apply(counter: Element[Int])(implicit name: Name[Int], collection: ElementCollection) = - new IntSelector(name, counter, collection) -} +/* + * IntSelector.scala + * Selection of an integer uniformly from 0 to a variable upper bound. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 17, 2011 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.compound + +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ + +/** + * An IntSelector represents the selection of an integer from 0 (inclusive) to a variable upper bound (exclusive). The + * upper bound is an element represented by the bound argument. The IntSelector class has been defined so that + * (1) the value is always uniformly distributed in the range, no matter how the upper bound changes, and + * (2) it attempts to avoid changing the value as much as it possibly can as the upper bound changes. + * This latter property makes the class useful in an algorithm like Metropolis-Hastings, where we would like to + * change as little as possible as we make a proposal. + */ +class IntSelector(name: Name[Int], val counter: Element[Int], collection: ElementCollection) + extends Element[Int](name, collection) with IfArgsCacheable[Int] with ValuesMaker[Int] { + // We achieve the two properties by making the randomness a random stream of doubles and selecting the index + // within range that has the highest randomness. If the bound changes, the double associated with the index + // does not change, so quite often the highest index will stay the same. + type Randomness = Stream[Double] + + def args = List(counter) + + def generateRandomness(): Randomness = Stream.continually(random.nextDouble()) + + def generateValue(rand: Randomness): Int = argmax(rand take counter.value) + + def makeValues(depth: Int): ValueSet[Int] = { + val counterValues = LazyValues(universe)(counter, depth - 1) + if (counterValues.regularValues.nonEmpty) { + val maxCounter = counterValues.regularValues.max + val all = List.tabulate(maxCounter)(i => i).toSet + if (counterValues.hasStar) ValueSet.withStar(all); else ValueSet.withoutStar(all) + } else { ValueSet.withStar(Set()) } + } + +// def makeFactors: List[Factor[Double]] = { +// val thisVar = Variable(this) +// val counterVar = Variable(counter) +// val comb = Factory.make[Double](List(thisVar, counterVar)) +// comb.fillByRule((l: List[Any]) => { +// val xvalue0 :: xvalue1 :: _ = l.asInstanceOf[List[Extended[Int]]] +// if (xvalue0.isRegular && xvalue1.isRegular) { +// if (xvalue0.value < xvalue1.value) 1.0/xvalue1.value; else 0.0 +// } else 1.0 +// +// }) +// List(comb) +// } +} + +object IntSelector { + /** + * Create an IntSelector using the counter element as the upper bound + */ + def apply(counter: Element[Int])(implicit name: Name[Int], collection: ElementCollection) = + new IntSelector(name, counter, collection) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/compound/MakeList.scala b/Figaro/src/main/scala/com/cra/figaro/library/compound/MakeList.scala index fafc787a..ab2ae5c0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/compound/MakeList.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/compound/MakeList.scala @@ -1,136 +1,135 @@ -/* - * MakeList.scala - * An element representing making a list of a random number of random items. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Oct 17, 2011 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.library.compound - -import com.cra.figaro.algorithm.ValuesMaker -import com.cra.figaro.algorithm.lazyfactored.{ValueSet, LazyValues, Regular} -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import scala.collection.mutable.Map - -/** - * An element representing making a list of a random number of random items. - * The first argument is an element representing the number of items. - * The second argument is an expression that generates an element representing an item. - * MakeList is designed to store all the item elements and not change them as the number of elements changes. - * - * @param numItems The element representing the number of items in the list - * @param itemMaker A function that creates an element representing a single item in the list - */ - -class MakeList[T]( - name: Name[List[T]], - val numItems: Element[Int], - val itemMaker: () => Element[T], - collection: ElementCollection) - extends Deterministic[List[T]](name, collection) - with ValuesMaker[List[T]] with ProbFactorMaker with IfArgsCacheable[List[T]] { - - /** - * An infinite stream of items in the list. At any point in time, the value of this element - * is the prefix of items specified by the value of numItems. - */ - lazy val items = Stream.continually({ - val item = itemMaker() - universe.registerUses(this, item) - item - }) - - override def args = numItems :: (items take numItems.value).toList - - override def generateValue = (items take numItems.value map (_.value)).toList - - /** - * Return the i-th item in the list. Throws IllegalArgumentException if i is greater - * than the current value of numItems - */ - def apply(i: Int) = i < numItems.value match { - case true => items(i) - case _ => throw new IllegalArgumentException("Invalid indices to MakeList") - } - - def values = LazyValues(universe) - - /* We need to make sure that values are computed on the embedded Injects. Therefore, we create them in makeValues, store them, and use them in makeFactors. - */ - val embeddedInject: Map[Int, Element[List[T]]] = Map() - - def makeValues(depth: Int): ValueSet[List[T]] = { - // This code is subtle. - // If we used itemMaker here, it would create bugs, as the items that appeared in the values would be different from the ones actually used by the Makelist. - // On the other hand, if we used Values(items(0)) as a template for the values of all items, it would create other bugs, as different indices would have the same values, - // even when the values include "new C", so they should all be different. - // Therefore, we use Values()(items(i)) to get the possible value for each item in the stream. - def possibleItemLists(length: Int): ValueSet[List[T]] = { - val inject = Inject(items.take(length):_*) - embeddedInject += length -> inject - values(inject, depth - 1) - } - val possibleLengthValues = values(numItems, depth - 1) - val possibleLengths = possibleLengthValues.regularValues - val itemListsForLengths = possibleLengths.map(possibleItemLists(_)) - val resultValues = - for { - itemLists <- itemListsForLengths - list <- itemLists.regularValues - } yield list - val incomplete = itemListsForLengths.exists(_.hasStar) || possibleLengthValues.hasStar - if (incomplete) ValueSet.withStar(resultValues); else ValueSet.withoutStar(resultValues) - } - - def makeFactors: List[Factor[Double]] = { - val parentVar = Variable(numItems) - // We need to create factors for the items and the lists themselves, which are encapsulated in this MakeList - val regularParents = parentVar.range.filter(_.isRegular).map(_.value) - val maxItem = regularParents.reduce(_ max _) - val itemFactors = List.tabulate(maxItem)((i: Int) => Factory.make(items(i))) - val indexedResultElemsAndFactors = - for { i <- regularParents } yield { - val elem = embeddedInject(i) - val factors = Factory.make(elem) - (Regular(i), elem, factors) - } - val conditionalFactors = - parentVar.range.zipWithIndex map (pair => - Factory.makeConditionalSelector(this, parentVar, pair._2, Variable(indexedResultElemsAndFactors.find(_._1 == pair._1).get._2))) - conditionalFactors ::: itemFactors.flatten ::: indexedResultElemsAndFactors.flatMap(_._3) - } - - override def isCachable = { - - if (itemMaker().isCachable == false) { - false - } - - for (arg <- args) yield { - if (arg.isCachable == false) { - false - } - } - - true - } - -} - -object MakeList { - /** - * Create a MakeList element using numItems to determine the number of items - * and itemMaker to create each item in the list. - */ - def apply[T](numItems: Element[Int], itemMaker: () => Element[T])(implicit name: Name[List[T]], collection: ElementCollection) = { - new MakeList(name, numItems, itemMaker, collection) - } -} +/* + * MakeList.scala + * An element representing making a list of a random number of random items. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 17, 2011 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.compound + +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored.{ValueSet, LazyValues, Regular} +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import scala.collection.mutable.Map + +/** + * An element representing making a list of a random number of random items. + * The first argument is an element representing the number of items. + * The second argument is an expression that generates an element representing an item. + * MakeList is designed to store all the item elements and not change them as the number of elements changes. + * + * @param numItems The element representing the number of items in the list + * @param itemMaker A function that creates an element representing a single item in the list + */ +class MakeList[T]( + name: Name[List[T]], + val numItems: Element[Int], + val itemMaker: () => Element[T], + collection: ElementCollection) + extends Deterministic[List[T]](name, collection) + with ValuesMaker[List[T]] with IfArgsCacheable[List[T]] { + + /** + * An infinite stream of items in the list. At any point in time, the value of this element + * is the prefix of items specified by the value of numItems. + */ + lazy val items = Stream.continually({ + val item = itemMaker() + universe.registerUses(this, item) + item + }) + + override def args = numItems :: (items take numItems.value).toList + + override def generateValue = (items take numItems.value map (_.value)).toList + + /** + * Return the i-th item in the list. Throws IllegalArgumentException if i is greater + * than the current value of numItems + */ + def apply(i: Int) = i < numItems.value match { + case true => items(i) + case _ => throw new IllegalArgumentException("Invalid indices to MakeList") + } + + private def values = LazyValues(universe) + + /* We need to make sure that values are computed on the embedded Injects. Therefore, we create them in makeValues, store them, and use them in makeFactors. + */ + val embeddedInject: Map[Int, Element[List[T]]] = Map() + + def makeValues(depth: Int): ValueSet[List[T]] = { + // This code is subtle. + // If we used itemMaker here, it would create bugs, as the items that appeared in the values would be different from the ones actually used by the Makelist. + // On the other hand, if we used Values(items(0)) as a template for the values of all items, it would create other bugs, as different indices would have the same values, + // even when the values include "new C", so they should all be different. + // Therefore, we use Values()(items(i)) to get the possible value for each item in the stream. + def possibleItemLists(length: Int): ValueSet[List[T]] = { + val inject = Inject(items.take(length):_*) + embeddedInject += length -> inject + values(inject, depth - 1) + } + val possibleLengthValues = values(numItems, depth - 1) + val possibleLengths = possibleLengthValues.regularValues + val itemListsForLengths = possibleLengths.map(possibleItemLists(_)) + val resultValues = + for { + itemLists <- itemListsForLengths + list <- itemLists.regularValues + } yield list + val incomplete = itemListsForLengths.exists(_.hasStar) || possibleLengthValues.hasStar + if (incomplete) ValueSet.withStar(resultValues); else ValueSet.withoutStar(resultValues) + } + +// def makeFactors: List[Factor[Double]] = { +// val parentVar = Variable(numItems) +// // We need to create factors for the items and the lists themselves, which are encapsulated in this MakeList +// val regularParents = parentVar.range.filter(_.isRegular).map(_.value) +// val maxItem = regularParents.reduce(_ max _) +// val itemFactors = List.tabulate(maxItem)((i: Int) => Factory.make(items(i))) +// val indexedResultElemsAndFactors = +// for { i <- regularParents } yield { +// val elem = embeddedInject(i) +// val factors = Factory.make(elem) +// (Regular(i), elem, factors) +// } +// val conditionalFactors = +// parentVar.range.zipWithIndex map (pair => +// Factory.makeConditionalSelector(this, parentVar, pair._2, Variable(indexedResultElemsAndFactors.find(_._1 == pair._1).get._2))) +// conditionalFactors ::: itemFactors.flatten ::: indexedResultElemsAndFactors.flatMap(_._3) +// } + + override def isCachable = { + + if (itemMaker().isCachable == false) { + false + } + + for (arg <- args) yield { + if (arg.isCachable == false) { + false + } + } + + true + } + +} + +object MakeList { + /** + * Create a MakeList element using numItems to determine the number of items + * and itemMaker to create each item in the list. + */ + def apply[T](numItems: Element[Int], itemMaker: () => Element[T])(implicit name: Name[List[T]], collection: ElementCollection) = { + new MakeList(name, numItems, itemMaker, collection) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/decision/Decision.scala b/Figaro/src/main/scala/com/cra/figaro/library/decision/Decision.scala index 556b4e02..83f5361c 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/decision/Decision.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/decision/Decision.scala @@ -1,260 +1,260 @@ -/* - * Decision.scala - * Element which represents decisions in a model - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Oct 4, 2012 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.library.decision - -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.lazyfactored._ -import com.cra.figaro.algorithm.decision._ -import com.cra.figaro.algorithm.decision.index._ -import com.cra.figaro.language._ -import com.cra.figaro.library.atomic.discrete._ -import scala.collection.mutable.Set - -/** - * A Decision class represents a decision that takes in a single parent element, applies a function (aka a policy) - * on the value of the parent, and returns a decision element. A decision is essentially a chain, with the added - * capability to change the function in the chain. If your decision has more than one parent, you can create an element - * tuple of the parents and use the tuple as the input to the decision - * - */ -/* - * - * Inside each decision class is a DecisionPolicy class. The DecisionPolicy class controls how the optimal - * decision for each parent value is chosen. Generally the default policy algorithm will work for most people, - * but they can be override if necessary. The decision class extends PolicyMaker, which controls how to create - * a DecisionPolicy for a decision. The default implementations of decisions automatically extend - * PolicyMaker as appropriate. If you want a different DecisionPolicy, you need to extend the trait - * PolicyMaker and declare the function makePolicy. - * - * There are two types of chains. A caching version and a noncaching version. The caching should only be used - * on domains where the parents values are discrete and have a small range, and it implements an exact policy - * (i.e., during decision inference, it must compute a policy for every possible value of parents). The - * noncaching uses an approximate policy algorithm (based on kNN) and should be used for continuous - * parents or discrete parents with a very large range. - */ -abstract class Decision[T, U](name: Name[U], parent: Element[T], private var fcn: T => Element[U], cacheSize: Int, collection: ElementCollection) - extends Chain(name, parent, fcn, cacheSize, collection) with PolicyMaker[T, U] { - - /** - * The parent type - */ - type PValue = this.ParentType - - /** - * The decision type - */ - type DValue = this.Value - - /** - * The decision function. fcn is declared as a var and can change depending on the policy - */ - override protected def cpd = this.fcn - - private[figaro] var policy: DecisionPolicy[T, U] = null - - /** - * Get the decision for a specific value of the parent - */ - def getPolicy(p: T): Element[U] = { val result = get(p); result.generate(); result } - - /** - * Get the complete policy object for this decision - */ - def getPolicy(): DecisionPolicy[T, U] = policy - - /** - * Set the policy for this decision using the DecisionPolicy class stored in the Decision - */ - def setPolicy(): Unit = setPolicy(policy.toFcn()) - - /** - * Set the policy for this decision from an Algorithm run on this decision - */ - def setPolicy(Alg: DecisionAlgorithm[T, U]): Unit = setPolicy(Alg.getUtility()) - - /** - * Set the policy for this decision using a map from parent/decision tuple values to decision samples - */ - def setPolicy(policyMap: Map[(T, U), DecisionSample]): Unit = { - policy = makePolicy(policyMap) - setPolicy(policy.toFcn()) - } - - /** - * Directly set the policy of this decision using a function from a parent value to a decision element - */ - def setPolicy(new_fcn: (T => Element[U])): Unit = { - // Override the fcn variable - fcn = new_fcn - - // Remove all factors since the old factors are now out of date - Factory.removeFactors - // Have to nullify the last result even if parents the same since the function changed - clearContext - // Have to clear the last element in the cache since clearTempory always leaves an element in the cache - if (cache.nonEmpty) resizeCache(cache.last._1) - // Have to remove the expansion of the universe since it is out of data - LazyValues.clear(universe) - // Must regenerate a new value since the cache should never be empty - generateValue() - } - -} - -/** - * Abstract class for a NonCachingDecision. It is abstract because makePolicy has not been defined yet - */ -abstract class NonCachingDecision[T, U](name: Name[U], parent: Element[T], fcn: T => Element[U], collection: ElementCollection) - extends Decision(name, parent, fcn, 1, collection) { - - override def toString = "NonCachingDecision(" + parent + ", " + this.cpd + ")" -} - -/** - * Abstract class for a CachingDecision. It is abstract because makePolicy has not been defined yet - */ -abstract class CachingDecision[T, U](name: Name[U], parent: Element[T], fcn: T => Element[U], collection: ElementCollection) - extends Decision(name, parent, fcn, 1000, collection) { - - override def toString = "CachingDecision(" + parent + ", " + this.cpd + ")" -} - -/* - * By default, noncaching decisions implement an approximate policy (a kNN algorithm) by extending the PolicyMaker trait and defining - * the required makePolicy function to create a DecisionPolicyApprox. If you need to modify the parameters of the DecisionPolicyApprox - * class or want a different class, you have to create an instance of NonCachingDecision, extend PolicyMaker and define makePolicy. - * See below for examples or in the example package - */ - -object NonCachingDecision { - - /** - * Create a NonCachingDecision with no parent that uses an approximate (kNN) PolicyMaker - */ - def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection, conversion: Int => Distance[Int]): Decision[Int, U] = { - new NonCachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with PolicyMaker[Int, U] { - def makePolicy(policyMap: Map[(Int, U), DecisionSample]) = DecisionPolicyNN(policyMap) - } - } - - /** - * Create a NonCachingDecision with one parent and a default function from values of the parent to Element[U]. - * Uses an approximate (kNN) PolicyMaker - */ - def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection, conversion: T => Distance[T]): Decision[T, U] = { - new NonCachingDecision(name, arg1, fcn, collection) with PolicyMaker[T, U] { - def makePolicy(policyMap: Map[(T, U), DecisionSample]) = DecisionPolicyNN(policyMap) - } - } - - /** - * Create a NonCachingDecision with no parent and sequence of the possible actions of the decision. - * Uses an approximate (kNN) PolicyMaker - */ - def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection, conversion: Int => Distance[Int]): Decision[Int, U] = { - val defElement = rangeToElement(range) - apply(() => defElement)(name, collection, conversion) - } - - /** - * Create a NonCachingDecision with one parent and sequence of the possible actions of the decision. - * Uses an approximate (kNN) PolicyMaker - */ - def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection, conversion: T => Distance[T]): Decision[T, U] = { - val defElement = rangeToElement(range) - apply(arg1, (d: T) => defElement)(name, collection, conversion) - } - - private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) -} - -/* - * Caching chain by default implements an exact policy. If you want to override this, see instructions/code for the - * noncaching chain. - */ - -object CachingDecision { - /** - * Create a CachingDecision with no parent that uses an exact PolicyMaker. - */ - def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { - new CachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with ExactPolicyMaker[Int, U] - } - - /** - * Create a CachingDecision with one parent and a default function from values of the parent to Element[U]. - * Uses an exact PolicyMaker - */ - def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { - new CachingDecision(name, arg1, fcn, collection) with ExactPolicyMaker[T, U] - } - - /** - * Create a CachingDecision with no parent and sequence of the possible actions of the decision. - * Uses an exact PolicyMaker - */ - def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { - apply(() => rangeToElement(range))(name, collection) - } - - /** - * Create a CachingDecision with one parent and sequence of the possible actions of the decision. - * Uses an exact PolicyMaker - */ - def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { - apply(arg1, (d: T) => rangeToElement(range))(name, collection) - } - - private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) -} - -/** - * By default, creating a Decision uses a Caching decision and an exact policy. See NonCachingDecision for - * approximate policies. - */ -object Decision { - /** - * Create a CachingDecision with no parent that uses an exact PolicyMaker - */ - def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { - new CachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with ExactPolicyMaker[Int, U] - } - - /** - * Create a CachingDecision with one parent and a default function from values of the parent to Element[U]. - * Uses an exact PolicyMaker - */ - def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { - new CachingDecision(name, arg1, fcn, collection) with ExactPolicyMaker[T, U] - } - - /** - * Create a CachingDecision with no parent and sequence of the possible actions of the decision. - * Uses an exact PolicyMaker - */ - def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { - apply(() => rangeToElement(range))(name, collection) - } - - /** - * Create a CachingDecision with one parent and sequence of the possible actions of the decision. - * Uses an exact PolicyMaker - */ - def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { - apply(arg1, (d: T) => rangeToElement(range))(name, collection) - } - - private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) -} +/* + * Decision.scala + * Element which represents decisions in a model + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Oct 4, 2012 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.decision + +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.algorithm.decision._ +import com.cra.figaro.algorithm.decision.index._ +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.discrete._ +import scala.collection.mutable.Set + +/** + * A Decision class represents a decision that takes in a single parent element, applies a function (aka a policy) + * on the value of the parent, and returns a decision element. A decision is essentially a chain, with the added + * capability to change the function in the chain. If your decision has more than one parent, you can create an element + * tuple of the parents and use the tuple as the input to the decision. + * + */ +/* + * + * Inside each decision class is a DecisionPolicy class. The DecisionPolicy class controls how the optimal + * decision for each parent value is chosen. Generally the default policy algorithm will work for most people, + * but they can be override if necessary. The decision class extends PolicyMaker, which controls how to create + * a DecisionPolicy for a decision. The default implementations of decisions automatically extend + * PolicyMaker as appropriate. If you want a different DecisionPolicy, you need to extend the trait + * PolicyMaker and declare the function makePolicy. + * + * There are two types of chains. A caching version and a noncaching version. The caching should only be used + * on domains where the parents values are discrete and have a small range, and it implements an exact policy + * (i.e., during decision inference, it must compute a policy for every possible value of parents). The + * noncaching uses an approximate policy algorithm (based on kNN) and should be used for continuous + * parents or discrete parents with a very large range. + */ +abstract class Decision[T, U](name: Name[U], parent: Element[T], private var fcn: T => Element[U], cacheSize: Int, collection: ElementCollection) + extends Chain(name, parent, fcn, cacheSize, collection) with PolicyMaker[T, U] { + + /** + * The parent type. + */ + type PValue = this.ParentType + + /** + * The decision type. + */ + type DValue = this.Value + + /** + * The decision function. fcn is declared as a var and can change depending on the policy. + */ + override protected def cpd = this.fcn + + private[figaro] var policy: DecisionPolicy[T, U] = null + + /** + * Get the decision for a specific value of the parent. + */ + def getPolicy(p: T): Element[U] = { val result = get(p); result.generate(); result } + + /** + * Get the complete policy object for this decision. + */ + def getPolicy(): DecisionPolicy[T, U] = policy + + /** + * Set the policy for this decision using the DecisionPolicy class stored in the Decision. + */ + def setPolicy(): Unit = setPolicy(policy.toFcn()) + + /** + * Set the policy for this decision from an Algorithm run on this decision. + */ + def setPolicy(Alg: DecisionAlgorithm[T, U]): Unit = setPolicy(Alg.getUtility()) + + /** + * Set the policy for this decision using a map from parent/decision tuple values to decision samples. + */ + def setPolicy(policyMap: Map[(T, U), DecisionSample]): Unit = { + policy = makePolicy(policyMap) + setPolicy(policy.toFcn()) + } + + /** + * Directly set the policy of this decision using a function from a parent value to a decision element. + */ + def setPolicy(new_fcn: (T => Element[U])): Unit = { + // Override the fcn variable + fcn = new_fcn + + // Remove all factors since the old factors are now out of date + Factory.removeFactors + // Have to nullify the last result even if parents the same since the function changed + clearContext + // Have to clear the last element in the cache since clearTempory always leaves an element in the cache + if (cache.nonEmpty) resizeCache(cache.last._1) + // Have to remove the expansion of the universe since it is out of data + LazyValues.clear(universe) + // Must regenerate a new value since the cache should never be empty + generateValue() + } + +} + +/** + * Abstract class for a NonCachingDecision. It is abstract because makePolicy has not been defined yet. + */ +abstract class NonCachingDecision[T, U](name: Name[U], parent: Element[T], fcn: T => Element[U], collection: ElementCollection) + extends Decision(name, parent, fcn, 1, collection) { + + override def toString = "NonCachingDecision(" + parent + ", " + this.cpd + ")" +} + +/** + * Abstract class for a CachingDecision. It is abstract because makePolicy has not been defined yet. + */ +abstract class CachingDecision[T, U](name: Name[U], parent: Element[T], fcn: T => Element[U], collection: ElementCollection) + extends Decision(name, parent, fcn, 1000, collection) { + + override def toString = "CachingDecision(" + parent + ", " + this.cpd + ")" +} + +/* + * By default, noncaching decisions implement an approximate policy (a kNN algorithm) by extending the PolicyMaker trait and defining + * the required makePolicy function to create a DecisionPolicyApprox. If you need to modify the parameters of the DecisionPolicyApprox + * class or want a different class, you have to create an instance of NonCachingDecision, extend PolicyMaker and define makePolicy. + * See below for examples or in the example package + */ + +object NonCachingDecision { + + /** + * Create a NonCachingDecision with no parent that uses an approximate (kNN) PolicyMaker. + */ + def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection, conversion: Int => Distance[Int]): Decision[Int, U] = { + new NonCachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with PolicyMaker[Int, U] { + def makePolicy(policyMap: Map[(Int, U), DecisionSample]) = DecisionPolicyNN(policyMap) + } + } + + /** + * Create a NonCachingDecision with one parent and a default function from values of the parent to Element[U]. + * Uses an approximate (kNN) PolicyMaker. + */ + def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection, conversion: T => Distance[T]): Decision[T, U] = { + new NonCachingDecision(name, arg1, fcn, collection) with PolicyMaker[T, U] { + def makePolicy(policyMap: Map[(T, U), DecisionSample]) = DecisionPolicyNN(policyMap) + } + } + + /** + * Create a NonCachingDecision with no parent and sequence of the possible actions of the decision. + * Uses an approximate (kNN) PolicyMaker. + */ + def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection, conversion: Int => Distance[Int]): Decision[Int, U] = { + val defElement = rangeToElement(range) + apply(() => defElement)(name, collection, conversion) + } + + /** + * Create a NonCachingDecision with one parent and sequence of the possible actions of the decision. + * Uses an approximate (kNN) PolicyMaker. + */ + def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection, conversion: T => Distance[T]): Decision[T, U] = { + val defElement = rangeToElement(range) + apply(arg1, (d: T) => defElement)(name, collection, conversion) + } + + private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) +} + +/* + * Caching chain by default implements an exact policy. If you want to override this, see instructions/code for the + * noncaching chain. + */ + +object CachingDecision { + /** + * Create a CachingDecision with no parent that uses an exact PolicyMaker. + */ + def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { + new CachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with ExactPolicyMaker[Int, U] + } + + /** + * Create a CachingDecision with one parent and a default function from values of the parent to Element[U]. + * Uses an exact PolicyMaker. + */ + def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { + new CachingDecision(name, arg1, fcn, collection) with ExactPolicyMaker[T, U] + } + + /** + * Create a CachingDecision with no parent and sequence of the possible actions of the decision. + * Uses an exact PolicyMaker. + */ + def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { + apply(() => rangeToElement(range))(name, collection) + } + + /** + * Create a CachingDecision with one parent and sequence of the possible actions of the decision. + * Uses an exact PolicyMaker. + */ + def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { + apply(arg1, (d: T) => rangeToElement(range))(name, collection) + } + + private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) +} + +/** + * By default, creating a Decision uses a Caching decision and an exact policy. See NonCachingDecision for + * approximate policies. + */ +object Decision { + /** + * Create a CachingDecision with no parent that uses an exact PolicyMaker. + */ + def apply[U](fcn: () => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { + new CachingDecision(name, Constant(0), (i: Int) => fcn(), collection) with ExactPolicyMaker[Int, U] + } + + /** + * Create a CachingDecision with one parent and a default function from values of the parent to Element[U]. + * Uses an exact PolicyMaker. + */ + def apply[T, U](arg1: Element[T], fcn: (T) => Element[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { + new CachingDecision(name, arg1, fcn, collection) with ExactPolicyMaker[T, U] + } + + /** + * Create a CachingDecision with no parent and sequence of the possible actions of the decision. + * Uses an exact PolicyMaker. + */ + def apply[U](range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[Int, U] = { + apply(() => rangeToElement(range))(name, collection) + } + + /** + * Create a CachingDecision with one parent and sequence of the possible actions of the decision. + * Uses an exact PolicyMaker. + */ + def apply[T, U](arg1: Element[T], range: Seq[U])(implicit name: Name[U], collection: ElementCollection): Decision[T, U] = { + apply(arg1, (d: T) => rangeToElement(range))(name, collection) + } + + private def rangeToElement[U](range: Seq[U]): Element[U] = Uniform[U](range: _*) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/decision/DecisionUtil.scala b/Figaro/src/main/scala/com/cra/figaro/library/decision/DecisionUtil.scala index 2b0e8baf..10049051 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/decision/DecisionUtil.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/decision/DecisionUtil.scala @@ -32,26 +32,26 @@ class IllegalDecisionNetwork(S: String) extends DecisionIllegalArgumentException object DecisionUtil { /** - * Computes the partial order of dependent decisions + * Computes the partial order of dependent decisions. */ def getDecisionOrder(Decisions: Traversable[Element[_]], universe: Universe): List[List[Element[_]]] = universe.layers(Decisions) /** - * Returns the utilities in a list of elements from a reference list of utilities + * Returns the utilities in a list of elements from a reference list of utilities. */ def utilitiesInElems(Elems: List[Element[_]], U: List[Element[_]]): List[Element[_]] = Elems.intersect(U) /** - * Computes the set of elements that the given list of utilites depends upon for computation + * Computes the set of elements that the given list of utilities depends upon for computation. */ def utilitiesUse(U: Traversable[Element[_]], universe: Universe): Set[Element[_]] = U.foldLeft(Set[Element[_]]())((s, u) => s union universe.uses(u)).toSet /** - * Computes all the relevant decisions that come before a decision in the decision order - * It does not return decisions that are partially ordered with D + * Computes all the relevant decisions that come before a decision in the decision order. + * It does not return decisions that are partially ordered with D. * */ def getPredDecisions(D: Element[_], Order: List[List[Element[_]]], universe: Universe) = { val contain = Order.map(s => s.contains(D)).indexOf(true) @@ -79,7 +79,7 @@ object DecisionUtil { } /** - * Computes all the elements that need to be simulated in order to compute a strategy for a decision + * Computes all the elements that need to be simulated in order to compute a strategy for a decision. */ def getElemsForDecision(D: Element[_], utilities: List[Element[_]], Order: List[List[Element[_]]], universe: Universe) = { val utilUses = getReleventUtil(D, utilities, Order, universe).foldLeft(Set[Element[_]]()) { (s, u) => @@ -105,7 +105,7 @@ object DecisionUtil { } /** - * Creates a dummy element pair of (parent, decision) used for inference algorithms + * Creates a dummy element pair of (parent, decision) used for inference algorithms. */ def createDecisionDummy[T, U](target: Decision[T, U]) = ^^(target.args(0).asInstanceOf[Element[T]], target) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/decision/PolicyMaker.scala b/Figaro/src/main/scala/com/cra/figaro/library/decision/PolicyMaker.scala index f6477c45..570daa78 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/decision/PolicyMaker.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/decision/PolicyMaker.scala @@ -21,22 +21,22 @@ import scala.collection.immutable.Map * Trait that defines how to make a policy for a particular decision. All decision elements must implement * this trait by defining makePolicy. The default usage of caching and noncaching decisions * provide implementations of the makePolicy function. If you want a different policy, you must - * define makePolicy + * define makePolicy. */ trait PolicyMaker[T, U] { /** - * Returns a DecisionPolicy from a map of parent/decision tuple values to DecisionSamples + * Returns a DecisionPolicy from a map of parent/decision tuple values to DecisionSamples. */ def makePolicy(policyMap: Map[(T, U), DecisionSample]): DecisionPolicy[T, U] /** - * Returns a DecisionPolicy from the given decision algorithm + * Returns a DecisionPolicy from the given decision algorithm . */ def makePolicy(alg: DecisionAlgorithm[T, U]): DecisionPolicy[T, U] = makePolicy(alg.getUtility()) } /** - * Trait that implements an exact policy + * Trait that implements an exact policy. */ trait ExactPolicyMaker[T, U] extends PolicyMaker[T, U] { def makePolicy(policyMap: Map[(T, U), DecisionSample]) = DecisionPolicyExact(policyMap) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/Array.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/Array.scala deleted file mode 100644 index aca0342d..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/Array.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -abstract class FixedArray[Value](size: Int) -extends Container[Int, Value] { - val indices = 0 until size - - override def rangeCheck(index: Int) = index >= 0 && index < size -} - -class FixedIndependentArray[Value]( - val size: Int, - val generator: Int => Element[Value], - override val name: Name[_] = "", - override val collection: ElementCollection = Universe.universe -) extends FixedArray[Value](size) with IndependentProcess[Int, Value] - diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/Container.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/Container.scala deleted file mode 100644 index f60a2793..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/Container.scala +++ /dev/null @@ -1,165 +0,0 @@ -package com.cra.figaro.library.process - -import com.cra.figaro.language._ -import scala.reflect.macros.Universe - -/** - * A Container is a Process with a defined sequence of indices. - */ -trait Container[Index, Value] -extends Process[Index, Value] { - val indices: Seq[Index] - - def rangeCheck(index: Index) = indices.contains(index) - - /** - * Apply the given function to every value in this container, returning a new container. - */ - override def map[Value2](f: Value => Value2): Container[Index, Value2] = { - val thisContainer = this - new Container[Index, Value2] { - val indices = thisContainer.indices - override val name: Name[_] = thisContainer.name + ".map" - override val collection = thisContainer.collection - def generate(i: Index) = thisContainer(i).map(f) - def generate(indices: List[Index]) = - thisContainer.generate(indices).mapValues(_.map(f)) - } - } - - /** - * Chain every value in this container through the given function, returning a new container. - */ - override def chain[Value2](f: Value => Element[Value2]): Container[Index, Value2] = { - val thisContainer = this - new Container[Index, Value2] { - val indices = thisContainer.indices - override val name: Name[_] = thisContainer.name + ".map" - override val collection = thisContainer.collection - def generate(i: Index) = thisContainer(i).flatMap(f) - def generate(indices: List[Index]) = - thisContainer.generate(indices).mapValues(_.flatMap(f)) - } - } - - /** - * Fold the values in this container through the given function. - */ - def foldLeft[Value2](start: Value2)(f: (Value2, Value) => Value2): Element[Value2] = { - val myArgs = indices.map(apply(_)).toList - new Deterministic[Value2](name + ".foldLeft", collection) { - def args = myArgs - def generateValue(): Value2 = { - args.map(_.value).foldLeft(start)(f) - } - } - } - - /** - * Fold the values in this container through the given function. - */ - def foldRight[Value2](start: Value2)(f: (Value, Value2) => Value2): Element[Value2] = { - val myArgs = indices.map(apply(_)).toList - new Deterministic[Value2](name + ".foldRight", collection) { - def args = myArgs - def generateValue(): Value2 = { - args.map(_.value).foldRight(start)(f) - } - } - } - - /** - * Reduce the values in this container through the given function. - */ - def reduce(f: (Value, Value) => Value): Element[Value] = { - val myArgs = indices.map(apply(_)).toList - new Deterministic[Value](name + ".foldRight", collection) { - def args = myArgs - def generateValue(): Value = { - args.map(_.value).reduce(f) - } - } - } - - /** - * Aggregate the results of applying an operator to each element. - */ - def aggregate[Value2](start: => Value2)(seqop: (Value2, Value) => Value2, combop: (Value2, Value2) => Value2): Element[Value2] = { - foldLeft(start)((v1: Value2, v2: Value) => combop(v1, seqop(v1, v2))) - } - - /** - * Returns an element representing the number of elements in the container whose values satisfy the predicate. - */ - def count(f: (Value) => Boolean): Element[Int] = { - foldLeft(0)((i: Int, v: Value) => if (f(v)) i + 1 else i) - } - - /** - * Returns an element representing whether the value of any element in the container satisfies the predicate. - */ - def exists(pred: Value => Boolean): Element[Boolean] = { - foldLeft(false)((b: Boolean, v: Value) => pred(v) || b) - } - - /** - * Returns an element representing whether the values of all elements in the container satisfy the predicate. - */ - def forall(pred: Value => Boolean): Element[Boolean] = { - foldLeft(true)((b: Boolean, v: Value) => pred(v) && b) - } - - /** - * Returns an element representing the optional index of the first element in the container whose value satisfies the predicate. - * The result has value None if no element is found. - */ - def findIndex(pred: Value => Boolean): Element[Option[Index]] = { - val argMap = indices.map(arg => (arg, apply(arg))).toMap - def step(oi: Option[Index], index: Index) = oi match { - case Some(_) => oi - case None => if (pred(argMap(index).value)) Some(index) else None - } - new Deterministic[Option[Index]](name + ".findIndex", collection) { - def args = argMap.values.toList - def generateValue(): Option[Index] = { - indices.foldLeft(None: Option[Index])(step) - } - } - } - - /** - * Returns a new container containing the elements of this container and the argument. - * If an index is defined in both container, the element of the argument is used. - */ - def ++(that: Container[Index, Value]): Container[Index, Value] = { - val thisContainer = this - new Container[Index, Value] { - val indices = { - val fromThis = thisContainer.indices.filterNot(that.indices.contains(_)) - fromThis ++ that.indices - } - override val name: Name[_] = thisContainer.name + "++" + that.name - override val collection = that.collection - def generate(i: Index) = - if (that.rangeCheck(i)) that.generate(i); else thisContainer.generate(i) - def generate(indices: List[Index]) = { - val (fromThatIndices, fromThisIndices) = indices.partition(that.rangeCheck(_)) - val fromThis = thisContainer.generate(fromThisIndices) - val fromThat = that.generate(indices) - fromThis ++ fromThat - } - } - } -} - -object Container { - def apply[Value](elements: Traversable[Element[Value]])(implicit name: Name[_], collection: ElementCollection): Container[Int, Value] = { - val elementArray = elements.toArray - new FixedIndependentArray[Value]( - elementArray.size, - (index: Int) => elementArray(index), - name, - collection - ) - } -} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/ContainerElement.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/ContainerElement.scala deleted file mode 100644 index 9aad2df8..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/ContainerElement.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* - -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -class ContainerElement[Index, Value](val element: Element[Container[Index, Value]]) { - /** - * Creates an element whose value is the value at the corresponding index in the value of the container element. - */ - def get(i: Index): Element[Option[Value]] = { - Chain(element, (p: Container[Index, Value]) => p.get(i)) - } - - /** - * Map the given function pointwise through the value of the container element. - */ - def map[Value2](f: Value => Value2): ContainerElement[Index, Value2] = { - new ContainerElement(Apply(element, (p: Container[Index, Value]) => p.map(f))) - } - - /** - * Chain the given function pointwise through the value of the container element. - */ - def chain[Value2](f: Value => Element[Value2]): ContainerElement[Index, Value2] = { - new ContainerElement(Apply(element, (p: Container[Index, Value]) => p.chain(f))) - } -} - -*/ \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/FiniteContainerElement.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/FiniteContainerElement.scala deleted file mode 100644 index 378cd408..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/FiniteContainerElement.scala +++ /dev/null @@ -1,54 +0,0 @@ -/* - -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -class FiniteContainerElement[Index, Value](val element: Element[FiniteContainer[Index, Value]]) -{ - /** - * Creates an element whose value is the value at the corresponding index in the value of the container element, - * if the index is in range, None otherwise. - */ - def get(i: Index): Element[Option[Value]] = { - Chain(element, (p: Container[Index, Value]) => p.get(i)) - } - - /** - * Map the given function pointwise through the value of the container element. - */ - def map[Value2](f: Value => Value2): FiniteContainerElement[Index, Value2] = { - new FiniteContainerElement(Apply(element, (p: FiniteContainer[Index, Value]) => p.map(f))) - } - - /** - * Chain the given function pointwise through the value of the container element. - */ - def chain[Value2](f: Value => Element[Value2]): FiniteContainerElement[Index, Value2] = { - new FiniteContainerElement(Apply(element, (p: FiniteContainer[Index, Value]) => p.chain(f))) - } - - /** - * Produce the element over values obtained by selecting a particular container and folding through its values. - */ - def foldLeft[Value2](start: Value2, f: (Value2, Value) => Value2): Element[Value2] = { - CachingChain(element, (p: FiniteContainer[Index, Value]) => p.foldLeft(start, f)) - } - - /** - * Produce the element over values obtained by selecting a particular container and folding through its values. - */ - def foldRight[Value2](start: Value2, f: (Value, Value2) => Value2): Element[Value2] = { - CachingChain(element, (p: FiniteContainer[Index, Value]) => p.foldRight(start, f)) - } - - /** - * Produce the element over values obtained by selecting a particular container and reducing through its values. - */ - def reduce(f: (Value, Value) => Value): Element[Value] = { - CachingChain(element, (p: FiniteContainer[Index, Value]) => p.reduce(f)) - } - -} - -*/ \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/IncrementalProcess.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/IncrementalProcess.scala deleted file mode 100644 index 1b0cdfca..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/IncrementalProcess.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -trait IncrementalContainer[Index, Value] extends Process[Index, Value] { - /** - * Produce the elements representing the value of the process at the given indices. - * Ensures that any dependencies between the elements are represented. - * Also return a new container such that when elements for future indices are produced, - * any dependencies between those elements and the ones for these indices are represented. - * This method must be implemented by implementations of IncrementalContainer. - * The first return value maps each provided index to the corresponding element. - * The second return values is the new container. - * This method does not assume that the indices have already been range checked. - */ - protected def generateIncremental(indices: List[Index]): (Map[Index, Element[Value]], Process[Index, Value]) -} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/IndependentProcess.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/IndependentProcess.scala deleted file mode 100644 index 8752a7c0..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/IndependentProcess.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -trait IndependentProcess[Index, Value] extends Process[Index, Value] { - val generator: Index => Element[Value] - - def generate(index: Index) = generator(index) - - def generate(indices: List[Index]) = { - Map(indices.map(index => (index, generator(index))):_*) - } -} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/library/process/VariableSizeArray.scala b/Figaro/src/main/scala/com/cra/figaro/library/process/VariableSizeArray.scala deleted file mode 100644 index 550745d3..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/library/process/VariableSizeArray.scala +++ /dev/null @@ -1,14 +0,0 @@ -/* - * -package com.cra.figaro.library.process - -import com.cra.figaro.language._ - -object VariableSizeArray { - def apply[Value](numItems: Element[Int], generator: Int => Element[Value]) = - new FiniteContainerElement[Int, Value]( - Apply(numItems, (size: Int) => new FixedIndependentArray(size, generator)) - ) -} - -*/ \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/patterns/learning/ModelParameters.scala b/Figaro/src/main/scala/com/cra/figaro/patterns/learning/ModelParameters.scala index 2d4e9ad5..5e4643d0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/patterns/learning/ModelParameters.scala +++ b/Figaro/src/main/scala/com/cra/figaro/patterns/learning/ModelParameters.scala @@ -1,3 +1,16 @@ +/* + * ModelParameters.scala + * Collections for defining prior and posterior parameters. + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Oct 29, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.patterns.learning import com.cra.figaro.language.Element @@ -9,25 +22,38 @@ import scala.collection.mutable.ListBuffer import com.cra.figaro.language.DoubleParameter import com.cra.figaro.language.ArrayParameter import com.cra.figaro.language.Parameter -//Double, array[double], (or matrix[double] -//Or element[double], element[array[double]] - -abstract class ParameterType +/** + * Case classes defining type parameters of parameter elements. + * These are used for matching on return types of parameter collections, and for + * correctly instantiating elements from the posterior and prior parameters. + */ +abstract class ParameterType +/** + * Parameters whose MAP value is a double + */ case class PrimitiveDouble(val d: Double) extends ParameterType { override def toString = d.toString() } +/** + * Learnable parameters whose MAP value is a double + */ case class ParameterDouble(val p: Parameter[Double]) extends ParameterType { override def toString = p.toString() } +/** + * Parameters whose MAP value is an array of doubles + */ case class PrimitiveArray(val a: Array[Double]) extends ParameterType { override def toString = a.toString() } +/** + * Learnable parameters whose MAP value is an array of doubles + */ case class ParameterArray(val p: Parameter[Array[Double]]) extends ParameterType { override def toString = p.toString() } - object ParameterType { def apply(d: Double) = new PrimitiveDouble(d) def apply(p: Parameter[_]) = { @@ -39,73 +65,119 @@ object ParameterType { def apply(a: Array[Double]) = new PrimitiveArray(a) } - -abstract class ParameterCollection { +/** + * Defines a collection of prior or posterior parameters, obtained from a set of ModelParameters + */ +trait ParameterCollection { def get(s: String): ParameterType } +/** + * A class representing the prior and posterior parameters of a model + */ class ModelParameters extends ElementCollection { + /** + * Add a parameter to the collection + */ override def add[T](element: Element[T]) = { element match { case p: Parameter[T] => super.add(p) + case default => { + //Do not add non-parameter elements to the set of model parameters. + } } } - - + + /** + * Convert the contents of to a list of parameter elements + */ def convertToParameterList: List[Parameter[_]] = { val l = ListBuffer.empty[Parameter[_]] for (p <- this.namedElements) { p match { case a: Parameter[_] => { - l += a + l += a } - case _ => { - + case default => { + //Do not add non-parameter elements to the result list. This should never be reached. } } } l.toList } - - - private class PriorParameterCollection extends ParameterCollection { + + private object PriorParameterCollection extends ParameterCollection { def get(s: String): ParameterType = { - val p = ModelParameters.super.getElementByReference(s) + val p = getElementByReference(s) val result = p match { case p: Parameter[_] => ParameterType(p) + case default => throw new IllegalArgumentException("Cannot retrieve non-parameter elements from parameter collection.") + } + result + } + + def apply(s: String): ParameterType = { + val p = getElementByReference(s) + val result = p match { + case p: Parameter[_] => ParameterType(p) + case default => throw new IllegalArgumentException("Cannot retrieve non-parameter elements from parameter collection.") } result } } - - private class PosteriorParameterCollection extends ParameterCollection { - def get(s: String): ParameterType = { - val p = ModelParameters.super.getElementByReference(s) - val result = p match {//Kind of lame to handle every parameter individually... - case x: Parameter[_] => { + + private object PosteriorParameterCollection extends ParameterCollection { + def get(s: String): ParameterType = { + val p = getElementByReference(s) + val result = p match { + case x: Parameter[_] => { x match { case d: DoubleParameter => ParameterType(d.MAPValue) case a: ArrayParameter => ParameterType(a.MAPValue) + case default => throw new IllegalArgumentException("Cannot retrieve non-parameter elements from parameter collection.") } } } - result + result + } + + def apply(s: String): ParameterType = { + val p = getElementByReference(s) + val result = p match { + case x: Parameter[_] => { + x match { + case d: DoubleParameter => ParameterType(d.MAPValue) + case a: ArrayParameter => ParameterType(a.MAPValue) + case default => throw new IllegalArgumentException("Cannot retrieve non-parameter elements from parameter collection.") + } + } + } + result } } + /** + * Get the collection of prior parameters + */ def priorParameters: ParameterCollection = { - val p = new PriorParameterCollection() + val p = PriorParameterCollection p } + /** + * Get the collection of posterior parameters (after learning) + */ def posteriorParameters: ParameterCollection = { - val p = new PosteriorParameterCollection() + val p = PosteriorParameterCollection p } } object ModelParameters { + /** + * Create a new set of model parameters. + */ def apply() = new ModelParameters() } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/util/HashMultiSet.scala b/Figaro/src/main/scala/com/cra/figaro/util/HashMultiSet.scala index 45465869..5c71305c 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/HashMultiSet.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/HashMultiSet.scala @@ -16,7 +16,7 @@ package com.cra.figaro.util import scala.collection.mutable.HashMap /** - * An implementation of a MultiSet backed by a HashMap + * An implementation of a MultiSet backed by a HashMap. */ class HashMultiSet[T] extends MultiSet[T] { private val map = HashMap[T, Int]() @@ -76,7 +76,7 @@ class HashMultiSet[T] extends MultiSet[T] { object HashMultiSet { /** - * Create a new HashMultiSet and adds the given values + * Create a new HashMultiSet and adds the given values. */ def apply[T](elems: T*): HashMultiSet[T] = { val result = new HashMultiSet[T]() diff --git a/Figaro/src/main/scala/com/cra/figaro/util/HeapPriorityMap.scala b/Figaro/src/main/scala/com/cra/figaro/util/HeapPriorityMap.scala index 025d06d7..cf9e550f 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/HeapPriorityMap.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/HeapPriorityMap.scala @@ -104,7 +104,7 @@ class HeapPriorityMap[T, U]()(implicit ord: Ordering[U]) extends PriorityMap[T, else sortHelper(extractMin() :: accum) /** - * Make a copy of this HeapPriorityMap, returning a new HeapPriorityMap + * Make a copy of this HeapPriorityMap, returning a new HeapPriorityMap. */ override def clone: HeapPriorityMap[T, U] = { val result = new HeapPriorityMap[T, U]() diff --git a/Figaro/src/main/scala/com/cra/figaro/util/MultiSet.scala b/Figaro/src/main/scala/com/cra/figaro/util/MultiSet.scala index 76ccbe83..b5a5e339 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/MultiSet.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/MultiSet.scala @@ -20,32 +20,32 @@ package com.cra.figaro.util */ trait MultiSet[T] extends Traversable[T] { /** - * Return the number of instances of the value in the Set + * Return the number of instances of the value in the set. */ def apply(t: T): Int /** - * Add an instance of this value to the set + * Add an instance of this value to the set. */ def addOne(t: T): MultiSet[T] /** - * Add several instances of this value to the set + * Add several instances of this value to the set. */ def addMany(t: T, count: Int): MultiSet[T] /** - * Remove one instance of this value from the set + * Remove one instance of this value from the set. */ def removeOne(t: T): MultiSet[T] - /** Remove all instances of this value from the set + /** Remove all instances of this value from the set. * */ def removeAll(t: T): MultiSet[T] /** - * Return the union of another set with this set + * Return the union of another set with this set. */ def union(that: MultiSet[T]): MultiSet[T] @@ -72,7 +72,7 @@ trait MultiSet[T] extends Traversable[T] { override def hashCode: Int = counts.hashCode /** - * Returns true if the counts are the same + * Returns true if the counts are the same. */ override def equals(that: Any): Boolean = { try { diff --git a/Figaro/src/main/scala/com/cra/figaro/util/Resampler.scala b/Figaro/src/main/scala/com/cra/figaro/util/Resampler.scala index eb621d2c..8bce00ba 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/Resampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/Resampler.scala @@ -18,7 +18,7 @@ import java.util.TreeMap /** * A resampler allows resampling from a set of weighted samples provided as the inputs. * - * @param inputs A sequence of pairs of proability and values. + * @param inputs A sequence of pairs of probability and values. */ abstract class Resampler[T](inputs: Seq[(Double, T)]) { /** @@ -32,7 +32,7 @@ abstract class Resampler[T](inputs: Seq[(Double, T)]) { * Creating the resampler takes O(N log N) * time, where N is the number of input samples, and selecting M samples takes O(M log N). * - * @param inputs A sequence of pairs of proability and values. + * @param inputs A sequence of pairs of probability and values. */ class MapResampler[T](inputs: Seq[(Double, T)]) extends Resampler(inputs) { // In this design, we create a map that maps a uniform random number to the sample that should be chosen. diff --git a/Figaro/src/main/scala/com/cra/figaro/util/package.scala b/Figaro/src/main/scala/com/cra/figaro/util/package.scala index 2225f683..4306f2a9 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/package.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/package.scala @@ -252,12 +252,15 @@ package object util { /** - * Sums two probabilities in log space + * Sums two probabilities in log space. */ def logSum(p1: Double, p2: Double): Double = { logSumMany(List(p1, p2)) } + /** + * Computes the sum of many probabilities in log space. + */ def logSumMany(xs: Traversable[Double]): Double = { val max = xs.foldLeft(Double.NegativeInfinity)(_ max _) if (max == Double.NegativeInfinity) Double.NegativeInfinity diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AbstractionTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AbstractionTest.scala index 592189f2..e0855411 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AbstractionTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AbstractionTest.scala @@ -1,355 +1,356 @@ -/* - * AbstractionTest.scala - * Abstraction tests. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.test.algorithm - -import org.scalatest.Matchers -import org.scalatest.WordSpec -import com.cra.figaro.algorithm._ -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.lazyfactored._ -import com.cra.figaro.language._ -import com.cra.figaro.library.atomic.continuous._ -import com.cra.figaro.library.compound._ -import com.cra.figaro.test.tags.NonDeterministic - -class AbstractionTest extends WordSpec with Matchers { - "A regular discretization" should { - "select to an ordered range with the specified number of elements in which the bins " + - "are of equal size" in { - Universe.createNew() - val inputs = List(0.2, 0.6, 0.3, 0.4, 0.8) - val points = AbstractionScheme.regularDiscretization.select(inputs, 3).toList - points.length should equal(3) - points(0) should be(0.3 +- 0.00000001) - points(1) should be(0.5 +- 0.00000001) - points(2) should be(0.7 +- 0.00000001) - } - } - - "A Double element when adding abstraction pragmas without providing an explicit abstraction scheme" should { - "have a pragma after adding it" in { - Universe.createNew() - val u = Uniform(0.0, 1.0) - val a = Abstraction[Double](10) - u.addPragma(a) - assert(u.pragmas contains a) - } - - "have an added pragma's abstraction scheme be discretization" in { - Universe.createNew() - val u = Uniform(0.0, 1.0) - val a = Abstraction[Double](10) - u.addPragma(a) - assert(a.scheme == AbstractionScheme.regularDiscretization) - } - - "have fromPragmas return the most recently added abstraction" in { - Universe.createNew() - val u = Uniform(0.0, 1.0) - val a1 = Abstraction[Double](10) - val a2 = Abstraction[Double](5) - u.addPragma(a1) - u.addPragma(a2) - u.addPragma(new Pragma[Double] {}) - Abstraction.fromPragmas(u.pragmas) should equal(Some(a2)) - } - - "have fromPragmas return None if no abstractions have been added" in { - Universe.createNew() - val u = Uniform(0.0, 1.0) - u.addPragma(new Pragma[Double] {}) - Abstraction.fromPragmas(u.pragmas) should equal(None) - } - } - - "Computing the values of an abstract element" when { - "given an atomic element using a regular discretization scheme" should { - "produce an sequence of the correct size of uniformly distributed values" taggedAs(NonDeterministic) in { - Universe.createNew() - val u = Uniform(0.0, 1.0) - u.addPragma(Abstraction(100)(AbstractionScheme.RegularDiscretization)) - val v = Values()(u).toList.sorted - v.length should equal(100) - - v.sorted should equal(v) - v(0) should be(0.005 +- 0.005) - v(99) should be(0.995 +- 0.005) - val diff = v(1) - v(0) - for { i <- 2 to 99 } { v(i) - v(i - 1) should be(diff +- 0.0000001) } - } - } - - "given a chain whose results are atomic elements using a regular discretization scheme" should { - "produce a sequence ranging from the minimum to maximum of all the results" in { - Universe.createNew() - def f(d: Double) = { - val u = Uniform(d, d + 1) - u.addPragma(Abstraction(100)(AbstractionScheme.RegularDiscretization)) - u - } - val c = Chain(Select(0.5 -> 0.0, 0.5 -> 3.0), f) - c.addPragma(Abstraction(20)(AbstractionScheme.RegularDiscretization)) - - val v = Values()(c).toList.sorted - v.head should be(0.1 +- 0.02) - v.last should be(3.9 +- 0.02) - } - } - - "given an Apply2 whose arguments are atomic elements using a regular discretization scheme" should { - "produce a sequence ranging from the minimum to maximum of all the results" in { - Universe.createNew() - val u = Uniform(0.0, 2.0) - u.addPragma(Abstraction(200)(AbstractionScheme.RegularDiscretization)) - val a = Apply(u, u, ((d1: Double, d2: Double) => d1 * d2)) - a.addPragma(Abstraction(20)(AbstractionScheme.RegularDiscretization)) - - val v = Values()(a).toList.sorted - v.head should be(0.1 +- 0.05) - v.last should be(3.9 +- 0.05) - } - } - } - - "Creating a list of factors for an abstract element" when { - "given an atomic argument using a regular discretization scheme" should { - "produce a single factor whose rows correspond to the discrete values with their associated densities" in { - Universe.createNew() - val max = 2.0 - val numBins = 20 - val uniform = Uniform(0.0, max) - uniform.addPragma(Abstraction(numBins)(AbstractionScheme.RegularDiscretization)) - Values()(uniform) - val factors = Factory.make(uniform) - factors.size should equal(1) - val factor = factors(0) - val variable = Variable(uniform) - factor.variables should equal(List(variable)) - val allIndices = factor.allIndices - allIndices.size should equal(numBins) - for { indices <- allIndices } { - factor.get(indices) should be(1.0 / max +- 0.000001) // constant density of Uniform(0, max) - } - } - } - - "given an apply of one argument using a regular discretization scheme" should { - "produce a single factor mapping the argument to the discretized result" in { - Universe.createNew() - val max = 2.0 - val numBinsUniform = 20 - val numBinsApply = 10 - val uniform = Uniform(0.0, max) - uniform.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val apply = Apply(uniform, (d: Double) => d * d) - apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) - Values()(apply) - val factors = Factory.make(apply) - factors.size should equal(1) - val factor = factors(0) - val uniformVariable = Variable(uniform) - val applyVariable = Variable(apply) - factor.variables should equal(List(uniformVariable, applyVariable)) - factor.allIndices.size should equal(numBinsUniform * numBinsApply) - val uniformValues: List[Double] = uniformVariable.range.map(_.value) - val applyValues: List[Double] = applyVariable.range.map(_.value) - def check(uniformValue: Double, applyValue: Double): Boolean = { - val resultValue = uniformValue * uniformValue - def minDiff: Double = - (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) - math.abs(resultValue - applyValue) <= minDiff - } - for { - i <- 0 until numBinsUniform - j <- 0 until numBinsApply - } { - if (check(uniformValues(i), applyValues(j))) { factor.get(List(i, j)) should equal(1.0) } - else { factor.get(List(i, j)) should equal(0.0) } - } - } - } - - "given an apply of two arguments using a regular discretization scheme" should { - "produce a single factor mapping the arguments to the discretized result" in { - Universe.createNew() - val max = 2.0 - val numBinsUniform = 5 - val numBinsApply = 10 - val uniform1 = Uniform(0.0, max) - uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val uniform2 = Uniform(0.0, max) - uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val apply = Apply(uniform1, uniform2, (d1: Double, d2: Double) => d1 * d2) - apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) - Values()(apply) - val factors = Factory.make(apply) - factors.size should equal(1) - val factor = factors(0) - val uniform1Variable = Variable(uniform1) - val uniform2Variable = Variable(uniform2) - val applyVariable = Variable(apply) - factor.variables should equal(List(uniform1Variable, uniform2Variable, applyVariable)) - factor.allIndices.size should equal(numBinsUniform * numBinsUniform * numBinsApply) - val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) - val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) - val applyValues: List[Double] = applyVariable.range.map(_.value) - def check(uniform1Value: Double, uniform2Value: Double, applyValue: Double): Boolean = { - val resultValue = uniform1Value * uniform2Value - def minDiff: Double = - (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) - math.abs(resultValue - applyValue) <= minDiff - } - for { - i <- 0 until numBinsUniform - j <- 0 until numBinsUniform - k <- 0 until numBinsApply - } { - if (check(uniform1Values(i), uniform2Values(j), applyValues(k))) { - factor.get(List(i, j, k)) should equal(1.0) - } else { factor.get(List(i, j, k)) should equal(0.0) } - } - } - } - - "given an apply of three arguments using a regular discretization scheme" should { - "produce a single factor mapping the arguments to the discretized result" in { - Universe.createNew() - val max = 2.0 - val numBinsUniform = 5 - val numBinsApply = 10 - val uniform1 = Uniform(0.0, max) - uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val uniform2 = Uniform(0.0, max) - uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val uniform3 = Uniform(0.0, max) - uniform3.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val apply = Apply(uniform1, uniform2, uniform3, (d1: Double, d2: Double, d3: Double) => d1 * d2 * d3) - apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) - Values()(apply) - val factors = Factory.make(apply) - factors.size should equal(1) - val factor = factors(0) - val uniform1Variable = Variable(uniform1) - val uniform2Variable = Variable(uniform2) - val uniform3Variable = Variable(uniform3) - val applyVariable = Variable(apply) - factor.variables should equal(List(uniform1Variable, uniform2Variable, uniform3Variable, applyVariable)) - factor.allIndices.size should equal(numBinsUniform * numBinsUniform * numBinsUniform * numBinsApply) - val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) - val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) - val uniform3Values: List[Double] = uniform3Variable.range.map(_.value) - val applyValues: List[Double] = applyVariable.range.map(_.value) - def check(uniform1Value: Double, uniform2Value: Double, uniform3Value: Double, applyValue: Double): Boolean = { - val resultValue = uniform1Value * uniform2Value * uniform3Value - def minDiff: Double = - (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) - math.abs(resultValue - applyValue) <= minDiff - } - for { - i <- 0 until numBinsUniform - j <- 0 until numBinsUniform - k <- 0 until numBinsUniform - l <- 0 until numBinsApply - } { - if (check(uniform1Values(i), uniform2Values(j), uniform3Values(k), applyValues(l))) { - factor.get(List(i, j, k, l)) should equal(1.0) - } else { factor.get(List(i, j, k, l)) should equal(0.0) } - } - } - } - - "given a chain using a regular discretization scheme" should { - "produce a list of conditional selector factors mapping the argument to the discretized result" in { - Universe.createNew() - val numBinsUniform = 3 - val numBinsChain = 4 - val flip = Flip(0.5) - val uniform1 = Uniform(0.0, 1.0) - val uniform2 = Uniform(1.0, 2.0) - uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) - val chain = Chain(flip, (b: Boolean) => if (b) uniform1; else uniform2) - chain.addPragma(Abstraction(numBinsChain)(AbstractionScheme.RegularDiscretization)) - Values()(chain) - val factors = Factory.make(chain) - factors.size should equal(2) // 2 for the conditional selectors - val List(factor1, factor2) = factors - val flipVariable = Variable(flip) - val uniform1Variable = Variable(uniform1) - val uniform2Variable = Variable(uniform2) - val chainVariable = Variable(chain) - factor1.variables should equal(List(flipVariable, chainVariable, uniform1Variable)) - factor1.allIndices.size should equal(2 * numBinsChain * numBinsUniform) - factor2.allIndices.size should equal(2 * numBinsChain * numBinsUniform) - val flipValues: List[Boolean] = flipVariable.range.map(_.value) - val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) - val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) - val chainValues: List[Double] = chainVariable.range.map(_.value) - def closest(chainValue: Double, uniformValue: Double): Boolean = { - def minDiff: Double = - (Double.MaxValue /: chainValues)((d1: Double, d2: Double) => d1 min math.abs(uniformValue - d2)) - math.abs(uniformValue - chainValue) <= minDiff - } - def check1(flipValue: Boolean, chainValue: Double, uniformValue: Double): Boolean = - !flipValue || closest(chainValue, uniformValue) - def check2(flipValue: Boolean, chainValue: Double, uniformValue: Double): Boolean = - flipValue || closest(chainValue, uniformValue) - for { - i <- 0 to 1 - j <- 0 until numBinsChain - k <- 0 until numBinsUniform - } { - if (check1(flipValues(i), chainValues(j), uniform1Values(k))) { factor1.get(List(i, j, k)) should equal(1.0) } - else { factor1.get(List(i, j, k)) should equal(0.0) } - } - for { - i <- 0 to 1 - j <- 0 until numBinsChain - k <- 0 until numBinsUniform - } { - if (check2(flipValues(i), chainValues(j), uniform2Values(k))) { factor2.get(List(i, j, k)) should equal(1.0) } - else { factor2.get(List(i, j, k)) should equal(0.0) } - } - } - } - } - - "Running variable elimination on a model with multiple discretized elements" should { - "produce the correct answer" taggedAs(NonDeterministic) in { - Universe.createNew() - val flip = Flip(0.5) - val uniform1 = Uniform(0.0, 1.0) - val uniform2 = Uniform(1.0, 2.0) - val chain = If(flip, uniform1, uniform2) - val apply = Apply(chain, (d: Double) => d + 1.0) - apply.addConstraint((d: Double) => d) - uniform1.addPragma(Abstraction(10)) - uniform2.addPragma(Abstraction(10)) - chain.addPragma(Abstraction(10)) - apply.addPragma(Abstraction(10)) - val ve = VariableElimination(flip) - ve.start() - /* - * Probability computation: - * Undiscretized: - * Uniform1 will result in (1,2) with expected weight 1.5. - * Uniform2 will result in (2,3) with expected weight 2.5. - * Therefore flip should be around 0.4 for true. - */ - ve.probability(flip, (b: Boolean) => b) should be(0.4 +- 0.02) - ve.kill - } - } -} - +/* + * AbstractionTest.scala + * Abstraction tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm + +import org.scalatest.Matchers +import org.scalatest.WordSpec +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored._ +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.compound._ +import com.cra.figaro.test.tags.NonDeterministic + +class AbstractionTest extends WordSpec with Matchers { + "A regular discretization" should { + "select to an ordered range with the specified number of elements in which the bins " + + "are of equal size" in { + Universe.createNew() + val inputs = List(0.2, 0.6, 0.3, 0.4, 0.8) + val points = AbstractionScheme.regularDiscretization.select(inputs, 3).toList + points.length should equal(3) + points(0) should be(0.3 +- 0.00000001) + points(1) should be(0.5 +- 0.00000001) + points(2) should be(0.7 +- 0.00000001) + } + } + + "A Double element when adding abstraction pragmas without providing an explicit abstraction scheme" should { + "have a pragma after adding it" in { + Universe.createNew() + val u = Uniform(0.0, 1.0) + val a = Abstraction[Double](10) + u.addPragma(a) + assert(u.pragmas contains a) + } + + "have an added pragma's abstraction scheme be discretization" in { + Universe.createNew() + val u = Uniform(0.0, 1.0) + val a = Abstraction[Double](10) + u.addPragma(a) + assert(a.scheme == AbstractionScheme.regularDiscretization) + } + + "have fromPragmas return the most recently added abstraction" in { + Universe.createNew() + val u = Uniform(0.0, 1.0) + val a1 = Abstraction[Double](10) + val a2 = Abstraction[Double](5) + u.addPragma(a1) + u.addPragma(a2) + u.addPragma(new Pragma[Double] {}) + Abstraction.fromPragmas(u.pragmas) should equal(Some(a2)) + } + + "have fromPragmas return None if no abstractions have been added" in { + Universe.createNew() + val u = Uniform(0.0, 1.0) + u.addPragma(new Pragma[Double] {}) + Abstraction.fromPragmas(u.pragmas) should equal(None) + } + } + + "Computing the values of an abstract element" when { + "given an atomic element using a regular discretization scheme" should { + "produce an sequence of the correct size of uniformly distributed values" taggedAs(NonDeterministic) in { + Universe.createNew() + val u = Uniform(0.0, 1.0) + u.addPragma(Abstraction(100)(AbstractionScheme.RegularDiscretization)) + val v = Values()(u).toList.sorted + v.length should equal(100) + + v.sorted should equal(v) + v(0) should be(0.005 +- 0.005) + v(99) should be(0.995 +- 0.005) + val diff = v(1) - v(0) + for { i <- 2 to 99 } { v(i) - v(i - 1) should be(diff +- 0.0000001) } + } + } + + "given a chain whose results are atomic elements using a regular discretization scheme" should { + "produce a sequence ranging from the minimum to maximum of all the results" in { + Universe.createNew() + def f(d: Double) = { + val u = Uniform(d, d + 1) + u.addPragma(Abstraction(100)(AbstractionScheme.RegularDiscretization)) + u + } + val c = Chain(Select(0.5 -> 0.0, 0.5 -> 3.0), f) + c.addPragma(Abstraction(20)(AbstractionScheme.RegularDiscretization)) + + val v = Values()(c).toList.sorted + v.head should be(0.1 +- 0.02) + v.last should be(3.9 +- 0.02) + } + } + + "given an Apply2 whose arguments are atomic elements using a regular discretization scheme" should { + "produce a sequence ranging from the minimum to maximum of all the results" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + u.addPragma(Abstraction(200)(AbstractionScheme.RegularDiscretization)) + val a = Apply(u, u, ((d1: Double, d2: Double) => d1 * d2)) + a.addPragma(Abstraction(20)(AbstractionScheme.RegularDiscretization)) + + val v = Values()(a).toList.sorted + v.head should be(0.1 +- 0.05) + v.last should be(3.9 +- 0.05) + } + } + } + + "Creating a list of factors for an abstract element" when { + "given an atomic argument using a regular discretization scheme" should { + "produce a single factor whose rows correspond to the discrete values with their associated densities" in { + Universe.createNew() + val max = 2.0 + val numBins = 20 + val uniform = Uniform(0.0, max) + uniform.addPragma(Abstraction(numBins)(AbstractionScheme.RegularDiscretization)) + Values()(uniform) + val factors = Factory.make(uniform) + factors.size should equal(1) + val factor = factors(0) + val variable = Variable(uniform) + factor.variables should equal(List(variable)) + val allIndices = factor.allIndices + allIndices.size should equal(numBins) + for { indices <- allIndices } { + factor.get(indices) should be(1.0 / max +- 0.000001) // constant density of Uniform(0, max) + } + } + } + + "given an apply of one argument using a regular discretization scheme" should { + "produce a single factor mapping the argument to the discretized result" in { + Universe.createNew() + val max = 2.0 + val numBinsUniform = 20 + val numBinsApply = 10 + val uniform = Uniform(0.0, max) + uniform.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val apply = Apply(uniform, (d: Double) => d * d) + apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) + Values()(apply) + val factors = Factory.make(apply) + factors.size should equal(1) + val factor = factors(0) + val uniformVariable = Variable(uniform) + val applyVariable = Variable(apply) + factor.variables should equal(List(uniformVariable, applyVariable)) + factor.allIndices.size should equal(numBinsUniform * numBinsApply) + val uniformValues: List[Double] = uniformVariable.range.map(_.value) + val applyValues: List[Double] = applyVariable.range.map(_.value) + def check(uniformValue: Double, applyValue: Double): Boolean = { + val resultValue = uniformValue * uniformValue + def minDiff: Double = + (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) + math.abs(resultValue - applyValue) <= minDiff + } + for { + i <- 0 until numBinsUniform + j <- 0 until numBinsApply + } { + if (check(uniformValues(i), applyValues(j))) { factor.get(List(i, j)) should equal(1.0) } + else { factor.get(List(i, j)) should equal(0.0) } + } + } + } + + "given an apply of two arguments using a regular discretization scheme" should { + "produce a single factor mapping the arguments to the discretized result" in { + Universe.createNew() + val max = 2.0 + val numBinsUniform = 5 + val numBinsApply = 10 + val uniform1 = Uniform(0.0, max) + uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val uniform2 = Uniform(0.0, max) + uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val apply = Apply(uniform1, uniform2, (d1: Double, d2: Double) => d1 * d2) + apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) + Values()(apply) + val factors = Factory.make(apply) + factors.size should equal(1) + val factor = factors(0) + val uniform1Variable = Variable(uniform1) + val uniform2Variable = Variable(uniform2) + val applyVariable = Variable(apply) + factor.variables should equal(List(uniform1Variable, uniform2Variable, applyVariable)) + factor.allIndices.size should equal(numBinsUniform * numBinsUniform * numBinsApply) + val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) + val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) + val applyValues: List[Double] = applyVariable.range.map(_.value) + def check(uniform1Value: Double, uniform2Value: Double, applyValue: Double): Boolean = { + val resultValue = uniform1Value * uniform2Value + def minDiff: Double = + (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) + math.abs(resultValue - applyValue) <= minDiff + } + for { + i <- 0 until numBinsUniform + j <- 0 until numBinsUniform + k <- 0 until numBinsApply + } { + if (check(uniform1Values(i), uniform2Values(j), applyValues(k))) { + factor.get(List(i, j, k)) should equal(1.0) + } else { factor.get(List(i, j, k)) should equal(0.0) } + } + } + } + + "given an apply of three arguments using a regular discretization scheme" should { + "produce a single factor mapping the arguments to the discretized result" in { + Universe.createNew() + val max = 2.0 + val numBinsUniform = 5 + val numBinsApply = 10 + val uniform1 = Uniform(0.0, max) + uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val uniform2 = Uniform(0.0, max) + uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val uniform3 = Uniform(0.0, max) + uniform3.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val apply = Apply(uniform1, uniform2, uniform3, (d1: Double, d2: Double, d3: Double) => d1 * d2 * d3) + apply.addPragma(Abstraction(numBinsApply)(AbstractionScheme.RegularDiscretization)) + Values()(apply) + val factors = Factory.make(apply) + factors.size should equal(1) + val factor = factors(0) + val uniform1Variable = Variable(uniform1) + val uniform2Variable = Variable(uniform2) + val uniform3Variable = Variable(uniform3) + val applyVariable = Variable(apply) + factor.variables should equal(List(uniform1Variable, uniform2Variable, uniform3Variable, applyVariable)) + factor.allIndices.size should equal(numBinsUniform * numBinsUniform * numBinsUniform * numBinsApply) + val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) + val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) + val uniform3Values: List[Double] = uniform3Variable.range.map(_.value) + val applyValues: List[Double] = applyVariable.range.map(_.value) + def check(uniform1Value: Double, uniform2Value: Double, uniform3Value: Double, applyValue: Double): Boolean = { + val resultValue = uniform1Value * uniform2Value * uniform3Value + def minDiff: Double = + (Double.MaxValue /: applyValues)((d1: Double, d2: Double) => d1 min math.abs(resultValue - d2)) + math.abs(resultValue - applyValue) <= minDiff + } + for { + i <- 0 until numBinsUniform + j <- 0 until numBinsUniform + k <- 0 until numBinsUniform + l <- 0 until numBinsApply + } { + if (check(uniform1Values(i), uniform2Values(j), uniform3Values(k), applyValues(l))) { + factor.get(List(i, j, k, l)) should equal(1.0) + } else { factor.get(List(i, j, k, l)) should equal(0.0) } + } + } + } + + "given a chain using a regular discretization scheme" should { + "produce a list of conditional selector factors mapping the argument to the discretized result" in { + Universe.createNew() + val numBinsUniform = 3 + val numBinsChain = 4 + val flip = Flip(0.5) + val uniform1 = Uniform(0.0, 1.0) + val uniform2 = Uniform(1.0, 2.0) + uniform1.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + uniform2.addPragma(Abstraction(numBinsUniform)(AbstractionScheme.RegularDiscretization)) + val chain = Chain(flip, (b: Boolean) => if (b) uniform1; else uniform2) + chain.addPragma(Abstraction(numBinsChain)(AbstractionScheme.RegularDiscretization)) + Values()(chain) + val factors = Factory.make(chain) + factors.size should equal(2) // 2 for the conditional selectors + val List(factor1, factor2) = factors + val flipVariable = Variable(flip) + val uniform1Variable = Variable(uniform1) + val uniform2Variable = Variable(uniform2) + val chainVariable = Variable(chain) + factor1.variables should equal(List(flipVariable, uniform1Variable, chainVariable)) + factor1.allIndices.size should equal(2 * numBinsChain * numBinsUniform) + factor2.allIndices.size should equal(2 * numBinsChain * numBinsUniform) + val flipValues: List[Boolean] = flipVariable.range.map(_.value) + val uniform1Values: List[Double] = uniform1Variable.range.map(_.value) + val uniform2Values: List[Double] = uniform2Variable.range.map(_.value) + val chainValues: List[Double] = chainVariable.range.map(_.value) + def closest(chainValue: Double, uniformValue: Double): Boolean = { + def minDiff: Double = + (Double.MaxValue /: chainValues)((d1: Double, d2: Double) => d1 min math.abs(uniformValue - d2)) + math.abs(uniformValue - chainValue) <= minDiff + } + def check1(flipValue: Boolean, uniformValue: Double, chainValue: Double): Boolean = + !flipValue || closest(chainValue, uniformValue) + def check2(flipValue: Boolean, uniformValue: Double, chainValue: Double): Boolean = + flipValue || closest(chainValue, uniformValue) + for { + i <- 0 to 1 + j <- 0 until numBinsUniform + k <- 0 until numBinsChain + } { + if (check1(flipValues(i), uniform1Values(j), chainValues(k))) { factor1.get(List(i, j, k)) should equal(1.0) } + else { factor1.get(List(i, j, k)) should equal(0.0) } + } + for { + i <- 0 to 1 + j <- 0 until numBinsUniform + k <- 0 until numBinsChain + } { + if (check2(flipValues(i), uniform2Values(j), chainValues(k))) { factor2.get(List(i, j, k)) should equal(1.0) } + else { factor2.get(List(i, j, k)) should equal(0.0) } + } + } + } + } + + "Running variable elimination on a model with multiple discretized elements" should { + "produce the correct answer" taggedAs(NonDeterministic) in { + Universe.createNew() + val flip = Flip(0.5) + val uniform1 = Uniform(0.0, 1.0) + val uniform2 = Uniform(1.0, 2.0) + val chain = If(flip, uniform1, uniform2) + val apply = Apply(chain, (d: Double) => d + 1.0) + apply.addConstraint((d: Double) => d) + uniform1.addPragma(Abstraction(10)) + uniform2.addPragma(Abstraction(10)) + chain.addPragma(Abstraction(10)) + apply.addPragma(Abstraction(10)) + val ve = VariableElimination(flip) + ve.start() + /* + * Probability computation: + * Undiscretized: + * Uniform1 will result in (1,2) with expected weight 1.5. + * Uniform2 will result in (2,3) with expected weight 2.5. + * Therefore flip should be around 0.4 for true. + */ + ve.probability(flip, (b: Boolean) => b) should be(0.4 +- 0.02) + ve.kill + } + } +} + diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/decision/DecisionVETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/decision/DecisionVETest.scala index 2ceb378d..21bf24ee 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/decision/DecisionVETest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/decision/DecisionVETest.scala @@ -20,7 +20,6 @@ import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.decision._ import com.cra.figaro.algorithm.factored._ import com.cra.figaro.algorithm.lazyfactored._ -import Factory._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.test.tags.Performance import com.cra.figaro.library.decision._ diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/BPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/BPTest.scala index a415a46b..85fb0904 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/BPTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/BPTest.scala @@ -1,327 +1,328 @@ -/* - * BPTest.scala - * Belief Propagation tests. - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Jan 15, 2014 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.test.algorithm.factored - -import scala.collection.mutable.Map -import org.scalatest.WordSpec -import org.scalatest.Matchers -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.factored.beliefpropagation._ -import com.cra.figaro.language._ -import com.cra.figaro.library.compound.If -import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} -import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} -import com.cra.figaro.library.compound.IntSelector -import com.cra.figaro.algorithm.lazyfactored.LazyValues -import com.cra.figaro.algorithm.UnsupportedAlgorithmException - -class BPTest extends WordSpec with Matchers { - - val globalTol = 0.025 - - "A basic factor graph" should { - "Create nodes for all factors and variables" in { - Universe.createNew - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - val semiring = SumProductSemiring - LazyValues(Universe.universe).expandAll(Universe.universe.activeElements.toSet.map((elem: Element[_]) => ((elem, Integer.MAX_VALUE)))) - val factors = Universe.universe.activeElements flatMap (Factory.make(_)) - val graph = new BasicFactorGraph(factors, semiring) - val fn = graph.adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) - val vn = graph.adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) - - fn.size should equal(6) - vn.size should equal(5) - } - - "Create an edge to between each factor and the variables it has" in { - Universe.createNew - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - val semiring = SumProductSemiring - LazyValues(Universe.universe).expandAll(Universe.universe.activeElements.toSet.map((elem: Element[_]) => ((elem, Integer.MAX_VALUE)))) - val factors = Universe.universe.activeElements flatMap (Factory.make(_)) - val graph = new BasicFactorGraph(factors, semiring) - val fn = graph.adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) - val vn = graph.adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) - - fn.foreach(n => { - val vars = n._1.asInstanceOf[FactorNode].variables.toSet - val edges = n._2.keys.map(v => v.asInstanceOf[VariableNode].variable).toSet - vars should equal(edges) - n._2.keys.foreach(v => graph.adjacencyList(v).contains(n._1) should equal(true)) - }) - - } - - } - - "Running BeliefPropagation" should { - "Send the correct type of message" in { - Universe.createNew - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - val bp = BeliefPropagation(3) - bp.start - val fn = bp.factorGraph.asInstanceOf[BasicFactorGraph].adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) - val vn = bp.factorGraph.asInstanceOf[BasicFactorGraph].adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) - - fn.foreach { s => - s._2.foreach { d => - val msg = bp.newMessage(s._1, d._1) - msg.variables should equal(List(d._1.asInstanceOf[VariableNode].variable)) - } - } - - vn.foreach { s => - s._2.foreach { d => - val msg = bp.newMessage(s._1, d._1) - msg.variables should equal(List(s._1.asInstanceOf[VariableNode].variable)) - } - } - } - - /* Due to the way that factors are implemented for Chain, all - * models that use chain will result in loops. To test a non-loopy - * graph we have to not use chain, which IntSelector does not. - */ - "with no loops in the factor graph give exact results" in { - Universe.createNew() - val e1 = DUniform(2, 3, 4) - val e2 = IntSelector(e1) - - val bp = BeliefPropagation(3, e2) - bp.start() - - val e2_0 = 0.33333333 * (0.5 + 0.3333333 + 0.25) - val e2_1 = 0.33333333 * (0.5 + 0.3333333 + 0.25) - val e2_2 = 0.33333333 * (0 + 0.3333333 + 0.25) - val e2_3 = 0.33333333 * (0 + 0 + 0.25) - - val tol = 0.000001 - bp.probability(e2, (i: Int) => i == 0) should be(e2_0 +- tol) - bp.probability(e2, (i: Int) => i == 1) should be(e2_1 +- tol) - bp.probability(e2, (i: Int) => i == 2) should be(e2_2 +- tol) - bp.probability(e2, (i: Int) => i == 3) should be(e2_3 +- tol) - } - - "with no conditions or constraints produce the correct result" in { - Universe.createNew() - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - test(f, (b: Boolean) => b, 0.6, globalTol) - } - - "with a condition on a dependent element produce the result with the correct probability" in { - Universe.createNew() - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - a.setCondition((i: Int) => i == 2) - // U(true) = \int_{0.2}^{1.0) 0.7 p = 0.35 * 0.96 - // U(false) = \int_{0.2}^{1.0) (1-p) - val u1 = 0.35 * 0.96 - val u2 = 0.32 - test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) - } - - "with a constraint on a dependent element produce the result with the correct probability" in { - Universe.createNew() - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - a.setConstraint((i: Int) => i.toDouble) - // U(true) = \int_{0.2}^{1.0} (0.3 + 2 * 0.7) p = 0.85 * 0.96 - // U(false) = \int_{0.2}^(1.0) (2 * (1-p)) = 0.64 - val u1 = 0.85 * 0.96 - val u2 = 0.64 - test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) - } - - "with an element that uses another element multiple times, " + - "always produce the same value for the different uses" in { - Universe.createNew() - val f = Flip(0.5) - val e = f === f - test(e, (b: Boolean) => b, 1.0, globalTol) - } - - "with a constraint on an element that is used multiple times, only factor in the constraint once" in { - Universe.createNew() - val f1 = Flip(0.5) - val f2 = Flip(0.3) - val e1 = f1 === f1 - val e2 = f1 === f2 - val d = Dist(0.5 -> e1, 0.5 -> e2) - f1.setConstraint((b: Boolean) => if (b) 3.0; else 2.0) - // Probability that f1 is true = 0.6 - // Probability that e1 is true = 1.0 - // Probability that e2 is true = 0.6 * 0.3 + 0.4 * 0.7 = 0.46 - // Probability that d is true = 0.5 * 1 + 0.5 * 0.46 = 0.73 - test(d, (b: Boolean) => b, 0.73, globalTol) - } - - "on a different universe from the current universe, produce the correct result" in { - val u1 = Universe.createNew() - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - Universe.createNew() - val tolerance = 0.0000001 - val algorithm = BeliefPropagation(10, f)(u1) - algorithm.start() - algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- globalTol) - algorithm.kill() - } - - "with a model using chain and no conditions or constraints, produce the correct answer" in { - Universe.createNew() - val f = Flip(0.3) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - test(c, (i: Int) => i == 1, 0.3 * 0.1 + 0.7 * 0.7, globalTol) - } - - "with a model using chain and a condition on the result, correctly condition the parent" in { - Universe.createNew() - val f = Flip(0.3) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - c.observe(1) - test(f, (b: Boolean) => b, 0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7), globalTol) - } - - "with a model using chain and a condition on one of the outcome elements, correctly condition the result but not change the belief about the parent" in { - Universe.createNew() - val f = Flip(0.3) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - - s1.observe(1) - //val c_actual = .79 - val c_actual = .70907 - - /* - * The "c_actual" value has been determine using Dimple to back up the results of Figaro. This exact - * same factor graph was run on dimple and returns .70907 for i == 1 - * The same is done with 'f' (set as an increased tolerance below), - * although this test is not doing much since the while point it to show - * that f does not change, at least it does not change significantly - * - * The factor model is no longer loopy so the exact result .79 applies (Glenn) - * - * UPDATE: We're going back to loopy since factor combining in ProbFactor is not default in BP. - */ - test(c, (i: Int) => i == 1, c_actual, globalTol) - test(f, (b: Boolean) => b, 0.3, 0.06) - } - - "with a dependent universe, correctly take into account probability of evidence in the dependent universe" in { - Universe.createNew() - //val x = Flip(0.1) - //val y = Flip(0.2) - val x = IntSelector(Constant(10)) - val y = IntSelector(Constant(10)) - val x1 = Apply(x, (i: Int) => i < 1) - val y1 = Apply(y, (i: Int) => i < 2) - val dependentUniverse = new Universe(List(x1, y1)) - val u1 = CUniform(0.0, 1.0)("", dependentUniverse) - val u2 = CUniform(0.0, 2.0)("", dependentUniverse) - - val a = CachingChain(x1, y1, (x: Boolean, y: Boolean) => if (x || y) u1; else u2)("a", dependentUniverse) - val condition = (d: Double) => d < 0.5 - val ve = BeliefPropagation(List((dependentUniverse, List(NamedEvidence("a", Condition(condition))))), 200, x1) - ve.start() - val peGivenXTrue = 0.5 - val peGivenXFalse = 0.2 * 0.5 + 0.8 * 0.25 - val unnormalizedPXTrue = 0.1 * peGivenXTrue - val unnormalizedPXFalse = 0.9 * peGivenXFalse - val pXTrue = unnormalizedPXTrue / (unnormalizedPXTrue + unnormalizedPXFalse) - ve.probability(x1, true) should be(pXTrue +- globalTol) - ve.kill() - } - - "with a contingent condition, correctly take into account the contingency" in { - Universe.createNew() - val x = Flip(0.1) - val y = Flip(0.2) - y.setCondition((b: Boolean) => b, List(Element.ElemVal(x, true))) - // Probability of y should be (0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8) (because the case where x is true and y is false has been ruled out) - val ve = BeliefPropagation(50, y) - ve.start() - ve.probability(y, true) should be(((0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8)) +- globalTol) - } - - "should not underflow" in { - Universe.createNew() - val x = Flip(0.99) - for (i <- 0 until 10) { - x.addConstraint((b: Boolean) => if (b) 1e-100; else 1e-120) - } - val bp = BeliefPropagation(5, x) - bp.start() - bp.probability(x, true) should be (1.0) - } - - // Removed, we now support non-caching chains - /* - "should not support non-caching chains" in { - Universe.createNew() - val f = Flip(0.5) - val x = NonCachingChain(f, (b: Boolean) => if (b) Constant(0) else Constant(1)) - val ve = BeliefPropagation(50) - an [UnsupportedAlgorithmException] should be thrownBy { ve.getNeededElements(List(x), Int.MaxValue) } - } +/* + * BPTest.scala + * Belief Propagation tests. + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Jan 15, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.factored + +import scala.collection.mutable.Map +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.factored.beliefpropagation._ +import com.cra.figaro.language._ +import com.cra.figaro.library.compound.If +import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} +import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} +import com.cra.figaro.library.compound.IntSelector +import com.cra.figaro.algorithm.lazyfactored.LazyValues +import com.cra.figaro.algorithm.UnsupportedAlgorithmException + +class BPTest extends WordSpec with Matchers { + + val globalTol = 0.025 + + "A basic factor graph" should { + "Create nodes for all factors and variables" in { + Universe.createNew + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + val semiring = SumProductSemiring + LazyValues(Universe.universe).expandAll(Universe.universe.activeElements.toSet.map((elem: Element[_]) => ((elem, Integer.MAX_VALUE)))) + val factors = Universe.universe.activeElements flatMap (Factory.make(_)) + val graph = new BasicFactorGraph(factors, semiring) + val fn = graph.adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) + val vn = graph.adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) + + fn.size should equal(6) + vn.size should equal(5) + } + + "Create an edge to between each factor and the variables it has" in { + Universe.createNew + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + val semiring = SumProductSemiring + LazyValues(Universe.universe).expandAll(Universe.universe.activeElements.toSet.map((elem: Element[_]) => ((elem, Integer.MAX_VALUE)))) + val factors = Universe.universe.activeElements flatMap (Factory.make(_)) + val graph = new BasicFactorGraph(factors, semiring) + val fn = graph.adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) + val vn = graph.adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) + + fn.foreach(n => { + val vars = n._1.asInstanceOf[FactorNode].variables.toSet + val edges = n._2.keys.map(v => v.asInstanceOf[VariableNode].variable).toSet + vars should equal(edges) + n._2.keys.foreach(v => graph.adjacencyList(v).contains(n._1) should equal(true)) + }) + + } + + } + + "Running BeliefPropagation" should { + "Send the correct type of message" in { + Universe.createNew + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + val bp = BeliefPropagation(3) + bp.start + val fn = bp.factorGraph.asInstanceOf[BasicFactorGraph].adjacencyList.filter(p => { p._1 match { case fn: FactorNode => true; case _ => false; } }) + val vn = bp.factorGraph.asInstanceOf[BasicFactorGraph].adjacencyList.filter(p => { p._1 match { case vn: VariableNode => true; case _ => false; } }) + + fn.foreach { s => + s._2.foreach { d => + val msg = bp.newMessage(s._1, d._1) + msg.variables should equal(List(d._1.asInstanceOf[VariableNode].variable)) + } + } + + vn.foreach { s => + s._2.foreach { d => + val msg = bp.newMessage(s._1, d._1) + msg.variables should equal(List(s._1.asInstanceOf[VariableNode].variable)) + } + } + } + + /* Due to the way that factors are implemented for Chain, all + * models that use chain will result in loops. To test a non-loopy + * graph we have to not use chain, which IntSelector does not. + */ + "with no loops in the factor graph give exact results" in { + Universe.createNew() + val e1 = DUniform(2, 3, 4) + val e2 = IntSelector(e1) + + val bp = BeliefPropagation(3, e2) + bp.start() + + val e2_0 = 0.33333333 * (0.5 + 0.3333333 + 0.25) + val e2_1 = 0.33333333 * (0.5 + 0.3333333 + 0.25) + val e2_2 = 0.33333333 * (0 + 0.3333333 + 0.25) + val e2_3 = 0.33333333 * (0 + 0 + 0.25) + + val tol = 0.000001 + bp.probability(e2, (i: Int) => i == 0) should be(e2_0 +- tol) + bp.probability(e2, (i: Int) => i == 1) should be(e2_1 +- tol) + bp.probability(e2, (i: Int) => i == 2) should be(e2_2 +- tol) + bp.probability(e2, (i: Int) => i == 3) should be(e2_3 +- tol) + } + + "with no conditions or constraints produce the correct result" in { + Universe.createNew() + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + test(f, (b: Boolean) => b, 0.6, globalTol) + } + + "with a condition on a dependent element produce the result with the correct probability" in { + Universe.createNew() + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setCondition((i: Int) => i == 2) + // U(true) = \int_{0.2}^{1.0) 0.7 p = 0.35 * 0.96 + // U(false) = \int_{0.2}^{1.0) (1-p) + val u1 = 0.35 * 0.96 + val u2 = 0.32 + test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) + } + + "with a constraint on a dependent element produce the result with the correct probability" in { + Universe.createNew() + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setConstraint((i: Int) => i.toDouble) + // U(true) = \int_{0.2}^{1.0} (0.3 + 2 * 0.7) p = 0.85 * 0.96 + // U(false) = \int_{0.2}^(1.0) (2 * (1-p)) = 0.64 + val u1 = 0.85 * 0.96 + val u2 = 0.64 + test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) + } + + "with an element that uses another element multiple times, " + + "always produce the same value for the different uses" in { + Universe.createNew() + val f = Flip(0.5) + val e = f === f + test(e, (b: Boolean) => b, 1.0, globalTol) + } + + "with a constraint on an element that is used multiple times, only factor in the constraint once" in { + Universe.createNew() + val f1 = Flip(0.5) + val f2 = Flip(0.3) + val e1 = f1 === f1 + val e2 = f1 === f2 + val d = Dist(0.5 -> e1, 0.5 -> e2) + f1.setConstraint((b: Boolean) => if (b) 3.0; else 2.0) + // Probability that f1 is true = 0.6 + // Probability that e1 is true = 1.0 + // Probability that e2 is true = 0.6 * 0.3 + 0.4 * 0.7 = 0.46 + // Probability that d is true = 0.5 * 1 + 0.5 * 0.46 = 0.73 + test(d, (b: Boolean) => b, 0.73, globalTol) + } + + "on a different universe from the current universe, produce the correct result" in { + val u1 = Universe.createNew() + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + Universe.createNew() + val tolerance = 0.0000001 + val algorithm = BeliefPropagation(10, f)(u1) + algorithm.start() + algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- globalTol) + algorithm.kill() + } + + "with a model using chain and no conditions or constraints, produce the correct answer" in { + Universe.createNew() + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + test(c, (i: Int) => i == 1, 0.3 * 0.1 + 0.7 * 0.7, globalTol) + } + + "with a model using chain and a condition on the result, correctly condition the parent" in { + Universe.createNew() + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + c.observe(1) + test(f, (b: Boolean) => b, 0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7), globalTol) + } + + "with a model using chain and a condition on one of the outcome elements, correctly condition the result but not change the belief about the parent" in { + Universe.createNew() + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + + s1.observe(1) + //val c_actual = .79 + val c_actual = .70907 + + /* + * The "c_actual" value has been determine using Dimple to back up the results of Figaro. This exact + * same factor graph was run on dimple and returns .70907 for i == 1 + * The same is done with 'f' (set as an increased tolerance below), + * although this test is not doing much since the while point it to show + * that f does not change, at least it does not change significantly + * + * The factor model is no longer loopy so the exact result .79 applies (Glenn) + * + * UPDATE: We're going back to loopy since factor combining in ProbFactor is not default in BP. + */ + test(c, (i: Int) => i == 1, c_actual, globalTol) + test(f, (b: Boolean) => b, 0.3, 0.06) + } + + "with a dependent universe, correctly take into account probability of evidence in the dependent universe" in { + Universe.createNew() + //val x = Flip(0.1) + //val y = Flip(0.2) + val x = IntSelector(Constant(10)) + val y = IntSelector(Constant(10)) + val x1 = Apply(x, (i: Int) => i < 1) + val y1 = Apply(y, (i: Int) => i < 2) + val dependentUniverse = new Universe(List(x1, y1)) + val u1 = CUniform(0.0, 1.0)("", dependentUniverse) + val u2 = CUniform(0.0, 2.0)("", dependentUniverse) + + val a = CachingChain(x1, y1, (x: Boolean, y: Boolean) => if (x || y) u1; else u2)("a", dependentUniverse) + val condition = (d: Double) => d < 0.5 + val ve = BeliefPropagation(List((dependentUniverse, List(NamedEvidence("a", Condition(condition))))), 200, x1) + ve.start() + val peGivenXTrue = 0.5 + val peGivenXFalse = 0.2 * 0.5 + 0.8 * 0.25 + val unnormalizedPXTrue = 0.1 * peGivenXTrue + val unnormalizedPXFalse = 0.9 * peGivenXFalse + val pXTrue = unnormalizedPXTrue / (unnormalizedPXTrue + unnormalizedPXFalse) + ve.probability(x1, true) should be(pXTrue +- globalTol) + ve.kill() + } + + "with a contingent condition, correctly take into account the contingency" in { + Universe.createNew() + val x = Flip(0.1) + val y = Flip(0.2) + y.setCondition((b: Boolean) => b, List(Element.ElemVal(x, true))) + // Probability of y should be (0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8) (because the case where x is true and y is false has been ruled out) + val ve = BeliefPropagation(50, y) + ve.start() + ve.probability(y, true) should be(((0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8)) +- globalTol) + } + + "should not underflow" in { + Universe.createNew() + val x = Flip(0.99) + for (i <- 0 until 10) { + x.addConstraint((b: Boolean) => if (b) 1e-100; else 1e-120) + } + val bp = BeliefPropagation(5, x) + bp.start() + bp.probability(x, true) should be (1.0) + } + + // Removed, we now support non-caching chains + /* + "should not support non-caching chains" in { + Universe.createNew() + val f = Flip(0.5) + val x = NonCachingChain(f, (b: Boolean) => if (b) Constant(0) else Constant(1)) + val ve = BeliefPropagation(50) + an [UnsupportedAlgorithmException] should be thrownBy { ve.getNeededElements(List(x), Int.MaxValue) } + } * - */ - } - - "MaxProductBeliefPropagation" should { - "compute the most likely values of all the variables given the conditions and constraints" in { - Universe.createNew() - val e1 = Flip(0.5) - e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) - val e2 = If(e1, Flip(0.4), Flip(0.9)) - val e3 = If(e1, Flip(0.52), Flip(0.4)) - val e4 = e2 === e3 - e4.observe(true) - // p(e1=T,e2=T,e3=T) = 0.75 * 0.4 * 0.52 - // p(e1=T,e2=F,e3=F) = 0.75 * 0.6 * 0.48 - // p(e1=F,e2=T,e3=T) = 0.25 * 0.9 * 0.4 - // p(e1=F,e2=F,e3=F) = 0.25 * 0.1 * 0.6 - // MPE: e1=T,e2=F,e3=F,e4=T - val alg = MPEBeliefPropagation(20) - alg.start() - alg.mostLikelyValue(e1) should equal(true) - alg.mostLikelyValue(e2) should equal(false) - alg.mostLikelyValue(e3) should equal(false) - alg.mostLikelyValue(e4) should equal(true) - } - - } - - def test[T](target: Element[T], predicate: T => Boolean, prob: Double, tol: Double) { - val algorithm = BeliefPropagation(100, target) - algorithm.start() - algorithm.probability(target, predicate) should be(prob +- tol) - } + */ + } + + "MaxProductBeliefPropagation" should { + "compute the most likely values of all the variables given the conditions and constraints" in { + Universe.createNew() + val e1 = Flip(0.5) + e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) + val e2 = If(e1, Flip(0.4), Flip(0.9)) + val e3 = If(e1, Flip(0.52), Flip(0.4)) + val e4 = e2 === e3 + e4.observe(true) + // p(e1=T,e2=T,e3=T) = 0.75 * 0.4 * 0.52 + // p(e1=T,e2=F,e3=F) = 0.75 * 0.6 * 0.48 + // p(e1=F,e2=T,e3=T) = 0.25 * 0.9 * 0.4 + // p(e1=F,e2=F,e3=F) = 0.25 * 0.1 * 0.6 + // MPE: e1=T,e2=F,e3=F,e4=T + val alg = MPEBeliefPropagation(20) + alg.start() + alg.mostLikelyValue(e1) should equal(true) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + } + + } + + def test[T](target: Element[T], predicate: T => Boolean, prob: Double, tol: Double) { + val algorithm = BeliefPropagation(100, target) + algorithm.start() + algorithm.probability(target, predicate) should be(prob +- tol) + } } \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/FactorTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/FactorTest.scala index 6d9ab5bb..ba9685ee 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/FactorTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/FactorTest.scala @@ -1,1301 +1,1299 @@ -/* - * FactorTest.scala - * Factor tests. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: Jan 1, 2009 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.test.algorithm.factored - -import org.scalatest.Matchers -import org.scalatest.PrivateMethodTester -import org.scalatest.WordSpec -import com.cra.figaro.algorithm.Values -import com.cra.figaro.algorithm.factored.Factory -import com.cra.figaro.algorithm.factored.SumProductSemiring -import com.cra.figaro.algorithm.factored.Variable -import com.cra.figaro.algorithm.lazyfactored.LazyValues -import com.cra.figaro.algorithm.lazyfactored.Regular -import com.cra.figaro.algorithm.lazyfactored.ValueSet -import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler -import com.cra.figaro.language.Apply -import com.cra.figaro.language.Apply3 -import com.cra.figaro.language.Apply4 -import com.cra.figaro.language.Apply5 -import com.cra.figaro.language.CachingChain -import com.cra.figaro.language.Chain -import com.cra.figaro.language.Condition -import com.cra.figaro.language.Constant -import com.cra.figaro.language.Dist -import com.cra.figaro.language.Flip -import com.cra.figaro.language.Inject -import com.cra.figaro.language.Name.stringToName -import com.cra.figaro.language.NamedEvidence -import com.cra.figaro.language.Reference.stringToReference -import com.cra.figaro.language.Select -import com.cra.figaro.language.Universe -import com.cra.figaro.library.atomic.continuous.Normal -import com.cra.figaro.library.atomic.continuous.Uniform -import com.cra.figaro.library.compound.CPD -import com.cra.figaro.algorithm.factored.ParticleGenerator - -class FactorTest extends WordSpec with Matchers with PrivateMethodTester { - - "A variable for an element" should { - "have range equal to the element's values" in { - Universe.createNew() - val e1 = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) - val xs = Values()(e1) - val v1 = Variable(e1) - for { x <- v1.range } { xs should contain(x.value) } - //for { x <- xs } { v1.range should contain(x) } - } - - "always be equal to another variable for the same element" in { - Universe.createNew() - val e1 = Flip(0.2) - Values()(e1) - val v1 = Variable(e1) - val v2 = Variable(e1) - v1 should equal(v2) - } - - "always contain the same id even if the Variable cache is cleared" in { - Universe.createNew() - val e1 = Flip(0.2) - Values()(e1) - val v1 = Variable(e1).id - Variable.clearCache - LazyValues.clear(Universe.universe) - Values()(e1) - val v2 = Variable(e1).id - v1 should equal(v2) - } - - "always be equal to a variable with the same id" in { - Universe.createNew() - val e1 = Flip(0.2) - Values()(e1) - val v1 = Variable(e1) - val v2 = new Variable(ValueSet.withStar(Set[Boolean]())) {override val id = v1.id} - v1 == v2 should equal(true) - } - - "be different to a variable for a different element with the same definition" in { - Universe.createNew() - val e1 = Flip(0.2) - val e2 = Flip(0.2) - Values()(e1) - Values()(e2) - val v1 = Variable(e1) - val v2 = Variable(e2) - v1 should not equal (v2) - } - } - - "A factor" when { - "get the same value for a given set of variable indices as was last set" in { - Universe.createNew() - val e1 = Flip(0.1) - val e2 = Constant(8) - val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e4 = Flip(0.7) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val indices = List(1, 0, 2, 1) - f.set(indices, 0.2) - f.set(indices, 0.3) - f.get(indices) should equal(0.3) - } - - "get updated set of factors for an element when the factors have been updated" in { - Universe.createNew() - val v1 = Flip(0.5) - Values()(v1) - val f1 = Factory.make(v1)(0) - val f1mod = f1.mapTo((d: Double) => 2.0*d, f1.variables) - Factory.updateFactor(v1, List(f1mod)) - Factory.make(v1)(0).get(List(0)) should equal(f1mod.get(List(0))) - } - - "have the first index List be all zeros" in { - Universe.createNew() - val e1 = Flip(0.1) - val e2 = Constant(8) - val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e4 = Flip(0.7) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - f.firstIndices should equal(List(0, 0, 0, 0)) - } - - "have the next index List carry and add correctly" in { - Universe.createNew() - val e1 = Flip(0.1) - val e2 = Constant(8) - val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e4 = Flip(0.7) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val ia = List(1, 0, 1, 1) - val ar = f.nextIndices(ia).get - ar should equal(List(0, 0, 2, 1)) - } - - "produce None when the index Lists are exhausted" in { - Universe.createNew() - val e1 = Flip(0.1) - val e2 = Constant(8) - val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e4 = Flip(0.7) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val ia = List(1, 0, 2, 1) - f.nextIndices(ia) should equal(None) - } - - "compute the union of variables in two factors and the correct index maps when calling unionVars" in { - Universe.createNew() - val e1 = Flip(0.1) - val e2 = Constant(8) - val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e4 = Flip(0.7) - val e5 = Constant('a) - val e6 = Select(0.1 -> 1.5, 0.9 -> 2.5) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - Values()(e5) - Values()(e6) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val v5 = Variable(e5) - val v6 = Variable(e6) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) - val unionVars = PrivateMethod[(List[Variable[_]], List[Int], List[Int])]('unionVars) - val (union, indexMap1, indexMap2) = f invokePrivate unionVars(g) - union should equal(List(v1, v2, v3, v4, v5, v6)) - indexMap1 should equal(List(0, 1, 2, 3)) - indexMap2 should equal(List(4, 2, 1, 5)) - } - - "multiplying with another factor" should { - "return the product of the two factors" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Constant(8) - val e3 = Flip(0.1) - val e4 = Flip(0.6) - Values()(e1) - Values()(e2) - Values()(e3) - Values()(e4) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val v4 = Variable(e4) - val f = Factory.make[Double](List(v1, v2, v3)) - val g = Factory.make[Double](List(v4, v3)) - f.set(List(0, 0, 0), 0.0) - f.set(List(1, 0, 0), 0.1) - f.set(List(2, 0, 0), 0.2) - f.set(List(0, 0, 1), 0.3) - f.set(List(1, 0, 1), 0.4) - f.set(List(2, 0, 1), 0.5) - g.set(List(0, 0), 0.6) - g.set(List(1, 0), 0.7) - g.set(List(0, 1), 0.8) - g.set(List(1, 1), 0.9) - val h = f.product(g, SumProductSemiring) - h.variables should equal(List(v1, v2, v3, v4)) - h.get(List(0, 0, 0, 0)) should be(0.0 +- 0.0001) - h.get(List(1, 0, 0, 0)) should be(0.06 +- 0.0001) - h.get(List(2, 0, 0, 0)) should be(0.12 +- 0.0001) - h.get(List(0, 0, 1, 0)) should be(0.24 +- 0.0001) - h.get(List(1, 0, 1, 0)) should be(0.32 +- 0.0001) - h.get(List(2, 0, 1, 0)) should be(0.4 +- 0.0001) - h.get(List(0, 0, 0, 1)) should be(0.0 +- 0.0001) - h.get(List(1, 0, 0, 1)) should be(0.07 +- 0.0001) - h.get(List(2, 0, 0, 1)) should be(0.14 +- 0.0001) - h.get(List(0, 0, 1, 1)) should be(0.27 +- 0.0001) - h.get(List(1, 0, 1, 1)) should be(0.36 +- 0.0001) - h.get(List(2, 0, 1, 1)) should be(0.45 +- 0.0001) - } - } - - "calling sumOver on a variable" should { - "return the sum over the variable of the factor" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Constant(8) - val e3 = Flip(0.1) - Values()(e1) - Values()(e2) - Values()(e3) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val f = Factory.make[Double](List(v1, v2, v3)) - f.set(List(0, 0, 0), 0.0) - f.set(List(1, 0, 0), 0.1) - f.set(List(2, 0, 0), 0.2) - f.set(List(0, 0, 1), 0.3) - f.set(List(1, 0, 1), 0.4) - f.set(List(2, 0, 1), 0.5) - val g = f.sumOver(v3, SumProductSemiring) - g.variables should equal(List(v1, v2)) - g.get(List(0, 0)) should be(0.3 +- 0.0001) - g.get(List(1, 0)) should be(0.5 +- 0.0001) - g.get(List(2, 0)) should be(0.7 +- 0.0001) - } - - "return itself if the variable not in the factor" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Constant(8) - val e3 = Flip(0.1) - Values()(e1) - Values()(e2) - Values()(e3) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val f = Factory.make[Double](List(v1, v2)) - f.set(List(0, 0), 0.0) - f.set(List(1, 0), 0.2) - f.set(List(2, 0), 0.4) - val g = f.sumOver(v3, SumProductSemiring) - g.variables should equal(f.variables) - for { indices <- f.allIndices } { - g.get(indices) should equal(f.get(indices)) - } - } - - "return a factor with all columns of the variable removed, ignoring rows in which " + - "the variable has different values in different columns" in { - Universe.createNew() - val e1 = Flip(0.9) - val e2 = Select(0.2 -> 1, 0.8 -> 2) - Values()(e1) - Values()(e2) - val v1 = Variable(e1) - val v2 = Variable(e2) - val f = Factory.make[Double](List(v1, v2, v1)) - f.set(List(0, 0, 0), 0.1) - f.set(List(1, 0, 0), 0.2) - f.set(List(0, 1, 0), 0.3) - f.set(List(1, 1, 0), 0.4) - f.set(List(0, 0, 1), 0.5) - f.set(List(1, 0, 1), 0.6) - f.set(List(0, 1, 1), 0.7) - f.set(List(1, 1, 1), 0.8) - val g = f.sumOver(v1, SumProductSemiring) - g.variables should equal(List(v2)) - g.get(List(0)) should equal(0.1 + 0.6) - g.get(List(1)) should equal(0.3 + 0.8) - } - } - - "calling record on a variable" should { - "return the argmax over the values associated with the variable" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Constant(8) - val e3 = Flip(0.1) - Values()(e1) - Values()(e2) - Values()(e3) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val f = Factory.make[Double](List(v1, v2, v3)) - f.set(List(0, 0, 0), 0.6) - f.set(List(1, 0, 0), 0.1) - f.set(List(2, 0, 0), 0.2) - f.set(List(0, 0, 1), 0.3) - f.set(List(1, 0, 1), 0.4) - f.set(List(2, 0, 1), 0.5) - val g = f.recordArgMax(v3, (x: Double, y: Double) => x < y) - g.variables should equal(List(v1, v2)) - g.get(List(0, 0)) should equal(true) - g.get(List(1, 0)) should equal(false) - g.get(List(2, 0)) should equal(false) - } - } - - "after marginalizing to a variable" should { - "return the marginal distribution over the variable" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Constant(8) - val e3 = Flip(0.1) - Values()(e1) - Values()(e2) - Values()(e3) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val f = Factory.make[Double](List(v1, v2, v3)) - f.set(List(0, 0, 0), 0.0) - f.set(List(1, 0, 0), 0.1) - f.set(List(2, 0, 0), 0.2) - f.set(List(0, 0, 1), 0.3) - f.set(List(1, 0, 1), 0.4) - f.set(List(2, 0, 1), 0.5) - val g = f.marginalizeTo(SumProductSemiring, v3) - g.variables should equal(List(v3)) - val p1 = 0.0 + 0.1 + 0.2 - val p2 = 0.3 + 0.4 + 0.5 - g.get(List(0)) should be(p1 +- 0.000001) - g.get(List(1)) should be(p2 +- 0.000001) - } - } - } - - "after marginalizing to two variables" should { - "return the marginal distribution over the variables" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Flip(0.5) - val e3 = Flip(0.1) - Values()(e1) - Values()(e2) - Values()(e3) - val v1 = Variable(e1) - val v2 = Variable(e2) - val v3 = Variable(e3) - val f = Factory.make[Double](List(v1, v2, v3)) - f.set(List(0, 0, 0), 0.0) - f.set(List(1, 0, 0), 0.05) - f.set(List(2, 0, 0), 0.1) - f.set(List(0, 0, 1), 0.15) - f.set(List(1, 0, 1), 0.2) - f.set(List(2, 0, 1), 0.25) - f.set(List(0, 1, 0), 0.0) - f.set(List(1, 1, 0), 0.05) - f.set(List(2, 1, 0), 0.1) - f.set(List(0, 1, 1), 0.15) - f.set(List(1, 1, 1), 0.2) - f.set(List(2, 1, 1), 0.25) - val g = f.marginalizeTo(SumProductSemiring, v1, v3) - g.variables should equal(List(v1, v3)) - g.get(List(0, 0)) should be(0.0 +- 0.000001) - g.get(List(1, 0)) should be(0.1 +- 0.000001) - g.get(List(2, 0)) should be(0.2 +- 0.000001) - g.get(List(0, 1)) should be(0.3 +- 0.000001) - g.get(List(1, 1)) should be(0.4 +- 0.000001) - g.get(List(2, 1)) should be(0.5 +- 0.000001) - } - } - - "after deduplicating a factor" should { - "have no repeated variables" in { - Universe.createNew() - val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") - val e2 = Flip(0.3) - Values()(e1) - Values()(e2) - val v1 = Variable(e1) - val v2 = Variable(e2) - val f = Factory.make[Double](List(v1, v2, v2)) - f.set(List(0, 0, 0), 0.06) - f.set(List(0, 0, 1), 0.25) - f.set(List(0, 1, 0), 0.44) - f.set(List(0, 1, 1), 0.25) - f.set(List(1, 0, 0), 0.15) - f.set(List(1, 0, 1), 0.2) - f.set(List(1, 1, 0), 0.15) - f.set(List(1, 1, 1), 0.5) - f.set(List(2, 0, 0), 0.1) - f.set(List(2, 0, 1), 0.25) - f.set(List(2, 1, 0), 0.4) - f.set(List(2, 1, 1), 0.25) - - val g = f.deDuplicate() - - g.variables.size should be(2) - g.variables.contains(v1) should be(true) - g.variables.contains(v2) should be(true) - - if (g.variables.indexOf(v1) == 0) { - g.get(List(0, 0)) should be(0.06 +- 0.000001) - g.get(List(0, 1)) should be(0.25 +- 0.000001) - g.get(List(1, 0)) should be(0.15 +- 0.000001) - g.get(List(1, 1)) should be(0.5 +- 0.000001) - g.get(List(2, 0)) should be(0.1 +- 0.000001) - g.get(List(2, 1)) should be(0.25 +- 0.000001) - } else { - g.get(List(0, 0)) should be(0.06 +- 0.000001) - g.get(List(1, 0)) should be(0.25 +- 0.000001) - g.get(List(0, 1)) should be(0.15 +- 0.000001) - g.get(List(1, 1)) should be(0.5 +- 0.000001) - g.get(List(0, 2)) should be(0.1 +- 0.000001) - g.get(List(1, 2)) should be(0.25 +- 0.000001) - } - - } - } - "Making factors from an element" when { - - "given a constant" should { - "produce a single factor with one entry whose value is 1.0" in { - Universe.createNew() - val v1 = Constant(7) - Values()(v1) - val List(factor) = Factory.make(v1) - factor.get(List(0)) should equal(1.0) - } - } - - "given a simple flip" should { - "produce a single factor in which the first entry is the probability of true " + - "and the second entry is the probability of false" in { - Universe.createNew() - val v1 = Flip(0.3) - Values()(v1) - val List(factor) = Factory.make(v1) - factor.get(List(0)) should equal(0.3) - factor.get(List(1)) should equal(0.7) - } - } - - "given a complex flip" should { - "produce a single factor in which each possible value of the parent is associated with two " + - "entries, one each for true and false, with the appropriate probabilities" in { - Universe.createNew() - val v1 = Select(0.2 -> 0.1, 0.8 -> 0.3) - val v2 = Flip(v1) - Values()(v2) - val List(factor) = Factory.make(v2) - val vals = Variable(v1).range - println("vals = " + vals) - println("v2.range = " + Variable(v2).range) - val i1 = vals.indexOf(Regular(0.1)) - val i2 = vals.toList.indexOf(Regular(0.3)) - factor.get(List(i1, 0)) should equal(0.1) - factor.get(List(i1, 1)) should equal(0.9) - factor.get(List(i2, 0)) should equal(0.3) - factor.get(List(i2, 1)) should equal(0.7) - } - } - - "given a simple select" should { - "produce a single factor in which each possible value is associated with the correct probability" in { - Universe.createNew() - val v1 = Select(0.2 -> 1, 0.3 -> 0, 0.1 -> 2, 0.05 -> 5, 0.35 -> 4) - Values()(v1) - val List(factor) = Factory.make(v1) - val vals = Variable(v1).range - val i1 = vals.indexOf(Regular(1)) - val i0 = vals.indexOf(Regular(0)) - val i2 = vals.indexOf(Regular(2)) - val i5 = vals.indexOf(Regular(5)) - val i4 = vals.indexOf(Regular(4)) - factor.get(List(i1)) should equal(0.2) - factor.get(List(i0)) should equal(0.3) - factor.get(List(i2)) should equal(0.1) - } - } - - "given a complex select" should { - "produce a single factor in which values of the parents are associated with " + - "values of the select according to the normalized parent values" in { - Universe.createNew() - val v1 = Select(0.2 -> 0.2, 0.8 -> 0.8) - val v2 = Select(0.4 -> 0.4, 0.6 -> 0.6) - val c1 = Constant(0.1) - val c2 = Constant(0.3) - val c3 = Constant(0.5) - val v3 = Select(v1 -> 1, v2 -> 2, c1 -> 4, c2 -> 5, c3 -> 3) - Values()(v3) - val List(factor) = Factory.make(v3) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v102 = v1Vals.indexOf(Regular(0.2)) - val v108 = v1Vals.indexOf(Regular(0.8)) - val v204 = v2Vals.indexOf(Regular(0.4)) - val v206 = v2Vals.indexOf(Regular(0.6)) - val v31 = v3Vals.indexOf(Regular(1)) - val v32 = v3Vals.indexOf(Regular(2)) - val v34 = v3Vals.indexOf(Regular(4)) - val v35 = v3Vals.indexOf(Regular(5)) - val v33 = v3Vals.indexOf(Regular(3)) - def makeIndices(a: List[Int]): List[Int] = { - val result: Array[Int] = Array.ofDim(a.size) - result(0) = a(0) - result(v31 + 1) = a(1) - result(v32 + 1) = a(2) - result(v33 + 1) = a(3) - result(v34 + 1) = a(4) - result(v35 + 1) = a(5) - result.toList - } - factor.get(makeIndices(List(v31, v102, v204, 0, 0, 0))) should be(0.2 / 1.5 +- 0.01) - factor.get(makeIndices(List(v32, v102, v204, 0, 0, 0))) should be(0.4 / 1.5 +- 0.01) - factor.get(makeIndices(List(v31, v108, v204, 0, 0, 0))) should be(0.8 / 2.1 +- 0.01) - factor.get(makeIndices(List(v32, v108, v204, 0, 0, 0))) should be(0.4 / 2.1 +- 0.01) - factor.get(makeIndices(List(v31, v102, v206, 0, 0, 0))) should be(0.2 / 1.7 +- 0.01) - factor.get(makeIndices(List(v32, v102, v206, 0, 0, 0))) should be(0.6 / 1.7 +- 0.01) - factor.get(makeIndices(List(v31, v108, v206, 0, 0, 0))) should be(0.8 / 2.3 +- 0.01) - factor.get(makeIndices(List(v32, v108, v206, 0, 0, 0))) should be(0.6 / 2.3 +- 0.01) - } - } - - "given a simple dist" should { - "produce a list of factors, one for each outcome and one representing the choice over outcomes; " + - "the factor for an outcome matches the outcome value to the dist value" in { - Universe.createNew() - val v1 = Flip(0.2) - val v2 = Constant(false) - val v3 = Dist(0.3 -> v1, 0.7 -> v2) - Values()(v3) - val v1Vals = Variable(v1).range - val v3Vals = Variable(v3).range - val v1TrueIndex = v1Vals.indexOf(Regular(true)) - val v1FalseIndex = v1Vals.indexOf(Regular(false)) - val v3TrueIndex = v3Vals.indexOf(Regular(true)) - val v3FalseIndex = v3Vals.indexOf(Regular(false)) - val v1Index = v3.outcomes.indexOf(v1) - val v2Index = v3.outcomes.indexOf(v2) - val selectFactor :: outcomeFactors = Factory.make(v3) - outcomeFactors.size should equal(2) - val v1Factor = outcomeFactors(v1Index) - val v2Factor = outcomeFactors(v2Index) - selectFactor.get(List(v1Index)) should equal(0.3) - selectFactor.get(List(v2Index)) should equal(0.7) - v1Factor.get(List(v1Index, v3TrueIndex, v1TrueIndex)) should equal(1.0) - v1Factor.get(List(v1Index, v3TrueIndex, v1FalseIndex)) should equal(0.0) - v1Factor.get(List(v1Index, v3FalseIndex, v1TrueIndex)) should equal(0.0) - v1Factor.get(List(v1Index, v3FalseIndex, v1FalseIndex)) should equal(1.0) - for { i <- 0 to 1; j <- 0 to 1 } v1Factor.get(List(v2Index, i, j)) should equal(1.0) - v2Factor.get(List(v2Index, v3FalseIndex, 0)) should equal(1.0) - v2Factor.get(List(v2Index, v3TrueIndex, 0)) should equal(0.0) - for { i <- 0 to 1; j <- 0 to 0 } v2Factor.get(List(v1Index, i, j)) should equal(1.0) - } - } - - "given a complex dist" should { - "produce a list of factors, one for each outcome and one representing the choice over outcomes; " + - "the factor for an outcome matches the outcome value to the dist value" in { - Universe.createNew() - val v1 = Select(0.2 -> 0.2, 0.8 -> 0.8) - val v2 = Select(0.4 -> 0.4, 0.6 -> 0.6) - val v3 = Flip(0.2) - val v4 = Constant(false) - val v5 = Dist(v1 -> v3, v2 -> v4) - Values()(v5) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v4Vals = Variable(v4).range - val v5Vals = Variable(v5).range - val v3Index = v5.outcomes.indexOf(v3) - val v4Index = v5.outcomes.indexOf(v4) - val v102 = v1Vals.indexOf(Regular(0.2)) - val v108 = v1Vals.indexOf(Regular(0.8)) - val v204 = v2Vals.indexOf(Regular(0.4)) - val v206 = v2Vals.indexOf(Regular(0.6)) - val v3f = v3Vals.indexOf(Regular(false)) - val v3t = v3Vals.indexOf(Regular(true)) - val v5f = v5Vals.indexOf(Regular(false)) - val v5t = v5Vals.indexOf(Regular(true)) - val selectFactor :: outcomeFactors = Factory.make(v5) - outcomeFactors.size should equal(2) - val v1Factor = outcomeFactors(v3Index) - val v2Factor = outcomeFactors(v4Index) - selectFactor.get(List(0, v102, v204)) should be(0.2 / 0.6 +- 0.0001) - selectFactor.get(List(1, v102, v204)) should be(0.4 / 0.6 +- 0.0001) - selectFactor.get(List(0, v102, v206)) should be(0.2 / 0.8 +- 0.0001) - selectFactor.get(List(1, v102, v206)) should be(0.6 / 0.8 +- 0.0001) - selectFactor.get(List(0, v108, v204)) should be(0.8 / 1.2 +- 0.0001) - selectFactor.get(List(1, v108, v204)) should be(0.4 / 1.2 +- 0.0001) - selectFactor.get(List(0, v108, v206)) should be(0.8 / 1.4 +- 0.0001) - selectFactor.get(List(1, v108, v206)) should be(0.6 / 1.4 +- 0.0001) - v1Factor.get(List(0, v5t, v3t)) should equal(1.0) - v1Factor.get(List(0, v5t, v3f)) should equal(0.0) - v1Factor.get(List(0, v5f, v3t)) should equal(0.0) - v1Factor.get(List(0, v5f, v3f)) should equal(1.0) - for { i <- 0 to 1; j <- 0 to 1 } v1Factor.get(List(1, i, j)) should equal(1.0) - v2Factor.get(List(1, v5f, 0)) should equal(1.0) - v2Factor.get(List(1, v5t, 0)) should equal(0.0) - for { i <- 0 to 1; j <- 0 to 0 } v2Factor.get(List(0, i, j)) should equal(1.0) - } - } - - "given an atomic not in the factor" should { - "automatically sample the element" in { - Universe.createNew() - val v1 = Normal(0.0, 1.0) - Values()(v1) - val factor = Factory.make(v1) - factor(0).size should equal(ParticleGenerator.defaultArgSamples) - factor(0).get(List(0)) should equal(1.0/ParticleGenerator.defaultArgSamples) - } - } - - "given a chain" should { - "produce a conditional selector for each parent value" in { - Universe.createNew() - val v1 = Flip(0.2) - val v2 = Select(0.1 -> 1, 0.9 -> 2) - val v3 = Constant(3) - val v4 = Chain(v1, (b: Boolean) => if (b) v2; else v3) - Values()(v4) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v4Vals = Variable(v4).range - val v1t = v1Vals indexOf Regular(true) - val v1f = v1Vals indexOf Regular(false) - val v21 = v2Vals indexOf Regular(1) - val v22 = v2Vals indexOf Regular(2) - val v41 = v4Vals indexOf Regular(1) - val v42 = v4Vals indexOf Regular(2) - val v43 = v4Vals indexOf Regular(3) - - val factor = Factory.make(v4) - val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) - - v4Factor.get(List(v1t, v41, v21, 0)) should equal(1.0) - v4Factor.get(List(v1t, v41, v22, 0)) should equal(0.0) - v4Factor.get(List(v1t, v42, v21, 0)) should equal(0.0) - v4Factor.get(List(v1t, v42, v22, 0)) should equal(1.0) - v4Factor.get(List(v1t, v43, v21, 0)) should equal(0.0) - v4Factor.get(List(v1t, v43, v22, 0)) should equal(0.0) - v4Factor.get(List(v1f, v41, v21, 0)) should equal(0.0) - v4Factor.get(List(v1f, v41, v22, 0)) should equal(0.0) - v4Factor.get(List(v1f, v42, v21, 0)) should equal(0.0) - v4Factor.get(List(v1f, v42, v22, 0)) should equal(0.0) - v4Factor.get(List(v1f, v43, v21, 0)) should equal(1.0) - v4Factor.get(List(v1f, v43, v22, 0)) should equal(1.0) - - } - - "produce a conditional selector for each non-temporary parent value" in { - Universe.createNew() - val v1 = Flip(0.2) - val v4 = Chain(v1, (b: Boolean) => if (b) Select(0.1 -> 1, 0.9 -> 2); else Constant(3)) - Values()(v4) - val v1Vals = Variable(v1).range - val v4Vals = Variable(v4).range - - val v1t = v1Vals indexOf Regular(true) - val v1f = v1Vals indexOf Regular(false) - val v41 = v4Vals indexOf Regular(1) - val v42 = v4Vals indexOf Regular(2) - val v43 = v4Vals indexOf Regular(3) - - val factor = Factory.make(v4) - val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) - - v4Factor.get(List(v1t, v41)) should equal(0.1) - v4Factor.get(List(v1t, v42)) should equal(0.9) - v4Factor.get(List(v1t, v43)) should equal(0.0) - v4Factor.get(List(v1f, v41)) should equal(0.0) - v4Factor.get(List(v1f, v42)) should equal(0.0) - v4Factor.get(List(v1f, v43)) should equal(1.0) - } - } - - "given a CPD with one argument" should { - "produce a single factor with a case for each parent value" in { - Universe.createNew() - val v1 = Flip(0.2) - - val v2 = CPD(v1, false -> Flip(0.1), true -> Flip(0.7)) - Values()(v2) - - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - - val v1t = v1Vals indexOf Regular(true) - val v1f = v1Vals indexOf Regular(false) - val v2t = v2Vals indexOf Regular(true) - val v2f = v2Vals indexOf Regular(false) - val v3t = 0 - val v3f = 1 - val v4t = 0 - val v4f = 1 - - val factor = Factory.make(v2) - val List(v2Factor) = Factory.combineFactors(factor, SumProductSemiring, true) - - v2Factor.get(List(v1t, v2t, v3t, v4t)) should equal(1.0) - v2Factor.get(List(v1t, v2t, v3t, v4f)) should equal(1.0) - v2Factor.get(List(v1t, v2t, v3f, v4t)) should equal(0.0) - v2Factor.get(List(v1t, v2t, v3f, v4f)) should equal(0.0) - v2Factor.get(List(v1t, v2f, v3t, v4t)) should equal(0.0) - v2Factor.get(List(v1t, v2f, v3t, v4f)) should equal(0.0) - v2Factor.get(List(v1t, v2f, v3f, v4t)) should equal(1.0) - v2Factor.get(List(v1t, v2f, v3f, v4f)) should equal(1.0) - v2Factor.get(List(v1f, v2t, v3t, v4t)) should equal(1.0) - v2Factor.get(List(v1f, v2t, v3t, v4f)) should equal(0.0) - v2Factor.get(List(v1f, v2t, v3f, v4t)) should equal(1.0) - v2Factor.get(List(v1f, v2t, v3f, v4f)) should equal(0.0) - v2Factor.get(List(v1f, v2f, v3t, v4t)) should equal(0.0) - v2Factor.get(List(v1f, v2f, v3t, v4f)) should equal(1.0) - v2Factor.get(List(v1f, v2f, v3f, v4t)) should equal(0.0) - v2Factor.get(List(v1f, v2f, v3f, v4f)) should equal(1.0) - } - } - - "given an apply of one argument" should { - "produce a factor that matches the argument to the result via the function" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Apply(v1, (i: Int) => i % 2) - Values()(v2) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v20 = v2Vals indexOf Regular(0) - val v21 = v2Vals indexOf Regular(1) - val List(factor) = Factory.make(v2) - factor.get(List(v11, v20)) should equal(0.0) - factor.get(List(v11, v21)) should equal(1.0) - factor.get(List(v12, v20)) should equal(1.0) - factor.get(List(v12, v21)) should equal(0.0) - factor.get(List(v13, v20)) should equal(0.0) - factor.get(List(v13, v21)) should equal(1.0) - } - } - - "given an apply of two arguments" should { - "produce a factor that matches the arguments to the result via the function" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Select(0.5 -> 2, 0.5 -> 3) - val v3 = Apply(v1, v2, (i: Int, j: Int) => i % j) - Values()(v3) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v22 = v2Vals indexOf Regular(2) - val v23 = v2Vals indexOf Regular(3) - val v30 = v3Vals indexOf Regular(0) - val v31 = v3Vals indexOf Regular(1) - val v32 = v3Vals indexOf Regular(2) - val List(factor) = Factory.make(v3) - factor.get(List(v11, v22, v30)) should equal(0.0) - factor.get(List(v11, v22, v31)) should equal(1.0) - factor.get(List(v11, v22, v32)) should equal(0.0) - factor.get(List(v11, v23, v30)) should equal(0.0) - factor.get(List(v11, v23, v31)) should equal(1.0) - factor.get(List(v11, v23, v32)) should equal(0.0) - factor.get(List(v12, v22, v30)) should equal(1.0) - factor.get(List(v12, v22, v31)) should equal(0.0) - factor.get(List(v12, v22, v32)) should equal(0.0) - factor.get(List(v12, v23, v30)) should equal(0.0) - factor.get(List(v12, v23, v31)) should equal(0.0) - factor.get(List(v12, v23, v32)) should equal(1.0) - factor.get(List(v13, v22, v30)) should equal(0.0) - factor.get(List(v13, v22, v31)) should equal(1.0) - factor.get(List(v13, v22, v32)) should equal(0.0) - factor.get(List(v13, v23, v30)) should equal(1.0) - factor.get(List(v13, v23, v31)) should equal(0.0) - factor.get(List(v13, v23, v32)) should equal(0.0) - } - } - - "given an apply of three arguments" should { - "produce a factor that matches the arguments to the result via the function" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Select(0.5 -> 1, 0.5 -> 2) - val v3 = Constant(1) - val v4: Apply3[Int, Int, Int, Int] = Apply(v1, v2, v3, (i: Int, j: Int, k: Int) => i % (j + k)) - Values()(v4) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v4Vals = Variable(v4).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v21 = v2Vals indexOf Regular(1) - val v22 = v2Vals indexOf Regular(2) - val v31 = v3Vals indexOf Regular(1) - val v40 = v4Vals indexOf Regular(0) - val v41 = v4Vals indexOf Regular(1) - val v42 = v4Vals indexOf Regular(2) - val List(factor) = Factory.make(v4) - factor.get(List(v11, v21, v31, v40)) should equal(0.0) - factor.get(List(v11, v21, v31, v41)) should equal(1.0) - factor.get(List(v11, v21, v31, v42)) should equal(0.0) - factor.get(List(v11, v22, v31, v40)) should equal(0.0) - factor.get(List(v11, v22, v31, v41)) should equal(1.0) - factor.get(List(v11, v22, v31, v42)) should equal(0.0) - factor.get(List(v12, v21, v31, v40)) should equal(1.0) - factor.get(List(v12, v21, v31, v41)) should equal(0.0) - factor.get(List(v12, v21, v31, v42)) should equal(0.0) - factor.get(List(v12, v22, v31, v40)) should equal(0.0) - factor.get(List(v12, v22, v31, v41)) should equal(0.0) - factor.get(List(v12, v22, v31, v42)) should equal(1.0) - factor.get(List(v13, v21, v31, v40)) should equal(0.0) - factor.get(List(v13, v21, v31, v41)) should equal(1.0) - factor.get(List(v13, v21, v31, v42)) should equal(0.0) - factor.get(List(v13, v22, v31, v40)) should equal(1.0) - factor.get(List(v13, v22, v31, v41)) should equal(0.0) - factor.get(List(v13, v22, v31, v42)) should equal(0.0) - } - } - - "given an apply of four arguments" should { - "produce a factor that matches the arguments to the result via the function" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Select(0.5 -> 1, 0.5 -> 2) - val v3 = Constant(1) - val v4 = Flip(0.7) - val v5: Apply4[Int, Int, Int, Boolean, Int] = - Apply(v1, v2, v3, v4, (i: Int, j: Int, k: Int, b: Boolean) => if (b) 0; else i % (j + k)) - Values()(v5) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v4Vals = Variable(v4).range - val v5Vals = Variable(v5).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v21 = v2Vals indexOf Regular(1) - val v22 = v2Vals indexOf Regular(2) - val v31 = v3Vals indexOf Regular(1) - val v4true = v4Vals indexOf Regular(true) - val v4false = v4Vals indexOf Regular(false) - val v50 = v5Vals indexOf Regular(0) - val v51 = v5Vals indexOf Regular(1) - val v52 = v5Vals indexOf Regular(2) - val List(factor) = Factory.make(v5) - factor.get(List(v11, v21, v31, v4false, v50)) should equal(0.0) - factor.get(List(v11, v21, v31, v4false, v51)) should equal(1.0) - factor.get(List(v11, v21, v31, v4false, v52)) should equal(0.0) - factor.get(List(v11, v22, v31, v4false, v50)) should equal(0.0) - factor.get(List(v11, v22, v31, v4false, v51)) should equal(1.0) - factor.get(List(v11, v22, v31, v4false, v52)) should equal(0.0) - factor.get(List(v12, v21, v31, v4false, v50)) should equal(1.0) - factor.get(List(v12, v21, v31, v4false, v51)) should equal(0.0) - factor.get(List(v12, v21, v31, v4false, v52)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v50)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v51)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v52)) should equal(1.0) - factor.get(List(v13, v21, v31, v4false, v50)) should equal(0.0) - factor.get(List(v13, v21, v31, v4false, v51)) should equal(1.0) - factor.get(List(v13, v21, v31, v4false, v52)) should equal(0.0) - factor.get(List(v13, v22, v31, v4false, v50)) should equal(1.0) - factor.get(List(v13, v22, v31, v4false, v51)) should equal(0.0) - factor.get(List(v13, v22, v31, v4false, v52)) should equal(0.0) - - factor.get(List(v11, v21, v31, v4true, v50)) should equal(1.0) - factor.get(List(v11, v21, v31, v4true, v51)) should equal(0.0) - factor.get(List(v11, v21, v31, v4true, v52)) should equal(0.0) - factor.get(List(v11, v22, v31, v4true, v50)) should equal(1.0) - factor.get(List(v11, v22, v31, v4true, v51)) should equal(0.0) - factor.get(List(v11, v22, v31, v4true, v52)) should equal(0.0) - factor.get(List(v12, v21, v31, v4true, v50)) should equal(1.0) - factor.get(List(v12, v21, v31, v4true, v51)) should equal(0.0) - factor.get(List(v12, v21, v31, v4true, v52)) should equal(0.0) - factor.get(List(v12, v22, v31, v4true, v50)) should equal(1.0) - factor.get(List(v12, v22, v31, v4true, v51)) should equal(0.0) - factor.get(List(v12, v22, v31, v4true, v52)) should equal(0.0) - factor.get(List(v13, v21, v31, v4true, v50)) should equal(1.0) - factor.get(List(v13, v21, v31, v4true, v51)) should equal(0.0) - factor.get(List(v13, v21, v31, v4true, v52)) should equal(0.0) - factor.get(List(v13, v22, v31, v4true, v50)) should equal(1.0) - factor.get(List(v13, v22, v31, v4true, v51)) should equal(0.0) - factor.get(List(v13, v22, v31, v4true, v52)) should equal(0.0) - } - } - - "given an apply of five arguments" should { - "produce a factor that matches the arguments to the result via the function" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Select(0.5 -> 1, 0.5 -> 2) - val v3 = Constant(1) - val v4 = Flip(0.7) - val v5 = Constant(false) - val v6: Apply5[Int, Int, Int, Boolean, Boolean, Int] = - Apply(v1, v2, v3, v4, v5, - (i: Int, j: Int, k: Int, b: Boolean, c: Boolean) => if (b || c) 0; else i % (j + k)) - Values()(v6) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v4Vals = Variable(v4).range - val v5Vals = Variable(v5).range - val v6Vals = Variable(v6).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v21 = v2Vals indexOf Regular(1) - val v22 = v2Vals indexOf Regular(2) - val v31 = v3Vals indexOf Regular(1) - val v4true = v4Vals indexOf Regular(true) - val v4false = v4Vals indexOf Regular(false) - val v5false = v5Vals indexOf Regular(false) - val v60 = v6Vals indexOf Regular(0) - val v61 = v6Vals indexOf Regular(1) - val v62 = v6Vals indexOf Regular(2) - val List(factor) = Factory.make(v6) - - factor.get(List(v11, v21, v31, v4false, v5false, v60)) should equal(0.0) - factor.get(List(v11, v21, v31, v4false, v5false, v61)) should equal(1.0) - factor.get(List(v11, v21, v31, v4false, v5false, v62)) should equal(0.0) - factor.get(List(v11, v22, v31, v4false, v5false, v60)) should equal(0.0) - factor.get(List(v11, v22, v31, v4false, v5false, v61)) should equal(1.0) - factor.get(List(v11, v22, v31, v4false, v5false, v62)) should equal(0.0) - factor.get(List(v12, v21, v31, v4false, v5false, v60)) should equal(1.0) - factor.get(List(v12, v21, v31, v4false, v5false, v61)) should equal(0.0) - factor.get(List(v12, v21, v31, v4false, v5false, v62)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v5false, v60)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v5false, v61)) should equal(0.0) - factor.get(List(v12, v22, v31, v4false, v5false, v62)) should equal(1.0) - factor.get(List(v13, v21, v31, v4false, v5false, v60)) should equal(0.0) - factor.get(List(v13, v21, v31, v4false, v5false, v61)) should equal(1.0) - factor.get(List(v13, v21, v31, v4false, v5false, v62)) should equal(0.0) - factor.get(List(v13, v22, v31, v4false, v5false, v60)) should equal(1.0) - factor.get(List(v13, v22, v31, v4false, v5false, v61)) should equal(0.0) - factor.get(List(v13, v22, v31, v4false, v5false, v62)) should equal(0.0) - - factor.get(List(v11, v21, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v11, v21, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v11, v21, v31, v4true, v5false, v62)) should equal(0.0) - factor.get(List(v11, v22, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v11, v22, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v11, v22, v31, v4true, v5false, v62)) should equal(0.0) - factor.get(List(v12, v21, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v12, v21, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v12, v21, v31, v4true, v5false, v62)) should equal(0.0) - factor.get(List(v12, v22, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v12, v22, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v12, v22, v31, v4true, v5false, v62)) should equal(0.0) - factor.get(List(v13, v21, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v13, v21, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v13, v21, v31, v4true, v5false, v62)) should equal(0.0) - factor.get(List(v13, v22, v31, v4true, v5false, v60)) should equal(1.0) - factor.get(List(v13, v22, v31, v4true, v5false, v61)) should equal(0.0) - factor.get(List(v13, v22, v31, v4true, v5false, v62)) should equal(0.0) - } - } - - "given an Inject" should { - "produces a factor that matches its inputs to the correct sequence" in { - Universe.createNew() - val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) - val v2 = Select(0.5 -> 4, 0.5 -> 5) - val v3 = Inject(v1, v2) - Values()(v3) - val List(factor) = Factory.make(v3) - - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v3Vals = Variable(v3).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v24 = v2Vals indexOf Regular(4) - val v25 = v2Vals indexOf Regular(5) - val v314 = v3Vals indexOf Regular(List(1, 4)) - val v315 = v3Vals indexOf Regular(List(1, 5)) - val v324 = v3Vals indexOf Regular(List(2, 4)) - val v325 = v3Vals indexOf Regular(List(2, 5)) - val v334 = v3Vals indexOf Regular(List(3, 4)) - val v335 = v3Vals indexOf Regular(List(3, 5)) - - factor.get(List(v314, v11, v24)) should equal(1.0) - factor.get(List(v315, v11, v25)) should equal(1.0) - factor.get(List(v324, v12, v24)) should equal(1.0) - factor.get(List(v325, v12, v25)) should equal(1.0) - factor.get(List(v334, v13, v24)) should equal(1.0) - factor.get(List(v335, v13, v25)) should equal(1.0) - - factor.get(List(v314, v11, v25)) should equal(0.0) - factor.get(List(v315, v11, v24)) should equal(0.0) - factor.get(List(v324, v12, v25)) should equal(0.0) - factor.get(List(v325, v12, v24)) should equal(0.0) - factor.get(List(v334, v13, v25)) should equal(0.0) - factor.get(List(v335, v13, v24)) should equal(0.0) - - factor.get(List(v314, v12, v24)) should equal(0.0) - factor.get(List(v315, v12, v25)) should equal(0.0) - factor.get(List(v324, v13, v24)) should equal(0.0) - factor.get(List(v325, v13, v25)) should equal(0.0) - factor.get(List(v334, v11, v24)) should equal(0.0) - factor.get(List(v335, v11, v25)) should equal(0.0) - - factor.get(List(v314, v12, v25)) should equal(0.0) - factor.get(List(v315, v12, v24)) should equal(0.0) - factor.get(List(v324, v13, v25)) should equal(0.0) - factor.get(List(v325, v13, v24)) should equal(0.0) - factor.get(List(v334, v11, v25)) should equal(0.0) - factor.get(List(v335, v11, v24)) should equal(0.0) - - factor.get(List(v314, v13, v24)) should equal(0.0) - factor.get(List(v315, v13, v25)) should equal(0.0) - factor.get(List(v324, v11, v24)) should equal(0.0) - factor.get(List(v325, v11, v25)) should equal(0.0) - factor.get(List(v334, v12, v24)) should equal(0.0) - factor.get(List(v335, v12, v25)) should equal(0.0) - - factor.get(List(v314, v13, v25)) should equal(0.0) - factor.get(List(v315, v13, v24)) should equal(0.0) - factor.get(List(v324, v11, v25)) should equal(0.0) - factor.get(List(v325, v11, v24)) should equal(0.0) - factor.get(List(v334, v12, v25)) should equal(0.0) - factor.get(List(v335, v12, v24)) should equal(0.0) - } - } - - "given a non-trivial condition and constraint" should { - "produce the correct constraint factors" in { - Universe.createNew() - val v1 = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) - v1.setCondition((i: Int) => i != 2) - v1.setConstraint(((i: Int) => i.toDouble)) - Values()(v1) - val List(condFactor, constrFactor, _) = Factory.make(v1) - val v1Vals = Variable(v1).range - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - condFactor.get(List(v11)) should be(1.0 +- 0.000000001) - condFactor.get(List(v12)) should be(0.0 +- 0.000000001) - condFactor.get(List(v13)) should be(1.0 +- 0.000000001) - constrFactor.get(List(v11)) should be(1.0 +- 0.000000001) - constrFactor.get(List(v12)) should be(2.0 +- 0.000000001) - constrFactor.get(List(v13)) should be(3.0 +- 0.000000001) - } - } - - "given an element whose expanded values are only *" should { - "produce no factors" in { - Universe.createNew() - val f = Flip(0.5) - val lv = LazyValues() - lv.expandAll(Set((f, -1))) - val factors = Factory.make(f) - factors should be(empty) - } - } - } - - "Making a factor for a dependent universe" should { - "produce a correct dependent factor" in { - Universe.createNew() - val x = Flip(0.1) - val y = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) - Values()(x) - Values()(y) - val dependentUniverse = new Universe(List(x, y)) - val u1 = Uniform(0.0, 1.0)("", dependentUniverse) - val u2 = Uniform(0.0, 2.0)("", dependentUniverse) - val a = CachingChain(x, y, (x: Boolean, y: Int) => if (x || y < 2) u1; else u2)("a", dependentUniverse) - Values(dependentUniverse)(a) - val evidence = List(NamedEvidence("a", Condition((d: Double) => d < 0.5))) - val factor = - Factory.makeDependentFactor(Universe.universe, dependentUniverse, () => ProbEvidenceSampler.computeProbEvidence(20000, evidence)(dependentUniverse)) - val xVar = Variable(x) - val yVar = Variable(y) - val variables = factor.variables - variables.toSet should equal(Set(xVar, yVar)) - val xIndex = variables indexOf xVar - val yIndex = variables indexOf yVar - val xFalse = xVar.range indexOf Regular(false) - val xTrue = xVar.range indexOf Regular(true) - val y1 = yVar.range indexOf Regular(1) - val y2 = yVar.range indexOf Regular(2) - val y3 = yVar.range indexOf Regular(3) - // If x is true or y is 1, pe is 0.5; if both false, 0.25. - if (xIndex == 0) { - factor.get(List(xFalse, y2)) should be(0.25 +- 0.01) - factor.get(List(xFalse, y3)) should be(0.25 +- 0.01) - factor.get(List(xFalse, y1)) should be(0.5 +- 0.01) - factor.get(List(xTrue, y1)) should be(0.5 +- 0.01) - factor.get(List(xTrue, y2)) should be(0.5 +- 0.01) - factor.get(List(xTrue, y3)) should be(0.5 +- 0.01) - } else { - factor.get(List(y2, xFalse)) should be(0.25 +- 0.01) - factor.get(List(y3, xFalse)) should be(0.25 +- 0.01) - factor.get(List(y1, xTrue)) should be(0.5 +- 0.01) - factor.get(List(y1, xFalse)) should be(0.5 +- 0.01) - factor.get(List(y2, xFalse)) should be(0.5 +- 0.01) - factor.get(List(y3, xFalse)) should be(0.5 +- 0.01) - } - } - } - - "Making factors for multiple universes" should { - "produce the same range of values as if it were in a single universe" when { - "given a simple model with two universes" in { - Universe.createNew() - val v1u1 = Select(0.3 -> 0, 0.5 -> 1, 0.2 -> 3) - val v2u1 = Apply(v1u1, (i: Int) => i % 3) - - Universe.createNew() - val v1u2 = Select(0.3 -> 0, 0.5 -> 1, 0.2 -> 3) - Universe.createNew - val v2u3 = Apply(v1u1, (i: Int) => i % 3) - - (Variable(v1u1).range) should equal(Variable(v1u2).range) - (Variable(v2u1).range) should equal(Variable(v2u3).range) - } - - "given a model with multiple universes" in { - Universe.createNew() - val func = (i: Int, b: Boolean) => if (b) i else i + 1 - val v1u1 = Select(0.1 -> 0, 0.2 -> 2, 0.7 -> 5) - val v2u1 = Flip(0.3) - val v3u1 = Apply(v1u1, v2u1, func) - val v4u1 = Flip(0.5) - val v5u1 = Apply(v3u1, v4u1, func) - - Universe.createNew() - val v1u2 = Select(0.1 -> 0, 0.2 -> 2, 0.7 -> 5) - Universe.createNew() - val v2u3 = Flip(0.3) - Universe.createNew() - val v3u4 = Apply(v1u1, v2u1, func) - Universe.createNew() - val v4u5 = Flip(0.5) - Universe.createNew() - val v5u6 = Apply(v3u1, v4u1, func) - - (Variable(v5u1).range) should equal(Variable(v5u6).range) - } - - "given a multi-universe model with Chains" in { - Universe.createNew() - val func1 = (i: Int) => if (i % 2 == 0) Constant(i) else Select(0.4 -> (i - 1), 0.6 -> (i + 1)) - val func2 = (i: Int) => if (i % 4 == 0) Select(0.2 -> (i - 1), 0.8 -> (i + 1)) else Constant(i) - val v1u1 = Select(0.2 -> 0, 0.5 -> 3, 0.3 -> 6) - val v2u1 = Chain(v1u1, func1) - val v3u1 = Chain(v2u1, func2) - - Universe.createNew() - val v1u2 = Select(0.2 -> 0, 0.5 -> 3, 0.3 -> 6) - Universe.createNew() - val v2u3 = Chain(v1u1, func1) - Universe.createNew() - val v3u4 = Chain(v2u1, func2) - - (Variable(v3u1).range) should equal(Variable(v3u4).range) - } - } - - "correctly produce a factor between elements in multiple universes" when { - "given a simple model in two universes" in { - Universe.createNew() - val v1 = Select(0.3 -> 0, 0.2 -> 1, 0.4 -> 2, 0.1 -> 3) - Universe.createNew() - val v2 = Apply(v1, (i: Int) => i / 2) - Values()(v2) - val v1Vals = Variable(v1).range - val v2Vals = Variable(v2).range - val v10 = v1Vals indexOf Regular(0) - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v13 = v1Vals indexOf Regular(3) - val v20 = v2Vals indexOf Regular(0) - val v21 = v2Vals indexOf Regular(1) - val List(factor) = Factory.make(v2) - factor.get(List(v10, v20)) should equal(1.0) - factor.get(List(v10, v21)) should equal(0.0) - factor.get(List(v11, v20)) should equal(1.0) - factor.get(List(v11, v21)) should equal(0.0) - factor.get(List(v12, v20)) should equal(0.0) - factor.get(List(v12, v21)) should equal(1.0) - factor.get(List(v13, v20)) should equal(0.0) - factor.get(List(v13, v21)) should equal(1.0) - } - - "given a multi-universe model with Chains" in { - Universe.createNew() - val v1 = Select(0.2 -> 0, 0.7 -> 1, 0.1 -> 2) - Universe.createNew() - val v2 = Constant(2) - Universe.createNew() - val v3 = Select(0.4 -> 0, 0.6 -> 1) - Universe.createNew() - val v4 = Chain(v1, (i: Int) => if (i % 2 == 0) v2 else v3) - Values()(v4) - val v1Vals = Variable(v1).range - val v3Vals = Variable(v3).range - val v4Vals = Variable(v4).range - val v10 = v1Vals indexOf Regular(0) - val v11 = v1Vals indexOf Regular(1) - val v12 = v1Vals indexOf Regular(2) - val v30 = v3Vals indexOf Regular(0) - val v31 = v3Vals indexOf Regular(1) - val v40 = v4Vals indexOf Regular(0) - val v41 = v4Vals indexOf Regular(1) - val v42 = v4Vals indexOf Regular(2) - - val factor = Factory.make(v4) - val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) - - v4Factor.get(List(v10, v40, 0, v30)) should equal(0.0) - v4Factor.get(List(v10, v40, 0, v31)) should equal(0.0) - v4Factor.get(List(v10, v41, 0, v30)) should equal(0.0) - v4Factor.get(List(v10, v41, 0, v31)) should equal(0.0) - v4Factor.get(List(v10, v42, 0, v30)) should equal(1.0) - v4Factor.get(List(v10, v42, 0, v31)) should equal(1.0) - v4Factor.get(List(v11, v40, 0, v30)) should equal(1.0) - v4Factor.get(List(v11, v40, 0, v31)) should equal(0.0) - v4Factor.get(List(v11, v41, 0, v30)) should equal(0.0) - v4Factor.get(List(v11, v41, 0, v31)) should equal(1.0) - v4Factor.get(List(v11, v42, 0, v30)) should equal(0.0) - v4Factor.get(List(v11, v42, 0, v31)) should equal(0.0) - v4Factor.get(List(v12, v40, 0, v30)) should equal(0.0) - v4Factor.get(List(v12, v40, 0, v31)) should equal(0.0) - v4Factor.get(List(v12, v41, 0, v30)) should equal(0.0) - v4Factor.get(List(v12, v41, 0, v31)) should equal(0.0) - v4Factor.get(List(v12, v42, 0, v30)) should equal(1.0) - v4Factor.get(List(v12, v42, 0, v31)) should equal(1.0) - } - } - } -} +/* + * FactorTest.scala + * Factor tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Jan 1, 2009 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.factored + +import org.scalatest.Matchers +import org.scalatest.PrivateMethodTester +import org.scalatest.WordSpec +import com.cra.figaro.algorithm.Values +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored.LazyValues +import com.cra.figaro.algorithm.lazyfactored.Regular +import com.cra.figaro.algorithm.lazyfactored.ValueSet +import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler +import com.cra.figaro.language.Apply +import com.cra.figaro.language.Apply3 +import com.cra.figaro.language.Apply4 +import com.cra.figaro.language.Apply5 +import com.cra.figaro.language.CachingChain +import com.cra.figaro.language.Chain +import com.cra.figaro.language.Condition +import com.cra.figaro.language.Constant +import com.cra.figaro.language.Dist +import com.cra.figaro.language.Flip +import com.cra.figaro.language.Inject +import com.cra.figaro.language.Name.stringToName +import com.cra.figaro.language.NamedEvidence +import com.cra.figaro.language.Reference.stringToReference +import com.cra.figaro.language.Select +import com.cra.figaro.language.Universe +import com.cra.figaro.library.atomic.continuous.Normal +import com.cra.figaro.library.atomic.continuous.Uniform +import com.cra.figaro.library.compound.CPD +import com.cra.figaro.algorithm.factored.ParticleGenerator + +class FactorTest extends WordSpec with Matchers with PrivateMethodTester { + + "A variable for an element" should { + "have range equal to the element's values" in { + Universe.createNew() + val e1 = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) + val xs = Values()(e1) + val v1 = Variable(e1) + for { x <- v1.range } { xs should contain(x.value) } + //for { x <- xs } { v1.range should contain(x) } + } + + "always be equal to another variable for the same element" in { + Universe.createNew() + val e1 = Flip(0.2) + Values()(e1) + val v1 = Variable(e1) + val v2 = Variable(e1) + v1 should equal(v2) + } + + "always contain the same id even if the Variable cache is cleared" in { + Universe.createNew() + val e1 = Flip(0.2) + Values()(e1) + val v1 = Variable(e1).id + Variable.clearCache + LazyValues.clear(Universe.universe) + Values()(e1) + val v2 = Variable(e1).id + v1 should equal(v2) + } + + "always be equal to a variable with the same id" in { + Universe.createNew() + val e1 = Flip(0.2) + Values()(e1) + val v1 = Variable(e1) + val v2 = new Variable(ValueSet.withStar(Set[Boolean]())) {override val id = v1.id} + v1 == v2 should equal(true) + } + + "be different to a variable for a different element with the same definition" in { + Universe.createNew() + val e1 = Flip(0.2) + val e2 = Flip(0.2) + Values()(e1) + Values()(e2) + val v1 = Variable(e1) + val v2 = Variable(e2) + v1 should not equal (v2) + } + } + + "A factor" when { + "get the same value for a given set of variable indices as was last set" in { + Universe.createNew() + val e1 = Flip(0.1) + val e2 = Constant(8) + val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e4 = Flip(0.7) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val indices = List(1, 0, 2, 1) + f.set(indices, 0.2) + f.set(indices, 0.3) + f.get(indices) should equal(0.3) + } + + "get updated set of factors for an element when the factors have been updated" in { + Universe.createNew() + val v1 = Flip(0.5) + Values()(v1) + val f1 = Factory.make(v1)(0) + val f1mod = f1.mapTo((d: Double) => 2.0*d) + Factory.updateFactor(v1, List(f1mod)) + Factory.make(v1)(0).get(List(0)) should equal(f1mod.get(List(0))) + } + + "have the first index List be all zeros" in { + Universe.createNew() + val e1 = Flip(0.1) + val e2 = Constant(8) + val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e4 = Flip(0.7) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + f.firstIndices should equal(List(0, 0, 0, 0)) + } + + "have the next index List carry and add correctly" in { + Universe.createNew() + val e1 = Flip(0.1) + val e2 = Constant(8) + val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e4 = Flip(0.7) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val ia = List(1, 0, 1, 1) + val ar = f.nextIndices(ia).get + ar should equal(List(1, 0, 2, 0)) + } + + "produce None when the index Lists are exhausted" in { + Universe.createNew() + val e1 = Flip(0.1) + val e2 = Constant(8) + val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e4 = Flip(0.7) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val ia = List(1, 0, 2, 1) + f.nextIndices(ia) should equal(None) + } + + "compute the union of variables in two factors and the correct index maps when calling unionVars" in { + Universe.createNew() + val e1 = Flip(0.1) + val e2 = Constant(8) + val e3 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e4 = Flip(0.7) + val e5 = Constant('a) + val e6 = Select(0.1 -> 1.5, 0.9 -> 2.5) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + Values()(e5) + Values()(e6) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val v5 = Variable(e5) + val v6 = Variable(e6) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) + val unionVars = PrivateMethod[(List[Variable[_]], List[Variable[_]], List[Int], List[Int])]('unionVars) + val (parents, output, indexMap1, indexMap2) = f invokePrivate unionVars(g) + val union = parents ::: output + union should equal(List(v1, v2, v3, v4, v5, v6)) + indexMap1 should equal(List(0, 1, 2, 3)) + indexMap2 should equal(List(4, 2, 1, 5)) + } + + "multiplying with another factor" should { + "return the product of the two factors" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Constant(8) + val e3 = Flip(0.1) + val e4 = Flip(0.6) + Values()(e1) + Values()(e2) + Values()(e3) + Values()(e4) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val v4 = Variable(e4) + val f = Factory.simpleMake[Double](List(v1, v2, v3)) + val g = Factory.simpleMake[Double](List(v4, v3)) + f.set(List(0, 0, 0), 0.0) + f.set(List(1, 0, 0), 0.1) + f.set(List(2, 0, 0), 0.2) + f.set(List(0, 0, 1), 0.3) + f.set(List(1, 0, 1), 0.4) + f.set(List(2, 0, 1), 0.5) + g.set(List(0, 0), 0.6) + g.set(List(1, 0), 0.7) + g.set(List(0, 1), 0.8) + g.set(List(1, 1), 0.9) + val h = f.product(g, SumProductSemiring) + h.variables should equal(List(v1, v2, v3, v4)) + h.get(List(0, 0, 0, 0)) should be(0.0 +- 0.0001) + h.get(List(1, 0, 0, 0)) should be(0.06 +- 0.0001) + h.get(List(2, 0, 0, 0)) should be(0.12 +- 0.0001) + h.get(List(0, 0, 1, 0)) should be(0.24 +- 0.0001) + h.get(List(1, 0, 1, 0)) should be(0.32 +- 0.0001) + h.get(List(2, 0, 1, 0)) should be(0.4 +- 0.0001) + h.get(List(0, 0, 0, 1)) should be(0.0 +- 0.0001) + h.get(List(1, 0, 0, 1)) should be(0.07 +- 0.0001) + h.get(List(2, 0, 0, 1)) should be(0.14 +- 0.0001) + h.get(List(0, 0, 1, 1)) should be(0.27 +- 0.0001) + h.get(List(1, 0, 1, 1)) should be(0.36 +- 0.0001) + h.get(List(2, 0, 1, 1)) should be(0.45 +- 0.0001) + } + } + + "calling sumOver on a variable" should { + "return the sum over the variable of the factor" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Constant(8) + val e3 = Flip(0.1) + Values()(e1) + Values()(e2) + Values()(e3) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val f = Factory.simpleMake[Double](List(v1, v2, v3)) + f.set(List(0, 0, 0), 0.0) + f.set(List(1, 0, 0), 0.1) + f.set(List(2, 0, 0), 0.2) + f.set(List(0, 0, 1), 0.3) + f.set(List(1, 0, 1), 0.4) + f.set(List(2, 0, 1), 0.5) + val g = f.sumOver(v3, SumProductSemiring) + g.variables should equal(List(v1, v2)) + g.get(List(0, 0)) should be(0.3 +- 0.0001) + g.get(List(1, 0)) should be(0.5 +- 0.0001) + g.get(List(2, 0)) should be(0.7 +- 0.0001) + } + + "return itself if the variable not in the factor" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Constant(8) + val e3 = Flip(0.1) + Values()(e1) + Values()(e2) + Values()(e3) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val f = Factory.simpleMake[Double](List(v1, v2)) + f.set(List(0, 0), 0.0) + f.set(List(1, 0), 0.2) + f.set(List(2, 0), 0.4) + val g = f.sumOver(v3, SumProductSemiring) + g.variables should equal(f.variables) + for { indices <- f.allIndices } { + g.get(indices) should equal(f.get(indices)) + } + } + + "return a factor with all columns of the variable removed, ignoring rows in which " + + "the variable has different values in different columns" in { + Universe.createNew() + val e1 = Flip(0.9) + val e2 = Select(0.2 -> 1, 0.8 -> 2) + Values()(e1) + Values()(e2) + val v1 = Variable(e1) + val v2 = Variable(e2) + val f = Factory.simpleMake[Double](List(v1, v2, v1)) + f.set(List(0, 0, 0), 0.1) + f.set(List(1, 0, 0), 0.2) + f.set(List(0, 1, 0), 0.3) + f.set(List(1, 1, 0), 0.4) + f.set(List(0, 0, 1), 0.5) + f.set(List(1, 0, 1), 0.6) + f.set(List(0, 1, 1), 0.7) + f.set(List(1, 1, 1), 0.8) + val g = f.sumOver(v1, SumProductSemiring) + g.variables should equal(List(v2)) + g.get(List(0)) should equal(0.1 + 0.6) + g.get(List(1)) should equal(0.3 + 0.8) + } + } + + "calling record on a variable" should { + "return the argmax over the values associated with the variable" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Constant(8) + val e3 = Flip(0.1) + Values()(e1) + Values()(e2) + Values()(e3) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val f = Factory.simpleMake[Double](List(v1, v2, v3)) + f.set(List(0, 0, 0), 0.6) + f.set(List(1, 0, 0), 0.1) + f.set(List(2, 0, 0), 0.2) + f.set(List(0, 0, 1), 0.3) + f.set(List(1, 0, 1), 0.4) + f.set(List(2, 0, 1), 0.5) + val g = f.recordArgMax(v3, (x: Double, y: Double) => x < y) + g.variables should equal(List(v1, v2)) + g.get(List(0, 0)) should equal(true) + g.get(List(1, 0)) should equal(false) + g.get(List(2, 0)) should equal(false) + } + } + + "after marginalizing to a variable" should { + "return the marginal distribution over the variable" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Constant(8) + val e3 = Flip(0.1) + Values()(e1) + Values()(e2) + Values()(e3) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val f = Factory.simpleMake[Double](List(v1, v2, v3)) + f.set(List(0, 0, 0), 0.0) + f.set(List(1, 0, 0), 0.1) + f.set(List(2, 0, 0), 0.2) + f.set(List(0, 0, 1), 0.3) + f.set(List(1, 0, 1), 0.4) + f.set(List(2, 0, 1), 0.5) + val g = f.marginalizeTo(SumProductSemiring, v3) + g.variables should equal(List(v3)) + val p1 = 0.0 + 0.1 + 0.2 + val p2 = 0.3 + 0.4 + 0.5 + g.get(List(0)) should be(p1 +- 0.000001) + g.get(List(1)) should be(p2 +- 0.000001) + } + } + } + + "after marginalizing to two variables" should { + "return the marginal distribution over the variables" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Flip(0.5) + val e3 = Flip(0.1) + Values()(e1) + Values()(e2) + Values()(e3) + val v1 = Variable(e1) + val v2 = Variable(e2) + val v3 = Variable(e3) + val f = Factory.simpleMake[Double](List(v1, v2, v3)) + f.set(List(0, 0, 0), 0.0) + f.set(List(1, 0, 0), 0.05) + f.set(List(2, 0, 0), 0.1) + f.set(List(0, 0, 1), 0.15) + f.set(List(1, 0, 1), 0.2) + f.set(List(2, 0, 1), 0.25) + f.set(List(0, 1, 0), 0.0) + f.set(List(1, 1, 0), 0.05) + f.set(List(2, 1, 0), 0.1) + f.set(List(0, 1, 1), 0.15) + f.set(List(1, 1, 1), 0.2) + f.set(List(2, 1, 1), 0.25) + val g = f.marginalizeTo(SumProductSemiring, v1, v3) + g.variables should equal(List(v1, v3)) + g.get(List(0, 0)) should be(0.0 +- 0.000001) + g.get(List(1, 0)) should be(0.1 +- 0.000001) + g.get(List(2, 0)) should be(0.2 +- 0.000001) + g.get(List(0, 1)) should be(0.3 +- 0.000001) + g.get(List(1, 1)) should be(0.4 +- 0.000001) + g.get(List(2, 1)) should be(0.5 +- 0.000001) + } + } + + "after deduplicating a factor" should { + "have no repeated variables" in { + Universe.createNew() + val e1 = Select(0.2 -> "a", 0.3 -> "b", 0.5 -> "c") + val e2 = Flip(0.3) + Values()(e1) + Values()(e2) + val v1 = Variable(e1) + val v2 = Variable(e2) + val f = Factory.simpleMake[Double](List(v1, v2, v2)) + f.set(List(0, 0, 0), 0.06) + f.set(List(0, 0, 1), 0.25) + f.set(List(0, 1, 0), 0.44) + f.set(List(0, 1, 1), 0.25) + f.set(List(1, 0, 0), 0.15) + f.set(List(1, 0, 1), 0.2) + f.set(List(1, 1, 0), 0.15) + f.set(List(1, 1, 1), 0.5) + f.set(List(2, 0, 0), 0.1) + f.set(List(2, 0, 1), 0.25) + f.set(List(2, 1, 0), 0.4) + f.set(List(2, 1, 1), 0.25) + + val g = f.deDuplicate() + + g.variables.size should be(2) + g.variables.contains(v1) should be(true) + g.variables.contains(v2) should be(true) + + if (g.variables.indexOf(v1) == 0) { + g.get(List(0, 0)) should be(0.06 +- 0.000001) + g.get(List(0, 1)) should be(0.25 +- 0.000001) + g.get(List(1, 0)) should be(0.15 +- 0.000001) + g.get(List(1, 1)) should be(0.5 +- 0.000001) + g.get(List(2, 0)) should be(0.1 +- 0.000001) + g.get(List(2, 1)) should be(0.25 +- 0.000001) + } else { + g.get(List(0, 0)) should be(0.06 +- 0.000001) + g.get(List(1, 0)) should be(0.25 +- 0.000001) + g.get(List(0, 1)) should be(0.15 +- 0.000001) + g.get(List(1, 1)) should be(0.5 +- 0.000001) + g.get(List(0, 2)) should be(0.1 +- 0.000001) + g.get(List(1, 2)) should be(0.25 +- 0.000001) + } + + } + } + "Making factors from an element" when { + + "given a constant" should { + "produce a single factor with one entry whose value is 1.0" in { + Universe.createNew() + val v1 = Constant(7) + Values()(v1) + val List(factor) = Factory.make(v1) + factor.get(List(0)) should equal(1.0) + } + } + + "given a simple flip" should { + "produce a single factor in which the first entry is the probability of true " + + "and the second entry is the probability of false" in { + Universe.createNew() + val v1 = Flip(0.3) + Values()(v1) + val List(factor) = Factory.make(v1) + factor.get(List(0)) should equal(0.3) + factor.get(List(1)) should equal(0.7) + } + } + + "given a complex flip" should { + "produce a single factor in which each possible value of the parent is associated with two " + + "entries, one each for true and false, with the appropriate probabilities" in { + Universe.createNew() + val v1 = Select(0.2 -> 0.1, 0.8 -> 0.3) + val v2 = Flip(v1) + Values()(v2) + val List(factor) = Factory.make(v2) + val vals = Variable(v1).range + val i1 = vals.indexOf(Regular(0.1)) + val i2 = vals.toList.indexOf(Regular(0.3)) + factor.get(List(i1, 0)) should equal(0.1) + factor.get(List(i1, 1)) should equal(0.9) + factor.get(List(i2, 0)) should equal(0.3) + factor.get(List(i2, 1)) should equal(0.7) + } + } + + "given a simple select" should { + "produce a single factor in which each possible value is associated with the correct probability" in { + Universe.createNew() + val v1 = Select(0.2 -> 1, 0.3 -> 0, 0.1 -> 2, 0.05 -> 5, 0.35 -> 4) + Values()(v1) + val List(factor) = Factory.make(v1) + val vals = Variable(v1).range + val i1 = vals.indexOf(Regular(1)) + val i0 = vals.indexOf(Regular(0)) + val i2 = vals.indexOf(Regular(2)) + val i5 = vals.indexOf(Regular(5)) + val i4 = vals.indexOf(Regular(4)) + factor.get(List(i1)) should equal(0.2) + factor.get(List(i0)) should equal(0.3) + factor.get(List(i2)) should equal(0.1) + } + } + + "given a complex select" should { + "produce a single factor in which values of the parents are associated with " + + "values of the select according to the normalized parent values" in { + Universe.createNew() + val v1 = Select(0.2 -> 0.2, 0.8 -> 0.8) + val v2 = Select(0.4 -> 0.4, 0.6 -> 0.6) + val c1 = Constant(0.1) + val c2 = Constant(0.3) + val c3 = Constant(0.5) + val v3 = Select(v1 -> 1, v2 -> 2, c1 -> 4, c2 -> 5, c3 -> 3) + Values()(v3) + val List(factor) = Factory.make(v3) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v102 = v1Vals.indexOf(Regular(0.2)) + val v108 = v1Vals.indexOf(Regular(0.8)) + val v204 = v2Vals.indexOf(Regular(0.4)) + val v206 = v2Vals.indexOf(Regular(0.6)) + val v31 = v3Vals.indexOf(Regular(1)) + val v32 = v3Vals.indexOf(Regular(2)) + val v34 = v3Vals.indexOf(Regular(4)) + val v35 = v3Vals.indexOf(Regular(5)) + val v33 = v3Vals.indexOf(Regular(3)) + def makeIndices(a: List[Int]): List[Int] = { + val result: Array[Int] = Array.ofDim(a.size) + result(v31) = a(1) + result(v32) = a(2) + result(v33) = a(3) + result(v34) = a(4) + result(v35) = a(5) + result(5) = a(0) + + result.toList + } + factor.get(makeIndices(List(v31, v102, v204, 0, 0, 0))) should be(0.2 / 1.5 +- 0.01) + factor.get(makeIndices(List(v32, v102, v204, 0, 0, 0))) should be(0.4 / 1.5 +- 0.01) + factor.get(makeIndices(List(v31, v108, v204, 0, 0, 0))) should be(0.8 / 2.1 +- 0.01) + factor.get(makeIndices(List(v32, v108, v204, 0, 0, 0))) should be(0.4 / 2.1 +- 0.01) + factor.get(makeIndices(List(v31, v102, v206, 0, 0, 0))) should be(0.2 / 1.7 +- 0.01) + factor.get(makeIndices(List(v32, v102, v206, 0, 0, 0))) should be(0.6 / 1.7 +- 0.01) + factor.get(makeIndices(List(v31, v108, v206, 0, 0, 0))) should be(0.8 / 2.3 +- 0.01) + factor.get(makeIndices(List(v32, v108, v206, 0, 0, 0))) should be(0.6 / 2.3 +- 0.01) + } + } + + "given a simple dist" should { + "produce a list of factors, one for each outcome and one representing the choice over outcomes; " + + "the factor for an outcome matches the outcome value to the dist value" in { + Universe.createNew() + val v1 = Flip(0.2) + val v2 = Constant(false) + val v3 = Dist(0.3 -> v1, 0.7 -> v2) + Values()(v3) + val v1Vals = Variable(v1).range + val v3Vals = Variable(v3).range + val v1TrueIndex = v1Vals.indexOf(Regular(true)) + val v1FalseIndex = v1Vals.indexOf(Regular(false)) + val v3TrueIndex = v3Vals.indexOf(Regular(true)) + val v3FalseIndex = v3Vals.indexOf(Regular(false)) + val v1Index = v3.outcomes.indexOf(v1) + val v2Index = v3.outcomes.indexOf(v2) + val selectFactor :: outcomeFactors = Factory.make(v3) + outcomeFactors.size should equal(2) + val v1Factor = outcomeFactors(v1Index) + val v2Factor = outcomeFactors(v2Index) + selectFactor.get(List(v1Index)) should equal(0.3) + selectFactor.get(List(v2Index)) should equal(0.7) + v1Factor.get(List(v1Index, v1TrueIndex, v3TrueIndex)) should equal(1.0) + v1Factor.get(List(v1Index, v1FalseIndex, v3TrueIndex)) should equal(0.0) + v1Factor.get(List(v1Index, v1TrueIndex, v3FalseIndex)) should equal(0.0) + v1Factor.get(List(v1Index, v1FalseIndex, v3FalseIndex)) should equal(1.0) + for { i <- 0 to 1; j <- 0 to 1 } v1Factor.get(List(v2Index, i, j)) should equal(1.0) + v2Factor.get(List(v2Index, 0, v3FalseIndex)) should equal(1.0) + v2Factor.get(List(v2Index, 0, v3TrueIndex)) should equal(0.0) + for { i <- 0 to 1} v2Factor.get(List(v1Index, 0, i)) should equal(1.0) + } + } + + "given a complex dist" should { + "produce a list of factors, one for each outcome and one representing the choice over outcomes; " + + "the factor for an outcome matches the outcome value to the dist value" in { + Universe.createNew() + val v1 = Select(0.2 -> 0.2, 0.8 -> 0.8) + val v2 = Select(0.4 -> 0.4, 0.6 -> 0.6) + val v3 = Flip(0.2) + val v4 = Constant(false) + val v5 = Dist(v1 -> v3, v2 -> v4) + Values()(v5) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v4Vals = Variable(v4).range + val v5Vals = Variable(v5).range + val v3Index = v5.outcomes.indexOf(v3) + val v4Index = v5.outcomes.indexOf(v4) + val v102 = v1Vals.indexOf(Regular(0.2)) + val v108 = v1Vals.indexOf(Regular(0.8)) + val v204 = v2Vals.indexOf(Regular(0.4)) + val v206 = v2Vals.indexOf(Regular(0.6)) + val v3f = v3Vals.indexOf(Regular(false)) + val v3t = v3Vals.indexOf(Regular(true)) + val v5f = v5Vals.indexOf(Regular(false)) + val v5t = v5Vals.indexOf(Regular(true)) + val selectFactor :: outcomeFactors = Factory.make(v5) + outcomeFactors.size should equal(2) + val v1Factor = outcomeFactors(v3Index) + val v2Factor = outcomeFactors(v4Index) + selectFactor.get(List(v102, v204, 0)) should be(0.2 / 0.6 +- 0.0001) + selectFactor.get(List(v102, v204, 1)) should be(0.4 / 0.6 +- 0.0001) + selectFactor.get(List(v102, v206, 0)) should be(0.2 / 0.8 +- 0.0001) + selectFactor.get(List(v102, v206, 1)) should be(0.6 / 0.8 +- 0.0001) + selectFactor.get(List(v108, v204, 0)) should be(0.8 / 1.2 +- 0.0001) + selectFactor.get(List(v108, v204, 1)) should be(0.4 / 1.2 +- 0.0001) + selectFactor.get(List(v108, v206, 0)) should be(0.8 / 1.4 +- 0.0001) + selectFactor.get(List(v108, v206, 1)) should be(0.6 / 1.4 +- 0.0001) + v1Factor.get(List(0, v3t, v5t)) should equal(1.0) + v1Factor.get(List(0, v3f, v5t)) should equal(0.0) + v1Factor.get(List(0, v3t, v5f)) should equal(0.0) + v1Factor.get(List(0, v3f, v5f)) should equal(1.0) + for { i <- 0 to 1; j <- 0 to 1 } v1Factor.get(List(1, i, j)) should equal(1.0) + v2Factor.get(List(1, 0, v5f)) should equal(1.0) + v2Factor.get(List(1, 0, v5t)) should equal(0.0) + for { i <- 0 to 0; j <- 0 to 1 } v2Factor.get(List(0, i, j)) should equal(1.0) + } + } + + "given an atomic not in the factor" should { + "automatically sample the element" in { + Universe.createNew() + val v1 = Normal(0.0, 1.0) + Values()(v1) + val factor = Factory.make(v1) + factor(0).size should equal(ParticleGenerator.defaultArgSamples) + factor(0).get(List(0)) should equal(1.0/ParticleGenerator.defaultArgSamples) + } + } + + "given a chain" should { + "produce a conditional selector for each parent value" in { + Universe.createNew() + val v1 = Flip(0.2) + val v2 = Select(0.1 -> 1, 0.9 -> 2) + val v3 = Constant(3) + val v4 = Chain(v1, (b: Boolean) => if (b) v2; else v3) + Values()(v4) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v4Vals = Variable(v4).range + val v1t = v1Vals indexOf Regular(true) + val v1f = v1Vals indexOf Regular(false) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v41 = v4Vals indexOf Regular(1) + val v42 = v4Vals indexOf Regular(2) + val v43 = v4Vals indexOf Regular(3) + + val factor = Factory.make(v4) + val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) + + v4Factor.get(List(v1t, v21, 0, v41)) should equal(1.0) + v4Factor.get(List(v1t, v22, 0, v41)) should equal(0.0) + v4Factor.get(List(v1t, v21, 0, v42)) should equal(0.0) + v4Factor.get(List(v1t, v22, 0, v42)) should equal(1.0) + v4Factor.get(List(v1t, v21, 0, v43)) should equal(0.0) + v4Factor.get(List(v1t, v22, 0, v43)) should equal(0.0) + v4Factor.get(List(v1f, v21, 0, v41)) should equal(0.0) + v4Factor.get(List(v1f, v22, 0, v41)) should equal(0.0) + v4Factor.get(List(v1f, v21, 0, v42)) should equal(0.0) + v4Factor.get(List(v1f, v22, 0, v42)) should equal(0.0) + v4Factor.get(List(v1f, v21, 0, v43)) should equal(1.0) + v4Factor.get(List(v1f, v22, 0, v43)) should equal(1.0) + + } + + "produce a conditional selector for each non-temporary parent value" in { + Universe.createNew() + val v1 = Flip(0.2) + val v4 = Chain(v1, (b: Boolean) => if (b) Select(0.1 -> 1, 0.9 -> 2); else Constant(3)) + Values()(v4) + val v1Vals = Variable(v1).range + val v4Vals = Variable(v4).range + + val v1t = v1Vals indexOf Regular(true) + val v1f = v1Vals indexOf Regular(false) + val v41 = v4Vals indexOf Regular(1) + val v42 = v4Vals indexOf Regular(2) + val v43 = v4Vals indexOf Regular(3) + + val factor = Factory.make(v4) + val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) + + v4Factor.get(List(v1t, v41)) should equal(0.1) + v4Factor.get(List(v1t, v42)) should equal(0.9) + v4Factor.get(List(v1t, v43)) should equal(0.0) + v4Factor.get(List(v1f, v41)) should equal(0.0) + v4Factor.get(List(v1f, v42)) should equal(0.0) + v4Factor.get(List(v1f, v43)) should equal(1.0) + } + } + + "given a CPD with one argument" should { + "produce a single factor with a case for each parent value" in { + Universe.createNew() + val v1 = Flip(0.2) + + val v2 = CPD(v1, false -> Flip(0.1), true -> Flip(0.7)) + Values()(v2) + + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + + val v1t = v1Vals indexOf Regular(true) + val v1f = v1Vals indexOf Regular(false) + val v2t = v2Vals indexOf Regular(true) + val v2f = v2Vals indexOf Regular(false) + val v3t = 0 + val v3f = 1 + val v4t = 0 + val v4f = 1 + + val factor = Factory.make(v2) + val List(v2Factor) = Factory.combineFactors(factor, SumProductSemiring, true) + + v2Factor.get(List(v1t, v3t, v4t, v2t)) should equal(1.0) + v2Factor.get(List(v1t, v3t, v4f, v2t)) should equal(1.0) + v2Factor.get(List(v1t, v3f, v4t, v2t)) should equal(0.0) + v2Factor.get(List(v1t, v3f, v4f, v2t)) should equal(0.0) + v2Factor.get(List(v1t, v3t, v4t, v2f)) should equal(0.0) + v2Factor.get(List(v1t, v3t, v4f, v2f)) should equal(0.0) + v2Factor.get(List(v1t, v3f, v4t, v2f)) should equal(1.0) + v2Factor.get(List(v1t, v3f, v4f, v2f)) should equal(1.0) + v2Factor.get(List(v1f, v3t, v4t, v2t)) should equal(1.0) + v2Factor.get(List(v1f, v3t, v4f, v2t)) should equal(0.0) + v2Factor.get(List(v1f, v3f, v4t, v2t)) should equal(1.0) + v2Factor.get(List(v1f, v3f, v4f, v2t)) should equal(0.0) + v2Factor.get(List(v1f, v3t, v4t, v2f)) should equal(0.0) + v2Factor.get(List(v1f, v3t, v4f, v2f)) should equal(1.0) + v2Factor.get(List(v1f, v3f, v4t, v2f)) should equal(0.0) + v2Factor.get(List(v1f, v3f, v4f, v2f)) should equal(1.0) + } + } + + "given an apply of one argument" should { + "produce a factor that matches the argument to the result via the function" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Apply(v1, (i: Int) => i % 2) + Values()(v2) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v20 = v2Vals indexOf Regular(0) + val v21 = v2Vals indexOf Regular(1) + val List(factor) = Factory.make(v2) + factor.get(List(v11, v20)) should equal(0.0) + factor.get(List(v11, v21)) should equal(1.0) + factor.get(List(v12, v20)) should equal(1.0) + factor.get(List(v12, v21)) should equal(0.0) + factor.get(List(v13, v20)) should equal(0.0) + factor.get(List(v13, v21)) should equal(1.0) + } + } + + "given an apply of two arguments" should { + "produce a factor that matches the arguments to the result via the function" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Select(0.5 -> 2, 0.5 -> 3) + val v3 = Apply(v1, v2, (i: Int, j: Int) => i % j) + Values()(v3) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v22 = v2Vals indexOf Regular(2) + val v23 = v2Vals indexOf Regular(3) + val v30 = v3Vals indexOf Regular(0) + val v31 = v3Vals indexOf Regular(1) + val v32 = v3Vals indexOf Regular(2) + val List(factor) = Factory.make(v3) + factor.get(List(v11, v22, v30)) should equal(0.0) + factor.get(List(v11, v22, v31)) should equal(1.0) + factor.get(List(v11, v22, v32)) should equal(0.0) + factor.get(List(v11, v23, v30)) should equal(0.0) + factor.get(List(v11, v23, v31)) should equal(1.0) + factor.get(List(v11, v23, v32)) should equal(0.0) + factor.get(List(v12, v22, v30)) should equal(1.0) + factor.get(List(v12, v22, v31)) should equal(0.0) + factor.get(List(v12, v22, v32)) should equal(0.0) + factor.get(List(v12, v23, v30)) should equal(0.0) + factor.get(List(v12, v23, v31)) should equal(0.0) + factor.get(List(v12, v23, v32)) should equal(1.0) + factor.get(List(v13, v22, v30)) should equal(0.0) + factor.get(List(v13, v22, v31)) should equal(1.0) + factor.get(List(v13, v22, v32)) should equal(0.0) + factor.get(List(v13, v23, v30)) should equal(1.0) + factor.get(List(v13, v23, v31)) should equal(0.0) + factor.get(List(v13, v23, v32)) should equal(0.0) + } + } + + "given an apply of three arguments" should { + "produce a factor that matches the arguments to the result via the function" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Select(0.5 -> 1, 0.5 -> 2) + val v3 = Constant(1) + val v4: Apply3[Int, Int, Int, Int] = Apply(v1, v2, v3, (i: Int, j: Int, k: Int) => i % (j + k)) + Values()(v4) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v4Vals = Variable(v4).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v31 = v3Vals indexOf Regular(1) + val v40 = v4Vals indexOf Regular(0) + val v41 = v4Vals indexOf Regular(1) + val v42 = v4Vals indexOf Regular(2) + val List(factor) = Factory.make(v4) + factor.get(List(v11, v21, v31, v40)) should equal(0.0) + factor.get(List(v11, v21, v31, v41)) should equal(1.0) + factor.get(List(v11, v21, v31, v42)) should equal(0.0) + factor.get(List(v11, v22, v31, v40)) should equal(0.0) + factor.get(List(v11, v22, v31, v41)) should equal(1.0) + factor.get(List(v11, v22, v31, v42)) should equal(0.0) + factor.get(List(v12, v21, v31, v40)) should equal(1.0) + factor.get(List(v12, v21, v31, v41)) should equal(0.0) + factor.get(List(v12, v21, v31, v42)) should equal(0.0) + factor.get(List(v12, v22, v31, v40)) should equal(0.0) + factor.get(List(v12, v22, v31, v41)) should equal(0.0) + factor.get(List(v12, v22, v31, v42)) should equal(1.0) + factor.get(List(v13, v21, v31, v40)) should equal(0.0) + factor.get(List(v13, v21, v31, v41)) should equal(1.0) + factor.get(List(v13, v21, v31, v42)) should equal(0.0) + factor.get(List(v13, v22, v31, v40)) should equal(1.0) + factor.get(List(v13, v22, v31, v41)) should equal(0.0) + factor.get(List(v13, v22, v31, v42)) should equal(0.0) + } + } + + "given an apply of four arguments" should { + "produce a factor that matches the arguments to the result via the function" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Select(0.5 -> 1, 0.5 -> 2) + val v3 = Constant(1) + val v4 = Flip(0.7) + val v5: Apply4[Int, Int, Int, Boolean, Int] = + Apply(v1, v2, v3, v4, (i: Int, j: Int, k: Int, b: Boolean) => if (b) 0; else i % (j + k)) + Values()(v5) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v4Vals = Variable(v4).range + val v5Vals = Variable(v5).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v31 = v3Vals indexOf Regular(1) + val v4true = v4Vals indexOf Regular(true) + val v4false = v4Vals indexOf Regular(false) + val v50 = v5Vals indexOf Regular(0) + val v51 = v5Vals indexOf Regular(1) + val v52 = v5Vals indexOf Regular(2) + val List(factor) = Factory.make(v5) + factor.get(List(v11, v21, v31, v4false, v50)) should equal(0.0) + factor.get(List(v11, v21, v31, v4false, v51)) should equal(1.0) + factor.get(List(v11, v21, v31, v4false, v52)) should equal(0.0) + factor.get(List(v11, v22, v31, v4false, v50)) should equal(0.0) + factor.get(List(v11, v22, v31, v4false, v51)) should equal(1.0) + factor.get(List(v11, v22, v31, v4false, v52)) should equal(0.0) + factor.get(List(v12, v21, v31, v4false, v50)) should equal(1.0) + factor.get(List(v12, v21, v31, v4false, v51)) should equal(0.0) + factor.get(List(v12, v21, v31, v4false, v52)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v50)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v51)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v52)) should equal(1.0) + factor.get(List(v13, v21, v31, v4false, v50)) should equal(0.0) + factor.get(List(v13, v21, v31, v4false, v51)) should equal(1.0) + factor.get(List(v13, v21, v31, v4false, v52)) should equal(0.0) + factor.get(List(v13, v22, v31, v4false, v50)) should equal(1.0) + factor.get(List(v13, v22, v31, v4false, v51)) should equal(0.0) + factor.get(List(v13, v22, v31, v4false, v52)) should equal(0.0) + + factor.get(List(v11, v21, v31, v4true, v50)) should equal(1.0) + factor.get(List(v11, v21, v31, v4true, v51)) should equal(0.0) + factor.get(List(v11, v21, v31, v4true, v52)) should equal(0.0) + factor.get(List(v11, v22, v31, v4true, v50)) should equal(1.0) + factor.get(List(v11, v22, v31, v4true, v51)) should equal(0.0) + factor.get(List(v11, v22, v31, v4true, v52)) should equal(0.0) + factor.get(List(v12, v21, v31, v4true, v50)) should equal(1.0) + factor.get(List(v12, v21, v31, v4true, v51)) should equal(0.0) + factor.get(List(v12, v21, v31, v4true, v52)) should equal(0.0) + factor.get(List(v12, v22, v31, v4true, v50)) should equal(1.0) + factor.get(List(v12, v22, v31, v4true, v51)) should equal(0.0) + factor.get(List(v12, v22, v31, v4true, v52)) should equal(0.0) + factor.get(List(v13, v21, v31, v4true, v50)) should equal(1.0) + factor.get(List(v13, v21, v31, v4true, v51)) should equal(0.0) + factor.get(List(v13, v21, v31, v4true, v52)) should equal(0.0) + factor.get(List(v13, v22, v31, v4true, v50)) should equal(1.0) + factor.get(List(v13, v22, v31, v4true, v51)) should equal(0.0) + factor.get(List(v13, v22, v31, v4true, v52)) should equal(0.0) + } + } + + "given an apply of five arguments" should { + "produce a factor that matches the arguments to the result via the function" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Select(0.5 -> 1, 0.5 -> 2) + val v3 = Constant(1) + val v4 = Flip(0.7) + val v5 = Constant(false) + val v6: Apply5[Int, Int, Int, Boolean, Boolean, Int] = + Apply(v1, v2, v3, v4, v5, + (i: Int, j: Int, k: Int, b: Boolean, c: Boolean) => if (b || c) 0; else i % (j + k)) + Values()(v6) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v4Vals = Variable(v4).range + val v5Vals = Variable(v5).range + val v6Vals = Variable(v6).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v31 = v3Vals indexOf Regular(1) + val v4true = v4Vals indexOf Regular(true) + val v4false = v4Vals indexOf Regular(false) + val v5false = v5Vals indexOf Regular(false) + val v60 = v6Vals indexOf Regular(0) + val v61 = v6Vals indexOf Regular(1) + val v62 = v6Vals indexOf Regular(2) + val List(factor) = Factory.make(v6) + + factor.get(List(v11, v21, v31, v4false, v5false, v60)) should equal(0.0) + factor.get(List(v11, v21, v31, v4false, v5false, v61)) should equal(1.0) + factor.get(List(v11, v21, v31, v4false, v5false, v62)) should equal(0.0) + factor.get(List(v11, v22, v31, v4false, v5false, v60)) should equal(0.0) + factor.get(List(v11, v22, v31, v4false, v5false, v61)) should equal(1.0) + factor.get(List(v11, v22, v31, v4false, v5false, v62)) should equal(0.0) + factor.get(List(v12, v21, v31, v4false, v5false, v60)) should equal(1.0) + factor.get(List(v12, v21, v31, v4false, v5false, v61)) should equal(0.0) + factor.get(List(v12, v21, v31, v4false, v5false, v62)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v5false, v60)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v5false, v61)) should equal(0.0) + factor.get(List(v12, v22, v31, v4false, v5false, v62)) should equal(1.0) + factor.get(List(v13, v21, v31, v4false, v5false, v60)) should equal(0.0) + factor.get(List(v13, v21, v31, v4false, v5false, v61)) should equal(1.0) + factor.get(List(v13, v21, v31, v4false, v5false, v62)) should equal(0.0) + factor.get(List(v13, v22, v31, v4false, v5false, v60)) should equal(1.0) + factor.get(List(v13, v22, v31, v4false, v5false, v61)) should equal(0.0) + factor.get(List(v13, v22, v31, v4false, v5false, v62)) should equal(0.0) + + factor.get(List(v11, v21, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v11, v21, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v11, v21, v31, v4true, v5false, v62)) should equal(0.0) + factor.get(List(v11, v22, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v11, v22, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v11, v22, v31, v4true, v5false, v62)) should equal(0.0) + factor.get(List(v12, v21, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v12, v21, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v12, v21, v31, v4true, v5false, v62)) should equal(0.0) + factor.get(List(v12, v22, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v12, v22, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v12, v22, v31, v4true, v5false, v62)) should equal(0.0) + factor.get(List(v13, v21, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v13, v21, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v13, v21, v31, v4true, v5false, v62)) should equal(0.0) + factor.get(List(v13, v22, v31, v4true, v5false, v60)) should equal(1.0) + factor.get(List(v13, v22, v31, v4true, v5false, v61)) should equal(0.0) + factor.get(List(v13, v22, v31, v4true, v5false, v62)) should equal(0.0) + } + } + + "given an Inject" should { + "produces a factor that matches its inputs to the correct sequence" in { + Universe.createNew() + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Select(0.5 -> 4, 0.5 -> 5) + val v3 = Inject(v1, v2) + Values()(v3) + val List(factor) = Factory.make(v3) + + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v3Vals = Variable(v3).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v24 = v2Vals indexOf Regular(4) + val v25 = v2Vals indexOf Regular(5) + val v314 = v3Vals indexOf Regular(List(1, 4)) + val v315 = v3Vals indexOf Regular(List(1, 5)) + val v324 = v3Vals indexOf Regular(List(2, 4)) + val v325 = v3Vals indexOf Regular(List(2, 5)) + val v334 = v3Vals indexOf Regular(List(3, 4)) + val v335 = v3Vals indexOf Regular(List(3, 5)) + + factor.get(List(v11, v24, v314)) should equal(1.0) + factor.get(List(v11, v25, v315)) should equal(1.0) + factor.get(List(v12, v24, v324)) should equal(1.0) + factor.get(List(v12, v25, v325)) should equal(1.0) + factor.get(List(v13, v24, v334)) should equal(1.0) + factor.get(List(v13, v25, v335)) should equal(1.0) + + factor.get(List(v11, v25, v314)) should equal(0.0) + factor.get(List(v11, v24, v315)) should equal(0.0) + factor.get(List(v12, v25, v324)) should equal(0.0) + factor.get(List(v12, v24, v325)) should equal(0.0) + factor.get(List(v13, v25, v334)) should equal(0.0) + factor.get(List(v13, v24, v335)) should equal(0.0) + + factor.get(List(v12, v24, v314)) should equal(0.0) + factor.get(List(v12, v25, v315)) should equal(0.0) + factor.get(List(v13, v24, v324)) should equal(0.0) + factor.get(List(v13, v25, v325)) should equal(0.0) + factor.get(List(v11, v24, v334)) should equal(0.0) + factor.get(List(v11, v25, v335)) should equal(0.0) + + factor.get(List(v12, v25, v314)) should equal(0.0) + factor.get(List(v12, v24, v315)) should equal(0.0) + factor.get(List(v13, v25, v324)) should equal(0.0) + factor.get(List(v13, v24, v325)) should equal(0.0) + factor.get(List(v11, v25, v334)) should equal(0.0) + factor.get(List(v11, v24, v335)) should equal(0.0) + + factor.get(List(v13, v24, v314)) should equal(0.0) + factor.get(List(v13, v25, v315)) should equal(0.0) + factor.get(List(v11, v24, v324)) should equal(0.0) + factor.get(List(v11, v25, v325)) should equal(0.0) + factor.get(List(v12, v24, v334)) should equal(0.0) + factor.get(List(v12, v25, v335)) should equal(0.0) + + factor.get(List(v13, v25, v314)) should equal(0.0) + factor.get(List(v13, v24, v315)) should equal(0.0) + factor.get(List(v11, v25, v324)) should equal(0.0) + factor.get(List(v11, v24, v324)) should equal(0.0) + factor.get(List(v12, v25, v334)) should equal(0.0) + factor.get(List(v12, v24, v335)) should equal(0.0) + } + } + + "given a non-trivial condition and constraint" should { + "produce the correct constraint factors" in { + Universe.createNew() + val v1 = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) + v1.setCondition((i: Int) => i != 2) + v1.setConstraint(((i: Int) => i.toDouble)) + Values()(v1) + val List(condFactor, constrFactor, _) = Factory.make(v1) + val v1Vals = Variable(v1).range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + condFactor.get(List(v11)) should be(1.0 +- 0.000000001) + condFactor.get(List(v12)) should be(0.0 +- 0.000000001) + condFactor.get(List(v13)) should be(1.0 +- 0.000000001) + constrFactor.get(List(v11)) should be(1.0 +- 0.000000001) + constrFactor.get(List(v12)) should be(2.0 +- 0.000000001) + constrFactor.get(List(v13)) should be(3.0 +- 0.000000001) + } + } + + "given an element whose expanded values are only *" should { + "produce no factors" in { + Universe.createNew() + val f = Flip(0.5) + val lv = LazyValues() + lv.expandAll(Set((f, -1))) + val factors = Factory.make(f) + factors should be(empty) + } + } + } + + "Making a factor for a dependent universe" should { + "produce a correct dependent factor" in { + Universe.createNew() + val x = Flip(0.1) + val y = Select(0.2 -> 1, 0.3 -> 2, 0.5 -> 3) + Values()(x) + Values()(y) + val dependentUniverse = new Universe(List(x, y)) + val u1 = Uniform(0.0, 1.0)("", dependentUniverse) + val u2 = Uniform(0.0, 2.0)("", dependentUniverse) + val a = CachingChain(x, y, (x: Boolean, y: Int) => if (x || y < 2) u1; else u2)("a", dependentUniverse) + Values(dependentUniverse)(a) + val evidence = List(NamedEvidence("a", Condition((d: Double) => d < 0.5))) + val factor = + Factory.makeDependentFactor(Universe.universe, dependentUniverse, () => ProbEvidenceSampler.computeProbEvidence(20000, evidence)(dependentUniverse)) + val xVar = Variable(x) + val yVar = Variable(y) + val variables = factor.variables + variables.toSet should equal(Set(xVar, yVar)) + val xIndex = variables indexOf xVar + val yIndex = variables indexOf yVar + val xFalse = xVar.range indexOf Regular(false) + val xTrue = xVar.range indexOf Regular(true) + val y1 = yVar.range indexOf Regular(1) + val y2 = yVar.range indexOf Regular(2) + val y3 = yVar.range indexOf Regular(3) + // If x is true or y is 1, pe is 0.5; if both false, 0.25. + if (xIndex == 0) { + factor.get(List(xFalse, y2)) should be(0.25 +- 0.01) + factor.get(List(xFalse, y3)) should be(0.25 +- 0.01) + factor.get(List(xFalse, y1)) should be(0.5 +- 0.01) + factor.get(List(xTrue, y1)) should be(0.5 +- 0.01) + factor.get(List(xTrue, y2)) should be(0.5 +- 0.01) + factor.get(List(xTrue, y3)) should be(0.5 +- 0.01) + } else { + factor.get(List(y2, xFalse)) should be(0.25 +- 0.01) + factor.get(List(y3, xFalse)) should be(0.25 +- 0.01) + factor.get(List(y1, xTrue)) should be(0.5 +- 0.01) + factor.get(List(y1, xFalse)) should be(0.5 +- 0.01) + factor.get(List(y2, xFalse)) should be(0.5 +- 0.01) + factor.get(List(y3, xFalse)) should be(0.5 +- 0.01) + } + } + } + + "Making factors for multiple universes" should { + "produce the same range of values as if it were in a single universe" when { + "given a simple model with two universes" in { + Universe.createNew() + val v1u1 = Select(0.3 -> 0, 0.5 -> 1, 0.2 -> 3) + val v2u1 = Apply(v1u1, (i: Int) => i % 3) + + Universe.createNew() + val v1u2 = Select(0.3 -> 0, 0.5 -> 1, 0.2 -> 3) + Universe.createNew + val v2u3 = Apply(v1u1, (i: Int) => i % 3) + + (Variable(v1u1).range) should equal(Variable(v1u2).range) + (Variable(v2u1).range) should equal(Variable(v2u3).range) + } + + "given a model with multiple universes" in { + Universe.createNew() + val func = (i: Int, b: Boolean) => if (b) i else i + 1 + val v1u1 = Select(0.1 -> 0, 0.2 -> 2, 0.7 -> 5) + val v2u1 = Flip(0.3) + val v3u1 = Apply(v1u1, v2u1, func) + val v4u1 = Flip(0.5) + val v5u1 = Apply(v3u1, v4u1, func) + + Universe.createNew() + val v1u2 = Select(0.1 -> 0, 0.2 -> 2, 0.7 -> 5) + Universe.createNew() + val v2u3 = Flip(0.3) + Universe.createNew() + val v3u4 = Apply(v1u1, v2u1, func) + Universe.createNew() + val v4u5 = Flip(0.5) + Universe.createNew() + val v5u6 = Apply(v3u1, v4u1, func) + + (Variable(v5u1).range) should equal(Variable(v5u6).range) + } + + "given a multi-universe model with Chains" in { + Universe.createNew() + val func1 = (i: Int) => if (i % 2 == 0) Constant(i) else Select(0.4 -> (i - 1), 0.6 -> (i + 1)) + val func2 = (i: Int) => if (i % 4 == 0) Select(0.2 -> (i - 1), 0.8 -> (i + 1)) else Constant(i) + val v1u1 = Select(0.2 -> 0, 0.5 -> 3, 0.3 -> 6) + val v2u1 = Chain(v1u1, func1) + val v3u1 = Chain(v2u1, func2) + + Universe.createNew() + val v1u2 = Select(0.2 -> 0, 0.5 -> 3, 0.3 -> 6) + Universe.createNew() + val v2u3 = Chain(v1u1, func1) + Universe.createNew() + val v3u4 = Chain(v2u1, func2) + + (Variable(v3u1).range) should equal(Variable(v3u4).range) + } + } + + "correctly produce a factor between elements in multiple universes" when { + "given a simple model in two universes" in { + Universe.createNew() + val v1 = Select(0.3 -> 0, 0.2 -> 1, 0.4 -> 2, 0.1 -> 3) + Universe.createNew() + val v2 = Apply(v1, (i: Int) => i / 2) + Values()(v2) + val v1Vals = Variable(v1).range + val v2Vals = Variable(v2).range + val v10 = v1Vals indexOf Regular(0) + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v20 = v2Vals indexOf Regular(0) + val v21 = v2Vals indexOf Regular(1) + val List(factor) = Factory.make(v2) + factor.get(List(v10, v20)) should equal(1.0) + factor.get(List(v10, v21)) should equal(0.0) + factor.get(List(v11, v20)) should equal(1.0) + factor.get(List(v11, v21)) should equal(0.0) + factor.get(List(v12, v20)) should equal(0.0) + factor.get(List(v12, v21)) should equal(1.0) + factor.get(List(v13, v20)) should equal(0.0) + factor.get(List(v13, v21)) should equal(1.0) + } + + "given a multi-universe model with Chains" in { + Universe.createNew() + val v1 = Select(0.2 -> 0, 0.7 -> 1, 0.1 -> 2) + Universe.createNew() + val v2 = Constant(2) + Universe.createNew() + val v3 = Select(0.4 -> 0, 0.6 -> 1) + Universe.createNew() + val v4 = Chain(v1, (i: Int) => if (i % 2 == 0) v2 else v3) + Values()(v4) + val v1Vals = Variable(v1).range + val v3Vals = Variable(v3).range + val v4Vals = Variable(v4).range + val v10 = v1Vals indexOf Regular(0) + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v30 = v3Vals indexOf Regular(0) + val v31 = v3Vals indexOf Regular(1) + val v40 = v4Vals indexOf Regular(0) + val v41 = v4Vals indexOf Regular(1) + val v42 = v4Vals indexOf Regular(2) + + val factor = Factory.make(v4) + val List(v4Factor) = Factory.combineFactors(factor, SumProductSemiring, true) + + v4Factor.get(List(v10, 0, v30, v40)) should equal(0.0) + v4Factor.get(List(v10, 0, v31, v40)) should equal(0.0) + v4Factor.get(List(v10, 0, v30, v41)) should equal(0.0) + v4Factor.get(List(v10, 0, v31, v41)) should equal(0.0) + v4Factor.get(List(v10, 0, v30, v42)) should equal(1.0) + v4Factor.get(List(v10, 0, v31, v42)) should equal(1.0) + v4Factor.get(List(v11, 0, v30, v40)) should equal(1.0) + v4Factor.get(List(v11, 0, v31, v40)) should equal(0.0) + v4Factor.get(List(v11, 0, v30, v41)) should equal(0.0) + v4Factor.get(List(v11, 0, v31, v41)) should equal(1.0) + v4Factor.get(List(v11, 0, v30, v42)) should equal(0.0) + v4Factor.get(List(v11, 0, v31, v42)) should equal(0.0) + v4Factor.get(List(v12, 0, v30, v40)) should equal(0.0) + v4Factor.get(List(v12, 0, v31, v40)) should equal(0.0) + v4Factor.get(List(v12, 0, v30, v41)) should equal(0.0) + v4Factor.get(List(v12, 0, v31, v41)) should equal(0.0) + v4Factor.get(List(v12, 0, v30, v42)) should equal(1.0) + v4Factor.get(List(v12, 0, v31, v42)) should equal(1.0) + } + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/SemiringTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/SemiringTest.scala index 9a5834e9..d8421476 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/SemiringTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/SemiringTest.scala @@ -1,5 +1,5 @@ - -/* * SemiringTest.scala +/* + * SemiringTest.scala * Semiring tests. * * Created By: Avi Pfeffer (apfeffer@cra.com) @@ -21,11 +21,7 @@ import org.scalatest.Matchers import org.scalatest.PrivateMethodTester import org.scalatest.WordSpec -import com.cra.figaro.algorithm.factored.MaxProductSemiring -import com.cra.figaro.algorithm.factored.Semiring -import com.cra.figaro.algorithm.factored.SufficientStatisticsSemiring -import com.cra.figaro.algorithm.factored.SumProductSemiring -import com.cra.figaro.algorithm.factored.SumProductUtilitySemiring +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.language.Parameter import com.cra.figaro.library.atomic.continuous.BetaParameter import com.cra.figaro.library.atomic.continuous.DirichletParameter @@ -301,7 +297,7 @@ class SemiringTest extends WordSpec with Matchers with PrivateMethodTester { } val semiring = new SufficientStatisticsSemiring(zeroSufficientStatisticsMap.toMap) - semiringProperties[(Double, Map[Parameter[_], Seq[Double]])](semiring.asInstanceOf[com.cra.figaro.algorithm.factored.Semiring[(Double, Map[Parameter[_], Seq[Double]])]], a, b, c, probPlusOrMinus, (0.001, zeroSufficientStatisticsMap.toMap)) + semiringProperties[(Double, Map[Parameter[_], Seq[Double]])](semiring.asInstanceOf[Semiring[(Double, Map[Parameter[_], Seq[Double]])]], a, b, c, probPlusOrMinus, (0.001, zeroSufficientStatisticsMap.toMap)) j += 1 } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/VETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/VETest.scala index ab6d0fbb..9a6663c1 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/VETest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/VETest.scala @@ -18,6 +18,7 @@ import org.scalatest.{ WordSpec, PrivateMethodTester } import math.log import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.language._ import com.cra.figaro.library.compound._ @@ -45,8 +46,8 @@ class VETest extends WordSpec with Matchers { val v4 = Variable(e4) val v5 = Variable(e5) val v6 = Variable(e6) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) val af = AbstractFactor(f.variables) val ag = AbstractFactor(g.variables) val graph = new VEGraph(List(f, g)) @@ -81,8 +82,8 @@ class VETest extends WordSpec with Matchers { val v4 = Variable(e4) val v5 = Variable(e5) val v6 = Variable(e6) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) val af = AbstractFactor(f.variables) val ag = AbstractFactor(g.variables) VEGraph.cost(List(af, ag)) should equal(18) // 2*1*3*2 + 1*3*1*2 @@ -113,9 +114,9 @@ class VETest extends WordSpec with Matchers { val v5 = Variable(e5) val v6 = Variable(e6) val v7 = Variable(e7) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) - val h = Factory.make[Double](List(v1, v7)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) + val h = Factory.simpleMake[Double](List(v1, v7)) val graph1 = new VEGraph(List(f, g, h)) val score = graph1.score(v3) score should equal(-10) // 2*1*2*1*2 - (2*1*3*2 + 1*3*1*2) @@ -140,9 +141,9 @@ class VETest extends WordSpec with Matchers { val v5 = Variable(e5) val v6 = Variable(e6) val v7 = Variable(e7) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) - val h = Factory.make[Double](List(v1, v7)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) + val h = Factory.simpleMake[Double](List(v1, v7)) val graph1 = new VEGraph(List(f, g, h)) val graph2 = graph1.eliminate(v3) val VariableInfo(v1Factors, v1Neighbors) = graph2.info(v1) @@ -166,9 +167,9 @@ class VETest extends WordSpec with Matchers { val v5 = Variable(e5) val v6 = Variable(e6) val v7 = Variable(e7) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) - val h = Factory.make[Double](List(v1, v7)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) + val h = Factory.simpleMake[Double](List(v1, v7)) val graph1 = new VEGraph(List(f, g, h)) val graph2 = graph1.eliminate(v3) val VariableInfo(v1Factors, v1Neighbors) = graph2.info(v1) @@ -205,10 +206,10 @@ class VETest extends WordSpec with Matchers { val v6 = Variable(e6) val v7 = Variable(e7) val v8 = Variable(e8) - val f = Factory.make[Double](List(v1, v2, v3, v4)) - val g = Factory.make[Double](List(v5, v3, v2, v6)) - val h = Factory.make[Double](List(v1, v7)) - val i = Factory.make[Double](List(v8, v1, v3)) + val f = Factory.simpleMake[Double](List(v1, v2, v3, v4)) + val g = Factory.simpleMake[Double](List(v5, v3, v2, v6)) + val h = Factory.simpleMake[Double](List(v1, v7)) + val i = Factory.simpleMake[Double](List(v8, v1, v3)) /* Old method that considered old cost of factor containing variable */ // Initially: // Assume we will not eliminate v5 or v8 @@ -293,7 +294,7 @@ class VETest extends WordSpec with Matchers { def make(numVars: Int): Traversable[Factor[Double]] = { val universe = new Universe val a: List[Variable[_]] = List.tabulate(numVars)(i => Variable(Flip(0.3)("", universe))) - for { i <- 0 to numVars - 2 } yield Factory.make[Double](List(a(i), a(i + 1))) + for { i <- 0 to numVars - 2 } yield Factory.simpleMake[Double](List(a(i), a(i + 1))) } val factors1 = make(small) val factors2 = make(large) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/filtering/FactoredFrontierTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/filtering/FactoredFrontierTest.scala index efeaacb3..300d7247 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/filtering/FactoredFrontierTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/filtering/FactoredFrontierTest.scala @@ -49,10 +49,10 @@ class FactoredFrontierTest extends WordSpec with Matchers { val f3 = Flip(f2)("f3", universe1) val f4 = If(f1 === f3, 0, 1)("f4", universe1) def trans(u: Universe): Universe = { - u.getElementByReference[Boolean]("f1") shouldBe an [AtomicSelect[Boolean]] - u.getElementByReference[Double]("f2") shouldBe an [AtomicSelect[Double]] - u.getElementByReference[Boolean]("f3") shouldBe an [AtomicSelect[Boolean]] - u.getElementByReference[Int]("f4") shouldBe an [AtomicSelect[Int]] + u.getElementByReference[Boolean]("f1") shouldBe an [AtomicSelect[ _ ]] + u.getElementByReference[Double]("f2") shouldBe an [AtomicSelect[ _ ]] + u.getElementByReference[Boolean]("f3") shouldBe an [AtomicSelect[ _ ]] + u.getElementByReference[Int]("f4") shouldBe an [AtomicSelect[ _ ]] createNew() } val ff = FactoredFrontier(universe1, trans(_), 3) @@ -69,10 +69,10 @@ class FactoredFrontierTest extends WordSpec with Matchers { val f3 = Flip(f2)("f3", universe1) val f4 = If(f1 === f3, 0, 1)("f4", universe1) def trans(static: Universe, previous: Universe): Universe = { - static.getElementByReference[Boolean]("f1") shouldBe an [AtomicSelect[Boolean]] - static.getElementByReference[Double]("f2") shouldBe an [AtomicSelect[Double]] - previous.getElementByReference[Boolean]("f3") shouldBe an [AtomicSelect[Boolean]] - previous.getElementByReference[Int]("f4") shouldBe an [AtomicSelect[Int]] + static.getElementByReference[Boolean]("f1") shouldBe an [AtomicSelect[ _ ]] + static.getElementByReference[Double]("f2") shouldBe an [AtomicSelect[ _ ]] + previous.getElementByReference[Boolean]("f3") shouldBe an [AtomicSelect[ _ ]] + previous.getElementByReference[Int]("f4") shouldBe an [AtomicSelect[ _ ]] previous.hasRef[Boolean]("f1") should equal(false) previous.hasRef[Double]("f2") should equal(false) static.hasRef[Boolean]("f3") should equal(false) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/lazyfactored/LazyVETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/lazyfactored/LazyVETest.scala index bbde45de..532caf61 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/lazyfactored/LazyVETest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/lazyfactored/LazyVETest.scala @@ -1,3 +1,16 @@ +/* + * LazyVETest.scala + * Lazy variable elimination tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Feb 21, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.test.algorithm.lazyfactored import org.scalatest.Matchers diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala index b4092ff8..36cc29c0 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala @@ -32,9 +32,36 @@ import com.cra.figaro.test.tags.NonDeterministic class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { - "Expectation Maximization with belief propagation" when + "Expectation Maximization with belief propagation" when { + "when provided a termination criteria based on sufficient statistics magnitudes" should { + "exit before reaching the maximum iterations" in { + val universe = Universe.createNew + val b = Beta(2, 2) + val terminationCriteria = EMTerminationCriteria.sufficientStatisticsMagnitude(0.05) + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithBP(terminationCriteria, 10, b)(universe) + algorithm.start + + val result = b.MAPValue + algorithm.kill + result should be(0.6666 +- 0.01) + + } + } + "used to estimate a Beta parameter" should { @@ -89,43 +116,42 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { } "learn the bias from observations of binomial elements" in { - val universe = Universe.createNew - val b = Beta(2, 2) + val universe = Universe.createNew + val b = Beta(2, 2) - val b1 = Binomial(7, b) - b1.observe(6) - val b2 = Binomial(3, b) - b2.observe(1) + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) - val algorithm = EMWithBP(15, 10, b)(universe) - algorithm.start + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start - val result = b.MAPValue - algorithm.kill - result should be(0.6666 +- 0.01) + val result = b.MAPValue + algorithm.kill + result should be(0.6666 +- 0.01) } } - "correctly use a uniform prior" in { - val universe = Universe.createNew - val b = Beta(1, 1) + "correctly use a uniform prior" in { + val universe = Universe.createNew + val b = Beta(1, 1) - val b1 = Binomial(7, b) - b1.observe(6) - val b2 = Binomial(3, b) - b2.observe(1) + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) - val algorithm = EMWithBP(15, 10, b)(universe) - algorithm.start + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start - val result = b.MAPValue - algorithm.kill - result should be(0.7 +- 0.01) + val result = b.MAPValue + algorithm.kill + result should be(0.7 +- 0.01) - - } + } - "used to estimate a Dirichlet parameter with two concentration parameters" should + "used to estimate a Dirichlet parameter with two concentration parameters" should { "detect bias after a large enough number of trials" in @@ -170,7 +196,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { f.observe(false) } - val algorithm = EMWithBP(15, 10, b)(universe) + val algorithm = EMWithBP(15, 10, b)(universe) algorithm.start val result = b.MAPValue @@ -179,8 +205,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { } - - } + } "used to estimate a Dirichlet parameter with three concentration parameters" should { @@ -234,10 +259,10 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { outcome.addCondition(x => x >= 3 && x <= 6) } - val algorithm = EMWithBP(2, 10, d) - algorithm.start + val algorithm = EMWithBP(2, 10, d) + algorithm.start - val result = d.MAPValue + val result = d.MAPValue algorithm.kill result(0) should be(0.0 +- 0.01) result(1) should be(0.25 +- 0.01) @@ -289,10 +314,10 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { for (i <- 1 to 3) { val f2 = Select(b, outcomes: _*) - f2.observe(1) + f2.observe(1) } - for (i <- 1 to 2) { + for (i <- 1 to 2) { val f3 = Select(b, outcomes: _*) f3.observe(2) } @@ -314,7 +339,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { } - "correctly use a uniform prior" in + "correctly use a uniform prior" in { val universe = Universe.createNew val b = Dirichlet(1.0, 1.0, 1.0) @@ -336,7 +361,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { val f1 = Select(b, outcomes: _*) f1.observe(3) } - + val algorithm = EMWithBP(3, 10, b)(universe) algorithm.start @@ -349,98 +374,98 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { } - "used to estimate multiple parameters" should - { + "used to estimate multiple parameters" should + { - "leave parameters having no observations unchanged" in - { - val universe = Universe.createNew - val d = Dirichlet(2.0, 4.0, 2.0) - val b = Beta(2.0, 2.0) - val outcomes = List(1, 2, 3) + "leave parameters having no observations unchanged" in + { + val universe = Universe.createNew + val d = Dirichlet(2.0, 4.0, 2.0) + val b = Beta(2.0, 2.0) + val outcomes = List(1, 2, 3) - for (i <- 1 to 4) { + for (i <- 1 to 4) { - val f2 = Select(d, outcomes: _*) - f2.observe(1) - } + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } - for (i <- 1 to 2) { + for (i <- 1 to 2) { - val f3 = Select(d, outcomes: _*) - f3.observe(2) - } + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } - for (i <- 1 to 4) { + for (i <- 1 to 4) { - val f1 = Select(d, outcomes: _*) - f1.observe(3) - } + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } - val algorithm = EMWithBP(100, 10, d, b)(universe) - algorithm.start + val algorithm = EMWithBP(100, 10, d, b)(universe) + algorithm.start - val result = d.MAPValue - algorithm.kill - result(0) should be(0.33 +- 0.01) - result(1) should be(0.33 +- 0.01) - result(2) should be(0.33 +- 0.01) + val result = d.MAPValue + algorithm.kill + result(0) should be(0.33 +- 0.01) + result(1) should be(0.33 +- 0.01) + result(2) should be(0.33 +- 0.01) - val betaResult = b.MAPValue - betaResult should be(0.5) + val betaResult = b.MAPValue + betaResult should be(0.5) - } + } - "correctly estimate all parameters with observations" in - { - val universe = Universe.createNew - val d = Dirichlet(2.0, 3.0, 2.0) - val b = Beta(3.0, 7.0) - val outcomes = List(1, 2, 3) + "correctly estimate all parameters with observations" in + { + val universe = Universe.createNew + val d = Dirichlet(2.0, 3.0, 2.0) + val b = Beta(3.0, 7.0) + val outcomes = List(1, 2, 3) - for (i <- 1 to 3) { + for (i <- 1 to 3) { - val f2 = Select(d, outcomes: _*) - f2.observe(1) - } + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } - for (i <- 1 to 2) { - val f3 = Select(d, outcomes: _*) - f3.observe(2) - } + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } - for (i <- 1 to 3) { + for (i <- 1 to 3) { - val f1 = Select(d, outcomes: _*) - f1.observe(3) - } + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } - for (i <- 1 to 7) { + for (i <- 1 to 7) { - val f = Flip(b) - f.observe(true) - } + val f = Flip(b) + f.observe(true) + } - for (i <- 1 to 3) { + for (i <- 1 to 3) { - val f = Flip(b) - f.observe(false) - } + val f = Flip(b) + f.observe(false) + } - val algorithm = EMWithBP(5, 10, b,d)(universe) - algorithm.start + val algorithm = EMWithBP(5, 10, b, d)(universe) + algorithm.start - val result = d.MAPValue + val result = d.MAPValue - result(0) should be(0.33 +- 0.01) - result(1) should be(0.33 +- 0.01) - result(2) should be(0.33 +- 0.01) + result(0) should be(0.33 +- 0.01) + result(1) should be(0.33 +- 0.01) + result(2) should be(0.33 +- 0.01) - val betaResult = b.MAPValue - betaResult should be(0.5 +- 0.01) + val betaResult = b.MAPValue + betaResult should be(0.5 +- 0.01) - } - } + } + } val observationProbability = 0.7 val trainingSetSize = 100 @@ -542,7 +567,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { if (random.nextDouble() < observationProbability) model.y.observe(datum.y) if (random.nextDouble() < observationProbability) model.z.observe(datum.z) if (random.nextDouble() < observationProbability) model.w.observe(datum.w) - } + } var nextSkip = 0 @@ -610,18 +635,17 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { algorithm.start() val resultUniverse = new Universe - def extractParameter(parameter: Element[Double], name: String) = + def extractParameter(parameter: Element[Double], name: String) = { - parameter match - { - case b: AtomicBeta => - { - - Constant(valueGetter(algorithm, parameter))(name, resultUniverse) - } - case _ => Constant(valueGetter(algorithm, parameter))(name, resultUniverse) - } - + parameter match { + case b: AtomicBeta => + { + + Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + case _ => Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + } val learnedParameters = new Parameters(resultUniverse) { val b1 = extractParameter(parameters.b1, "b1"); b1.generate() @@ -642,7 +666,7 @@ class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { (new Model(learnedParameters, normalFlipConstructor), totalTime) } - "derive parameters within a reasonable accuracy for random data" taggedAs(NonDeterministic) in + "derive parameters within a reasonable accuracy for random data" taggedAs (NonDeterministic) in { val numEMIterations = 5 diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala index 682ffb7c..fb5c78dc 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala @@ -33,6 +33,33 @@ import com.cra.figaro.test.tags.NonDeterministic class EMWithImportanceTest extends WordSpec with PrivateMethodTester with Matchers { "Expectation Maximization with importance sampling" when { + "when provided a termination criteria based on sufficient statistics magnitudes" should { + "exit before reaching the maximum iterations" in { + val universe = Universe.createNew + val b = Beta(2, 2) + val terminationCriteria = EMTerminationCriteria.sufficientStatisticsMagnitude(0.05) + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithImportance(terminationCriteria, 10, b)(universe) + algorithm.start + + val result = b.MAPValue + algorithm.kill + result should be(0.6666 +- 0.01) + + } + } + "used to estimate a Beta parameter" should { "detect bias after a large enough number of trials" in diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala index 4f57232c..1078d1ed 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala @@ -34,6 +34,33 @@ class EMWithMHTest extends WordSpec with PrivateMethodTester with Matchers { "Expectation Maximization with MetropolisHastings" when { + "when provided a termination criteria based on sufficient statistics magnitudes" should { + "exit before reaching the maximum iterations" in { + val universe = Universe.createNew + val b = Beta(2, 2) + val terminationCriteria = EMTerminationCriteria.sufficientStatisticsMagnitude(0.05) + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithBP(terminationCriteria, 10, b)(universe) + algorithm.start + + val result = b.MAPValue + algorithm.kill + result should be(0.6666 +- 0.01) + + } + } + "used to estimate a Beta parameter" should { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala index 9e6e25c0..1cd1bf4c 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala @@ -54,7 +54,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement @@ -80,7 +80,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement @@ -98,7 +98,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with val b2 = Binomial(3, b) b2.observe(1) - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement @@ -117,7 +117,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with val b2 = Binomial(3, b) b2.observe(1) - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement @@ -149,7 +149,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = EM.withVE(10, b)(universe) + val algorithm = EMWithVE(10, b)(universe) algorithm.start val result = b.getLearnedElement(List(true, false)) @@ -176,7 +176,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement(List(true, false)) @@ -197,7 +197,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with val d = DirichletParameter(alphas: _*) val outcomes = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) val outcome = Select(d, outcomes: _*) - val algorithm = EM.withVE(5, d) + val algorithm = EMWithVE(5, d) algorithm.start val result = d.getLearnedElement(outcomes) @@ -239,7 +239,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with outcome.addCondition(x => x >= 3 && x <= 6) } - val algorithm = EM.withVE(10, d) + val algorithm = EMWithVE(10, d) algorithm.start val result = d.getLearnedElement(outcomes) algorithm.kill @@ -292,7 +292,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(3) } - val algorithm = EM.withVE(10, b)(universe) + val algorithm = EMWithVE(10, b)(universe) algorithm.start val result = b.getLearnedElement(outcomes) @@ -329,7 +329,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f1.observe(3) } - val algorithm = EM.withVE(15, b)(universe) + val algorithm = EMWithVE(15, b)(universe) algorithm.start val result = b.getLearnedElement(outcomes) @@ -363,7 +363,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f1.observe(3) } - val algorithm = EM.withVE(3, b)(universe) + val algorithm = EMWithVE(3, b)(universe) algorithm.start val result = b.getLearnedElement(outcomes) @@ -401,7 +401,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f1.observe(3) } - val algorithm = EM.withVE(10, d, b)(universe) + val algorithm = EMWithVE(10, d, b)(universe) algorithm.start val result = d.getLearnedElement(outcomes) @@ -451,7 +451,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = EM.withVE(15, b,d)(universe) + val algorithm = EMWithVE(15, b,d)(universe) algorithm.start val result = d.getLearnedElement(outcomes) @@ -674,7 +674,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with def learner(parameters: Parameters): Algorithm = { parameters match { - case ps: LearnableParameters => EM.withVE(numEMIterations, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) + case ps: LearnableParameters => EMWithVE(numEMIterations, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) case _ => throw new IllegalArgumentException("Not learnable parameters") } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/SufficientStatisticsVariableEliminationTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/SufficientStatisticsVariableEliminationTest.scala index 63f492fc..4379f20a 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/SufficientStatisticsVariableEliminationTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/SufficientStatisticsVariableEliminationTest.scala @@ -18,6 +18,7 @@ import org.scalatest.{ PrivateMethodTester, WordSpec } import com.cra.figaro.algorithm.learning._ import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.algorithm.lazyfactored._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.language._ diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/AnnealingTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/AnnealingTest.scala index 674ba6b3..44c8e2de 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/AnnealingTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/AnnealingTest.scala @@ -55,7 +55,6 @@ class AnnealingTest extends WordSpec with Matchers with PrivateMethodTester { annealer.stop() for { i <- 1 to 4 } { val a = Universe.universe.getElementByReference[(Boolean, Boolean)]("a" + i) - println(i) annealer.mostLikelyValue(a) should equal(true, true) } annealer.kill diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala index 2675bdce..62c04e6b 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala @@ -19,6 +19,7 @@ import com.cra.figaro.algorithm._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.language._ import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.atomic._ import com.cra.figaro.library.atomic.discrete.Binomial import com.cra.figaro.library.compound._ import com.cra.figaro.test._ @@ -26,7 +27,7 @@ import com.cra.figaro.util.logSum import JSci.maths.statistics._ import com.cra.figaro.test.tags.Performance import com.cra.figaro.test.tags.NonDeterministic -import scala.language.reflectiveCalls +import scala.language.reflectiveCalls class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { @@ -170,6 +171,23 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { // Expected value of flip argument is (\int_0^1 x^2 dx) / (\int_0^1 x dx) = 2/3 sampleOneTest(f, (b: Boolean) => b, 2.0 / 3.0) } + + "correctly resample an element's arguments when the arguments change during samples" in { + Universe.createNew() + class Test { + val count = discrete.Uniform(1, 2) + val array = MakeList(count, () => Flip(.9)) + } + val test = Constant(new Test) + val c = Chain(test, (t: Test) => { + val B = Inject(t.array) + Apply(B, (b: List[List[Boolean]]) => b.head) + }) + val alg = Importance(1, c) + val state = Importance.State() + alg.sampleOne(state, c, None) + c.value.asInstanceOf[List[Boolean]].head should be (true || false) + } } "Producing a weighted sample of an element in a universe" should { @@ -229,8 +247,8 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { // Probability that d is true = 0.5 * 1 + 0.5 * 0.46 = 0.73 weightedSampleTest(d, (b: Boolean) => b, 0.73) } - - "with an observation on a compound flip, terminate quickly and produce the correct result" taggedAs(NonDeterministic) in { + + "with an observation on a compound flip, terminate quickly and produce the correct result" taggedAs (NonDeterministic) in { // Tests the likelihood weighting implementation for compound flip Universe.createNew() val b = Uniform(0.0, 1.0) @@ -282,15 +300,15 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { // Uniform(0,1) is beta(1,1) // Result is beta(1 + 16,1 + 4) // Expectation is (alpha) / (alpha + beta) = 17/22 - alg.expectation(b, (d: Double) => d) should be ((17.0/22.0) +- 0.02) + alg.expectation(b, (d: Double) => d) should be((17.0 / 22.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample (time1 - time0) should be <= (500L) alg.shutdown } - - "with an observation on a parameterized flip, terminate quickly and produce the correct result" taggedAs(NonDeterministic) in { + + "with an observation on a parameterized flip, terminate quickly and produce the correct result" taggedAs (NonDeterministic) in { // Tests the likelihood weighting implementation for compound flip Universe.createNew() val b = BetaParameter(2.0, 5.0) @@ -341,14 +359,14 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { alg.stop() // Result is beta(2 + 16,5 + 4) // Expectation is (alpha) / (alpha + beta) = 18/27 - alg.expectation(b, (d: Double) => d) should be ((18.0/27.0) +- 0.02) + alg.expectation(b, (d: Double) => d) should be((18.0 / 27.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample (time1 - time0) should be <= (500L) alg.shutdown } - + "with an observation on a parameterized binomial, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for chain Universe.createNew() @@ -362,7 +380,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { alg.stop() // Result is beta(2 + 1600,5 + 400) // Expectation is (alpha) / (alpha + beta) = 1602/2007 - alg.expectation(beta, (d: Double) => d) should be ((1602.0/2007.0) +- 0.02) + alg.expectation(beta, (d: Double) => d) should be((1602.0 / 2007.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample @@ -384,7 +402,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { // uniform(0,1) is beta(1,1) // Result is beta(1 + 1600,1 + 400) // Expectation is (alpha) / (alpha + beta) = 1601/2003 - alg.expectation(beta, (d: Double) => d) should be ((1601.0/2003.0) +- 0.02) + alg.expectation(beta, (d: Double) => d) should be((1601.0 / 2003.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample @@ -405,7 +423,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { alg.stop() // Result is beta(2 + 1600,5 + 400) // Expectation is (alpha) / (alpha + beta) = 1602/2007 - alg.expectation(beta, (d: Double) => d) should be ((1602.0/2007.0) +- 0.02) + alg.expectation(beta, (d: Double) => d) should be((1602.0 / 2007.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample @@ -413,8 +431,8 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { alg.shutdown } } - - "Running importance sampling" should { + + "Running importance sampling" should { "produce the correct answer each time when run twice with different conditions" in { Universe.createNew() val s = Select(0.5 -> 0.3, 0.5 -> 0.6) @@ -439,24 +457,24 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { val b = Apply(a, (t: temp) => t.t1.value) val alg = Importance(10000, b) alg.start - alg.probability(b, true) should be (0.9 +- .01) + alg.probability(b, true) should be(0.9 +- .01) alg.kill } - "resample elements inside class defined in a chain for foward sampling" taggedAs(NonDeterministic) in { + "resample elements inside class defined in a chain for foward sampling" taggedAs (NonDeterministic) in { Universe.createNew() class temp { val t1 = Flip(0.9) } val a = CachingChain(Constant(0), (i: Int) => Constant(new temp)) val b = Apply(a, (t: temp) => t.t1.value) - val prob = List.fill(5000){Forward(Universe.universe); b.value} - prob.count(_ == true).toDouble/5000.0 should be (0.9 +- .02) + val prob = List.fill(5000) { Forward(Universe.universe); b.value } + prob.count(_ == true).toDouble / 5000.0 should be(0.9 +- .02) //alg.probability(b, true) should be (0.9 +- .01) } - "not suffer from stack overflow with small probability of success" taggedAs (Performance) in { + "not suffer from stack overflow with small probability of success" taggedAs (Performance) in { Universe.createNew() val f = Flip(0.000001) f.observe(true) @@ -593,8 +611,8 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { (state.weight, value.asInstanceOf[T]) } catch { case Importance.Reject => attempt() - } - + } + } var totalWeight = Double.NegativeInfinity @@ -603,17 +621,16 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { val (weight, value) = attempt() if (predicate(value)) successWeight = logSum(weight, successWeight) totalWeight = logSum(weight, totalWeight) - } + } math.exp(successWeight - totalWeight) should be(prob +- tolerance) - + imp.shutdown } - + def probEvidenceTest(prob: Double, evidence: List[NamedEvidence[_]]) { val alg = Importance(10000) alg.start() - alg.probabilityOfEvidence(evidence) should be (prob +- 0.01) + alg.probabilityOfEvidence(evidence) should be(prob +- 0.01) } - } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/MHTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/MHTest.scala index d37e9f8b..3760eccc 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/MHTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/MHTest.scala @@ -288,8 +288,6 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { val alg = MetropolisHastings(numSamples, ProposalScheme.default, elem2) try { alg.start() - println(elem1.value) - println(elem2.value) alg.stop() alg.probability(elem2, (d: Double) => 1.4 < d && d < 1.6) should be(1.0 +- tolerance) } finally { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/SamplingUtil.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/SamplingUtil.scala index ecae1708..d1196358 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/SamplingUtil.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/SamplingUtil.scala @@ -1,18 +1,31 @@ -package com.cra.figaro.test.algorithm.sampling - -object SamplingUtil { - - /* - * Compute the number of samples such that P(E[X] \in [p-epsilon, p+epsilon]) >= 1-lambda +/* + * SamplingUtil.scala + * Utility functions supporting tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 6, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.sampling + +object SamplingUtil { + + /* + * Compute the number of samples such that P(E[X] \in [p-epsilon, p+epsilon]) >= 1-lambda * Uses Chernoff bounds to compute - */ - def computeNumberOfSamples(epsilon: Double, lambda: Double): Int = { - if (lambda <= 0.0 || epsilon <= 0.0) throw new IllegalArgumentException - - val n = (2+epsilon)/(epsilon*epsilon) * math.log(2/lambda) - if (n > 1000000) println("WARNING: LARGE NUMBER OF SAMPLES COMPUTED") - n.toInt - } - - + */ + def computeNumberOfSamples(epsilon: Double, lambda: Double): Int = { + if (lambda <= 0.0 || epsilon <= 0.0) throw new IllegalArgumentException + + val n = (2+epsilon)/(epsilon*epsilon) * math.log(2/lambda) + if (n > 1000000) println("WARNING: LARGE NUMBER OF SAMPLES COMPUTED") + n.toInt + } + + } \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/particlebp/PBPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/particlebp/PBPTest.scala index a6fe4442..3e05d720 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/experimental/particlebp/PBPTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/particlebp/PBPTest.scala @@ -1,290 +1,290 @@ -/* - * BPTest.scala - * Belief Propagation tests. - * - * Created By: Brian Ruttenberg (bruttenberg@cra.com) - * Creation Date: Jan 15, 2014 - * - * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. - * See http://www.cra.com or email figaro@cra.com for information. - * - * See http://www.github.com/p2t2/figaro for a copy of the software license. - */ - -package com.cra.figaro.test.experimental.particlebp - -import org.scalatest.WordSpec -import org.scalatest.Matchers -import com.cra.figaro.algorithm.factored._ -import com.cra.figaro.algorithm.factored.beliefpropagation._ -import com.cra.figaro.language._ -import com.cra.figaro.library.compound.If -import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} -import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} -import com.cra.figaro.library.compound.IntSelector -import com.cra.figaro.algorithm.UnsupportedAlgorithmException -import com.cra.figaro.library.atomic.continuous.Normal -import com.cra.figaro.experimental.particlebp.ParticleBeliefPropagation -import com.cra.figaro.algorithm.factored.ParticleGenerator -import com.cra.figaro.language.Name.stringToName -import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} -import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} -import com.cra.figaro.library.atomic.discrete.Geometric -import com.cra.figaro.library.atomic.continuous.Beta -import com.cra.figaro.algorithm.sampling.Importance - -class PBPTest extends WordSpec with Matchers { - - val globalTol = 0.025 - - // Probably need to change these tests to use things other than normals when - // all infinite elements are enabled - - "Running ParticleBeliefPropagation" should { - - "resample after the inner loop" in { - Universe.createNew() - val n = Normal(0.0, 1.0) - val bpb = ParticleBeliefPropagation(1, 1, n) - bpb.runInnerLoop(Set(), Set()) - val pbpSampler = ParticleGenerator(Universe.universe) - val samples = pbpSampler(n) - bpb.resample - samples should not be pbpSampler(n) - } - - "change the factor structure with new samples on the outer loop" in { - Universe.createNew() - val n = Normal(2.0, 2.0) - val number = Apply(n, (d: Double) => d.round.toInt) - val items = Chain(number, (num: Int) => { - val f = for {i <- 0 until num} yield Flip(0.5) - Inject(f:_*) - }) - val pbpSampler = ParticleGenerator(Universe.universe ) - pbpSampler.update(n, List[(Double, _)]((1.0, 2.0))) - val bpb = ParticleBeliefPropagation(1, 1, items) - bpb.runOuterLoop - val fg_2 = bpb.bp.factorGraph.getNodes.filter(p => p.isInstanceOf[VariableNode]).toSet - - pbpSampler.update(n, List[(Double, _)]((1.0, 3.0))) - val dependentElems = Set[Element[_]](n, number, items) - bpb.runInnerLoop(dependentElems, Set()) - // Currently have to subtract 3 since the old factors for n = 2 also get created since they exist in the chain cache - val fg_3 = bpb.bp.factorGraph.getNodes.filter(p => p.isInstanceOf[VariableNode]).toSet - val diff = fg_3 -- fg_2 - diff.nonEmpty should equal(true) - } - - /* Due to the way that factors are implemented for Chain, all - * models that use chain will result in loops. To test a non-loopy - * graph we have to not use chain, which IntSelector does not. - */ - "with no loops in the factor graph give exact results" in { - Universe.createNew() - //com.cra.figaro.util.setSeed(13) - val e1 = CUniform(1.5, 4.5) - val ep = Apply(e1, (d: Double) => d.round.toInt) - val e2 = IntSelector(ep) - - val bp = ParticleBeliefPropagation(10, 30, e2, e1, ep) - bp.start - - val e2_0 = 0.33333333 * (0.5 + 0.3333333 + 0.25) - val e2_1 = 0.33333333 * (0.5 + 0.3333333 + 0.25) - val e2_2 = 0.33333333 * (0 + 0.3333333 + 0.25) - val e2_3 = 0.33333333 * (0 + 0 + 0.25) - - val tol = 0.025 - val a = bp.distribution(e1).toList - println(a) - val b = bp.distribution(ep).toList - println(b) - bp.probability(e2, (i: Int) => i == 0) should be(e2_0 +- tol) - bp.probability(e2, (i: Int) => i == 1) should be(e2_1 +- tol) - bp.probability(e2, (i: Int) => i == 2) should be(e2_2 +- tol) - bp.probability(e2, (i: Int) => i == 3) should be(e2_3 +- tol) - } - - "with no conditions or constraints produce the correct result" in { - Universe.createNew() - val u = CUniform(0.3, 0.9) - //val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - test(f, (b: Boolean) => b, 0.6, globalTol) - } - - "with a condition on a dependent element produce the result with the correct probability" in { - Universe.createNew() - val u = CUniform(0.3, 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - a.setCondition((i: Int) => i == 2) - // U(true) = \int_{0.2}^{1.0) 0.7 p = 0.35 * 0.96 - // U(false) = \int_{0.2}^{1.0) (1-p) - val u1 = 0.35 * 0.96 - val u2 = 0.32 - test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) - } - - "with a constraint on a dependent element produce the result with the correct probability" in { - Universe.createNew() - val u = CUniform(0.3, 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - a.setConstraint((i: Int) => i.toDouble) - // U(true) = \int_{0.2}^{1.0} (0.3 + 2 * 0.7) p = 0.85 * 0.96 - // U(false) = \int_{0.2}^(1.0) (2 * (1-p)) = 0.64 - val u1 = 0.85 * 0.96 - val u2 = 0.64 - test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) - } - - "with an element that uses another element multiple times, " + - "always produce the same value for the different uses" in { - Universe.createNew() - val f = CUniform(0.3, 0.9) - val e = f === f - test(e, (b: Boolean) => b, 1.0, globalTol) - } - - "with a constraint on an element that is used multiple times, only factor in the constraint once" in { - Universe.createNew() - val f1 = Flip(0.5) - val f2 = Flip(0.3) - val e1 = f1 === f1 - val e2 = f1 === f2 - val d = Dist(0.5 -> e1, 0.5 -> e2) - f1.setConstraint((b: Boolean) => if (b) 3.0; else 2.0) - // Probability that f1 is true = 0.6 - // Probability that e1 is true = 1.0 - // Probability that e2 is true = 0.6 * 0.3 + 0.4 * 0.7 = 0.46 - // Probability that d is true = 0.5 * 1 + 0.5 * 0.46 = 0.73 - test(d, (b: Boolean) => b, 0.73, globalTol) - } - - /* - "on a different universe from the current universe, produce the correct result" in { - val u1 = Universe.createNew() - val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) - val f = Flip(u) - val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) - Universe.createNew() - val tolerance = 0.0000001 - val algorithm = BeliefPropagation(10, f)(u1) - algorithm.start() - algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- globalTol) - algorithm.kill() - } +/* + * BPTest.scala + * Belief Propagation tests. + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: Jan 15, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.experimental.particlebp + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.factored.beliefpropagation._ +import com.cra.figaro.language._ +import com.cra.figaro.library.compound.If +import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} +import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} +import com.cra.figaro.library.compound.IntSelector +import com.cra.figaro.algorithm.UnsupportedAlgorithmException +import com.cra.figaro.library.atomic.continuous.Normal +import com.cra.figaro.experimental.particlebp.ParticleBeliefPropagation +import com.cra.figaro.algorithm.factored.ParticleGenerator +import com.cra.figaro.language.Name.stringToName +import com.cra.figaro.library.atomic.continuous.{Uniform => CUniform} +import com.cra.figaro.library.atomic.discrete.{Uniform => DUniform} +import com.cra.figaro.library.atomic.discrete.Geometric +import com.cra.figaro.library.atomic.continuous.Beta +import com.cra.figaro.algorithm.sampling.Importance + +class PBPTest extends WordSpec with Matchers { + + val globalTol = 0.025 + + // Probably need to change these tests to use things other than normals when + // all infinite elements are enabled + + "Running ParticleBeliefPropagation" should { + + "resample after the inner loop" in { + Universe.createNew() + val n = Normal(0.0, 1.0) + val bpb = ParticleBeliefPropagation(1, 1, n) + bpb.runInnerLoop(Set(), Set()) + val pbpSampler = ParticleGenerator(Universe.universe) + val samples = pbpSampler(n) + bpb.resample + samples should not be pbpSampler(n) + } + + "change the factor structure with new samples on the outer loop" in { + Universe.createNew() + val n = Normal(2.0, 2.0) + val number = Apply(n, (d: Double) => d.round.toInt) + val items = Chain(number, (num: Int) => { + val f = for {i <- 0 until num} yield Flip(0.5) + Inject(f:_*) + }) + val pbpSampler = ParticleGenerator(Universe.universe ) + pbpSampler.update(n, List[(Double, _)]((1.0, 2.0))) + val bpb = ParticleBeliefPropagation(1, 1, items) + bpb.runOuterLoop + val fg_2 = bpb.bp.factorGraph.getNodes.filter(p => p.isInstanceOf[VariableNode]).toSet + + pbpSampler.update(n, List[(Double, _)]((1.0, 3.0))) + val dependentElems = Set[Element[_]](n, number, items) + bpb.runInnerLoop(dependentElems, Set()) + // Currently have to subtract 3 since the old factors for n = 2 also get created since they exist in the chain cache + val fg_3 = bpb.bp.factorGraph.getNodes.filter(p => p.isInstanceOf[VariableNode]).toSet + val diff = fg_3 -- fg_2 + diff.nonEmpty should equal(true) + } + + /* Due to the way that factors are implemented for Chain, all + * models that use chain will result in loops. To test a non-loopy + * graph we have to not use chain, which IntSelector does not. + */ + "with no loops in the factor graph give exact results" in { + Universe.createNew() + //com.cra.figaro.util.setSeed(13) + val e1 = CUniform(1.5, 4.5) + val ep = Apply(e1, (d: Double) => d.round.toInt) + val e2 = IntSelector(ep) + + val bp = ParticleBeliefPropagation(10, 30, e2, e1, ep) + bp.start + + val e2_0 = 0.33333333 * (0.5 + 0.3333333 + 0.25) + val e2_1 = 0.33333333 * (0.5 + 0.3333333 + 0.25) + val e2_2 = 0.33333333 * (0 + 0.3333333 + 0.25) + val e2_3 = 0.33333333 * (0 + 0 + 0.25) + + val tol = 0.025 + val a = bp.distribution(e1).toList + + val b = bp.distribution(ep).toList + + bp.probability(e2, (i: Int) => i == 0) should be(e2_0 +- tol) + bp.probability(e2, (i: Int) => i == 1) should be(e2_1 +- tol) + bp.probability(e2, (i: Int) => i == 2) should be(e2_2 +- tol) + bp.probability(e2, (i: Int) => i == 3) should be(e2_3 +- tol) + } + + "with no conditions or constraints produce the correct result" in { + Universe.createNew() + val u = CUniform(0.3, 0.9) + //val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + test(f, (b: Boolean) => b, 0.6, globalTol) + } + + "with a condition on a dependent element produce the result with the correct probability" in { + Universe.createNew() + val u = CUniform(0.3, 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setCondition((i: Int) => i == 2) + // U(true) = \int_{0.2}^{1.0) 0.7 p = 0.35 * 0.96 + // U(false) = \int_{0.2}^{1.0) (1-p) + val u1 = 0.35 * 0.96 + val u2 = 0.32 + test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) + } + + "with a constraint on a dependent element produce the result with the correct probability" in { + Universe.createNew() + val u = CUniform(0.3, 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setConstraint((i: Int) => i.toDouble) + // U(true) = \int_{0.2}^{1.0} (0.3 + 2 * 0.7) p = 0.85 * 0.96 + // U(false) = \int_{0.2}^(1.0) (2 * (1-p)) = 0.64 + val u1 = 0.85 * 0.96 + val u2 = 0.64 + test(f, (b: Boolean) => b, u1 / (u1 + u2), globalTol) + } + + "with an element that uses another element multiple times, " + + "always produce the same value for the different uses" in { + Universe.createNew() + val f = CUniform(0.3, 0.9) + val e = f === f + test(e, (b: Boolean) => b, 1.0, globalTol) + } + + "with a constraint on an element that is used multiple times, only factor in the constraint once" in { + Universe.createNew() + val f1 = Flip(0.5) + val f2 = Flip(0.3) + val e1 = f1 === f1 + val e2 = f1 === f2 + val d = Dist(0.5 -> e1, 0.5 -> e2) + f1.setConstraint((b: Boolean) => if (b) 3.0; else 2.0) + // Probability that f1 is true = 0.6 + // Probability that e1 is true = 1.0 + // Probability that e2 is true = 0.6 * 0.3 + 0.4 * 0.7 = 0.46 + // Probability that d is true = 0.5 * 1 + 0.5 * 0.46 = 0.73 + test(d, (b: Boolean) => b, 0.73, globalTol) + } + + /* + "on a different universe from the current universe, produce the correct result" in { + val u1 = Universe.createNew() + val u = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + Universe.createNew() + val tolerance = 0.0000001 + val algorithm = BeliefPropagation(10, f)(u1) + algorithm.start() + algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- globalTol) + algorithm.kill() + } * - */ - - "with a posterior different than the prior, converge upon the correct answer on a continuous variable" in { - Universe.createNew() - val fp = CUniform(0.0, 1.0) - val f = new CompoundFlip("", fp, Universe.universe ) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - c.observe(1) - test(f, (b: Boolean) => b == true, .1/.8, globalTol) - } - - "with a model using chain and no conditions or constraints, produce the correct answer" in { - Universe.createNew() - val f = Flip(0.3) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - test(c, (i: Int) => i == 1, 0.3 * 0.1 + 0.7 * 0.7, globalTol) - } - - "with a model using chain and a condition on the result, correctly condition the parent" in { - Universe.createNew() - val f = Flip(0.3) - val s1 = Select(0.1 -> 1, 0.9 -> 2) - val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val c = Chain(f, (b: Boolean) => if (b) s1; else s2) - c.observe(1) - test(f, (b: Boolean) => b, 0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7), globalTol) - } - - - /* - "with a dependent universe, correctly take into account probability of evidence in the dependent universe" in { - Universe.createNew() - //val x = Flip(0.1) - //val y = Flip(0.2) - val x = IntSelector(Constant(10)) - val y = IntSelector(Constant(10)) - val x1 = Apply(x, (i: Int) => i < 1) - val y1 = Apply(y, (i: Int) => i < 2) - val dependentUniverse = new Universe(List(x1, y1)) - val u1 = CUniform(0.0, 1.0)("", dependentUniverse) - val u2 = CUniform(0.0, 2.0)("", dependentUniverse) - - val a = CachingChain(x1, y1, (x: Boolean, y: Boolean) => if (x || y) u1; else u2)("a", dependentUniverse) - val condition = (d: Double) => d < 0.5 - val ve = BeliefPropagation(List((dependentUniverse, List(NamedEvidence("a", Condition(condition))))), 200, x1) - ve.start() - val peGivenXTrue = 0.5 - val peGivenXFalse = 0.2 * 0.5 + 0.8 * 0.25 - val unnormalizedPXTrue = 0.1 * peGivenXTrue - val unnormalizedPXFalse = 0.9 * peGivenXFalse - val pXTrue = unnormalizedPXTrue / (unnormalizedPXTrue + unnormalizedPXFalse) - ve.probability(x1, true) should be(pXTrue +- globalTol) - ve.kill() - } + */ + + "with a posterior different than the prior, converge upon the correct answer on a continuous variable" in { + Universe.createNew() + val fp = CUniform(0.0, 1.0) + val f = new CompoundFlip("", fp, Universe.universe ) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + c.observe(1) + test(f, (b: Boolean) => b == true, .1/.8, globalTol) + } + + "with a model using chain and no conditions or constraints, produce the correct answer" in { + Universe.createNew() + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + test(c, (i: Int) => i == 1, 0.3 * 0.1 + 0.7 * 0.7, globalTol) + } + + "with a model using chain and a condition on the result, correctly condition the parent" in { + Universe.createNew() + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.9 -> 2) + val s2 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val c = Chain(f, (b: Boolean) => if (b) s1; else s2) + c.observe(1) + test(f, (b: Boolean) => b, 0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7), globalTol) + } + + + /* + "with a dependent universe, correctly take into account probability of evidence in the dependent universe" in { + Universe.createNew() + //val x = Flip(0.1) + //val y = Flip(0.2) + val x = IntSelector(Constant(10)) + val y = IntSelector(Constant(10)) + val x1 = Apply(x, (i: Int) => i < 1) + val y1 = Apply(y, (i: Int) => i < 2) + val dependentUniverse = new Universe(List(x1, y1)) + val u1 = CUniform(0.0, 1.0)("", dependentUniverse) + val u2 = CUniform(0.0, 2.0)("", dependentUniverse) + + val a = CachingChain(x1, y1, (x: Boolean, y: Boolean) => if (x || y) u1; else u2)("a", dependentUniverse) + val condition = (d: Double) => d < 0.5 + val ve = BeliefPropagation(List((dependentUniverse, List(NamedEvidence("a", Condition(condition))))), 200, x1) + ve.start() + val peGivenXTrue = 0.5 + val peGivenXFalse = 0.2 * 0.5 + 0.8 * 0.25 + val unnormalizedPXTrue = 0.1 * peGivenXTrue + val unnormalizedPXFalse = 0.9 * peGivenXFalse + val pXTrue = unnormalizedPXTrue / (unnormalizedPXTrue + unnormalizedPXFalse) + ve.probability(x1, true) should be(pXTrue +- globalTol) + ve.kill() + } * - */ - - "with a contingent condition, correctly take into account the contingency" in { - Universe.createNew() - val x = Flip(0.1) - val y = Flip(0.2) - y.setCondition((b: Boolean) => b, List(Element.ElemVal(x, true))) - // Probability of y should be (0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8) (because the case where x is true and y is false has been ruled out) - val ve = ParticleBeliefPropagation(3, 50, y) - ve.start() - ve.probability(y, true) should be(((0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8)) +- globalTol) - } - - } - - /* - "MaxProductBeliefPropagation" should { - "compute the most likely values of all the variables given the conditions and constraints" in { - Universe.createNew() - val e1 = Flip(0.5) - e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) - val e2 = If(e1, Flip(0.4), Flip(0.9)) - val e3 = If(e1, Flip(0.52), Flip(0.4)) - val e4 = e2 === e3 - e4.observe(true) - // p(e1=T,e2=T,e3=T) = 0.75 * 0.4 * 0.52 - // p(e1=T,e2=F,e3=F) = 0.75 * 0.6 * 0.48 - // p(e1=F,e2=T,e3=T) = 0.25 * 0.9 * 0.4 - // p(e1=F,e2=F,e3=F) = 0.25 * 0.1 * 0.6 - // MPE: e1=T,e2=F,e3=F,e4=T - val alg = MPEBeliefPropagation(20) - alg.start() - alg.mostLikelyValue(e1) should equal(true) - alg.mostLikelyValue(e2) should equal(false) - alg.mostLikelyValue(e3) should equal(false) - alg.mostLikelyValue(e4) should equal(true) - } - - } + */ + + "with a contingent condition, correctly take into account the contingency" in { + Universe.createNew() + val x = Flip(0.1) + val y = Flip(0.2) + y.setCondition((b: Boolean) => b, List(Element.ElemVal(x, true))) + // Probability of y should be (0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8) (because the case where x is true and y is false has been ruled out) + val ve = ParticleBeliefPropagation(3, 50, y) + ve.start() + ve.probability(y, true) should be(((0.1 * 0.2 + 0.9 * 0.2) / (0.1 * 0.2 + 0.9 * 0.2 + 0.9 * 0.8)) +- globalTol) + } + + } + + /* + "MaxProductBeliefPropagation" should { + "compute the most likely values of all the variables given the conditions and constraints" in { + Universe.createNew() + val e1 = Flip(0.5) + e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) + val e2 = If(e1, Flip(0.4), Flip(0.9)) + val e3 = If(e1, Flip(0.52), Flip(0.4)) + val e4 = e2 === e3 + e4.observe(true) + // p(e1=T,e2=T,e3=T) = 0.75 * 0.4 * 0.52 + // p(e1=T,e2=F,e3=F) = 0.75 * 0.6 * 0.48 + // p(e1=F,e2=T,e3=T) = 0.25 * 0.9 * 0.4 + // p(e1=F,e2=F,e3=F) = 0.25 * 0.1 * 0.6 + // MPE: e1=T,e2=F,e3=F,e4=T + val alg = MPEBeliefPropagation(20) + alg.start() + alg.mostLikelyValue(e1) should equal(true) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + } + + } * - */ - - - - - def test[T](target: Element[T], predicate: T => Boolean, prob: Double, tol: Double) { - val algorithm = ParticleBeliefPropagation(10, 50, target) - algorithm.start() - algorithm.probability(target, predicate) should be(prob +- tol) - } + */ + + + + + def test[T](target: Element[T], predicate: T => Boolean, prob: Double, tol: Double) { + val algorithm = ParticleBeliefPropagation(10, 50, target) + algorithm.start() + algorithm.probability(target, predicate) should be(prob +- tol) + } } \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala index 8f55e2a6..f4214dba 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala @@ -47,11 +47,8 @@ class ParameterTest extends WordSpec with Matchers { a4.addCondition((i: Int) => (i >= 4)) val numberOfIterations = 2 - val algorithm = EM.withVE(numberOfIterations, b) + val algorithm = EMWithVE(numberOfIterations, b) algorithm.start - - println(b.toString) - } "properly calculate expected value" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala index 07f1d62f..bf29192e 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala @@ -31,6 +31,8 @@ class ContinuousTest extends WordSpec with Matchers { val alg = Importance(20000, elem) alg.start() alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(0.25 +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -39,6 +41,8 @@ class ContinuousTest extends WordSpec with Matchers { val alg = MetropolisHastings(20000, ProposalScheme.default, elem) alg.start() alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(0.25 +- 0.01) + alg.stop() + alg.kill } "for an input within the interval have density equal to 1 divided by the size of the interval" in { @@ -68,6 +72,8 @@ class ContinuousTest extends WordSpec with Matchers { // p(1.25 < x < 1.5 | lower = l) = 0.25 / (2-l) // Expectation of l = \int_{0}^{1} 1 / (2-l) dl = 0.25(-ln(2-1) + ln(2-0)) = 0.1733 alg.probability(uniformComplex, (d: Double) => 1.25 <= d && d < 1.5) should be(0.1733 +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -87,6 +93,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new NormalDistribution(1.0, 2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -97,6 +105,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new NormalDistribution(1.0, 2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -124,6 +134,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -147,6 +159,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.25 * getProb(dist1) + 0.25 * getProb(dist2) + 0.25 * getProb(dist3) + 0.25 * getProb(dist4) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -159,8 +173,8 @@ class ContinuousTest extends WordSpec with Matchers { "produce the right probability when conditioned under Metropolis-Hastings" in { val sampleUniverse = Universe.createNew() val nSamples = Normal(2.5, 2.0)("", sampleUniverse) - val samples = for(i <- 1 to 100) - yield nSamples.generateValue(nSamples.generateRandomness()) + val samples = for (i <- 1 to 100) + yield nSamples.generateValue(nSamples.generateRandomness()) val samplesMean = samples.sum / samples.size val samplesVariance = samples.map(s => (s - samplesMean) * (s - samplesMean)).sum / samples.size @@ -174,34 +188,62 @@ class ContinuousTest extends WordSpec with Matchers { } val alg = MetropolisHastings(100000, ProposalScheme.default, mean, variance) alg.start() - alg.mean(mean) should be(samplesMean +- 0.1) - alg.mean(variance) should be(samplesVariance +- 0.1) + alg.mean(mean) should be(samplesMean +- 1.0) + alg.mean(variance) should be(samplesVariance +- 1.0) + alg.stop() + alg.kill } + + "produce the right probability when conditioned under Importance Sampling" in { + val sampleUniverse = Universe.createNew() + val nSamples = Normal(2.5, 2.0)("", sampleUniverse) + val samples = for (i <- 1 to 25) + yield nSamples.generateValue(nSamples.generateRandomness()) + + val samplesMean = samples.sum / samples.size + val samplesVariance = samples.map(s => (s - samplesMean) * (s - samplesMean)).sum / samples.size + + val universe = Universe.createNew() + val mean = Uniform(-5, 5)("mean", universe) + val variance = Uniform(0, 5)("variance", universe) + for (sample <- samples) { + val normal = Normal(mean, variance) + normal.observe(sample) + } + + val alg = Importance(20000, mean, variance) + alg.start() + alg.mean(mean) should be(samplesMean +- 1.0) + alg.mean(variance) should be(samplesVariance +- 1.0) + alg.stop() + alg.kill + } + } "An AtomicMultivariateNormal" should { - val means = List(1.0, 2.0) - val covariances = List(List(.25, .15), List(.15, .25)) - -// "have value within a range with probability equal to the cumulative probability of the upper minus the lower" in { -// Universe.createNew() -// val elem = Normal(1.0, 2.0) -// val alg = Importance(20000, elem) -// alg.start() -// val dist = new NormalDistribution(1.0, 2.0) -// val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) -// alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) -// } - -// "compute the correct probability under Metropolis-Hastings" in { -// Universe.createNew() -// val elem = Normal(1.0, 2.0) -// val alg = MetropolisHastings(20000, ProposalScheme.default, elem) -// alg.start() -// val dist = new NormalDistribution(1.0, 2.0) -// val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) -// alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) -// } + val means = List(1.0, 2.0) + val covariances = List(List(.25, .15), List(.15, .25)) + + // "have value within a range with probability equal to the cumulative probability of the upper minus the lower" in { + // Universe.createNew() + // val elem = Normal(1.0, 2.0) + // val alg = Importance(20000, elem) + // alg.start() + // val dist = new NormalDistribution(1.0, 2.0) + // val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + // alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + // } + + // "compute the correct probability under Metropolis-Hastings" in { + // Universe.createNew() + // val elem = Normal(1.0, 2.0) + // val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + // alg.start() + // val dist = new NormalDistribution(1.0, 2.0) + // val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + // alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + // } "have the correct density" in { Universe.createNew() @@ -222,7 +264,7 @@ class ContinuousTest extends WordSpec with Matchers { var ss2 = 0.0 var sc12 = 0.0 - for (i <- 0 to 10000) { + for (i <- 0 to 10000) { val rand = elem.generateRandomness() val values = elem.generateValue(rand) @@ -243,17 +285,17 @@ class ContinuousTest extends WordSpec with Matchers { val var2 = (ss2 - (sumx2 * sumx2 / n)) / (n - 1) val cov = (sc12 - (sumx1 * sumx2 / n)) / (n - 1) - mean1 should be (means(0) +- 0.01) - mean2 should be (means(1) +- 0.01) + mean1 should be(means(0) +- 0.01) + mean2 should be(means(1) +- 0.01) - var1 should be (covariances(0)(0) +- 0.01) - var2 should be (covariances(1)(1) +- 0.01) - cov should be (covariances(0)(1) +- 0.01) + var1 should be(covariances(0)(0) +- 0.01) + var2 should be(covariances(1)(1) +- 0.01) + cov should be(covariances(0)(1) +- 0.01) } "convert to the correct string" in { Universe.createNew() - MultivariateNormal(means, covariances).toString should equal( "MultivariateNormal(" + means + ",\n" + covariances + ")") + MultivariateNormal(means, covariances).toString should equal("MultivariateNormal(" + means + ",\n" + covariances + ")") } } @@ -266,6 +308,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new ExponentialDistribution(2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -276,6 +320,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new ExponentialDistribution(2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -304,6 +350,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -315,8 +363,8 @@ class ContinuousTest extends WordSpec with Matchers { "produce the right probability when conditioned under Metropolis-Hastings" in { val sampleUniverse = Universe.createNew() val nSamples = Exponential(2)("", sampleUniverse) - val samples = for(i <- 1 to 100) - yield nSamples.generateValue(nSamples.generateRandomness()) + val samples = for (i <- 1 to 100) + yield nSamples.generateValue(nSamples.generateRandomness()) val universe = Universe.createNew() val lambda = Uniform(0, 10)("lambda", universe) @@ -327,6 +375,27 @@ class ContinuousTest extends WordSpec with Matchers { val alg = MetropolisHastings(200000, ProposalScheme.default, lambda) alg.start() alg.mean(lambda) should be(2.0 +- 0.5) + alg.stop() + alg.kill + } + + "produce the right probability when conditioned under Importance Sampling" in { + val sampleUniverse = Universe.createNew() + val nSamples = Exponential(2)("", sampleUniverse) + val samples = for (i <- 1 to 25) + yield nSamples.generateValue(nSamples.generateRandomness()) + + val universe = Universe.createNew() + val lambda = Uniform(0, 10)("lambda", universe) + for (sample <- samples) { + val exponential = Exponential(lambda) + exponential.observe(sample) + } + val alg = Importance(20000, lambda) + alg.start() + alg.mean(lambda) should be(2.0 +- 0.5) + alg.stop() + alg.kill } } @@ -341,6 +410,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct value under Metropolis-Hastings" in { @@ -352,6 +423,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -378,6 +451,8 @@ class ContinuousTest extends WordSpec with Matchers { def cdf(x: Double) = 1 - exp(-x / theta) val targetProb = cdf(1.2) - cdf(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -390,6 +465,8 @@ class ContinuousTest extends WordSpec with Matchers { def cdf(x: Double) = 1 - exp(-x / theta) val targetProb = cdf(1.2) - cdf(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -417,6 +494,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -428,6 +507,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } } @@ -441,6 +522,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -452,6 +535,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } } } @@ -468,6 +553,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -489,6 +576,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -501,8 +590,8 @@ class ContinuousTest extends WordSpec with Matchers { "produce the right probability when conditioned under Metropolis-Hastings" in { val sampleUniverse = Universe.createNew() val nSamples = Gamma(2, 2)("", sampleUniverse) - val samples = for(i <- 1 to 100) - yield nSamples.generateValue(nSamples.generateRandomness()) + val samples = for (i <- 1 to 100) + yield nSamples.generateValue(nSamples.generateRandomness()) val universe = Universe.createNew() val k = Uniform(0, 10)("k", universe) @@ -513,9 +602,35 @@ class ContinuousTest extends WordSpec with Matchers { } val alg = MetropolisHastings(200000, ProposalScheme.default, k, theta) alg.start() - alg.mean(k) should be(2.0 +- 0.5) - alg.mean(theta) should be(2.0 +- 0.5) + alg.mean(k) should be(2.0 +- 1.0) + alg.mean(theta) should be(2.0 +- 1.0) + alg.stop() + alg.kill + } + + "produce the right probability when conditioned under Importance Sampling" in { + val sampleUniverse = Universe.createNew() + val nSamples = Gamma(2, 2)("", sampleUniverse) + + val samples = for (i <- 1 to 25) + yield nSamples.generateValue(nSamples.generateRandomness()) + + val universe = Universe.createNew() + val k = Uniform(0, 10)("k", universe) + val theta = Uniform(0, 10)("theta", universe) + for (sample <- samples) { + val gamma = Gamma(k, theta) + gamma.observe(sample) + } + + val alg = Importance(20000, k, theta) + alg.start() + alg.mean(k) should be(2.0 +- 1.0) + alg.mean(theta) should be(2.0 +- 1.0) + alg.stop() + alg.kill } + } "A AtomicBeta" should { @@ -529,6 +644,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new BetaDistribution(a, b) val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "compute the correct probability under Metropolis-Hastings" in { @@ -541,6 +658,8 @@ class ContinuousTest extends WordSpec with Matchers { val dist = new BetaDistribution(a, b) val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -572,6 +691,8 @@ class ContinuousTest extends WordSpec with Matchers { def getProb(dist: ProbabilityDistribution) = dist.cumulative(0.3) - dist.cumulative(0.2) val targetProb = 0.25 * getProb(dist1) + 0.25 * getProb(dist2) + 0.25 * getProb(dist3) + 0.25 * getProb(dist4) alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "convert to the correct string" in { @@ -584,8 +705,8 @@ class ContinuousTest extends WordSpec with Matchers { "produce the right probability when conditioned under Metropolis-Hastings" in { val sampleUniverse = Universe.createNew() val nSamples = Beta(2, 5)("", sampleUniverse) - val samples = for(i <- 1 to 100) - yield nSamples.generateValue(nSamples.generateRandomness()) + val samples = for (i <- 1 to 100) + yield nSamples.generateValue(nSamples.generateRandomness()) val universe = Universe.createNew() val a = Uniform(0, 10)("a", universe) @@ -596,8 +717,31 @@ class ContinuousTest extends WordSpec with Matchers { } val alg = MetropolisHastings(200000, ProposalScheme.default, a, b) alg.start() - alg.mean(a) should be(2.0 +- 0.5) - alg.mean(b) should be(5.0 +- 0.5) + alg.mean(a) should be(2.0 +- 1.0) + alg.mean(b) should be(5.0 +- 1.0) + alg.stop() + alg.kill + } + + "produce the right probability when conditioned under Importance Sampling" in { + val sampleUniverse = Universe.createNew() + val nSamples = Beta(2, 5)("", sampleUniverse) + val samples = for (i <- 1 to 25) + yield nSamples.generateValue(nSamples.generateRandomness()) + + val universe = Universe.createNew() + val a = Uniform(0, 10)("a", universe) + val b = Uniform(0, 10)("b", universe) + for (sample <- samples) { + val beta = Beta(a, b) + beta.observe(sample) + } + val alg = Importance(20000, a, b) + alg.start() + alg.mean(a) should be(2.0 +- 1.0) + alg.mean(b) should be(5.0 +- 1.0) + alg.stop() + alg.kill } } @@ -617,6 +761,8 @@ class ContinuousTest extends WordSpec with Matchers { 0.2 <= r && r < 0.3 } alg.probability(elem, (ds: Array[Double]) => check(ds)) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "produce the correct probability under Metropolis-Hastings" in { @@ -633,6 +779,8 @@ class ContinuousTest extends WordSpec with Matchers { 0.2 <= r && r < 0.3 } alg.probability(elem, (ds: Array[Double]) => check(ds)) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -673,6 +821,8 @@ class ContinuousTest extends WordSpec with Matchers { 0.2 <= r && r < 0.3 } alg.probability(elem, (ds: Array[Double]) => check(ds)) should be(targetProb +- 0.01) + alg.stop() + alg.kill } "have the correct density" in { @@ -694,8 +844,8 @@ class ContinuousTest extends WordSpec with Matchers { "produce the right probability when conditioned under Metropolis-Hastings" in { val sampleUniverse = Universe.createNew() val nSamples = Dirichlet(1, 2, 3)("", sampleUniverse) - val samples = for(i <- 1 to 100) - yield nSamples.generateValue(nSamples.generateRandomness()) + val samples = for (i <- 1 to 100) + yield nSamples.generateValue(nSamples.generateRandomness()) val universe = Universe.createNew() val alpha1 = Uniform(0, 10)("a1", universe) @@ -707,9 +857,34 @@ class ContinuousTest extends WordSpec with Matchers { } val alg = MetropolisHastings(200000, ProposalScheme.default, alpha1, alpha2, alpha3) alg.start() - alg.mean(alpha1) should be(1.0 +- 0.5) - alg.mean(alpha2) should be(2.0 +- 0.5) - alg.mean(alpha3) should be(3.0 +- 0.5) + alg.mean(alpha1) should be(1.0 +- 1.0) + alg.mean(alpha2) should be(2.0 +- 1.0) + alg.mean(alpha3) should be(3.0 +- 1.0) + alg.stop() + alg.kill + } + + "produce the right probability when conditioned under Importance Sampling" in { + val sampleUniverse = Universe.createNew() + val nSamples = Dirichlet(1, 2, 3)("", sampleUniverse) + val samples = for (i <- 1 to 25) + yield nSamples.generateValue(nSamples.generateRandomness()) + + val universe = Universe.createNew() + val alpha1 = Uniform(0, 10)("a1", universe) + val alpha2 = Uniform(0, 10)("a2", universe) + val alpha3 = Uniform(0, 10)("a3", universe) + for (sample <- samples) { + val dirichlet = Dirichlet(alpha1, alpha2, alpha3) + dirichlet.observe(sample) + } + val alg = Importance(30000, alpha1, alpha2, alpha3) + alg.start() + alg.mean(alpha1) should be(1.0 +- 1.0) + alg.mean(alpha2) should be(2.0 +- 1.0) + alg.mean(alpha3) should be(3.0 +- 1.0) + alg.stop() + alg.kill } } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerElementTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerElementTest.scala new file mode 100644 index 00000000..80907c46 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerElementTest.scala @@ -0,0 +1,163 @@ +/* + * ContainerElementTest.scala + * Container element tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.library.collection + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.library.collection._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import com.cra.figaro.algorithm.sampling.{Importance, Forward} +import com.cra.figaro.library.compound._ + +class ContainerElementTest extends WordSpec with Matchers { + "A container element" should { + "create elements in the right universes" in { + val u1 = Universe.createNew() + val contElem = create() + val vsa1 = VariableSizeArray(Constant(1), i => Constant(true)) + val u2 = Universe.createNew() + val vsa2 = VariableSizeArray(Constant(1), i => Constant(false)) + contElem(0).universe should equal (u1) + contElem.get(2).universe should equal (u1) + contElem.map(!_)(0).universe should equal (u1) + contElem.chain(if (_) Flip(0.6) else Flip(0.9))(0).universe should equal (u1) + def and(b1: Boolean, b2: Boolean) = b1 && b2 + contElem.reduce(and).universe should equal (u1) + contElem.foldLeft(true)(and).universe should equal (u1) + contElem.foldRight(true)(and).universe should equal (u1) + contElem.aggregate(true)((b1: Boolean, b2: Boolean) => !b1 || b2, (b1: Boolean, b2: Boolean) => b1 && b2).universe should equal (u1) + contElem.count((b: Boolean) => b).universe should equal (u1) + contElem.exists((b: Boolean) => b).universe should equal (u1) + contElem.forall((b: Boolean) => b).universe should equal (u1) + contElem.length.universe should equal (u1) + val contElem2 = new ContainerElement(Constant(Container(Flip(0.6)))) + vsa1.concat(vsa2).element.universe should equal (u1) + } + + "get the right element using apply" in { + Universe.createNew() + val contElem = create() + VariableElimination.probability(contElem(0), true) should be ((0.5 * 0.1 + 0.5 * 0.3) +- 0.0000000001) + } + + "get the right optional element using get" in { + Universe.createNew() + val contElem = create() + VariableElimination.probability(contElem.get(2), Some(true)) should be ((0.5 * 0.5) +- 0.000000000001) + } + + "map a function through all possible values correctly" in { + Universe.createNew() + val contElem = create() + VariableElimination.probability(contElem.map(!_)(0), false) should be ((0.5 * 0.1 + 0.5 * 0.3) +- 0.0000000001) + } + + "chain a function through all possible values correctly" in { + Universe.createNew() + val contElem = create() + val p1 = 0.5 * 0.1 + 0.5 * 0.3 + val p2 = 1 - p1 + val answer = p1 * 0.6 + p2 * 0.9 + VariableElimination.probability(contElem.chain(if (_) Flip(0.6) else Flip(0.9))(0), true) should be (answer +- 0.0000001) + } + + "correctly fold through elements" in { + Universe.createNew() + val contElem = create() + val p1 = 0.1 * 0.2 + val p2 = 0.3 * 0.4 * 0.5 + val answer = 0.5 * p1 + 0.5 * p2 + def and(b1: Boolean, b2: Boolean) = b1 && b2 + val e1 = contElem.reduce(and) + val e2 = contElem.foldLeft(true)(and) + val e3 = contElem.foldRight(true)(and) + val e4 = contElem.aggregate(true)((b1: Boolean, b2: Boolean) => !b1 || b2, (b1: Boolean, b2: Boolean) => b1 && b2) + val alg = VariableElimination(e1, e2, e3, e4) + alg.start() + alg.probability(e1, true) should be (answer +- 0.0000001) + alg.probability(e2, true) should be (answer +- 0.0000001) + alg.probability(e3, true) should be (answer +- 0.0000001) + alg.probability(e4, true) should be (answer +- 0.0000001) + alg.kill() + } + + "when quantifying over elements satisfying a predicate, produce the right answer" in { + Universe.createNew() + val contElem = create() + val elem1 = contElem.count((b: Boolean) => b) + val elem2 = contElem.exists((b: Boolean) => b) + val elem3 = contElem.forall((b: Boolean) => b) + val alg = VariableElimination(elem1, elem2, elem3) + alg.start() + val p10 = 0.9 * 0.8 + val p20 = 0.7 * 0.6 * 0.5 + val p11 = 0.9 * 0.2 + 0.1 * 0.8 + val p21 = 0.7 * 0.6 * 0.5 + 0.7 * 0.4 * 0.5 + 0.3 * 0.6 * 0.5 + val p12 = 0.1 * 0.2 + val p22 = 0.7 * 0.4 * 0.5 + 0.3 * 0.6 * 0.5 + 0.3 * 0.4 * 0.5 + val p23 = 0.3 * 0.4 * 0.5 + alg.probability(elem1, 0) should be ((0.5 * p10 + 0.5 * p20) +- 0.0000001) + alg.probability(elem1, 1) should be ((0.5 * p11 + 0.5 * p21) +- 0.0000001) + alg.probability(elem1, 2) should be ((0.5 * p12 + 0.5 * p22) +- 0.0000001) + alg.probability(elem2, true) should be ((1 - (0.5 * p10 + 0.5 * p20)) +- 0.0000001) + alg.probability(elem3, true) should be ((0.5 * p12 + 0.5 * p23) +- 0.0000001) + alg.kill() + } + + "correctly produce an element over the length of the container" in { + Universe.createNew() + val contElem = create() + val len = contElem.length + val alg = VariableElimination(len) + val answer = 0.5 * 2 + 0.5 * 3 + alg.start() + alg.expectation(len, (i: Int) => i.toDouble) should be (answer +- 0.0000001) + alg.kill() + } + + "when concatenating, have all the elements of both processes, with the second process following the first" in { + Universe.createNew() + val vsa1 = VariableSizeArray(Select(0.2 -> 1, 0.8 -> 2), i => Flip(0.9)) + val vsa2 = VariableSizeArray(Constant(1), i => Flip(0.3)) + val vsa = vsa1.concat(vsa2) + val all = vsa.forall((b: Boolean) => b) + val p1 = 0.9 * 0.3 // probability first choice for contElem1 are all true and contElem2 is all true + val p2 = 0.9 * 0.9 * 0.3 // probability second choice for contElem1 are all true and contElem2 is all true + val answer = 0.2 * p1 + 0.8 * p2 + Importance.probability(all, true) should be (answer +- 0.01) + } + + "select a random element correctly without throwing IndexOutOfRangeException" in { + Universe.createNew() + val vsa = VariableSizeArray(Select(0.2 -> 1, 0.8 -> 2), i => Constant(i)) + val elem = vsa.randomElement + Importance.probability(elem, 1) should be ((0.8 * 0.5) +- 0.01) + } + + } + + def create() = { + val elem1 = Flip(0.1) + val elem2 = Flip(0.2) + val elem3 = Flip(0.3) + val elem4 = Flip(0.4) + val elem5 = Flip(0.5) + val container1: Container[Int, Boolean] = Container(elem1, elem2) + val container2: Container[Int, Boolean] = Container(elem3, elem4, elem5) + val containerChooser = Select(0.5 -> container1, 0.5 -> container2) + new ContainerElement(containerChooser) + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerTest.scala new file mode 100644 index 00000000..f4b99550 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ContainerTest.scala @@ -0,0 +1,251 @@ +/* + * ContainerTest.scala + * Container tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.library.collection + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.library.collection._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import com.cra.figaro.algorithm.sampling.Importance +import com.cra.figaro.library.compound._ +import com.cra.figaro.algorithm.sampling.MetropolisHastings + +class ContainerTest extends WordSpec with Matchers { + "A Container" should { + "create elements in the correct universe" in { + val u1 = Universe.createNew() + val proc = createContainer(List(2,3)) + val fsa1 = new FixedSizeArray(1, i => Constant(true)("", u1)) + val u2 = Universe.createNew() + val fsa2 = new FixedSizeArray(1, i => Constant(false)("", u2)) + val e1 = proc(2) + e1.universe should equal (u1) + val e2 = proc.get(2) + e2.universe should equal (u1) + val e3 = proc(List(2,3))(2) + e3.universe should equal (u1) + val e4 = proc.get(List(2,3))(2) + e4.universe should equal (u1) + val e5 = proc.map(!_)(2) + e5.universe should equal (u1) + val e6 = proc.chain(if (_) Flip(0.3) else Flip(0.6))(2) + e6.universe should equal (u1) + val proc2 = createContainer(List(4)) + val proc3 = proc ++ proc2 + // It is possible to have elements from different universes in the same process. Getting an element should produce an element from + // the correct universe + val e7 = proc3.get(2) + val e8 = proc3.get(4) + e7.universe should equal (u1) + e8.universe should equal (u2) + val e9 = proc3.map(!_)(2) + val e10 = proc3.map(!_)(4) + e9.universe should equal (u1) + e10.universe should equal (u2) + val e11 = proc3.chain(if (_) Flip(0.3) else Flip(0.6))(2) + val e12 = proc3.chain(if (_) Flip(0.3) else Flip(0.6))(4) + e11.universe should equal (u1) + e12.universe should equal (u2) + val fsa3 = fsa1.concat(fsa2) + // It is possible to have elements from different universes in the same process. Getting an element should produce an element from + // the correct universe + val e13 = fsa3.get(0) + val e14 = fsa3.get(1) + e13.universe should equal (u1) + e14.universe should equal (u2) + val e15 = fsa3.map(!_)(0) + val e16 = fsa3.map(!_)(1) + e15.universe should equal (u1) + e16.universe should equal (u2) + val e17 = fsa3.chain(if (_) Flip(0.3) else Flip(0.6))(0) + val e18 = fsa3.chain(if (_) Flip(0.3) else Flip(0.6))(1) + e17.universe should equal (u1) + e18.universe should equal (u2) + } + + "check range correctly" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + proc.rangeCheck(2) should equal (true) + proc.rangeCheck(1) should equal (false) + } + + "generate the correct elements" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + val elems = proc.elements + assert(elems(0).isInstanceOf[AtomicFlip]) + elems(0).asInstanceOf[AtomicFlip].prob should equal (0.5) + assert(elems(1).isInstanceOf[AtomicFlip]) + elems(1).asInstanceOf[AtomicFlip].prob should be ((1.0/3) +- 0.0000000001) + elems.length should equal (2) + } + + "generate the same elements each time" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + val elems1 = proc.elements + val elems2 = proc.elements + elems1(0) should equal (elems2(0)) + elems1(1) should equal (elems2(1)) + } + + "generate the correct map" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + } + + "when mapping, have each point mapped according to the function" in { + Universe.createNew() + val proc = createContainer(List(2,3)).map(!_) + val elem = proc(3) + VariableElimination.probability(elem, true) should be ((2.0 / 3) +- 0.000000001) + } + + "when chaining, have each point flatMapped according to the function" in { + Universe.createNew() + val proc = createContainer(List(2,3)).chain(if (_) Flip(0.3) else Flip(0.6)) + val elem = proc(3) + VariableElimination.probability(elem, true) should be ((1.0 / 3 * 0.3 + 2.0 / 3 * 0.6) +- 0.000000001) + } + + "when appending, have all the elements of both processes, with the second process replacing the first when necessary" in { + Universe.createNew() + val proc1 = createContainer(List(2,3)) + val proc2 = createContainer(List(3,4), true) + val proc = proc1 ++ proc2 + val elem2 = proc(2) + val elem3 = proc(3) + val elem4 = proc(4) + an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } + val alg = VariableElimination(elem2, elem3, elem4) + alg.start() + alg.probability(elem2, true) should be (0.5 +- 0.000001) + alg.probability(elem3, true) should be (2.0 / 3.0 +- 0.00000001) // inverted + alg.probability(elem4, true) should be (3.0 / 4.0 +- 0.00000001) // inverted + alg.kill() + } + + "when folding or reducing, have the points folded according to the function" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + val elem1 = proc.foldLeft(true)((b1: Boolean, b2: Boolean) => b1 && b2) + val elem2 = proc.foldRight(true)((b1: Boolean, b2: Boolean) => b1 && b2) + val elem3 = proc.reduce((b1: Boolean, b2: Boolean) => b1 && b2) + val elem4 = proc.aggregate(true)((b1: Boolean, b2: Boolean) => !b1 || b2, (b1: Boolean, b2: Boolean) => b1 && b2) + val alg = Importance(10000, elem1, elem2, elem3, elem4) + alg.start() + alg.probability(elem1, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + alg.probability(elem2, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + alg.probability(elem3, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + alg.probability(elem4, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + alg.kill() + VariableElimination.probability(elem1, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + MetropolisHastings.probability(elem1, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) + } + + "when quantifying over elements satisfying a predicate, produce the right answer" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + val elem1 = proc.count((b: Boolean) => b) + val elem2 = proc.exists((b: Boolean) => b) + val elem3 = proc.forall((b: Boolean) => b) + val alg = Importance(10000, elem1, elem2, elem3) + alg.start() + val p0 = 1.0 / 2 * 2.0 / 3 + val p1 = 1.0 / 2 * 1.0 / 3 + 1.0 / 2 * 2.0 / 3 + val p2 = 1.0 / 2 * 1.0 / 3 + alg.probability(elem1, 0) should be (p0 +- 0.02) + alg.probability(elem1, 1) should be (p1 +- 0.02) + alg.probability(elem1, 2) should be (p2 +- 0.02) + alg.probability(elem2, true) should be ((p1 + p2) +- 0.02) + alg.probability(elem3, true) should be (p2 +- 0.02) + alg.kill() + } + + "when finding the index of the first element, produce the right answer" in { + Universe.createNew() + val proc = createContainer(List(2,3)) + val elem = proc.findIndex((b: Boolean) => b) + val alg = Importance(10000, elem) + alg.start() + val p2 = 1.0 / 2.0 + val p3 = (1 - p2) * 1.0 / 3.0 + val pNone = 1 - p2 - p3 + alg.probability(elem, Some(2)) should be (p2 +- 0.02) + alg.probability(elem, Some(3)) should be (p3 +- 0.02) + alg.probability(elem, None) should be (pNone +- 0.02) + alg.kill() + } + + "when concatenating, have all the elements of both processes, with the second process following the first" in { + Universe.createNew() + val fsa1 = new FixedSizeArray(2, i => Constant(i)) + val fsa2 = new FixedSizeArray(2, i => Constant(i + 2)) + val fsa = fsa1.concat(fsa2) + fsa.indices.toList should equal (List(0, 1, 2, 3)) + val e0 = fsa(0) + val e1 = fsa(1) + val e2 = fsa(2) + val e3 = fsa(3) + assert(e0.isInstanceOf[Constant[Int]]) + e0.asInstanceOf[Constant[Int]].constant should equal (0) + assert(e2.isInstanceOf[Constant[Int]]) + e2.asInstanceOf[Constant[Int]].constant should equal (2) + } + + "when choosing a random element, choose one of the elements uniformly at random" in { + Universe.createNew() + val fsa = new FixedSizeArray(2, i => Constant(i)) + val elem = fsa.randomElement + VariableElimination.probability(elem, 1) should be (0.5 +- 0.00000001) + } + + "when choosing two random elements, have them be independent" in { + Universe.createNew() + val fsa = new FixedSizeArray(2, i => Constant(i)) + val elem1 = fsa.randomElement() + val elem2 = fsa.randomElement() + val eq = elem1 === elem2 + VariableElimination.probability(eq, true) should be (0.5 +- 0.000000001) + } + } + + def createContainer(is: List[Int], invert: Boolean = false): Container[Int, Boolean] = new Container[Int, Boolean] { + val universe = Universe.universe + val indices = is + def generate(index: Int) = if (invert) Flip(1.0 - 1.0 / index)("", universe) else Flip(1.0 / index)("", universe) + def generate(indices: List[Int]) = { + val unary = for { + index <- indices + } yield (index, generate(index)) + val map = Map(unary:_*) + val binary = + for { + index1 <- indices + index2 <- indices + if index1 < index2 + } yield { + val elem1 = map(index1) + val elem2 = map(index2) + val pair = ^^(elem1, elem2)("", universe) + pair.addConstraint((pair: (Boolean, Boolean)) => if (pair._1 != pair._2) 1.0 / (index1 + index2) else 1.0) + pair + } + Map(unary:_*) + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessElementTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessElementTest.scala new file mode 100644 index 00000000..a25d7f44 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessElementTest.scala @@ -0,0 +1,76 @@ +/* + * ProcessElementTest.scala + * Process element tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.library.collection + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.library.collection._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import com.cra.figaro.algorithm.sampling.Importance +import com.cra.figaro.library.compound._ + +class ProcessElementTest extends WordSpec with Matchers { + "A process element" should { + "create elements in the right universes" in { + val u1 = Universe.createNew() + val procElem = create() + val u2 = Universe.createNew() + procElem(0).universe should equal (u1) + procElem.get(2).universe should equal (u1) + procElem.map(!_)(0).universe should equal (u1) + procElem.chain(if (_) Flip(0.6) else Flip(0.9))(0).universe should equal (u1) + } + + "get the right element using apply" in { + Universe.createNew() + val procElem = create() + VariableElimination.probability(procElem(0), true) should be ((0.5 * 0.1 + 0.5 * 0.3) +- 0.0000000001) + } + + "get the right optional element using get" in { + Universe.createNew() + val procElem = create() + VariableElimination.probability(procElem.get(2), Some(true)) should be ((0.5 * 0.5) +- 0.000000000001) + } + + "map a function through all possible values correctly" in { + Universe.createNew() + val procElem = create() + VariableElimination.probability(procElem.map(!_)(0), false) should be ((0.5 * 0.1 + 0.5 * 0.3) +- 0.0000000001) + } + + "chain a function through all possible values correctly" in { + Universe.createNew() + val procElem = create() + val p1 = 0.5 * 0.1 + 0.5 * 0.3 + val p2 = 1 - p1 + val answer = p1 * 0.6 + p2 * 0.9 + VariableElimination.probability(procElem.chain(if (_) Flip(0.6) else Flip(0.9))(0), true) should be (answer +- 0.0000001) + } + } + + def create() = { + val elem1 = Flip(0.1) + val elem2 = Flip(0.2) + val elem3 = Flip(0.3) + val elem4 = Flip(0.4) + val elem5 = Flip(0.5) + val process1: Process[Int, Boolean] = Container(elem1, elem2) + val process2: Process[Int, Boolean] = Container(elem3, elem4, elem5) + val processChooser = Select(0.5 -> process1, 0.5 -> process2) + new ProcessElement(processChooser) + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessTest.scala new file mode 100644 index 00000000..91a86797 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/ProcessTest.scala @@ -0,0 +1,188 @@ +/* + * ProcessTest.scala + * Process tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.library.collection + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.library.collection._ +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import com.cra.figaro.algorithm.sampling.Importance +import com.cra.figaro.library.compound._ + +class ProcessTest extends WordSpec with Matchers { + "A process" should { + "create elements in the correct universe" in { + val u1 = Universe.createNew() + val proc = createProcess(List(2,3)) + val u2 = Universe.createNew() + val e1 = proc(2) + e1.universe should equal (u1) + val e2 = proc.get(2) + e2.universe should equal (u1) + val e3 = proc(List(2,3))(2) + e3.universe should equal (u1) + val e4 = proc.get(List(2,3))(2) + e4.universe should equal (u1) + val e5 = proc.map(!_)(2) + e5.universe should equal (u1) + val e6 = proc.chain(if (_) Flip(0.3) else Flip(0.6))(2) + e6.universe should equal (u1) + val proc2 = createProcess(List(4)) + val proc3 = proc ++ proc2 + // It is possible to have elements from different universes in the same process. Getting an element should produce an element from + // the correct universe + val e7 = proc3.get(2) + val e8 = proc3.get(4) + e7.universe should equal (u1) + e8.universe should equal (u2) + val e9 = proc3.map(!_)(2) + val e10 = proc3.map(!_)(4) + e9.universe should equal (u1) + e10.universe should equal (u2) + val e11 = proc3.chain(if (_) Flip(0.3) else Flip(0.6))(2) + val e12 = proc3.chain(if (_) Flip(0.3) else Flip(0.6))(4) + e11.universe should equal (u1) + e12.universe should equal (u2) + } + + "get the right element for an index in range" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + val elem = proc(2) + val alg = VariableElimination(elem) + alg.start() + alg.probability(elem, true) should be (0.5 +- 0.0000000001) + alg.kill() + } + + "correctly cache a single element when using apply" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + val elem1 = proc(2) + val elem2 = proc(2) + elem1 should equal (elem2) + } + + "get the right elements for multiple indices in range" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + val elems = proc(List(2,3)) + val alg = VariableElimination(elems(2)) + alg.start() + val q23 = (1.0 / 2) * (1.0 / 3) + val q2not3 = (1.0 / 2) * (2.0 / 3) * (1.0 / 5) + val qnot23 = (1.0 / 2) * (1.0 / 3) * (1.0 / 5) + val qnot2not3 = (1.0 / 2) * (2.0 / 3) + val q2 = q23 + q2not3 + val qnot2 = qnot23 + qnot2not3 + val p2 = q2 / (q2 + qnot2) + alg.probability(elems(2), true) should be (p2 +- 0.0000000001) + alg.kill() + } + + "throw IndexOutOfRange exception for an index out of range" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } + an [proc.IndexOutOfRangeException] should be thrownBy { proc(List(2,1)) } + } + + "get the right optional element for an index in or out of range" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + val elem1 = proc.get(2) + val elem2 = proc.get(1) + val alg = VariableElimination(elem1, elem2) + alg.start() + alg.probability(elem1, Some(true)) should be (0.5 +- 0.00000001) + alg.probability(elem2, None) should be (1.0 +- 0.000000001) + alg.kill() + } + + "get the right optional elements for multiple indices in or out of range" in { + Universe.createNew() + val proc = createProcess(List(2,3)) + val elems = proc.get(List(2, 1)) + val alg = VariableElimination(elems(1), elems(2)) + alg.start() + alg.probability(elems(2), Some(true)) should be (0.5 +- 0.00000001) + alg.probability(elems(1), None) should be (1.0 +- 0.000000001) + alg.kill() + } + + "when mapping, have each point mapped according to the function" in { + Universe.createNew() + val proc = createProcess(List(2,3)).map(!_) + val elem = proc(3) + val alg = VariableElimination(elem) + alg.start() + alg.probability(elem, true) should be ((2.0 / 3) +- 0.000000001) + alg.kill() + } + + "when chaining, have each point flatMapped according to the function" in { + Universe.createNew() + val proc = createProcess(List(2,3)).chain(if (_) Flip(0.3) else Flip(0.6)) + val elem = proc(3) + val alg = VariableElimination(elem) + alg.start() + alg.probability(elem, true) should be ((1.0 / 3 * 0.3 + 2.0 / 3 * 0.6) +- 0.000000001) + alg.kill() + } + + "when appending, have all the elements of both processes, with the second process replacing the first when necessary" in { + Universe.createNew() + val proc1 = createProcess(List(2,3)) + val proc2 = createProcess(List(3,4), true) + val proc = proc1 ++ proc2 + val elem2 = proc(2) + val elem3 = proc(3) + val elem4 = proc(4) + an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } + val alg = VariableElimination(elem2, elem3, elem4) + alg.start() + alg.probability(elem2, true) should be (0.5 +- 0.000001) + alg.probability(elem3, true) should be (2.0 / 3.0 +- 0.00000001) // inverted + alg.probability(elem4, true) should be (3.0 / 4.0 +- 0.00000001) // inverted + alg.kill() + } + } + + def createProcess(indices: List[Int], invert: Boolean = false): Process[Int, Boolean] = new Process[Int, Boolean] { + val universe = Universe.universe + def rangeCheck(index: Int) = indices.contains(index) + def generate(index: Int) = if (invert) Flip(1.0 - 1.0 / index)("", universe) else Flip(1.0 / index)("", universe) + def generate(indices: List[Int]) = { + val unary = for { + index <- indices + } yield (index, generate(index)) + val map = Map(unary:_*) + val binary = + for { + index1 <- indices + index2 <- indices + if index1 < index2 + } yield { + val elem1 = map(index1) + val elem2 = map(index2) + val pair = ^^(elem1, elem2)("", universe) + pair.addConstraint((pair: (Boolean, Boolean)) => if (pair._1 != pair._2) 1.0 / (index1 + index2) else 1.0) + pair + } + Map(unary:_*) + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/collection/VariableSizeArrayTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/VariableSizeArrayTest.scala new file mode 100644 index 00000000..368417bf --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/collection/VariableSizeArrayTest.scala @@ -0,0 +1,151 @@ +/* + * VariableSizeArrayTest.scala + * Variable size array tests. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Nov 27, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.library.collection + +import org.scalatest.WordSpec +import org.scalatest.Matchers +import com.cra.figaro.language._ +import com.cra.figaro.library.collection._ +import com.cra.figaro.algorithm.sampling.{Importance, MetropolisHastings, ProposalScheme} +import com.cra.figaro.algorithm.factored.VariableElimination +import com.cra.figaro.algorithm.factored.beliefpropagation.BeliefPropagation +import com.cra.figaro.library.atomic.discrete.Geometric +import com.cra.figaro.algorithm.lazyfactored.LazyValues + +class VariableSizeArrayTest extends WordSpec with Matchers { + + "A variable size array" should { + "correctly create random lists of the appropriate lengths" in { + Universe.createNew() + def generator(n: Int) = Flip(1.0 / (n + 1)) + val lengthChooser = Select(0.4 -> 2, 0.6 -> 3) + val array = Container(lengthChooser, generator _) + val all = array.forall((b: Boolean) => b) + val p1 = 1.0 / 1 * 1.0 / 2 + val p2 = p1 * 1.0 / 3 + val answer = 0.4 * p1 + 0.6 * p2 + Importance.probability(all, true) should be (answer +- 0.02) + } + + "always use the same elements for the value at an index" in { + Universe.createNew() + var count = 0 + def generator(n: Int) = { count += 1; Flip(1.0 / (n + 1)) } + val lengthChooser = Select(0.5 -> 1, 0.5 -> 2) + val array = Container(lengthChooser, generator _) + lengthChooser.observe(1) // causes the array element to generate + lengthChooser.observe(2) // causes the array element to generate + count should equal (2) // proves that we've only generated two elements, so the first one was shared + } + + "have the number of items be distributed according to the first argument" in { + Universe.createNew() + val x1 = Geometric(0.3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2.length + val answer = 0.3 * 0.3 * 0.7 + Importance.probability(x3, 3) should be(answer +- 0.01) + } + + "have each item be distributed according to the element generator" in { + Universe.createNew() + val x1 = Select(0.5 -> 2, 0.5 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2(0) + val x4 = x2(1) + val answer = 0.2 + VariableElimination.probability(x3, true) should be(answer +- 0.0000001) + VariableElimination.probability(x4, true) should be(answer +- 0.0000001) + } + + "have the items be generated independently" in { + Universe.createNew() + val x1 = Select(0.5 -> 2, 0.5 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2(0) + val x4 = x2(1) + val x5 = x3 === x4 + val answer = 0.2 * 0.2 + 0.8 * 0.8 + VariableElimination.probability(x5, true) should be(answer +- 0.0000001) + } + + "have the correct set of possible values, generating values for the items" in { + Universe.createNew() + val x1 = Select(0.4 -> 2, 0.6 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val values = LazyValues(Universe.universe) + val valueSet = values(x2.element, Integer.MAX_VALUE) + valueSet.hasStar should equal (false) + val regVals = valueSet.regularValues.toList + regVals.size should equal (2) + val container0 = regVals(0).asInstanceOf[Container[Int, Boolean]] + val container1 = regVals(1).asInstanceOf[Container[Int, Boolean]] + val item00 = container0(0) + val item01 = container0(1) + val item10 = container1(0) + val item11 = container1(1) + val item12 = container1(2) + for { item <- List(item00, item01, item10, item11, item12) } { + val itemValueSet = values.storedValues(item) + itemValueSet.hasStar should equal (false) + itemValueSet.regularValues should equal (Set(false, true)) + } + } + + "return the correct probability under importance sampling" in { + Universe.createNew() + val x1 = Select(0.4 -> 2, 0.6 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2.exists { (b: Boolean) => b } + x3.observe(true) + val alg = Importance(10000, x1) + alg.start() + val p2 = 0.4 * (1 - 0.8 * 0.8) + val p3 = 0.6 * (1 - 0.8 * 0.8 * 0.8) + val answer = p3 / (p2 + p3) + alg.probability(x1, 3) should be(answer +- 0.01) + alg.kill() + } + + "return the correct probability under variable elimination" in { + Universe.createNew() + val x1 = Select(0.4 -> 2, 0.6 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2.exists { (b: Boolean) => b } + x3.observe(true) + val alg = VariableElimination(x1) + alg.start() + val p2 = 0.4 * (1 - 0.8 * 0.8) + val p3 = 0.6 * (1 - 0.8 * 0.8 * 0.8) + val answer = p3 / (p2 + p3) + alg.probability(x1, 3) should be(answer +- 0.0000001) + alg.kill() + } + + "return the correct probability under Metropolis-Hastings" in { + Universe.createNew() + val x1 = Select(0.4 -> 2, 0.6 -> 3) + val x2 = Container(x1, (i: Int) => Flip(0.2)) + val x3 = x2.exists { (b: Boolean) => b } + x3.observe(true) + val alg = MetropolisHastings(200000, ProposalScheme.default, x1) + alg.start() + val p2 = 0.4 * (1 - 0.8 * 0.8) + val p3 = 0.6 * (1 - 0.8 * 0.8 * 0.8) + val answer = p3 / (p2 + p3) + alg.probability(x1, 3) should be(answer +- 0.02) + alg.kill() + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/compound/CompoundTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/compound/CompoundTest.scala index 7c27f68b..3887ccc3 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/compound/CompoundTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/compound/CompoundTest.scala @@ -1,13 +1,13 @@ /* - * CompoundTest.scala + * CompoundTest.scala * Compound element tests. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -250,7 +250,7 @@ class CompoundTest extends WordSpec with Matchers { val y = CPD(x, true -> Flip(0.7)) val alg = VariableElimination(y) alg.start() - } + } } } @@ -283,7 +283,7 @@ class CompoundTest extends WordSpec with Matchers { (true, 3) -> Flip(0.6)) val alg = VariableElimination(y) alg.start() - } + } } } @@ -318,7 +318,7 @@ class CompoundTest extends WordSpec with Matchers { (true, 3, 7) -> Flip(0.6)) val alg = VariableElimination(y) alg.start() - } + } } } @@ -370,7 +370,7 @@ class CompoundTest extends WordSpec with Matchers { (true, 3, 7, true) -> Flip(0.65)) val alg = VariableElimination(y) alg.start() - } + } } } @@ -424,7 +424,7 @@ class CompoundTest extends WordSpec with Matchers { (true, 3, 7, true, 'a) -> Flip(0.65)) val alg = VariableElimination(y) alg.start() - } + } } } @@ -640,14 +640,14 @@ class CompoundTest extends WordSpec with Matchers { x2.value = x2.generateValue(x2.randomness) x2.value should equal(0) } - + "produce the correct values when Values() called" in { Universe.createNew() val x1 = Select(0.75 -> 3, 0.25 -> 5) val x2 = IntSelector(x1) Values()(x2) should equal(Set(0, 1, 2, 3, 4)) } - + "produce the correct factors" in { def prob(counter: Int, value: Int) = if (value < counter) 1.0/counter else 0.0 Universe.createNew() @@ -660,8 +660,49 @@ class CompoundTest extends WordSpec with Matchers { dist.foreach{v => v._1 should be(clauses.filter(v._2 < _._2).map(c => c._1/c._2).sum +- 0.0001) } - + + } + + } + + "Folding" should { + "produce the correct result under importance sampling" in { + val elems = List(Flip(0.3), Flip(0.4), Flip(0.6)) + val fl = FoldLeft(true, (b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val fr = FoldRight(true, (b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val red = Reduce((b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val alg = Importance(20000, fl, fr, red) + alg.start() + val answer = 0.3 * 0.4 * 0.6 + alg.probability(fl, true) should be (answer +- 0.02) + alg.probability(fr, true) should be (answer +- 0.02) + alg.probability(red, true) should be (answer +- 0.02) + alg.kill() + } + + "produce the correct result under variable elimination" in { + val elems = List(Flip(0.3), Flip(0.4), Flip(0.6)) + val fl = FoldLeft(true, (b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val fr = FoldRight(true, (b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val red = Reduce((b1: Boolean, b2: Boolean) => (b1 && b2))(elems:_*) + val alg = VariableElimination(fl, fr, red) + alg.start() + val answer = 0.3 * 0.4 * 0.6 + alg.probability(fl, true) should be (answer +- 0.00000001) + alg.probability(fr, true) should be (answer +- 0.00000001) + alg.probability(red, true) should be (answer +- 0.00000001) + alg.kill() + } + + "process items in the correct order" in { + val elems = List(Constant('a), Constant('b)) + val fl = FoldLeft('z, (x: Symbol, y: Symbol) => if (x == 'z) y else x)(elems:_*) + val fr = FoldRight('z, (x: Symbol, y: Symbol) => if (y == 'z) x else y)(elems:_*) + val alg = VariableElimination(fl, fr) + alg.start() + alg.probability(fl, 'a) should equal (1.0) + alg.probability(fr, 'b) should equal (1.0) + alg.kill() } - } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/process/ContainerTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/process/ContainerTest.scala deleted file mode 100644 index 4a7cafb0..00000000 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/process/ContainerTest.scala +++ /dev/null @@ -1,127 +0,0 @@ -package com.cra.figaro.test.library.process - -import org.scalatest.WordSpec -import org.scalatest.Matchers -import com.cra.figaro.library.process._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import com.cra.figaro.algorithm.factored.VariableElimination -import com.cra.figaro.algorithm.sampling.Importance -import com.cra.figaro.library.compound._ - -class ContainerTest extends WordSpec with Matchers { - "A Container" should { - "check range correctly" in { - Universe.createNew() - val proc = createContainer(List(2,3)) - proc.rangeCheck(2) should equal (true) - proc.rangeCheck(1) should equal (false) - } - - "when mapping, have each point mapped according to the function" in { - Universe.createNew() - val proc = createContainer(List(2,3)).map(!_) - val elem = proc(3) - VariableElimination.probability(elem, true) should be ((2.0 / 3) +- 0.000000001) - } - - "when chaining, have each point flatMapped according to the function" in { - Universe.createNew() - val proc = createContainer(List(2,3)).chain(if (_) Flip(0.3) else Flip(0.6)) - val elem = proc(3) - VariableElimination.probability(elem, true) should be ((1.0 / 3 * 0.3 + 2.0 / 3 * 0.6) +- 0.000000001) - } - - "when appending, have all the elements of both processes, with the second process replacing the first when necessary" in { - Universe.createNew() - val proc1 = createContainer(List(2,3)) - val proc2 = createContainer(List(3,4), true) - val proc = proc1 ++ proc2 - val elem2 = proc(2) - val elem3 = proc(3) - val elem4 = proc(4) - an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } - val alg = VariableElimination(elem2, elem3, elem4) - alg.start() - alg.probability(elem2, true) should be (0.5 +- 0.000001) - alg.probability(elem3, true) should be (2.0 / 3.0 +- 0.00000001) // inverted - alg.probability(elem4, true) should be (3.0 / 4.0 +- 0.00000001) // inverted - alg.kill() - } - - "when folding or reducing, have the points folded according to the function" in { - Universe.createNew() - val proc = createContainer(List(2,3)) - val elem1 = proc.foldLeft(true)((b1: Boolean, b2: Boolean) => b1 && b2) - val elem2 = proc.foldRight(true)((b1: Boolean, b2: Boolean) => b1 && b2) - val elem3 = proc.reduce((b1: Boolean, b2: Boolean) => b1 && b2) - val elem4 = proc.aggregate(true)((b1: Boolean, b2: Boolean) => !b1 || b2, (b1: Boolean, b2: Boolean) => b1 && b2) - val alg = Importance(10000, elem1, elem2, elem3, elem4) - alg.start() - alg.probability(elem1, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) - alg.probability(elem2, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) - alg.probability(elem3, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) - alg.probability(elem4, true) should be (((1.0 / 2.0) * (1.0 / 3.0)) +- 0.02) - alg.kill() - } - - "when quantifying over elements satisfying a predicate, produce the right answer" in { - Universe.createNew() - val proc = createContainer(List(2,3)) - val elem1 = proc.count((b: Boolean) => b) - val elem2 = proc.exists((b: Boolean) => b) - val elem3 = proc.forall((b: Boolean) => b) - val alg = Importance(10000, elem1, elem2, elem3) - alg.start() - val p0 = 1.0 / 2 * 2.0 / 3 - val p1 = 1.0 / 2 * 1.0 / 3 + 1.0 / 2 * 2.0 / 3 - val p2 = 1.0 / 2 * 1.0 / 3 - alg.probability(elem1, 0) should be (p0 +- 0.02) - alg.probability(elem1, 1) should be (p1 +- 0.02) - alg.probability(elem1, 2) should be (p2 +- 0.02) - alg.probability(elem2, true) should be ((p1 + p2) +- 0.02) - alg.probability(elem3, true) should be (p2 +- 0.02) - alg.kill() - } - - "when finding the index of the first element, produce the right answer" in { - Universe.createNew() - val proc = createContainer(List(2,3)) - val elem = proc.findIndex((b: Boolean) => b) - val alg = Importance(10000, elem) - alg.start() - val p2 = 1.0 / 2.0 - val p3 = (1 - p2) * 1.0 / 3.0 - val pNone = 1 - p2 - p3 - alg.probability(elem, Some(2)) should be (p2 +- 0.02) - alg.probability(elem, Some(3)) should be (p3 +- 0.02) - alg.probability(elem, None) should be (pNone +- 0.02) - alg.kill() - } - - } - - def createContainer(is: List[Int], invert: Boolean = false): Container[Int, Boolean] = new Container[Int, Boolean] { - val indices = is - def generate(index: Int) = if (invert) Flip(1.0 - 1.0 / index) else Flip(1.0 / index) - def generate(indices: List[Int]) = { - val unary = for { - index <- indices - } yield (index, generate(index)) - val map = Map(unary:_*) - val binary = - for { - index1 <- indices - index2 <- indices - if index1 < index2 - } yield { - val elem1 = map(index1) - val elem2 = map(index2) - val pair = ^^(elem1, elem2) - pair.addConstraint((pair: (Boolean, Boolean)) => if (pair._1 != pair._2) 1.0 / (index1 + index2) else 1.0) - pair - } - Map(unary:_*) - } - } -} \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/process/ProcessTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/process/ProcessTest.scala deleted file mode 100644 index 7c1638f5..00000000 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/process/ProcessTest.scala +++ /dev/null @@ -1,374 +0,0 @@ -package com.cra.figaro.test.library.process - -import org.scalatest.WordSpec -import org.scalatest.Matchers -import com.cra.figaro.library.process._ -import com.cra.figaro.language._ -import com.cra.figaro.util._ -import com.cra.figaro.algorithm.factored.VariableElimination -import com.cra.figaro.algorithm.sampling.Importance -import com.cra.figaro.library.compound._ - -class ProcessTest extends WordSpec with Matchers { - "A process" should { - "get the right element for an index in range" in { - Universe.createNew() - val proc = createProcess(List(2,3)) - val elem = proc(2) - val alg = VariableElimination(elem) - alg.start() - alg.probability(elem, true) should be (0.5 +- 0.0000000001) - alg.kill() - } - - "get the right elements for multiple indices in range" in { - Universe.createNew() - val proc = createProcess(List(2,3)) - val elems = proc(List(2,3)) - val alg = VariableElimination(elems(2)) - alg.start() - val q23 = (1.0 / 2) * (1.0 / 3) - val q2not3 = (1.0 / 2) * (2.0 / 3) * (1.0 / 5) - val qnot23 = (1.0 / 2) * (1.0 / 3) * (1.0 / 5) - val qnot2not3 = (1.0 / 2) * (2.0 / 3) - val q2 = q23 + q2not3 - val qnot2 = qnot23 + qnot2not3 - val p2 = q2 / (q2 + qnot2) - alg.probability(elems(2), true) should be (p2 +- 0.0000000001) - alg.kill() - } - - "throw IndexOutOfRange exception for an index out of range" in { - Universe.createNew() - val proc = createProcess(List(2,3)) - an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } - an [proc.IndexOutOfRangeException] should be thrownBy { proc(List(2,1)) } - } - - "get the right optional element for an index in or out of range" in { - Universe.createNew() - val proc = createProcess(List(2,3)) - val elem1 = proc.get(2) - val elem2 = proc.get(1) - val alg = VariableElimination(elem1, elem2) - alg.start() - alg.probability(elem1, Some(true)) should be (0.5 +- 0.00000001) - alg.probability(elem2, None) should be (1.0 +- 0.000000001) - alg.kill() - } - - "get the right optional elements for multiple indices in or out of range" in { - Universe.createNew() - val proc = createProcess(List(2,3)) - val elems = proc.get(List(2, 1)) - val alg = VariableElimination(elems(1), elems(2)) - alg.start() - alg.probability(elems(2), Some(true)) should be (0.5 +- 0.00000001) - alg.probability(elems(1), None) should be (1.0 +- 0.000000001) - alg.kill() - } - - "when mapping, have each point mapped according to the function" in { - Universe.createNew() - val proc = createProcess(List(2,3)).map(!_) - val elem = proc(3) - val alg = VariableElimination(elem) - alg.start() - alg.probability(elem, true) should be ((2.0 / 3) +- 0.000000001) - alg.kill() - } - - "when chaining, have each point flatMapped according to the function" in { - Universe.createNew() - val proc = createProcess(List(2,3)).chain(if (_) Flip(0.3) else Flip(0.6)) - val elem = proc(3) - val alg = VariableElimination(elem) - alg.start() - alg.probability(elem, true) should be ((1.0 / 3 * 0.3 + 2.0 / 3 * 0.6) +- 0.000000001) - alg.kill() - } - - "when appending, have all the elements of both processes, with the second process replacing the first when necessary" in { - Universe.createNew() - val proc1 = createProcess(List(2,3)) - val proc2 = createProcess(List(3,4), true) - val proc = proc1 ++ proc2 - val elem2 = proc(2) - val elem3 = proc(3) - val elem4 = proc(4) - an [proc.IndexOutOfRangeException] should be thrownBy { proc(1) } - val alg = VariableElimination(elem2, elem3, elem4) - alg.start() - alg.probability(elem2, true) should be (0.5 +- 0.000001) - alg.probability(elem3, true) should be (2.0 / 3.0 +- 0.00000001) // inverted - alg.probability(elem4, true) should be (3.0 / 4.0 +- 0.00000001) // inverted - alg.kill() - } - } - /* - "A fixed independent array" should { - "get the correct elements at individual points" in { - Universe.createNew() - val array = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val elem0 = array(0) - val elem3 = array(3) - assert (elem0.isInstanceOf[AtomicFlip]) - elem0.asInstanceOf[AtomicFlip].prob should be(1.0 +- 0.000000000001) - assert (elem3.isInstanceOf[AtomicFlip]) - elem3.asInstanceOf[AtomicFlip].prob should be(0.25 +- 0.00000000001) - } - - "range check an element" in { - Universe.createNew() - val array = new FixedIndependentArray(4, (i: Int) => Flip(random.nextDouble)) - an [array.IndexOutOfRangeException] should be thrownBy { array(-1) } - an [array.IndexOutOfRangeException] should be thrownBy { array(4) } - } - - "safely get an optional element using get" in { - Universe.createNew() - val array = new FixedIndependentArray(4, (i: Int) => Constant(i)) - val elem0 = array.get(0) - val elem4 = array.get(4) - val alg0 = VariableElimination(elem0) - alg0.start() - alg0.probability(elem0, Some(0)) should be (1.0 +- 0.0000001) - alg0.kill() - val alg4 = VariableElimination(elem4) - alg4.start() - alg4.probability(elem4, None) should be (1.0 +- 0.0000001) - alg4.kill() - } - - "when mapping, have each point mapped according to the function" in { - Universe.createNew() - val array1 = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2 = array1.map((b: Boolean) => !b) - val elem0 = array2(0) - val elem3 = array2(3) - val algorithm = VariableElimination(elem0, elem3) - algorithm.start() - algorithm.probability(elem0, true) should be (0.0 +- 0.000000001) - algorithm.probability(elem3, true) should be (0.75 +- 0.000000001) - algorithm.kill() - } - - "when chaining, have each point mapped according to the function" in { - Universe.createNew() - val array1 = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2 = array1.chain((b: Boolean) => if (b) Constant(1); else Constant(2)) - val elem0 = array2(0) - val elem3 = array2(3) - val algorithm = VariableElimination(elem0, elem3) - algorithm.start() - algorithm.probability(elem0, 2) should be (0.0 +- 0.000000001) - algorithm.probability(elem3, 2) should be (0.75 +- 0.000000001) - algorithm.kill() - } - - "when folding or reducing, have the points folded according to the function" in { - Universe.createNew() - val array1 = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val elem2 = array1.foldLeft(true)((b1: Boolean, b2: Boolean) => b1 && b2) - val elem3 = array1.foldRight(true)((b1: Boolean, b2: Boolean) => b1 && b2) - val elem4 = array1.reduce((b1: Boolean, b2: Boolean) => b1 && b2) - val alg2 = Importance(10000, elem2) - alg2.start() - alg2.probability(elem2, true) should be (((1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg2.kill() - val alg3 = Importance(10000, elem3) - alg3.start() - alg3.probability(elem3, true) should be (((1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg3.kill() - val alg4 = Importance(10000, elem4) - alg4.start() - alg4.probability(elem4, true) should be (((1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg4.kill() - } - } - * - */ - /* - "A process element" should { - "create an element over values at a particular index when calling get" in { - Universe.createNew() - val array1: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem = new ContainerElement[Int, Boolean](switch) - val elem1 = procElem.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(true)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - - "map a function pointwise through the process" in { - val array1: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem1 = new ContainerElement[Int, Boolean](switch) - val procElem2 = procElem1.map((b: Boolean) => !b) - val elem1 = procElem2.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(false)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - - "chain a function pointwise through the process" in { - val array1: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: Container[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem1 = new ContainerElement[Int, Boolean](switch) - val procElem2 = procElem1.chain((b: Boolean) => if (b) Constant(1); else Constant(2)) - val elem1 = procElem2.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(1)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - } -*/ - /* - "A finite process element" should { - "create an element over values at a particular index when calling get" in { - Universe.createNew() - val array1: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem = new FiniteContainerElement[Int, Boolean](switch) - val elem1 = procElem.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(true)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - - "map a function pointwise through the process" in { - Universe.createNew() - val array1: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem1 = new FiniteContainerElement[Int, Boolean](switch) - val procElem2 = procElem1.map((b: Boolean) => !b) - val elem1 = procElem2.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(false)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - - "chain a function pointwise through the process" in { - Universe.createNew() - val array1: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem1 = new FiniteContainerElement[Int, Boolean](switch) - val procElem2 = procElem1.chain((b: Boolean) => if (b) Constant(1); else Constant(2)) - val elem1 = procElem2.get(3) - val alg = VariableElimination(elem1) - alg.start() - alg.probability(elem1, Some(1)) should be ((0.2 * 1.0 / 4.0 + 0.8 * 3.0 / 4.0) +- 0.0000000001) - alg.kill() - } - - "fold and reduce pointwise through the process" in { - Universe.createNew() - val array1: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 / (i + 1))) - val array2: FiniteContainer[Int, Boolean] = new FixedIndependentArray(4, (i: Int) => Flip(1.0 - 1.0 / (i + 1))) - val switch = Select(0.2 -> array1, 0.8 -> array2) - val procElem = new FiniteContainerElement[Int, Boolean](switch) - val elem2 = procElem.foldLeft(true, (b1: Boolean, b2: Boolean) => b1 && b2) - val elem3 = procElem.foldRight(true, (b1: Boolean, b2: Boolean) => b1 && b2) - val elem4 = procElem.reduce((b1: Boolean, b2: Boolean) => b1 && b2) - val alg2 = Importance(10000, elem2) - alg2.start() - // With probability 0.8, array2 is chosen, which are never all true because array2(0) is Flip(0) - alg2.probability(elem2, true) should be ((0.2 * (1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg2.kill() - val alg3 = Importance(10000, elem3) - alg3.start() - // With probability 0.8, array2 is chosen, which are never all true because array2(0) is Flip(0) - alg3.probability(elem3, true) should be ((0.2 * (1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg3.kill() - val alg4 = Importance(10000, elem4) - alg4.start() - // With probability 0.8, array2 is chosen, which are never all true because array2(0) is Flip(0) - alg4.probability(elem4, true) should be ((0.2 * (1.0 / 1.0) * (1.0 / 2.0) * (1.0 / 3.0) * (1.0 / 4.0)) +- 0.01) - alg4.kill() - } - } - */ - /* - "Making an array of independent elements of variable size" should { - "have the correct size distribution" in { - Universe.createNew() - val numItems = Select(0.4 -> 1, 0.6 -> 2) - def generator(i: Int): Element[Int] = Select(0.2 -> (i + 1), 0.8 -> (i + 2)) - val array = VariableSizeArray(numItems, generator) - val count = array.foldLeft(0, (i1: Int, i2: Int) => i1 + 1) - val alg = Importance(1000, count) - alg.start() - alg.probability(count, 1) should be (0.4 +- 0.05) - alg.probability(count, 2) should be (0.6 +- 0.05) - } - - "have the correct distribution over an element" in { - Universe.createNew() - val numItems = Select(0.4 -> 1, 0.6 -> 2) - def generator(i: Int): Element[Int] = Select(0.2 -> (i + 1), 0.8 -> (i + 2)) - val array = VariableSizeArray(numItems, generator) - val elem1 = array.get(1) - val alg = Importance(1000, elem1) - alg.start() - alg.probability(elem1, None) should be (0.4 +- 0.05) - alg.probability(elem1, Some(2)) should be (0.6 * 0.2 +- 0.05) - alg.probability(elem1, Some(3)) should be (0.6 * 0.8 +- 0.05) - } - - "have the correct aggregate" in { - Universe.createNew() - val numItems = Select(0.4 -> 1, 0.6 -> 2) - def generator(i: Int): Element[Int] = Select(0.2 -> (i + 1), 0.8 -> (i + 2)) - val array = VariableSizeArray(numItems, generator) - val total = array.reduce(_ + _) - val alg = Importance(1000, total) - alg.start() - val p1 = 0.4 * 0.2 - val p2 = 0.4 * 0.8 - val p12 = 0.6 * 0.2 * 0.2 - val p13 = 0.6 * 0.2 * 0.8 - val p22 = 0.6 * 0.8 * 0.2 - val p23 = 0.6 * 0.8 * 0.8 - val answer = p1 * 1 + p2 * 2 + p12 * 3 + p13 * 4 + p22 * 4 + p23 * 5 - alg.expectation(total, (i: Int) => i.toDouble) should be (answer +- 0.15) - } - } - * - */ - - def createProcess(indices: List[Int], invert: Boolean = false): Process[Int, Boolean] = new Process[Int, Boolean] { - def rangeCheck(index: Int) = indices.contains(index) - def generate(index: Int) = if (invert) Flip(1.0 - 1.0 / index) else Flip(1.0 / index) - def generate(indices: List[Int]) = { - val unary = for { - index <- indices - } yield (index, generate(index)) - val map = Map(unary:_*) - val binary = - for { - index1 <- indices - index2 <- indices - if index1 < index2 - } yield { - val elem1 = map(index1) - val elem2 = map(index2) - val pair = ^^(elem1, elem2) - pair.addConstraint((pair: (Boolean, Boolean)) => if (pair._1 != pair._2) 1.0 / (index1 + index2) else 1.0) - pair - } - Map(unary:_*) - } - } -} \ No newline at end of file diff --git a/FigaroExamples/META-INF/MANIFEST.MF b/FigaroExamples/META-INF/MANIFEST.MF index 10d2312e..bf4c9d63 100644 --- a/FigaroExamples/META-INF/MANIFEST.MF +++ b/FigaroExamples/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: FigaroExamples Bundle-SymbolicName: com.cra.figaro.examples -Bundle-Version: 2.5.0 -Require-Bundle: com.cra.figaro;bundle-version="2.5.0", +Bundle-Version: 3.0.0 +Require-Bundle: com.cra.figaro;bundle-version="3.0.0", org.scala-lang.scala-library Bundle-Vendor: Charles River Analytics Bundle-RequiredExecutionEnvironment: JavaSE-1.6 diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/LazyList.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/LazyList.scala index bc710ecc..392be26c 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/LazyList.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/LazyList.scala @@ -1,3 +1,16 @@ +/* + * LazyList.scala + * A lazy list. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Feb 26, 2014 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + package com.cra.figaro.example import com.cra.figaro.language._ @@ -7,11 +20,11 @@ import com.cra.figaro.algorithm.lazyfactored.LazyValues object LazyList { val universe = Universe.createNew() - + class L case object Empty extends L case class Cons(head: Element[Symbol], tail: Element[L]) extends L - + def contains(target: Symbol, el: Element[L]): Element[Boolean] = { Chain(el, (l: L) => { l match { @@ -24,20 +37,18 @@ object LazyList { def generate(): Element[L] = { Apply(Flip(0.5), (b: Boolean) => if (b) Empty; else Cons(Select(0.6 -> 'a, 0.4 -> 'b), generate())) } - + def main(args: Array[String]) { - + val el = generate() val cb = contains('b, el) val ca = contains('a, el) ca.observe(true) val alg = new LazyVariableElimination(cb) - + println("DEPTH " + 1) alg.start() println(alg.currentResult.toReadableString) - //alg.debug = true - //LazyValues.debug = true for { i <- 2 to 20 } { println("DEPTH " + i) alg.pump() @@ -46,4 +57,4 @@ object LazyList { println("Correct probability of true is " + (3.0 / 7.0)) } -} \ No newline at end of file +} diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/MultiValuedReferenceUncertainty.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/MultiValuedReferenceUncertainty.scala index e815e164..509c315c 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/MultiValuedReferenceUncertainty.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/MultiValuedReferenceUncertainty.scala @@ -1,13 +1,13 @@ /* * MultiValuedReferenceUncertainty.scala * A simple model example with multi-valued reference uncertainty and aggregates. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/MutableMovie.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/MutableMovie.scala index 09a7866a..72647289 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/MutableMovie.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/MutableMovie.scala @@ -1,13 +1,13 @@ /* * MutableMovie.scala * A probabilistic relational model example with multi-valued attributes. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -17,6 +17,7 @@ import com.cra.figaro.algorithm.factored._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.language._ import com.cra.figaro.library.compound._ +import com.cra.figaro.library.collection.Container import com.cra.figaro.library.atomic.discrete._ import com.cra.figaro.util @@ -31,15 +32,19 @@ object MutableMovie { lazy val skillful = Flip(0.1) - lazy val famous = Flip(Apply(Inject(movies.map(_.quality): _*), probFamous _)) + lazy val qualities = Container(movies.map(_.quality):_*) - private def probFamous(qualities: Seq[Symbol]) = if (qualities.count(_ == 'high) >= 2) 0.8; else 0.1 + lazy val numGoodMovies = qualities.count(_ == 'high) + + lazy val famous = Chain(numGoodMovies, (n: Int) => if (n >= 2) Flip(0.8) else Flip(0.1)) } private class Movie { var actors: List[Actor] = List() - lazy val actorsAllGood = Apply(Inject(actors.map(_.skillful): _*), (s: Seq[Boolean]) => !(s.contains(false))) + lazy val skills = Container(actors.map(_.skillful):_*) + + lazy val actorsAllGood = skills.exists(b => b) lazy val probLow = Apply(actorsAllGood, (b: Boolean) => if (b) 0.2; else 0.5) diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverse.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverse.scala index c3e12b43..a9675fac 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverse.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverse.scala @@ -1,13 +1,13 @@ /* * OpenUniverse.scala * An example of open universe reasoning. - * + * * Created By: Avi Pfeffer (apfeffer@cra.com) * Creation Date: Jan 1, 2009 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -18,6 +18,7 @@ import com.cra.figaro.language._ import com.cra.figaro.library.atomic.continuous.{ Uniform, Normal } import com.cra.figaro.library.atomic.discrete.{ Geometric, FromRange } import com.cra.figaro.library.compound._ +import com.cra.figaro.library.collection._ import com.cra.figaro.language.Universe._ import com.cra.figaro.util._ @@ -27,9 +28,11 @@ import com.cra.figaro.util._ object OpenUniverse { createNew() - private def source(): Element[Double] = Uniform(0.0, 1.0) + private def source(): Element[Double] = { + Uniform(0.0, 1.0) + } - private val numSources = Geometric(0.9) + private val numSources = FromRange(1,4) private val sources = MakeList(numSources, source _) @@ -48,17 +51,18 @@ object OpenUniverse { def chooseScheme(): ProposalScheme = DisjointScheme( - (0.25, () => ProposalScheme(numSources)), - (0.25, () => ProposalScheme(sources.items(random.nextInt(numSources.value)))), - (0.25, () => ProposalScheme(samples(random.nextInt(numSamples)).sourceNum)), - (0.25, () => ProposalScheme(samples(random.nextInt(numSamples)).position.resultElement))) + (0.1, () => ProposalScheme(numSources)), + (0.9, () => ProposalScheme.default)) def main(args: Array[String]) = { sample1.position.addCondition((y: Double) => y >= 0.7 && y < 0.8) sample2.position.addCondition((y: Double) => y >= 0.7 && y < 0.8) - val alg = MetropolisHastings(100000, chooseScheme, 5000, equal) + val alg = MetropolisHastings(chooseScheme, 5000, equal) alg.start() + Thread.sleep(10000) + alg.stop() println(alg.probability(equal, true)) + println(alg.getSampleCount + " samples taken") alg.kill } } diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverseLearning.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverseLearning.scala index 484752e3..963643bd 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverseLearning.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/OpenUniverseLearning.scala @@ -131,7 +131,7 @@ object OpenUniverseLearning { for (datum <- trainingSet) observe(new LearningModel, datum) val time0 = System.currentTimeMillis() - val algorithm = EM.withVE(numEMIterations, betaContinue1, betaContinue2, betaObserve1, betaObserve2) + val algorithm = EMWithVE(numEMIterations, betaContinue1, betaContinue2, betaObserve1, betaObserve2) algorithm.start() val resultUniverse = new Universe diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/SimpleLearning.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/SimpleLearning.scala index 8c43fd2e..a27499c2 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/SimpleLearning.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/SimpleLearning.scala @@ -241,7 +241,7 @@ object SimpleLearning { def learner(parameters: Parameters): Algorithm = { parameters match { - case ps: LearnableParameters => EM.withVE(numEMIterations, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) + case ps: LearnableParameters => EMWithVE(numEMIterations, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) case _ => throw new IllegalArgumentException("Not learnable parameters") } } diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/Protein.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/Protein.scala index 7991250b..2dc8ec79 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/Protein.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/Protein.scala @@ -1,13 +1,13 @@ /* * Protein.scala * A protein class, which is an Element[AminoAcidSequence]. - * + * * Created By: Brian Ruttenberg (bruttenberg@cra.com) * Creation Date: Oct 1, 2012 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ @@ -16,33 +16,28 @@ package com.cra.figaro.example.dosage import com.cra.figaro.language._ import com.cra.figaro.library.atomic.discrete._ import com.cra.figaro.example.dosage.Conversion._ +import com.cra.figaro.library.collection._ /** - * A protein class, which is an Element[AminoAcidSequence]. + * A protein is an Element[AminoAcidSequence]. * Given an input string, the protein represents a distribution over * the possible AminoAcidSequences that can be generated from the string */ -class Protein(name: Name[AminoAcidSequence], arg1: String, collection: ElementCollection) - extends Apply1[List[AminoAcidSequence], AminoAcidSequence](name, Protein.genInjectFcn(arg1), Protein.genApplyFcn, collection) - object Protein { - - def genInjectFcn = (s: String) => genFcn(s) - def genApplyFcn = (l: List[AminoAcidSequence]) => (AminoAcidSequence("") /: l)(_ + _) + def genApplyFcn = (l: Container[Int, AminoAcidSequence]) => l.foldLeft(AminoAcidSequence(""))(_ + _) /* A '-' means any AA, so this is a uniform distribution over existing AA. A * named AA is represented as a constant */ - def genFcn(s: String): Inject[AminoAcidSequence] = { + def genFcn(s: String): Container[Int, AminoAcidSequence] = { val elems = s.map { c => c match { case '-' => Uniform(aaListAsSeq: _*) case _ => Constant(AminoAcidSequence(c.toString)) } } - Inject(elems: _*) + Container(elems: _*) } - def apply(arg: String)(implicit name: Name[AminoAcidSequence], collection: ElementCollection) = - new Protein(name, arg, collection) + def apply(arg: String) = genApplyFcn(genFcn(arg)) } diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/ScoringMatrix.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/ScoringMatrix.scala index 44c9414f..9948b1a3 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/ScoringMatrix.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/dosage/ScoringMatrix.scala @@ -1,13 +1,13 @@ /* * ScoringMatrix.scala * A scoring matrix. - * + * * Created By: Brian Ruttenberg (bruttenberg@cra.com) * Creation Date: Oct 1, 2012 - * + * * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. * See http://www.cra.com or email figaro@cra.com for information. - * + * * See http://www.github.com/p2t2/figaro for a copy of the software license. */ diff --git a/FigaroLaTeX/Tutorial/FigaroTutorial.pdf b/FigaroLaTeX/Tutorial/FigaroTutorial.pdf index 4c5e555a..d504e63c 100644 Binary files a/FigaroLaTeX/Tutorial/FigaroTutorial.pdf and b/FigaroLaTeX/Tutorial/FigaroTutorial.pdf differ diff --git a/FigaroLaTeX/Tutorial/Sections/10Elements.tex b/FigaroLaTeX/Tutorial/Sections/10Elements.tex index 6176472c..1c8495cb 100644 --- a/FigaroLaTeX/Tutorial/Sections/10Elements.tex +++ b/FigaroLaTeX/Tutorial/Sections/10Elements.tex @@ -10,7 +10,7 @@ \chapter{Creating a new element class} % Chapter title \section{Creating an atomic class with inheritance} -The easiest way to create a new class is to inherit from an existing class. For example, a discrete uniform distribution is just a special case of a discrete selection where every element has the same probability. We can create this element class simply with +The easiest way to create a new class is to inherit from an existing class. For example, a discrete uniform distribution is just a special case of a discrete selection where every element has the same probability. We can create this element class simply with: \begin{flushleft} \texttt{class AtomicUniform[T](name: Name[T], options: Seq[T], collection: \newline ElementCollection) extends @@ -22,22 +22,19 @@ \section{Creating an atomic class with inheritance} } \end{flushleft} -The atomic uniform class is one for which the options are explicitly specified values of type \texttt{T}, as opposed to the compound uniform in which the options are elements over values of type \texttt{T}. The atomic uniform class takes three arguments: a name (which every class takes), an element collection (likewise), and a sequence specifying the options the uniform distribution can produce. The class inherits the \texttt{AtomicSelect} class, which represents selection over a discrete set of options - -with their associated probabilities. There is also one other trait that is extended in the \texttt{AtomicUniform}, +The atomic uniform class is one for which the options are explicitly specified values of type \texttt{T}, as opposed to the compound uniform in which the options are elements over values of type \texttt{T}. The atomic uniform class takes three arguments: a name (which every class takes), an element collection (likewise), and a sequence specifying the options the uniform distribution can produce. The class inherits the \texttt{AtomicSelect} class, which represents selection over a discrete set of options with their associated probabilities. There is also one other trait that is extended in the \texttt{AtomicUniform}, \texttt{Cacheable[T]}. This trait is used to determine what type of chain should be created at the chain instantiation time. If the parent of a \texttt{Chain} extends \texttt{Cacheable}, a \texttt{CachingChain} is instantiated when a chain element is created. This trait is not required (elements by default are assumed to be not cacheable), but can result in increased performance if the support of the new element is small. -To carry out the inheritance, we need to transform the sequence of options into a list of (probability,value) pairs, which are the argument to \texttt{AtomicSelect}. This is accomplished by the expression \texttt{options.toList map (1.0 -> \_)}. This turns the sequence of options into a list and applies to all elements of the list the function that maps an option to the pair \texttt{(1.0, option)}. +To carry out the inheritance, we need to transform the sequence of options into a list of (probability, value) pairs, which are the argument to \texttt{AtomicSelect}. This is accomplished by the expression \texttt{options.toList map (1.0 -> \_)}. This turns the sequence of options into a list and applies to all elements of the list the function that maps an option to the pair \texttt{(1.0, option)}. Let us understand the notation \texttt{(1.0 -> \_)}. This is Scala shorthand for the function which maps an option to the pair \texttt{(1.0, option)}. There are two things in this shorthand worth noting. First, \texttt{1.0 -> \_} is another way of describing the pair \texttt{(1.0, \_)}. It is a more descriptive way of saying "with probability 1.0, you get \_," rather than just "the pair of 1.0 and \_." Second, \_ denotes the argument to the function, when you know you are defining a function. Here, you know you are defining a function because it - \marginpar{Scala contains many transformations on sequences besides \texttt{toList}} appears in the context of applying a function to all elements of a list. This underscore notation can only be used when the argument appears exactly once in the body of the function. Thus \texttt{(1.0 -> \_)} is Scala's shorthand for the function \texttt{(t: T) => (1.0, t)}. It really doesn't matter if this shorthand is meaningful to you; feel free to use the longer version wherever you want. Note that the probabilities in the \texttt{AtomicSelect} are not normalized; \texttt{AtomicSelect} automatically takes care of the normalization. The only thing the body of \texttt{AtomicUniform} does is to override the \texttt{toString} method that every Scala class has. The method produces something meaningful when the element is converted into a string. \texttt{options.mkString(", ")} creates a string consisting of each of the options separated by a comma and a space. -A problem with the above class definition is that to create an instance, you have to say +A problem with the above class definition is that to create an instance, you have to say: \begin{flushleft} \texttt{new AtomicUniform(name, options, collection)} @@ -63,7 +60,7 @@ \section{Creating an atomic class with inheritance} Second, a method named \texttt{apply} is special. It can be invoked simply by providing the name of the object and listing its arguments in parentheses. So instead of saying \texttt{Uniform.apply(options)}, you can say \texttt{Uniform(options)}. Methods named \texttt{apply} are often used for defining factory constructors. -Third, Scala allows \emph{curried functions}. These are functions that can be applied to one set of arguments to yield a function that can be applied to more arguments. Scala indicates this by providing multiple argument lists to a function. So, in our example, the first argument list consists of the sequence of options,while the second consists of the name and element collection. +Third, Scala allows \emph{curried functions}. These are functions that can be applied to one set of arguments to yield a function that can be applied to more arguments. Scala indicates this by providing multiple argument lists to a function. So, in our example, the first argument list consists of the sequence of options, while the second consists of the name and element collection. Finally, the second argument list to \texttt{apply} is \emph{implicit}. This means that you can leave out the argument list and Scala will implicitly fill it in with special values defined elsewhere. In this case, "" is the implicit value of type \texttt{Name} and the current universe is the implicit value of type \texttt{ElementCollection}. This is why you don't have to supply these arguments when you create an element unless you explicitly want to specify a different name or element collection. @@ -98,7 +95,7 @@ \subsection{Inheriting from Chain} \subsection{Inheriting from Apply} -Inheriting from \texttt{Apply} will typically be used when you want to create an element class that captures a common function. When you inherit from \texttt{Apply}, you have to explicitly inherit from the \texttt{Apply} class that has the right number of arguments. For example, if your function has two arguments, you inherit from \texttt{Apply2}. For example, the element class that represents the comparison of the values of two elements for equality is defined by +Inheriting from \texttt{Apply} will typically be used when you want to create an element class that captures a common function. When you inherit from \texttt{Apply}, you have to explicitly inherit from the \texttt{Apply} class that has the right number of arguments. For example, if your function has two arguments, you inherit from \texttt{Apply2}. For example, the element class that represents the comparison of the values of two elements for equality is defined by: \begin{flushleft} \texttt{class Eq[T](name: Name[Boolean], arg1: Element[T], arg2: Element[T], \newline collection: ElementCollection) extends Apply2(name, arg1, arg2, (t1: T, t2: T) => t1 == t2, collection) \{ @@ -129,10 +126,11 @@ \section{Creating an atomic class without inheritance} \newline class AtomicUniform(name: Name[Double], val lower: Double, val upper: Double, collection: ElementCollection) \newline extends Element[Double](name, collection) with Atomic[Double] \{ -\newline \tab type Randomness = Double val diff = upper $-$ lower +\newline \tab type Randomness = Double +\newline \tab val diff = upper $-$ lower \newline -\newline \tab def generateRandomness() = random.nextDouble() * diff + lower def \newline -\newline \tab generateValue(rand: Randomness) = rand +\newline \tab def generateRandomness() = random.nextDouble() * diff + lower +\newline \tab def generateValue(rand: Randomness) = rand \newline \newline \tab val constantDensity = 1.0 / diff \newline @@ -146,7 +144,7 @@ \section{Creating an atomic class without inheritance} This should be self-explanatory given everything we've seen so far. In this class, we defined \texttt{generateRandomness} to actually produce the value, and \texttt{generateValue} to simply pass it along, but a different design would have been possible. For instance, an atomic normal distribution would compute its randomness value using the standard normal distribution, and the value of the element would be the randomness shifted by the mean and scaled by the variance. For other atomic non-deterministic classes, the logic of the methods would be richer, but the general structure would be the same. Note that the \texttt{generateRandomness} function uses the Figaro random number generator called \texttt{random} to generate random values. One can use any random number generator to generate the randomness of an element. However, using the Figaro supplied random number generator allows one to globally set the seed of the generator in the Figaro package, thus enabling reproducible random processes. -\section{Creating an compound class without inheritance} +\section{Creating a compound class without inheritance} Creating a compound class without inheritance is unusual, as \texttt{Chain} and \texttt{Apply} are ubiquitous. The most common use will probably be to create variants of \texttt{Chain} and \texttt{Apply} that take more arguments than the built-in classes. To do that, you should take the code for \texttt{Chain} or \texttt{Apply} as a model and base your new class on that. Otherwise, for a deterministic compound class, you need to define the following elements: @@ -154,14 +152,14 @@ \section{Creating an compound class without inheritance} \item The \texttt{args} method that returns a list of the elements on which this element depends. Make sure this is a \texttt{def}, not a \texttt{val}. (Otherwise, you might run into a nasty Scala issue with abstract fields in a superclass being initialized in a concrete subclass. When an instance of the subclass is constructed, the superclass instance is constructed first, and a superclass of all element classes is the \texttt{Element} class, which uses \texttt{args} in its constructor. If \texttt{args} were a \texttt{val}, it would be uninitialized at that time and throw a null pointer exception.) \item The \texttt{generateValue} method that takes no arguments and produces the value of the element as a function of the values of the arguments of the element and its randomness. -For example, \texttt{Apply1} is defined by +For example, \texttt{Apply1} is defined by: \end{itemize} \begin{flushleft} \texttt{class Apply1[T1,U](name: Name[U], val arg1: Element[T1], val fn: T1 => U, collection: ElementCollection) -\newline extends Deterministic[U](name, collection) \{ +\newline extends Apply[U](name, collection) \{ \newline \tab def args: List[Element[\_]] = List(arg1) \newline \newline \tab type Arg1Type = T1 @@ -179,7 +177,7 @@ \section{Making a class usable by variable elimination} Certain algorithms rely on element classes being able to support specific functionality. For example, computing ranges requires that it be possible to enumerate the values of every element in the universe. One way to make a new element class support value enumeration would be to modify the code that enumerates values in \texttt{Values.scala}, This approach would not be modular; it is undesirable for a user to have to modify library code. -Figaro provides a different solution. There is a trait called \texttt{ValuesMa\-ker} that characterizes element classes for which values can be enumerated. If you want your element class to support range computation, make it extend \texttt{ValuesMaker} and have it implement the \texttt{makeValues} method, which produces an enumeration of the possible values of the element. For example, we might want to enumerate the possible values of an atomic binomial element. If n is the number of trials of the binomial, we can define the function +Figaro provides a different solution. There is a trait called \texttt{ValuesMa\-ker} that characterizes element classes for which values can be enumerated. If you want your element class to support range computation, make it extend \texttt{ValuesMaker} and have it implement the \texttt{makeValues} method, which produces an enumeration of the possible values of the element. For example, we might want to enumerate the possible values of an atomic binomial element. If n is the number of trials of the binomial, we can define the function: \begin{flushleft} \texttt{def makeValues: Set[Int] = (for \{ i <- 0 to n \} yield i).toSet} @@ -194,29 +192,29 @@ \section{Making a class usable by variable elimination} For example, the \texttt{AtomicBinomial} class extends \texttt{ProbFactorMaker} and includes the following code: \begin{flushleft} -\texttt{def makeFactors: List[Factor[Double]] = \{ -\newline \tab val binVar = Variable(this) +\texttt{def makeFactors(binomial: AtomicBinomial): List[Factor[Double]] = \{ +\newline \tab val binVar = Variable(binomial) \newline \tab val factor = new Factor[Double](Array(binVar)) \newline \tab for \{ (value, index) <- binVar.range.zipWithIndex \} \{ -\newline \tab factor.set(Array(index), density(value)) +\newline \tab factor.set(List(index), binomial.density(value.value)) \newline \} \newline \tab List(factor) \newline \} } \end{flushleft} -The \texttt{makeFactors} method returns a list of factors. A factor is a table defined over a set of \emph{variables}. To create a variable out of an element, use \texttt{Variable}. For example, the \texttt{Variable(this)} line above creates a variable out of this atomic binomial element. Creating variables is memoized, so you can be sure that every time you call \texttt{Variable} on an element you get the same variable. This is important if an element participates in multiple factors. To create a factor, you pass it an array of its variables. +The \texttt{makeFactors} method returns a list of factors. A factor is a table defined over a set of \emph{variables}. To create a variable out of an element, use \texttt{Variable}. For example, the \texttt{Variable(binomial)} line above creates a variable out of this atomic binomial element. Creating variables is memoized, so you can be sure that every time you call \texttt{Variable} on an element you get the same variable. This is important if an element participates in multiple factors. To create a factor, you pass it an array of its variables. -Each row in a factor associates a value with a set of indices into the variable's ranges. To specify the factor, you need to set these values. This is accomplished with the \texttt{set} method of \texttt{Factor}. In the above example, we have +Each row in a factor associates a value with a set of indices into the variable's ranges. To specify the factor, you need to set these values. This is accomplished with the \texttt{set} method of \texttt{Factor}. In the above example, we have: \begin{flushleft} \texttt{for \{ (value, index) <- binVar.range.zipWithIndex \} \{ -\newline \tab factor.set(Array(index), density(value)) +\newline \tab factor.set(List(index), binomial.density(value.value)) \newline \} } \end{flushleft} -The first line uses a for comprehension to get at pairs of values of the binomial variable together with their index into the range. The standard Scala library method \texttt{zipWithIndex} takes a list and associates each element of the list with its index in the list. For example, \texttt{List("a", "b").zipWithIndex} is \texttt{List(("a", 0), ("b", 1))}. The first argument to \texttt{factor.set} is an array of indices into the ranges of the variables, in the same order as the array used to create the factor. The second argument is the value to associate with those indices. +The first line uses a for comprehension to get at pairs of values of the binomial variable together with their index into the range. The standard Scala library method \texttt{zipWithIndex} takes a list and associates each element of the list with its index in the list. For example, \texttt{List("a", "b").zipWithIndex} is \texttt{List(("a", 0), ("b", 1))}. The first argument to \texttt{factor.set} is an list of indices into the ranges of the variables, in the same order as the array used to create the factor. The second argument is the value to associate with those indices. At the end, \texttt{makeFactors} returns a list consisting of this single factor. This is the basic principle behind creating factors. You can find a variety of more complex examples, including some with multiple variables, in \texttt{ProbFactor.scala} For atomic elements, the process should usually be similarly simple to that for binomials. @@ -224,7 +222,7 @@ \section{Making parameters and parameterized elements} Support for learning models from data is accomplished through parameters and parameterized elements. Defining new elements of these types requires the use of a couple of traits $-$ \texttt{Parameter} and \texttt{Parameter\-ized} $-$ and a method to produce factors. Much of the required code is centered on the idea of sufficient statistics, as they are currently the means by which parameters are learned. -A parameter must extend the \texttt{Parameter} trait. This trait contains several important methods which allow use with the learning algorithms. First, the method \texttt{zeroSufficientStatistics} must provide an appropriate result for this parameter type. For example, a Beta parameter has two hyperparameters, alpha and beta. Hence, \texttt{zeroSufficientStatistics} returns a sequence of length two. +A parameter must extend the \texttt{Parameter} trait. This trait contains several important methods which allow use with the learning algorithms. First, the method \texttt{zeroSufficientStatistics} must provide an appropriate result for this parameter type. For example, a Beta parameter has two hyperparameters, alpha and beta. Hence, \texttt{zeroSuffic\-ientStatistics} returns a sequence of length two. \begin{flushleft} \texttt{override def zeroSufficientStatistics (): Seq[Double] = \{ @@ -235,11 +233,11 @@ \section{Making parameters and parameterized elements} An additional method, \texttt{sufficientStatistics}, provides sufficient statistics with a value of 1.0 in the position specified by an index or value. This method can be useful when creating factors for parameterized elements. The \texttt{parameter} trait also defines a method for calculating the expected value of the parameter. Expected value is used during the parameter learning process, and also as an argument during the creation of learned elements. -We can create a parameterized version of an existing element by extending that type of element and including the \texttt{Parameter} trait. In the case of \texttt{Beta}, we have +We can create a parameterized version of an existing element by extending that type of element and including the \texttt{Parameter} trait. In the case of \texttt{Beta}, we have: \begin{flushleft} -\texttt{class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCollection) extends AtomicBeta(name, a, b, collection) with -Parameter[Double] with ValuesMaker[Double] \{ ... \} +\texttt{class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCollection) Element[Double](name, collection) with Atomic[Double] with +DoubleParameter with ValuesMaker[Double] \{ ... \} } \end{flushleft} @@ -255,8 +253,8 @@ \section{Making parameters and parameterized elements} \begin{flushleft} \texttt{def MAPValue: Double = \{ -\newline \tab if (learnedAlpha + learnedBeta == 2) 0.5 (alpha) / (alpha + beta) -\newline \tab else (learnedAlpha - 1) / ( learnedAlpha + learnedBeta - 2) +\newline \tab if (learnedAlpha + learnedBeta == 2) 0.5 +\newline \tab else (learnedAlpha - 1) / ( learnedAlpha + learnedBeta - 2) \newline \} } \end{flushleft} @@ -266,8 +264,8 @@ \section{Making parameters and parameterized elements} \begin{flushleft} \texttt{def maximize(sufficientStatistics: Seq[Double]) \{ \newline \tab require(sufficientStatistics.size == 2) -\newline \tab alpha = sufficientStatistics(0) + a -\newline \tab beta = sufficientStatistics(1) + b +\newline \tab learnedAlpha = sufficientStatistics(0) + a +\newline \tab learnedBeta = sufficientStatistics(1) + b \newline \} } \end{flushleft} @@ -275,9 +273,9 @@ \section{Making parameters and parameterized elements} Having created a parameter, we may now create an element which uses it. Compound elements which use \texttt{Parameter}s as their arguments are defined by the \texttt{Parameterized} trait. This trait is quite simple and contains only a reference to the element's parameter. Continuing the example, we can create a version of \texttt{Flip} which uses \texttt{Beta} in the following way: \begin{flushleft} -\texttt{class Flip(name: Name[Boolean], override val parameter: AtomicBeta, collection: ElementCollection) +\texttt{class ParameterizedFlip(name: Name[Boolean], override val parameter: AtomicBeta, collection: ElementCollection) \newline extends Element[Boolean](name, collection) with Flip -\newline with Parameterized[Boolean] +\newline with SingleParameterized[Boolean] } \end{flushleft} @@ -290,12 +288,12 @@ \section{Making parameters and parameterized elements} If an existing element is being extended, it is advisable to define a factory method in the companion object which accepts a \texttt{Parameter} element as input, and creates an instance of the parameterized element. To illustrate, consider the apply method for \texttt{ParameterizedFlip}: \begin{flushleft} -\texttt{def apply(prob: AtomicBeta)(implicit name: Name[Boolean], collection: ElementCollection) = -\newline \tab new ParameterizedFlip(name, prob, collection) +\texttt{def apply(prob: Element[Double])(implicit name: Name[Boolean], collection: ElementCollection) = +\newline \tab new ParameterizedFlip(name, prob.asInstanceOf[AtomicBeta], collection) } \end{flushleft} -To learn the MAP value of the parameter, Figaro provides an implementation of the expectation maximization algorithm. During the expectation step, the algorithm retrieves the distribution of the element according to the current value of the parameter, then converts the distribution to sufficient statistics. This conversion needs to be defined inside the parameter, using the method \texttt{distributionToStatistics}. It accepts as an argument a Scala stream consisting of pairs of double values (probabilities) and possible outcomes of the element. The implementation for a \texttt{Flip} is shown below. +To learn the MAP value of the parameter, Figaro provides an implementation of the expectation maximization algorithm. During the expectation step, the algorithm retrieves the distribution of the element according to the current value of the parameter, then converts the distribution to sufficient statistics. This conversion needs to be defined inside the parameter, using the method \texttt{distributionToStatistics}. It accepts as an argument a Scala stream consisting of pairs of double values (probabilities) and possible outcomes of the element. The implementation for a \texttt{Flip} is shown below. \begin{flushleft} \texttt{def distributionToStatistics(distribution: Stream[(Double, Boolean)]): Seq[Double] = \{ @@ -319,12 +317,12 @@ \section{Creating a class with special Metropolis-Hastings behavior} By default, proposing an element in Metropolis-Hastings uses the class's standard \texttt{generateRandomness} to propose the new randomness. Earlier, we described how it is sometimes useful to create a special proposal distribution and gave \texttt{SwitchingFlip} as an example. \texttt{SwitchingFlip} is just like an ordinary \texttt{Flip} except that each time it is proposed, it switches to the opposite value. -Creating a different proposal distribution for an element is achieved through the \texttt{nextRandomness} method. In Metropolis-Hastings, the acceptable probability of a sample is defined as +Creating a different proposal distribution for an element is achieved through the \texttt{nextRandomness} method. In Metropolis-Hastings, the acceptable probability of a sample is defined as: \[\frac{\text{P(r\textsuperscript{1} -> r\textsuperscript{0})P(r\textsuperscript{1})}}{\text{P(r\textsuperscript{0} -> r\textsuperscript{1})P(\textsuperscript{0})}}\] where r\textsuperscript{0} is the original randomness, r\textsuperscript{1} is the proposed randomness, P(r\textsuperscript{1}) is the probability of \texttt{generateRandomness} returning r\textsuperscript{1}, and P(r\textsuperscript{0} -> r\textsuperscript{1}) indicates the probability of \texttt{nextRandomness} returning r\textsuperscript{1} when its argument is r\textsuperscript{0}. The \texttt{nextRandomness} method returns three values; the new randomness, the transition probability ratio (P(r\textsuperscript{1} -> r\textsuperscript{0})/P(r\textsuperscript{0} -> r\textsuperscript{1})) and the model probability ratio (P(r\textsuperscript{1})/P(r\textsuperscript{0})). These ratios are separate values because some algorithms, such as simulated annealing, need to access these ratios before they are multiplied together. -By default, the \texttt{nextRandomness} method simply uses the element's \texttt{generateRandomness} method and returns 1.0 for the both probability ratios. This is correct in most cases, and is used for most of the built-in elements. However, it can be overridden if desired. For example, the definition of \texttt{SwitchingFlip} includes +By default, the \texttt{nextRandomness} method simply uses the element's \texttt{generateRandomness} method and returns 1.0 for the both probability ratios. This is correct in most cases, and is used for most of the built-in elements. However, it can be overridden if desired. For example, the definition of \texttt{SwitchingFlip} includes: \begin{flushleft} \texttt{override def nextRandomness(rand: Randomness) = diff --git a/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex b/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex index 2b4bf430..edbe57ff 100644 --- a/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex +++ b/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex @@ -46,23 +46,23 @@ \subsection{Sampling} \subsection{Expansion and factors} -A useful operation is to expand all chains in a universe to obtain the complete set of elements in the universe. This is achieved using the syntax +A useful operation is to expand all chains in a universe to obtain the complete set of elements in the universe. This is achieved using the syntax: \begin{flushleft} \texttt{Expand(universe).expand()} \end{flushleft} -As usual, the \texttt{universe} argument can be omitted, using the current default universe. Support is provided for algorithms that are based on factors. Variable elimination is one example, but there are many other such algorithms. To create all the factors for an element, use +As usual, the \texttt{universe} argument can be omitted, using the current default universe. Support is provided for algorithms that are based on factors. Variable elimination is one example, but there are many other such algorithms. To create all the factors for an element, use: \begin{flushleft} \texttt{ProbFactor.make(element)} \end{flushleft} -The standard procedure to turn a universe into a list of factors is to +The standard procedure to turn a universe into a list of factors is to: \begin{enumerate} \item Expand the universe. \item Call \texttt{universe.activeElements} to get all the elements in the universe. -\item Make the factors for every element and collect them +\item Make the factors for every element and collect them. \end{enumerate} Operations in factored algorithms are defined by a semiring algebraic structure. There are several semiring definitions in the package \texttt{com.cra.figaro.algorithm.factored}. Each semiring defines a product and sum operation, and a value for zero and one which satisfy a set of properties. Different semirings are appropriate for certain algorithms and data types; however, the most frequently used set of operations is \texttt{SumProductSemiring}. @@ -107,12 +107,13 @@ \subsection{Code sharing} \newline \tab with AnytimeProbQuerySampler \newline \newline \tab def apply(myNumSamples: Int, targets: Element[\_]*)(implicit universe: Universe) = new Importance(universe, targets:\_*) -\newline \tab with OneTimeProbQuerySampler \{ val numSamples = myNumSamples \} +\newline \tab with OneTimeProbQuerySampler \{ +\newline \tab val numSamples = myNumSamples \} \newline \} } \end{flushleft} -The first constructor takes has two argument lists. The first is a comma-separated sequence of query targets, and the second provides the universe. Since it implicit, it can be omitted and the default universe is used. Since the number of samples is not explicitly provided, it is assumed that the anytime version is wanted, so the constructor inherits from \texttt{AnytimeProbQuerySampler}. In the second, case, the number of samples is specified, so it inherits from \texttt{OneTimeProbQuerySampler}. One detail to note is that \texttt{OneTimeProbQuerySampler} contains an abstract field named \texttt{numSamples} that must be defined to create an instance of the trait. This is accomplished through the code +The first constructor takes has two argument lists. The first is a comma-separated sequence of query targets, and the second provides the universe. Since it implicit, it can be omitted and the default universe is used. Since the number of samples is not explicitly provided, it is assumed that the anytime version is wanted, so the constructor inherits from \texttt{AnytimeProbQuerySampler}. In the second, case, the number of samples is specified, so it inherits from \texttt{OneTimeProbQuerySampler}. One detail to note is that \texttt{OneTimeProbQuerySampler} contains an abstract field named \texttt{numSamples} that must be defined to create an instance of the trait. This is accomplished through the code: \newline \texttt{OneTimeProbQuerySampler \{ val numSamples = myNumSamples \}} This creates an anonymous subclass of \texttt{OneTimeProbQuerySampler} in which the \texttt{numSamples} field is defined to be the value passed into the constructor. @@ -128,39 +129,38 @@ \section{Allowing extension to new element classes} \begin{flushleft} -\texttt{private def concreteValues[T](element: Element[T]): Set[T] = -element match \{ -\newline \tab case c: Constant [\_] => Set(c.constant) -\newline \tab case f: Flip => Set(true, false) +\texttt{private concreteValues[T](element: Element[T], depth: Int, numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = element match \{ +\newline \tab case c: Constant [\_] => withoutStar(Set(c.constant)) +\newline \tab case f: Flip => withoutStar(Set(true, false)) \newline \tab ... -\newline \tab case v: ValuesMaker[\_] => v.makeValues.toSet -\newline \tab case \_ => throw new UnsupportedAlgorithmException(element) +\newline \tab case v: ValuesMaker[\_] => v.makeValues(depth) +\newline \tab case \_ => withStar(Set()) \newline \} } \end{flushleft} -This function takes an element and tests to see what kind of element it is. If it is a constant, the values is a singleton set containing the constant; if it is a flip, it is a set containing true and false, and so on. If the value fails to match any of the built-in types for which this function is defined, it arrives at the second to last case. This tests if the value is an instance of \texttt{ValuesMaker}. If it is, the values \texttt{makeValues} method is used. The final case is a catchall - the notation \_ represents a pattern that catches all values. If the value has arrived at this case, its values cannot be computed, so we throw an \texttt{UnsupportedAlgorithmException}. +This function takes an element and tests to see what kind of element it is. If it is a constant, the values is a singleton set containing the constant; if it is a flip, it is a set containing true and false, and so on. If the value fails to match any of the built-in types for which this function is defined, it arrives at the second to last case. This tests if the value is an instance of \texttt{ValuesMaker}. If it is, the values \texttt{makeValues} method is used. The final case is a catchall: the notation \_ represents a pattern that catches all values. If the value has arrived at this case, we can't compute the values and we just make them *, so the rest of the computation can proceed. \section{Creating a new category of algorithm} Suppose you want to create a new category of algorithm. For example, probability of query algorithms, probability of evidence, and most likely value algorithms are all different categories. Figaro provides some infrastructure to help with creating a new kind of algorithm. We will illustrate how this is done for most likely value algorithms, and the same pattern can be used elsewhere. -All algorithms extend the \texttt{Algorithm} trait, which defines the general interface to algorithms using \texttt{start, stop, resume}, and \texttt{kill}. To define a new category of algorithm, you extend algorithm and define methods for the different ways the algorithm can be queried. For example, +All algorithms extend the \texttt{Algorithm} trait, which defines the general interface to algorithms using \texttt{start, stop, resume}, and \texttt{kill}. To define a new category of algorithm, you extend algorithm and define methods for the different ways the algorithm can be queried. For example: \marginpar{The \texttt{val} in front of the universe argument indicates that universe is a field of MPEAlgorithm that can be accessed} \begin{flushleft} -\texttt{abstract class MPEAlgorithm(val universe: Universe) -\newline extends Algorithm \{ +\texttt{trait MPEAlgorithm extends Algorithm \{ +\newline val universe: Universe \newline \tab /** -\newline \tab * Returns the most likely value for the target element. +\newline \tab * Particular implementations of algorithm must provide the following method. \newline \tab */ \newline \tab def mostLikelyValue[T](target: Element[T]): T \newline \} } \end{flushleft} -An \texttt{MPEAlgorithm} takes the universe on which it is defined as an argument. It provides one query method, which returns the most likely value of a target method. This method is abstract (it has no implementation) and must be implemented in a particular implementation of \texttt{MPEAlgorithm}. +An \texttt{MPEAlgorithm} contains the universe on which it is defined as an argument. It provides one query method, which returns the most likely value of a target method. This method is abstract (it has no implementation) and must be implemented in a particular implementation of \texttt{MPEAlgorithm}. Next, we provide one-time and anytime traits for MPE algorithms. The one-time trait is very easy: @@ -217,7 +217,7 @@ \section{Creating a new category of algorithm} } \end{flushleft} -Anytime algorithms run in a separate thread, and we need to be able to communicate with the thread to get the probability of evidence out of it. This is accomplished using Scala's \emph{actors} framework. Actors communicate by sending and processing messages. The \texttt{Anytime} trait defines the \texttt{runner} field, which is the actor that runs the algorithm. A request can be sent to the runner to compute the most likely value of a target element. The syntax for sending the message to the runner is +Anytime algorithms run in a separate thread, and we need to be able to communicate with the thread to get the probability of evidence out of it. This is accomplished using Scala's \emph{actors} framework. Actors communicate by sending and processing messages. The \texttt{Anytime} trait defines the \texttt{runner} field, which is the actor that runs the algorithm. A request can be sent to the runner to compute the most likely value of a target element. The syntax for sending the message to the runner is: \begin{flushleft} \texttt{runner ! Handle(ComputeMostLikelyValue(target))} @@ -230,17 +230,17 @@ \section{Creating a new category of algorithm} To summarize, to define an anytime version of the algorithm, you need to do the following: \begin{enumerate} -\item Create a case class or object to represent the services provided by your algorithm. Here, it is accomplished by +\item Create a case class or object to represent the services provided by your algorithm. Here, it is accomplished by: \newline \texttt{case class ComputeMostLikelyValue(target: Element[T]) \newline extends Service} \item Create a case class or object to represent the responses provided by these -services. Here, +services. Here: \newline \texttt{case class MostLikelyValue[T](value: T) \newline extends Response} \item Create a handler in the method \texttt{handle} that takes a service, performs some computation, and returns a response. \item In each method that provides an interface to querying the algorithm \end{enumerate} \begin{aenumerate} -\item Send a message to the runner asking for the appropriate service +\item Send a message to the runner asking for the appropriate service. \item Receive a message from the runner, extract the result, and return it. \end{aenumerate} \ No newline at end of file diff --git a/FigaroLaTeX/Tutorial/Sections/12Conclusion.tex b/FigaroLaTeX/Tutorial/Sections/12Conclusion.tex index 4e1a6213..d9c52829 100644 --- a/FigaroLaTeX/Tutorial/Sections/12Conclusion.tex +++ b/FigaroLaTeX/Tutorial/Sections/12Conclusion.tex @@ -6,14 +6,6 @@ \chapter{Conclusion} % Chapter title %---------------------------------------------------------------------------------------- -As you can see, there's quite a lot to Figaro. We hope you will find it useful in your probabilistic reasoning projects. If you have any comments, suggestions, bug fixes, etc., please refer to the GitHub site (\url{https://github.com/p2t2}) for the best way to contact us, or send them to figaro@cra.com. - -Some of the planned next steps for Figaro are: -\begin{itemize} -\item Better dynamic reasoning algorithms -\item Distributed reasoning algorithms -\end{itemize} - -If you have any suggestions to make along these lines or about other possible next steps, we would love to hear from you. If you want to make a contribution, we would be delighted. +As you can see, there's quite a lot to Figaro. We hope you will find it useful in your probabilistic reasoning projects. If you have any comments, suggestions, bug fixes, feature requests, etc., please refer to the GitHub site (\url{https://github.com/p2t2}) for the best way to contact us, or send them to \href{mailto:figaro@cra.com}{figaro@cra.com}. Thanks for reading, and enjoy! \ No newline at end of file diff --git a/FigaroLaTeX/Tutorial/Sections/1Introduction.tex b/FigaroLaTeX/Tutorial/Sections/1Introduction.tex index f869dd3c..d6c12143 100644 --- a/FigaroLaTeX/Tutorial/Sections/1Introduction.tex +++ b/FigaroLaTeX/Tutorial/Sections/1Introduction.tex @@ -31,19 +31,22 @@ \section{What is Figaro?} Figaro's library of reasoning algorithms is also extensible. Current built-in algorithms include: \begin{itemize} \item Exact inference using variable elimination +\item Belief propagation +\item Lazy factored inference for infinite models \item Importance sampling \item Metropolis-Hastings, with an expressive language to define proposal distributions \item Support computation -\item Most probable explanation (MPE) using variable elimination or simulated annealing -\item Probability of evidence using importance sampling -\item Particle Filtering +\item Most probable explanation (MPE) using variable elimination, belief propagation, or simulated annealing +\item Probability of evidence using importance sampling, belief propagation, variable elimination, and particle filtering +\item Particle filtering +\item Factored frontier \item Parameter learning using expectation maximization \end{itemize} Figaro provides both regular (the algorithm is run once) and anytime (the algorithm is run until stopped) versions of some of these algorithms. In addition to the built-in algorithms, Figaro provides a number of tools for creating your own reasoning algorithms. Figaro is free and is released under an open-source license (see license file). The public code -repository for Figaro can also be found at \url{https://github.com/p2t2} +repository for Figaro can also be found at \url{https://github.com/p2t2}. %------------------------------------------------ @@ -62,67 +65,71 @@ \section{This tutorial} This tutorial assumes some basic knowledge of probabilistic modeling and inference to derive the maximum benefit from it. Also, while this tutorial is not an introduction to Scala, it will explain some Scala constructs as it goes along, so that the reader can make basic use of Figaro after reading the tutorial. However, to get the full benefit of Figaro, it is recommended that the reader learn some Scala. This could prove well worth the reader's while, because Scala is a language that combines elegance and practicality in a useful way. "Programming in Scala" by Martin Odersky is available for free online. +The tutorial is not a complete and comprehensive guide to all of Figaro's features. The official reference is the Scaladoc, which documents Figaro's methods. For a broader introduction to probabilistic programming and Figaro, see "Practical Probabilistic Programming" by Avi Pfeffer, published by Manning (\url{http://www.manning.com/pfeffer/}). + After presenting a "Hello world!" example, the tutorial will begin with a discussion of Figaro's representation, i.e. the data structures that underlie the probabilistic models. Next, it will give examples using Scala of creating Figaro models. It will then describe how to use the built-in reasoning algorithms, including a brief discussion of probabilistic programming for dynamic models, decision networks, -parameter learning and hierarchical reasoning. The last two sections of the tutorial are geared towards users who want to extend Figaro, first describing how to create new modeling data structures and then describing how to create new algorithms. All of the code for the examples presented in this tutorial can be -found with the set of examples distributed with Figaro. +parameter learning and hierarchical reasoning. The last two sections of the tutorial are geared towards users who want to extend Figaro, first describing how to create new modeling data structures and then describing how to create new algorithms. All of the code for the examples presented in this tutorial can be found with the set of examples distributed with Figaro. \section{Installation} -To run Figaro, you will first need Scala. The Scala compiler can either be run from the command line or within an Integrated Development Environment (IDE). Available IDEs that support Scala development include Eclipse and IntelliJ Idea. NetBeans also has a Scala plugin but it does not appear to support recent versions of Scala. In the following guide, I'm going to show you how to obtain Scala and Figaro and run Scala programs that use Figaro from the command line. I'm not going to provide instructions for specific IDEs. Please see the documentation of your IDEs and Scala plugins for details of how to include the Figaro library. - -\begin{enumerate} -\item To get started with Scala, download Scala from http://scala-lang.org/download/. You will need either Scala version 2.10.0 or later, or Scala version 2.11.0 or later, to run Figaro. Follow the Scala installation instructions at http://scala-lang.org/download/install.html and make sure you can run, compile, and execute the "Hello World" program provided in the documentation. -\item The next step is to obtain Figaro. The Figaro binary distribution is hosted at the Charles River Analytics, Inc. Web site. Go to https://www.cra.com/figaro. The current version, as of June 2014, is 2.2.1.0, and is available for either Scala 2.10 or Scala 2.11. Each available download link is a compressed archive containing the Figaro jar (jar is the Java/Scala format for compiled byte code), examples, documentation, Scaladoc, and source code files. Click the appropriate link and then uncompress the downloaded archive to access the file. -\item [Optional] Add the fully qualified path name of the Figaro jar to your class path. This can be done by adding the Figaro jar to the CLASSPATH environment variable in your operating system. The process for editing the CLASSPATH varies from operating system to operating system. You can see details about using the PATH and CLASSPATH environment variables in http://docs.oracle -\newline .com/javase/tutorial/essential/environment/paths.html. -\begin{enumerate} -\item If the CLASSPATH does not exist yet, create. I always like the CLASSPATH to include the current working directory, so set the CLASSPATH to ".". Then proceed to add the Figaro jar, as in the next step. -\item By this point, the CLASSPATH already exists, so we can add the Figaro path to it. For example, on Windows 7, if figaro-2.1.0.0.jar is in the "C:\textbackslash Users\textbackslash apfeffer" folder, and the CLASSPATH is currently equal to ".", change the CLASSPATH to "C:\textbackslash Users\textbackslash apfeffer\textbackslash figaro-2.2.1.0;.". Replace 2.2.1.0 with the appropriate version number. -\end{enumerate} -\item Now you can compile and run Figaro programs just like any Scala program. Put the Test program below in a file named Test.scala. First, let's assume you followed step 4and updated the CLASSPATH. -\begin{enumerate} -\item If you run scala Test.scala from the directory containing Test.scala, the Scala compiler will first compile the program and then execute it. It should produce the output 1.0. -\item If you run scalac Test.scala (note the c at the end of "scalac"), the Scala compiler runs and produces .class files. You can then execute the program by running scala Test from the same directory. -\item If you did not follow step 4, you can set the CLASSPATH from the command line using the --cp option. For example, to compile and execute Test.scala, assuming figaro-2.2.1.0.jar is in the "C:\textbackslash Users\textbackslash apfeffer" folder, you can run scala --cp C:\textbackslash Users\textbackslash apfeffer\textbackslash figaro-2.2.1.0 Test.scala. -\end{enumerate} -\end{enumerate} - -Here's the test program: - -\texttt{ -\newline import com.cra.figaro.language.\_ -\newline import com.cra.figaro.algorithm.sampling.\_ -\newline -\newline object Test \{ -\newline def main(args: Array[String]) \{ -\newline val test = Constant("Test") -\newline val algorithm = Importance(1000, test) -\newline algorithm.start() -\newline println(algorithm.probability(test, "Test")) -\newline \} -\newline \} -} -\newline - -This program should output 1.0. - -Finally, Figaro is maintained as open source on GitHub. The GitHub project is Probabilistic Programming Tools and Techniques (P2T2), located at https://github.com/p2t2. P2T2 currently contains the Figaro sources, but we plan to update it with more tools. If you want to see the source code and build Figaro yourself, please visit our GitHub site. - -Figaro uses the Simple Build Tool (SBT v0.13.5) to manage builds. To build Figaro from GitHub source, make a fork of the repository to your GitHub account, then use git's clone feature to get the source code from your GitHub account to your machine. - -\texttt{git clone https://github.com/[your-github-username]/figaro.git} - -There are several branches available; checkout "master" for the latest stable release or the latest "DEV" branch for more cutting edge work and features (this is work in progress and therefore less stable). Download and install SBT, start the program to get its command prompt, and enter these commands in order - -\texttt{ -\newline > + clean -\newline > + compile -\newline > + package -\newline > + assembly -\newline > exit -} -\newline - -This will create cross-compiled versions of Figaro for Scala 2.10 and Scala 2.11; you will find the artifacts in the "target" directory. +% To run Figaro, you will first need Scala. The Scala compiler can either be run from the command line or within an Integrated Development Environment (IDE). Available IDEs that support Scala development include Eclipse and IntelliJ Idea. NetBeans also has a Scala plugin but it does not appear to support recent versions of Scala. In the following guide, I'm going to show you how to obtain Scala and Figaro and run Scala programs that use Figaro from the command line. I'm not going to provide instructions for specific IDEs. Please see the documentation of your IDEs and Scala plugins for details of how to include the Figaro library. + +% \begin{enumerate} +% \item To get started with Scala, download Scala from http://scala-lang.org/download/. You will need either Scala version 2.10.0 or later, or Scala version 2.11.0 or later, to run Figaro. Follow the Scala installation instructions at http://scala-lang.org/download/install.html and make sure you can run, compile, and execute the "Hello World" program provided in the documentation. +% \item The next step is to obtain Figaro. The Figaro binary distribution is hosted at the Charles River Analytics, Inc. Web site. Go to https://www.cra.com/figaro. The current version, as of June 2014, is 2.2.1.0, and is available for either Scala 2.10 or Scala 2.11. Each available download link is a compressed archive containing the Figaro jar (jar is the Java/Scala format for compiled byte code), examples, documentation, Scaladoc, and source code files. Click the appropriate link and then uncompress the downloaded archive to access the file. +% \item [Optional] Add the fully qualified path name of the Figaro jar to your class path. This can be done by adding the Figaro jar to the CLASSPATH environment variable in your operating system. The process for editing the CLASSPATH varies from operating system to operating system. You can see details about using the PATH and CLASSPATH environment variables in http://docs.oracle +% \newline .com/javase/tutorial/essential/environment/paths.html. +% \begin{enumerate} +% \item If the CLASSPATH does not exist yet, create. I always like the CLASSPATH to include the current working directory, so set the CLASSPATH to ".". Then proceed to add the Figaro jar, as in the next step. +% \item By this point, the CLASSPATH already exists, so we can add the Figaro path to it. For example, on Windows 7, if figaro-2.1.0.0.jar is in the "C:\textbackslash Users\textbackslash apfeffer" folder, and the CLASSPATH is currently equal to ".", change the CLASSPATH to "C:\textbackslash Users\textbackslash apfeffer\textbackslash figaro-2.2.1.0;.". Replace 2.2.1.0 with the appropriate version number. +% \end{enumerate} +% \item Now you can compile and run Figaro programs just like any Scala program. Put the Test program below in a file named Test.scala. First, let's assume you followed step 4and updated the CLASSPATH. +% \begin{enumerate} +% \item If you run scala Test.scala from the directory containing Test.scala, the Scala compiler will first compile the program and then execute it. It should produce the output 1.0. +% \item If you run scalac Test.scala (note the c at the end of "scalac"), the Scala compiler runs and produces .class files. You can then execute the program by running scala Test from the same directory. +% \item If you did not follow step 4, you can set the CLASSPATH from the command line using the --cp option. For example, to compile and execute Test.scala, assuming figaro-2.2.1.0.jar is in the "C:\textbackslash Users\textbackslash apfeffer" folder, you can run scala --cp C:\textbackslash Users\textbackslash apfeffer\textbackslash figaro-2.2.1.0 Test.scala. +% \end{enumerate} +% \end{enumerate} + +% Here's the test program: + +% \texttt{ +% \newline import com.cra.figaro.language.\_ +% \newline import com.cra.figaro.algorithm.sampling.\_ +% \newline +% \newline object Test \{ +% \newline def main(args: Array[String]) \{ +% \newline val test = Constant("Test") +% \newline val algorithm = Importance(1000, test) +% \newline algorithm.start() +% \newline println(algorithm.probability(test, "Test")) +% \newline \} +% \newline \} +% } +% \newline + +% This program should output 1.0. + +% Finally, Figaro is maintained as open source on GitHub. The GitHub project is Probabilistic Programming Tools and Techniques (P2T2), located at https://github.com/p2t2. P2T2 currently contains the Figaro sources, but we plan to update it with more tools. If you want to see the source code and build Figaro yourself, please visit our GitHub site. + +% Figaro uses the Simple Build Tool (SBT v0.13.5) to manage builds. To build Figaro from GitHub source, make a fork of the repository to your GitHub account, then use git's clone feature to get the source code from your GitHub account to your machine. + +% \texttt{git clone https://github.com/[your-github-username]/figaro.git} +% There are several branches available; checkout "master" for the latest stable release or the latest "DEV" branch for more cutting edge work and features (this is work in progress and therefore less stable). Download and install SBT, start the program to get its command prompt, and enter these commands in order + + +% \texttt{ +% \newline > + clean +% \newline > + compile +% \newline > + package +% \newline > + assembly +% \newline > exit +% } +% \newline + +%This will create cross-compiled versions of Figaro for Scala 2.10 and Scala 2.11; you will find the artifacts in the "target" directory. +Please see the Quick Start Guide for instructions on installing Figaro. There are several ways to use Figaro, including just using the binary distribution or compiling from the source code. + +Figaro is maintained as open source on GitHub. The GitHub project is Probabilistic Programming Tools and Techniques (P2T2), located at \url{https://github.com/p2t2}. If you want to see the source code and build Figaro yourself, please visit our GitHub site. We welcome contributions from the community. diff --git a/FigaroLaTeX/Tutorial/Sections/2HelloWorld.tex b/FigaroLaTeX/Tutorial/Sections/2HelloWorld.tex index 0fe16ea8..0d519b74 100644 --- a/FigaroLaTeX/Tutorial/Sections/2HelloWorld.tex +++ b/FigaroLaTeX/Tutorial/Sections/2HelloWorld.tex @@ -6,15 +6,15 @@ \chapter{Hello world!} % Chapter title %---------------------------------------------------------------------------------------- -Make sure Scala version 2.10.0 or later is installed on your machine. Follow the instructions to either extract the figaro.jar to some location or build the jar from the code repository. Change directory to that location and enter at the command prompt +Make sure Scala version 2.11.4 or later is installed on your machine. Follow the instructions to either extract the Figaro jar to some location or build the jar from the code repository. Change the directory to that location and enter the line below in the command prompt: \begin{flushleft} \texttt{scala $-$classpath "figaro.jar;\$CLASSPATH"} \end{flushleft} -This starts the Scala interactive console and makes sure all the Figaro classes are available. The interactive console reads one line of Scala code at a time and interprets it. It is useful for learning and trying new things. Ordinarily, you would use the compiler to compile a program into Java byte code and run it. To use the Scala compiler, use the \texttt{scalac} or \texttt{fsc} command, again making sure \texttt{Figaro.jar} is in the class path. +This starts the Scala interactive console and makes sure all the Figaro classes are available. The interactive console reads one line of Scala code at a time and interprets it. It is useful for learning and trying new things. Ordinarily, you would use the compiler to compile a program into Java byte code and run it. To use the Scala compiler, use the \texttt{scalac} or \texttt{fsc} command, again making sure the \texttt{Figaro.jar} is in the class path. -Once in the interactive console, at the Scala prompt, enter +Once in the interactive console, at the Scala prompt, enter: \begin{flushleft} \texttt{import com.cra.figaro.language.\_} @@ -26,7 +26,7 @@ \chapter{Hello world!} % Chapter title \texttt{val hw = Constant("Hello world!")} \end{flushleft} -This line creates a field \texttt{hw} whose value is the probabilistic model that produces the string "Hello world!" with probability 1. To exercise the model, we need to create an instance of an algorithm. We'll use an importance sampling algorithm. First we need to import the algorithm's definition: +This line creates a field \texttt{hw} whose value is the probabilistic model that produces the string "Hello world!" with probability 1.0. To exercise the model, we need to create an instance of an algorithm. We'll use an importance sampling algorithm. First we need to import the algorithm's definition: \begin{flushleft} \texttt{import com.cra.figaro.algorithm.sampling.\_} @@ -43,23 +43,23 @@ \chapter{Hello world!} % Chapter title \begin{flushleft} \texttt{alg.start()} \end{flushleft} -We can now ask for the probability of various strings. Enter +We can now ask for the probability of various strings. Enter: \begin{flushleft} \texttt{alg.probability(hw, "Hello world!")} \end{flushleft} -Scala responds with something like +Scala responds with something like: \begin{flushleft} \texttt{res3: Double = 1.0} \end{flushleft} -This means that the answer is of type \texttt{Double}, has value 1.0, and is given the name \texttt{res3}. We can similarly ask +This means that the answer is of type \texttt{Double}, has value 1.0, and is given the name \texttt{res3}. We can similarly ask: \begin{flushleft} \texttt{alg.probability(hw, "Goodbye!")} \end{flushleft} -Scala responds with something like +Scala responds with something like: \begin{flushleft} \texttt{res4: Double = 0.0} diff --git a/FigaroLaTeX/Tutorial/Sections/3FigaroRepresentation.tex b/FigaroLaTeX/Tutorial/Sections/3FigaroRepresentation.tex index 320d4249..105967fd 100644 --- a/FigaroLaTeX/Tutorial/Sections/3FigaroRepresentation.tex +++ b/FigaroLaTeX/Tutorial/Sections/3FigaroRepresentation.tex @@ -7,7 +7,7 @@ \chapter{Figaro's representation} % Chapter title This section describes the basic building blocks of Figaro models. We present the basic definitions of different kinds of model components. In the following section, we will show how to use these components to create a rich variety of models. \section{Elements} -All data structures that are part of a Figaro model are \emph{elements}. Elements can be combined in various ways to produce more complex elements. The simplest elements are \emph{atomic} elements that do not depend on other elements. An example of an atomic element is +All data structures that are part of a Figaro model are \emph{elements}. Elements can be combined in various ways to produce more complex elements. The simplest elements are \emph{atomic} elements that do not depend on other elements. An example of an atomic element is: \begin{flushleft} \marginpar{Figaro classes are capitalized, while Scala reserved words are not} @@ -15,12 +15,12 @@ \section{Elements} \end{flushleft} This defines the probabilistic model that produces the integer 6 with probability -1. Another atomic element is +1.0. Another atomic element is: \begin{flushleft} \texttt{Constant("Hello")} \end{flushleft} -which produces the string "Hello" with probability 1. These two examples illustrate that every Figaro element has a \emph{value type}, which in the first case is \texttt{Int} and in the second case \texttt{String}. The value type is the type of values produced by the probabilistic model defined by the element. +which produces the string "Hello" with probability 1.0. These two examples illustrate that every Figaro element has a \emph{value type}, which in the first case is \texttt{Int} and in the second case is \texttt{String}. The value type is the type of values produced by the probabilistic model defined by the element. \marginpar{Scala uses type inference, so the value type of the parameter can often be omitted at class creation (the compiler will determine the type)} @@ -43,36 +43,37 @@ \section{Atomic elements} \item The continuous \texttt{Uniform(0.0, 2.0)} is an \texttt{Element[Double]} that represents the continuous uniform probability distribution between 0 and 2. \end{itemize} -Elements also contain a method for generating a new value for the element. This is accomplished by called the \texttt{generate()} method of an element. Generating a new value of an element is a two step process. First, the \texttt{generate()} method generates a new \emph{randomness} for the element. Then, the value of an atomic element is \emph{deterministically} produced as a function of the randomness of the element. For example, consider the \texttt{Flip(0.7)} shown above. To determine the value of this \texttt{Flip}, a number between 0 and 1 is uniformly chosen as the randomness of the element, and if the value is less than 0.7, we assign the \texttt{Flip} a value of \texttt{true}, and \texttt{false} otherwise. +% Elements also contain a method for generating a new value for the element. This is accomplished by called the \texttt{generate()} method of an element. Generating a new value of an element is a two step process. First, the \texttt{generate()} method generates a new \emph{randomness} for the element. Then, the value of an atomic element is \emph{deterministically} produced as a function of the randomness of the element. For example, consider the \texttt{Flip(0.7)} shown above. To determine the value of this \texttt{Flip}, a number between 0 and 1 is uniformly chosen as the randomness of the element, and if the value is less than 0.7, we assign the \texttt{Flip} a value of \texttt{true}, and \texttt{false} otherwise. -While \texttt{Flip} and \texttt{Select} are in the \texttt{language} package that was imported earlier, \texttt{Uniform} is in the \texttt{library.atomic.continuous} package that needs to be imported using +While \texttt{Flip} and \texttt{Select} are in the \texttt{language} package that was imported earlier, \texttt{Uniform} is in the \texttt{library.atomic.continuous} package that needs to be imported using: \begin{flushleft} \marginpar{The \_ is the Scala version of Java's * for imports} \texttt{import com.cra.figaro.library.atomic.continuous.\_} \end{flushleft} -Other built-in continuous atomic classes include \texttt{Normal, Exponent\-ial, Gamma, Beta}, and \texttt{Dirichlet}, also found in the \texttt{library.atomic.\-continuous package}, while discrete elements include discrete \texttt{Uniform, Geometric, Binomial}, and \texttt{Poisson}, to be found in the \texttt{library.atomic.\-discrete package}. +Other built-in continuous atomic classes include \texttt{Normal, Exponent\-ial, Gamma, Beta}, and \texttt{Dirichlet}, also found in the \texttt{library.atomic.\-continuous} package, while discrete elements include discrete \texttt{Uniform, Geometric, Binomial}, and \texttt{Poisson}, to be found in the \texttt{library.atom\-ic.discrete} package. \section{Compound elements} -In \texttt{Flip(0.7)}, the argument to \texttt{Flip} is a \texttt{Double}. There is another version of \texttt{Flip} in which the argument is an \texttt{Element[Double]}. For example, we might have +In \texttt{Flip(0.7)}, the argument to \texttt{Flip} is a \texttt{Double}. There is another version of \texttt{Flip} in which the argument is an \texttt{Element[Double]}. For example, we might have: \begin{flushleft} -\texttt{Flip(Uniform(0.0, 1.0)) } +\texttt{Flip(Uniform(0.0, 1.0))} \end{flushleft} which represents the probabilistic model that produces \texttt{true} with a probability that is uniformly distributed between 0 and 1. This is a \emph{compound} element that is built from another element. Most of the atomic elements described in the previous subsection have compound versions. -Another example of a compound element is a conditional. The element +Another example of a compound element is a conditional. The element: \begin{flushleft} \marginpar{Note \texttt{If} is a Figaro class, not the Scala \texttt{if} reserved word } \texttt{If(Flip(0.7), Constant(1), Select(0.4 -> 2, 0.6 -> 3))} \end{flushleft} -represents the \texttt{Element[Int]} in which with probability 0.7, \texttt{Constant\-(1)} is chosen, producing 1 with probability 1, while with probability 0.3, \texttt{Select(0.4 --> 2, 0.6 -> 3)} is chosen, producing 2 with probability 0.4 and 3 with probability 0.6. Overall, 1 is produced with probability 0.7 * 1 = 0.7, 2 with probability 0.3 * 0.4 = 0.12, and 3 with probability 0.3 * 0.6 = 0.18. The first argument to \texttt{If} must be an \texttt{Element[Boolean]}, while the other two arguments must have the same value type, which also becomes the value type of the \texttt{If}. \texttt{If} can be found in the \texttt{library.compound package}. Similar to the atomic elements, the value of a compound element can be generated by calling the \texttt{generate()} method of the element. In this case, however, the value of an element is a deterministic function of its randomness and the current value of the element's parents. +represents the \texttt{Element[Int]} in which with probability 0.7, \texttt{Constant\-(1)} is chosen, producing 1 with probability 1.0, while with probability 0.3, \texttt{Select(0.4 +-> 2, 0.6 -> 3)} is chosen, producing 2 with probability 0.4 and 3 with probability 0.6. Overall, 1 is produced with probability 0.7 * 1 = 0.7, 2 with probability 0.3 * 0.4 = 0.12, and 3 with probability 0.3 * 0.6 = 0.18. The first argument to \texttt{If} must be an \texttt{Element[Boolean]}, while the other two arguments must have the same value type, which also becomes the value type of the \texttt{If}. \texttt{If} can be found in the \texttt{library.compound} package. +% Similar to the atomic elements, the value of a compound element can be generated by calling the \texttt{generate()} method of the element. In this case, however, the value of an element is a deterministic function of its randomness and the current value of the element's parents. \section{Chain} @@ -82,25 +83,25 @@ \section{Chain} \marginpar{Scala notation for the type of a function is: \texttt{inType => outType}} model in which first a value of type \texttt{T} is produced from the parent argument, then the function in the second argument is applied to this value to generate a particular \texttt{Element[U]}, and finally a particular value of type \texttt{U} is randomly produced from the generated \texttt{Element[U]}. Therefore, a \texttt{Chain[T,U]} is an \texttt{Element[U]}. -For example, +For example: \begin{flushleft} \texttt{Chain(Flip(0.7), (b: Boolean) => \newline \tab if (b) Constant(1); else Select(0.4 -> 2, 0.6 -> 3))} \end{flushleft} -represents exactly the same probabilistic model as +represents exactly the same probabilistic model as: \begin{flushleft} \texttt{If(Flip(0.7), Constant(1), Select(0.4 -> 2, 0.6 -> 3))} \end{flushleft} -Let's understand this example from the inside out. First, +Let's understand this example from the inside out. First: \begin{flushleft} \texttt{if (b) Constant(1); else Select(0.4 -> 2, 0.6 -> 3)} \end{flushleft} -is a Scala expression. \texttt{b} is a Boolean variable. If \texttt{b} is true, the expression produces the element \texttt{Constant(1)}, otherwise it produces the element \texttt{Select(0.4 -> 2, 0.6 -> 3)}. Note that this is a Scala expression, not Figaro's conditional data structure (all Figaro classes are capitalized). Now, +is a Scala expression. \texttt{b} is a Boolean variable. If \texttt{b} is true, the expression produces the element \texttt{Constant(1)}, otherwise it produces the element \texttt{Select(0.4 -> 2, 0.6 -> 3)}. Note that this is a Scala expression, not Figaro's conditional data structure (all Figaro classes are capitalized). Now: \begin{flushleft} \texttt{(b: Boolean) => @@ -113,12 +114,13 @@ \section{Chain} This is exactly the same model as that represented by the conditional element in the previous subsection. It is easy to see that any conditional can be represented by a chain in a similar way. Chaining is in fact an extremely powerful concept and we will see a number of examples of it in this tutorial. It is sufficient to represent all compound elements. All the compound elements in the previous section can be represented using a chain, and many of them are actually implemented that way. Note that there is a version of \texttt{Chain} that utilizes two parents and requires a function from a tuple of the parent types to the output type. If more parents are required for a \texttt{Chain}, multiple \texttt{Chains} can be nested together. -\section{Apply and Inject} +% \section{Apply and Inject} +\section{Apply} -Another useful tool for building elements is \texttt{Apply.Apply} serves to lift Scala functions that operate on values to Figaro elements. For example, +Another useful tool for building elements is \texttt{Apply. Apply} serves to lift Scala functions that operate on values to Figaro elements. For example: \begin{flushleft} -\marginpar{Figaro \texttt{Apply} is a class, different than the Scala apply which is a method defined on many classes} +\marginpar{Figaro \texttt{Apply} is a class, different than the Scala \texttt{apply} which is a method defined on many classes} \texttt{(i: Int) => i + 5} \end{flushleft} @@ -132,44 +134,44 @@ \section{Apply and Inject} \marginpar{Sequences in Scala are similar to Java. \texttt{Seq} is the superclass in Scala for many types of data structures, such as \texttt{List}.} -Often, one needs to apply a function to a whole sequence of arguments. \texttt{Inject} is provided for this. \texttt{Inject} takes a sequence of elements with value type \texttt{T} and produces an element whose value type is sequences of values of type \texttt{T}. In Scala notation, \texttt{Inject} takes a variable number of arguments of type \texttt{Element[T]} and produces an \texttt{Element[Seq[T]]}. The name "inject" refers to the fact that the sequence of arguments is injected inside the element. +% Often, one needs to apply a function to a whole sequence of arguments. \texttt{Inject} is provided for this. \texttt{Inject} takes a sequence of elements with value type \texttt{T} and produces an element whose value type is sequences of values of type \texttt{T}. In Scala notation, \texttt{Inject} takes a variable number of arguments of type \texttt{Element[T]} and produces an \texttt{Element[Seq[T]]}. The name "inject" refers to the fact that the sequence of arguments is injected inside the element. -In Scala, a variable number of arguments are simply listed as arguments. For example, suppose we have the elements \texttt{Constant(1)} and \texttt{Select(0.2 -> 2, 0.8 -> 3)}. Then +% In Scala, a variable number of arguments are simply listed as arguments. For example, suppose we have the elements \texttt{Constant(1)} and \texttt{Select(0.2 -> 2, 0.8 -> 3)}. Then -\begin{flushleft} -\marginpar{The notation: \_* will turn any Scala sequence into a variable argument list. The function receiving the variable argument list will have a * on the last argument} -\texttt{Inject(Constant(1), Select(0.2 -> 2, 0.8 -> 3))} -\end{flushleft} +% \begin{flushleft} +% \marginpar{The notation: \_* will turn any Scala sequence into a variable argument list. The function receiving the variable argument list will have a * on the last argument} +% \texttt{Inject(Constant(1), Select(0.2 -> 2, 0.8 -> 3))} +% \end{flushleft} -represents the probabilistic model that produces the sequence (1, 2) with probability -0.2 and (1, 3) with probability 0.8. +% represents the probabilistic model that produces the sequence (1, 2) with probability +% 0.2 and (1, 3) with probability 0.8. -If you already have a sequence, it can be turned into a variable argument list using the notation :\_*. For example, you can use +% If you already have a sequence, it can be turned into a variable argument list using the notation :\_*. For example, you can use -\begin{flushleft} -\texttt{Inject(List(Constant(1), Select(0.2 -> 2, 0.8 -> 3)):\_*)} -\end{flushleft} +% \begin{flushleft} +% \texttt{Inject(List(Constant(1), Select(0.2 -> 2, 0.8 -> 3)):\_*)} +% \end{flushleft} -which has the same effect as listing the individual entries of the \texttt{List} in the argument list. +% which has the same effect as listing the individual entries of the \texttt{List} in the argument list. -Using \texttt{Inject} and \texttt{Apply}, we can apply a function to a sequence of arguments. First, consider the Scala function +% Using \texttt{Inject} and \texttt{Apply}, we can apply a function to a sequence of arguments. First, consider the Scala function -\begin{flushleft} -\marginpar{Calling map on a Scala sequence will apply the same function to every value in the sequence and return a new sequence. It uses anonymous function semantics} -\texttt{(xs: Seq[Int]) => xs.map((x: Int) => x + 5)} -\end{flushleft} +% \begin{flushleft} +% \marginpar{Calling map on a Scala sequence will apply the same function to every value in the sequence and return a new sequence. It uses anonymous function semantics} +% \texttt{(xs: Seq[Int]) => xs.map((x: Int) => x + 5)} +% \end{flushleft} -\texttt{map} is a Scala method that can be applied to sequences. It takes as argument a function on elements of the sequence, applies the function to every member of the sequence, and returns the resulting sequence. So, the above function takes a sequence of integers and adds 5 to every element in the sequence. Now we can write +% \texttt{map} is a Scala method that can be applied to sequences. It takes as argument a function on elements of the sequence, applies the function to every member of the sequence, and returns the resulting sequence. So, the above function takes a sequence of integers and adds 5 to every element in the sequence. Now we can write -\begin{flushleft} -\texttt{Apply(Inject(Constant(1), Select(0.2 -> 2, 0.8 -> 3)), -\newline \tab (xs: Seq[Int]) => xs.map((x: Int) => x + 5))} -\end{flushleft} +% \begin{flushleft} +% \texttt{Apply(Inject(Constant(1), Select(0.2 -> 2, 0.8 -> 3)), +% \newline \tab (xs: Seq[Int]) => xs.map((x: Int) => x + 5))} +% \end{flushleft} -This represents the probabilistic model that produces the sequence (6, 7) with probability 0.2 and (6, 8)with probability 0.8. +% This represents the probabilistic model that produces the sequence (6, 7) with probability 0.2 and (6, 8)with probability 0.8. -Finally, there are a variety of operators and functions that are defined using \texttt{Apply}. For example +There are a variety of operators and functions that are defined using \texttt{Apply}. For example: \begin{itemize} \item \textasciicircum \textasciicircum \ creates tuples. For example, \textasciicircum \textasciicircum(x, y) where \texttt{x} and \texttt{y} are elements, creates an element of pairs. \textasciicircum \textasciicircum \ is defined for up to five arguments. The arguments can have different value types. \item If \texttt{x} is an element whose value type is a \texttt{tuple}, x.\_1 is an element that corresponds to extracting the first component of \texttt{x}. Similarly for \_2, \_3, \_4, and \_5. @@ -177,6 +179,29 @@ \section{Apply and Inject} \item A standard set of Boolean and arithmetic operators is provided. \end{itemize} +\section{Processes and containers} + +New to Figaro 3.0 is a collections library. The general trait of Figaro collections is \texttt{Process}, which represents a possibly infinite collection of random variables. Formally, a \texttt{Process} is a mapping from an index set to an element. A \texttt{Process} is parameterized by two types: the type of the indices and the type of the values of the elements in the collection. The \texttt{Process} is an extremely general class that can be used to represent things like Gaussian processes or continuous time Markov processes. + +When creating a \texttt{Process}, you need to specify how elements in the collection are generated given an index. Not only that, in some collections, the elements are dependent. Therefore, the \texttt{Process} class contains a method to generate elements for many indices simultaneously, including the dependencies between them. This method must also be provided by the user. If all the elements are independent, you can use the \texttt{IndependentProcess} trait to specify this method. + +There are a number of operations that are defined on every process. These include: +\begin{itemize} +\item Getting the element at an index. If \texttt{p} is a \texttt{Process[Int, Double]}, \texttt{p(5)} gets the \texttt{Element[Double]} at index 5. This method throws \texttt{IndexOutOfRangeException} if no element is defined at index 5. +\item Getting elements at many indices simultaneously, for example, using \texttt{p(List(4,5,6))}. This method can also throw \texttt{IndexOutOf\-RangeException}. The method creates a Scala \texttt{Map} from indices to elements. Any elements representing dependencies between the elements at these indices are also created but they are not returned by this method. +\item Safely getting an optional element at an index. \texttt{p.get(5)} will return an \texttt{Element[Option[Double]]}. This element will always have value \texttt{None} if no element is defined at index 5. +\item Safely getting an optional element at many indices. +\item Mapping the values of every element in the process through a function. For example, \texttt{p.map(\_ > 0)} will produce a \texttt{Process[Int, Boolean]}. +\item Chaining the value of every element in the process through a function that returns an element. For example, \texttt{p.chain(Normal(\_, 1))} will produce a new collection in which every element is normally distributed with mean equal to the value of the corresponding element in the original process. +\end{itemize} + +If you have a finite index set, you can use a \texttt{Container}, which takes a sequence of indices. Because they are finite, containers have many more operations defined on them, including a variety of folds and aggregates. See the Scaladoc for the available operations. + +A specific kind of container is a \texttt{FixedSizeArray}, which takes the number of elements as the first argument and a function that generates an element for a given index as the second argument. For example, \texttt{new FixedSizeArray(10, (i: Int) => Flip(1.0 / (i + 1)))} creates a container of ten Boolean elements. + +There is a \texttt{Container} constructor that takes any number of elements and produces a container with those elements. For example, \texttt{Container(Flip(0.2), Flip(0.4))} creates a container consisting of the two elements. +You can, naturally, have elements whose values are processes or containers. Figaro provides the \texttt{ProcessElement} and \texttt{ContainerElement} classes to represent these. Similar operations are defined for \texttt{ProcessEl\-ement} and \texttt{ContainerElement} as for processes and containers. +\texttt{VariableSizeArray} represents a collection of an unknown number of elements, where the number is itself defined by an element. It takes two arguments, the number element, and a function that generates an element for a given index, like a fixed size array. For example, \texttt{VariableSizeArray(Binomial(20, 0.5), (i: Int) => Flip(1.0 / (i + 1))} creates a container of between 0 and 20 Boolean elements. diff --git a/FigaroLaTeX/Tutorial/Sections/4CreatingModels.tex b/FigaroLaTeX/Tutorial/Sections/4CreatingModels.tex index bab17c6f..b5b17697 100644 --- a/FigaroLaTeX/Tutorial/Sections/4CreatingModels.tex +++ b/FigaroLaTeX/Tutorial/Sections/4CreatingModels.tex @@ -29,7 +29,7 @@ \section{Basic models} \newline val y = x === x} \end{flushleft} -Although we don't know the value, \texttt{x} must produce the same value on both sides of the equality test. Therefore, \texttt{y} produces the value \texttt{true} with probability 1. In contrast, in +Although we don't know the value, \texttt{x} must produce the same value on both sides of the equality test. Therefore, \texttt{y} produces the value \texttt{true} with probability 1.0. In contrast, in: \begin{flushleft} \texttt{val y = Flip(0.5) === Flip(0.5)} @@ -64,16 +64,16 @@ \section{Basic models} \begin{flushleft} \texttt{import com.cra.figaro.language.\_} -\newline \texttt{import com.cra.figaro.library.compound.RichCPD} +\newline \texttt{import com.cra.figaro.library.compound.\_} \newline \texttt{val x1 = Select(0.1 -> 1, 0.2 -> 2, 0.3 -> 3, 0.4 -> 4)} \newline \texttt{val x2 = Flip(0.6) -\newline val x3 = Constant (5) +\newline val x3 = Constant(5) \newline val x4 = Flip(0.8) \newline val y = RichCPD(x1, x2, x3, x4, \newline \tab (OneOf(1, 2), *, OneOf(5), *) -> Flip(0.1), \newline \tab (NoneOf(4), OneOf(false), *, *) -> Flip(0.7), \newline \tab (*, *, NoneOf(6, 7), OneOf(true)) -> Flip(0.9), -\newline \tab (*, *, *, OneOf(false)) -> Constant (true))} +\newline \tab (*, *, *, OneOf(false)) -> Constant(true))} \end{flushleft} A particular combination of values of the parents is matched against each row in turn, and the first match is chosen. For example, the combination (1, false, 5, true) matches the first three rows, so the first result (\texttt{Flip(0.1)}) is chosen. All possible values of the parent still need to be accounted for in the argument list using a combination of \texttt{OneOf}, \texttt{NoneOf} and *. @@ -97,7 +97,7 @@ \section{Conditions and constraints} \texttt{x1.addCondition((i: Int) => i \% 2 == 1)} \end{flushleft} -The \texttt{observe} method provides an easy way to specify a condition that only allows a single value. For example, to specify that \texttt{x1} must have the value 2, we can use +The \texttt{observe} method provides an easy way to specify a condition that only allows a single value. For example, to specify that \texttt{x1} must have the value 2, we can use: \begin{flushleft} \texttt{x1.observe(2)} @@ -107,7 +107,9 @@ \section{Conditions and constraints} A \emph{constraint} provides a way to specify a potential or weighting over an element. It is a function from a value of the element to a Double, so if the element has type \texttt{Element[T]}, the constraint is of type \texttt{T => Double}. -Constraints serve multiple purposes in Figaro. One is to specify soft evidence on an element. For example, if in the above Bayesian network we think we heard John call but we're not sure, we might introduce the constraint +Constraint values should always be non-negative. Also, although it is not strictly enforced, we recommend that constraint values be at most 1. Some algorithms compute upper bounds on probabilities and need to assume an upper bound on constraint values. An upper bound of 1 is assumed, so if the actual value can be higher, these algorithms will be incorrect. For algorithms that don't compute upper bounds in this way, it doesn't matter. Currently, the only algorithms that compute upper bounds like this are the lazy factored inference algorithms. If you have a constraint value greater than 1, a warning will be issued. + +Constraints serve multiple purposes in Figaro. One is to specify soft evidence on an element. For example, if in the above Bayesian network we think we heard John call but we're not sure, we might introduce the constraint: \begin{flushleft} \texttt{johnCalls.setConstraint((b: Boolean) => @@ -141,7 +143,7 @@ \section{Conditions and constraints} The next line is interesting. It allows us to identify the bid of the winning bidder as an element with a name, even though we don't know who the winner is. We can do this because even though we don't know who the winner is, we can refer to the \texttt{winner} field, and because the value of \texttt{winner}, whatever it is, is a \texttt{Firm} that has a \texttt{bid} field, which is an element that can be referred to. It is important to realize that this \texttt{Chain} does not create a new element but rather refers to the element \texttt{f.bid} that was created previously. -Finally, we introduce the constraint, which says that a winning bid of d has weight \texttt{20 $-$ d}. This means that a winning bid of 5 is 15 times more likely than a winning bid of 19. The effect is to make the winning bid more likely to be low. Note that in this model, the winning bid is not necessarily the lowest bid. For various reasons, the lowest bidder might not win the contract, perhaps because they offer a poor quality service or they don't have the right connections. Using a constraint, the model is specified very simply using a discrete uniform selection and a simple constraint. +Finally, we introduce the constraint, which says that a winning bid of \texttt{d} has weight \texttt{20 $-$ d}. This means that a winning bid of 5 is 15 times more likely than a winning bid of 19. The effect is to make the winning bid more likely to be low. Note that in this model, the winning bid is not necessarily the lowest bid. For various reasons, the lowest bidder might not win the contract, perhaps because they offer a poor quality service or they don't have the right connections. Using a constraint, the model is specified very simply using a discrete uniform selection and a simple constraint. Constraints are also useful for expressing undirected models such as relational Markov networks or Markov logic networks. To illustrate, we will use a version of the friends and smokers example. This example involves a number of people and their smoking habits. People have some propensity to smoke, and people are likely to have the same smoking habit as their friends. @@ -270,7 +272,7 @@ \section{Mutable fields} Up to this point, all our Figaro programs have been purely functional. All elements have been defined by a \texttt{val}, and they have been immutable. In principle, all programs can be written in a purely functional style. However, this can make it quite inconvenient to represent situations in which different entities refer to each other. Scala supports both functional and non-functional styles of programming, allowing us to gain the benefits of both. -For example, let's expand the actors and movies example so that actors have a skill, and the quality of a movie depends on the skill of the actors in it. In turn, the fame of an actor depends on the quality of the movies in which he or she has appeared. We have created a mutual dependence of actors on movies which is hard to represent in a purely functional style. We can capture it in Figaro using the following code: +For example, let's expand the actors and movies example so that actors have a skill, and the quality of a movie depends on the skill of the actors in it. In turn, the fame of an actor depends on the quality of the movies in which he or she has appeared. We have created a mutual dependence of actors on movies which is hard to represent in a purely functional style. We can capture it in Figaro using the following code. This code also illustrates a use of Figaro collections. \begin{flushleft} \marginpar{This example can be found in MutableMovie.scala} @@ -280,21 +282,22 @@ \section{Mutable fields} \newline class Actor \{ \newline \tab var movies: List[Movie] = List() \newline \tab lazy val skillful = Flip(0.1) -\newline \tab lazy val famous = -\newline \tab Flip(Apply(Inject(movies.map(\_.quality):\_*), probFamous \_)) -\newline \tab private def probFamous(qualities: Seq[Symbol]) = -\newline \tab if (qualities.count(\_ == 'high) >= 2) 0.8; else 0.1 +\newline \tab lazy val qualities = Container(movies.map(\_.quality):\_*) +\newline \tab lazy val numGoodMovies = qualities.count(\_ == 'high) +\newline \tab lazy val famous = Chain(numGoodMovies, (n: Int) => +\newline \tab if (n >= 2) Flip(0.8) else Flip(0.1)) \newline \} \newline \newline class Movie \{ \newline \tab var actors: List[Actor] = List() -\newline \tab lazy val actorsAllGood = Apply(Inject(actors.map(\_.skillful):\_*), (s: Seq[Boolean]) => !(s.contains(false))) -\newline lazy val probLow = -\newline Apply(actorsAllGood, (b: Boolean) => if (b) 0.2; else 0.5) -\newline lazy val probHigh = -\newline Apply(actorsAllGood, (b: Boolean) => if (b) 0.5; else 0.2) -\newline lazy val quality = -\newline Select(probLow -> 'low, Constant(0.3) -> 'medium, probHigh -> 'high) +\newline \tab lazy val skills = Container(actors.map(\_.skillful):\_*) +\newline \tab lazy val actorsAllGood = skills.exists(b => b) +\newline \tab lazy val probLow = +\newline \tab Apply(actorsAllGood, (b: Boolean) => if (b) 0.2; else 0.5) +\newline \tab lazy val probHigh = +\newline \tab Apply(actorsAllGood, (b: Boolean) => if (b) 0.5; else 0.2) +\newline \tab lazy val quality = +\newline \tab Select(probLow -> 'low, Constant(0.3) -> 'medium, probHigh -> 'high) \newline \} \newline \newline class Appearance(actor: Actor, movie: Movie) \{ @@ -338,20 +341,20 @@ \section{Mutable fields} The initial value of both \texttt{movies} and \texttt{actors} is an empty list. We add elements to them later. In fact, whenever we create an appearance, we make sure to add the movie to the actor's list of movies and vice versa. This is achieved by the first two lines of the \texttt{Appearance} class. The notation \texttt{actor.movies ::= movie} is short for \texttt{actor.movies = movie :: actor.movies}, which prepends movie to the current \texttt{actor.movies} list, and replaces the current list with the new list. The \texttt{::=} notation is a variant of the familiar \texttt{+=} notation common in many languages. -The \texttt{Actor} class has \texttt{skillful} and \texttt{famous} fields. Rather than an ordinary \texttt{val}, each of these fields is defined to be \texttt{lazy val}, which means that their contents are not determined until they are required by some other computation. This is necessary for us because their contents can depend on the list of movies the actor appears in. For example, whether the actor is famous depends on whether at least two movies have high quality, as defined by \texttt{probFamous}. (Comment on the notation: the underscore after \texttt{probFamous} is required here to tell Scala that what is desired is the \texttt{probFamous} function itself, not its application to arguments.) If \texttt{famous} was an ordinary \texttt{val}, its value (an \texttt{Element[Boolean]}) would be computed at the point it is defined, so it would use an empty list of movies. Because we want to use the correct list of movies in defining it, we postpone evaluating it until the movies list has been filled. For \texttt{actor3}, this will happen when we make the observation \texttt{actor3.famous.observe(true)}, which we make sure to delay until after all the appearances have been created. For other actors, the \texttt{famous} field will be evaluated even later, during inference. Care should be taken with declaring elements as lazy. Side effects and unintended consequences can occur if a lazy element declared outside a Chain is first required (i.e., created) during the execution of a Chain. +The \texttt{Actor} class has \texttt{skillful} and \texttt{famous} fields. Rather than an ordinary \texttt{val}, each of these fields is defined to be \texttt{lazy val}, which means that their contents are not determined until they are required by some other computation. This is necessary for us because their contents can depend on the list of movies the actor appears in. For example, whether the actor is famous depends on whether at least two movies have high quality. If \texttt{famous} was an ordinary \texttt{val}, its value (an \texttt{Element[Boolean]}) would be computed at the point it is defined, so it would use an empty list of movies. Because we want to use the correct list of movies in defining it, we postpone evaluating it until the movies list has been filled. For \texttt{actor3}, this will happen when we make the observation \texttt{actor3.famous.observe(true)}, which we make sure to delay until after all the appearances have been created. For other actors, the \texttt{famous} field will be evaluated even later, during inference. Care should be taken with declaring elements as lazy. Side effects and unintended consequences can occur if a lazy element declared outside a \texttt{Chain} is first required (i.e., created) during the execution of a \texttt{Chain}. Do not hesitate to use mutation if it will help you organize your program in a logical way. In one application, we have found it convenient to use a hash table that maps concepts to their associated elements. This allowed us to create the element associated with a concept as the concept was introduced. If we later had to refer to the same concept again, we could easily access its element. \section{Universes} -A central concept in Figaro is a \emph{universe}. A universe is simply a collection of elements. Reasoning algorithms operate on a universe (or, as we shall see for dependent universe reasoning, on multiple connected universes). Most of the time while using Figaro, you will not need to create a new universe and can rely on the default universe, which is just called \texttt{universe}. It can be accessed using +A central concept in Figaro is a \emph{universe}. A universe is simply a collection of elements. Reasoning algorithms operate on a universe (or, as we shall see for dependent universe reasoning, on multiple connected universes). Most of the time while using Figaro, you will not need to create a new universe and can rely on the default universe, which is just called \texttt{universe}. It can be accessed using: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ \newline import com.cra.figaro.language.Universe.\_} \end{flushleft} -If you do need a different universe, you can call \texttt{Universe.create\-New()}. This creates a new universe and sets the default universe to it. If you are going to need the old default universe, you will need a way to refer to it. You could use +If you do need a different universe, you can call \texttt{Universe.create\-New()}. This creates a new universe and sets the default universe to it. If you are going to need the old default universe, you will need a way to refer to it. You could use: \begin{flushleft} \texttt{val u1 = Universe.universe @@ -406,7 +409,7 @@ \section{Names, element collections, and references} } \end{flushleft} -In the first line we make the \texttt{Car} class inherit from \texttt{ElementCollect\-ion}, so that every instance of \texttt{Car} is an element collection. In the fourth line, we assign engine the name "engine" and add it to the instance of Car being created, which is referred to by \texttt{this} within the \texttt{Car} class. We similarly make the abstract \texttt{Engine} class inherit element collections and assign \texttt{power} the name "power" within \texttt{V*, V6, and MySuperEngine} within each subclass of \texttt{Engine}. +In the first line we make the \texttt{Car} class inherit from \texttt{ElementCollect\-ion}, so that every instance of \texttt{Car} is an element collection. In the last line, we assign engine the name "engine" and add it to the instance of \texttt{Car} being created, which is referred to by \texttt{this} within the \texttt{Car} class. We similarly make the abstract \texttt{Engine} class inherit element collections and assign \texttt{power} the name "power" within \texttt{V8, V6, and MySuperEngine} within each subclass of \texttt{Engine}. An element collection, like a universe, is simply a set of elements. The difference is that a universe is also a set of elements on which a reasoning algorithm operates. An element collection provides the ability to refer to an element by name. For example, if \texttt{car} is an instance of \texttt{Car}, we can use \texttt{car.get[Engine]("engine")} to get at the element named "engine". The \texttt{get} method takes a type parameter, which is the value type of the element being referred to. The notation \texttt{[Engine]} specifies this type parameter, and serves to make sure that the expression \texttt{car.get[Engine]("engine")} has type \texttt{Element[Engine]}. @@ -415,7 +418,6 @@ \section{Names, element collections, and references} So, finally, the answer to our puzzle is that in place of the question mark, we put \texttt{get[Symbol]("engine.power")}. This applies the get method to the instance of \texttt{Car} being created. Here is the full example: \begin{flushleft} -\marginpar{This example can be found in MultiValuedReferenceUncertainty.scala} \texttt{import com.cra.figaro.language.\_ \newline \newline abstract class Engine extends ElementCollection \{ @@ -446,6 +448,7 @@ \section{Multi-valued references and aggregates} The previous subsection described how to refer to elements using references that identify a single element. A feature of PRMs is the ability to define multi-valued relationships, where an entity is related to multiple entities via an attribute. In Figaro, we use multi-valued references and aggregates to capture these kinds of situations. For example: +\marginpar{This example can be found in MultiValuedReferenceUncertainty.scala} \begin{flushleft} \texttt{import com.cra.figaro.language.\_ \newline @@ -477,45 +480,45 @@ \section{Multi-valued references and aggregates} A comment on the code: The code defining \texttt{sum} might look mysterious. This code takes a list of integers and returns their sum. This is a standard Scala idiom that unfortunately is a bit obscure if you're not familiar with it. It is used to "fold" a function through a list. We begin with 0 and then repeatedly add the current result to the next element of the list until the list is exhausted. The notation \texttt{(\_ + \_)} is shorthand for the function that takes two arguments and adds them. The notation \texttt{(0 /: xs}) means that this function should be folded through \texttt{xs}, starting from 0. -\section{Open Universe Models} - -We close this section by showing how Figaro can be used to represent "open universe" situations. An open universe situation is one in which we don't know what objects are there, how many there are, which objects are the same as which other objects, and so on. In our example situation, there are an unknown number of sources that is geometrically distributed. Each source is uniformly distributed between 0 and 1. There is some number of observed samples, each drawn from a single unknown source. This is the classic data association problem in which we want to determine which sample comes from which source, and in particular which two samples actually come from the same source. The Figaro code for the example is as follows: - -\begin{flushleft} -\marginpar{This example can be found in OpenUniverse.scala} -\texttt{import com.cra.figaro.language.\_ -\newline import com.cra.figaro.library.atomic.continuous.Uniform -\newline import com.cra.figaro.library.atomic.continuous.Normal -\newline import com.cra.figaro.library.atomic.discrete.Geometric -\newline import com.cra.figaro.library.compound.\{MakeList, IntSelector\} -\newline -\newline def source(): Element[Double] = Uniform(0.0, 1.0) -\newline -\newline val numSources = Geometric(0.9) -\newline -\newline val sources = MakeList(numSources, source \_) -\newline -\newline class Sample \{ -\newline \tab val sourceNum = IntSelector(numSources) -\newline \tab val source = -\newline \tab Apply(sources, sourceNum, (s: Seq[Double], i: Int) => s(i)) -\newline \tab val position = -\newline \tab NonCachingChain(source, (x: Double) => Normal(x, 1.0)) -\newline \} -\newline -\newline val sample1 = new Sample -\newline val sample2 = new Sample -\newline -\newline val equal = sample1.source === sample2.source -\newline -\newline sample1.position.addCondition((y: Double) => y >= 0.7 \&\& y < 0.8) -\newline sample2.position.addCondition((y: Double) => y >= 0.7 \&\& y < 0.8) -} -\end{flushleft} - -Most of this should be self-explanatory at this point. There are a couple of interesting new element classes being used. \texttt{MakeList} takes an element over integers and a function that generates elements over a certain type (in this case doubles). It returns an element over lists of the appropriate type (in this case lists of doubles) whose length is distributed according to the first argument and in which each element is generated according to the second argument. In our example, \texttt{sources} is a list of sources whose length is geometrically distributed and in which each source is generated according to the source model. A notable aspect of the \texttt{MakeList} class, which is important for reasoning algorithms, is that the elements generating the values in the list are stored as an infinite lazy stream. Depending on the value of the first argument, the value of the \texttt{MakeList} is a finite prefix of the values of elements in the stream. As a result of this design, we don't create a completely fresh list each time the length of the list changes. \texttt{MakeList} could also have been used in the previous section's example to define the \texttt{components} element of the \texttt{Container} class. - -The second new element class is \texttt{IntSelector} which takes an element over integers and returns an element that produces uniformly a number between 0 and the value of its argument (exclusive). This element can be used to generate a random index into a list produced by \texttt{MakeList}. \texttt{IntSelector} also has an interesting implementation that has benefits for reasoning algorithms (especially Metropolis- Hastings). The \texttt{Randomness} is an infinite stream of uniformly distributed doubles between 0 and 1. Given a particular value of the integer argument, the selected index is the one with the highest randomness value in the finite portion of the stream defined by the argument. +% \section{Open Universe Models} + +% We close this section by showing how Figaro can be used to represent "open universe" situations. An open universe situation is one in which we don't know what objects are there, how many there are, which objects are the same as which other objects, and so on. In our example situation, there are an unknown number of sources that is geometrically distributed. Each source is uniformly distributed between 0 and 1. There is some number of observed samples, each drawn from a single unknown source. This is the classic data association problem in which we want to determine which sample comes from which source, and in particular which two samples actually come from the same source. The Figaro code for the example is as follows: + +% \begin{flushleft} +% \marginpar{This example can be found in OpenUniverse.scala} +% \texttt{import com.cra.figaro.language.\_ +% \newline import com.cra.figaro.library.atomic.continuous.Uniform +% \newline import com.cra.figaro.library.atomic.continuous.Normal +% \newline import com.cra.figaro.library.atomic.discrete.Geometric +% \newline import com.cra.figaro.library.compound.\{MakeList, IntSelector\} +% \newline +% \newline def source(): Element[Double] = Uniform(0.0, 1.0) +% \newline +% \newline val numSources = Geometric(0.9) +% \newline +% \newline val sources = MakeList(numSources, source \_) +% \newline +% \newline class Sample \{ +% \newline \tab val sourceNum = IntSelector(numSources) +% \newline \tab val source = +% \newline \tab Apply(sources, sourceNum, (s: Seq[Double], i: Int) => s(i)) +% \newline \tab val position = +% \newline \tab NonCachingChain(source, (x: Double) => Normal(x, 1.0)) +% \newline \} +% \newline +% \newline val sample1 = new Sample +% \newline val sample2 = new Sample +% \newline +% \newline val equal = sample1.source === sample2.source +% \newline +% \newline sample1.position.addCondition((y: Double) => y >= 0.7 \&\& y < 0.8) +% \newline sample2.position.addCondition((y: Double) => y >= 0.7 \&\& y < 0.8) +% } +% \end{flushleft} + +% Most of this should be self-explanatory at this point. There are a couple of interesting new element classes being used. \texttt{MakeList} takes an element over integers and a function that generates elements over a certain type (in this case doubles). It returns an element over lists of the appropriate type (in this case lists of doubles) whose length is distributed according to the first argument and in which each element is generated according to the second argument. In our example, \texttt{sources} is a list of sources whose length is geometrically distributed and in which each source is generated according to the source model. A notable aspect of the \texttt{MakeList} class, which is important for reasoning algorithms, is that the elements generating the values in the list are stored as an infinite lazy stream. Depending on the value of the first argument, the value of the \texttt{MakeList} is a finite prefix of the values of elements in the stream. As a result of this design, we don't create a completely fresh list each time the length of the list changes. \texttt{MakeList} could also have been used in the previous section's example to define the \texttt{components} element of the \texttt{Container} class. + +% The second new element class is \texttt{IntSelector} which takes an element over integers and returns an element that produces uniformly a number between 0 and the value of its argument (exclusive). This element can be used to generate a random index into a list produced by \texttt{MakeList}. \texttt{IntSelector} also has an interesting implementation that has benefits for reasoning algorithms (especially Metropolis- Hastings). The \texttt{Randomness} is an infinite stream of uniformly distributed doubles between 0 and 1. Given a particular value of the integer argument, the selected index is the one with the highest randomness value in the finite portion of the stream defined by the argument. diff --git a/FigaroLaTeX/Tutorial/Sections/5Reasoning.tex b/FigaroLaTeX/Tutorial/Sections/5Reasoning.tex index 76e7b12a..3e78ab14 100644 --- a/FigaroLaTeX/Tutorial/Sections/5Reasoning.tex +++ b/FigaroLaTeX/Tutorial/Sections/5Reasoning.tex @@ -14,20 +14,20 @@ \section{Computing ranges} For (2), most built in element classes have a finite number of possible values. Exceptions are the atomic continuous classes like \texttt{Uniform} and \texttt{Normal}. -To compute the values of elements in universe \texttt{u}, you first create a \texttt{Values} object using +To compute the values of elements in universe \texttt{u}, you first create a \texttt{Values} object using: \begin{flushleft} \texttt{import com.cra.figaro.algorithm.\_ \newline val values = Values(u)} \end{flushleft} -You can also create a \texttt{Values} object for the current universe simply with +You can also create a \texttt{Values} object for the current universe simply with: \begin{flushleft} \texttt{val values = Values()} \end{flushleft} -\texttt{values} can then be used to get the possible values of any object. For example, +\texttt{values} can then be used to get the possible values of any object. For example: \begin{flushleft} \texttt{val e1 = Flip(0.7) @@ -46,11 +46,11 @@ \section{Asserting evidence} There are a variety of situations where using named evidence is beneficial. One might have a situation where the actual element referred to by a reference is uncertain, so we can't directly specify a condition or constraint on the element, but by associating the evidence with the reference, we can ensure that it is applied correctly. Names also allow us to keep track of and apply evidence to elements that correspond to the same object in different universes, as will be seen below with dynamic reasoning. Finally, associating evidence with names and references allows us to keep the evidence separate from the definition of the probabilistic model, which is not achieved by conditions and constraints. -Named evidence is specified by +Named evidence is specified by: \newline \texttt{NamedEvidence(reference, evidence)} \newline where \texttt{reference} is a reference, and \texttt{evidence} is an instance of the \texttt{Evidence} class. There are three concrete subclasses of \texttt{Evidence: Condit\-ion, Constraint, and Observation}, which behave like an element's \texttt{setCond\-ition, setConstraint}, and \texttt{observe} methods respectively. -For example, +For example: \newline \texttt{NamedEvidence("car.size", Condition((s: Symbol) => s != 'small\-)))} \newline represents the evidence that the element referred to by \texttt{"car.size"} does not have value \texttt{'small}. @@ -63,9 +63,9 @@ \section{Exact inference using variable elimination} \item Apply variable elimination to all the factors. \end{enumerate} -Step 1, like for range computation, requires that the expansion of the universe terminate in a finite amount of time. Step 2 requires that each element be of a class that can be converted into a set of factors. Every built-in class can be converted into a set of factors except for atomic continuous classes with infinite range, although see later in the section on abstractions how to make variable elimination work for continuous classes. Also see later, in the section on creating a new element class, how to specify a way to convert a new class into a set of factors. +Step 1, like for range computation, requires that the expansion of the universe terminate in a finite amount of time. Step 2 requires that each element be of a class that can be converted into a set of factors. Every built-in class can be converted into a set of exact factors. Atomic continuous elements with infinite range are handled in one of two ways. As discussed later in the section, abstractions can be used to make variable elimination work for continuous classes. If no abstractions are defined for continuous elements, then each continuous element is sampled and a factor is created from the samples. Figaro outputs a warning in this instance to ensure the user intends to use a continuous variable in a factored algorithm. Also see later, in the section on creating a new element class, how to specify a way to convert a new class into a set of factors. -To use variable elimination, you need to specify a set of query elements whose conditional probability you want to compute given the evidence. For example, +To use variable elimination, you need to specify a set of query elements whose conditional probability you want to compute given the evidence. For example: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -79,7 +79,7 @@ \section{Exact inference using variable elimination} \newline val ve = VariableElimination(e2)} \end{flushleft} -This will create a \texttt{VariableElimination} object that will apply variable elimination to the universe containing \texttt{e1, e2, and e3}, leaving query variable \texttt{e2} uneliminated. However, it won't perform the variable elimination immediately. To tell it to perform variable elimination, you have to say +This will create a \texttt{VariableElimination} object that will apply variable elimination to the universe containing \texttt{e1, e2, and e3}, leaving query variable \texttt{e2} uneliminated. However, it won't perform the variable elimination immediately. To tell it to perform variable elimination, you have to say: \begin{flushleft} \texttt{ve.start()} @@ -98,6 +98,12 @@ \section{Exact inference using variable elimination} These methods \texttt{start, kill, distribution, probability}, and \texttt{ex\-pectation} are a uniform interface to all reasoning algorithms that compute the conditional probability of query variables given evidence. We will see below how this interface is extended for anytime algorithms. +For convenience, Figaro also provides a one-line query method using variable elimination. Just use: + +\texttt{VariableElimination.probability(element, value)} + +This will take care of instantiating the algorithm and running inference and returns the probability that the element has the given value. + \section{Approximate inference using belief propagation} Figaro also contains another factored inference algorithm called belief propagation (BP). BP is a message passing algorithm on a factor graph (a bipartite graph of variables and factors). On factor graphs with no loops, BP is an exact inference algorithm. On graphs with loops (loopy factor graph), BP can be used to perform approximate inference on the target variables. Note that in Figaro, the way that Chains are converted to factors always produces a loopy factor graph, even if the actual definition of the model contains no loops. Therefore, most inference with BP in Figaro is approximate. @@ -105,12 +111,12 @@ \section{Approximate inference using belief propagation} The algorithm works in three steps: \begin{enumerate} \item Expand the universe to include all elements generated in any possible world. -\item Convert each element into a factor and create a factor graph from the factors -\item Pass messages between the factor nodes and variables nodes for the specified number of iterations -\item Queries are answered on the targets using the posterior distributions computed at each variable node +\item Convert each element into a factor and create a factor graph from the factors. +\item Pass messages between the factor nodes and variables nodes for the specified number of iterations. +\item Queries are answered on the targets using the posterior distributions computed at each variable node. \end{enumerate} -Steps 1 and 2 operate in the same manner as variable elimination, and the same restrictions on factors also applies. Just like in variable elimination, you need to specify a set of query elements whose conditional probability you want to compute given the evidence. For example, +Steps 1 and 2 operate in the same manner as variable elimination, and the same restrictions on factors also applies. Just like in variable elimination, you need to specify a set of query elements whose conditional probability you want to compute given the evidence. For example: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -125,13 +131,21 @@ \section{Approximate inference using belief propagation} \newline val bp = BeliefPropagation(100, e2)} \end{flushleft} -This will create a \texttt{BeliefPropagation} object that will pass messages on a factor graph created from the universe containing \texttt{e1, e2, and e3}. The first argument is the number of iterations to pass messages between the factor and variable nodes. However, it won't perform BP immediately. To tell it to run the algorithm, you have to say +This will create a \texttt{BeliefPropagation} object that will pass messages on a factor graph created from the universe containing \texttt{e1, e2, and e3}. The first argument is the number of iterations to pass messages between the factor and variable nodes. However, it won't perform BP immediately. To tell it to run the algorithm, you have to say: \begin{flushleft} \texttt{bp.start()} \end{flushleft} -When this call terminates, you can use \texttt{bp} to answer the same queries as defined in the variable elimination section. +When this call terminates, you can use \texttt{bp} to answer the same queries as defined in the variable elimination section. You can also use a one-line shortcut like for variable elimination. + +Continuous elements are handled in BP the same was as in variable elimination (abstractions or sampled). + +\section{Lazy factored inference} + +Ordinarily, factored inference algorithms like variable elimination and belief propagation cannot be applied to infinitely recursive models. It's easy to define such models, such as probabilistic grammars for natural language, in Figaro. Figaro provides lazy factored inference algorithms that expand the factor graph to a bounded depth and precisely quantify the effect of the unexplored part of the graph on the query. It uses this information to compute lower and upper bounds on the probability of the query. + +To use lazy variable elimination, create an instance of \texttt{LazyVariable\-Elimination}. You can use the \texttt{pump} method to increase the depth of expansion by 1. You can also use \texttt{run(depth)} to expand to the given depth. You can find an example of lazy variable elimination in action in \texttt{LazyList.scala} in the Figaro examples. You can also use lazy belief propagation. \section{Importance sampling} @@ -139,7 +153,7 @@ \section{Importance sampling} Unlike variable elimination, this algorithm can be applied to models whose expansion produces an infinite number of elements, provided any particular possible world only requires a finite number of elements to be generated. Also, this algorithm works for atomic continuous models. In addition, as an approximate algorithm, it can produce reasonably accurate answers much more quickly than the exact variable elimination. -The interface to importance sampling is very similar to that to variable elimination. For example, +The interface to importance sampling is very similar to that to variable elimination. For example: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -157,7 +171,7 @@ \section{Importance sampling} The importance sampling algorithm used above is an example of a "one-time" algorithm. That is, the algorithm is run for 10,000 iterations and terminates; it cannot be used again. Figaro also provides an "anytime" importance sampling algorithm that runs in a separate thread and continues to accumulate samples until it is stopped. A major benefit of an anytime algorithm is that it can be queried while it is running. Another benefit is that you can tell it how long you want it to run. -Two additional methods are provided in the interface. \texttt{imp.stop()} stops it from accumulating samples, while \texttt{imp.resume()} starts it going again, carrying on from where it left off before. In addition, the kill method has the additional effect of killing the thread, so it is essential that it be called when you are finished with the \texttt{Importance} object. To create an anytime importance algorithm, simply omit the number of samples argument to \texttt{Importance}. A typical way of using anytime importance sampling, allowing it to run for one second, is as follows: +Two additional methods are provided in the interface. \texttt{imp.stop()} stops it from accumulating samples, while \texttt{imp.resume()} starts it going again, carrying on from where it left off before. In addition, the \texttt{kill} method has the additional effect of killing the thread, so it is essential that it be called when you are finished with the \texttt{Importance} object. To create an anytime importance algorithm, simply omit the number of samples argument to \texttt{Importance}. A typical way of using anytime importance sampling, allowing it to run for one second, is as follows: \begin{flushleft} \texttt{val imp = Importance(e2) @@ -168,6 +182,8 @@ \section{Importance sampling} \newline imp.kill() } \end{flushleft} +Importance sampling also provides a one-line query shortcut. + \section{Markov chain Monte Carlo} Figaro provides a Metropolis-Hastings Markov chain Monte Carlo algorithm. Metropolis-Hastings uses a proposal distribution to propose a new state at each step of the algorithm, and either accepts or rejects the proposal. In Figaro, a proposal involves proposing new randomnesses for any number of elements. After proposing these new randomnesses, any element that depends on those randomnesses must have its value updated. Recall that the value of an element is a deterministic function of its randomness and the values of its arguments, so this update process is a deterministic result of the randomness proposal. @@ -177,7 +193,7 @@ \section{Markov chain Monte Carlo} Computing the acceptance probability requires computing the ratio of the element's constraint of the new value divided by the constraint of the old value. Ordinarily, this is achieved by applying the constraint to the new and old value separately and taking the ratio. However, sometimes we want to define a constraint on a large data structure, and applying the constraint to either the new or old value will produce overflow or underflow, so the ratio won't be well defined. It might be the case that the ratio is well defined even though the constraints are large, since only a small part of the data structure changes in a single Metropolis-Hastings situation. For example, we might want to define a constraint on an ordering, penalizing the number of items out of order. The total number of items out of order might be large, but if a single iteration consists of swapping two elements, the number that change might be small. For this reason, an element contains a \texttt{score} method that takes the old value and the new value and produces the ratio of the constraint of the new value to the old value. -Figaro allows the user to specify which elements get proposed using a \emph{proposal scheme}. Figaro also provides a default proposal scheme that simply chooses a non-deterministic element in the universe uniformly at random and proposes a new randomness for it. To create an anytime Metropolis-Hastings algorithm using the default proposal scheme, use +Figaro allows the user to specify which elements get proposed using a \emph{proposal scheme}. Figaro also provides a default proposal scheme that simply chooses a non-deterministic element in the universe uniformly at random and proposes a new randomness for it. To create an anytime Metropolis-Hastings algorithm using the default proposal scheme, use: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -196,6 +212,8 @@ \section{Markov chain Monte Carlo} To use a one-time (i.e., non-anytime) Metropolis-Hastings algorithm, simply provide the number of samples as the first argument. +Metropolis-Hastings also provides a one-line query shortcut. + \subsection{Defining a proposal scheme} A proposal scheme is an instance of the \texttt{ProposalScheme} class. A number of constructs are provided to help define proposal schemes. We will illustrate some of them using the first movie example from the section titled "Classes, instances, and relationships". The default proposal scheme does not work well for this example because it is unlikely to maintain the condition that exactly one appearance is awarded. A better proposal scheme will maintain this condition by always replacing one awarded appearance with another. @@ -241,7 +259,7 @@ \subsection{Defining a proposal scheme} } \end{flushleft} -In general, the proposal scheme argument of \texttt{MetropolisHastings} is actually a function of zero arguments that returns a \texttt{ProposalScheme}. The \texttt{ProposalScheme.default} is just that. Since \texttt{chooseScheme} is the same, it can be passed directly to \texttt{MetropolisHastings}. So we can call +In general, the proposal scheme argument of \texttt{MetropolisHastings} is actually a function of zero arguments that returns a \texttt{ProposalScheme}. The \texttt{ProposalScheme.default} is just that. Since \texttt{chooseScheme} is the same, it can be passed directly to \texttt{MetropolisHastings}. So we can call: \begin{flushleft} \texttt{val alg = @@ -270,11 +288,11 @@ \subsection{Debugging Metropolis-Hastings} In addition, if you have a \texttt{Metropolis-Hastings} object \texttt{mh}, you can define an initial state by setting the values of elements. Then call \texttt{mh.test} and provide it a number of samples to generate. It will repeatedly propose a new state from the initial state and either accept or reject it, restoring to the original state each time. You can provide a sequence of predicates, and it will report how often each predicate was satisfied after one step of Metropolis-Hastings from the initial state. You can also provide a sequence of elements to track, and it will report how often each element is proposed. For example, in the movies example, you could set the initial state to be one in which exactly one appearance is awarded and test the fraction of times this condition holds after one step. -\section{Probability of evidence algorithm} +\section{Probability of evidence algorithms} -The previous three algorithms all computed the conditional probability of query variables given evidence. Sometimes we just want to compute the probability of evidence. Since there is the potential for ambiguity here, Figaro is careful to define what constitutes evidence for computing probability of evidence. Conditions and constraints often constitute evidence. Sometimes, however, they can be considered to be part of the model specification. Consider, for example, the constraint on pairs of friends that they share the same smoking habits — this is part of the model definition, not evidence. +The previous three algorithms all computed the conditional probability of query variables given evidence. Sometimes we just want to compute the probability of evidence. Since there is the potential for ambiguity here, Figaro is careful to define what constitutes evidence for computing probability of evidence. Conditions and constraints often constitute evidence. Sometimes, however, they can be considered to be part of the model specification. Consider, for example, the constraint on pairs of friends that they share the same smoking habits (this is part of the model definition, not evidence). -For this reason, Figaro allows the probability of evidence to be computed in steps. To compute the probability of conditions and constraints that are in the Figaro program, you can use +For this reason, Figaro allows the probability of evidence to be computed in steps. To compute the probability of conditions and constraints that are in the Figaro program, you can use: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -297,13 +315,13 @@ \section{Probability of evidence algorithm} \texttt{ProbEvidenceSampler} will then compute the probability of all the evidence, both the named evidence and the existing evidence in the program. It does this by temporarily asserting the named evidence, running the probability of evidence computation, and then retracting the named evidence. -If you don't want to include the existing conditions and constraints in the program in the probability of evidence calculation, there are four ways to proceed. Each method is more verbose than the previous but provides more control. The simplest is to use +If you don't want to include the existing conditions and constraints in the program in the probability of evidence calculation, there are four ways to proceed. Each method is more verbose than the previous but provides more control. The simplest is to use: \begin{flushleft} \texttt{ProbEvidenceSampler.computeProbEvidence(n, namedEvidence)} \end{flushleft} -This takes care of running the necessary algorithms and returns the probability of the named evidence, treating the existing conditions and constraints as part of the program definition. You can also use the following +This takes care of running the necessary algorithms and returns the probability of the named evidence, treating the existing conditions and constraints as part of the program definition. You can also use the following: \begin{flushleft} \texttt{val alg = ProbEvidenceSampler(n, namedEvidence) @@ -311,7 +329,7 @@ \section{Probability of evidence algorithm} } \end{flushleft} -This method enables you to control when to run \texttt{alg}, and also to reuse \texttt{alg} for different purposes. The final two methods explicitly compute probability of the conditions and constraints in the program, which becomes the denominator for subsequent probability of evidence computations. The \texttt{ProbEvidenceSampler} class provides a method called \texttt{probAdditionalEvidence} that creates a new algorithm that uses the probability of evidence of the current algorithm as denominator. You could proceed as follows +This method enables you to control when to run \texttt{alg}, and also to reuse \texttt{alg} for different purposes. The final two methods explicitly compute probability of the conditions and constraints in the program, which becomes the denominator for subsequent probability of evidence computations. The \texttt{ProbEvidenceSampler} class provides a method called \texttt{probAdditionalEvidence} that creates a new algorithm that uses the probability of evidence of the current algorithm as denominator. You could proceed as follows: \begin{flushleft} \texttt{val alg1 = new ProbEvidenceSampler(universe) with @@ -336,17 +354,81 @@ \section{Probability of evidence algorithm} In this example, a different number of samples is used for the initial denominator calculation and the subsequent probability of evidence calculation. -Currently, only forward sampling probability of evidence is provided, but the framework exists to create additional probability of evidence algorithms. There is also an anytime version of the probability of evidence algorithms. To create one, use +There is also an anytime version of the probability of evidence algorithm forward sampling algorithm. To create one, use: \begin{flushleft} \texttt{new ProbEvidenceSampler(universe) with AnytimeProbEvidenceSampler} \end{flushleft} -For the methods that require you to specify the number of samples \texttt{n}, replace \texttt{n} with \texttt{t}, where \texttt{t} is a long value indicating the number of milliseconds to wait while computing the denominator (and also while computing the probability of the named evidence for the \texttt{computeProbEvidence} shorthand method.) +For the methods that require you to specify the number of samples \texttt{n}, replace \texttt{n} with \texttt{t}, where \texttt{t} is a long value indicating the number of milliseconds to wait while computing the denominator (and also while computing the probability of the named evidence for the \texttt{computeProbEvidence} shorthand method). + +Additionally, the probability of evidence can be computed using algorithms like importance sampling, belief propagation and particle filtering. Examples are shown for the simple model below: + +\begin{flushleft} + \texttt{val universe = Universe.createNew() + \newline val u = Uniform(0.0,0.2,0.4,0.6,0.8,1.0)("u", universe) + \newline val condition = (d: Double) => d < 0.4 + \newline val evidence = List(NamedEvidence("u", Condition(condition))) +} +\end{flushleft} + +This model defines a uniform with six outcomes, and a condition having two satisfying outcomes. + +With belief propagation, we compute the probability of evidence with and without the condition and divide. + +\begin{flushleft} +\texttt{val bp1 = BeliefPropagation(10, u)(universe) + \newline bp1.start + \newline bp1.stop + \newline val withoutCondition = bp1.computeEvidence() + \newline bp1.kill() + \newline + \newline universe.assertEvidence(evidence) + \newline bp2.start + \newline bp2.stop + \newline val withCondition = bp2.computeEvidence() + \newline bp2.kill() + \newline val e1 = withConditionwithoutCondition +} +\end{flushleft} + +For importance sampling, the evidence is provided as an argument to the \texttt{computeProbEvidence} method. + +\begin{flushleft} + \texttt{val importance = Importance(100000, u) + \newline importance.start() + \newline importance.stop() + \newline val e2 = importance.probabilityOfEvidence(evidence) + } +\end{flushleft} + +In particle filtering, the probability of evidence at the current time step can be computed using \texttt{probEvidence()}. + +\begin{flushleft} + \texttt{val pf = ParticleFilter(universe, t, 10000) + \newline pf.start() + \newline val condition = (d: Double) => d < 0.4 + \newline val evidence = List(NamedEvidence("u", Condition(condition))) + \newline pf.advanceTime(evidence) + \newline val e3 = pf.probEvidence() + \newline pf.stop() + \newline pf.kill() + \newline e3 + } +\end{flushleft} + +The result of each computation is approximately .333. + +\texttt{println(e1 + " " + e2 + " " + e3)} yields: + +\begin{flushleft} + \texttt{0.3333333333333333 0.3338586042039474 0.3269} +\end{flushleft} + \section{Computing the most likely values of elements} -Rather than computing a probability distribution over the values of elements given evidence, a natural question to ask is "What are the most likely values of all the elements given the available evidence?" This is known as computing the most probable explanation (MPE) of the evidence. There are two ways to compute MPE: Variable Elimination and Simulated Annealing. An example that shows how to compute the MPE using Variable Elimination is: +Rather than computing a probability distribution over the values of elements given evidence, a natural question to ask is "What are the most likely values of all the elements given the available evidence?" This is known as computing the most probable explanation (MPE) of the evidence. There are two ways to compute MPE: (1) Variable elimination, and (2) Simulated annealing. An example that shows how to compute the MPE using variable elimination is: \begin{flushleft} \texttt{import com.cra.figaro.language.\_ @@ -449,10 +531,10 @@ \section{Reasoning with dependent universes} \texttt{val source1 = new Source("Source 1") \newline val source2 = new Source("Source 2") \newline val source3 = new Source("Source 3") +\newline \newline val sample1 = new Sample("Sample 1") \{ \newline \tab val fromSource = Select(0.5 -> source1, 0.5 -> source2) \newline \} -\newline \newline val sample2 = new Sample("Sample 2") \{ \newline \tab val fromSource = Select(0.3 -> source1, 0.7 -> source3) \newline \} @@ -495,7 +577,7 @@ \section{Reasoning with dependent universes} \newline \} } \end{flushleft} -Observe that each element created in the Pair class is added to the universe of the Pair, not the universe that contains \texttt{sample.fromSource}. Now, we can use variable elimination and condition each of the source assignment on the probability of the evidence in the corresponding dependent universe. To do this, we pass a list of the dependent universes as extra arguments to variable elimination, along +Observe that each element created in the \texttt{Pair} class is added to the universe of the \texttt{Pair}, not the universe that contains \texttt{sample.fromSource}. Now, we can use variable elimination and condition each of the source assignment on the probability of the evidence in the corresponding dependent universe. To do this, we pass a list of the dependent universes as extra arguments to variable elimination, along with a function that provides the algorithm to use to compute the probability of evidence in a dependent universe, as follows: \begin{flushleft} @@ -544,8 +626,7 @@ \section{Abstractions} \newline \newline val ve = VariableElimination(flip) \newline ve.start() -\newline println(ve.probability(flip, true)) -\newline // should print about 0.4 } +\newline println(ve.probability(flip, true)) // should print about 0.4 } \end{flushleft} It is up to individual algorithms to decide whether and to use a pragma such as an abstraction. For example, importance sampling, which has no difficulty with elements with many possible values, ignores abstractions. The process of computing ranges, which is a subroutine of variable elimination and can also be used in other algorithms, does use abstractions. @@ -556,7 +637,7 @@ \section{Reproducing inference results} Running inference on a model is generally a random process, and performing the same inference repeatedly on a model may produce slightly different results. This can sometimes make debugging difficult, as bugs may or may not be encountered, depending on the random values that were generated during inference. For that reason, Figaro has the ability to generate reproducible inference runs. -All elements in Figaro use the same random number generator to retrieve random values. This can be accessed by importing the \texttt{util} Figaro package and using the value random, which is Figaro's random number generator. For example, the \texttt{generateRandomness()} function in the Select element is: +All elements in Figaro use the same random number generator to retrieve random values. This can be accessed by importing the \texttt{util} Figaro package and using the value random, which is Figaro's random number generator. For example, the \texttt{generateRandomness()} function in the \texttt{Select} element is: \begin{flushleft} \texttt{import com.cra.figaro.util.\_ diff --git a/FigaroLaTeX/Tutorial/Sections/6DynamicModels.tex b/FigaroLaTeX/Tutorial/Sections/6DynamicModels.tex index 8fd3c36c..be68f094 100644 --- a/FigaroLaTeX/Tutorial/Sections/6DynamicModels.tex +++ b/FigaroLaTeX/Tutorial/Sections/6DynamicModels.tex @@ -4,7 +4,7 @@ \chapter{Dynamic models and filtering} % Chapter title \label{Dynamic models and filterings} % For referencing the chapter elsewhere, use -Figaro provides constructs to create dynamic probabilistic programs that describe a domain that changes over time. All the power of the language can be used in creating dynamic programs. A dynamic probabilistic program consists of two parts: (1) an initial model, which is a universe, describing the distribution over the initial state, and (2) a transition model, which is a function from a universe representing the distribution at one time point to a universe representing the distribution at the next time point. +Figaro provides constructs to create dynamic probabilistic programs that describe a domain that changes over time. All the power of the language can be used in creating dynamic programs. A dynamic probabilistic program consists of two parts: (1) an initial model, which is a universe, describing the distribution over the initial state, and (2) a transition model, which is a function from a universe representing the distribution at one time point to a universe representing the distribution at the next time point. The transition model may also optionally take a static universe as an input that represents static, non--time dependent variables. The following code shows the typical method for creating initial and transition models: @@ -25,20 +25,22 @@ \chapter{Dynamic models and filtering} % Chapter title We then define the transition model. It takes the previous universe as argument and returns a universe. The first thing it does is create a new universe, which is returned at the end of defining the transition model. It then creates an element named "f" that depends on the previous value of "f". The previous value of "f" is the value of the element named "f" in the previous universe. Note that we can give this new element the same name as the previous element since they are part of different universes. We get at this element using \texttt{previousUniverse.get[Boolean]("f")}. Essentially, when elements in different universes at different time points have the same name, they represent the state of the same variable at different points in time. Using this procedure, we can create any manner of dependency between the previous state and the current state by referring to elements in the previous universe by reference. +There are two dynamic reasoning algorithms available in Figaro: (1) Particle filtering, and (2) factored frontier. + \subsection{Particle filtering} -Currently, the only algorithm provided by Figaro for reasoning about dynamic models is a vanilla particle filter. To create the particle filter, use +To create the particle filter, use: \begin{flushleft} \texttt{val pf = ParticleFilter(initial, trans, numParticles)} \end{flushleft} -where \texttt{intial} is the initial universe, trans is the transition model (a function from \texttt{Universe => Universe}), and \texttt{numParticles} is the number of particles the algorithm should produce at each time step. +where \texttt{initial} is the initial universe, trans is the transition model (a function from \texttt{Universe => Universe}), and \texttt{numParticles} is the number of particles the algorithm should produce at each time step. One tricky aspect about using a particle filter is that the universes are produced by a function, so it is hard to get a handle on them to observe evidence. This problem is solved by the use of named evidence, so we can refer to the correct element without having a handle on the specific universe. To tell the particle filter to create the initial set of particles from the initial model, we call the start method. The filter then waits until it is told it is time to move to the next time step. To tell the particle -filter to move forward in time and tell it the evidence at the new time point, we call the \texttt{advanceTime} method, which takes a list of \texttt{NamedEvidence} as argument. For example, +filter to move forward in time and tell it the evidence at the new time point, we call the \texttt{advanceTime} method, which takes a list of \texttt{NamedEvidence} as argument. For example: \begin{flushleft} \texttt{pf.start() @@ -49,7 +51,7 @@ \subsection{Particle filtering} This creates the initial particles and advances two time steps with different evidence at each time. The query methods provided for a filtering algorithm are \texttt{currentDi\-stribution}, -\texttt{currentExpectation}, and \texttt{currentProbability}. These are similar to the corresponding methods for algorithms that compute conditional probabilities for static models, except that they return the distribution, expectation, or probability at the current point in time. For example, +\texttt{currentExpectation}, and \texttt{currentProbability}. These are similar to the corresponding methods for algorithms that compute conditional probabilities for static models, except that they return the distribution, expectation, or probability at the current point in time. For example: \begin{flushleft} \texttt{pf.start() @@ -62,3 +64,15 @@ \subsection{Particle filtering} There are a couple of implementation notes about particle filtering that a user should be aware of. First, since particle filtering estimates probabilities of elements using many particles, it can get expensive (memory wise) to store the state estimates for every element in the model. Therefore, the states of only \emph{named} elements are tracked through time. This means that queries to filter for an element probability or expectation must be on named elements. In addition, because filtering tracks estimates through time, we want to free up memory from old universes that are no longer used. To accomplish this, when \texttt{advanceTime} is called, all named elements from the previous universe are copied as constants to a new, temporary universe. The temporary universe is then used in the transition function, allowing the real previous universe to be freed while still letting the new universe use the correct values from the old universe. +\subsection{Factored frontier} + +Factored frontier is a dynamic reasoning algorithm that uses factors instead of sampling. To create an instance of factored frontier, use: + +\begin{flushleft} +\texttt{val ff = FactoredFrontier(static, initial, transition, numIterations)} +\end{flushleft} + +where \texttt{static} is a universe with the static variables, \texttt{initial} is the initial universe, \texttt{transition} is the transition model (a function from \texttt{(Universe, Universe) => Universe}), and \texttt{numIterations} is the number of internal BP iterations that should be performed at each time step (as BP is used internally in the algorithm). Optionally, an anytime version of BP can be used instead, where the \texttt{numIterations} is replaced by \texttt{stepTimeMillis}, a long that indicates how long to run BP at each time step. + +The same restrictions and behaviors for continuous variables that apply to BP also apply to factored frontier. The interfaces for advancing time, applying evidence, and querying the model are the same for factored frontier and particle filtering. As in particle filtering, only named elements are propagated to the next time step for building the model and querying. + diff --git a/FigaroLaTeX/Tutorial/Sections/7Decisions.tex b/FigaroLaTeX/Tutorial/Sections/7Decisions.tex index 98bcc7c1..f79461f5 100644 --- a/FigaroLaTeX/Tutorial/Sections/7Decisions.tex +++ b/FigaroLaTeX/Tutorial/Sections/7Decisions.tex @@ -75,11 +75,11 @@ \section{Decisions in Figaro} \texttt{def setPolicy(new\_fcn: (T => Element[U])): Unit} \end{flushleft} -That is, setting the policy of a decision is just providing a new function from the value of a parent to an \texttt{Element[U]}. Users can also get the policy for a specific value of the parent by calling \texttt{getPolicy(p: T): Element[U]}. Various other ways to set the policy can also be found in the Decision code. +That is, setting the policy of a decision is just providing a new function from the value of a parent to an \texttt{Element[U]}. Users can also get the policy for a specific value of the parent by calling \texttt{getPolicy(p: T): Element[U]}. Various other ways to set the policy can also be found in the \texttt{com.cra.figaro.algorithm.decision} package. \section{Single decision models and policy generation} -Single decision models can be created in Figaro by simply inserting a \texttt{Decision} element into the model. Once the model has been created, the goal is usually to compute the optimal policy for the decision that maximizes the expected utility of the model. This is done as two explicit steps in Figaro;computing the expected utility of each parent and decision pair, then determining the decision that has the maximum expected utility for each parent value. The policy is then set as a function that returns the maximum expected utility decision as a \texttt{Constant} for any parent value. This policy computation is performed using one of Figaro's built-in inference algorithms. Two alternative methods are provided. One is generally used when the support of the parent is finite, the other when it is infinite. However, there are some cases where the support is finite but very large and the infinite support method is preferable. Alternatively, for some distributions with infinite support, like Poisson or Geometric, only a small number of values are likely, and the finite support method can be used. +Single decision models can be created in Figaro by simply inserting a \texttt{Decision} element into the model. Once the model has been created, the goal is usually to compute the optimal policy for the decision that maximizes the expected utility of the model. This is done as two explicit steps in Figaro; computing the expected utility of each parent and decision pair, then determining the decision that has the maximum expected utility for each parent value. The policy is then set as a function that returns the maximum expected utility decision as a \texttt{Constant} for any parent value. This policy computation is performed using one of Figaro's built-in inference algorithms. Two alternative methods are provided. One is generally used when the support of the parent is finite, the other when it is infinite. However, there are some cases where the support is finite but very large and the infinite support method is preferable. Alternatively, for some distributions with infinite support, like Poisson or Geometric, only a small number of values are likely, and the finite support method can be used. \subsection{Finite parent support} @@ -150,7 +150,7 @@ \section{Multiple decision models and policy generation} \begin{flushleft} \marginpar{A multiple decision example can be found in MultiDecision.scala} \texttt{val alg = MultiDecisionVariableElimination(List(utility1, utility2), decision1, decision2) -\newline val alg = MultiDecisionImportanceSampling(10000, List(utility1, utility2), decision1, decision2) +\newline val alg = MultiDecisionImportance(10000, List(utility1, utility2), decision1, decision2) \newline val alg = MultiDecisionMetropolisHastings(10000, maker: ProposalMakerType, 1000, List(utility1, utility2), decision1, decision2) } \end{flushleft} @@ -161,7 +161,7 @@ \section{Multiple decision models and policy generation} \texttt{type ProposalMakerType = (Universe, Element[\_]) => ProposalScheme} \end{flushleft} -Only the one-time versions of the decision algorithms can be used for multi-decision models. To compute the optimal policy for every decision in the model, the user simply does, for example,: +Only the one-time versions of the decision algorithms can be used for multi-decision models. To compute the optimal policy for every decision in the model, the user simply does, for example: \begin{flushleft} \texttt{val propmaker = (mv: Universe, e: Element[\_]) => ProposalScheme.default(mv) @@ -169,4 +169,4 @@ \section{Multiple decision models and policy generation} \newline alg.start()} \end{flushleft} -The ProposalMaker for this small example just uses the default proposal for each instantiation of \texttt{DecisionMetropolisHastings} for a decision. However, we could also change the proposal scheme for each decision. There is also no need to call \texttt{alg.setPolicy}, since the multi-decision algorithm will set the optimal policy for each decision as it is needed for backward induction. Figaro will automatically compute the partial order of the decisions that are in the parameter list. +The \texttt{ProposalMaker} for this small example just uses the default proposal for each instantiation of \texttt{DecisionMetropolisHastings} for a decision. However, we could also change the proposal scheme for each decision. There is also no need to call \texttt{alg.setPolicy}, since the multi-decision algorithm will set the optimal policy for each decision as it is needed for backward induction. Figaro will automatically compute the partial order of the decisions that are in the parameter list. diff --git a/FigaroLaTeX/Tutorial/Sections/8LearningParams.tex b/FigaroLaTeX/Tutorial/Sections/8LearningParams.tex index 72ce2117..370c46ad 100644 --- a/FigaroLaTeX/Tutorial/Sections/8LearningParams.tex +++ b/FigaroLaTeX/Tutorial/Sections/8LearningParams.tex @@ -8,9 +8,9 @@ \chapter{Learning model parameters from data} % Chapter title \section{Parameters and parameterized elements} -This section discusses elements which are learnable parameters. For clarity, a distinction should be made on the meaning of the word \emph{parameter} in this context. This is different from a method parameter or Scala type parameter. In this section, we use \emph{parameter} to refer to a Figaro element which can be learned from data. There are currently two such types of parameters in Figaro - \texttt{Beta} and \texttt{Dirichlet}. +This section discusses elements which are learnable parameters. For clarity, a distinction should be made on the meaning of the word \emph{parameter} in this context. This is different from a method parameter or Scala type parameter. In this section, we use \emph{parameter} to refer to a Figaro element which can be learned from data. There are currently two such types of parameters in Figaro: (1) \texttt{Beta}, and (2) \texttt{Dirichlet}. -A customary illustration of parameter learning is to consider the outcomes of a coin flip and determine whether or not the coin is fair. In the case of a \texttt{Flip} element (which is a Bernoulli distribution), the conjugate prior distribution is a Beta distribution. If the coin is not fair, we would expect a prior distribution to have a higher value of alpha or beta (the shape variables of a Beta). First, we will create the conjugate prior distribution of a \texttt{Flip} +A customary illustration of parameter learning is to consider the outcomes of a coin flip and determine whether or not the coin is fair. In the case of a \texttt{Flip} element (which is a Bernoulli distribution), the conjugate prior distribution is a Beta distribution. If the coin is not fair, we would expect a prior distribution to have a higher value of alpha or beta (the shape variables of a Beta). First, we will create the conjugate prior distribution of a \texttt{Flip}: \begin{flushleft} \texttt{val fairness = Beta(1,1)} @@ -36,13 +36,9 @@ \section{Parameters and parameterized elements} The following block of Scala code will iterate through each of the items in the sequence, create a Flip element using the parameter, and observe true or false based on the side of the coin: \begin{flushleft} -\texttt{data.foreach \{ d => -\newline \tab val f = Flip(fairness) -\newline \tab \tab if (d == 'H') \{ -\newline \tab \tab f.observe(true) -\newline \tab \} else if (d == 'T') \{ -\newline \tab \tab f.observe(false) -\newline \tab \} +\texttt{data zip model.trials foreach \{ +\newline \tab (datum: (Char, Flip)) => if (datum.\_1 == 'H') +\newline \tab datum.\_2.observe(true) else datum.\_2.observe(false) \newline \} } \end{flushleft} @@ -53,7 +49,7 @@ \section{Expectation maximization} A learning algorithm can be used to determine the maximum a posteriori estimate for parameter elements. Parameter elements have a \texttt{MAPValue} which is set when the parameter is used as a target in a learning algorithm. Presently, Figaro provides one learning algorithm, expectation maximization, which uses existing Figaro algorithms to estimate sufficient statistics. Recall that expectation maximization is an iterative algorithm consisting of an expectation step and a maximization step. During the expectation step, an estimate is produced for the sufficient statistics of the parameter. The estimates are then used in the maximization step to find the most likely value of the parameter. This continues for a set number of iterations and converges toward the true MAP value. -From a practical standpoint, learning a parameter with expectation maximization is very simple. We need only provide the target parameter and, optionally, the number of iterations to the algorithm. The default number of iterations is 10. We can also choose an inference algorithm for estimating the sufficient statistics of the target parameters. Currently, Metropolis Hastings, Importance Sampling, Variable Elimination or Belief Propagation can be used for this purpose. Figaro's Generalized EM algorithm is used in the following way: +From a practical standpoint, learning a parameter with expectation maximization is very simple. We need only provide the target parameter and, optionally, the number of iterations to the algorithm. The default number of iterations is 10. We can also choose an inference algorithm for estimating the sufficient statistics of the target parameters. Currently, Metropolis Hastings, importance sampling, variable elimination or belief propagation can be used for this purpose. Figaro's Generalized EM algorithm is used in the following way: \begin{flushleft} \texttt{val learningAlgorithm = EMwithMH(fairness) @@ -68,7 +64,7 @@ \section{Expectation maximization} \end{flushleft} The line \texttt{val learningAlgorithm = EMwithMH(fairness)} creates an EM algorithm which uses Metropolis Hastings to estimate sufficient statistics. We could also have used \texttt{EMwithBP(fairness)} or -\texttt{EMwithImportance(fairness)}. +\texttt{EMwithImp\-ortance(fairness)}. After the algorithm has finished running, we can create an element learned from the parameter by using \texttt{Flip(fairness.MAPValue)}. The element \texttt{coin} is a \texttt{Flip}, where the probability of producing true is determined from the data we observed above. @@ -79,7 +75,7 @@ \section{Expectation maximization} 0.7159090909090909} \end{flushleft} -We may want to make further explorations about the learned model. For instance, if we wanted to know the probability that two flips of this coin show the same side, we could use +We may want to make further explorations about the learned model. For instance, if we wanted to know the probability that two flips of this coin show the same side, we could use: \begin{flushleft} \texttt{val t1 = Flip(fairness.MAPValue) @@ -103,9 +99,11 @@ \section{Expectation maximization} \texttt{The probability of two coins which exhibit this fairness showing the same side is: 0.5932334710743803} \end{flushleft} -\section{A second example} +\section{Parameter collections} -In the previous sections, parameter learning was discussed using a Beta parameter. This section will explain the use of Dirichlet parameters. The Dirichlet distribution is a multidimensional generalization of the Beta with a variable number of concentration parameters or alpha values. These values correspond to the weight of each possible outcome in the posterior distribution. In a Dirichlet parameter with two dimensions, the alpha values might again correspond to the outcome of heads and tails, or true and false. Using a higher number of dimensions, we can model a number of different categories or outcomes. +In the previous sections, parameter learning was discussed using a Beta parameter. The Beta parameters were supplied individually to the learning algorithm, and the MAP value for each parameter was retrieved individually. For more complicated models, it is often useful to define a model structure with parameters which can be learned, and then use the values of the learned parameters in the same structure. The \texttt{ModelParameters} pattern is a simple way of accomplishing this. By using \texttt{ParameterCollection}, the model structure only needs to be defined once. Parameters are added to the collection in a similar fashion to \texttt{Element} collections. We specify the parameter name and add it to the collection when we create the parameters. + +This section will also explain the use of Dirichlet parameters. The Dirichlet distribution is a multidimensional generalization of the Beta with a variable number of concentration parameters or alpha values. These values correspond to the weight of each possible outcome in the posterior distribution. In a Dirichlet parameter with two dimensions, the alpha values might again correspond to the outcome of heads and tails, or true and false. Using a higher number of dimensions, we can model a number of different categories or outcomes. Suppose we are given a set of data in which each record represents a roll of two die out of three possible die. The sum of the die is available, as well as which die were selected for the roll. However, the individual outcome of each die is not available. Our task is to learn the fairness of each die. @@ -116,17 +114,17 @@ \section{A second example} \texttt{val outcomes = List(1, 2, 3, 4, 5, 6)} \end{flushleft} -Next, we create a parameter representing the fairness of each die: +Next, we create a set of model parameters representing the parameters of the fair dice model. We create a parameter representing the fairness of each die and add it to the collection of model parameters. \begin{flushleft} -\texttt{val fairness1 = Dirichlet(1,1,1,1,1,1) -\newline val fairness2 = Dirichlet(1,1,1,1,1,1) -\newline val fairness3 = Dirichlet(1,1,1,1,1,1) -\newline val parameters = Seq(fairness1, fairness2, fairness3) +\texttt{val params = ModelParameters() +\newline val fairness1 = Dirichlet(2.0, 2.0, 2.0, 2.0, 2.0, 2.0)("fairness1", params) +\newline val fairness2 = Dirichlet(2.0, 2.0, 2.0, 2.0, 2.0, 2.0)("fairness1", params) +\newline val fairness3 = Dirichlet(2.0, 2.0, 2.0, 2.0, 2.0, 2.0)("fairness1", params) } \end{flushleft} -Each die is initially assumed to be fair. For convenience, we can place all three parameters in a Scala sequence named \texttt{parameters}. An item this sequence at index i can be accessed with \texttt{parameters(i)}. This sequence will help us to concisely observe the data from which the parameters are learned. We can represent the data in another sequence: +Each die is initially assumed to be fair. For convenience, the data which we will learn the parameters from is represented in Scala sequence: \begin{flushleft} \texttt{val data = Seq((2, 3, 8), (1, 3, 7), (1, 2, 3), (1, 2, 3), ...} @@ -134,48 +132,66 @@ \section{A second example} \texttt{data} is a sequence of 50 Scala tuples. The first two values in each tuple indicate which two die were chosen to roll. The third value is the sum of the two die. -To model the outcome of the sum, we can use an \texttt{Apply} element with a function which sums the outcome of its arguments. +The next step is to define a class representing the model structure. \begin{flushleft} -\texttt{def trial(p1: AtomicDirichlet, p2: AtomicDirichlet, result: Int) = \{ -\newline \tab val sum = (i1: Int, i2: Int) => i1 + i2 -\newline \tab val die1 = Select(p1, outcomes: \_*) -\newline \tab val die2 = Select(p2, outcomes: \_*) -\newline \tab val t = Apply(die1, die2, sum) -\newline \tab t.observe(result) -\newline \} +\texttt{class DiceModel(val parameters: ParameterCollection, val data: Seq[(Int, Int, Int)], val outcomes: List[Int])} +\end{flushleft} + +This defines a class which accepts a \texttt{ParameterCollection}, a set of data, and a list of outcomes as its arguments. As we will see, the values which are retrieved from the \texttt{ParameterCollection} depend on whether we are working with the prior or posterior parameters. To model the outcome of the sum, we can use an \texttt{Apply} element with a function which sums the outcome of its arguments. We place the following loop inside the \texttt{DiceModel} class: + +\begin{flushleft} +\texttt{val sum = (i1: Int, i2: Int) => i1 + i2 + \newline val trials = for (datum <- data) yield \{ + \newline \tab val die1 = Select(parameters.get("fairness" + datum.\_1), outcomes: \_*) + \newline \tab val die2 = Select(parameters.get("fairness" + datum.\_2), outcomes: \_*) + \newline \tab Apply(die1, die2, sum) + \newline \} + } +\end{flushleft} + +The code section above defines a Scala function which accepts two Dirichlet parameters and an integer value. \texttt{val sum = (i1: Int, i2: Int) => i1 + i2} defines a Scala function which accepts two integer values and returns their sum. Next, two Select elements are created and parameterized by the input parameters. We retrieve the parameters by using the \texttt{get} method from the input \texttt{ParameterCollection}. + +Note that the arguments to \texttt{Select} are different from what has been presented previously. Instead of directly enumerating each probability and outcome, we specify a Dirichlet parameter and the list of possible outcomes. The last two lines of \texttt{trial} apply the sum function to the die and observe the result. By calling the \texttt{trial} function for each tuple in the sequence, we can create a model learned from the data. + +We can now create an instance of the \texttt{DiceModel} class, using the prior parameters from the parameter collection. + +\begin{flushleft} + \texttt{val model = new DiceModel(params.priorParameters, data, outcomes) } \end{flushleft} -The code section above defines a Scala function which accepts two Dirichlet parameters and an integer value. \texttt{val sum = (i1: Int, i2: Int) => i1 + i2} defines a Scala function which accepts two integer values and returns their sum. Next, two Select elements are created and parameterized by the input parameters. Note that the arguments to \texttt{Select} are different from what has been presented previously. Instead of directly enumerating each probability and outcome, we specify a Dirichlet parameter and the list of possible outcomes. The last two lines of \texttt{trial} apply the sum function to the die and observe the result. By calling the \texttt{trial} function for each tuple in the sequence, we can create a model learned from the data. +To apply evidence to the model, we can write another loop over the contents of the data and the trials defined inside the model class. \begin{flushleft} -\texttt{data.foreach \{ d => -\newline if (d.\_1 == 1 \&\& d.\_2 == 2) \{ -\newline \tab trial(parameters(0), parameters(1), d.\_3) -\newline \tab \} else if (d.\_1 == 1 \&\& d.\_2 == 3) \{ -\newline \tab trial(parameters(0), parameters(2), d.\_3) -\newline \} else \{ -\newline \tab trial(parameters(1), parameters(2), d.\_3) -\newline \} -\newline \} +\texttt{for ((datum,trial) <- data zip model.trials) \{ + \newline \tab trial.observe(datum.\_3) + \} } \end{flushleft} -Just as in the fair coin example, we create an expectation maximization algorithm and use the list of parameters as input. Additionally, this time we have chosen to raise the number of iterations to 100. +Just as in the fair coin example, we create an expectation maximization algorithm. This time, instead of passing the parameters in a list or sequence, we can simply use the collection of parameters as an input argument. \begin{flushleft} -\texttt{val numberOfIterations = 100 -\newline val algorithm = EMWithMH(numberOfIterations, parameters: \_*) -\newline algorithm.start -\newline -\newline val die1 = Select(fairness1.MAPValue.view(0,fairness1.MAPValue.size).toList, outcomes) -\newline val die2 = Select(fairness2.MAPValue.view(0,fairness2.MAPValue.size).toList, outcomes) -\newline val die3 = Select(fairness3.MAPValue.view(0,fairness3.MAPValue.size).toList, outcomes) +\texttt{val numberOfBPIterations = 10 + \newline val numberOfEMIterations = 10 + \newline val algorithm = EMWithBP(numberOfEMIterations, numberOfBPIterations, params) + \newline algorithm.start + \newline algorithm.stop + \newline val d1 = Select(params.posteriorParameters.get("fairness1"), outcomes:\_*) + \newline val d2 = Select(params.posteriorParameters.get("fairness2"), outcomes:\_*) + \newline val d3 = Select(params.posteriorParameters.get("fairness3"), outcomes:\_*) +} +\end{flushleft} + +The code block above will create \texttt{Select} elements using to the MAP value of the learned parameters. We retrieve the MAP value of the parameters by using the \texttt{posteriorParameters.get} method of our parameter collection. If we wanted to create another set of 50 trials using the learned parameter values, we could simply use: + +\begin{flushleft} + \texttt{val model = new DiceModel(params.posteriorParameters, data, outcomes) } \end{flushleft} -The code block above will create \texttt{Select} elements according to the MAP value of the learned parameters.. Note that for a Select, a list of outcomes must be supplied as an argument to along with their corresponding probabilities, which is the array returned by \texttt{MAPValue}. This is because the number of concentration parameters is may vary, and the type of the outcomes is not fixed. Running this code results in the following output, in which we see the model has estimated the probabilities of each value for each die. If one examines the full data declaration in the example code, it is quite easy to see that there are only three observed values of the sum of the die (3, 7 and 8), so the learning algorithm has correctly inferred that the most likely values of the die are 1, 2 and 6, respectively. +Note that for a \texttt{Select}, a list of outcomes must be supplied as an argument to along with their corresponding probabilities. This is because the number of concentration parameters is may vary, and the type of the outcomes is not fixed. Running this code results in the following output, in which we see the model has estimated the probabilities of each value for each die. If one examines the full data declaration in the example code, it is quite easy to see that there are only three observed values of the sum of the die (3, 7 and 8), so the learning algorithm has correctly inferred that the most likely values of the die are 1, 2 and 6, respectively. \begin{flushleft} \texttt{The probabilities of seeing each side of d\_1 are: diff --git a/FigaroLaTeX/Tutorial/tutorial-config.tex b/FigaroLaTeX/Tutorial/tutorial-config.tex index ee6768a0..bea1f069 100644 --- a/FigaroLaTeX/Tutorial/tutorial-config.tex +++ b/FigaroLaTeX/Tutorial/tutorial-config.tex @@ -82,7 +82,7 @@ %------------------------------------------------ %\renewcommand*{\acsfont}[1]{\textssc{#1}} % For MinionPro -\renewcommand{\bflabel}[1]{{#1}\hfill} % Fix the list of acronyms +%\renewcommand{\bflabel}[1]{{#1}\hfill} % Fix the list of acronyms %------------------------------------------------ diff --git a/FigaroWork/project/plugins.sbt b/FigaroWork/project/plugins.sbt index 6b2ec7d4..9bf478d9 100644 --- a/FigaroWork/project/plugins.sbt +++ b/FigaroWork/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") diff --git a/README.md b/README.md index 1e81750c..8ad80daa 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,69 @@ Figaro Programming Language & Core Libraries = -Figaro is a probabilistic programming language that supports development of very rich probabilistic models and provides reasoning algorithms that can be applied to models to draw useful conclusions from evidence. Both model representation and reasoning algorithm development can be challenging tasks. - -Figaro makes it possible to express probabilistic models using the power of programming languages, giving the modeler the expressive tools to create a wide variety of models. Figaro comes with a number of built-in reasoning algorithms that can be applied automatically to new models. In addition, Figaro models are data structures in the Scala programming language, which is interoperable with Java, and can be constructed, manipulated, and used directly within any Scala or Java program. - -## What algorithms are supported in Figaro? - -Current built-in algorithms include: -``` -Exact inference using variable elimination -Importance sampling -Metropolis-Hastings, with an expressive language to define proposal distributions -Support computation -Most probable explanation (MPE) using variable elimination or simulated annealing -Probability of evidence using importance sampling -Particle Filtering -Parameter learning using expectation maximization -``` +## What is Figaro? + +Reasoning under uncertainty requires taking what you know and inferring what you don’t know, when what you know doesn’t tell you for sure what you don’t know. A well-established approach for reasoning under uncertainty is probabilistic reasoning. Typically, you create a probabilistic model over all the variables you’re interested in, observe the values of some variables, and query others. There is a huge variety of probabilistic models, and new ones are being developed constantly. Figaro is designed to help build and reason with the wide range of probabilistic models. +Developing a new probabilistic model normally requires developing a representation for the model and a reasoning algorithm that can draw useful conclusions from evidence, and in many cases also an algorithm to learn aspects of the model from data. These can be challenging tasks, making probabilistic reasoning require significant effort and expertise. Furthermore, most probabilistic reasoning tools are standalone and difficult to integrate into larger programs. + +Figaro is a probabilistic programming language that helps address both these issues. Figaro makes it possible to express probabilistic models using the power of programming languages, giving the modeler the expressive tools to create all sorts of models. Figaro comes with a number of built-in reasoning algorithms that can be applied automatically to new models. In addition, Figaro models are data structures in the Scala programming language, which is interoperable with Java, and can be constructed, manipulated, and used directly within any Scala or Java program. +Figaro is extremely expressive. It can represent a wide variety of models, including: + +* directed and undirected models +* models in which conditions and constraints are expressed by arbitrary Scala functions +* models involving inter-related objects +* open universe models in which we don’t know what or how many objects exist +* models involving discrete and continuous elements +* models in which the elements are rich data structures such as trees +* models with structured decisions +* models with unknown parameters + +Figaro provides a rich library of constructs to build these models, and provides ways to extend this library to create your own model elements. + +Figaro’s library of reasoning algorithms is also extensible. Current built-in algorithms include: + +* Exact inference using variable elimination +* Belief propagation +* Lazy factored inference for infinite models +* Importance sampling +* Metropolis-Hastings, with an expressive language to define proposal distributions +* Support computation +* Most probable explanation (MPE) using variable elimination or simulated annealing +* Probability of evidence using importance sampling +* Particle filtering +* Factored frontier +* Parameter learning using expectation maximization Figaro provides both regular (the algorithm is run once) and anytime (the algorithm is run until stopped) versions of some of these algorithms. In addition to the built-in algorithms, Figaro provides a number of tools for creating your own reasoning algorithms. -## Where can I get Figaro binary distributions? - -Figaro “fat JAR” binary distributions are available for download from the [Charles River Analytics, Inc. Web site](http://www.cra.com/figaro). - -Each binary bundle comes with all required Figaro libraries, Scaladoc, examples, and complete source code. - -## What Scala versions are supported by Figaro? - -Figaro supports Scala 2.11. Scala 2.10 users should use Figaro v2.2.2.0. - -## How can I use Figaro in my project? - -Figaro and its dependencies are available on [Maven Central](http://search.maven.org). Shown below are a few examples of how you can integrate Figaro into your existing software project: +Figaro is free and is released under an open-source license (see license file). The public code repository for Figaro can also be found at https://github.com/p2t2. -Simple Build Tool (SBT) Projects -``` -libraryDependencies += "com.cra.figaro" %% "figaro" % "2.5.0.0" -``` +## Where can I get Figaro binary distributions? -Apache Maven Projects -``` - - com.cra.figaro - figaro_2.11 - 2.5.0.0 - -``` +The latest stable Figaro binary release is available for download from the [Charles River Analytics, Inc. Web site](http://www.cra.com/figaro). -Apache Ivy Projects -``` - -``` +Each binary release comes with Figaro, all required libraries, Scaladoc, examples, and source code. ## How do I run the Figaro Examples? -1) Download and install Scala 2.11 +The easiest way to run the Figaro examples is to install Scala and use the latest Figaro binary release. -2) Download the latest Figaro binary bundle for Scala 2.11 and uncompress the archive +To get started, download Scala from [http://scala-lang.org/download/](http://scala-lang.org/download/). You will need Scala version 2.11.2 or later to run the latest Figaro release. Follow the Scala installation instructions at [http://scala-lang.org/download/install.html](http://scala-lang.org/download/install.html) and make sure you can run, compile, and execute the “Hello World” program provided in the documentation. -3) Open a command prompt +The next step is to obtain Figaro. The Figaro binary distribution is hosted at the Charles River Analytics, Inc. Web site at [https://www.cra.com/figaro](https://www.cra.com/figaro). The current version, as of January 2015, is 3.0.0.0 and is available for Scala 2.11. Make sure the Figaro version you use matches the Scala version. Each available download link is a compressed archive containing the Figaro jar (jar is the Java/Scala format for compiled byte code), examples, documentation, Scaladoc, and source code files. Click the appropriate link and then uncompress the downloaded archive to access the files. In each distribution, you will find a Figaro jar with a name ending with “fat” (such as “figaro_2.11-3.0.0.0-fat.jar”), indicating that this is a fat jar containing all the necessary libraries to run Figaro. -4) Switch to the uncompressed Figaro binary bundle directory, and use the Scala command line: +The final step is to open a command prompt and switch to the uncompressed Figaro download directory. Using the Scala command line program, run any Figaro example by setting the Scala classpath and invoking the desired class: ``` -scala -cp figaro_2.11-2.5.0.0-fat.jar;figaroexamples_2.11-2.5.0.0.jar +scala -cp figaro_2.11-3.0.0.0-fat.jar;figaroexamples_2.11-3.0.0.0.jar ``` -For instance: +For example, to run the Burglary program: ``` -scala -cp figaro_2.11-2.5.0.0-fat.jar;figaroexamples_2.11-2.5.0.0.jar com.cra.figaro.example.Burglary +scala -cp figaro_2.11-3.0.0.0-fat.jar;figaroexamples_2.11-3.0.0.0.jar com.cra.figaro.example.Burglary ``` ## What Figaro Example classes are available? -The following examples are available in the Figaro Examples JAR file included in the binary bundle: +The following examples are available in the Figaro Examples JAR file included in the binary release: ``` com.cra.figaro.example.AnnealingSmokers com.cra.figaro.example.Burglary @@ -96,3 +86,142 @@ com.cra.figaro.example.Sources com.cra.figaro.example.dosage.DosageDecision com.cra.figaro.example.graph.GraphDecision ``` + +## How do I run my own Figaro programs? +The simplest way to compile and run the Figaro programs you create is to use the Simple Build Tool (SBT) program and the FigaroWork project. + +FigaroWork is an SBT project that enables users to quickly start writing their own Figaro programs. The project is set up to automatically pull in the relevant versions of Scala and Figaro, so there is nothing else for you to install. SBT also makes sure the Scala classpath is configured correctly for your project, saving you some hassle when running your programs. + +To get started, download and uncompress the FigaroWork files to a directory on your machine. The FigaroWork project is hosted at the Charles River Analytics, Inc. Web site at https://www.cra.com/figaro. Then download the latest release of SBT v0.13 for your operating system at http://www.scala-sbt.org/download.html and install it following these guidelines http://www.scala-sbt.org/0.13/tutorial/Manual-Installation.html. + +When you have finished installing, you will have the following directories and files on your machine +``` +\FigaroWork + README.txt + \project + build.properties + Build.scala + plugins.sbt + \src + \main + \scala + Test.scala +``` + +Test your new build environment by running the simple Figaro test program provided with the project. Open a command prompt, navigate to your local FigaroWork directory (ex. C:\FigaroWork), and run this command +``` +sbt "runMain Test" +``` + +This command tells SBT to compile the Scala files in the project and execute the main() method of the Test class. Remember to include the quotes around the runMain command. You should see output similar to this +``` +[info] Running Test +1.0 +``` + +Now you can copy your existing Figaro program packages and Scala source files to +``` +\FigaroWork\src\main\scala +``` + +or create new ones there. Run your Figaro program like this +``` +sbt "runMain " +``` + +Replace with the package and class that contains the main() method that starts your Figaro program. Replace with the list of command line parameters (if any) your program may need to run. For example +``` +sbt "runMain com.cra.test.FigaroTest parameter1 parameter2 parameter3" +``` + +## How do I run my own Figaro programs without SBT? + +While SBT is a useful tool, you may want to manage your own workspace differently. + +To run Figaro, you will first need Scala. The Scala compiler can either be run from the command line or within an Integrated Development Environment (IDE). Two IDEs that support Scala development are Eclipse and IntelliJ Idea. NetBeans also has a Scala plugin but it does not appear to support recent versions of Scala (but that may have changed). This section focuses on how to obtain Scala and Figaro and run Scala programs that use Figaro from the command line. If you choose to use an IDE, please see the documentation of your IDEs and Scala plugins for details of how to include the Figaro library. + +To get started, download Scala from http://scala-lang.org/download/. You will need Scala version 2.11.2 or later to run the latest version of Figaro. Follow the Scala installation instructions at http://scala-lang.org/download/install.html and make sure you can run, compile, and execute the “Hello World” program provided in the documentation. + +The next step is to obtain Figaro. The Figaro binary distribution is hosted at the Charles River Analytics, Inc. Web site. Go to https://www.cra.com/figaro. The current version, as of January 2015, is 3.0.0.0, and is available for Scala 2.11. Always make sure the Figaro version you use matches the Scala version you’re using. Each available download link is a compressed archive containing the Figaro jar (jar is the Java/Scala format for compiled byte code), examples, documentation, Scaladoc, and source code files. Click the appropriate link and then uncompress the downloaded archive to access the Figaro jar file. In the distribution, the Figaro jar name ends with “fat”, indicating that this is a fat jar containing all the necessary libraries to run Figaro. Using a fat jar simplifies the Scala classpath needed to run Figaro programs. + +Optionally, you can add the fully qualified path name of the Figaro jar to your classpath. This can be done by adding the Figaro jar to the CLASSPATH environment variable in your operating system. The process for editing the CLASSPATH varies from operating system to operating system. You can see details about using the PATH and CLASSPATH environment variables in http://docs.oracle.com/javase/tutorial/essential/environment/paths.html. + +If the CLASSPATH does not exist yet, create it. It is good practice to include the current working directory, so set the CLASSPATH to “.”, then proceed to add the Figaro jar, as in the next step. + +By this point, the CLASSPATH already exists, so we can add the Figaro path to it. For example, on Windows 7, if figaro_2.11-3.0.0.0-fat.jar is in the “C:\Users\apfeffer” folder and the CLASSPATH is currently equal to “.”, change the CLASSPATH to “C:\Users\apfeffer\figaro_2.11-3.0.0.0-fat.jar;.” (replace 3.0.0.0 with the appropriate Figaro version number). + +Now you can compile and run Figaro programs just like any Scala program. Put the Test program below in a file named Test.scala. First, let’s assume you followed step 4 and updated the CLASSPATH. + +If you run scala Test.scala from the directory containing Test.scala, the Scala compiler will first compile the program and then execute it. It should produce the output 1.0. + +If you run scalac Test.scala (note the c at the end of “scalac”), the Scala compiler runs and produces .class files. You can then execute the program by running scala Test from the same directory. + +If you did not follow step 4, you can set the CLASSPATH from the command line using the –cp option. For example, to compile and execute Test.scala, assuming figaro_2.11-3.0.0.0-fat.jar is in the “C:\Users\apfeffer” folder, you can run +``` +scala –cp C:\Users\apfeffer\figaro_2.11-3.0.0.0-fat.jar Test.scala +``` + +Here’s the Test program: +``` +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.sampling._ + +object Test { + def main(args: Array[String]) { + val test = Constant("Test") + val algorithm = Importance(1000, test) + algorithm.start() + println(algorithm.probability(test, "Test")) + } +} +``` + +This program should output 1.0 when run. + +## How do I compile Figaro from source code? + +Figaro is maintained as open source on GitHub. The GitHub project is Probabilistic Programming Tools and Techniques (P2T2), located at [https://github.com/p2t2](https://github.com/p2t2). P2T2 currently contains the Figaro sources, but we plan to update it with more tools. If you want to see the source code and build Figaro yourself, please visit our GitHub site. + +To build Figaro from GitHub source, make a fork of the repository to your GitHub account, then use git’s clone feature to get the source code from your GitHub account to your machine. +``` +git clone https://github.com/[your-github-username]/figaro.git +``` + +There are several branches available; checkout “master” for the latest stable release or the latest “DEV” branch for more cutting edge work and features (this is work in progress and therefore less stable). + +Figaro uses Simple Build Tool (SBT) to manage builds, located at [http://www.scala-sbt.org/](http://www.scala-sbt.org/). Download and install SBT, open a command prompt, and enter this SBT command set: +``` +sbt clean compile package publishLocal assembly copy-deps +``` + +This will create Figaro for Scala 2.11; you will find the resulting artifacts in the “target” directory. + +To run the Figaro unit tests, use this SBT command +``` +sbt test +``` + +Note that some of the unit tests may not always pass because their results are non-deterministic. + +## How can I use Figaro in my project? + +If you wish to integrate Figaro's features into your own software project, Figaro is available on [Maven Central](http://search.maven.org). Shown below are a few examples of how you can add Figaro as a dependency to your existing project: + +Simple Build Tool (SBT) Projects +``` +libraryDependencies += "com.cra.figaro" %% "figaro" % "3.0.0.0" +``` + +Apache Maven Projects +``` + + com.cra.figaro + figaro_2.11 + 3.0.0.0 + +``` + +Apache Ivy Projects +``` + +``` diff --git a/doc/Figaro 2.0 Release Notes.pdf b/doc/Figaro 2.0 Release Notes.pdf deleted file mode 100644 index ec8b43e4..00000000 Binary files a/doc/Figaro 2.0 Release Notes.pdf and /dev/null differ diff --git a/doc/Figaro 3.0 Release Notes.pdf b/doc/Figaro 3.0 Release Notes.pdf new file mode 100644 index 00000000..2b201704 Binary files /dev/null and b/doc/Figaro 3.0 Release Notes.pdf differ diff --git a/doc/Figaro Quick Start Guide.pdf b/doc/Figaro Quick Start Guide.pdf index 34413a30..82bc8be9 100644 Binary files a/doc/Figaro Quick Start Guide.pdf and b/doc/Figaro Quick Start Guide.pdf differ diff --git a/doc/Figaro Tutorial.pdf b/doc/Figaro Tutorial.pdf index dd42871e..d504e63c 100644 Binary files a/doc/Figaro Tutorial.pdf and b/doc/Figaro Tutorial.pdf differ diff --git a/project/Build.scala b/project/Build.scala index b48ffb6b..db332bca 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -7,11 +7,24 @@ import scoverage.ScoverageSbtPlugin._ object FigaroBuild extends Build { + // Copy dependency JARs to /target//lib + // Courtesy of + // http://stackoverflow.com/questions/7351280/collecting-dependencies-under-sbt-0-10-putting-all-dependency-jars-to-target-sc + lazy val copyDependencies = TaskKey[Unit]("copy-deps") + + def copyDepTask = copyDependencies <<= (update, crossTarget, scalaVersion) map { + (updateReport, out, scalaVer) => + updateReport.allFiles foreach { srcPath => + val destPath = out / "lib" / srcPath.getName + IO.copyFile(srcPath, destPath, preserveLastModified=true) + } + } + override val settings = super.settings ++ Seq( organization := "com.cra.figaro", description := "Figaro: a language for probablistic programming", - version := "2.5.0.0", - scalaVersion := "2.11.2", + version := "3.0.0.0", + scalaVersion := "2.11.4", crossPaths := true, publishMavenStyle := true, pomExtra := @@ -39,12 +52,12 @@ object FigaroBuild extends Build { lazy val scalaMajorMinor = "2.11" - // Read exisiting Figaro MANIFEST.MF rom file + // Read exisiting Figaro MANIFEST.MF from file lazy val figaroManifest = Using.fileInputStream(file("Figaro/META-INF/MANIFEST.MF")) { in => new java.util.jar.Manifest(in) } - // Read exisiting FigaroExamples MANIFEST.MF rom file + // Read exisiting FigaroExamples MANIFEST.MF from file lazy val examplesManifest = Using.fileInputStream(file("FigaroExamples/META-INF/MANIFEST.MF")) { in => new java.util.jar.Manifest(in) } @@ -67,8 +80,9 @@ object FigaroBuild extends Build { "asm" % "asm" % "3.3.1", "org.apache.commons" % "commons-math3" % "3.3", "net.sf.jsci" % "jsci" % "1.2", - "com.typesafe.akka" % "akka-actor_2.11" % "2.3.4", - "org.scalatest" % "scalatest_2.11" % "2.2.1" % "test" + "com.typesafe.akka" %% "akka-actor" % "2.3.8", + "org.scalanlp" %% "breeze" % "0.10", + "org.scalatest" %% "scalatest" % "2.2.1" % "test" )) // test settings .settings(parallelExecution in Test := false) @@ -86,11 +100,15 @@ object FigaroBuild extends Build { // sbt-scoverage settings .settings(instrumentSettings: _*) .settings(parallelExecution in ScoverageTest := false) + // Copy dependency JARs + .settings(copyDepTask) lazy val examples = Project("FigaroExamples", file("FigaroExamples")) .dependsOn(figaro) .settings(packageOptions := Seq(Package.JarManifest(examplesManifest))) - + // Copy dependency JARs + .settings(copyDepTask) + lazy val detTest = config("det") extend(Test) lazy val nonDetTest = config("nonDet") extend(Test) } diff --git a/project/plugins.sbt b/project/plugins.sbt index cf428adf..bbc0cbc5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "0.99.7.1")