0

I'm trying to create an agent-based model in Python. For the environment, I have been using a numpy array of MxN size. Each pixel represents a patch of land. I would like to assign each patch of land an owner such that the larger blocks that are created are contiguous. Ideally, I would like to be able to specify the number of larger blocks.

This is what I'm envisioning

I'm having a surprisingly hard time trying to generate a randomized map. I have been able to hack together a really crude solution, but it still has some major flaws. I thought I'd elicit other people for ideas before accepting this fate.

Chris
  • 13
  • 3
  • questions are suppsed to be code centric here, but an idea would be to randomly seed a 0 filled matrix with single 'owner" numbers, then iterativley "grow" each into the 0 filled regions until they meet – f5r5e5d Apr 10 '17 at 22:11
  • Thanks and sorry! I wasn't sure where to post since the problem I'm having is with the coding portion of this model. That's actually the way I have it working now. I select a random coordinate pair and random search radius (r) then grow into all 0 cells r distance away. The problem is that I end up with oddly shaped long patches of land. It may be that this is the best I can get with a random method, though. – Chris Apr 10 '17 at 22:28

1 Answers1

1

Couldn't resist giving it a shot, so here is an attempt using scipy.ndimage.grey_dilation which appears to be reasonably fast. grey_dilation expands territory using "structure elements" -- "growth kernels" in the code below. I don't have any experience as to how much control they give over the process but they are something you could play with:

import numpy as np
from scipy import ndimage

growth_kernels = """
010 000 010 111
111 111 010 111
010 000 010 111
"""

growth_kernels = """
555555555 543212345
444444444 543212345
333333333 543212345
222222222 543212345
111101111 543202345
222222222 543212345
333333333 543212345
444444444 543212345
555555555 543212345
"""

def patches(shape, N, maxiter=100):
    # load kernels
    kernels = np.array([[[int(d) for d in s] for s in l.strip().split()]
                        for l in growth_kernels.split('\n')
                        if l.strip()], np.int)
    nlev = np.max(kernels) + 1
    # special case for binary kernels
    if nlev == 2:
        kernels = 2 - kernels
        nlev = 3
    kernels = -kernels.swapaxes(0, 1) * N
    key, kex = kernels.shape[1:]
    kernels[:, key//2, kex//2] = 0
    # seed patches leave a gap between 0 and the first patch
    out = np.zeros(shape, int)
    out.ravel()[np.random.choice(out.size, N)] = np.arange((nlev-1)*N+1, nlev*N+1)
    # shuffle labels after each iteration, so larger numbers do not get
    # a systematic advantage
    shuffle = np.arange((nlev+1)*N+1)
    # also map negative labels to zero
    shuffle[nlev*N+1:] = 0
    shuffle_helper = shuffle[1:nlev*N+1].reshape(nlev, -1)
    for j in range(maxiter):
        # pick one of the kernels
        k = np.random.randint(0, kernels.shape[0])
        # grow patches
        out = ndimage.grey_dilation(
            out, kernels.shape[1:], structure=kernels[k], mode='constant')
        # shuffle
        shuffle_helper[...] = np.random.permutation(
            shuffle[(nlev-1)*N+1:nlev*N+1])
        out = shuffle[out]
        if np.all(out):
            break
    return out % N

res = patches((30, 80), 26)
print(len(np.unique(res)))
for line in res:
    print(''.join(chr(j+65) for j in line))

Sample output:

WWWWWKKKKKKKKKKKKMMMLLLLLLLLLLJJJJJJJJJCCCCCCCCCCCCCCCCCSSSSSSSSSAAAAAAAAAAAAAAA
WWWWWKKKKKKKKKKKKMMMLLLLLLLLLLJJJJJJJJJCCCCCCCCCCCCCSSSSSSSSSSSSSAAAAAAAAAAAAAAA
WWWWWKKKKKKKKKKKKMMMLLLLLLLLLLJJJJJJJJJJJJJCCCCCCCCCSSSSSSSSSSSSSAAAAAAAAAAAAAAA
WWWWWKKKKKKKKKKKKMMMLLLLLLLLLLLLLLJJJJJJJJJCCCCCCCCCSSSSSRRRRRRRRRAAAAAAAAAAAAAA
WWWFFFFKKKKKKKKKKMMMLLLLLLLLLLLLLLJJJJJJJJJCCCCCCCCCSSSSSRRRRRRRRRAAAAAAAAAAAAAA
WWWFFFFKKKKKMMMMMMMMMLLLLLLLLLLLLLJJJJJJJJJCCCCCCCCCSSSSSRRRRRRRRRAAAAAAAAAAAAAA
WWWFFFFKKKKKMMMMMMMMMLLLLLLLLLLLLLJJJJJJJJJJJJJCSSSSSSSSSRRZZZZZZZZZZZGGGGGGGGGG
WWWFFFFFFFFFMMMMMMMMTTTTTTLLLLLLLLJJJJJJJJJJJJJCSSSSSSSSSRRZZZZZZZZZZZGGGGGGGGGG
WWWFFFFFFFFFMMMMMMMMTTTTTTLLLLLLLLJJJJJJJJJJJJJHSSSSSSSSSRRZZZZZZZZZZZGGGGGGGGGG
FFFFFFNNNNNNMMMMMMMMTTTTTTLLLLLLLLJJJJJJJJJJJJJHSSSSSSSSSRRZZZZZZZZZZZGGGGGGGGGG
FFFFFFNNNNNNMMMMMMMMTTTTTTLLLLLLLLJJJJJHHHHHHHHHSRRRRRRRRRRZZZZZZZZZZZGGGGGGGGGG
FFFFFFNNNNNNMMMMMMMMTTTTTTLLLLLLLLLHHHHHHHHHHHHHSRRRRRRRRRRZZZZZZZZZZZGGGGGGGGGO
FFFFFFNNNNNNMMMMMMMMTTTTTTTTTTTTHHHHHHHHHHHHHHHHDRRRRZZZZZZZZZZZZZZZZZGGGGGGGGGO
NNNNNNNNNNNNMMMMMMMMTTTTTTTTTTTTHHHHHHHHDDDDDDDDDDRRRZZZZZZZZZZZZZZZZZGGGGGGGGGO
NNNNNNNNNNNNNNNTTTTTTTTTTTTTTTTTHHHHHHHHDDDDDDDDDDUUUZZZZZZZZZZZZZZZZZGGGGGGGGGO
EEEEENNNNNNNNNNTTTTTTTTTTTTTTTTTHHHHHHHHDDDDDDDDDDUUUUUUUUUUUUUUUUZZZZGOOOOOOOOO
EEEEENNNNNNNNNNNTTTTTTTTTTTTTTTHHHHHHHHHDDDDDDDDDUUUUUUUUUUUUUUUUUZZZZGOOOOOOOOO
EEEEENNNNNNNNNNNTTTTTTTTTTTTTTTHHHHHHHHHDDDDDDDDDUUUUUUUUUUUUUUUUUZZZZGOOOOOOOOO
EEEEEEEEEEEENNNNTTTTTTTTTTTTTTTHHHHHHHHHDDDDDDDDDUUUUUUUUUUUUUUUUUXXXXGOOOOOOOOO
EEEEEEEEEEEENNNNTTTTTTTTTTTTTTTHHDDDDDDDDDDDDDDDDUUUUUUUUUUUUUUUUUXXXXXOOOOOOOOO
EEEEEEEEEEEENNNNVVVVVVVVVVVTTTTHHDDDDDDDDDDDDDDDDPPPPPBBUUUUUUUUUUXXXXXOOOOOOOOO
EEEEEEEEEEEEVVVVVVVVVVVVVVVTTTTQQDDDDDDDDDDDDDDDDPPPPPBBUUUUUUUUUUXXXXXXXXXXXXXX
EEEEEEEEEEEEVVVVVVVVVVVVVVVTTTTQQQQQQQQQQQQQQPPPPPPPPPBBUUUUUUUUUUXXXXXXXXXXXXXX
EEEEEEEEEEEEVVVVVVVVVVVVVVVTTTTQQQQQQQQQQQQQQQQQPPPPPPBBUUUUUUUUUUXXXXXXXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVHHQQQQQQQQQQQQQQQQQPPPPPPBBUUUUUIIIIIIIIXXXXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVQQQQQQQQQQQQQQQQQQQPPPPPPBBBBBIIIIIIIIYYYYYXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVQQQQQQQQQQQQQQQQQQQPPPPPPBBBBBIIIIIIIIYYYYYXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVQQQQQQQQQQQQQQQQQQQPPPPPPBBBBBIIIIIIIIYYYYYXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVQQQQQQQQQQQQQQQQQQQPPPPPPBBBBBIIIIIIIIYYYYYXXXXXXXX
EEEEEEEEEEVVVVVVVVVVVVVVVVVVVQQQQQQQQQQQQQQQQQQQPPBBBBBBIIIIIIIIIIIYYYYYXXXXXXXX
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • This works brilliantly. Great find! I'll play with the parameters some, but my initial reaction is that it works for my needs! – Chris Apr 11 '17 at 04:15
  • I've been playing with the kernel a bit, but I'm still pretty far away from understanding it. Any quick tips for getting the edges more square and less angular? – Chris Apr 11 '17 at 14:31
  • @Chris Hm, I don't understand them too well myself. Have a look at the updated answer, that's how square I could get it more or less by trial and error. That what you had in mind? – Paul Panzer Apr 11 '17 at 18:56
  • Thanks so much. That's where I got as well. I wasn't sure if I was missing something. Regardless, it's a good solution that's nearly perfect for what I need. – Chris Apr 11 '17 at 19:35
  • Sorry to keep coming back. I've been getting some funny behavior where I get negative values. The value is always N-1. I'm assuming it happens with the return statement, but I haven't pinned it down yet. Any quick thoughts? – Chris Apr 11 '17 at 22:39
  • Just to be sure: you did copy the entire new code not just the new kernels? -- If so, did it work in the beginning? -- If so what exactly did you change since then? -- Also, what exactly is the funny behaviour? `N-1` is not negative, is it? Since you mention the return statement, with the modulo in there I really wouldn't expect negative outputs. Puzzling. Please try and give me better information; otherwise I can only guess. – Paul Panzer Apr 12 '17 at 00:03
  • Thanks! That was it. I didn't realize you changed more than the kernel. Everything works as expected now. I wish I could upvote you more! – Chris Apr 12 '17 at 00:41