Synthesis component search script

For scripts to aid with computation or simulation in cellular automata.
Post Reply
User avatar
praosylen
Posts: 2443
Joined: September 13th, 2014, 5:36 pm
Location: Pembina University, Home of the Gliders
Contact:

Synthesis component search script

Post by praosylen » July 24th, 2019, 10:53 pm

Here's a lifelib script I've been working on to find synthesis components by randomly colliding gliders with a given still life:

Code: Select all

#collisrc.py version 0.3
import sys
sys.path.append("/.../path/to/lifelib/")
from random import randint, choice
from time import clock, sleep
import lifelib
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                    default="B3/S23")
parser.add_argument("-s", "--sl",
                    help="still life to serve as the basis for generated components",
                    default="2o$2o!")
parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
parser.add_argument("-b", "--backtrack", help="distance to backtrack gliders (in cells)",
                    default=10, type=int)
parser.add_argument("-d", "--directions",
                    help="directions of arriving gliders (as string containing any or all of characters '0123' representing standard quadrants (with 0 being used in place of 4, as if it wasn't confusing enough))",
                    default="0123")
parser.add_argument("-u", "--area-width",
                    help="half of x width of area of glider generation", default=5,
                    type=int)
parser.add_argument("-v", "--area-height",
                    help="half of y height of area of glider generation", default=5,
                    type=int)
parser.add_argument("-f", "--first-interact-before",
                    help="maximum time of first interaction", default=50, type=int)
parser.add_argument("-i", "--max-interact-time",
                    help="maximum time between beginning and end of interaction",
                    default=30, type=int)
parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                    default=40, type=int)
parser.add_argument("-z", "--min-stator-size",
                    help="minimum number of inactive cells in still life", default=0,
                    type=int)
parser.add_argument("-t", "--time", help="for profiling purposes", action="store_true")
parser.add_argument("-c", "--cpu-save", help="slow cpu usage to 50%", action="store_true")
args = parser.parse_args()
lt = lifelib.load_rules(args.rule).lifetree()
gliders = map(lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack).__getitem__, xrange(4))
ngliders = args.n_gliders
dirs = sorted(map(int, list(set(args.directions))))
sl = lt.pattern(args.sl).centre()(args.x, args.y)
gencol_t = 0
def gencol():
    return reduce(lambda x, y: x+y, map(lambda _: gliders[randint(0, 3)](("rot"+str(choice(dirs)*90)).replace("rot0", "identity"), randint(-args.area_width, args.area_width), randint(-args.area_height, args.area_height)), [0]*ngliders)) + sl
testsane_t = 0
def testsane(col):
    return len(set(map(lambda x:col[x].population, xrange(5)) + [ngliders * 5 + sl.population])) == 1
results = []
rcols = []
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]
testres_t = 0
def testres(res, update=True):
    global results
    for q in results:
        if not update and q == res: continue
        for i in map(q, orientations):
            #Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res-z
            if y[1] == y: res = y
            if not res.population: return False
    return res
def updateres(res):
    global results
    newresults = [res]
    for i in xrange(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults
    results = sorted(newresults, key=lambda x: x.population)
    print map(lambda x: x.population, results)
testcol_t = 0
def testcol():
    global gencol_t, testsane_t, testres_t
    t = clock()
    col = gencol()
    gencol_t += clock() - t
    col0 = col.__copy__()
    t = clock()
    if not testsane(col):
        testsane_t += clock() - t
        return None
    testsane_t += clock() - t
    gens = 5
    for i in xrange(args.first_interact_before/5):
        col = col[5]
        if not col or col == col[1]: return None
        if col.population > args.max_int_pop: return None
        if sl - col:
            gens += i*5
            break
    else:
        return None
    gens0 = gens
    for i in xrange(args.max_interact_time/5):
        if not sl & col: return None
        if col.population > args.max_int_pop: return None
        if col == col[1]:
            gens += i*5
            break
        col = col[5]
    else:
        return None
    t = clock()
    q = testres(col)
    if q and (not args.min_stator_size or reduce(lambda x, y: x & y, map(col0.__getitem__, xrange(gens0-5, gens))).population >= args.min_stator_size):
        updateres(q)
        testres_t += clock() - t
        return q.apgcode, col0.rle_string()
    testres_t += clock() - t
    return None
def search():
    global testcol_t
    cct = 0
    while 1:
        t = clock()
        q = testcol()
        testcol_t += clock() - t
        if q is not None:
            print "Object %s produced by the following collision: \n%s" % q
            rcols.append(q[1])
        cct += 1
        if not cct%500:
            print cct, "collisions searched,", len(rcols), "components found."
            if args.time:
                print "Time per collision spent in testres():", testres_t / cct * 1000
                print "Time per collision spent elsewhere in testcol():", (testcol_t - testres_t - gencol_t - testsane_t) / cct * 1000
                print "Time per collision spent in gencol():", gencol_t / cct * 1000
                print "Time per collision spent in testsane():", testsane_t / cct * 1000
        if args.cpu_save and not cct%5000:
            sleep(testcol_t / cct * 5000)
try:
    search()
except KeyboardInterrupt:
    for i in rcols:
        print i
Basic instructions: save as "collisrc.py" (or really whatever you want) and replace "/.../path/to/lifelib/" in the third line with the actual path to lifelib on your system. Run using the command "python collisrc.py -n [number of gliders] -s [still life target in rle format, enclosed with single quotes]". Ideally you could run "python collisrc.py -h" to see the script's other options, but argparse is giving me an error for no clear reason, so just look in the code instead. The most important other options are -r, which sets the rule (must support gliders); -d, which sets which directions gliders can come from; and -x, -y, -u, and -v, which set the x and y displacement of the still life from where the gliders are aimed and the width and height of the regions in which gliders can be generated, respectively. -z, which is intended to specify a minimum number of stator cells in the interaction, would be a very useful option, but it's fairly buggy and so doesn't always work properly.

I'm sure there are a lot of improvements that could be made — for one, the script is a lot slower than I would like, and I suspect there are major enhancements that I'm missing due to inexperience with lifelib, and for another object separation doesn't work particularly well in certain cases. I apologize in advance that I won't have much time to answer questions/work on this more due to a busy personal schedule, but anyone who wants to improve/make modifications is welcome to do so.
former username: A for Awesome
praosylen#5847 (Discord)

The only decision I made was made
of flowers, to jump universes to one of springtime in
a land of former winter, where no invisible walls stood,
or could stand for more than a few hours at most...

wildmyron
Posts: 1544
Joined: August 9th, 2013, 12:45 am
Location: Western Australia

Re: Synthesis component search script

Post by wildmyron » July 26th, 2019, 7:04 am

Fantastic to see some progress on this search idea. Thank you for publishing the script.

From the results you've been posting so far I think that there's plenty of scope for interesting new results to be found with it. I haven't yet run it in earnest on a search problem - just tried to find some known 3G converters acting on the eater head.

I've made a few modifications which I was going to outline in detail, but I think it's just easier to post the revised version. I haven't really looked at testcol() yet and I'm finding the updateres() function rather confusing - I'm not entirely sure what that's meant to do.
  • Fix help output (unescaped '%' in help string) and add default values to help printout
  • Python 2/3 compatibility
  • Pre-calculate rotated glider patterns
  • Remove map / reduce from gencols() - combined with above this gives a reasonable performance improvement (overall effect dependent on n-gliders)
  • Modify testsane() for a decent performance improvement (40%)
  • Bump up the update interval and add a search speed indicator
  • I guess that qualifies as a version bump
For a 4G collision run with an eater as SL I saw an improvement from 1300 collisions/second to 1650 collisions/second. Not too big, but a start nonetheless. (That was with Python 2. Surprisingly, I see a significant performance reduction with Python 3)

Code: Select all

#collisrc.py version 0.4
from __future__ import print_function
from functools import reduce
import sys
# sys.path.append("/.../path/to/lifelib/")
from random import randint, choice
from time import clock, sleep
import lifelib
import argparse
if sys.version_info[0] > 2:
    xrange = range
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                    default="B3/S23")
parser.add_argument("-s", "--sl",
                    help="still life to serve as the basis for generated components",
                    default="2o$2o!")
parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
parser.add_argument("-b", "--backtrack", help="distance to backtrack gliders (in cells)",
                    default=10, type=int)
parser.add_argument("-d", "--directions",
                    help="directions of arriving gliders (as string containing any or all of characters '0123' representing standard quadrants (with 0 being used in place of 4, as if it wasn't confusing enough))",
                    default="0123")
parser.add_argument("-u", "--area-width",
                    help="half of x width of area of glider generation", default=5,
                    type=int)
parser.add_argument("-v", "--area-height",
                    help="half of y height of area of glider generation", default=5,
                    type=int)
parser.add_argument("-f", "--first-interact-before",
                    help="maximum time of first interaction", default=50, type=int)
parser.add_argument("-i", "--max-interact-time",
                    help="maximum time between beginning and end of interaction",
                    default=30, type=int)
parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                    default=40, type=int)
parser.add_argument("-z", "--min-stator-size",
                    help="minimum number of inactive cells in still life", default=0,
                    type=int)
parser.add_argument("-t", "--time", help="for profiling purposes", action="store_true")
parser.add_argument("-c", "--cpu-save", help="slow cpu usage to 50%%", action="store_true")
args = parser.parse_args()
lt = lifelib.load_rules(args.rule).lifetree()
glider = lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack)
# gliders = [glider[t] for t in range(4)]
ngliders = args.n_gliders
dirs = sorted(map(int, list(set(args.directions))))
rotations = [("rot"+str(d*90)).replace("rot0", "identity") for d in dirs]
rotated_gliders = rotated_gliders = [glider(rot)[t] for rot in rotations for t in range(4)]
sl = lt.pattern(args.sl).centre()(args.x, args.y)
gencol_t = 0
def gencol():
    glider_col = sl(0, 0)
    for _ in range(ngliders):
        x = randint(-args.area_width, args.area_width)
        y = randint(-args.area_height, args.area_height)
        glider_col += choice(rotated_gliders)(x, y)
    return glider_col
testsane_t = 0
def testsane(col):
    expected_pop = ngliders * 5 + sl.population
    return not any((col[x].population != expected_pop for x in xrange(5)))
results = []
rcols = []
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]
testres_t = 0
def testres(res, update=True):
    global results
    for q in results:
        if not update and q == res: continue
        for i in map(q, orientations):
            #Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res-z
            if y[1] == y: res = y
            if not res.population: return False
    return res
def updateres(res):
    global results
    newresults = [res]
    for i in xrange(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults
    results = sorted(newresults, key=lambda x: x.population)
    print([r.population for r in results])
testcol_t = 0
def testcol():
    global gencol_t, testsane_t, testres_t
    t = clock()
    col = gencol()
    gencol_t += clock() - t
    col0 = col.__copy__()
    t = clock()
    if not testsane(col):
        testsane_t += clock() - t
        return None
    testsane_t += clock() - t
    gens = 5
    for i in xrange(args.first_interact_before//5):
        col = col[5]
        if not col or col == col[1]: return None
        if col.population > args.max_int_pop: return None
        if sl - col:
            gens += i*5
            break
    else:
        return None
    gens0 = gens
    for i in xrange(args.max_interact_time//5):
        if not sl & col: return None
        if col.population > args.max_int_pop: return None
        if col == col[1]:
            gens += i*5
            break
        col = col[5]
    else:
        return None
    t = clock()
    q = testres(col)
    if q and (not args.min_stator_size or reduce(lambda x, y: x & y, map(col0.__getitem__, xrange(gens0-5, gens))).population >= args.min_stator_size):
        updateres(q)
        testres_t += clock() - t
        return q.apgcode, col0.rle_string()
    testres_t += clock() - t
    return None
def search():
    global testcol_t
    update_int = 10000
    update_t = clock()
    cct = 0
    while 1:
        t = clock()
        q = testcol()
        testcol_t += clock() - t
        if q is not None:
            print("Object %s produced by the following collision: \n%s" % q)
            rcols.append(q[1])
        cct += 1
        if not cct % update_int:
            t = clock()
            print("%d collisions searched, %d collisions/second, %d components found." % (cct, update_int//(t-update_t), len(rcols)))
            update_t = t
            if args.time:
                print("Time per collision spent in testres(): %.4f" % (testres_t / cct * 1000))
                print("Time per collision spent elsewhere in testcol(): %.4f" % ((testcol_t - testres_t - gencol_t - testsane_t) / cct * 1000))
                print("Time per collision spent in gencol(): %.4f" % (gencol_t / cct * 1000))
                print("Time per collision spent in testsane(): %.4f" % (testsane_t / cct * 1000))
            sys.stdout.flush()
        if args.cpu_save and not cct%5000:
            sleep(testcol_t / cct * 5000)
try:
    search()
except KeyboardInterrupt:
    for i in rcols:
        print(i)
The 5S project (Smallest Spaceships Supporting Specific Speeds) is now maintained by AforAmpere. The latest collection is hosted on GitHub and contains well over 1,000,000 spaceships.

Semi-active here - recovering from a severe case of LWTDS.

User avatar
praosylen
Posts: 2443
Joined: September 13th, 2014, 5:36 pm
Location: Pembina University, Home of the Gliders
Contact:

Re: Synthesis component search script

Post by praosylen » July 26th, 2019, 9:44 am

wildmyron wrote:I've made a few modifications which I was going to outline in detail, but I think it's just easier to post the revised version. I haven't really looked at testcol() yet and I'm finding the updateres() function rather confusing - I'm not entirely sure what that's meant to do.
  • Fix help output (unescaped '%' in help string) and add default values to help printout
  • Python 2/3 compatibility
  • Pre-calculate rotated glider patterns
  • Remove map / reduce from gencols() - combined with above this gives a reasonable performance improvement (overall effect dependent on n-gliders)
  • Modify testsane() for a decent performance improvement (40%)
  • Bump up the update interval and add a search speed indicator
  • I guess that qualifies as a version bump
For a 4G collision run with an eater as SL I saw an improvement from 1300 collisions/second to 1650 collisions/second. Not too big, but a start nonetheless. (That was with Python 2. Surprisingly, I see a significant performance reduction with Python 3)
Thanks! That's pretty helpful! To explain the updateres() function, its purpose is to optimize the results array by running testres() again on every result in order to eliminate items such as constellations of SLs that were erroneously added before the individual SLs themselves had been found, resulting in a huge optimization in speed. It also sorts the results array by increasing population, which brings somewhat of a speed increase as well. (It also occurred to me that the optional "update" parameter in testres() is not being used, since it was included in an earlier version in which testres() recursively called itself to do what updateres() now does.)

I also found that adding these two lines:

Code: Select all

    q = col[args.first_interact_before + args.max_interact_time]
    if q.population > args.max_int_pop or q != q[1]: return None
after

Code: Select all

    testsane_t += clock() - t
leads to a significant speed increase, due to lifelib's incredibly fast iteration but relatively slow editing/pattern analysis. It's possible more pre-checks like this could improve things further.
former username: A for Awesome
praosylen#5847 (Discord)

The only decision I made was made
of flowers, to jump universes to one of springtime in
a land of former winter, where no invisible walls stood,
or could stand for more than a few hours at most...

wildmyron
Posts: 1544
Joined: August 9th, 2013, 12:45 am
Location: Western Australia

Re: Synthesis component search script

Post by wildmyron » July 26th, 2019, 10:36 am

Thanks for the explanation about updateres(). I was rather confused by why the whole results array is scanned for every result in the array.
A for awesome wrote:I also found that adding these two lines:

Code: Select all

    q = col[args.first_interact_before + args.max_interact_time]
    if q.population > args.max_int_pop or q != q[1]: return None
after

Code: Select all

    testsane_t += clock() - t
leads to a significant speed increase, due to lifelib's incredibly fast iteration but relatively slow editing/pattern analysis. It's possible more pre-checks like this could improve things further.
Yes that's a decent improvement. I almost mentioned in my previous post that I believe to get the most out of lifelib would require switching to C++. The main advantages are that the gencol() could use the bitworld type instead of pattern types (which is light-weight for editing 2-state patterns) and that the collision could be evolved with the upattern type instead of pattern2 (which is what apgsearch uses because it is unhashed and better suited to chaotic patterns).
The 5S project (Smallest Spaceships Supporting Specific Speeds) is now maintained by AforAmpere. The latest collection is hosted on GitHub and contains well over 1,000,000 spaceships.

Semi-active here - recovering from a severe case of LWTDS.

User avatar
praosylen
Posts: 2443
Joined: September 13th, 2014, 5:36 pm
Location: Pembina University, Home of the Gliders
Contact:

Re: Synthesis component search script

Post by praosylen » July 26th, 2019, 3:55 pm

Fixed the -z option, I think.

Code: Select all

#collisrc.py version 0.41
from __future__ import print_function
from functools import reduce
import sys
# sys.path.append("/.../path/to/lifelib/")
from random import randint, choice
from time import clock, sleep
import lifelib
import argparse
if sys.version_info[0] > 2:
    xrange = range
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                    default="B3/S23")
parser.add_argument("-s", "--sl",
                    help="still life to serve as the basis for generated components",
                    default="2o$2o!")
parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
parser.add_argument("-b", "--backtrack", help="distance to backtrack gliders (in cells)",
                    default=10, type=int)
parser.add_argument("-d", "--directions",
                    help="directions of arriving gliders (as string containing any or all of characters '0123' representing standard quadrants (with 0 being used in place of 4, as if it wasn't confusing enough))",
                    default="0123")
parser.add_argument("-u", "--area-width",
                    help="half of x width of area of glider generation", default=5,
                    type=int)
parser.add_argument("-v", "--area-height",
                    help="half of y height of area of glider generation", default=5,
                    type=int)
parser.add_argument("-f", "--first-interact-before",
                    help="maximum time of first interaction", default=50, type=int)
parser.add_argument("-i", "--max-interact-time",
                    help="maximum time between beginning and end of interaction",
                    default=30, type=int)
parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                    default=40, type=int)
parser.add_argument("-z", "--min-stator-size",
                    help="minimum number of inactive cells in still life", default=0,
                    type=int)
parser.add_argument("-t", "--time", help="for profiling purposes", action="store_true")
parser.add_argument("-c", "--cpu-save", help="slow cpu usage to 50%%", action="store_true")
args = parser.parse_args()
lt = lifelib.load_rules(args.rule).lifetree()
glider = lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack)
# gliders = [glider[t] for t in range(4)]
ngliders = args.n_gliders
dirs = sorted(map(int, list(set(args.directions))))
rotations = [("rot"+str(d*90)).replace("rot0", "identity") for d in dirs]
rotated_gliders = rotated_gliders = [glider(rot)[t] for rot in rotations for t in range(4)]
sl = lt.pattern(args.sl).centre()(args.x, args.y)
gencol_t = 0
def gencol():
    glider_col = sl(0, 0)
    for _ in range(ngliders):
        x = randint(-args.area_width, args.area_width)
        y = randint(-args.area_height, args.area_height)
        glider_col += choice(rotated_gliders)(x, y)
    return glider_col
testsane_t = 0
def testsane(col):
    expected_pop = ngliders * 5 + sl.population
    return not any((col[x].population != expected_pop for x in xrange(5)))
results = []
rcols = []
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]
testres_t = 0
def testres(res, update=True):
    global results
    for q in results:
        if not update and q == res: continue
        for i in map(q, orientations):
            #Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res-z
            if y[1] == y: res = y
            if not res.population: return False
    return res
def updateres(res):
    global results
    newresults = [res]
    for i in xrange(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults
    results = sorted(newresults, key=lambda x: x.population)
    #print([r.population for r in results])
testcol_t = 0
def testcol():
    global gencol_t, testsane_t, testres_t
    t = clock()
    col = gencol()
    gencol_t += clock() - t
    col0 = col.__copy__()
    t = clock()
    if not testsane(col):
        testsane_t += clock() - t
        return None
    testsane_t += clock() - t
    q = col[args.first_interact_before + args.max_interact_time]
    if q.population > args.max_int_pop or q != q[1]: return None
    gens = 0
    for i in xrange(args.first_interact_before//5):
        col = col[5]
        if not col or col == col[1]: return None
        if col.population > args.max_int_pop: return None
        if sl - col:
            gens += (i+1)*5
            break
    else:
        return None
    gens0 = gens - 5
    for i in xrange(args.max_interact_time//5):
        if not sl & col: return None
        if col.population > args.max_int_pop: return None
        if col == col[1]:
            gens += (i+1)*5
            break
        col = col[5]
    else:
        return None
    t = clock()
    q = testres(col)
    if q and (not args.min_stator_size or reduce(lambda x, y: x & y, map(col0.__getitem__, xrange(gens0, gens)) + [sl]).population >= args.min_stator_size):
        updateres(q)
        testres_t += clock() - t
        return q.apgcode, col0.rle_string()
    testres_t += clock() - t
    return None
def search():
    global testcol_t
    update_int = 10000
    update_t = clock()
    cct = 0
    while 1:
        t = clock()
        q = testcol()
        testcol_t += clock() - t
        if q is not None:
            print("Object %s produced by the following collision: \n%s" % q)
            rcols.append(q[1])
        cct += 1
        if not cct % update_int:
            t = clock()
            print("%d collisions searched, %d collisions/second, %d components found." % (cct, update_int//(t-update_t), len(rcols)))
            update_t = t
            if args.time:
                print("Time per collision spent in testres(): %.4f" % (testres_t / cct * 1000))
                print("Time per collision spent elsewhere in testcol(): %.4f" % ((testcol_t - testres_t - gencol_t - testsane_t) / cct * 1000))
                print("Time per collision spent in gencol(): %.4f" % (gencol_t / cct * 1000))
                print("Time per collision spent in testsane(): %.4f" % (testsane_t / cct * 1000))
            sys.stdout.flush()
        if args.cpu_save and not cct%update_int:
            sleep(testcol_t / cct * update_int)
try:
    search()
except KeyboardInterrupt:
    for i in rcols:
        print(i)
former username: A for Awesome
praosylen#5847 (Discord)

The only decision I made was made
of flowers, to jump universes to one of springtime in
a land of former winter, where no invisible walls stood,
or could stand for more than a few hours at most...

User avatar
Saka
Posts: 3627
Joined: June 19th, 2015, 8:50 pm
Location: Indonesia
Contact:

Re: Synthesis component search script

Post by Saka » July 28th, 2019, 12:42 am

Would it be possible to add an option to set the minimum polyplet stator?
Also what about minimum cell count of the result?

GUYTU6J
Posts: 2200
Joined: August 5th, 2016, 10:27 am
Location: 拆哪!I repeat, CHINA! (a.k.a. 种花家)
Contact:

Re: Synthesis component search script

Post by GUYTU6J » July 30th, 2019, 8:34 pm

Speaking of "randomly colliding gliders with a given still life", it reminds me of Gabriel Nivasch's stable and glider, which aims to find half-bakery-like reaction.

AGreason
Posts: 54
Joined: January 31st, 2018, 9:02 pm

Re: Synthesis component search script

Post by AGreason » August 4th, 2019, 10:27 am

I made some modifications to the script.
1. I removed the requirement that at least one cell of the initial target remain alive throughout the reaction

2. it can now search in parallel, set the --n-threads argument for the number of search threads

3. instead of running collisions with a single object indefinitely, it now runs a specified number of collisions against every still life in shinjuku's database, in ascending order of synthesis cost. Set --max-cols for the number of collisions per target still life, and if desired set --start-index if you want it to start somewhere other than the start of the list (to give credit: I got the list of still lives by grabbing a chunk of four-g.py from shinjuku, and also used that to determine how to make the result mosaic)

4. it now reports the cost of the created object, and by default will only report reductions (cases when the known cost of the object is greater than the cost of the target object + n_gliders). If you want to override this, set --min-cost, and it will report all produced objects that cost that much or more. Set to 0 to report all results

5. it now combines all result rles into a single output and writes it to a file. set --out-dir to control where that file is written to. This is for ease of submission to catagolue. Note that it does not clean up previous outputs, so disk usage grows quadratically during a run and the folder should be cleaned up after a run. I've not had any runs produce enough syntheses for this to be an issue, so I didn't bother to fix it. Also note that the filename is just "mosaic" + (number of syntheses) + ".rle", so if you want to keep an output around give it a different name and put it in a different folder, to prevent it from being deleted or overwritten.

Note that it now requires shinjuku as well as lifelib, and needs python 3.7 (3.6 might also work, I haven't checked)

Code: Select all

#!/usr/bin/env python3.7
import argparse
import os
import sys
from multiprocessing import Process, Queue
from random import randint, choice
from time import perf_counter as clock

import requests

from shinjuku import lt
from shinjuku.search import dijkstra


dirs = sorted(map(int, list(set("0123"))))
glider = lt.pattern("3o$o$bo!").centre()(10, 10)
rotations = [("rot" + str(d * 90)).replace("rot0", "identity") for d in dirs]
rotated_gliders = [glider(rot)[t] for rot in rotations for t in range(4)]
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]


def getsortedsls():
    objs = []
    r = requests.get("https://catagolue.appspot.com/textcensus/b3s23/synthesis-costs")
    lines = r.content.decode().partition("\n")[2].splitlines()
    llines = len(lines)
    for (k, line) in enumerate(lines, 1):
        apgcode, cost = [x.strip('"') for x in line.split(",")]
        if k % 100 == 0:
            print(f"{k}/{llines} - {line}")
        if not apgcode.startswith("xs"):
            continue  # not a still life
        cost = int(cost)
        if cost == 999999_999999_999999:
            continue  # infinity
        if cost < 100000_000000_000002:  # pseudo
            pass
        else:
            cost -= 100000_000000_000000  # cost according to Cata

        if apgcode not in min_paths:
            print(f"{apgcode} is not in local Shinjuku but in Cata!")
            min_paths[apgcode] = (cost, None)
        sjk_cost = min_paths[apgcode][0]
        if sjk_cost > cost:
            print(f"{apgcode} has cost {sjk_cost} by local Shinjuku but {cost} by Cata")
            min_paths[apgcode][0] = cost
        objs.append((apgcode, cost))

    objs.sort(key=lambda x: x[1])
    return objs


def cost(apgcode):
    if apgcode in min_paths:
        return min_paths[apgcode][0]
    return -1


def gencol(sl, ngliders, args):
    glider_col = sl(0, 0)
    for _ in range(ngliders):
        x = randint(-args.area_width, args.area_width)
        y = randint(-args.area_height, args.area_height)
        glider_col += choice(rotated_gliders)(x, y)
    return glider_col


def testsane(col, sl, ngliders):
    expected_pop = ngliders * 5 + sl.population
    return not any((col[x].population != expected_pop for x in range(5)))


def testres(res, results, update=True):
    for q in results:
        if not update and q == res:
            continue
        for i in map(q, orientations):
            # Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res - z
            if y[1] == y:
                res = y
            if not res.population:
                return False
    return res


def updateres(res, results):
    newresults = [res]
    for i in range(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], results, update=False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults


def testcol(results, sl, ngliders, args):
    col = gencol(sl, ngliders, args)
    col0 = col.__copy__()
    if not testsane(col, sl, ngliders):
        return None
    q = col[args.first_interact_before + args.max_interact_time]
    if q.population > args.max_int_pop or q != q[1]:
        return None
    gens = 5
    for i in range(args.first_interact_before // 5):
        col = col[5]
        if not col or col == col[1]:
            return None
        if col.population > args.max_int_pop:
            return None
        if sl - col:
            gens += i * 5
            break
    else:
        return None
    for i in range(args.max_interact_time // 5):
        if col.population > args.max_int_pop:
            return None
        if col == col[1]:
            gens += i * 5
            break
        col = col[5]
    else:
        return None
    q = testres(col, results, ngliders)
    if q and (not args.min_stator_size or (sl & col).population >= args.min_stator_size):
        updateres(q, results)
        return q.apgcode, col0.rle_string(filename=f"%spattern.rle" % os.getpid())
    return None


def search(sl, sl_cost, ngliders, mincost, results, rcols, queue, args, maxcols=0):
    update_int = 100000
    update_t = clock()
    cct = 0
    while maxcols <= 0 or cct < maxcols:
        q = testcol(results, sl, ngliders, args)
        if q is not None:
            q_cost = cost(q[0])
            vals = (q[0], q_cost, sl_cost + ngliders, q[1])
            if mincost <= 0 or q_cost >= mincost:
                queue.put(vals)
        cct += 1
        if not cct % update_int:
            t = clock()
            print("%d collisions searched, %d collisions/second, %d components found." % (
                cct, update_int // (t - update_t), len(rcols)))
            sys.stdout.flush()


def workerfunc(gliders, maxcols, numobjs, args, resqueue, inqueue):
    queue = resqueue
    rcols = []
    results = []
    while True:
        task = inqueue.get()
        if task == "terminate":
            return
        apgcode, slcost, num = task
        if num % 50 == 0:
            print(f"testing %s, cost %s, %s/%s, time %s" % (apgcode, slcost, num, numobjs, int(clock() - starttime)))
        sl = lt.pattern(apgcode).centre()(args.x, args.y)
        sl_code = sl.apgcode
        if not args.override_cost >= 0:
            sl_cost = cost(sl_code)
        else:
            sl_cost = args.override_cost
        if args.min_cost < 0:
            mincost = sl_cost + gliders + 1
        else:
            mincost = args.min_cost
        search(sl, sl_cost, gliders, mincost, results, rcols, queue, args, maxcols=maxcols)


def getargs():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                        default="B3/S23")
    parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
    parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
    parser.add_argument("-u", "--area-width",
                        help="half of x width of area of glider generation", default=5,
                        type=int)
    parser.add_argument("-v", "--area-height",
                        help="half of y height of area of glider generation", default=5,
                        type=int)
    parser.add_argument("-f", "--first-interact-before",
                        help="maximum time of first interaction", default=50, type=int)
    parser.add_argument("-i", "--max-interact-time",
                        help="maximum time between beginning and end of interaction",
                        default=30, type=int)
    parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                        default=40, type=int)
    parser.add_argument("-z", "--min-stator-size",
                        help="minimum number of inactive cells in still life", default=0,
                        type=int)
    parser.add_argument("-ov", "--override-cost", help="change the reported cost of the initial still life",
                        default=-1, type=int)
    parser.add_argument("-m", "--min-cost", help="only report objects that cost more than this, if not set will only"
                                                 " report reductions", default=-1, type=int)
    parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
    parser.add_argument("-c", "--max-cols", help="how many collisions to try per target object", default=1000, type=int)
    parser.add_argument("-s", "--start-index", help="which target number to start at "
                                                    "(for resuming interrupted searches)", default=0, type=int)
    parser.add_argument("-t", "--n-threads", help="number of search threads to use", default=2, type=int)
    parser.add_argument("-d", "--out-dir", help="directory to store synthesis rles in", default=".")
    args = parser.parse_args()
    return args


def makemosaic(synths, targetdir):
    mosaic = lt.pattern()
    nsynths = len(synths)
    for i, synth in enumerate(synths):
        xoff = (i % 50) * 100
        yoff = (i // 50) * 100
        rle = synths[synth][0]
        mosaic += lt.pattern(rle)(xoff, yoff)
    mosaic.write_rle(f"%s/%s.rle" % (targetdir, nsynths))


if __name__ == "__main__":
    min_paths = dijkstra()

    starttime = clock()
    args = getargs()
    gliders = args.n_gliders
    maxcols = args.max_cols
    startindex = args.start_index
    nthreads = args.n_threads
    
    objs = getsortedsls()
    numobjs = len(objs)
    
    workqueue = Queue()
    resqueue = Queue()
    arguments = (gliders, maxcols, numobjs, args, resqueue, workqueue)
    workers = [Process(target=workerfunc, args=arguments) for i in range(nthreads)]
    tasks = [workqueue.put((objs[i][0], objs[i][1], i + 1)) for i in range(startindex, len(objs))]
    print(f"running %s objects" % numobjs)
    [w.start() for w in workers]
    
    synths = {}
    try:
        while True:
            synth = resqueue.get()
            prevcost = 9999
            apgcode = synth[0]
            if apgcode in synths:
                prevcost = synths[apgcode][1]
            if synth[2] < prevcost:
                msg = "Object %s (previous cost %s, this cost %s) produced by the following collision: \n%s" % synth
                print(msg)
                synths[apgcode] = (synth[3], synth[2])
                print(f"%s total synths" % len(synths))
                makemosaic(synths, args.out_dir)
    except KeyboardInterrupt:
        [workqueue.put("terminate") for w in workers]
        [w.terminate() for w in workers]
        raise KeyboardInterrupt

EDIT: Oh, one thing I forgot. It'll only report results that already have a listed cost in shinjuku, even if min_cost is set to 0. To instead report all results that are not in shinjuku, change the cost function to

Code: Select all

def cost(apgcode):
    if apgcode in min_paths:
        return min_paths[apgcode][0]
    return 9999

User avatar
praosylen
Posts: 2443
Joined: September 13th, 2014, 5:36 pm
Location: Pembina University, Home of the Gliders
Contact:

Re: Synthesis component search script

Post by praosylen » August 8th, 2019, 3:51 pm

A small change to my version:

Code: Select all

#collisrc.py version 0.42
from __future__ import print_function
from functools import reduce
import sys
sys.path.append("/Users/aidanpierce/Documents/CA/apgluxe")
from random import randint, choice
from time import clock, sleep
import lifelib
import argparse
if sys.version_info[0] > 2:
    xrange = range
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                    default="B3/S23")
parser.add_argument("-s", "--sl",
                    help="still life to serve as the basis for generated components",
                    default="2o$2o!")
parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
parser.add_argument("-b", "--backtrack", help="distance to backtrack gliders (in cells)",
                    default=10, type=int)
parser.add_argument("-d", "--directions",
                    help="directions of arriving gliders (as string containing any or all of characters '0123' representing standard quadrants (with 0 being used in place of 4, as if it wasn't confusing enough))",
                    default="0123")
parser.add_argument("-u", "--area-width",
                    help="half of x width of area of glider generation", default=5,
                    type=int)
parser.add_argument("-v", "--area-height",
                    help="half of y height of area of glider generation", default=5,
                    type=int)
parser.add_argument("-f", "--first-interact-before",
                    help="maximum time of first interaction", default=50, type=int)
parser.add_argument("-i", "--max-interact-time",
                    help="maximum time between beginning and end of interaction",
                    default=30, type=int)
parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                    default=40, type=int)
parser.add_argument("-z", "--min-stator-size",
                    help="minimum number of inactive cells in still life", default=0,
                    type=int)
parser.add_argument("-t", "--time", help="for profiling purposes", action="store_true")
parser.add_argument("-c", "--cpu-save", help="slow cpu usage to 50%%", action="store_true")
args = parser.parse_args()
lt = lifelib.load_rules(args.rule).lifetree()
glider = lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack)
# gliders = [glider[t] for t in range(4)]
ngliders = args.n_gliders
dirs = sorted(map(int, list(set(args.directions))))
rotations = [("rot"+str(d*90)).replace("rot0", "identity") for d in dirs]
rotated_gliders = rotated_gliders = [glider(rot)[t] for rot in rotations for t in range(4)]
sl = lt.pattern(args.sl).centre()(args.x, args.y)
gencol_t = 0
def gencol():
    glider_col = sl(0, 0)
    for _ in range(ngliders):
        x = randint(-args.area_width, args.area_width)
        y = randint(-args.area_height, args.area_height)
        glider_col += choice(rotated_gliders)(x, y)
    return glider_col
testsane_t = 0
def testsane(col):
    expected_pop = ngliders * 5 + sl.population
    return not any((col[x].population != expected_pop for x in xrange(5)))
results = []
rcols = []
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]
testres_t = 0
def testres(res, update=True):
    global results
    for q in results:
        if not update and q == res: continue
        for i in map(q, orientations):
            #Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res-z
            if y[1] == y: res = y
            if not res.population: return False
    return res
def updateres(res):
    global results
    newresults = [res]
    for i in xrange(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults
    results = sorted(newresults, key=lambda x: x.population)
    #print([r.population for r in results])
testcol_t = 0
def testcol():
    global gencol_t, testsane_t, testres_t
    t = clock()
    col = gencol()
    gencol_t += clock() - t
    col0 = col.__copy__()
    t = clock()
    if not testsane(col):
        testsane_t += clock() - t
        return None
    testsane_t += clock() - t
    q = col[args.first_interact_before + args.max_interact_time]
    if q.population > args.max_int_pop or q != q[1]: return None
    gens = 0
    for i in xrange(args.first_interact_before//5):
        col = col[5]
        if not col or col == col[1]: return None
        if col.population > args.max_int_pop: return None
        if sl - col:
            gens += (i+1)*5
            break
    else:
        return None
    gens0 = gens - 5
    for i in xrange(args.max_interact_time//5):
        if not sl & col: return None
        if col.population > args.max_int_pop: return None
        if col == col[1]:
            gens += (i+1)*5
            break
        col = col[5]
    else:
        return None
    t = clock()
    q = testres(col)
    if q and (not args.min_stator_size or ((sl & q).population >= args.min_stator_size and (not args.min_stator_size or reduce(lambda x, y: x & y, map(col0.__getitem__, xrange(gens0, gens)) + [sl]).population >= args.min_stator_size))):
        updateres(q)
        testres_t += clock() - t
        return q.apgcode, col0.rle_string()
    testres_t += clock() - t
    return None
def search():
    global testcol_t
    update_int = 10000
    update_t = clock()
    cct = 0
    while 1:
        t = clock()
        q = testcol()
        testcol_t += clock() - t
        if q is not None:
            print("Object %s produced by the following collision: \n%s" % q)
            rcols.append(q[1])
        cct += 1
        if not cct % update_int:
            t = clock()
            print("%d collisions searched, %d collisions/second, %d components found." % (cct, update_int//(t-update_t), len(rcols)))
            update_t = t
            if args.time:
                print("Time per collision spent in testres(): %.4f" % (testres_t / cct * 1000))
                print("Time per collision spent elsewhere in testcol(): %.4f" % ((testcol_t - testres_t - gencol_t - testsane_t) / cct * 1000))
                print("Time per collision spent in gencol(): %.4f" % (gencol_t / cct * 1000))
                print("Time per collision spent in testsane(): %.4f" % (testsane_t / cct * 1000))
            sys.stdout.flush()
        if args.cpu_save and not cct%update_int:
            sleep(testcol_t / cct * update_int)
try:
    search()
except KeyboardInterrupt:
    for i in rcols:
        print(i)
The -z option has been improved somewhat to ensure that the stator cells are in fact part of the result reported rather than part of an incidental unreported SL — as an example, "components" similar to the following will no longer be reported for the parameters -s "2o$obo$bo" -z 2:

Code: Select all

x = 8, y = 14, rule = B3/S23
5b2o$5bobo$6bo3$5b2o$5bobo$5bo2$2o$b2o$o3b2o$4bobo$4bo!
Hopefully this doesn't ruin anyone's day, but my thinking is that the new behavior is much more useful for almost all purposes than the previous one.
former username: A for Awesome
praosylen#5847 (Discord)

The only decision I made was made
of flowers, to jump universes to one of springtime in
a land of former winter, where no invisible walls stood,
or could stand for more than a few hours at most...

AGreason
Posts: 54
Joined: January 31st, 2018, 9:02 pm

Re: Synthesis component search script

Post by AGreason » September 22nd, 2019, 12:56 am

I ported the main loop of my version to C++, for a significant speedup, and put it on github https://github.com/AlexGreason/Collisearch. Note that you'll have to manually set the lifelib path in CMakeLists.txt to wherever your lifelib installation is

This version gets almost 50k collisions/sec (2-glider collisions) on 6 threads of an i7 6700k.

User avatar
testitemqlstudop
Posts: 1367
Joined: July 21st, 2016, 11:45 am
Location: in catagolue
Contact:

Re: Synthesis component search script

Post by testitemqlstudop » September 22nd, 2019, 2:06 am

...Wow.

GUYTU6J
Posts: 2200
Joined: August 5th, 2016, 10:27 am
Location: 拆哪!I repeat, CHINA! (a.k.a. 种花家)
Contact:

Re: Synthesis component search script

Post by GUYTU6J » January 31st, 2020, 10:49 pm

Apologies for necroposting, but can the script adapt to non-totalistic rules with custom "gliders"?

User avatar
testitemqlstudop
Posts: 1367
Joined: July 21st, 2016, 11:45 am
Location: in catagolue
Contact:

Re: Synthesis component search script

Post by testitemqlstudop » February 2nd, 2020, 2:20 am

I think so, you can change this line:

Code: Select all

glider = lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack)
to the rle of the custom glider.

GUYTU6J
Posts: 2200
Joined: August 5th, 2016, 10:27 am
Location: 拆哪!I repeat, CHINA! (a.k.a. 种花家)
Contact:

Re: Synthesis component search script

Post by GUYTU6J » February 4th, 2020, 1:11 pm

testitemqlstudop wrote:
February 2nd, 2020, 2:20 am
I think so, you can change this line:

Code: Select all

glider = lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack)
to the rle of the custom glider.
I thought there're other lines to change for custom gliders because of their speed & bounding box. Is that so?

User avatar
testitemqlstudop
Posts: 1367
Joined: July 21st, 2016, 11:45 am
Location: in catagolue
Contact:

Re: Synthesis component search script

Post by testitemqlstudop » February 4th, 2020, 10:30 pm

You may perhaps need to change this line too:

Code: Select all

    expected_pop = ngliders * 5 + sl.population

GUYTU6J
Posts: 2200
Joined: August 5th, 2016, 10:27 am
Location: 拆哪!I repeat, CHINA! (a.k.a. 种花家)
Contact:

Re: Synthesis component search script

Post by GUYTU6J » March 4th, 2020, 9:54 pm

What happens?

Code: Select all

$ python collisrc.py
Generating code for rules ['b3s23']...
Compiling lifelib shared object (estimated time: 20.0 seconds)...
[================================================================]
...compilation complete!
C:\Users\art\Desktop\xfig_home_directory\collisrc\lifelib\cygbash.sh: 行 12:  12
79 Segmentation fault      "$@"
Traceback (most recent call last):
  File "collisrc.py", line 45, in <module>
    lt = lifelib.load_rules(args.rule).lifetree()
  File "C:\Users\art\Desktop\xfig_home_directory\collisrc\lifelib\pythlib\sessio
n.py", line 245, in lifetree
    return Lifetree(self, *args, **kwargs)
  File "C:\Users\art\Desktop\xfig_home_directory\collisrc\lifelib\pythlib\sessio
n.py", line 25, in __init__
    self.ptr = lifelib('CreateLifetree', memory, n_layers)
  File "C:\Users\art\Desktop\xfig_home_directory\collisrc\lifelib\pythlib\lowlev
el.py", line 206, in __call__
    retval = pickle.load(self.the_library.stdout)
EOFError
Exception AttributeError: "'Lifetree' object has no attribute 'ptr'" in <bound m
ethod Lifetree.__del__ of <lifelib.pythlib.session.Lifetree object at 0x00000000
03D9ED30>> ignored

User avatar
praosylen
Posts: 2443
Joined: September 13th, 2014, 5:36 pm
Location: Pembina University, Home of the Gliders
Contact:

Re: Synthesis component search script

Post by praosylen » March 5th, 2020, 1:03 am

GUYTU6J wrote:
March 4th, 2020, 9:54 pm
What happens?

Code: Select all

error
I'm pretty sure this is a general lifelib error, rather than a script-specific error. As someone wholly unfamiliar with lifelib's inner workings, I'd hazard a guess that you have some essential file that's missing, truncated, or corrupted, but precisely what file that would be I have no idea.
former username: A for Awesome
praosylen#5847 (Discord)

The only decision I made was made
of flowers, to jump universes to one of springtime in
a land of former winter, where no invisible walls stood,
or could stand for more than a few hours at most...

Hunting
Posts: 4395
Joined: September 11th, 2017, 2:54 am

Re: Synthesis component search script

Post by Hunting » January 2nd, 2021, 11:14 am

A for awesome wrote:
July 24th, 2019, 10:53 pm
Here's a lifelib script I've been working on to find synthesis components by randomly colliding gliders with a given still life:

Code: Select all

#collisrc.py version 0.3
import sys
sys.path.append("/.../path/to/lifelib/")
from random import randint, choice
from time import clock, sleep
import lifelib
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-n", "--n-gliders", help="number of gliders", default=2, type=int)
parser.add_argument("-r", "--rule", help="rule to use (must allow gliders)",
                    default="B3/S23")
parser.add_argument("-s", "--sl",
                    help="still life to serve as the basis for generated components",
                    default="2o$2o!")
parser.add_argument("-x", "--x", help="x displacement of still life", default=0, type=int)
parser.add_argument("-y", "--y", help="y displacement of still life", default=0, type=int)
parser.add_argument("-b", "--backtrack", help="distance to backtrack gliders (in cells)",
                    default=10, type=int)
parser.add_argument("-d", "--directions",
                    help="directions of arriving gliders (as string containing any or all of characters '0123' representing standard quadrants (with 0 being used in place of 4, as if it wasn't confusing enough))",
                    default="0123")
parser.add_argument("-u", "--area-width",
                    help="half of x width of area of glider generation", default=5,
                    type=int)
parser.add_argument("-v", "--area-height",
                    help="half of y height of area of glider generation", default=5,
                    type=int)
parser.add_argument("-f", "--first-interact-before",
                    help="maximum time of first interaction", default=50, type=int)
parser.add_argument("-i", "--max-interact-time",
                    help="maximum time between beginning and end of interaction",
                    default=30, type=int)
parser.add_argument("-p", "--max-int-pop", help="maximum intermediate population",
                    default=40, type=int)
parser.add_argument("-z", "--min-stator-size",
                    help="minimum number of inactive cells in still life", default=0,
                    type=int)
parser.add_argument("-t", "--time", help="for profiling purposes", action="store_true")
parser.add_argument("-c", "--cpu-save", help="slow cpu usage to 50%", action="store_true")
args = parser.parse_args()
lt = lifelib.load_rules(args.rule).lifetree()
gliders = map(lt.pattern("3o$o$bo!").centre()(args.backtrack, args.backtrack).__getitem__, xrange(4))
ngliders = args.n_gliders
dirs = sorted(map(int, list(set(args.directions))))
sl = lt.pattern(args.sl).centre()(args.x, args.y)
gencol_t = 0
def gencol():
    return reduce(lambda x, y: x+y, map(lambda _: gliders[randint(0, 3)](("rot"+str(choice(dirs)*90)).replace("rot0", "identity"), randint(-args.area_width, args.area_width), randint(-args.area_height, args.area_height)), [0]*ngliders)) + sl
testsane_t = 0
def testsane(col):
    return len(set(map(lambda x:col[x].population, xrange(5)) + [ngliders * 5 + sl.population])) == 1
results = []
rcols = []
orientations = ["rot270", "rot180", "rot90", "identity", "flip_x", "flip_y", "swap_xy", "swap_xy_flip"]
testres_t = 0
def testres(res, update=True):
    global results
    for q in results:
        if not update and q == res: continue
        for i in map(q, orientations):
            #Rudimentary and not 100% functional form of object separation
            z = res.match(i, halo="3o$3o$3o!").convolve(i)
            y = res-z
            if y[1] == y: res = y
            if not res.population: return False
    return res
def updateres(res):
    global results
    newresults = [res]
    for i in xrange(len(results)):
        if results[i].population < res.population + 3:
            q = testres(results[i], False)
            if q:
                newresults = [q] + newresults
        else:
            newresults = [results[i]] + newresults
    results = sorted(newresults, key=lambda x: x.population)
    print map(lambda x: x.population, results)
testcol_t = 0
def testcol():
    global gencol_t, testsane_t, testres_t
    t = clock()
    col = gencol()
    gencol_t += clock() - t
    col0 = col.__copy__()
    t = clock()
    if not testsane(col):
        testsane_t += clock() - t
        return None
    testsane_t += clock() - t
    gens = 5
    for i in xrange(args.first_interact_before/5):
        col = col[5]
        if not col or col == col[1]: return None
        if col.population > args.max_int_pop: return None
        if sl - col:
            gens += i*5
            break
    else:
        return None
    gens0 = gens
    for i in xrange(args.max_interact_time/5):
        if not sl & col: return None
        if col.population > args.max_int_pop: return None
        if col == col[1]:
            gens += i*5
            break
        col = col[5]
    else:
        return None
    t = clock()
    q = testres(col)
    if q and (not args.min_stator_size or reduce(lambda x, y: x & y, map(col0.__getitem__, xrange(gens0-5, gens))).population >= args.min_stator_size):
        updateres(q)
        testres_t += clock() - t
        return q.apgcode, col0.rle_string()
    testres_t += clock() - t
    return None
def search():
    global testcol_t
    cct = 0
    while 1:
        t = clock()
        q = testcol()
        testcol_t += clock() - t
        if q is not None:
            print "Object %s produced by the following collision: \n%s" % q
            rcols.append(q[1])
        cct += 1
        if not cct%500:
            print cct, "collisions searched,", len(rcols), "components found."
            if args.time:
                print "Time per collision spent in testres():", testres_t / cct * 1000
                print "Time per collision spent elsewhere in testcol():", (testcol_t - testres_t - gencol_t - testsane_t) / cct * 1000
                print "Time per collision spent in gencol():", gencol_t / cct * 1000
                print "Time per collision spent in testsane():", testsane_t / cct * 1000
        if args.cpu_save and not cct%5000:
            sleep(testcol_t / cct * 5000)
try:
    search()
except KeyboardInterrupt:
    for i in rcols:
        print i
Basic instructions: save as "collisrc.py" (or really whatever you want) and replace "/.../path/to/lifelib/" in the third line with the actual path to lifelib on your system. Run using the command "python collisrc.py -n [number of gliders] -s [still life target in rle format, enclosed with single quotes]". Ideally you could run "python collisrc.py -h" to see the script's other options, but argparse is giving me an error for no clear reason, so just look in the code instead. The most important other options are -r, which sets the rule (must support gliders); -d, which sets which directions gliders can come from; and -x, -y, -u, and -v, which set the x and y displacement of the still life from where the gliders are aimed and the width and height of the regions in which gliders can be generated, respectively. -z, which is intended to specify a minimum number of stator cells in the interaction, would be a very useful option, but it's fairly buggy and so doesn't always work properly.

I'm sure there are a lot of improvements that could be made — for one, the script is a lot slower than I would like, and I suspect there are major enhancements that I'm missing due to inexperience with lifelib, and for another object separation doesn't work particularly well in certain cases. I apologize in advance that I won't have much time to answer questions/work on this more due to a busy personal schedule, but anyone who wants to improve/make modifications is welcome to do so.
I tried running it with a block (python collisrc.py -r B2n3/S23-q -n 3 -s "2o$2o") but it keep giving stuff like

Code: Select all

x = 15, y = 18, rule = B2n3/S23-q
12bobo$12b2o$13bo9$3o4$9b3o$9bo$10bo!
Which uses a blinker.

Post Reply