diff --git a/.gitignore b/.gitignore index a14af547..5bfc248f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ project/plugins/project/ .classpath .project /bin/ + +#IntelliJ +.idea/ diff --git a/Figaro/.gitignore b/Figaro/.gitignore index 21ca5302..e6c44a3c 100644 --- a/Figaro/.gitignore +++ b/Figaro/.gitignore @@ -1,3 +1,4 @@ /bin/ /.cache-main /.cache-tests +/build.properties diff --git a/Figaro/META-INF/MANIFEST.MF b/Figaro/META-INF/MANIFEST.MF index 87dbb207..7347f6ef 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: 4.0.0 +Bundle-Version: 4.1.0 Export-Package: com.cra.figaro.algorithm, com.cra.figaro.algorithm.decision, com.cra.figaro.algorithm.decision.index, @@ -13,6 +13,7 @@ Export-Package: com.cra.figaro.algorithm, com.cra.figaro.language, com.cra.figaro.library.atomic.continuous, com.cra.figaro.library.atomic.discrete, + com.cra.figaro.library.collection, com.cra.figaro.library.compound, com.cra.figaro.library.decision, com.cra.figaro.util @@ -20,9 +21,8 @@ Bundle-Vendor: Charles River Analytics Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Require-Bundle: org.scala-lang.scala-library, org.scala-lang.scala-reflect, - com.typesafe.akka.actor;bundle-version="2.3.3", - com.typesafe.config;bundle-version="1.2.1", - org.scalatest;bundle-version="2.1.6" + com.typesafe.config, + com.typesafe.akka.actor Import-Package: org.apache.commons.math3.distribution;version="3.3.0" diff --git a/Figaro/figaro_build.properties b/Figaro/figaro_build.properties index 9052a6eb..358c657e 100644 --- a/Figaro/figaro_build.properties +++ b/Figaro/figaro_build.properties @@ -1 +1 @@ -version=4.0.0.0 +version=4.1.0.0 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 69281e3b..fd5a0228 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/ProbQueryAlgorithm.scala @@ -126,6 +126,18 @@ trait BaseProbQueryAlgorithm[U[_]] doExpectation(target, function) } + /** + * Return an estimate of the expectation of the function under the marginal probability distribution + * of the target. + * Throws NotATargetException if called on a target that is not in the list of + * targets of the algorithm. + * Throws AlgorithmInactiveException if the algorithm is inactive. + */ + def expectation[T](target: U[T])(function: T => Double, c: Any = DummyImplicit): Double = { + check(target) + doExpectation(target, function) + } + /** * Return the mean of the probability density function for the given continuous element. */ @@ -154,6 +166,18 @@ trait BaseProbQueryAlgorithm[U[_]] doProbability(target, predicate) } + /** + * Return an estimate of the probability of the predicate under the marginal probability distribution + * of the target. + * Throws NotATargetException if called on a target that is not in the list of + * targets of the algorithm. + * Throws AlgorithmInactiveException if the algorithm is inactive. + */ + def probability[T](target: U[T])(predicate: T => Boolean, c: Any = DummyImplicit): Double = { + probability(target, predicate) + } + + /** * Return an estimate of the probability that the target produces the value. * Throws NotATargetException if called on a target that is not in the list of @@ -165,3 +189,12 @@ trait BaseProbQueryAlgorithm[U[_]] doProbability(target, (t: T) => t == value) } } + + +trait StreamableProbQueryAlgorithm extends ProbQueryAlgorithm { + /** + * Sample an value from the posterior of this element + */ + def sampleFromPosterior[T](element: Element[T]): Stream[T] +} + 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 7eb85b74..c2dd1a83 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 @@ -137,7 +137,7 @@ class ProbQueryVariableEliminationDecision[T, U](override val universe: Universe * */ private def marginalizeToTarget(factor: Factor[(Double, Double)], target: Element[_]): Unit = { - val unnormalizedTargetFactor = factor.marginalizeTo(semiring, Variable(target)) + val unnormalizedTargetFactor = factor.marginalizeTo(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)) 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 66793b2f..3e0b16e9 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 @@ -41,7 +41,7 @@ trait FactoredAlgorithm[T] extends Algorithm { * If any of these elements has * in its range, the lower and upper bounds of factors will be different, so we need to compute both. * If they don't, we don't need to compute bounds. */ - def getNeededElements(starterElements: List[Element[_]], depth: Int): (List[Element[_]], Boolean) = { + def getNeededElements(starterElements: List[Element[_]], depth: Int, parameterized: Boolean = false): (List[Element[_]], Boolean) = { // Since there may be evidence on the dependent universes, we have to include their parents as important elements val dependentUniverseParents = for { @@ -63,7 +63,7 @@ trait FactoredAlgorithm[T] extends Algorithm { } // Make sure we compute values from scratch in case the elements have changed LazyValues.clear(universe) - val values = LazyValues(universe) + val values = LazyValues(universe, parameterized) /* * Beginning with the given element at the given depth, find all elements that the given element is used by within the depth. 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 ba397427..bfcfd3b0 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 @@ -24,13 +24,21 @@ import com.cra.figaro.util.MapResampler import com.cra.figaro.algorithm.factored.factors.Factor /** - * 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 + * Class to handle sampling from continuous elements to make factors + * @param numSamplesFromAtomics Maximum number of samples to take from atomic elements + * @param maxNumSamplesAtChain 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) { +class ParticleGenerator(de: DensityEstimator, val numSamplesFromAtomics: Int, val maxNumSamplesAtChain: Int) { + @deprecated("numArgSamples is deprecated. Please use numSamplesFromAtomics", "4.1") + val numArgSamples = numSamplesFromAtomics + + @deprecated("numTotalSamples is deprecated. Please use maxNumSamplesAtChain", "4.1") + val numTotalSamples = maxNumSamplesAtChain + + var warningIssued = false + // Caches the samples for an element private val sampleMap = Map[Element[_], (List[(Double, _)], Int)]() @@ -52,7 +60,7 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot /** * Retrieves the samples for an element using the default number of samples. */ - def apply[T](elem: Element[T]): List[(Double, T)] = apply(elem, numArgSamples) + def apply[T](elem: Element[T]): List[(Double, T)] = apply(elem, numSamplesFromAtomics) /** * Retrieves the samples for an element using the indicated number of samples @@ -63,6 +71,10 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot e.asInstanceOf[(List[(Double, T)], Int)]._1 } case None => { + if (!warningIssued) { + println("Warning: you are using a factored algorithm with continuous or infinite elements. The element will be sampled " + numSamples + " times") + warningIssued = true + } val sampler = ElementSampler(elem, numSamples) sampler.start val result = sampler.computeDistribution(elem).toList @@ -86,9 +98,9 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot def nextDouble(d: Double) = random.nextGaussian() * proposalVariance + d val numSamples = sampleMap(elem)._2 - + val sampleDensity: Double = 1.0 / numSamples - + // Generate new samples given the old samples for an element val newSamples = elem match { /* If the element is an instance of OneShifter (Geometric, poisson, etc), @@ -97,7 +109,7 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot case o: OneShifter => { val toResample = if (beliefs.size < numSamples) { val resampler = new MapResampler(beliefs.map(s => (s._1, s._2))) - List.fill(numSamples)(1.0/numSamples, resampler.resample) + List.fill(numSamples)(1.0 / numSamples, resampler.resample) } else { beliefs } @@ -118,7 +130,7 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot // return the new particles samples.groupBy(_._2).toList.map(s => (s._2.unzip._1.sum, s._2.head._2)) } - + /* For atomic doubles, we do the same thing as the OneShifters, but we assume * that we never need to resample since the number of particles equals numSamples. * We propose a new double and check its acceptance. Note the proposal is symmetric. @@ -146,9 +158,9 @@ class ParticleGenerator(de: DensityEstimator, val numArgSamples: Int, val numTot * we estimate the density using the density estimator, then multiple all of the estimates together. Finally, since * we only sample atomic elements, we multiple each result but the density of the values in the original element */ - private def accept[T](elem: Atomic[_], oldValue: T, newValue: T, proposalProb: Double, beliefs: List[List[(Double, T)]]): T = { - val oldDensity = beliefs.map(de.getDensity(oldValue, _)).product*elem.asInstanceOf[Atomic[T]].density(oldValue) - val newDensity = beliefs.map(de.getDensity(newValue, _)).product*elem.asInstanceOf[Atomic[T]].density(newValue) + private def accept[T](elem: Atomic[_], oldValue: T, newValue: T, proposalProb: Double, beliefs: List[List[(Double, T)]]): T = { + val oldDensity = beliefs.map(de.getDensity(oldValue, _)).product * elem.asInstanceOf[Atomic[T]].density(oldValue) + val newDensity = beliefs.map(de.getDensity(newValue, _)).product * elem.asInstanceOf[Atomic[T]].density(newValue) val ratio = (newDensity / oldDensity) * proposalProb val nextValue = if (ratio > 1) { @@ -164,12 +176,18 @@ object ParticleGenerator { /** * Maximum number of particles to generate per atomic */ - var defaultArgSamples = 15 + var defaultNumSamplesFromAtomics = 15 + @deprecated("defaultArgSamples is deprecated. Please use defaultNumSamplesFromAtomics", "4.1") + var defaultArgSamples = defaultNumSamplesFromAtomics + /** * Maximum number of particles to generate through a chain. */ - var defaultTotalSamples = 15 + var defaultMaxNumSamplesAtChain = 15 + + @deprecated("defaultTotalSamples is deprecated. Please use defaultMaxNumSamplesAtChain", "4.1") + var defaultTotalSamples = defaultMaxNumSamplesAtChain private val samplerMap: Map[Universe, ParticleGenerator] = Map() @@ -186,11 +204,11 @@ object ParticleGenerator { /** * Create a new particle generator for the given universe, using the given density estimatore, number of argument samples and total number of samples */ - def apply(univ: Universe, de: DensityEstimator, numArgSamples: Int, numTotalSamples: Int): ParticleGenerator = + def apply(univ: Universe, de: DensityEstimator, numSamplesFromAtomics: Int, maxNumSamplesAtChain: Int): ParticleGenerator = samplerMap.get(univ) match { case Some(e) => e case None => { - samplerMap += (univ -> new ParticleGenerator(de, numArgSamples, numTotalSamples)) + samplerMap += (univ -> new ParticleGenerator(de, numSamplesFromAtomics, maxNumSamplesAtChain)) univ.registerUniverse(samplerMap) samplerMap(univ) } @@ -200,7 +218,13 @@ object ParticleGenerator { * Create a new particle generate for a universe using a constant density estimator and default samples */ def apply(univ: Universe): ParticleGenerator = apply(univ, new ConstantDensityEstimator, - defaultArgSamples, defaultTotalSamples) + defaultNumSamplesFromAtomics, defaultMaxNumSamplesAtChain) + + /** + * Create a new particle generator for a universe using a constant density estimator and the number of argument samples and total number of samples + */ + def apply(univ: Universe, numSamplesFromAtomics: Int, maxNumSamplesAtChain: Int): ParticleGenerator = apply(univ, new ConstantDensityEstimator, + numSamplesFromAtomics, maxNumSamplesAtChain) /** * Check if a particle generate exists for this 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 90ef0a19..05338dd6 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 @@ -48,7 +48,7 @@ class VEGraph private (val info: Map[Variable[_], VariableInfo]) { * variables appearing in a factor with the eliminated variable, and excludes all factors in which the * eliminated variable appears. */ - def eliminate(variable: Variable[_]): VEGraph = { + def eliminate(variable: Variable[_]): (VEGraph, Double) = { val VariableInfo(oldFactors, allVars) = info(variable) val newFactor = AbstractFactor((allVars - variable).toList) var newInfo = VEGraph.makeInfo(info, List(newFactor), oldFactors) @@ -57,7 +57,7 @@ class VEGraph private (val info: Map[Variable[_], VariableInfo]) { newInfo += neighbor -> VariableInfo(oldNeighborFactors, oldNeighborNeighbors - variable) } newInfo(variable).neighbors foreach (removeNeighbor(_)) - (new VEGraph(newInfo)) + (new VEGraph(newInfo), VEGraph.cost(newFactor)) } /** 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 7a39ee3d..db0ccb96 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 @@ -222,7 +222,7 @@ class ProbQueryVariableElimination(override val universe: Universe, targets: Ele val semiring = SumProductSemiring() private def marginalizeToTarget(factor: Factor[Double], target: Element[_]): Unit = { - val unnormalizedTargetFactor = factor.marginalizeTo(semiring.asInstanceOf[Semiring[Double]], Variable(target)) + val unnormalizedTargetFactor = factor.marginalizeTo(Variable(target)) val z = unnormalizedTargetFactor.foldLeft(semiring.zero, _ + _) //val targetFactor = Factory.make[Double](unnormalizedTargetFactor.variables) val targetFactor = unnormalizedTargetFactor.mapTo((d: Double) => d / z) @@ -269,29 +269,32 @@ object VariableElimination { * minimizes the number of extra factor entries that would be created when it is eliminated. * Override this method if you want a different rule. * - * Returns the score of the ordering as well as the ordering. + * Returns the score of the ordering as well as the ordering. If useBestScore is set to false, then it returns the total score of the + * entire eliminiation operation */ - def eliminationOrder[T](factors: Traversable[Factor[T]], toPreserve: Traversable[Variable[_]]): (Double, List[Variable[_]]) = { + def eliminationOrder[T](factors: Traversable[Factor[T]], toPreserve: Traversable[Variable[_]], useBestScore: Boolean = true): (Double, 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, Double.NegativeInfinity, List()) + val initScore = if (useBestScore) Double.NegativeInfinity else 0.0 + eliminationOrderHelper(candidates, toPreserve, initialGraph, initScore, List(), useBestScore) } @tailrec private def eliminationOrderHelper(candidates: PriorityMap[Variable[_], Double], toPreserve: Traversable[Variable[_]], graph: VEGraph, currentScore: Double, - accum: List[Variable[_]]): (Double, List[Variable[_]]) = { + accum: List[Variable[_]], useBestScore: Boolean): (Double, List[Variable[_]]) = { if (candidates.isEmpty) (currentScore, accum.reverse) else { val (best, bestScore) = candidates.extractMin() // 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) + val (nextGraph, newCost) = graph.eliminate(best) touched foreach (v => candidates += v -> graph.score(v)) - eliminationOrderHelper(candidates, toPreserve, nextGraph, bestScore max currentScore, best :: accum) + val nextScore = if (useBestScore) bestScore max currentScore else newCost+currentScore + eliminationOrderHelper(candidates, toPreserve, nextGraph, nextScore, best :: accum, useBestScore) } } 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 5f71bd24..4f9e688c 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 @@ -55,8 +55,18 @@ trait BeliefPropagation[T] extends FactoredAlgorithm[T] { /** * Since BP uses division to compute messages, the semiring has to have a division function defined + * and must be log convertable. + * Note that BP operates in log space and any semiring must be log convertible + * If you define a non-log semiring, it will automatically convert, and convert it back to normal space at the end + * If you define a log semiring, it won't convert to log or convert from log. + * In other words, it outputs the answer in the space specified by the semiring */ - override val semiring: DivideableSemiRing[T] + override val semiring: LogConvertibleSemiRing[T] + + /** + * Returns the log space version of the semiring (or the semiring if already in log space) + */ + protected def logSpaceSemiring(): LogConvertibleSemiRing[T] = if (semiring.isLog) semiring else semiring.convert /** * Elements towards which queries are directed. By default, these are the target elements. @@ -94,11 +104,11 @@ trait BeliefPropagation[T] extends FactoredAlgorithm[T] { * messages from all other Nodes (except the destination node), * marginalized over all variables except the variable: */ - private def getNewMessageFactorToVar(fn: FactorNode, vn: VariableNode) = { + protected 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) + val total = beliefMap(fn).combination(vnFactor, logSpaceSemiring().divide) + total.marginalizeTo(vn.variable) } /* @@ -106,10 +116,10 @@ trait BeliefPropagation[T] extends FactoredAlgorithm[T] { * 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) = { + protected def getNewMessageVarToFactor(vn: VariableNode, fn: FactorNode) = { val fnFactor = factorGraph.getLastMessage(fn, vn) - val total = beliefMap(vn).combination(fnFactor, semiring.divide) + val total = beliefMap(vn).combination(fnFactor, logSpaceSemiring().divide) total } @@ -121,7 +131,7 @@ trait BeliefPropagation[T] extends FactoredAlgorithm[T] { if (messageList.isEmpty) { source match { - case fn: FactorNode => factorGraph.getFactorForNode(fn)//factorGraph.uniformFactor(fn.variables.toList) + case fn: FactorNode => factorGraph.getFactorForNode(fn) //factorGraph.uniformFactor(fn.variables.toList) case vn: VariableNode => factorGraph.uniformFactor(List(vn.variable)) } } else { @@ -184,9 +194,9 @@ trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { * Normalize a factor. */ def normalize(factor: Factor[Double]): Factor[Double] = { - val z = semiring.sumMany(factor.contents.values) + val z = logSpaceSemiring().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.mapTo((d: Double) => if (z != logSpaceSemiring().zero) d - z else logSpaceSemiring().zero) } /* @@ -202,22 +212,50 @@ trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { * 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 thisUniverseFactors = (neededElements flatMap (Factory.makeFactorsForElement(_, upperBounds, true))).filterNot(_.isEmpty) + val parameterized = this match { + case p: ParameterLearner => true + case _ => false + } + val thisUniverseFactors = (neededElements flatMap (Factory.makeFactorsForElement(_, upperBounds, parameterized))).filterNot(_.isEmpty) val dependentUniverseFactors = for { (dependentUniverse, evidence) <- dependentUniverses } yield Factory.makeDependentFactor(Variable.cc, universe, dependentUniverse, dependentAlgorithm(dependentUniverse, evidence)) val factors = dependentUniverseFactors ::: thisUniverseFactors - // To prevent underflow, we do all computation in log space - factors.map(makeLogarithmic(_)) + // To prevent underflow, we do all computation in log space + convertFactors(factors) + } + + /* + * Convert factors to log space if necessary + */ + protected def convertFactors(factors: List[Factor[Double]]) = { + if (factors.size == 0) { + factors + } else { + if (factors.head.semiring == semiring && semiring.isLog == false) { + // semiring in factors matches semiring for BP, and not in log space so need to convert + factors.map(makeLogarithmic(_)) + } else if (factors.head.semiring == semiring && semiring.isLog == true) { + // semiring in factors matches semiring for BP, and already in log space so no need to convert + factors + } else if (factors.head.semiring == semiring.convert() && semiring.isLog == false) { + // semiring in factors matches the converted semiring, and not in log space. But the factors are already in log space so no need to convert + factors + } else if (factors.head.semiring == semiring.convert() && semiring.isLog == true) { + // semiring in factors matches the converted semiring, and in log space. Need to convert + factors.map(makeLogarithmic(_)) + } else { + // Incompatible case. This might cause a problem if they are not converted eventually + factors.map(makeLogarithmic(_)) + } + } } private[figaro] def makeLogarithmic(factor: Factor[Double]): Factor[Double] = { - factor.mapTo((d: Double) => Math.log(d), LogSumProductSemiring()) + factor.mapTo((d: Double) => Math.log(d), logSpaceSemiring()) } private[figaro] def unmakeLogarithmic(factor: Factor[Double]): Factor[Double] = { - factor.mapTo((d: Double) => Math.exp(d), SumProductSemiring()) + factor.mapTo((d: Double) => Math.exp(d), logSpaceSemiring().convert) } /** @@ -259,7 +297,11 @@ trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { val variable = factor.variables(0) val ff = normalize(factor) - ff.getIndices.filter(f => variable.range(f.head).isRegular).map(f => (Math.exp(ff.get(f)), variable.range(f.head).value)).toList + val inOriginalSpace = semiring.isLog match { + case true => ff + case false => unmakeLogarithmic(ff) + } + inOriginalSpace.getIndices.filter(f => variable.range(f.head).isRegular).map(f => (inOriginalSpace.get(f), variable.range(f.head).value)).toList } /** @@ -275,7 +317,7 @@ trait ProbabilisticBeliefPropagation extends BeliefPropagation[Double] { * Trait for One Time BP algorithms. */ trait OneTimeProbabilisticBeliefPropagation extends ProbabilisticBeliefPropagation with OneTime { - val iterations: Int + def iterations: Int def run() = { if (debug) { val varNodes = factorGraph.getNodes.filter(_.isInstanceOf[VariableNode]) @@ -328,13 +370,17 @@ abstract class ProbQueryBeliefPropagation(override val universe: Universe, targe val queryTargets = targetElements - val semiring = LogSumProductSemiring() + val semiring = SumProductSemiring() var neededElements: List[Element[_]] = _ var needsBounds: Boolean = _ def generateGraph() = { - val needs = getNeededElements(starterElements, depth) + val parameterized = this match { + case p: ParameterLearner => true + case _ => false + } + val needs = getNeededElements(starterElements, depth, parameterized) neededElements = needs._1 needsBounds = needs._2 @@ -345,7 +391,7 @@ abstract class ProbQueryBeliefPropagation(override val universe: Universe, targe getFactors(neededElements, targetElements) } - factorGraph = new BasicFactorGraph(factors, semiring): FactorGraph[Double] + factorGraph = new BasicFactorGraph(factors, logSpaceSemiring()): FactorGraph[Double] } override def initialize() = { 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 1c9defa9..507fdab7 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 @@ -29,7 +29,7 @@ abstract class MPEBeliefPropagation(override val universe: Universe)( val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) extends MPEAlgorithm with ProbabilisticBeliefPropagation { - override val semiring = LogMaxProductSemiring() + override val semiring = MaxProductSemiring() /* * Empty for MPE Algorithms */ @@ -37,8 +37,7 @@ abstract class MPEBeliefPropagation(override val universe: Universe)( override def initialize() = { val (neededElements, _) = getNeededElements(universe.activeElements, Int.MaxValue) - - factorGraph = new BasicFactorGraph(getFactors(neededElements, targetElements), semiring): FactorGraph[Double] + factorGraph = new BasicFactorGraph(getFactors(neededElements, targetElements), logSpaceSemiring()): FactorGraph[Double] super.initialize } @@ -46,8 +45,10 @@ abstract class MPEBeliefPropagation(override val universe: Universe)( * Convert factors to use MaxProduct */ override def getFactors(allElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[Double]] = { - val factors = super.getFactors(allElements, targetElements, upper) - factors.map (_.mapTo(x => x, semiring)) + val factors = super.getFactors(allElements, targetElements, upper) + // Not needed since BP now converts factors to log space of the defined semiring + //factors.map (_.mapTo(x => x, logSpaceSemiring())) + factors } def mostLikelyValue[T](target: Element[T]): T = { diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/ProbEvidenceBeliefPropagation.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/ProbEvidenceBeliefPropagation.scala index 6eaf7216..faf709f6 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/ProbEvidenceBeliefPropagation.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/beliefpropagation/ProbEvidenceBeliefPropagation.scala @@ -33,13 +33,13 @@ import scala.collection.mutable.Map trait ProbEvidenceBeliefPropagation extends ProbabilisticBeliefPropagation with ProbEvidenceAlgorithm { - private def logFcn: (Double => Double) = (semiring: DivideableSemiRing[Double]) match { + private def logFcn: (Double => Double) = (logSpaceSemiring(): DivideableSemiRing[Double]) 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: DivideableSemiRing[Double]) match { - case LogSumProductSemiring() => (d: Double) => if (d == semiring.zero) 0 else math.exp(d) + private def probFcn: (Double => Double) = (logSpaceSemiring(): DivideableSemiRing[Double]) match { + case LogSumProductSemiring() => (d: Double) => if (d == logSpaceSemiring().zero) 0 else math.exp(d) case SumProductSemiring() => (d: Double) => d } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala index 571dbd49..a4e90994 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/BasicFactor.scala @@ -36,10 +36,6 @@ class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_ override def createFactor[T](parents: List[Variable[_]], output: List[Variable[_]], _semiring: Semiring[T] = semiring): Factor[T] = new BasicFactor[T](parents, output, _semiring) - override def convert[U](semiring: Semiring[U]): Factor[U] = { - createFactor[U](parents, output, semiring) - } - /** * 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. @@ -116,7 +112,8 @@ class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_ that: Factor[T], op: (T, T) => T): Factor[T] = { that match { - case _:SparseFactor[T] => that.combination(this, op) + // Switch the order of op because it might not be commutative + case _:SparseFactor[T] => that.combination(this, (a,b) => op(b, a)) case _ => { val (allParents, allChildren, indexMap1, indexMap2) = unionVars(that) val result: Factor[T] = that.createFactor(allParents, allChildren) @@ -135,21 +132,9 @@ class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_ } } - private def computeSum( - resultIndices: List[Int], - summedVariable: Variable[_], - summedVariableIndices: List[Int], - semiring: Semiring[T]): T = { - 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[_]): Factor[T] = { + variable: Variable[_], + sum: (T, T) => T = semiring.sum): Factor[T] = { if (variables contains variable) { // The summed over variable does not necessarily appear exactly once in the factor. val indicesOfSummedVariable = indices(variables, variable) @@ -174,7 +159,7 @@ class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_ val value = get(index) val newIndices: List[Int] = indexMap map (index(_)) val oldValue = result.get(newIndices) - result.set(newIndices, semiring.sum(oldValue, value)) + result.set(newIndices, sum(oldValue, value)) } } result @@ -226,13 +211,13 @@ class BasicFactor[T](val parents: List[Variable[_]], val output: List[Variable[_ result } - override def marginalizeTo( - semiring: Semiring[T], + override def marginalizeToWithSum( + sum: (T, T) => T, targets: Variable[_]*): Factor[T] = { val marginalized = (this.asInstanceOf[Factor[T]] /: variables)((factor: Factor[T], variable: Variable[_]) => if (targets contains variable) factor - else factor.sumOver(variable)) + else factor.sumOver(variable, sum)) // 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) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala index 23abbe63..a9de74d1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Factor.scala @@ -135,8 +135,12 @@ trait Factor[T] { * 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. + * + * If no funciton is provided, this defaults to the sum function in this factor's semiring. */ - def sumOver(variable: Variable[_]): Factor[T] + def sumOver( + variable: Variable[_], + sum: (T, T) => T = semiring.sum): Factor[T] /** * Returns a factor that maps values of the other variables to the value of the given variable that @@ -150,11 +154,22 @@ trait Factor[T] { def recordArgMax[U](variable: Variable[U], comparator: (T, T) => Boolean, _semiring: Semiring[U] = semiring.asInstanceOf[Semiring[U]]): Factor[U] /** - * Returns the marginalization of the factor to a variable according to the given addition function. - * This involves summing out all other variables. + * Returns the marginalization of the factor to a variable according to the addition + * function in this factor's semiring. This involves summing out all other variables. */ - def marginalizeTo( - semiring: Semiring[T], + def marginalizeTo(targets: Variable[_]*): Factor[T] = marginalizeToWithSum(semiring.sum, targets:_*) + + /** + * Returns the marginalization of the factor to a variable according to the given + * addition function. Unlike marginalizeTo, this uses the provided sum function. + * This is useful e.g. to easily switch between max-product and sum-product operations + * when the data within are unchanged and the product operation is the same. + * + * The returned factor uses the semiring associated with this factor; it does not + * override the sum function of the semiring with the function given here. + */ + def marginalizeToWithSum( + sum: (T, T) => T, targets: Variable[_]*): Factor[T] /** @@ -162,11 +177,6 @@ trait Factor[T] { */ def deDuplicate(): Factor[T] - /** - * Creates a new Factor of the same class with a different type and semiring - */ - def convert[U](semiring: Semiring[U]): Factor[U] - /** * Produce a readable string representation of the factor */ diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala index 5194ad4b..bde3ee45 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/Semiring.scala @@ -56,6 +56,18 @@ trait DivideableSemiRing[T] extends Semiring[T] { def divide(x: T, y: T): T } +trait LogConvertibleSemiRing[T] extends DivideableSemiRing[T] { + /** + * Is true if this semiring is in log space + */ + def isLog(): Boolean + /** + * Converts this semiring into its log inverse + * For non-log space semirings, converts to log, and for log semirings exponentiates + */ + def convert(): LogConvertibleSemiRing[T] +} + case class SumProductUtilitySemiring() extends DivideableSemiRing[(Double, Double)] { /** * Decision joint factor combination. @@ -102,7 +114,7 @@ case class BooleanSemiring() extends Semiring[Boolean] { val one = true } -case class SumProductSemiring() extends DivideableSemiRing[Double] { +case class SumProductSemiring() extends DivideableSemiRing[Double] with LogConvertibleSemiRing[Double] { /** * Standard multiplication */ @@ -125,12 +137,16 @@ case class SumProductSemiring() extends DivideableSemiRing[Double] { * 1 */ val one = 1.0 + + val isLog = false + + def convert() = LogSumProductSemiring() } /** * Semiring for computing sums and products with log probabilities. */ -case class LogSumProductSemiring() extends DivideableSemiRing[Double] { +case class LogSumProductSemiring() extends DivideableSemiRing[Double] with LogConvertibleSemiRing[Double] { val zero = Double.NegativeInfinity val one = 0.0 @@ -150,12 +166,16 @@ case class LogSumProductSemiring() extends DivideableSemiRing[Double] { } def sum(x: Double, y: Double) = sumMany(List(x, y)) + + val isLog = true + + def convert() = SumProductSemiring() } /** * Semiring for computing maxs and products with log probabilities. */ -case class LogMaxProductSemiring() extends DivideableSemiRing[Double] { +case class LogMaxProductSemiring() extends DivideableSemiRing[Double] with LogConvertibleSemiRing[Double] { val zero = Double.NegativeInfinity val one = 0.0 @@ -165,6 +185,10 @@ case class LogMaxProductSemiring() extends DivideableSemiRing[Double] { def divide(x: Double, y: Double) = if (y == zero) zero else x - y def sum(x: Double, y: Double) = x max y + + val isLog = true + + def convert() = MaxProductSemiring() } /** @@ -194,7 +218,8 @@ case class BoundsSumProductSemiring() extends DivideableSemiRing[(Double, Double val one = (1.0, 1.0) } -case class MaxProductSemiring() extends DivideableSemiRing[Double] { +case class MaxProductSemiring() extends DivideableSemiRing[Double] with LogConvertibleSemiRing[Double] { + /** * Standard multiplication */ @@ -218,5 +243,9 @@ case class MaxProductSemiring() extends DivideableSemiRing[Double] { * 1 */ val one = 1.0 + + val isLog = false + + def convert() = LogMaxProductSemiring() } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ChainFactory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ChainFactory.scala index 43707814..42bad4c5 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ChainFactory.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/ChainFactory.scala @@ -26,9 +26,19 @@ object ChainFactory { /** * Make the factors associated with a chain element. */ - - import com.cra.figaro.algorithm.factored.factors.factory.Factory - def makeFactors[T, U](cc: ComponentCollection, chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + + import com.cra.figaro.algorithm.factored.factors.factory.Factory + + def makeFactors[T, U](cc: ComponentCollection, chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val chainComp = cc(chain) + if (chainComp.allSubproblemsEliminatedCompletely && cc.useSingleChainFactor) { + makeSingleFactor[T, U](cc, chain)(mapper) + } else { + makeMultipleFactors[T, U](cc, chain)(mapper) + } + } + + def makeMultipleFactors[T, U](cc: ComponentCollection, chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { val chainComp = cc(chain) val parentVar = Factory.getVariable(cc, chain.parent) val chainVar = Factory.getVariable(cc, chain) @@ -51,8 +61,7 @@ object ChainFactory { cc.variableParents(chainVar) += actualVar chainComp.actualSubproblemVariables += parentVal.value -> actualVar List(Factory.makeConditionalSelector(pairVar, parentVal, actualVar, chainComp.range.regularValues)(mapper)) - } - else { + } 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 @@ -62,5 +71,44 @@ object ChainFactory { }) pairFactor :: tempFactors } - + + def makeSingleFactor[T, U](cc: ComponentCollection, chain: Chain[T, U])(implicit mapper: PointMapper[U]): List[Factor[Double]] = { + val chainComp = cc(chain) + val parentVar = Factory.getVariable(cc, chain.parent) + val childVar = Factory.getVariable(cc, chain) + val factor = new BasicFactor[Double](List(parentVar), List(childVar)) + for { parentIndex <- 0 until parentVar.range.length } { + val parentXV = parentVar.range(parentIndex) + if (parentXV.isRegular && chainComp.subproblems.contains(parentXV.value) && !chainComp.subproblems(parentXV.value).solution.isEmpty) { + val subproblem = chainComp.subproblems(parentXV.value) + // Need to normalize subsolution in case there's any nested evidence + val subsolution = subproblem.solution.reduce(_.product(_)) + //val sum = subsolution.foldLeft(subsolution.semiring.zero, subsolution.semiring.sum(_, _)) + val subVars = subsolution.variables + if (subVars.length == 1) { + val subVar = subVars(0) + for { subVal <- subVar.range } { + val childIndex = childVar.range.indexOf(subVal) + val subIndex = subVar.range.indexOf(subVal) + //val entry = subsolution.semiring.product(subsolution.get(List(subIndex)), 1.0 / sum) + factor.set(List(parentIndex, childIndex), subsolution.get(List(subIndex))) + } + } else { // This should be a case where the subproblem is empty and the value is * + val starIndex = childVar.range.indexWhere(!_.isRegular) + factor.set(List(parentIndex, starIndex), factor.semiring.one) + } + + } else { + for { childIndex <- 0 until childVar.range.length } { + val entry = if (childVar.range(childIndex).isRegular) factor.semiring.zero else factor.semiring.one + factor.set(List(parentIndex, childIndex), entry) + } + val childIndex = childVar.range.indexWhere(!_.isRegular) + factor.set(List(parentIndex, childIndex), factor.semiring.one) + } + } + List(factor) + + } + } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/Factory.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/Factory.scala index 4d983fb6..7671e710 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/Factory.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/factors/factory/Factory.scala @@ -207,41 +207,49 @@ object Factory { } } + + def parameterCheck(elem: Element[_], parameterized: Boolean): Boolean = { + elem match { + case parameter: DoubleParameter => parameterized + case _ => false + } + } + /** * Invokes Factor constructors for a standard set of Elements. This method uses various * secondary factories. */ def concreteFactors[T](cc: ComponentCollection, elem: Element[T], parameterized: Boolean): List[Factor[Double]] = { - elem match { - case flip: ParameterizedFlip => DistributionFactory.makeFactors(cc, flip, parameterized) - case pSelect: ParameterizedSelect[_] => SelectFactory.makeFactors(cc, pSelect, parameterized) - case pBin: ParameterizedBinomialFixedNumTrials => DistributionFactory.makeFactors(cc, pBin, parameterized) - case parameter: DoubleParameter => makeParameterFactors(cc, parameter) - case array: ArrayParameter => makeParameterFactors(cc, array) - case constant: Constant[_] => makeFactors(cc, constant) - case f: AtomicFlip => DistributionFactory.makeFactors(cc, f) - case f: CompoundFlip => DistributionFactory.makeFactors(cc, f) - case ab: AtomicBinomial => DistributionFactory.makeFactors(cc, ab) - case s: AtomicSelect[_] => SelectFactory.makeFactors(cc, s) - case s: CompoundSelect[_] => SelectFactory.makeFactors(cc, s) - case d: AtomicDist[_] => SelectFactory.makeFactors(cc, d) - case d: CompoundDist[_] => SelectFactory.makeFactors(cc, d) - case s: IntSelector => SelectFactory.makeFactors(cc, s) - case c: Chain[_, _] => ChainFactory.makeFactors(cc, c) - case a: Apply1[_, _] => ApplyFactory.makeFactors(cc, a) - case a: Apply2[_, _, _] => ApplyFactory.makeFactors(cc, a) - case a: Apply3[_, _, _, _] => ApplyFactory.makeFactors(cc, a) - case a: Apply4[_, _, _, _, _] => ApplyFactory.makeFactors(cc, a) - case a: Apply5[_, _, _, _, _, _] => ApplyFactory.makeFactors(cc, a) - case i: Inject[_] => makeFactors(cc, i) - case r: SingleValuedReferenceElement[_] => ComplexFactory.makeFactors(cc, r) - case r: MultiValuedReferenceElement[_] => ComplexFactory.makeFactors(cc, r) - case r: Aggregate[_, _] => ComplexFactory.makeFactors(cc, r) + (elem, parameterized) match { + case (flip: ParameterizedFlip, _) => DistributionFactory.makeFactors(cc, flip, parameterized) + case (pSelect: ParameterizedSelect[_], _) => SelectFactory.makeFactors(cc, pSelect, parameterized) + case (pBin: ParameterizedBinomialFixedNumTrials, _) => DistributionFactory.makeFactors(cc, pBin, parameterized) + case (parameter: DoubleParameter, true) => makeParameterFactors(cc, parameter) + case (array: ArrayParameter, true) => makeParameterFactors(cc, array) + case (constant: Constant[_], _) => makeFactors(cc, constant) + case (f: AtomicFlip, _) => DistributionFactory.makeFactors(cc, f) + case (f: CompoundFlip, _) => DistributionFactory.makeFactors(cc, f) + case (ab: AtomicBinomial, _) => DistributionFactory.makeFactors(cc, ab) + case (s: AtomicSelect[_], _) => SelectFactory.makeFactors(cc, s) + case (s: CompoundSelect[_], _) => SelectFactory.makeFactors(cc, s) + case (d: AtomicDist[_], _) => SelectFactory.makeFactors(cc, d) + case (d: CompoundDist[_], _) => SelectFactory.makeFactors(cc, d) + case (s: IntSelector, _) => SelectFactory.makeFactors(cc, s) + case (c: Chain[_, _], _) => ChainFactory.makeFactors(cc, c) + case (a: Apply1[_, _], _) => ApplyFactory.makeFactors(cc, a) + case (a: Apply2[_, _, _], _) => ApplyFactory.makeFactors(cc, a) + case (a: Apply3[_, _, _, _], _) => ApplyFactory.makeFactors(cc, a) + case (a: Apply4[_, _, _, _, _], _) => ApplyFactory.makeFactors(cc, a) + case (a: Apply5[_, _, _, _, _, _], _) => ApplyFactory.makeFactors(cc, a) + case (i: Inject[_], _) => makeFactors(cc, i) + case (r: SingleValuedReferenceElement[_], _) => ComplexFactory.makeFactors(cc, r) + case (r: MultiValuedReferenceElement[_], _) => ComplexFactory.makeFactors(cc, r) + case (r: Aggregate[_, _], _) => ComplexFactory.makeFactors(cc, r) //case m: MakeList[_] => ComplexFactory.makeFactors(cc, m) - case m: MakeArray[_] => ComplexFactory.makeFactors(cc, m) - case f: FoldLeft[_, _] => ComplexFactory.makeFactors(cc, f) - case f: FactorMaker[_] => f.makeFactors - case a: Atomic[_] => makeFactors(cc, a) + case (m: MakeArray[_], _) => ComplexFactory.makeFactors(cc, m) + case (f: FoldLeft[_, _], _) => ComplexFactory.makeFactors(cc, f) + case (f: FactorMaker[_], _) => f.makeFactors + case (a: Atomic[_], _) => makeFactors(cc, a) case _ => throw new UnsupportedAlgorithmException(elem) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/Gibbs.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/Gibbs.scala index bf5f4e72..c11cf278 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/Gibbs.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/Gibbs.scala @@ -58,12 +58,12 @@ trait Gibbs[T] extends BaseUnweightedSampler with FactoredAlgorithm[T] { /** * Number of samples to throw away initially. */ - val burnIn: Int + def burnIn(): Int /** * Iterations thrown away between samples. */ - val interval: Int + def interval(): Int /** * Method to create a blocking scheme given information about the model and factors. diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/WalkSAT.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/WalkSAT.scala index e65cf11f..760b5ac0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/WalkSAT.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/gibbs/WalkSAT.scala @@ -69,7 +69,7 @@ object WalkSAT { val adjacentFactors = nonConstraintFactors.filter(f => f.variables.contains(variableToSample)) val varAndParents = variableParents.getOrElse(variableToSample, Set()) + variableToSample // Marginalize to the variable and its parents - val parentFactors = adjacentFactors.map(_.marginalizeTo(semiring, varAndParents.toList:_*)) + val parentFactors = adjacentFactors.map(_.marginalizeTo(varAndParents.toList:_*)) // Produce a sample val sampleOption = (0 until variableToSample.size).find(sample => parentFactors.forall(factor => { factor.get(factor.variables.map(currentSamples.getOrElse(_, sample))) != semiring.zero diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/filtering/ParticleFilter.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/filtering/ParticleFilter.scala index 72953399..e7438d36 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/filtering/ParticleFilter.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/filtering/ParticleFilter.scala @@ -197,6 +197,7 @@ class OneTimeParticleFilter(static: Universe = new Universe(), initial: Universe def run(): Unit = { val lw = new LikelihoodWeighter(currentUniverse, new PermanentCache(currentUniverse)) doTimeStep((i: Int) => initialWeightedParticle(static, currentUniverse, lw)) + lw.deregisterDependencies() } /** @@ -210,6 +211,7 @@ class OneTimeParticleFilter(static: Universe = new Universe(), initial: Universe doTimeStep((i: Int) => addWeightedParticle(evidence, i, newWindow, lw)) previousUniverse = newWindow.previous currentUniverse = newWindow.current + lw.deregisterDependencies() } } 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 30538a77..ecb87a9a 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 @@ -33,7 +33,7 @@ import com.cra.figaro.algorithm.factored.ParticleGenerator * 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) { +class LazyValues(universe: Universe, paramaterized: Boolean = false) { 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. @@ -42,8 +42,8 @@ class LazyValues(universe: Universe) { // 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 _ => element.args + case c: Chain[_, _] => List(c.parent) + case _ => element.args } for { arg <- elementArgs } { @@ -52,22 +52,23 @@ class LazyValues(universe: Universe) { } Abstraction.fromPragmas(element.pragmas) match { - case None => concreteValues(element, depth, numArgSamples, numTotalSamples) + case None => concreteValues(element, depth, numArgSamples, numTotalSamples) case Some(abstraction) => abstractValues(element, abstraction, depth, numArgSamples, numTotalSamples) } } private def concreteValues[T](element: Element[T], depth: Int, numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = - element match { - case c: Constant[_] => withoutStar(Set(c.constant)) - case f: Flip => withoutStar(Set(true, false)) - case d: Select[_, _] => withoutStar(Set(d.outcomes: _*)) - case d: Dist[_, _] => + (element, paramaterized) match { + case (p: Parameter[_], true) => ValueSet.withoutStar(Set(p.MAPValue)) + case (c: Constant[_], _) => withoutStar(Set(c.constant)) + case (f: Flip, _) => withoutStar(Set(true, false)) + case (d: Select[_, _], _) => withoutStar(Set(d.outcomes: _*)) + case (d: Dist[_, _], _) => val componentSets = d.outcomes.map(storedValues(_)) componentSets.reduce(_ ++ _) //case i: FastIf[_] => withoutStar(Set(i.thn, i.els)) - case a: Apply1[_, _] => + case (a: Apply1[_, _], _) => val applyMap = getMap(a) val vs1 = LazyValues(a.arg1.universe).storedValues(a.arg1) val resultsSet = @@ -77,7 +78,7 @@ class LazyValues(universe: Universe) { getOrElseInsert(applyMap, arg1Val, a.fn(arg1Val)) } if (vs1.hasStar) withStar(resultsSet); else withoutStar(resultsSet) - case a: Apply2[_, _, _] => + case (a: Apply2[_, _, _], _) => val applyMap = getMap(a) val vs1 = LazyValues(a.arg1.universe).storedValues(a.arg1) val vs2 = LazyValues(a.arg2.universe).storedValues(a.arg2) @@ -92,7 +93,7 @@ class LazyValues(universe: Universe) { getOrElseInsert(applyMap, (arg1Val, arg2Val), a.fn(arg1Val, arg2Val)) } if (vs1.hasStar || vs2.hasStar) withStar(resultsList.toSet); else withoutStar(resultsList.toSet) - case a: Apply3[_, _, _, _] => + case (a: Apply3[_, _, _, _], _) => val applyMap = getMap(a) val vs1 = LazyValues(a.arg1.universe).storedValues(a.arg1) val vs2 = LazyValues(a.arg2.universe).storedValues(a.arg2) @@ -109,7 +110,7 @@ class LazyValues(universe: Universe) { getOrElseInsert(applyMap, (arg1Val, arg2Val, arg3Val), a.fn(arg1Val, arg2Val, arg3Val)) } if (vs1.hasStar || vs2.hasStar || vs3.hasStar) withStar(resultsList.toSet); else withoutStar(resultsList.toSet) - case a: Apply4[_, _, _, _, _] => + case (a: Apply4[_, _, _, _, _], _) => val applyMap = getMap(a) val vs1 = LazyValues(a.arg1.universe).storedValues(a.arg1) val vs2 = LazyValues(a.arg2.universe).storedValues(a.arg2) @@ -128,7 +129,7 @@ class LazyValues(universe: Universe) { getOrElseInsert(applyMap, (arg1Val, arg2Val, arg3Val, arg4Val), a.fn(arg1Val, arg2Val, arg3Val, arg4Val)) } if (vs1.hasStar || vs2.hasStar || vs3.hasStar || vs4.hasStar) withStar(resultsList.toSet); else withoutStar(resultsList.toSet) - case a: Apply5[_, _, _, _, _, _] => + case (a: Apply5[_, _, _, _, _, _], _) => val applyMap = getMap(a) val vs1 = LazyValues(a.arg1.universe).storedValues(a.arg1) val vs2 = LazyValues(a.arg2.universe).storedValues(a.arg2) @@ -149,7 +150,7 @@ class LazyValues(universe: Universe) { getOrElseInsert(applyMap, (arg1Val, arg2Val, arg3Val, arg4Val, arg5Val), a.fn(arg1Val, arg2Val, arg3Val, arg4Val, arg5Val)) } if (vs1.hasStar || vs2.hasStar || vs3.hasStar || vs4.hasStar || vs5.hasStar) withStar(resultsList.toSet); else withoutStar(resultsList.toSet) - case c: Chain[_, _] => + case (c: Chain[_, _], _) => 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 => @@ -168,28 +169,25 @@ class LazyValues(universe: Universe) { val chainMap = getMap(c) val parentVS = LazyValues(c.parent.universe).storedValues(c.parent) - val samplesPerValue = math.max(1, (numTotalSamples.toDouble/parentVS.regularValues.size).toInt) + val samplesPerValue = math.max(1, (numTotalSamples.toDouble / parentVS.regularValues.size).toInt) val resultVSs = findChainValues(c, chainMap, parentVS, samplesPerValue) val startVS: ValueSet[c.Value] = if (parentVS.hasStar) withStar[c.Value](Set()); else withoutStar[c.Value](Set()) resultVSs.foldLeft(startVS)(_ ++ _) - case i: Inject[_] => + case (i: Inject[_], _) => val elementVSs = i.args.map(arg => LazyValues(arg.universe).storedValues(arg)) val incomplete = elementVSs.exists(_.hasStar) val elementValues = elementVSs.toList.map(_.regularValues.toList) val resultValues = homogeneousCartesianProduct(elementValues: _*).toSet.asInstanceOf[Set[i.Value]] if (incomplete) withStar(resultValues); else withoutStar(resultValues) - case v: ValuesMaker[_] => { + case (v: ValuesMaker[_], _) => { v.makeValues(depth) } - case a: Atomic[_] => { - if (!ParticleGenerator.exists(universe)) { - println("Warning: Sampling element " + a + " even though no sampler defined for this universe") - } + case (a: Atomic[_], _) => { val thisSampler = ParticleGenerator(universe) - val samples = thisSampler(a, numArgSamples) + val samples = thisSampler(a, numArgSamples) withoutStar(samples.unzip._2.toSet) } case _ => @@ -198,7 +196,7 @@ class LazyValues(universe: Universe) { } private def abstractValues[T](element: Element[T], abstraction: Abstraction[T], depth: Int, - numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = { + numArgSamples: Int, numTotalSamples: Int): ValueSet[T] = { val (inputs, hasStar): (List[T], Boolean) = { element match { case _: Atomic[_] => @@ -241,9 +239,9 @@ class LazyValues(universe: Universe) { def apply[T](element: Element[T], depth: Int): ValueSet[T] = { val (numArgSamples, numTotalSamples) = if (ParticleGenerator.exists(universe)) { val pg = ParticleGenerator(universe) - (pg.numArgSamples, pg.numTotalSamples ) + (pg.numSamplesFromAtomics, pg.maxNumSamplesAtChain) } else { - (ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples) + (ParticleGenerator.defaultNumSamplesFromAtomics, ParticleGenerator.defaultMaxNumSamplesAtChain) } apply(element, depth, numArgSamples, numTotalSamples) } @@ -309,7 +307,7 @@ class LazyValues(universe: Universe) { def storedValues[T](element: Element[T]): ValueSet[T] = { memoValues.get(element) match { case Some((result, _)) => result.asInstanceOf[ValueSet[T]] - case None => new ValueSet[T](Set()) + case None => new ValueSet[T](Set()) } } @@ -353,7 +351,7 @@ class LazyValues(universe: Universe) { def getMap[U](apply: Apply[U]): Map[Any, U] = { getOrElseInsert(applyMaps, apply, Map[Any, Any]()).asInstanceOf[Map[Any, U]] } - + /** * Gets the mapping from apply arguments to values. */ @@ -421,11 +419,11 @@ object LazyValues { * Create an object for computing the range of values of elements in the universe. This object is only * created once for a universe. */ - def apply(universe: Universe = Universe.universe): LazyValues = { + def apply(universe: Universe = Universe.universe, parameterized: Boolean = false): LazyValues = { expansions.get(universe) match { case Some(e) => e case None => - val expansion = new LazyValues(universe) + val expansion = new LazyValues(universe, parameterized) expansions += (universe -> expansion) universe.registerUniverse(expansions) expansion 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 c3dafbe9..2e9cef43 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 @@ -206,7 +206,7 @@ with LazyAlgorithm { 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)) + val targetFactor = factor.marginalizeToWithSum(BoundsSumProductSemiring().sum, Variable(target)) targetFactors += target -> targetFactor } @@ -354,7 +354,7 @@ with LazyAlgorithm { 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) + val (nextGraph, newCost) = 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 9c72cdaa..16bf72db 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 @@ -45,18 +45,22 @@ class ValueSet[T](val xvalues: Set[Extended[T]]) { */ val regularValues = xvalues.filter(_.isRegular).map(_.value) - /* + /** + * Indicates whether there are no values, regular or star, in the value set + */ + val isEmpty = !hasStar && regularValues.isEmpty + /** * Returns the particular Star value in this value set. Throws an exception if there is no Star. */ def starValue: Star[T] = { try { - values.find(_.isInstanceOf[Star[T]]).get.asInstanceOf[Star[T]] + xvalues.find(_.isInstanceOf[Star[T]]).get.asInstanceOf[Star[T]] } catch { case _: NoSuchElementException => throw new RuntimeException("Attempt to get the Star value of a value set without a Star") } } - */ + /** * Apply a function to each value while keeping the same Star nature. 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 ebf80f1c..4d2f4f8d 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 @@ -24,6 +24,10 @@ import com.cra.figaro.algorithm.online.Online import com.cra.figaro.algorithm.factored.VariableElimination import com.cra.figaro.algorithm.factored.factors.Variable import com.cra.figaro.algorithm.sampling.Forward +import com.cra.figaro.algorithm.OneTimeProbQuery +import com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeProbabilisticBeliefPropagation +import com.cra.figaro.algorithm.factored.beliefpropagation.ProbQueryBeliefPropagation +import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler /** * Expectation maximization iteratively produces an estimate of sufficient statistics for learnable parameters, @@ -235,7 +239,10 @@ object EMWithBP { private def makeBP(numIterations: Int, targets: Seq[Element[_]])(universe: Universe) = { Variable.clearCache - BeliefPropagation(numIterations, targets: _*)(universe) + new ProbQueryBeliefPropagation(universe, targets: _*)( + List(), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + with OneTimeProbabilisticBeliefPropagation with OneTimeProbQuery with ParameterLearner { val iterations = numIterations } } /** * An expectation maximization algorithm using Belief Propagation sampling for inference. 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 52087c51..5644c5e3 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 @@ -16,8 +16,6 @@ package com.cra.figaro.algorithm.sampling import com.cra.figaro.language._ import com.cra.figaro.library.cache.Cache import com.cra.figaro.library.cache.NoCache -import com.cra.figaro.algorithm.sampling.LikelihoodWeighter - class ForwardWeighter(universe: Universe, cache: Cache) extends LikelihoodWeighter(universe, cache) { override def rejectionAction() = () 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 3ab2225d..8569e144 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 @@ -21,8 +21,8 @@ import scala.collection.mutable.{ Set, Map } import com.cra.figaro.experimental.particlebp.AutomaticDensityEstimator import com.cra.figaro.algorithm.factored.ParticleGenerator import com.cra.figaro.algorithm.sampling.parallel.ParImportance -import com.cra.figaro.algorithm.sampling.LikelihoodWeighter import com.cra.figaro.library.cache.PermanentCache +import com.cra.figaro.library.collection.Container /** * Importance samplers. @@ -45,6 +45,13 @@ abstract class Importance(universe: Universe, targets: Element[_]*) numSamples = 0 } + override def kill () { + super.kill() + lw.clearCache() + lw.deregisterDependencies() + universe.deregisterAlgorithm(this) + } + /* * 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. @@ -130,8 +137,8 @@ object Importance { /** * 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) + def probability[T](target: Element[T], predicate: T => Boolean)(implicit universe: Universe): Double = { + val alg = Importance(10000, target)(universe) alg.start() val result = alg.probability(target, predicate) alg.kill() @@ -141,11 +148,24 @@ object Importance { /** * Use IS 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)(implicit universe: Universe): Double = + probability(target, (t: T) => t == value)(universe) + + /** + * Use IS to sample the joint posterior distribution of several variables + */ + def sampleJointPosterior(targets: Element[_]*)(implicit universe: Universe): Stream[List[Any]] = { + val jointElement = Container(targets: _*).foldLeft(List[Any]())((l: List[Any], i: Any) => l :+ i) + val alg = Importance(10000, jointElement)(universe) + alg.start() + val posterior = alg.sampleFromPosterior(jointElement) + alg.kill() + posterior + } /** * The parallel implementation of IS */ def par = ParImportance + } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/LikelihoodWeighter.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/LikelihoodWeighter.scala index 88f8eb3b..cb96b185 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/LikelihoodWeighter.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/LikelihoodWeighter.scala @@ -39,6 +39,20 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { private[figaro] val dependencies = scala.collection.mutable.Map[Element[_], Set[Element[_]]]() universe.register(dependencies) + /** + * Clear the cache + */ + def clearCache() = { + cache.clear + } + + /** + * Deregister the map of dependencies between elements + */ + def deregisterDependencies() = { + universe.deregister(dependencies) + } + /** * Sample each element in the list of elements and compute their likelihood weight */ @@ -51,7 +65,7 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { } /* - * Traverse the elements in generative order, and return the weight + * Traverse the elements in generative order, and return the weight */ @tailrec private[figaro] final def traverse(currentStack: List[(Element[_], Option[_], Option[Element[_]])], @@ -146,7 +160,7 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { /* - * Finds the set of elements that need to be resampled when the likelihood weighting went in the wrong order + * Finds the set of elements that need to be resampled when the likelihood weighting went in the wrong order */ private def findDependentElements(elem: Element[_], result: Element[_]) = { val chainUsedBy = universe.usedBy(elem) + elem @@ -155,7 +169,7 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { } /* - * Get the observation on an element, merging with any propagated observation from likelihood weighting + * Get the observation on an element, merging with any propagated observation from likelihood weighting */ protected def getObservation(element: Element[_], observation: Option[_]): Option[Any] = { (observation, element.observation) match { @@ -177,7 +191,7 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { * * If there is an observation on the element, we implement likelihood weighting. If the element has a density * function, we add the log density to the current weight. If it doesn't have a density, we check to see if - * it satisfies the observation + * it satisfies the observation */ private[figaro] def computeNextWeight(currentWeight: Double, element: Element[_], obs: Option[_]): Double = { val nextWeight = if (obs.isEmpty) { @@ -210,7 +224,7 @@ class LikelihoodWeighter(universe: Universe, cache: Cache) { protected def rejectionAction(): Unit = throw Importance.Reject /* - * Undo the application of this elements weight if we did likelihood weighting in the wrong order + * Undo the application of this elements weight if we did likelihood weighting in the wrong order */ private def undoWeight(weight: Double, elem: Element[_]) = weight - computeNextWeight(0.0, elem, elem.observation) 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 369c9bf4..3a689edb 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 @@ -21,6 +21,7 @@ import scala.language.existentials import scala.math.log import scala.annotation.tailrec import com.cra.figaro.library.cache._ +import com.cra.figaro.library.collection.Container /** * Metropolis-Hastings samplers. @@ -45,7 +46,7 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc protected var accepts = 0 protected var rejects = 0 - private val currentConstraintValues: Map[Element[_], Double] = Map() + protected val currentConstraintValues: Map[Element[_], Double] = Map() universe.register(currentConstraintValues) /** @@ -60,7 +61,7 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc private def newState: State = State(Map(), Map(), 0.0, 0.0, scala.collection.mutable.Set(), List()) - private val fastTargets = targets.toSet + protected val fastTargets = targets.toSet protected var chainCache: Cache = new MHCache(universe) @@ -83,11 +84,11 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc result.value } } 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) val newDissatisfied = if (elem.condition(newValue)) state.dissatisfied -= elem; else state.dissatisfied += elem - elem.value = newValue + elem.value = newValue State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied, state.visitOrder :+ elem) } @@ -178,10 +179,10 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc val state1 = switch(state, proposeChainCheck(elem1), proposeChainCheck(elem2)) continue(state1, rest) } - + private def proposeChainCheck(elem: Element[_]): Element[_] = { val e = chainCache(elem) - if (e.isEmpty) elem else e.get.asInstanceOf[Element[_]] + if (e.isEmpty) elem else e.get.asInstanceOf[Element[_]] } protected def runScheme(): State = runStep(newState, proposalScheme) @@ -285,11 +286,11 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc state.visitOrder.foreach { elem => elem match { - case c: Chain[_, _] => { + case c: Chain[_, _] => { chainCache(c) match { - case Some(result) => c.value = result.value.asInstanceOf[c.Value] + case Some(result) => c.value = result.value.asInstanceOf[c.Value] case None => throw new AlgorithmException - } + } } case _ => { if (state.oldRandomness.contains(elem)) elem.randomness = state.oldRandomness(elem).asInstanceOf[elem.Randomness] @@ -405,6 +406,15 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc proposalCounts = savedProposalCounts (acceptanceRatio, successResults, proposalResults) } + + /** + * Clean up the sampler, freeing memory. + */ + override def cleanUp(): Unit = { + super.cleanUp() + universe.clearTemporaries() + chainCache.clear() + } } /** @@ -425,14 +435,6 @@ class AnytimeMetropolisHastings(universe: Universe, super.initialize() doInitialize() } - - /** - * Clean up the sampler, freeing memory. - */ - override def cleanUp(): Unit = { - universe.clearTemporaries() - super.cleanUp() - } } /** @@ -521,6 +523,18 @@ object MetropolisHastings { def probability[T](target: Element[T], value: T): Double = probability(target, (t: T) => t == value) + /** + * Use MH to sample the joint posterior distribution of several variables + */ + def sampleJointPosterior(targets: Element[_]*)(implicit universe: Universe): Stream[List[Any]] = { + val jointElement = Container(targets: _*).foldLeft(List[Any]())((l: List[Any], i: Any) => l :+ i) + val alg = MetropolisHastings(1000000, ProposalScheme.default, jointElement)(universe) + alg.start() + val posterior = alg.sampleFromPosterior(jointElement) + alg.kill() + posterior + } + private[figaro] case class State(oldValues: Map[Element[_], Any], oldRandomness: Map[Element[_], Any], proposalProb: Double, 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 fb1b8975..0fc2d387 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 @@ -87,10 +87,10 @@ abstract class MetropolisHastingsAnnealer(universe: Universe, proposalScheme: Pr private def saveState: Map[Element[_], Any] = { bestEnergy = currentEnergy - Map(universe.activeElements.map(e => (e -> e.value)): _*) + Map(universe.permanentElements.map(e => (e -> e.value)): _*) } - override protected def initUpdates() = allLastUpdates = Map(universe.activeElements.map(e => (e -> (e.value, 0))): _*) + override protected def initUpdates() = allLastUpdates = Map(universe.permanentElements.map(e => (e -> (e.value, 0))): _*) override protected def updateTimesSeenForTarget[T](elem: Element[T], newValue: T): Unit = { allLastUpdates += (elem -> (newValue, sampleCount)) @@ -122,7 +122,7 @@ abstract class MetropolisHastingsAnnealer(universe: Universe, proposalScheme: Pr val nextState = mhStep() currentEnergy += nextState.modelProb } - initUpdates() + initUpdates() if (dissatisfied.nonEmpty) bestEnergy = Double.MinValue else bestEnergy = currentEnergy } 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 471034c0..500e1df8 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 @@ -26,8 +26,8 @@ import com.cra.figaro.library.cache.PermanentCache */ abstract class ProbEvidenceSampler(override val universe: Universe, override val evidence: List[NamedEvidence[_]] = List[NamedEvidence[_]](), partition: Double = 1.0) extends ProbEvidenceAlgorithm with Sampler { - private var successWeight: Double = _ - private var totalWeight: Double = _ + protected var successWeight: Double = _ + protected var totalWeight: Double = _ //Logarithmic versions. @@ -69,6 +69,14 @@ abstract class ProbEvidenceSampler(override val universe: Universe, override val if (!active) throw new AlgorithmInactiveException logComputedResult } + + override def cleanUp(): Unit = { + // Prevent memory leaks for when we run multiple ProbEvidenceSamplers in the same universe + super.cleanUp() + lw.clearCache() + lw.deregisterDependencies() + universe.deregisterAlgorithm(this) + } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/UnweightedSampler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/UnweightedSampler.scala index 339dfb9c..83e885bf 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/UnweightedSampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/UnweightedSampler.scala @@ -19,6 +19,7 @@ import scala.language.postfixOps import com.cra.figaro.algorithm._ import com.cra.figaro.language._ +import com.cra.figaro.util._ /** * Samplers that use samples without weights. @@ -77,7 +78,7 @@ abstract class BaseUnweightedSampler(val universe: Universe, targets: Element[_] val s = sample() if (sampleCount == 0 && s._1) { initUpdates - } + } if (s._1) { sampleCount += 1 s._2 foreach (t => updateTimesSeenForTarget(t._1.asInstanceOf[Element[t._1.Value]], t._2.asInstanceOf[t._1.Value])) @@ -86,14 +87,14 @@ abstract class BaseUnweightedSampler(val universe: Universe, targets: Element[_] protected def update(): Unit = { sampleCount += 1 - if (allLastUpdates.nonEmpty) targets.foreach(t => updateTimesSeenForTarget(t.asInstanceOf[Element[t.Value]], t.value)) + if (allLastUpdates.nonEmpty) targets.foreach(t => updateTimesSeenForTarget(t.asInstanceOf[Element[t.Value]], t.value)) sampleCount -= 1 } } -trait UnweightedSampler extends BaseUnweightedSampler with ProbQuerySampler { - +trait UnweightedSampler extends BaseUnweightedSampler with ProbQuerySampler with StreamableProbQueryAlgorithm { + /** * Total weight of samples taken, in log space */ @@ -101,12 +102,28 @@ trait UnweightedSampler extends BaseUnweightedSampler with ProbQuerySampler { override protected[algorithm] def computeProjection[T](target: Element[T]): List[(T, Double)] = { if (allLastUpdates.nonEmpty) { - val timesSeen = allTimesSeen.find(_._1 == target).get._2.asInstanceOf[Map[T, Int]] - timesSeen.mapValues(_ / sampleCount.toDouble).toList + val timesSeen = allTimesSeen.find(_._1 == target).get._2.asInstanceOf[Map[T, Int]] + timesSeen.mapValues(_ / sampleCount.toDouble).toList } else { println("Error: MH sampler did not accept any samples") List() } } + + def sampleFromPosterior[T](element: Element[T]): Stream[T] = { + def nextSample(posterior: List[(Double, T)]): Stream[T] = sampleMultinomial(posterior) #:: nextSample(posterior) + + if (!allTimesSeen.contains(element)) { + throw new NotATargetException(element) + } else { + val (values, weights) = allTimesSeen(element).toList.unzip + if (values.isEmpty) throw new NotATargetException(element) + + if (sampleCount == 0) throw new ZeroTotalUnnormalizedProbabilityException + + val weightedValues = weights.map(w => w.toDouble/sampleCount.toDouble).zip(values.asInstanceOf[List[T]]) + nextSample(weightedValues) + } + } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala index af6bacaa..dbec27d7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala @@ -17,12 +17,12 @@ import com.cra.figaro.algorithm._ import com.cra.figaro.language._ import scala.collection.mutable.Map import scala.language.postfixOps -import com.cra.figaro.util.logSum +import com.cra.figaro.util._ /** * Samplers that use weighted samples. */ -abstract class WeightedSampler(override val universe: Universe, targets: Element[_]*) extends ProbQuerySampler with Sampler { +abstract class WeightedSampler(override val universe: Universe, targets: Element[_]*) extends ProbQuerySampler with Sampler with StreamableProbQueryAlgorithm { lazy val queryTargets = targets.toList /** * A sample consists of a weight and a map from elements to their values. @@ -79,4 +79,22 @@ abstract class WeightedSampler(override val universe: Universe, targets: Element val weightSeen = allWeightsSeen.find(_._1 == target).get._2.asInstanceOf[Map[T, Double]] weightSeen.mapValues(s => math.exp(s - getTotalWeight)).toList } + + def sampleFromPosterior[T](element: Element[T]): Stream[T] = { + def nextSample(posterior: List[(Double, T)]): Stream[T] = sampleMultinomial(posterior) #:: nextSample(posterior) + + val elementIndex = allWeightsSeen.indexWhere(_._1 == element) + if (elementIndex < 0) { + throw new NotATargetException(element) + } else { + val (values, weights) = allWeightsSeen(elementIndex)._2.toList.unzip + if (values.isEmpty) throw new NotATargetException(element) + + val logSum = logSumMany(weights) + if (logSum == Double.NegativeInfinity) throw new ZeroTotalUnnormalizedProbabilityException + + val weightedValues = weights.map(w => math.exp(w - logSum)).zip(values.asInstanceOf[List[T]]) + nextSample(weightedValues) + } + } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ComponentCollection.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ComponentCollection.scala index ab4a48a1..bc108ab0 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ComponentCollection.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ComponentCollection.scala @@ -1,126 +1,143 @@ -/* - * ComponentCollection.scala - * A data structure that holds all the problem components used in a top-level problem and its subproblems. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: March 1, 2015 - * - * Copyright 2015 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.structured - -import com.cra.figaro.language._ -import com.cra.figaro.library.collection.MakeArray -import scala.collection.mutable.Map -import com.cra.figaro.algorithm.factored.factors._ - - -/** - * A collection of problem components. This data structure manages all the components being used in the solution of a top-level - * problem and its nested subproblems. - */ - -/* - * Every element exists in at most one component. - * To create a new component for an element, you need to say what problem it belongs to. - */ -class ComponentCollection { - /** - * Maps a variable to the parents needed for creating blocks using Gibbs sampling. - * TODO: test if this variable causes memory leaks. - */ - val variableParents: Map[Variable[_], Set[Variable[_]]] = Map().withDefaultValue(Set()) - - /** All the components in the collection, each associated with an element. */ - val components: Map[Element[_], ProblemComponent[_]] = Map() - - /** - * Intermediate variables defined during the construction of factors. - * These are not associated with any element or component and are to be eliminated wherever they appear. - */ - var intermediates: Set[Variable[_]] = Set() - - /** - * A map from a function and parent value to the associated subproblem. - */ - val expansions: Map[(Function1[_, Element[_]], _), NestedProblem[_]] = Map() - - /** - * Get the nested subproblem associated with a particular function and parent value. - * Checks in the cache if an expansion exists and creates one if necessary. - */ - private[structured] def expansion[P, V](component: ExpandableComponent[P, V], function: Function1[P, Element[V]], parentValue: P): NestedProblem[V] = { - expansions.get((function, parentValue)) match { - case Some(p) => - p.asInstanceOf[NestedProblem[V]] - case None => - val result = new NestedProblem(this, component.expandFunction(parentValue)) - expansions += (function, parentValue) -> result - result - } - } - - /** - * Returns the problem component associated with a particular variable. - * Not valid for intermediate variables. - */ - val variableToComponent: Map[Variable[_], ProblemComponent[_]] = Map() - - /** Does the element have a component in this collection? */ - def contains[T](element: Element[T]): Boolean = - components.contains(element) - - /** - * Get the component associated with this element in this collection. - * Throws an exception if the element is not associated with any component. - */ - def apply[T](element: Element[T]): ProblemComponent[T] = components(element).asInstanceOf[ProblemComponent[T]] - /** - * Get the component associated with this element in this collection. - * Throws an exception if the element is not associated with any component. - */ - def apply[P, T](chain: Chain[P, T]): ChainComponent[P, T] = components(chain).asInstanceOf[ChainComponent[P, T]] - - /** - * Get the component associated with this element in this collection. - * Throws an exception if the element is not associated with any component. - */ - def apply[T](apply: Apply[T]): ApplyComponent[T] = components(apply).asInstanceOf[ApplyComponent[T]] - - /** - * Get the component associated with this element in this collection. - * Throws an exception if the element is not associated with any component. - */ - def apply[T](makeArray: MakeArray[T]): MakeArrayComponent[T] = components(makeArray).asInstanceOf[MakeArrayComponent[T]] - - /** - * Add a component for the given element in the given problem to the component collection and return the component. - */ - private[structured] def add[T](element: Element[T], problem: Problem): ProblemComponent[T] = { - if (problem.collection.contains(element)) { - val component = problem.collection(element) - if (component.problem != problem) throw new RuntimeException("Trying to add a component to a different problem") - component - } - else { - val component: ProblemComponent[T] = - element match { - case chain: Chain[_, T] => new ChainComponent(problem, chain) - case makeArray: MakeArray[_] => new MakeArrayComponent(problem, makeArray) - case apply: Apply[_] => new ApplyComponent(problem, apply) - case _ => new ProblemComponent(problem, element) - } - components += element -> component - problem.components ::= component - component - } - } - - private[structured] def remove[T](element: Element[T]) { - components -= element - } -} +/* + * ComponentCollection.scala + * A data structure that holds all the problem components used in a top-level problem and its subproblems. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: March 1, 2015 + * + * Copyright 2015 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.structured + +import com.cra.figaro.language._ +import com.cra.figaro.library.collection.MakeArray +import scala.collection.mutable.Map +import com.cra.figaro.algorithm.factored.factors._ +import scala.collection.mutable.HashMap +/** +* To speed up factor creation time, it's necessary to override the hashcode of component collections. +*/ +object ComponentHash { + var hashCodeState = 10 + def nextCode: Int = { + hashCodeState += 1 + hashCodeState + } +} + +/** + * A collection of problem components. This data structure manages all the components being used in the solution of a top-level + * problem and its nested subproblems. + */ + +/* + * Every element exists in at most one component. + * To create a new component for an element, you need to say what problem it belongs to. + */ +class ComponentCollection { + /** Indicates whether to create chain factors by decomposing the chain into several factors or a single factor + * This defaults to false since all the existing code a decomposition + */ + var useSingleChainFactor = false + + /** + * Maps a variable to the parents needed for creating blocks using Gibbs sampling. + * TODO: test if this variable causes memory leaks. + */ + val variableParents: Map[Variable[_], Set[Variable[_]]] = Map().withDefaultValue(Set()) + + /** All the components in the collection, each associated with an element. */ + val components: Map[Element[_], ProblemComponent[_]] = new HashMap[Element[_], ProblemComponent[_]]() { + override val hashCode = ComponentHash.nextCode + } + + /** + * Intermediate variables defined during the construction of factors. + * These are not associated with any element or component and are to be eliminated wherever they appear. + */ + var intermediates: Set[Variable[_]] = Set() + + /** + * A map from a function and parent value to the associated subproblem. + */ + val expansions: Map[(Function1[_, Element[_]], _), NestedProblem[_]] = Map() + + /** + * Get the nested subproblem associated with a particular function and parent value. + * Checks in the cache if an expansion exists and creates one if necessary. + */ + private[structured] def expansion[P, V](component: ExpandableComponent[P, V], function: Function1[P, Element[V]], parentValue: P): NestedProblem[V] = { + expansions.get((function, parentValue)) match { + case Some(p) => + p.asInstanceOf[NestedProblem[V]] + case None => + val result = new NestedProblem(this, component.expandFunction(parentValue)) + expansions += (function, parentValue) -> result + result + } + } + + /** + * Returns the problem component associated with a particular variable. + * Not valid for intermediate variables. + */ + val variableToComponent: Map[Variable[_], ProblemComponent[_]] = Map() + + /** Does the element have a component in this collection? */ + def contains[T](element: Element[T]): Boolean = + components.contains(element) + + /** + * Get the component associated with this element in this collection. + * Throws an exception if the element is not associated with any component. + */ + def apply[T](element: Element[T]): ProblemComponent[T] = components(element).asInstanceOf[ProblemComponent[T]] + /** + * Get the component associated with this element in this collection. + * Throws an exception if the element is not associated with any component. + */ + def apply[P, T](chain: Chain[P, T]): ChainComponent[P, T] = components(chain).asInstanceOf[ChainComponent[P, T]] + + /** + * Get the component associated with this element in this collection. + * Throws an exception if the element is not associated with any component. + */ + def apply[T](apply: Apply[T]): ApplyComponent[T] = components(apply).asInstanceOf[ApplyComponent[T]] + + /** + * Get the component associated with this element in this collection. + * Throws an exception if the element is not associated with any component. + */ + def apply[T](makeArray: MakeArray[T]): MakeArrayComponent[T] = components(makeArray).asInstanceOf[MakeArrayComponent[T]] + + /** + * Add a component for the given element in the given problem to the component collection and return the component. + */ + private[structured] def add[T](element: Element[T], problem: Problem): ProblemComponent[T] = { + if (problem.collection.contains(element)) { + val component = problem.collection(element) + if (component.problem != problem) throw new RuntimeException("Trying to add a component to a different problem") + component + } + else { + val component: ProblemComponent[T] = + element match { + case chain: Chain[_, T] => new ChainComponent(problem, chain) + case makeArray: MakeArray[_] => new MakeArrayComponent(problem, makeArray) + case apply: Apply[_] => new ApplyComponent(problem, apply) + case _ => new ProblemComponent(problem, element) + } + components += element -> component + problem.components ::= component + component + } + } + + private[structured] def remove[T](element: Element[T]) { + components -= element + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Problem.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Problem.scala index 380a2cc1..b56edc03 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Problem.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Problem.scala @@ -40,7 +40,14 @@ class Problem(val collection: ComponentCollection, val targets: List[Element[_]] * Factors over globals produced by solving the problem. */ var solution: List[Factor[Double]] = List() - + + /** + * A map for each variable that indicates the value of the variable that is maximal for each possible value of the interface + * The support of each factor is over the product of the supports of the interface variables + * + */ + var recordingFactors: Map[Variable[_], Factor[_]] = Map() + /** * A flag indicating whether the problem has been solved. */ @@ -98,13 +105,12 @@ class Problem(val collection: ComponentCollection, val targets: List[Element[_]] val allVariables = (Set[Variable[_]]() /: allFactors)(_ ++ _.variables) val (toEliminate, toPreserve) = allVariables.partition(internal(_)) globals = toPreserve.map(collection.variableToComponent(_)) - solution = strategy.solve(this, toEliminate, toPreserve, allFactors) + val (solution, recordingFactors) = strategy.solve(this, toEliminate, toPreserve, allFactors) + this.solution = solution + this.recordingFactors = recordingFactors solved = true toEliminate.foreach((v: Variable[_]) => { if (collection.intermediates.contains(v)) collection.intermediates -= v - // It's unsafe to remove the component for the element because it might appear in a reused - // version of the nested subproblem. -// else collection.remove(collection.variableToComponent(v).element) }) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ProblemComponent.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ProblemComponent.scala index 04637cb8..994c222d 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ProblemComponent.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/ProblemComponent.scala @@ -93,7 +93,7 @@ class ProblemComponent[Value](val problem: Problem, val element: Element[Value]) * The range will include * based on argument ranges including * or any subproblem not being expanded.\ * */ - def generateRange(numValues: Int = ParticleGenerator.defaultTotalSamples) { + def generateRange(numValues: Int = ParticleGenerator.defaultMaxNumSamplesAtChain) { val newRange = Range(this, numValues) if ((newRange.hasStar ^ range.hasStar) || (newRange.regularValues != range.regularValues)) { range = newRange @@ -147,7 +147,7 @@ class ApplyComponent[Value](problem: Problem, element: Element[Value]) extends P * @param element the element to which this component corresponds */ abstract class ExpandableComponent[ParentValue, Value](problem: Problem, parent: Element[ParentValue], element: Element[Value]) - extends ProblemComponent(problem, element) { + extends ProblemComponent(problem, element) { /** * Expand for all values of the parent, based on the current range of the parent. */ @@ -159,25 +159,25 @@ abstract class ExpandableComponent[ParentValue, Value](problem: Problem, parent: /** Expand for a particular parent value. */ def expand(parentValue: ParentValue): Unit - - val expandFunction: (ParentValue) => Element[Value] + + val expandFunction: (ParentValue) => Element[Value] + + /** + * The subproblems nested inside this expandable component. + * They are created for particular parent values. + */ + var subproblems: Map[ParentValue, NestedProblem[Value]] = Map() } /** * A problem component created for a chain element. */ class ChainComponent[ParentValue, Value](problem: Problem, val chain: Chain[ParentValue, Value]) - extends ExpandableComponent[ParentValue, Value](problem, chain.parent, chain) { + extends ExpandableComponent[ParentValue, Value](problem, chain.parent, chain) { val elementsCreated: scala.collection.mutable.Set[Element[_]] = scala.collection.mutable.Set() ++ chain.universe.contextContents(chain) val expandFunction = chain.get _ - - /** - * The subproblems represent nested problems from chains. - * They are created for particular parent values. - */ - var subproblems: Map[ParentValue, NestedProblem[Value]] = Map() /** * The subproblems are defined in terms of formal variables. @@ -198,15 +198,33 @@ class ChainComponent[ParentValue, Value](problem: Problem, val chain: Chain[Pare subproblems += parentValue -> subproblem } + /* + * If all the subproblems have been eliminated completely and use no globals, we can use the new chain method. + */ + private[figaro] def allSubproblemsEliminatedCompletely: Boolean = { + for { + (parentValue, subproblem) <- subproblems + } { + val factors = subproblem.solution + val vars = factors.flatMap(_.variables) + val comps = vars.map(problem.collection.variableToComponent(_)) + if (comps.exists(!subproblem.components.contains(_))) return false // the factors contain a global variable + if (problem.collection(subproblem.target).problem != subproblem) return false // the target is global + } + return true + } + /** * Make the non-constraint factors for this component by using the solutions to the subproblems. */ override def makeNonConstraintFactors(parameterized: Boolean = false) { super.makeNonConstraintFactors(parameterized) - for { - (parentValue, subproblem) <- subproblems - } { - raiseSubproblemSolution(parentValue, subproblem) + if (!problem.collection.useSingleChainFactor || !allSubproblemsEliminatedCompletely) { + for { + (parentValue, subproblem) <- subproblems + } { + raiseSubproblemSolution(parentValue, subproblem) + } } } @@ -243,13 +261,13 @@ class ChainComponent[ParentValue, Value](problem: Problem, val chain: Chain[Pare * A problem component for a MakeArray element. */ class MakeArrayComponent[Value](problem: Problem, val makeArray: MakeArray[Value]) - extends ExpandableComponent[Int, FixedSizeArray[Value]](problem, makeArray.numItems, makeArray) { + extends ExpandableComponent[Int, FixedSizeArray[Value]](problem, makeArray.numItems, makeArray) { /** The maximum number of items expanded so far. */ var maxExpanded = 0 // This isn't really needed in MakeArray since the main expand function won't call this. val expandFunction = (i: Int) => Constant(makeArray.makeValues(i).regularValues.head) - + /** * Ensure that the given number of items is expanded. */ diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Range.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Range.scala index bfe753ee..444347c1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Range.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/Range.scala @@ -24,6 +24,7 @@ import com.cra.figaro.library.collection.FixedSizeArray import com.cra.figaro.library.atomic.discrete.{ AtomicBinomial, ParameterizedBinomialFixedNumTrials } import com.cra.figaro.library.compound.FoldLeft import com.cra.figaro.library.compound.IntSelector +import com.cra.figaro.algorithm.ValuesMaker object Range { private def getRange[U](collection: ComponentCollection, otherElement: Element[U]): ValueSet[U] = { @@ -154,10 +155,10 @@ object Range { def apply[V](component: ProblemComponent[V], numValues: Int): ValueSet[V] = { component match { - case cc: ChainComponent[_, V] => chainRange(cc) + case cc: ChainComponent[_, V] => chainRange(cc) case mc: MakeArrayComponent[V] => makeArrayRange(mc) - case ac: ApplyComponent[V] => applyRange(ac) - case _ => otherRange(component, numValues) + case ac: ApplyComponent[V] => applyRange(ac) + case _ => otherRange(component, numValues) } } @@ -281,7 +282,7 @@ object Range { component.element match { case c: Constant[_] => withoutStar(Set(c.constant)) - case f: AtomicFlip => withoutStar(Set(true, false)) + case f: AtomicFlip => withoutStar(Set(true, false)) case f: ParameterizedFlip => if (getRange(collection, f.parameter).hasStar) withStar(Set(true, false)) else withoutStar(Set(true, false)) @@ -324,18 +325,9 @@ object Range { val resultValues = homogeneousCartesianProduct(elementValues: _*).toSet.asInstanceOf[Set[i.Value]] if (incomplete) withStar(resultValues); else withoutStar(resultValues) - case a: Atomic[_] => { - if (!ParticleGenerator.exists(a.universe)) { - println("Warning: Sampling element " + a + " even though no sampler defined for this universe") - } - val thisSampler = ParticleGenerator(a.universe) - val samples = thisSampler(a, numValues) - withoutStar(samples.unzip._2.toSet) - } - case r: SingleValuedReferenceElement[_] => getRangeOfSingleValuedReference(collection, r.collection, r.reference) - case r: MultiValuedReferenceElement[_] => getRangeOfMultiValuedReference(collection, r.collection, r.reference) + case r: MultiValuedReferenceElement[_] => getRangeOfMultiValuedReference(collection, r.collection, r.reference) case a: Aggregate[_, _] => val inputs = getRange(collection, a.mvre) @@ -353,6 +345,17 @@ object Range { if (counterValues.hasStar) ValueSet.withStar(all); else ValueSet.withoutStar(all) } else { ValueSet.withStar(Set()) } + // Make values is hardcoded with depth. SFI should control iterative deepening, so call make values with infinite depth + case v: ValuesMaker[_] => { + v.makeValues(Int.MaxValue) + } + + case a: Atomic[_] => { + val thisSampler = ParticleGenerator(a.universe) + val samples = thisSampler(a, numValues) + withoutStar(samples.unzip._2.toSet) + } + case _ => /* A new improvement - if we can't compute the values, we just make them *, so the rest of the computation can proceed */ withStar(Set()) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredMPEAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredMPEAlgorithm.scala new file mode 100644 index 00000000..78538806 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredMPEAlgorithm.scala @@ -0,0 +1,55 @@ +/* + * StructuredAlgorithm.scala + * Abstract class for algorithms that are structured + * + * Created By: Brian Ruttenberg (bruttenberg@cra.com) + * Creation Date: December 30, 2015 + * + * Copyright 2015 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.structured.algorithm + +import com.cra.figaro.algorithm.Algorithm +import com.cra.figaro.language._ +import scala.collection.mutable.Map +import com.cra.figaro.algorithm.factored.factors.Factor +import com.cra.figaro.algorithm.factored.factors.Semiring +import com.cra.figaro.algorithm.structured.Problem +import com.cra.figaro.algorithm.structured.ComponentCollection +import com.cra.figaro.algorithm.OneTimeMPE +import com.cra.figaro.algorithm.AlgorithmException + +abstract class StructuredMPEAlgorithm(val universe: Universe) extends Algorithm with OneTimeMPE { + + def run(): Unit + + val semiring: Semiring[Double] + + //val targetFactors: Map[Element[_], Factor[Double]] = Map() + + val cc: ComponentCollection = new ComponentCollection + + val problem = new Problem(cc, List()) + // We have to add all active elements to the problem since these elements, if they are every used, need to have components created at the top level problem + universe.permanentElements.foreach(problem.add(_)) + val evidenceElems = universe.conditionedElements ::: universe.constrainedElements + + def initialComponents() = (universe.permanentElements ++ evidenceElems).distinct.map(cc(_)) + + /** + * Returns the most likely value for the target element. + */ + def mostLikelyValue[T](target: Element[T]): T = { + val targetVar = cc(target).variable + val factor = problem.recordingFactors(targetVar).asInstanceOf[Factor[T]] + if (factor.size != 1) throw new AlgorithmException//("Final factor for most likely value has more than one entry") + factor.get(List()) + } + + +} + + diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredProbQueryAlgorithm.scala similarity index 89% rename from Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredAlgorithm.scala rename to Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredProbQueryAlgorithm.scala index 915b5041..ff41cee5 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredAlgorithm.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/StructuredProbQueryAlgorithm.scala @@ -21,7 +21,7 @@ import com.cra.figaro.algorithm.factored.factors.Semiring import com.cra.figaro.algorithm.structured.Problem import com.cra.figaro.algorithm.structured.ComponentCollection -abstract class StructuredAlgorithm(val universe: Universe, val queryTargets: Element[_]*) extends Algorithm with OneTimeProbQuery { +abstract class StructuredProbQueryAlgorithm(val universe: Universe, val queryTargets: Element[_]*) extends Algorithm with OneTimeProbQuery { def run(): Unit @@ -40,7 +40,7 @@ abstract class StructuredAlgorithm(val universe: Universe, val queryTargets: Ele protected def marginalizeToTarget(target: Element[_], jointFactor: Factor[Double]): Unit = { val targetVar = cc(target).variable - val unnormalizedTargetFactor = jointFactor.marginalizeTo(semiring, targetVar) + val unnormalizedTargetFactor = jointFactor.marginalizeTo(targetVar) val z = unnormalizedTargetFactor.foldLeft(0.0, _ + _) val targetFactor = unnormalizedTargetFactor.mapTo((d: Double) => d / z) targetFactors += target -> targetFactor diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatBP.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatBP.scala index 2cda4a58..4a13e067 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatBP.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatBP.scala @@ -22,14 +22,14 @@ import com.cra.figaro.algorithm.structured.algorithm._ import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.factors.factory._ -class FlatBP(universe: Universe, iterations: Int, targets: Element[_]*) extends StructuredAlgorithm(universe, targets:_*) { +class FlatBP(universe: Universe, iterations: Int, targets: Element[_]*) extends StructuredProbQueryAlgorithm(universe, targets:_*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(beliefPropagation(iterations)), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(marginalBeliefPropagation(iterations)), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatGibbs.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatGibbs.scala index 18d86907..1b6c6ca7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatGibbs.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatGibbs.scala @@ -26,14 +26,14 @@ import com.cra.figaro.algorithm.factored.gibbs._ import com.cra.figaro.algorithm.factored.gibbs.BlockSampler class FlatGibbs(universe: Universe, numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator, targets: Element[_]*) - extends StructuredAlgorithm(universe, targets: _*) { + extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(gibbs(numSamples, burnIn, interval, blockToSampler)), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(marginalGibbs(numSamples, burnIn, interval, blockToSampler)), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatVE.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatVE.scala index 2034093c..ab95aceb 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatVE.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/flat/FlatVE.scala @@ -23,14 +23,14 @@ import com.cra.figaro.algorithm.structured.algorithm._ import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.factors.factory._ -class FlatVE(universe: Universe, targets: Element[_]*) extends StructuredAlgorithm(universe, targets:_*) { +class FlatVE(universe: Universe, targets: Element[_]*) extends StructuredProbQueryAlgorithm(universe, targets:_*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(variableElimination), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(marginalVariableElimination), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/RaisingVE.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/RaisingVE.scala index e361db8d..b658933f 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/RaisingVE.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/RaisingVE.scala @@ -23,14 +23,14 @@ import com.cra.figaro.algorithm.structured.algorithm._ import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.factors.factory._ -class RaisingVE(universe: Universe, targets: Element[_]*) extends StructuredAlgorithm(universe, targets:_*) { +class RaisingVE(universe: Universe, targets: Element[_]*) extends StructuredProbQueryAlgorithm(universe, targets:_*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveRaisingStrategy(problem, new ConstantStrategy(variableElimination), RaisingStrategy.raiseIfGlobal, defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveRaisingStrategy(problem, new ConstantStrategy(marginalVariableElimination), RaisingStrategy.raiseIfGlobal, defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPChooser.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPChooser.scala index 36b5aeaa..71220ca9 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPChooser.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPChooser.scala @@ -24,14 +24,14 @@ import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.factors.factory._ class StructuredVEBPChooser(universe: Universe, scoreThreshold: Double, BPIterations: Int, targets: Element[_]*) - extends StructuredAlgorithm(universe, targets: _*) { + extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new VEBPStrategy(scoreThreshold, BPIterations), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPGibbsChooser.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPGibbsChooser.scala index f6ed5e19..d7067d53 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPGibbsChooser.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEBPGibbsChooser.scala @@ -26,14 +26,14 @@ import com.cra.figaro.algorithm.factored.gibbs.Gibbs import com.cra.figaro.algorithm.factored.gibbs.BlockSampler class StructuredVEBPGibbsChooser(universe: Universe, scoreThreshold: Double, determThreshold: Double, bpIters: Int, numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator, targets: Element[_]*) - extends StructuredAlgorithm(universe, targets: _*) { + extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new VEBPGibbsStrategy(scoreThreshold, determThreshold, bpIters, numSamples, burnIn, interval, blockToSampler), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEGibbsChooser.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEGibbsChooser.scala index 9a231675..061b83a2 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEGibbsChooser.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/hybrid/StructuredVEGibbsChooser.scala @@ -26,14 +26,14 @@ import com.cra.figaro.algorithm.factored.gibbs.Gibbs import com.cra.figaro.algorithm.factored.gibbs.BlockSampler class StructuredVEGibbsChooser(universe: Universe, scoreThreshold: Double, numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator, targets: Element[_]*) - extends StructuredAlgorithm(universe, targets: _*) { + extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new VEGibbsStrategy(scoreThreshold, numSamples, burnIn, interval, blockToSampler), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredBP.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredBP.scala index 1e2884bc..5b0af6b7 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredBP.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredBP.scala @@ -19,16 +19,16 @@ import com.cra.figaro.algorithm.structured._ import com.cra.figaro.algorithm.structured.solver._ import com.cra.figaro.algorithm.structured.strategy.solve.ConstantStrategy import com.cra.figaro.language._ -import com.cra.figaro.algorithm.structured.algorithm.StructuredAlgorithm +import com.cra.figaro.algorithm.structured.algorithm.StructuredProbQueryAlgorithm import com.cra.figaro.algorithm.structured.strategy.decompose._ -class StructuredBP(universe: Universe, iterations: Int, targets: Element[_]*) extends StructuredAlgorithm(universe, targets:_*) { +class StructuredBP(universe: Universe, iterations: Int, targets: Element[_]*) extends StructuredProbQueryAlgorithm(universe, targets:_*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(beliefPropagation(iterations)), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(marginalBeliefPropagation(iterations)), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredGibbs.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredGibbs.scala index 549ba564..ac9ac81a 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredGibbs.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredGibbs.scala @@ -20,19 +20,19 @@ import com.cra.figaro.algorithm.structured._ import com.cra.figaro.algorithm.structured.solver._ import com.cra.figaro.algorithm.structured.strategy.solve.ConstantStrategy import com.cra.figaro.language._ -import com.cra.figaro.algorithm.structured.algorithm.StructuredAlgorithm +import com.cra.figaro.algorithm.structured.algorithm.StructuredProbQueryAlgorithm import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.gibbs.Gibbs import com.cra.figaro.algorithm.factored.gibbs.BlockSampler class StructuredGibbs(universe: Universe, numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator, targets: Element[_]*) - extends StructuredAlgorithm(universe, targets: _*) { + extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(gibbs(numSamples, burnIn, interval, blockToSampler)), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(marginalGibbs(numSamples, burnIn, interval, blockToSampler)), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEBP.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEBP.scala new file mode 100644 index 00000000..fa49fd44 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEBP.scala @@ -0,0 +1,53 @@ +/* + * StructuredVE.scala + * A structured variable elimination algorithm. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: March 1, 2015 + * + * Copyright 2015 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.structured.algorithm.structured + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.structured._ +import com.cra.figaro.algorithm.structured.strategy._ +import com.cra.figaro.algorithm.structured.solver._ +import com.cra.figaro.algorithm.structured.strategy.solve.ConstantStrategy +import com.cra.figaro.algorithm.structured.algorithm._ +import com.cra.figaro.algorithm.structured.strategy.decompose._ +import com.cra.figaro.algorithm.factored.factors.factory._ +import com.cra.figaro.algorithm.factored.factors.MaxProductSemiring + + +class StructuredMPEBP(universe: Universe, iterations: Int) extends StructuredMPEAlgorithm(universe) { + + val semiring = MaxProductSemiring() + + def run() { + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(mpeBeliefPropagation(iterations)), defaultRangeSizer, Lower, false) + strategy.execute(initialComponents) + } +} + +object StructuredMPEBP { + /** Create a structured variable elimination algorithm with the given query targets. */ + def apply(iterations: Int)(implicit universe: Universe) = { + new StructuredMPEBP(universe, iterations) + } + + /** + * Use VE to compute the probability that the given element satisfies the given predicate. + */ + def mostLikelyValue[T](target: Element[T], iterations: Int): T = { + val alg = new StructuredMPEBP(target.universe, iterations) + alg.start() + val result = alg.mostLikelyValue(target) + alg.kill() + result + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEVE.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEVE.scala new file mode 100644 index 00000000..9cf1e27f --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredMPEVE.scala @@ -0,0 +1,53 @@ +/* + * StructuredVE.scala + * A structured variable elimination algorithm. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: March 1, 2015 + * + * Copyright 2015 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.structured.algorithm.structured + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.structured._ +import com.cra.figaro.algorithm.structured.strategy._ +import com.cra.figaro.algorithm.structured.solver._ +import com.cra.figaro.algorithm.structured.strategy.solve.ConstantStrategy +import com.cra.figaro.algorithm.structured.algorithm._ +import com.cra.figaro.algorithm.structured.strategy.decompose._ +import com.cra.figaro.algorithm.factored.factors.factory._ +import com.cra.figaro.algorithm.factored.factors.MaxProductSemiring + + +class StructuredMPEVE(universe: Universe) extends StructuredMPEAlgorithm(universe) { + + val semiring = MaxProductSemiring() + + def run() { + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(mpeVariableElimination), defaultRangeSizer, Lower, false) + strategy.execute(initialComponents) + } +} + +object StructuredMPEVE { + /** Create a structured variable elimination algorithm with the given query targets. */ + def apply()(implicit universe: Universe) = { + new StructuredMPEVE(universe) + } + + /** + * Use VE to compute the probability that the given element satisfies the given predicate. + */ + def mostLikelyValue[T](target: Element[T]): T = { + val alg = new StructuredMPEVE(target.universe) + alg.start() + val result = alg.mostLikelyValue(target) + alg.kill() + result + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredVE.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredVE.scala index 1a75b352..4fe00d8b 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredVE.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/algorithm/structured/StructuredVE.scala @@ -23,14 +23,15 @@ import com.cra.figaro.algorithm.structured.algorithm._ import com.cra.figaro.algorithm.structured.strategy.decompose._ import com.cra.figaro.algorithm.factored.factors.factory._ -class StructuredVE(universe: Universe, targets: Element[_]*) extends StructuredAlgorithm(universe, targets: _*) { + +class StructuredVE(universe: Universe, targets: Element[_]*) extends StructuredProbQueryAlgorithm(universe, targets: _*) { val semiring = SumProductSemiring() def run() { - val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(variableElimination), defaultRangeSizer, Lower, false) + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new ConstantStrategy(marginalVariableElimination), defaultRangeSizer, Lower, false) strategy.execute(initialComponents) - val joint = problem.solution.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) + val joint = problem.solution.foldLeft(Factory.unit(semiring))(_.product(_)) targets.foreach(t => marginalizeToTarget(t, joint)) } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/BPSolver.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/BPSolver.scala index b89aa332..1c84bcac 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/BPSolver.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/BPSolver.scala @@ -24,19 +24,23 @@ import com.cra.figaro.algorithm.factored.factors.factory.Factory._ import com.cra.figaro.algorithm.factored.beliefpropagation._ import com.cra.figaro.algorithm.structured._ -private[figaro] class BPSolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]], val iterations: Int) -extends com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeProbabilisticBeliefPropagation { +class BPSolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]], val iters: Int, + val semiring: LogConvertibleSemiRing[Double]) + extends com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeProbabilisticBeliefPropagation { // We need to create a joint probability distribution over the interface to this nested subproblem. // To achieve this, we create a new variable representing the tuple of the attributes to preserve. // We create a factor to represent the tuple creation. // We then run BP as usual. // At the end, we sum the tuple variable out of this factor to obtain the solution. - val (tupleVar, tupleFactor): (Variable[_], Factor[Double]) = makeTupleVarAndFactor(problem.collection, None, toPreserve.toList:_*) + def iterations = iters + + val (tupleVar, tupleFactor): (Variable[_], Factor[Double]) = makeTupleVarAndFactor(problem.collection, None, toPreserve.toList: _*) def generateGraph() = { val allFactors = tupleFactor :: factors - factorGraph = new BasicFactorGraph(allFactors.map(makeLogarithmic(_)), semiring): FactorGraph[Double] + //factorGraph = new BasicFactorGraph(allFactors.map(makeLogarithmic(_)), logSpaceSemiring()): FactorGraph[Double] + factorGraph = new BasicFactorGraph(convertFactors(allFactors), logSpaceSemiring()): FactorGraph[Double] } override def initialize() = { @@ -44,28 +48,41 @@ extends com.cra.figaro.algorithm.factored.beliefpropagation.OneTimeProbabilistic super.initialize } - def go(): List[Factor[Double]] = { + def go(): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { initialize() run() val targetVars = toPreserve.toList ::: List(tupleVar) val tupleBelief = belief(FactorNode(toPreserve + tupleVar)) val targetBelief = tupleBelief.sumOver(tupleVar) - List(unmakeLogarithmic(normalize(targetBelief))) - } + val targetFactors = if (semiring.isLog()) { + List(normalize(targetBelief)) + } else { + List(unmakeLogarithmic(normalize(targetBelief))) + } - def computeDistribution[T](target: Element[T]): Stream[(Double, T)] = getBeliefsForElement(target).toStream + if (toPreserve.isEmpty && semiring == MaxProductSemiring()) { + val max = toEliminate.map { v => v -> makeRecordingFactor(v)} + (targetFactors, max.toMap) + } else { + (targetFactors, Map()) + } - def computeExpectation[T](target: Element[T], function: T => Double): Double = { - computeDistribution(target).map((pair: (Double, T)) => pair._1 * function(pair._2)).sum } + def makeRecordingFactor[U](v: Variable[U]): Factor[U] = { + val bf = new BasicFactor[U](List(), List()) + val maxInd = beliefMap(VariableNode(v)).contents.maxBy(_._2)._1 + val maxValue = v.range(maxInd(0)) + bf.set(List(), maxValue.value.asInstanceOf[v.Value]) + bf + } + + /* Not needed for SFI */ val dependentUniverses = null val dependentAlgorithm = null val universe = null - val semiring = LogSumProductSemiring() - val targetElements = null } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/GibbsSolver.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/GibbsSolver.scala index 26d9069b..a86f9e3b 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/GibbsSolver.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/GibbsSolver.scala @@ -24,32 +24,41 @@ import com.cra.figaro.algorithm.factored.gibbs.ProbabilisticGibbs import com.cra.figaro.algorithm.factored.gibbs.Gibbs import com.cra.figaro.algorithm.factored.gibbs.WalkSAT -private[figaro] class GibbsSolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], _factors: List[Factor[Double]], - val numSamples: Int, val burnIn: Int, val interval: Int, val blockToSampler: Gibbs.BlockSamplerCreator) -extends BaseUnweightedSampler(null) with ProbabilisticGibbs with OneTime { - override def initialize() = { - super.initialize +class GibbsSolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], _factors: List[Factor[Double]], + _numSamples: Int, _burnIn: Int, _interval: Int, val blockToSampler: Gibbs.BlockSamplerCreator) + extends BaseUnweightedSampler(null) with ProbabilisticGibbs with OneTime { + + def numSamples() = _numSamples + def burnIn() = _burnIn + def interval() = _interval + + def initializeBlocks() = { factors = _factors.map(_.mapTo(math.log, semiring)) variables = factors.flatMap(_.variables).toSet val blocks = createBlocks() // Create block samplers blockSamplers = blocks.map(block => blockToSampler((block, factors.filter(_.variables.exists(block.contains(_)))))) + } + + override def initialize() = { + super.initialize + if (blockSamplers == null) initializeBlocks() // Initialize the samples to a valid state and take the burn-in samples val initialSample = WalkSAT(factors, variables, semiring, chainMapper) variables.foreach(v => currentSamples(v) = initialSample(v)) - for(_ <- 1 to burnIn) sampleAllBlocks() + for (_ <- 1 to burnIn) sampleAllBlocks() } - def chainMapper(chain: Chain[_,_]): Set[Variable[_]] = problem.collection(chain).actualSubproblemVariables.values.toSet - + def chainMapper(chain: Chain[_, _]): Set[Variable[_]] = problem.collection(chain).actualSubproblemVariables.values.toSet + def run = {} def go(): List[Factor[Double]] = { initialize() val targetVars = toPreserve.toList val result = new SparseFactor[Double](targetVars, List()) - for(_ <- 0 until numSamples) { - for(_ <- 0 until interval) { + for (_ <- 0 until numSamples) { + for (_ <- 0 until interval) { sampleAllBlocks() } val factorIndex = targetVars.map(currentSamples(_)) @@ -74,10 +83,9 @@ extends BaseUnweightedSampler(null) with ProbabilisticGibbs with OneTime { // Start with the purely stochastic variables with no parents val starterVariables = variables.filter(variableParents(_).isEmpty) - @tailrec - // Recursively add deterministic children to the block + @tailrec // Recursively add deterministic children to the block def expandBlock(expand: Set[Variable[_]], block: Set[Variable[_]] = Set()): Gibbs.Block = { - if(expand.isEmpty) block.toList + if (expand.isEmpty) block.toList else { val expandNext = expand.flatMap(variableChildren(_)) expandBlock(expandNext, block ++ expand) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VEBPChooser.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VEBPChooser.scala deleted file mode 100644 index 70b4d3a7..00000000 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VEBPChooser.scala +++ /dev/null @@ -1,66 +0,0 @@ -/* - * VEBPChooser.scala - * A hybrid solver that chooses between variable elimination and belief propagation. - * - * Created By: Avi Pfeffer (apfeffer@cra.com) - * Creation Date: March 1, 2015 - * - * Copyright 2015 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.structured.solver - -import com.cra.figaro.algorithm.factored.VariableElimination -import com.cra.figaro.algorithm.factored.factors._ -import com.cra.figaro.algorithm.structured.Problem -import com.cra.figaro.util.HashMultiSet -import com.cra.figaro.util.MultiSet - -/* - * The score threshold is the threshold to determine whether VE or BP will be used. - * This score represents the maximum increase in cost of the factors through using VE, compared to the initial factors. - */ -private[figaro] class VEBPChooser(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]], - val scoreThreshold: Double, val iterations: Int) -extends VariableElimination[Double] { - def go(): List[Factor[Double]] = { - val (score, order) = VariableElimination.eliminationOrder(factors, toPreserve) -// print("Score = " + score + " - ") - if (score > scoreThreshold) { -// println("Choosing BP") - val bp = new BPSolver(problem, toEliminate, toPreserve, factors, iterations) - result = bp.go() - } else { -// println("Choosing VE") - // Since we've already computed the order, we don't call doElimination but only do the steps after computing the order - val factorsAfterElimination = eliminateInOrder(order, HashMultiSet(factors: _*), initialFactorMap(factors)) - finish(factorsAfterElimination, order) - } - result - } - - val semiring: Semiring[Double] = SumProductSemiring() - - private var result: List[Factor[Double]] = _ - - def finish(factorsAfterElimination: MultiSet[Factor[Double]], eliminationOrder: List[Variable[_]]): Unit = { - result = factorsAfterElimination.toList - } - - val dependentAlgorithm: (com.cra.figaro.language.Universe, List[com.cra.figaro.language.NamedEvidence[_]]) => () => Double = null - - val dependentUniverses: List[(com.cra.figaro.language.Universe, List[com.cra.figaro.language.NamedEvidence[_]])] = null - - def getFactors(neededElements: List[com.cra.figaro.language.Element[_]], - targetElements: List[com.cra.figaro.language.Element[_]], - upperBounds: Boolean): List[com.cra.figaro.algorithm.factored.factors.Factor[Double]] = null - - val showTiming: Boolean = false - - val targetElements: List[com.cra.figaro.language.Element[_]] = null - - val universe: com.cra.figaro.language.Universe = null - -} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VESolver.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VESolver.scala index 731e7b62..6e5125da 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VESolver.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/VESolver.scala @@ -15,35 +15,82 @@ package com.cra.figaro.algorithm.structured.solver import com.cra.figaro.algorithm.factored.factors._ import com.cra.figaro.algorithm.structured.Problem import com.cra.figaro.util.MultiSet +import com.cra.figaro.algorithm.factored.factors.Semiring +import com.cra.figaro.util +import com.cra.figaro.algorithm.lazyfactored._ -private[figaro] class VESolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]) -extends com.cra.figaro.algorithm.factored.VariableElimination[Double] { - def go(): List[Factor[Double]] = { - doElimination(factors, toPreserve.toList) - result - } +class VESolver(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]], val semiring: Semiring[Double]) + extends com.cra.figaro.algorithm.factored.VariableElimination[Double] { debug = false + + override val comparator = semiring match { + case sum: SumProductSemiring => None + case max: MaxProductSemiring => Some((x: Double, y: Double) => x < y) + } - val semiring: Semiring[Double] = SumProductSemiring() + def go(): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + // Convert factors to MaxProduct for MPE + val convertedFactors = semiring match { + case sum: SumProductSemiring => factors + case max: MaxProductSemiring => factors.map(_.mapTo(x => x, semiring)) + } + doElimination(convertedFactors, toPreserve.toList) + (resultFactors, recordingFactorsMap) + } - private var result: List[Factor[Double]] = _ + private var resultFactors: List[Factor[Double]] = _ + // A map from each variable to a factor that maps values of the toPreserve variables to maximal values of the variable + // Note that when the toPreserve is empty, this represents the maximal value of each variable + private var recordingFactorsMap: Map[Variable[_], Factor[_]] = Map() + private def getRecordingFactor[T](variable: Variable[T]): Factor[T] = recordingFactorsMap(variable).asInstanceOf[Factor[variable.Value]] def finish(factorsAfterElimination: MultiSet[Factor[Double]], eliminationOrder: List[Variable[_]]): Unit = { - result = factorsAfterElimination.toList + semiring match { + case sum: SumProductSemiring => finishSum(factorsAfterElimination, eliminationOrder) + case max: MaxProductSemiring => finishMax(factorsAfterElimination, eliminationOrder) + } + } + + /* Finish function for marginal VE */ + def finishSum(factorsAfterElimination: MultiSet[Factor[Double]], eliminationOrder: List[Variable[_]]): Unit = { + resultFactors = factorsAfterElimination.toList + } + + /* Finish function for MPE VE */ + def finishMax(factorsAfterElimination: MultiSet[Factor[Double]], eliminationOrder: List[Variable[_]]): Unit = { + resultFactors = factorsAfterElimination.toList + /* If empty, we need to know the max values for all variables in this set of factors + * Otherwise, we assume that the eliminated varaibles are internal and therefore are not queryable + * When we have non-chain decompositions, this may not hold anymore + */ + if (factorsAfterElimination.forall(f => f.size == 1 && f.numVars == 0)) { + for { (variable, factor) <- eliminationOrder.reverse.zip(recordingFactors) } { backtrackOne(factor, variable) } + } + } + + private def backtrackOne[T](factor: Factor[_], variable: Variable[T]): Unit = { + val indices = + for { variable <- factor.variables } yield util.indices(variable.range, Regular(getRecordingFactor(variable).contents.head._2)).head + recordingFactorsMap += variable -> { + val bf = factor.asInstanceOf[Factor[variable.Value]].createFactor(List(), List()) + bf.set(List(), factor.asInstanceOf[Factor[variable.Value]].get(indices)) + bf + } } + /* Functions not needed for SFI */ val dependentAlgorithm: (com.cra.figaro.language.Universe, List[com.cra.figaro.language.NamedEvidence[_]]) => () => Double = null val dependentUniverses: List[(com.cra.figaro.language.Universe, List[com.cra.figaro.language.NamedEvidence[_]])] = null def getFactors(neededElements: List[com.cra.figaro.language.Element[_]], - targetElements: List[com.cra.figaro.language.Element[_]], - upperBounds: Boolean): List[com.cra.figaro.algorithm.factored.factors.Factor[Double]] = null + targetElements: List[com.cra.figaro.language.Element[_]], + upperBounds: Boolean): List[com.cra.figaro.algorithm.factored.factors.Factor[Double]] = null - val showTiming: Boolean = false + val showTiming: Boolean = false - val targetElements: List[com.cra.figaro.language.Element[_]] = null + val targetElements: List[com.cra.figaro.language.Element[_]] = null - val universe: com.cra.figaro.language.Universe = null + val universe: com.cra.figaro.language.Universe = null } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/package.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/package.scala index 76b7a51e..2f0f5ba1 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/package.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/solver/package.scala @@ -15,18 +15,19 @@ package com.cra.figaro.algorithm.structured import com.cra.figaro.algorithm.factored.factors.Factor import com.cra.figaro.algorithm.factored.factors.Variable +import com.cra.figaro.algorithm.factored.gibbs.Gibbs import com.cra.figaro.algorithm.structured.solver.BPSolver import com.cra.figaro.algorithm.structured.solver.GibbsSolver -import com.cra.figaro.algorithm.structured.solver.VEBPChooser import com.cra.figaro.algorithm.structured.solver.VESolver -import com.cra.figaro.algorithm.factored.gibbs.Gibbs +import com.cra.figaro.algorithm.factored.factors.SumProductSemiring +import com.cra.figaro.algorithm.factored.factors.MaxProductSemiring package object solver { /** * A Solver takes a set of variables to eliminate, a set of variables to preserve, and a list of factors. * It returns a list of factors that mention only the preserved variables. */ - type Solver = (Problem, Set[Variable[_]], Set[Variable[_]], List[Factor[Double]]) => List[Factor[Double]] + type Solver = (Problem, Set[Variable[_]], Set[Variable[_]], List[Factor[Double]]) => (List[Factor[Double]], Map[Variable[_], Factor[_]]) /** * Creates a Gibbs sampling solver. @@ -39,10 +40,9 @@ package object solver { * @param toPreserve the variables to be preserved (not eliminated) * @param factors all the factors in the problem */ - def gibbs(numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator) - (problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { + def marginalGibbs(numSamples: Int, burnIn: Int, interval: Int, blockToSampler: Gibbs.BlockSamplerCreator)(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { val gibbs = new GibbsSolver(problem, toEliminate, toPreserve, factors, numSamples, burnIn, interval, blockToSampler) - gibbs.go() + (gibbs.go(), Map()) } /** @@ -52,8 +52,20 @@ package object solver { * @param toPreserve the variables to be preserved (not eliminated) * @param factors all the factors in the problem */ - def variableElimination(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { - val ve = new VESolver(problem, toEliminate, toPreserve, factors) + def marginalVariableElimination(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + val ve = new VESolver(problem, toEliminate, toPreserve, factors, SumProductSemiring()) + ve.go() + } + + /** + * Creates an MPE variable elimination solver. + * @param problem the problem to solve + * @param toEliminate the variables to be eliminated + * @param toPreserve the variables to be preserved (not eliminated) + * @param factors all the factors in the problem + */ + def mpeVariableElimination(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + val ve = new VESolver(problem, toEliminate, toPreserve, factors, MaxProductSemiring()) ve.go() } @@ -65,24 +77,24 @@ package object solver { * @param toPreserve the variables to be preserved (not eliminated) * @param factors all the factors in the problem */ - def beliefPropagation(iterations: Int = 100)(problem: Problem, toEliminate: Set[Variable[_]], - toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { - val bp = new BPSolver(problem, toEliminate, toPreserve, factors, iterations) + def marginalBeliefPropagation(iterations: Int = 100)(problem: Problem, toEliminate: Set[Variable[_]], + toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + val bp = new BPSolver(problem, toEliminate, toPreserve, factors, iterations, SumProductSemiring()) bp.go() } - - /** - * Creates a hybrid solver that chooses between variable elimination and belief propagation. - * @param threshold the minimum score increase on eliminating a variable that will cause the solver to choose belief propagation - * @param iterations number of iterations if belief propagation is chosen + + /** + * Creates an MPE belief propagation solver. + * @param iterations number of iterations of BP to run * @param problem the problem to solve * @param toEliminate the variables to be eliminated * @param toPreserve the variables to be preserved (not eliminated) * @param factors all the factors in the problem */ - def chooseVEOrBP(threshold: Double, iterations: Int = 100)(problem: Problem, toEliminate: Set[Variable[_]], - toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { - val solver = new VEBPChooser(problem, toEliminate, toPreserve, factors, threshold, iterations) - solver.go() + def mpeBeliefPropagation(iterations: Int = 100)(problem: Problem, toEliminate: Set[Variable[_]], + toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + val bp = new BPSolver(problem, toEliminate, toPreserve, factors, iterations, MaxProductSemiring()) + bp.go() } + } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/RecursiveStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/RecursiveStrategy.scala index b335046c..2ecc7255 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/RecursiveStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/RecursiveStrategy.scala @@ -22,6 +22,7 @@ private[figaro] class RecursiveStructuredStrategy(problem: Problem, solvingStrat rangeSizer: RangeSizer, bounds: Bounds, parameterized: Boolean) extends StructuredStrategy(problem, solvingStrategy, (p: Problem) => new RecursiveStructuredStrategy(p, solvingStrategy, rangeSizer, bounds, parameterized), rangeSizer, bounds, parameterized) { + problem.collection.useSingleChainFactor = true } /** diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/package.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/package.scala index 22cfc5cf..58a0746e 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/package.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/decompose/package.scala @@ -30,5 +30,5 @@ package object decompose { */ type RangeSizer = ProblemComponent[_] => Int - def defaultRangeSizer(pc: ProblemComponent[_]) = ParticleGenerator.defaultTotalSamples + def defaultRangeSizer(pc: ProblemComponent[_]) = ParticleGenerator.defaultMaxNumSamplesAtChain } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/ConstantStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/ConstantStrategy.scala index 8c027302..157ff344 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/ConstantStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/ConstantStrategy.scala @@ -22,7 +22,7 @@ import com.cra.figaro.algorithm.factored.factors.Variable */ class ConstantStrategy(solverToUse: solver.Solver) extends SolvingStrategy { - def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { solverToUse(problem, toEliminate, toPreserve, factors) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/SolvingStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/SolvingStrategy.scala index 84aef8ab..437842b4 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/SolvingStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/SolvingStrategy.scala @@ -25,6 +25,6 @@ abstract class SolvingStrategy { * Solve the given problem with the indicated preserve and eliminate variables, and return a list of factors representing * the joint distribution over the preserved variables. */ - def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPGibbsStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPGibbsStrategy.scala index b9110bb9..8c082b20 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPGibbsStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPGibbsStrategy.scala @@ -25,10 +25,10 @@ import com.cra.figaro.algorithm.structured.solver */ class VEBPGibbsStrategy(val scoreThreshold: Double, val determThreshold: Double, val bpIters: Int, val numSamples: Int, val burnIn: Int, val interval: Int, val blockToSampler: Gibbs.BlockSamplerCreator) extends SolvingStrategy { - def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { val (score, order) = VariableElimination.eliminationOrder(factors, toPreserve) if (score <= scoreThreshold) { - solver.variableElimination(problem, toEliminate, toPreserve, factors) + solver.marginalVariableElimination(problem, toEliminate, toPreserve, factors) } else { /* * Choose between BP and Gibbs. @@ -42,9 +42,9 @@ class VEBPGibbsStrategy(val scoreThreshold: Double, val determThreshold: Double, val totalVars = toEliminate.size + toPreserve.size if ((numDeterministicVars.toDouble / totalVars) > determThreshold) { - solver.beliefPropagation(bpIters)(problem, toEliminate, toPreserve, factors) + solver.marginalBeliefPropagation(bpIters)(problem, toEliminate, toPreserve, factors) } else { - solver.gibbs(numSamples, burnIn, interval, blockToSampler)(problem, toEliminate, toPreserve, factors) + solver.marginalGibbs(numSamples, burnIn, interval, blockToSampler)(problem, toEliminate, toPreserve, factors) } } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPStrategy.scala index cac5dbbe..bf4f0508 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEBPStrategy.scala @@ -23,12 +23,12 @@ import com.cra.figaro.algorithm.structured.solver */ class VEBPStrategy(val scoreThreshold: Double, val iterations: Int) extends SolvingStrategy { - def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { val (score, order) = VariableElimination.eliminationOrder(factors, toPreserve) if (score > scoreThreshold) { - solver.beliefPropagation(iterations)(problem, toEliminate, toPreserve, factors) + solver.marginalBeliefPropagation(iterations)(problem, toEliminate, toPreserve, factors) } else { - solver.variableElimination(problem, toEliminate, toPreserve, factors) + solver.marginalVariableElimination(problem, toEliminate, toPreserve, factors) } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEGibbsStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEGibbsStrategy.scala index c63ae9be..3d8fb9aa 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEGibbsStrategy.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/structured/strategy/solve/VEGibbsStrategy.scala @@ -24,12 +24,12 @@ import com.cra.figaro.algorithm.structured.solver */ class VEGibbsStrategy(val scoreThreshold: Double, val numSamples: Int, val burnIn: Int, val interval: Int, val blockToSampler: Gibbs.BlockSamplerCreator) extends SolvingStrategy { - def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): List[Factor[Double]] = { + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { val (score, order) = VariableElimination.eliminationOrder(factors, toPreserve) if (score > scoreThreshold) { - solver.gibbs(numSamples, burnIn, interval, blockToSampler)(problem, toEliminate, toPreserve, factors) + solver.marginalGibbs(numSamples, burnIn, interval, blockToSampler)(problem, toEliminate, toPreserve, factors) } else { - solver.variableElimination(problem, toEliminate, toPreserve, factors) + solver.marginalVariableElimination(problem, toEliminate, toPreserve, factors) } } diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedGibbs.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedGibbs.scala new file mode 100644 index 00000000..4e4db728 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedGibbs.scala @@ -0,0 +1,417 @@ +/* + * CollapsedGibbs.scala + * An implementation of collapsed Gibbs sampling (on factor graphs) using the algorithm in + * "Dynamic Blocking and Collapsing for Gibbs Sampling" Gogate and Venugopal 2013 + * + * Created By: Cory Scott (cscott@cra.com) + * Creation Date: June 27, 2016 + * + * 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.experimental.collapsedgibbs + +import com.cra.figaro.algorithm.lazyfactored.{ LazyValues, ValueSet } +import scala.annotation.tailrec +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import scala.collection.mutable.{ Map => MutableMap, Set => MutableSet} +import com.cra.figaro.algorithm.factored.factors.{ Variable, ElementVariable, InternalChainVariable, Factor} +import com.cra.figaro.algorithm.structured.Problem +import com.cra.figaro.algorithm.factored.factors.factory.Factory +import com.cra.figaro.algorithm.factored.gibbs._ +import com.cra.figaro.algorithm.factored.VEGraph +import com.cra.figaro.algorithm.{ AlgorithmException, UnsupportedAlgorithmException } +import com.cra.figaro.library.compound.^^ + + +object CollapsedGibbs { + + // A block is just a list of variables + type Block = List[Variable[_]] + + // Information passed to BlockSampler constructor + type BlockInfo = (Block, List[Factor[Double]]) + + // Constructor for BlockSampler + type BlockSamplerCreator = BlockInfo => BlockSampler + + /** + * Create a one-time collapsed Gibbs sampler using the given number of samples and target elements. + * This sampler will use the default collapsing strategy (Heuristic) and default params for that strategy. + */ + def apply(mySamples: Int, targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, 0, 1, BlockSampler.default, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the given strategy for collapsing, number of samples, + * and target elements. + * Uses the default params for the strategy. + */ + def apply(strategy:String, mySamples: Int, targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, 0, 1, BlockSampler.default, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the specified strategy and parameters, and the + * given number of samples and target elements. + */ + def apply(strategy:String, collapseParameters:Seq[Int], mySamples: Int, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, collapseParameters, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, 0, 1, BlockSampler.default, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the given number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(mySamples: Int, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, burnIn, interval, blockToSampler, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the given strategy, number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + * Uses the default params for the strategy. + */ + def apply(strategy: String, mySamples: Int, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, burnIn, interval, blockToSampler, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the specified strategy and parameters, + * number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(strategy: String, collapseParameters:Seq[Int], mySamples: Int, burnIn: Int, + interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + mySamples, burnIn, interval, blockToSampler, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the + * default strategy with default parameters, given dependent universes and algorithm, + * the number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + mySamples: Int, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", + dependentUniverses, + dependentAlgorithm, + mySamples, burnIn, interval, blockToSampler, targets: _*) + + /** + * Create a one-time collapsed Gibbs sampler using the given strategy, + * dependent universes and algorithm, + * the number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(strategy:String, dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + mySamples: Int, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, targets: Element[_]*)(implicit universe: Universe) = + strategy match { + case "SIMPLE" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs { + val numSamples = mySamples } + case "FACTOR" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with FactorSizeCollapseStrategy { val numSamples = mySamples } + case "DETERM" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with DeterministicCollapseStrategy { val numSamples = mySamples } + case "RECURR" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with RecurringCollapseStrategy { val numSamples = mySamples } + case _ => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with HeuristicCollapseStrategy { val numSamples = mySamples } + } + /** + * Create a one-time collapsed Gibbs sampler using the given strategy and parameters, + * dependent universes and algorithm, + * the number of samples, the number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(strategy:String, collapseParameters:Seq[Int], dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + mySamples: Int, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, targets: Element[_]*)(implicit universe: Universe) = + strategy match { + /* + * In all the constructors below: + * alpha is the maximum degree of any variable to eliminate. + * gamma is the maximum number of edges we are allowed to add while eliminating variavles + */ + case "SIMPLE" => { + val Seq(alpha, gamma) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs { + val numSamples = mySamples } + } + case "FACTOR" => { + //factorThresh is the maximum total cost of all variables eliminated. + //when we exceed this value, collapsing stops, even if there are still candidates. + val Seq(alpha, gamma, factorThresh) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with FactorSizeCollapseStrategy { + val numSamples = mySamples + override val factorThreshold = factorThresh + } + } + case "DETERM" => { + val Seq(alpha, gamma) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with DeterministicCollapseStrategy { + val numSamples = mySamples } + } + case "RECURR" => { + //sampleResetFrequency is the frequency with which we reset and re-collapse the model. + //sampleSaveFrequency is the frequency with which we store a sample to use in our marginal estimates. + val Seq(alpha, gamma, sampleResetFrequency, sampleSaveFrequency) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with RecurringCollapseStrategy { + val numSamples = mySamples + override val sampleReset = sampleResetFrequency + override val sampleRecurrence = sampleSaveFrequency + } + } + case _ => { + val Seq(alpha, gamma) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with OneTimeProbQuerySampler with ChainApplyBlockingGibbs + with HeuristicCollapseStrategy {val numSamples = mySamples } + } + } + + /** + * Create an anytime collapsed Gibbs sampler using the given target elements. + */ + def apply(targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + 0, 1, BlockSampler.default, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the given strategy and target elements. + */ + def apply(strategy:String, targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + 0, 1, BlockSampler.default, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the given strategy, parameters, and target elements. + */ + def apply(strategy: String, collapseParameters:Seq[Int], targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, collapseParameters, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + 0, 1, BlockSampler.default, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the default strategy with default parameters, + * given number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + burnIn, interval, blockToSampler, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the given strategy, number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(strategy:String, burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + burnIn, interval, blockToSampler, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the given number of samples to burn in, + * the sampling interval, the BlockSampler generator, and target elements. + */ + def apply(strategy:String, collapseParameters:Seq[Int], burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply(strategy, collapseParameters, List[(Universe, List[NamedEvidence[_]])](), + (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u), + burnIn, interval, blockToSampler, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the default strategy with default paramters, + * given dependent universes and algorithm, + * the number of samples to burn in, the sampling interval, + * the BlockSampler generator, and target elements. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, + targets: Element[_]*)(implicit universe: Universe):CollapsedProbQueryGibbs = + this.apply("", + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, targets: _*) + + /** + * Create an anytime collapsed Gibbs sampler using the given strategy (with default parameters), + * given dependent universes and algorithm, + * the number of samples to burn in, the sampling interval, + * the BlockSampler generator, and target elements. + */ + def apply(strategy: String, dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, targets: Element[_]*)(implicit universe: Universe) = + strategy match { + case "SIMPLE" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + case "FACTOR" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with FactorSizeCollapseStrategy + case "DETERM" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with DeterministicCollapseStrategy + case "RECURR" => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with RecurringCollapseStrategy + case _ => new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with HeuristicCollapseStrategy + } + + /** + * Create an anytime collapsed Gibbs sampler using the given strategy, parameters, + * dependent universes and algorithm, + * the number of samples to burn in, the sampling interval, + * the BlockSampler generator, and target elements. + */ + def apply(strategy: String, collapseParameters:Seq[Int], dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + burnIn: Int, interval: Int, blockToSampler: BlockSamplerCreator, targets: Element[_]*)(implicit universe: Universe) = + strategy match { + /* + * In all the constructors below: + * alpha is the maximum degree of any variable to eliminate. + * gamma is the maximum number of edges we are allowed to add while eliminating variavles + */ + case "SIMPLE" => { + val Seq(alpha, gamma) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + } + //factorThresh is the maximum total cost of all variables eliminated. + //when we exceed this value, collapsing stops, even if there are still candidates. + case "FACTOR" => { + val Seq(alpha, gamma, factorThresh) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with FactorSizeCollapseStrategy { + override val factorThreshold = factorThresh + } + } + case "DETERM" => { + val Seq(alpha, gamma) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with DeterministicCollapseStrategy + } + case "RECURR" => { + //sampleResetFrequency is the frequency with which we reset and re-collapse the model. + //sampleSaveFrequency is the frequency with which we store a sample to use in our marginal estimates. + val Seq(alpha, gamma, sampleResetFrequency, sampleSaveFrequency) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with RecurringCollapseStrategy { + override val sampleReset = sampleResetFrequency + override val sampleRecurrence = sampleSaveFrequency + } + } + case _ => { + val Seq(alpha, gamma, trackSamples) = collapseParameters + new CollapsedProbQueryGibbs(universe, targets: _*)( + dependentUniverses, + dependentAlgorithm, + burnIn, interval, blockToSampler, alpha, gamma) with AnytimeProbQuerySampler with ChainApplyBlockingGibbs + with HeuristicCollapseStrategy { + override val trackingSamples = trackSamples + } + } + } + + + /** + * Use Gibbs sampling to compute the probability that the given element satisfies the given predicate. + */ + def probability[T](target: Element[T], predicate: T => Boolean): Double = { + val alg = CollapsedGibbs(10000, target) + alg.start() + val result = alg.probability(target, predicate) + alg.kill() + result + } + + /** + * Use Gibbs sampling 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) +} + diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedProbQueryGibbs.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedProbQueryGibbs.scala new file mode 100644 index 00000000..8a7ef9a2 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/CollapsedProbQueryGibbs.scala @@ -0,0 +1,271 @@ +/* + * CollapsedProbQueryGibbs.scala + * Core class for the collapsed Gibbs sampler. All other Collapsed Gibbs samplers extend this class, + * with traits mixed in to specify strategy. + * + * Created By: Cory Scott (cscott@cra.com) + * Creation Date: July 25, 2016 + * + * 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.experimental.collapsedgibbs + +import com.cra.figaro.algorithm.lazyfactored.{ LazyValues, ValueSet } +import scala.annotation.tailrec +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import scala.collection.mutable.{ Map => MutableMap, Set => MutableSet} +import com.cra.figaro.algorithm.factored.factors.{ Variable, ElementVariable, InternalChainVariable, Factor} +import com.cra.figaro.algorithm.structured.Problem +import com.cra.figaro.algorithm.factored.factors.factory.Factory +import com.cra.figaro.algorithm.factored.gibbs._ +import com.cra.figaro.algorithm.factored.VEGraph +import com.cra.figaro.algorithm.{ AlgorithmException, UnsupportedAlgorithmException } +import com.cra.figaro.library.compound.^^ + +/** + * CollapsedProbQueryGibbs only uses graph information and the list of targets to collapse some variables. + * extend with HeuristicCollapser or RecurringCollapser to implement other features described in Gogate et. al. + */ +abstract class CollapsedProbQueryGibbs(override val universe: Universe, targets: Element[_]*)( +override val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], +override val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, +override val burnIn: Int, override val interval: Int, +override val blockToSampler: Gibbs.BlockSamplerCreator, alphaIn: Int = 10, gammaIn:Int = 1000, + upperBounds: Boolean = false) +extends ProbQueryGibbs(universe, targets: _*)(dependentUniverses, dependentAlgorithm, 0, interval, + blockToSampler, upperBounds) +with CollapsedProbabilisticGibbs { + override def initialize() = { + super.initialize() + //need to create the original blocks before we eliminate any variables + originalBlocks = createBlocks() + alpha = alphaIn + gamma = gammaIn + targs = targets + alphaChoose2 = math.max((alpha*(alpha-1.0))/2.0, 1.0) + varsInOrder = variables.toList + targetVariables = targets.toList.map(x => Variable(x)) + globalGraph = new VEGraph(factors) + collapseVariables() + //create the correct blocks by filtering out eliminated variables + val blocks = correctBlocks(originalBlocks) + //this is all the same as in ProbQueryGibbs + blockSamplerCreate = blockToSampler + blockSamplers = blocks.map(block => + blockToSampler((block, factors.filter(_.variables.exists(block.contains(_)))))) + val initialSample = WalkSAT(factors, variables, semiring, chainMapper) + variables.foreach(v => currentSamples(v) = initialSample(v)) + for (_ <- 1 to burnIn) sampleAllBlocks() + } +} + +trait CollapsedProbabilisticGibbs extends ProbabilisticGibbs { + /** + * We need a list of variables in order so we can access them by index. + */ + var varsInOrder: List[Variable[_]] = List() + + + var originalBlocks:List[Gibbs.Block] = List() + /** + * List of variables corresponding to target elements. + * Creating these is memoized, so we don't need to worry about duplicates. + */ + var targetVariables: List[Variable[_]] = List() + + /** + * Only variables with alpha or fewer neighbors in the primal graph are candidates for collapsing. + */ + var alpha:Int = _ + /* + gamma controls the trade-off between blocking and collapsing. See collapseVariables. + */ + var gamma:Int = _ + + + /** + * We use ( alpha C 2 ) often, may as well store it. + */ + var alphaChoose2:Double = _ + + + /** + * globalGraph lets us traverse the primal graph. + */ + var globalGraph: VEGraph = _ + + + /** + * Store which elements are our target variables so that subclasses can make use of them. + */ + var targs:Seq[Element[_]] = _ + + /** + * Store which elements are our target variables so that subclasses can make use of them. + */ + var upperB:Boolean = _ + + /* + * Keep the BlockSamplerCreator for later use. + */ + var blockSamplerCreate: Gibbs.BlockSamplerCreator = _ + + /** + * Returns how many edges would be added to the primal graph by removing var1. + * Note: this is number of edges added, NOT net edges added and removed. + * Source paper is somewhat ambiguous on whether this should be added or net. + */ + def graphTerm[T](var1: Variable[T]):Double = { + //val (updatedGraph, trash) = globalGraph.eliminate(var1) + val updatedGraph = globalGraph.eliminate(var1)._1 + val graphTerm = (variables.filter(x => x != var1).toList.map(varT => updatedGraph.info(varT).neighbors.toList.length - 1 ).sum/2 + - variables.filter(x => x != var1).toList.map(varT => (globalGraph.info(varT).neighbors.filter(x => x != var1).toList.length - 1) ).sum/2) + graphTerm + } + + + /** + * We want to alter the original blocks so that we filter out any variables which have + * been eliminated. If the original blocks overlapped a lot, then there'll be a lot of + * redundancy in the filtered blocks, so we take a further step of eliminating any block + * xs which is fully contained in another block ys. + */ + def correctBlocks(originalBlocks:List[Gibbs.Block]):List[Gibbs.Block] = { + val initial = MutableSet[Set[Variable[_]]]() + for {x <- originalBlocks.map(bl => bl.filter(y => variables.contains(y)).toSet).distinct} { + initial add x + } + for {xs <- initial} { + for {ys <- initial}{ + //println("&" + xs + " " + ys) + if ((xs subsetOf ys) && (xs != ys)) { + initial remove xs + } + } + } + initial.map(x => x.toList).toList + } + + /** + * The heuristic of a node is how many edges would be added to the primal graph by removing that variable. + * Because we make a clique over the variable's neighbors. + * Since we only eliminate variables with alpha or fewer neighbors, this is capped at (alpha C 2). + * So we return the number of edges as a percentage of (alpha C 2). + */ + def graphHeuristicFunction[T](var1: Variable[T]):Double = { + ((alphaChoose2 - graphTerm(var1))/alphaChoose2) + } + + /** + * Eliminate a variable. This follows the same approach as in VariableElimination.scala. + }*/ + def eliminate( + variable: Variable[_], + factors: MultiSet[Factor[Double]], + map: MutableMap[Variable[_], MultiSet[Factor[Double]]]): Unit = { + val varFactors = map(variable) + if (varFactors nonEmpty) { + //flatten all of varFactors into one factor + val productFactor = varFactors reduceLeft (_.product(_)) + //marginalize that factor to all variables other than variable + val resultFactor = productFactor.marginalizeTo( + productFactor.variables.filter(_ != variable): _*) + //update our multiset of factors, and our map variables ->: factors + varFactors.foreach(factors.removeOne(_)) + factors.addOne(resultFactor) + varFactors.foreach(removeFactor(_, map)) + map -= variable + addFactor(resultFactor, map) + } + } + + + /** + * Marginalize a factor to a particular variable. + */ + def marginalizeToTarget(factor: Factor[Double], target: Variable[_]) = { + val unnormalizedTargetFactor = factor.marginalizeTo(target) + val z = unnormalizedTargetFactor.foldLeft(semiring.zero, _ + _) + //val targetFactor = Factory.make[Double](unnormalizedTargetFactor.variables) + val targetFactor = unnormalizedTargetFactor.mapTo((d: Double) => d ) + targetFactor + } + + /** + * Marginalize all factors to their component variables. + */ + def marginalize(resultFactor: Factor[Double]) = + variables.map(marginalizeToTarget(resultFactor, _)).toList + + /** + * Combine all the remaining factors into one 'result factor', as in VE. + */ + def makeResultFactor(factorsAfterElimination: MultiSet[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(_)) + } + + /** + * add a factor to the list + */ + def addFactor[T](factor: Factor[T], map: MutableMap[Variable[_], MultiSet[Factor[T]]]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, HashMultiSet()).addOne(factor))) + + /** + * remove a factor from the list + */ + def removeFactor[T](factor: Factor[T], map: MutableMap[Variable[_], MultiSet[Factor[T]]]): Unit = + factor.variables foreach (v => map += v -> (map.getOrElse(v, HashMultiSet()).removeOne(factor))) + + + /** + * Sort variables by the target heuristic, if they have fewer than alpha neighbors and are not targets. + */ + def sortByHeuristic(varList:List[Variable[_]], HeuristicMap:MutableMap[Variable[_], Double]) = { + varList.filter(x => !targetVariables.contains(x) && + (globalGraph.info(x).neighbors.toList.length - 1) <= alpha).sortWith(HeuristicMap(_) > HeuristicMap(_)) + } + /** + * Perform the collapsing step. + */ + def collapseVariables() = { + //store the heuristic for every variable, so we don't have to calculate it as often. + val graphHeuristic:MutableMap[Variable[_], Double] = MutableMap() ++ variables.map(v => + v-> graphHeuristicFunction(v)).toMap + //sort the variables using stored values + var sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + var edgesAdded:Int = 0 + //map and tempFactors are to help with variable elimination. + val map = MutableMap[Variable[_], MultiSet[Factor[Double]]]() + val tempFactors = HashMultiSet[Factor[Double]]() + factors foreach (x => tempFactors.addOne(x)) + for {fact <- tempFactors} { + fact.variables foreach (v => map += v -> (map.getOrElse(v, HashMultiSet()).addOne(fact))) + } + //we collapse variables until either we are out of candidates or we've added too many edges. + while (sortedVars.length > 0 && edgesAdded < gamma) { + //eliminate the variable with highest heuristic. + var toRemove:Variable[_] = sortedVars(0) + eliminate(toRemove, tempFactors, map) + variables = variables.filter(_ != toRemove) + var oldNeighbors = globalGraph.info(toRemove).neighbors.filter(_ != toRemove) + globalGraph = new VEGraph(tempFactors) + //update all of the neighbors of the variable we just eliminated, since their scores will have changed. + for {x <- oldNeighbors} graphHeuristic(x) = graphHeuristicFunction(x) + sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + } + //update the list of factors to reflect the changes we've made. + factors = tempFactors.elements + } +} + + + diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/Collapsers.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/Collapsers.scala new file mode 100644 index 00000000..f8ad0bbb --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/collapsedgibbs/Collapsers.scala @@ -0,0 +1,361 @@ +/* + * Collapsers.scala + * Three alternative collapsing strategies for collapsed Gibbs sampling. + * Each of these is a trait that must be mixed in to a CollapsedProbQueryGibbs. + * + * Created By: Cory Scott (cscott@cra.com) + * Creation Date: July 21, 2016 + * + * 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.experimental.collapsedgibbs + +import com.cra.figaro.algorithm.lazyfactored.{ LazyValues, ValueSet } +import scala.annotation.tailrec +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.util._ +import com.cra.figaro.algorithm.factored.VariableElimination +import scala.collection.mutable.{ Map => MutableMap, Set => MutableSet} +import com.cra.figaro.algorithm.factored.factors.{ Variable, ElementVariable, InternalChainVariable, Factor} +import com.cra.figaro.algorithm.structured.Problem +import com.cra.figaro.algorithm.factored.factors.factory.Factory +import com.cra.figaro.algorithm.factored.gibbs._ +import com.cra.figaro.algorithm.factored.VEGraph +import com.cra.figaro.algorithm.{ AlgorithmException, UnsupportedAlgorithmException } +import com.cra.figaro.library.compound.^^ + + +/** + * HeuristicCollapsedGibbs adds the Hellinger-distance-based term to the elimination heuristic. + * So the heuristic is now based on the marginal probabilities and pairwise marginals. + * These have to be estimated by some number of saved samples. + */ +trait HeuristicCollapseStrategy extends CollapsedProbabilisticGibbs { + var numSamplesSeenSoFar: Int = 0 + var totalSamples: Int = 0 + var marginals: MutableMap[Int,MutableMap[Int,Double]] = MutableMap() + var pairwiseMarignals: MutableMap[(Int, Int), MutableMap[(Int, Int), Double]] = MutableMap() + var hellingerDistances: MutableMap[(Int, Int), Double] = MutableMap() + // trackingSamples is the number of initial samples we take to estimate correlation bewteen variables. + val trackingSamples = 200 + + override def initialize() = { + super.initialize() + totalSamples = 0 + val neededElements = getNeededElements(targetElements, Int.MaxValue)._1 + factors = getFactors(neededElements, targetElements, upperB) + variables = factors.flatMap(_.variables).toSet + varsInOrder = variables.toList + targetVariables = targs.toList.map(x => Variable(x)) + globalGraph = new VEGraph(factors) + //Initialize the maps that keep track of marginals and pairwise marginals. + resetMarginals() + //Get the initial samples. + for {_ <- 1 to trackingSamples} sampleAllBlocksWithTracking + //Compute the Hellinger Distances, then use them to collapse some variables. + updateDistances() + collapseVariables() + //Perform update the original blocks to reflect eliminated variables. + val blocks = correctBlocks(originalBlocks) + blockSamplers = blocks.map(block => blockSamplerCreate((block, + factors.filter(_.variables.exists(block.contains(_)))))) + val initialSample = WalkSAT(factors, variables, semiring, + (chain: Chain[_,_]) => LazyValues(chain.universe).getMap(chain).values.map(Variable(_)).toSet) + variables.foreach(v => currentSamples(v) = initialSample(v)) + for (_ <- 1 to burnIn) sampleAllBlocks() + } + + /** + * Sample all blocks, then store that sample in the marginal and p.m. maps. + */ + def sampleAllBlocksWithTracking() = { + super.sampleAllBlocks() + numSamplesSeenSoFar += 1 + for {testVar <- 0 until varsInOrder.length} { + val samp1 = currentSamples.getOrElse(varsInOrder(testVar), -1) + marginals(testVar)(samp1) = marginals(testVar).getOrElse(samp1, 0.0) + 1.0 + // only need to go up to testVar, since we can fill upper and lower triangles of matrix simultaneously. + for {otherVar <- 0 until testVar} { + val samp2 = currentSamples.getOrElse(varsInOrder(otherVar),-1) + pairwiseMarignals((testVar, otherVar))((samp1, samp2)) = pairwiseMarignals((testVar, otherVar)).getOrElse(((samp1, samp2)) ,0.0) + 1.0 + pairwiseMarignals((otherVar, testVar))((samp2, samp1)) = pairwiseMarignals((otherVar, testVar)).getOrElse(((samp2, samp1)) ,0.0) + 1.0 + } + } + } + + /** + * Reset all the marginal and p.m. maps to empty maps. + */ + def resetMarginals() = { + numSamplesSeenSoFar = 0 + for {testVar <- 0 until varsInOrder.length} { + marginals(testVar) = MutableMap() + for {otherVar <- 0 until testVar} { + pairwiseMarignals((testVar, otherVar)) = MutableMap() + pairwiseMarignals((otherVar, testVar)) = MutableMap() + } + } + } + + /** + * Use the marginal maps to compute Hellinger maps. + */ + def updateDistances() = { + for {testVar <- 0 until varsInOrder.length} { + for {otherVar <- 0 until testVar} { + val dist = distributionDistance(varsInOrder(testVar), varsInOrder(otherVar)) + hellingerDistances((testVar, otherVar)) = dist + hellingerDistances((otherVar, testVar)) = dist + } + } + + } + + /** + * Hellinger distance is defined in the source paper (amongst other places). + * It's the sum over all values of X1 and X2 of (sqrt(P(X1,X2)) - sqrt(P(X1)*P(X2)))^2 + */ + def distributionDistance[T,U](var1: Variable[T], var2: Variable[U]) = { + var dist = 0.0 + var index1 = varsInOrder.indexOf(var1) + var index2 = varsInOrder.indexOf(var2) + for {a <- 0 to var1.range.length} { + for {b <- 0 to var2.range.length} { + dist += (math.pow(math.sqrt(pairwiseMarignals.getOrElse((index1, index2), Map[(Int, Int), Double]()).getOrElse((a, b), 0.0)/numSamplesSeenSoFar) + - math.sqrt(marginals.getOrElse(index1,Map[Int,Double]()).getOrElse(a,0.0)*marginals.getOrElse(index2,Map[Int,Double]()).getOrElse(b,0.0))/numSamplesSeenSoFar, 2.0)) + } + } + dist + } + + /** + * Compute the score of a given variable. + */ + override def graphHeuristicFunction[T](var1: Variable[T]) = { + //get number of edges left to add and distance from this variable to others + var index = varsInOrder.indexOf(var1) + var otherIndices = for {v <- 0 until varsInOrder.length if v != index} yield v + //get the graph-based score from superclass, and add the score from Hellinger Distances + super.graphHeuristicFunction(var1) + otherIndices.map(x => hellingerDistances.getOrElse((index, x), 0.0)).sum + } + + /** + * Perform the collapsing step. + */ + override def collapseVariables() = { + //store the heuristic for every variable, so we don't have to calculate it as often. + var graphHeuristic:MutableMap[Variable[_], Double] = MutableMap() ++ variables.map(v => v-> graphHeuristicFunction(v)).toMap + //sort the variables using stored values + var sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + var edgesAdded:Int = 0 + //map and tempFactors are to help with variable elimination + var map = MutableMap[Variable[_], MultiSet[Factor[Double]]]() + var tempFactors = HashMultiSet[Factor[Double]]() + factors foreach (x => tempFactors.addOne(x)) + for {fact <- tempFactors} { + fact.variables foreach (v => map += v -> (map.getOrElse(v, HashMultiSet()).addOne(fact))) + } + //we collapse variables until either we are out of candidates or we've added too many edges + while (sortedVars.length > 0 && edgesAdded < gamma) { + //eliminate the variable with highest heuristic. + var toRemove:Variable[_] = sortedVars(0) + eliminate(toRemove, tempFactors, map) + variables = variables.filter(_ != toRemove) + var oldNeighbors = globalGraph.info(toRemove).neighbors.filter(_ != toRemove) + globalGraph = new VEGraph(tempFactors) + //update all of the neighbors of the variable we just eliminated, since their scores will have changed. + for {x <- oldNeighbors} graphHeuristic(x) = graphHeuristicFunction(x) + /** In this version of collapseVariables, we also have to subtract the Hellinger Distance + * between our removed variable and all other variables from each of their scores. + */ + for {v <- variables.filter(!oldNeighbors.contains(_))} graphHeuristic(v) -= hellingerDistances.getOrElse((varsInOrder.indexOf(toRemove), varsInOrder.indexOf(v)), 0.0) + sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + } + //update the list of factors to reflect the changes we've made. + //factors = marginalize(makeResultFactor(tempFactors)) + factors = tempFactors.elements + } + +} + + +/** + * This trait causes variables to collapsed until the total summed size of all of the factors collapsed + * thus far exceeds a threshold. + */ +trait FactorSizeCollapseStrategy extends CollapsedProbabilisticGibbs { + + val factorThreshold:Int = 1000 + + override def makeResultFactor(factorsAfterElimination: MultiSet[Factor[Double]]): Factor[Double] = { + //println(factorsAfterElimination.map((x:Factor[Double]) => x.size)) + super.makeResultFactor(factorsAfterElimination) + } + + /** + * Collapse variables + */ + override def collapseVariables() = { + //store the heuristic for every variable, so we don't have to calculate it as often. + var graphHeuristic:MutableMap[Variable[_], Double] = MutableMap() ++ variables.map(v => v-> graphHeuristicFunction(v)).toMap + //sort the variables using stored values + var sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + var edgesAdded:Int = 0 + //map and tempFactors are to help with variable elimination + val map = MutableMap[Variable[_], MultiSet[Factor[Double]]]() + var tempFactors = HashMultiSet[Factor[Double]]() + factors foreach (x => tempFactors.addOne(x)) + for {fact <- tempFactors} { + fact.variables foreach (v => map += v -> (map.getOrElse(v, HashMultiSet()).addOne(fact))) + } + var costSum = 0 + //we collapse variables until either we are out of candidates or we've added too many edges + while (costSum < factorThreshold && sortedVars.length > 0) { + //eliminate the variable with highest heuristic. + //println(tempFactors.map((x:Factor[Double]) => x.variables.map(y => varsInOrder.indexOf(y)))) + var toRemove:Variable[_] = sortedVars(0) + val cost = map(toRemove).map((x:Factor[Double]) => x.size).product + //check to see if adding this cost will bring us above threshold. If so, move on to another variable. + if (cost + costSum < factorThreshold) { + eliminate(toRemove, tempFactors, map) + var oldNeighbors = globalGraph.info(toRemove).neighbors.filter(_ != toRemove) + globalGraph = new VEGraph(tempFactors) + variables = variables.filter(_ != toRemove) + //update all of the neighbors of the variable we just eliminated, since their scores will have changed. + for {x <- oldNeighbors} graphHeuristic(x) = graphHeuristicFunction(x) + sortedVars = sortByHeuristic(variables.toList, graphHeuristic) + costSum += cost + } + else { + sortedVars = sortByHeuristic(sortedVars.filter(_ != toRemove), graphHeuristic) + } + } + factors = tempFactors.elements + } + +} + + + +/** + * Experimental collapser that doesn't need to calculate marginal probabilities. + * The original paper doesn't distinguish between model variables, or use any meta-information + * about the variables. + * Since Figaro knows which variables are deterministic, we can use this as a proxy for the + * correlation heuristic. + */ +trait DeterministicCollapseStrategy extends CollapsedProbabilisticGibbs { + + /** + * For this strategy we need access to the distance maps as well as the blocks. + */ + var hellingerDistances: MutableMap[(Int, Int), Double] = MutableMap() + var blocks: List[Gibbs.Block] = _ + + /** + * Unlike other + */ + override def initialize() = { + super.initialize() + updateDistances() + collapseVariables() + //Perform update the original blocks to reflect eliminated variables. + val blocks = correctBlocks(originalBlocks) + blockSamplers = blocks.map(block => blockSamplerCreate((block, + factors.filter(_.variables.exists(block.contains(_)))))) + val initialSample = WalkSAT(factors, variables, semiring, + (chain: Chain[_,_]) => LazyValues(chain.universe).getMap(chain).values.map(Variable(_)).toSet) + variables.foreach(v => currentSamples(v) = initialSample(v)) + for (_ <- 1 to burnIn) sampleAllBlocks() + } + + /** + * Override the distance function. + */ + def updateDistances() = { + blocks = createBlocks() + for {testVar <- 0 until varsInOrder.length} { + for {otherVar <- 0 until testVar} { + var dist = 0.0 + /* Instead of the given definition of Hellinger Distance, use the following distance function: + * For every block that contains both variables: + * sum up the difference in index between the first variable and the second variable, + * normalized by block size. + * Since blocks are added by starting with stochastic variables and recursively adding + * children, this should be a rough measure of how much otherVar depends on testVar + */ + for {b <- blocks} { + if (b.contains(testVar) && b.contains(otherVar)) { + dist += (b.indexOf(otherVar) - b.indexOf(testVar))/b.size + } + } + hellingerDistances((testVar, otherVar)) = dist + hellingerDistances((otherVar, testVar)) = -1.0*dist + } + } + + } + + /** + * Override the heuristic function to use the new measure of distance. + */ + override def graphHeuristicFunction[T](var1: Variable[T]) = { + //get number of edges left to add and distance from this variable to others + var index = varsInOrder.indexOf(var1) + var otherIndices = for {v <- 0 until varsInOrder.length if v != index} yield v + super.graphHeuristicFunction(var1) + otherIndices.map(x => hellingerDistances.getOrElse((index, x), 0.0)).sum + } + +} + + +/** + * In the paper, the authors recommend updating the marginals every N samples and re-collapsing every few iterations. + * In practice, this is pretty slow. + * This trait will keep a running tally of samples of each of the used variables and re-collapse the factor graph + * (starting from the initial graph) periodically. + */ +trait RecurringCollapseStrategy extends HeuristicCollapseStrategy { + /** + * How often we want to re-collapse. + */ + val sampleReset:Int = 2000 + + /** + * How often we want to save one of our samples for marginal estimation. + */ + val sampleRecurrence: Int = 50 + + /** + * We override resetMarginals() to do nothing so that we don't get rid of our saved marginal data every time we re-initialize. + * If we haven't built the marginal maps yes, build them, else do nothing. + */ + override def resetMarginals() = { + if (marginals.isEmpty) { + super.resetMarginals() + } + } + + /** + * Every sampleRecurrence many samples, we alter the marginals and PairwiseMarginal Maps to record the current sample. + * Every sampleReset many samples, we re-initialize. + */ + override def sampleAllBlocks() = { + totalSamples += 1 + if ((totalSamples % sampleRecurrence) == 0) { + sampleAllBlocksWithTracking() + } + else if (totalSamples == sampleReset) { + initialize() + } + else { + super.sampleAllBlocks() + } + } +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/AnytimeMarginalMAP.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/AnytimeMarginalMAP.scala new file mode 100644 index 00000000..a4a8813e --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/AnytimeMarginalMAP.scala @@ -0,0 +1,50 @@ +/* + * AnytimeMarginalMAP.scala + * Anytime algorithms that compute the most likely values of some elements, and marginalize over all other elements. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 27, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm._ +import com.cra.figaro.language._ +import akka.pattern.ask + +/** + * Anytime algorithms that compute most likely values of some elements, and marginalize over all other elements. + * A class that implements this trait must implement initialize, runStep, and computeMostLikelyValue methods. + */ +trait AnytimeMarginalMAP extends MarginalMAPAlgorithm with Anytime { + /** + * A message instructing the handler to compute the most likely value of the target element. + */ + case class ComputeMostLikelyValue[T](target: Element[T]) extends Service + + /** + * A message from the handler containing the most likely value of the previously requested element. + */ + case class MostLikelyValue[T](value: T) extends Response + + def handle(service: Service): Response = + service match { + case ComputeMostLikelyValue(target) => + MostLikelyValue(computeMostLikelyValue(target)) + } + + protected def doMostLikelyValue[T](target: Element[T]): T = { + awaitResponse(runner ? Handle(ComputeMostLikelyValue(target)), messageTimeout.duration) match { + case MostLikelyValue(result) => result.asInstanceOf[T] + case _ => { + println("Error: Response not recognized from algorithm") + target.value + } + } + } +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPAlgorithm.scala new file mode 100644 index 00000000..8eeb7fae --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPAlgorithm.scala @@ -0,0 +1,59 @@ +/* + * MarginalMAPAlgorithm.scala + * Algorithms that compute the most likely values of some elements, and marginalize over all other elements. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 2, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.{Algorithm, AlgorithmException, AlgorithmInactiveException} +import com.cra.figaro.language._ + +/** + * Algorithms that compute max a posteriori (MAP) values of some elements, and marginalize over all other elements. + */ +trait MarginalMAPAlgorithm extends Algorithm { + class NotATargetException[T](target: Element[T]) extends AlgorithmException + + def universe: Universe + + /** + * Elements for which to perform MAP queries. This algorithm marginalizes over elements not in this list. + */ + def mapElements: Seq[Element[_]] + + /* + * Particular implementations of algorithm must provide the following method. + */ + def computeMostLikelyValue[T](target: Element[T]): T + + /* + * Defined in one time and anytime marginal MAP versions of this class. Does not need to be defined + * by particular algorithm implementations. + */ + protected def doMostLikelyValue[T](target: Element[T]): T + + private def check[T](target: Element[T]): Unit = { + if (!active) throw new AlgorithmInactiveException + if (!(mapElements contains target)) throw new NotATargetException(target) + } + + /** + * Returns an estimate of the max a posteriori value of the target. + * @throws NotATargetException if called on a target that is not in the list of MAP elements. + * @throws AlgorithmInactiveException if the algorithm is inactive. + */ + def mostLikelyValue[T](target: Element[T]): T = { + check(target) + doMostLikelyValue(target) + } + + universe.registerAlgorithm(this) +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPBeliefPropagation.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPBeliefPropagation.scala new file mode 100644 index 00000000..cd8e8841 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPBeliefPropagation.scala @@ -0,0 +1,177 @@ +/* + * MarginalMAPBeliefPropagation.scala + * A marginal MAP belief propagation algorithm. Based on the algorithm by Liu and Ihler (2013). + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 10, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.factored.beliefpropagation._ +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.sampling.ProbEvidenceSampler +import com.cra.figaro.language._ + +abstract class MarginalMAPBeliefPropagation(override val universe: Universe, targets: Element[_]*)( + val dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + val dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double) + extends MarginalMAPAlgorithm + with ProbabilisticBeliefPropagation { + + val targetElements = targets.toList + + val mapElements = targetElements + + /* + * Variables corresponding to MAP elements. This is set in the initialize() method. + */ + protected var maxVariables: Set[Variable[_]] = _ + + /** + * Value used to compute arg max messages. This could be thought of as an inverse + * "temperature", but here it is a large fixed value. + */ + val argMaxFactor = 1e5 + + override protected def getNewMessageFactorToVar(fn: FactorNode, vn: VariableNode) = { + val vnFactor = factorGraph.getLastMessage(vn, fn) + + if(maxVariables.contains(vn.variable)) { + val total = beliefMap(fn).combination(vnFactor, LogSumProductSemiring().divide) + + // Use sum-product to sum over sum variables + val sumOverSumVars = total.marginalizeTo(fn.variables.intersect(maxVariables).toSeq:_*) + // Use max-product to sum over max variables except vn.variable + val sumOverMaxVars = sumOverSumVars.marginalizeToWithSum(LogMaxProductSemiring().sum, vn.variable) + sumOverMaxVars + } + else { + // Use sum-product to sum over sum variables, note that we don't divide by the last message here + val beliefOverMaxVars = beliefMap(fn).marginalizeTo(fn.variables.intersect(maxVariables).toSeq:_*) + val maxBelief = beliefOverMaxVars.foldLeft(LogSumProductSemiring().zero, _ max _) + // Filter the indices that maximize the belief over the max variables in this factor + // This approximation of the "indicator function" is used for two reasons: + // 1) To prevent division by zero when we divide the belief map by the last message + // 2) To give nonzero weight to values close to the arg max that differ due to floating point errors + val argMaxIndicator = beliefOverMaxVars.mapTo(d => (d - maxBelief) * argMaxFactor) + + // Now the total includes the indicator function + val total = beliefMap(fn).combination(vnFactor, LogSumProductSemiring().divide).product(argMaxIndicator) + + // Use sum-product to sum over all variables except vn.variable + val sumOverAllVars = total.marginalizeTo(vn.variable) + sumOverAllVars + } + } + + /* + * We use sum product by default and switch to max product only when needed. + * Notably, we only use the sum operation through getNewMessageFactorToVar. + */ + val semiring = SumProductSemiring() + + override def initialize() = { + val (neededElements, _) = getNeededElements(universe.activeElements, Int.MaxValue) + factorGraph = new BasicFactorGraph(getFactors(neededElements, targetElements), logSpaceSemiring()) + maxVariables = factorGraph.getNodes.collect { + case VariableNode(ev: ElementVariable[_]) if mapElements.contains(ev.element) => ev + }.toSet + super.initialize() + } + + def computeMostLikelyValue[T](target: Element[T]): T = { + val beliefs = getBeliefsForElement(target) + beliefs.maxBy(_._1)._2 + } +} + +object MarginalMAPBeliefPropagation { + /** + * Creates a One Time marginal MAP belief propagation computer in the current default universe. + * @param myIterations Iterations of mixed-product BP to run. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply(myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + with OneTimeProbabilisticBeliefPropagation with OneTimeMarginalMAP { val iterations = myIterations } + + /** + * Creates an Anytime marginal MAP belief propagation computer in the current default universe. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply(targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + with AnytimeProbabilisticBeliefPropagation with AnytimeMarginalMAP + + /** + * Creates a One Time marginal MAP belief propagation computer in the current default universe. + * @param dependentUniverses Dependent universes for this algorithm. + * @param myIterations Iterations of mixed-product BP to run. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + dependentUniverses, (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + with OneTimeProbabilisticBeliefPropagation with OneTimeMarginalMAP { val iterations = myIterations } + + /** + * Creates an Anytime marginal MAP belief propagation computer in the current default universe. + * @param dependentUniverses Dependent universes for this algorithm. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + dependentUniverses, (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) + with AnytimeProbabilisticBeliefPropagation with AnytimeMarginalMAP + + /** + * Creates a One Time marginal MAP belief propagation computer in the current default universe. + * @param dependentUniverses Dependent universes for this algorithm. + * @param dependentAlgorithm Used to determine algorithm for computing probability of evidence in dependent universes. + * @param myIterations Iterations of mixed-product BP to run. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + myIterations: Int, targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + dependentUniverses, dependentAlgorithm) + with OneTimeProbabilisticBeliefPropagation with OneTimeMarginalMAP { val iterations = myIterations } + + /** + * Creates an Anytime marginal MAP belief propagation computer in the current default universe. + * @param dependentUniverses Dependent universes for this algorithm. + * @param dependentAlgorithm Used to determine algorithm for computing probability of evidence in dependent universes. + * @param targets MAP elements, which can be queried. Elements not supplied here are summed over. + */ + def apply( + dependentUniverses: List[(Universe, List[NamedEvidence[_]])], + dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, + targets: Element[_]*)(implicit universe: Universe) = + new MarginalMAPBeliefPropagation(universe, targets: _*)( + dependentUniverses, dependentAlgorithm) + with AnytimeProbabilisticBeliefPropagation with AnytimeMarginalMAP + + /** + * Use belief propagation to compute the most likely value of the given element. + * Runs 10 iterations of mixed-product BP. + * @param target Element for which to compute MAP value. + * @param mapElements Additional elements to MAP. Elements not in this list are summed over. + */ + def mostLikelyValue[T](target: Element[T], mapElements: Element[_]*): T = { + val alg = MarginalMAPBeliefPropagation(10, (target +: mapElements).distinct:_*) + alg.start() + val result = alg.mostLikelyValue(target) + alg.kill() + result + } +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPVEStrategy.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPVEStrategy.scala new file mode 100644 index 00000000..f464fd02 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/MarginalMAPVEStrategy.scala @@ -0,0 +1,44 @@ +/* + * MarginalMAPVEStrategy.scala + * A class that solves a marginal MAP problem using VE. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: July 1, 2015 + * + * Copyright 2015 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.marginalmap + +import com.cra.figaro.algorithm.factored.factors.{Factor, Variable} +import com.cra.figaro.algorithm.structured.{NestedProblem, Problem, solver} +import com.cra.figaro.algorithm.structured.strategy.solve.SolvingStrategy + +/** + * A solving strategy that uses MPE VE to solve non-nested problems, and performs the MAP step at the top level. + * It is assumed that at the top level, "toPreserve" elements are the MAP elements. + */ +class MarginalMAPVEStrategy extends SolvingStrategy { + + def solve(problem: Problem, toEliminate: Set[Variable[_]], toPreserve: Set[Variable[_]], factors: List[Factor[Double]]): + (List[Factor[Double]], Map[Variable[_], Factor[_]]) = { + problem match { + case _: NestedProblem[_] => { + // A problem needed for the initial step of summing out the non-MAP variables + // Use marginal VE for this + solver.marginalVariableElimination(problem, toEliminate, toPreserve, factors) + } + case _ => { + // Sum over the remaining non-MAP variables (i.e. toEliminate), and MAP the rest (i.e. toPreserve) + // marginalizedFactors is a set of factors over just the MAP variables + val (marginalizedFactors, _) = solver.marginalVariableElimination(problem, toEliminate, toPreserve, factors) + // Now that we have eliminated the sum variables, we effectively just do MPE over the remaining variables + // For MPE, we eliminate all remaining variables (i.e. toPreserve), and preserve no variables (i.e. Set()) + solver.mpeVariableElimination(problem, toPreserve, Set(), marginalizedFactors) + } + } + } + +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/OneTimeMarginalMAP.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/OneTimeMarginalMAP.scala new file mode 100644 index 00000000..a32cadcf --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/OneTimeMarginalMAP.scala @@ -0,0 +1,25 @@ +/* + * OneTimeMarginalMAP.scala + * One time algorithms that compute the most likely values of some elements, and marginalize over all other elements. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 2, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.OneTime +import com.cra.figaro.language._ + +/** + * One-time algorithms that compute the most likely values of some elements, and marginalize over others. + * A class that implements this trait must implement run and computeMostLikelyValue methods. + */ +trait OneTimeMarginalMAP extends MarginalMAPAlgorithm with OneTime { + protected def doMostLikelyValue[T](target: Element[T]): T = computeMostLikelyValue(target) +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/ProbEvidenceMarginalMAP.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/ProbEvidenceMarginalMAP.scala new file mode 100644 index 00000000..7dd0a789 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/ProbEvidenceMarginalMAP.scala @@ -0,0 +1,412 @@ +/* + * ProbEvidenceMarginalMAP.scala + * A marginal MAP algorithm based on probability of evidence. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 1, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.language.Element.ElemVal +import com.cra.figaro.language._ +import com.cra.figaro.util._ + +import scala.annotation.tailrec +import scala.collection.mutable + +/** + * An algorithm for marginal MAP. This algorithm works by searching for the assignment to the MAP elements that + * maximizes the probability of evidence of observing that assignment. Uses one time probability of evidence sampling at + * each iteration for the given number of samples. Since the probability of evidence is just an estimate, this algorithm + * is allowed to repeatedly take more probability of evidence samples until it believes with high confidence that one + * state is better than another state, or it has reached the maximum number of allowed runs. The maximization is done by + * simulated annealing. + * @param universe Universe on which to run the algorithm. + * @param tolerance Confidence level used deciding to accept or reject under uncertainty. This corresponds to a maximum + * allowed p-value. Thus, setting this to 0.05 means we only accept or reject if we are at least 95% confident that + * we're making the right decision. Must between 0 and 0.5. + * @param samplesPerIteration Number of probability of evidence samples to take per run. Must be strictly greater than 1. + * A reasonable starting point is 100. + * @param maxRuns Maximum number of runs of probability of evidence sampling allowed before giving up and returning the + * proposal with higher estimated probability of evidence. Thus, at each iteration of simulated annealing, this + * algorithm can take as many as `samplesPerIteration * maxRuns` probability of evidence samples. Setting to 1 + * corresponds to using no hypothesis test at all. Setting to `Int.MaxValue` corresponds to running indefinitely until + * we are confident that the proposal should be accepted or rejected. + * @param proposalScheme Scheme for proposing new values. This can propose any element in the universe, but updates to + * non-MAP elements are only used for generating new values for MAP elements. + * @param schedule Schedule that produces an increasing temperature for simulated annealing. + * @param mapElements List of elements over which to perform marginal MAP. These elements must not have evidence on them + * that is contingent on the values of non-MAP elements. Additionally, these elements must be "observable", in the sense + * that observing values for these elements and computing the probability of evidence of those observations should not + * uniquely return zero. Typically, this is satisfiable by elements that are not both continuous and deterministic. The + * algorithm will still run if this condition is not satisfied, but it will not converge. + */ +abstract class ProbEvidenceMarginalMAP(universe: Universe, + tolerance: Double, + samplesPerIteration: Int, + maxRuns: Int, + proposalScheme: ProposalScheme, + schedule: Schedule, + val mapElements: List[Element[_]]) + // Burn-in and interval aren't needed in this context, so they are set to 0 and 1, respectively + extends MetropolisHastings(universe, proposalScheme, 0, 1, mapElements:_*) with MarginalMAPAlgorithm { + import MetropolisHastings._ + + require(samplesPerIteration >= 2, "samples per iteration must be at least 2") + require(0 < tolerance && tolerance < 0.5, "tolerance must be between 0 and 0.5") + require(maxRuns >= 1, "maximum allowed runs must be at least 1") + + // The probability of evidence sampler associated with the current state of the MAP variables. Initialized when this + // (i.e. the ProbEvidenceMarginalMAP) is started. In general, while this is active, probEvidenceSampler refers to + // an active MMAPProbEvidenceSampler that can be run for additional iterations to improve its estimate. + protected var probEvidenceSampler: MMAPProbEvidenceSampler = _ + + // Elements created by MH (and stored in chainCache) that should not be deleted while sampling probability of evidence. + // This is needed because ProbEvidenceSampler can create temporary elements while running, and they must be cleared to + // avoid memory leaks. However, we don't just call universe.clearTemporaries() because this would also clear + // chainCache, which we don't want. This is a var (as opposed to an argument to MMAPProbEvidenceSampler) because it + // may change between iterations of MH. + protected var preserve: Set[Element[_]] = _ + + // Increasing temperature used for simulated annealing. + protected var temperature = 1.0 + + /** + * Get the current temperature. Used for debugging. + */ + def getTemperature = temperature + + override protected def initConstrainedValues() = { + // We only initialize constraints for MAP elements + // After this point, the keys in the map are assumed to be fixed because the constrained MAP elements won't change + for(elem <- universe.constrainedElements.intersect(mapElements)) { + currentConstraintValues(elem) = elem.constraintValue + } + } + + override protected def computeScores(): Double = { + // Compute the log ratio of constraint values for only the MAP elements + val scores = currentConstraintValues.keys.map(elem => elem.constraintValue - currentConstraintValues(elem)) + scores.sum + } + + override protected def mhStep(): State = { + // This state is not constrained. Constraints are handled in decideToAccept because we incorporate them in the + // probability of evidence computation. + val newState = proposeAndUpdate() + // We don't care about dissatisfied elements that aren't MAP elements; remove them + newState.dissatisfied.retain(fastTargets.contains) + + if(decideToAccept(newState)) { + accepts += 1 + accept(newState) + } else { + rejects += 1 + undo(newState) + } + newState + } + + /** + Decide whether or not to accept the new (unconstrained) state, first taking into account conditions on the MAP + * elements. Does not change the state of the universe. Updates the temperature, preserved elements, and probability + * of evidence sampler accordingly. Incorporates constraints on the MAP elements. + */ + override protected def decideToAccept(newState: State): Boolean = { + // Use the same satisfied / dissatisfied rule as MH + val nothingNewDissatisfied = newState.dissatisfied subsetOf dissatisfied + val somethingOldSatisfied = dissatisfied exists (_.conditionSatisfied) + if (nothingNewDissatisfied && somethingOldSatisfied) true + else if (!nothingNewDissatisfied && !somethingOldSatisfied) false + else { + decideToAcceptSatisfied() + } + } + + /** + * Like decideToAccept, but assume all conditions on the MAP elements are satisfied. + */ + protected def decideToAcceptSatisfied(): Boolean = { + // Update the temperature + temperature = schedule.temperature(temperature, sampleCount) + + // Always accept if none of the MAP values changed. This isn't necessarily meaningless; the values of other elements + // may have changed, which could result in new proposals later. These are "observations" because they are the values + // we observe in the probability of evidence sampler. + val observations = currentMAPValues + if(observations == probEvidenceSampler.observations) return true + + // Record the universe state because probability of evidence sampling may corrupt it + val universeState = new UniverseState(universe) + // Record the elements that probability of evidence sampling shouldn't deactivate (includes those in the cache) + preserve = universeState.myActiveElements + + // Create and start a probability of evidence sampler over the current values of the MAP elements + val newProbEvidenceSampler = new MMAPProbEvidenceSampler(observations) + // This initially runs the sampler for samplesPerIteration samples + newProbEvidenceSampler.start() + + // Normally, we test if log(U[0,1]) < (log(newProbEvidence) - log(oldProbEvidence) + computeScores) * temperature. + // We want to reformat this as a hypothesis test involving the log of the mean of two sampled random variables. + // This yields log(oldProbEvidence) + (log(U[0,1]) / temperature - computeScores) < log(newProbEvidence). + // Thus, logConstant is the multiplicative constant applied to the old probability of evidence. + + // Note that in this implementation, we choose to ignore the proposal probability. In theory, this should not make a + // difference in the limiting case, but in practice choosing to incorporate or not incorporate this probability + // could affect the rate of convergence. Here, we choose to ignore it because the proposal probability may not + // correspond to a meaningful change over the MAP elements. In particular, if new values are proposed for non-MAP + // elements but none of the values of MAP elements change, it would not be sensible to weight one state as being + // more or less favorable than the other. + val logConstant = math.log(random.nextDouble) / temperature - computeScores() + // We've already run newProbEvidence sampler once by calling start(), so use maxRuns - 1 + val accepted = compareMeans(probEvidenceSampler, newProbEvidenceSampler, logConstant, maxRuns - 1) + + // Update the probability of evidence sampler and kill the one we don't keep + // Calling deregister might be unnecessary if the algorithm deregisters itself when killed + if(accepted) { + probEvidenceSampler.kill() + universe.deregisterAlgorithm(probEvidenceSampler) + probEvidenceSampler = newProbEvidenceSampler + } + else { + newProbEvidenceSampler.kill() + universe.deregisterAlgorithm(newProbEvidenceSampler) + } + + // Restore the universe state, since it may have been modified while running probability of evidence sampling. + universeState.restore() + + accepted + } + + /** + * Record the current values of all MAP elements. + */ + protected def currentMAPValues: List[ElemVal[_]] = { + // For whatever reason, the Scala compiler complains if we try to make this an anonymous function. + def makeElemVal[T](elem: Element[_]) = ElemVal[T](elem.asInstanceOf[Element[T]], elem.value.asInstanceOf[T]) + mapElements.map(makeElemVal) + } + + /** + * Decides whether or not the mean of the old sampler, multiplied by the constant given, is likely to be less than the + * mean of the new sampler. Computes in log space to avoid underflow. This may mutate the state of the universe. This + * does not take into account conditions and constraints on the MAP elements directly; these should be incorporated in + * the log constant provided. + * @param oldSampler Probability of evidence sampler for the previous state of the MAP elements. + * @param newSampler Probability of evidence sampler for the next state of the MAP elements. + * @param logConstant Log of a multiplicative constant, by which we multiply the mean of the old sampler. + * @param runs Maximum allowed additional runs of probability of evidence sampling before this method should return a + * best guess. This is a kill switch to avoid taking an absurd number of samples when the difference between the means + * is negligible. Must be >= 0. Setting this to 0 is equivalent to performing no hypothesis test at all and just + * comparing the values. + * @return A decision to accept based on a one-sided t-test of the weights sampled from the two samplers. + */ + @tailrec + protected final def compareMeans(oldSampler: MMAPProbEvidenceSampler, newSampler: MMAPProbEvidenceSampler, + logConstant: Double, runs: Int): Boolean = { + val oldLogStats = oldSampler.totalLogStatistics.multiplyByConstant(logConstant) + val newLogStats = newSampler.totalLogStatistics + + // If we aren't allowed to take more samples, our best guess is to return the comparison of the sample means. + // Otherwise, perform the t-test to see if we're confident that the means differ. We are sure that both counts are + // greater than 1 because both counts must be at least samplesPerIteration. + if(runs == 0 || LogStatistics.oneSidedTTest(oldLogStats, newLogStats) < tolerance) { + oldLogStats.logMean < newLogStats.logMean + } + // If we can't decide with the information we have, take more samples and try again. + // Run both samplers for the same number of additional iterations. + else { + oldSampler.run() + newSampler.run() + compareMeans(oldSampler, newSampler, logConstant, runs - 1) + } + } + + override def sample(): (Boolean, Sample) = { + mhStep() + if(dissatisfied.isEmpty) { + // Update as long as no MAP elements are dissatisfied + val values = mapElements.map(elem => elem -> elem.value) + (true, mutable.Map(values:_*)) + } + else { + (false, mutable.Map()) + } + + } + + override protected def updateTimesSeenForTarget[T](elem: Element[T], newValue: T): Unit = { + // Override the last update, which will later be returned as the most likely value + allLastUpdates(elem) = (newValue, sampleCount) + } + + override def computeMostLikelyValue[T](target: Element[T]): T = { + allLastUpdates(target)._1.asInstanceOf[T] + } + + override protected def doInitialize(): Unit = { + super.doInitialize() + // Only record dissatisfied MAP elements + dissatisfied = dissatisfied.intersect(fastTargets) + + // Copy the universe state, since the probability of evidence sampler may corrupt it + val universeState = new UniverseState(universe) + preserve = universeState.myActiveElements + + // Run the probability of evidence sampler for samplesPerIteration + probEvidenceSampler = new MMAPProbEvidenceSampler(currentMAPValues) + probEvidenceSampler.start() + universeState.restore() + } + + /* + * Prevent memory leaks by killing internal algorithms and clearing the cache. + */ + override def cleanUp(): Unit = { + super.cleanUp() + universe.clearTemporaries() + chainCache.clear() + probEvidenceSampler.kill() + // This is unnecessary if the algorithm deregisters itself when killed + universe.deregisterAlgorithm(probEvidenceSampler) + probEvidenceSampler = null + preserve = null + } + + /* + * We don't want to force updates at the end of sampling, since the current values of MAP elements are not necessarily + * the values that maximize the probability of evidence. We only want to make changes to allLastUpdates when a new set + * of values increases the probability of evidence. + */ + override def update(): Unit = {} + + /** + * Special probability of evidence sampler used for marginal MAP. Unlike a regular probability of evidence sampler, + * this records its own variance. It does so in an online fashion, and computes it in log space to prevent underflow. + * Additionally, this algorithm may be run multiple times. The rolling mean and variance computation incorporates the + * samples taken from all runs. + * @param observations Elements and corresponding values that should be observed each time this algorithm is run. + * Normally, this contains MAP elements and their proposed values. + */ + class MMAPProbEvidenceSampler(val observations: List[ElemVal[_]]) extends ProbEvidenceSampler(universe) + with OneTimeProbEvidenceSampler with OnlineLogStatistics { + + override val numSamples = samplesPerIteration + + /** + * Observe the necessary values of MAP elements, then run the algorithm. After this is initialized, calling this + * method again is allowed. The additional samples are accounted for when returning the total log statistics. + */ + override def run(): Unit = { + for(ElemVal(elem, value) <- observations) elem.observe(value) + super.run() + } + + /** + * Perform sampling, but additionally update the variance and clear only elements that shouldn't be preserved. + */ + override protected def doSample(): Unit = { + totalWeight += 1 + + try { + val weight = lw.computeWeight(universe.activeElements) + successWeight = logSum(successWeight, weight) + // Record the weight for the variance computation + record(weight) + } catch { + case Importance.Reject => record(Double.NegativeInfinity) + } + + // Deactivate only the temporary elements created during probability of evidence sampling + for(elem <- universe.activeElements) { + // Since an element deactivates its direct context contents when deactivated, it's possible that an element + // in the list will be deactivated before we reach it, so we have to check again that it is active + if(elem.active && !preserve.contains(elem)) elem.deactivate() + } + } + } +} + +class AnytimeProbEvidenceMarginalMAP(universe: Universe, + tolerance: Double, + samplesPerIteration: Int, + maxRuns: Int, + proposalScheme: ProposalScheme, + schedule: Schedule, + mapElements: List[Element[_]]) + extends ProbEvidenceMarginalMAP(universe, tolerance, samplesPerIteration, maxRuns, proposalScheme, schedule, mapElements) + with AnytimeSampler with AnytimeMarginalMAP { + /** + * Initialize the algorithm. + */ + override def initialize(): Unit = { + super.initialize() + doInitialize() + } +} + +class OneTimeProbEvidenceMarginalMAP(val numSamples: Int, + universe: Universe, + tolerance: Double, + samplesPerIteration: Int, + maxRuns: Int, + proposalScheme: ProposalScheme, + schedule: Schedule, + mapElements: List[Element[_]]) + extends ProbEvidenceMarginalMAP(universe, tolerance, samplesPerIteration, maxRuns, proposalScheme, schedule, mapElements) + with OneTimeSampler with OneTimeMarginalMAP { + + /** + * Run the algorithm, performing its computation to completion. + */ + override def run(): Unit = { + doInitialize() + super.run() + } +} + +object ProbEvidenceMarginalMAP { + /** + * Creates a one time marginal MAP algorithm that uses probability of evidence. + * @see [[com.cra.figaro.experimental.marginalmap.ProbEvidenceMarginalMAP]] abstract class for a complete description + * of the parameters. + */ + def apply(iterations: Int, tolerance: Double, samplesPerIteration: Int, maxRuns: Int, proposalScheme: ProposalScheme, + schedule: Schedule, mapElements: Element[_]*)(implicit universe: Universe) = + new OneTimeProbEvidenceMarginalMAP(iterations, universe, tolerance, samplesPerIteration, maxRuns, proposalScheme, schedule, mapElements.toList) + + /** + * Creates an anytime marginal MAP algorithm that uses probability of evidence. + * @see [[com.cra.figaro.experimental.marginalmap.ProbEvidenceMarginalMAP]] abstract class for a complete description + * of the parameters. + */ + def apply(tolerance: Double, samplesPerIteration: Int, maxRuns: Int, proposalScheme: ProposalScheme, + schedule: Schedule, mapElements: Element[_]*)(implicit universe: Universe) = + new AnytimeProbEvidenceMarginalMAP(universe, tolerance, samplesPerIteration, maxRuns, proposalScheme, schedule, mapElements.toList) + + /** + * Creates a one time marginal MAP algorithm that uses probability of evidence. Takes 100 samples per iteration at a + * tolerance of 0.05, up to a maximum of 100 runs. Uses the default proposal scheme and schedule. + * @see [[com.cra.figaro.experimental.marginalmap.ProbEvidenceMarginalMAP]] abstract class for a complete description + * of the parameters. + */ + def apply(iterations: Int, mapElements: Element[_]*)(implicit universe: Universe) = + new OneTimeProbEvidenceMarginalMAP(iterations, universe, 0.05, 100, 100, ProposalScheme.default(universe), Schedule.default(), mapElements.toList) + + /** + * Creates an anytime marginal MAP algorithm that uses probability of evidence. Takes 100 samples per iteration at a + * tolerance of 0.05, up to a maximum of 100 runs. Uses the default proposal scheme and schedule. + * @see [[com.cra.figaro.experimental.marginalmap.ProbEvidenceMarginalMAP]] abstract class for a complete description + * of the parameters. + */ + def apply(mapElements: Element[_]*)(implicit universe: Universe) = + new AnytimeProbEvidenceMarginalMAP(universe, 0.05, 100, 100, ProposalScheme.default(universe), Schedule.default(), mapElements.toList) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPAlgorithm.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPAlgorithm.scala new file mode 100644 index 00000000..df6afb5b --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPAlgorithm.scala @@ -0,0 +1,51 @@ +/* + * StructuredMarginalMAPAlgorithm.scala + * Abstract class for structured marginal MAP algorithms. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 3, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.{Algorithm, AlgorithmException} +import com.cra.figaro.algorithm.factored.factors.Factor +import com.cra.figaro.algorithm.structured.{ComponentCollection, Problem} +import com.cra.figaro.language._ + +/** + * A structured marginal MAP algorithm. + * @param universe Universe on which to perform inference. + * @param mapElements Elements for which to compute MAP queries. Elements not in this list are summed over. + */ +abstract class StructuredMarginalMAPAlgorithm(val universe: Universe, val mapElements: List[Element[_]]) + extends Algorithm with OneTimeMarginalMAP { + + def run(): Unit + + val cc: ComponentCollection = new ComponentCollection + + // Targets are our MAP elements, since the first step is to eliminate the other elements + val problem = new Problem(cc, mapElements) + + // We have to add all active elements to the problem since these elements, if they are every used, need to have components created at the top level problem + universe.permanentElements.foreach(problem.add(_)) + val evidenceElems = universe.conditionedElements ::: universe.constrainedElements + + def initialComponents() = (problem.targets ++ evidenceElems).distinct.map(cc(_)) + + def computeMostLikelyValue[T](target: Element[T]): T = { + val targetVar = cc(target).variable + val factor = problem.recordingFactors(targetVar).asInstanceOf[Factor[T]] + if (factor.size != 1) throw new AlgorithmException//("Final factor for most likely value has more than one entry") + factor.get(List()) + } + + +} + + diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPVE.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPVE.scala new file mode 100644 index 00000000..4c778baa --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/marginalmap/StructuredMarginalMAPVE.scala @@ -0,0 +1,56 @@ +/* + * StructuredMarginalMAPVE.scala + * A structured variable elimination algorithm to compute marginal MAP queries. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Jun 3, 2016 + * + * Copyright 2015 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.marginalmap + +import com.cra.figaro.algorithm.structured._ +import com.cra.figaro.algorithm.structured.strategy.decompose._ +import com.cra.figaro.language._ + +/** + * A structured marginal MAP algorithm that uses VE to compute MAP queries. + * @param universe Universe on which to perform inference. + * @param mapElements Elements for which to compute MAP queries. Elements not in this list are summed over. + */ +class StructuredMarginalMAPVE(universe: Universe, mapElements: List[Element[_]]) + extends StructuredMarginalMAPAlgorithm(universe, mapElements) { + + def run() { + val strategy = DecompositionStrategy.recursiveStructuredStrategy(problem, new MarginalMAPVEStrategy, defaultRangeSizer, Lower, false) + strategy.execute(initialComponents) + } +} + +object StructuredMarginalMAPVE { + /** + * Create a structured variable elimination algorithm with the given query targets. + * @param mapElements Elements for which to compute MAP queries. Elements not in this list are summed over, + * and cannot be queried. + */ + def apply(mapElements: Element[_]*)(implicit universe: Universe) = { + new StructuredMarginalMAPVE(universe, mapElements.toList) + } + + /** + * Use variable elimination to compute the most likely value of the given element. + * @param target Element for which to compute MAP value. + * @param mapElements Additional elements to MAP. Elements not in this list are summed over. + */ + def mostLikelyValue[T](target: Element[T], mapElements: Element[_]*): T = { + val alg = StructuredMarginalMAPVE((target +: mapElements).distinct:_*) + alg.start() + val result = alg.mostLikelyValue(target) + alg.kill() + result + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Beta.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Beta.scala new file mode 100644 index 00000000..757246dc --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Beta.scala @@ -0,0 +1,127 @@ +/* + * Beta.scala + * Elements representing Beta distributions. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Feb 2, 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.experimental.normalproposals + +import com.cra.figaro.language._ + +import math.{ pow, sqrt } +import JSci.maths.SpecialMath.beta +import com.cra.figaro.algorithm.ValuesMaker +import com.cra.figaro.algorithm.lazyfactored.ValueSet +import com.cra.figaro.library.atomic.continuous.Util + +/** + * A Beta distribution in which the alpha and beta parameters are provided. + * This Beta element can be used as the parameter for a ParameterizedFlip. + * + * @param a The prior alpha parameter + * @param b The prior beta parameter + */ +class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCollection) + extends Element[Double](name, collection) with HasDensity[Double] with NormalProposer with DoubleParameter with com.cra.figaro.library.atomic.continuous.Beta { + // Bounds for normal proposals + override def lower = 0.0 + override def upper = 1.0 + // Proposal scale is 20% of the standard deviation + def proposalScale = 0.2 * sqrt(a * b / (a + b + 1)) / (a + b) + + override def nextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + // If a or b is greater than 1, the distribution is unimodal, so normal proposals are appropriate + if(a >= 1 || b >= 1) super[NormalProposer].nextRandomness(oldRandomness) + // If both a and b are less than 1, the distribution is bimodal, so we're better off proposing from the prior + else super[HasDensity].nextRandomness(oldRandomness) + } + + /** + * The learned alpha parameter + */ + var learnedAlpha = a + /** + * The learned beta parameter + */ + var learnedBeta = b + def aValue = learnedAlpha + def bValue = learnedBeta + def generateRandomness() = Util.generateBeta(a, b) + + /** + * The normalizing factor. + */ + val normalizer = 1 / beta(a, b) + + /** + * Density of a value. + */ + def density(x: Double) = pow(x, a - 1) * pow(1 - x, b - 1) * normalizer + + /** + * Returns an empty sufficient statistics vector. + */ + override def zeroSufficientStatistics(): Seq[Double] = { + Seq(0.0, 0.0) + } + + /** + * Returns an element that models the learned distribution. + * + * @deprecated + */ + def getLearnedElement: AtomicFlip = { + new AtomicFlip("", MAPValue, collection) + } + + override def sufficientStatistics[Boolean](b: Boolean): Seq[Double] = { + if (b == true) { + Seq(1.0, 0.0) + } else { + Seq(0.0, 1.0) + } + } + + private[figaro] override def sufficientStatistics[Boolean](i: Int): Seq[Double] = { + if (i == 0) { + Seq(1.0, 0.0) + } else { + Seq(0.0, 1.0) + } + } + + def expectedValue: Double = { + (learnedAlpha) / (learnedAlpha + learnedBeta) + } + + def MAPValue: Double = { + if (learnedAlpha + learnedBeta == 2) 0.5 + else (learnedAlpha - 1) / (learnedAlpha + learnedBeta - 2) + } + + def makeValues(depth: Int) = ValueSet.withoutStar(Set(MAPValue)) + + def maximize(sufficientStatistics: Seq[Double]) { + require(sufficientStatistics.size == 2) + learnedAlpha = sufficientStatistics(0) + a + learnedBeta = sufficientStatistics(1) + b + + } + + override def toString = "Beta(" + a + ", " + b + ")" +} + +object Beta { + /** + * Create a Beta distribution in which the parameters are constants. + */ + def apply(a: Double, b: Double)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicBeta(name, a, b, collection) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Exponential.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Exponential.scala new file mode 100644 index 00000000..c0ab49b9 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Exponential.scala @@ -0,0 +1,47 @@ +/* + * Exponential.scala + * Elements representing exponential 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.experimental.normalproposals + +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous.Util + +import scala.math.{exp, log} + +/** + * An exponential distribution in which the parameter is a constant. + */ +class AtomicExponential(name: Name[Double], val lambda: Double, collection: ElementCollection) + extends Element[Double](name, collection) with NormalProposer { + // Lower bound for normal proposals + override def lower = 0.0 + // Proposal scale is 20% of the standard deviation + def proposalScale = 0.2 * lambda + + def generateRandomness() = Util.generateExponential(lambda) + + /** + * Density of a value. + */ + def density(d: Double) = if (d < 0.0) 0.0 else lambda * exp(-lambda * d) + + override def toString = "Exponential(" + lambda + ")" +} + +object Exponential { + /** + * Create an exponential distribution in which the parameter is a constant. + */ + def apply(lambda: Double)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicExponential(name, lambda, collection) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Gamma.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Gamma.scala new file mode 100644 index 00000000..345c86b1 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Gamma.scala @@ -0,0 +1,74 @@ +/* + * Gamma.scala + * Elements representing Gamma elements. + * + * 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.experimental.normalproposals + +import JSci.maths.SpecialMath.gamma +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous.Util + +import scala.math._ + +/** + * A Gamma distribution in which both the k and theta parameters are constants. + * Theta defaults to 1. + */ +class AtomicGamma(name: Name[Double], k: Double, theta: Double = 1.0, collection: ElementCollection) + extends Element[Double](name, collection) with HasDensity[Double] with NormalProposer { + // Lower bound for normal proposals + override def lower = 0.0 + // Proposal scale is 20% of the standard deviation of the underlying Gamma randomness + def proposalScale = 0.2 * sqrt(k) + + def generateRandomness() = Util.generateGamma(k) + + override def generateValue(rand: Randomness) = + rand * theta // due to scaling property of Gamma + + override def generateValueDerivative(rand: Randomness) = theta + + override def nextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + // If k is large, then the density is spread out enough for normal proposals to work reasonably well. + if(k >= 1) super[NormalProposer].nextRandomness(oldRandomness) + // If k is small, too much density is concentrated extremely close to 0 for normal proposals to be effective. + // For example, if k=0.1, the median is ~0.00059. + else super[HasDensity].nextRandomness(oldRandomness) + } + + /** + * The normalizing factor. + */ + private val normalizer = 1.0 / (gamma(k) * pow(theta, k)) + + /** + * Density of a value. + */ + def density(x: Double) = { + if (x < 0.0) 0.0 else { + val numer = pow(x, k - 1) * exp(-x / theta) + numer * normalizer + } + } + + override def toString = + if (theta == 1.0) "Gamma(" + k + ")" + else "Gamma(" + k + ", " + theta + ")" +} + +object Gamma { + /** + * Create a Gamma element in which both k and theta parameters are constants. Theta defaults to 1. + */ + def apply(k: Double, theta: Double = 1.0)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicGamma(name, k, theta, collection) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/InverseGamma.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/InverseGamma.scala new file mode 100644 index 00000000..94906d85 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/InverseGamma.scala @@ -0,0 +1,75 @@ +/* + * 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.experimental.normalproposals + +import JSci.maths.SpecialMath.gamma +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous.Util + +import scala.math._ + +/** + * 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 HasDensity[Double] with NormalProposer { + // Lower bound for normal proposals + override def lower = 0.0 + // Proposal scale is 20% of the standard deviation of the underlying Gamma randomness + def proposalScale = 0.2 * sqrt(shape) + + def generateRandomness() = Util.generateGamma(shape) + + override def generateValue(rand: Randomness) = 1.0 / (rand * scale) // due to scaling property of Gamma + + override def generateValueDerivative(rand: Randomness) = 1.0 / (rand * rand * scale) + + override def nextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + // If k is large, then the density is spread out enough for normal proposals to work reasonably well. + if(shape >= 1) super[NormalProposer].nextRandomness(oldRandomness) + // If k is small, too much density is concentrated extremely close to 0 for normal proposals to be effective. + // For example, if k=0.1, the median is ~0.00059. + else super[HasDensity].nextRandomness(oldRandomness) + } + + /** + * 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/experimental/normalproposals/Normal.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Normal.scala new file mode 100644 index 00000000..17112c7e --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Normal.scala @@ -0,0 +1,83 @@ +/* + * Normal.scala + * Elements representing normal distributions + * + * 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.experimental.normalproposals + +import JSci.maths.SpecialMath.error +import com.cra.figaro.language._ +import com.cra.figaro.util.random + +import scala.math._ + +/** + * A normal distribution in which the mean and variance are constants. + */ +class AtomicNormal(name: Name[Double], val mean: Double, val variance: Double, collection: ElementCollection) + extends Element[Double](name, collection) with NormalProposer { + lazy val standardDeviation = sqrt(variance) + + // Proposal scale is 20% of the standard deviation of the underlying standard Normal randomness + def proposalScale = 0.2 + + def generateRandomness() = { + val u1 = random.nextDouble() + val u2 = random.nextDouble() + val w = sqrt(-2.0 * log(u1)) + val x = 2.0 * Pi * u2 + w * sin(x) + } + + override def generateValue(rand: Randomness) = rand * standardDeviation + mean + + override def generateValueDerivative(rand: Randomness) = standardDeviation + + /** + * The normalizing factor. + */ + private val normalizer = 1.0 / sqrt(2.0 * Pi * variance) + + /** + * Density of a value. + */ + def density(d: Double) = Normal.density(mean, variance, normalizer)(d) + + override def toString = "Normal(" + mean + ", " + variance + ")" +} + +object Normal { + + def probability(mean: Double, stDev: Double)(lower: Double, upper: Double) = { + val denominator = stDev * sqrt(2.0) + val erfLower = (lower - mean) / denominator + val erfUpper = (upper - mean) / denominator + 0.5 * (error(erfUpper) - error(erfLower)) + } + + def density(mean: Double, stDev: Double)(d: Double) = { + val diff = d - mean + val exponent = -(diff * diff) / (2.0 * stDev * stDev) + exp(exponent) / (sqrt(2.0 * Pi) * stDev) + } + + def density(mean: Double, variance: Double, normalizer: Double)(d: Double) = { + val diff = d - mean + val exponent = -(diff * diff) / (2.0 * variance) + normalizer * exp(exponent) + } + + /** + * Create a normal distribution in which the mean and variance are constants. + */ + def apply(mean: Double, variance: Double)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicNormal(name, mean, variance, collection) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/NormalProposer.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/NormalProposer.scala new file mode 100644 index 00000000..210a17dd --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/NormalProposer.scala @@ -0,0 +1,134 @@ +/* + * NormalProposer.scala + * Normally distributed proposals. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 17, 2016 + * + * Copyright 2016 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.normalproposals + +import com.cra.figaro.language._ +import com.cra.figaro.util._ +import org.apache.commons.math3.distribution.NormalDistribution + +import scala.annotation.tailrec + +/** + * Normally distributed proposals for univariate continuous elements. This works by proposing from a truncated normal + * distribution over the randomness of this element. This implementation assumes that the probability density of values + * associated with randomnesses in the range (`lower`, `upper`) are finite. + */ +trait NormalProposer extends Atomic[Double] { + type Randomness = Double + + /** + * Exclusive lower bound of the range of the randomness of this element. Defaults to -Infinity. Must be strictly less + * than the upper bound. + */ + def lower: Double = Double.NegativeInfinity + /** + * Exclusive upper bound of the range of the randomness of this element. Defaults to Infinity. Must be strictly + * greater than the lower bound. + */ + def upper: Double = Double.PositiveInfinity + + /** + * The scale of the normally distributed proposal. This corresponds to the standard deviation of the proposal before + * truncation. If the randomness has finite variance, this should be less than or equal to its standard deviation. + * A good place to start is e.g. 20% of the standard deviation. + */ + def proposalScale: Double + + /** + * A strictly monotone differentiable function defined on (`lower`, `upper`). Defaults to the identity function. + */ + def generateValue(rand: Randomness) = rand + + /** + * The absolute value of the derivative of `generateValue` with respect to the randomness given. This is needed to + * compute a proposal density over the transformed randomness. Defaults to 1.0, corresponding to the case where + * `generateValue` is the identity function. + */ + def generateValueDerivative(rand: Randomness): Double = 1.0 + + /** + * Generate the next randomness given the current randomness. + * Returns three values: The next randomness, the Metropolis-Hastings proposal probability + * ratio, which is: + * + * P(new -> old) / P(old -> new) + * + * and the model probability ratio, which is: + * + * P(new) / P(old) + * + * By default, this implementation proposes a normally distributed randomness from the previous randomness, truncated + * to be within the appropriate range. The probability ratios returned are associated with the values of this element + * rather than the randomness. This is for the purpose of simulated annealing, since the most likely randomness is not + * necessarily the most likely value, depending on the form of the generateValue function. + * + * One can override this to only use normal proposals in certain special cases. + */ + override def nextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + normalNextRandomness(oldRandomness) + } + + /** + * Computes the normal proposal for nextRandomness. This is separated from the nextRandomness so that subclasses can + * choose when to use normal proposals. + */ + @tailrec + protected final def normalNextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + /* + * Sample from a normal distribution centered at oldRandomness, and reject if it's outside the range. + * Rejection sampling to be in the range is justified here because of the assumptions on the scale. + * If both lower and upper bounds are finite, then the scale is less than the maximum possible standard deviation of + * (upper - lower) / 2. This yields a minimum acceptance probability of ~47.72% (cumulative density of a standard + * normal distribution from 0 to 2) when oldRandomness is one of the bounds. + * If only one of the bounds is finite, then we reject less than 50% of the time, since no more than half of the + * distribution is truncated by the bounds. If the distribution is unbounded, we never reject. + * So, the expected number of trials is never more than 1 / 0.4772 = 2.095, which is quite reasonable. + */ + val newRandomness = oldRandomness + random.nextGaussian() * proposalScale + if(lower < newRandomness && newRandomness < upper) { + val proposalRatio = proposalProb(newRandomness, oldRandomness) / proposalProb(oldRandomness, newRandomness) + val modelRatio = density(generateValue(newRandomness)) / density(generateValue(oldRandomness)) + (newRandomness, proposalRatio, modelRatio) + } + else normalNextRandomness(oldRandomness) + } + + /** + * Computes the proposal probability density in one direction. Both values should be in the interval (lower, upper). + * @param oldRandomness The previous randomness. + * @param newRandomness The newly proposed randomness. + * @return The probability density associated with the transition from `generateValue(oldRandomness)` to + * `generateValue(newRandomness)`. This is a density over the corresponding values (as opposed to randomnesses). + */ + protected def proposalProb(oldRandomness: Double, newRandomness: Double): Double = { + val stDev = proposalScale + + // The probability density of proposing the new randomness given the old randomness is the density of the new + // randomness from the normal distribution, divided by the normalizing constant of the cumulative probability + // between the upper and lower bounds. This cumulative probability corresponds to the probability of not rejecting + // when we sample from the regular normal proposal. This ensures that the PDF of the truncated distribution + // integrates to 1. + val uncorrected = + Normal.density(oldRandomness, stDev)(newRandomness) / Normal.probability(oldRandomness, stDev)(lower, upper) + + // Correct for the scaling factor associated with newRandomness. This is needed because even though the proposal + // distribution over the randomness of this element is a truncated normal distribution, the resulting proposal + // distribution over the actual values of this element may be different. Consider, for example, how an inverse Gamma + // element might use a Gamma distribution as its randomness, then invert the randomness to produce a value. Then, a + // truncated normal distribution over the randomness would have some other distribution over the values. Since we + // have the restriction that the deterministic transformation function is strictly monotone and differentiable, we + // can account for this difference; this is a standard "change of variable" computation. + uncorrected / generateValueDerivative(newRandomness) + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Uniform.scala b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Uniform.scala new file mode 100644 index 00000000..558fe39c --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/experimental/normalproposals/Uniform.scala @@ -0,0 +1,47 @@ +/* + * Uniform.scala + * Elements representing continuous uniform distributions. + * + * Created By: Avi Pfeffer (apfeffer@cra.com) + * Creation Date: Oct 18, 2010 + * + * 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.experimental.normalproposals + +import com.cra.figaro.language._ +import com.cra.figaro.util.{bound, random} + +import scala.math.log + +/** + * A continuous uniform distribution in which the parameters are constants. + */ +class AtomicUniform(name: Name[Double], override val lower: Double, override val upper: Double, collection: ElementCollection) + extends Element[Double](name, collection) with NormalProposer { + + private lazy val diff = upper - lower + + // Proposal scale is 20% of the standard deviation + def proposalScale = 0.2 * diff / math.sqrt(12) + + def generateRandomness() = random.nextDouble() * diff + lower + + private lazy val constantDensity = 1.0 / diff + + def density(d: Double) = if (d >= lower && d < upper) constantDensity; else 0.0 + + override def toString = "Uniform(" + lower + ", " + upper + ")" +} + +object Uniform { + /** + * Create a continuous uniform distribution in which the parameters are constants. + */ + def apply(lower: Double, upper: Double)(implicit name: Name[Double], collection: ElementCollection) = + new AtomicUniform(name, lower, upper, collection) +} 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 ed5edd6d..004adae1 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 @@ -261,7 +261,7 @@ 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, + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultNumSamplesFromAtomics, ParticleGenerator.defaultMaxNumSamplesAtChain, universe, targets: _*)(List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with OneTimeParticleBeliefPropagation with OneTimeProbQuery { val outerIterations = myOuterIterations @@ -273,7 +273,7 @@ object ParticleBeliefPropagation { */ def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, myOuterIterations: Int, myInnerIterations: Int, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultNumSamplesFromAtomics, ParticleGenerator.defaultMaxNumSamplesAtChain, universe, targets: _*)(dependentUniverses, dependentAlgorithm) with OneTimeParticleBeliefPropagation with OneTimeProbQuery { val outerIterations = myOuterIterations val innerIterations = myInnerIterations @@ -306,7 +306,7 @@ object ParticleBeliefPropagation { * Creates a Anytime belief propagation computer in the current default universe. */ def apply(stepTimeMillis: Long, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultNumSamplesFromAtomics, ParticleGenerator.defaultMaxNumSamplesAtChain, universe, targets: _*)(List(), (u: Universe, e: List[NamedEvidence[_]]) => () => ProbEvidenceSampler.computeProbEvidence(10000, e)(u)) with AnytimeParticleBeliefPropagation with AnytimeProbQuery { val myStepTimeMillis = stepTimeMillis @@ -317,7 +317,7 @@ object ParticleBeliefPropagation { */ def apply(dependentUniverses: List[(Universe, List[NamedEvidence[_]])], dependentAlgorithm: (Universe, List[NamedEvidence[_]]) => () => Double, stepTimeMillis: Long, targets: Element[_]*)(implicit universe: Universe) = - new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultArgSamples, ParticleGenerator.defaultTotalSamples, + new ProbQueryParticleBeliefPropagation(ParticleGenerator.defaultNumSamplesFromAtomics, ParticleGenerator.defaultMaxNumSamplesAtChain, universe, targets: _*)(dependentUniverses, dependentAlgorithm) with AnytimeParticleBeliefPropagation with AnytimeProbQuery { val myStepTimeMillis = stepTimeMillis } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/ApplyC.scala b/Figaro/src/main/scala/com/cra/figaro/language/ApplyC.scala new file mode 100644 index 00000000..05e6095e --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/language/ApplyC.scala @@ -0,0 +1,41 @@ +package com.cra.figaro.language + +/** + * This class is a workaround for adding easier type inference to Apply class + * Since Apply object is already using apply method with different number of arguments + * it is not easy to find a workaround adding currying support to apply methods in Apply class + */ +object ApplyC { + /** + * Application of a function to one argument. + */ + def apply[T1, U](arg1: Element[T1])(fn: T1 => U)(implicit name: Name[U], collection: ElementCollection) = + new Apply1(name, arg1, fn, collection) + + /** + * Application of a function to two arguments. + */ + def apply[T1, T2, U](arg1: Element[T1], arg2: Element[T2])(fn: (T1, T2) => U)(implicit name: Name[U], collection: ElementCollection) = + new Apply2(name, arg1, arg2, fn, collection) + + /** + * Application of a function to three arguments. + */ + def apply[T1, T2, T3, U](arg1: Element[T1], arg2: Element[T2], arg3: Element[T3])(fn: (T1, T2, T3) => U)(implicit name: Name[U], collection: ElementCollection) = + new Apply3(name, arg1, arg2, arg3, fn, collection) + + /** + * Application of a function to four arguments. + */ + def apply[T1, T2, T3, T4, U](arg1: Element[T1], arg2: Element[T2], arg3: Element[T3], arg4: Element[T4]) + (fn: (T1, T2, T3, T4) => U)(implicit name: Name[U], collection: ElementCollection) = + new Apply4(name, arg1, arg2, arg3, arg4, fn, collection) + + /** + * Application of a function to five arguments. + */ + def apply[T1, T2, T3, T4, T5, U](arg1: Element[T1], arg2: Element[T2], arg3: Element[T3], arg4: Element[T4], + arg5: Element[T5]) + (fn: (T1, T2, T3, T4, T5) => U)(implicit name: Name[U], collection: ElementCollection) = + new Apply5(name, arg1, arg2, arg3, arg4, arg5, fn, collection) +} 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 e02c7a34..1eaef441 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Element.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Element.scala @@ -17,6 +17,8 @@ import com.cra.figaro.library.compound._ import scala.collection.mutable.Set import scala.language.implicitConversions +class EvidenceNotAllowedException(element: Element[_]) extends RuntimeException("Evidence not allowed on " + element.toString + " because it is a temporary element") + /** * An Element is the core component of a probabilistic model. Elements can be understood as * defining a probabilistic process. Elements are parameterized by the type of Value the process @@ -222,7 +224,9 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) universe.registerUses(this, elem) } - private var myConditions: List[(Condition, Contingency)] = List() + private def checkIfEvidenceAllowed = if (isTemporary) throw new EvidenceNotAllowedException(this) + + private[language] var myConditions: List[(Condition, Contingency)] = List() /** All the conditions defined on this element.*/ def allConditions = myConditions @@ -237,7 +241,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * 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 + private[figaro] var observation: Option[Value] = 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. @@ -268,6 +272,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) /** Add the given condition to the existing conditions of the element. By default, the contingency is empty. */ def addCondition(condition: Condition, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed universe.makeConditioned(this) contingency.foreach(ev => ensureContingency(ev.elem)) observation = None @@ -287,11 +292,12 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * Set the condition associated with the contingency. Removes previous conditions associated with the contingency. By default, the contingency is empty. */ def setCondition(newCondition: Condition, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed removeConditions(contingency) addCondition(newCondition, contingency) } - private var myConstraints: List[(Constraint, Contingency)] = List() + private[language] var myConstraints: List[(Constraint, Contingency)] = List() /** * The current soft constraints on the element. @@ -349,6 +355,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * Add a contingent constraint to the element. By default, the contingency is empty. */ def addConstraint(constraint: Constraint, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed universe.makeConstrained(this) contingency.foreach(ev => ensureContingency(ev.elem)) myConstraints ::= (ProbConstraintType(constraint), contingency) @@ -358,6 +365,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * Add a log contingent constraint to the element. By default, the contingency is empty. */ def addLogConstraint(constraint: Constraint, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed universe.makeConstrained(this) contingency.foreach(ev => ensureContingency(ev.elem)) myConstraints ::= (LogConstraintType(constraint), contingency) @@ -381,6 +389,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * Set the constraint associated with the contingency. Removes previous constraints associated with the contingency. By default, the contingency is empty. */ def setConstraint(newConstraint: Constraint, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed removeConstraints(contingency) addConstraint(newConstraint, contingency) } @@ -389,6 +398,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * Set the log constraint associated with the contingency. Removes previous constraints associated with the contingency. By default, the contingency is empty. */ def setLogConstraint(newConstraint: Constraint, contingency: Contingency = List()): Unit = { + checkIfEvidenceAllowed removeConstraints(contingency) addLogConstraint(newConstraint, contingency) } @@ -397,6 +407,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) * 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. */ def observe(observation: Value): Unit = { + checkIfEvidenceAllowed removeConditions() set(observation) universe.makeConditioned(this) @@ -412,7 +423,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) removeConditions() } - private var setFlag: Boolean = false + private[language] var setFlag: Boolean = false /** * Allows different values of the element to be generated. @@ -448,7 +459,7 @@ abstract class Element[T](val name: Name[T], val collection: ElementCollection) set(generateValue(randomness)) } - private var myPragmas: List[Pragma[Value]] = List() + private[language] var myPragmas: List[Pragma[Value]] = List() /** * The pragmas attached to the element. 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 be03d619..26d99d7e 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala @@ -19,4 +19,35 @@ package com.cra.figaro.language trait HasDensity[T] extends Element[T] { /** The probability density of a value. */ def density(t: T): Double + + /** + * Generate the next randomness given the current randomness. + * Returns three values: The next randomness, the Metropolis-Hastings proposal probability + * ratio, which is: + * + * P(new -> old) / P(old -> new) + * + * and the model probability ratio, which is: + * + * P(new) / P(old) + * + * This implementation produces a sample using generateRandomness, which means that: + * + * P(new -> old) / P(old -> new) = P(old) / P(new) + * + * We use the fact that this element can compute densities for values to compute P(new) and + * P(old) explicitly. Note that the two returned ratios will still multiply to 1. This does + * not affect normal Metropolis-Hastings, but helps the Metropolis-Hastings annealer find + * maxima. + */ + override def nextRandomness(oldRandomness: Randomness): (Randomness, Double, Double) = { + val newRandomness = generateRandomness() + val pOld = density(generateValue(oldRandomness)) + val pNew = density(generateValue(newRandomness)) + // Note that these density ratios could overflow/underflow, particularly if there is nonzero probability of + // generating a value that has infinite density. For example, this may occur when generating from a Gamma + // distribution with sufficiently small shape parameter such that the value 0 may be produced, which has infinite + // density. We don't account for this explicitly because MH is unlikely to work well for these models to begin with. + (newRandomness, pOld / pNew, pNew / pOld) + } } 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 926016fc..d3cda666 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Universe.scala @@ -36,7 +36,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol */ override val universe = this - private val myActiveElements: Set[Element[_]] = Set() + private[language] val myActiveElements: Set[Element[_]] = Set() /** * The active elements in the universe. @@ -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[language] val myConditionedElements: Set[Element[_]] = Set() /** Elements in the universe that have had a condition applied to them. */ def conditionedElements: List[Element[_]] = myConditionedElements.toList @@ -55,7 +55,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol private[language] def makeUnconditioned(elem: Element[_]) { myConditionedElements -= elem } - private val myConstrainedElements: Set[Element[_]] = Set() + private[language] val myConstrainedElements: Set[Element[_]] = Set() /** Elements in the universe that have had a constraint applied to them. */ def constrainedElements: List[Element[_]] = myConstrainedElements.toList @@ -64,7 +64,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol private[language] def makeUnconstrained(elem: Element[_]) { myConstrainedElements -= elem } - private val myStochasticElements = new HashSelectableSet[Element[_]] + private[language] val myStochasticElements = new HashSelectableSet[Element[_]] /** * The active non-deterministic elements in the universe. @@ -89,7 +89,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol * any Chain. Conversely, if it is not empty, it must have been created within a chain, so it is temporary. It is * possible to remove all temporary Elements from a Map. */ - private var myContextStack: List[Element[_]] = List() + private[language] var myContextStack: List[Element[_]] = List() private[figaro] def contextStack = myContextStack @@ -118,12 +118,12 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol myContextStack = myContextStack dropWhile (_ != element) drop 1 } - private val myUses: Map[Element[_], Set[Element[_]]] = Map() + private[language] val myUses: Map[Element[_], Set[Element[_]]] = Map() - private val myUsedBy: Map[Element[_], Set[Element[_]]] = Map() + private[language] val myUsedBy: Map[Element[_], Set[Element[_]]] = Map() - private val myRecursiveUsedBy: Map[Element[_], Set[Element[_]]] = Map() - private val myRecursiveUses: Map[Element[_], Set[Element[_]]] = Map() + private[language] val myRecursiveUsedBy: Map[Element[_], Set[Element[_]]] = Map() + private[language] val myRecursiveUses: Map[Element[_], Set[Element[_]]] = Map() /** * Returns the set of elements that the given element uses in its generation, either directly or recursively. @@ -143,7 +143,7 @@ class Universe(val parentElements: List[Element[_]] = List()) extends ElementCol } /** - * Returns the set of elements that are directly used by the given element, without recursing. + * Returns the set of elements that use the given element in their generation, without recursing. */ def directlyUsedBy(elem: Element[_]): Set[Element[_]] = myUsedBy.getOrElse(elem, Set()) diff --git a/Figaro/src/main/scala/com/cra/figaro/language/UniverseState.scala b/Figaro/src/main/scala/com/cra/figaro/language/UniverseState.scala new file mode 100644 index 00000000..67ffd078 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/language/UniverseState.scala @@ -0,0 +1,166 @@ +/* + * UniverseState.scala + * Saving and restoring universe state. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 3, 2016 + * + * Copyright 2016 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.collection.mutable + +/** + * Saves the mutable state of a universe. This is useful for algorithms that need to maintain a state over elements, but + * involve a sub-procedure that mutates the state of the universe. This class provides the functionality for restoring + * the previous state. Immutable. + * + * @param universe Universe to save. Information about the current state of this universe is copied in the constructor. + */ +class UniverseState(universe: Universe) { + // Immutable types + val myContextStack = universe.myContextStack + + // Mutable types, stored as immutable types + val myActiveElements = universe.myActiveElements.toSet + val myStochasticElements = universe.myStochasticElements.toSet + + val myConditionedElements = universe.myConditionedElements.toSet + val myConstrainedElements = universe.myConstrainedElements.toSet + + val myUses = makeImmutable(universe.myUses) + val myUsedBy = makeImmutable(universe.myUsedBy) + val myRecursiveUses = makeImmutable(universe.myRecursiveUses) + val myRecursiveUsedBy = makeImmutable(universe.myRecursiveUsedBy) + + // States over elements + val elementStates: Map[Element[_], ElementState] = myActiveElements.map(e => (e, new ElementState(e))).toMap + + /** + * Restores the universe to its state at the time of construction of this class. In general, this means that any calls + * to the public API in `Universe` and `Element` will behave as if they were called when this class was instantiated, + * regardless of what calls to the API were made since instantiation. + * + * There are subtle exceptions to this rule. In particular, registered universe maps, element maps, and universe maps + * will not be changed. Additionally, `myElementMap` from `ElementCollection` will not be changed. + * + * Note that classes that extend functionality of `Element` and `Universe` with additional mutability may have + * undefined behavior with respect to this mutable information. For example, parameters that store learned values will + * not have their learned values copied. + * + * Since this class is immutable, this method can be called multiple times to repeatedly restore the same state after + * subsequent changes to the universe are made. + */ + def restore(): Unit = { + // Deactivate any newly created elements + for(newElement <- universe.myActiveElements -- myActiveElements) newElement.deactivate() + + // For immutable types, we can just replace the references + universe.myContextStack = myContextStack + + // For mutable types, we get the reference to the set and replace its contents + replace(universe.myActiveElements, myActiveElements) + replace(universe.myStochasticElements, myStochasticElements) + replace(universe.myConditionedElements, myConditionedElements) + replace(universe.myConstrainedElements, myConstrainedElements) + + replace(universe.myUses, myUses) + replace(universe.myUsedBy, myUsedBy) + replace(universe.myRecursiveUses, myRecursiveUses) + replace(universe.myRecursiveUsedBy, myRecursiveUsedBy) + + // Element states update on their own + elementStates.values.foreach(_.restore()) + } + + /** + * Replace the contents of a mutable set with the contents of an immutable set. + * @param toReplace Set whose elements should be replaced. + * @param replaceWith Set of elements to replace with. + */ + private def replace[T](toReplace: mutable.Set[T], replaceWith: TraversableOnce[T]): Unit = { + toReplace.clear() + toReplace ++= replaceWith + } + + /** + * Replace the contents of a mutable map to mutable sets with the contents of an immutable map to immutable sets. + * @param toReplace Map whose elements should be replaced. + * @param replaceWith Map of elements to replace with. + */ + private def replace[T](toReplace: mutable.Map[T, mutable.Set[T]], replaceWith: Map[T, Set[T]]): Unit = { + toReplace.clear() + for((key, value) <- replaceWith) { + toReplace(key) = mutable.Set() ++ value + } + } + + private def makeImmutable[T](map: mutable.Map[T, mutable.Set[T]]): Map[T, Set[T]] = { + // map.toMap.mapValues(_.toSet) + // Wait! Calling mapValues returns a VIEW into the original map, which we don't want because the underlying sets are + // mutable. See Scala issues SI-4776. + map.map{ case (key, value) => (key, value.toSet) }.toMap + } +} + +/** + * Saves the mutable state of a single element. This only saves and restores the mutable information found in the + * `Element` class; it does not propagate this information to the universe level. Immutable. + * + * @param element Element to save. Information about the current state of this element is copied in the constructor. + */ +class ElementState(val element: Element[_]) { + // Immutable types + val active = element.active + val setFlag = element.setFlag + + val value: element.Value = element.value + val randomness: element.Randomness = element.randomness + + val myPragmas: List[Pragma[element.Value]] = element.myPragmas + + val myConditions = element.myConditions + val myConstraints = element.myConstraints + val observation: Option[element.Value] = element.observation + + val myContext = element.myContext + + // Mutable types, stored as immutable types + val myDirectContextContents = element.directContextContents.toSet + + /** + * Restores the element to its state at the time of construction of this class. + * + * This only restores mutable information found in the `Element` class; any mutable information added by subclasses + * will be ignored. Furthermore, information about this element is not propagated to the universe level. + * + * Since this class is immutable, this method can be called multiple times to repeatedly restore the same state after + * subsequent changes to the element are made. + */ + def restore(): Unit = { + // For immutable types, we can just replace the references + element.active = active + element.setFlag = setFlag + + element.value = value + element.randomness = randomness + + element.myPragmas = myPragmas + + element.myConditions = myConditions + element.myConstraints = myConstraints + element.observation = observation + + element.myContext = myContext + + // For mutable types, we get the reference to the set and replace its contents + val referenceToMyDirectContextContents = element.directContextContents + referenceToMyDirectContextContents.clear() + referenceToMyDirectContextContents ++= myDirectContextContents + } +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala index 430a1d92..4d804ef9 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala @@ -29,7 +29,7 @@ import argonaut._, Argonaut._ * @param b The prior beta parameter */ class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCollection) - extends Element[Double](name, collection) with Atomic[Double] with DoubleParameter with ValuesMaker[Double] with Beta{ + extends Element[Double](name, collection) with Atomic[Double] with DoubleParameter with Beta{ type Randomness = Double /** @@ -97,6 +97,8 @@ class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCo else (learnedAlpha - 1) / (learnedAlpha + learnedBeta - 2) } + // Values for Beta parameters now handled directly in the algorithms + @deprecated("Values for Beta parameters are now handled directly in the algorithms", "4.1.0") def makeValues(depth: Int) = ValueSet.withoutStar(Set(MAPValue)) def maximize(sufficientStatistics: Seq[Double]) { diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala index 555a2a30..37937b8f 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala @@ -32,7 +32,7 @@ import argonaut.Argonaut._ * @param alphas the prior concentration parameters */ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], collection: ElementCollection) - extends Element[Array[Double]](name, collection) with Atomic[Array[Double]] with ArrayParameter with ValuesMaker[Array[Double]] with Dirichlet{ + extends Element[Array[Double]](name, collection) with Atomic[Array[Double]] with ArrayParameter with Dirichlet { /** * The number of concentration parameters in the Dirichlet distribution. @@ -134,6 +134,8 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll result } + // Values for Beta parameters now handled directly in the algorithms + @deprecated("Values for Beta parameters are now handled directly in the algorithms", "4.1.0") def makeValues(depth: Int) = ValueSet.withoutStar(Set(MAPValue)) override def toString = "Dirichlet(" + alphas.mkString(", ") + ")" @@ -143,12 +145,12 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll * Dirichlet distributions in which the parameters are elements. */ class CompoundDirichlet(name: Name[Array[Double]], alphas: Array[Element[Double]], collection: ElementCollection) - extends NonCachingChain[List[Double], Array[Double]]( - name, - new Inject("", alphas, collection), - (aa: Seq[Double]) => new AtomicDirichlet("", aa.toArray, collection), - collection) - with Dirichlet { + extends NonCachingChain[List[Double], Array[Double]]( + name, + new Inject("", alphas, collection), + (aa: Seq[Double]) => new AtomicDirichlet("", aa.toArray, collection), + collection) + with Dirichlet { def alphaValues = alphas.map(_.value) @@ -184,7 +186,7 @@ object Dirichlet extends Creatable { //Needs to be a nested field or a jEmptyArray implicit def DirichletEncodeJson: EncodeJson[Dirichlet] = EncodeJson((d: Dirichlet) => - ("name" := d.name.string) ->: ("alphaValues" := jArray((for (a <- d.alphaValues) yield { jNumber(a) }).toList)) ->: jEmptyObject) + ("name" := d.name.string) ->: ("alphaValues" := jArray((for (a <- d.alphaValues) yield { jNumber(a) }).toList)) ->: jEmptyObject) implicit def DirichletDecodeJson(implicit collection: ElementCollection): DecodeJson[AtomicDirichlet] = DecodeJson(c => for { @@ -197,17 +199,15 @@ object Dirichlet extends Creatable { */ def apply(alphas: Double*)(implicit name: Name[Array[Double]], collection: ElementCollection) = new AtomicDirichlet(name, alphas.toArray, collection) - + def apply(alphas: Array[Double])(implicit name: Name[Array[Double]], collection: ElementCollection) = new AtomicDirichlet(name, alphas, collection) - + /** * Create a Dirichlet distribution in which the parameters are elements. */ def apply(alphas: Element[Double]*)(implicit name: Name[Array[Double]], collection: ElementCollection) = new CompoundDirichlet(name, alphas.toArray, collection) - - type ResultType = Array[Double] diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/KernelDensity.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/KernelDensity.scala new file mode 100644 index 00000000..abad1283 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/KernelDensity.scala @@ -0,0 +1,63 @@ +/* + * KernelDensity.scala + * Element representing a kernel density estimate + * + * Created By: Dan Garant (dgarant@cra.com) + * Creation Date: May 27, 2016 + * + * Copyright 2016 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._ + +/** + * Performs density estimation using a Gaussian kernel to smooth density estimates around observed inputs. + * @param samples Observed points + * @param bandwidth Parameter of the Gaussian kernel + */ +class KernelDensity(name: Name[Double], val samples: Seq[Double], val bandwidth: Double, collection: ElementCollection) + extends Element[Double](name, collection) with Atomic[Double] { + + // this represents the Gaussian kernel centered at one of the input points + val normalElt = Normal(0, bandwidth) + + // randomness is a random index along with a random element from normalElt + type Randomness = (Int, normalElt.Randomness) + + /** Generates a random sample index and offset */ + def generateRandomness:Randomness = { + val idx = com.cra.figaro.util.random.nextInt(samples.length) + val rand = normalElt.generateRandomness() + (idx, rand) + } + + /** Generates a random value from the KD distribution */ + def generateValue(rand:Randomness):Double = { + val shift = normalElt.generateValue(rand._2) + return samples(rand._1) + shift + } + + /** Computes the density of a new point */ + def density(point:Double):Double = { + val densities = samples.map(s => { + normalElt.density(s - point) + }) + + densities.sum / densities.length + } + + override def toString = "KernelDensity(bandwidth=" + this.bandwidth + ")" +} + +object KernelDensity { + /** + * Create a kernel density estimator with specified bandwidth + */ + def apply(samples: Seq[Double], bandwidth: Double)(implicit name: Name[Double], collection: ElementCollection) = + new KernelDensity(name, samples, bandwidth, collection) +} \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/util/LogStatistics.scala b/Figaro/src/main/scala/com/cra/figaro/util/LogStatistics.scala new file mode 100644 index 00000000..244df7d0 --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/util/LogStatistics.scala @@ -0,0 +1,117 @@ +/* + * LogStatistics.scala + * Utilities for statistics in log space. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 16, 2016 + * + * Copyright 2016 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.util + +import org.apache.commons.math3.distribution.TDistribution + +import scala.math._ + +/** + * Represents basic statistics of a sample from a nonnegative-valued univariate distribution. All computations on these + * statistics are done in log space to prevent underflow. + * @param logMean The log of the mean of the samples. + * @param logVariance The log of the variance of the samples. + * @param count The number of samples taken. + */ +case class LogStatistics(logMean: Double, logVariance: Double, count: Int) { + /** + * Returns the statistics corresponding to the distribution wherein each sample is multiplied by the given constant. + * @param logConstant The log of the constant to multiply by. + * @return Statistics such that the mean and variance are updated, and the count is unchanged. + */ + def multiplyByConstant(logConstant: Double) = { + LogStatistics(logMean + logConstant, logVariance + logConstant * 2, count) + } +} + +object LogStatistics { + /** + * Performs a one-sided t-test for the comparison of v1.logMean and v2.logMean. This compares the smaller mean to the + * larger mean, i.e. it computes a p-value for min(v1.logMean, v2.logMean) < max(v1.logMean, v2.logMean). + * @param v1 Mean, variance, and sample count from first distribution. Requires v1.count > 1. + * @param v2 Mean, variance, and sample count from second distribution. Requires v2.count > 1. + * @return A p-value for the hypothesis. A small p-value indicates high confidence that the population mean of the + * sample with lesser mean is less than the population mean of the other sample. + */ + def oneSidedTTest(v1: LogStatistics, v2: LogStatistics): Double = { + require(v1.count > 1 && v2.count > 1, "t-test requires counts > 1") + // If both variances are 0, we return here to avoid NaNs. + if(v1.logVariance == Double.NegativeInfinity && v2.logVariance == Double.NegativeInfinity) { + if(v1.logMean == v2.logMean) 0.5 + else 0.0 + } + // Otherwise, at least one of the variances is positive, so everything below is well-defined. + else { + // log(v1.variance / v1.count), respectively v2. These quantities are used multiple times in computing the degrees + // of freedom and the t-score, so it's helpful not having to recompute them. + val logVar1OverCount1 = v1.logVariance - log(v1.count) + val logVar2OverCount2 = v2.logVariance - log(v2.count) + + // Compute log numerator and denominator of the degrees of freedom using the Welch-Satterthwaite equation. + val logDoFNum = 2 * logSum(logVar1OverCount1, logVar2OverCount2) + val logDoFDenom = logSum(2 * logVar1OverCount1 - log(v1.count - 1), 2 * logVar2OverCount2 - log(v2.count - 1)) + val degreesOfFreedom = exp(logDoFNum - logDoFDenom) + + // To get a negative t-score, take the positive difference in log space, then invert the sign after exponentiation. + val logNegativeTScore = logDiff(v1.logMean max v2.logMean, v1.logMean min v2.logMean) - + 0.5 * logSum(logVar1OverCount1, logVar2OverCount2) + val tScore = -exp(logNegativeTScore) + new TDistribution(degreesOfFreedom).cumulativeProbability(tScore) + } + } +} + +/** + * Trait for computing mean in variance in log space in an online fashion. + */ +trait OnlineLogStatistics { + + // Fields for recording the mean and variance across runs. See Wikipedia, "Algorithms for calculating variance". + // Modified to work in log space to prevent underflow. + // Number of samples taken + protected var count = 0 + // Log of mean of samples taken so far + protected var logMean = Double.NegativeInfinity + // Log of variance of samples taken so far, multiplied by (count - 1) + protected var logM2 = Double.NegativeInfinity + + /** + * Record the weight in the rolling mean and variance computation. + * @param logWeight Log of the weight to record. + */ + def record(logWeight: Double) = { + // Online variance algorithm from Wikipedia + count += 1 + if(logWeight >= logMean) { + val logDelta = logDiff(logWeight, logMean) + logMean = logSum(logMean, logDelta - math.log(count)) + // assert(logMean >= logWeight) + logM2 = logSum(logM2, logDelta + logDiff(logWeight, logMean)) + } + else { + val logNegativeDelta = logDiff(logMean, logWeight) + logMean = logDiff(logMean, logNegativeDelta - math.log(count)) + // assert(logMean < logWeight) + // The negatives cancel in the right hand size + logM2 = logSum(logM2, logNegativeDelta + logDiff(logMean, logWeight)) + } + } + + /** + * Return the combined statistics for the log probability of evidence over all runs of this sampler. + * If the number of observations is 0, the returned log mean is -Infinity. + * If the number of observations is 0 or 1, the returned log variance is NaN. + */ + def totalLogStatistics = LogStatistics(logMean, logM2 - math.log(count - 1), count) +} 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 4306f2a9..9e808096 100644 --- a/Figaro/src/main/scala/com/cra/figaro/util/package.scala +++ b/Figaro/src/main/scala/com/cra/figaro/util/package.scala @@ -249,7 +249,17 @@ package object util { if (seq.isEmpty) throw new IllegalArgumentException("Empty list") else helper(seq.tail.toList, 1, 0, seq.head) } - + + /** + * Computes the difference of two probabilities in log space. + * Returns NaN if p2>p1, -Infinity if p2=p1 + */ + def logDiff(p1: Double, p2: Double): Double = { + // Also covers the case where p1 = p2 = -Infinity + if(p2 == Double.NegativeInfinity) p1 + // log(exp(p1) - exp(p2)) = log(exp(p1) * (1 - exp(p2) / exp(p1))) = p1 + log(1 - exp(p2 - p1)) + else p1 + Math.log1p(-Math.exp(p2 - p1)) + } /** * Sums two probabilities in log space. 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 83271d06..159d4673 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 @@ -375,7 +375,7 @@ class AbstractionTest extends WordSpec with Matchers { * 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.probability(flip)(b => b) should be(0.4 +- 0.02) ve.kill } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AlgorithmTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AlgorithmTest.scala index 8d016d71..afe1d356 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AlgorithmTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/AlgorithmTest.scala @@ -33,7 +33,7 @@ class AlgorithmTest extends WordSpec with Matchers { val a = new SimpleAlgorithm(c) an[AlgorithmInactiveException] should be thrownBy { a.distribution(c) } an[AlgorithmInactiveException] should be thrownBy { a.expectation(c, (b: Boolean) => 1.0) } - an[AlgorithmInactiveException] should be thrownBy { a.probability(c, (b: Boolean) => true) } + an[AlgorithmInactiveException] should be thrownBy { a.probability(c)(b => true) } an[AlgorithmInactiveException] should be thrownBy { a.probability(c, true) } } @@ -44,17 +44,17 @@ class AlgorithmTest extends WordSpec with Matchers { a.start() a.distribution(c) a.expectation(c, (b: Boolean) => 1.0) - a.probability(c, (b: Boolean) => true) + a.probability(c)(b => true) a.probability(c, true) a.stop() a.distribution(c) - a.expectation(c, (b: Boolean) => 1.0) - a.probability(c, (b: Boolean) => true) + a.expectation(c)(b => 1.0) + a.probability(c)(b => true) a.probability(c, true) a.resume() a.distribution(c) - a.expectation(c, (b: Boolean) => 1.0) - a.probability(c, (b: Boolean) => true) + a.expectation(c)(b => 1.0) + a.probability(c)(b => true) a.probability(c, true) } @@ -66,7 +66,7 @@ class AlgorithmTest extends WordSpec with Matchers { a.kill() an[AlgorithmInactiveException] should be thrownBy { a.distribution(c) } an[AlgorithmInactiveException] should be thrownBy { a.expectation(c, (b: Boolean) => 1.0) } - an[AlgorithmInactiveException] should be thrownBy { a.probability(c, (b: Boolean) => true) } + an[AlgorithmInactiveException] should be thrownBy { a.probability(c)(b => true) } an[AlgorithmInactiveException] should be thrownBy { a.probability(c, true) } } @@ -101,7 +101,7 @@ class AlgorithmTest extends WordSpec with Matchers { val f = Flip(0.3) val a = new SimpleAlgorithm(f) a.start() - a.probability(f, (b: Boolean) => b) should equal(0.3) + a.probability(f)(b => b) should equal(0.3) } "compute the probability of a value" in { @@ -129,7 +129,7 @@ class AlgorithmTest extends WordSpec with Matchers { val a = new SimpleAnytime(c) a.start() a.stop() - val x = a.expectation(c, (b: Boolean) => -1.0) + val x = a.expectation(c)(b => -1.0) a.expectation(c, (b: Boolean) => -1.0) should equal(x) a.kill() } @@ -142,7 +142,7 @@ class AlgorithmTest extends WordSpec with Matchers { a.stop() a.resume() val x = a.expectation(c, (b: Boolean) => -1.0) - a.expectation(c, (b: Boolean) => -1.0) should be > (x) + a.expectation(c)(b => -1.0) should be > (x) a.kill() } @@ -199,7 +199,7 @@ class AlgorithmTest extends WordSpec with Matchers { val myFlip = Flip(0.8) val s = new SimpleWeighted(myFlip) s.start() - s.probability(myFlip, (b: Boolean) => b) should be(1.0 / 3 +- 0.001) + s.probability(myFlip)(b => b) should be(1.0 / 3 +- 0.001) } } 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 ff346178..6cbb2d96 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 @@ -119,9 +119,9 @@ class BPTest extends WordSpec with Matchers { 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)(_ == 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) + bp.probability(e2)(_ == 3) should be(e2_3 +- tol) } "with no conditions or constraints produce the correct result" in { @@ -190,7 +190,7 @@ class BPTest extends WordSpec with Matchers { val tolerance = 0.0000001 val algorithm = BeliefPropagation(10, f)(u1) algorithm.start() - algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- globalTol) + algorithm.probability(f)(b => b) should be(0.6 +- globalTol) algorithm.kill() } 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 3c546532..7c528d47 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 @@ -374,7 +374,7 @@ class FactorTest extends WordSpec with Matchers with PrivateMethodTester { 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().asInstanceOf[Semiring[Double]], v3) + val g = f.marginalizeTo(v3) g.variables should equal(List(v3)) val p1 = 0.0 + 0.1 + 0.2 val p2 = 0.3 + 0.4 + 0.5 @@ -409,7 +409,7 @@ class FactorTest extends WordSpec with Matchers with PrivateMethodTester { 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().asInstanceOf[Semiring[Double]], v1, v3) + val g = f.marginalizeTo(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) @@ -689,8 +689,8 @@ class FactorTest extends WordSpec with Matchers with PrivateMethodTester { val v1 = Normal(0.0, 1.0) Values()(v1) val factor = Factory.makeFactorsForElement(v1) - factor(0).size should equal(ParticleGenerator.defaultArgSamples) - factor(0).get(List(0)) should equal(1.0 / ParticleGenerator.defaultArgSamples) + factor(0).size should equal(ParticleGenerator.defaultNumSamplesFromAtomics) + factor(0).get(List(0)) should equal(1.0 / ParticleGenerator.defaultNumSamplesFromAtomics) } "correctly create factors for continuous elements through chains" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/GibbsTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/GibbsTest.scala index df96d03c..e1920620 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/GibbsTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/GibbsTest.scala @@ -80,6 +80,7 @@ class GibbsTest extends WordSpec with Matchers { } "with a constraint on a Chain produce the correct result for the parent" in { + Universe.createNew val f = Flip(0.3) val c = If(f, Flip(0.8), Constant(false)) c.addConstraint(b => if (b) 2.0 else 1.0) @@ -88,6 +89,7 @@ class GibbsTest extends WordSpec with Matchers { } "with a constraint on a Chain result correctly constrain the Chain but not the parent" in { + Universe.createNew val f = Flip(0.3) val r1 = Flip(0.8) r1.addConstraint(b => if (b) 2.0 else 1.0) @@ -99,6 +101,7 @@ class GibbsTest extends WordSpec with Matchers { } "with an element used multiple times use the same value each time" in { + Universe.createNew val f = Flip(0.3) val e = f === f test[Boolean](e, identity, 1.0) @@ -151,6 +154,7 @@ class GibbsTest extends WordSpec with Matchers { "A default block sampler" should { "produce sub-factors on initialization" in { + Universe.createNew val v1 = new Variable(ValueSet.withoutStar(Set(0, 1, 2, 3))) val v2 = new Variable(ValueSet.withoutStar(Set(0, 1))) val semiring = LogSumProductSemiring() @@ -171,6 +175,7 @@ class GibbsTest extends WordSpec with Matchers { } "normalize and make un-logarithmic a factor" in { + Universe.createNew val v1 = new Variable(ValueSet.withoutStar(Set(0, 1))) val v2 = new Variable(ValueSet.withoutStar(Set(0, 1))) val semiring = LogSumProductSemiring() @@ -188,6 +193,7 @@ class GibbsTest extends WordSpec with Matchers { } "compute a sampling factor" in { + Universe.createNew val v1 = new Variable(ValueSet.withoutStar(Set(0, 1))) val v2 = new Variable(ValueSet.withoutStar(Set(0, 1))) val v3 = new Variable(ValueSet.withoutStar(Set(0, 1))) @@ -211,6 +217,7 @@ class GibbsTest extends WordSpec with Matchers { } "cache recent sampling factors" in { + Universe.createNew val v1 = new Variable(ValueSet.withoutStar(Set(0, 1))) val v2 = new Variable(ValueSet.withoutStar(Set(0, 1))) val semiring = LogSumProductSemiring() @@ -238,4 +245,4 @@ class GibbsTest extends WordSpec with Matchers { 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/VETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/VETest.scala index 923158f7..8447ad37 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 @@ -29,6 +29,7 @@ import scala.collection.mutable.Map import com.cra.figaro.test.tags.Performance import com.cra.figaro.test.tags.NonDeterministic import com.cra.figaro.algorithm.factored.factors.factory.Factory +import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredMPEVE class VETest extends WordSpec with Matchers { "A VEGraph" when { @@ -146,7 +147,7 @@ class VETest extends WordSpec with Matchers { val g = Factory.defaultFactor[Double](List(v5, v3, v2, v6), List()) val h = Factory.defaultFactor[Double](List(v1, v7), List()) val graph1 = new VEGraph(List(f, g, h)) - val graph2 = graph1.eliminate(v3) + val (graph2, _) = graph1.eliminate(v3) val VariableInfo(v1Factors, v1Neighbors) = graph2.info(v1) v1Factors.size should equal(2) // h and the new factor assert(v1Factors exists ((factor: AbstractFactor) => factor.variables.size == 5)) // all except v3 and v7 @@ -172,7 +173,7 @@ class VETest extends WordSpec with Matchers { val g = Factory.defaultFactor[Double](List(v5, v3, v2, v6), List()) val h = Factory.defaultFactor[Double](List(v1, v7), List()) val graph1 = new VEGraph(List(f, g, h)) - val graph2 = graph1.eliminate(v3) + val (graph2, _) = graph1.eliminate(v3) val VariableInfo(v1Factors, v1Neighbors) = graph2.info(v1) v1Neighbors should not contain (v3) } @@ -316,7 +317,7 @@ class VETest extends WordSpec with Matchers { val tolerance = 0.0000001 val algorithm = VariableElimination(f)(u1) algorithm.start() - algorithm.probability(f, (b: Boolean) => b) should be(0.6 +- tolerance) + algorithm.probability(f)(b => b) should be(0.6 +- tolerance) algorithm.kill() } @@ -414,7 +415,7 @@ class VETest extends WordSpec with Matchers { // p(e1=F,e2=T,e3=T) = 0.25 * 0.9 * 0.4 = .09 // p(e1=F,e2=F,e3=F) = 0.25 * 0.1 * 0.6 = .015 // MPE: e1=T,e2=F,e3=F,e4=T - val alg = MPEVariableElimination() + val alg = MPEVariableElimination() alg.start alg.mostLikelyValue(e1) should equal(true) alg.mostLikelyValue(e2) should equal(false) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/factors/SparseFactorTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/factors/SparseFactorTest.scala index c117c936..d1934e8b 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/factors/SparseFactorTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/factored/factors/SparseFactorTest.scala @@ -344,7 +344,7 @@ class SparseFactorTest extends WordSpec with Matchers with PrivateMethodTester { 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) + val g = f.marginalizeTo(v3) g.variables should equal(List(v3)) val p1 = 0.0 + 0.1 + 0.2 val p2 = 0.3 + 0.4 + 0.5 @@ -379,7 +379,7 @@ class SparseFactorTest extends WordSpec with Matchers with PrivateMethodTester { 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) + val g = f.marginalizeTo(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) 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 26069794..8fe42c07 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 @@ -58,7 +58,7 @@ class EMWithMHTest extends WordSpec with PrivateMethodTester with Matchers { f.observe(false) } - val algorithm = EMWithBP(terminationCriteria, 10, b)(universe) + val algorithm = EMWithMH(terminationCriteria, 10000, b)(universe) algorithm.start val result = b.MAPValue 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 4a7cf5e3..d64498e8 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 @@ -196,7 +196,7 @@ class AnnealingTest extends WordSpec with Matchers with PrivateMethodTester { annealer.kill } catch { case knf: Exception => fail("running algorithm should not produce exceptions.") - case _ => + case _: Throwable => () } } } 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 afa26d8f..7acb88eb 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 @@ -211,7 +211,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { Apply(B, (b: List[List[Boolean]]) => b.head) }) val alg = Importance(1, c) - alg.lw.computeWeight(List(c)) + alg.lw.computeWeight(List(c)) c.value.asInstanceOf[List[Boolean]].head should be(true || false) } } @@ -326,7 +326,7 @@ 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 => 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 @@ -428,7 +428,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 => 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 @@ -619,6 +619,44 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { } } + "Sampling the posterior" should { + "produce the correct answer for marginals" in { + Universe.createNew() + val u = Uniform(0.2, 1.0) + 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 + val pos = Importance.sampleJointPosterior(f) + val probTrue = (pos.take(1000).toList.map(t => t(0).asInstanceOf[Boolean])).count(p => p) + probTrue.toDouble / 1000.0 should be(u1 / (u1 + u2) +- .01) + } + + "produce the correct answer for joint" in { + Universe.createNew() + val u = Uniform(0.2, 1.0) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setConstraint((i: Int) => i.toDouble) + val pair = ^^(f,a) + val alg = Importance(20000, pair) + alg.start() + + val pos = Importance.sampleJointPosterior(f, a) + val samples = pos.take(5000).toList.map(t => (t(0).asInstanceOf[Boolean], t(1).asInstanceOf[Int])) + + val probTrueTwo = samples.count(p => p._1 == true && p._2 == 2).toDouble/5000.0 should be(alg.probability(pair, (true, 2)) +- .01) + val probTrueOne = samples.count(p => p._1 == true && p._2 == 1).toDouble/5000.0 should be(alg.probability(pair, (true, 1)) +- .01) + val probFalseTwo = samples.count(p => p._1 == false && p._2 == 2).toDouble/5000.0 should be(alg.probability(pair, (false, 2)) +- .01) + val probFalseOne = samples.count(p => p._1 == false && p._2 == 1).toDouble/5000.0 should be(alg.probability(pair, (false, 1)) +- .01) + alg.kill() + } + + } + } def weightedSampleTest[T](target: Element[T], predicate: T => Boolean, prob: Double) { 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 28a6815d..2d9f4ab7 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 @@ -257,7 +257,7 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { try { mh.start() mh.stop() - mh.probability(f1, (b: Boolean) => b) should be(p1 / (p1 + p2) +- tolerance) + mh.probability(f1)(b => b) should be(p1 / (p1 + p2) +- tolerance) } finally { mh.kill() } @@ -276,7 +276,7 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { alg.stop() val p1 = 0.2 * 0.7 val p2 = 0.8 * 0.4 - alg.probability(elem1, (b: Boolean) => b) should be(p1 / (p1 + p2) +- tolerance) + alg.probability(elem1)(b => b) should be(p1 / (p1 + p2) +- tolerance) } finally { alg.kill() } @@ -293,7 +293,7 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { try { alg.start() alg.stop() - alg.probability(elem2, (d: Double) => 1.4 < d && d < 1.6) should be(1.0 +- tolerance) + alg.probability(elem2)(d => 1.4 < d && d < 1.6) should be(1.0 +- tolerance) } finally { alg.kill() } @@ -361,7 +361,7 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { try { mh.start() mh.stop() - mh.probability(f1, (b: Boolean) => b) should be(p1 / (p1 + p2) +- tolerance) + mh.probability(f1)(b => b) should be(p1 / (p1 + p2) +- tolerance) } finally { mh.kill() } @@ -479,6 +479,44 @@ class MHTest extends WordSpec with Matchers with PrivateMethodTester { method.invoke(mh).asInstanceOf[State] } + "Sampling the posterior" should { + "produce the correct answer for marginals" in { + Universe.createNew() + val u = Uniform(0.2, 1.0) + 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 + val pos = MetropolisHastings.sampleJointPosterior(f) + val probTrue = (pos.take(1000).toList.map(t => t(0).asInstanceOf[Boolean])).count(p => p) + probTrue.toDouble / 1000.0 should be(u1 / (u1 + u2) +- .01) + } + + "produce the correct answer for joint" in { + Universe.createNew() + val u = Uniform(0.2, 1.0) + val f = Flip(u) + val a = If(f, Select(0.3 -> 1, 0.7 -> 2), Constant(2)) + a.setConstraint((i: Int) => i.toDouble) + val pair = ^^(f, a) + val alg = Importance(20000, pair) + alg.start() + + val pos = MetropolisHastings.sampleJointPosterior(f, a) + val samples = pos.take(5000).toList.map(t => (t(0).asInstanceOf[Boolean], t(1).asInstanceOf[Int])) + + val probTrueTwo = samples.count(p => p._1 == true && p._2 == 2).toDouble / 5000.0 should be(alg.probability(pair, (true, 2)) +- .01) + val probTrueOne = samples.count(p => p._1 == true && p._2 == 1).toDouble / 5000.0 should be(alg.probability(pair, (true, 1)) +- .01) + val probFalseTwo = samples.count(p => p._1 == false && p._2 == 2).toDouble / 5000.0 should be(alg.probability(pair, (false, 2)) +- .01) + val probFalseOne = samples.count(p => p._1 == false && p._2 == 1).toDouble / 5000.0 should be(alg.probability(pair, (false, 1)) +- .01) + alg.kill() + } + + } + def getDissatisfied(mh: MetropolisHastings): Set[Element[_]] = { val cls = mh.getClass().getSuperclass() val method = cls.getDeclaredMethod("getDissatisfied") diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ParImportanceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ParImportanceTest.scala index e412bb02..bc373dda 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ParImportanceTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ParImportanceTest.scala @@ -110,10 +110,10 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester "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 val gen = () => { - val universe = Universe.createNew() + val universe = new Universe val b = Uniform(0.0, 1.0)("b", universe) - for (_ <- 1 to 16) { Flip(b).observe(true) } - for (_ <- 1 to 4) { Flip(b).observe(false) } + for (_ <- 1 to 16) { Flip(b)("", universe).observe(true) } + for (_ <- 1 to 4) { Flip(b)("", universe).observe(false) } universe } val alg = Importance.par(gen, numThreads, "b") @@ -136,10 +136,10 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester "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 val gen = () => { - val universe = Universe.createNew() + val universe = new Universe val b = Beta(2.0, 5.0)("b", universe) - for (_ <- 1 to 16) { Flip(b).observe(true) } - for (_ <- 1 to 4) { Flip(b).observe(false) } + for (_ <- 1 to 16) { Flip(b)("", universe).observe(true) } + for (_ <- 1 to 4) { Flip(b)("", universe).observe(false) } universe } val alg = Importance.par(gen, numThreads, "b") @@ -149,7 +149,7 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester alg.stop() // Result is beta(2 + 16,5 + 4) // Expectation is (alpha) / (alpha + beta) = 18/27 - val exp = alg.expectation("b", (d: Double) => d) + val exp = alg.expectation("b")((d: Double) => d) 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 @@ -161,9 +161,9 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester "with an observation on a parameterized binomial, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for chain val gen = () => { - val universe = Universe.createNew() + val universe = new Universe val beta = Beta(2.0, 5.0)("beta", universe) - val bin = Binomial(2000, beta) + val bin = Binomial(2000, beta)("", universe) bin.observe(1600) universe } @@ -174,7 +174,7 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester alg.stop() // Result is beta(2 + 1600,5 + 400) // Expectation is (alpha) / (alpha + beta) = 1602/2007 - val exp = alg.expectation("beta", (d: Double) => d) + val exp = alg.expectation("beta")((d: Double) => d) 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 @@ -186,9 +186,9 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester "with an observation on a chain, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for chain val gen = () => { - val universe = Universe.createNew() + val universe = new Universe val beta = Uniform(0.0, 1.0)("beta", universe) - val bin = Binomial(2000, beta) + val bin = Binomial(2000, beta)("", universe) bin.observe(1600) universe } @@ -200,7 +200,7 @@ class ParImportanceTest 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 - val exp = alg.expectation("beta", (d: Double) => d) + val exp = alg.expectation("beta")((d: Double) => d) 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 @@ -212,9 +212,9 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester "with an observation on a dist, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for dist val gen = () => { - val universe = Universe.createNew() + val universe = new Universe val beta = Beta(2.0, 5.0)("beta", universe) - val dist = Dist(0.5 -> Constant(1000)(Name.default, universe), 0.5 -> Binomial(2000, beta)(Name.default, universe)) + val dist = Dist(0.5 -> Constant(1000)(Name.default, universe), 0.5 -> Binomial(2000, beta)(Name.default, universe))("", universe) dist.observe(1600) // forces it to choose bin, and observation should propagate to it universe } @@ -225,7 +225,7 @@ class ParImportanceTest extends WordSpec with Matchers with PrivateMethodTester alg.stop() // Result is beta(2 + 1600,5 + 400) // Expectation is (alpha) / (alpha + beta) = 1602/2007 - val exp = alg.expectation("beta", (d: Double) => d) + val exp = alg.expectation("beta")((d: Double) => d) 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 diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/ExpandTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/ExpandTest.scala index 043a8dda..346fec04 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/ExpandTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/ExpandTest.scala @@ -145,6 +145,7 @@ class ExpandTest extends WordSpec with Matchers { "add non-constraint factors to the chain" in { Universe.createNew() val cc = new ComponentCollection + // raising should use the old chain method val pr = new Problem(cc) val e1 = Flip(0.2) def f(b: Boolean) = if (b) Uniform(1,2) else Uniform(3,4) @@ -169,6 +170,7 @@ class ExpandTest extends WordSpec with Matchers { "add constraint factors to the chain" in { Universe.createNew() val cc = new ComponentCollection + // raising should use the old chain method val pr = new Problem(cc) val u1 = Uniform(1,2) val u2 = Uniform(3,4) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/FactorMakerTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/FactorMakerTest.scala index ce64b8a9..20533134 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/FactorMakerTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/FactorMakerTest.scala @@ -12,11 +12,13 @@ */ package com.cra.figaro.test.algorithm.structured -import org.scalatest.{WordSpec, Matchers} +import org.scalatest.{Matchers, WordSpec} import com.cra.figaro.language._ import com.cra.figaro.algorithm.structured._ -import com.cra.figaro.algorithm.lazyfactored.{Star, Regular, ValueSet} -import ValueSet.{withoutStar, withStar} +import com.cra.figaro.algorithm.structured.strategy.solve.ConstantStrategy +import com.cra.figaro.algorithm.structured.solver._ +import com.cra.figaro.algorithm.lazyfactored.{Regular, Star, ValueSet} +import ValueSet.{withStar, withoutStar} import com.cra.figaro.library.atomic.continuous.Beta import com.cra.figaro.library.atomic.continuous.Dirichlet import com.cra.figaro.algorithm.factored.ParticleGenerator @@ -29,7 +31,8 @@ import com.cra.figaro.util.HashMultiSet import com.cra.figaro.library.atomic.discrete.FromRange import com.cra.figaro.library.collection.MakeArray import com.cra.figaro.library.compound.FoldLeft -import com.cra.figaro.algorithm.factored.factors.factory.Factory.{makeTupleVarAndFactor, makeConditionalSelector} +import com.cra.figaro.algorithm.factored.factors.factory.Factory.{makeConditionalSelector, makeTupleVarAndFactor} +import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredVE class FactorMakerTest extends WordSpec with Matchers { "Making a tuple variable and factor for a set of variables" should { @@ -466,7 +469,7 @@ class FactorMakerTest extends WordSpec with Matchers { val List(factor) = c2.nonConstraintFactors factor.variables should equal (List(c1.variable, c2.variable)) - factor.size should equal (ParticleGenerator.defaultTotalSamples * 2) + factor.size should equal (ParticleGenerator.defaultMaxNumSamplesAtChain * 2) for { (p, index) <- c1.variable.range.zipWithIndex } { factor.get(List(index, 0)) should equal (p.value) factor.get(List(index, 1)) should equal (1 - p.value) @@ -729,7 +732,7 @@ class FactorMakerTest extends WordSpec with Matchers { val List(factor) = c2.nonConstraintFactors factor.variables should equal (List(c1.variable, c2.variable)) - factor.size should equal (ParticleGenerator.defaultTotalSamples * 2) + factor.size should equal (ParticleGenerator.defaultMaxNumSamplesAtChain * 2) for { (xprobs, i) <- c1.variable.range.zipWithIndex j <- 0 until c2.variable.range.size @@ -832,10 +835,11 @@ class FactorMakerTest extends WordSpec with Matchers { val index = c2.variable.range.indexOf(Regular(n)) factor.get(List(index)) should equal (0.0) } + } } - } - "given a parameterized binomial with a fixed number of trials with an added success probability, when the parameterized flag = false" should { + + "given a parameterized binomial with a fixed number of trials with an added success probability, when the parameterized flag = false" should { "use the chain decomposition, conditioning on the values of the parameter" in { Universe.createNew() val cc = new ComponentCollection @@ -855,14 +859,14 @@ class FactorMakerTest extends WordSpec with Matchers { val List(var1, var2, tupleVar) = tupleFactor.variables var1 should equal (c1.variable) var2 should equal (c2.variable) - factors.size should equal (ParticleGenerator.defaultTotalSamples) + factors.size should equal (ParticleGenerator.defaultMaxNumSamplesAtChain) val vars = factors(0).variables vars.size should equal (2) vars(0) should equal (tupleVar) } } - "given a parameterized binomial with a fixed number of trials with an unadded success probability, when the parameterized flag = false" should { + "given a parameterized binomial with a fixed number of trials with an unadded success probability, when the parameterized flag = false" should { "create a simple factor with probability 1 for * and probability 0 for the outcomes" in { Universe.createNew() val cc = new ComponentCollection @@ -900,7 +904,7 @@ class FactorMakerTest extends WordSpec with Matchers { val List(factor) = c1.nonConstraintFactors factor.variables should equal (List(c1.variable)) - factor.size should equal (ParticleGenerator.defaultTotalSamples) + factor.size should equal (ParticleGenerator.defaultMaxNumSamplesAtChain) } } @@ -1036,7 +1040,7 @@ class FactorMakerTest extends WordSpec with Matchers { } } - "given a chain with parent without *" should { + "given a chain with parent without * using the old chain method" should { "produce a conditional selector for each parent value" in { Universe.createNew() val cc = new ComponentCollection @@ -1208,7 +1212,7 @@ class FactorMakerTest extends WordSpec with Matchers { } } - "given a chain with parent with *" should { + "given a chain with parent with * using the old chain method" should { "produce an appropriate conditional selector for the * parent value" in { Universe.createNew() val cc = new ComponentCollection @@ -1244,6 +1248,276 @@ class FactorMakerTest extends WordSpec with Matchers { } } + "given a chain with no globals, no *, and the same values for each parent value, using the new chain method" should { + "produce a single factor connecting parent and child" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Chain(v1, (i: Int) => Flip(i / 10.0)) + pr.add(v1) + pr.add(v2) + val c1 = cc(v1) + val c2 = cc(v2) + c1.generateRange() + c2.expand() + + val pr1 = cc.expansions(v2.chainFunction, 1) + val subV1 = pr1.target + val subC1 = cc(subV1) + subC1.generateRange() + subC1.makeNonConstraintFactors() + pr1.solve(new ConstantStrategy(marginalVariableElimination)) + + val pr2 = cc.expansions(v2.chainFunction, 2) + val subV2 = pr2.target + val subC2 = cc(subV2) + subC2.generateRange() + subC2.makeNonConstraintFactors() + pr2.solve(new ConstantStrategy(marginalVariableElimination)) + val pr3 = cc.expansions(v2.chainFunction, 3) + val subV3 = pr3.target + val subC3 = cc(subV3) + subC3.generateRange() + subC3.makeNonConstraintFactors() + pr3.solve(new ConstantStrategy(marginalVariableElimination)) + + c2.generateRange() + c2.makeNonConstraintFactors() + + val List(factor) = c2.nonConstraintFactors + factor.variables should equal (List(c1.variable, c2.variable)) + val v1Vals = c1.variable.range + val v2Vals = c2.variable.range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v2f = v2Vals indexOf Regular(false) + val v2t = v2Vals indexOf Regular(true) + factor.size should equal (6) + factor.get(List(v11,v2f)) should be (0.9 +- 0.0001) + factor.get(List(v11,v2t)) should be (0.1 +- 0.0001) + factor.get(List(v12,v2f)) should be (0.8 +- 0.0001) + factor.get(List(v12,v2t)) should be (0.2 +- 0.0001) + factor.get(List(v13,v2f)) should be (0.7 +- 0.0001) + factor.get(List(v13,v2t)) should be (0.3 +- 0.0001) + } + } + + "given a chain with no globals, no *, different values for each parent value, using new chain method" should { + "produce a factor mapping the parent to the child with the union of the values" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val v1 = Flip(0.5) + val v2 = Chain(v1, (b: Boolean) => if (b) Select(0.1 -> 1, 0.9 -> 2) else Select(0.2 -> 2, 0.8 -> 3)) + pr.add(v1) + pr.add(v2) + val c1 = cc(v1) + val c2 = cc(v2) + c1.generateRange() + c2.expand() + + val prf = cc.expansions(v2.chainFunction, false) + val subVf = prf.target + val subCf = cc(subVf) + subCf.generateRange() + subCf.makeNonConstraintFactors() + prf.solve(new ConstantStrategy(marginalVariableElimination)) + val prt = cc.expansions(v2.chainFunction, true) + val subVt = prt.target + val subCt = cc(subVt) + subCt.generateRange() + subCt.makeNonConstraintFactors() + prt.solve(new ConstantStrategy(marginalVariableElimination)) + + c2.generateRange() + c2.makeNonConstraintFactors() + + val List(factor) = c2.nonConstraintFactors + factor.variables should equal (List(c1.variable, c2.variable)) + val v1Vals = c1.variable.range + val v2Vals = c2.variable.range + val v1f = v1Vals indexOf Regular(false) + val v1t = v1Vals indexOf Regular(true) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v23 = v2Vals indexOf Regular(3) + factor.size should equal (6) + factor.get(List(v1f,v21)) should be (0.0 +- 0.0001) + factor.get(List(v1f,v22)) should be (0.2 +- 0.0001) + factor.get(List(v1f,v23)) should be (0.8 +- 0.0001) + factor.get(List(v1t,v21)) should be (0.1 +- 0.0001) + factor.get(List(v1t,v22)) should be (0.9 +- 0.0001) + factor.get(List(v1t,v23)) should be (0.0 +- 0.0001) + + } + } + + "given a chain with no globals, * in subproblem, using new chain method" should { + "produce a factor mapping parent to child including *" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val v1 = Select(0.3 -> 1, 0.2 -> 2, 0.5 -> 3) + val v2 = Chain(v1, (i: Int) => Flip(i / 10.0)) + pr.add(v1) + pr.add(v2) + val c1 = cc(v1) + val c2 = cc(v2) + c1.generateRange() + c2.expand() + + val pr1 = cc.expansions(v2.chainFunction, 1) + val subV1 = pr1.target + val subC1 = cc(subV1) + subC1.generateRange() + subC1.makeNonConstraintFactors() + pr1.solve(new ConstantStrategy(marginalVariableElimination)) + val pr2 = cc.expansions(v2.chainFunction, 2) + val subV2 = pr2.target + val subC2 = cc(subV2) + subC2.generateRange() + subC2.makeNonConstraintFactors() + pr2.solve(new ConstantStrategy(marginalVariableElimination)) + val pr3 = cc.expansions(v2.chainFunction, 3) + // no range generation or factor creation + pr3.solve(new ConstantStrategy(marginalVariableElimination)) + + c2.generateRange() + c2.makeNonConstraintFactors() + + val List(factor) = c2.nonConstraintFactors + factor.variables should equal (List(c1.variable, c2.variable)) + val v1Vals = c1.variable.range + val v2Vals = c2.variable.range + val v11 = v1Vals indexOf Regular(1) + val v12 = v1Vals indexOf Regular(2) + val v13 = v1Vals indexOf Regular(3) + val v2f = v2Vals indexOf Regular(false) + val v2t = v2Vals indexOf Regular(true) + val v2Star = v2Vals indexWhere (!_.isRegular) + factor.size should equal (9) + factor.get(List(v11,v2f)) should be (0.9 +- 0.0001) + factor.get(List(v11,v2t)) should be (0.1 +- 0.0001) + factor.get(List(v11,v2Star)) should be (0.0 +- 0.0001) + factor.get(List(v12,v2f)) should be (0.8 +- 0.0001) + factor.get(List(v12,v2t)) should be (0.2 +- 0.0001) + factor.get(List(v12,v2Star)) should be (0.0 +- 0.0001) + factor.get(List(v13,v2f)) should be (0.0 +- 0.0001) + factor.get(List(v13,v2t)) should be (0.0 +- 0.0001) + factor.get(List(v13,v2Star)) should be (1.0 +- 0.0001) + + } + } + + "given a chain with no globals, * in parent values, using new chain method" should { + "produce a factor mapping parent to child including *" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val vt = Constant(true) + val vf = Constant(false) + val v1 = Uniform(vt, vf) + val v2 = Chain(v1, (b: Boolean) => if (b) Select(0.1 -> 1, 0.9 -> 2) else Select(0.2 -> 1, 0.8 -> 2)) + pr.add(vt) + pr.add(vf) + pr.add(v1) + pr.add(v2) + val ct = cc(vt) + val cf = cc(vf) + val c1 = cc(v1) + val c2 = cc(v2) + ct.generateRange() + ct.makeNonConstraintFactors() + // do not generate range for cf + c1.generateRange() // will include true and * + c1.makeNonConstraintFactors() + c2.expand() + + val prt = cc.expansions(v2.chainFunction, true) + val subVt = prt.target + val subCt = cc(subVt) + subCt.generateRange() + subCt.makeNonConstraintFactors() + prt.solve(new ConstantStrategy(marginalVariableElimination)) + + c2.generateRange() + c2.makeNonConstraintFactors() + + val List(factor) = c2.nonConstraintFactors + factor.variables should equal (List(c1.variable, c2.variable)) + val v1Vals = c1.variable.range + val v2Vals = c2.variable.range + val v1Star = v1Vals.indexWhere(!_.isRegular) + val v1t = v1Vals indexOf Regular(true) + val v21 = v2Vals indexOf Regular(1) + val v22 = v2Vals indexOf Regular(2) + val v2Star = v2Vals.indexWhere(!_.isRegular) + factor.size should equal (6) + factor.get(List(v1Star,v21)) should be (0.0 +- 0.0001) + factor.get(List(v1Star,v22)) should be (0.0 +- 0.0001) + factor.get(List(v1Star,v2Star)) should be (1.0 +- 0.0001) + factor.get(List(v1t,v21)) should be (0.1 +- 0.0001) + factor.get(List(v1t,v22)) should be (0.9 +- 0.0001) + factor.get(List(v1t,v2Star)) should be (0.0 +- 0.0001) + } + } + + "given a chain with globals" should { + "not use the new method even when the new method is being used" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val v1 = Constant(1) + val v2 = Flip(0.5) + val v3 = Chain(v2, (b: Boolean) => if (b) Apply(v1, (i: Int) => i) else Constant(2)) + pr.add(v1) + pr.add(v2) + pr.add(v3) + val c1 = cc(v1) + val c2 = cc(v2) + val c3 = cc(v3) + c1.generateRange() + c2.generateRange() + c3.expand() + val subPf = cc.expansions(v3.chainFunction, false) + val vPf = subPf.target + val cPf = cc(vPf) + cPf.generateRange() + cPf.makeNonConstraintFactors() + subPf.solve(new ConstantStrategy(marginalVariableElimination)) + val subPt = cc.expansions(v3.chainFunction, true) + val vPt = subPt.target + val cPt = cc(vPt) + cPt.generateRange() + cPt.makeNonConstraintFactors() + subPt.solve(new ConstantStrategy(marginalVariableElimination)) + c3.generateRange() + c3.makeNonConstraintFactors() + + c3.nonConstraintFactors.length should be > (1) + } + } + + "given a chain in which the outcome is a global, using the new method" should { + "produce the right result" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val pr = new Problem(cc) + val v1 = Constant(1) + val v2 = Flip(0.5) + val v3 = Chain(v2, (b: Boolean) => if (b) v1 else Constant(2)) + StructuredVE.probability(v3, 1) should equal (0.5) + } + } + "given an apply of one argument without *" should { "produce a sparse factor that matches the argument to the result via the function" in { Universe.createNew() diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/BPSolverTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/BPSolverTest.scala index d585453b..df963fab 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/BPSolverTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/BPSolverTest.scala @@ -12,7 +12,7 @@ */ package com.cra.figaro.test.algorithm.structured.solver -import org.scalatest.{WordSpec, Matchers} +import org.scalatest.{ WordSpec, Matchers } import com.cra.figaro.language._ import com.cra.figaro.library.compound._ import com.cra.figaro.algorithm.factored.factors.Factor @@ -23,7 +23,6 @@ import com.cra.figaro.algorithm.structured.strategy.solve._ import com.cra.figaro.algorithm.structured._ import com.cra.figaro.algorithm.structured.solver._ - class BPSolverTest extends WordSpec with Matchers { "Making a tuple factor for the BP solver" should { @@ -41,13 +40,13 @@ class BPSolverTest extends WordSpec with Matchers { c2.generateRange() val v1 = c1.variable val v2 = c2.variable - val bp = new BPSolver(pr, Set(), Set(v1, v2), List(), 100) + val bp = new BPSolver(pr, Set(), Set(v1, v2), List(), 100, SumProductSemiring()) val vars = bp.tupleFactor.variables - vars.size should equal (3) - vars.contains(v1) should equal (true) - vars.contains(v2) should equal (true) - vars.contains(bp.tupleVar) should equal (true) + vars.size should equal(3) + vars.contains(v1) should equal(true) + vars.contains(v2) should equal(true) + vars.contains(bp.tupleVar) should equal(true) } "create a variable whose range is all the tuples of the targets, without * when the targets do not have *" in { @@ -64,14 +63,14 @@ class BPSolverTest extends WordSpec with Matchers { c2.generateRange() val v1 = c1.variable val v2 = c2.variable - val bp = new BPSolver(pr, Set(), Set(v1, v2), List(), 100) + val bp = new BPSolver(pr, Set(), Set(v1, v2), List(), 100, SumProductSemiring()) val vs = bp.tupleVar.valueSet - vs.hasStar should equal (false) + vs.hasStar should equal(false) if (bp.tupleFactor.variables(0) == v1) { - vs.regularValues should equal (Set(List(Regular(true), Regular(1)), List(Regular(false), Regular(1)))) + vs.regularValues should equal(Set(List(Regular(true), Regular(1)), List(Regular(false), Regular(1)))) } else { - vs.regularValues should equal (Set(List(Regular(1), Regular(true)), List(Regular(1), Regular(false)))) + vs.regularValues should equal(Set(List(Regular(1), Regular(true)), List(Regular(1), Regular(false)))) } } @@ -96,16 +95,16 @@ class BPSolverTest extends WordSpec with Matchers { val v3 = c3.variable val v3IndexStar = v3.range.indexWhere(!_.isRegular) val v3Star = v3.range(v3IndexStar) - val bp = new BPSolver(pr, Set(), Set(v1, v3), List(), 100) + val bp = new BPSolver(pr, Set(), Set(v1, v3), List(), 100, SumProductSemiring()) val vs = bp.tupleVar.valueSet - vs.hasStar should equal (false) + vs.hasStar should equal(false) if (bp.tupleFactor.variables(0) == v1) { - vs.regularValues should equal (Set(List(Regular(true), Regular(1)), List(Regular(false), Regular(1)), - List(Regular(true), v3Star), List(Regular(false), v3Star))) + vs.regularValues should equal(Set(List(Regular(true), Regular(1)), List(Regular(false), Regular(1)), + List(Regular(true), v3Star), List(Regular(false), v3Star))) } else { - vs.regularValues should equal (Set(List(Regular(1), Regular(true)), List(Regular(1), Regular(false)), - List(v3Star, Regular(true)), List(v3Star, Regular(false)))) + vs.regularValues should equal(Set(List(Regular(1), Regular(true)), List(Regular(1), Regular(false)), + List(v3Star, Regular(true)), List(v3Star, Regular(false)))) } } @@ -128,11 +127,11 @@ class BPSolverTest extends WordSpec with Matchers { c3.generateRange() val v1 = c1.variable val v3 = c3.variable - val bp = new BPSolver(pr, Set(), Set(v1, v3), List(), 100) + val bp = new BPSolver(pr, Set(), Set(v1, v3), List(), 100, SumProductSemiring()) val factor = bp.tupleFactor val vt = bp.tupleVar - factor.contents.size should equal (4) + factor.contents.size should equal(4) val v1IndexT = v1.range.indexOf(Regular(true)) val v1IndexF = v1.range.indexOf(Regular(false)) val v3Index1 = v3.range.indexOf(Regular(1)) @@ -143,24 +142,24 @@ class BPSolverTest extends WordSpec with Matchers { val vtIndexF1 = vt.range.indexOf(Regular(List(Regular(false), Regular(1)))) val vtIndexTStar = vt.range.indexOf(Regular(List(Regular(true), v3Star))) val vtIndexFStar = vt.range.indexOf(Regular(List(Regular(false), v3Star))) - factor.get(List(v1IndexT, v3Index1, vtIndexT1)) should equal (1.0) - factor.get(List(v1IndexF, v3Index1, vtIndexF1)) should equal (1.0) - factor.get(List(v1IndexT, v3IndexStar, vtIndexTStar)) should equal (1.0) - factor.get(List(v1IndexF, v3IndexStar, vtIndexFStar)) should equal (1.0) + factor.get(List(v1IndexT, v3Index1, vtIndexT1)) should equal(1.0) + factor.get(List(v1IndexF, v3Index1, vtIndexF1)) should equal(1.0) + factor.get(List(v1IndexT, v3IndexStar, vtIndexTStar)) should equal(1.0) + factor.get(List(v1IndexF, v3IndexStar, vtIndexFStar)) should equal(1.0) } else { val vtIndex1T = vt.range.indexOf(Regular(List(Regular(1), Regular(true)))) val vtIndex1F = vt.range.indexOf(Regular(List(Regular(1), Regular(false)))) val vtIndexStarT = vt.range.indexOf(Regular(List(v3IndexStar, Regular(true)))) val vtIndexStarF = vt.range.indexOf(Regular(List(v3IndexStar, Regular(false)))) - factor.get(List(v3Index1, v1IndexT, vtIndex1T)) should equal (1.0) - factor.get(List(v3Index1, v1IndexF, vtIndex1F)) should equal (1.0) - factor.get(List(v3IndexStar, v1IndexT, vtIndexStarT)) should equal (1.0) - factor.get(List(v3IndexStar, v1IndexF, vtIndexStarF)) should equal (1.0) + factor.get(List(v3Index1, v1IndexT, vtIndex1T)) should equal(1.0) + factor.get(List(v3Index1, v1IndexF, vtIndex1F)) should equal(1.0) + factor.get(List(v3IndexStar, v1IndexT, vtIndexStarT)) should equal(1.0) + factor.get(List(v3IndexStar, v1IndexF, vtIndexStarF)) should equal(1.0) } } } - "Running beliefPropagation without *" when { + "Running marginalBeliefPropagation without *" when { "given a flat model with no conditions or constraints" should { "produce the correct result over a single element" in { @@ -184,17 +183,17 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c2)) - pr.solved should equal (true) + pr.globals should equal(Set(c2)) + pr.solved should equal(true) val result = multiplyAll(pr.solution) - result.variables should equal (List(c2.variable)) - result.size should equal (2) + result.variables should equal(List(c2.variable)) + result.size should equal(2) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) - result.get(List(c2IndexT)) should be (0.6 +- 0.00000001) - result.get(List(c2IndexF)) should be (0.4 +- 0.00000001) + result.get(List(c2IndexT)) should be(0.6 +- 0.00000001) + result.get(List(c2IndexF)) should be(0.4 +- 0.00000001) } "produce the correct result over multiple elements" in { @@ -217,31 +216,35 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c2, c3)) + pr.globals should equal(Set(c2, c3)) val result = multiplyAll(pr.solution) - result.variables.size should equal (2) + result.variables.size should equal(2) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) val c3IndexT = c3.variable.range.indexOf(Regular(true)) val c3IndexF = c3.variable.range.indexOf(Regular(false)) - result.size should equal (4) + result.size should equal(4) val var0 = result.variables(0) val var1 = result.variables(1) if (var0 == c2.variable) { - var1 should equal (c3.variable) - result.get(List(c2IndexT, c3IndexT)) should equal (0.6) - result.get(List(c2IndexT, c3IndexF)) should equal (0.0) - result.get(List(c2IndexF, c3IndexT)) should equal (0.0) - result.get(List(c2IndexF, c3IndexF)) should equal (0.4) + var1 should equal(c3.variable) + // Note the answers are incorrect, but since the model is loopy now we can't guarantee the answer. This check is to ensure + // that any subsequent changes to BP that change this value should be noted + result.get(List(c2IndexT, c3IndexT)) should equal(0.36 +- 0.00001) // should be 0.6 + result.get(List(c2IndexT, c3IndexF)) should equal(0.24 +- 0.00001) // should be 0 + result.get(List(c2IndexF, c3IndexT)) should equal(0.24 +- 0.00001) // 0 + result.get(List(c2IndexF, c3IndexF)) should equal(0.16 +- 0.00001) // .16 } else { - var0 should equal (c3.variable) - var1 should equal (c2.variable) - result.get(List(c3IndexT, c2IndexT)) should equal (0.6) - result.get(List(c3IndexT, c2IndexF)) should equal (0.0) - result.get(List(c3IndexF, c2IndexT)) should equal (0.0) - result.get(List(c3IndexF, c2IndexF)) should equal (0.4) + var0 should equal(c3.variable) + var1 should equal(c2.variable) + // Note the answers are incorrect, but since the model is loopy now we can't guarantee the answer. This check is to ensure + // that any subsequent changes to BP that change this value should be noted + result.get(List(c3IndexT, c2IndexT)) should equal(0.36 +- 0.00001) // should be 0.6 + result.get(List(c3IndexT, c2IndexF)) should equal(0.24 +- 0.00001) // should be 0 + result.get(List(c3IndexF, c2IndexT)) should equal(0.24 +- 0.00001) // 0 + result.get(List(c3IndexF, c2IndexF)) should equal(0.16 +- 0.00001) // .16 } } } @@ -269,24 +272,24 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) + result.size should equal(4) val x3 = 0.25 * 0.3 val x5 = 0.25 * 0.5 val x7 = 0.25 * 0.7 val x9 = 0.25 * 0.9 val z = x3 + x5 + x7 + x9 - result.get(List(c1Index3)) should be ((x3 / z) +- 0.000000001) - result.get(List(c1Index5)) should be ((x5 / z) +- 0.000000001) - result.get(List(c1Index7)) should be ((x7 / z) +- 0.000000001) - result.get(List(c1Index9)) should be ((x9 / z) +- 0.000000001) + result.get(List(c1Index3)) should be((x3 / z) +- 0.000000001) + result.get(List(c1Index5)) should be((x5 / z) +- 0.000000001) + result.get(List(c1Index7)) should be((x7 / z) +- 0.000000001) + result.get(List(c1Index9)) should be((x9 / z) +- 0.000000001) } } @@ -313,24 +316,24 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) + result.size should equal(4) val x3 = 0.25 * (0.3 * 0.5 + 0.7 * 0.2) val x5 = 0.25 * (0.5 * 0.5 + 0.5 * 0.2) val x7 = 0.25 * (0.7 * 0.5 + 0.3 * 0.2) val x9 = 0.25 * (0.9 * 0.5 + 0.1 * 0.2) val z = x3 + x5 + x7 + x9 - result.get(List(c1Index3)) should be (x3 / z +- 0.000000001) - result.get(List(c1Index5)) should be (x5 / z +- 0.000000001) - result.get(List(c1Index7)) should be (x7 / z +- 0.000000001) - result.get(List(c1Index9)) should be (x9 / z +- 0.000000001) + result.get(List(c1Index3)) should be(x3 / z +- 0.000000001) + result.get(List(c1Index5)) should be(x5 / z +- 0.000000001) + result.get(List(c1Index7)) should be(x7 / z +- 0.000000001) + result.get(List(c1Index9)) should be(x9 / z +- 0.000000001) } } @@ -358,24 +361,24 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) + result.size should equal(4) val x3 = 0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1) val x5 = 0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1) val x7 = 0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1) val x9 = 0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1) val z = x3 + x5 + x7 + x9 - result.get(List(c1Index3)) should be (x3 / z +- 0.000000001) - result.get(List(c1Index5)) should be (x5 / z +- 0.000000001) - result.get(List(c1Index7)) should be (x7 / z +- 0.000000001) - result.get(List(c1Index9)) should be (x9 / z +- 0.000000001) + result.get(List(c1Index3)) should be(x3 / z +- 0.000000001) + result.get(List(c1Index5)) should be(x5 / z +- 0.000000001) + result.get(List(c1Index7)) should be(x7 / z +- 0.000000001) + result.get(List(c1Index9)) should be(x9 / z +- 0.000000001) } } @@ -403,24 +406,24 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) + result.size should equal(4) val x3 = 0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1) val x5 = 0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1) val x7 = 0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1) val x9 = 0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1) val z = x3 + x5 + x7 + x9 - result.get(List(c1Index3)) should be (x3 / z +- 0.000000001) - result.get(List(c1Index5)) should be (x5 / z +- 0.000000001) - result.get(List(c1Index7)) should be (x7 / z +- 0.000000001) - result.get(List(c1Index9)) should be (x9 / z +- 0.000000001) + result.get(List(c1Index3)) should be(x3 / z +- 0.000000001) + result.get(List(c1Index5)) should be(x5 / z +- 0.000000001) + result.get(List(c1Index7)) should be(x7 / z +- 0.000000001) + result.get(List(c1Index9)) should be(x9 / z +- 0.000000001) } } @@ -449,18 +452,18 @@ class BPSolverTest extends WordSpec with Matchers { c11.makeConstraintFactors() c12.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - pr.globals should equal (Set(c2)) + pr.globals should equal(Set(c2)) val result = multiplyAll(pr.solution) val c2Index1 = c2.variable.range.indexOf(Regular(ec1)) val c2Index2 = c2.variable.range.indexOf(Regular(ec2)) - result.size should equal (2) + result.size should equal(2) val x1 = (0.8 * 0.6) val x2 = (0.2 * 0.3) val z = x1 + x2 - result.get(List(c2Index1)) should be ((x1 / z) +- 0.000000001) - result.get(List(c2Index2)) should be ((x2 / z) +- 0.000000001) + result.get(List(c2Index1)) should be((x1 / z) +- 0.000000001) + result.get(List(c2Index2)) should be((x2 / z) +- 0.000000001) } } @@ -480,12 +483,12 @@ class BPSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c1.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) - result.get(List(c2IndexT)) should be (1.0 +- 0.000000001) - result.get(List(c2IndexF)) should be (0.0 +- 0.000000001) + result.get(List(c2IndexT)) should be(1.0 +- 0.000000001) + result.get(List(c2IndexF)) should be(0.0 +- 0.000000001) } "with a constraint on an element that is used multiple times, only factor in the constraint once" in { @@ -523,7 +526,7 @@ class BPSolverTest extends WordSpec with Matchers { ce1.makeConstraintFactors() ce2.makeConstraintFactors() cd.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) // Probability that f1 is true = 0.6 // Probability that e1 is true = 1.0 @@ -534,7 +537,7 @@ class BPSolverTest extends WordSpec with Matchers { val dIndexF = cd.variable.range.indexOf(Regular(false)) val pT = result.get(List(dIndexT)) val pF = result.get(List(dIndexF)) - (pT / (pT + pF)) should be (0.73 +- 0.000000001) + (pT / (pT + pF)) should be(0.73 +- 0.000000001) } "with elements that are not used by the query or evidence, produce the correct result" in { @@ -559,13 +562,13 @@ class BPSolverTest extends WordSpec with Matchers { cu.makeConstraintFactors() cf.makeConstraintFactors() ca.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val fIndexT = cf.variable.range.indexOf(Regular(true)) val fIndexF = cf.variable.range.indexOf(Regular(false)) val pT = result.get(List(fIndexT)) val pF = result.get(List(fIndexF)) - (pT / (pT + pF)) should be (0.6 +- 0.01) + (pT / (pT + pF)) should be(0.6 +- 0.01) } "with a model using chain and no conditions or constraints, when the outcomes are at the top level, produce the correct answer" in { @@ -596,10 +599,10 @@ class BPSolverTest extends WordSpec with Matchers { c2.makeConstraintFactors() c3.makeConstraintFactors() c4.makeConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) - result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) + result.get(List(c4Index1)) should be((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and no conditions or constraints, when the outcomes are nested, produce the correct answer" in { @@ -627,17 +630,17 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) - result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) + result.get(List(c4Index1)) should be((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on the result, when the outcomes are at the top level, correctly condition the parent" in { Universe.createNew() - val e1= Flip(0.3) + val e1 = Flip(0.3) val e2 = Select(0.1 -> 1, 0.9 -> 2) val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) @@ -665,19 +668,19 @@ class BPSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) val c1IndexF = c1.variable.range.indexOf(Regular(false)) val pT = result.get(List(c1IndexT)) val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be ((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) + (pT / (pT + pF)) should be((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) } "with a model using chain and a condition on the result, when the outcomes are nested, correctly condition the parent" in { Universe.createNew() - val e1= Flip(0.3) + val e1 = Flip(0.3) val e2 = Select(0.1 -> 1, 0.9 -> 2) val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) @@ -702,16 +705,16 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) val c1IndexF = c1.variable.range.indexOf(Regular(false)) val pT = result.get(List(c1IndexT)) val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be ((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) + (pT / (pT + pF)) should be((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are at the top level, correctly condition the result" in { @@ -743,7 +746,7 @@ class BPSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -752,48 +755,48 @@ class BPSolverTest extends WordSpec with Matchers { val p1 = result.get(List(c4Index1)) val p2 = result.get(List(c4Index2)) val p3 = result.get(List(c4Index3)) - (p1 / (p1 + p2 + p3)) should be ((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) + (p1 / (p1 + p2 + p3)) should be((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are at the top level, " + - "not change the belief about the parent" in { - Universe.createNew() - val e1 = Flip(0.3) - val e2 = Select(0.1 -> 1, 0.9 -> 2) - val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) - e2.observe(1) - val cc = new ComponentCollection - val pr = new Problem(cc, List(e1)) - pr.add(e2) - pr.add(e3) - pr.add(e4) - val c1 = cc(e1) - val c2 = cc(e2) - val c3 = cc(e3) - val c4 = cc(e4) - c1.generateRange() - c2.generateRange() - c3.generateRange() - c4.expand() - c4.generateRange() - c1.makeConstraintFactors() - c2.makeConstraintFactors() - c3.makeConstraintFactors() - c4.makeConstraintFactors() - c1.makeNonConstraintFactors() - c2.makeNonConstraintFactors() - c3.makeNonConstraintFactors() - c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + "not change the belief about the parent" in { + Universe.createNew() + val e1 = Flip(0.3) + val e2 = Select(0.1 -> 1, 0.9 -> 2) + val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) + e2.observe(1) + val cc = new ComponentCollection + val pr = new Problem(cc, List(e1)) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.expand() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - val result = multiplyAll(pr.solution) - val c1IndexT = c1.variable.range.indexOf(Regular(true)) - val c1IndexF = c1.variable.range.indexOf(Regular(false)) - val pT = result.get(List(c1IndexT)) - val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be (0.3 +- 0.01) - } + val result = multiplyAll(pr.solution) + val c1IndexT = c1.variable.range.indexOf(Regular(true)) + val c1IndexF = c1.variable.range.indexOf(Regular(false)) + val pT = result.get(List(c1IndexT)) + val pF = result.get(List(c1IndexF)) + (pT / (pT + pF)) should be(0.3 +- 0.01) + } "with a model using chain and a condition on one of the outcome elements, when the outcomes are nested, correctly condition the result" in { Universe.createNew() @@ -822,9 +825,9 @@ class BPSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -833,52 +836,185 @@ class BPSolverTest extends WordSpec with Matchers { val p1 = result.get(List(c4Index1)) val p2 = result.get(List(c4Index2)) val p3 = result.get(List(c4Index3)) - (p1 / (p1 + p2 + p3)) should be ((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) + (p1 / (p1 + p2 + p3)) should be((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are nested, " + - "not change the belief about the parent" in { - Universe.createNew() - val e1 = Flip(0.3) - val e2 = Select(0.1 -> 1, 0.9 -> 2) - val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) - e2.observe(1) + "not change the belief about the parent" in { + Universe.createNew() + val e1 = Flip(0.3) + val e2 = Select(0.1 -> 1, 0.9 -> 2) + val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) + e2.observe(1) - val cc = new ComponentCollection - val pr = new Problem(cc, List(e1)) - pr.add(e4) - val c1 = cc(e1) - val c4 = cc(e4) - c1.generateRange() - c4.expand() - val c2 = cc(e2) - val c3 = cc(e3) - c2.generateRange() - c3.generateRange() - c4.generateRange() - c1.makeConstraintFactors() - c2.makeConstraintFactors() - c3.makeConstraintFactors() - c4.makeConstraintFactors() - c1.makeNonConstraintFactors() - c2.makeNonConstraintFactors() - c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) - c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(beliefPropagation())) + val cc = new ComponentCollection + val pr = new Problem(cc, List(e1)) + pr.add(e4) + val c1 = cc(e1) + val c4 = cc(e4) + c1.generateRange() + c4.expand() + val c2 = cc(e2) + val c3 = cc(e3) + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) + c4.makeNonConstraintFactors() + pr.solve(new ConstantStrategy(marginalBeliefPropagation())) - val result = multiplyAll(pr.solution) - val c1IndexT = c1.variable.range.indexOf(Regular(true)) - val c1IndexF = c1.variable.range.indexOf(Regular(false)) - val pT = result.get(List(c1IndexT)) - val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be (0.3 +- 0.01) + val result = multiplyAll(pr.solution) + val c1IndexT = c1.variable.range.indexOf(Regular(true)) + val c1IndexF = c1.variable.range.indexOf(Regular(false)) + val pT = result.get(List(c1IndexT)) + val pF = result.get(List(c1IndexF)) + (pT / (pT + pF)) should be(0.3 +- 0.01) + } + + } + + "Running MPE VariableElimination" when { + "given a target" should { + "produce the most likely factor over the target" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val pr = new Problem(cc, List(e1)) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + // If we leave e1 un-eliminated, we should end up with a factor that has e1=.2 at .48 and e1=.3 at .1225 + // However, since BP normalizes according to a MaxProduct semiring, the values are not normalized, so we look at the ratio + pr.solve(new ConstantStrategy(mpeBeliefPropagation(20))) + val f = pr.solution reduceLeft (_.product(_)) + f.numVars should equal(1) + if (f.get(List(0)) > f.get(List(1))) { + f.get(List(0)) / f.get(List(1)) should be(0.48 / 0.1225 +- 0.000001) + } else { + f.get(List(1)) / f.get(List(0)) should be(0.48 / 0.1225 +- 0.000001) + } + } } + "given a flat model" should { + "produce the correct most likely values for all elements with no conditions or constraints" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val pr = new Problem(cc, List()) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + pr.solve(new ConstantStrategy(mpeBeliefPropagation(20))) + pr.recordingFactors(c1.variable).get(List()).asInstanceOf[Double] should be(0.2 +- .0000001) + pr.recordingFactors(c2.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c3.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c4.variable).get(List()).asInstanceOf[Boolean] should be(true) + } + + "produce the correct most likely values for all elements with conditions and constraints" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.5 -> 0.2, 0.5 -> 0.3) + e1.addConstraint((d: Double) => if (d < 0.25) 3.0 else 1.0) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + e4.observe(true) + val pr = new Problem(cc, List()) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // MPE: e1=.2,e2=F,e3=F,e4=T + pr.solve(new ConstantStrategy(mpeBeliefPropagation(20))) + pr.recordingFactors(c1.variable).get(List()).asInstanceOf[Double] should be(0.2 +- .0000001) + pr.recordingFactors(c2.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c3.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c4.variable).get(List()).asInstanceOf[Boolean] should be(true) + } + } } def multiplyAll(factors: List[Factor[Double]]): Factor[Double] = factors.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) - class EC1 extends ElementCollection { } + class EC1 extends ElementCollection {} } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/GibbsSolverTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/GibbsSolverTest.scala index 2f270281..c00ceb91 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/GibbsSolverTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/GibbsSolverTest.scala @@ -55,7 +55,7 @@ class GibbsSolverTest extends WordSpec with Matchers { val solver = new GibbsSolver(pr, Set(), Set(v2 , v3), pr.components.flatMap(_.nonConstraintFactors), 1, 0, 1, BlockSampler.default) // Call initialize to set solver.variables so createBlocks may be called solver.initialize() - solver.createBlocks().map(_.toSet) should contain theSameElementsAs(List(Set(v1, v3), Set(v2, v3))) + solver.createBlocks().map(_.toSet) should contain theSameElementsAs List(Set(v1, v3), Set(v2, v3)) } "given a problem that uses Chain" in { @@ -93,15 +93,90 @@ class GibbsSolverTest extends WordSpec with Matchers { // Call initialize to set solver.variables so createBlocks may be called solver.initialize() val v5 = (solver.variables -- Set(v1, v2, v3, v4)).head - solver.createBlocks().map(_.toSet) should contain theSameElementsAs(List(Set(v1, v5), Set(v2, v4, v5), Set(v3, v4, v5))) + solver.createBlocks().map(_.toSet) should contain theSameElementsAs List(Set(v1, v5), Set(v2, v4, v5), Set(v3, v4, v5)) + } + + "given a Chain with compact factors" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val e1 = Flip(0.4) + val e2 = Chain(e1, (b: Boolean) => if(b) Uniform(1, 2, 3, 4) else Uniform(3, 4, 5)) + val e3 = Apply(e2, (i: Int) => i * 2) + val pr = new Problem(cc, List(e3)) + pr.add(e1) + pr.add(e2) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + c1.generateRange() + c1.makeNonConstraintFactors() + c2.expand() + for((_, spr) <- c2.subproblems) { + val target = cc(spr.target) + target.generateRange() + target.makeNonConstraintFactors() + spr.solve(new ConstantStrategy(marginalVariableElimination)) + } + c2.generateRange() + c2.makeNonConstraintFactors() + c3.generateRange() + c3.makeNonConstraintFactors() + val v1 = c1.variable + val v2 = c2.variable + val v3 = c3.variable + + val solver = new GibbsSolver(pr, Set(), Set(v3), pr.components.flatMap(_.nonConstraintFactors), 1, 0, 1, BlockSampler.default) + // Call initialize to set solver.variables so createBlocks may be called + solver.initialize() + // Chain should not be blocked with parent, but should still be blocked with its deterministic children + solver.createBlocks().map(_.toSet) should contain theSameElementsAs List(Set(v1), Set(v2, v3)) } } } // Largely the same as VESolver tests, but modified to include tolerance and replacing Dist where needed "Running Gibbs without *" when { - "given a flat model with no conditions or constraints" should { + "produce the correct result with a compact Chain factor" in { + Universe.createNew() + val cc = new ComponentCollection + cc.useSingleChainFactor = true + val e1 = Flip(0.4) + val e2 = Chain(e1, (b: Boolean) => if(b) Uniform(1, 2) else Uniform(2, 3)) + val e3 = Apply(e2, (i: Int) => i * 2) + val pr = new Problem(cc, List(e3)) + pr.add(e1) + pr.add(e2) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + c1.generateRange() + c1.makeNonConstraintFactors() + c2.expand() + for((_, spr) <- c2.subproblems) { + val target = cc(spr.target) + target.generateRange() + target.makeNonConstraintFactors() + spr.solve(new ConstantStrategy(marginalVariableElimination)) + } + c2.generateRange() + c2.makeNonConstraintFactors() + c3.generateRange() + c3.makeNonConstraintFactors() + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) + + val result = multiplyAll(pr.solution) + result.variables should equal (List(c3.variable)) + result.size should equal (3) + val c3Index2 = c3.variable.range.indexOf(Regular(2)) + val c3Index4 = c3.variable.range.indexOf(Regular(4)) + val c3Index6 = c3.variable.range.indexOf(Regular(6)) + result.get(List(c3Index2)) should be (0.2 +- tol) + result.get(List(c3Index4)) should be (0.5 +- tol) + result.get(List(c3Index6)) should be (0.3 +- tol) + } + "produce the correct result over a single element" in { Universe.createNew() val cc = new ComponentCollection @@ -123,7 +198,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c2)) pr.solved should equal (true) @@ -156,7 +231,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c2, c3)) val result = multiplyAll(pr.solution) @@ -208,7 +283,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c1)) val result = multiplyAll(pr.solution) @@ -252,7 +327,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c1)) val result = multiplyAll(pr.solution) @@ -297,7 +372,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c1)) val result = multiplyAll(pr.solution) @@ -342,7 +417,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c1)) val result = multiplyAll(pr.solution) @@ -388,7 +463,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c11.makeConstraintFactors() c12.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) pr.globals should equal (Set(c2)) val result = multiplyAll(pr.solution) @@ -419,7 +494,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c1.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) @@ -469,7 +544,7 @@ class GibbsSolverTest extends WordSpec with Matchers { ce2.makeConstraintFactors() cf3.makeConstraintFactors() cd.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) // Probability that f1 is true = 0.6 // Probability that e1 is true = 1.0 @@ -505,7 +580,7 @@ class GibbsSolverTest extends WordSpec with Matchers { cu.makeConstraintFactors() cf.makeConstraintFactors() ca.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val fIndexT = cf.variable.range.indexOf(Regular(true)) val fIndexF = cf.variable.range.indexOf(Regular(false)) @@ -542,7 +617,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c2.makeConstraintFactors() c3.makeConstraintFactors() c4.makeConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- tol) @@ -573,9 +648,9 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default)))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default)))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- tol) @@ -611,7 +686,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) @@ -648,9 +723,9 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default)))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default)))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) @@ -689,7 +764,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -731,7 +806,7 @@ class GibbsSolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) @@ -768,9 +843,9 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default)))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default)))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -810,9 +885,9 @@ class GibbsSolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default)))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default)))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(gibbs(10000, 0, 1, BlockSampler.default))) + pr.solve(new ConstantStrategy(marginalGibbs(10000, 0, 1, BlockSampler.default))) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VEBPChooserTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VEBPChooserTest.scala index 385455ac..aadae275 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VEBPChooserTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VEBPChooserTest.scala @@ -469,7 +469,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.PositiveInfinity, 100)) @@ -545,7 +545,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.PositiveInfinity, 100)) @@ -649,6 +649,8 @@ class VEBPChooserTest extends WordSpec with Matchers { val cc = new ComponentCollection val pr = new Problem(cc, List(e4)) pr.add(e1) + pr.add(e2) + pr.add(e3) val c1 = cc(e1) val c4 = cc(e4) c1.generateRange() @@ -665,7 +667,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.PositiveInfinity, 100)) @@ -690,6 +692,8 @@ class VEBPChooserTest extends WordSpec with Matchers { e2.observe(1) val cc = new ComponentCollection val pr = new Problem(cc, List(e1)) + pr.add(e2) + pr.add(e3) pr.add(e4) val c1 = cc(e1) val c4 = cc(e4) @@ -707,7 +711,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.PositiveInfinity, 100)) @@ -777,7 +781,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) pr.globals should equal (Set(c2, c3)) val result = multiplyAll(pr.solution) @@ -1187,7 +1191,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.NegativeInfinity, 100)) val result = multiplyAll(pr.solution) @@ -1262,7 +1266,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.NegativeInfinity, 100)) @@ -1382,7 +1386,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.NegativeInfinity, 100)) @@ -1424,7 +1428,7 @@ class VEBPChooserTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(beliefPropagation()))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalBeliefPropagation()))) c4.makeNonConstraintFactors() pr.solve(new VEBPStrategy(Double.NegativeInfinity, 100)) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VESolverTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VESolverTest.scala index 991c1fe9..255ff8ef 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VESolverTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/solver/VESolverTest.scala @@ -12,7 +12,7 @@ */ package com.cra.figaro.test.algorithm.structured.solver -import org.scalatest.{WordSpec, Matchers} +import org.scalatest.{ WordSpec, Matchers } import com.cra.figaro.language._ import com.cra.figaro.library.compound._ import com.cra.figaro.algorithm.factored.factors.Factor @@ -22,7 +22,8 @@ import com.cra.figaro.algorithm.lazyfactored.Regular import com.cra.figaro.algorithm.structured.strategy.solve._ import com.cra.figaro.algorithm.structured._ import com.cra.figaro.algorithm.structured.solver._ - +import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredMPEVE +import com.cra.figaro.library.atomic.discrete.Uniform class VESolverTest extends WordSpec with Matchers { "Running VariableElimination without *" when { @@ -48,17 +49,17 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c2)) - pr.solved should equal (true) + pr.globals should equal(Set(c2)) + pr.solved should equal(true) val result = multiplyAll(pr.solution) - result.variables should equal (List(c2.variable)) - result.size should equal (2) + result.variables should equal(List(c2.variable)) + result.size should equal(2) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) - result.get(List(c2IndexT)) should be (0.6 +- 0.00000001) - result.get(List(c2IndexF)) should be (0.4 +- 0.00000001) + result.get(List(c2IndexT)) should be(0.6 +- 0.00000001) + result.get(List(c2IndexF)) should be(0.4 +- 0.00000001) } "produce the correct result over multiple elements" in { @@ -81,31 +82,31 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c2, c3)) + pr.globals should equal(Set(c2, c3)) val result = multiplyAll(pr.solution) - result.variables.size should equal (2) + result.variables.size should equal(2) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) val c3IndexT = c3.variable.range.indexOf(Regular(true)) val c3IndexF = c3.variable.range.indexOf(Regular(false)) - result.size should equal (4) + result.size should equal(4) val var0 = result.variables(0) val var1 = result.variables(1) if (var0 == c2.variable) { - var1 should equal (c3.variable) - result.get(List(c2IndexT, c3IndexT)) should equal (0.6) - result.get(List(c2IndexT, c3IndexF)) should equal (0.0) - result.get(List(c2IndexF, c3IndexT)) should equal (0.0) - result.get(List(c2IndexF, c3IndexF)) should equal (0.4) + var1 should equal(c3.variable) + result.get(List(c2IndexT, c3IndexT)) should equal(0.6) + result.get(List(c2IndexT, c3IndexF)) should equal(0.0) + result.get(List(c2IndexF, c3IndexT)) should equal(0.0) + result.get(List(c2IndexF, c3IndexF)) should equal(0.4) } else { - var0 should equal (c3.variable) - var1 should equal (c2.variable) - result.get(List(c3IndexT, c2IndexT)) should equal (0.6) - result.get(List(c3IndexT, c2IndexF)) should equal (0.0) - result.get(List(c3IndexF, c2IndexT)) should equal (0.0) - result.get(List(c3IndexF, c2IndexF)) should equal (0.4) + var0 should equal(c3.variable) + var1 should equal(c2.variable) + result.get(List(c3IndexT, c2IndexT)) should equal(0.6) + result.get(List(c3IndexT, c2IndexF)) should equal(0.0) + result.get(List(c3IndexF, c2IndexT)) should equal(0.0) + result.get(List(c3IndexF, c2IndexF)) should equal(0.4) } } } @@ -133,19 +134,19 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) - result.get(List(c1Index3)) should be ((0.25 * 0.3) +- 0.000000001) - result.get(List(c1Index5)) should be ((0.25 * 0.5) +- 0.000000001) - result.get(List(c1Index7)) should be ((0.25 * 0.7) +- 0.000000001) - result.get(List(c1Index9)) should be ((0.25 * 0.9) +- 0.000000001) + result.size should equal(4) + result.get(List(c1Index3)) should be((0.25 * 0.3) +- 0.000000001) + result.get(List(c1Index5)) should be((0.25 * 0.5) +- 0.000000001) + result.get(List(c1Index7)) should be((0.25 * 0.7) +- 0.000000001) + result.get(List(c1Index9)) should be((0.25 * 0.9) +- 0.000000001) } } @@ -172,19 +173,19 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) - result.get(List(c1Index3)) should be ((0.25 * (0.3 * 0.5 + 0.7 * 0.2)) +- 0.000000001) - result.get(List(c1Index5)) should be ((0.25 * (0.5 * 0.5 + 0.5 * 0.2)) +- 0.000000001) - result.get(List(c1Index7)) should be ((0.25 * (0.7 * 0.5 + 0.3 * 0.2)) +- 0.000000001) - result.get(List(c1Index9)) should be ((0.25 * (0.9 * 0.5 + 0.1 * 0.2)) +- 0.000000001) + result.size should equal(4) + result.get(List(c1Index3)) should be((0.25 * (0.3 * 0.5 + 0.7 * 0.2)) +- 0.000000001) + result.get(List(c1Index5)) should be((0.25 * (0.5 * 0.5 + 0.5 * 0.2)) +- 0.000000001) + result.get(List(c1Index7)) should be((0.25 * (0.7 * 0.5 + 0.3 * 0.2)) +- 0.000000001) + result.get(List(c1Index9)) should be((0.25 * (0.9 * 0.5 + 0.1 * 0.2)) +- 0.000000001) } } @@ -212,19 +213,19 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) - result.get(List(c1Index3)) should be ((0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index5)) should be ((0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index7)) should be ((0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index9)) should be ((0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1)) +- 0.000000001) + result.size should equal(4) + result.get(List(c1Index3)) should be((0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index5)) should be((0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index7)) should be((0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index9)) should be((0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1)) +- 0.000000001) } } @@ -252,19 +253,19 @@ class VESolverTest extends WordSpec with Matchers { c1.makeConstraintFactors() c2.makeConstraintFactors() c3.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c1)) + pr.globals should equal(Set(c1)) val result = multiplyAll(pr.solution) val c1Index3 = c1.variable.range.indexOf(Regular(0.3)) val c1Index5 = c1.variable.range.indexOf(Regular(0.5)) val c1Index7 = c1.variable.range.indexOf(Regular(0.7)) val c1Index9 = c1.variable.range.indexOf(Regular(0.9)) - result.size should equal (4) - result.get(List(c1Index3)) should be ((0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index5)) should be ((0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index7)) should be ((0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1)) +- 0.000000001) - result.get(List(c1Index9)) should be ((0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1)) +- 0.000000001) + result.size should equal(4) + result.get(List(c1Index3)) should be((0.25 * (0.3 * 0.5 * 0.4 + 0.7 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index5)) should be((0.25 * (0.5 * 0.5 * 0.4 + 0.5 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index7)) should be((0.25 * (0.7 * 0.5 * 0.4 + 0.3 * 0.2 * 0.1)) +- 0.000000001) + result.get(List(c1Index9)) should be((0.25 * (0.9 * 0.5 * 0.4 + 0.1 * 0.2 * 0.1)) +- 0.000000001) } } @@ -293,15 +294,15 @@ class VESolverTest extends WordSpec with Matchers { c11.makeConstraintFactors() c12.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) - pr.globals should equal (Set(c2)) + pr.globals should equal(Set(c2)) val result = multiplyAll(pr.solution) val c2Index1 = c2.variable.range.indexOf(Regular(ec1)) val c2Index2 = c2.variable.range.indexOf(Regular(ec2)) - result.size should equal (2) - result.get(List(c2Index1)) should be ((0.8 * 0.6) +- 0.000000001) - result.get(List(c2Index2)) should be ((0.2 * 0.3) +- 0.000000001) + result.size should equal(2) + result.get(List(c2Index1)) should be((0.8 * 0.6) +- 0.000000001) + result.get(List(c2Index2)) should be((0.2 * 0.3) +- 0.000000001) } } @@ -321,12 +322,12 @@ class VESolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c1.makeConstraintFactors() c2.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c2IndexT = c2.variable.range.indexOf(Regular(true)) val c2IndexF = c2.variable.range.indexOf(Regular(false)) - result.get(List(c2IndexT)) should be (1.0 +- 0.000000001) - result.get(List(c2IndexF)) should be (0.0 +- 0.000000001) + result.get(List(c2IndexT)) should be(1.0 +- 0.000000001) + result.get(List(c2IndexF)) should be(0.0 +- 0.000000001) } "with a constraint on an element that is used multiple times, only factor in the constraint once" in { @@ -364,7 +365,7 @@ class VESolverTest extends WordSpec with Matchers { ce1.makeConstraintFactors() ce2.makeConstraintFactors() cd.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) // Probability that f1 is true = 0.6 // Probability that e1 is true = 1.0 @@ -375,7 +376,7 @@ class VESolverTest extends WordSpec with Matchers { val dIndexF = cd.variable.range.indexOf(Regular(false)) val pT = result.get(List(dIndexT)) val pF = result.get(List(dIndexF)) - (pT / (pT + pF)) should be (0.73 +- 0.000000001) + (pT / (pT + pF)) should be(0.73 +- 0.000000001) } "with elements that are not used by the query or evidence, produce the correct result" in { @@ -400,13 +401,13 @@ class VESolverTest extends WordSpec with Matchers { cu.makeConstraintFactors() cf.makeConstraintFactors() ca.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val fIndexT = cf.variable.range.indexOf(Regular(true)) val fIndexF = cf.variable.range.indexOf(Regular(false)) val pT = result.get(List(fIndexT)) val pF = result.get(List(fIndexF)) - (pT / (pT + pF)) should be (0.6 +- 0.000000001) + (pT / (pT + pF)) should be(0.6 +- 0.000000001) } "with a model using chain and no conditions or constraints, when the outcomes are at the top level, produce the correct answer" in { @@ -437,10 +438,10 @@ class VESolverTest extends WordSpec with Matchers { c2.makeConstraintFactors() c3.makeConstraintFactors() c4.makeConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) - result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) + result.get(List(c4Index1)) should be((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and no conditions or constraints, when the outcomes are nested, produce the correct answer" in { @@ -468,17 +469,17 @@ class VESolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) - result.get(List(c4Index1)) should be ((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) + result.get(List(c4Index1)) should be((0.3 * 0.1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on the result, when the outcomes are at the top level, correctly condition the parent" in { Universe.createNew() - val e1= Flip(0.3) + val e1 = Flip(0.3) val e2 = Select(0.1 -> 1, 0.9 -> 2) val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) @@ -506,19 +507,19 @@ class VESolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) val c1IndexF = c1.variable.range.indexOf(Regular(false)) val pT = result.get(List(c1IndexT)) val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be ((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) + (pT / (pT + pF)) should be((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) } "with a model using chain and a condition on the result, when the outcomes are nested, correctly condition the parent" in { Universe.createNew() - val e1= Flip(0.3) + val e1 = Flip(0.3) val e2 = Select(0.1 -> 1, 0.9 -> 2) val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) @@ -543,16 +544,16 @@ class VESolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c1IndexT = c1.variable.range.indexOf(Regular(true)) val c1IndexF = c1.variable.range.indexOf(Regular(false)) val pT = result.get(List(c1IndexT)) val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be ((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) + (pT / (pT + pF)) should be((0.3 * 0.1 / (0.3 * 0.1 + 0.7 * 0.7)) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are at the top level, correctly condition the result" in { @@ -584,7 +585,7 @@ class VESolverTest extends WordSpec with Matchers { c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -593,48 +594,48 @@ class VESolverTest extends WordSpec with Matchers { val p1 = result.get(List(c4Index1)) val p2 = result.get(List(c4Index2)) val p3 = result.get(List(c4Index3)) - (p1 / (p1 + p2 + p3)) should be ((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) + (p1 / (p1 + p2 + p3)) should be((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are at the top level, " + - "not change the belief about the parent" in { - Universe.createNew() - val e1 = Flip(0.3) - val e2 = Select(0.1 -> 1, 0.9 -> 2) - val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) - e2.observe(1) - val cc = new ComponentCollection - val pr = new Problem(cc, List(e1)) - pr.add(e2) - pr.add(e3) - pr.add(e4) - val c1 = cc(e1) - val c2 = cc(e2) - val c3 = cc(e3) - val c4 = cc(e4) - c1.generateRange() - c2.generateRange() - c3.generateRange() - c4.expand() - c4.generateRange() - c1.makeConstraintFactors() - c2.makeConstraintFactors() - c3.makeConstraintFactors() - c4.makeConstraintFactors() - c1.makeNonConstraintFactors() - c2.makeNonConstraintFactors() - c3.makeNonConstraintFactors() - c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + "not change the belief about the parent" in { + Universe.createNew() + val e1 = Flip(0.3) + val e2 = Select(0.1 -> 1, 0.9 -> 2) + val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) + e2.observe(1) + val cc = new ComponentCollection + val pr = new Problem(cc, List(e1)) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.expand() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + pr.solve(new ConstantStrategy(marginalVariableElimination)) - val result = multiplyAll(pr.solution) - val c1IndexT = c1.variable.range.indexOf(Regular(true)) - val c1IndexF = c1.variable.range.indexOf(Regular(false)) - val pT = result.get(List(c1IndexT)) - val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be (0.3 +- 0.000000001) - } + val result = multiplyAll(pr.solution) + val c1IndexT = c1.variable.range.indexOf(Regular(true)) + val c1IndexF = c1.variable.range.indexOf(Regular(false)) + val pT = result.get(List(c1IndexT)) + val pF = result.get(List(c1IndexF)) + (pT / (pT + pF)) should be(0.3 +- 0.000000001) + } "with a model using chain and a condition on one of the outcome elements, when the outcomes are nested, correctly condition the result" in { Universe.createNew() @@ -647,6 +648,8 @@ class VESolverTest extends WordSpec with Matchers { val cc = new ComponentCollection val pr = new Problem(cc, List(e4)) pr.add(e1) + pr.add(e2) + pr.add(e3) val c1 = cc(e1) val c4 = cc(e4) c1.generateRange() @@ -663,9 +666,9 @@ class VESolverTest extends WordSpec with Matchers { c1.makeNonConstraintFactors() c2.makeNonConstraintFactors() c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + pr.solve(new ConstantStrategy(marginalVariableElimination)) val result = multiplyAll(pr.solution) val c4Index1 = c4.variable.range.indexOf(Regular(1)) @@ -674,52 +677,183 @@ class VESolverTest extends WordSpec with Matchers { val p1 = result.get(List(c4Index1)) val p2 = result.get(List(c4Index2)) val p3 = result.get(List(c4Index3)) - (p1 / (p1 + p2 + p3)) should be ((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) + (p1 / (p1 + p2 + p3)) should be((0.3 * 1 + 0.7 * 0.7) +- 0.000000001) } "with a model using chain and a condition on one of the outcome elements, when the outcomes are nested, " + - "not change the belief about the parent" in { - Universe.createNew() - val e1 = Flip(0.3) - val e2 = Select(0.1 -> 1, 0.9 -> 2) - val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) - val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) - e2.observe(1) + "not change the belief about the parent" in { + Universe.createNew() + val e1 = Flip(0.3) + val e2 = Select(0.1 -> 1, 0.9 -> 2) + val e3 = Select(0.7 -> 1, 0.2 -> 2, 0.1 -> 3) + val e4 = Chain(e1, (b: Boolean) => if (b) e2; else e3) + e2.observe(1) - val cc = new ComponentCollection - val pr = new Problem(cc, List(e1)) - pr.add(e4) - val c1 = cc(e1) - val c4 = cc(e4) - c1.generateRange() - c4.expand() - val c2 = cc(e2) - val c3 = cc(e3) - c2.generateRange() - c3.generateRange() - c4.generateRange() - c1.makeConstraintFactors() - c2.makeConstraintFactors() - c3.makeConstraintFactors() - c4.makeConstraintFactors() - c1.makeNonConstraintFactors() - c2.makeNonConstraintFactors() - c3.makeNonConstraintFactors() - c4.subproblems.values.foreach(_.solve(new ConstantStrategy(variableElimination))) - c4.makeNonConstraintFactors() - pr.solve(new ConstantStrategy(variableElimination)) + val cc = new ComponentCollection + val pr = new Problem(cc, List(e1)) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c4 = cc(e4) + c1.generateRange() + c4.expand() + val c2 = cc(e2) + val c3 = cc(e3) + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.subproblems.values.foreach(_.solve(new ConstantStrategy(marginalVariableElimination))) + c4.makeNonConstraintFactors() + pr.solve(new ConstantStrategy(marginalVariableElimination)) - val result = multiplyAll(pr.solution) - val c1IndexT = c1.variable.range.indexOf(Regular(true)) - val c1IndexF = c1.variable.range.indexOf(Regular(false)) - val pT = result.get(List(c1IndexT)) - val pF = result.get(List(c1IndexF)) - (pT / (pT + pF)) should be (0.3 +- 0.000000001) + val result = multiplyAll(pr.solution) + val c1IndexT = c1.variable.range.indexOf(Regular(true)) + val c1IndexF = c1.variable.range.indexOf(Regular(false)) + val pT = result.get(List(c1IndexT)) + val pF = result.get(List(c1IndexF)) + (pT / (pT + pF)) should be(0.3 +- 0.000000001) + } + + } + + "Running MPE VariableElimination" when { + "given a target" should { + "produce the most likely factor over the target" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val pr = new Problem(cc, List(e1)) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + // If we leave e1 un-eliminated, we should end up with a factor that has e1=.2 at .48 and e1=.3 at .1225 + pr.solve(new ConstantStrategy(mpeVariableElimination)) + val f = pr.solution reduceLeft (_.product(_)) + f.numVars should equal(1) + f.get(List(0)) should be({ if (c1.variable.range(0).value == .2) 0.48 else 0.1225 } +- 0.000000001) + f.get(List(1)) should be({ if (c1.variable.range(1).value == .2) 0.48 else 0.1225 } +- 0.000000001) + } } + "given a flat model" should { + "produce the correct most likely values for all elements with no conditions or constraints" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val pr = new Problem(cc, List()) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + pr.solve(new ConstantStrategy(mpeVariableElimination)) + pr.recordingFactors(c1.variable).get(List()).asInstanceOf[Double] should be(0.2 +- .0000001) + pr.recordingFactors(c2.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c3.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c4.variable).get(List()).asInstanceOf[Boolean] should be(true) + } + + "produce the correct most likely values for all elements with conditions and constraints" in { + Universe.createNew() + val cc = new ComponentCollection + val e1 = Select(0.5 -> 0.2, 0.5 -> 0.3) + e1.addConstraint((d: Double) => if (d < 0.25) 3.0 else 1.0) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + e4.observe(true) + val pr = new Problem(cc, List()) + pr.add(e1) + pr.add(e2) + pr.add(e3) + pr.add(e4) + val c1 = cc(e1) + val c2 = cc(e2) + val c3 = cc(e3) + val c4 = cc(e4) + c1.generateRange() + c2.generateRange() + c3.generateRange() + c4.generateRange() + c1.makeConstraintFactors() + c2.makeConstraintFactors() + c3.makeConstraintFactors() + c4.makeConstraintFactors() + c1.makeNonConstraintFactors() + c2.makeNonConstraintFactors() + c3.makeNonConstraintFactors() + c4.makeNonConstraintFactors() + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // MPE: e1=.2,e2=F,e3=F,e4=T + pr.solve(new ConstantStrategy(mpeVariableElimination)) + pr.recordingFactors(c1.variable).get(List()).asInstanceOf[Double] should be(0.2 +- .0000001) + pr.recordingFactors(c2.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c3.variable).get(List()).asInstanceOf[Boolean] should be(false) + pr.recordingFactors(c4.variable).get(List()).asInstanceOf[Boolean] should be(true) + } + } } def multiplyAll(factors: List[Factor[Double]]): Factor[Double] = factors.foldLeft(Factory.unit(SumProductSemiring()))(_.product(_)) - class EC1 extends ElementCollection { } + class EC1 extends ElementCollection {} } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/FlatTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/FlatTest.scala index 4c203b99..c7b4b8c5 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/FlatTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/FlatTest.scala @@ -39,7 +39,7 @@ class FlatTest extends WordSpec with Matchers { }) val cc = new ComponentCollection val problem = new Problem(cc, List(r1)) - val fs = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(variableElimination), defaultRangeSizer, Lower, false) + val fs = DecompositionStrategy.recursiveFlattenStrategy(problem, new ConstantStrategy(marginalVariableElimination), defaultRangeSizer, Lower, false) fs.backwardChain(problem.components , Set()) val factors =problem.components.flatMap(_.nonConstraintFactors) factors.foreach(f => println(f.toReadableString)) @@ -108,6 +108,7 @@ class FlatTest extends WordSpec with Matchers { } } + /* "given a one-level nested model with nested evidence" should { "produce the correct answer" in { Universe.createNew() @@ -119,6 +120,8 @@ class FlatTest extends WordSpec with Matchers { alg.probability(e3, true) should equal (0.6) } } + * + */ "given a two-level nested model" should { "produce the correct answer" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredBPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredBPTest.scala index 66060cb3..bcf67db4 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredBPTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredBPTest.scala @@ -12,12 +12,13 @@ */ package com.cra.figaro.test.algorithm.structured.strategy -import org.scalatest.{WordSpec, Matchers} +import org.scalatest.{ WordSpec, Matchers } import com.cra.figaro.language._ import com.cra.figaro.library.compound.If import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredBP import com.cra.figaro.algorithm.lazyfactored.ValueSet._ import com.cra.figaro.language.Element.toBooleanElement +import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredMPEBP class StructuredBPTest extends WordSpec with Matchers { "Executing a recursive structured BP solver strategy" when { @@ -26,7 +27,7 @@ class StructuredBPTest extends WordSpec with Matchers { Universe.createNew() val e2 = Flip(0.6) val e3 = Apply(e2, (b: Boolean) => b) - StructuredBP.probability(e3, true) should be (0.6 +- 0.000000001) + StructuredBP.probability(e3, true) should be(0.6 +- 0.000000001) } } @@ -36,7 +37,7 @@ class StructuredBPTest extends WordSpec with Matchers { val e1 = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) val e2 = Flip(e1) val e3 = Apply(e2, (b: Boolean) => b) - StructuredBP.probability(e3, true) should be (0.6 +- 0.000000001) + StructuredBP.probability(e3, true) should be(0.6 +- 0.000000001) } } @@ -47,7 +48,7 @@ class StructuredBPTest extends WordSpec with Matchers { val e2 = Flip(e1) val e3 = Apply(e2, (b: Boolean) => b) e3.observe(true) - StructuredBP.probability(e1, 0.3) should be (0.125 +- 0.000000001) + StructuredBP.probability(e1, 0.3) should be(0.125 +- 0.000000001) } } @@ -59,8 +60,8 @@ class StructuredBPTest extends WordSpec with Matchers { val e3 = Apply(e2, (b: Boolean) => b) val alg = StructuredBP(100, e2, e3) alg.start() - alg.probability(e2, true) should be (0.6 +- 0.000000001) - alg.probability(e3, true) should equal (0.6 +- 0.000000001) + alg.probability(e2, true) should be(0.6 +- 0.000000001) + alg.probability(e3, true) should equal(0.6 +- 0.000000001) } } @@ -73,8 +74,8 @@ class StructuredBPTest extends WordSpec with Matchers { e3.observe(true) val alg = StructuredBP(100, e2, e1) alg.start() - alg.probability(e2, true) should equal (1.0) - alg.probability(e1, 0.3) should be (0.125 +- 0.000000001) + alg.probability(e2, true) should equal(1.0) + alg.probability(e1, 0.3) should be(0.125 +- 0.000000001) } } @@ -86,10 +87,11 @@ class StructuredBPTest extends WordSpec with Matchers { val e3 = If(e2, Constant(true), Constant(false)) val alg = StructuredBP(100, e3) alg.start() - alg.probability(e3, true) should be (0.6 +- 0.000000001) + alg.probability(e3, true) should be(0.6 +- 0.000000001) } } + /* "given a one-level nested model with nested evidence" should { "produce the correct answer" in { Universe.createNew() @@ -98,9 +100,11 @@ class StructuredBPTest extends WordSpec with Matchers { val e3 = If(e2, { val e = Flip(0.5); e.observe(true); e }, Constant(false)) val alg = StructuredBP(100, e3) alg.start() - alg.probability(e3, true) should be (0.6 +- 0.000000001) + alg.probability(e3, true) should be(0.6 +- 0.000000001) } } + * + */ "given a two-level nested model" should { "produce the correct answer" in { @@ -110,7 +114,7 @@ class StructuredBPTest extends WordSpec with Matchers { val e3 = If(e2, If(Flip(0.9), Constant(true), Constant(false)), Constant(false)) val alg = StructuredBP(100, e3) alg.start() - alg.probability(e3, true) should be ((0.6 * 0.9) +- 0.000000001) + alg.probability(e3, true) should be((0.6 * 0.9) +- 0.000000001) } } @@ -120,7 +124,7 @@ class StructuredBPTest extends WordSpec with Matchers { val e1 = Flip(0.4) val e2 = Flip(0.3) val e3 = e1 && e2 - StructuredBP.probability(e3, true) should be (0.12 +- 0.000000001) + StructuredBP.probability(e3, true) should be(0.12 +- 0.000000001) } } @@ -130,23 +134,23 @@ class StructuredBPTest extends WordSpec with Matchers { Universe.createNew() val e1 = Apply(Constant(true), (b: Boolean) => { count += 1; 5 }) val e2 = e1 === e1 - StructuredBP.probability(e2, true) should equal (1.0) - count should equal (1) + StructuredBP.probability(e2, true) should equal(1.0) + count should equal(1) // Note that this should now only expand once since Apply Maps have been added to Components } } // The below test is loopy so BP's answer can't be predicted easily -// "expanding an argument that needs another argument later expanded" should { -// "create values for the ancestor argument first" in { -// Universe.createNew() -// val e1 = Flip(0.4) -// val e2 = If(e1, Constant(1), Constant(2)) -// val e3 = Apply(e2, e1, (i: Int, b: Boolean) => if (b) i + 1 else i + 2) -// // e3 is 2 iff e1 is true, because then e2 is 1 -// StructuredBP.probability(e3, 2) should be (0.4 +- 0.000000001) -// } -// } + // "expanding an argument that needs another argument later expanded" should { + // "create values for the ancestor argument first" in { + // Universe.createNew() + // val e1 = Flip(0.4) + // val e2 = If(e1, Constant(1), Constant(2)) + // val e3 = Apply(e2, e1, (i: Int, b: Boolean) => if (b) i + 1 else i + 2) + // // e3 is 2 iff e1 is true, because then e2 is 1 + // StructuredBP.probability(e3, 2) should be (0.4 +- 0.000000001) + // } + // } "solving a problem with a reused nested subproblem" should { "only process the nested subproblem once" in { @@ -158,8 +162,8 @@ class StructuredBPTest extends WordSpec with Matchers { val e1 = Chain(Flip(0.5), f) val e2 = Chain(Flip(0.4), f) val e3 = e1 && e2 - StructuredBP.probability(e3, true) should be ((0.5 * 0.4) +- 0.000000001) - count should equal (2) // One each for p = true and p = false, but only expanded once + StructuredBP.probability(e3, true) should be((0.5 * 0.4) +- 0.000000001) + count should equal(2) // One each for p = true and p = false, but only expanded once } } @@ -168,9 +172,86 @@ class StructuredBPTest extends WordSpec with Matchers { var count = 0 val e1 = Apply(Constant(1), (i: Int) => { count += 1; 5 }) val e2 = Flip(0.5) - StructuredBP.probability(e2, true) should equal (0.5) - count should equal (0) + StructuredBP.probability(e2, true) should equal(0.5) + count should equal(0) } } } + + "MPE BP" when { + "given a flat model without evidence should produce the right answer" in { + Universe.createNew() + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val alg = StructuredMPEBP(20) + alg.start + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + alg.mostLikelyValue(e1) should be(0.2 +- 0.0000001) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + alg.kill + } + + "given a flat model with evidence should produce the right answer" in { + Universe.createNew() + val e1 = Select(0.5 -> 0.2, 0.5 -> 0.3) + e1.addConstraint((d: Double) => if (d < 0.25) 3.0 else 1.0) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + e4.observe(true) + val alg = StructuredMPEBP(20) + alg.start + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // MPE: e1=.2,e2=F,e3=F,e4=T + alg.mostLikelyValue(e1) should be(0.2 +- 0.0000001) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + alg.kill + } + + "given a structured model with evidence should produce the right answer" in { + Universe.createNew() + val e1 = Flip(0.5) + e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) + val e2 = Chain(e1, (b: Boolean) => { + if (b) Flip(0.4) || Flip(0.2) + else Flip(0.9) || Flip(0.2) + }) + val e3 = If(e1, Flip(0.52), Flip(0.4)) + val e4 = e2 === e3 + e4.observe(true) + // p(e1=T,e2=T,f1=T,f2=T,e3=T) = 0.75 * 0.4 * 0.2 * 0.52 = .0312 + // p(e1=T,e2=T,f1=T,f2=F,e3=T) = 0.75 * 0.4 * 0.8 * 0.52 = .1248 + // p(e1=T,e2=T,f1=F,f2=T,e3=T) = 0.75 * 0.6 * 0.2 * 0.52 = .0468 + // p(e1=T,e2=F,f1=F,f2=F,e3=F) = 0.75 * 0.6 * 0.8 * 0.48 = .1728 + // p(e1=F,e2=T,f1=T,f2=T,e3=T) = 0.25 * 0.9 * 0.2 * 0.4 = .018 + // p(e1=F,e2=T,f1=T,f2=F,e3=T) = 0.25 * 0.9 * 0.8 * 0.4 = .072 + // p(e1=F,e2=T,f1=F,f2=T,e3=T) = 0.25 * 0.1 * 0.2 * 0.4 = .002 + // p(e1=F,e2=F,f1=F,f2=F,e3=F) = 0.25 * 0.1 * 0.8 * 0.6 = .012 + // MPE: e1=T,e2=F,e3=F,e4=T + val alg = StructuredMPEBP(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) + alg.kill + } + } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEBPChooserTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEBPChooserTest.scala index 938749d5..81dbfc6e 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEBPChooserTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEBPChooserTest.scala @@ -90,6 +90,7 @@ class StructuredVEBPChooserTest extends WordSpec with Matchers { } } + /* "given a one-level nested model with nested evidence" should { "produce the correct answer" in { Universe.createNew() @@ -101,6 +102,8 @@ class StructuredVEBPChooserTest extends WordSpec with Matchers { alg.probability(e3, true) should equal (0.6) } } + * + */ "given a two-level nested model" should { "produce the correct answer" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEGibbsChooserTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEGibbsChooserTest.scala index f032508e..6faf8454 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEGibbsChooserTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVEGibbsChooserTest.scala @@ -90,6 +90,7 @@ class StructuredVEGibbsChooserTest extends WordSpec with Matchers { } } + /* "given a one-level nested model with nested evidence" should { "produce the correct answer" in { Universe.createNew() @@ -101,6 +102,8 @@ class StructuredVEGibbsChooserTest extends WordSpec with Matchers { alg.probability(e3, true) should be (0.6 +- tol) } } + * + */ "given a two-level nested model" should { "produce the correct answer" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVETest.scala index 09f99e8f..8b4ff067 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVETest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/structured/strategy/StructuredVETest.scala @@ -12,12 +12,13 @@ */ package com.cra.figaro.test.algorithm.structured.strategy -import org.scalatest.{WordSpec, Matchers} +import org.scalatest.{Matchers, WordSpec} import com.cra.figaro.language._ import com.cra.figaro.library.compound.If import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredVE import com.cra.figaro.algorithm.lazyfactored.ValueSet._ import com.cra.figaro.language.Element.toBooleanElement +import com.cra.figaro.algorithm.structured.algorithm.structured.StructuredMPEVE class StructuredVETest extends WordSpec with Matchers { "Executing a recursive structured VE solver strategy" when { @@ -26,7 +27,7 @@ class StructuredVETest extends WordSpec with Matchers { Universe.createNew() val e2 = Flip(0.6) val e3 = Apply(e2, (b: Boolean) => b) - StructuredVE.probability(e3, true) should equal (0.6) + StructuredVE.probability(e3, true) should equal(0.6) } } @@ -36,7 +37,7 @@ class StructuredVETest extends WordSpec with Matchers { val e1 = Select(0.25 -> 0.3, 0.25 -> 0.5, 0.25 -> 0.7, 0.25 -> 0.9) val e2 = Flip(e1) val e3 = Apply(e2, (b: Boolean) => b) - StructuredVE.probability(e3, true) should equal (0.6) + StructuredVE.probability(e3, true) should equal(0.6) } } @@ -47,7 +48,7 @@ class StructuredVETest extends WordSpec with Matchers { val e2 = Flip(e1) val e3 = Apply(e2, (b: Boolean) => b) e3.observe(true) - StructuredVE.probability(e1, 0.3) should be (0.125 +- 0.000000001) + StructuredVE.probability(e1, 0.3) should be(0.125 +- 0.000000001) } } @@ -59,8 +60,8 @@ class StructuredVETest extends WordSpec with Matchers { val e3 = Apply(e2, (b: Boolean) => b) val alg = StructuredVE(e2, e3) alg.start() - alg.probability(e2, true) should equal (0.6) - alg.probability(e3, true) should equal (0.6) + alg.probability(e2, true) should equal(0.6) + alg.probability(e3, true) should equal(0.6) } } @@ -73,8 +74,8 @@ class StructuredVETest extends WordSpec with Matchers { e3.observe(true) val alg = StructuredVE(e2, e1) alg.start() - alg.probability(e2, true) should equal (1.0) - alg.probability(e1, 0.3) should be (0.125 +- 0.000000001) + alg.probability(e2, true) should equal(1.0) + alg.probability(e1, 0.3) should be(0.125 +- 0.000000001) } } @@ -86,10 +87,11 @@ class StructuredVETest extends WordSpec with Matchers { val e3 = If(e2, Constant(true), Constant(false)) val alg = StructuredVE(e3) alg.start() - alg.probability(e3, true) should equal (0.6) + alg.probability(e3, true) should equal(0.6) } } + /* "given a one-level nested model with nested evidence" should { "produce the correct answer" in { Universe.createNew() @@ -98,9 +100,11 @@ class StructuredVETest extends WordSpec with Matchers { val e3 = If(e2, { val e = Flip(0.5); e.observe(true); e }, Constant(false)) val alg = StructuredVE(e3) alg.start() - alg.probability(e3, true) should equal (0.6) + alg.probability(e3, true) should equal(0.6) } } + * + */ "given a two-level nested model" should { "produce the correct answer" in { @@ -110,7 +114,7 @@ class StructuredVETest extends WordSpec with Matchers { val e3 = If(e2, If(Flip(0.9), Constant(true), Constant(false)), Constant(false)) val alg = StructuredVE(e3) alg.start() - alg.probability(e3, true) should be ((0.6 * 0.9) +- 0.000000001) + alg.probability(e3, true) should be((0.6 * 0.9) +- 0.000000001) } } @@ -120,7 +124,7 @@ class StructuredVETest extends WordSpec with Matchers { val e1 = Flip(0.4) val e2 = Flip(0.3) val e3 = e1 && e2 - StructuredVE.probability(e3, true) should be (0.12 +- 0.000000001) + StructuredVE.probability(e3, true) should be(0.12 +- 0.000000001) } } @@ -130,8 +134,8 @@ class StructuredVETest extends WordSpec with Matchers { Universe.createNew() val e1 = Apply(Constant(true), (b: Boolean) => { count += 1; 5 }) val e2 = e1 === e1 - StructuredVE.probability(e2, true) should equal (1.0) - count should equal (1) + StructuredVE.probability(e2, true) should equal(1.0) + count should equal(1) // Note that this should now only expand once since Apply Maps have been added to Components } } @@ -143,7 +147,7 @@ class StructuredVETest extends WordSpec with Matchers { val e2 = If(e1, Constant(1), Constant(2)) val e3 = Apply(e2, e1, (i: Int, b: Boolean) => if (b) i + 1 else i + 2) // e3 is 2 iff e1 is true, because then e2 is 1 - StructuredVE.probability(e3, 2) should be (0.4 +- 0.000000001) + StructuredVE.probability(e3, 2) should be(0.4 +- 0.000000001) } } @@ -157,8 +161,8 @@ class StructuredVETest extends WordSpec with Matchers { val e1 = Chain(Flip(0.5), f) val e2 = Chain(Flip(0.4), f) val e3 = e1 && e2 - StructuredVE.probability(e3, true) should be ((0.5 * 0.4) +- 0.000000001) - count should equal (2) // One each for p = true and p = false, but only expanded once + StructuredVE.probability(e3, true) should be((0.5 * 0.4) +- 0.000000001) + count should equal(2) // One each for p = true and p = false, but only expanded once } } @@ -167,9 +171,100 @@ class StructuredVETest extends WordSpec with Matchers { var count = 0 val e1 = Apply(Constant(1), (i: Int) => { count += 1; 5 }) val e2 = Flip(0.5) - StructuredVE.probability(e2, true) should equal (0.5) - count should equal (0) + StructuredVE.probability(e2, true) should equal(0.5) + count should equal(0) } } } + + "MPE VE" when { + "given a disconnected model should produce the right answer" in { + Universe.createNew() + val e1 = Flip(0.4) + val e2 = Flip(0.6) + val alg = StructuredMPEVE() + alg.start + alg.mostLikelyValue(e1) should equal(false) + alg.mostLikelyValue(e2) should equal(true) + alg.kill + } + + "given a flat model without evidence should produce the right answer" in { + Universe.createNew() + val e1 = Select(0.75 -> 0.2, 0.25 -> 0.3) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + val alg = StructuredMPEVE() + alg.start + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // p(e1=.2,e2=T,e3=F,e4=F) = 0.75 * 0.2 * 0.8 = .12 + // p(e1=.2,e2=F,e3=T,e4=F) = 0.75 * 0.8 * 0.2 = .12 + // p(e1=.3,e2=T,e3=F,e4=F) = 0.25 * 0.3 * 0.7 = .0525 + // p(e1=.3,e2=F,e3=T,e4=F) = 0.25 * 0.7 * 0.3 = .0525 + // MPE: e1=.2,e2=F,e3=F,e4=T + alg.mostLikelyValue(e1) should be(0.2 +- 0.0000001) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + alg.kill + } + + "given a flat model with evidence should produce the right answer" in { + Universe.createNew() + val e1 = Select(0.5 -> 0.2, 0.5 -> 0.3) + e1.addConstraint((d: Double) => if (d < 0.25) 3.0 else 1.0) + val e2 = Flip(e1) + val e3 = Flip(e1) + val e4 = e2 === e3 + e4.observe(true) + val alg = StructuredMPEVE() + alg.cc.useSingleChainFactor = true + alg.start + // p(e1=.2,e2=T,e3=T,e4=T) = 0.75 * 0.2 * 0.2 = .03 + // p(e1=.2,e2=F,e3=F,e4=T) = 0.75 * 0.8 * 0.8 = .48 + // p(e1=.3,e2=T,e3=T,e4=T) = 0.25 * 0.3 * 0.3 = .0225 + // p(e1=.3,e2=F,e3=F,e4=T) = 0.25 * 0.7 * 0.7 = .1225 + // MPE: e1=.2,e2=F,e3=F,e4=T + alg.mostLikelyValue(e1) should be(0.2 +- 0.0000001) + alg.mostLikelyValue(e2) should equal(false) + alg.mostLikelyValue(e3) should equal(false) + alg.mostLikelyValue(e4) should equal(true) + alg.kill + } + + "given a structured model with evidence should produce the right answer" in { + Universe.createNew() + val e1 = Flip(0.5) + e1.setConstraint((b: Boolean) => if (b) 3.0; else 1.0) + val e2 = Chain(e1, (b: Boolean) => { + if (b) Flip(0.4) || Flip(0.2) + else Flip(0.9) || Flip(0.2) + }) + val e3 = If(e1, Flip(0.52), Flip(0.4)) + val e4 = e2 === e3 + e4.observe(true) + // p(e1=T,e2=T,f1=T,f2=T,e3=T) = 0.75 * 0.4 * 0.2 * 0.52 = .0312 + // p(e1=T,e2=T,f1=T,f2=F,e3=T) = 0.75 * 0.4 * 0.8 * 0.52 = .1248 + // p(e1=T,e2=T,f1=F,f2=T,e3=T) = 0.75 * 0.6 * 0.2 * 0.52 = .0468 + // p(e1=T,e2=F,f1=F,f2=F,e3=F) = 0.75 * 0.6 * 0.8 * 0.48 = .1728 + // p(e1=F,e2=T,f1=T,f2=T,e3=T) = 0.25 * 0.9 * 0.2 * 0.4 = .018 + // p(e1=F,e2=T,f1=T,f2=F,e3=T) = 0.25 * 0.9 * 0.8 * 0.4 = .072 + // p(e1=F,e2=T,f1=F,f2=T,e3=T) = 0.25 * 0.1 * 0.2 * 0.4 = .002 + // p(e1=F,e2=F,f1=F,f2=F,e3=F) = 0.25 * 0.1 * 0.8 * 0.6 = .012 + // MPE: e1=T,e2=F,e3=F,e4=T + val alg = StructuredMPEVE() + alg.cc.useSingleChainFactor = true + 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) + alg.kill + } + + } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/book/chap05/ProductDistribution.scala b/Figaro/src/test/scala/com/cra/figaro/test/book/chap05/ProductDistribution.scala index dfca0bdb..271bef36 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/book/chap05/ProductDistribution.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/book/chap05/ProductDistribution.scala @@ -57,7 +57,7 @@ object ProductDistribution { val model = new Model(targetPopularity, productQuality, affordability) val algorithm = Importance(1000, model.numberBuy) algorithm.start() - val result = algorithm.expectation(model.numberBuy, (i: Int) => i.toDouble) + val result = algorithm.expectation(model.numberBuy)(i=> i.toDouble) algorithm.kill() result } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/book/chap06/SalesPrediction.scala b/Figaro/src/test/scala/com/cra/figaro/test/book/chap06/SalesPrediction.scala index c92bd27a..00c28f20 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/book/chap06/SalesPrediction.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/book/chap06/SalesPrediction.scala @@ -122,9 +122,9 @@ class SalesPredictionTest extends WordSpec with Matchers { algorithm.stop() val hiresProd0 = algorithm.expectation(numHiresByProduct(0), (n: Int) => n.toDouble) - val hiresProd1 = algorithm.expectation(numHiresByProduct(1), (n: Int) => n.toDouble) + val hiresProd1 = algorithm.expectation(numHiresByProduct(1))(n => n.toDouble) val hiresProd2 = algorithm.expectation(numHiresByProduct(2), (n: Int) => n.toDouble) - val hiresProd3 = algorithm.expectation(numHiresByProduct(3), (n: Int) => n.toDouble) + val hiresProd3 = algorithm.expectation(numHiresByProduct(3))(n => n.toDouble) val hiresProd4 = algorithm.expectation(numHiresByProduct(4), (n: Int) => n.toDouble) algorithm.kill() diff --git a/Figaro/src/test/scala/com/cra/figaro/test/book/chap08/Restaurant.scala b/Figaro/src/test/scala/com/cra/figaro/test/book/chap08/Restaurant.scala index 7e256143..f76c0464 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/book/chap08/Restaurant.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/book/chap08/Restaurant.scala @@ -61,7 +61,7 @@ object Restaurant { def main(args: Array[String]) { val alg = Importance(10000, waiting(numSteps - 1)) alg.start() - println(alg.probability(waiting(numSteps - 1), (i: Int) => i > 4)) + println(alg.probability(waiting(numSteps - 1))(_ > 4)) alg.kill() } } @@ -72,7 +72,7 @@ class RestaurantTest extends WordSpec with Matchers { "produce a probability = 0.465 +- 0.01" taggedAs (BookExample, NonDeterministic) in { val alg = Importance(10000, Restaurant.waiting(Restaurant.numSteps - 1)) alg.start() - val prob = alg.probability(Restaurant.waiting(Restaurant.numSteps - 1), (i: Int) => i > 4) + val prob = alg.probability(Restaurant.waiting(Restaurant.numSteps - 1))(_ > 4) alg.kill() prob should be(0.465 +- 0.01) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/book/chap12/JointDistribution.scala b/Figaro/src/test/scala/com/cra/figaro/test/book/chap12/JointDistribution.scala index 6571f855..78118343 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/book/chap12/JointDistribution.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/book/chap12/JointDistribution.scala @@ -37,13 +37,13 @@ object JointDistribution { val imp = Importance(10000, totalSales) imp.start() println("Probability first sales will be less than 100 = " + - ve.probability(sales(0), (i: Int) => i < 100)) + ve.probability(sales(0))(_ < 100)) println("Probability second sales will be less than 100 = " + - ve.probability(sales(1), (i: Int) => i < 100)) + ve.probability(sales(1))(_ < 100)) println("Probability both sales will be less than 100 = " + - ve.probability(salesPair, (pair: (Int, Int)) => pair._1 < 100 && pair._2 < 100)) + ve.probability(salesPair)(pair => pair._1 < 100 && pair._2 < 100)) println("Probability total sales are less than 2000 = " + - imp.probability(totalSales, (i: Int) => i < 2000)) + imp.probability(totalSales)(_ < 2000)) println("Mean individual sales = " + ve.expectation(sales(0), (i: Int) => i.toDouble)) println("Mean total sales = " + @@ -61,10 +61,10 @@ class JointDistributionTest extends WordSpec with Matchers { val imp = Importance(10000, JointDistribution.totalSales) imp.start() val firstSales = ve.probability(JointDistribution.sales(0), (i: Int) => i < 100) - val secondSales = ve.probability(JointDistribution.sales(1), (i: Int) => i < 100) + val secondSales = ve.probability(JointDistribution.sales(1))(_ < 100) val bothSales = ve.probability(salesPair, (pair: (Int, Int)) => pair._1 < 100 && pair._2 < 100) - val totalSales = imp.probability(JointDistribution.totalSales, (i: Int) => i < 2000) - val meanIndSales = ve.expectation(JointDistribution.sales(0), (i: Int) => i.toDouble) + val totalSales = imp.probability(JointDistribution.totalSales)(_ < 2000) + val meanIndSales = ve.expectation(JointDistribution.sales(0))(i => i.toDouble) val meanTotSales = imp.expectation(JointDistribution.totalSales, (i: Int) => i.toDouble) ve.kill() imp.kill() diff --git a/Figaro/src/test/scala/com/cra/figaro/test/example/FairDiceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/example/FairDiceTest.scala index 58857e09..93e0dd06 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/example/FairDiceTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/example/FairDiceTest.scala @@ -102,15 +102,15 @@ class FairDiceTest extends WordSpec with Matchers { ve.stop() println("\nThe probabilities of seeing each side of d_1 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d1, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d1)(_ == o) + " -> " + o) } println("\nThe probabilities of seeing each side of d_2 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d2, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d2)(_ == o) + " -> " + o) } println("\nThe probabilities of seeing each side of d_3 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d3, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d3)(_ == o) + " -> " + o) } - (ve.probability(d1, (i: Int) => i == outcomes(0)) > .8) should be(true) - (ve.probability(d2, (i: Int) => i == outcomes(1)) > .8) should be(true) - (ve.probability(d3, (i: Int) => i == outcomes(5)) > .8) should be(true) + (ve.probability(d1)(_ == outcomes(0)) > .8) should be(true) + (ve.probability(d2)(_ == outcomes(1)) > .8) should be(true) + (ve.probability(d3)(_ == outcomes(5)) > .8) should be(true) ve.kill() algorithm.kill diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/CollapsedGibbsTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/CollapsedGibbsTest.scala new file mode 100644 index 00000000..911d505a --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/CollapsedGibbsTest.scala @@ -0,0 +1,262 @@ +/* + * CollapsedGibbsTest.scala + * Collapsed Gibbs sampler tests. + * + * Created By: Cory Scott (scottcb@uci.edu) + * Creation Date: July 21, 2016 + * + * Copyright 2015 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 + +import com.cra.figaro.algorithm.factored.factors._ +import com.cra.figaro.algorithm.lazyfactored.LazyValues +import com.cra.figaro.language._ +import com.cra.figaro.library.compound.If +import com.cra.figaro.library.compound.^^ +import org.scalatest.Matchers +import org.scalatest.WordSpec +import com.cra.figaro.algorithm.factored.factors.factory.Factory +import com.cra.figaro.algorithm.factored.gibbs.Gibbs +import com.cra.figaro.experimental.collapsedgibbs._ + +class CollapsedGibbsTest extends WordSpec with Matchers { + "A Collapsed Gibbs sampler" should { + + "with an unconstrained model produce the correct result" in { + Universe.createNew + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.4 -> 2, 0.5 -> 3) + val s2 = Select(0.7 -> 2, 0.1 -> 3, 0.2 -> 4) + val c = If(f, s1, s2) + // 0.3 * 0.4 + 0.7 * 0.7 = 0.61 + test[Int](c, _ == 2, 0.61) + } + + "with a constrained model produce the correct result" in { + Universe.createNew + val f = Flip(0.3) + val s1 = Select(0.1 -> 1, 0.4 -> 2, 0.5 -> 3) + val s2 = Select(0.7 -> 2, 0.1 -> 3, 0.2 -> 4) + val c = If(f, s1, s2) + c.addConstraint(identity) + // 0.61 * 2 / (0.3*0.1*1 + 0.3*0.4*2 + 0.3*0.5*3 + 0.7*0.7*2 + 0.7*0.1*3 + 0.7*0.2*4) + test[Int](c, _ == 2, 0.4939) + } + + "with a constraint on a Chain produce the correct result for the parent" in { + Universe.createNew + val f = Flip(0.3) + val c = If(f, Flip(0.8), Constant(false)) + c.addConstraint(b => if (b) 2.0 else 1.0) + // (0.3 * 0.8 * 2) / (0.3 * 0.8 * 2 + 0.3 * 0.2 + 0.7) + test[Boolean](c, identity, 0.3871) + } + + "with a constraint on a Chain result correctly constrain the Chain but not the parent" in { + Universe.createNew + val f = Flip(0.3) + val r1 = Flip(0.8) + r1.addConstraint(b => if (b) 2.0 else 1.0) + val c = If(f, r1, Constant(false)) + test[Boolean](f, identity, 0.3) + // r1 true with probability (0.8 * 2) / (0.8 * 2 + 0.2) = 0.8889 + // 0.3 * 0.8889 + test[Boolean](c, identity, 0.2667) + } + + "with an element used multiple times use the same value each time" in { + Universe.createNew + val f = Flip(0.3) + val e = f === f + test[Boolean](e, identity, 1.0) + } + + "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) + } + test[Boolean](x, identity, 1.0, 2) + } + + "with the default collapser (default parameters) produce the correct result in a deterministic model" in { + testStrategyDeterministicModel[Boolean]("") + } + "with the DETERM collapser (default parameters) produce the correct result in a deterministic model" in { + testStrategyDeterministicModel[Boolean]("DETERM") + } + "with the RECURR collapser (default parameters) produce the correct result in a deterministic model" in { + testStrategyDeterministicModel[Boolean]("RECURR") + } + "with the FACTOR collapser (default parameters) produce the correct result in a deterministic model" in { + testStrategyDeterministicModel[Boolean]("FACTOR") + } + "with the SIMPLE collapser (default parameters) produce the correct result in a deterministic model" in { + testStrategyDeterministicModel[Boolean]("SIMPLE") + } + + "with the default collapser (high alpha and gamma) produce the correct result in a deterministic model" in { + testStrategyWithParamsDeterministicModel[Boolean]("", Seq(10, 2000)) + } + "with the DETERM collapser (high alpha and gamma) produce the correct result in a deterministic model" in { + testStrategyWithParamsDeterministicModel[Boolean]("DETERM", Seq(10, 2000)) + } + "with the RECURR collapser (high alpha, gamma, frequent sample saving) produce the correct result in a deterministic model" in { + testStrategyWithParamsDeterministicModel[Boolean]("RECURR", Seq(10, 2000, 2000, 1)) + } + "with the FACTOR collapser (high alpha, gamma, and factorThreshhold) produce the correct result in a deterministic model" in { + testStrategyWithParamsDeterministicModel[Boolean]("FACTOR", Seq(10, 2000, 10000)) + } + "with the SIMPLE collapser (high alpha and gamma) produce the correct result in a deterministic model" in { + testStrategyWithParamsDeterministicModel[Boolean]("SIMPLE", Seq(10, 2000)) + } + + "with the default collapser (default parameters) produce the correct result in an Ising model" in { + testStrategyIsingModel[Boolean]("") + } + "with the DETERM collapser (default parameters) produce the correct result in an Ising model" in { + testStrategyIsingModel[Boolean]("DETERM") + } + "with the RECURR collapser (default parameters) produce the correct result in an Ising model" in { + testStrategyIsingModel[Boolean]("RECURR") + } + "with the FACTOR collapser (default parameters) produce the correct result in an Ising model" in { + testStrategyIsingModel[Boolean]("FACTOR") + } + "with the SIMPLE collapser (default parameters) produce the correct result in an Ising model" in { + testStrategyIsingModel[Boolean]("SIMPLE") + } + + "with the default collapser (high alpha and gamma) produce the correct result in a Ising model" in { + testStrategyWithParamsIsingModel[Boolean]("", Seq(10, 2000)) + } + "with the DETERM collapser (high alpha and gamma) produce the correct result in a Ising model" in { + testStrategyWithParamsIsingModel[Boolean]("DETERM", Seq(10, 2000)) + } + "with the RECURR collapser (high alpha, gamma, frequent sample saving) produce the correct result in a Ising model" in { + testStrategyWithParamsIsingModel[Boolean]("RECURR", Seq(10, 2000, 2000, 1)) + } + "with the FACTOR collapser (high alpha, gamma, and factorThreshhold) produce the correct result in a Ising model" in { + testStrategyWithParamsIsingModel[Boolean]("FACTOR", Seq(10, 2000, 10000)) + } + "with the SIMPLE collapser (high alpha and gamma) produce the correct result in a Ising model" in { + testStrategyWithParamsIsingModel[Boolean]("SIMPLE", Seq(10, 2000)) + } + + "with the default collapser and default parameters collapse down to query variables" in { + Universe.createNew + val a1 = Flip(0.5) + val a2 = Flip(0.5) + val a3 = Flip(0.5) + val b1 = Apply(a1, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b2 = Apply(a3, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b3 = Apply(a1, a3, (b1: Boolean, b2: Boolean) => b1 || b2) + val c1 = Apply(b1, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c2 = Apply(b3, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c3 = Apply(b1, b3, (b1: Boolean, b2: Boolean) => b1 || b2) + val d3 = Apply(c1, c2, c3, (b1: Boolean, b2: Boolean, b3: Boolean) => b1 || b2 || b3) + val alg2 = CollapsedGibbs(10000, d3) + alg2.initialize() + alg2.variables.toList.length should be (1) + } + + } + + def makeFactors(): List[Factor[Double]] = { + LazyValues(Universe.universe).expandAll(Universe.universe.activeElements.toSet.map((elem: Element[_]) => ((elem, Integer.MAX_VALUE)))) + Universe.universe.activeElements.foreach(Variable(_)) + Universe.universe.activeElements flatMap (Factory.makeFactorsForElement(_)) + } + + def test[T](target: Element[T], predicate: T => Boolean, prob: Double, tol: Double = 0.025) { + val algorithm = CollapsedGibbs(100000, target) + algorithm.start() + algorithm.stop() + algorithm.probability(target, predicate) should be(prob +- tol) + algorithm.kill() + } + def testStrategyDeterministicModel[T](strategy:String, tol: Double = 0.025) { + Universe.universe.finalize() + Universe.createNew + val a1 = Flip(0.5) + val a2 = Flip(0.5) + val a3 = Flip(0.5) + val b1 = Apply(a1, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b2 = Apply(a3, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b3 = Apply(a1, a3, (b1: Boolean, b2: Boolean) => b1 || b2) + val c1 = Apply(b1, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c2 = Apply(b3, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c3 = Apply(b1, b3, (b1: Boolean, b2: Boolean) => b1 || b2) + val d3 = Apply(c1, c2, c3, (b1: Boolean, b2: Boolean, b3: Boolean) => b1 || b2 || b3) + val algorithm = CollapsedGibbs(strategy:String, 100000, d3) + algorithm.start() + algorithm.stop() + algorithm.probability(d3, true) should be(.875 +- tol) + algorithm.kill() + } + def testStrategyWithParamsDeterministicModel[T](strategy:String, params:Seq[Int], tol: Double = 0.025) { + Universe.universe.finalize() + Universe.createNew + val a1 = Flip(0.5) + val a2 = Flip(0.5) + val a3 = Flip(0.5) + val b1 = Apply(a1, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b2 = Apply(a3, a2, (b1: Boolean, b2: Boolean) => b1 || b2) + val b3 = Apply(a1, a3, (b1: Boolean, b2: Boolean) => b1 || b2) + val c1 = Apply(b1, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c2 = Apply(b3, b2, (b1: Boolean, b2: Boolean) => b1 || b2) + val c3 = Apply(b1, b3, (b1: Boolean, b2: Boolean) => b1 || b2) + val d3 = Apply(c1, c2, c3, (b1: Boolean, b2: Boolean, b3: Boolean) => b1 || b2 || b3) + val algorithm = CollapsedGibbs(strategy:String, params, 100000, d3) + algorithm.start() + algorithm.stop() + algorithm.probability(d3, true) should be(.875 +- tol) + algorithm.kill() + } + def testStrategyIsingModel[T](strategy:String, tol: Double = 0.025) { + Universe.universe.finalize() + Universe.createNew() + def IsingConstraint(pair: (Boolean, Boolean)) = if (pair._1 == pair._2) 1.1; else 1.0 + val IsingSize = 4 + var allFlips = for {i <- 0 until IsingSize} yield (for {j <- 0 until IsingSize} yield Flip(0.5)).toList + for {i <- 0 until IsingSize} { + for {j <- 0 until IsingSize} { + ^^(allFlips(i)(j), allFlips((i+1) % IsingSize)(j)).setConstraint(IsingConstraint) + ^^(allFlips(i)(j), allFlips(i)((j + 1) % IsingSize)).setConstraint(IsingConstraint) + } + } + allFlips(IsingSize/2)(IsingSize/2).observe(false) + val toTest = allFlips(0)(0) + val algorithm = CollapsedGibbs(strategy:String, 100000, toTest) + algorithm.start() + algorithm.stop() + algorithm.probability(toTest, true) should be(Gibbs.probability(toTest, true) +- tol) + algorithm.kill() + } + def testStrategyWithParamsIsingModel[T](strategy:String, params:Seq[Int], tol: Double = 0.025) { + Universe.universe.finalize() + Universe.createNew() + def IsingConstraint(pair: (Boolean, Boolean)) = if (pair._1 == pair._2) 1.1; else 1.0 + val IsingSize = 4 + var allFlips = for {i <- 0 until IsingSize} yield (for {j <- 0 until IsingSize} yield Flip(0.5)).toList + for {i <- 0 until IsingSize} { + for {j <- 0 until IsingSize} { + ^^(allFlips(i)(j), allFlips((i+1) % IsingSize)(j)).setConstraint(IsingConstraint) + ^^(allFlips(i)(j), allFlips(i)((j + 1) % IsingSize)).setConstraint(IsingConstraint) + } + } + allFlips(IsingSize/2)(IsingSize/2).observe(false) + val toTest = allFlips(0)(0) + val algorithm = CollapsedGibbs(strategy:String, params, 100000, toTest) + algorithm.start() + algorithm.stop() + algorithm.probability(toTest, true) should be(Gibbs.probability(toTest, true) +- tol) + algorithm.kill() + } +} \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/MarginalMAPBPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/MarginalMAPBPTest.scala new file mode 100644 index 00000000..3e9c9d79 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/MarginalMAPBPTest.scala @@ -0,0 +1,241 @@ +/* + * MarginalMAPBPTest.scala + * Marginal MAP Belief Propagation tests. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 11, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.experimental.marginalmap.MarginalMAPBeliefPropagation +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.discrete.Uniform +import com.cra.figaro.library.collection.Container +import com.cra.figaro.library.compound.If +import org.scalatest.{Matchers, WordSpec} + +class MarginalMAPBPTest extends WordSpec with Matchers { + "Marginal MAP BP" when { + "given a model with MAP queries on all elements" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.8) + val b11 = Flip(0.7) + val b12 = Flip(0.6) + val b1 = b11 && b12 + val b2 = Constant(false) + val b = If(a, b1, b2) + // Even though b is a Chain, the result elements of b are MAP elements + // This should produce the same result as an MPE query + + // p(a=T,b11=T,b12=T) = 0.8 * 0.7 * 0.6 = 0.336 + // p(a=T,b11=T,b12=F) = 0.8 * 0.7 * 0.4 = 0.224 + // p(a=T,b11=F,b12=T) = 0.8 * 0.3 * 0.6 = 0.144 + // p(a=T,b11=F,b12=F) = 0.8 * 0.3 * 0.4 = 0.096 + // p(a=F,b11=T,b12=T) = 0.2 * 0.7 * 0.6 = 0.084 + // p(a=F,b11=T,b12=F) = 0.2 * 0.7 * 0.4 = 0.054 + // p(a=F,b11=F,b12=T) = 0.2 * 0.3 * 0.6 = 0.036 + // p(a=F,b11=F,b12=F) = 0.2 * 0.3 * 0.4 = 0.024 + // MAP: a=T,b11=T,b12=T which implies b1=T,b2=F,b=T + val alg = MarginalMAPBeliefPropagation(20, a, b11, b12, b1, b2, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b11) should equal(true) + alg.mostLikelyValue(b12) should equal(true) + alg.mostLikelyValue(b1) should equal(true) + alg.mostLikelyValue(b2) should equal(false) + alg.mostLikelyValue(b) should equal(true) + alg.kill + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.8) + val b11 = Flip(0.7) + val b12 = Flip(0.6) + val b1 = b11 && b12 + val b2 = Constant(false) + val b = If(a, b1, b2) + b.observe(false) + // Even though b is a Chain, the result elements of b are MAP elements + // This should produce the same result as an MPE query + + // These weights are not normalized + // p(a=T,b11=T,b12=T) = 0 + // p(a=T,b11=T,b12=F) = 0.8 * 0.7 * 0.4 = 0.224 + // p(a=T,b11=F,b12=T) = 0.8 * 0.3 * 0.6 = 0.144 + // p(a=T,b11=F,b12=F) = 0.8 * 0.3 * 0.4 = 0.096 + // p(a=F,b11=T,b12=T) = 0.2 * 0.7 * 0.6 = 0.084 + // p(a=F,b11=T,b12=F) = 0.2 * 0.7 * 0.4 = 0.054 + // p(a=F,b11=F,b12=T) = 0.2 * 0.3 * 0.6 = 0.036 + // p(a=F,b11=F,b12=F) = 0.2 * 0.3 * 0.4 = 0.024 + // MAP: a=T,b11=T,b12=F which implies b1=F,b2=F,b=F + val alg = MarginalMAPBeliefPropagation(20, a, b11, b12, b1, b2, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b11) should equal(true) + alg.mostLikelyValue(b12) should equal(false) + alg.mostLikelyValue(b1) should equal(false) + alg.mostLikelyValue(b2) should equal(false) + alg.mostLikelyValue(b) should equal(false) + alg.kill + } + } + + "given a model with MAP queries on all permanent elements" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.3) && Flip(0.5), Flip(0.4)) + // The result elements of b are marginalized + // Then, the first result element of b is effectively a Flip(0.15) + + // p(a=T,b=T) = 0.6 * 0.15 = 0.09 + // p(a=T,b=F) = 0.6 * 0.85 = 0.51 + // p(a=F,b=T) = 0.4 * 0.4 = 0.16 + // p(a=F,b=F) = 0.4 * 0.6 = 0.24 + // MAP: a=T,b=F + val alg = MarginalMAPBeliefPropagation(20, a, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b) should equal(false) + alg.kill + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.3) && Flip(0.5), Flip(0.4)) + b.observe(true) + // The result elements of b are marginalized + // Then, the first result element of b is effectively a Flip(0.15) + + // These weights are not normalized + // p(a=T,b=T) = 0.6 * 0.15 = 0.09 + // p(a=T,b=F) = 0 + // p(a=F,b=T) = 0.4 * 0.4 = 0.16 + // p(a=F,b=F) = 0 + // MAP: a=F,b=T + val alg = MarginalMAPBeliefPropagation(20, a, b) + alg.start + alg.mostLikelyValue(a) should equal(false) + alg.mostLikelyValue(b) should equal(true) + alg.kill + } + } + + "given a model with MAP queries on a single element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + // num4 is effectively a binomial distribution with n=10, p=0.25 + // The mode is floor(p*(n+1))=2 + val alg = MarginalMAPBeliefPropagation(20, num4) + alg.start + alg.mostLikelyValue(num4) should equal(2) + alg.kill + } + + "produce the right answer with evidence" in { + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + num4.addCondition(_ >= 5) + + // Since the pmf of a binomial distribution is strictly decreasing past the mode, + // the most likely value should be the least possible value given the evidence + val alg = MarginalMAPBeliefPropagation(20, num4) + alg.start + alg.mostLikelyValue(num4) should equal(5) + alg.kill + } + } + + "given a model with MAP queries on more than one element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0.1512+0.0288+0.1296+0.0864=0.396 -> MAP + + val alg = MarginalMAPBeliefPropagation(10, c, d) + alg.start() + alg.mostLikelyValue(c) should equal(false) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // These weights are not normalized + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 -> MAP + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0 + + (c || d).observe(true) + + val alg = MarginalMAPBeliefPropagation(10, c, d) + alg.start() + alg.mostLikelyValue(c) should equal(true) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/ProbEvidenceMarginalMAPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/ProbEvidenceMarginalMAPTest.scala new file mode 100644 index 00000000..9a195e54 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/ProbEvidenceMarginalMAPTest.scala @@ -0,0 +1,362 @@ +/* + * ProbEvidenceMarginalMAPTest.scala + * Tests for probability of evidence-based marginal MAP. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 1, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.experimental.marginalmap.ProbEvidenceMarginalMAP +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous.Normal +import com.cra.figaro.library.atomic.discrete.{Uniform, Util} +import com.cra.figaro.library.collection.Container +import com.cra.figaro.library.compound.If +import org.scalatest.{Matchers, WordSpec} + +class ProbEvidenceMarginalMAPTest extends WordSpec with Matchers { + // For testing, it's more efficient to use a linear schedule on simple models where we just want quick convergence, + // rather than exploration of a large state space + val linearSchedule = Schedule((temp, iter) => iter) + + def anytime(elems: Element[_]*) = + ProbEvidenceMarginalMAP(0.05, 100, 100, ProposalScheme.default, linearSchedule, elems:_*) + + def oneTime(elems: Element[_]*) = + ProbEvidenceMarginalMAP(2000, 0.05, 100, 100, ProposalScheme.default, linearSchedule, elems:_*) + + "Marginal MAP using probability of evidence" should { + "increase temperature with additional iterations" in { + Universe.createNew() + val elem = Flip(0.6) + + val alg = ProbEvidenceMarginalMAP(elem) + alg.start() + val temp1 = alg.getTemperature + Thread.sleep(500) + val temp2 = alg.getTemperature + alg.kill() + + temp2 should be > temp1 + } + + "increase temperature faster with lower k" in { + Universe.createNew() + val elem = Flip(0.6) + + // k = 2.0 + val alg1 = ProbEvidenceMarginalMAP(100, 0.05, 2, 100, ProposalScheme(elem), Schedule.default(2.0), elem) + alg1.start() + val temp1 = alg1.getTemperature + alg1.kill() + + // k = 4.0 + val alg2 = ProbEvidenceMarginalMAP(100, 0.05, 2, 100, ProposalScheme(elem), Schedule.default(4.0), elem) + alg2.start() + val temp2 = alg2.getTemperature + alg2.kill() + + temp2 should be < temp1 + } + } + + "Running anytime marginal MAP using probability of evidence" when { + "given a model with a MAP query on a top-level parameter" should { + "correctly estimate the parameter with evidence" in { + Universe.createNew() + val parameterMean = 5.0 + val parameterVariance = 1.0 + val variance = 1.0 + + // We're using paramater as a prior to estimate a normal with known variance + val parameter = Normal(parameterMean, parameterVariance) + + val observations = List(6.1, 7.3, 5.8, 5.3, 6.4) + for(obs <- observations) { + Normal(parameter, variance).observe(obs) + } + + // The MAP value of parameter is just the the posterior mean + val meanEstimate = (parameterMean / parameterVariance + observations.sum / variance) / + (1.0 / parameterVariance + observations.length / variance) + + val alg = ProbEvidenceMarginalMAP(0.05, 2, 1, ProposalScheme(parameter), linearSchedule, parameter) + alg.start() + Thread.sleep(2500) + alg.stop() + alg.mostLikelyValue(parameter) should equal(meanEstimate +- 0.05) + alg.kill() + } + + } + + "given a model with MAP queries on more than one element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0.1512+0.0288+0.1296+0.0864=0.396 -> MAP + + val alg = anytime(c, d) + alg.start() + Thread.sleep(2500) + alg.stop() + alg.mostLikelyValue(c) should equal(false) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // These weights are not normalized + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 -> MAP + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0 + + (c || d).observe(true) + + val alg = anytime(c, d) + alg.start() + Thread.sleep(2500) + alg.stop() + alg.mostLikelyValue(c) should equal(true) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + } + + "given evidence on MAP elements" should { + "produce the right answer with a condition" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + num4.addCondition(_ >= 5) + + // Since the pmf of a binomial distribution is strictly decreasing past the mode, + // the most likely value should be the least possible value given the evidence + val alg = anytime(num4) + alg.start() + Thread.sleep(10000) + alg.stop() + alg.mostLikelyValue(num4) should equal(5) + alg.kill() + } + + "produce the right answer with a constraint" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + val constraint = (x: Int) => math.exp(- (x - 6) * (x - 6)) + num4.addConstraint(constraint) + val max = (0 to 10).maxBy(x => Util.binomialDensity(10, 0.25, x) * constraint(x)) + max should equal(5) + + val alg = anytime(num4) + alg.start() + Thread.sleep(10000) + alg.stop() + alg.mostLikelyValue(num4) should equal(max) + alg.kill() + } + } + } + + "Running one time marginal MAP using probability of evidence" when { + "given a model with a MAP query on a top-level parameter" should { + "correctly estimate the parameter with evidence" in { + Universe.createNew() + val parameterMean = 5.0 + val parameterVariance = 1.0 + val variance = 1.0 + + // We're using paramater as a prior to estimate a normal with known variance + val parameter = Normal(parameterMean, parameterVariance) + + val observations = List(6.1, 7.3, 5.8, 5.3, 6.4) + for(obs <- observations) { + Normal(parameter, variance).observe(obs) + } + + // The MAP value of parameter is just the the posterior mean + val meanEstimate = (parameterMean / parameterVariance + observations.sum / variance) / + (1.0 / parameterVariance + observations.length / variance) + + val alg = ProbEvidenceMarginalMAP(1000, 0.05, 2, 1, ProposalScheme(parameter), linearSchedule, parameter) + alg.start() + alg.mostLikelyValue(parameter) should equal(meanEstimate +- 0.05) + alg.kill() + } + + } + + "given a model with MAP queries on more than one element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0.1512+0.0288+0.1296+0.0864=0.396 -> MAP + + val alg = oneTime(c, d) + alg.start() + alg.mostLikelyValue(c) should equal(false) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // These weights are not normalized + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 -> MAP + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0 + + (c || d).observe(true) + + val alg = oneTime(c, d) + alg.start() + alg.mostLikelyValue(c) should equal(true) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + } + + "given evidence on MAP elements" should { + "produce the right answer with a condition" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + num4.addCondition(_ >= 5) + + // Since the pmf of a binomial distribution is strictly decreasing past the mode, + // the most likely value should be the least possible value given the evidence + val alg = oneTime(num4) + alg.start() + alg.mostLikelyValue(num4) should equal(5) + alg.kill() + } + + "produce the right answer with a constraint" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + val constraint = (x: Int) => math.exp(- (x - 6) * (x - 6)) + num4.addConstraint(constraint) + val max = (0 to 10).maxBy(x => Util.binomialDensity(10, 0.25, x) * constraint(x)) + max should equal(5) + + val alg = oneTime(num4) + alg.start() + alg.mostLikelyValue(num4) should equal(max) + alg.kill() + } + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/StructuredMMAPVETest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/StructuredMMAPVETest.scala new file mode 100644 index 00000000..b4f0b45d --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/marginalmap/StructuredMMAPVETest.scala @@ -0,0 +1,241 @@ +/* + * StructuredMMAPVETest.scala + * Structured marginal MAP VE tests. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 11, 2016 + * + * Copyright 2016 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.marginalmap + +import com.cra.figaro.experimental.marginalmap.StructuredMarginalMAPVE +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.discrete.Uniform +import com.cra.figaro.library.collection.Container +import com.cra.figaro.library.compound.If +import org.scalatest.{Matchers, WordSpec} + +class StructuredMMAPVETest extends WordSpec with Matchers { + "Marginal MAP VE" when { + "given a model with MAP queries on all elements" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.8) + val b11 = Flip(0.7) + val b12 = Flip(0.6) + val b1 = b11 && b12 + val b2 = Constant(false) + val b = If(a, b1, b2) + // Even though b is a Chain, the result elements of b are MAP elements + // This should produce the same result as an MPE query + + // p(a=T,b11=T,b12=T) = 0.8 * 0.7 * 0.6 = 0.336 + // p(a=T,b11=T,b12=F) = 0.8 * 0.7 * 0.4 = 0.224 + // p(a=T,b11=F,b12=T) = 0.8 * 0.3 * 0.6 = 0.144 + // p(a=T,b11=F,b12=F) = 0.8 * 0.3 * 0.4 = 0.096 + // p(a=F,b11=T,b12=T) = 0.2 * 0.7 * 0.6 = 0.084 + // p(a=F,b11=T,b12=F) = 0.2 * 0.7 * 0.4 = 0.054 + // p(a=F,b11=F,b12=T) = 0.2 * 0.3 * 0.6 = 0.036 + // p(a=F,b11=F,b12=F) = 0.2 * 0.3 * 0.4 = 0.024 + // MAP: a=T,b11=T,b12=T which implies b1=T,b2=F,b=T + val alg = StructuredMarginalMAPVE(a, b11, b12, b1, b2, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b11) should equal(true) + alg.mostLikelyValue(b12) should equal(true) + alg.mostLikelyValue(b1) should equal(true) + alg.mostLikelyValue(b2) should equal(false) + alg.mostLikelyValue(b) should equal(true) + alg.kill + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.8) + val b11 = Flip(0.7) + val b12 = Flip(0.6) + val b1 = b11 && b12 + val b2 = Constant(false) + val b = If(a, b1, b2) + b.observe(false) + // Even though b is a Chain, the result elements of b are MAP elements + // This should produce the same result as an MPE query + + // These weights are not normalized + // p(a=T,b11=T,b12=T) = 0 + // p(a=T,b11=T,b12=F) = 0.8 * 0.7 * 0.4 = 0.224 + // p(a=T,b11=F,b12=T) = 0.8 * 0.3 * 0.6 = 0.144 + // p(a=T,b11=F,b12=F) = 0.8 * 0.3 * 0.4 = 0.096 + // p(a=F,b11=T,b12=T) = 0.2 * 0.7 * 0.6 = 0.084 + // p(a=F,b11=T,b12=F) = 0.2 * 0.7 * 0.4 = 0.054 + // p(a=F,b11=F,b12=T) = 0.2 * 0.3 * 0.6 = 0.036 + // p(a=F,b11=F,b12=F) = 0.2 * 0.3 * 0.4 = 0.024 + // MAP: a=T,b11=T,b12=F which implies b1=F,b2=F,b=F + val alg = StructuredMarginalMAPVE(a, b11, b12, b1, b2, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b11) should equal(true) + alg.mostLikelyValue(b12) should equal(false) + alg.mostLikelyValue(b1) should equal(false) + alg.mostLikelyValue(b2) should equal(false) + alg.mostLikelyValue(b) should equal(false) + alg.kill + } + } + + "given a model with MAP queries on all permanent elements" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.3) && Flip(0.5), Flip(0.4)) + // The result elements of b are marginalized + // Then, the first result element of b is effectively a Flip(0.15) + + // p(a=T,b=T) = 0.6 * 0.15 = 0.09 + // p(a=T,b=F) = 0.6 * 0.85 = 0.51 + // p(a=F,b=T) = 0.4 * 0.4 = 0.16 + // p(a=F,b=F) = 0.4 * 0.6 = 0.24 + // MAP: a=T,b=F + val alg = StructuredMarginalMAPVE(a, b) + alg.start + alg.mostLikelyValue(a) should equal(true) + alg.mostLikelyValue(b) should equal(false) + alg.kill + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.3) && Flip(0.5), Flip(0.4)) + b.observe(true) + // The result elements of b are marginalized + // Then, the first result element of b is effectively a Flip(0.15) + + // These weights are not normalized + // p(a=T,b=T) = 0.6 * 0.15 = 0.09 + // p(a=T,b=F) = 0 + // p(a=F,b=T) = 0.4 * 0.4 = 0.16 + // p(a=F,b=F) = 0 + // MAP: a=F,b=T + val alg = StructuredMarginalMAPVE(a, b) + alg.start + alg.mostLikelyValue(a) should equal(false) + alg.mostLikelyValue(b) should equal(true) + alg.kill + } + } + + "given a model with MAP queries on a single element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + // num4 is effectively a binomial distribution with n=10, p=0.25 + // The mode is floor(p*(n+1))=2 + val alg = StructuredMarginalMAPVE(num4) + alg.start + alg.mostLikelyValue(num4) should equal(2) + alg.kill + } + + "produce the right answer with evidence" in { + val rolls = for { i <- 1 to 10 } yield Uniform(1,2,3,4) + val c = Container(rolls: _*) + val num4 = c.count(_ == 4) + + num4.addCondition(_ >= 5) + + // Since the pmf of a binomial distribution is strictly decreasing past the mode, + // the most likely value should be the least possible value given the evidence + val alg = StructuredMarginalMAPVE(num4) + alg.start + alg.mostLikelyValue(num4) should equal(5) + alg.kill + } + } + + "given a model with MAP queries on more than one element" should { + "produce the right answer without evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0.1512+0.0288+0.1296+0.0864=0.396 -> MAP + + val alg = StructuredMarginalMAPVE(c, d) + alg.start() + alg.mostLikelyValue(c) should equal(false) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + + "produce the right answer with evidence" in { + Universe.createNew() + val a = Flip(0.6) + val b = If(a, Flip(0.7), Flip(0.4)) + val c = If(a, Flip(0.6), Flip(0.1)) + val d = If(b, Flip(0.1), Flip(0.6)) + + // p(a=T,b=T,c=T,d=T)=0.6*0.7*0.6*0.1=0.0252 + // p(a=T,b=T,c=T,d=F)=0.6*0.7*0.6*0.9=0.2268 + // p(a=T,b=T,c=F,d=T)=0.6*0.7*0.4*0.1=0.0168 + // p(a=T,b=T,c=F,d=F)=0.6*0.7*0.4*0.9=0.1512 + // p(a=T,b=F,c=T,d=T)=0.6*0.3*0.6*0.6=0.0648 + // p(a=T,b=F,c=T,d=F)=0.6*0.3*0.6*0.4=0.0432 + // p(a=T,b=F,c=F,d=T)=0.6*0.3*0.4*0.6=0.0432 + // p(a=T,b=F,c=F,d=F)=0.6*0.3*0.4*0.4=0.0288 + // p(a=F,b=T,c=T,d=T)=0.4*0.4*0.1*0.1=0.0016 + // p(a=F,b=T,c=T,d=F)=0.4*0.4*0.1*0.9=0.0144 + // p(a=F,b=T,c=F,d=T)=0.4*0.4*0.9*0.1=0.0144 + // p(a=F,b=T,c=F,d=F)=0.4*0.4*0.9*0.9=0.1296 + // p(a=F,b=F,c=T,d=T)=0.4*0.6*0.1*0.6=0.0144 + // p(a=F,b=F,c=T,d=F)=0.4*0.6*0.1*0.4=0.0096 + // p(a=F,b=F,c=F,d=T)=0.4*0.6*0.9*0.6=0.1296 + // p(a=F,b=F,c=F,d=F)=0.4*0.6*0.9*0.4=0.0864 + + // These weights are not normalized + // p(c=T,d=T)=0.0252+0.0648+0.0016+0.0144=0.106 + // p(c=T,d=F)=0.2268+0.0432+0.0144+0.0096=0.294 -> MAP + // p(c=F,d=T)=0.0168+0.0432+0.0144+0.1296=0.204 + // p(c=F,d=F)=0 + + (c || d).observe(true) + + val alg = StructuredMarginalMAPVE(c, d) + alg.start() + alg.mostLikelyValue(c) should equal(true) + alg.mostLikelyValue(d) should equal(false) + alg.kill() + } + } + } +} diff --git a/Figaro/src/test/scala/com/cra/figaro/test/experimental/normalproposals/NormalProposerTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/experimental/normalproposals/NormalProposerTest.scala new file mode 100644 index 00000000..fd917e9d --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/experimental/normalproposals/NormalProposerTest.scala @@ -0,0 +1,291 @@ +/* + * NormalProposerTest.scala + * Atomic continuous element tests. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 25, 2016 + * + * Copyright 2016 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.normalproposals + +import org.scalatest.Matchers +import org.scalatest.WordSpec +import scala.math.exp +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.experimental.normalproposals._ +import com.cra.figaro.language._ +import JSci.maths.statistics._ +import com.cra.figaro.test.tags.NonDeterministic +import com.cra.figaro.ndtest._ + +// Largely copied from ContinuousTest, but applied only to NormalProposer elements using MH. The idea is that if we +// integrate the NormalProposer elements into the main library, then we can reuse the previous tests. Some additional +// tests were added for multiple Beta parameters, since Beta only conditionally uses Normal proposals. +class NormalProposerTest extends WordSpec with Matchers { + + val alpha: Double = 0.05 + + def varStatistic(value: Double, target: Double, n: Int) = { + val df = n - 1 + + // df * sample value / target value is distributed as chisq with df degrees of freedom + val chisq = df * value / target + + // (chisq - df) / sqrt(2 * df) is approximately N(0, 1) for high df + val stat = (chisq - df) / math.sqrt(2 * df) + + stat + } + + "An AtomicUniform" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + val target = 0.25 + Universe.createNew() + val elem = Uniform(0.0, 2.0) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicUniformTestResults", target, alpha) + } + } + + ndtest.run(10) + } + } + + "An AtomicNormal" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + 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 target = dist.cumulative(1.2) - dist.cumulative(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result - target, NDTest.TTEST, "AtomicNormalTestResults", 0.0, alpha) + } + } + + ndtest.run(10) + } + } + + "An AtomicExponential" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val elem = Exponential(2.0) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new ExponentialDistribution(2.0) + val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicExponentialTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "An AtomicGamma" when { + "k > 1.0, theta = 1.0" should { + "compute the correct value under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val k = 2.5 + val elem = Gamma(k) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new GammaDistribution(k) + val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "k = 1.0, theta is not 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val theta = 2.0 + val elem = Gamma(1.0, theta) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + // Using the fact that for Gamma(1,theta), the CDF is given by F(x) = 1 - exp(-x/theta) + def cdf(x: Double) = 1 - exp(-x / theta) + val targetProb = cdf(1.2) - cdf(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "k = 1.0, theta = 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val k = 1.0 + val elem = Gamma(k) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new GammaDistribution(k) + val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "k < 1.0, theta = 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val k = 0.6 + val elem = Gamma(k) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new GammaDistribution(k) + val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + } + + "An AtomicBeta" when { + "a >= 1.0 and b >= 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val a = 1.3 + val b = 2.7 + val elem = Beta(a, b) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new BetaDistribution(a, b) + val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "a >= 1.0 and b < 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val a = 1.2 + val b = 0.5 + val elem = Beta(a, b) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new BetaDistribution(a, b) + val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "a < 1.0 and b >= 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val a = 0.3 + val b = 1.0 + val elem = Beta(a, b) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new BetaDistribution(a, b) + val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + + "a < 1.0 and b < 1.0" should { + "compute the correct probability under Metropolis-Hastings" taggedAs NonDeterministic in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + val a = 0.3 + val b = 0.6 + val elem = Beta(a, b) + val alg = MetropolisHastings(20000, ProposalScheme.default, elem) + alg.start() + val dist = new BetaDistribution(a, b) + val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) + alg.stop() + alg.kill() + update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) + } + } + + ndtest.run(10) + } + } + } +} 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 bb1e0b45..1444f82f 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 @@ -78,12 +78,12 @@ class PBPTest extends WordSpec with Matchers { Inject(f: _*) }) val pbpSampler = ParticleGenerator(Universe.universe) - pbpSampler.update(n, pbpSampler.numArgSamples, List[(Double, _)]((1.0, 2.0))) + pbpSampler.update(n, pbpSampler.numSamplesFromAtomics, 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, pbpSampler.numArgSamples, List[(Double, _)]((1.0, 3.0))) + pbpSampler.update(n, pbpSampler.numSamplesFromAtomics, 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 @@ -116,9 +116,9 @@ class PBPTest extends WordSpec with Matchers { 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)(_ == 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) + bp.probability(e2)(_ == 3) should be(e2_3 +- tol) } "correctly retrieve the last messages to recompute sample densities" in { @@ -270,7 +270,7 @@ class PBPTest extends WordSpec with Matchers { Universe.createNew() val algorithm = ParticleBeliefPropagation(1, 20, 100, 100, f)(u1) algorithm.start() - val result = algorithm.probability(f, (b: Boolean) => b) + val result = algorithm.probability(f)(b => b) algorithm.kill() update(result, NDTest.TTEST, "DifferentUniverse", 0.6, alpha) } @@ -331,7 +331,7 @@ class PBPTest extends WordSpec with Matchers { }) val algorithm = ParticleBeliefPropagation(10, 4, 15, 15, loc, locX, locY) algorithm.start() - val locE = algorithm.expectation(loc, (d: (Double, Double)) => d._1 * d._2) + val locE = algorithm.expectation(loc)(d => d._1 * d._2) val cov = locE - algorithm.mean(locX) * algorithm.mean(locY) ParticleGenerator.clear diff --git a/Figaro/src/test/scala/com/cra/figaro/test/language/ElementsTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/language/ElementsTest.scala index eaf2442b..2f028e5f 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/language/ElementsTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/language/ElementsTest.scala @@ -311,6 +311,25 @@ class ElementsTest extends WordSpec with Matchers { } } + "An ApplyC with one argument" should { + "have value equal to its function applied to its argument" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val a = ApplyC(u)(_ + 1.0) + u.value = 1.3 + a.generate() + a.value should equal(2.3) + } + + "convert to the correct string" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val f = (d: Double) => d + 1.0 + ApplyC(u)(f).toString should equal("Apply(" + u + ", " + f + ")") + } + + } + "An Apply with two arguments" should { "have value equal to its function applied to its arguments" in { Universe.createNew() @@ -331,6 +350,27 @@ class ElementsTest extends WordSpec with Matchers { } } + "An ApplyC with two arguments" should { + "have value equal to its function applied to its arguments" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val a = ApplyC(u, v)( _ + _ + 1.0) + u.value = 1.3 + v.value = 1.0 + a.generate() + a.value should equal(3.3) + } + + "convert to the correct string" in { + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val f = (d1: Double, d2: Double) => d1 + d2 + 1.0 + ApplyC(u, v)(f).toString should equal("Apply(" + u + ", " + v + ", " + f + ")") + } + } + + "An Apply with three arguments" should { "have value equal to its function applied to its arguments" in { Universe.createNew() @@ -355,6 +395,31 @@ class ElementsTest extends WordSpec with Matchers { } } + "An ApplyC with three arguments" should { + "have value equal to its function applied to its arguments" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val a = ApplyC(u, v, w)(_ + _ + _ + 1.0) + u.value = 1.3 + v.value = 1.0 + w.value = 5.0 + a.generate() + a.value should equal(8.3) + } + + "convert to the correct string" in { + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val f = (d1: Double, d2: Double, d3: Double) => d1 + d2 + d3 + 1.0 + ApplyC(u, v, w)(f).toString should equal( + "Apply(" + u + ", " + v + ", " + w + ", " + f + ")") + } + } + + "An Apply with four arguments" should { "have value equal to its function applied to its arguments" in { Universe.createNew() @@ -382,6 +447,34 @@ class ElementsTest extends WordSpec with Matchers { } } + "An ApplyC with four arguments" should { + "have value equal to its function applied to its arguments" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val x = Constant(-2.0) + val a = ApplyC(u, v, w, x)(_ + _ + _ + _ + 1.0) + u.value = 1.3 + v.value = 1.0 + w.value = 5.0 + x.value = -2.0 + a.generate() + a.value should equal(6.3) + } + + "convert to the correct string" in { + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val x = Constant(-2.0) + val f = (d1: Double, d2: Double, d3: Double, d4: Double) => d1 + d2 + d3 + d4 + 1.0 + ApplyC(u, v, w, x)(f).toString should equal( + "Apply(" + u + ", " + v + ", " + w + ", " + x + ", " + f + ")") + } + } + + "An Apply with five arguments" should { "have value equal to its function applied to its arguments" in { Universe.createNew() @@ -415,6 +508,40 @@ class ElementsTest extends WordSpec with Matchers { } } + "An ApplyC with five arguments" should { + "have value equal to its function applied to its arguments" in { + Universe.createNew() + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val x = Constant(-2.0) + val y = Constant(0.5) + val a = + ApplyC(u, v, w, x, y)(_ + _ + _ + _ + _ + 1.0) + u.value = 1.3 + v.value = 1.0 + w.value = 5.0 + x.value = -2.0 + y.value = 0.5 + a.generate() + a.value should equal(6.8) + } + + "convert to the correct string" in { + val u = Uniform(0.0, 2.0) + val v = Constant(1.0) + val w = Select(0.5 -> 0.0, 0.5 -> 5.0) + val x = Constant(-2.0) + val y = Constant(0.5) + val f = + (d1: Double, d2: Double, d3: Double, d4: Double, d5: Double) => d1 + d2 + d3 + d4 + d5 + 1.0 + ApplyC(u, v, w, x, y)(f).toString should equal( + "Apply(" + u + ", " + v + ", " + w + ", " + x + ", " + y + ", " + f + ")") + } + } + + + "An Inject" should { "have value equal to the sequence of values of its arguments" in { Universe.createNew() diff --git a/Figaro/src/test/scala/com/cra/figaro/test/language/UniverseStateTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/language/UniverseStateTest.scala new file mode 100644 index 00000000..1c880f61 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/language/UniverseStateTest.scala @@ -0,0 +1,412 @@ +/* + * UniverseStateTest.scala + * Tests for saving and restoring universe state. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 3, 2016 + * + * Copyright 2016 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.language + +import com.cra.figaro.algorithm.OneTime +import com.cra.figaro.language.Element.ElemVal +import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.continuous.{Normal, Uniform} +import org.scalatest.{Matchers, WordSpec} + +import scala.collection.mutable + +class UniverseStateTest extends WordSpec with Matchers { + "A universe state" when { + "restoring elements in the universe" should { + "restore value and randomness" in { + val universe = Universe.createNew() + val e1 = Uniform(0.0, 1.0) + e1.generate() + val e1Randomness = e1.randomness + val e1Value = e1.value + val state = new UniverseState(universe) + + e1.generate() + + state.restore() + e1.randomness should equal(e1Randomness) + e1.value should equal(e1Value) + } + + "restore set and unset elements" in { + val universe = Universe.createNew() + val e1 = Normal(0.0, 1.0) + val e2 = Uniform(0.0, 1.0) + e1.set(100.0) + e2.generate() + val state = new UniverseState(universe) + + e1.unset() + e1.generate() + e2.set(0.5) + + state.restore() + e1.generate() // Won't produce anything other than 100.0 unless e1 is unset + e2.generate() // Produces something other than 0.5 with probability 1 + e1.value should equal(100.0) + e2.value should not equal 0.5 + } + + "restore previous evidence with and without contingencies" when { + "conditions are changed" in { + val universe = Universe.createNew() + val e1 = Uniform(0.0, 1.0) + val e2 = Flip(0.2) + e1.addCondition(_ > 0.3) + e1.addCondition(_ < 0.8, List(ElemVal(e2, true))) + val e1Conditions = e1.allConditions + val state = new UniverseState(universe) + + e1.removeConditions() // Only removes the first (non-contingent) condition + e1.addCondition(_ > 0.5, List(ElemVal(e2, false))) + e2.addCondition((b: Boolean) => b) + + state.restore() + e1.allConditions should contain theSameElementsAs e1Conditions + e2.allConditions should be(empty) + universe.conditionedElements should contain theSameElementsAs List(e1) + } + + "constraints are changed" in { + val universe = Universe.createNew() + val e1 = Uniform(0.0, 1.0) + val e2 = Flip(0.2) + e1.addConstraint(_ * 2.0) + e1.addConstraint(_ + 1.0, List(ElemVal(e2, true))) + val e1Constraints = e1.allConstraints + val state = new UniverseState(universe) + + e1.removeConstraints() // Only removes the first (non-contingent) constraint + e1.addConstraint(_ * 3.0, List(ElemVal(e2, false))) + e2.addConstraint((b: Boolean) => if(b) 2.0 else 1.0) + + state.restore() + e1.allConstraints should contain theSameElementsAs e1Constraints + e2.allConstraints should be(empty) + universe.constrainedElements should contain theSameElementsAs List(e1) + } + + "observations are changed" in { + val universe = Universe.createNew() + val e1 = Uniform(0.0, 1.0) + val e2 = Flip(0.2) + e1.observe(0.3) + val state = new UniverseState(universe) + + e1.unobserve() + e2.observe(true) + + state.restore() + e1.observation should equal(Some(0.3)) + e2.observation should equal(None) + universe.conditionedElements should contain theSameElementsAs List(e1) + } + } + + "activate the correct elements" when { + "elements are deactivated manually" in { + val universe = Universe.createNew() + val e1 = Constant(8) + val e2 = Constant(7) + e2.deactivate() + val state = new UniverseState(universe) + + e1.deactivate() + e2.activate() + + state.restore() + e1.active should be(true) + e2.active should be(false) + universe.activeElements should contain theSameElementsAs List(e1) + } + + "temporary elements are cleared" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val e2 = Constant(7) // This makes e2 temporary + val state = new UniverseState(universe) + + universe.clearTemporaries() + + state.restore() + universe.activeElements should contain theSameElementsAs List(e1, e2) + } + + "the universe is cleared" in { + val universe = Universe.createNew() + val e1 = Constant(8) + val state = new UniverseState(universe) + + universe.clear() + + state.restore() + e1.active should be(true) + universe.activeElements should contain theSameElementsAs List(e1) + } + } + + "restore context and direct context contents" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val e2 = Constant(7) + val state = new UniverseState(universe) + + e2.deactivate() + + state.restore() + e1.directContextContents should contain theSameElementsAs List(e2) + e2.context should contain theSameElementsAs List(e1) + } + } + + "restoring information about the universe structure" should { + "copy the previous context stack" when { + "elements in the stack are deactivated" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val e2 = Constant(7) + universe.pushContext(e2) + val e3 = Constant(6) + universe.pushContext(e3) + val state = new UniverseState(universe) + + e2.deactivate() + + state.restore() + universe.contextStack should equal(List(e3, e2, e1)) + } + + "the context stack is modified manually" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val e2 = Constant(7) + universe.pushContext(e2) + val e3 = Constant(6) + universe.pushContext(e3) + val state = new UniverseState(universe) + + universe.popContext(e2) + + state.restore() + universe.contextStack should equal(List(e3, e2, e1)) + } + + "temporary elements are cleared" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val e2 = Constant(7) + universe.pushContext(e2) + val e3 = Constant(6) + universe.pushContext(e3) + val state = new UniverseState(universe) + + universe.clearTemporaries() + + state.restore() + universe.contextStack should equal(List(e3, e2, e1)) + } + + "the universe is cleared" in { + val universe = Universe.createNew() + val e1 = Constant(8) + universe.pushContext(e1) + val state = new UniverseState(universe) + + universe.clear() + + state.restore() + universe.contextStack should equal(List(e1)) + } + } + + "repopulate the same stochastic elements" in { + val universe = Universe.createNew() + val e1 = Normal(0.0, 1.0) + val e2 = Uniform(0.0, 1.0) + e2.deactivate() + val state = new UniverseState(universe) + + e1.deactivate() + e2.activate() + + state.restore() + universe.stochasticElements should contain theSameElementsAs List(e1) + } + + "repopulate uses and usedBy" when { + "registered uses are changed manually" in { + val universe = Universe.createNew() + val e1 = Constant(8) + val e2 = Constant(7) + val e3 = Constant(6) + val e4 = Constant(5) + // e1 -> e2, e2 -> e3, e1 -> e4, e2 -> e4 + universe.registerUses(e2, e1) + universe.registerUses(e3, e2) + universe.registerUses(e4, e1) + universe.registerUses(e4, e2) + val state = new UniverseState(universe) + + universe.deregisterUses(e2, e1) + universe.deregisterUses(e3, e2) + universe.registerUses(e3, e1) + + state.restore() + universe.uses(e1) should be(empty) + universe.usedBy(e1) should equal(Set(e2, e3, e4)) + universe.directlyUsedBy(e1) should equal(Set(e2, e4)) + universe.uses(e2) should equal(Set(e1)) + universe.usedBy(e2) should equal(Set(e3, e4)) + universe.directlyUsedBy(e2) should equal(Set(e3, e4)) + universe.uses(e3) should equal(Set(e1, e2)) + universe.usedBy(e3) should be(empty) + universe.directlyUsedBy(e3) should be(empty) + universe.uses(e4) should equal(Set(e1, e2)) + universe.usedBy(e4) should be(empty) + universe.directlyUsedBy(e4) should be(empty) + } + + "elements are added to the model" in { + val universe = Universe.createNew() + val e1 = Constant(8) + val e2 = -e1 + val state = new UniverseState(universe) + + val e3 = e1 ++ e2 + val e4 = -e3 + + state.restore() + universe.uses(e1) should be(empty) + universe.usedBy(e1) should equal(Set(e2)) + universe.directlyUsedBy(e1) should equal(Set(e2)) + universe.uses(e2) should equal(Set(e1)) + universe.usedBy(e2) should be(empty) + universe.directlyUsedBy(e2) should be(empty) + universe.uses(e3) should be(empty) + universe.usedBy(e3) should be(empty) + universe.directlyUsedBy(e3) should be(empty) + universe.uses(e4) should be(empty) + universe.usedBy(e4) should be(empty) + universe.directlyUsedBy(e4) should be(empty) + } + + "elements are activated or deactivated" in { + val universe = Universe.createNew() + val e1 = Constant(8) + val e2 = -e1 + val e3 = -e2 + e3.deactivate() + val state = new UniverseState(universe) + val a = state.myUses(e2) + + e3.activate() + e2.deactivate() + + val b = state.myUses(e2) + + state.restore() + universe.uses(e1) should be(empty) + universe.usedBy(e1) should equal(Set(e2)) + universe.directlyUsedBy(e1) should equal(Set(e2)) + universe.uses(e2) should equal(Set(e1)) + universe.usedBy(e2) should be(empty) + universe.directlyUsedBy(e2) should be(empty) + universe.uses(e3) should be(empty) + universe.usedBy(e3) should be(empty) + universe.directlyUsedBy(e3) should be(empty) + } + + "contingent evidence changes" in { + val universe = Universe.createNew() + val e1 = Flip(0.2) + val e2 = Flip(0.3) + e1.addCondition((b: Boolean) => b, List(ElemVal(e2, false))) + val state = new UniverseState(universe) + + e1.removeConditions() + e2.addConstraint((b: Boolean) => if(b) 2.0 else 1.0, List(ElemVal(e1, true))) + + state.restore() + universe.uses(e1) should equal(Set(e2)) + universe.usedBy(e1) should be(empty) + universe.directlyUsedBy(e1) should be(empty) + universe.uses(e2) should be(empty) + universe.usedBy(e2) should equal(Set(e1)) + universe.directlyUsedBy(e2) should equal(Set(e1)) + } + } + + "not modify registered maps" when { + "registered algorithms change" in { + val universe = Universe.createNew() + var alg1Killed = false + var alg2Killed = false + val alg1 = new OneTime { override def run() = {} ; override def kill() = { alg1Killed = true } } + val alg2 = new OneTime { override def run() = {} ; override def kill() = { alg2Killed = true } } + alg1.start() + alg2.start() + universe.registerAlgorithm(alg1) + universe.deregisterAlgorithm(alg2) + val state = new UniverseState(universe) + + universe.deregisterAlgorithm(alg1) + universe.registerAlgorithm(alg2) + + state.restore() + universe.clear() // Kills registered algorithms (i.e. alg2) + alg1Killed should be(false) + alg2Killed should be(true) + } + + "registered element maps change" in { + val universe = Universe.createNew() + val e1 = Flip(0.2) + val set1: mutable.Set[Element[_]] = mutable.Set(e1) + val set2: mutable.Set[Element[_]] = mutable.Set(e1) + universe.register(set1) + val state = new UniverseState(universe) + + universe.deregister(set1) + universe.register(set2) + + state.restore() + universe.clear() // Clears registered element maps (i.e. set2) + set1 should equal(Set(e1)) + set2 should be(empty) + } + + "registered universe maps change" in { + val universe = Universe.createNew() + val map1 = mutable.Map(universe -> 1) + val map2 = mutable.Map(universe -> 2) + universe.registerUniverse(map1) + val state = new UniverseState(universe) + + universe.deregisterUniverse(map1) + universe.registerUniverse(map2) + + state.restore() + universe.clear() // Removes universe from registered maps (i.e. map2) + map1 should equal(Map(universe -> 1)) + map2 should be(empty) + } + } + } + } +} 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 ec57cb4d..bac54451 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 @@ -24,6 +24,7 @@ import JSci.maths.SpecialMath.gamma import org.apache.commons.math3.distribution.MultivariateNormalDistribution import com.cra.figaro.test.tags.NonDeterministic import com.cra.figaro.ndtest._ +import scala.collection.mutable.ArrayBuffer class ContinuousTest extends WordSpec with Matchers { @@ -50,7 +51,7 @@ class ContinuousTest extends WordSpec with Matchers { val elem = Uniform(0.0, 2.0) val alg = Importance(20000, elem) alg.start() - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicUniformTestResults", target, alpha) @@ -68,7 +69,7 @@ class ContinuousTest extends WordSpec with Matchers { val elem = Uniform(0.0, 2.0) val alg = MetropolisHastings(20000, ProposalScheme.default, elem) alg.start() - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicUniformTestResults", target, alpha) @@ -109,7 +110,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() // 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 - val result = alg.probability(uniformComplex, (d: Double) => 1.25 <= d && d < 1.5) + val result = alg.probability(uniformComplex)(d => 1.25 <= d && d < 1.5) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundUniformTestResults", target, alpha) @@ -137,7 +138,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new NormalDistribution(1.0, 2.0) val target = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result - target, NDTest.TTEST, "AtomicNormalTestResults", 0.0, alpha) @@ -156,7 +157,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new NormalDistribution(1.0, 2.0) val target = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result - target, NDTest.TTEST, "AtomicNormalTestResults", 0.0, alpha) @@ -194,7 +195,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist2 = new NormalDistribution(1.0, 2.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val target = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result - target, NDTest.TTEST, "CompoundNormalMeanResultsDiff", 0.0, alpha) @@ -225,7 +226,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist2 = new NormalDistribution(2.0, 3.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundNormalMeanTestResults", targetProb, alpha) @@ -257,7 +258,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist4 = new NormalDistribution(1.0, 3.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val target = 0.25 * getProb(dist1) + 0.25 * getProb(dist2) + 0.25 * getProb(dist3) + 0.25 * getProb(dist4) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result - target, NDTest.TTEST, "CompoundNormalTestResults", 0.0, alpha) @@ -428,6 +429,58 @@ class ContinuousTest extends WordSpec with Matchers { } } + "A KernelDensity" should { + "have the correct density" in { + Universe.createNew() + + val bandwidth = 2.0 + val data = Seq(1.0, 2.0, 3.0) + val dataDistrs = data.map(d => new NormalDistribution(d, bandwidth)) + + val elem = KernelDensity(data, bandwidth) + val result = elem.density(2.5) + val target = dataDistrs.map(dist => dist.probability(2.5)).sum / 3.0 + result should be(target +- 1e-6) + } + + "produce samples centered at an input point" taggedAs (NonDeterministic) in { + val ndtest = new NDTest { + override def oneTest = { + Universe.createNew() + // this gives get three disjoint normals, simplifying testing + val data = Seq(1.0, 100, 200) + // choosing a weird bandwidth to make sure it's actually getting used + val elem = KernelDensity(data, 2.3) + + val deviations = ArrayBuffer[Double]() + val selectedIndices = ArrayBuffer[Int]() + for (i <- 0 to 1000) { + val rand = elem.generateRandomness() + val value = elem.generateValue(rand) + val selectedInfo = data.zipWithIndex.map(v => (Math.pow(v._1 - value, 2), v._2)).sortBy(v => v._1).head + deviations += selectedInfo._1 + selectedIndices += selectedInfo._2 + } + val sampleStdDev = deviations.sum / (deviations.length - 1) + // selected indices should be uniform + val indDistr = selectedIndices.groupBy(d=>d).mapValues(s => 1.0 * s.length / selectedIndices.length) + + update(sampleStdDev, NDTest.TTEST, "KernelDensityTestResultsStdDev", 2.3, alpha) + update(indDistr(0), NDTest.TTEST, "KernelDensityTestResultsSampleDistr1", 0.33, alpha) + update(indDistr(1), NDTest.TTEST, "KernelDensityTestResultsSampleDistr2", 0.33, alpha) + update(indDistr(2), NDTest.TTEST, "KernelDensityTestResultsSampleDistr3", 0.33, alpha) + } + } + + ndtest.run(10) + } + + "convert to the correct string" in { + Universe.createNew() + KernelDensity(Seq(1, 2, 3), 2.0).toString should equal("KernelDensity(bandwidth=" + 2.0 + ")") + } + } + "An AtomicMultivariateNormal" should { val means = List(1.0, 2.0) val covariances = List(List(.25, .15), List(.15, .25)) @@ -523,7 +576,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new ExponentialDistribution(2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicExponentialTestResults", targetProb, alpha) @@ -542,7 +595,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new ExponentialDistribution(2.0) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicExponentialTestResults", targetProb, alpha) @@ -580,7 +633,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist2 = new ExponentialDistribution(2.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundExponentialTestResults", targetProb, alpha) @@ -666,7 +719,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -686,7 +739,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -722,7 +775,7 @@ class ContinuousTest extends WordSpec with Matchers { // Using the fact that for Gamma(1,theta), the CDF is given by F(x) = 1 - exp(-x/theta) def cdf(x: Double) = 1 - exp(-x / theta) val targetProb = cdf(1.2) - cdf(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -743,7 +796,7 @@ class ContinuousTest extends WordSpec with Matchers { // Using the fact that for Gamma(1,theta), the CDF is given by F(x) = 1 - exp(-x/theta) def cdf(x: Double) = 1 - exp(-x / theta) val targetProb = cdf(1.2) - cdf(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -780,7 +833,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -800,7 +853,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -822,7 +875,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -842,7 +895,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new GammaDistribution(k) val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicGammaTestResults", targetProb, alpha) @@ -867,7 +920,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist2 = new GammaDistribution(3.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundGammaKTestResults", targetProb, alpha) @@ -897,7 +950,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist2 = new GammaDistribution(1.0) def getProb(dist: ProbabilityDistribution) = dist.cumulative(1.2) - dist.cumulative(0.7) val targetProb = 0.5 * getProb(dist1) + 0.5 * getProb(dist2) - val result = alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) + val result = alg.probability(elem)(d => 0.7 <= d && d < 1.2) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundGammaTestResults", targetProb, alpha) @@ -990,7 +1043,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new BetaDistribution(a, b) val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) - val result = alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) @@ -1011,7 +1064,7 @@ class ContinuousTest extends WordSpec with Matchers { alg.start() val dist = new BetaDistribution(a, b) val targetProb = dist.cumulative(0.3) - dist.cumulative(0.2) - val result = alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicBetaTestResults", targetProb, alpha) @@ -1052,7 +1105,7 @@ class ContinuousTest extends WordSpec with Matchers { val dist4 = new BetaDistribution(1.0, 3.0) 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) - val result = alg.probability(elem, (d: Double) => 0.2 <= d && d < 0.3) + val result = alg.probability(elem)(d => 0.2 <= d && d < 0.3) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundBetaTestResults", targetProb, alpha) @@ -1148,7 +1201,7 @@ class ContinuousTest extends WordSpec with Matchers { val r = ds(0) / (ds(0) + ds(1)) 0.2 <= r && r < 0.3 } - val result = alg.probability(elem, (ds: Array[Double]) => check(ds)) + val result = alg.probability(elem)(ds => check(ds)) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicDirichletTestResults", targetProb, alpha) @@ -1173,7 +1226,7 @@ class ContinuousTest extends WordSpec with Matchers { val r = ds(0) / (ds(0) + ds(1)) 0.2 <= r && r < 0.3 } - val result = alg.probability(elem, (ds: Array[Double]) => check(ds)) + val result = alg.probability(elem)(ds => check(ds)) alg.stop() alg.kill update(result, NDTest.TTEST, "AtomicDirichletTestResults", targetProb, alpha) @@ -1223,7 +1276,7 @@ class ContinuousTest extends WordSpec with Matchers { val r = ds(0) / (ds(0) + ds(1)) 0.2 <= r && r < 0.3 } - val result = alg.probability(elem, (ds: Array[Double]) => check(ds)) + val result = alg.probability(elem)(ds => check(ds)) alg.stop() alg.kill update(result, NDTest.TTEST, "CompoundDirichletTestResults", targetProb, alpha) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/discrete/DiscreteTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/discrete/DiscreteTest.scala index 002bb0f5..07380772 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/discrete/DiscreteTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/discrete/DiscreteTest.scala @@ -30,7 +30,7 @@ class DiscreteTest extends WordSpec with Matchers { val elem = Uniform(1, 2, 3, 4) val alg = Importance(20000, elem) alg.start() - alg.probability(elem, (i: Int) => 1 <= i && i < 3) should be(0.5 +- 0.01) + alg.probability(elem)(i => 1 <= i && i < 3) should be(0.5 +- 0.01) } "for an input within the options have density equal to 1 divided by the number of options" in { @@ -76,7 +76,7 @@ class DiscreteTest extends WordSpec with Matchers { val alg = Importance(20000, elem) alg.start() val targetProb = 0.5 * 0.5 + 0.5 * 0.3 - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "convert to the correct string" in { @@ -95,7 +95,7 @@ class DiscreteTest extends WordSpec with Matchers { val alg = Importance(20000, elem) alg.start() val targetProb = 0.9 * 0.9 * 0.1 - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "produce the correct result under Metropolis-Hastings" in { @@ -104,7 +104,7 @@ class DiscreteTest extends WordSpec with Matchers { val alg = MetropolisHastings(50000, ProposalScheme.default, elem) alg.start() val targetProb = 0.9 * 0.9 * 0.1 - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "have the correct density" in { @@ -127,7 +127,7 @@ class DiscreteTest extends WordSpec with Matchers { val alg = Importance(20000, elem) alg.start() val targetProb = 0.5 * 0.9 * 0.9 * 0.1 + 0.5 * 0.7 * 0.7 * 0.3 - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "convert to the correct string" in { @@ -145,7 +145,7 @@ class DiscreteTest extends WordSpec with Matchers { alg.start() val dist = new PoissonDistribution(0.9) val targetProb = dist.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "produce the correct result under Metropolis-Hastings" in { @@ -155,7 +155,7 @@ class DiscreteTest extends WordSpec with Matchers { alg.start() val dist = new PoissonDistribution(0.9) val targetProb = dist.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "have the correct density" in { @@ -191,7 +191,7 @@ class DiscreteTest extends WordSpec with Matchers { val dist1 = new PoissonDistribution(0.7) val dist2 = new PoissonDistribution(0.9) val targetProb = 0.5 * dist1.probability(3) + 0.5 * dist2.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "convert to the correct string" in { @@ -209,7 +209,7 @@ class DiscreteTest extends WordSpec with Matchers { alg.start() val dist = new BinomialDistribution(5, 0.9) val targetProb = dist.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "produce the correct result under Metropolis-Hastings" in { @@ -219,7 +219,7 @@ class DiscreteTest extends WordSpec with Matchers { alg.start() val dist = new BinomialDistribution(5, 0.9) val targetProb = dist.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "have the correct density" in { @@ -283,7 +283,7 @@ class DiscreteTest extends WordSpec with Matchers { val dist1 = new BinomialDistribution(5, 0.7) val dist2 = new BinomialDistribution(5, 0.9) val targetProb = 0.5 * dist1.probability(3) + 0.5 * dist2.probability(3) - alg.probability(elem, (i: Int) => i == 3) should be(targetProb +- 0.01) + alg.probability(elem)(_ == 3) should be(targetProb +- 0.01) } "convert to the correct string" in { @@ -300,7 +300,7 @@ class DiscreteTest extends WordSpec with Matchers { val elem = SwitchingFlip(0.8) val alg = Importance(40000, elem) alg.start() - alg.probability(elem, (b: Boolean) => b) should be(0.8 +- 0.01) + alg.probability(elem)(b => b) should be(0.8 +- 0.01) } "always produce the opposite value for the next randomness" in { @@ -324,7 +324,7 @@ class DiscreteTest extends WordSpec with Matchers { val elem = SwitchingFlip(0.7) val alg = MetropolisHastings(20000, ProposalScheme.default, elem) alg.start() - alg.probability(elem, (b: Boolean) => b) should be(0.7 +- 0.01) + alg.probability(elem)(b => b) should be(0.7 +- 0.01) } "produce the right probability when conditioned under Metropolis-Hastings" in { @@ -336,7 +336,7 @@ class DiscreteTest extends WordSpec with Matchers { alg.start() val p1 = 0.2 * 0.7 val p2 = 0.8 * 0.4 - alg.probability(elem1, (b: Boolean) => b) should be(p1 / (p1 + p2) +- 0.01) + alg.probability(elem1)(b => b) should be(p1 / (p1 + p2) +- 0.01) } } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/util/LogStatisticsTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/util/LogStatisticsTest.scala new file mode 100644 index 00000000..6280c35f --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/util/LogStatisticsTest.scala @@ -0,0 +1,203 @@ +/* + * LogStatistics.scala + * Tests for statistics in log space. + * + * Created By: William Kretschmer (kretsch@mit.edu) + * Creation Date: Aug 22, 2016 + * + * Copyright 2016 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.util + +import com.cra.figaro.util._ +import com.cra.figaro.util.LogStatistics.oneSidedTTest +import org.scalatest.{Matchers, WordSpec} + +class LogStatisticsTest extends WordSpec with Matchers { + // Used to test underflow/overflow + val logOffset = 100000 + + "Running a one-sided t-test" should { + "throw an exception" when { + "one of the counts is less than 2" in { + val v1 = LogStatistics(0.0, 1.0, 1) + val v2 = LogStatistics(1.0, 1.0, 30) + an [IllegalArgumentException] shouldBe thrownBy(oneSidedTTest(v1, v2)) + an [IllegalArgumentException] shouldBe thrownBy(oneSidedTTest(v2, v1)) + } + } + + "handle 0 variance" when { + "one variance is 0" in { + val v1 = LogStatistics(0.0, Double.NegativeInfinity, 20) + val v2 = LogStatistics(Double.NegativeInfinity, 0.0, 20) + val v3 = LogStatistics(0.2, 0.0, 20) + val v4 = LogStatistics(0.0, 0.0, 20) + + oneSidedTTest(v1, v2) should be (1.3060E-4 +- 1E-8) + oneSidedTTest(v1, v3) should be (0.16727 +- 1E-5) + oneSidedTTest(v1, v4) should be (0.5 +- 1E-5) + } + + "both variances are 0" in { + val v1 = LogStatistics(0.0, Double.NegativeInfinity, 20) + val v2 = LogStatistics(Double.NegativeInfinity, Double.NegativeInfinity, 20) + + oneSidedTTest(v1, v2) should be (0.0 +- 1E-5) + oneSidedTTest(v1, v1) should be (0.5 +- 1E-5) + oneSidedTTest(v2, v2) should be (0.5 +- 1E-5) + } + } + + "handle 0 mean" when { + "one mean is 0" in { + val v1 = LogStatistics(Double.NegativeInfinity, 0.0, 20) + val v2 = LogStatistics(0.0, 1.0, 20) + + oneSidedTTest(v1, v2) should be (0.013536 +- 1E-6) + } + + "both means are 0" in { + val v1 = LogStatistics(Double.NegativeInfinity, 0.0, 20) + val v2 = LogStatistics(Double.NegativeInfinity, 1.0, 20) + + oneSidedTTest(v1, v2) should be (0.5 +- 1E-5) + } + } + + "compute degrees of freedom" when { + "the counts are the same" in { + val v1 = LogStatistics(0.0, 0.0, 5) + val v2 = LogStatistics(0.1, 0.1, 5) + + oneSidedTTest(v1, v2) should be (0.43763 +- 1E-5) + } + + "the counts are different" in { + val v1 = LogStatistics(0.0, 0.0, 3) + val v2 = LogStatistics(0.1, 0.1, 7) + + oneSidedTTest(v1, v2) should be (0.44396 +- 1E-5) + } + } + + "compare the lesser mean to the greater mean" when { + "the means are the same" in { + val v1 = LogStatistics(0.0, 0.0, 20) + val v2 = LogStatistics(0.0, 0.1, 30) + + oneSidedTTest(v1, v2) should be (0.5 +- 1E-5) + oneSidedTTest(v2, v1) should be (0.5 +- 1E-5) + } + + "the means are different" in { + val v1 = LogStatistics(0.0, 0.0, 20) + val v2 = LogStatistics(0.1, 0.1, 30) + + oneSidedTTest(v1, v2) should be (0.36147 +- 1E-5) + oneSidedTTest(v2, v1) should be (0.36147 +- 1E-5) + } + } + + "remain stable in log space" when { + "exponentiation could underflow" in { + val v1 = LogStatistics(0.0, 0.0, 20).multiplyByConstant(-logOffset) + val v2 = LogStatistics(0.1, 0.1, 30).multiplyByConstant(-logOffset) + + oneSidedTTest(v1, v2) should be (0.36147 +- 1E-5) + } + + "exponentiation could overflow" in { + val v1 = LogStatistics(0.0, 0.0, 20).multiplyByConstant(logOffset) + val v2 = LogStatistics(0.1, 0.1, 30).multiplyByConstant(logOffset) + + oneSidedTTest(v1, v2) should be (0.36147 +- 1E-5) + } + } + } + + "Running online log statistics" should { + "handle 0 observations" when { + "all of the observations are 0" in { + val ols = new OnlineLogStatistics {} + val numObservations = 100 + for(_ <- 1 to numObservations) ols.record(Double.NegativeInfinity) + + val logStats = ols.totalLogStatistics + logStats.logMean should be (Double.NegativeInfinity) + logStats.logVariance should be (Double.NegativeInfinity) + logStats.count should be (numObservations) + } + + "some of the observations are 0" in { + val ols = new OnlineLogStatistics {} + val observations = List(Double.NegativeInfinity, 0.0, -0.5, Double.NegativeInfinity, -2.2, 0.1) + observations.foreach(ols.record) + + val logStats = ols.totalLogStatistics + logStats.logMean should be (-0.75413 +- 1E-5) + logStats.logVariance should be (-1.3674 +- 1E-4) + } + } + + "compute the correct result" when { + "given no observations" in { + val ols = new OnlineLogStatistics {} + + val logStats = ols.totalLogStatistics + logStats.logMean should be (Double.NegativeInfinity) + logStats.logVariance.isNaN should be (true) + logStats.count should be (0) + } + + "given one observation" in { + val ols = new OnlineLogStatistics {} + ols.record(1.5) + + val logStats = ols.totalLogStatistics + logStats.logMean should be (1.5 +- 1E-4) + logStats.logVariance.isNaN should be (true) + logStats.count should be (1) + } + + "given several observations" in { + val ols = new OnlineLogStatistics {} + val observations = List(-3.7, 2.1, 0.8, 1.2, 0.8, -0.5) + observations.foreach(ols.record) + + val logStats = ols.totalLogStatistics + logStats.logMean should be (1.0158 +- 1E-4) + logStats.logVariance should be (2.1337 +- 1E-4) + logStats.count should be (observations.length) + } + } + + "remain stable in log space" when { + "exponentiation could underflow" in { + val ols = new OnlineLogStatistics {} + val observations = List(-3.7, 2.1, 0.8, 1.2, 0.8, -0.5).map(_ - logOffset) + observations.foreach(ols.record) + + val logStats = ols.totalLogStatistics.multiplyByConstant(logOffset) + logStats.logMean should be (1.0158 +- 1E-4) + logStats.logVariance should be (2.1337 +- 1E-4) + logStats.count should be (observations.length) + } + + "exponentiation could overflow" in { + val ols = new OnlineLogStatistics {} + val observations = List(-3.7, 2.1, 0.8, 1.2, 0.8, -0.5).map(_ + logOffset) + observations.foreach(ols.record) + + val logStats = ols.totalLogStatistics.multiplyByConstant(-logOffset) + logStats.logMean should be (1.0158 +- 1E-4) + logStats.logVariance should be (2.1337 +- 1E-4) + logStats.count should be (observations.length) + } + } + } +} diff --git a/FigaroExamples/META-INF/MANIFEST.MF b/FigaroExamples/META-INF/MANIFEST.MF index 07c9c94c..9a6ff07d 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: 4.0.0 -Require-Bundle: com.cra.figaro;bundle-version="4.0.0", +Bundle-Version: 4.1.0 +Require-Bundle: com.cra.figaro;bundle-version="4.1.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/CarAndEngine.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/CarAndEngine.scala index 561e29fc..7156e70e 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/CarAndEngine.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/CarAndEngine.scala @@ -55,7 +55,7 @@ object CarAndEngine { val alg = VariableElimination(car.speed) alg.start() alg.stop() - println(alg.expectation(car.speed, (d: Double) => d)) + println(alg.expectation(car.speed)(d => d)) alg.kill() } } diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/FairDice.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/FairDice.scala index 85678b28..c555e4d4 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/FairDice.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/FairDice.scala @@ -95,11 +95,11 @@ object FairDice { ve.stop() println("\nThe probabilities of seeing each side of d_1 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d1, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d1)(_ == o) + " -> " + o) } println("\nThe probabilities of seeing each side of d_2 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d2, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d2)( _ == o) + " -> " + o) } println("\nThe probabilities of seeing each side of d_3 are: ") - outcomes.foreach { o => println("\t" + ve.probability(d3, (i: Int) => i == o) + " -> " + o) } + outcomes.foreach { o => println("\t" + ve.probability(d3)( _ == o) + " -> " + o) } ve.kill() diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/GaussianProcessTraining.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/GaussianProcessTraining.scala new file mode 100644 index 00000000..63876981 --- /dev/null +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/GaussianProcessTraining.scala @@ -0,0 +1,131 @@ +/* + * GaussianProcessTraining.scala + * Demonstrates use of externally trained models in Figaro. + * + * Created By: Dan Garant (dgarant@cra.com) + * Creation Date: May 20, 2016 + * + * Copyright 2016 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 org.apache.commons.math3.linear._ +import com.cra.figaro.language._ +import com.cra.figaro.util.random +import com.cra.figaro.library.atomic._ +import com.cra.figaro.library.atomic.continuous.Normal +import com.cra.figaro.algorithm.sampling.Importance + +/** + * Trains a GaussianProcess model and uses it as part of a chain + */ +object GaussianProcessTraining { + + def main(args:Array[String]) = { + + // set up the model + // y = x^2 + eps, eps ~ N(0, 1) + val x = Range.Double(1, 10, 1) + val y = x.map(xi => math.pow(xi, 2) + random.nextGaussian()) + + // wire together dependence structure + val gp = new GaussianProcess(new GaussianCovarianceFunction(1 / 2.0)) + gp.train(x zip y toList) + val xElement = continuous.Uniform(0, 11) + val yElement = Chain(xElement, gp.model) + + // estimate conditional expectation + xElement.observe(7.5) + var importance = Importance(1000, yElement) + importance.start() + val expectedYVal = importance.computeExpectation(yElement, (v: Double) => v) + importance.kill() + println("E[Y|X=7.5] = " + expectedYVal) + + // now adding an effect of y + val zElement = Chain(yElement, (v:Double) => Normal(v + 3, 1)) + + importance = Importance(1000, zElement) + importance.start() + val expectedZVal = importance.computeExpectation(zElement, (v: Double) => v) + importance.kill() + println("E[Z|X=7.5] = " + expectedZVal) + } +} + +/** + * General form of a covariance function, + * taking two items of type T and producing a measure + */ +trait CovarianceFunction[T] { + def apply(v1: T, v2: T): Double +} + +/** The Gaussian, or radial basis function kernel / covariance function between univariate observations */ +class GaussianCovarianceFunction(var gamma: Double) extends CovarianceFunction[Double] { + + /** Computes covariance using the L2 norm */ + override def apply(v1: Double, v2: Double): Double = { + Math.exp(-gamma * Math.pow(v1 - v2, 2)) + } + + override def toString = "GaussianCovarianceFunction(gamma=" + gamma + ")" +} + +/** + * Estimates and performs posterior prediction from a Gaussian process. + * @param covarianceFunction Defines the inner product between feature vectors + * @param noiseVariance Prior variance of noise at design points + */ +class GaussianProcess[Input](var covarianceFunction: CovarianceFunction[Input], noiseVariance: Double = 0.001) { + + var priorMean: Double = 0 + var covarianceInverse: RealMatrix = null + var inputs: Seq[Input] = null + var alpha: RealMatrix = null + var responses: RealVector = null + var numDimensions: Integer = null + + /** + * Estimate the conditional density of a new point. + * @param newInputs The point to evaluate at + * @returns Posterior normal distribution + */ + def model(newInput: Input): Element[Double] = { + if (covarianceInverse == null) { + throw new IllegalArgumentException("The Gaussian process must be fit before 'model' can be called") + } + + val newCovariance = new ArrayRealVector((0 until inputs.length).map(i => covarianceFunction(inputs(i), newInput)).toArray) + var variance = 1 - covarianceInverse.preMultiply(newCovariance).dotProduct(newCovariance) + val mean = alpha.preMultiply(newCovariance).getEntry(0) + + Normal(priorMean + mean, Math.sqrt(variance)) + } + + /** + * Estimates the parameters of the Gaussian process (the inverse of the covariance matrix) from data + * @param data A sequence of input, output pairs to use when fitting the model + */ + def train(data: List[(Input, Double)]) = { + inputs = data map { _._1 } + priorMean = (data map { _._2 } sum) / data.length + responses = new ArrayRealVector(data map { _._2 - priorMean } toArray) + + // construct covariance matrix + val rows = (0 until data.length).map(i => { + (0 until data.length).map(j => { + covarianceFunction(data(i)._1, data(j)._1) + }).toArray + }).toArray + + val covariance = new Array2DRowRealMatrix(rows, false) + covarianceInverse = MatrixUtils.inverse(covariance.add(MatrixUtils.createRealIdentityMatrix(data.length).scalarMultiply(noiseVariance))) + + alpha = covarianceInverse.multiply(new Array2DRowRealMatrix(responses toArray)) + } +} diff --git a/FigaroExamples/src/main/scala/com/cra/figaro/example/Sources.scala b/FigaroExamples/src/main/scala/com/cra/figaro/example/Sources.scala index 01794a39..b52aaf37 100644 --- a/FigaroExamples/src/main/scala/com/cra/figaro/example/Sources.scala +++ b/FigaroExamples/src/main/scala/com/cra/figaro/example/Sources.scala @@ -77,7 +77,7 @@ object Sources { def peAlg(universe: Universe, evidence: List[NamedEvidence[_]]) = () => ProbEvidenceSampler.computeProbEvidence(100000, evidence)(universe) val alg = VariableElimination(List(ue1, ue2, ue3, ue4), peAlg _, sample1.fromSource) alg.start() - val result = alg.probability(sample1.fromSource, (s: Source) => s == source1) + val result = alg.probability(sample1.fromSource)(_ == source1) println("Probability of Source 1: " + result) alg.kill() diff --git a/FigaroLaTeX/Tutorial/FigaroTutorial.pdf b/FigaroLaTeX/Tutorial/FigaroTutorial.pdf index a6d7c64a..a86d6ccd 100644 Binary files a/FigaroLaTeX/Tutorial/FigaroTutorial.pdf and b/FigaroLaTeX/Tutorial/FigaroTutorial.pdf differ diff --git a/FigaroLaTeX/Tutorial/FigaroTutorial.tex b/FigaroLaTeX/Tutorial/FigaroTutorial.tex index ad0b1449..355b46f8 100644 --- a/FigaroLaTeX/Tutorial/FigaroTutorial.tex +++ b/FigaroLaTeX/Tutorial/FigaroTutorial.tex @@ -83,7 +83,8 @@ \include{Sections/9HierReasoning} \include{Sections/10Elements} \include{Sections/11CreateNew} -\include{Sections/12Conclusion} +\include{Sections/12Experimental} +\include{Sections/13Conclusion} %---------------------------------------------------------------------------------------- diff --git a/FigaroLaTeX/Tutorial/Sections/10Elements.tex b/FigaroLaTeX/Tutorial/Sections/10Elements.tex index bb7a6547..543891ab 100644 --- a/FigaroLaTeX/Tutorial/Sections/10Elements.tex +++ b/FigaroLaTeX/Tutorial/Sections/10Elements.tex @@ -84,7 +84,7 @@ \subsection{Inheriting from Chain} } \end{flushleft} -First, note that it inherits from \texttt{CachingChain}. When you inherit from \texttt{Chain}, you have two options. You can specify either a caching or non-caching version of the chain (which has preset cache capacities), or you can directly instantiate \texttt{Chain} with a specified cache capacity. Note that chains themselves do not extend the \texttt{Cacheable} trait, since the support of a can be infinite. Also, when you inherit from a class, you have to explicitly pass along the name and collection arguments. +First, note that it inherits from \texttt{CachingChain}. When you inherit from \texttt{Chain}, you have two options. You can specify either a caching or non-caching version of the chain. Chains themselves perform no caching, but the cacheable or non--cacheable status of a chain tells an algorithm whether it is allowed to cache chain results. Note that chains themselves do not extend the \texttt{Cacheable} trait, since the support of a can be infinite. Also, when you inherit from a class, you have to explicitly pass along the name and collection arguments. The operation of the chain can be thought of as follows: first, produce specific values for each of the options. Then, given such a specific set of values, create an atomic uniform element over those values. Finally, generate a specific value from the atomic uniform element, i.e., a uniformly chosen value from those values. @@ -180,13 +180,12 @@ \section{Making a class usable by factored algorithms} 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} +\texttt{def makeValues(depth: Int): ValueSet[Int] = ValueSet.withoutStar((for \{ i <- 0 to n \} yield i).toSet}) \end{flushleft} -The \texttt{makeValues} method returns a set of values. For a binomial, this is simply all the integers from -0 to the number of trials. This set is computed through a for comprehension whose result is turned into a set. We also make \texttt{AtomicBinomial} extend \texttt{ValuesMaker}. +The \texttt{makeValues} method returns a set of values in a set called a ValueSet. A ValueSet is a set of over a type but also includes a special value call * (star) that is used for lazy inference. In addition, makeValues function requires a depth value that is also needed for lazy inference. For a binomial, the makeValues function is simply all the integers from 0 to the number of trials. This set is computed through a for comprehension whose result is turned into a set. We also make \texttt{AtomicBinomial} extend \texttt{ValuesMaker}. -Similarly, factored algorithms like variable elimination requires both that it be possible to enumerate the values of an element and that it be possible to turn into a set of factors. To specify that it has the latter capability, you make it +Generally, factored algorithms like variable elimination requires both that it be possible to enumerate the values of an element and that it be possible to turn into a set of factors. However, factored algorithms will operate on any Figaro model by sampling values from an element if no ValuesMaker is defined. It is still better though to explicitly specify how to make factors for a new element. To do this, you make it extend \texttt{FactorMaker} and implement the \texttt{makeFactors} method. Factors are parameterized by the type of values they contain; in this case, since we are creating a factor representing probabilities, we make a \texttt{Factor[Double]}. For example, the one could extend the \texttt{AtomicBinomial} class to extend \texttt{FactorMaker} and include the following code to generate factors: diff --git a/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex b/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex index a59c4753..cd04c042 100644 --- a/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex +++ b/FigaroLaTeX/Tutorial/Sections/11CreateNew.tex @@ -55,7 +55,7 @@ \subsection{Expansion and factors} 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{Factory.make(element)} +\texttt{Factory.makeFactorsForElement(element)} \end{flushleft} The standard procedure to turn a universe into a list of factors is to: diff --git a/FigaroLaTeX/Tutorial/Sections/12Conclusion.tex b/FigaroLaTeX/Tutorial/Sections/13Conclusion.tex similarity index 100% rename from FigaroLaTeX/Tutorial/Sections/12Conclusion.tex rename to FigaroLaTeX/Tutorial/Sections/13Conclusion.tex diff --git a/doc/Figaro Quick Start Guide.pdf b/doc/Figaro Quick Start Guide.pdf index f708fbe4..0ad2718f 100644 Binary files a/doc/Figaro Quick Start Guide.pdf and b/doc/Figaro Quick Start Guide.pdf differ diff --git a/doc/Figaro Release Notes.pdf b/doc/Figaro Release Notes.pdf index 6cb5fc61..39c09810 100644 Binary files a/doc/Figaro Release Notes.pdf and b/doc/Figaro Release Notes.pdf differ diff --git a/project/Build.scala b/project/Build.scala index e85d505a..59e8eb0c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -24,7 +24,7 @@ object FigaroBuild extends Build { override val settings = super.settings ++ Seq( organization := "com.cra.figaro", description := "Figaro: a language for probablistic programming", - version := "4.0.0.0", + version := "4.1.0.0", scalaVersion := "2.11.7", crossPaths := true, publishMavenStyle := true,