1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
|
# Go to a requested generation. The given generation can be:
#
# * an absolute number like 1,000,000 (commas are optional)
# * a number relative to the current generation like +9 or -6
# * an expression like "17*(2^(3*143)+2^(2*3*27))"
# * an expression starting with + or -, indicating a relative move
# * any of the above preceded by "f" (fast). This wil set the base
# step to 2 and provide less visual feedback of progress
#
# If the target generation is less than the current generation then
# we go back to the starting generation (normally 0) and advance to
# the target.
#
# If the input is preceded by F "fast" or q "quick" then we use the
# algorithm of goto-fast.py, which sets the base to 2 and jumps by
# powers of 2.
#
# Authors:
# Original goto.py by Andrew Trevorrow and Dave Greene, April 2006.
# Updated Sept-Oct 2006 -- XRLE support and reusable default value.
# Updated April 2010 -- much faster, thanks to PM 2Ring.
# goto-expression.py by Robert Munafo, using regexp expression
# evaluation like Hypercalc (mrob.com/pub/perl/hypercalc.html)
# 20111103 First stand-alone version of "expr.py" written as an
# exercise to learn me some Python
# 20111105 Add expr_2 using re.sub (a cleaner implementation)
# 20120624 Remove whitespace before evaluating
# 20130213 Move expr_2 code to "goto-expression.py" for Golly.
# 20130214 Fix precedednce bugs, add expr_3
# 20130215 Remove redundant g.getstep and g.setstep calls. Full handling
# of scientific notation and leading signs (like "300+-3")
#
# TODO:
# Allow decimal point in EE notation, so "6.02e23" would work (right
# now you have to do "602e21")
# Make - and / associate left-to-right, so 100-10+1 gives 91 instead of 89
# Remove use of deprecated string.find and string.replace functions
# Why is it much faster when you start from gen 0? For example, start
# with puffer-train.rle, use this script to goto 10^100 then goto 2*10^100
# it's much faster if you start from gen 0 and go directly to 2*10^100
# Given that gofast saves and restores the base, should we use it always?
# Note that patterns with a lot of period-P oscillators run more
# efficiently when the base is set to P or a factor of P, so this
# is not a simple decision.
from glife import validint
from time import time
import golly as g
import re
# --------------------------------------------------------------------
# Regexp-based expression evaluator. The main loop contains match
# cases for each type of operation, and performs one operation per
# loop until there are none left to match. We use re.sub and pass
# functions for the repl parameter. See:
# http://docs.python.org/2/library/re.html#re.sub
# http://docs.python.org/2/library/re.html#re.MatchObject.group
p_whitespace = re.compile('[ \t]+')
p_paren = re.compile('\((\d+)\)')
p_parexp = re.compile('\((\d+[*/+-^][^()]*\d)\)')
p_exp = re.compile('(\d+)\^(\d+)')
p_mul = re.compile('(\d+)([*/])(n?\d+)')
p_add = re.compile('(\d+)([-+])(n?\d+)')
p_ee = re.compile('([.0-9]+)e([+-]?\d+)')
p_mantissa = re.compile('^n?\d*\.?\d+$')
p_leadn = re.compile('^n')
p_digits = re.compile('^[+-]?\d+$')
p_dot = re.compile('\.')
p_plusminus = re.compile('([-+])([-+])')
# erf = expression replace function
def erf_paren(match):
"Remove parentheses: (123) -> 123"
a = match.group(1)
return a
# ee_approx - Python-like integer approximation of a number in scientific
# notation. The mantissa and exponent are given separately and
# may be either numeric or string. (the caller is expected to
# generate them separately, e.g. by using a regexp to match
# parts of a string). The exponent must be an integer or an
# equivalent string; more flexibility is available for the
# mantissa (see examples)
# All of the following examples evaluate to True:
# ee_approx(2.0, 20) == 2*10**20
# ee_approx('2.', 20) == 2*10**20
# ee_approx('12000', -3) == 12
# ee_approx(12e+03, '-3') == 12
# The following return 0 because of an error:
# ee_approx(2.3, 4.0) # Exponent may not be a float
# ee_approx(6.02, '23.') # Exponent may not contain '.'
# The following evaluate to False because of roundoff error:
# ee_approx('.02', 22) == 2*10**20
def ee_approx(m, e):
"Integer approximation of m*10^e given m and e"
m = str(m)
# Make sure mantissa matches its pattern
if p_dot.search(m):
m = m + '0'
else:
m = m + '.0'
if not p_mantissa.search(m):
return 0
m = float(m)
m = int(m * float(2**64) * (1.0+2.0**-52.0))
e = str(e)
if not p_digits.search(e):
return 0
e = int(e)
if e<0:
e = 10**(-e)
return m/(e*2**64)
else:
e = 10**e
return (m*e)/(2**64)
def erf_ee(match):
"Scientific notation: 1.2e5 -> 120000"
a = match.group(1)
b = match.group(2)
return str(ee_approx(a,b))
def erf_exp(match):
"Exponentiation: 2^24 -> 16777216"
a = int(match.group(1))
b = int(match.group(2))
return str(a**b)
def erf_mul(match):
"Multiplication and (integer) division"
a = int(match.group(1))
# match.group(2) is operator
b = match.group(3)
# Check for leading 'n'
if p_leadn.search(b):
b = p_leadn.sub('-', b)
b = int(b)
if(match.group(2) == '*'):
return str(a*b)
else:
return str(a//b)
def erf_add(match):
"Addition and subtraction"
a = int(match.group(1))
# match.group(2) is operator
b = match.group(3)
# Check for leading 'n'
if p_leadn.search(b):
b = p_leadn.sub('-', b)
b = int(b)
if(match.group(2) == '+'):
return str(a+b)
else:
return str(a-b)
""" Evaluate an expression without parentheses, like 2+3*4^5 = 3074
The precedence is:
If we match something like "6.02e23", expand it
Else if we match something like "4^5", expand it
Else if we match something like "3*17", expand it
Else if we match something like "2+456", expand it
Else return
It loops in all cases but the last
"""
def expr_2(p):
going = 1
# print 'e2:', p
while going:
if p_ee.search(p):
p = p_ee.sub(erf_ee, p, count=1)
# print 'e2 ee, got:', p
elif p_exp.search(p):
p = p_exp.sub(erf_exp, p, count=1)
# print 'e2 exp, got:', p
elif p_mul.search(p):
p = p_mul.sub(erf_mul, p, count=1)
# print 'e2 mul, got:', p
elif p_add.search(p):
p = p_add.sub(erf_add, p, count=1)
# print 'e2 add, got:', p
else:
# print 'e2 done'
going = 0
# print 'e2 return:', p
return p
def erf_e2(match):
"Parenthesized bare expression"
a = expr_2(match.group(1))
return str(a)
def erf_plusminus(match):
"--, -+, +-, or ++"
if match.group(2) == '-':
return match.group(1)+'n'
return match.group(1)
"""
Evaluate an expression possibly including parenthesized sub-expressions
and numbers in scientific notation,
like 17*4^((7^2-3)*6^2+2e6)+602e21
The precedence is:
If we match something like "6.02e23", expand it
Else if we match something like "(3+4*5)", expand it using expr_2
Else if we match something like "(23456)", remove the parens
Else expand the whole string using expr_2 and return
It loops in all cases but the last
"""
def expr_3(p):
p = p_whitespace.sub('', p)
if p_plusminus.search(p):
p = p_plusminus.sub(erf_plusminus, p)
going = 1
while going:
if p_ee.search(p):
p = p_ee.sub(erf_ee, p, count=1)
elif p_parexp.search(p):
p = p_parexp.sub(erf_e2, p, count=1)
elif p_paren.search(p):
p = p_paren.sub(erf_paren, p, count=1)
else:
p = expr_2(p)
going = 0
return p
# --------------------------------------------------------------------
"""
gt_setup computes how many generations to move forwards. If we need to
move backwards, it rewinds as far as possible, then returns the number
of generations we need to move forwards.
"""
def gt_setup(gen):
currgen = int(g.getgen())
# Remove leading '+' or '-' if any, and convert rest to int or long
if gen[0] == '+':
n = int(gen[1:])
newgen = currgen + n
elif gen[0] == '-':
n = int(gen[1:])
if currgen > n:
newgen = currgen - n
else:
newgen = 0
else:
newgen = int(gen)
if newgen < currgen:
# try to go back to starting gen (not necessarily 0) and
# then forwards to newgen; note that reset() also restores
# algorithm and/or rule, so too bad if user changed those
# after the starting info was saved;
# first save current location and scale
midx, midy = g.getpos()
mag = g.getmag()
g.reset()
# restore location and scale
g.setpos(midx, midy)
g.setmag(mag)
# current gen might be > 0 if user loaded a pattern file
# that set the gen count
currgen = int(g.getgen())
if newgen < currgen:
g.error("Can't go back any further; pattern was saved " +
"at generation " + str(currgen) + ".")
return 0
return newgen - currgen
elif newgen > currgen:
return newgen - currgen
else:
return 0
# --------------------------------------------------------------------
def intbase(n, b):
# convert integer n >= 0 to a base b digit list (thanks to PM 2Ring)
digits = []
while n > 0:
digits += [n % b]
n //= b
return digits or [0]
def goto(newgen, delay):
g.show("goto running, hit escape to abort...")
oldsecs = time()
# before stepping we advance by 1 generation, for two reasons:
# 1. if we're at the starting gen then the *current* step size
# will be saved (and restored upon Reset/Undo) rather than a
# possibly very large step size
# 2. it increases the chances the user will see updates and so
# get some idea of how long the script will take to finish
# (otherwise if the base is 10 and a gen like 1,000,000,000
# is given then only a single step() of 10^9 would be done)
if delay <= 1.0:
g.run(1)
newgen -= 1
# use fast stepping (thanks to PM 2Ring)
for i, d in enumerate(intbase(newgen, g.getbase())):
if d > 0:
g.setstep(i)
for j in xrange(d):
if g.empty():
g.show("Pattern is empty.")
return
g.step()
newsecs = time()
if newsecs - oldsecs >= delay: # time to do an update?
oldsecs = newsecs
g.update()
g.show("")
# --------------------------------------------------------------------
# This is the "fast goto" algorithm from goto-fast.py that uses
# binary stepsizes and does not do that initial step by 1 generation.
def intbits(n):
''' Convert integer n to a bit list '''
bits = []
while n:
bits += [n & 1]
n >>= 1
return bits
def gofast(newgen, delay):
''' Fast goto '''
#Save current settings
oldbase = g.getbase()
# oldhash = g.setoption("hashing", True)
g.show('gofast running, hit escape to abort')
oldsecs = time()
#Advance by binary powers, to maximize advantage of hashing
g.setbase(2)
for i, b in enumerate(intbits(newgen)):
if b:
g.setstep(i)
g.step()
g.dokey(g.getkey())
newsecs = time()
if newsecs - oldsecs >= delay: # do an update every sec
oldsecs = newsecs
g.update()
if g.empty():
break
g.show('')
#Restore settings
# g.setoption("hashing", oldhash)
g.setbase(oldbase)
# --------------------------------------------------------------------
def savegen(filename, gen):
try:
f = open(filename, 'w')
f.write(gen)
f.close()
except:
g.warn("Unable to save given gen in file:\n" + filename)
# --------------------------------------------------------------------
# use same file name as in goto.lua
GotoINIFileName = g.getdir("data") + "goto.ini"
previousgen = ""
try:
f = open(GotoINIFileName, 'r')
previousgen = f.readline()
f.close()
if not validint(previousgen):
previousgen = ""
except:
# should only happen 1st time (GotoINIFileName doesn't exist)
pass
gen = g.getstring("Enter the desired generation number as an\n" +
"expression, prepend -/+ for relative\n" +
"move back/forwards, prepend f to use faster\n"
"powers-of-2 steps:",
previousgen, "Go to generation")
# Evaluate the expression. This leaves leading "f" and/or "+/-"
# intact.
gen = expr_3(gen)
# Find out if they want to get there quickly
# %%% TODO: Use re instead of string.find and string.replace (which are
# deprecated in later versions of Python)
fastmode = 0
if(gen.find("f") == 0):
gen = gen.replace("f","")
fastmode = 1
if len(gen) == 0:
g.exit()
elif gen == "+" or gen == "-":
# clear the default
savegen(GotoINIFileName, "")
elif not validint(gen):
g.exit('Sorry, but "' + gen + '" is not a valid integer.')
else:
# best to save given gen now in case user aborts script
savegen(GotoINIFileName, gen)
oldstep = g.getstep()
# %%% TODO: Use re instead of string.replace to remove the commas
newgen = gt_setup(gen.replace(",",""))
if newgen > 0:
if fastmode:
gofast(newgen, 10.0)
else:
goto(newgen, 1.0)
g.setstep(oldstep)
|