Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/cfg_500_000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"PLATE_WIDTH": 1500,
"PLATE_HEIGHT": 1000,
"CATS_N": 500000,
"CAT_RADIUS": 0.4,
"MOVE_RADIUS": 0.6,
"RADIUS_0": 0.6,
"RADIUS_1": 0.9,
"MOVE_PATTERN_ID": "MOVE_PATTERN_PHIS_ID",
"DISTANCE": "EUCLIDEAN_DISTANCE",
"BORDER_INTERACTION": true,
"FAV_CATS_AMOUNT": 1,
"FAV_CATS_OBSERVING": true
}
11 changes: 11 additions & 0 deletions examples/cfg_beautiful.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"PLATE_WIDTH": 1500,
"PLATE_HEIGHT": 1000,
"CATS_N": 60,
"CAT_RADIUS": 20,
"MOVE_RADIUS": 40,
"RADIUS_0": 40,
"RADIUS_1": 120,
"MOVE_PATTERN_ID": "MOVE_PATTERN_PHIS_ID",
"DISTANCE": "EUCLIDEAN_DISTANCE"
}
12 changes: 12 additions & 0 deletions examples/cfg_border_magic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"PLATE_WIDTH": 1500,
"PLATE_HEIGHT": 1000,
"CATS_N": 10,
"CAT_RADIUS": 50,
"MOVE_RADIUS": 100,
"RADIUS_0": 100,
"RADIUS_1": 300,
"MOVE_PATTERN_ID": "MOVE_PATTERN_PHIS_ID",
"DISTANCE": "EUCLIDEAN_DISTANCE",
"FAV_CATS_OBSERVING": false
}
121 changes: 81 additions & 40 deletions src/catsim/__main__.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
import argparse
from pathlib import Path

import taichi as ti
import taichi.math as tm

import catsim.config as cfg
from catsim.config import Config
from catsim.cat import Cat, init_cat_env
from catsim.enums import (
ALWAYS_VISIBLE,
VISIBLE,
)
from catsim.enums import ALWAYS_VISIBLE, VISIBLE, COLORS, COLORS_IGN, COLORS_FAV
from catsim.grid import setup_grid, update_statuses
from catsim.spawner import Spawner

POINTS = tm.vec2.field(shape=(cfg.CATS_N,))
COLORS = ti.field(ti.i32, shape=(cfg.CATS_N,))
RADIUSES = ti.field(ti.f32, shape=(cfg.CATS_N,))
POINTS: tm.vec2.field
CAT_COLORS: ti.field
RADII: ti.field

LINES1_BEGIN = tm.vec2.field(shape=(cfg.CATS_N,))
LINES1_END = tm.vec2.field(shape=(cfg.CATS_N,))
LINES1_BEGIN: tm.vec2.field
LINES1_END: tm.vec2.field

LINES2_BEGIN = tm.vec2.field(shape=(cfg.CATS_N,))
LINES2_END = tm.vec2.field(shape=(cfg.CATS_N,))
LINES2_BEGIN: tm.vec2.field
LINES2_END: tm.vec2.field

LINE_LENGTH = tm.vec2([cfg.RADIUS_1 / cfg.PLATE_WIDTH, cfg.RADIUS_1 / cfg.PLATE_HEIGHT])
ANGLE_SHIFT = tm.vec2(
[
tm.asin(cfg.CAT_RADIUS / cfg.PLATE_WIDTH / LINE_LENGTH[0] * 1.5),
tm.asin(cfg.CAT_RADIUS / cfg.PLATE_HEIGHT / LINE_LENGTH[1] * 1.5),
]
)
LINE_LENGTH: tm.vec2
ANGLE_SHIFT: tm.vec2


@ti.kernel
def arrange_visuals(cats: ti.template()) -> tuple[ti.i32, ti.i32]:
def arrange_visuals(
cats: ti.template(),
fav_cats_amount: ti.i8,
fav_cats_observing: bool,
cats_n: ti.i32,
) -> tuple[ti.i32, ti.i32]:
render_idx: ti.i32 = 0

for cat_idx in range(cfg.FAV_CATS_AMOUNT, cfg.CATS_N):
for cat_idx in range(fav_cats_amount, cats_n):
# skip favorite cats for now to render them later
# so they appear "above" others

cat = cats[cat_idx]
if cat.visibility_status == VISIBLE:
POINTS[render_idx] = cat.norm_point
RADIUSES[render_idx] = cat.radius
COLORS[render_idx] = (
cfg.COLORS[cat.status]
if cat.observed or (not cfg.FAV_CATS_OBSERVING)
else cfg.COLORS_IGN[cat.status]
RADII[render_idx] = cat.radius
CAT_COLORS[render_idx] = (
COLORS[cat.status]
if cat.observed or (not fav_cats_observing)
else COLORS_IGN[cat.status]
)

ti.atomic_add(render_idx, 1)

line_idx: ti.i32 = 0
for cat_idx in range(cfg.FAV_CATS_AMOUNT):
for cat_idx in range(fav_cats_amount):
cat = cats[cat_idx]
assert cat.visibility_status == ALWAYS_VISIBLE

POINTS[render_idx] = cat.norm_point
RADIUSES[render_idx] = cat.radius
COLORS[render_idx] = cfg.COLORS_FAV[cat.status]
RADII[render_idx] = cat.radius
CAT_COLORS[render_idx] = COLORS_FAV[cat.status]

ti.atomic_add(render_idx, 1)

if cfg.FAV_CATS_OBSERVING:
if fav_cats_observing:
LINES1_BEGIN[line_idx] = cat.norm_point
LINES2_BEGIN[line_idx] = cat.norm_point

Expand All @@ -84,17 +84,19 @@ def arrange_visuals(cats: ti.template()) -> tuple[ti.i32, ti.i32]:


@ti.kernel
def move_cats(cats: ti.template()):
for idx in range(cfg.CATS_N):
def move_cats(cats: ti.template(), cats_n: ti.i32):
for idx in range(cats_n):
cats[idx].move()


def mainloop(cats: ti.template(), gui: ti.GUI):
def mainloop(cfg: Config, cats: ti.template(), gui: ti.GUI):
while gui.running:
move_cats(cats)
move_cats(cats, cfg.CATS_N)
update_statuses(cats)

count_cats, count_lines = arrange_visuals(cats)
count_cats, count_lines = arrange_visuals(
cats, cfg.FAV_CATS_AMOUNT, cfg.FAV_CATS_OBSERVING, cfg.CATS_N
)

if count_lines != 0:
gui.lines(
Expand All @@ -111,14 +113,14 @@ def mainloop(cats: ti.template(), gui: ti.GUI):
if count_cats != 0:
gui.circles(
pos=POINTS.to_numpy()[:count_cats],
radius=RADIUSES.to_numpy()[:count_cats],
color=COLORS.to_numpy()[:count_cats],
radius=RADII.to_numpy()[:count_cats],
color=CAT_COLORS.to_numpy()[:count_cats],
)

gui.show()


def validate_config():
def validate_config(cfg):
if cfg.PLATE_HEIGHT <= 0 or cfg.PLATE_WIDTH <= 0:
raise ValueError("Plate height/width must be > 0")

Expand All @@ -143,8 +145,43 @@ def validate_config():
raise ValueError("Radius 1 must be > Radius 0")


def init_env(cfg: Config):
global POINTS, CAT_COLORS, RADII
POINTS = tm.vec2.field(shape=(cfg.CATS_N,))
CAT_COLORS = ti.field(ti.i32, shape=(cfg.CATS_N,))
RADII = ti.field(ti.f32, shape=(cfg.CATS_N,))

global LINES1_BEGIN, LINES1_END, LINES2_BEGIN, LINES2_END
LINES1_BEGIN = tm.vec2.field(shape=(cfg.CATS_N,))
LINES1_END = tm.vec2.field(shape=(cfg.CATS_N,))

LINES2_BEGIN = tm.vec2.field(shape=(cfg.CATS_N,))
LINES2_END = tm.vec2.field(shape=(cfg.CATS_N,))

global LINE_LENGTH, ANGLE_SHIFT
LINE_LENGTH = tm.vec2(
[cfg.RADIUS_1 / cfg.PLATE_WIDTH, cfg.RADIUS_1 / cfg.PLATE_HEIGHT]
)
ANGLE_SHIFT = tm.vec2(
[
tm.asin(cfg.CAT_RADIUS / cfg.PLATE_WIDTH / LINE_LENGTH[0] * 1.5),
tm.asin(cfg.CAT_RADIUS / cfg.PLATE_HEIGHT / LINE_LENGTH[1] * 1.5),
]
)


def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("config_file", type=str)
return parser.parse_args()


def main():
validate_config()
args = parse_arguments()
cfg = Config.generate_from_json(Path(args.config_file))
validate_config(cfg)

init_env(cfg)

init_cat_env(
move_radius=cfg.MOVE_RADIUS,
Expand All @@ -156,13 +193,17 @@ def main():
prob_inter=cfg.PROB_INTERACTION,
border_inter=cfg.BORDER_INTERACTION,
distance_type=cfg.DISTANCE,
fav_cats_observing=cfg.FAV_CATS_OBSERVING,
observable_angle_span=cfg.OBSERVABLE_ANGLE_SPAN,
)

setup_grid(
cat_n=cfg.CATS_N,
r1=cfg.RADIUS_1,
width=cfg.PLATE_WIDTH,
height=cfg.PLATE_HEIGHT,
fav_cats_amount=cfg.FAV_CATS_AMOUNT,
fav_cats_log=cfg.FAV_CATS_LOGGING,
)

cats = Cat.field(shape=(cfg.CATS_N,))
Expand All @@ -181,7 +222,7 @@ def main():
for idx in range(cfg.FAV_CATS_AMOUNT):
spawner.set_always_visible_cat(idx, 0)

mainloop(cats, gui)
mainloop(cfg, cats, gui)


if __name__ == "__main__":
Expand Down
36 changes: 25 additions & 11 deletions src/catsim/cat.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import taichi as ti
import taichi.math as tm

from catsim.config import FAV_CATS_OBSERVING, OBSERVABLE_ANGLE_SPAN
from catsim.enums import (
ALWAYS_VISIBLE,
INTERACTION_LEVEL_0,
Expand Down Expand Up @@ -35,6 +34,9 @@
_PROB_INTER: bool
_BORDER_INTER: bool

_FAV_CATS_OBSERVING: bool
_OBSERVABLE_ANGLE_SPAN: float


def init_cat_env(
move_radius: ti.f32,
Expand All @@ -46,6 +48,8 @@ def init_cat_env(
prob_inter: bool,
distance_type: ti.i32,
border_inter: bool,
fav_cats_observing: bool,
observable_angle_span: float,
):
global \
_RADIUS_0, \
Expand All @@ -57,7 +61,9 @@ def init_cat_env(
_PROB_INTER, \
_BORDER_INTER, \
_DISTANCE_TYPE, \
_MAX_DISTANCE
_MAX_DISTANCE, \
_FAV_CATS_OBSERVING, \
_OBSERVABLE_ANGLE_SPAN

_MOVE_RADIUS = move_radius
_RADIUS_0 = r0
Expand All @@ -68,6 +74,8 @@ def init_cat_env(
_PROB_INTER = prob_inter
_BORDER_INTER = border_inter
_DISTANCE_TYPE = distance_type
_FAV_CATS_OBSERVING = fav_cats_observing
_OBSERVABLE_ANGLE_SPAN = observable_angle_span

p0 = tm.vec2([0, 0])
p1 = tm.vec2([0, _PLATE_WIDTH])
Expand Down Expand Up @@ -204,14 +212,16 @@ def move(self):
direction_angle = tm.atan2(
self.point[1] - self.prev_point[1], self.point[0] - self.prev_point[0]
)
self.observable_angle[0] = direction_angle - OBSERVABLE_ANGLE_SPAN
self.observable_angle[1] = direction_angle + OBSERVABLE_ANGLE_SPAN
self.observable_angle[0] = direction_angle - _OBSERVABLE_ANGLE_SPAN
self.observable_angle[1] = direction_angle + _OBSERVABLE_ANGLE_SPAN

if _BORDER_INTER:
self.update_visibility_status()

@ti.func
def fight_with(self, other_cat: ti.template()):
def fight_with(self, other_cat: ti.template()) -> ti.i32:
_st = INTERACTION_NO

if (
other_cat.visibility_status == VISIBLE
or other_cat.visibility_status == ALWAYS_VISIBLE
Expand All @@ -220,10 +230,12 @@ def fight_with(self, other_cat: ti.template()):

if dist > _RADIUS_1 or (_PROB_INTER and ti.random() >= 1.0 / (dist * dist)):
self.status = ti.max(self.status, INTERACTION_NO)
_st = INTERACTION_NO
pass

else:
observing = True
if self.visibility_status == ALWAYS_VISIBLE and FAV_CATS_OBSERVING:
if self.visibility_status == ALWAYS_VISIBLE and _FAV_CATS_OBSERVING:
relative_angle = tm.atan2(
other_cat.point[1] - self.point[1],
other_cat.point[0] - self.point[0],
Expand All @@ -238,8 +250,10 @@ def fight_with(self, other_cat: ti.template()):
observing = False

if observing:
self.status = (
INTERACTION_LEVEL_0
if dist <= _RADIUS_0
else ti.max(self.status, INTERACTION_LEVEL_1)
)
if dist <= _RADIUS_0:
self.status = INTERACTION_LEVEL_0
_st = INTERACTION_LEVEL_0
else:
self.status = ti.max(self.status, INTERACTION_LEVEL_1)
_st = INTERACTION_LEVEL_1
return _st
13 changes: 13 additions & 0 deletions src/catsim/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"PLATE_WIDTH": 1500,
"PLATE_HEIGHT": 1000,
"CATS_N": 100,
"CAT_RADIUS": 20,
"MOVE_RADIUS": 40,
"RADIUS_0": 40,
"RADIUS_1": 120,
"MOVE_PATTERN_ID": "MOVE_PATTERN_PHIS",
"DISTANCE": "EUCLIDEAN_DISTANCE",
"FAV_CATS_AMOUNT": 1,
"FAV_CATS_OBSERVING": true
}
Loading
Loading