tinysim 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,190 @@
1
+ import json
2
+ import math
3
+ import numpy as np
4
+ from pathlib import Path
5
+ from .. import SimEnvironment
6
+
7
+
8
+ try:
9
+ with open(Path(__file__).parent / "track_0.json", "r") as f:
10
+ track = json.load(f)
11
+ except FileNotFoundError:
12
+ raise FileNotFoundError("Unable to load track data.")
13
+
14
+
15
+ MAX_VEL = 20.0
16
+ ACCELERATION = 8.0
17
+ VEL_FRICT = 2.0
18
+ TURN_SPEED = math.radians(100)
19
+
20
+ CAR_LENGTH = 1.6 # WORLD units (smaller)
21
+ CAR_WIDTH = 0.8
22
+ CAR_RADIUS = 0.5
23
+
24
+ LOCAL_WALLS = track["walls"]
25
+ CHECKPOINTS = np.array(track["checkpoints"], dtype=np.float32)
26
+ CHECKPOINT_RADIUS = 3.0
27
+
28
+ WORLD_WALLS = np.array(
29
+ [(x, y, w, h, math.radians(rot)) for x, y, w, h, rot in LOCAL_WALLS],
30
+ dtype=np.float32,
31
+ )
32
+
33
+ W_X, W_Y, W_W, W_H, W_ROT = WORLD_WALLS.T
34
+ W_HW = W_W * 0.5
35
+ W_HH = W_H * 0.5
36
+ W_COS = np.cos(-W_ROT)
37
+ W_SIN = np.sin(-W_ROT)
38
+
39
+ # Precompute ray offsets
40
+ RAY_SPREAD = math.radians(75) # total fan angle
41
+ RAY_LENGTH = 12.0 # world units
42
+ RAY_COUNT = 5
43
+ ray_offsets = np.linspace(
44
+ -RAY_SPREAD * 0.5, RAY_SPREAD * 0.5, RAY_COUNT, dtype=np.float32
45
+ )
46
+ EPS = 1e-6
47
+
48
+
49
+ def cast_rays(x, y, angle):
50
+ # x,y,angle: (N,)
51
+ a = angle[:, None] + ray_offsets[None, :] # (N,R)
52
+ dx = np.cos(a)
53
+ dy = np.sin(a)
54
+ best_t = np.full((x.shape[0], RAY_COUNT), RAY_LENGTH, dtype=np.float32)
55
+
56
+ # Expand dims for broadcasting
57
+ ox = x[:, None, None]
58
+ oy = y[:, None, None]
59
+ rdx = dx[:, :, None]
60
+ rdy = dy[:, :, None]
61
+
62
+ wx = W_X[None, None, :]
63
+ wy = W_Y[None, None, :]
64
+
65
+ # transform ray to wall space
66
+ rox = (ox - wx) * W_COS - (oy - wy) * W_SIN
67
+ roy = (ox - wx) * W_SIN + (oy - wy) * W_COS
68
+ rdxl = rdx * W_COS - rdy * W_SIN
69
+ rdyl = rdx * W_SIN + rdy * W_COS
70
+
71
+ tmin = np.full_like(rox, -1e9)
72
+ tmax = np.full_like(rox, 1e9)
73
+ mask_x = np.abs(rdxl) > EPS
74
+ safe_rdxl = np.where(mask_x, rdxl, 1.0)
75
+ tx1 = (-W_HW - rox) / safe_rdxl
76
+ tx2 = (W_HW - rox) / safe_rdxl
77
+ tmin = np.where(mask_x, np.maximum(tmin, np.minimum(tx1, tx2)), tmin)
78
+ tmax = np.where(mask_x, np.minimum(tmax, np.maximum(tx1, tx2)), tmax)
79
+ mask_y = np.abs(rdyl) > EPS
80
+ safe_rdyl = np.where(mask_y, rdyl, 1.0)
81
+ ty1 = (-W_HH - roy) / safe_rdyl
82
+ ty2 = (W_HH - roy) / safe_rdyl
83
+ tmin = np.where(mask_y, np.maximum(tmin, np.minimum(ty1, ty2)), tmin)
84
+ tmax = np.where(mask_y, np.minimum(tmax, np.maximum(ty1, ty2)), tmax)
85
+ valid = (tmax >= tmin) & (tmax > 0.0)
86
+ t = np.where(valid, np.where(tmin > 0, tmin, tmax), np.inf)
87
+ best_t = np.minimum(best_t, t.min(axis=2))
88
+ return best_t
89
+
90
+
91
+ def collides(cx, cy):
92
+ dx = cx[:, None] - W_X[None, :]
93
+ dy = cy[:, None] - W_Y[None, :]
94
+ lx = dx * W_COS[None, :] - dy * W_SIN[None, :]
95
+ ly = dx * W_SIN[None, :] + dy * W_COS[None, :]
96
+ px = np.clip(lx, -W_HW[None, :], W_HW[None, :])
97
+ py = np.clip(ly, -W_HH[None, :], W_HH[None, :])
98
+ ddx = lx - px
99
+ ddy = ly - py
100
+ hit = (ddx**2 + ddy**2) <= CAR_RADIUS**2
101
+ return hit.any(axis=1)
102
+
103
+
104
+ class TopDownDrivingEnv(SimEnvironment):
105
+ def __init__(self, num_envs: int = 1):
106
+ self.num_envs = num_envs
107
+ self.reset()
108
+
109
+ def reset(self):
110
+ self.x = np.full(self.num_envs, -85.0, dtype=np.float32)
111
+ self.y = np.full(self.num_envs, -42.0, dtype=np.float32)
112
+ self.angle = np.zeros(self.num_envs, dtype=np.float32)
113
+ self.velocity = np.zeros(self.num_envs, dtype=np.float32)
114
+ self.rays = []
115
+
116
+ self.checkpoint_idx = np.zeros(self.num_envs, dtype=np.int32)
117
+ cx, cy = CHECKPOINTS[0]
118
+ self.prev_dist = np.hypot(self.x - cx, self.y - cy)
119
+ return {
120
+ "x": self.x,
121
+ "y": self.y,
122
+ "angle": self.angle,
123
+ "velocity": self.velocity,
124
+ "rays": self.rays,
125
+ }
126
+
127
+ def step(self, action, dt=0.02):
128
+ throttle = action.get("throttle", 0.0)
129
+ steer = action.get("steer", 0.0)
130
+
131
+ if np.isscalar(throttle) and np.isscalar(steer):
132
+ throttle = np.full(self.num_envs, throttle, dtype=np.float32)
133
+ steer = np.full(self.num_envs, steer, dtype=np.float32)
134
+ elif isinstance(throttle, np.ndarray) and isinstance(steer, np.ndarray):
135
+ throttle = np.asarray(throttle, dtype=np.float32)
136
+ steer = np.asarray(steer, dtype=np.float32)
137
+ if throttle.shape[0] != self.num_envs or steer.shape[0] != self.num_envs:
138
+ raise ValueError(
139
+ f"Expected actions of shape ({self.num_envs},), got {throttle.shape} and {steer.shape}"
140
+ )
141
+ else:
142
+ raise ValueError(
143
+ "Inputs throttle and steer must both be either scalars or numpy arrays."
144
+ )
145
+
146
+ self.velocity += throttle * ACCELERATION * dt
147
+ self.velocity = np.clip(self.velocity, 0.0, MAX_VEL)
148
+ self.angle -= steer * TURN_SPEED * dt
149
+
150
+ dx = np.cos(self.angle) * self.velocity * dt
151
+ dy = np.sin(self.angle) * self.velocity * dt
152
+ nx, ny = self.x + dx, self.y + dy
153
+
154
+ hit = collides(nx, ny)
155
+ self.x = np.where(hit, self.x, nx)
156
+ self.y = np.where(hit, self.y, ny)
157
+ self.velocity = np.where(hit, 0.0, self.velocity)
158
+
159
+ mask = np.abs(throttle) < 1e-3
160
+ self.velocity = np.where(
161
+ mask, np.maximum(0.0, self.velocity - VEL_FRICT * dt), self.velocity
162
+ )
163
+ self.rays = cast_rays(self.x, self.y, self.angle)
164
+
165
+ # TODO: should reward be scaled by the distance between checkpoints?
166
+ # return done if the car hits a wall?
167
+ cp = CHECKPOINTS[self.checkpoint_idx]
168
+ dist = np.hypot(self.x - cp[:, 0], self.y - cp[:, 1])
169
+
170
+ # Reward is the delta to see if the car is getting closer to the checkpoint
171
+ reward = self.prev_dist - dist
172
+ reached = dist <= CHECKPOINT_RADIUS
173
+
174
+ bonus = 1.0
175
+ reward += np.where(reached, bonus, 0.0)
176
+ self.checkpoint_idx = np.where(
177
+ reached, self.checkpoint_idx + 1, self.checkpoint_idx
178
+ )
179
+
180
+ cp = CHECKPOINTS[self.checkpoint_idx]
181
+ self.prev_dist = np.hypot(self.x - cp[:, 0], self.y - cp[:, 1])
182
+
183
+ return {
184
+ "x": self.x,
185
+ "y": self.y,
186
+ "angle": self.angle,
187
+ "velocity": self.velocity,
188
+ "rays": self.rays,
189
+ "reward": reward,
190
+ }
@@ -0,0 +1,136 @@
1
+ export default {
2
+ async render({ model, el }) {
3
+ const [CANVAS_W, CANVAS_H] = model.get("_viewport_size") || [800, 600];
4
+
5
+ const container = document.createElement("div");
6
+ container.style.position = "relative";
7
+ el.appendChild(container);
8
+
9
+ const canvas = document.createElement("canvas");
10
+ canvas.width = CANVAS_W;
11
+ canvas.height = CANVAS_H;
12
+ container.appendChild(canvas);
13
+
14
+ const ctx = canvas.getContext("2d");
15
+
16
+ const walls = model.get("wall_positions") || [];
17
+ let sim_state = model.get("sim_state") || {};
18
+
19
+ model.on("change:sim_state", () => {
20
+ sim_state = model.get("sim_state") || {};
21
+ });
22
+ const xs = [];
23
+ const ys = [];
24
+
25
+ for (const [x, y, w, h] of walls) {
26
+ const r = Math.hypot(w, h) * 0.5;
27
+ xs.push(x - r, x + r);
28
+ ys.push(y - r, y + r);
29
+ }
30
+
31
+ const min_x = Math.min(...xs);
32
+ const max_x = Math.max(...xs);
33
+ const min_y = Math.min(...ys);
34
+ const max_y = Math.max(...ys);
35
+
36
+ const scale = Math.min(
37
+ (CANVAS_W - 2) / (max_x - min_x),
38
+ (CANVAS_H - 2) / (max_y - min_y)
39
+ );
40
+
41
+ const offset_x = -min_x * scale;
42
+ const offset_y = max_y * scale;
43
+
44
+ const worldToScreen = (x, y) => {
45
+ return [
46
+ x * scale + offset_x,
47
+ -y * scale + offset_y,
48
+ ];
49
+ };
50
+
51
+ const drawWalls = () => {
52
+ ctx.fillStyle = "#cccccc";
53
+ ctx.strokeStyle = "#000";
54
+
55
+ for (const [x, y, w, h, rot] of walls) {
56
+ const [sx, sy] = worldToScreen(x, y);
57
+
58
+ ctx.save();
59
+ ctx.translate(sx, sy);
60
+ const rad = (rot * Math.PI) / 180;
61
+ ctx.rotate(-rad);
62
+
63
+ ctx.beginPath();
64
+ ctx.rect(
65
+ (-w * scale) / 2,
66
+ (-h * scale) / 2,
67
+ w * scale,
68
+ h * scale
69
+ );
70
+ ctx.fill();
71
+ ctx.stroke();
72
+
73
+ ctx.restore();
74
+ }
75
+ };
76
+
77
+ const drawCars = () => {
78
+ if (!sim_state.x) return;
79
+
80
+ const xs = sim_state.x;
81
+ const ys = sim_state.y;
82
+ const angles = sim_state.angle;
83
+ const CAR_LENGTH = 1.0;
84
+ const CAR_WIDTH = 0.5;
85
+ const COLORS = [
86
+ "red",
87
+ "orange",
88
+ "yellow",
89
+ "green",
90
+ "blue",
91
+ "indigo",
92
+ "violet",
93
+ ];
94
+
95
+ for (let i = 0; i < xs.length; i++) {
96
+ const [sx, sy] = worldToScreen(xs[i], ys[i]);
97
+
98
+ ctx.save();
99
+ ctx.translate(sx, sy);
100
+ ctx.rotate(angles[i]);
101
+
102
+ ctx.fillStyle = COLORS[i % COLORS.length];
103
+ ctx.strokeStyle = "black";
104
+
105
+ ctx.beginPath();
106
+ ctx.rect(
107
+ (-CAR_LENGTH * scale) / 2,
108
+ (-CAR_WIDTH * scale) / 2,
109
+ CAR_LENGTH * scale,
110
+ CAR_WIDTH * scale
111
+ );
112
+ ctx.fill();
113
+ ctx.stroke();
114
+
115
+ ctx.restore();
116
+ }
117
+ };
118
+
119
+ const draw = () => {
120
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
121
+
122
+ ctx.fillStyle = "#ffffff";
123
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
124
+
125
+ drawWalls();
126
+ drawCars();
127
+
128
+ requestAnimationFrame(draw);
129
+ };
130
+
131
+ draw();
132
+
133
+ model.set("_view_ready", true);
134
+ model.save_changes();
135
+ }
136
+ };
@@ -0,0 +1,180 @@
1
+ import asyncio
2
+ import math
3
+ from . import (
4
+ TopDownDrivingEnv,
5
+ CAR_LENGTH,
6
+ CAR_WIDTH,
7
+ LOCAL_WALLS,
8
+ CHECKPOINTS,
9
+ RAY_COUNT,
10
+ RAY_SPREAD,
11
+ )
12
+
13
+ try:
14
+ import tkinter as tk
15
+ from .. import _tk_base
16
+ except ImportError:
17
+ raise ImportError("tkinter is required for MountainCarTkFrontend")
18
+
19
+
20
+ CHECKPOINT_RADIUS = 0.85
21
+ COLOR_MAP = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
22
+
23
+ xs, ys = [], []
24
+
25
+ for x, y, w, h, rot in LOCAL_WALLS:
26
+ r = math.hypot(w, h) * 0.5
27
+ xs.extend([x - r, x + r])
28
+ ys.extend([y - r, y + r])
29
+
30
+
31
+ min_x, max_x = min(xs), max(xs)
32
+ min_y, max_y = min(ys), max(ys)
33
+
34
+ CANVAS_W, CANVAS_H = 800, 600
35
+ scale = min((CANVAS_W - 2) / (max_x - min_x), (CANVAS_H - 2) / (max_y - min_y))
36
+ offset_x = -min_x * scale
37
+ offset_y = max_y * scale
38
+
39
+
40
+ def rotated_rect(cx, cy, w, h, deg):
41
+ rad = math.radians(-deg)
42
+ c, s = math.cos(rad), math.sin(rad)
43
+ hw, hh = w / 2, h / 2
44
+ pts = []
45
+ for x, y in [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)]:
46
+ rx = x * c - y * s
47
+ ry = x * s + y * c
48
+ pts.extend([cx + rx, cy + ry])
49
+ return pts
50
+
51
+
52
+ def world_to_screen(x, y):
53
+ return x * scale + offset_x, -y * scale + offset_y
54
+
55
+
56
+ class TopDownDrivingTkFrontend(_tk_base.TkBaseFrontend):
57
+
58
+ def __init__(self, viewport_size=(800, 600), sim_env=None):
59
+ super().__init__()
60
+ if sim_env is None:
61
+ sim_env = TopDownDrivingEnv()
62
+ self.sim_env = sim_env
63
+ self._viewport_size = viewport_size
64
+ self.show_rays = False
65
+
66
+ self.keys = set()
67
+
68
+ async def step(self, action, dt=0.02):
69
+ state = self.sim_env.step(action)
70
+
71
+ if self._root:
72
+ self._root.after(0, lambda s=state: self._draw_state(self.sim_env))
73
+
74
+ await asyncio.sleep(dt)
75
+ return state
76
+
77
+ async def reset(self):
78
+ state = self.sim_env.reset()
79
+ if self._canvas:
80
+ self._draw_state(self.sim_env)
81
+ return state
82
+
83
+ def _create_window(self, root):
84
+ w, h = self._viewport_size
85
+ root.title("Top Down Driving")
86
+ canvas = tk.Canvas(root, width=w, height=h, bg="white")
87
+ canvas.pack(expand=True)
88
+
89
+ for x, y, w, h, rot in LOCAL_WALLS:
90
+ cx, cy = world_to_screen(x, y)
91
+ pts = rotated_rect(cx, cy, w * scale, h * scale, rot)
92
+ canvas.create_polygon(pts, fill="#cccccc", outline="black")
93
+
94
+ r = CHECKPOINT_RADIUS * scale
95
+ for i, (x, y) in enumerate(CHECKPOINTS):
96
+ sx, sy = world_to_screen(x, y)
97
+
98
+ canvas.create_oval(
99
+ sx - r,
100
+ sy - r,
101
+ sx + r,
102
+ sy + r,
103
+ fill="green",
104
+ outline="",
105
+ stipple="gray25",
106
+ )
107
+ # add checkpoint number
108
+ canvas.create_text(
109
+ sx, sy, text=str(i + 1), fill="white", font=("Arial", int(r))
110
+ )
111
+
112
+ root.bind("<KeyPress>", lambda e: self.keys.add(e.keysym))
113
+ root.bind("<KeyRelease>", lambda e: self.keys.discard(e.keysym))
114
+
115
+ self.bring_to_front(root)
116
+ self._root = root
117
+ self._canvas = canvas
118
+ self._draw_state(self.sim_env)
119
+ self._pump()
120
+ root.mainloop()
121
+
122
+ def _draw_state(self, sim_env):
123
+ if not self._canvas:
124
+ return
125
+
126
+ c = self._canvas
127
+ c.delete("car")
128
+ c.delete("ray")
129
+
130
+ xs = sim_env.x
131
+ ys = sim_env.y
132
+ angles = sim_env.angle
133
+ n = len(xs)
134
+
135
+ for i in range(n):
136
+ cx, cy = world_to_screen(xs[i], ys[i])
137
+
138
+ pts = rotated_rect(
139
+ cx,
140
+ cy,
141
+ CAR_LENGTH * scale,
142
+ CAR_WIDTH * scale,
143
+ math.degrees(angles[i]),
144
+ )
145
+ c.create_polygon(
146
+ pts,
147
+ fill=COLOR_MAP[i % len(COLOR_MAP)],
148
+ outline="black",
149
+ tags="car",
150
+ )
151
+
152
+ if not self.show_rays:
153
+ return
154
+
155
+ ray_offsets = [
156
+ -RAY_SPREAD * 0.5 + RAY_SPREAD * i / (RAY_COUNT - 1)
157
+ for i in range(RAY_COUNT)
158
+ ]
159
+
160
+ for i in range(n):
161
+ ox, oy = xs[i], ys[i]
162
+ base = angles[i]
163
+
164
+ sx1, sy1 = world_to_screen(ox, oy)
165
+
166
+ for r_idx, dist in enumerate(sim_env.rays[i]):
167
+ a = base + ray_offsets[r_idx]
168
+ x2 = ox + math.cos(a) * dist
169
+ y2 = oy + math.sin(a) * dist
170
+ sx2, sy2 = world_to_screen(x2, y2)
171
+
172
+ c.create_line(
173
+ sx1,
174
+ sy1,
175
+ sx2,
176
+ sy2,
177
+ fill="red",
178
+ width=1,
179
+ tags="ray",
180
+ )