Skip to content

Commit 9614965

Browse files
committed
Parallel search
1 parent a58e409 commit 9614965

File tree

6 files changed

+105
-62
lines changed

6 files changed

+105
-62
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ case class Configuration(
8484
recurrenceRelations: Boolean,
8585
combinatorialFunctions: Boolean,
8686
transcendentalFunctions: Boolean,
87+
parallelSearch: Boolean,
8788
numericalTest: Boolean,
8889
printProgress: Boolean,
8990
outputLaTeX: Boolean

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name := "Sequencer"
22

3-
version := "1.2.1"
3+
version := "1.3.0"
44

55
organization := "com.worldwidemann"
66

src/main/scala/com/worldwidemann/sequencer/FormulaGenerator.scala

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ class FormulaGenerator(configuration: Configuration) {
9090
}))
9191
else List()))
9292

93+
// Ensures that TreeGenerator builds all trees required to accommodate the expressions above
94+
def getMaxChildren = expressions.size - 1
95+
9396
private def rollNodeExpression(node: Node) = {
9497
val allowedExpressions = expressions(node.children.size)
9598
node.expressionIndex = if (node.expressionIndex + 1 >= allowedExpressions.size) 0 else node.expressionIndex + 1
@@ -98,33 +101,28 @@ class FormulaGenerator(configuration: Configuration) {
98101
node.expressionIndex == 0
99102
}
100103

104+
// Generates all formulas based on the specified expression tree skeleton.
101105
// The callback pattern is much more efficient than returning a collection
102106
// because the number of formulas grows extremely fast with the number of nodes
103-
def getFormulas(nodes: Int, formulaCallback: Node => Unit, progressCallback: Double => Unit) = {
104-
val trees = new TreeGenerator(expressions.size - 1).getTrees(nodes)
105-
106-
for ((tree, i) <- trees.view.zipWithIndex) {
107-
val nodes = tree.getTreeNodes
108-
// Set initial expressions
109-
nodes.foreach(rollNodeExpression)
107+
def getFormulas(tree: Node, formulaCallback: Node => Unit) {
108+
val nodes = tree.getTreeNodes
109+
// Set initial expressions
110+
nodes.foreach(rollNodeExpression)
110111

111-
var lastRolled = false
112-
while (!lastRolled) {
113-
// Pass the formula in its current form.
114-
// The object is mutable, so any processing has to happen in the callback
115-
formulaCallback(tree)
112+
var lastRolled = false
113+
while (!lastRolled) {
114+
// Pass the formula in its current form.
115+
// The object is mutable, so any processing has to happen in the callback
116+
formulaCallback(tree)
116117

117-
var j = 0
118-
var rolled = true
119-
while (j < nodes.size && rolled) {
120-
rolled = rollNodeExpression(nodes(j))
121-
j += 1
122-
if (j == nodes.size && rolled)
123-
lastRolled = true
124-
}
118+
var i = 0
119+
var rolled = true
120+
while (i < nodes.size && rolled) {
121+
rolled = rollNodeExpression(nodes(i))
122+
i += 1
123+
if (i == nodes.size && rolled)
124+
lastRolled = true
125125
}
126-
127-
progressCallback((i + 1).toDouble / trees.size)
128126
}
129127
}
130128
}

src/main/scala/com/worldwidemann/sequencer/Sequencer.scala

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,95 @@
1010

1111
package com.worldwidemann.sequencer
1212

13-
import scala.collection.mutable.ListBuffer
13+
import java.util.concurrent.atomic.AtomicInteger
14+
import scala.collection.mutable.SynchronizedQueue
15+
import scala.collection.generic.Growable
1416

1517
case class Configuration(maximumComplexity: Int, maximumIdentifications: Int, predictionLength: Int,
1618
recurrenceRelations: Boolean, combinatorialFunctions: Boolean, transcendentalFunctions: Boolean,
17-
numericalTest: Boolean, printProgress: Boolean, outputLaTeX: Boolean)
19+
parallelSearch: Boolean, numericalTest: Boolean, printProgress: Boolean, outputLaTeX: Boolean)
1820

1921
case class SequenceIdentification(formula: String, continuation: Seq[String])
2022

2123
class Sequencer(configuration: Configuration) {
24+
// Note that neither of the two classes keeps a state,
25+
// so sharing these objects is thread safe
26+
private val formulaGenerator = new FormulaGenerator(configuration)
27+
private val treeGenerator = new TreeGenerator(formulaGenerator.getMaxChildren)
28+
2229
def identifySequence(sequence: Seq[String]): Seq[SequenceIdentification] = {
2330
val sequenceSimplified = sequence.map(Simplifier.simplify)
2431
val sequenceNumerical = sequenceSimplified.map(Utilities.getNumericalValue)
2532

26-
val identifications = new ListBuffer[SequenceIdentification]
33+
val identifications = new SynchronizedQueue[SequenceIdentification]
2734

2835
for (nodes <- 1 to configuration.maximumComplexity) {
29-
new FormulaGenerator(configuration).getFormulas(nodes, formula => {
30-
// Consider recurrence relations only if they predict at least one element
31-
// of the sequence without referencing a seed value
32-
if (sequence.size > 2 * (Utilities.getStartIndex(formula) - 1)) {
33-
if (!configuration.numericalTest || Verifier.testFormula(formula, sequenceNumerical)) {
34-
// Sequence matched numerically (or test skipped) => verify symbolically
35-
if (Verifier.verifyFormula(formula, sequenceSimplified)) {
36-
try {
37-
val continuation = Predictor.predict(formula, sequenceSimplified, configuration.predictionLength)
38-
.map(element => if (configuration.outputLaTeX) Utilities.getLaTeX(element) else element)
39-
identifications += SequenceIdentification(getFullFormula(formula, sequenceSimplified), continuation)
40-
if (configuration.maximumIdentifications > 0 && identifications.distinct.size >= configuration.maximumIdentifications)
41-
return identifications.distinct.sortBy(_.formula.length)
42-
} catch {
43-
// Occasionally, simplification or prediction throw an exception although
44-
// symbolic verification did not. This indicates a bug in Symja
45-
// and is simply ignored
46-
case e: Exception => {}
47-
}
48-
}
49-
}
50-
}
51-
}, progress => {
52-
if (configuration.printProgress)
53-
print("\rTrying formulas with complexity " + nodes + "... " + "%3d".format((progress * 100).round) + " %")
36+
val trees = treeGenerator.getTrees(nodes)
37+
val progress = new AtomicInteger(0)
38+
39+
// TreeGenerator generates tree objects that are completely independent of each other,
40+
// so searching for formulas based on them can be parallelized
41+
(if (configuration.parallelSearch) trees.par else trees).foreach(tree => {
42+
identifySequenceTask(tree, sequenceSimplified, sequenceNumerical, identifications)
43+
44+
if (configuration.printProgress && !maximumReached(identifications))
45+
// TODO: Concurrency issues with interleaving print statements?
46+
print("\rTrying formulas with complexity " + nodes + "... " +
47+
"%3d".format(((progress.incrementAndGet.toDouble / trees.size) * 100).round) + " %")
5448
})
5549

50+
if (maximumReached(identifications))
51+
return processIdentifications(identifications)
52+
5653
if (configuration.printProgress)
5754
println
5855
}
5956

60-
identifications.distinct.sortBy(_.formula.length)
57+
processIdentifications(identifications)
58+
}
59+
60+
// Parallel execution unit.
61+
// Finds formulas generating the sequence based on the specified expression tree skeleton
62+
private def identifySequenceTask(tree: Node, sequence: Seq[String], sequenceNumerical: Seq[Double],
63+
identifications: Seq[SequenceIdentification] with Growable[SequenceIdentification]) {
64+
formulaGenerator.getFormulas(tree, formula => {
65+
// Consider recurrence relations only if they predict at least one element
66+
// of the sequence without referencing a seed value
67+
if (sequence.size > 2 * (Utilities.getStartIndex(formula) - 1)) {
68+
if (!configuration.numericalTest || Verifier.testFormula(formula, sequenceNumerical)) {
69+
// Sequence matched numerically (or test skipped) => verify symbolically
70+
if (Verifier.verifyFormula(formula, sequence)) {
71+
try {
72+
val continuation = Predictor.predict(formula, sequence, configuration.predictionLength)
73+
.map(element => if (configuration.outputLaTeX) Utilities.getLaTeX(element) else element)
74+
identifications += SequenceIdentification(getFullFormula(formula, sequence), continuation)
75+
} catch {
76+
// Occasionally, simplification or prediction throw an exception although
77+
// symbolic verification did not. This indicates a bug in Symja
78+
// and is simply ignored
79+
case e: Exception => {}
80+
}
81+
}
82+
}
83+
}
84+
85+
if (maximumReached(identifications))
86+
return
87+
})
88+
}
89+
90+
private def maximumReached(identifications: Seq[SequenceIdentification]) =
91+
configuration.maximumIdentifications > 0 && identifications.distinct.size >= configuration.maximumIdentifications
92+
93+
private def processIdentifications(identifications: Seq[SequenceIdentification]) = {
94+
// Sort alphabetically before sorting by length to get deterministic ordering independent of search order
95+
val sortedIdentifications = identifications.distinct.sortBy(_.formula).sortBy(_.formula.length)
96+
if (configuration.maximumIdentifications > 0)
97+
// Necessary because the tasks might find additional formulas
98+
// before they determine that the maximum has been reached and return
99+
sortedIdentifications.take(configuration.maximumIdentifications)
100+
else
101+
sortedIdentifications
61102
}
62103

63104
// Returns the full descriptive string form of the formula,

src/main/scala/com/worldwidemann/sequencer/SequencerRunner.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import scala.collection.mutable.ListBuffer
1818
import scopt.OptionParser
1919

2020
object SequencerRunner {
21-
def main(args: Array[String]): Unit = {
22-
println("Sequencer 1.2.1 (https://github.com/p-e-w/sequencer)\n")
21+
def main(args: Array[String]) {
22+
println("Sequencer 1.3.0 (https://github.com/p-e-w/sequencer)\n")
2323

2424
// Suppress annoying Symja console output (idea from http://stackoverflow.com/a/8363580).
2525
// This is a very brittle solution. In particular, if we do not use the Console stream
@@ -36,7 +36,10 @@ object SequencerRunner {
3636
} text ("search depth (maximum number of nodes in expression tree) [default: 6]")
3737
opt[Int]('r', "results") action { (x, c) =>
3838
c.copy(maximumIdentifications = x)
39-
} text ("maximum number of formulas to return, 0 for unbounded [default: 5]")
39+
} text ("maximum number of formulas to return, 0 for unlimited [default: 5]. " +
40+
"Note that for parallel searches, the order in which formulas are found is not deterministic " +
41+
"so limiting the number of search results can lead to unreproducible searches " +
42+
"unless the --sequential option is also used.")
4043
opt[Int]('p', "predict") action { (x, c) =>
4144
c.copy(predictionLength = x)
4245
} text ("number of elements to predict in sequence continuation [default: 5]")
@@ -49,6 +52,9 @@ object SequencerRunner {
4952
opt[Unit]('t', "no-transcendentals") action { (x, c) =>
5053
c.copy(transcendentalFunctions = false)
5154
} text ("do not search for transcendental functions (speeds up search)")
55+
opt[Unit]('q', "sequential") action { (x, c) =>
56+
c.copy(parallelSearch = false)
57+
} text ("disable search parallelization (single-threaded search)")
5258
opt[Unit]('s', "symbolic") action { (x, c) =>
5359
c.copy(numericalTest = false)
5460
} text ("skip numerical test (symbolic verification only; slows down search)")
@@ -63,7 +69,7 @@ object SequencerRunner {
6369
} text ("list of numbers to search for (symbolic expressions allowed)")
6470
}
6571

66-
parser.parse(args, Configuration(6, 5, 5, true, true, true, true, true, false)) match {
72+
parser.parse(args, Configuration(6, 5, 5, true, true, true, true, true, true, false)) match {
6773
case Some(configuration) => {
6874
println("Searching for formulas for sequence " +
6975
(if (configuration.outputLaTeX)

src/main/scala/com/worldwidemann/sequencer/Utilities.scala

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ import org.matheclipse.core.eval.EvalUtilities
1717
import org.matheclipse.core.eval.TeXUtilities
1818

1919
object Utilities {
20-
private val evaluator = new EvalUtilities(false, true)
21-
22-
def evaluateSymja(expression: String) = evaluator.evaluate(expression).toString
20+
// Symja isn't thread safe, so the EvalUtilities object can not be shared between calls
21+
def evaluateSymja(expression: String) = new EvalUtilities(false, true).evaluate(expression).toString
2322

2423
def isNumerical(value: Double) = !value.isNaN && !value.isInfinite
2524

@@ -31,11 +30,9 @@ object Utilities {
3130
case e: Exception => false
3231
}
3332

34-
private val texUtilities = new TeXUtilities(new EvalEngine(true), true)
35-
3633
def getLaTeX(expression: String) = {
3734
val writer = new StringWriter
38-
texUtilities.toTeX(expression, writer)
35+
new TeXUtilities(new EvalEngine(true), true).toTeX(expression, writer)
3936
writer.toString
4037
}
4138

0 commit comments

Comments
 (0)