simsim314 wrote:@calcyman I was wondering can you give a review when you recommend to use lifelib and when golly? Is it more search oriented?
I'd continue to use Golly for manual editing and experimenting, and continue to use Golly Python scripts to augment that (such as using shift.py). For assembling large patterns, you're probably better off with lifelib. In fact, if you move your Caterloopillar construction script to lifelib, I imagine it would both run faster and handle larger constructions.
As a case in point, trying to construct metapatterns in Golly is incredibly slow and painful (because it's manipulating cell lists instead of directly operating on quadtrees), whereas this runs quickly irrespective of the size of the initial pattern:
https://gitlab.com/apgoucher/slmake/blo ... etafier.py
What functionality there is in lifelib which you don't have in golly?
- Pattern matching (including finding/replacing subpatterns);
- Kronecker products (useful for constructing metapatterns);
- Construction of spaceship streams;
- Ability to run spaceships and oscillators backwards;
- Compatibility with LifeViewer (if you're running from a Jupyter notebook).
There isn't the idea of having to store patterns in a limited number of 'layers' either: patterns are objects, so you can pass them to functions and operate upon them.
or is it only a question of performance?
That's the main point: all pattern editing operations (translation, rotation/reflection, boolean operations, convolutions, etc.) operate directly on compressed quadtrees instead of on cell lists, so they're fast in the way that hashlife is. Also, the hashlife implementation itself falls back on a machine-optimised assembly routine for iterating 32x32 blocks, so you don't have to decide between HashLife or QuickLife; the same algorithm is efficient for both structured patterns and random patterns.
Also I understand you moved to python instead of what you previously thought of making user friendly c++ version of lifelib.
You can use either Python (2 or 3) or C++11 (or above) with lifelib. Using Python is recommended for several reasons. Firstly, lifelib's Python bindings are designed to support unbounded integers for operations such as advancing and shifting patterns. So in Python, you can do:
Code: Select all
import lifelib
lt = lifelib.load_rules('b3s23').lifetree()
spacefiller = lt.pattern('''
#N Max
#O Tim Coe
#C A spacefiller that fills space with zebra stripes.
#C www.conwaylife.com/wiki/index.php?title=Max
x = 27, y = 27, rule = B3/S23
18bo8b$17b3o7b$12b3o4b2o6b$11bo2b3o2bob2o4b$10bo3bobo2bobo5b$10bo4bobo
bobob2o2b$12bo4bobo3b2o2b$4o5bobo4bo3bob3o2b$o3b2obob3ob2o9b2ob$o5b2o
5bo13b$bo2b2obo2bo2bob2o10b$7bobobobobobo5b4o$bo2b2obo2bo2bo2b2obob2o
3bo$o5b2o3bobobo3b2o5bo$o3b2obob2o2bo2bo2bob2o2bob$4o5bobobobobobo7b$
10b2obo2bo2bob2o2bob$13bo5b2o5bo$b2o9b2ob3obob2o3bo$2b3obo3bo4bobo5b4o
$2b2o3bobo4bo12b$2b2obobobobo4bo10b$5bobo2bobo3bo10b$4b2obo2b3o2bo11b$
6b2o4b3o12b$7b3o17b$8bo!
''')
spacefiller[10 ** 60].bounding_box
to obtain the bounding box of the spacefiller after 10^60 generations.
Also, the ordeal of switching between rules is smoothed out in the Python bindings; you can just run:
Code: Select all
sess = lifelib.load_rules("b38s23")
It's somewhat unfortunate,
Why so? They have exactly the same performance, because all of the low-level code is implemented in C++ and assembly.
can you point me in the direction of how you used lifelib in c++ currently?
It's a header-only library, so it's best to have the lifelib repository in a subdirectory of your project. Then, most of the core functionality can be included using:
Your patterns themselves reside in an object called a lifetree, which is a garbage-collected compressed forest of hashed quadtrees. So you'll need to create a lifetree at the start:
Code: Select all
int main() {
apg::lifetree<uint32_t, 1> lt(1000);
// code goes here
return 0;
}
The template parameter uint32_t means that it uses 32-bit indices internally. The template parameter 1 means that you only want 1-bit states (0 or 1), ideal for 2-state rules. The constructor argument 1000 gives the lifetree a garbage collection threshold of 1000 MB; whenever it exceeds that, it will run garbage collection.
Now you can create patterns inside this lifetree. For example, you might want to run:
Code: Select all
apg::pattern pat(<, "file_to_load.mc");
or alternatively:
Code: Select all
apg::pattern lidka(<, "bo$obo$bo8$8bo$6bobo$5b2obo2$4b3o!", "b3s23");
You can then do things such as run it for 30000 generations:
Code: Select all
apg::pattern lidka_30k = lidka[30000];
and print its population:
Code: Select all
std::cout << lidka_30k.totalPopulation() << std::endl;
Both the Python and C++11 bindings have full support for the binary operators:
- Conjunction: & and &=
- Disjunction: + (or |) and += (or |=)
- Exclusive disjunction: ^ and ^=
- Subtraction: - and -=
- Kronecker product: * and *=
You can also use square brackets to advance and round brackets to shift (again, in both Python and C++11), just like in Golly's glife:
Code: Select all
apg::pattern two_lidkas = lidka + lidka(1000, 0);
More niche functionality is provided in classifier.h (for object separation), upattern.h (for running unhashed patterns -- basically useful only for random soups), and streamlife.h (for a variant of hashlife which doesn't suffer when nearby glider streams run in opposite directions).
If you want to support a different rule in your C++ program, such as B38/S23, you need to run the following in Python (from your project's root directory, assuming that is the immediate parent of the lifelib repository directory):
Code: Select all
from lifelib.autocompile import reset_tree
reset_tree("b38s23")
This is what apgsearch uses to change rules, for instance. You can have up to 8 rules coexisting in a single lifelib by providing a list of rulestrings instead of a single string.