stashing
This commit is contained in:
parent
ea6fdd33c6
commit
7a8fbda82c
215
notebooks/v1sim.ipynb
Normal file
215
notebooks/v1sim.ipynb
Normal file
File diff suppressed because one or more lines are too long
|
@ -5,8 +5,8 @@ description = "A solar car racing simulation library and GUI tool"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "saji", email = "saji@saji.dev"},
|
{name = "saji", email = "saji@saji.dev"},
|
||||||
]
|
]
|
||||||
dependencies = ["pyqtgraph>=0.13.7", "jax>=0.4.35", "pytest>=8.3.3"]
|
dependencies = ["pyqtgraph>=0.13.7", "jax>=0.4.35", "pytest>=8.3.3", "pyside6>=6.8.0.2", "matplotlib>=3.9.2", "gymnasium>=1.0.0", "pyvista>=0.44.2", "pyvistaqt>=0.11.1"]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.10,<3.13"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|
||||||
|
@ -17,3 +17,16 @@ build-backend = "pdm.backend"
|
||||||
|
|
||||||
[tool.pdm]
|
[tool.pdm]
|
||||||
distribution = true
|
distribution = true
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.basedpyright]
|
||||||
|
reportInvalidTypeForm = false
|
||||||
|
typeCheckingMode = "off"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ipykernel>=6.29.5",
|
||||||
|
]
|
||||||
|
|
|
@ -1,3 +1,121 @@
|
||||||
# models to generate different environments that the car can drive in.
|
# models to generate different environments that the car can drive in.
|
||||||
# This includes terrain, clouds, wind, solar conditions, and the route along the terrain.
|
# This includes terrain, clouds, wind, solar conditions, and the route along the terrain.
|
||||||
|
|
||||||
|
|
||||||
|
import jax
|
||||||
|
import jax.numpy as jnp
|
||||||
|
from jax import random
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from functools import partial
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
from typing import NamedTuple
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class TerrainParams(NamedTuple):
|
||||||
|
size: int = 256
|
||||||
|
octaves: int = 6
|
||||||
|
persistence: float = 0.5
|
||||||
|
lacunarity: float = 2.0
|
||||||
|
seed: int = 42
|
||||||
|
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
# assume a and b are pairs of numbers
|
||||||
|
x = jnp.array([0,1])
|
||||||
|
f = jnp.array([a,b])
|
||||||
|
return jnp.interp(t, x, f)
|
||||||
|
|
||||||
|
# @partial(jax.jit, static_argnums=(2,))
|
||||||
|
# def _make_noise_layer(key: random.PRNGKey, frequency: float, shape) -> jnp.ndarray:
|
||||||
|
#
|
||||||
|
# noise = random.normal(key, shape)
|
||||||
|
# # create the grid.
|
||||||
|
# x = jnp.linspace(0, shape[0] - 1, )
|
||||||
|
|
||||||
|
import jax
|
||||||
|
import jax.numpy as jnp
|
||||||
|
|
||||||
|
def generate_permutation():
|
||||||
|
"""Generate a permutation table."""
|
||||||
|
p = jnp.arange(256, dtype=jnp.int32)
|
||||||
|
return jnp.concatenate([p, p])
|
||||||
|
|
||||||
|
@jax.jit
|
||||||
|
def fade(t):
|
||||||
|
"""Fade function for smooth interpolation."""
|
||||||
|
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
@jax.jit
|
||||||
|
def lerp(t, a, b):
|
||||||
|
"""Linear interpolation."""
|
||||||
|
return a + t * (b - a)
|
||||||
|
|
||||||
|
@jax.jit
|
||||||
|
def grad(hash, x, y):
|
||||||
|
"""Calculate gradient."""
|
||||||
|
h = hash & 15
|
||||||
|
grad_x = jnp.where(h < 8, x, y)
|
||||||
|
grad_y = jnp.where(h < 4, y, jnp.where((h == 12) | (h == 14), x, y))
|
||||||
|
return jnp.where(h & 1, -grad_x, grad_x) + jnp.where(h & 2, -grad_y, grad_y)
|
||||||
|
|
||||||
|
def perlin(pos):
|
||||||
|
""" Perlin noise. Shape (N) where N = n_dims (2,3) """
|
||||||
|
|
||||||
|
cellpos = pos % 1.0 # get the position inside the cell
|
||||||
|
|
||||||
|
upos = fade(pos)
|
||||||
|
|
||||||
|
@jax.jit
|
||||||
|
def perlin_noise_2d(x, y, p):
|
||||||
|
"""Generate 2D Perlin noise value."""
|
||||||
|
# Floor coordinates
|
||||||
|
xi = jnp.floor(x).astype(jnp.int32) & 255
|
||||||
|
yi = jnp.floor(y).astype(jnp.int32) & 255
|
||||||
|
|
||||||
|
# Fractional coordinates
|
||||||
|
xf = x - jnp.floor(x)
|
||||||
|
yf = y - jnp.floor(y)
|
||||||
|
|
||||||
|
# Fade curves
|
||||||
|
u = fade(xf)
|
||||||
|
v = fade(yf)
|
||||||
|
|
||||||
|
# Hash coordinates of cube corners
|
||||||
|
aa = p[p[xi] + yi]
|
||||||
|
ab = p[p[xi] + yi + 1]
|
||||||
|
ba = p[p[xi + 1] + yi]
|
||||||
|
bb = p[p[xi + 1] + yi + 1]
|
||||||
|
|
||||||
|
# Gradients
|
||||||
|
g1 = grad(aa, xf, yf)
|
||||||
|
g2 = grad(ba, xf - 1, yf)
|
||||||
|
g3 = grad(ab, xf, yf - 1)
|
||||||
|
g4 = grad(bb, xf - 1, yf - 1)
|
||||||
|
|
||||||
|
# Interpolate
|
||||||
|
x1 = lerp(u, g1, g2)
|
||||||
|
x2 = lerp(u, g3, g4)
|
||||||
|
return lerp(v, x1, x2)
|
||||||
|
|
||||||
|
def generate_noise_grid(width, height, scale=50.0):
|
||||||
|
"""Generate a grid of Perlin noise values."""
|
||||||
|
p = generate_permutation()
|
||||||
|
# compute the gradient grid.
|
||||||
|
gradgrid =
|
||||||
|
x = jnp.linspace(0, width/scale, width)
|
||||||
|
y = jnp.linspace(0, height/scale, height)
|
||||||
|
X, Y = jnp.meshgrid(x, y)
|
||||||
|
return perlin_noise_2d(X, Y, p)
|
||||||
|
|
||||||
|
# Example usage:
|
||||||
|
key = jax.random.PRNGKey(23)
|
||||||
|
noise = generate_noise_grid(256, 256)
|
||||||
|
plt.imshow(noise)
|
||||||
|
plt.savefig("output.png")
|
||||||
|
|
||||||
|
|
||||||
|
def GymV1():
|
||||||
|
""" Makes a version 1 gym - simply an elevation profile. """
|
||||||
|
|
||||||
|
|
34
src/solarcarsim/gym.py
Normal file
34
src/solarcarsim/gym.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
import numpy as np
|
||||||
|
import gymnasium as gym
|
||||||
|
from solarcarsim.physsim import CarParams, DefaultCar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SolarRaceV1(gym.Env):
|
||||||
|
""" A primitive hill climber. Aims to solve the given route optimizing
|
||||||
|
for energy usage and on-time arrival. Does not have wind or cloud simulations.
|
||||||
|
Does simulate drag, rolling resistance, and slope power. The action space is the
|
||||||
|
velocity of the car.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, car: CarParams = DefaultCar(), terrain = None, timestep: float = 1.0):
|
||||||
|
# TODO: terrain parameters
|
||||||
|
|
||||||
|
# car max speed.
|
||||||
|
self.params = {
|
||||||
|
"timestep": timestep,
|
||||||
|
"car": car
|
||||||
|
}
|
||||||
|
|
||||||
|
self.observation_space = gym.spaces.Dict({
|
||||||
|
"position": gym.spaces.Box(0, 1000.0, shape=(1,)),
|
||||||
|
"time": gym.spaces.Box(0,1000),
|
||||||
|
"energy": gym.spaces.Box(-1.0e6, 0.)
|
||||||
|
# TODO: add the elevation profile to the observations.
|
||||||
|
})
|
||||||
|
|
||||||
|
self.action_space = gym.spaces.Box(0, 5.0, shape=(1,)) #velocity, m/s
|
137
src/solarcarsim/perlin.py
Normal file
137
src/solarcarsim/perlin.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import jax
|
||||||
|
import jax.numpy as jnp
|
||||||
|
from jax import grad, jit, vmap
|
||||||
|
from functools import partial
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
|
def generate_permutation(key: jax.random.PRNGKey, size: int = 256) -> jnp.ndarray:
|
||||||
|
"""Generate a random permutation table."""
|
||||||
|
perm = jax.random.permutation(key, size)
|
||||||
|
# Double the permutation to avoid need for wrapping
|
||||||
|
return jnp.concatenate([perm, perm])
|
||||||
|
|
||||||
|
@partial(jit, static_argnums=(1,))
|
||||||
|
def generate_gradients(key: jax.random.PRNGKey, dim: int, size: int = 256) -> jnp.ndarray:
|
||||||
|
"""Generate random unit vectors for gradient table."""
|
||||||
|
# Generate random vectors
|
||||||
|
grads = jax.random.normal(key, (size, dim))
|
||||||
|
# Normalize to unit vectors
|
||||||
|
grads = grads / jnp.linalg.norm(grads, axis=1, keepdims=True)
|
||||||
|
# Double the gradients to avoid need for wrapping
|
||||||
|
return jnp.concatenate([grads, grads], axis=0)
|
||||||
|
|
||||||
|
@jit
|
||||||
|
def fade(t: jnp.ndarray) -> jnp.ndarray:
|
||||||
|
"""Fade function 6t^5 - 15t^4 + 10t^3."""
|
||||||
|
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
def perlin_noise(coords: jnp.ndarray,
|
||||||
|
gradients: jnp.ndarray,
|
||||||
|
perm: jnp.ndarray,
|
||||||
|
octaves: int = 1,
|
||||||
|
persistence: float = 0.5,
|
||||||
|
lacunarity: float = 2.0,
|
||||||
|
ridged: bool = False,
|
||||||
|
billow: bool = False) -> jnp.ndarray:
|
||||||
|
"""
|
||||||
|
Generate N-dimensional Perlin noise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coords: Array of shape (..., dim) containing coordinates
|
||||||
|
gradients: Gradient vectors of shape (size, dim)
|
||||||
|
perm: Permutation table
|
||||||
|
octaves: Number of octaves for fractal noise
|
||||||
|
persistence: Amplitude multiplier for each octave
|
||||||
|
lacunarity: Frequency multiplier for each octave
|
||||||
|
ridged: If True, generates ridged multifractal noise
|
||||||
|
billow: If True, generates billow noise (squared)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of noise values
|
||||||
|
"""
|
||||||
|
def single_noise(p):
|
||||||
|
# Get integer coordinates
|
||||||
|
pi = jnp.floor(p).astype(jnp.int32)
|
||||||
|
|
||||||
|
# Get decimal part
|
||||||
|
pf = p - pi
|
||||||
|
|
||||||
|
# Generate corner coordinates
|
||||||
|
corners = jnp.stack(jnp.meshgrid(
|
||||||
|
*[jnp.array([0, 1]) for _ in range(p.shape[0])],
|
||||||
|
indexing='ij'
|
||||||
|
)).reshape(p.shape[0], -1).T
|
||||||
|
|
||||||
|
# Get gradients for each corner
|
||||||
|
corner_gradients = jnp.zeros((corners.shape[0], p.shape[0]))
|
||||||
|
|
||||||
|
for i in range(corners.shape[0]):
|
||||||
|
corner_coords = pi + corners[i]
|
||||||
|
# Hash coordinates to get gradient index
|
||||||
|
index = corner_coords[0]
|
||||||
|
for j in range(1, p.shape[0]):
|
||||||
|
index = perm[index] + corner_coords[j]
|
||||||
|
index = perm[index] % (gradients.shape[0] // 2)
|
||||||
|
corner_gradients = corner_gradients.at[i].set(gradients[index])
|
||||||
|
|
||||||
|
# Calculate dot products
|
||||||
|
vectors = pf - corners
|
||||||
|
dots = jnp.sum(vectors * corner_gradients, axis=1)
|
||||||
|
|
||||||
|
# Interpolate
|
||||||
|
fade_coords = fade(pf)
|
||||||
|
for i in range(p.shape[0]):
|
||||||
|
n_corners = 2 ** i
|
||||||
|
for j in range(0, dots.shape[0], n_corners * 2):
|
||||||
|
t = fade_coords[i]
|
||||||
|
dots = dots.at[j:j+n_corners].set(
|
||||||
|
dots[j:j+n_corners] + t * (dots[j+n_corners:j+2*n_corners] - dots[j:j+n_corners])
|
||||||
|
)
|
||||||
|
|
||||||
|
return dots[0]
|
||||||
|
|
||||||
|
# Vectorize over multiple coordinates
|
||||||
|
noise_fn = vmap(single_noise)
|
||||||
|
|
||||||
|
# Initialize accumulator
|
||||||
|
result = jnp.zeros(coords.shape[:-1])
|
||||||
|
frequency = 1.0
|
||||||
|
amplitude = 1.0
|
||||||
|
max_value = 0.0
|
||||||
|
|
||||||
|
# Generate fractal noise
|
||||||
|
for _ in range(octaves):
|
||||||
|
noise = noise_fn(coords * frequency)
|
||||||
|
|
||||||
|
if ridged:
|
||||||
|
noise = 1.0 - jnp.abs(noise)
|
||||||
|
elif billow:
|
||||||
|
noise = noise * noise
|
||||||
|
|
||||||
|
result += noise * amplitude
|
||||||
|
max_value += amplitude
|
||||||
|
|
||||||
|
frequency *= lacunarity
|
||||||
|
amplitude *= persistence
|
||||||
|
|
||||||
|
# Normalize
|
||||||
|
result /= max_value
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Example usage for 1D noise
|
||||||
|
key = jax.random.PRNGKey(0)
|
||||||
|
key1, key2 = jax.random.split(key)
|
||||||
|
|
||||||
|
# Generate tables
|
||||||
|
perm = generate_permutation(key1)
|
||||||
|
gradients = generate_gradients(key2, dim=1)
|
||||||
|
|
||||||
|
# Generate 1D coordinates
|
||||||
|
x = jnp.linspace(0, 10, 1000)
|
||||||
|
coords = jnp.expand_dims(x, axis=1)
|
||||||
|
|
||||||
|
# Generate different types of noise
|
||||||
|
basic_noise = perlin_noise(coords, gradients, perm)
|
||||||
|
fractal_noise = perlin_noise(coords, gradients, perm, octaves=6)
|
||||||
|
ridged_noise = perlin_noise(coords, gradients, perm, octaves=6, ridged=True)
|
||||||
|
billow_noise = perlin_noise(coords, gradients, perm, octaves=6, billow=True)
|
|
@ -1,16 +1,133 @@
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
|
from jax import grad, jit, vmap, lax
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
def calculate_energy_consumption(car_params, elevation_profile):
|
from typing import NamedTuple, Tuple
|
||||||
|
|
||||||
|
class MotorParams(NamedTuple):
|
||||||
|
kv: float
|
||||||
|
kt: float
|
||||||
|
resistance: float
|
||||||
|
friction_coeff: float
|
||||||
|
iron_coeff: float
|
||||||
|
|
||||||
|
|
||||||
|
class BatteryParams(NamedTuple):
|
||||||
|
shape: Tuple[int, int] # (series,parallel) array of batteries
|
||||||
|
resistance: float # ohms
|
||||||
|
initial_energy: float # joules
|
||||||
|
|
||||||
|
class CarParams(NamedTuple):
|
||||||
|
""" Physical Data for Solar Car Parameters """
|
||||||
|
mass: float = 800 # kg
|
||||||
|
frontal_area: float = 1.3 # m^2
|
||||||
|
drag_coeff: float = 0.018 # drag coefficient, dimensionless
|
||||||
|
rolling_coeff: float = 0.002 # rolling resistance.
|
||||||
|
moter_eff: float = 0.93 # 0 < x < 1 scaling factor
|
||||||
|
solar_area: float = 5.0 # m^2, typically 5.0
|
||||||
|
solar_eff: float = 0.20 # 0 < x < 1, typically ~.25
|
||||||
|
n_motors: int = 2 # how many motors we have.
|
||||||
|
motor: MotorParams = MotorParams(8.43, 1.1, 100.0, 0.001, 0.001) # mitsuba m2090 estimate
|
||||||
|
battery: BatteryParams = BatteryParams((36,19), 0.0126, 66.6e3) # freebasing 50s pack.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def DefaultCar() -> CarParams:
|
||||||
|
""" Creates a basic car """
|
||||||
|
return CarParams(1000, 1.3, 0.18, 0.002, 0.85, 5.0, 0.23)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# some physics equations using jax
|
||||||
|
|
||||||
|
|
||||||
|
@jit
|
||||||
|
def normal_force(mass, theta):
|
||||||
|
return mass * 9.8 * jnp.cos(theta)
|
||||||
|
|
||||||
|
@jit
|
||||||
|
def downslope_force(mass, theta):
|
||||||
|
return mass * 9.8 * jnp.sin(theta)
|
||||||
|
|
||||||
|
@partial(jit, static_argnames=['crr'])
|
||||||
|
def rolling_force(mass, theta, crr):
|
||||||
|
return normal_force(mass, theta) * crr
|
||||||
|
|
||||||
|
@partial(jit, static_argnames=['area', 'cd', 'rho'])
|
||||||
|
def drag_force(u, area, cd, rho):
|
||||||
|
return 0.5 * rho * jnp.pow(u, 2) * cd * area
|
||||||
|
|
||||||
|
# we can use those forces above to determine what forces we have to overcome. Sum(F)=0
|
||||||
|
|
||||||
|
@partial(jit, static_argnums=(3,4,5,6,7,))
|
||||||
|
def bldc_power_draw(torque, velocity, resistance=0.1, kt=0.1,
|
||||||
|
Cf=0.01, iron_loss_coeff=0.005):
|
||||||
"""
|
"""
|
||||||
Calculate energy consumption based on car parameters and elevation profile.
|
Approximates power draw of a BLDC motor outputting a torque at a given velocity
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
car_params (dict): Dictionary containing car parameters.
|
torq: Applied force in Newton/meters
|
||||||
elevation_profile (list): List of elevation points.
|
velocity: Angular velocity in rad/s
|
||||||
|
resistance: Motor phase resistance (ohms)
|
||||||
|
kt: Torque constant (N⋅m/A)
|
||||||
|
friction_coeff: Mechanical friction coefficient
|
||||||
|
iron_loss_coeff: Iron loss coefficient (core losses)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
float: Total energy consumed.
|
Total electrical power draw in Watts
|
||||||
"""
|
"""
|
||||||
# Placeholder for actual calculations
|
|
||||||
return 0.0
|
# Current required for torque (simplified relationship)
|
||||||
|
current = torque / kt
|
||||||
|
|
||||||
|
# Copper losses (I²R)
|
||||||
|
copper_losses = resistance * current**2
|
||||||
|
# Mechanical friction losses
|
||||||
|
friction_losses = Cf * velocity**2
|
||||||
|
# Iron losses (simplified model - primarily dependent on speed)
|
||||||
|
iron_losses = iron_loss_coeff * velocity**2
|
||||||
|
# Mechanical power output
|
||||||
|
mechanical_power = torque * velocity
|
||||||
|
|
||||||
|
# Total electrical power input
|
||||||
|
total_power = mechanical_power + copper_losses + friction_losses + iron_losses
|
||||||
|
|
||||||
|
return total_power
|
||||||
|
|
||||||
|
@partial(jit, static_argnames=['resistance', 'kt', 'kv', 'vmax', 'Cf'])
|
||||||
|
def bldc_torque(velocity, current_limit, resistance, kt, kv, vmax, Cf):
|
||||||
|
|
||||||
|
bemf = velocity / kv
|
||||||
|
v_avail = jnp.clip(vmax - bemf, 0.0, vmax)
|
||||||
|
current = jnp.clip(v_avail / resistance, 0.0, current_limit)
|
||||||
|
|
||||||
|
torque = kt * current
|
||||||
|
friction_torque = Cf * velocity
|
||||||
|
net_torque = jnp.maximum(torque - friction_torque, 0.0)
|
||||||
|
stall_torque = kt * current_limit
|
||||||
|
return jnp.where(velocity < 0.01, stall_torque, net_torque)
|
||||||
|
|
||||||
|
@partial(jit, static_argnums=(2,3,))
|
||||||
|
def battery_powerloss(current,cell_r, battery_shape: Tuple[int,int]):
|
||||||
|
r_array = jnp.full(battery_shape, cell_r)
|
||||||
|
branch_current = current / battery_shape[1]
|
||||||
|
I_array = jnp.full(battery_shape, branch_current)
|
||||||
|
cell_Ploss = jnp.square(I_array) * r_array
|
||||||
|
return jnp.sum(cell_Ploss)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def forward(state, timestep, control, params: CarParams):
|
||||||
|
# state is (position, time, velocity, energy)
|
||||||
|
# control is -1 to 1 (motor max current percent.)
|
||||||
|
# timestep is >0 time to advance
|
||||||
|
# params is the params dictionary.
|
||||||
|
# returns the next state with (position', time + timestep, velocity', energy')
|
||||||
|
# TODO: terrain, weather, solar
|
||||||
|
|
||||||
|
# determine the forces acting on the car.
|
||||||
|
dragf = drag_force(state[2], params.frontal_area, params.drag_coeff, 1.184)
|
||||||
|
rollf = rolling_force(params.mass, 0, params.rolling_coeff)
|
||||||
|
hillforce = downslope_force(params.mass, 0)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
import pyray as ray
|
Loading…
Reference in a new issue