From fa7ba28893746238a4c2bbb15473ef67c9325ccc Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:18:16 -0800 Subject: [PATCH 1/8] notes on sim todo --- Docs/SimulationTodo.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Docs/SimulationTodo.md diff --git a/Docs/SimulationTodo.md b/Docs/SimulationTodo.md new file mode 100644 index 0000000..baa9816 --- /dev/null +++ b/Docs/SimulationTodo.md @@ -0,0 +1,27 @@ +# Simulation Todo + +1. Better drag model that takes into account aeropackage (FS-3) +1. Individual wheel models + 1. Suspension + 1. Travel (x) + 1. Velocity (v) + 1. How will this react under acceleration in any direction (steering causes lateral acceleration) (throttle/brakes causes longitudinal acceleration) + 1. Wheel + 1. Brake temp (more complex soon) + 1. Wheel temp (more complex soon) + 1. Wheel rpm/speed +1. Differential + Drivetrain losses + 1. Energy loss due to chain, tripods, axle, hub/upright rubbing + 1. Model rolling resistance better + 1. Model limited slip differential + 1. Log losses so we have an idea of energy loss in the drivetrain +1. Motor Efficiency + Heating + 1. Function of temp and current draw + 1. Keep track of losses + 1. Efficiency loss goes into heat of motor. Need an estimate of its thermal mass and then change in temp. (Motor temp new var) +1. Cleaner logging + 1. Log everything without having to add more rows constantly + 1. Keep efficiency in mind +1. Tractive system heat generation (Not acc) + 1. Estimate how much heat is generated in the accumulator + 1. Not high priority unless we can get more data. From 0b688069916d9a5b3b19421924efe887973925e2 Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:08:37 -0800 Subject: [PATCH 2/8] more notes --- Data/temp.py | 39 +++--------------------------- Docs/SimulationTodo.md | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/Data/temp.py b/Data/temp.py index 587d9e0..9b5c197 100644 --- a/Data/temp.py +++ b/Data/temp.py @@ -3,8 +3,8 @@ import cantools.database as db from Data.DataDecoding_N_CorrectionScripts.dataDecodingFunctions import * -from Data.AnalysisFunctions import * -from Data.integralsAndDerivatives import * +from Data.FSLib.AnalysisFunctions import * +from Data.FSLib.IntegralsAndDerivatives import * from scipy.interpolate import CubicSpline dbcPath = "../fs-3/CANbus.dbc" @@ -70,38 +70,7 @@ etcRTDButton = "ETC_STATUS_RTD_BUTTON" etcBrakeVoltage = "ETC_STATUS_BRAKE_SENSE_VOLTAGE" -df = read("C:/Projects/FormulaSlug/fs-data/FS-3/10112025/firstDriveMCError30.parquet") -df = df.with_columns( - df["timestamp"].alias("Time") -) +df = read("C:/Projects/FormulaSlug/fs-data/FS-3/10082025/fixed_wheels_nathaniel_inv_test_w_fault.parquet") -df = read("C:/Projects/FormulaSlug/fs-data/FS-3/10112025/firstDriveMCError30-filled-null.parquet") -df = df.with_columns( - simpleTimeCol(df) -) - -fig = plt.figure() -ax = fig.add_subplot(111) - -ax.plot(df[t], df[frT], label=frT, c="blue") -ax.plot(df[t], df[flT], label=flT, c="red") -ax.plot(df[t], df[brT], label=brT, c="orange") -ax.plot(df[t], df[blT], label=blT, c="cyan") -ax.set_title("Suspension Travel during First Drive with MC Fault") -ax.set_xlabel("Time") -ax.set_ylabel("Suspension Travel (mm)") -ax.legend() -plt.show() - - -dfNullless = df.drop_nulls(subset=[frT, flT, brT, blT]) - -cs = CubicSpline(dfNullless[t], dfNullless[frT]) - -fig = plt.figure() -ax = fig.add_subplot(111) - -ax.scatter(dfNullless[t], cs(dfNullless[t]), label=frT, s=0.5) -ax.scatter(dfNullless[t], in_place_derive(cs(dfNullless[t])), label=f"Derived {frT}", s=0.5) -ax.legend() +plt.plot(df[busV]) plt.show() diff --git a/Docs/SimulationTodo.md b/Docs/SimulationTodo.md index baa9816..ef03388 100644 --- a/Docs/SimulationTodo.md +++ b/Docs/SimulationTodo.md @@ -25,3 +25,58 @@ 1. Tractive system heat generation (Not acc) 1. Estimate how much heat is generated in the accumulator 1. Not high priority unless we can get more data. +1. Steering model +1. Suspension Model + + +# New simulation architecture idea + + +```python +# Dynamic Vars +posX = 0 +posY = 1 +velX = 2 +velY = 3 +accelX = 4 +accelY = 5 + +arr = np.array((simSteps, 6+1)) +arr[0] = step0 + +def step(): + newPosX = step[posX] + step[velX] * t + +for i in range(simSteps): + arr[i+1] = step(arr[i]) +``` + +```python +# Dictionary Idea #1 +# Array of dictionaries where each time step gets its own dictionary +# Trivial to access a specific thing from any row + +arr = np.array((simSteps)) +arr[0] = step0 + +def step(): + newPosX = arr[posX] + arr[velX] * t + +for i in range(simSteps): + arr[i+1] = step(arr[i]) +``` + +```python +# Dictionary Idea #2 +# Dictionary of arrays. Each key is a column and each array is the length of the simulation +# Trivial to access columns which is typically how we access data + +arr = np.array((simSteps)) +arr[0] = step0 + +def step(): + newPosX = arr[posX] + arr[velX] * t + +for i in range(simSteps): + arr[i+1] = step(arr[i]) +``` \ No newline at end of file From b5aaf4c728f98dadeeecd6cf4ca91cd83bac18c5 Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:22:54 -0800 Subject: [PATCH 3/8] trailing commas and removed unused import --- FullVehicleSim/params.json5 | 4 ++-- FullVehicleSim/state.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FullVehicleSim/params.json5 b/FullVehicleSim/params.json5 index 32daac1..8f7cee4 100644 --- a/FullVehicleSim/params.json5 +++ b/FullVehicleSim/params.json5 @@ -46,7 +46,7 @@ "brakeSurfaceArea": 0.001180643, "brakepadThickness": 0.007874, "brakeMass": 0.408, - "maxBrakeForce": 1500 + "maxBrakeForce": 1500, }, "Magic": { "shape-factor": 0.696268618106842, @@ -149,6 +149,6 @@ "pressureYA": -3.086921788053587e-05, "pressureYB": -9.812999633140862e-05, "pressureYC": 0.9998338222503662, - "gysign": -0.10000000149011612 + "gysign": -0.10000000149011612, } } diff --git a/FullVehicleSim/state.py b/FullVehicleSim/state.py index 7976733..8bc12c4 100644 --- a/FullVehicleSim/state.py +++ b/FullVehicleSim/state.py @@ -1,5 +1,5 @@ import numpy as np -from paramLoader import Parameters, Magic +from paramLoader import Parameters from dataclasses import dataclass @dataclass From 66b9a820ccfb68c91c76619ffa215cca88299b2a Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:39:48 -0800 Subject: [PATCH 4/8] making some progress --- FullVehicleSim/Mech/braking.py | 27 +++++++++--------- FullVehicleSim/Mech/general.py | 10 +++---- FullVehicleSim/engine.py | 51 ++++------------------------------ FullVehicleSim/main.py | 23 ++++++--------- FullVehicleSim/paramLoader.py | 42 ++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 78 deletions(-) diff --git a/FullVehicleSim/Mech/braking.py b/FullVehicleSim/Mech/braking.py index 688d8e4..480f13e 100644 --- a/FullVehicleSim/Mech/braking.py +++ b/FullVehicleSim/Mech/braking.py @@ -1,5 +1,5 @@ from Mech import brakepadFrictionModel -from paramLoader import Parameters, Magic +from paramLoader import * import numpy as np from state import VehicleState # Docs: @@ -9,46 +9,47 @@ def brakePSI_toNewtons(psi:float) -> float: return psi * Parameters["brakeCaliperArea"] * 4.448222 # lb force to Newtons -def calcBrakeForce(prevWorld:VehicleState, inputs) -> tuple[float,float]: +def calcBrakeForce(worldArray:np.ndarray, step:int) -> tuple[float,float]: """ Calculate the brake force. FrictionCoeff(temp) * maxBrakeForce * 4 (for 4 wheels) - :param prevWorld: World State Previous + :param worldArray: World State Array + :param step: Current step index :return: Brake Force """ - frontBrakePSI = inputs[1] - rearBrakePSI = inputs[2] + frontBrakePSI = worldArray[step, varBrakePressureFront] + rearBrakePSI = worldArray[step, varBrakePressureRear] frontBrakeForce = brakePSI_toNewtons(frontBrakePSI) rearBrakeForce = brakePSI_toNewtons(rearBrakePSI) # Calculate Brake Force - frontBrakeForce:float = brakepadFrictionModel.calcFrictionCoeff(prevWorld.frontBrakeTemperature) * frontBrakeForce * 2 * Parameters["brakeDiscRadius"] / Parameters["wheelRadius"] - rearBrakeForce:float = brakepadFrictionModel.calcFrictionCoeff(prevWorld.rearBrakeTemperature) * rearBrakeForce * 2 * Parameters["brakeDiscRadius"] / Parameters["wheelRadius"] + frontBrakeForce:float = brakepadFrictionModel.calcFrictionCoeff(worldArray[step-1, varFrontBrakeTemperature]) * frontBrakeForce * 2 * Parameters["brakeDiscRadius"] / Parameters["wheelRadius"] + rearBrakeForce:float = brakepadFrictionModel.calcFrictionCoeff(worldArray[step-1, varRearBrakeTemperature]) * rearBrakeForce * 2 * Parameters["brakeDiscRadius"] / Parameters["wheelRadius"] return frontBrakeForce, rearBrakeForce -def calcBrakeCooling(prevWorld:VehicleState) -> tuple[float,float]: +def calcBrakeCooling(worldArray:np.ndarray, step:int) -> tuple[float,float]: """ Calculate the cooled brake temperature. :param prevWorld: World State :return: Change in Temperature """ - frontBrakeCooling = Parameters["ambientTemperature"] + (prevWorld.frontBrakeTemperature - Parameters["ambientTemperature"]) * np.e ** (-1 / Parameters["stepsPerSecond"]/50.2) - rearBrakeCooling = Parameters["ambientTemperature"] + (prevWorld.rearBrakeTemperature - Parameters["ambientTemperature"]) * np.e ** (-1 / Parameters["stepsPerSecond"]/50.2) + frontBrakeCooling = Parameters["ambientTemperature"] + (worldArray[step-1, varFrontBrakeTemperature] - Parameters["ambientTemperature"]) * np.e ** (-1 / Parameters["stepsPerSecond"]/50.2) + rearBrakeCooling = Parameters["ambientTemperature"] + (worldArray[step-1, varRearBrakeTemperature] - Parameters["ambientTemperature"]) * np.e ** (-1 / Parameters["stepsPerSecond"]/50.2) return frontBrakeCooling, rearBrakeCooling #q = (initTemperature - parameters["ambientTemperature"]) * parameters["brakeMass"] * parameters["brakeSpecificHeatCapacity"] #change = (q * parameters["brakepadThickness"])/(initTemperature * parameters["brakeThermalConductivity"] * parameters["brakeSurfaceArea"] #return initTemperature - change -def calcBrakeHeating(prevWorld:VehicleState, inputs) -> tuple[float,float]: +def calcBrakeHeating(worldArray:np.ndarray, step:int) -> tuple[float,float]: # Calculate Brake Force - frontBrakeForce, rearBrakeForce = calcBrakeForce(prevWorld, inputs) + frontBrakeForce, rearBrakeForce = calcBrakeForce(worldArray, step) # Guess energy increase based on kinetic energy decrease of the vehicle. # Assumption is 100% of kinetic energy lost goes into brake heating. speedChange = (frontBrakeForce + rearBrakeForce) / Parameters["Mass"] / Parameters["stepsPerSecond"] # momentum impulse - energyChange = 0.5 * Parameters["Mass"] * (prevWorld.speed - (prevWorld.speed - speedChange)) + energyChange = 0.5 * Parameters["Mass"] * (worldArray[step-1, varSpeed] - (worldArray[step-1, varSpeed] - speedChange)) tempChange = energyChange/(Parameters["brakeMass"] * Parameters["brakeSpecificHeatCapacity"]) # While this doesn't seem physically intuitive, it is based on the idea that the front and rear brakes share heat based on their contribution to total braking force. diff --git a/FullVehicleSim/Mech/general.py b/FullVehicleSim/Mech/general.py index ea4bb5b..9462ccd 100644 --- a/FullVehicleSim/Mech/general.py +++ b/FullVehicleSim/Mech/general.py @@ -1,5 +1,5 @@ from Mech.traction import calcCorneringStiffness -from paramLoader import Parameters, Magic +from paramLoader import * from state import VehicleState from Mech.braking import calcBrakeForce from Mech.aero import calcDrag @@ -7,12 +7,12 @@ from Mech.tireLoad import calcLoadTransfer import numpy as np -def calcResistiveForces(worldPrev:VehicleState, inputs): - if worldPrev.speed <= 1e-5: # Floating point error +def calcResistiveForces(worldArray:np.ndarray, step:int): + if worldArray[step-1, varSpeed] <= 1e-5: # Floating point error return 0 else: - frontBrakeForce, rearBrakeForce = calcBrakeForce(worldPrev, inputs) - return -1 * (calcDrag(worldPrev) + frontBrakeForce + rearBrakeForce) + frontBrakeForce, rearBrakeForce = calcBrakeForce(worldArray, step) + return -1 * (calcDrag(worldArray, step) + frontBrakeForce + rearBrakeForce) def calculateYawRate(prevWorld:VehicleState, steerAngle:float, initAcceleration:float, heading:np.ndarray, initYawRate:float, timeSinceLastSteer:float): """Calculate the yaw rate of the vehicle at the current state. diff --git a/FullVehicleSim/engine.py b/FullVehicleSim/engine.py index ec1eabe..0747a6d 100644 --- a/FullVehicleSim/engine.py +++ b/FullVehicleSim/engine.py @@ -1,4 +1,4 @@ -from paramLoader import Parameters, Magic +from paramLoader import * import numpy as np from state import VehicleState from Mech.braking import calcBrakeCooling, calcBrakeHeating, calcBrakeForce @@ -27,7 +27,7 @@ def calculateHeading(heading, yaw_rate, time_increment): return np.append(new_heading, 0) -def stepState(worldPrev:VehicleState, inputs): +def stepState(worldArray:np.ndarray, step:int): # Empirically we see that throttle can only go from about 0-.75. # TODO: Update later @@ -37,10 +37,10 @@ def stepState(worldPrev:VehicleState, inputs): delta = 1/Parameters["stepsPerSecond"] maxTraction = 180.0 # Needs a more complex implementation before being used. Potentially something akin to the gaussian kernel of the voltage histeresis model but for acceleration? Or literally based on the suspension travel. - voltage = calcVoltage() # Not yet implemented. Returns 120 for now. - maxPower = calcMaxPower(voltage) # Watts + worldArray[step, varVoltage] = calcVoltage() # Not yet implemented. Returns 120 for now. + worldArray[step, varMaxPower] = calcMaxPower(worldArray[step, varVoltage]) # Watts - resistiveForces = calcResistiveForces(worldPrev, inputs) + worldArray[step, varResistiveForces] = calcResistiveForces(worldArray, step) frontBrakeHeating, rearBrakeHeating = calcBrakeHeating(worldPrev, inputs) frontBrakeCooling, rearBrakeCooling = calcBrakeCooling(worldPrev) frontBrakeTemperature = worldPrev.frontBrakeTemperature + frontBrakeHeating - frontBrakeCooling @@ -70,43 +70,4 @@ def stepState(worldPrev:VehicleState, inputs): frontSlipAngle, rearSlipAngle = calcSlipAngle(worldPrev, inputs) maxWheelTorque = calcMaxWheelTorque(maxMotorTorque) - # cols = ["x", "y", "z", "vX", "vY", "vZ", "speed", - # "headingX", "headingY", "headingZ", - # "yawRate", "frontBrakeTemperature", "rearBrakeTemperature", - # "charge", "drag", "resistiveForces", - # "motorTorque", "motorForce", "netForce", - # "maxTraction", "wheelRotationsHZ", "motorRPM", - # "motorRotationsHZ", "current", - # "maxWheelTorque", "maxPower", "power", - # "voltage", "downForce", - # "frontBrakeForce", "rearBrakeForce", - # "frontBrakeHeating", "rearBrakeHeating", - # "frontBrakeCooling", "rearBrakeCooling", - # "frontSlipAngle", "rearSlipAngle"] - - log:list[float] = [position[0], position[1], position[2], - worldPrev.velocity[0], worldPrev.velocity[1], worldPrev.velocity[2], - worldPrev.speed, - worldPrev.heading[0], worldPrev.heading[1], worldPrev.heading[2], - worldPrev.yawRate, frontBrakeTemperature, rearBrakeTemperature, - charge, drag, resistiveForces, - motorTorque, motorForce, netForce, - maxTraction, worldPrev.wheelRotationsHZ, worldPrev.motorRPM, - worldPrev.motorRotationsHZ, current, - maxWheelTorque, maxPower, power, - voltage, - frontBrakeForce, rearBrakeForce, - frontBrakeHeating, rearBrakeHeating, - frontBrakeCooling, rearBrakeCooling, - frontSlipAngle, rearSlipAngle] - - worldNext = VehicleState( - position=position, - speed=speed, - heading = heading, - charge=charge, - frontBrakeTemperature = frontBrakeTemperature, - rearBrakeTemperature = rearBrakeTemperature, - yawRate = worldPrev.yawRate - ) - return worldNext, log + return None diff --git a/FullVehicleSim/main.py b/FullVehicleSim/main.py index c869c1c..a2cc348 100644 --- a/FullVehicleSim/main.py +++ b/FullVehicleSim/main.py @@ -4,7 +4,7 @@ import argparse import time -from paramLoader import Magic, Parameters +from paramLoader import * from state import * from engine import * @@ -50,10 +50,10 @@ "frontSlipAngle", "rearSlipAngle"] log = np.zeros((totalSteps + 1, len(cols))) - worldArray = np.zeros(totalSteps + 1, dtype=VehicleState) + worldArray = np.zeros((totalSteps + 1, 41), dtype=np.float32) # Set the inital time to 0 if not already 0 - timeSeries = df_controls['time'] - df_controls['time'][0] + timeSeries = df_controls['time'] - df_controls['time'][0] # Normalize to start at 0 # This takes the last time step and copies it out to the end of the simulation duration. # This has the effect of holding the last command constant until the end of the simulation duration. @@ -82,21 +82,14 @@ else: raise Exception("Unsupported interpolation method. Please use 'cubic' or 'linear'.") - worldArray[0] = VehicleState( - position=np.asarray([0,0,0], dtype=np.float32), - speed=0, - heading = np.asarray([1,0,0], dtype=np.float32), - charge=Parameters["vehicleSOC"], - yawRate = 0, - frontBrakeTemperature = Parameters["initialBrakeTemperature"], - rearBrakeTemperature= Parameters["initialBrakeTemperature"] - ) - - timeCol = np.arange(0, Parameters["simulationDuration"] + 1/Parameters["stepsPerSecond"], 1/Parameters["stepsPerSecond"]) + worldArray[0,varCharge] = Parameters["vehicleSOC"] + worldArray[0,varFrontBrakeTemperature] = Parameters["initialBrakeTemperature"] + worldArray[0,varRearBrakeTemperature] = Parameters["initialBrakeTemperature"] + worldArray[:, varTime] = np.arange(0, Parameters["simulationDuration"] + 1/Parameters["stepsPerSecond"], 1/Parameters["stepsPerSecond"]) start = time.time() for i in range(totalSteps): - worldArray[i+1], log[i+1] = stepState(worldArray[i], controlInputs[i]) # Step forward!! + stepState(worldArray, i) # Step forward!! ## This was above the stepState but I moved it down to make it clearer to read. # timeRunning += 1/stepsPerSecond # timeSinceLastSteer += 1/stepsPerSecond diff --git a/FullVehicleSim/paramLoader.py b/FullVehicleSim/paramLoader.py index 8e06320..2fab9c1 100644 --- a/FullVehicleSim/paramLoader.py +++ b/FullVehicleSim/paramLoader.py @@ -6,4 +6,46 @@ Magic = params["Magic"] Parameters = params["Parameters"] del params + +varTime = 0 +varThrottle = 1 +varBrakePressureFront = 2 +varBrakePressureRear = 3 +varSteerAngle = 4 +varPosX = 5 +varPosY = 6 +varPosZ = 7 +varVelX = 8 +varVelY = 9 +varVelZ = 10 +varSpeed = 11 +varHeadingX = 12 +varHeadingY = 13 +varHeadingZ = 14 +varYawRate = 15 +varFrontBrakeTemperature = 16 +varRearBrakeTemperature = 17 +varCharge = 18 +varDrag = 19 +varResistiveForces = 20 +varMotorTorque = 21 +varMotorForce = 22 +varNetForce = 23 +varMaxTraction = 24 +varWheelRotationsHZ = 25 +varMotorRPM = 26 +varMotorRotationsHZ = 27 +varCurrent = 28 +varMaxWheelTorque = 29 +varMaxPower = 30 +varPower = 31 +varVoltage = 32 +varFrontBrakeForce = 33 +varRearBrakeForce = 34 +varFrontBrakeHeating = 35 +varRearBrakeHeating = 36 +varFrontBrakeCooling = 37 +varRearBrakeCooling = 38 +varFrontSlipAngle = 39 +varRearSlipAngle = 40 print("Parameters loaded...") From 6ffbecb61982b7d11f81369b63279f8d4f9f1014 Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:22:34 -0800 Subject: [PATCH 5/8] ok I think it works again --- FullVehicleSim/Electrical/powertrain.py | 21 ++-- FullVehicleSim/Electrical/tractiveBattery.py | 2 +- FullVehicleSim/Mech/aero.py | 9 +- FullVehicleSim/Mech/braking.py | 1 - FullVehicleSim/Mech/general.py | 13 +-- FullVehicleSim/Mech/steering.py | 18 +-- FullVehicleSim/Mech/tireLoad.py | 2 +- .../controls.json | 0 .../simulationControls.csv | 6 + .../simulationControls.csv | 6 - FullVehicleSim/basicViewer.py | 25 ++-- FullVehicleSim/engine.py | 109 ++++++++++++------ FullVehicleSim/main.py | 93 +++++++-------- FullVehicleSim/paramLoader.py | 54 ++++++++- FullVehicleSim/params.json5 | 3 + FullVehicleSim/state.py | 41 ------- FullVehicleSim/visualizeManual.py | 1 - 17 files changed, 217 insertions(+), 187 deletions(-) rename FullVehicleSim/{SimultionControlInputs => SimulationControlInputs}/controls.json (100%) create mode 100644 FullVehicleSim/SimulationControlInputs/simulationControls.csv delete mode 100644 FullVehicleSim/SimultionControlInputs/simulationControls.csv delete mode 100644 FullVehicleSim/state.py diff --git a/FullVehicleSim/Electrical/powertrain.py b/FullVehicleSim/Electrical/powertrain.py index 53d4bd9..f583df7 100644 --- a/FullVehicleSim/Electrical/powertrain.py +++ b/FullVehicleSim/Electrical/powertrain.py @@ -1,24 +1,23 @@ -from state import VehicleState -from paramLoader import Parameters, Magic +import numpy as np +from paramLoader import * -def calcMaxMotorTorque(worldPrev:VehicleState, resistiveForces:float, maxPower:float, maxTractionTorqueAtWheel:float): +def calcMaxMotorTorque(worldArray:np.ndarray, step:int, resistiveForces:float, maxPower:float, maxTractionTorqueAtWheel:float): ''' Motor Torque at the wheel minimum(rpm limited torque, power limited torque, perfect traction torque) ''' ## RPM Limited Torque (Motor Controller limits it to ~ this in practice. Maybe something more like 7490ish) - if worldPrev.motorRPM > 7490: + if worldArray[step-1, varMotorRPM] > 7490: return -1 * resistiveForces * Parameters["wheelRadius"] - if worldPrev.motorRotationsHZ != 0: ## If rolling, torque may be power limited. - maxPowerTorque = maxPower / worldPrev.motorRotationsHZ * Parameters["gearRatio"] + if worldArray[step-1, varMotorRotationsHZ] != 0: ## If rolling, torque may be power limited. + maxPowerTorque = maxPower / worldArray[step-1, varMotorRotationsHZ] * Parameters["gearRatio"] else: ## Avoid divide by 0 error but it's just the same as the max torque that the motor can deliver (180 Nm) maxPowerTorque = 180.0 # Nm at 0 rpm - perfectTractionTorque = Parameters["maxTorque"] - torque = min(perfectTractionTorque, maxPowerTorque, maxTractionTorqueAtWheel/Parameters["gearRatio"]) + torque = min(Parameters["maxTorque"], maxPowerTorque, maxTractionTorqueAtWheel/Parameters["gearRatio"]) return torque -def calcCurrent(power, voltage): +def calcCurrent(power:float, voltage:float) -> float: if (power / voltage) > Parameters["tractiveIMax"]: return Parameters["tractiveIMax"] return power / voltage @@ -29,10 +28,10 @@ def calcMaxWheelTorque(maxMotorTorque): ''' return maxMotorTorque * Parameters["gearRatio"] -def calcMotorForce(maxWheelTorque): +def calcMotorForce(maxWheelTorque:float) -> float: return (maxWheelTorque / Parameters["wheelRadius"]) -def calcMaxPower(voltage): +def calcMaxPower(voltage:float) -> float: return Parameters["tractiveIMax"] * voltage def calcVoltage(): diff --git a/FullVehicleSim/Electrical/tractiveBattery.py b/FullVehicleSim/Electrical/tractiveBattery.py index 7a10b44..d069f2f 100644 --- a/FullVehicleSim/Electrical/tractiveBattery.py +++ b/FullVehicleSim/Electrical/tractiveBattery.py @@ -1,2 +1,2 @@ -from FullVehicleSim.paramLoader import Parameters, Magic +from FullVehicleSim.paramLoader import * diff --git a/FullVehicleSim/Mech/aero.py b/FullVehicleSim/Mech/aero.py index 8be586d..6b5ecbc 100644 --- a/FullVehicleSim/Mech/aero.py +++ b/FullVehicleSim/Mech/aero.py @@ -1,9 +1,8 @@ import numpy as np -from paramLoader import Parameters, Magic -from state import VehicleState +from paramLoader import * -def calcDrag(prevWorld:VehicleState) -> float: - return 0.5 * Parameters["airDensity"] * Parameters["dragCoeffAreaCombo"] * prevWorld.speed**2 +def calcDrag(worldArray:np.ndarray, step:int) -> float: + return 0.5 * Parameters["airDensity"] * Parameters["dragCoeffAreaCombo"] * worldArray[step-1, varSpeed]**2 -def calcDownForce(prevWorld:VehicleState) -> np.ndarray: +def calcDownForce(worldArray:np.ndarray, step:int) -> np.ndarray: return np.asarray([0,0,0,0], dtype=float) diff --git a/FullVehicleSim/Mech/braking.py b/FullVehicleSim/Mech/braking.py index 480f13e..4a15070 100644 --- a/FullVehicleSim/Mech/braking.py +++ b/FullVehicleSim/Mech/braking.py @@ -1,7 +1,6 @@ from Mech import brakepadFrictionModel from paramLoader import * import numpy as np -from state import VehicleState # Docs: # https://docs.google.com/document/d/1oGsGDnY0DEKWpE3S6481A9yZ0F9qUEwWkSXJwTSz4E4/edit?tab=t.2rmbsj26c7w # The goal of these functions are to calculate the net force on the brakes, applied reverse to heading diff --git a/FullVehicleSim/Mech/general.py b/FullVehicleSim/Mech/general.py index 9462ccd..5bad33b 100644 --- a/FullVehicleSim/Mech/general.py +++ b/FullVehicleSim/Mech/general.py @@ -1,6 +1,5 @@ from Mech.traction import calcCorneringStiffness from paramLoader import * -from state import VehicleState from Mech.braking import calcBrakeForce from Mech.aero import calcDrag from Mech.steering import calcSlipAngle, calcYawRate @@ -14,7 +13,7 @@ def calcResistiveForces(worldArray:np.ndarray, step:int): frontBrakeForce, rearBrakeForce = calcBrakeForce(worldArray, step) return -1 * (calcDrag(worldArray, step) + frontBrakeForce + rearBrakeForce) -def calculateYawRate(prevWorld:VehicleState, steerAngle:float, initAcceleration:float, heading:np.ndarray, initYawRate:float, timeSinceLastSteer:float): +def calculateYawRate(worldArray:np.ndarray, step:int, initAcceleration:float, initYawRate:float, timeSinceLastSteer:float): """Calculate the yaw rate of the vehicle at the current state. This function computes the yaw rate by calculating tire loads, slip angles, cornering stiffness, and then applying the vehicle dynamics equations. @@ -32,9 +31,9 @@ def calculateYawRate(prevWorld:VehicleState, steerAngle:float, initAcceleration: ----- Slip ratio is fixed at 0.15. """ - tireLoad = calcLoadTransfer(initAcceleration * heading[0], initAcceleration * heading[1], initYawRate) - slipAngle = calcSlipAngle(initYawRate, prevWorld.velocity, steerAngle, Parameters) + tireLoad = calcLoadTransfer(initAcceleration * worldArray[step-1, varHeadingX], initAcceleration * worldArray[step-1, varHeadingY], initYawRate) + slipAngle = calcSlipAngle(worldArray, step) slipRatio = 0.15 - corneringStiffness = calcCorneringStiffness(tireLoad, slipAngle, slipRatio, prevWorld.speed, 80, 40, Parameters, Magic) # Works but unused - res = calcYawRate(initYawRate, prevWorld.speed, steerAngle, timeSinceLastSteer, corneringStiffness[0], corneringStiffness[1], Parameters) - return res \ No newline at end of file + corneringStiffness = calcCorneringStiffness(tireLoad, slipAngle, slipRatio, worldArray[step-1, varSpeed], 80, 40, Parameters, Magic) # Works but unused + res = calcYawRate(initYawRate, worldArray[step-1, varSpeed], worldArray[step, varSteerAngle], timeSinceLastSteer, corneringStiffness[0], corneringStiffness[1]) + return res diff --git a/FullVehicleSim/Mech/steering.py b/FullVehicleSim/Mech/steering.py index 44d4486..4c06198 100644 --- a/FullVehicleSim/Mech/steering.py +++ b/FullVehicleSim/Mech/steering.py @@ -1,9 +1,8 @@ # Steering model import numpy as np -from state import VehicleState -from paramLoader import Parameters, Magic +from paramLoader import * -def calcSlipAngle(prevWorld:VehicleState, inputs) -> tuple[float,float]: +def calcSlipAngle(worldArray:np.ndarray, step:int) -> tuple[float,float]: """ Calculate Slip Angle Based on yawRate, Velocity, and Steering Angle. @@ -15,15 +14,16 @@ def calcSlipAngle(prevWorld:VehicleState, inputs) -> tuple[float,float]: :param steerAngle: Description :return: (frontSlipAngle, rearSlipAngle) """ - steerAngle = inputs[3] - speed = np.sqrt(prevWorld.velocity[0] ** 2 + prevWorld.velocity[1] ** 2 + prevWorld.velocity[2]**2) - if prevWorld.yawRate == 0 or speed == 0: # WRONG. RELAXATION LENGTH. PROJECT + steerAngle = worldArray[step, varSteerAngle] + speed = worldArray[step-1, varSpeed] + yawRate = worldArray[step-1, varYawRate] + if yawRate == 0 or speed == 0: # WRONG. RELAXATION LENGTH. PROJECT return (0, 0) else: - bodySlip = np.arctan(prevWorld.velocity[1]/prevWorld.velocity[0]) + bodySlip = np.arctan(worldArray[step-1, varVelY]/worldArray[step-1, varVelX]) - frontSlipAngle = calcVirtualSlipAngle() + bodySlip + (Parameters["wheelBase"]*Parameters["frontWeightDist"]/100 * prevWorld.yawRate)/speed - steerAngle - rearSlipAngle = bodySlip - (Parameters["wheelBase"]*(100-Parameters["frontWeightDist"])/100 * prevWorld.yawRate)/speed + frontSlipAngle = calcVirtualSlipAngle() + bodySlip + (Parameters["wheelBase"]*Parameters["frontWeightDist"]/100 * yawRate)/speed - steerAngle + rearSlipAngle = bodySlip - (Parameters["wheelBase"]*(100-Parameters["frontWeightDist"])/100 * yawRate)/speed return (frontSlipAngle, rearSlipAngle) diff --git a/FullVehicleSim/Mech/tireLoad.py b/FullVehicleSim/Mech/tireLoad.py index 13fbe81..5eeb645 100644 --- a/FullVehicleSim/Mech/tireLoad.py +++ b/FullVehicleSim/Mech/tireLoad.py @@ -1,4 +1,4 @@ -from paramLoader import Parameters +from paramLoader import * def calcLoadTransfer(accelerationX, accelerationY, yawVelocity:float) -> tuple[float, float, float, float]: # TODO: add weight transfer for torsional compliancy diff --git a/FullVehicleSim/SimultionControlInputs/controls.json b/FullVehicleSim/SimulationControlInputs/controls.json similarity index 100% rename from FullVehicleSim/SimultionControlInputs/controls.json rename to FullVehicleSim/SimulationControlInputs/controls.json diff --git a/FullVehicleSim/SimulationControlInputs/simulationControls.csv b/FullVehicleSim/SimulationControlInputs/simulationControls.csv new file mode 100644 index 0000000..c19041a --- /dev/null +++ b/FullVehicleSim/SimulationControlInputs/simulationControls.csv @@ -0,0 +1,6 @@ +time,throttle,brakePressureFront,brakePressureRear,steerAngle +0.0,0.0,0.0,0.0,0.0 +10.0,1.0,0.0,0.0,0.0 +20.0,0.0,0.0,0.0,0.0 +30.0,0.0,300.0,300.0,0.0 +40.0,0.0,0.0,0.0,0.0 \ No newline at end of file diff --git a/FullVehicleSim/SimultionControlInputs/simulationControls.csv b/FullVehicleSim/SimultionControlInputs/simulationControls.csv deleted file mode 100644 index dd8b409..0000000 --- a/FullVehicleSim/SimultionControlInputs/simulationControls.csv +++ /dev/null @@ -1,6 +0,0 @@ -time,throttle,brakesFront,brakesRear,steerAngle -0.0,0.0,0.0,0.0,0.0 -10.0,1.0,0.0,0.0,0.0 -20.0,0.0,0.0,0.0,0.0 -30.0,0.0,500.0,1.0,0.0 -40.0,0.0,0.0,0.0,0.0 \ No newline at end of file diff --git a/FullVehicleSim/basicViewer.py b/FullVehicleSim/basicViewer.py index 07f28e7..1687a8e 100644 --- a/FullVehicleSim/basicViewer.py +++ b/FullVehicleSim/basicViewer.py @@ -2,22 +2,17 @@ import matplotlib.pyplot as plt df = pl.read_parquet("FullVehicleSim/simulation_output.parquet") +t = df["time"] -cols = ["x", "y", "z", "vX", "vY", "vZ", "speed", - "headingX", "headingY", "headingZ", - "yawRate", "frontBrakeTemperature", "rearBrakeTemperature", - "charge", "drag", "resistiveForces", - "motorTorque", "motorForce", "netForce", - "maxTraction", "wheelRotationsHZ", "motorRPM", - "motorRotationsHZ", "current", - "maxWheelTorque", "maxPower", "power", - "voltage", - "frontBrakeForce", "rearBrakeForce", - "frontBrakeHeating", "rearBrakeHeating", - "frontBrakeCooling", "rearBrakeCooling", - "frontSlipAngle", "rearSlipAngle"] -plt.plot(df["time"], df["motorForce"], label="motorForce") -plt.plot(df["time"], df["frontBrakeForce"] + df["rearBrakeForce"], label="Brake Force") +plt.plot(t, df["throttle"]*300, label="throttle") +plt.plot(t, df["brakePressureFront"], label="brakesF") +plt.plot(t, df["netForce"], label="netForce") +plt.plot(t, df["motorForce"], label="motorForce") +plt.plot(t, df["motorTorque"], label="motorTorque") +# plt.plot(t, df["motorRPM"], label="motorRPM") +plt.plot(t, df["speed"], label="speed") plt.legend() plt.show() + +df["speed"].max() \ No newline at end of file diff --git a/FullVehicleSim/engine.py b/FullVehicleSim/engine.py index 0747a6d..45abde5 100644 --- a/FullVehicleSim/engine.py +++ b/FullVehicleSim/engine.py @@ -1,6 +1,5 @@ from paramLoader import * import numpy as np -from state import VehicleState from Mech.braking import calcBrakeCooling, calcBrakeHeating, calcBrakeForce from Mech.aero import calcDrag, calcDownForce from Mech.steering import calcSlipAngle @@ -10,9 +9,10 @@ # Vibe coded but it looks about right so idk. # TODO: Verify that this is correct -def calculateHeading(heading, yaw_rate, time_increment): - initial_heading = heading[:2] - rotation_angle = yaw_rate * time_increment +def calculateHeading(worldArray:np.ndarray, step:int) -> np.ndarray: + time_increment = 1/Parameters["stepsPerSecond"] + initial_heading = worldArray[step-1, varHeadingX:varHeadingZ] # Yes this removes Z, we just want X and Y for this simplification + rotation_angle = worldArray[step-1, varYawRate] * time_increment cos_theta = np.cos(rotation_angle) sin_theta = np.sin(rotation_angle) @@ -27,47 +27,82 @@ def calculateHeading(heading, yaw_rate, time_increment): return np.append(new_heading, 0) -def stepState(worldArray:np.ndarray, step:int): +def stepState(worldArray:np.ndarray, step:int) -> np.ndarray: + """ + The order by which things get updated in this function is incredibly important. + If you calculate velocity before you calculate acceleration, + you would just wind up using the 0 that is present in the array at that index. + Update acceleration before you update velocity. This will also fail somewhat + silently so be cautious. - # Empirically we see that throttle can only go from about 0-.75. - # TODO: Update later - # Made it so you can just comment this out when it's fixed. - # Throttle, brakesFront, brakesRear, steering angle - # 0-1, PSI, PSI, Radians + The worldArray will also fail silently if it doen't contain a row before step. + The 0-1 evaluates to -1 so it just grabs the last row in the array instead of the + previous one. + + :param worldArray: The main world array. To work properly, the worldArray needs to contain a row (step) and a previous row (step-1) with the same format as the output of this function. The function will read from the previous row to calculate the new values for the current step. + :type worldArray: np.ndarray + :param step: The current step in the simulation + :type step: int + :return: The updated state array for the current step + :rtype: ndarray[Any, Any] + """ + + # Empirically we see that throttle can only go from about 0-.75. Currently not accounted for. + arr = np.copy(worldArray[step, :]) # This is the array that is updated and then returned. delta = 1/Parameters["stepsPerSecond"] - maxTraction = 180.0 # Needs a more complex implementation before being used. Potentially something akin to the gaussian kernel of the voltage histeresis model but for acceleration? Or literally based on the suspension travel. - worldArray[step, varVoltage] = calcVoltage() # Not yet implemented. Returns 120 for now. - worldArray[step, varMaxPower] = calcMaxPower(worldArray[step, varVoltage]) # Watts + arr[varMaxTraction] = 180.0 # Needs a more complex implementation before being used. Potentially something akin to the gaussian kernel of the voltage histeresis model but for acceleration? Or literally based on the suspension travel. + arr[varVoltage] = calcVoltage() # Not yet implemented. Returns 120 for now. + arr[varMaxPower] = calcMaxPower(arr[varVoltage]) # Watts - worldArray[step, varResistiveForces] = calcResistiveForces(worldArray, step) - frontBrakeHeating, rearBrakeHeating = calcBrakeHeating(worldPrev, inputs) - frontBrakeCooling, rearBrakeCooling = calcBrakeCooling(worldPrev) - frontBrakeTemperature = worldPrev.frontBrakeTemperature + frontBrakeHeating - frontBrakeCooling - rearBrakeTemperature = worldPrev.rearBrakeTemperature + rearBrakeHeating - rearBrakeCooling + arr[varResistiveForces] = calcResistiveForces(worldArray, step) + arr[varFrontBrakeHeating], arr[varRearBrakeHeating] = calcBrakeHeating(worldArray, step) + arr[varFrontBrakeCooling], arr[varRearBrakeCooling] = calcBrakeCooling(worldArray, step) + arr[varFrontBrakeForce], arr[varRearBrakeForce] = calcBrakeForce(worldArray, step) + arr[varFrontBrakeTemperature] = worldArray[step-1, varFrontBrakeTemperature] + arr[varFrontBrakeHeating] - arr[varFrontBrakeCooling] + arr[varRearBrakeTemperature] = worldArray[step-1, varRearBrakeTemperature] + arr[varRearBrakeHeating] - arr[varRearBrakeCooling] - maxMotorTorque = calcMaxMotorTorque(worldPrev, resistiveForces, maxPower, maxTraction) - motorTorque = min(Parameters["maxTorque"]*inputs[0], maxMotorTorque) # Nm + arr[varMaxMotorTorque] = calcMaxMotorTorque(worldArray, step, arr[varResistiveForces], arr[varMaxPower], arr[varMaxTraction]) + arr[varMotorTorque] = min(Parameters["maxTorque"]*arr[varThrottle], arr[varMaxMotorTorque]) # Nm - power = motorTorque * worldPrev.motorRotationsHZ # Watts - motorForce = calcMotorForce(motorTorque) # Newtons - netForce = motorForce + resistiveForces # Newtons + arr[varPower] = arr[varMotorTorque] * worldArray[step-1, varMotorRotationsHZ] # Watts + arr[varMotorForce] = calcMotorForce(arr[varMotorTorque]) # Newtons + arr[varNetForce] = arr[varMotorForce] + arr[varResistiveForces] # Newtons - acceleration = netForce / Parameters["Mass"] # m/s^2 + arr[varAcceleration] = arr[varNetForce] / Parameters["Mass"] # m/s^2 - current = power/voltage # Amps + arr[varCurrent] = arr[varPower] / arr[varVoltage] # Amps - charge = worldPrev.charge - current * delta / 3600.0 - position = worldPrev.position + worldPrev.velocity * delta - speed = max(0, worldPrev.speed + acceleration * delta) # Sometimes braking falls a tad below 0 so we just correct that because otherwise everything breaks - yawRate = worldPrev.yawRate - if inputs[2] == 0: - yawRate = 0 - heading = calculateHeading(worldPrev.heading, yawRate, delta) + arr[varCharge] = worldArray[step-1, varCharge] - worldArray[step, varCurrent] * delta / 3600.0 + arr[varPosX:varPosZ+1] = worldArray[step-1, varPosX:varPosZ+1] + worldArray[step-1, varVelX:varVelZ+1] * delta + arr[varSpeed] = max(0, worldArray[step-1, varSpeed] + arr[varAcceleration] * delta) # Sometimes braking falls a tad below 0 so we just correct that because otherwise everything breaks + arr[varYawRate] = worldArray[step-1, varYawRate] + if worldArray[step, varSteerAngle] == 0: + arr[varYawRate] = 0 + arr[varVelX:varVelZ+1] = arr[varSpeed] * worldArray[step-1, varHeadingX:varHeadingZ+1] + arr[varHeadingX:varHeadingZ+1] = calculateHeading(worldArray, step) - drag = calcDrag(worldPrev) - frontBrakeForce, rearBrakeForce = calcBrakeForce(worldPrev, inputs) - frontSlipAngle, rearSlipAngle = calcSlipAngle(worldPrev, inputs) - maxWheelTorque = calcMaxWheelTorque(maxMotorTorque) + arr[varDrag] = calcDrag(worldArray, step) + + arr[varFrontSlipAngle], arr[varRearSlipAngle] = calcSlipAngle(worldArray, step) + arr[varMaxWheelTorque] = calcMaxWheelTorque(arr[varMaxMotorTorque]) + arr[varWheelRotationsHZ] = arr[varSpeed] / Parameters["wheelCircumferance"] * 2.0 * np.pi + arr[varMotorRotationsHZ] = arr[varWheelRotationsHZ] / Parameters["gearRatio"] + arr[varWheelRPM] = arr[varWheelRotationsHZ] * 60.0 + arr[varMotorRPM] = arr[varWheelRPM] / Parameters["gearRatio"] + return arr - return None +def dynamicStepState(step:np.ndarray) -> np.ndarray: + """ + Interface for simulation step state that takes a single row with inputs and a previous step. + Separates this into 2 rows with the first row being the previous step and the second row being the current inputs. + Then calls stepState to get the new state for the current step. + + :param step: Step you wish to input (Array of all features) + :type step: np.ndarray + :return: Next step in the simulation given your contorl input and vehicle state. + :rtype: ndarray[Any, Any] + """ + arr = np.array([step, step]) + arr[1, 5:] = np.zeros((step.shape[0]-5)) + return stepState(arr, 1) \ No newline at end of file diff --git a/FullVehicleSim/main.py b/FullVehicleSim/main.py index a2cc348..62d0f5f 100644 --- a/FullVehicleSim/main.py +++ b/FullVehicleSim/main.py @@ -5,7 +5,6 @@ import time from paramLoader import * -from state import * from engine import * if __name__ == "__main__": @@ -29,30 +28,19 @@ raise Exception("Please provide a valid simulation controls file path using --simulation_controls or -c") ## Double check it has the correct columns - if df_controls.columns != ['time', 'throttle', 'brakesFront','brakesRear', 'steerAngle']: - raise Exception("Simulation controls file must contain the following columns: 'time', 'throttle', 'brakesFront', 'brakesRear', 'steerAngle'") + if df_controls.columns != ['time', 'throttle', 'brakePressureFront','brakePressureRear', 'steerAngle']: + raise Exception("Simulation controls file must contain the following columns: 'time', 'throttle', 'brakePressureFront', 'brakePressureRear', 'steerAngle'") totalSteps = int(Parameters["stepsPerSecond"] * Parameters["simulationDuration"]) steps = np.arange(0, Parameters["simulationDuration"], 1/Parameters["stepsPerSecond"]) - cols = ["x", "y", "z", "vX", "vY", "vZ", "speed", - "headingX", "headingY", "headingZ", - "yawRate", "frontBrakeTemperature", "rearBrakeTemperature", - "charge", "drag", "resistiveForces", - "motorTorque", "motorForce", "netForce", - "maxTraction", "wheelRotationsHZ", "motorRPM", - "motorRotationsHZ", "current", - "maxWheelTorque", "maxPower", "power", - "voltage", - "frontBrakeForce", "rearBrakeForce", - "frontBrakeHeating", "rearBrakeHeating", - "frontBrakeCooling", "rearBrakeCooling", - "frontSlipAngle", "rearSlipAngle"] - - log = np.zeros((totalSteps + 1, len(cols))) - worldArray = np.zeros((totalSteps + 1, 41), dtype=np.float32) - # Set the inital time to 0 if not already 0 + ## This is structured so the first row is the initial conditions (inputs don't matter and will just be left to 0), and the + ## rest are generated as the simulation progresses. This means that a simulation array will always be 1 longer than just the time steps + ## and duration would indicate. + worldArray = np.zeros((totalSteps + 1, len(VARIABLE_NAMES)), dtype=np.float32) + + # Set the inital time to 0 if not already 0. Eg. [1.79, 2.36, 3.13] becomes [0.0, 0.57, 1.34] timeSeries = df_controls['time'] - df_controls['time'][0] # Normalize to start at 0 # This takes the last time step and copies it out to the end of the simulation duration. @@ -61,8 +49,8 @@ df_controls = df_controls.vstack(pl.DataFrame({ 'time': [Parameters["simulationDuration"]], 'throttle': df_controls["throttle"][-1], - 'brakesFront': df_controls["brakesFront"][-1], - 'brakesRear': df_controls["brakesRear"][-1], + 'brakePressureFront': df_controls["brakePressureFront"][-1], + 'brakePressureRear': df_controls["brakePressureRear"][-1], 'steerAngle': df_controls["steerAngle"][-1]})) timeSeries = df_controls['time'] @@ -76,20 +64,29 @@ elif Parameters["interpolationMethod"] == "linear": controlInputs = np.zeros((len(steps), 4)) controlInputs[:,0] = np.interp(steps, timeSeries, df_controls['throttle']) - controlInputs[:,1] = np.interp(steps, timeSeries, df_controls['brakesFront']) - controlInputs[:,2] = np.interp(steps, timeSeries, df_controls['brakesRear']) + controlInputs[:,1] = np.interp(steps, timeSeries, df_controls['brakePressureFront']) + controlInputs[:,2] = np.interp(steps, timeSeries, df_controls['brakePressureRear']) controlInputs[:,3] = np.interp(steps, timeSeries, df_controls['steerAngle']) else: raise Exception("Unsupported interpolation method. Please use 'cubic' or 'linear'.") + ## Setup initial conditions. Leaves row 0 with no inputs (don't matter anyway since sim runs from 1 -> end) + ## Some other initial conditions based on input parameters. + worldArray[1:, varThrottle] = controlInputs[:,0] + worldArray[1:, varBrakePressureFront] = controlInputs[:,1] + worldArray[1:, varBrakePressureRear] = controlInputs[:,2] + worldArray[1:, varSteerAngle] = controlInputs[:,3] worldArray[0,varCharge] = Parameters["vehicleSOC"] worldArray[0,varFrontBrakeTemperature] = Parameters["initialBrakeTemperature"] worldArray[0,varRearBrakeTemperature] = Parameters["initialBrakeTemperature"] + worldArray[0, varHeadingX:varHeadingZ+1] = Parameters["initHeading"] + worldArray[0, varPosX:varPosZ+1] = Parameters["initPosition"] + worldArray[0, varVelX:varVelZ+1] = Parameters["initVelocity"] worldArray[:, varTime] = np.arange(0, Parameters["simulationDuration"] + 1/Parameters["stepsPerSecond"], 1/Parameters["stepsPerSecond"]) start = time.time() - for i in range(totalSteps): - stepState(worldArray, i) # Step forward!! + for i in range(1, totalSteps): + worldArray[i, :] = stepState(worldArray, i) # Step forward!! ## This was above the stepState but I moved it down to make it clearer to read. # timeRunning += 1/stepsPerSecond # timeSinceLastSteer += 1/stepsPerSecond @@ -109,18 +106,12 @@ # 'cooledBrakeTemperature', 'wheelRPM', 'wheelRotationsHZ', # 'rpm', 'motorRotationsHZ', 'charge', 'voltage', 'current', # 'power', 'maxPower', 'stepSize', 'timeSinceLastSteer'] + # print(VARIABLE_NAMES) - df = pl.DataFrame(log, schema=cols, orient="row") + df = pl.DataFrame(worldArray, schema=VARIABLE_NAMES, orient="row") # print(f"df shape: {df.shape}") # print(f"control inputs shape: {controlInputs.shape}") # print(f"timeCol shape: {timeCol.shape}") - df = df[1:].with_columns( - pl.Series(timeCol[1:]).alias("time"), - pl.Series(controlInputs[:,0]).alias("throttle"), - pl.Series(controlInputs[:,1]).alias("brakesFront"), - pl.Series(controlInputs[:,2]).alias("brakesRear"), - pl.Series(controlInputs[:,3]).alias("steerAngle") - ) df.write_parquet("simulation_output.parquet") @@ -131,30 +122,32 @@ torque = df['motorTorque'] yawRate = df['yawRate'] frontBrakeTemperature = df['frontBrakeTemperature'] - ax1 = plt.subplot(1,4,1) - ax2 = plt.subplot(1,4,2) - ax3 = plt.subplot(1,4,3) - ax4 = plt.subplot(1,4,4) - - ax1.set_title("Current vs Time") + ax1 = plt.subplot(411) + ax11 = ax1.twinx() + ax2 = plt.subplot(412) + ax22 = ax2.twinx() + ax3 = plt.subplot(413) + ax33 = ax3.twinx() + ax4 = plt.subplot(414) + ax44 = ax4.twinx() + + ax1.set_title("I (Blue)/V (Orange) vs Time") ax1.set_xlabel("Time (s)") - ax1.set_ylabel("Current (A)") - ax1.plot(t, current) + ax1.set_ylabel("Current (A) / Voltage (V)") + ax1.plot(t, current, label="Current") + ax11.plot(t, voltage, label="Voltage", color='orange') ax2.set_title("Speed vs Time") ax2.set_xlabel("Time (s)") ax2.set_ylabel("Speed (m/s)") ax2.plot(t, speed) - ax3.set_title("Voltage vs Time") - ax3.set_xlabel("Time (s)") - ax3.set_ylabel("Voltage (V)") - ax3.plot(t, voltage) - - ax3.set_title("Voltage vs Time") + ax3.set_title("Throttle (Blue)/Brakes (Orange) vs Time") ax3.set_xlabel("Time (s)") - ax3.set_ylabel("Voltage (V)") - ax3.plot(t, voltage) + ax3.set_ylabel("Throttle (0-1)") + ax33.set_ylabel("Brake Pressure (PSI)") + ax3.plot(t, df["throttle"], label="Throttle") + ax33.plot(t, df["brakePressureFront"], color='orange') ax4.set_title("rvt") ax4.plot(t, yawRate) diff --git a/FullVehicleSim/paramLoader.py b/FullVehicleSim/paramLoader.py index 2fab9c1..5249d9d 100644 --- a/FullVehicleSim/paramLoader.py +++ b/FullVehicleSim/paramLoader.py @@ -1,12 +1,16 @@ import json5 -Magic:dict -Parameters:dict +from typing import Dict, List, Tuple + +Magic: dict +Parameters: dict with open('params.json5', 'r') as file: params = json5.load(file) Magic = params["Magic"] Parameters = params["Parameters"] del params +# Variable definitions - maintain original order for compatibility + varTime = 0 varThrottle = 1 varBrakePressureFront = 2 @@ -48,4 +52,50 @@ varRearBrakeCooling = 38 varFrontSlipAngle = 39 varRearSlipAngle = 40 +varMaxMotorTorque = 41 +varAcceleration = 42 +varWheelRPM = 43 + +# Automatically generate schema from defined variables +def generate_variable_schema() -> Dict[int, str]: + """ + Generate a schema mapping variable indices to their names. + Preserves the order of definition in the file. + """ + schema = {} + + # Get all variables that start with 'var' from the current module + current_module = globals() + var_items = [(name, value) for name, value in current_module.items() + if name.startswith('var') and isinstance(value, int)] + + # Sort by value to maintain order + var_items.sort(key=lambda x: x[1]) + + # Create the schema + for name, index in var_items: + # Convert variable name to readable format + readable_name = name[3].lower() + name[4:] # Remove 'var' prefix and lowercase first letter + schema[index] = readable_name + + return schema + +def get_variable_names() -> List[str]: + """ + Get ordered list of variable names (without 'var' prefix). + """ + schema = generate_variable_schema() + return [schema[i] for i in range(len(schema))] + +def get_variable_mapping() -> Dict[str, int]: + """ + Get mapping from variable names to indices. + """ + schema = generate_variable_schema() + return {name: index for index, name in schema.items()} + +# Generate the schema on module load +VARIABLE_SCHEMA = generate_variable_schema() +VARIABLE_NAMES = get_variable_names() +VARIABLE_MAPPING = get_variable_mapping() print("Parameters loaded...") diff --git a/FullVehicleSim/params.json5 b/FullVehicleSim/params.json5 index 8f7cee4..99123d2 100644 --- a/FullVehicleSim/params.json5 +++ b/FullVehicleSim/params.json5 @@ -11,6 +11,9 @@ "initialBatteryTemperature": 20, // °C "vehicleSOC": 1.0, // Out of 1 "initSpeed": 0.0, // m/s + "initHeading": [1.0, 0.0, 0.0], // Vector pointing in the direction of the front of the car + "initPosition": [0.0, 0.0, 0.0], // Starting position of the car in the world frame + "initVelocity": [0.0, 0.0, 0.0], // Initial velocity vector in the world frame "airDensity": 1.230, "dragCoeffAreaCombo": 1.0858790012112278, // Combines area and drag coeff into one constant diff --git a/FullVehicleSim/state.py b/FullVehicleSim/state.py deleted file mode 100644 index 8bc12c4..0000000 --- a/FullVehicleSim/state.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np -from paramLoader import Parameters -from dataclasses import dataclass - -@dataclass -class VehicleState: - def __init__(self, position, speed, heading, charge, yawRate, frontBrakeTemperature, rearBrakeTemperature): - self.position:np.ndarray = position - self.speed:float = speed - self.heading:np.ndarray = heading - self.charge:float = charge - self.frontBrakeTemperature:float = frontBrakeTemperature - self.rearBrakeTemperature:float = rearBrakeTemperature - self.yawRate:float = yawRate - - #self.wheelRPM: np.array = np.asarray([0,0,0,0], dtype=np.float32) - #self.wheelRotationsHz: float = self.speed / self.WheelCircumference * 2.0 * np.pi - self.tires:np.ndarray = np.asarray([None, None, None, None])#, dtype=tire.Tire) # [FL, FR, BL, BR] - - @property - def velocity(self): - return self.heading * self.speed - - @property - def wheelRPM(self): - return self.speed / Parameters["wheelCircumferance"] * 60.0 - - @property - def wheelRotationsHZ(self): - return self.speed / Parameters["wheelCircumferance"] * 2.0 * np.pi - @property - def motorRPM(self): - return self.wheelRPM * Parameters["gearRatio"] - - @property - def motorRotationsHZ(self): - return self.wheelRotationsHZ * Parameters["gearRatio"] - - @property - def maxTractionTorqueAtWheel(self): - return Parameters["maxTractionTorque"] * Parameters["wheelRadius"] diff --git a/FullVehicleSim/visualizeManual.py b/FullVehicleSim/visualizeManual.py index 107e30e..5318590 100644 --- a/FullVehicleSim/visualizeManual.py +++ b/FullVehicleSim/visualizeManual.py @@ -3,7 +3,6 @@ import numpy as np import asyncio import platform -from state import VehicleState from engine import stepState #Simulated control inputs (since no file I/O in Pyodide) From e8b7c8bb1a16583de130d8468ac1f14155a2645c Mon Sep 17 00:00:00 2001 From: goob10000 <65315624+goob10000@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:30:57 -0800 Subject: [PATCH 6/8] generated data for gaddiel --- Data/emeterDecode.py | 37 +++++++++++++++++++++-- endur1Curr.csv | 72 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 endur1Curr.csv diff --git a/Data/emeterDecode.py b/Data/emeterDecode.py index 9f93c99..3743894 100644 --- a/Data/emeterDecode.py +++ b/Data/emeterDecode.py @@ -1,5 +1,6 @@ from nptdms import TdmsFile import polars as pl +import numpy as np import matplotlib.pyplot as plt from Data.FSLib.IntegralsAndDerivatives import * from Data.FSLib.AnalysisFunctions import * @@ -9,10 +10,10 @@ autoxDaniel12File = "FS-3/compEmeterData/autoxDaniel12.tdms" accel1 = "FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250620-203307_ ACCEL-EV.tdms" accel2 = "FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250620-205609_ ACCEL-EV.tdms" -endur1 = "FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250621-154731_ ENDUR-EV.tdms" -endur2 = "FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250621-160530_ ENDUR-EV.tdms" +endur1 = "../fs-data/FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250621-154731_ ENDUR-EV.tdms" +endur2 = "../fs-data/FS-3/compEmeterData/216_univ-of-calif---santa-cruz-_250621-160530_ ENDUR-EV.tdms" -dfLaptimes = pl.read_csv("FS-3/compLapTimes.csv") +dfLaptimes = pl.read_csv("../fs-data/FS-3/compLapTimes.csv") firstHalf = dfLaptimes.filter(pl.col("Lap") < 12)["Time"].sum() secondHalf = dfLaptimes.filter(pl.col("Lap") > 11)["Time"].sum() @@ -93,7 +94,37 @@ def fileTodf(path): pl.Series(arr).cast(pl.Int64).alias("Lap") ) +l = [] +for i in np.unique(dfendur1["Lap"]): + # plt.plot(dfendur1.filter(pl.col("Lap") == i)[I]) + l.append(dfendur1.filter(pl.col("Lap") == i)[I]) +plt.show() + +shortest = min([len(x) for x in l]) +l2 = [x[:shortest].alias(f"Current_Lap_{i}") for i, x in enumerate(l)] +df2 = pl.DataFrame(l2) +plt.plot(df2.mean_horizontal()) +plt.show() + +from scipy.fft import fft, ifft + +f = fft(df2.mean_horizontal().to_numpy()) +# freq = np.fft.fftfreq(len(df2.mean_horizontal()), d=0.01) + +plt.plot(np.append(np.log(f[-len(f)//2:]), np.log(f[:len(f)//2]))) +plt.show() + +fFiltered = np.where(np.log(f) > 10, f, 0) +invF = ifft(fFiltered) +plt.plot(invF) +plt.plot(df2.mean_horizontal()) +plt.show() + +dfTableCurrOut = pl.DataFrame({"Current": df2.mean_horizontal().gather_every(100), "Time": np.arange(0, df2.height/100)}) +dfTableCurrOut.write_csv("endur1Curr.csv") + +df2.mean_horizontal() dfendur2 = fileTodf(endur2).filter(pl.col(t) > endur2_StartTime).filter(pl.col(t) < endur2_EndTime) diff --git a/endur1Curr.csv b/endur1Curr.csv new file mode 100644 index 0000000..322bda2 --- /dev/null +++ b/endur1Curr.csv @@ -0,0 +1,72 @@ +Current,Time +24.611364,0.0 +32.075455,1.0 +83.82,2.0 +103.79028,3.0 +1.6792728,4.0 +47.380276,5.0 +156.73936,6.0 +181.45502,7.0 +178.01755,8.0 +266.4711,9.0 +314.56467,10.0 +337.80002,11.0 +123.42737,12.0 +18.743092,13.0 +41.31073,14.0 +23.57373,15.0 +71.3661,16.0 +147.76573,17.0 +38.92582,18.0 +21.131275,19.0 +15.494909,20.0 +40.06673,21.0 +118.78701,22.0 +115.37664,23.0 +147.45909,24.0 +223.76947,25.0 +148.38336,26.0 +78.637184,27.0 +23.648548,28.0 +43.008003,29.0 +58.465275,30.0 +91.7551,31.0 +77.75927,32.0 +24.253273,33.0 +37.92873,34.0 +42.02046,35.0 +48.948368,36.0 +88.985916,37.0 +89.52327,38.0 +99.00618,39.0 +67.61691,40.0 +23.409546,41.0 +59.80364,42.0 +79.542,43.0 +83.21628,44.0 +128.63428,45.0 +151.5899,46.0 +113.44146,47.0 +91.6961,48.0 +24.420546,49.0 +25.824457,50.0 +60.09946,51.0 +69.13337,52.0 +98.356,53.0 +57.09346,54.0 +55.792725,55.0 +43.36391,56.0 +39.913273,57.0 +53.080276,58.0 +73.36591,59.0 +83.82373,60.0 +54.693638,61.0 +52.09064,62.0 +48.891186,63.0 +48.31355,64.0 +57.849186,65.0 +29.795094,66.0 +37.14237,67.0 +22.748,68.0 +34.09791,69.0 +30.930456,70.0 From 22c06986f4442d983b54b5c17e3c65604bad3fac Mon Sep 17 00:00:00 2001 From: arshgupta54 Date: Mon, 6 Apr 2026 14:34:38 -0700 Subject: [PATCH 7/8] Added additional parameters and properties to Tire class: Longitudinal Force, Lateral Force, Vertical Load per Tire, Suspension Travel. --- FullVehicleSim/Mech/tireState.py | 85 +++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/FullVehicleSim/Mech/tireState.py b/FullVehicleSim/Mech/tireState.py index a3ecc5d..9ec394b 100644 --- a/FullVehicleSim/Mech/tireState.py +++ b/FullVehicleSim/Mech/tireState.py @@ -4,7 +4,7 @@ from paramLoader import Parameters, Magic class Tire: - def __init__(self, normalForce, slipRatio, slipAngle, velocityX, pressure, temperature): + def __init__(self, normalForce, slipRatio, slipAngle, velocityX, pressure, temperature, massTire, accelX, accelY, carHeight, frontBack, leftRight, frontAxleFraction, remainingFraction, springConstant, dampingCoeff): self.normalForce = normalForce * -1 self.velocityX = velocityX self.slipRatioInit = slipRatio @@ -14,6 +14,17 @@ def __init__(self, normalForce, slipRatio, slipAngle, velocityX, pressure, tempe self.tireTemperature = temperature self.actPressure = 12 # Actual PSI self.camber = 0 # Radians + self.mass = massTire + self.gravity = 9.81 + self.accelX = accelX + self.accelY = accelY + self.carHeight = carHeight + self.frontBackDist = frontBack + self.leftRightDist = leftRight + self.frontAxleFraction = frontAxleFraction + self.remainingFraction = remainingFraction + self.springConstant = springConstant + self.dampingCoeff = dampingCoeff #if(lat): self.normDeltaLoadLat = self.normalizeLoadLat() @@ -212,3 +223,75 @@ def updateParams(self, normalForce=-1, slipRatio=-1, velocityX=-1): self.slipRatio = slipRatio if velocityX != -1: self.velocityX = velocityX + + + w = (self.mass*self.gravity)/4 ## If tires are the same + + @property + def F_long(self): + f_long = (self.mass * self.accelX * self.carHeight)/self.frontBackDist + return f_long + @property + def F_lat(self): + f_lat = (self.mass * self.accelY * self.carHeight)/self.leftRightDist + return f_lat + @property + def F_front_left(self): + f_front_left = self.remainingFraction(self.frontAxleFraction * self.mass * self.gravity) + return f_front_left + @property + def F_front_right(self): + f_front_right = (1-self.remainingFraction) * (self.frontAxleFraction * self.mass * self.gravity) + return f_front_right + @property + def F_back_left(self): + f_back_left = self.remainingFraction * ((1-self.frontAxleFraction) * self.mass * self.gravity) + return f_back_left + @property + def F_back_right(self): + f_back_right = (1-self.remainingFraction) * ((1-self.frontAxleFraction) * self.mass * self.gravity) + return f_back_right + + ## vertical load on each tire : F_z + @property + def F_zfl(self): + f_z = (F_front_left + (F_long)/2 + (F_lat)/2) + return f_z + + @property + def F_zfr(self): + f_z = (F_front_right + (F_long)/2 - (F_lat)/2) + return f_z + + @property + def F_zbl(self): + f_z = (F_back_left - (F_long)/2 + (F_lat)/2) + return f_z + + @property + def F_zbr(self): + f_z = (F_back_right - (F_long)/2 - (F_lat)/2) + return f_z + + ## suspension travel: + @property + def x_fl(self): + x_fl = (F_zfl - self.dampingCoeff)/self.springConstant + return x_fl + + @property + def x_fr(self): + x_fr = (F_zfr - self.dampingCoeff)/self.springConstant + return x_fr + + @property + def x_bl(self): + x_bl = (F_zbl - self.dampingCoeff)/self.springConstant + return x_bl + + @property + def x_br(self): + x_br = (F_zbr - self.dampingCoeff)/self.SpringConstant + return x_br + + From 2b4a3443ccb45e63e09845726e327b089a7e9164 Mon Sep 17 00:00:00 2001 From: aagarw68-eng Date: Sat, 11 Apr 2026 20:09:23 -0700 Subject: [PATCH 8/8] Document tire model coefficients and load transfer equations Added detailed explanations and formulas for static corner weight, longitudinal and lateral load transfer, static axle and corner loads, dynamic vertical tire loads, suspension travel, full dynamic suspension equation, and test methods for validation. --- .../TireModel/Implementing Coefficients | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 FullVehicleSim/TireModel/Implementing Coefficients diff --git a/FullVehicleSim/TireModel/Implementing Coefficients b/FullVehicleSim/TireModel/Implementing Coefficients new file mode 100644 index 0000000..b106b2b --- /dev/null +++ b/FullVehicleSim/TireModel/Implementing Coefficients @@ -0,0 +1,148 @@ +# Static Corner Weight +The car is still, all the tires are the same, and the center of gravity is perfectly centered. + +**Formula:** +w = (m * g) / 4 + +- m — total vehicle mass in kg +- g — gravitational acceleration: 9.81 m/s² +- 4 — number of tires (equal weight distribution assumption) + +This is only true when: +- frontAxleFraction = 0.5 +- remainingFraction = 0.5 + +--- + +# Longitudinal Load Transfer +When the car accelerates or brakes, weight shifts between the front and rear. + +**Formula:** +F_long = (m * a_x * h) / L + +- m — vehicle mass (kg) +- a_x — longitudinal acceleration (m/s²) + - Positive = accelerating + - Negative = braking +- h — center of gravity height (m) +- L — wheelbase (m) + +**Key Concept:** +- h / L = pitch transfer coefficient + +Per-corner distribution: +F_long / 2 + +--- + +# Lateral Load Transfer +Cornering creates weight transfer between left and right tires. + +**Formula:** +F_lat = (m * a_y * h) / T + +- m — vehicle mass (kg) +- a_y — lateral acceleration (m/s²) + - Positive = left turn + - Negative = right turn +- h — center of gravity height (m) +- T — track width (m) + +**Key Concept:** +- h / T = roll transfer coefficient + +Per-corner distribution: +F_lat / 2 + +--- + +# Static Axle and Corner Loads + +F_FL,static = r * f * m * g +F_FR,static = (1 - r) * f * m * g +F_RL,static = r * (1 - f) * m * g +F_RR,static = (1 - r) * (1 - f) * m * g + +- f — front axle fraction +- r — left-side fraction + +Constraints: +- f + (1 - f) = 1 +- r + (1 - r) = 1 + +--- + +# Dynamic Vertical Tire Loads + +F_z,FL = F_FL,static - F_long/2 - F_lat/2 +F_z,FR = F_FR,static - F_long/2 + F_lat/2 +F_z,RL = F_RL,static + F_long/2 - F_lat/2 +F_z,RR = F_RR,static + F_long/2 + F_lat/2 + +- F_static — baseline corner load +- F_long / 2 — front/rear transfer split +- F_lat / 2 — left/right transfer split + +Total load always equals: +m * g + +--- + +# Assumption: Quasi-Static Conditions +- The car has already settled after acceleration +- Best for steady-state cornering and gradual inputs +- Less accurate for bumps or sudden steering + +--- + +# Suspension Travel + +x = (F_z - F_preload) / k + +- F_z — dynamic tire load (N) +- F_preload — preload force (N) +- k — spring constant (N/m) + +- x is in meters +- Positive = compression + +Note: +Damping is NOT included in static models + +--- + +# Full Dynamic Suspension Equation + +F_z = k * x + c_v + +- c_v — damping coefficient + +--- + +# Test Methods + +## Lateral Acceleration + +### Method 1: Constant Radius Test +- Drive in a circle with known radius +- Increase speed until failure + +a = v² / r + +### Method 2: Steering Variation +- Maintain constant speed +- Vary steering angle + +--- + +## Longitudinal Acceleration + +### Method: +- Accelerate from 0 to max speed in a straight line + +--- + +# Validation +By measuring longitudinal and lateral acceleration, you can validate: +- longitudinal load transfer +- lateral load transfer equations