vi.config
1from __future__ import annotations 2 3from copy import deepcopy 4from dataclasses import dataclass, field 5from typing import TYPE_CHECKING 6 7 8if TYPE_CHECKING: 9 from typing import Any, Self, TypeIs 10 11 12__all__ = [ 13 "Config", 14 "Matrix", 15 "Mono", 16 "Poly", 17 "Schema", 18 "Window", 19] 20 21 22def _embiggen(input_list: list[Any], copies: int) -> None: 23 """The in-place deep-copy variant of list multiplication.""" 24 head = input_list[:] 25 26 for _ in range(copies - 1): 27 input_list.extend(deepcopy(head)) 28 29 30def _is_list(obj: Any) -> TypeIs[list[Any]]: # noqa: ANN401 31 return isinstance(obj, list) 32 33 34def _matrixify(matrix: dict[str, Any | list[Any]]) -> list[dict[str, Any]]: # noqa: C901 35 combinations: list[dict[str, Any]] = [] 36 37 for key, values in matrix.items(): 38 if key.startswith("_"): 39 continue 40 41 # Skip this key if its value is an empty list 42 if _is_list(values) and len(values) == 0: 43 continue 44 45 # Initially the list is empty, so the dicts have to be 46 # manually created on the first iteration. 47 if len(combinations) == 0: 48 # Multiple values 49 if _is_list(values): 50 combinations.extend({key: value} for value in values) 51 52 # Single value 53 elif values is not None: 54 combinations.append({key: values}) 55 56 # If we have a list of dicts, we can simply add our key! 57 elif _is_list(values): # Multiple values 58 original_length = len(combinations) 59 _embiggen(combinations, len(values)) 60 61 for index, entry in enumerate(combinations): 62 value_index = index // original_length 63 entry[key] = values[value_index] 64 65 elif values is not None: # Single value 66 for entry in combinations: 67 entry[key] = values 68 69 for index, entry in enumerate(combinations): 70 entry["id"] = index + 1 71 72 return combinations 73 74 75@dataclass 76class Window: 77 """Settings related to the simulation window.""" 78 79 width: int = 750 80 """The width of the simulation window in pixels.""" 81 82 height: int = 750 83 """The height of the simulation window in pixels.""" 84 85 @classmethod 86 def square(cls, size: int) -> Self: 87 return cls(width=size, height=size) 88 89 def as_tuple(self) -> tuple[int, int]: 90 return (self.width, self.height) 91 92 93type Mono[T] = T 94type Poly[T] = T | list[T] 95 96 97@dataclass 98class Schema[Int: Poly[int], Float: Poly[float]]: 99 """All values shared between `Config` and `Matrix`. 100 101 NOTE: DOCUMENTATION OF SCHEMA IS INCORRECT AND WILL BE UPDATED IN VERSION 0.3.1. 102 IF YOU ARE SEEING THIS MESSAGE, MAKE SURE TO UPDATE VIOLET. 103 104 A sprinkle of ✨ [magical typing](https://mypy.readthedocs.io/en/stable/generics.html) ✨ makes list values in the `Matrix` class possible without any overrides. 105 You'll notice that the `Schema` class is generic over two type parameters: `Int` and `Float`. 106 These type parameters can either be a single int/float value or a list of int/float values respectively. 107 In combination with the [`Union`](https://docs.python.org/3/library/typing.html#typing.Union) of int/float, both a `Matrix` and `Config` class can be derived. 108 109 Examples 110 -------- 111 To build your own `Matrix`, you first want to create a `Schema` with the configuration options that you want to add. 112 For this example, let's say that we want to add an `infectability` option to our custom `Schema`. 113 We want this infectability to be a `float`. 114 However, in our `Matrix`, we want the possibility to pass multiple floats to automatically generate unique config combinations. 115 116 As we want our `infectability` to be a `float` or a `list[float]` depending on whether we have a `Config` or `Matrix` respectively, 117 we need to create a type parameter which will act as our placeholder. 118 119 ```python 120 from typing import TypeVar 121 MatrixFloat = TypeVar("MatrixFloat", float, list[float]) 122 ``` 123 124 Next up, we can create our custom `Schema`. 125 Note that we do not inherit `Schema`. 126 Instead, we state that our schema is generic over `MatrixFloat`. 127 128 ```python 129 from typing import Generic, Union 130 from vi.config import dataclass 131 132 @dataclass 133 class CovidSchema(Generic[MatrixFloat]): 134 infectability: Union[float, MatrixFloat] = 1.0 135 ``` 136 137 By making the type of `infectability` a union of `float` and `MatrixFloat`, 138 we state that `infectability` will either be a `float` or a `MatrixFloat`. 139 But if you've been playing close attention, 140 you'll notice that our `MatrixFloat` itself can either be a `float` or a `list[float]`. 141 This little trick allows us to state that `infectability` can always be a `float`. 142 But if `MatrixFloat` is a `list[float]`, then `infectability` can be a `list[float]` as well. 143 144 Now, to create our own `ConfigConfig` and `ConfigMatrix`, we inherit `Config` and `Matrix` respectively. 145 However, we also inherit our `CovidSchema` which we just created. 146 147 ```python 148 from vi.config import Config, Matrix 149 150 @dataclass 151 class CovidConfig(Config, CovidSchema[float]): 152 ... 153 154 @dataclass 155 class CovidMatrix(Matrix, CovidSchema[list[float]]): 156 ... 157 ``` 158 159 The classes themselves don't add any values. 160 Instead, we simply state that these classes combine `Config`/`Matrix` with their respective `CovidSchema`. 161 And here's where our generic `MatrixFloat` type parameter makes a return. 162 When inheriting `CovidSchema`, we state what the type of `MatrixFloat` must be. 163 By passing a `float` in `CovidConfig`, we ensure that the config will always have only one value for `infectability`. 164 Similarly, passing `list[float]` in `CovidMatrix` allows us to either supply a single value in the matrix, 165 or a list of values for `infectability`. 166 167 Sometimes, it does not make sense to make a value matrix-configurable. 168 In those cases, you do not have to create a `MatrixFloat`-like type parameter. 169 Instead, you can simply use a normal type annotation. 170 171 ```python 172 @dataclass 173 class CovidSchema: 174 infectability: float = 1.0 175 ``` 176 177 Note that we still create a `Schema`, as you should never inherit and add values to `Matrix` directly. 178 Instead, you should always create a `Schema` and derive the config and matrix classes. 179 180 If you want to support both matrix floats as well as integers, 181 you can simply make your `Schema` generic over multiple type parameters. 182 183 ```python 184 MatrixFloat = TypeVar("MatrixFloat", float, list[float]) 185 MatrixInt = TypeVar("MatrixInt", int, list[int]) 186 187 @dataclass 188 class CovidSchema(Generic[MatrixFloat, MatrixInt]): 189 infectability: Union[float, MatrixFloat] = 1.0 190 recovery_time: Union[int, MatrixInt] = 60 191 ``` 192 193 Just make sure to use the same order when deriving the `CovidConfig` and `CovidMatrix` classes. 194 195 ```python 196 @dataclass 197 class CovidConfig(Config, CovidSchema[float, int]): 198 # 👆 199 200 @dataclass 201 class CovidMatrix(Matrix, CovidSchema[list[float], list[int]]): 202 # MatrixInt is on the second position too 👆 203 ``` 204 205 """ 206 207 id: int = 0 208 """The identifier of the config.""" 209 210 duration: int = 0 211 """The duration of the simulation in frames. 212 213 Defaults to `0`, indicating that the simulation runs indefinitely. 214 """ 215 216 fps_limit: int = 60 217 """Limit the number of frames-per-second. 218 219 Defaults to 60 fps, equal to most screens' refresh rates. 220 221 Set to `0` to uncap the framerate. 222 """ 223 224 image_rotation: bool = False 225 """Opt-in image rotation support. 226 227 Please be aware that the rotation of images degrades performance by ~15% 228 and currently causes a bug where agents clip into obstacles. 229 """ 230 231 movement_speed: float | Float = 0.5 232 """The per-frame movement speed of the agents.""" 233 234 print_fps: bool = False 235 """Print the current number of frames-per-second in the terminal""" 236 237 radius: int | Int = 25 238 """The radius (in pixels) in which agents are considered to be in proximity.""" 239 240 seed: int | Int | None = None 241 """The PRNG seed to use for the simulation. 242 243 Defaults to `None`, indicating that no seed is used. 244 """ 245 246 visualise_chunks: bool = False 247 """Draw the borders of the proximity-chunks on screen.""" 248 249 window: Window = field(default_factory=Window) 250 """The simulation window""" 251 252 def to_configs[T: Config]( 253 self, 254 target: type[T], 255 ) -> list[T]: 256 """Generate a config for every unique combination of values in the matrix.""" 257 return [target(**values) for values in _matrixify(self.__dict__)] 258 259 260Matrix = Schema[Poly[int], Poly[float]] 261"""`Matrix` is `Config` on steroids. 262 263It allows you to supply a list of values on certain configuration options, 264to automatically generate multiple unique `Config` instances. 265 266Examples 267-------- 268Imagine that you want to research the effect of the `radius` parameter. 269Instead of only testing the default value of 25 pixels, 270you also want to test a radius of 10 and 50 pixels. 271 272A brute-force approach would be to create three unique `Config` instances manually. 273 274```python 275config1 = Config(radius=10) 276config2 = Config(radius=25) 277config3 = Config(radius=50) 278``` 279 280However, perhaps we also want to override some other default values, 281such as adding a `duration` to the simulation. 282If we follow the same approach, then our code becomes messy rather quickly. 283 284```python 285config1 = Config(radius=10, duration=60 * 10) 286config2 = Config(radius=25, duration=60 * 10) 287config3 = Config(radius=50, duration=60 * 10) 288``` 289 290So what do we do? 291 292We use `Matrix`! 😎 293 294`Matrix` allows us to write multiple configurations as if we are writing one configuration. 295If we want to test multiple values of `radius`, then we can simply supply a list of values. 296 297```python 298matrix = Matrix(duration=60 * 10, radius=[10, 25, 50]) 299``` 300 301It's that easy! 302Now, if we want to generate a `Config` for each of the values in the radius list, 303we can call the `to_configs` method. 304 305```python 306configs = matrix.to_configs(Config) 307``` 308 309The list of configs returned by the `to_configs` method is equivalent to the brute-force approach we took earlier. 310However, by utilising `Matrix`, our code is way more compact and easier to read. 311 312And the fun doesn't stop there, as we can supply lists to multiple config options as well! 313Let's say that we not only want to test the effect of `radius`, but also the effect of `movement_speed`. 314We can simply pass a list of values to `movement_speed` and `Matrix` will automatically compute 315the unique `Config` combinations that it can make between the values of `radius` and `movement_speed`. 316 317```python 318matrix = Matrix( 319 duration=60 * 10, 320 radius=[10, 25, 50], 321 movement_speed=[0.5, 1.0], 322) 323``` 324 325If we now check the number of configs generated, 326we will see that the above matrix produces 6 unique combinations (3 x 2). 327 328>>> len(matrix.to_configs(Config)) 3296 330 331`Matrix` is an essential tool for analysing the effect of your simulation's parameters. 332It allows you to effortlessly create multiple configurations, while keeping your code tidy. 333 334Now, before you create a for-loop and iterate over the list of configs, 335allow me to introduce you to [multiprocessing](https://docs.python.org/3/library/multiprocessing.html). 336This built-in Python library allows us to run multiple simulations in parallel. 337 338As you might already know, your processor (or CPU) consists of multiple cores. 339Parallelisation allows us to run one simulation on every core of your CPU. 340So if you have a beefy 10-core CPU, you can run 10 simulations in the same time as running one simulation individually. 341 342However, your GPU might not be able to keep up with rendering 10 simulations at once. 343Therefore, it's best to switch to `HeadlessSimulation` when running multiple simulations in parallel, 344as this simulation class disables all the rendering-related logic. 345Thus, removing the GPU from the equation. 346 347To learn more about parallelisation, please check out the [multiprocessing documentation](https://docs.python.org/3/library/multiprocessing.html). 348For Violet, the following code is all you need to get started with parallelisation. 349 350```python 351from multiprocessing import Pool 352 353import polars as pl 354 355from vi import Agent, Config, HeadlessSimulation, Matrix 356 357 358class ParallelAgent(Agent): 359 config: Config 360 361 def update(self): 362 # We save the radius and seed config values to our DataFrame, 363 # so we can make comparisons between these config values later. 364 self.save_data("radius", self.config.radius) 365 self.save_data("seed", self.config.seed) 366 367 368def run_simulation(config: Config) -> pl.DataFrame: 369 return ( 370 HeadlessSimulation(config) 371 .batch_spawn_agents(100, ParallelAgent, ["examples/images/white.png"]) 372 .run() 373 .snapshots 374 ) 375 376 377if __name__ == "__main__": 378 # We create a threadpool to run our simulations in parallel 379 with Pool() as p: 380 # The matrix will create four unique configs 381 matrix = Matrix(radius=[25, 50], seed=[1, 2]) 382 383 # Create unique combinations of matrix values 384 configs = matrix.to_configs(Config) 385 386 # Combine our individual DataFrames into one big DataFrame 387 df = pl.concat(p.map(run_simulation, configs)) 388 389 print(df) 390``` 391 392""" 393 394 395Config = Schema[Mono[int], Mono[float]] 396"""`Config` allows you to tweak the settings of your experiment. 397 398Examples 399-------- 400If you want to change the proximity `radius` of your agents, 401you can create a new `Config` instance and pass a custom value for `radius`. 402 403```python 404from vi import Agent, Config, Simulation 405 406 407( 408 # 👇 we override the default radius value 409 Simulation(Config(radius=50)) 410 .batch_spawn_agents(100, Agent, ["examples/images/white.png"]) 411 .run() 412) 413``` 414 415To add your own values to `Config`, 416you can simply inherit `Config`, decorate it with [`@dataclass`](https://docs.python.org/3/library/dataclasses.html) and add your own options. 417However, make sure to declare the [type](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html) of the configuration option 418along with its default value. 419 420```python 421@dataclass 422class MyConfig(Config): 423 # 👇 type 424 excitement: int = 100 425 # 👆 default value 426``` 427 428Last but not least, declare that your agent is using the `MyConfig` class 429and pass it along to the constructor of `Simulation`. 430 431```python 432# 👇 use our custom config 433class MyAgent(Agent[MyConfig]): ... 434 435( 436 # 👇 here too! 437 Simulation(MyConfig()) 438 .batch_spawn_agents(100, MyAgent, ["examples/images/white.png"]) 439 .run() 440) 441``` 442 443"""
Config
allows you to tweak the settings of your experiment.
Examples
If you want to change the proximity radius
of your agents,
you can create a new Config
instance and pass a custom value for radius
.
from vi import Agent, Config, Simulation
(
# 👇 we override the default radius value
Simulation(Config(radius=50))
.batch_spawn_agents(100, Agent, ["examples/images/white.png"])
.run()
)
To add your own values to Config
,
you can simply inherit Config
, decorate it with @dataclass
and add your own options.
However, make sure to declare the type of the configuration option
along with its default value.
@dataclass
class MyConfig(Config):
# 👇 type
excitement: int = 100
# 👆 default value
Last but not least, declare that your agent is using the MyConfig
class
and pass it along to the constructor of Simulation
.
# 👇 use our custom config
class MyAgent(Agent[MyConfig]): ...
(
# 👇 here too!
Simulation(MyConfig())
.batch_spawn_agents(100, MyAgent, ["examples/images/white.png"])
.run()
)
It allows you to supply a list of values on certain configuration options,
to automatically generate multiple unique Config
instances.
Examples
Imagine that you want to research the effect of the radius
parameter.
Instead of only testing the default value of 25 pixels,
you also want to test a radius of 10 and 50 pixels.
A brute-force approach would be to create three unique Config
instances manually.
config1 = Config(radius=10)
config2 = Config(radius=25)
config3 = Config(radius=50)
However, perhaps we also want to override some other default values,
such as adding a duration
to the simulation.
If we follow the same approach, then our code becomes messy rather quickly.
config1 = Config(radius=10, duration=60 * 10)
config2 = Config(radius=25, duration=60 * 10)
config3 = Config(radius=50, duration=60 * 10)
So what do we do?
We use Matrix
! 😎
Matrix
allows us to write multiple configurations as if we are writing one configuration.
If we want to test multiple values of radius
, then we can simply supply a list of values.
matrix = Matrix(duration=60 * 10, radius=[10, 25, 50])
It's that easy!
Now, if we want to generate a Config
for each of the values in the radius list,
we can call the to_configs
method.
configs = matrix.to_configs(Config)
The list of configs returned by the to_configs
method is equivalent to the brute-force approach we took earlier.
However, by utilising Matrix
, our code is way more compact and easier to read.
And the fun doesn't stop there, as we can supply lists to multiple config options as well!
Let's say that we not only want to test the effect of radius
, but also the effect of movement_speed
.
We can simply pass a list of values to movement_speed
and Matrix
will automatically compute
the unique Config
combinations that it can make between the values of radius
and movement_speed
.
matrix = Matrix(
duration=60 * 10,
radius=[10, 25, 50],
movement_speed=[0.5, 1.0],
)
If we now check the number of configs generated, we will see that the above matrix produces 6 unique combinations (3 x 2).
>>> len(matrix.to_configs(Config))
6
Matrix
is an essential tool for analysing the effect of your simulation's parameters.
It allows you to effortlessly create multiple configurations, while keeping your code tidy.
Now, before you create a for-loop and iterate over the list of configs, allow me to introduce you to multiprocessing. This built-in Python library allows us to run multiple simulations in parallel.
As you might already know, your processor (or CPU) consists of multiple cores. Parallelisation allows us to run one simulation on every core of your CPU. So if you have a beefy 10-core CPU, you can run 10 simulations in the same time as running one simulation individually.
However, your GPU might not be able to keep up with rendering 10 simulations at once.
Therefore, it's best to switch to HeadlessSimulation
when running multiple simulations in parallel,
as this simulation class disables all the rendering-related logic.
Thus, removing the GPU from the equation.
To learn more about parallelisation, please check out the multiprocessing documentation. For Violet, the following code is all you need to get started with parallelisation.
from multiprocessing import Pool
import polars as pl
from vi import Agent, Config, HeadlessSimulation, Matrix
class ParallelAgent(Agent):
config: Config
def update(self):
# We save the radius and seed config values to our DataFrame,
# so we can make comparisons between these config values later.
self.save_data("radius", self.config.radius)
self.save_data("seed", self.config.seed)
def run_simulation(config: Config) -> pl.DataFrame:
return (
HeadlessSimulation(config)
.batch_spawn_agents(100, ParallelAgent, ["examples/images/white.png"])
.run()
.snapshots
)
if __name__ == "__main__":
# We create a threadpool to run our simulations in parallel
with Pool() as p:
# The matrix will create four unique configs
matrix = Matrix(radius=[25, 50], seed=[1, 2])
# Create unique combinations of matrix values
configs = matrix.to_configs(Config)
# Combine our individual DataFrames into one big DataFrame
df = pl.concat(p.map(run_simulation, configs))
print(df)
98@dataclass 99class Schema[Int: Poly[int], Float: Poly[float]]: 100 """All values shared between `Config` and `Matrix`. 101 102 NOTE: DOCUMENTATION OF SCHEMA IS INCORRECT AND WILL BE UPDATED IN VERSION 0.3.1. 103 IF YOU ARE SEEING THIS MESSAGE, MAKE SURE TO UPDATE VIOLET. 104 105 A sprinkle of ✨ [magical typing](https://mypy.readthedocs.io/en/stable/generics.html) ✨ makes list values in the `Matrix` class possible without any overrides. 106 You'll notice that the `Schema` class is generic over two type parameters: `Int` and `Float`. 107 These type parameters can either be a single int/float value or a list of int/float values respectively. 108 In combination with the [`Union`](https://docs.python.org/3/library/typing.html#typing.Union) of int/float, both a `Matrix` and `Config` class can be derived. 109 110 Examples 111 -------- 112 To build your own `Matrix`, you first want to create a `Schema` with the configuration options that you want to add. 113 For this example, let's say that we want to add an `infectability` option to our custom `Schema`. 114 We want this infectability to be a `float`. 115 However, in our `Matrix`, we want the possibility to pass multiple floats to automatically generate unique config combinations. 116 117 As we want our `infectability` to be a `float` or a `list[float]` depending on whether we have a `Config` or `Matrix` respectively, 118 we need to create a type parameter which will act as our placeholder. 119 120 ```python 121 from typing import TypeVar 122 MatrixFloat = TypeVar("MatrixFloat", float, list[float]) 123 ``` 124 125 Next up, we can create our custom `Schema`. 126 Note that we do not inherit `Schema`. 127 Instead, we state that our schema is generic over `MatrixFloat`. 128 129 ```python 130 from typing import Generic, Union 131 from vi.config import dataclass 132 133 @dataclass 134 class CovidSchema(Generic[MatrixFloat]): 135 infectability: Union[float, MatrixFloat] = 1.0 136 ``` 137 138 By making the type of `infectability` a union of `float` and `MatrixFloat`, 139 we state that `infectability` will either be a `float` or a `MatrixFloat`. 140 But if you've been playing close attention, 141 you'll notice that our `MatrixFloat` itself can either be a `float` or a `list[float]`. 142 This little trick allows us to state that `infectability` can always be a `float`. 143 But if `MatrixFloat` is a `list[float]`, then `infectability` can be a `list[float]` as well. 144 145 Now, to create our own `ConfigConfig` and `ConfigMatrix`, we inherit `Config` and `Matrix` respectively. 146 However, we also inherit our `CovidSchema` which we just created. 147 148 ```python 149 from vi.config import Config, Matrix 150 151 @dataclass 152 class CovidConfig(Config, CovidSchema[float]): 153 ... 154 155 @dataclass 156 class CovidMatrix(Matrix, CovidSchema[list[float]]): 157 ... 158 ``` 159 160 The classes themselves don't add any values. 161 Instead, we simply state that these classes combine `Config`/`Matrix` with their respective `CovidSchema`. 162 And here's where our generic `MatrixFloat` type parameter makes a return. 163 When inheriting `CovidSchema`, we state what the type of `MatrixFloat` must be. 164 By passing a `float` in `CovidConfig`, we ensure that the config will always have only one value for `infectability`. 165 Similarly, passing `list[float]` in `CovidMatrix` allows us to either supply a single value in the matrix, 166 or a list of values for `infectability`. 167 168 Sometimes, it does not make sense to make a value matrix-configurable. 169 In those cases, you do not have to create a `MatrixFloat`-like type parameter. 170 Instead, you can simply use a normal type annotation. 171 172 ```python 173 @dataclass 174 class CovidSchema: 175 infectability: float = 1.0 176 ``` 177 178 Note that we still create a `Schema`, as you should never inherit and add values to `Matrix` directly. 179 Instead, you should always create a `Schema` and derive the config and matrix classes. 180 181 If you want to support both matrix floats as well as integers, 182 you can simply make your `Schema` generic over multiple type parameters. 183 184 ```python 185 MatrixFloat = TypeVar("MatrixFloat", float, list[float]) 186 MatrixInt = TypeVar("MatrixInt", int, list[int]) 187 188 @dataclass 189 class CovidSchema(Generic[MatrixFloat, MatrixInt]): 190 infectability: Union[float, MatrixFloat] = 1.0 191 recovery_time: Union[int, MatrixInt] = 60 192 ``` 193 194 Just make sure to use the same order when deriving the `CovidConfig` and `CovidMatrix` classes. 195 196 ```python 197 @dataclass 198 class CovidConfig(Config, CovidSchema[float, int]): 199 # 👆 200 201 @dataclass 202 class CovidMatrix(Matrix, CovidSchema[list[float], list[int]]): 203 # MatrixInt is on the second position too 👆 204 ``` 205 206 """ 207 208 id: int = 0 209 """The identifier of the config.""" 210 211 duration: int = 0 212 """The duration of the simulation in frames. 213 214 Defaults to `0`, indicating that the simulation runs indefinitely. 215 """ 216 217 fps_limit: int = 60 218 """Limit the number of frames-per-second. 219 220 Defaults to 60 fps, equal to most screens' refresh rates. 221 222 Set to `0` to uncap the framerate. 223 """ 224 225 image_rotation: bool = False 226 """Opt-in image rotation support. 227 228 Please be aware that the rotation of images degrades performance by ~15% 229 and currently causes a bug where agents clip into obstacles. 230 """ 231 232 movement_speed: float | Float = 0.5 233 """The per-frame movement speed of the agents.""" 234 235 print_fps: bool = False 236 """Print the current number of frames-per-second in the terminal""" 237 238 radius: int | Int = 25 239 """The radius (in pixels) in which agents are considered to be in proximity.""" 240 241 seed: int | Int | None = None 242 """The PRNG seed to use for the simulation. 243 244 Defaults to `None`, indicating that no seed is used. 245 """ 246 247 visualise_chunks: bool = False 248 """Draw the borders of the proximity-chunks on screen.""" 249 250 window: Window = field(default_factory=Window) 251 """The simulation window""" 252 253 def to_configs[T: Config]( 254 self, 255 target: type[T], 256 ) -> list[T]: 257 """Generate a config for every unique combination of values in the matrix.""" 258 return [target(**values) for values in _matrixify(self.__dict__)]
All values shared between Config
and Matrix
.
NOTE: DOCUMENTATION OF SCHEMA IS INCORRECT AND WILL BE UPDATED IN VERSION 0.3.1. IF YOU ARE SEEING THIS MESSAGE, MAKE SURE TO UPDATE VIOLET.
A sprinkle of ✨ magical typing ✨ makes list values in the Matrix
class possible without any overrides.
You'll notice that the Schema
class is generic over two type parameters: Int
and Float
.
These type parameters can either be a single int/float value or a list of int/float values respectively.
In combination with the Union
of int/float, both a Matrix
and Config
class can be derived.
Examples
To build your own Matrix
, you first want to create a Schema
with the configuration options that you want to add.
For this example, let's say that we want to add an infectability
option to our custom Schema
.
We want this infectability to be a float
.
However, in our Matrix
, we want the possibility to pass multiple floats to automatically generate unique config combinations.
As we want our infectability
to be a float
or a list[float]
depending on whether we have a Config
or Matrix
respectively,
we need to create a type parameter which will act as our placeholder.
from typing import TypeVar
MatrixFloat = TypeVar("MatrixFloat", float, list[float])
Next up, we can create our custom Schema
.
Note that we do not inherit Schema
.
Instead, we state that our schema is generic over MatrixFloat
.
from typing import Generic, Union
from vi.config import dataclass
@dataclass
class CovidSchema(Generic[MatrixFloat]):
infectability: Union[float, MatrixFloat] = 1.0
By making the type of infectability
a union of float
and MatrixFloat
,
we state that infectability
will either be a float
or a MatrixFloat
.
But if you've been playing close attention,
you'll notice that our MatrixFloat
itself can either be a float
or a list[float]
.
This little trick allows us to state that infectability
can always be a float
.
But if MatrixFloat
is a list[float]
, then infectability
can be a list[float]
as well.
Now, to create our own ConfigConfig
and ConfigMatrix
, we inherit Config
and Matrix
respectively.
However, we also inherit our CovidSchema
which we just created.
from vi.config import Config, Matrix
@dataclass
class CovidConfig(Config, CovidSchema[float]):
...
@dataclass
class CovidMatrix(Matrix, CovidSchema[list[float]]):
...
The classes themselves don't add any values.
Instead, we simply state that these classes combine Config
/Matrix
with their respective CovidSchema
.
And here's where our generic MatrixFloat
type parameter makes a return.
When inheriting CovidSchema
, we state what the type of MatrixFloat
must be.
By passing a float
in CovidConfig
, we ensure that the config will always have only one value for infectability
.
Similarly, passing list[float]
in CovidMatrix
allows us to either supply a single value in the matrix,
or a list of values for infectability
.
Sometimes, it does not make sense to make a value matrix-configurable.
In those cases, you do not have to create a MatrixFloat
-like type parameter.
Instead, you can simply use a normal type annotation.
@dataclass
class CovidSchema:
infectability: float = 1.0
Note that we still create a Schema
, as you should never inherit and add values to Matrix
directly.
Instead, you should always create a Schema
and derive the config and matrix classes.
If you want to support both matrix floats as well as integers,
you can simply make your Schema
generic over multiple type parameters.
MatrixFloat = TypeVar("MatrixFloat", float, list[float])
MatrixInt = TypeVar("MatrixInt", int, list[int])
@dataclass
class CovidSchema(Generic[MatrixFloat, MatrixInt]):
infectability: Union[float, MatrixFloat] = 1.0
recovery_time: Union[int, MatrixInt] = 60
Just make sure to use the same order when deriving the CovidConfig
and CovidMatrix
classes.
@dataclass
class CovidConfig(Config, CovidSchema[float, int]):
# 👆
@dataclass
class CovidMatrix(Matrix, CovidSchema[list[float], list[int]]):
# MatrixInt is on the second position too 👆
The duration of the simulation in frames.
Defaults to 0
, indicating that the simulation runs indefinitely.
Limit the number of frames-per-second.
Defaults to 60 fps, equal to most screens' refresh rates.
Set to 0
to uncap the framerate.
Opt-in image rotation support.
Please be aware that the rotation of images degrades performance by ~15% and currently causes a bug where agents clip into obstacles.
The PRNG seed to use for the simulation.
Defaults to None
, indicating that no seed is used.
253 def to_configs[T: Config]( 254 self, 255 target: type[T], 256 ) -> list[T]: 257 """Generate a config for every unique combination of values in the matrix.""" 258 return [target(**values) for values in _matrixify(self.__dict__)]
Generate a config for every unique combination of values in the matrix.
76@dataclass 77class Window: 78 """Settings related to the simulation window.""" 79 80 width: int = 750 81 """The width of the simulation window in pixels.""" 82 83 height: int = 750 84 """The height of the simulation window in pixels.""" 85 86 @classmethod 87 def square(cls, size: int) -> Self: 88 return cls(width=size, height=size) 89 90 def as_tuple(self) -> tuple[int, int]: 91 return (self.width, self.height)
Settings related to the simulation window.