Script to generate illustrations for the Hensel notation

For scripts to aid with computation or simulation in cellular automata.
Post Reply
marioxcc
Posts: 11
Joined: June 7th, 2017, 10:12 pm

Script to generate illustrations for the Hensel notation

Post by marioxcc » July 5th, 2017, 6:33 pm

I made this script to generate the images I am about to place in the article non-totalistic Life-like cellular automaton of the wiki.

It is not written with the intent to be of general purpose or a masterpiece of programming, but instead as a pragmatic tool that is more practical than generating the illustrations manually with GIMP.

Code: Select all

# Copyright (C) 2017 Mario Castelán Castro
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Instructions:
#
# Run this program with Python 3 and no command line arguments.  A set
# of PPM files will be generated, one for each cell environment that
# can be specified for non-totalistic rules in the Hensel notation.
#
# These files can then be converted to PNG.  I used optipng
# <http://optipng.sourceforge.net/>.

nhds = {
    "0": "   | X |   ",
    "1c":"-  | X |   ",
    "1e":" - | X |   ",
    "2c":"- -| X |   ",
    "2e":" - |-X |   ",
    "2k":" - | X |-  ",
    "2a":"-- | X |   ",
    "2i":" - | X | - ",
    "2n":"-  | X |  -",
    "3c":"- -| X |  -",
    "3e":" - |-X | - ",
    "3k":" - |-X |  -",
    "3a":"-- |-X |   ",
    "3i":"-  |-X |-  ",
    "3n":"-  |-X |  -",
    "3y":"- -| X | - ",
    "3q":"-  | X | --",
    "3j":"   |-X | --",
    "3r":" - | X | --",
    "4c":"- -| X |- -",
    "4e":" - |-X-| - ",
    "4k":"- -|-X | - ",
    "4a":"-  |-X |-- ",
    "4i":"- -|-X-|   ",
    "4n":"-  |-X |- -",
    "4y":"- -| X-|-  ",
    "4q":" --| X-|-  ",
    "4j":" - |-X-|-  ",
    "4r":"   |-X-|-- ",
    "4t":"---| X | - ",
    "4w":"-  |-X | --",
    "4z":"-- | X | --"}

def parse(string):
    result = 0
    error_str = "Bad environment specifier."
    if len(string) != 11:
        raise Exception(error_str)
    # Check fixed characters
    for (pos, char) in [(3, "|"), (5, "X"), (7, "|")]:
        if string[pos] != char:
            raise Exception(error_str)
    # Check for variable characters
    for (i, pos) in zip(range(8), [0, 1, 2, 4, 6, 8, 9, 10]):
        if string[pos] == " ":
            pass
        elif string[pos] == "-":
            result = result | 1 << i
        else:
            raise Exception(error_str)
    result = result & 0xf | (result & 0xf0) << 1
    return result

def check_non_negative_integer(*l):
    for x in l:
        if not isinstance(x, int) or x < 0:
            raise Exception("Not a non-negative integer.")


def get_bit(cell_env, x, y):
    if x >= 3 or x < 0 or y >= 3 or y < 0:
        return 0
    else:
        index = 3 * y + x
        return 1 & cell_env >> index

def count_bits(x):
    check_non_negative_integer(x)
    bits = 0
    while x != 0:
        x = x & x - 1
        bits += 1
    return bits

def strides(initial, stride, count):
    return range(initial, initial + count * stride, stride)

def set_strides(output, offset, input, repeats, stride_step, stride_count):
    check_non_negative_integer(offset, repeats, stride_step, stride_count)
    if offset + stride_step * (stride_count - 1) + len(input) * repeats > len(output):
        raise Exception("Output bytearray too small.")
    for stride_offset in strides(offset, stride_step, stride_count):
        for i in strides(stride_offset, len(input), repeats):
            output[i:i + len(input)] = input

def write_ppm(cell_env,
              file_name,
              cell_size = 16,
              # Colors
              color_live        = (  0,   0,   0),
              color_dead        = (255, 255, 255),
              color_marked      = (192, 239, 192),
              color_grid        = (192, 192, 192),
              color_grid_marked = ( 82, 192,  82)):
    file = open(file_name, "wb")
    img_side_len = cell_size * 3 + 1
    header = "P6 {0} {0} 255 ".format(img_side_len).encode()
    header_size = len(header)
    img_total_size = header_size + 3 * img_side_len ** 2
    img_raw = bytearray(img_total_size)
    img_raw[0:header_size] = header
    color = None
    # We divide the image in an irregular grid of size 7×7. There is
    # one element of this grid for each interior of cell, horizontal
    # border segment, vertical border segment, and intersection.
    for j in range (7):
        for i in range (7):
            x = i // 2
            y = j // 2
            # i and j are coordinates of the irregular 7×7 grid just
            # described. x and y are coordinates of a cell.
            offset_x = cell_size * x + i % 2
            offset_y = cell_size * y + j % 2
            if (i % 2) == 1 and (j % 2) == 1:
                # This element is the interior of a cell
                width = cell_size - 1
                height = cell_size - 1
                if x == 1 and y == 1:
                    color = color_live
                elif get_bit(cell_env, x, y) == 1:
                    color = color_marked
                else:
                    color = color_dead
            else:
                # This element is part of the grid
                width = 1 if i % 2 == 0 else cell_size - 1
                height = 1 if j % 2 == 0 else cell_size - 1
                is_marked = get_bit(cell_env, x, y)
                if i % 2 == 0:
                    is_marked = is_marked | get_bit(cell_env, x - 1, y)
                if j % 2 == 0:
                    is_marked = is_marked | get_bit(cell_env, x, y - 1)
                if i % 2 == 0 and j % 2 == 0:
                    is_marked = is_marked | get_bit(cell_env, x - 1, y - 1)
                if is_marked == 1:
                    color = color_grid_marked
                else:
                    color = color_grid
            offset = header_size + 3 * (img_side_len * offset_y + offset_x)
            # print("({}, {}): ({}, {}) {}".format(i, j, width, height, color))
            # Fill the rectangle
            set_strides(img_raw, offset, color, width, 3 * img_side_len, height)
    file.write(img_raw)
    file.close()

def gen_files():
    for string in nhds:
        if not (string != "0" or string != "8") \
           and (len(string) != 2 \
                or not (ord("1") <= ord(string[0]) <= ord("7")) \
                or "cekainyqjrtwz".find(string[1]) == -1):
            print(string)
            raise Exception("Bad cell environment name.")
        n = int(string[0])
        cell_env = parse (nhds[string])
        if n != count_bits(cell_env):
            raise Exception("Bad number of bits.")
        write_ppm(cell_env, "{}.ppm".format(string))
        if n < 4:
            if n == 0:
                cell_env_name = "8.ppm"
            else:
                cell_env_name = "{}{}.ppm".format(8 - n, string[1])
            write_ppm(0b111101111 & ~cell_env, cell_env_name)

gen_files()

User avatar
Andrew
Moderator
Posts: 927
Joined: June 2nd, 2009, 2:08 am
Location: Melbourne, Australia
Contact:

Re: Script to generate illustrations for the Hensel notation

Post by Andrew » July 6th, 2017, 7:06 am

marioxcc wrote:# Run this program with Python 3 ...
For people (like me) who don't have Python 3 it will also run with Python 2.7 if a few non-ASCII characters are changed. On line 1 change á to a. On lines 128 and 135 change × to x.

In future, if you want to create PNG files you might find it a lot easier to do it with a Golly script using overlay commands. Below is a Lua version of your script that creates the desired set of PNG files. Best to save the script (eg. as hensel.lua) in a location where you want the .png files to be created. (If you select the code and use Golly's Run Clipboard command the files will be created in your data directory -- add g.note(g.getdir("data")) to see where that is on your platform.)

Code: Select all

-- Copyright (C) 2017 Mario Castelán Castro.
-- Converted to Lua by Andrew Trevorrow so that it can be
-- run from Golly to create a set of PNG files.

local g = golly()
local ov = g.overlay

local nhds = {
    ["0"]  = "   | X |   ",
    ["1c"] = "-  | X |   ",
    ["1e"] = " - | X |   ",
    ["2c"] = "- -| X |   ",
    ["2e"] = " - |-X |   ",
    ["2k"] = " - | X |-  ",
    ["2a"] = "-- | X |   ",
    ["2i"] = " - | X | - ",
    ["2n"] = "-  | X |  -",
    ["3c"] = "- -| X |  -",
    ["3e"] = " - |-X | - ",
    ["3k"] = " - |-X |  -",
    ["3a"] = "-- |-X |   ",
    ["3i"] = "-  |-X |-  ",
    ["3n"] = "-  |-X |  -",
    ["3y"] = "- -| X | - ",
    ["3q"] = "-  | X | --",
    ["3j"] = "   |-X | --",
    ["3r"] = " - | X | --",
    ["4c"] = "- -| X |- -",
    ["4e"] = " - |-X-| - ",
    ["4k"] = "- -|-X | - ",
    ["4a"] = "-  |-X |-- ",
    ["4i"] = "- -|-X-|   ",
    ["4n"] = "-  |-X |- -",
    ["4y"] = "- -| X-|-  ",
    ["4q"] = " --| X-|-  ",
    ["4j"] = " - |-X-|-  ",
    ["4r"] = "   |-X-|-- ",
    ["4t"] = "---| X | - ",
    ["4w"] = "-  |-X | --",
    ["4z"] = "-- | X | --"}

-- this hack allows strings to be indexed with 1st byte at index 0
getmetatable('').__index = function(str,i)
    if type(i) == 'number' then
        return string.sub(str,i+1,i+1)
    else
        return string[i]
    end
end

function parse(str)
    local result = 0
    for i, pos in ipairs( {0, 1, 2, 4, 6, 8, 9, 10} ) do
        if str[pos] == " " then
            -- pass
        elseif str[pos] == "-" then
            result = result | 1 << (i-1)
        else
            g.exit("Bad environment specifier: "..tostring(str[pos]))
        end
    end
    result = result & 0xf | (result & 0xf0) << 1
    return result
end

function draw_line(x0, y0, x1, y1)
    ov("line "..x0.." "..y0.." "..x1.." "..y1)
end

function get_bit(cell_env, x, y)
    if x >= 3 or x < 0 or y >= 3 or y < 0 then
        return 0
    else
        return 1 & cell_env >> (3 * y + x)
    end
end

function write_png(cell_env, file_name)
    local cell_size = 16
    local img_side_len = cell_size * 3 + 1
    local color_live        = "rgba   0   0   0 255"
    local color_dead        = "rgba 255 255 255 255"
    local color_marked      = "rgba 192 239 192 255"
    local color_grid        = "rgba 192 192 192 255"
    local color_grid_marked = "rgba  82 192  82 255"
    
    -- create an overlay of the desired size
    ov("create "..img_side_len.." "..img_side_len)
    
    -- fill background with white
    ov(color_dead)
    ov("fill")
    
    -- draw grid lines
    ov(color_grid)
    for x = 0, img_side_len, cell_size do
        draw_line(x, 0, x, img_side_len-1)
    end
    for y = 0, img_side_len, cell_size do
        draw_line(0, y, img_side_len-1, y)
    end
    
    -- fill central cell
    ov(color_live)
    ov("flood "..math.floor(img_side_len/2).." "..math.floor(img_side_len/2))
    
    -- draw marked cells
    for y = 0, 2 do
        for x = 0, 2 do
            if get_bit(cell_env, x, y) == 1 then
                ov(color_grid_marked)
                local x0 = x*cell_size
                local y0 = y*cell_size
                draw_line(x0, y0, x0+cell_size, y0)
                draw_line(x0+cell_size, y0, x0+cell_size, y0+cell_size)
                draw_line(x0+cell_size, y0+cell_size, x0, y0+cell_size)
                draw_line(x0, y0+cell_size, x0, y0)
                ov(color_marked)
                ov("flood "..math.floor(x0+cell_size/2).." "..math.floor(y0+cell_size/2))
            end
        end
    end
    
    -- save overlay image in given PNG file and delete the overlay
    ov("save 0 0 0 0 "..file_name)
    ov("delete")
end

function gen_files()
    for key, str in pairs(nhds) do
        local n = tonumber(key[0])
        local cell_env = parse(str)
        local cell_env_name
        write_png(cell_env, key..".png")
        if n < 4 then
            if n == 0 then
                cell_env_name = "8.png"
            else
                cell_env_name = (8-n)..key[1]..".png"
            end
            write_png(tonumber("111101111",2) & ~cell_env, cell_env_name)
        end
    end
    g.note("Success!")
end

gen_files()
Use Glu to explore CA rules on non-periodic tilings: DominoLife and HatLife

fluffykitty
Posts: 1175
Joined: June 14th, 2014, 5:03 pm
Contact:

Re: Script to generate illustrations for the Hensel notation

Post by fluffykitty » August 18th, 2017, 10:12 pm

Aren't there GPL violations or something going on here?

User avatar
Andrew
Moderator
Posts: 927
Joined: June 2nd, 2009, 2:08 am
Location: Melbourne, Australia
Contact:

Re: Script to generate illustrations for the Hensel notation

Post by Andrew » August 18th, 2017, 11:20 pm

fluffykitty wrote:Aren't there GPL violations or something going on here?
What exactly are you worried about? If Mario had any objections to me converting his Python code to Lua then presumably he would have let me know by now.
Use Glu to explore CA rules on non-periodic tilings: DominoLife and HatLife

Post Reply