robosim.utils.utils
utils.py - generic utilities
1"""utils.py - generic utilities""" 2from __future__ import annotations 3from typing import Any 4from pathlib import Path 5from dataclasses import dataclass, field 6from collections import OrderedDict 7import threading 8import zlib 9 10from loggez import make_logger 11import numpy as np 12import raylib as rl 13 14logger = make_logger("ROBOSIM", exists_ok=True) 15 16Rot3x3 = np.ndarray 17Pose4x4 = np.ndarray 18Point2D = np.ndarray 19Point3D = np.ndarray 20Point6D = np.ndarray 21Mat4x4 = np.ndarray 22Mat3x3 = np.ndarray 23 24def fmt(arr: np.ndarray, precision: int=2) -> str: 25 """formats a numpy array for logging as tuple""" 26 return "(" + ", ".join(f"{float(x):.{precision}g}" for x in arr) + ")" 27 28def get_project_root() -> Path: 29 """The project root""" 30 return Path(__file__).parents[2] 31 32@dataclass 33class FPVData: 34 """The first perso view camera data. Latest frame (as bytes), a lock (for concurrency) and a way to compress it""" 35 frame: bytes 36 frame_shape: tuple[int, int, int] 37 lock: threading.Lock = field(default_factory=threading.Lock) 38 frame_id: int = 0 # used for de-dup on client side 39 _frame_compressed: bytes | None = None 40 41 @property 42 def frame_compressed(self) -> bytes: 43 """compress the frame on demand when the client requests a get_state_with_frame. NOTE: use lock on call.""" 44 if self._frame_compressed is None: 45 self._frame_compressed = zlib.compress(self.frame, level=1) 46 return self._frame_compressed 47 48# raylib utils 49 50RlTexture = Any 51RlRenderTexture = Any 52RlModel = Any 53RlVector3 = Any 54RlMesh = Any 55 56def vec3_from(arr: np.ndarray) -> RlVector3: 57 """convers the numpy array to a raylib vector""" 58 return rl.ffi.from_buffer("Vector3 *", np.ascontiguousarray(arr))[0] 59 60@dataclass 61class RlModelWithTexture: 62 """basic raylib model with an optional texture applied to it""" 63 model: RlModel 64 texture: RlTexture | None 65 _bbox: tuple[np.ndarray, np.ndarray] | None = None 66 67 def __post_init__(self): 68 if self.texture is not None: 69 self.model.materials.maps[rl.MATERIAL_MAP_ALBEDO].texture = self.texture 70 71 @staticmethod 72 def from_path(model_path: Path, texture_path: Path | None = None) -> RlModelWithTexture: 73 """builds a RLModelWithTexture from a path""" 74 assert model_path.exists(), f"Model path: '{model_path}' doesn't exist." 75 logger.info(f"Loading model from '{model_path}'") 76 model = rl.LoadModel(str(model_path).encode()) 77 texture = rl.LoadTexture(str(texture_path).encode()) if texture_path is not None else None 78 return RlModelWithTexture(model=model, texture=texture) 79 80 @property 81 def bbox(self) -> tuple[np.ndarray, np.ndarray]: 82 """gets the top left and bottom right 3D points of this mesh. Used for collisions. Cahed once after 1st call""" 83 if self._bbox is None: 84 mins, maxs = np.float32([1<<31, 1<<31, 1<<31]), np.float32([-1<<31, -1<<31, -1<<31]) 85 for i in range(self.model.meshCount): 86 mesh: RlMesh = self.model.meshes[i] # of type struct Mesh & 87 buf = rl.ffi.buffer(mesh.vertices, mesh.vertexCount * 3 * 4) 88 vertices = np.frombuffer(buf, "float32").reshape(-1, 3) 89 mins = np.minimum(mins, np.min(vertices, axis=0)) 90 maxs = np.maximum(maxs, np.max(vertices, axis=0)) 91 self._bbox = mins, maxs 92 return self._bbox 93 94 def __del__(self): 95 if self.model is not None: 96 rl.UnloadModel(self.model) 97 if self.texture is not None: 98 rl.UnloadTexture(self.texture) 99 100def rl_get_device_and_renderer() -> tuple[str, str]: 101 """returns the device and renderer used by raylib for logging""" 102 gl_renderer = 0x1F01 103 gl_get_string = rl.ffi.cast("unsigned char *(*)(unsigned int)", rl.rlGetProcAddress(b"glGetString")) 104 gstr = lambda e: rl.ffi.string(rl.ffi.cast("char*", gl_get_string(e))).decode() 105 renderer = gstr(gl_renderer) 106 software = ("llvmpipe", "softpipe", "swrast", "lavapipe") 107 device = "CPU (software rasterizer)" if any(s in renderer.lower() for s in software) else "GPU" 108 return device, renderer 109 110# generic utils below 111 112def make_arr(*data): 113 """makes a numpy array from variable data, e.g. make_arr(1,2,3) <=> np.array([1,2,3], "float32")""" 114 return np.float32(data) 115 116class FixedSizeDict(OrderedDict): 117 """An dict with a fixed size. Useful for caching purposes.""" 118 def __init__(self, *args, maxlen: int = 0, **kwargs): 119 self._maxlen = maxlen 120 super().__init__(*args, **kwargs) 121 122 def __setitem__(self, key, value): 123 existed_before_add = key in self 124 super().__setitem__(key, value) 125 if not existed_before_add and len(self) > self._maxlen: 126 self.popitem(False)
logger =
<loggez.loggez.LoggezLogger object>
Rot3x3 =
<class 'numpy.ndarray'>
Pose4x4 =
<class 'numpy.ndarray'>
Point2D =
<class 'numpy.ndarray'>
Point3D =
<class 'numpy.ndarray'>
Point6D =
<class 'numpy.ndarray'>
Mat4x4 =
<class 'numpy.ndarray'>
Mat3x3 =
<class 'numpy.ndarray'>
def
fmt(arr: numpy.ndarray, precision: int = 2) -> str:
25def fmt(arr: np.ndarray, precision: int=2) -> str: 26 """formats a numpy array for logging as tuple""" 27 return "(" + ", ".join(f"{float(x):.{precision}g}" for x in arr) + ")"
formats a numpy array for logging as tuple
def
get_project_root() -> pathlib.Path:
The project root
@dataclass
class
FPVData:
33@dataclass 34class FPVData: 35 """The first perso view camera data. Latest frame (as bytes), a lock (for concurrency) and a way to compress it""" 36 frame: bytes 37 frame_shape: tuple[int, int, int] 38 lock: threading.Lock = field(default_factory=threading.Lock) 39 frame_id: int = 0 # used for de-dup on client side 40 _frame_compressed: bytes | None = None 41 42 @property 43 def frame_compressed(self) -> bytes: 44 """compress the frame on demand when the client requests a get_state_with_frame. NOTE: use lock on call.""" 45 if self._frame_compressed is None: 46 self._frame_compressed = zlib.compress(self.frame, level=1) 47 return self._frame_compressed
The first perso view camera data. Latest frame (as bytes), a lock (for concurrency) and a way to compress it
FPVData( frame: bytes, frame_shape: tuple[int, int, int], lock: <built-in function allocate_lock> = <factory>, frame_id: int = 0, _frame_compressed: bytes | None = None)
frame_compressed: bytes
42 @property 43 def frame_compressed(self) -> bytes: 44 """compress the frame on demand when the client requests a get_state_with_frame. NOTE: use lock on call.""" 45 if self._frame_compressed is None: 46 self._frame_compressed = zlib.compress(self.frame, level=1) 47 return self._frame_compressed
compress the frame on demand when the client requests a get_state_with_frame. NOTE: use lock on call.
RlTexture =
typing.Any
RlRenderTexture =
typing.Any
RlModel =
typing.Any
RlVector3 =
typing.Any
RlMesh =
typing.Any
def
vec3_from(arr: numpy.ndarray) -> Any:
57def vec3_from(arr: np.ndarray) -> RlVector3: 58 """convers the numpy array to a raylib vector""" 59 return rl.ffi.from_buffer("Vector3 *", np.ascontiguousarray(arr))[0]
convers the numpy array to a raylib vector
@dataclass
class
RlModelWithTexture:
61@dataclass 62class RlModelWithTexture: 63 """basic raylib model with an optional texture applied to it""" 64 model: RlModel 65 texture: RlTexture | None 66 _bbox: tuple[np.ndarray, np.ndarray] | None = None 67 68 def __post_init__(self): 69 if self.texture is not None: 70 self.model.materials.maps[rl.MATERIAL_MAP_ALBEDO].texture = self.texture 71 72 @staticmethod 73 def from_path(model_path: Path, texture_path: Path | None = None) -> RlModelWithTexture: 74 """builds a RLModelWithTexture from a path""" 75 assert model_path.exists(), f"Model path: '{model_path}' doesn't exist." 76 logger.info(f"Loading model from '{model_path}'") 77 model = rl.LoadModel(str(model_path).encode()) 78 texture = rl.LoadTexture(str(texture_path).encode()) if texture_path is not None else None 79 return RlModelWithTexture(model=model, texture=texture) 80 81 @property 82 def bbox(self) -> tuple[np.ndarray, np.ndarray]: 83 """gets the top left and bottom right 3D points of this mesh. Used for collisions. Cahed once after 1st call""" 84 if self._bbox is None: 85 mins, maxs = np.float32([1<<31, 1<<31, 1<<31]), np.float32([-1<<31, -1<<31, -1<<31]) 86 for i in range(self.model.meshCount): 87 mesh: RlMesh = self.model.meshes[i] # of type struct Mesh & 88 buf = rl.ffi.buffer(mesh.vertices, mesh.vertexCount * 3 * 4) 89 vertices = np.frombuffer(buf, "float32").reshape(-1, 3) 90 mins = np.minimum(mins, np.min(vertices, axis=0)) 91 maxs = np.maximum(maxs, np.max(vertices, axis=0)) 92 self._bbox = mins, maxs 93 return self._bbox 94 95 def __del__(self): 96 if self.model is not None: 97 rl.UnloadModel(self.model) 98 if self.texture is not None: 99 rl.UnloadTexture(self.texture)
basic raylib model with an optional texture applied to it
RlModelWithTexture( model: Any, texture: typing.Any | None, _bbox: tuple[numpy.ndarray, numpy.ndarray] | None = None)
@staticmethod
def
from_path( model_path: pathlib.Path, texture_path: pathlib.Path | None = None) -> RlModelWithTexture:
72 @staticmethod 73 def from_path(model_path: Path, texture_path: Path | None = None) -> RlModelWithTexture: 74 """builds a RLModelWithTexture from a path""" 75 assert model_path.exists(), f"Model path: '{model_path}' doesn't exist." 76 logger.info(f"Loading model from '{model_path}'") 77 model = rl.LoadModel(str(model_path).encode()) 78 texture = rl.LoadTexture(str(texture_path).encode()) if texture_path is not None else None 79 return RlModelWithTexture(model=model, texture=texture)
builds a RLModelWithTexture from a path
bbox: tuple[numpy.ndarray, numpy.ndarray]
81 @property 82 def bbox(self) -> tuple[np.ndarray, np.ndarray]: 83 """gets the top left and bottom right 3D points of this mesh. Used for collisions. Cahed once after 1st call""" 84 if self._bbox is None: 85 mins, maxs = np.float32([1<<31, 1<<31, 1<<31]), np.float32([-1<<31, -1<<31, -1<<31]) 86 for i in range(self.model.meshCount): 87 mesh: RlMesh = self.model.meshes[i] # of type struct Mesh & 88 buf = rl.ffi.buffer(mesh.vertices, mesh.vertexCount * 3 * 4) 89 vertices = np.frombuffer(buf, "float32").reshape(-1, 3) 90 mins = np.minimum(mins, np.min(vertices, axis=0)) 91 maxs = np.maximum(maxs, np.max(vertices, axis=0)) 92 self._bbox = mins, maxs 93 return self._bbox
gets the top left and bottom right 3D points of this mesh. Used for collisions. Cahed once after 1st call
def
rl_get_device_and_renderer() -> tuple[str, str]:
101def rl_get_device_and_renderer() -> tuple[str, str]: 102 """returns the device and renderer used by raylib for logging""" 103 gl_renderer = 0x1F01 104 gl_get_string = rl.ffi.cast("unsigned char *(*)(unsigned int)", rl.rlGetProcAddress(b"glGetString")) 105 gstr = lambda e: rl.ffi.string(rl.ffi.cast("char*", gl_get_string(e))).decode() 106 renderer = gstr(gl_renderer) 107 software = ("llvmpipe", "softpipe", "swrast", "lavapipe") 108 device = "CPU (software rasterizer)" if any(s in renderer.lower() for s in software) else "GPU" 109 return device, renderer
returns the device and renderer used by raylib for logging
def
make_arr(*data):
113def make_arr(*data): 114 """makes a numpy array from variable data, e.g. make_arr(1,2,3) <=> np.array([1,2,3], "float32")""" 115 return np.float32(data)
makes a numpy array from variable data, e.g. make_arr(1,2,3) <=> np.array([1,2,3], "float32")
class
FixedSizeDict(collections.OrderedDict):
117class FixedSizeDict(OrderedDict): 118 """An dict with a fixed size. Useful for caching purposes.""" 119 def __init__(self, *args, maxlen: int = 0, **kwargs): 120 self._maxlen = maxlen 121 super().__init__(*args, **kwargs) 122 123 def __setitem__(self, key, value): 124 existed_before_add = key in self 125 super().__setitem__(key, value) 126 if not existed_before_add and len(self) > self._maxlen: 127 self.popitem(False)
An dict with a fixed size. Useful for caching purposes.