Skip to content

Commit ae6abdc

Browse files
oteretSelforg Gardener
authored andcommitted
Add unique colors to plants, and shade to aging plants.
Add the option (recommended but not default) to have self-balancing environments where soil never takes over the who environment or disappears. Update two notebooks to show how to add self-balancing soils to the simulations. PiperOrigin-RevId: 584826124
1 parent 90f6a26 commit ae6abdc

File tree

5 files changed

+269
-10
lines changed

5 files changed

+269
-10
lines changed

self_organising_systems/biomakerca/env_logic.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,3 +1429,85 @@ def env_increase_age(env: Environment, etd: EnvTypeDef) -> Environment:
14291429
new_state_grid = env.state_grid.at[:,:, evm.AGE_IDX].set(
14301430
env.state_grid[:,:, evm.AGE_IDX] + is_aging_m)
14311431
return evm.update_env_state_grid(env, new_state_grid)
1432+
1433+
### Balancing the soil
1434+
1435+
def balance_soil(key: KeyType, env: Environment, config: EnvConfig):
1436+
"""Balance the earth/air proportion for each vertical slice.
1437+
1438+
If any vertical slice has a proportion of Earth to air past the
1439+
config.soil_unbalance_limit (a percentage of the maximum height), then we find
1440+
the boundary of (earth|immovable) and (air|sun), and replace one of them with:
1441+
1) air, for high altitudes, or 2) earth, for low altitudes.
1442+
This is approximated by detecting this boundary and checking its altitude.
1443+
That is, we do not actually count the amounts of earth and air cells.
1444+
1445+
upd_perc is used to make this effect gradual.
1446+
1447+
Note that if agent cells are at the boundary, nothing happens. So, if plants
1448+
actually survive past the boundary, as long as they don't die, soil is not
1449+
balanced.
1450+
"""
1451+
type_grid = env.type_grid
1452+
etd = config.etd
1453+
1454+
h = type_grid.shape[0]
1455+
unbalance_limit = config.soil_unbalance_limit
1456+
# This is currently hard coded. Feel free to change this if needed and
1457+
# consider sending a pull request.
1458+
upd_perc = 0.05
1459+
earth_min_r = jp.array(h * unbalance_limit, dtype=jp.int32)
1460+
air_max_r = jp.array(h * (1 -unbalance_limit), dtype=jp.int32)
1461+
1462+
# create a mask that checks: 'you are earth|immovable and above is air|sun'.
1463+
# the highest has index 0 (it is inverted).
1464+
# the index starts from 1 (not 0) since the first row can never be fine.
1465+
mask = (((type_grid[1:] == etd.types.EARTH) |
1466+
(type_grid[1:] == etd.types.IMMOVABLE)) &
1467+
((type_grid[:-1] == etd.types.AIR) |
1468+
(type_grid[:-1] == etd.types.SUN)))
1469+
# only get the highest value, if it exists.
1470+
# note that these indexes now would return the position of the 'high-end' of
1471+
# the seed. That is, the idx is where the top cell (leaf) would be, and idx+1
1472+
# would be where the bottom (root) cell would be.
1473+
best_idx_per_column = (mask * jp.arange(mask.shape[0]+1, 1, -1)[:, None]
1474+
).argmax(axis=0)
1475+
# We need to make sure that it is a valid position, otherwise a value of 0
1476+
# would be confused.
1477+
column_m = mask[best_idx_per_column, jp.arange(mask.shape[1])]
1478+
1479+
# if best idx is low, create air where earth would be.
1480+
target_for_air = (best_idx_per_column+1)
1481+
ku, key = jr.split(key)
1482+
make_air_mask = (column_m & (target_for_air < earth_min_r) &
1483+
(jr.uniform(ku, target_for_air.shape) < upd_perc))
1484+
1485+
column_idx = jp.arange(type_grid.shape[1])
1486+
type_grid = type_grid.at[target_for_air, column_idx].set(
1487+
etd.types.AIR * make_air_mask +
1488+
type_grid[target_for_air, column_idx] * (1 - make_air_mask))
1489+
env = evm.update_env_type_grid(env, type_grid)
1490+
# We need to reset the state too.
1491+
state_grid = env.state_grid
1492+
old_state_slice = state_grid[target_for_air, column_idx]
1493+
state_grid = state_grid.at[target_for_air, column_idx].set(
1494+
jp.zeros_like(old_state_slice) * make_air_mask[..., None] +
1495+
old_state_slice * (1 - make_air_mask[..., None]))
1496+
env = evm.update_env_state_grid(env, state_grid)
1497+
1498+
# if best idx is high, create earth where air would be.
1499+
ku, key = jr.split(key)
1500+
make_earth_mask = (column_m & (best_idx_per_column > air_max_r) &
1501+
(jr.uniform(ku, target_for_air.shape) < upd_perc))
1502+
type_grid = type_grid.at[best_idx_per_column, column_idx].set(
1503+
etd.types.EARTH * make_earth_mask +
1504+
type_grid[best_idx_per_column, column_idx] * (1 - make_earth_mask))
1505+
env = evm.update_env_type_grid(env, type_grid)
1506+
# We need to reset the state too.
1507+
old_state_slice = state_grid[best_idx_per_column, column_idx]
1508+
state_grid = state_grid.at[best_idx_per_column, column_idx].set(
1509+
jp.zeros_like(old_state_slice) * make_earth_mask[..., None] +
1510+
old_state_slice * (1 - make_earth_mask[..., None]))
1511+
env = evm.update_env_state_grid(env, state_grid)
1512+
1513+
return env

self_organising_systems/biomakerca/environments.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,15 @@ class EnvConfig:
423423
increasing number of materials until they would lose 100% of them at
424424
max_lifetime. You can essentially disable this feature by setting an
425425
enormous value for this attribute.
426+
soil_unbalance_limit: if this value is > 0, the environment will try to
427+
balance the earth/air proportion for each vertical slice as described in
428+
env_logic.balance_soil. This is useful for making sure that the
429+
environment doesn't degenerate into losing either all earth or air.
430+
Defaults to 0 to not break older experiments, but the recommended value is
431+
1/3.
432+
Note that we could add one more parameter here: the update probability.
433+
But since it is almost entirely irrelevant, we keep a default value of
434+
0.05. Let us know if there are reasons to vary this.
426435
"""
427436

428437
def __init__(self,
@@ -439,7 +448,8 @@ def __init__(self,
439448
n_reproduce_per_step=2,
440449
nutrient_cap=DEFAULT_NUTRIENT_CAP,
441450
material_nutrient_cap=DEFAULT_MATERIAL_NUTRIENT_CAP,
442-
max_lifetime=int(1e6)):
451+
max_lifetime=int(1e6),
452+
soil_unbalance_limit=0):
443453
self.agent_state_size = agent_state_size
444454
self.etd = etd
445455
self.env_state_size = 4 + self.agent_state_size
@@ -455,6 +465,7 @@ def __init__(self,
455465
self.nutrient_cap = nutrient_cap
456466
self.material_nutrient_cap = material_nutrient_cap
457467
self.max_lifetime = max_lifetime
468+
self.soil_unbalance_limit = soil_unbalance_limit
458469

459470
def __str__(self):
460471
return stringify_class(self)
@@ -758,15 +769,48 @@ def slice_environment_from_center(env, new_w):
758769

759770
### Visualization of environments
760771

761-
@partial(jit, static_argnames=["config"])
762-
def grab_image_from_env(env, config):
772+
def hue_to_rgb(p, q, t):
773+
t = jp.mod(t, 1.0)
774+
775+
# exclusive conditions
776+
t_lt_1d6 = (t < 1/6)
777+
done = t_lt_1d6
778+
t_lt_1d2 = jp.logical_and(jp.logical_not(done), (t < 1/2))
779+
done = jp.logical_or(done, t_lt_1d2)
780+
t_lt_2d3 = jp.logical_and(jp.logical_not(done), (t < 2/3))
781+
t_else = jp.logical_and(jp.logical_not(done), jp.logical_not(t_lt_2d3))
782+
return (t_lt_1d6 * (p+(q-p)*6*t) +
783+
t_lt_1d2 * q +
784+
t_lt_2d3 * (p + (q - p) * (2/3 - t) * 6) +
785+
t_else * p)
786+
787+
788+
def hsl_to_rgb(h,s,l):
789+
"""Return an array containing the converted RGB colors, in range [0,1].
790+
Assumes h,s,l are in the range [0,1].
791+
"""
792+
l_lt_05 = jp.array(l < 0.5).astype(jp.float32)
793+
q = l_lt_05 * l * (1 + s) + (1. - l_lt_05) * (l + s - l * s)
794+
print(q)
795+
p = 2 * l - q
796+
print(p)
797+
return jp.stack(
798+
[hue_to_rgb(p,q,h+1/3), hue_to_rgb(p,q,h), hue_to_rgb(p,q,h-1/3)], -1)
799+
800+
801+
@partial(jit, static_argnames=["config", "color_by_id"])
802+
def grab_image_from_env(env, config, color_by_id=True, id_color_intensity=0.15):
763803
"""Create a visualization of the environment.
764-
804+
765805
Resulting values are floats ranging from [0,1].
806+
807+
If color_by_id is True, we blend the agent cell colors with unique per id
808+
colors with a mix of id_color_intensity.
766809
"""
767810
etd = config.etd
768811
def map_cell(cell_type, state):
769812
env_c = etd.type_color_map[cell_type]
813+
770814
# EARTH and AIR colors degrade by how little nutrients they have.
771815
is_earth_f = (cell_type == etd.types.EARTH).astype(jp.float32)
772816
is_air_f = (cell_type == etd.types.AIR).astype(jp.float32)
@@ -777,4 +821,30 @@ def map_cell(cell_type, state):
777821
state[EN_ST+AIR_NUTRIENT_RPOS]/
778822
config.material_nutrient_cap[AIR_NUTRIENT_RPOS])*0.7)
779823
return env_c
780-
return vmap2(map_cell)(env.type_grid, env.state_grid)
824+
env_c_grid = vmap2(map_cell)(env.type_grid, env.state_grid)
825+
826+
if color_by_id:
827+
def add_id_colors(env_c, cell_type, agent_id):
828+
is_agent_f = etd.is_agent_fn(cell_type).astype(jp.float32)
829+
# Agents are slightly colored towards a random hue based on the agent id.
830+
# Just using two prime numbers for breakign cyclical coloring.
831+
agent_hue = jp.mod(agent_id * 41 / 137, 1.)
832+
agent_c = hsl_to_rgb(agent_hue, 0.5, 0.5)
833+
env_c = env_c * (1. - is_agent_f) + is_agent_f * (
834+
env_c * (1. - id_color_intensity) + agent_c * id_color_intensity)
835+
return env_c
836+
env_c_grid = vmap2(add_id_colors)(
837+
env_c_grid, env.type_grid, env.agent_id_grid)
838+
839+
# Then degrade colors by how old agents are.
840+
def decay_by_age(env_c, cell_type, state):
841+
# Agents colors degrade by how old they are.
842+
is_agent_f = etd.is_agent_fn(cell_type).astype(jp.float32)
843+
age_perc = state[AGE_IDX] / config.max_lifetime
844+
env_c = env_c * (1. - is_agent_f) + env_c * is_agent_f * (
845+
0.3 + (1 - age_perc) * 0.7)
846+
return env_c
847+
env_c_grid = vmap2(decay_by_age)(
848+
env_c_grid, env.type_grid, env.state_grid)
849+
850+
return env_c_grid

self_organising_systems/biomakerca/examples/notebooks/eruption_example.ipynb

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"\n",
1111
"This colab shows how to run experiments on the Eruption configuration. Eruption contains LAVA and FIRE as extra materials. See the code base for examples on how to create your own materials.\n",
1212
"\n",
13+
"Set soil_unbalance_limit to 1/3 for a more optimal set of experiments. The value of 0 reproduces experiments in the [original YouTube video](https://youtu.be/e8Gl0Ns4XiM).\n",
14+
"\n",
1315
"Copyright 2023 Google LLC\n",
1416
"\n",
1517
"Licensed under the Apache License, Version 2.0 (the \"License\");\n",
@@ -124,7 +126,10 @@
124126
},
125127
"outputs": [],
126128
"source": [
129+
"soil_unbalance_limit = 0 #@param [0, \"1/3\"] {type:\"raw\"}\n",
130+
"\n",
127131
"config = get_eruption_config()\n",
132+
"config.soil_unbalance_limit = soil_unbalance_limit\n",
128133
"etd = config.etd\n",
129134
"# Create the exclusive fs\n",
130135
"excl_fs = make_eruption_excl_fs(etd)\n",
@@ -410,18 +415,80 @@
410415
"agent_logic = BasicAgentLogic(config, minimal_net=agent_model==\"minimal\")\n",
411416
"\n",
412417
"mutator_type = \"randomly_adaptive\" #@param ['basic', 'randomly_adaptive']\n",
413-
"sd = 1e-3\n",
418+
"sd = 1e-3 #@param ['1e-3', '1e-4'] {type:\"raw\"}\n",
414419
"mutator = (BasicMutator(sd=sd, change_perc=0.2) if mutator_type == \"basic\"\n",
415420
" else RandomlyAdaptiveMutator(init_sd=sd, change_perc=0.2))"
416421
]
417422
},
423+
{
424+
"cell_type": "markdown",
425+
"metadata": {
426+
"id": "FpWCdi2GHeoS"
427+
},
428+
"source": [
429+
"### Testing init on a regular height (72)"
430+
]
431+
},
432+
{
433+
"cell_type": "code",
434+
"execution_count": null,
435+
"metadata": {
436+
"id": "JawG5qGMHeoe"
437+
},
438+
"outputs": [],
439+
"source": [
440+
"# first just initialize the environment to a certain state, without making videos.\n",
441+
"key = jr.PRNGKey(47)\n",
442+
"\n",
443+
"h = 72\n",
444+
"\n",
445+
"n_steps = 50000\n",
446+
"\n",
447+
"use_tiny_dna = False\n",
448+
"\n",
449+
"if use_tiny_dna:\n",
450+
" program = updated_tiny_dna\n",
451+
"else:\n",
452+
" ku, key = jr.split(key)\n",
453+
" program = agent_logic.initialize(ku)\n",
454+
"\n",
455+
"ku, key = jr.split(key)\n",
456+
"program = mutator.initialize(ku, program)\n",
457+
"\n",
458+
"# 128 is TOO SMALL!\n",
459+
"N_MAX_PROGRAMS = 256\n",
460+
"\n",
461+
"programs = jp.repeat(program[None, :], N_MAX_PROGRAMS, axis=0)\n",
462+
"\n",
463+
"env = create_eruption_env(h, config)\n",
464+
"ku, key = jr.split(key)\n",
465+
"programs, env = run_eruption_env(\n",
466+
" ku, config, programs, env, agent_logic, mutator, n_steps, zoom_sz=6)\n"
467+
]
468+
},
469+
{
470+
"cell_type": "code",
471+
"execution_count": null,
472+
"metadata": {
473+
"id": "jq-haImhJf0l"
474+
},
475+
"outputs": [],
476+
"source": [
477+
"# continue...\n",
478+
"n_steps = 50000\n",
479+
"ku, key = jr.split(key)\n",
480+
"programs, env = run_eruption_env(\n",
481+
" ku, config, programs, env, agent_logic, mutator, n_steps, zoom_sz=6,\n",
482+
" steps_per_frame=64, when_to_double_speed=[])"
483+
]
484+
},
418485
{
419486
"cell_type": "markdown",
420487
"metadata": {
421488
"id": "WiFLI7fAPNWF"
422489
},
423490
"source": [
424-
"### Testing on a small height (36)"
491+
"### Testing tiny_dna on a small height (36)"
425492
]
426493
},
427494
{
@@ -462,13 +529,27 @@
462529
" ku, config, programs, env, agent_logic, mutator, n_steps, zoom_sz=12)\n"
463530
]
464531
},
532+
{
533+
"cell_type": "code",
534+
"execution_count": null,
535+
"metadata": {
536+
"id": "0v-T-EzGF2wh"
537+
},
538+
"outputs": [],
539+
"source": [
540+
"# If the video is too big, and trying to run it crashes the runtime, perform a\n",
541+
"# manual download.\n",
542+
"from colabtools import fileedit\n",
543+
"fileedit.download_file('video.mp4', ephemeral=True)"
544+
]
545+
},
465546
{
466547
"cell_type": "markdown",
467548
"metadata": {
468549
"id": "4mtJeIeUPR2z"
469550
},
470551
"source": [
471-
"### Testing on a regular height (72)"
552+
"### Testing tiny_dna on a regular height (72)"
472553
]
473554
},
474555
{
@@ -485,7 +566,7 @@
485566
"\n",
486567
"h = 72\n",
487568
"\n",
488-
"n_steps = 100000\n",
569+
"n_steps = 50000\n",
489570
"\n",
490571
"use_tiny_dna = True\n",
491572
"\n",
@@ -509,6 +590,22 @@
509590
" ku, config, programs, env, agent_logic, mutator, n_steps, zoom_sz=6)\n"
510591
]
511592
},
593+
{
594+
"cell_type": "code",
595+
"execution_count": null,
596+
"metadata": {
597+
"id": "nfUj1iWwNmIE"
598+
},
599+
"outputs": [],
600+
"source": [
601+
"# continue...\n",
602+
"n_steps = 50000\n",
603+
"ku, key = jr.split(key)\n",
604+
"programs, env = run_eruption_env(\n",
605+
" ku, config, programs, env, agent_logic, mutator, n_steps, zoom_sz=6,\n",
606+
" steps_per_frame=64, when_to_double_speed=[])"
607+
]
608+
},
512609
{
513610
"cell_type": "markdown",
514611
"metadata": {

self_organising_systems/biomakerca/examples/notebooks/run_configuration.ipynb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@
109109
"id": "aRRMQ1aNhqv6"
110110
},
111111
"source": [
112-
"## Select the configuration, the agent logic and the mutator"
112+
"## Select the configuration, the agent logic and the mutator\n",
113+
"\n",
114+
"Set soil_unbalance_limit to 0 to reproduce the original environment. Set it to 1/3 for having self-balancing environments (recommended)."
113115
]
114116
},
115117
{
@@ -122,8 +124,11 @@
122124
"source": [
123125
"ec_id = \"pestilence\" #@param ['persistence', 'pestilence', 'collaboration', 'sideways']\n",
124126
"env_width_type = \"landscape\" #@param ['wide', 'landscape', 'square', 'petri']\n",
127+
"soil_unbalance_limit = 0 #@param [0, \"1/3\"] {type:\"raw\"}\n",
128+
"\n",
125129
"env_and_config = evm.get_env_and_config(ec_id, width_type=env_width_type)\n",
126130
"st_env, config = env_and_config\n",
131+
"config.soil_unbalance_limit = soil_unbalance_limit\n",
127132
"\n",
128133
"agent_model = \"extended\" #@param ['minimal', 'extended']\n",
129134
"agent_logic = BasicAgentLogic(config, minimal_net=agent_model==\"minimal\")\n",

0 commit comments

Comments
 (0)