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 = [
|
||||
{name = "saji", email = "saji@saji.dev"},
|
||||
]
|
||||
dependencies = ["pyqtgraph>=0.13.7", "jax>=0.4.35", "pytest>=8.3.3"]
|
||||
requires-python = ">=3.12"
|
||||
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.10,<3.13"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
|
@ -17,3 +17,16 @@ build-backend = "pdm.backend"
|
|||
|
||||
[tool.pdm]
|
||||
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.
|
||||
# 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
|
||||
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:
|
||||
car_params (dict): Dictionary containing car parameters.
|
||||
elevation_profile (list): List of elevation points.
|
||||
torq: Applied force in Newton/meters
|
||||
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:
|
||||
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