gabse 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gabse-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carl Toller Melén
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include README.md
2
+ include LICENSE
3
+ exclude examples/*
4
+ exclude .idea/*
5
+ exclude .venv/*
6
+ exclude dist/*
gabse-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: gabse
3
+ Version: 0.1.1
4
+ Summary: The GABSE (Generic Agent-Based Simulation for Engineering) framework
5
+ Author: Carl Toller Melén
6
+ Author-email: Carl Toller Melén <carl@tollermelen.se>
7
+ Maintainer-email: Carl Toller Melén <carl@tollermelen.se>
8
+ License-Expression: MIT
9
+ Project-URL: Homepage, https://github.com/tm-carl/GABSE
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=2.3.4
14
+ Requires-Dist: scipy>=1.16.3
15
+ Requires-Dist: sortedcontainers>=2.4.0
16
+ Dynamic: license-file
17
+
18
+ # GABSE
19
+ This is the GABSE (Generic Agent-Based Simulation for Engineering) framework. It provides classes and methods to create and manage simulations involving agents, their actions, sensors, and the simulation context. It is based on agent-based modeling technique and is developed with the intention of being lightweight, scalable, and flexible. This package provides the engine, action scheduling, generic agent functionality, and sensory data collection and management tools.
20
+
21
+ EXAMPLES:
22
+ - Zombie apocalypse (both 2D and 3D)
23
+
24
+ REQUIRED packages:
25
+ - sortedcontainers
26
+ - numpy
27
+ - copy
gabse-0.1.1/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # GABSE
2
+ This is the GABSE (Generic Agent-Based Simulation for Engineering) framework. It provides classes and methods to create and manage simulations involving agents, their actions, sensors, and the simulation context. It is based on agent-based modeling technique and is developed with the intention of being lightweight, scalable, and flexible. This package provides the engine, action scheduling, generic agent functionality, and sensory data collection and management tools.
3
+
4
+ EXAMPLES:
5
+ - Zombie apocalypse (both 2D and 3D)
6
+
7
+ REQUIRED packages:
8
+ - sortedcontainers
9
+ - numpy
10
+ - copy
@@ -0,0 +1,29 @@
1
+ """
2
+ Copyright (C), 2025, Carl Toller Melén
3
+
4
+ This is the GABSE (Generic Agent-Based Simulation for Engineering) framework.
5
+
6
+ """
7
+
8
+ # version number
9
+ __name__ = "gabse"
10
+ __author__ = "Carl Toller Melén"
11
+ __version__ = "0.1.1"
12
+
13
+ from gabse import engine
14
+ from gabse.agent import Agent
15
+ from gabse.data import Sensor, DataCollector
16
+ from gabse.schedule import Action, Schedule
17
+ from gabse.engine import Engine
18
+
19
+ from gabse.context import Context
20
+
21
+ __all__ = [
22
+ "Engine",
23
+ "Action",
24
+ "Schedule",
25
+ "Agent",
26
+ "Sensor",
27
+ "Context",
28
+ "DataCollector",
29
+ ]
@@ -0,0 +1,131 @@
1
+ """
2
+ @author: Carl Toller Melén
3
+
4
+ """
5
+
6
+ # %%
7
+ # Import required packages
8
+ import numpy as np
9
+ from scipy.spatial import cKDTree as _cKDTree
10
+
11
+ # %%
12
+ # Agent class for representing entities in the simulation
13
+
14
+ class Agent:
15
+ # Static variable to keep track of agent IDs
16
+ _id_counter = 0
17
+
18
+ # Initialize agent with unique ID, position, engine reference, and empty sensor
19
+ def __init__(self, engine, position=None):
20
+ Agent._id_counter += 1
21
+ self.id = Agent._id_counter
22
+ if position is None:
23
+ position = [0, 0, 0]
24
+ self.engine = engine
25
+ self.position = np.array(position)
26
+ self.neighbours = list()
27
+ self.sensor = ""
28
+
29
+ # Find nearest neighbours based on Euclidean distance
30
+ def find_neighbours(self, agents, noOfNeighbours):
31
+ """
32
+ agents: iterable of agents to consider (can exclude self prior to call)
33
+ returns: list of nearest agents, or single agent if noOfNeighbours == 1
34
+ """
35
+ if not agents:
36
+ return [] if noOfNeighbours != 1 else None
37
+
38
+ n = len(agents)
39
+ k = min(noOfNeighbours, n)
40
+
41
+ # Try KDTree for large n or repeated queries
42
+ try:
43
+ pos = np.vstack([a.get_position() for a in agents])
44
+ tree = _cKDTree(pos)
45
+ dists, idxs = tree.query(self.get_position(), k=k)
46
+ if k == 1:
47
+ return agents[int(idxs)]
48
+ if np.isscalar(idxs):
49
+ idxs = [int(idxs)]
50
+ else:
51
+ idxs = [int(i) for i in np.atleast_1d(idxs)]
52
+ return [agents[i] for i in idxs]
53
+ except Exception:
54
+ # Get self position
55
+ self_pos = self.get_position()
56
+
57
+ # stack positions (shape: (n, dim)) and compute squared distances
58
+ pos = self.engine.context.get_positions_array()
59
+
60
+ if pos.size == 0:
61
+ return [] if noOfNeighbours != 1 else None
62
+
63
+ # compute squared Euclidean distances
64
+ d2 = np.sum((pos - self_pos) ** 2, axis=1)
65
+
66
+ # Return based if only one neighbours requested
67
+ if k == 1:
68
+ return agents[int(np.argmin(d2))]
69
+ if k < n:
70
+ idx_k = np.argpartition(d2, k - 1)[:k]
71
+ idx_sorted = idx_k[np.argsort(d2[idx_k])]
72
+ else:
73
+ idx_sorted = np.argsort(d2)
74
+
75
+ return [agents[i] for i in idx_sorted[:k]]
76
+
77
+
78
+ # Check if agent is out of bounds and change the position so that it is within bounds
79
+ def check_out_of_bounds(self):
80
+ bounds = np.array(self.engine.context.get_dimensions())
81
+
82
+ minValues = bounds[0:3]
83
+ maxValues = bounds[3:]
84
+
85
+ return np.clip(self.position, minValues, maxValues)
86
+
87
+ #Move agent to a specific position
88
+ def move_position(self, position):
89
+ self.position = position
90
+ self.position = self.check_out_of_bounds()
91
+ # print(self.position)
92
+
93
+ try:
94
+ self.engine.context.mark_dirty()
95
+ except Exception:
96
+ pass
97
+
98
+ def move_vector(self, vector):
99
+ self.position += vector
100
+ self.position = self.check_out_of_bounds()
101
+ # print(self.position)
102
+
103
+ try:
104
+ self.engine.context.mark_dirty()
105
+ except Exception:
106
+ pass
107
+
108
+ # Calculate Euclidean distance between two agents
109
+ @staticmethod
110
+ def get_distance(p1, p2):
111
+ return np.linalg.norm(p1.get_position() - p2.get_position())
112
+
113
+
114
+ def find_shortest_path(self, network):
115
+ pass
116
+
117
+ # Add sensor to agent
118
+ def add_sensor(self, sensor):
119
+ self.sensor = sensor
120
+
121
+ # Getters and Setters
122
+ def get_sensor(self):
123
+ return self.sensor
124
+
125
+ def get_position(self):
126
+ return self.position
127
+
128
+ def set_position(self, position):
129
+ self.position = position
130
+
131
+
@@ -0,0 +1,86 @@
1
+ """
2
+ @author: Carl Toller Melén
3
+
4
+ """
5
+ # %%
6
+ # Import required packages
7
+ import numpy as np
8
+
9
+ # %%
10
+ # Context class for managing agents within a defined space
11
+
12
+ class Context:
13
+ # Initializes the context with dimensions and empty agent list
14
+ def __init__(self, dimensions):
15
+ self._positions_cache = None
16
+ self.dimensions = np.array(dimensions)
17
+ self.agents = list()
18
+ self._dirty = True
19
+
20
+ # Adds a new agent to the list
21
+ def add_agent(self, agent):
22
+ self.agents.append(agent)
23
+ self._dirty = True
24
+
25
+ # Removes an agent from the list
26
+ def remove_agent(self, agent):
27
+ # Finds the right agent in the list and removes it
28
+ self.agents.remove(agent)
29
+ self._dirty = True
30
+
31
+ def mark_dirty(self):
32
+ self._dirty = True
33
+
34
+ # Checks if an object is of a specific class name
35
+ @staticmethod
36
+ def check_class(obj, name):
37
+ if obj.__class__.__name__ == name:
38
+ return True
39
+ else:
40
+ return False
41
+
42
+ # Getters
43
+ def get_agents(self):
44
+ return self.agents
45
+
46
+ def get_agents_by_class(self, class_name):
47
+ return [agent for agent in self.agents if self.check_class(agent, class_name)]
48
+
49
+ def get_dimensions(self):
50
+ return self.dimensions
51
+
52
+ def get_positions_array(self):
53
+ """
54
+ Returns a (n, dim) numpy array of agent positions.
55
+ Cached until an agent calls context.mark_dirty().
56
+ """
57
+ if self._dirty:
58
+ if not self.agents:
59
+ self._positions_cache = np.empty((0, self.dimensions.size // 2))
60
+ else:
61
+ # list comprehension into vstack once per rebuild
62
+ self._positions_cache = np.vstack([a.get_position() for a in self.agents])
63
+ self._dirty = False
64
+ return self._positions_cache
65
+
66
+ def get_agent_count(self, classes=None):
67
+ entry = dict()
68
+
69
+ # if no classes provided, return total count for each agent type
70
+ if not classes:
71
+ unique_classes = set(obj.__class__.__name__ for obj in self.agents)
72
+ for cls in unique_classes:
73
+ a = sum(self.check_class(obj, cls) for obj in self.agents)
74
+ entry[cls] = a
75
+ return entry
76
+
77
+
78
+ for arg in classes:
79
+ # print(arg)
80
+ a = sum(self.check_class(obj, arg) for obj in self.agents)
81
+ # print(a)
82
+ entry[arg] = a
83
+
84
+ #print(entry)
85
+ return entry
86
+
@@ -0,0 +1,72 @@
1
+ """
2
+ @author: Carl Toller Melén
3
+
4
+ """
5
+
6
+ # %%
7
+ # Import required packages
8
+ import numpy as np
9
+ import copy
10
+
11
+ # %%
12
+ # Sensor class for logging agent data over time
13
+
14
+ class Sensor:
15
+ # Initializes the sensor with engine reference, parent agent, empty logger, and frequency
16
+ def __init__(self, engine, parent, frequency):
17
+ self.engine = engine
18
+ self.parent = parent
19
+ self.logger = list()
20
+ self.frequency = frequency
21
+
22
+ # Logs data entries based on specified getters
23
+ def entry(self, *getters):
24
+ entry = {"tick": self.engine.get_tick()}
25
+
26
+ for arg in getters:
27
+ # print(g)
28
+ method = getattr(self.parent, "get_" + arg)
29
+ if not callable(method):
30
+ continue
31
+
32
+ data = method()
33
+ #check if data is numpy array and convert to list
34
+ if isinstance(data, np.ndarray):
35
+ data = data.tolist() # to avoid reference issues with mutable data types
36
+ else:
37
+ try:
38
+ data = copy.copy(data) # to avoid reference issues with mutable data types
39
+ except Exception:
40
+ pass
41
+
42
+ entry[arg] = data
43
+
44
+ self.logger.append(entry)
45
+ # print(self.engine.getTick())
46
+
47
+ # Getters
48
+ def get_frequency(self):
49
+ return self.frequency
50
+
51
+ def get_logger(self):
52
+ return self.logger
53
+
54
+
55
+ # %%
56
+ # Data Collector class for gathering and exporting data from agents' and context sensors
57
+
58
+ class DataCollector:
59
+ def __init__(self, engine):
60
+ self.engine = engine
61
+ self.repo = dict()
62
+
63
+ # Collects data from all agents' sensors and stores it in the repository
64
+ def collect_data(self):
65
+ for agt in self.engine.context.get_agents():
66
+ self.repo[f"{agt.__class__.__name__} {agt.id}"] = agt.get_sensor().get_logger()
67
+
68
+ # print(self.repo)
69
+
70
+ # Exports the collected data repository
71
+ def export_data(self):
72
+ return self.repo
@@ -0,0 +1,41 @@
1
+ """
2
+ @author: Carl Toller Melén
3
+
4
+ """
5
+
6
+ # %%
7
+ # Import required packages
8
+ from gabse.schedule import Schedule
9
+ from gabse.context import Context
10
+
11
+ # %%
12
+ # Engine class for managing the simulation
13
+
14
+ class Engine:
15
+ def __init__(self, modelTime, dimensions, context=None):
16
+ self.tick = 0.0
17
+ self.modelTime = modelTime
18
+ self.schedule = Schedule(self.tick)
19
+
20
+ # Initialize context, allowing for custom context to be passed
21
+ if context is None:
22
+ self.context = Context(dimensions)
23
+ else:
24
+ self.context = context
25
+
26
+ def run(self):
27
+ while self.tick <= self.modelTime and self.schedule.get_size() > 0:
28
+ self.tick = self.schedule.step()
29
+ # print(self.tick)
30
+
31
+ # print("RUN COMPLETED!")
32
+
33
+ def abort(self):
34
+ self.schedule.clear_schedule()
35
+ print(f"Stopped at: {self.tick}")
36
+
37
+ def get_tick(self):
38
+ return self.tick
39
+
40
+ def get_context(self):
41
+ return self.context
@@ -0,0 +1,131 @@
1
+ """
2
+ @author: Carl Toller Melén
3
+
4
+ """
5
+
6
+ # %%
7
+ # Import required packages
8
+ from sortedcontainers import SortedList
9
+
10
+ # %%
11
+ # Action class for representing scheduled actions
12
+
13
+ class Action:
14
+ """
15
+ tick: float
16
+ The simulation tick at which the action is scheduled to occur.
17
+ agent: Agent
18
+ The agent that will perform the action.
19
+ method: str
20
+ The name of the method to be called on the agent.
21
+ args: list, optional
22
+ The arguments to be passed to the method. Can be None, empty list, or "" if no arguments are needed.
23
+ priority: int, optional
24
+ The priority of the action (lower values indicate higher priority). Default is 0.
25
+ interval: float, optional
26
+ The interval for recurring actions. If greater than 0, the action will be rescheduled
27
+ after execution. Default is 0.
28
+ """
29
+ def __init__(self, tick, agent, method, args=None, priority=0, interval=0):
30
+ self.tick = float(tick)
31
+ self.agent = agent
32
+ self.method = method
33
+ self.args = args
34
+ self.priority = int(priority)
35
+ self.interval = float(interval)
36
+
37
+ def __str__(self):
38
+ return f"Action entry:\ntick: {self.tick}, agent: {self.agent}, method: {self.method}, arguments: {self.args}, priority: {self.priority}, interval: {self.interval}"
39
+
40
+
41
+ # %%
42
+ # Schedule class for managing and executing scheduled actions
43
+ class Schedule:
44
+
45
+ # Creates an empty schedule (list) and tick timer, set to zero
46
+ # List is sorted based on tick value of actions and priority
47
+ def __init__(self, tick):
48
+ self.schedule = SortedList(key=lambda a: (a.tick, a.priority))
49
+ self.tick = tick
50
+
51
+ # Schedule method for adding an action in schedule
52
+ def schedule_action(self, action: Action):
53
+ self.schedule.add(action)
54
+
55
+ # Method for stepping forward in simulation
56
+ def step(self):
57
+ # If schedule is empty, return current tick
58
+ if not self.schedule:
59
+ return self.tick
60
+
61
+ # Checks if previous actions exist and, if so, removes them
62
+ while self.schedule[0].tick < self.tick:
63
+ self.schedule.pop(0)
64
+
65
+ # If schedule is empty after removing past actions, return current tick
66
+ if not self.schedule:
67
+ return self.tick
68
+
69
+ # Load the first action in schedule
70
+ action = self.schedule[0]
71
+
72
+ # Step to next action tick
73
+ self.tick = action.tick
74
+
75
+ # Calls action agent method
76
+ method = getattr(action.agent, action.method)
77
+
78
+ # print(f"{action.agent} at {self.tick}")
79
+ # print(action.method)
80
+ # print(args)
81
+
82
+ # Check and call
83
+ if callable(method):
84
+ if action.args is None or len(action.args) == 0 or action.args == "":
85
+ method()
86
+ else:
87
+ method(*action.args)
88
+ # print(result) # Output: 1, 2, 3
89
+ else:
90
+ print("Method not found or not callable.")
91
+
92
+ # Checks if the action is recurring and, if so, schedules next instance
93
+ if action.interval > 0.0:
94
+ nextAction = Action(
95
+ tick=action.tick + action.interval,
96
+ agent=action.agent,
97
+ method=action.method,
98
+ args=action.args,
99
+ interval=action.interval
100
+ )
101
+ self.schedule_action(nextAction)
102
+
103
+ # Remove the executed action from the schedule
104
+ self.schedule.pop(0)
105
+
106
+ #Return current tick
107
+ return self.tick
108
+
109
+
110
+ # Filter out all actions related to the target agent
111
+ def remove_agent_from_list(self, target):
112
+ self.schedule = SortedList(
113
+ [action for action in self.schedule if action.agent != target],
114
+ key=lambda action: (action.tick, action.priority)
115
+ )
116
+
117
+ def get_schedule(self):
118
+ return self.schedule
119
+
120
+ def print_schedule(self):
121
+ for action in self.schedule:
122
+ print(action)
123
+
124
+ def get_tick(self):
125
+ return self.tick
126
+
127
+ def get_size(self):
128
+ return len(self.schedule)
129
+
130
+ def clear_schedule(self):
131
+ self.schedule.clear()
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: gabse
3
+ Version: 0.1.1
4
+ Summary: The GABSE (Generic Agent-Based Simulation for Engineering) framework
5
+ Author: Carl Toller Melén
6
+ Author-email: Carl Toller Melén <carl@tollermelen.se>
7
+ Maintainer-email: Carl Toller Melén <carl@tollermelen.se>
8
+ License-Expression: MIT
9
+ Project-URL: Homepage, https://github.com/tm-carl/GABSE
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=2.3.4
14
+ Requires-Dist: scipy>=1.16.3
15
+ Requires-Dist: sortedcontainers>=2.4.0
16
+ Dynamic: license-file
17
+
18
+ # GABSE
19
+ This is the GABSE (Generic Agent-Based Simulation for Engineering) framework. It provides classes and methods to create and manage simulations involving agents, their actions, sensors, and the simulation context. It is based on agent-based modeling technique and is developed with the intention of being lightweight, scalable, and flexible. This package provides the engine, action scheduling, generic agent functionality, and sensory data collection and management tools.
20
+
21
+ EXAMPLES:
22
+ - Zombie apocalypse (both 2D and 3D)
23
+
24
+ REQUIRED packages:
25
+ - sortedcontainers
26
+ - numpy
27
+ - copy
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.cfg
6
+ gabse/__init__.py
7
+ gabse/agent.py
8
+ gabse/context.py
9
+ gabse/data.py
10
+ gabse/engine.py
11
+ gabse/schedule.py
12
+ gabse.egg-info/PKG-INFO
13
+ gabse.egg-info/SOURCES.txt
14
+ gabse.egg-info/dependency_links.txt
15
+ gabse.egg-info/requires.txt
16
+ gabse.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy>=2.3.4
2
+ scipy>=1.16.3
3
+ sortedcontainers>=2.4.0
@@ -0,0 +1 @@
1
+ gabse
@@ -0,0 +1,36 @@
1
+ # toml
2
+ [build-system]
3
+ requires = ["setuptools>=64.0"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "gabse"
8
+ version = "0.1.1"
9
+ description = "The GABSE (Generic Agent-Based Simulation for Engineering) framework"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ license = "MIT"
13
+ authors = [
14
+ { name = "Carl Toller Melén", email = "carl@tollermelen.se" },
15
+ ]
16
+ maintainers = [
17
+ { name = "Carl Toller Melén", email = "carl@tollermelen.se" },
18
+ ]
19
+
20
+ dependencies = [
21
+ "numpy>=2.3.4",
22
+ "scipy>=1.16.3",
23
+ "sortedcontainers>=2.4.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/tm-carl/GABSE"
28
+ #Documentation = "https://eriktoller.github.io/andfn/n"
29
+ #Repository = "https://github.com/eriktoller/andfn"
30
+ #Issues = "https://github.com/eriktoller/andfn/issues"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["gabse"]
34
+
35
+ [tool.setuptools.dynamic]
36
+ version = { attr = "gabse.__version__" }
gabse-0.1.1/setup.cfg ADDED
@@ -0,0 +1,16 @@
1
+ [metadata]
2
+ name = gabse
3
+ version = 0.1.1
4
+ description = The GABSE (Generic Agent-Based Simulation for Engineering) framework
5
+ author = Carl Toller Melén
6
+ author_email = carl@tollermelen.se
7
+ license = MIT
8
+
9
+ [options]
10
+ packages = find:
11
+ python_requires = >=3.11
12
+
13
+ [egg_info]
14
+ tag_build =
15
+ tag_date = 0
16
+