Initial Commit

This commit is contained in:
SchrodingerError 2024-08-14 14:42:16 -05:00
commit 482d724e20
100 changed files with 16378 additions and 0 deletions

1386
README.md Normal file

File diff suppressed because it is too large Load Diff

21
code/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1"
},
"args": [
"-Xfrozen_modules=off"
]
}
]
}

3
code/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.REPL.enableREPLSmartSend": false
}

View File

@ -0,0 +1,197 @@
import numpy as np
from Physics_Elements import SharedRigidActuator
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
from Physics_Elements import StateSpace, Spring
class RigidActuatorController():
def __init__(self, controller_settings: dict):
self.controller_settings = controller_settings
self.attached_sim_target: SharedRigidActuator = None
self.shared_attributes: SharedRigidActuatorController = None # For plotting if desired
self.parse_settings()
self.input_voltage: float = 0.0
self.digital_command: int = 0 # 0 means hold position, 1 means control using velocity
self.controlled_length: float = 0.0
self.controlled_vel_scalar: float = 0.0
self.controlled_pos = np.array([0.0, 0.0, 0.0])
self.controlled_vel = np.array([0.0, 0.0, 0.0])
def parse_settings(self):
self.name = self.controller_settings["name"]
self.input_channel = self.controller_settings["analog_input_channel"]
self.digital_channel = self.controller_settings["digital_input_channel"]
self.max_length = self.controller_settings["max_length"]
self.min_length = self.controller_settings["min_length"]
self.neutral_length = self.controller_settings.get("neutral_length", 0)
self.controls_pos_vel_accel = self.controller_settings["controls_pos/vel/accel"]
def calc_neutral_pos(self):
delta_pos_from_child = -1 * self.neutral_length * self.r_unit_vector
self.neutral_pos = self.attached_sim_target.get_child_statespace().get_pos() + delta_pos_from_child
def get_input_channel(self) -> int:
return self.input_channel
def get_digital_channel(self) -> int:
return self.digital_channel
def attach_sim_target(self, target: 'SharedRigidActuator'):
if not isinstance(target._getvalue(), SharedRigidActuator):
raise TypeError(f"target for RigidActuatorController {self.name} is not a SharedRigidActuator")
self.attached_sim_target = target
self.r_unit_vector = self.get_r_unit_vector()
self.calc_neutral_pos()
self.controlled_pos = self.neutral_pos
self.attached_sim_target.set_parent_statespace_pos(self.controlled_pos)
def attach_shared_attributes(self, shared_attributes: 'SharedRigidActuatorController'):
if not isinstance(shared_attributes._getvalue(), SharedRigidActuatorController):
raise TypeError(f"shared_attributes for RigidActuatorController {self.name} is not a SharedRigidActuatorController")
self.shared_attributes = shared_attributes
def get_r_unit_vector(self):
return self.attached_sim_target.get_r_unit_vector()
def set_input_voltage(self, voltage: float):
'''After reading a voltage, set it to this controller element'''
self.input_voltage = voltage
def set_digital_command(self, command: int):
self.digital_command = command
def set_controlled_attribute(self):
if self.controls_pos_vel_accel == 0: # Control pos
self.controlled_length = (self.input_voltage - self.controller_settings["neutral_voltage"]) * self.controller_settings["units_per_volt"] + self.neutral_length
self.set_controlled_pos(self.controlled_length)
elif self.controls_pos_vel_accel == 1: # Controls vel
self.set_controlled_vel()
elif self.controls_pos_vel_accel == 2: # Controls accel
self.set_controlled_accel()
def set_controlled_vel(self):
# Check if the controlled vel would put us outside our max or min displacement
# if it is, set controlled vel to 0 and set our controlled pos manually
current_length = self.attached_sim_target.get_length()
self.controlled_vel_scalar = (self.input_voltage - self.controller_settings["neutral_voltage"]) * self.controller_settings["units_per_volt"]
if current_length > self.max_length:
self.set_controlled_pos(self.max_length)
if self.controlled_vel_scalar > 0:
self.controlled_vel_scalar = 0
elif current_length < self.min_length:
self.set_controlled_pos(self.min_length)
if self.controlled_vel_scalar < 0:
self.controlled_vel_scalar = 0
self.r_unit_vector = self.get_r_unit_vector()
controlled_vel = -1*self.r_unit_vector * self.controlled_vel_scalar # -1 * r vector becaus r vector describes child - parent
self.controlled_vel = controlled_vel
def set_controlled_accel(self):
pass
def set_controlled_pos(self, length:float):
self.r_unit_vector = self.get_r_unit_vector()
if length > self.max_length:
self.controlled_length = self.max_length
elif length < self.min_length:
self.controlled_length = self.min_length
delta_pos_from_child = self.controlled_length * -1*self.r_unit_vector # -1 * r vector becaus r vector describes child - parent
child_pos = self.attached_sim_target.get_child_statespace().get_pos()
self.controlled_pos = delta_pos_from_child + child_pos
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_digital_command(self.digital_command)
self.shared_attributes.set_input_voltage(self.input_voltage)
self.shared_attributes.set_controlled_pos(self.controlled_pos)
self.shared_attributes.set_controlled_vel(self.controlled_vel)
self.shared_attributes.set_controlled_length(self.controlled_length)
self.shared_attributes.set_controlled_vel_scalar(self.controlled_vel_scalar)
def update_sim_target(self):
'''Update the sim target with the values we input from the controller'''
if self.controls_pos_vel_accel == 0: # Control pos
if self.digital_command == 1: # We are actively controlling the actuator
self.attached_sim_target.set_parent_statespace_pos(self.controlled_pos)
elif self.controls_pos_vel_accel == 1: # Controls vel
if self.digital_command == 0: # We are NOT actively controlling the actuator
self.attached_sim_target.set_parent_statespace_vel([0,0,0])
else: # We are actively controlling the vel
self.attached_sim_target.set_parent_statespace_vel(self.controlled_vel)
elif self.controls_pos_vel_accel == 2: # Controls accel
pass
class SharedRigidActuatorController():
def __init__(self):
self.input_voltage: float = 0.0
self.digital_command: int = 0
self.controlled_length: float = 0.0
self.controlled_vel_scalar: float = 0.0
self.controlled_pos = np.array([0.0, 0.0, 0.0])
self.controlled_vel = np.array([0.0, 0.0, 0.0])
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_input_voltage(self, voltage: float):
self.input_voltage = voltage
def set_digital_command(self, command: int):
self.digital_command = command
def set_controlled_pos(self, new_pos):
self.controlled_pos = new_pos
def set_controlled_vel(self, new_vel):
self.controlled_vel = new_vel
def set_controlled_length(self, disp: float) -> None:
self.controlled_length = disp
def set_controlled_vel_scalar(self, vel_scalar: float) -> None:
self.controlled_vel_scalar = vel_scalar
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
def get_controlled_pos(self):
return self.controlled_pos
def get_controlled_vel(self):
return self.controlled_vel
def get_controlled_length(self) -> float:
return self.controlled_length
def get_controlled_vel_scalar(self) -> float:
return self.controlled_vel_scalar
def get_input_voltage(self) -> float:
return self.input_voltage
def get_digital_command(self) -> int:
return self.digital_command

147
code/HITL_Controller.py Normal file
View File

@ -0,0 +1,147 @@
import time
from mcculw.ul import ignore_instacal
import threading
from Physics_Elements import Joint, Spring, StateSpace
from MCC_Interface import MCC3114, MCC202
from typing import List
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Sensor_Elements import LoadCell, SharedLoadCellJoint, LoadCellSpring, SharedLoadCellSpring, DisplacementSensor
from Controller_Input_Elements import RigidActuatorController
class HITLController():
def __init__(self, stop_event, settings: dict):
self.stop_event = stop_event
self.settings = settings
self.parse_settings()
self.load_cells: List[LoadCell] = []
#self.displacement_actuators: List[DisplacementSensor] = []
self.displacement_sensors: List[DisplacementSensor] = []
self.rigid_actuator_controllers: List[RigidActuatorController] = []
def parse_settings(self):
'''Parse the settings to get the settings for the MCC devices'''
self.pull_from_sim_period = self.settings["pull_from_sim_period"] / 1000.0
self.updated_plotting_shared_attributes_period = self.settings["plotting_update_period"] / 1000.0
def attach_load_cell(self, load_cell: 'LoadCell'):
self.load_cells.append(load_cell)
def attach_displacement_sensor(self, sensor: 'DisplacementSensor'):
self.displacement_sensors.append(sensor)
def attach_rigid_actuator_controller(self, controller: 'RigidActuatorController'):
self.rigid_actuator_controllers.append(controller)
def update_from_sim(self):
# Updates the local sensors based on the sim values
for lc in self.load_cells:
lc.pull_from_sim()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.pull_from_sim()
for sensor in self.displacement_sensors:
sensor.pull_from_sim()
def update_from_sim_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_from_sim()
time.sleep(self.pull_from_sim_period / 1000)
def update_internal_sensor_attributes(self):
'''Updates internal attributes. This should be updated as fast as possible because it generates
sensor noise.'''
for lc in self.load_cells:
lc.update_internal_attributes()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.update_internal_attributes()
for sensor in self.displacement_sensors:
sensor.update_internal_attributes()
def update_plotting_shared_attributes(self):
for lc in self.load_cells:
lc.update_shared_attributes()
for rigid_actuator_controller in self.rigid_actuator_controllers:
rigid_actuator_controller.update_shared_attributes()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.update_shared_attributes()
for sensor in self.displacement_sensors:
sensor.update_shared_attributes()
def update_plotting_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_plotting_shared_attributes()
time.sleep(self.updated_plotting_shared_attributes_period / 1000)
def update_mcc3114(self):
for lc in self.load_cells:
channel = lc.sensor_settings["output_channel"]
voltage = lc.get_noisy_voltage_scalar()
self.mcc3114.voltage_write(channel, voltage)
for sensor in self.displacement_sensors:
channel = sensor.sensor_settings["output_channel"]
voltage = sensor.get_noisy_voltage_scalar()
self.mcc3114.voltage_write(channel, voltage)
def read_from_mcc202(self):
for rigid_actuator_controller in self.rigid_actuator_controllers:
input_channel:int = rigid_actuator_controller.get_input_channel()
voltage = self.mcc202.voltage_read(channel=input_channel)
digital_channel:int = rigid_actuator_controller.get_digital_channel()
digital_command = self.mcc202.digital_read(channel=digital_channel)
rigid_actuator_controller.set_digital_command(digital_command)
rigid_actuator_controller.set_input_voltage(voltage)
rigid_actuator_controller.set_controlled_attribute()
def update_sim_targets(self):
for rigid_actuator_controller in self.rigid_actuator_controllers:
rigid_actuator_controller.update_sim_target()
def controller_loop(self):
while self.stop_event.is_set() == False:
self.loop_time = time.time()
# Update the internal attributes of the sensors
self.update_internal_sensor_attributes()
# Update MCC3114 (analog output)
self.update_mcc3114()
# Get readings from the MCC202
self.read_from_mcc202()
# Update the shared actuators for the sim to respond to
self.update_sim_targets()
def start_mcc(self):
ignore_instacal() # ONLY CALL ONCE
self.mcc3114 = MCC3114()
self.mcc202 = MCC202()
def run_process(self):
self.start_mcc()
# Make the threads for interacting with shared elements
self.spare_stop_event = threading.Event()
self.spare_stop_event.clear()
updating_plotting_elements_thread = threading.Thread(target=self.update_plotting_thread)
update_from_sim_thread = threading.Thread(target=self.update_from_sim_thread)
updating_plotting_elements_thread.start()
update_from_sim_thread.start()
self.controller_loop()
self.spare_stop_event.set()
updating_plotting_elements_thread.join()
update_from_sim_thread.join()

200
code/MCC_Interface.py Normal file
View File

@ -0,0 +1,200 @@
from mcculw import ul as mcculw
from mcculw.enums import (AiChanType, BoardInfo, Status, InterfaceType,
FunctionType, InfoType, ScanOptions,
ULRange, AnalogInputMode, DigitalPortType, DigitalIODirection)
import time
class MCC3114():
def __init__(self):
self.range = ULRange.BIP10VOLTS
self.board_num = self.setup_device()
if self.board_num == -1:
print("Could not setup MCC3114")
self.config_outputs()
self.current_voltage_outputs: list[float] = [0.0]*16
self.set_all_to_zero()
def __del__(self):
self.set_all_to_zero()
def setup_device(self) -> int:
'''Sets up the device without Instacal configuration files'''
board_num = 0
board_index = 0
find_device = "USB-3114"
board_num = -1
dev_list = mcculw.get_daq_device_inventory(InterfaceType.USB)
if len(dev_list) > 0:
for device in dev_list:
if str(device) == find_device:
board_num = board_index
mcculw.create_daq_device(board_num, device)
board_index = board_index + 1
if board_num == -1:
print(f"Device {find_device} not found")
return board_num
else:
print("No devices detected")
return board_num
return board_num
def config_outputs(self):
if self.board_num == -1:
return
for ch in range(16):
mcculw.set_config(
InfoType.BOARDINFO, self.board_num, ch,
BoardInfo.DACRANGE, self.range
)
def set_all_to_zero(self):
'''Make the voltage outputs be 0.0 V when the program exits or we delete the object'''
if self.board_num == -1:
return
for i in range(16):
self.voltage_write(channel=i, voltage=0.0)
def voltage_write(self, channel:int, voltage: float):
if self.board_num == -1:
return
if channel < 0 or channel > 15:
print(f"Channel: {channel} is out of range for the MCC USB-3114. Valid range is [0, 15]")
return
if voltage < -10:
print(f"Voltage: {voltage} is out of range for the MCC USB-3114 in this configuration. Valid range is [-10, 10] V")
voltage = -10
if voltage > 10:
print(f"Voltage: {voltage} is out of range for the MCC USB-3114 in this configuration. Valid range is [-10, 10] V")
voltage = 10
try:
mcculw.v_out(self.board_num, channel, self.range, voltage)
self.current_voltage_outputs[channel] = voltage
except Exception as exc:
print(f"Exception occurred doing voltage write to MCC USB-3114: {exc}")
class MCC202():
def __init__(self):
self.range = ULRange.BIP10VOLTS
self.digital_type = DigitalPortType.AUXPORT
self.board_num = self.setup_device()
if self.board_num == -1:
print("Could not setup MCC202")
self.current_analog_inputs: list[float] = [0.0] * 8
self.current_digital_inputs: list[int] = [0] * 8
def __del__(self):
pass # No specific cleanup required for inputs
def setup_device(self) -> int:
'''Sets up the device without Instacal configuration files'''
board_num = 0
board_index = 0
find_device = "USB-202"
board_num = -1
dev_list = mcculw.get_daq_device_inventory(InterfaceType.USB)
if len(dev_list) > 0:
for device in dev_list:
if str(device) == find_device:
board_num = board_index
mcculw.create_daq_device(board_num, device)
board_index = board_index + 1
if board_num == -1:
print(f"Device {find_device} not found")
return board_num
else:
print("No devices detected")
return board_num
return board_num
def config_inputs(self):
if self.board_num == -1:
return
for ch in range(8):
mcculw.set_config(
InfoType.BOARDINFO, self.board_num, ch,
BoardInfo.RANGE, self.range
)
for ch in range(8):
mcculw.d_config_port(self.board_num, self.digital_type, DigitalIODirection.IN)
def print_all_channels(self):
for i in range(8):
print(f"Analog channel {i} reads {self.current_analog_inputs[i]} Volts")
for i in range(8):
print(f"Digital channel {i} reads {self.current_digital_inputs[i]}")
def voltage_read(self, channel: int) -> float:
if self.board_num == -1:
return 0.0
if channel < 0 or channel > 7:
print(f"Channel: {channel} is out of range for the MCC USB-202. Valid range is [0, 7]")
return 0.0
try:
voltage = mcculw.v_in(self.board_num, channel, self.range)
self.current_analog_inputs[channel] = voltage
return voltage
except Exception as exc:
print(f"Exception occurred doing voltage read from MCC USB-202: {exc}")
return 0.0
def read_all_analog_channels(self):
'''Read all analog input channels and update the current_analog_inputs list'''
for i in range(8):
self.voltage_read(i)
def digital_read(self, channel: int) -> int:
if self.board_num == -1:
return -1
if channel < 0 or channel > 7:
print(f"Digital channel: {channel} is out of range for the MCC USB-202. Valid range is [0, 7]")
return -1
try:
state = mcculw.d_bit_in(self.board_num, self.digital_type, channel)
self.current_digital_inputs[channel] = state
return state
except Exception as exc:
print(f"Exception occurred doing digital read from MCC USB-202: {exc}")
return -1
def read_all_digital_channels(self):
'''Read all digital input channels and update the current_digital_inputs list'''
for i in range(8):
self.digital_read(i)
def read_all_channels(self):
for i in range(8):
self.voltage_read(i)
self.digital_read(i)
def main():
mcculw.ignore_instacal() # ONLY CALL ONCE
#mcc3114 = MCC3114()
mcc202 = MCC202()
while True:
mcc202.read_all_channels()
mcc202.print_all_channels()
time.sleep(3)
#mcc3114.voltage_write(1, 5)
#mcc3114.voltage_write(7, 5)
#time.sleep(100)
if __name__ == "__main__":
main()

9
code/Object_Sharing.py Normal file
View File

@ -0,0 +1,9 @@
class SharedFloat:
def __init__(self, initial_value:float=0.0):
self.value = initial_value
def get(self):
return self.value
def set(self, new_value):
self.value = new_value

711
code/Physics_Elements.py Normal file
View File

@ -0,0 +1,711 @@
from typing import List, Callable
import numpy as np
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Physics_Elements import StateSpace, Joint, Spring, SharedRigidActuator, SharedSpring
ZERO_VECTOR = np.array([0.0, 0.0, 0.0])
'''
TODO
1. Change spring_force to just force
Include getters and setters, lest important for attributes
2. Add Object_Sharing objects to this file
'''
def stiffness_function_const_then_rigid(length, unstretched_length):
percent_length = 0.9 # For the first bit of length in compression, it follors k*x
k = 1000
if length < (1-percent_length)* unstretched_length:
# It is really compressed, so make k = k *10
k = k + k*10/(1 + np.exp(-10*(unstretched_length - length - percent_length*unstretched_length)))
return k
def damping_force_x(mass: 'Joint', t):
x_vel = mass.statespace.get_vel()[0]
a = 1
b = 0.01
return x_vel*a*-1 + x_vel**2 * b*-1
def damping_force_y(mass: 'Joint', t):
y_vel = mass.statespace.get_vel()[1]
a = 1
b = 0.01
return y_vel*a*-1 + y_vel**2 * b*-1
def damping_force_z(mass: 'Joint', t):
z_vel = mass.statespace.get_vel()[2]
a = 1
b = 0.01
return z_vel*a*-1 + z_vel**2 * b*-1
'''
TODO
'''
class StateSpace():
def __init__(self, pos=np.copy(ZERO_VECTOR), vel=np.copy(ZERO_VECTOR), accel=np.copy(ZERO_VECTOR), force=np.copy(ZERO_VECTOR), integration_method="simple"):
self.pos = pos
self.vel = vel
self.accel = accel
self.force = force
self.integration_method = self.parse_integration_method(integration_method)
self.max_saved = 2
self.previous_positions: list[list[float, float, float]] = []
self.previous_vel: list[list[float, float, float]] = []
self.previous_accel: list[list[float, float, float]] = []
self.verlet_constants = [
[],
[],
[-1, 2, 1],
[0, -1, 2, 1],
[1/11, -4/11, -6/11, 20/11, 12/11],
]
def parse_integration_method(self, method_str):
if method_str == "simple":
return 1
elif method_str == "verlet":
return 2
elif method_str == "adams-bashforth":
return 3
def integrate(self, delta_t):
'''
NOTE: This is run after accel has been updated.
'''
if self.integration_method == 1:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
elif self.integration_method == 2:
# Verlet integration
self.save_last()
if delta_t >= 0.005:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
else:
self.vel = self.vel + self.accel * delta_t
self.verlet_integration(delta_t)
elif self.integration_method == 3: # Adams-Bashforth
self.save_last()
if delta_t >= 0.005:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
else:
self.adams_bashforth_integration(delta_t)
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
def adams_bashforth_integration(self, delta_t):
'''
Testing.
2 points seems to damp the system. 3 does better at maintaining. Above 3 it is worse
'''
num_prev_accel = len(self.previous_accel)
match num_prev_accel:
case 0 | 1:
self.vel = self.vel + self.accel * delta_t
case 2:
self.vel = self.vel + delta_t * (1.5 * self.previous_accel[-1] - 0.5 * self.previous_accel[-2])
case 3:
self.vel = self.vel + delta_t * (23/12 * self.previous_accel[-1] - 4/3 * self.previous_accel[-2] + 5/12 * self.previous_accel[-3])
case 4:
self.vel = self.vel + delta_t * (55/24 * self.previous_accel[-1] - 59/24 * self.previous_accel[-2] + 37/24 * self.previous_accel[-3] - 9/24 * self.previous_accel[-4])
case 5:
self.vel = self.vel + delta_t * ((1901 * self.previous_accel[-1] - 2774 * self.previous_accel[-2] + 2616 * self.previous_accel[-3] - 1274 * self.previous_accel[-4] + 251 * self.previous_accel[-5]) / 720)
case _:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
def verlet_integration(self, delta_t):
num_prev_positions = len(self.previous_positions)
match num_prev_positions:
case 0 | 1:
self.pos = self.pos + self.vel * delta_t
case 2:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(2):
new_pos += self.verlet_constants[2][i] * self.previous_positions[i]
new_pos += self.verlet_constants[2][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case 3:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(3):
new_pos += self.verlet_constants[3][i] * self.previous_positions[i]
new_pos += self.verlet_constants[3][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case 4:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(4):
new_pos += self.verlet_constants[4][i] * self.previous_positions[i]
new_pos += self.verlet_constants[4][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case _:
self.pos = self.pos + self.vel * delta_t
def save_last(self):
self.previous_positions.append(np.copy(self.pos))
self.previous_positions = self.previous_positions[-1*self.max_saved:]
self.previous_vel.append(np.copy(self.vel))
self.previous_vel = self.previous_vel[-1*self.max_saved:]
self.previous_accel.append(np.copy(self.accel))
self.previous_accel = self.previous_accel[-1*self.max_saved:]
def save_last_statespace(self):
if self.last_statespace is None:
self.last_statespace = StateSpace()
self.last_statespace.pos = np.copy(self.pos)
self.last_statespace.vel = np.copy(self.vel)
self.last_statespace.accel = np.copy(self.accel)
def set_pos(self, new_pos):
self.pos = new_pos
def set_vel(self, new_vel):
self.vel = new_vel
def set_accel(self, new_accel):
self.accel = new_accel
def set_force(self, new_force):
self.force = new_force
def get_pos(self):
return self.pos
def get_vel(self):
return self.vel
def get_accel(self):
return self.accel
def get_force(self):
return self.force
class Joint():
def __init__(self, pos=np.copy(ZERO_VECTOR), mass: float=0.1, fixed: bool=False, name: str="Joint", integration_method:str="simple"):
self.name = name
self.statespace = StateSpace(pos, ZERO_VECTOR, ZERO_VECTOR, ZERO_VECTOR, integration_method=integration_method)
self.connected_springs: List[Spring] = []
self.constant_external_forces: List[List[float, float, float]] = []
self.connected_actuators: List[RigidActuator] = []
self.variable_external_forces = [[], [], []]
self.fixed = fixed
self.connected_to_rigid_actuator = False
self.mass = mass
self.parent_joints: List[Joint] = []
self.child_joints: List[Joint] = []
self.damping = False
self.sharing_attributes = False
self.shared_attributes = None
def is_same(self, other_joint: 'Joint'):
if self.name == other_joint.get_name():
return True
return False
def get_statespace(self):
return self.statespace
def get_pos(self):
return self.statespace.get_pos()
def get_vel(self):
return self.statespace.get_vel()
def get_accel(self):
return self.statespace.get_accel()
def set_state_space_pos(self, new_pos):
self.statespace.set_pos(new_pos)
def set_state_space_vel(self, new_vel):
self.statespace.set_vel(new_vel)
def set_connected_to_rigid_actuator(self, state):
self.connected_to_rigid_actuator = state
def get_connected_springs(self):
return self.connected_springs
def get_connected_actuators(self):
return self.connected_actuators
def get_name(self):
return self.name
def add_spring(self, new_spring):
self.connected_springs.append(new_spring)
def add_actuator(self, new_actuator):
self.connected_actuators.append(new_actuator)
def add_constant_force(self, new_force):
self.constant_external_forces.append(new_force)
def integrate_statespace(self, delta_t):
self.statespace.integrate(delta_t)
def add_gravity(self):
gravity_force = np.array([
0,
-9.81 * self.mass,
0
])
self.add_constant_force(gravity_force)
def add_variable_force(self, force_vect: list):
if force_vect[0] is not None:
self.variable_external_forces[0].append(force_vect[0])
if force_vect[1] is not None:
self.variable_external_forces[1].append(force_vect[1])
if force_vect[2] is not None:
self.variable_external_forces[2].append(force_vect[2])
def calc_net_spring_force(self):
net_spring_force = 0
for spring in self.get_connected_springs():
if self.is_same(spring.get_parent_joint()):
spring_force = spring.get_spring_force() # This is the force the spring exerts on the joint ASSUMING self is parent_joint of the spring
else:
spring_force = -1*spring.get_spring_force()
net_spring_force += spring_force
return net_spring_force
def calc_net_constant_external_force(self):
net_external_force = np.copy(ZERO_VECTOR)
for force in self.constant_external_forces:
net_external_force += force
return net_external_force
def calc_net_variable_external_force(self, t):
net_variable_external_force = np.array([0.0, 0.0, 0.0])
for i in range(3):
for func in self.variable_external_forces[i]:
force = func(t)
net_variable_external_force[i] += force
return net_variable_external_force
def calc_net_force(self, t):
net_force = np.copy(ZERO_VECTOR)
if len(self.constant_external_forces) != 0:
net_force += self.calc_net_constant_external_force()
if len(self.variable_external_forces) != 0:
net_force += self.calc_net_variable_external_force(t)
net_spring_force = self.calc_net_spring_force()
if self.damping == True:
net_force += self.calc_damping(net_spring_force)
net_force += net_spring_force
self.statespace.set_force(net_force)
return self.statespace.get_force()
def add_damping(self, vel_ratio=0.25, mom_ratio=None):
self.damping = True
if mom_ratio is not None:
self.damping_vel_ratio = self.mass * mom_ratio
else:
self.damping_vel_ratio = vel_ratio
def calc_damping(self, spring_force):
vel_damping = self.damping_vel_ratio * (self.statespace.get_vel()) * -1
return vel_damping
def calc_accel(self):
if self.fixed == False and self.connected_to_rigid_actuator == False:
self.statespace.set_accel(self.statespace.get_force() / self.mass)
def is_in_list(self, joints: list['Joint']):
for j in joints:
if self.is_same(j):
return True
return False
def find_parent_joints(self):
for spring in self.connected_springs:
if self.is_same(spring.get_child_joint()):
parent_joint = spring.get_parent_joint()
if not parent_joint.is_in_list(self.parent_joints):
self.parent_joints.append(parent_joint)
def find_child_joints(self):
for spring in self.connected_springs:
if self.is_same(spring.get_parent_joint()):
child_joint = spring.get_child_joint()
if not child_joint.is_in_list(self.child_joints):
self.child_joints.append(child_joint)
def print_attributes(self):
print(f"Joint: {self.name}")
print(f"Force: {self.statespace.get_force()}")
print(f"Accel: {self.statespace.get_accel()}")
print(f"Vel: {self.statespace.get_vel()}")
print(f"Pos: {self.statespace.get_pos()}")
def attach_shared_attributes(self, shared_attributes: 'SharedJoint'):
if not isinstance(shared_attributes._getvalue(), SharedJoint):
raise TypeError(f"shared_attributes for Joint {self.name} is not a SharedJoint")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.shared_attributes.set_name(f"{self.name}")
self.update_shared_attributes()
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_statespace(self.statespace)
class Spring():
def __init__(self, parent_joint: 'Joint', child_joint: 'Joint', unstretched_length: float=0, constant_stiffness: float = None, stiffness_func: Callable[[float, float], float]=None, name: str="Spring"):
self.name = name
self.parent_joint = parent_joint
self.child_joint = child_joint
self.unstretched_length = unstretched_length
self.stiffness_func = stiffness_func
self.constant_stiffness = constant_stiffness
self.current_stiffness = 0.0
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector: list[float] = self.calc_r_unit_vector()
self.spring_force_scalar: float = 0.0
self.spring_force: list[float] = self.calc_spring_force()
self.parent_joint.add_spring(self)
self.child_joint.add_spring(self)
self.sharing_attributes = False
self.shared_attributes: 'SharedSpring' = None
def get_name(self):
return self.name
def get_parent_joint(self):
return self.parent_joint
def get_child_joint(self):
return self.child_joint
def get_spring_force(self):
return self.spring_force
def calc_r_vector(self):
return self.child_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
return self.vector / self.length
def calc_stiffness(self):
if self.stiffness_func is not None:
return self.stiffness_func(self.length, self.unstretched_length)
else:
return self.constant_stiffness
def calc_spring_force(self):
'''Positive force is in tension. Aka, the spring PULLS on the other object'''
self.length = self.calc_length()
del_length = self.length - self.unstretched_length
self.current_stiffness = self.calc_stiffness()
self.spring_force_scalar = del_length * self.current_stiffness
self.vector = self.calc_r_vector()
self.r_unit_vector = self.calc_r_unit_vector()
self.spring_force = self.spring_force_scalar * self.r_unit_vector
return self.spring_force
def print_attributes(self):
print(f"Spring: {self.name}")
print(f"Length: {self.length}")
print(f"Spring Force: {self.spring_force}")
def get_pos(self):
return self.parent_statespace.get_pos()
def get_axis_vector(self):
return self.child_statespace.get_pos() - self.parent_statespace.get_pos()
def attach_shared_attributes(self, shared_attributes: 'SharedSpring'):
if not isinstance(shared_attributes._getvalue(), SharedSpring):
raise TypeError(f"shared_attributes for Spring {self.name} is not a SharedSpring")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.update_shared_attributes()
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_child_statespace(self.child_joint.get_statespace())
self.shared_attributes.set_parent_statespace(self.parent_joint.get_statespace())
self.shared_attributes.set_spring_force(self.spring_force)
self.shared_attributes.set_spring_force_scalar(self.spring_force_scalar)
self.shared_attributes.set_length(self.length)
self.shared_attributes.set_unstretched_length(self.unstretched_length)
self.shared_attributes.set_stiffness(self.current_stiffness)
def is_same(self, other_spring: 'Spring'):
if self.name == other_spring.get_name():
return True
return False
def is_in_list(self, springs: list['Spring']):
for j in springs:
if self.is_same(j):
return True
return False
'''
It connects 2 joints.
1 joint is completely fixed. Cannot move. Ever. It will be referred to as "grounded".
The other joint is contrained to the grounded joint. The actuator controls the vector displacement <x,y,z> from the grounded joint
'''
class RigidActuator():
def __init__(self, parent_joint: 'Joint', grounded_joint: 'Joint', name: str = "Rigid Actuator", max_length:float=1, min_length:float=0.1, control_code=0):
self.name = name
self.parent_joint = parent_joint
self.grounded_joint = grounded_joint # Same as child joint
self.parent_joint.add_actuator(self)
self.parent_joint.set_connected_to_rigid_actuator(True)
self.grounded_joint.add_actuator(self)
self.grounded_joint.set_connected_to_rigid_actuator(True)
self.max_length = max_length
self.min_length = min_length
self.control_code = control_code # 0 for pos, 1 for vel, 2 for accel
self.calc_r_unit_vector()
self.shared_attributes: 'SharedRigidActuator' = None
def get_name(self):
return self.name
def get_parent_joint(self):
return self.parent_joint
def get_child_joint(self):
return self.grounded_joint
def is_same(self, other: 'RigidActuator'):
if self.name == other.get_name():
return True
return False
def is_in_list(self, actuators: list['RigidActuator']):
for j in actuators:
if self.is_same(j):
return True
return False
def calc_r_vector(self):
return self.grounded_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
return self.vector / self.length
def get_pos(self):
return self.parent_joint.get_statespace().get_pos()
def get_axis_vector(self):
return self.grounded_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def get_r_unit_vector(self):
return self.calc_r_unit_vector()
def update_shared_attributes(self):
# These 2 lines are used to make sure we set the internal properties correctly
self.vector = self.calc_r_vector()
self.length = self.calc_length()
# END
if self.shared_attributes is not None:
self.shared_attributes.set_child_statespace(self.grounded_joint.get_statespace())
self.shared_attributes.set_parent_statespace(self.parent_joint.get_statespace())
self.shared_attributes.set_length(self.length)
def attach_shared_attributes(self, shared_attributes: 'SharedRigidActuator'):
if not isinstance(shared_attributes._getvalue(), SharedRigidActuator):
raise TypeError(f"shared_attributes for RigidActuator {self.name} is not a SharedRigidActuator")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.update_shared_attributes()
def set_parent_pos(self, new_pos):
'''Set the position of the parent joint's statestpace'''
self.parent_joint.set_state_space_pos(new_pos)
def update_from_controller(self):
if self.control_code == 0: # Controller controls pos
self.set_pos_to_controlled_pos()
elif self.control_code == 1: # Controller controls vel
self.set_vel_to_controlled_vel()
elif self.control_code == 2: # Controls controls accel
pass
def set_vel_to_controlled_vel(self):
controlled_vel = self.shared_attributes.get_vel()
self.parent_joint.set_state_space_vel(controlled_vel)
def set_pos_to_controlled_pos(self):
controlled_pos = self.shared_attributes.get_pos()
self.parent_joint.set_state_space_pos(controlled_pos)
class SharedPhysicsElement():
def __init__(self) -> None:
self.name: str = None
self.statespace: StateSpace = None
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_name(self, name:str) -> None:
self.name = name
def set_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_name(self) -> str:
return self.name
def get_statespace(self) -> StateSpace:
return self.statespace
def get_pos(self) -> List[float]:
return self.statespace.get_pos()
def get_vel(self) -> List[float]:
return self.statespace.get_vel()
def get_accel(self) -> List[float]:
return self.statespace.get_accel()
def get_force(self) -> List[float]:
return self.statespace.get_force()
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
class SharedJoint(SharedPhysicsElement):
def _init__(self) -> None:
super().__init__()
class SharedSpring(SharedPhysicsElement):
'''self.statespace is the statespace of the parent'''
def _init__(self) -> None:
super().__init__()
self.child_statespace: StateSpace = None
self.unstretched_length: float = None
self.length: float = None
self.stiffness: float = None
self.spring_force: float = None
def set_parent_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_child_statespace(self, statespace:StateSpace) -> None:
self.child_statespace = statespace
def set_unstretched_length(self, unstretched_length: float) -> None:
self.unstretched_length = unstretched_length
def set_length(self, length: float) -> None:
self.length = length
def set_stiffness(self, stiffness: float) -> None:
self.stiffness = stiffness
def set_spring_force(self, set_spring_force: float) -> None:
self.spring_force = set_spring_force
def set_spring_force_scalar(self, scalar_force: float) -> None:
self.spring_force_scalar = scalar_force
def get_parent_statespace(self) -> StateSpace:
return self.statespace
def get_child_statespace(self) -> StateSpace:
return self.child_statespace
def get_unstretched_length(self) -> float:
return self.unstretched_length
def get_length(self) -> float:
return self.length
def get_stiffness(self) -> float:
return self.stiffness
def get_spring_force(self) -> list[float]:
return self.spring_force
def get_spring_force_scalar(self) -> float:
return self.spring_force_scalar
def get_axis_vector(self) -> List[float]:
return self.child_statespace.get_pos() - self.statespace.get_pos()
class SharedRigidActuator(SharedPhysicsElement):
'''self.statespace is the statespace of the parent'''
def _init__(self) -> None:
super().__init__()
self.child_statespace: StateSpace = None
self.length: float = None
def set_parent_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_parent_statespace_pos(self, new):
self.statespace.set_pos(new)
def set_parent_statespace_vel(self, new):
self.statespace.set_vel(new)
def set_child_statespace(self, statespace:StateSpace) -> None:
self.child_statespace = statespace
def set_length(self, length: float) -> None:
self.length = length
def get_parent_statespace(self) -> StateSpace:
return self.statespace
def get_child_statespace(self) -> StateSpace:
return self.child_statespace
def get_length(self) -> float:
return self.length
def get_axis_vector(self) -> List[float]:
return self.child_statespace.get_pos() - self.statespace.get_pos()
def get_r_unit_vector(self):
return self.calc_r_unit_vector()
def calc_r_vector(self):
return self.child_statespace.get_pos() - self.statespace.get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
return self.vector / self.length

451
code/Sensor_Elements.py Normal file
View File

@ -0,0 +1,451 @@
import numpy as np
from math import log, copysign
from Physics_Elements import SharedRigidActuator, SharedJoint, SharedSpring
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
from Physics_Elements import StateSpace, Spring
'''
TODO
1.
2. Better noise generation
Non-linearity, hysteresis, background noise, AC noise
'''
class LoadCell():
'''Provides net force feedback.'''
def __init__(self, sensor_settings: dict={}):
self.sensor_settings = sensor_settings
self.attached_sim_source = None
self.shared_attributes: SharedLoadCell = None # For plotting if desired
self.true_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.true_force_scalar: float = 0.0
self.last_true_force_scalars: list[float] = [] # Stores the last true force for hysteresis reasons
self.true_voltage_scalar:float = 0.0
self.noisy_force_vector = np.array([0.0, 0.0, 0.0])
self.noisy_force_scalar: float = 0.0
self.noisy_voltage_scalar: float = 0.0
self.parse_settings()
self.calc_noise_parameters()
def parse_settings(self):
self.name = self.sensor_settings["name"]
self.scale = self.sensor_settings["mV/V"] / 1000.0
self.excitation = self.sensor_settings["excitation"]
self.full_scale_force = self.sensor_settings["full_scale_force"]
self.output_channel = self.sensor_settings["output_channel"]
self.noise_settings = self.sensor_settings.get("noise_settings", dict())
self.full_scale_voltage = self.scale * self.excitation
def calc_noise_parameters(self):
# Calculate static error
self.static_error_stdev = self.full_scale_voltage * self.noise_settings.get("static_error_band", 0) / 100
# Non-linearity
self.nonlinearity_exponent = self.calc_nonlinearity_exponent()
# Hysteresis
self.hysteresis = self.noise_settings.get("hysteresis", 0) / 100
# Repeatability
self.repeatability_offset = np.random.normal(0, self.full_scale_voltage * self.noise_settings.get("repeatability", 0) / 100)
# Thermal applied at a given true voltage
self.thermal_error = self.noise_settings.get("thermal_error", 0) / 100 * self.noise_settings.get("temperature_offset", 0)
def calc_nonlinearity_exponent(self):
voltage = 10
if self.full_scale_voltage != 1:
self.full_scale_voltage
error = self.noise_settings.get("non-linearity", 0) / 100
b = log(1 + error, voltage)
return b
def attach_sim_source(self, sim_source):
self.attached_sim_source = sim_source
def attach_shared_attributes(self, shared_attributes):
self.shared_attributes = shared_attributes
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_true_force_vector(self.true_force_vector)
self.shared_attributes.set_true_force_scalar(self.true_force_scalar)
self.shared_attributes.set_true_voltage_scalar(self.true_voltage_scalar)
self.shared_attributes.set_noisy_voltage_scalar(self.noisy_voltage_scalar)
def update_internal_attributes(self):
self.calc_sensor_force_scalar()
self.calc_true_voltage_scalar()
# Calc noise
self.calc_noisy_voltage_scalar()
def pull_from_sim(self):
'''Gets the real net force from the shared attribute updated by the sim'''
# Must be overridden
pass
def calc_sensor_force_scalar(self):
self.last_true_force_scalars.append(self.true_force_scalar)
self.last_true_force_scalars = self.last_true_force_scalars[-5:]
self.true_force_scalar = ((self.true_force_vector[0])**2 + (self.true_force_vector[1])**2 + (self.true_force_vector[2])**2)**0.5
def get_sensor_force_scalar(self):
return self.true_force_scalar
def get_sensor_force_vector(self):
return self.true_force_vector
def get_true_voltage_scalar(self) -> float:
return self.true_voltage_scalar
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage_scalar
def calc_true_voltage_scalar(self):
force_fraction = self.true_force_scalar / self.full_scale_force
full_force_voltage = self.excitation * self.scale
self.true_voltage_scalar = force_fraction * full_force_voltage
def calc_noisy_voltage_scalar(self):
# Calculate static error
static_error = np.random.normal(0, self.static_error_stdev)
# Non-linearity
if self.true_voltage_scalar > 1E-5:
nonlinearity_multiplier = self.true_voltage_scalar**(self.nonlinearity_exponent)
else:
nonlinearity_multiplier = 1
# Hysteresis
average_last = np.average(self.last_true_force_scalars)
#hysteresis_error = (self.true_force_scalar - average_last) / self.true_force_scalar * self.hysteresis * self.full_scale_voltage
hysteresis_error = copysign(1, self.true_force_scalar - average_last) * self.hysteresis * self.full_scale_voltage
# Repeatability
# self.repeatability_offset
# Thermal
thermal_error = self.thermal_error * self.true_voltage_scalar
self.noisy_voltage_scalar = self.true_voltage_scalar*nonlinearity_multiplier + static_error + hysteresis_error + self.repeatability_offset + thermal_error
class LoadCellJoint(LoadCell):
def __init__(self, sensor_settings: dict={}):
super().__init__(sensor_settings=sensor_settings)
self.attached_sim_source: SharedJoint = None
def attach_sim_source(self, sim_source: 'SharedJoint'):
if not isinstance(sim_source._getvalue(), SharedJoint):
raise TypeError(f"sim_source for LoadCellJoint {self.name} is not a SharedJoint")
self.attached_sim_source = sim_source
def pull_from_sim(self):
self.true_force_vector = self.attached_sim_source.get_force()
class LoadCellSpring(LoadCell):
'''Provides feedback on the force along the axis of a spring.'''
def __init__(self, sensor_settings: dict={}):
super().__init__(sensor_settings=sensor_settings)
self.attached_sim_source: SharedSpring = None
self.unstretched_length: float = 0.0
self.true_length: float = 0.0
def attach_sim_source(self, sim_source: 'SharedSpring'):
if not isinstance(sim_source._getvalue(), SharedSpring):
raise TypeError(f"sim_source for LoadCellSpring {self.name} is not a SharedSpring")
self.attached_sim_source = sim_source
self.unstretched_length = sim_source.get_unstretched_length()
def pull_from_sim(self):
self.true_force_vector = self.attached_sim_source.get_spring_force()
self.calc_sensor_force_scalar()
self.true_length = self.attached_sim_source.get_length()
def calc_sensor_force_scalar(self):
self.last_true_force_scalars.append(self.true_force_scalar)
self.last_true_force_scalars = self.last_true_force_scalars[-5:]
self.true_force_scalar = ((self.true_force_vector[0])**2 + (self.true_force_vector[1])**2 + (self.true_force_vector[2])**2)**0.5
if self.true_length >= self.unstretched_length: # Tension is positive
self.true_force_scalar *= 1
else:
self.true_force_scalar*= -1
def calc_true_voltage_scalar(self):
'''Include the possibility to be negative if in tension'''
force_fraction = self.true_force_scalar / self.full_scale_force
full_force_voltage = self.excitation * self.scale
self.true_voltage_scalar = force_fraction * full_force_voltage
class SharedLoadCell():
def __init__(self):
self.true_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.true_force_scalar: float = 0.0
self.true_voltage_scalar: float = 0.0
self.noisy_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.noisy_force_scalar: float = 0.0
self.noisy_voltage_scalar: float = 0.0
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_true_force_vector(self, true_force_vector):
self.true_force_vector = true_force_vector
def get_true_force_vector(self) -> float:
return self.true_force_vector
def set_true_force_scalar(self, true_force_scalar):
self.true_force_scalar = true_force_scalar
def get_true_force_scalar(self) -> float:
return self.true_force_scalar
def set_true_voltage_scalar(self, true_voltage_scalar):
self.true_voltage_scalar = true_voltage_scalar
def get_true_voltage_scalar(self) -> float:
return self.true_voltage_scalar
def set_noisy_voltage_scalar(self, noisy_voltage_scalar):
self.noisy_voltage_scalar = noisy_voltage_scalar
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage_scalar
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
class SharedLoadCellJoint(SharedLoadCell):
def __init__(self):
super().__init__()
class SharedLoadCellSpring(SharedLoadCell):
def __init__(self):
super().__init__()
class DisplacementSensor():
'''Measures the displacement between 2 joints.
The output is the non-negative scalar distance between parent and child'''
def __init__(self, parent_joint: 'SharedJoint', child_joint: 'SharedJoint', sensor_settings: dict={}):
self.sensor_settings = sensor_settings
self.parse_settings()
self.calc_noise_parameters()
if not isinstance(parent_joint._getvalue(), SharedJoint):
raise TypeError(f"parent_joint for DisplacementSensor {self.name} is not a SharedJoint")
if not isinstance(child_joint._getvalue(), SharedJoint):
raise TypeError(f"child_joint for DisplacementSensor {self.name} is not a SharedJoint")
parent_joint.set_connected_to_sensor(True)
child_joint.set_connected_to_sensor(True)
self.sim_source_parent:SharedJoint = parent_joint
self.sim_source_child:SharedJoint = child_joint
self.shared_attributes: SharedDisplacementSensor = None # For plotting if desired
self.length: float = 0.0 # distance from child to parent
self.true_disp_scalar: float = 0.0 # length - neutral_length
self.last_true_disp_scalars: list[float] = [] # Stores the last true displacements for hysteresis reasons
self.calc_sensor_true_disp()
self.true_voltage: float = 0.0
self.noisy_voltage: float = 0.0
def parse_settings(self):
self.name = self.sensor_settings["name"]
self.scale = self.sensor_settings["volts_per_meter"]
self.output_channel = self.sensor_settings["output_channel"]
self.neutral_length = self.sensor_settings["neutral_length"]
self.neutral_voltage = self.sensor_settings.get("neutral_voltage", 0)
self.noise_settings = self.sensor_settings.get("noise_settings", dict())
self.full_scale_voltage = self.scale * (self.sensor_settings["max_length"] - self.neutral_length)
def calc_noise_parameters(self):
# Calculate static error
self.static_error_stdev = self.full_scale_voltage * self.noise_settings.get("static_error_band", 0) / 100
# Non-linearity
self.nonlinearity_exponent = self.calc_nonlinearity_exponent()
# Hysteresis
self.hysteresis = self.noise_settings.get("hysteresis", 0) / 100
# Repeatability
self.repeatability_offset = np.random.normal(0, self.full_scale_voltage * self.noise_settings.get("repeatability", 0) / 100)
# Thermal applied at a given true voltage
self.thermal_error = self.noise_settings.get("thermal_error", 0) / 100 * self.noise_settings.get("temperature_offset", 0)
def calc_nonlinearity_exponent(self):
voltage = 10
if self.full_scale_voltage != 1:
self.full_scale_voltage
error = self.noise_settings.get("non-linearity", 0) / 100
b = log(1 + error, voltage)
return b
def attach_shared_attributes(self, shared_attributes):
if not isinstance(shared_attributes._getvalue(), SharedDisplacementSensor):
raise TypeError(f"shared_attributes for DisplacementSensor {self.name} is not a SharedDisplacementSensor")
self.shared_attributes = shared_attributes
self.shared_attributes.set_neutral_length(self.neutral_length)
def pull_from_sim(self):
'''Gets the real displacement from the shared attribute updated by the sim'''
parent_pos = self.sim_source_parent.get_pos()
child_pos = self.sim_source_child.get_pos()
axis_vector = child_pos - parent_pos
self.length = ((axis_vector[0])**2 + (axis_vector[1])**2 + (axis_vector[2])**2)**0.5
def calc_sensor_true_disp(self):
self.last_true_disp_scalars.append(self.true_disp_scalar)
self.last_true_disp_scalars = self.last_true_disp_scalars[-5:]
self.true_disp_scalar = self.length - self.neutral_length
def calc_true_voltage(self):
self.true_voltage = self.true_disp_scalar * self.scale
def calc_noisy_voltage(self):
# Calculate static error
static_error = np.random.normal(0, self.static_error_stdev)
# Non-linearity
if self.true_voltage > 1E-5:
nonlinearity_multiplier = self.true_voltage**(self.nonlinearity_exponent)
else:
nonlinearity_multiplier = 1
# Hysteresis
average_last = np.average(self.last_true_disp_scalars)
#hysteresis_error = (self.true_force_scalar - average_last) / self.true_force_scalar * self.hysteresis * self.full_scale_voltage
hysteresis_error = copysign(1, self.true_disp_scalar - average_last) * self.hysteresis * self.full_scale_voltage
# Repeatability
# self.repeatability_offset
# Thermal
thermal_error = self.thermal_error * self.true_voltage
self.noisy_voltage = self.true_voltage*nonlinearity_multiplier + static_error + hysteresis_error + self.repeatability_offset + thermal_error
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_current_length(self.length)
self.shared_attributes.set_true_voltage(self.true_voltage)
self.shared_attributes.set_noisy_voltage(self.noisy_voltage)
self.shared_attributes.set_displacement(self.true_disp_scalar)
def update_internal_attributes(self):
self.calc_sensor_true_disp()
self.calc_true_voltage()
self.calc_noisy_voltage()
def get_true_voltage_scalar(self) -> float:
return self.true_voltage
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage
class SharedDisplacementSensor():
def __init__(self):
self.neutral_length: float = 0.0
self.length: float = 0.0
self.displacement: float = 0.0
self.true_voltage: float = 0.0
self.noisy_voltage: float = 0.0
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_neutral_length(self, new:float):
self.neutral_length = new
def set_current_length(self, new:float):
self.length = new
def set_displacement(self, new:float):
self.displacement = new
def set_true_voltage(self, new:float):
self.true_voltage = new
def set_noisy_voltage(self, new:float):
self.noisy_voltage = new
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
def get_neutral_length(self) -> float:
return self.neutral_length
def get_displacement(self) -> float:
return self.displacement
def get_current_length(self) -> float:
return self.length
def get_true_voltage(self) -> float:
return self.true_voltage
def get_noisy_voltage(self) -> float:
return self.noisy_voltage

236
code/Simulation.py Normal file
View File

@ -0,0 +1,236 @@
import time
import threading
import numpy as np
from typing import List
from Visualization import Visualization
from Physics_Elements import Joint, Spring, RigidActuator
class BFSSimulation():
def __init__(self, parent_joint: Joint, settings:dict):
if not isinstance(parent_joint, Joint):
raise TypeError(f"parent_joint for BFSSimulation is not a Joint")
self.parent_joint = parent_joint
self.duration = settings["duration"]
self.delta_t = settings["delta_t"]
self.plotting_update_period = settings["plotting_update_period"]
self.sensor_update_period = settings["sensor_update_period"]
self.controller_pull_period = settings["controller_pull_period"]
self.joints: List[Joint] = []
self.springs: List[Spring] = []
self.actuators: List[RigidActuator] = []
self.rigid_actuators: List[RigidActuator] = []
self.get_joints_bfs()
#self.get_parent_joints_for_every_joint()
# Plotting or viz elements are updated slower than sensor
self.plotting_or_viz_only_shared = []
self.sensor_shared = []
self.controller_shared = []
self.sort_shared_attributes_into_frequencies()
self.vis:Visualization = None
self.attached_shared_time:float = None
self.sim_time:float = 0.0
def is_in_joints(self, joint: Joint):
for j in self.joints:
if joint.is_same(j):
return True
return False
def is_in_queue(self, joint: Joint, queue):
for j in queue:
if joint.is_same(j):
return True
return False
def get_joints_bfs(self):
queue: List[Joint] = []
queue.append(self.parent_joint)
while queue:
parent_joint: Joint
parent_joint = queue.pop(0)
if parent_joint is None:
continue
if not parent_joint.is_in_list(self.joints):
self.joints.append(parent_joint)
connected_springs = parent_joint.get_connected_springs()
for spring in connected_springs:
if not spring.is_in_list(self.springs):
self.springs.append(spring)
child_joint = spring.get_child_joint()
if not self.is_in_joints(child_joint) and not self.is_in_queue(child_joint, queue):
queue.append(child_joint)
connected_actuators = parent_joint.get_connected_actuators()
actuator: RigidActuator = None
for actuator in connected_actuators:
if actuator not in self.actuators:
self.rigid_actuators.append(actuator)
if actuator.get_child_joint() not in self.joints and actuator.get_child_joint() not in queue:
queue.append(actuator.get_child_joint())
def get_parent_joints_for_every_joint(self):
for joint in self.joints:
joint.find_parent_joints()
def get_child_joints_for_every_joint(self):
for joint in self.joints:
joint.find_child_joints()
def update_actuators_from_controller(self):
for rigid_actuator in self.rigid_actuators:
rigid_actuator.update_from_controller()
def pull_actuator_positions_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_actuators_from_controller()
time.sleep(self.controller_pull_period)
def update_sensor_attributes(self):
'''Updates the joints, springs, and actuators that are attached to sensors'''
for obj in self.sensor_shared:
obj.update_shared_attributes()
def update_sensor_attributes_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_sensor_attributes()
time.sleep(self.sensor_update_period)
def update_plotting_or_viz_only_attributes(self):
'''Updates the joints, springs, and actuators that are only attached for plotting or visualization'''
for obj in self.plotting_or_viz_only_shared:
obj.update_shared_attributes()
def update_plotting_or_viz_only_attributes_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_plotting_or_viz_only_attributes()
time.sleep(self.plotting_update_period)
def sort_shared_attributes_into_frequencies(self):
'''For each shared attribute connected to anything, put them in lists so we know how fast we need to update them.'''
for joint in self.joints:
shared = joint.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(joint)
elif plot or vis:
self.plotting_or_viz_only_shared.append(joint)
if contr:
self.controller_shared.append(joint)
for spring in self.springs:
shared = spring.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(spring)
elif plot or vis:
self.plotting_or_viz_only_shared.append(spring)
if contr:
self.controller_shared.append(spring)
for actuator in self.rigid_actuators:
shared = actuator.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(actuator)
elif plot or vis:
self.plotting_or_viz_only_shared.append(actuator)
if contr:
self.controller_shared.append(actuator)
def attach_shared_time(self, attached_shared_time):
self.attached_shared_time = attached_shared_time
def run_process(self):
time_values = []
self.sim_time = 0
#time_last = time.perf_counter()
time_last = time.time()
count = 0
self.spare_stop_event = threading.Event()
self.spare_stop_event.clear()
pull_actuator_thread = threading.Thread(target=self.pull_actuator_positions_thread)
pull_actuator_thread.start()
update_plotting_or_viz_only_thread = threading.Thread(target=self.update_plotting_or_viz_only_attributes_thread)
update_plotting_or_viz_only_thread.start()
update_sensor_thread = threading.Thread(target=self.update_sensor_attributes_thread)
update_sensor_thread.start()
while self.sim_time < self.duration:
count += 1
if self.delta_t is None:
#new_time = time.perf_counter()
new_time = time.time()
delta_time = new_time - time_last
time_last = new_time
for spring in self.springs:
spring.calc_spring_force()
joint: Joint
for joint in self.joints:
if joint.fixed == True:
continue
# Get joint forces
joint.calc_net_force(self.sim_time)
# Get joint accels.
joint.calc_accel()
# Integrate joint vel and pos
if self.delta_t is None:
joint.integrate_statespace(delta_time)
else:
joint.integrate_statespace(self.delta_t)
time_values.append(self.sim_time)
if self.delta_t is None:
self.sim_time += delta_time
else:
self.sim_time += self.delta_t
# Update the shared sim time as fast as possible
self.attached_shared_time.set(self.sim_time)
self.spare_stop_event.set()
pull_actuator_thread.join()
update_plotting_or_viz_only_thread.join()
update_sensor_thread.join()
print(f"Average delta_t = {self.sim_time / count}")

594
code/Visualization.py Normal file
View File

@ -0,0 +1,594 @@
import vpython
import time
from copy import deepcopy
import multiprocessing
import threading
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore
from Physics_Elements import Joint, Spring, SharedRigidActuator, SharedSpring, SharedJoint
from Sensor_Elements import SharedLoadCellJoint, SharedLoadCellSpring, SharedDisplacementSensor
from Controller_Input_Elements import SharedRigidActuatorController
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
'''
TODO
2. Make this in a separate thread
'''
class Visualization():
def __init__(self, stop_event, scene_settings:dict):
self.stop_event = stop_event
self.scene_settings = scene_settings
self.attached_attributes = []
self.shared_attributes_object_settings = []
self.vpython_objects = []
def draw_scene(self):
vpython.scene = vpython.canvas(width=self.scene_settings["canvas_width"], height=self.scene_settings["canvas_height"])
side = self.scene_settings["cube_size"]
thk = self.scene_settings["wall_thickness"]
wall_length = side + 2*thk
wall_bottom = vpython.box(pos=vpython.vector(0, -thk/2, 0), size=vpython.vector(wall_length, thk, wall_length), color=vpython.color.gray(0.9))
wall_left = vpython.box(pos=vpython.vector(-(side + thk)/2, side/2, 0), size=vpython.vector(thk, side, side), color=vpython.color.gray(0.7))
wall_right = vpython.box(pos=vpython.vector((side + thk)/2, side/2, 0), size=vpython.vector(thk, side, side), color=vpython.color.gray(0.7))
wall_back = vpython.box(pos=vpython.vector(0, (side)/2, -(side+ thk)/2), size=vpython.vector(wall_length, side, thk), color=vpython.color.gray(0.7))
def attach_shared_attributes(self, shared_attributes, object_settings):
self.attached_attributes.append(shared_attributes)
self.shared_attributes_object_settings.append(object_settings)
shared_attributes.set_connected_to_visualization(True)
def generate_vpython_objects(self):
for i in range(len(self.attached_attributes)):
object_settings = self.shared_attributes_object_settings[i]
if object_settings["type"] == "sphere":
shared_attribute:Joint = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.sphere(pos=object_pos, radius=object_settings["radius"], color=object_color))
elif object_settings["type"] == "helix":
shared_attribute:Spring = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_axis = deepcopy(shared_attribute.get_axis_vector())
object_axis = vpython.vector(object_axis[0], object_axis[1], object_axis[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.helix(pos=object_pos, axis=object_axis, radius=object_settings["radius"], thickness=object_settings["thickness"], color=object_color))
elif object_settings["type"] == "cylinder":
shared_attribute:Spring = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_axis = deepcopy(shared_attribute.get_axis_vector())
object_axis = vpython.vector(object_axis[0], object_axis[1], object_axis[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.cylinder(pos=object_pos, axis=object_axis, radius=object_settings["radius"], color=object_color))
def return_vpython_color(self, color_str):
if color_str == "blue":
return vpython.color.blue
elif color_str == "black":
return vpython.color.black
elif color_str == "red":
return vpython.color.red
elif color_str == "green":
return vpython.color.green
elif color_str == "white":
return vpython.color.white
def update_scene(self):
for i in range(len(self.vpython_objects)):
element = self.vpython_objects[i]
shared_attributes = self.attached_attributes[i]
if isinstance(element, vpython.sphere):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
elif isinstance(element, vpython.helix):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
updated_axis = shared_attributes.get_axis_vector()
element.axis = deepcopy(vpython.vector(*(updated_axis)))
elif isinstance(element, vpython.cylinder):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
updated_axis = shared_attributes.get_axis_vector()
element.axis = deepcopy(vpython.vector(*(updated_axis)))
def run_process(self):
# Draw scene
self.draw_scene()
# Generate vpython objects from settings
self.generate_vpython_objects()
while self.stop_event.is_set() == False:
# Update pos and axes of all vpython objects
self.update_scene()
class Plotter():
def __init__(self, plot_settings: dict, stop_event: multiprocessing.Event):
self.plot_settings = plot_settings
self.title = plot_settings["title"]
self.window_length = self.plot_settings["window_length"]
self.update_interval = self.plot_settings["update_interval"]
self.pull_interval = self.plot_settings["pull_interval"]
self.stop_event: multiprocessing.Event = stop_event
self.attached_attributes = []
self.shared_attributes_plot_settings = []
self.pull_data_stop_event: threading.Event = None
self.shared_x: SharedFloat = None
self.plot_setup = {}
def attach_shared_attributes(self, shared_attributes, plot_settings):
self.attached_attributes.append(shared_attributes)
self.shared_attributes_plot_settings.append(plot_settings)
shared_attributes.set_connected_to_plotter(True)
def attach_shared_x(self, shared_x):
self.shared_x = shared_x
def pull_data_thread(self):
while self.pull_data_stop_event.is_set() == False:
self.pull_data()
time.sleep(self.pull_interval / 1000)
def pull_data(self):
self.raw_xdata.append(self.shared_x.get())
for i in range(len(self.shared_attributes_plot_settings)):
foo = self.attached_attributes[i]._getvalue()
for key, value in self.shared_attributes_plot_settings[i].items():
if isinstance(foo, SharedLoadCellJoint):
bar: SharedLoadCellJoint = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_true_force_vector()
for subkey, settings in value.items():
if subkey == "x":
ylabel: str = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif key == "true_voltage":
voltage_scalar = bar.get_true_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif key == "noisy_voltage":
voltage_scalar = bar.get_noisy_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif isinstance(foo, SharedLoadCellSpring):
bar: SharedLoadCellSpring = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_true_force_vector()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif subkey == "scalar":
force_scalar = bar.get_true_force_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_scalar)
elif key == "true_voltage":
voltage_scalar = bar.get_true_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif key == "noisy_voltage":
voltage_scalar = bar.get_noisy_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif isinstance(foo, SharedRigidActuator):
bar: SharedRigidActuator = self.attached_attributes[i]
if key == "pos":
pos_vector = bar.get_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
if key == "vel":
pos_vector = self.attached_attributes[i].get_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
elif isinstance(foo, SharedRigidActuatorController):
bar: SharedRigidActuatorController = self.attached_attributes[i]
if key == "input_voltage":
input_voltage = bar.get_input_voltage()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(input_voltage)
elif key == "digital_input":
digital_input = bar.get_digital_command()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(digital_input)
elif key == "pos":
controlled_vel = bar.get_controlled_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[2])
elif key == "vel":
controlled_vel = bar.get_controlled_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[2])
if subkey == "scalar":
vel_scalar = bar.get_controlled_vel_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_scalar)
elif key == "disp":
disp_scalar = bar.get_controlled_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(disp_scalar)
elif isinstance(foo, SharedDisplacementSensor):
bar: SharedDisplacementSensor = self.attached_attributes[i]
if key == "voltage":
for subkey, settings in value.items():
if subkey == "noisy":
voltage_scalar = bar.get_noisy_voltage()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif subkey == "true":
voltage_scalar = bar.get_true_voltage()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
if key == "disp":
disp_scalar = bar.get_displacement()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(disp_scalar)
elif key == "length":
length_scalar = bar.get_current_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(length_scalar)
elif isinstance(foo, SharedSpring):
bar: SharedSpring = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_spring_force()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif subkey == "scalar":
force_scalar = bar.get_spring_force_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_scalar)
elif key == "length":
length = bar.get_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(length)
elif isinstance(foo, SharedJoint):
bar: SharedJoint = self.attached_attributes[i]
if key == "accel":
accel_vector = bar.get_accel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[2])
elif key == "vel":
vel_vector = bar.get_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[2])
elif key == "pos":
pos_vector = bar.get_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
if subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
elif key == "force":
force_vector = bar.get_spring_force()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
def update_live_window(self):
if self.stop_event.is_set():
self.timer.stop()
self.pull_data_stop_event.set()
return
final_time = self.raw_xdata[-1]
target_time = final_time - self.window_length
closest_index = min(range(len(self.raw_xdata)), key=lambda i: abs(self.raw_xdata[i] - target_time))
self.window_xdata = self.raw_xdata[closest_index:]
# Attach the data to the appropriate sublines in the subplots
for i in range(self.num_plots):
for j in range(len(self.subplot_keys[i])):
window_ydata = self.raw_ydata[i][j][closest_index:]
if self.plot_settings["step_or_plot"] == "plot":
if len(self.window_xdata) == len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata, window_ydata)
elif len(self.window_xdata) > len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata[:-1], window_ydata)
elif self.plot_settings["step_or_plot"] == "step":
if len(self.window_xdata) == len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata, window_ydata[:-1])
elif len(self.window_xdata) > len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata[:-1], window_ydata[:-1])
def generate_plots(self):
# Create the application
self.app = QtWidgets.QApplication([])
# Create a plot window
self.win = pg.GraphicsLayoutWidget(show=True, title=self.title)
self.win.resize(1000, 600)
self.win.setWindowTitle(self.plot_settings["title"])
self.num_plots = self.plot_settings["num_subplots"]
self.plots = []
for i in range(self.num_plots):
plot = self.win.addPlot(title=self.plot_settings["subplots"][i]["ylabel"])
plot.setLabel('left', self.plot_settings["subplots"][i]["ylabel"])
plot.addLegend()
self.plots.append(plot)
# Move us to the next row for the subplot
if i < self.num_plots - 1:
self.win.nextRow()
def generate_curves(self):
self.subplot_keys = [[] for _ in range(self.num_plots)] # list for each subplot
self.subplot_curves = [[] for _ in range(self.num_plots)] # list for each subplot
self.raw_xdata = []
self.raw_ydata = [[] for _ in range(self.num_plots)] # list for each subplot
self.raw_data_map = {}
for setting in self.shared_attributes_plot_settings:
'''
Each setting looks like {
"accel": {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Accel"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Accel"
}
},
"vel": {
"x": {
"subplot": 2,
"ylabel": "Main Mass x-Vel"
}
},
"pos": {
"x": {
"subplot": 3,
"ylabel": "Main Mass x-Vel"
}
}
}
'''
for key, value in setting.items():
''' key would be like "accel"
value would be like {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Accel"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Accel"
}
}
'''
for line, line_settings in value.items():
''' line would be like "x"
line_settings would be like {
"subplot": 1,
"ylabel": "Main Mass x-Accel",
"color": "w"
}
'''
subplot = line_settings["subplot"]
label = line_settings["ylabel"]
color = line_settings.get("color", "w") # Default to white if no color is specified
self.subplot_keys[subplot].append(label)
if self.plot_settings["step_or_plot"] == "plot":
self.subplot_curves[subplot].append(self.plots[subplot].plot(name=label, pen=pg.mkPen(color=color)))
elif self.plot_settings["step_or_plot"] == "step":
self.subplot_curves[subplot].append(self.plots[subplot].plot(name=label, stepMode=True, pen=pg.mkPen(color=color)))
self.raw_data_map[label] = (subplot, len(self.raw_ydata[subplot]))
self.raw_ydata[subplot].append([])
self.window_xdata = []
self.window_ydata = [[] for _ in range(self.num_plots)] # list for each subplot
def show_plot(self):
pass
def run_process(self):
# Generate the figure and subplots
self.generate_plots()
# Generate the curves
self.generate_curves()
self.pull_data_stop_event = threading.Event()
self.pull_data_stop_event.clear()
pull_data_thread = threading.Thread(target=self.pull_data_thread)
pull_data_thread.start()
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_live_window)
self.timer.start(self.update_interval)
# Start the Qt event loop
QtWidgets.QApplication.instance().exec_()
pull_data_thread.join()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,268 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, SharedJoint, SharedSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting. 1 for the mass and the other for the spring
mass_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Joint Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
}
],
"window_length": 100, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
mass_plotter.attach_shared_x(shared_sim_time)
spring_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Spring Plotter",
"xlabel": "Time (s)",
"num_subplots": 2,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Length (m)"
},
{
"ylabel": "Force (N)"
}
],
"window_length": 100, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 milliseconds
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
spring_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 5, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
wall_joint = Joint(
pos = np.array([-10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "Wall Joint"
)
spring = Spring(
parent_joint = main_mass,
child_joint = wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "Spring"
)
'''Adding items to be plotted'''
# Since we want to plot the physics of the mass and spring, we need shared attributes for them
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
shared_spring: SharedSpring = base_manager.SharedSpring()
spring.attach_shared_attributes(shared_spring)
# Attach the shared elements to the plotter
mass_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
spring_plotter.attach_shared_attributes(
shared_attributes = shared_spring,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": "Spring Length",
"color": "r"
}
},
"force": {
"x": {
"subplot": 1,
"ylabel": "Spring x-Force",
"color": "g"
}
}
}
)
'''Adding items to be visualized'''
# We aready have shared_main_mass and shared_spring from the plotting, so there is no need to make more shared elements
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
mass_plotter_process = multiprocessing.Process(target=mass_plotter.run_process)
spring_plotter_process = multiprocessing.Process(target=spring_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
mass_plotter_process.start()
spring_plotter_process.start()
visualization_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
mass_plotter_process.join()
spring_plotter_process.join()
visualization_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,430 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from HITL_Controller import HITLController
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
from Sensor_Elements import DisplacementSensor, LoadCellSpring, SharedDisplacementSensor, SharedLoadCellSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting.
physics_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
},
{
"ylabel": "Force (N)"
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
physics_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the controller input plotting.
controller_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Pos (m)",
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
controller_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the sensor output plotting
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 2,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Sensor Output (V)",
},
],
"window_length": 10, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Creating an instance for the HITLController'''
hitl_controller = HITLController(
stop_event = stop_event,
settings = {
"pull_from_sim_period": 5,
"plotting_update_period": 10
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 5, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
# Since we want to plot the physics of the mass we need shared attributes for it
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
north_wall_joint = Joint(
pos = np.array([10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "North Wall Joint"
)
north_spring = Spring(
parent_joint = main_mass,
child_joint = north_wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "North Spring"
)
shared_north_spring: SharedSpring = base_manager.SharedSpring()
north_spring.attach_shared_attributes(shared_north_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_north_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# Setting up the actuator. This requires the "grounded_joint" to fix to. We also want a spring in between the actuator and main_mass
actuator_joint = Joint(
pos = np.array([-5, 5, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Actuator Joint"
)
actuator_spring = Spring(
parent_joint = main_mass,
child_joint = actuator_joint,
unstretched_length = 5,
constant_stiffness = 150,
name = "Actuator Spring"
)
shared_actuator_spring: SharedSpring = base_manager.SharedSpring()
actuator_spring.attach_shared_attributes(shared_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Actuator Spring Force",
"color": "r"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
south_wall_joint = Joint(
pos = np.array([-10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "South Wall Joint"
)
rigid_actuator = RigidActuator(
parent_joint = actuator_joint,
grounded_joint = south_wall_joint,
name = "Rigid Actuator",
max_length = 8,
min_length = 2,
control_code = 0 # Position Control
)
shared_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
rigid_actuator.attach_shared_attributes(shared_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Rigid Actuator Controller",
"analog_input_channel": 0,
"digital_input_channel": 0,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 0, # Controls position
"max_length": 8,
"min_length": 2
}
)
rigid_actuator_controller.attach_sim_target(shared_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(rigid_actuator_controller)
shared_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
rigid_actuator_controller.attach_shared_attributes(shared_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Controller Voltage",
"color": "b"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Actuator Controller CTRL Input",
"color": "b"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Actuator Controller Displacement",
"color": "b"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_south_wall_joint: SharedJoint = base_manager.SharedJoint()
south_wall_joint.attach_shared_attributes(shared_south_wall_joint)
shared_actuator_joint: SharedJoint = base_manager.SharedJoint()
actuator_joint.attach_shared_attributes(shared_actuator_joint)
rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_actuator_joint,
child_joint = shared_south_wall_joint,
sensor_settings = {
"name": f"Actuator Displacement Sensor",
"output_channel": 0,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
rigid_actuator_displacement_sensor.attach_shared_attributes(shared_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Displacement Length",
"color": "b"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 1,
"ylabel": f"Actuator Displacement Voltage",
"color": "b"
}
},
}
)
hitl_controller.attach_displacement_sensor(rigid_actuator_displacement_sensor)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
hitl_controller_process = multiprocessing.Process(target=hitl_controller.run_process)
physics_plotter_process.start()
controller_plotter_process.start()
sensor_plotter_process.start()
visualization_process.start()
hitl_controller_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
physics_plotter_process.join()
controller_plotter_process.join()
sensor_plotter_process.join()
visualization_process.join()
hitl_controller_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,649 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from HITL_Controller import HITLController
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
from Sensor_Elements import DisplacementSensor, LoadCellSpring, SharedDisplacementSensor, SharedLoadCellSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting.
physics_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
},
{
"ylabel": "Force (N)"
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
physics_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the controller input plotting.
controller_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Pos (m)",
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
controller_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the sensor output plotting
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Load Cell Force (N)",
},
{
"ylabel": "Sensor Output (V)",
},
],
"window_length": 10, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Creating an instance for the HITLController'''
hitl_controller = HITLController(
stop_event = stop_event,
settings = {
"pull_from_sim_period": 5,
"plotting_update_period": 10
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 10, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
main_mass.add_damping(mom_ratio=0.0)
# Since we want to plot the physics of the mass we need shared attributes for it
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
north_wall_joint = Joint(
pos = np.array([10, 15, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "North Wall Joint"
)
north_spring = Spring(
parent_joint = main_mass,
child_joint = north_wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "North Spring"
)
shared_north_spring: SharedSpring = base_manager.SharedSpring()
north_spring.attach_shared_attributes(shared_north_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_north_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
west_wall_joint = Joint(
pos = np.array([0, 15, -10]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "West Wall Joint"
)
west_spring = Spring(
parent_joint = main_mass,
child_joint = west_wall_joint,
unstretched_length = 9,
constant_stiffness = 100,
name = "West Spring"
)
shared_west_spring: SharedSpring = base_manager.SharedSpring()
west_spring.attach_shared_attributes(shared_west_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_west_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# Setting up the actuator. This requires the "grounded_joint" to fix to. We also want a spring in between the actuator and main_mass
actuator_joint = Joint(
pos = np.array([-5, 10, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Actuator Joint"
)
actuator_spring = Spring(
parent_joint = main_mass,
child_joint = actuator_joint,
unstretched_length = 7,
constant_stiffness = 150,
name = "Actuator Spring"
)
shared_actuator_spring: SharedSpring = base_manager.SharedSpring()
actuator_spring.attach_shared_attributes(shared_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Actuator Spring Force",
"color": "b"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
south_wall_joint = Joint(
pos = np.array([-10, 10, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "South Wall Joint"
)
rigid_actuator = RigidActuator(
parent_joint = actuator_joint,
grounded_joint = south_wall_joint,
name = "Rigid Actuator",
max_length = 8,
min_length = 2,
control_code = 0 # Position Control
)
shared_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
rigid_actuator.attach_shared_attributes(shared_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Rigid Actuator Controller",
"analog_input_channel": 0,
"digital_input_channel": 0,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 0, # Controls position
"max_length": 8,
"min_length": 2
}
)
rigid_actuator_controller.attach_sim_target(shared_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(rigid_actuator_controller)
shared_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
rigid_actuator_controller.attach_shared_attributes(shared_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Controller Voltage",
"color": "r"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Actuator Controller CTRL Input",
"color": "r"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Actuator Controller Displacement",
"color": "r"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_south_wall_joint: SharedJoint = base_manager.SharedJoint()
south_wall_joint.attach_shared_attributes(shared_south_wall_joint)
shared_actuator_joint: SharedJoint = base_manager.SharedJoint()
actuator_joint.attach_shared_attributes(shared_actuator_joint)
rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_actuator_joint,
child_joint = shared_south_wall_joint,
sensor_settings = {
"name": f"Actuator Displacement Sensor",
"output_channel": 0,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
rigid_actuator_displacement_sensor.attach_shared_attributes(shared_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Displacement Length",
"color": "r"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 2,
"ylabel": f"Actuator Displacement Voltage",
"color": "r"
}
},
}
)
hitl_controller.attach_displacement_sensor(rigid_actuator_displacement_sensor)
'''Second actuator'''
bottom_actuator_joint = Joint(
pos = np.array([0, 5, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Bottom Actuator Joint"
)
bottom_actuator_spring = Spring(
parent_joint = main_mass,
child_joint = bottom_actuator_joint,
unstretched_length = 7.5,
constant_stiffness = 1000,
name = "Bottom Actuator Spring"
)
shared_bottom_actuator_spring: SharedSpring = base_manager.SharedSpring()
bottom_actuator_spring.attach_shared_attributes(shared_bottom_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_bottom_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Bottom Actuator Spring Force",
"color": "r"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_bottom_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# LoadCell on the bottom spring
bottom_spring_loadcell = LoadCellSpring(sensor_settings = {
"name": "Bottom Spring LC",
"mV/V": 1000,
"excitation": 10,
"full_scale_force": 5000, # What is the max force on the load cell
"output_channel": 8 # The output channel that the load cell outputs through the MCC3114. Must be between [0, 15]
}
)
bottom_spring_loadcell.attach_sim_source(shared_bottom_actuator_spring) # Point the LoadCellSpring to pull physics values from the SharedSpring
shared_bottom_spring_loadcell: SharedLoadCellSpring = base_manager.SharedLoadCellSpring()
bottom_spring_loadcell.attach_shared_attributes(shared_bottom_spring_loadcell)
hitl_controller.attach_load_cell(bottom_spring_loadcell)
sensor_plotter.attach_shared_attributes(shared_attributes = shared_bottom_spring_loadcell,
plot_settings = {
"force": {
"scalar": {
"subplot": 1, # Must be in range [0, num_subplots-1]
"ylabel": "Bottom Spring LC-Force",
"color": "g" # White is default if not listed. "b", "g", "r", "c", "m", "y", "k", "w"
},
},
"noisy_voltage": {
"scalar": {
"subplot": 2, # Must be in range [0, num_subplots-1]
"ylabel": "Bottom Spring LC-Voltage",
"color": "g" # White is default if not listed. "b", "g", "r", "c", "m", "y", "k", "w"
}
}
}
)
bottom_wall_joint = Joint(
pos = np.array([0, 0, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "Bottom Wall Joint"
)
bottom_rigid_actuator = RigidActuator(
parent_joint = bottom_actuator_joint,
grounded_joint = bottom_wall_joint,
name = "Bottom Rigid Actuator",
max_length = 10,
min_length = 1,
control_code = 1 # Position Control
)
shared_bottom_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
bottom_rigid_actuator.attach_shared_attributes(shared_bottom_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_bottom_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
bottom_rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Bottom Rigid Actuator Controller",
"analog_input_channel": 1,
"digital_input_channel": 2,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 1.5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 1, # Controls position
"max_length": 10,
"min_length": 1
}
)
bottom_rigid_actuator_controller.attach_sim_target(shared_bottom_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(bottom_rigid_actuator_controller)
shared_bottom_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
bottom_rigid_actuator_controller.attach_shared_attributes(shared_bottom_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_bottom_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Bottom Actuator Controller Voltage",
"color": "b"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Bottom Actuator Controller CTRL Input",
"color": "b"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Bottom Actuator Controller Displacement",
"color": "b"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_bottom_wall_joint: SharedJoint = base_manager.SharedJoint()
bottom_wall_joint.attach_shared_attributes(shared_bottom_wall_joint)
shared_bottom_actuator_joint: SharedJoint = base_manager.SharedJoint()
bottom_actuator_joint.attach_shared_attributes(shared_bottom_actuator_joint)
bottom_rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_bottom_actuator_joint,
child_joint = shared_bottom_wall_joint,
sensor_settings = {
"name": f"Bottom Actuator Displacement Sensor",
"output_channel": 1,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_bottom_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
bottom_rigid_actuator_displacement_sensor.attach_shared_attributes(shared_bottom_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_bottom_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Bottom Actuator Displacement Length",
"color": "b"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 2,
"ylabel": f"Bottom Actuator Displacement Voltage",
"color": "b"
}
},
}
)
hitl_controller.attach_displacement_sensor(bottom_rigid_actuator_displacement_sensor)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
hitl_controller_process = multiprocessing.Process(target=hitl_controller.run_process)
physics_plotter_process.start()
controller_plotter_process.start()
sensor_plotter_process.start()
visualization_process.start()
hitl_controller_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
physics_plotter_process.join()
controller_plotter_process.join()
sensor_plotter_process.join()
visualization_process.join()
hitl_controller_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,514 @@
import numpy as np
import multiprocessing
from multiprocessing.managers import BaseManager
import time
# if TYPE_CHECKING:
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedRigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from Object_Sharing import SharedFloat
from HITL_Controller import HITLController
from Sensor_Elements import SharedLoadCellJoint, LoadCellSpring, SharedLoadCellSpring, DisplacementSensor, SharedDisplacementSensor
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
steel_elastic_modulus = 200E9 # 200GPa
gravity = 9.81 # m/s^2
# Stage setup
stage_mass = 500_000 # Mass in kilograms
stage_weight = stage_mass * gravity
max_thrust = 1.5E7 # Max thrust in Newtons. Currently 15_000_000 N
stage_diameter = 5.5 # meters
stage_height = 60 # Meters
wall_thickness = 0.03 # Meters
# Actuator setup
rope_area = 5E-4 # Area in m^2. Currently 5 cm^2
rope_force_at_neutral_actuator = (max_thrust - stage_weight) / 4.0
lateral_offset = 2 # Meters away from the stage wall
actuator_neutral_length = 2
actuator_min_length = 0.5
actuator_max_length = 2.5
actuator_controller_neutral_voltage = 0
actuator_controller_mps_per_volt = 1 # meters per second per volt for the RMS controller to command the sim
actuator_displacement_sensor_neutral_voltage = 0
actuator_displacement_sensor_neutral_length = 0
actuator_displacement_sensor_volts_per_meter = 3 # Volts per meter for the actuator displacement sensor to output
rope_volts_per_newton = 1E-6 # 1 volt is 1 million newtons
# Stage calculations for stiffness of rope supporting main mass
stage_wall_area = np.pi / 4.0 * (stage_diameter**2 - (stage_diameter - wall_thickness*2)**2)
stage_stiffness = steel_elastic_modulus * stage_wall_area / stage_height # k = EA/l
stage_unstretched_length = stage_height - stage_weight / stage_stiffness
# Actuator calculations
rope_stiffness = steel_elastic_modulus * rope_area / stage_height # Estimate due to stageheight != rope length but it is close enough
rope_unstretched_length = rope_force_at_neutral_actuator / rope_stiffness + 56.45
actuator_lateral_offset = stage_diameter / 2 + lateral_offset
actuator_angle = np.arctan(stage_height/actuator_lateral_offset)
actuator_parent_height = actuator_neutral_length * np.sin(actuator_angle)
actuator_parent_lateral_offset = actuator_neutral_length * np.cos(actuator_angle)
def sinusoid_force1(time):
return 100_000*np.sin(0.1*time)
def sinusoid_force2(time):
return 125_000*np.sin(0.2*time+0.3)
def rope_stiffness_func(length, unstretched_length):
if length > unstretched_length:
return rope_stiffness
else:
return 0
def stage_setup_1(_max_run_time=100):
max_run_time = _max_run_time
# SIM SETUP #############################################
manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedLoadCellJoint', SharedLoadCellJoint)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
manager.start()
stop_event = multiprocessing.Event()
stop_event.clear()
shared_sim_time = manager.SharedFloat()
shared_sim_time.set(0.0)
physics_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 5,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Accel (m/s/s)",
},
{
"ylabel": "Vel (m/s)",
},
{
"ylabel": "Pos (m)",
},
{
"ylabel": "Pos 2 (m)",
},
{
"ylabel": "Force (N)",
},
],
"window_length": 100,
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds
})
physics_plotter.attach_shared_x(shared_sim_time)
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Spring Force (N)",
},
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Actuator Position Output (V)",
},
{
"ylabel": "Load Cell Output (V)",
},
],
"window_length": 100, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
controller_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Vel (m/s)",
},
],
"window_length": 100, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
controller_plotter.attach_shared_x(shared_sim_time)
visualizer = Visualization(stop_event=stop_event, scene_settings={
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 100
})
hitl_controller = HITLController(stop_event=stop_event, settings={
"pull_from_sim_period": 5, # Pull values for the sensors every XXX milliseconds. Shouldn't be faster than the update_period for the sim
"plotting_update_period": 10, # Update the shared attributes (for plotting) every YYY milliseconds
})
# SIM ELEMENTS SETUP
# Changes height to 59.78258707863 rather than 60m to that it is in equilibrium
main_mass = Joint(np.array([0, 60, 0]), mass=stage_mass, fixed=False, name="Stage Mass", integration_method="adams-bashforth")
main_mass_shared:SharedJoint = manager.SharedJoint()
main_mass.attach_shared_attributes(main_mass_shared)
main_mass.add_damping(mom_ratio=1.5)
main_mass.add_gravity()
main_mass.add_variable_force([sinusoid_force1, None, sinusoid_force2])
physics_plotter.attach_shared_attributes(main_mass_shared, plot_settings={
"accel": {
"x": {
"subplot": 0,
"ylabel": "Main Mass x-Accel",
"color": "r"
},
"y": {
"subplot": 0,
"ylabel": "Main Mass y-Accel",
"color": "g"
},
"z": {
"subplot": 0,
"ylabel": "Main Mass z-Accel",
"color": "b"
},
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Vel",
"color": "r"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Vel",
"color": "g"
},
"z": {
"subplot": 1,
"ylabel": "Main Mass z-Vel",
"color": "b"
}
},
"pos": {
"x": {
"subplot": 2,
"ylabel": "Main Mass x-Pos",
"color": "r"
},
"y": {
"subplot": 3,
"ylabel": "Main Mass y-Pos",
"color": "g"
},
"z": {
"subplot": 2,
"ylabel": "Main Mass z-Pos",
"color": "b"
}
}
})
visualizer.attach_shared_attributes(main_mass_shared, object_settings={
"type": "sphere",
"color": "red",
"radius": stage_diameter/2.0
})
support_spring_joint = Joint(np.array([0, stage_height*2, 0]), mass=0, fixed=True, name="Support Spring Joint")
support_spring = Spring(parent_joint=main_mass, child_joint=support_spring_joint, unstretched_length=stage_unstretched_length, constant_stiffness=stage_stiffness, name="Support Spring")
support_spring_shared_attributes:SharedSpring = manager.SharedSpring()
support_spring.attach_shared_attributes(support_spring_shared_attributes)
physics_plotter.attach_shared_attributes(support_spring_shared_attributes, plot_settings={
"force": {
"y": {
"subplot": 4,
"ylabel": "Support Spring y-Force",
"color": "w"
}
}
})
# Setting up the actuators and ropes
for i in range(4):
if i == 0:
floor_x_pos = actuator_lateral_offset
floor_z_pos = 0
parent_x_pos = actuator_lateral_offset - actuator_parent_lateral_offset
parent_z_pos = 0
name = "East Actuator"
name2 = "East Spring"
color = "r"
elif i == 1:
floor_x_pos = 0
floor_z_pos = actuator_lateral_offset
parent_x_pos = 0
parent_z_pos = actuator_lateral_offset - actuator_parent_lateral_offset
name = "South Actuator"
name2 = "South Spring"
color = "g"
elif i == 2:
floor_x_pos = -1*actuator_lateral_offset
floor_z_pos = 0
parent_x_pos = -1*actuator_lateral_offset + actuator_parent_lateral_offset
parent_z_pos = 0
name = "West Actuator"
name2 = "West Spring"
color = "b"
elif i == 3:
floor_x_pos = 0
floor_z_pos = -1*actuator_lateral_offset
parent_x_pos = 0
parent_z_pos = -1*actuator_lateral_offset + actuator_parent_lateral_offset
name = "North Actuator"
name2 = "North Spring"
color = "w"
actuator_spring_joint = Joint(np.array([parent_x_pos, actuator_parent_height, parent_z_pos]), mass=0.001, fixed=False, name=f"{name} Spring Joint")
actuator_floor_joint = Joint(np.array([floor_x_pos, 0, floor_z_pos]), mass=0.001, fixed=False, name=f"{name} Floor Joint")
#rope = Spring(parent_joint=main_mass, child_joint=actuator_spring_joint, unstretched_length=rope_unstretched_length, constant_stiffness=rope_stiffness, name=name2)
rope = Spring(parent_joint=main_mass, child_joint=actuator_spring_joint, unstretched_length=rope_unstretched_length, stiffness_func=rope_stiffness_func, name=name2)
rope_shared:SharedSpring = manager.SharedSpring()
rope.attach_shared_attributes(rope_shared)
visualizer.attach_shared_attributes(rope_shared, object_settings={
"type": "helix",
"color": "white",
"radius": 0.1,
"thickness": 0.1
})
rope_lc = LoadCellSpring(sensor_settings={
"name": f"{name2} LC",
"mV/V": 1000,
"excitation": 10,
"full_scale_force": (1/rope_volts_per_newton * 10),
"output_channel": i*4 + 3,
"noise_settings": {
"static_error_band": 0.15, # % of FSO
"non-linearity": 0.15, # % of FSO
"hysteresis": 0.07, # % of FSO
"repeatability": 0.02, # % of FSO
"thermal_error": 0.005, # % of reading per degree F
"temperature_offset": 10 # Degrees F off from calibration temperature
}
})
rope_lc.attach_sim_source(rope_shared)
rope_lc_shared_attributes = manager.SharedLoadCellSpring()
rope_lc.attach_shared_attributes(rope_lc_shared_attributes)
hitl_controller.attach_load_cell(rope_lc)
sensor_plotter.attach_shared_attributes(rope_lc_shared_attributes, plot_settings={
"force": {
"scalar": {
"subplot": 0,
"ylabel": f"{name2} LC Force",
"color": color
}
},
"noisy_voltage": {
"scalar": {
"subplot": 3,
"ylabel": f"{name2} LC Noisy-Voltage",
"color": color
}
},
})
actuator = RigidActuator(actuator_spring_joint, actuator_floor_joint, name=name, max_length=actuator_max_length, min_length=actuator_min_length, control_code=1)
actuator_shared_attributes:SharedRigidActuator = manager.SharedRigidActuator()
actuator.attach_shared_attributes(actuator_shared_attributes)
visualizer.attach_shared_attributes(actuator_shared_attributes, object_settings={
"type": "cylinder",
"color": "black",
"radius": 0.3
})
shared_actuator_spring_joint: SharedJoint = manager.SharedJoint()
actuator_spring_joint.attach_shared_attributes(shared_actuator_spring_joint)
shared_actuator_floor_joint: SharedJoint = manager.SharedJoint()
actuator_floor_joint.attach_shared_attributes(shared_actuator_floor_joint)
actuator_disp_sensor = DisplacementSensor(parent_joint=shared_actuator_spring_joint, child_joint=shared_actuator_floor_joint, sensor_settings={
"name": f"{name} Sensor",
"output_channel": i*4 + 2,
"volts_per_meter": actuator_displacement_sensor_volts_per_meter,
"neutral_length": actuator_displacement_sensor_neutral_length, # Length at neutral voltage
"neutral_voltage": actuator_displacement_sensor_neutral_voltage,
"max_length": actuator_max_length,
"noise_settings": {
"static_error_band": 0.15, # % of FSO
"non-linearity": 0.15, # % of FSO
"hysteresis": 0.07, # % of FSO
"repeatability": 0.02, # % of FSO
"thermal_error": 0.005, # % of reading per degree F
"temperature_offset": 10 # Degrees F off from calibration temperature
}
})
actuator_disp_sensor_shared:SharedDisplacementSensor = manager.SharedDisplacementSensor()
actuator_disp_sensor.attach_shared_attributes(actuator_disp_sensor_shared)
sensor_plotter.attach_shared_attributes(actuator_disp_sensor_shared, plot_settings={
"length": {
"scalar": {
"subplot": 1,
"ylabel": f"{name} Length",
"color": color
}
},
"voltage": {
"noisy": {
"subplot": 2,
"ylabel": f"{name} Noisy-Voltage",
"color": color
}
},
})
hitl_controller.attach_displacement_sensor(actuator_disp_sensor)
actuator_controller = RigidActuatorController(controller_settings={
"name": "Main Actuator Controller",
"analog_input_channel": i,
"digital_input_channel": i,
"units_per_volt": actuator_controller_mps_per_volt, # How far the actuator moves per input volt (m/s or m/s/s if controlling vel or accel)
"neutral_length": actuator_neutral_length, # Length at neutral voltage
"neutral_voltage": actuator_controller_neutral_voltage, # Voltage to go to neutral length
"controls_pos/vel/accel": 1, # 0 = controls pos, 1 = controls vel, 2 = controls accel,
"max_length": actuator_max_length,
"min_length": actuator_min_length
})
actuator_controller_shared_attributes = manager.SharedRigidActuatorController() # For plotting
actuator_controller.attach_shared_attributes(actuator_controller_shared_attributes)
actuator_controller.attach_sim_target(actuator_shared_attributes) # So the controller can update sim elements
hitl_controller.attach_rigid_actuator_controller(actuator_controller)
controller_plotter.attach_shared_attributes(actuator_controller_shared_attributes, plot_settings={
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"{name} Controller Voltage",
"color": color
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"{name} Controller CTRL Input",
"color": color
}
},
"vel": {
"x": {
"subplot": 2,
"ylabel": f"{name} Controlled x-Vel",
"color": color
}
}
})
# PROCESSES #############################################################
# Make the simulation process
simulation = BFSSimulation(parent_joint=main_mass, settings={
"duration": max_run_time,
"delta_t": None,
"shared_update_period": 0.1, # not used
"plotting_update_period": 0.01,
"sensor_update_period": 0.01,
"controller_pull_period": 0.01
})
simulation.attach_shared_time(shared_sim_time)
simulation_process = multiprocessing.Process(target=simulation.run_process)
# Start the processes
# Start the visualization process
visualization_process = multiprocessing.Process(target=visualizer.run_process)
visualization_process.start()
# Start the physics_plotter process
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
physics_plotter_process.start()
# Start the sensor physics_plotter process
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
sensor_plotter_process.start()
# Start the sensor physics_plotter process
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
controller_plotter_process.start()
# Start the HITL interface process
hitl_process = multiprocessing.Process(target=hitl_controller.run_process)
hitl_process.start()
time.sleep(5)
simulation_process.start()
# Join the processes
simulation_process.join()
stop_event.set()
visualization_process.join()
physics_plotter_process.join()
sensor_plotter_process.join()
controller_plotter_process.join()
hitl_process.join()
# Close the manager
manager.shutdown()
def main():
max_run_time = 10000
#multiprocessed_double_mass_spring(100)
#single_spring(max_run_time)
stage_setup_1(max_run_time)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,112 @@
import matplotlib.pyplot as plt
from copy import deepcopy
class StateSpace():
def __init__(self, pos, vel, accel):
self.pos = pos
self.vel = vel
self.accel = accel
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
class Block():
def __init__(self):
self.mass = 5
self.statespace = StateSpace(5,0,0)
self.net_force = 0
class Spring():
def __init__(self):
self.mass = 1
self.pos1 = 0
self.pos2 = 10
self.vel = 0
self.accel = 0
self.length = self.pos2 - self.pos1
self.zero_length = 10
self.k = 10
self.del_l = self.length - self.zero_length
self.force = self.del_l * self.k
def get_force(self):
self.length = self.pos2 - self.pos1
self.del_l = self.length - self.zero_length
self.force = self.del_l * self.k
return self.force
applied_force = 0
block = Block()
spring = Spring()
# Initialize lists to store data for plotting
t_values = []
pos_values = []
vel_values = []
accel_values = []
force_net_values = []
spring_force_values = []
del_l_values = []
del_t = 0.1
t = 0
while t < 10:
spring.pos1 = block.statespace.pos
spring.force = spring.get_force()
block.net_force = applied_force + spring.force
block.statespace.accel = block.net_force / block.mass
block.statespace.vel += block.statespace.accel * del_t
block.statespace.pos += block.statespace.vel * del_t
# Store data for plotting
t_values.append(t)
pos_values.append(block.statespace.pos)
vel_values.append(block.statespace.vel)
accel_values.append(block.statespace.accel)
force_net_values.append(block.net_force)
spring_force_values.append(spring.force)
del_l_values.append(spring.del_l)
t += del_t
print(max(pos_values))
# Plot the data
plt.figure(figsize=(12, 8))
plt.subplot(3, 1, 1)
plt.plot(t_values, pos_values)
plt.plot(t_values, del_l_values)
plt.title('Position vs Time')
plt.legend(['Position', 'Delta Length'])
plt.xlabel('Time (s)')
plt.ylabel('Position')
plt.subplot(3, 1, 2)
plt.plot(t_values, vel_values)
plt.title('Velocity vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Velocity')
plt.subplot(3, 1, 3)
plt.plot(t_values, accel_values)
plt.plot(t_values, force_net_values)
plt.plot(t_values, spring_force_values)
plt.legend(['Acceleration', 'Net Force', 'Spring Force'])
plt.title('Acceleration, Net Force, and Spring Force vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Value')
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,436 @@
import math
import numpy as np
from typing import List
import matplotlib.pyplot as plt
ZERO_VECTOR = np.array([0.0, 0.0, 0.0])
class StateSpace():
def __init__(self, pos=np.copy(ZERO_VECTOR), vel=np.copy(ZERO_VECTOR), accel=np.copy(ZERO_VECTOR), force=np.copy(ZERO_VECTOR)):
self.pos = pos
self.vel = vel
self.accel = accel
self.force = force
def get_pos(self):
return self.pos
def get_vel(self):
return self.vel
def get_accel(self):
return self.accel
def get_force(self):
return self.force
def set_pos(self, new_pos):
self.pos = new_pos
def set_vel(self, new_vel):
self.vel = new_vel
def set_accel(self, new_accel):
self.accel = new_accel
def set_force(self, new_force):
self.force = new_force
def integrate(self, delta_t):
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
class Joint():
def __init__(self, pos=np.copy(ZERO_VECTOR), fixed: bool=False):
self.statespace = StateSpace(pos, ZERO_VECTOR, ZERO_VECTOR, ZERO_VECTOR)
self.connected_springs: List[Spring] = []
self.connected_masses: List[Mass] = []
self.connected_rigid_bodies: List[RigidBody] = []
self.external_forces: List[List[float, float, float]] = []
self.fixed = fixed
self.parent_joints: List[Joint] = []
self.child_joints: List[Joint] = []
def get_pos(self):
return self.statespace.get_pos()
def get_vel(self):
return self.statespace.get_vel()
def get_accel(self):
return self.statespace.get_accel()
def get_force(self):
return self.statespace.get_force()
def get_connected_springs(self):
return self.connected_springs
def get_connected_masses(self):
return self.connected_masses
def get_connected_rigid_bodies(self):
return self.connected_rigid_bodies
def is_fixed(self):
return self.fixed
def add_spring(self, new_spring):
self.connected_springs.append(new_spring)
def add_mass(self, new_mass):
self.connected_masses.append(new_mass)
def add_rigid_body(self, new_rigid_body):
self.connected_rigid_bodies.append(new_rigid_body)
def add_extenal_force(self, new_force = ZERO_VECTOR):
self.external_forces.append(new_force)
def calc_net_spring_force(self):
net_spring_force = 0
for spring in self.connected_springs:
spring_force = spring.calc_spring_force() # This is the force the spring exerts on the joint ASSUMING self is parent_joint of the spring
net_spring_force += spring_force
return net_spring_force
def calc_net_external_force(self):
net_external_force = np.copy(ZERO_VECTOR)
for force in self.external_forces:
net_external_force += force
return net_external_force
def calc_net_force(self):
net_external_force = self.calc_net_external_force()
net_spring_force = self.calc_net_spring_force()
self.statespace.force = net_external_force + net_spring_force
return self.statespace.force
def integrate_accel_vel(self, delta_t: float=0.1):
self.statespace.integrate(delta_t)
def set_accel(self, accel):
self.statespace.set_accel(accel)
def find_parent_joints(self):
for spring in self.connected_springs:
if spring.child_joint == self:
if spring.parent_joint not in self.parent_joints:
self.parent_joints.append(spring.parent_joint)
for mass in self.connected_masses:
if mass.child_joint == self:
if mass.parent_joint not in self.parent_joints:
self.parent_joints.append(mass.parent_joint)
def find_child_joints(self):
for spring in self.connected_springs:
if spring.parent_joint == self:
if spring.child_joint not in self.child_joints:
self.child_joints.append(spring.child_joint)
for mass in self.connected_masses:
if mass.parent_joint == self:
if mass.child_joint not in self.child_joints:
self.child_joints.append(mass.child_joint)
class Spring():
def __init__(self, parent_joint: Joint, child_joint: Joint, zero_length: float=0, stiffness: float=0):
self.parent_joint = parent_joint
self.child_joint = child_joint
self.zero_length = zero_length
self.stiffness = stiffness
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector = self.calc_r_unit_vector()
self.spring_force = self.calc_spring_force()
self.parent_joint.add_spring(self)
self.child_joint.add_spring(self)
def calc_r_vector(self):
self.vector = self.child_joint.get_pos() - self.parent_joint.get_pos()
return self.vector
def calc_length(self):
self.vector = self.calc_r_vector()
return np.linalg.norm(self.vector)
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector = self.vector / self.length
return self.unit_vector
def calc_spring_force(self):
'''Positive force is in tension. Aka, the spring PULLS on the other object'''
self.length = self.calc_length()
del_length = self.length - self.zero_length
spring_force = del_length * self.stiffness
self.r_unit_vector = self.calc_r_unit_vector()
self.spring_force = spring_force * self.r_unit_vector
return self.spring_force
class Mass():
'''
A mass is a point mass located in the center of 2 joints.
It cannot exert a force
'''
def __init__(self, parent_joint: Joint, child_joint: Joint, mass: float=0):
self.parent_joint = parent_joint
self.child_joint = child_joint
self.mass = mass
self.parent_joint.add_mass(self)
self.child_joint.add_mass(self)
self.statespace = StateSpace((child_joint.get_pos() + parent_joint.get_pos()) / 2.0,
(child_joint.get_vel() + parent_joint.get_vel()) / 2.0,
(child_joint.get_accel() + parent_joint.get_accel()) / 2.0,
(child_joint.get_force() + parent_joint.get_force()) / 2.0)
def set_accel(self, accel):
self.statespace.set_accel(accel)
def integrate_accel_vel(self, delta_t: float=0.1):
self.statespace.integrate(delta_t)
class Simulation():
def __init__(self, parent_joint: Joint, duration: float, delta_t: float):
self.parent_joint = parent_joint
self.duration = duration
self.delta_t = delta_t
self.joints: List[Joint] = []
self.masses: List[Mass] = []
self.springs: List[Spring] = []
self.rigid_bodies: List[RigidBody] = []
def get_all_nodes_and_edges(self):
'''
Do a BFS to get all of the joints (nodes) and springs/masses (edges) in the system
'''
queue: List[Joint] = []
queue.append(self.parent_joint)
while queue:
node: Joint
node = queue.pop(0)
if node not in self.joints:
self.joints.append(node)
connected_springs = node.get_connected_springs()
connected_masses = node.get_connected_masses()
connected_rigid_bodies = node.get_connected_rigid_bodies()
for spring in connected_springs:
if spring not in self.springs:
self.springs.append(spring)
if spring.child_joint not in self.joints and spring.child_joint not in queue:
queue.append(spring.child_joint)
for mass in connected_masses:
if mass not in self.masses:
self.masses.append(mass)
if mass.child_joint not in self.joints and mass.child_joint not in queue:
queue.append(mass.child_joint)
for rigid_body in connected_rigid_bodies:
if rigid_body not in self.rigid_bodies:
self.rigid_bodies.append(rigid_body)
if rigid_body.child_joint not in self.joints and rigid_body.child_joint not in queue:
queue.append(rigid_body.child_joint)
def print_components(self):
print("Joints:")
count = 0
for joint in self.joints:
print(f"Accel: {joint.get_accel()}")
print(f"Vel: {joint.get_vel()}")
print(f"Pos: {joint.get_pos()}")
print()
print("Masses:")
count = 0
for mass in self.masses:
print(f"Accel: {mass.statespace.get_accel()}")
print(f"Vel: {mass.statespace.get_vel()}")
print(f"Pos: {mass.statespace.get_pos()}")
print()
print("Springs:")
count = 0
for spring in self.springs:
print(f"Spring Force: {spring.spring_force}")
print(f"Spring Length: {spring.length}")
print()
print("Rigid Bodies:")
count = 0
for rigid_body in self.rigid_bodies:
print(f"Transfer Force: {rigid_body.force}")
print()
def calc_force_at_every_joint(self):
'''
At every joint, calculate the net force at each joint (this accounts for springs and external forces).
'''
for joint in self.joints:
joint.calc_net_force()
def calc_rigid_body_force(self):
for body in self.rigid_bodies:
body.force = 0
def calc_accel_at_every_mass(self):
'''
Using the sum of the forces at the 2 joints on each mass, we calc the accel of the mass
'''
for mass in self.masses:
net_force = mass.parent_joint.get_force() + mass.child_joint.get_force()
accel = net_force / mass.mass
mass.set_accel(accel)
def assign_joint_accel(self):
'''
If the joint is fixed, accel = 0
'''
net_joint_accel = np.copy(ZERO_VECTOR)
for joint in self.joints:
if joint.is_fixed() == True:
continue
for mass in joint.get_connected_masses():
net_joint_accel += mass.statespace.get_accel()
joint.set_accel(net_joint_accel)
def integrate_timestep(self):
self.calc_force_at_every_joint()
self.calc_accel_at_every_mass()
self.assign_joint_accel()
for joint in self.joints:
joint.integrate_accel_vel(self.delta_t)
for mass in self.masses:
mass.integrate_accel_vel(self.delta_t)
def run(self):
mass_pos_values = []
time_values = []
t = 0
while t < self.duration:
self.integrate_timestep()
self.print_components()
mass_pos_values.append(self.masses[0].statespace.get_pos())
time_values.append(t)
print("*"*100)
t += self.delta_t
plt.close()
# Plot the data
plt.figure(figsize=(12, 8))
plt.plot(time_values, mass_pos_values)
plt.title('Position vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Position')
plt.show()
class BFSSimulation():
def __init__(self, parent_joint: Joint, duration: float, delta_t: float):
self.parent_joint = parent_joint
self.duration = duration
self.delta_t = delta_t
self.joints: List[Joint] = []
self.get_joints_bfs()
self.get_parent_joints_for_every_joint()
def get_joints_bfs(self):
queue: List[Joint] = []
queue.append(self.parent_joint)
visited_springs: List[Spring] = []
visited_masses: List[Spring] = []
while queue:
node: Joint
node = queue.pop(0)
if node not in self.joints:
self.joints.append(node)
connected_springs = node.get_connected_springs()
connected_masses = node.get_connected_masses()
for spring in connected_springs:
if spring not in visited_springs:
visited_springs.append(spring)
if spring.child_joint not in self.joints and spring.child_joint not in queue:
queue.append(spring.child_joint)
for mass in connected_masses:
if mass not in visited_masses:
visited_masses.append(mass)
if mass.child_joint not in self.joints and mass.child_joint not in queue:
queue.append(mass.child_joint)
def get_parent_joints_for_every_joint(self):
for joint in self.joints:
joint.find_parent_joints()
def get_child_joints_for_every_joint(self):
for joint in self.joints:
joint.find_child_joints()
def integrate_joints(self):
for joint in self.joints:
joint.calc_net_force()
def main():
joint_left_mass1 = Joint(np.array([0,0,0]), fixed=False)
joint_mass1_spring1 = Joint(np.array([0,0,0]), fixed=False)
joint_spring1_rigidbody1 = Joint(np.array([10,0,0]), fixed=False)
joint_rigidbody1_spring2 = Joint(np.array([10,0,0]), fixed=False)
joint_right_spring2 = Joint(np.array([20,0,0]), fixed=True)
mass1 = Mass(joint_left_mass1, joint_mass1_spring1, 5)
spring1 = Spring(joint_mass1_spring1, joint_spring1_rigidbody1, 10, 10)
#rigidbody1 = RigidBody(joint_spring1_rigidbody1, joint_rigidbody1_spring2, 0.0)
spring2 = Spring(joint_rigidbody1_spring2, joint_right_spring2, 10, 100)
joint_left_mass1.add_extenal_force(np.array([5, 0, 0]))
simulation = BFSSimulation(joint_left_mass1, 1, 0.1)
simulation.get_all_nodes_and_edges()
simulation.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,30 @@
import pickle
from PhysicsElements import StateSpace, ZERO_VECTOR, Joint, Spring
import numpy as np
from Object_Sharing import SharedJointList
def is_picklable(obj):
try:
pickle.dumps(obj)
return True
except pickle.PicklingError:
return False
from multiprocessing.managers import BaseManager
def func(x,y):
return 10
if __name__ == '__main__':
BaseManager.register('Joint', Joint)
BaseManager.register('Spring', Spring)
manager = BaseManager()
manager.start()
north_wall_joint:Joint = manager.Joint(np.array([10,0,0]), mass=0.001, fixed=True, name="North Wall Joint")
main_mass:Joint = manager.Joint(np.array([0,0,0]), mass=3, fixed=False, name="Blue Mass")
middle_mass:Joint = manager.Joint(np.array([5,0,0]), mass=5, fixed=False, name="White Mass")
spring1 = manager.Spring(parent_joint=middle_mass, child_joint=north_wall_joint, unstretched_length=7.5, stiffness_func=func, name="Spring 1")
print(is_picklable(spring1))

View File

@ -0,0 +1,111 @@
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import random
from multiprocessing import Process, Manager
class LivePlot:
def __init__(self, mult=1, max_data_points=100):
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(8, 6))
self.fig = fig
self.ax1 = ax1
self.ax2 = ax2
self.ax3 = ax3
self.mult = mult
self.ax1.set_ylabel('Plot 1')
self.ax2.set_ylabel('Plot 2')
self.ax3.set_ylabel('Plot 3')
self.ax3.set_xlabel('Time')
self.max_data_points = max_data_points
self.t = 0
self.x_data = []
self.y_data1 = []
self.y_data1_2 = []
self.y_data2 = []
self.y_data3 = []
# Initialize lines (empty initially)
self.line1, = self.ax1.plot([], [], label='Plot 1')
self.line1_2, = self.ax1.plot([], [], label='Plot 1.2')
self.line2, = self.ax2.plot([], [], label='Plot 2')
self.line3, = self.ax3.plot([], [], label='Plot 3')
self.ax1.legend()
self.ax2.legend()
self.ax3.legend()
def generate_random_data(self):
return random.randint(1, 100)
def update_data_external(self):
# Simulate external updates (replace with your actual data update mechanism)
new_x = self.t
self.t += 1
new_y1 = self.generate_random_data() * self.mult
new_y2 = self.generate_random_data() * self.mult
new_y3 = self.generate_random_data() * self.mult
self.x_data.append(new_x)
self.y_data1.append(new_y1)
self.y_data1_2.append(new_y1 * -1) # Example modification of data
self.y_data2.append(new_y2)
self.y_data3.append(new_y3)
# Keep only the last `max_data_points` data points
self.x_data = self.x_data[-self.max_data_points:]
self.y_data1 = self.y_data1[-self.max_data_points:]
self.y_data1_2 = self.y_data1_2[-self.max_data_points:]
self.y_data2 = self.y_data2[-self.max_data_points:]
self.y_data3 = self.y_data3[-self.max_data_points:]
def update_plot(self, i):
self.update_data_external()
# Update plot data
self.line1.set_data(self.x_data, self.y_data1)
self.line1_2.set_data(self.x_data, self.y_data1_2)
self.line2.set_data(self.x_data, self.y_data2)
self.line3.set_data(self.x_data, self.y_data3)
# Adjust plot limits (x-axis)
if len(self.x_data) > 1:
self.ax1.set_xlim(self.x_data[0], self.x_data[-1])
self.ax2.set_xlim(self.x_data[0], self.x_data[-1])
self.ax3.set_xlim(self.x_data[0], self.x_data[-1])
# Adjust plot limits (y-axis) - optional
self.ax1.relim()
self.ax1.autoscale_view()
self.ax2.relim()
self.ax2.autoscale_view()
self.ax3.relim()
self.ax3.autoscale_view()
#return self.line1, self.line2, self.line3
def animate(self):
anim = animation.FuncAnimation(self.fig, self.update_plot, frames=1000, interval=10, blit=False)
plt.show()
#anim.save(filename="test.mp4")
# Function to run animation in a separate process
def run_animation(fig, ax1, ax2, ax3):
#live_plot = LivePlot(fig, ax1, ax2, ax3)
#live_plot.animate()
pass
# Example usage with multiprocessing
if __name__ == '__main__':
plot1 = LivePlot(mult=1)
plot2 = LivePlot(mult=5)
process1 = Process(target=plot1.animate)
process2 = Process(target=plot2.animate)
process1.start()
process2.start()
process1.join()
process2.join()

View File

@ -0,0 +1,59 @@
from mcculw import ul
from mcculw.enums import DigitalIODirection
from mcculw.ul import ULError
# Define the board number
BOARD_NUM = 0
# Define the ports and the direction of data flow
PORT_A = 10
PORT_B = 11
PORT_C_LOW = 12
PORT_C_HIGH = 13
# Configure ports as input or output
def configure_ports():
try:
ul.d_config_port(BOARD_NUM, PORT_A, DigitalIODirection.OUT)
ul.d_config_port(BOARD_NUM, PORT_B, DigitalIODirection.IN)
ul.d_config_port(BOARD_NUM, PORT_C_LOW, DigitalIODirection.OUT)
ul.d_config_port(BOARD_NUM, PORT_C_HIGH, DigitalIODirection.IN)
print("Ports configured successfully.")
except ULError as e:
print(f"Error configuring ports: {e}")
# Write data to a port
def write_data(port, data):
try:
ul.d_out(BOARD_NUM, port, data)
print(f"Data {data} written to port {port} successfully.")
except ULError as e:
print(f"Error writing data to port {port}: {e}")
# Read data from a port
def read_data(port):
try:
data = ul.d_in(BOARD_NUM, port)
print(f"Data read from port {port}: {data}")
return data
except ULError as e:
print(f"Error reading data from port {port}: {e}")
return None
def main():
# Configure the ports
configure_ports()
# Write some data to PORT_A and PORT_C_LOW
write_data(PORT_A, 0xFF) # Write 0xFF (255) to PORT_A
write_data(PORT_C_LOW, 0xAA) # Write 0xAA (170) to PORT_C_LOW
# Read data from PORT_B and PORT_C_HIGH
data_b = read_data(PORT_B)
data_c_high = read_data(PORT_C_HIGH)
# Perform additional processing as needed
# For example, you might want to perform some logic based on the input data
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
import cProfile
import pstats
# Load the profiling data from the file
stats = pstats.Stats('profile_results.txt')
# Print the top 10 functions by cumulative time
stats.sort_stats('cumulative').print_stats(25)

View File

@ -0,0 +1,78 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore
import numpy as np
class RealTimePlot:
def __init__(self, update_interval=1, max_display_time=10, num_subplots=2, line_styles=None):
# Create the application
self.app = QtWidgets.QApplication([])
# Create a plot window
self.win = pg.GraphicsLayoutWidget(show=True, title="Real-Time Plot")
self.win.resize(1000, 600)
# Add plots
self.plots = []
self.curves = []
self.line_styles = line_styles if line_styles is not None else ['line'] * num_subplots
for i in range(num_subplots):
plot = self.win.addPlot(title=f"Subplot {i+1}")
plot.setLabel('left', f'Subplot {i+1} Y-Axis')
plot.addLegend() # Add a legend to each plot
self.plots.append(plot)
if self.line_styles[i] == 'step':
curve = plot.plot(pen='y', name=f'Subplot {i+1} Data', stepMode=True)
else:
curve = plot.plot(pen='y', name=f'Subplot {i+1} Data')
self.curves.append(curve)
if i < num_subplots - 1:
self.win.nextRow()
# Data buffers
self.xdata = [np.empty(0) for _ in range(num_subplots)]
self.ydata = [np.empty(0) for _ in range(num_subplots)]
# Parameters
self.update_interval = update_interval
self.max_display_time = max_display_time
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_live_window)
self.timer.start(update_interval)
def update_live_window(self):
for i in range(len(self.plots)):
# Generate new data
t = self.xdata[i][-1] + self.update_interval / 1000.0 if len(self.xdata[i]) > 0 else 0
y = np.sin(2 * np.pi * t + i) # Different phase for each subplot
# Append new data to buffers
self.xdata[i] = np.append(self.xdata[i], t)
self.ydata[i] = np.append(self.ydata[i], y)
# Remove old data to keep the buffer size within max_display_time
if t > self.max_display_time:
self.xdata[i] = self.xdata[i][self.xdata[i] > t - self.max_display_time]
self.ydata[i] = self.ydata[i][-len(self.xdata[i]):]
# Ensure correct lengths for step mode
if self.line_styles[i] == 'step':
xdata_step = np.append(self.xdata[i], self.xdata[i][-1] + self.update_interval / 1000.0)
self.curves[i].setData(xdata_step, self.ydata[i])
else:
self.curves[i].setData(self.xdata[i], self.ydata[i])
def run(self):
# Start the Qt event loop
QtWidgets.QApplication.instance().exec_()
# Parameters
update_interval = 100 # milliseconds
max_display_time = 10 # seconds
num_subplots = 2 # number of subplots
line_styles = ['line', 'step'] # specify 'line' or 'step' for each subplot
# Create and run the real-time plot
rt_plot = RealTimePlot(update_interval, max_display_time, num_subplots, line_styles)
rt_plot.run()

View File

@ -0,0 +1,99 @@
import multiprocessing
from multiprocessing.managers import BaseManager
import numpy as np
import time
import random
from PhysicsElements import Joint, Spring, SpringWithMass, Actuator, StateSpace
class CustomManager(BaseManager):
# nothing
pass
def worker_process(shared_state_space):
while True:
print("Worker Process:")
shared_state_space.print()
time.sleep(0.1)
def worker_process2(shared_joint: Joint):
while True:
print("Worker Process:")
statespace = shared_joint.get_state_space()
print(statespace.print())
time.sleep(0.1)
def worker_process3(shared_joint_list: list):
while True:
print("Worker Process:")
statespace:StateSpace = shared_joint_list.at(0).get_state_space()
statespace.print()
time.sleep(0.1)
def statespace_changer(shared_joint_list: list):
while True:
rand_int = random.randint(0, 10)
shared_joint_list.at(0).set_state_space_pos(np.array([rand_int,0,0]))
print("Changed pos")
time.sleep(1)
class SharedJointList():
def __init__(self):
self.list: list = []
def append(self, new_joint: Joint):
self.list.append(new_joint)
def at(self, index: int):
return self.list[index]
if __name__ == "__main__":
# Create a multiprocessing manager
# CustomManager.register('StateSpace', StateSpace)
# CustomManager.register('Joint', Joint)
# CustomManager.register('SharedJointList', SharedJointList)
#BaseManager.register('StateSpace', StateSpace)
BaseManager.register('Joint', Joint)
BaseManager.register('SharedJointList', SharedJointList)
with BaseManager() as manager:
#shared_statespace = manager.StateSpace()
shared_joint:Joint = manager.Joint(pos=np.array([0,0,-4]), mass=0.001, fixed=True, name="North Actuator Joint")
joint_list:SharedJointList = manager.SharedJointList()
rand_joint = manager.Joint(pos=np.array([0,2,15]),
mass=0.001,
fixed=True,
name="Random Joint")
joint_list.append(rand_joint)
#proc = multiprocessing.Process(target=worker_process, args=(shared_statespace,))
#proc = multiprocessing.Process(target=worker_process2, args=(shared_joint,))
proc = multiprocessing.Process(target=worker_process3, args=(joint_list,))
proc.start()
changer_proc = multiprocessing.Process(target=statespace_changer, args=(joint_list,))
changer_proc.start()
time.sleep(2)
#shared_statespace.set_pos(np.array([-8,-4,-2]))
#shared_joint.set_state_space_pos((np.array([-8,-4,-2])))
while True:
print("Main: ", end="")
joint_list.at(0).get_state_space().print()
time.sleep(1)
proc.join()
changer_proc.join()

View File

@ -0,0 +1,3 @@
{
"python.REPL.enableREPLSmartSend": false
}

1386
code/setup/README.md Normal file

File diff suppressed because it is too large Load Diff

21
code/setup/code/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1"
},
"args": [
"-Xfrozen_modules=off"
]
}
]
}

3
code/setup/code/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.REPL.enableREPLSmartSend": false
}

View File

@ -0,0 +1,197 @@
import numpy as np
from Physics_Elements import SharedRigidActuator
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
from Physics_Elements import StateSpace, Spring
class RigidActuatorController():
def __init__(self, controller_settings: dict):
self.controller_settings = controller_settings
self.attached_sim_target: SharedRigidActuator = None
self.shared_attributes: SharedRigidActuatorController = None # For plotting if desired
self.parse_settings()
self.input_voltage: float = 0.0
self.digital_command: int = 0 # 0 means hold position, 1 means control using velocity
self.controlled_length: float = 0.0
self.controlled_vel_scalar: float = 0.0
self.controlled_pos = np.array([0.0, 0.0, 0.0])
self.controlled_vel = np.array([0.0, 0.0, 0.0])
def parse_settings(self):
self.name = self.controller_settings["name"]
self.input_channel = self.controller_settings["analog_input_channel"]
self.digital_channel = self.controller_settings["digital_input_channel"]
self.max_length = self.controller_settings["max_length"]
self.min_length = self.controller_settings["min_length"]
self.neutral_length = self.controller_settings.get("neutral_length", 0)
self.controls_pos_vel_accel = self.controller_settings["controls_pos/vel/accel"]
def calc_neutral_pos(self):
delta_pos_from_child = -1 * self.neutral_length * self.r_unit_vector
self.neutral_pos = self.attached_sim_target.get_child_statespace().get_pos() + delta_pos_from_child
def get_input_channel(self) -> int:
return self.input_channel
def get_digital_channel(self) -> int:
return self.digital_channel
def attach_sim_target(self, target: 'SharedRigidActuator'):
if not isinstance(target._getvalue(), SharedRigidActuator):
raise TypeError(f"target for RigidActuatorController {self.name} is not a SharedRigidActuator")
self.attached_sim_target = target
self.r_unit_vector = self.get_r_unit_vector()
self.calc_neutral_pos()
self.controlled_pos = self.neutral_pos
self.attached_sim_target.set_parent_statespace_pos(self.controlled_pos)
def attach_shared_attributes(self, shared_attributes: 'SharedRigidActuatorController'):
if not isinstance(shared_attributes._getvalue(), SharedRigidActuatorController):
raise TypeError(f"shared_attributes for RigidActuatorController {self.name} is not a SharedRigidActuatorController")
self.shared_attributes = shared_attributes
def get_r_unit_vector(self):
return self.attached_sim_target.get_r_unit_vector()
def set_input_voltage(self, voltage: float):
'''After reading a voltage, set it to this controller element'''
self.input_voltage = voltage
def set_digital_command(self, command: int):
self.digital_command = command
def set_controlled_attribute(self):
if self.controls_pos_vel_accel == 0: # Control pos
self.controlled_length = (self.input_voltage - self.controller_settings["neutral_voltage"]) * self.controller_settings["units_per_volt"] + self.neutral_length
self.set_controlled_pos(self.controlled_length)
elif self.controls_pos_vel_accel == 1: # Controls vel
self.set_controlled_vel()
elif self.controls_pos_vel_accel == 2: # Controls accel
self.set_controlled_accel()
def set_controlled_vel(self):
# Check if the controlled vel would put us outside our max or min displacement
# if it is, set controlled vel to 0 and set our controlled pos manually
current_length = self.attached_sim_target.get_length()
self.controlled_vel_scalar = (self.input_voltage - self.controller_settings["neutral_voltage"]) * self.controller_settings["units_per_volt"]
if current_length > self.max_length:
self.set_controlled_pos(self.max_length)
if self.controlled_vel_scalar > 0:
self.controlled_vel_scalar = 0
elif current_length < self.min_length:
self.set_controlled_pos(self.min_length)
if self.controlled_vel_scalar < 0:
self.controlled_vel_scalar = 0
self.r_unit_vector = self.get_r_unit_vector()
controlled_vel = -1*self.r_unit_vector * self.controlled_vel_scalar # -1 * r vector becaus r vector describes child - parent
self.controlled_vel = controlled_vel
def set_controlled_accel(self):
pass
def set_controlled_pos(self, length:float):
self.r_unit_vector = self.get_r_unit_vector()
if length > self.max_length:
self.controlled_length = self.max_length
elif length < self.min_length:
self.controlled_length = self.min_length
delta_pos_from_child = self.controlled_length * -1*self.r_unit_vector # -1 * r vector becaus r vector describes child - parent
child_pos = self.attached_sim_target.get_child_statespace().get_pos()
self.controlled_pos = delta_pos_from_child + child_pos
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_digital_command(self.digital_command)
self.shared_attributes.set_input_voltage(self.input_voltage)
self.shared_attributes.set_controlled_pos(self.controlled_pos)
self.shared_attributes.set_controlled_vel(self.controlled_vel)
self.shared_attributes.set_controlled_length(self.controlled_length)
self.shared_attributes.set_controlled_vel_scalar(self.controlled_vel_scalar)
def update_sim_target(self):
'''Update the sim target with the values we input from the controller'''
if self.controls_pos_vel_accel == 0: # Control pos
if self.digital_command == 1: # We are actively controlling the actuator
self.attached_sim_target.set_parent_statespace_pos(self.controlled_pos)
elif self.controls_pos_vel_accel == 1: # Controls vel
if self.digital_command == 0: # We are NOT actively controlling the actuator
self.attached_sim_target.set_parent_statespace_vel([0,0,0])
else: # We are actively controlling the vel
self.attached_sim_target.set_parent_statespace_vel(self.controlled_vel)
elif self.controls_pos_vel_accel == 2: # Controls accel
pass
class SharedRigidActuatorController():
def __init__(self):
self.input_voltage: float = 0.0
self.digital_command: int = 0
self.controlled_length: float = 0.0
self.controlled_vel_scalar: float = 0.0
self.controlled_pos = np.array([0.0, 0.0, 0.0])
self.controlled_vel = np.array([0.0, 0.0, 0.0])
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_input_voltage(self, voltage: float):
self.input_voltage = voltage
def set_digital_command(self, command: int):
self.digital_command = command
def set_controlled_pos(self, new_pos):
self.controlled_pos = new_pos
def set_controlled_vel(self, new_vel):
self.controlled_vel = new_vel
def set_controlled_length(self, disp: float) -> None:
self.controlled_length = disp
def set_controlled_vel_scalar(self, vel_scalar: float) -> None:
self.controlled_vel_scalar = vel_scalar
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
def get_controlled_pos(self):
return self.controlled_pos
def get_controlled_vel(self):
return self.controlled_vel
def get_controlled_length(self) -> float:
return self.controlled_length
def get_controlled_vel_scalar(self) -> float:
return self.controlled_vel_scalar
def get_input_voltage(self) -> float:
return self.input_voltage
def get_digital_command(self) -> int:
return self.digital_command

View File

@ -0,0 +1,147 @@
import time
from mcculw.ul import ignore_instacal
import threading
from Physics_Elements import Joint, Spring, StateSpace
from MCC_Interface import MCC3114, MCC202
from typing import List
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Sensor_Elements import LoadCell, SharedLoadCellJoint, LoadCellSpring, SharedLoadCellSpring, DisplacementSensor
from Controller_Input_Elements import RigidActuatorController
class HITLController():
def __init__(self, stop_event, settings: dict):
self.stop_event = stop_event
self.settings = settings
self.parse_settings()
self.load_cells: List[LoadCell] = []
#self.displacement_actuators: List[DisplacementSensor] = []
self.displacement_sensors: List[DisplacementSensor] = []
self.rigid_actuator_controllers: List[RigidActuatorController] = []
def parse_settings(self):
'''Parse the settings to get the settings for the MCC devices'''
self.pull_from_sim_period = self.settings["pull_from_sim_period"] / 1000.0
self.updated_plotting_shared_attributes_period = self.settings["plotting_update_period"] / 1000.0
def attach_load_cell(self, load_cell: 'LoadCell'):
self.load_cells.append(load_cell)
def attach_displacement_sensor(self, sensor: 'DisplacementSensor'):
self.displacement_sensors.append(sensor)
def attach_rigid_actuator_controller(self, controller: 'RigidActuatorController'):
self.rigid_actuator_controllers.append(controller)
def update_from_sim(self):
# Updates the local sensors based on the sim values
for lc in self.load_cells:
lc.pull_from_sim()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.pull_from_sim()
for sensor in self.displacement_sensors:
sensor.pull_from_sim()
def update_from_sim_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_from_sim()
time.sleep(self.pull_from_sim_period / 1000)
def update_internal_sensor_attributes(self):
'''Updates internal attributes. This should be updated as fast as possible because it generates
sensor noise.'''
for lc in self.load_cells:
lc.update_internal_attributes()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.update_internal_attributes()
for sensor in self.displacement_sensors:
sensor.update_internal_attributes()
def update_plotting_shared_attributes(self):
for lc in self.load_cells:
lc.update_shared_attributes()
for rigid_actuator_controller in self.rigid_actuator_controllers:
rigid_actuator_controller.update_shared_attributes()
# for rigid_actuator in self.displacement_actuators:
# rigid_actuator.update_shared_attributes()
for sensor in self.displacement_sensors:
sensor.update_shared_attributes()
def update_plotting_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_plotting_shared_attributes()
time.sleep(self.updated_plotting_shared_attributes_period / 1000)
def update_mcc3114(self):
for lc in self.load_cells:
channel = lc.sensor_settings["output_channel"]
voltage = lc.get_noisy_voltage_scalar()
self.mcc3114.voltage_write(channel, voltage)
for sensor in self.displacement_sensors:
channel = sensor.sensor_settings["output_channel"]
voltage = sensor.get_noisy_voltage_scalar()
self.mcc3114.voltage_write(channel, voltage)
def read_from_mcc202(self):
for rigid_actuator_controller in self.rigid_actuator_controllers:
input_channel:int = rigid_actuator_controller.get_input_channel()
voltage = self.mcc202.voltage_read(channel=input_channel)
digital_channel:int = rigid_actuator_controller.get_digital_channel()
digital_command = self.mcc202.digital_read(channel=digital_channel)
rigid_actuator_controller.set_digital_command(digital_command)
rigid_actuator_controller.set_input_voltage(voltage)
rigid_actuator_controller.set_controlled_attribute()
def update_sim_targets(self):
for rigid_actuator_controller in self.rigid_actuator_controllers:
rigid_actuator_controller.update_sim_target()
def controller_loop(self):
while self.stop_event.is_set() == False:
self.loop_time = time.time()
# Update the internal attributes of the sensors
self.update_internal_sensor_attributes()
# Update MCC3114 (analog output)
self.update_mcc3114()
# Get readings from the MCC202
self.read_from_mcc202()
# Update the shared actuators for the sim to respond to
self.update_sim_targets()
def start_mcc(self):
ignore_instacal() # ONLY CALL ONCE
self.mcc3114 = MCC3114()
self.mcc202 = MCC202()
def run_process(self):
self.start_mcc()
# Make the threads for interacting with shared elements
self.spare_stop_event = threading.Event()
self.spare_stop_event.clear()
updating_plotting_elements_thread = threading.Thread(target=self.update_plotting_thread)
update_from_sim_thread = threading.Thread(target=self.update_from_sim_thread)
updating_plotting_elements_thread.start()
update_from_sim_thread.start()
self.controller_loop()
self.spare_stop_event.set()
updating_plotting_elements_thread.join()
update_from_sim_thread.join()

View File

@ -0,0 +1,200 @@
from mcculw import ul as mcculw
from mcculw.enums import (AiChanType, BoardInfo, Status, InterfaceType,
FunctionType, InfoType, ScanOptions,
ULRange, AnalogInputMode, DigitalPortType, DigitalIODirection)
import time
class MCC3114():
def __init__(self):
self.range = ULRange.BIP10VOLTS
self.board_num = self.setup_device()
if self.board_num == -1:
print("Could not setup MCC3114")
self.config_outputs()
self.current_voltage_outputs: list[float] = [0.0]*16
self.set_all_to_zero()
def __del__(self):
self.set_all_to_zero()
def setup_device(self) -> int:
'''Sets up the device without Instacal configuration files'''
board_num = 0
board_index = 0
find_device = "USB-3114"
board_num = -1
dev_list = mcculw.get_daq_device_inventory(InterfaceType.USB)
if len(dev_list) > 0:
for device in dev_list:
if str(device) == find_device:
board_num = board_index
mcculw.create_daq_device(board_num, device)
board_index = board_index + 1
if board_num == -1:
print(f"Device {find_device} not found")
return board_num
else:
print("No devices detected")
return board_num
return board_num
def config_outputs(self):
if self.board_num == -1:
return
for ch in range(16):
mcculw.set_config(
InfoType.BOARDINFO, self.board_num, ch,
BoardInfo.DACRANGE, self.range
)
def set_all_to_zero(self):
'''Make the voltage outputs be 0.0 V when the program exits or we delete the object'''
if self.board_num == -1:
return
for i in range(16):
self.voltage_write(channel=i, voltage=0.0)
def voltage_write(self, channel:int, voltage: float):
if self.board_num == -1:
return
if channel < 0 or channel > 15:
print(f"Channel: {channel} is out of range for the MCC USB-3114. Valid range is [0, 15]")
return
if voltage < -10:
print(f"Voltage: {voltage} is out of range for the MCC USB-3114 in this configuration. Valid range is [-10, 10] V")
voltage = -10
if voltage > 10:
print(f"Voltage: {voltage} is out of range for the MCC USB-3114 in this configuration. Valid range is [-10, 10] V")
voltage = 10
try:
mcculw.v_out(self.board_num, channel, self.range, voltage)
self.current_voltage_outputs[channel] = voltage
except Exception as exc:
print(f"Exception occurred doing voltage write to MCC USB-3114: {exc}")
class MCC202():
def __init__(self):
self.range = ULRange.BIP10VOLTS
self.digital_type = DigitalPortType.AUXPORT
self.board_num = self.setup_device()
if self.board_num == -1:
print("Could not setup MCC202")
self.current_analog_inputs: list[float] = [0.0] * 8
self.current_digital_inputs: list[int] = [0] * 8
def __del__(self):
pass # No specific cleanup required for inputs
def setup_device(self) -> int:
'''Sets up the device without Instacal configuration files'''
board_num = 0
board_index = 0
find_device = "USB-202"
board_num = -1
dev_list = mcculw.get_daq_device_inventory(InterfaceType.USB)
if len(dev_list) > 0:
for device in dev_list:
if str(device) == find_device:
board_num = board_index
mcculw.create_daq_device(board_num, device)
board_index = board_index + 1
if board_num == -1:
print(f"Device {find_device} not found")
return board_num
else:
print("No devices detected")
return board_num
return board_num
def config_inputs(self):
if self.board_num == -1:
return
for ch in range(8):
mcculw.set_config(
InfoType.BOARDINFO, self.board_num, ch,
BoardInfo.RANGE, self.range
)
for ch in range(8):
mcculw.d_config_port(self.board_num, self.digital_type, DigitalIODirection.IN)
def print_all_channels(self):
for i in range(8):
print(f"Analog channel {i} reads {self.current_analog_inputs[i]} Volts")
for i in range(8):
print(f"Digital channel {i} reads {self.current_digital_inputs[i]}")
def voltage_read(self, channel: int) -> float:
if self.board_num == -1:
return 0.0
if channel < 0 or channel > 7:
print(f"Channel: {channel} is out of range for the MCC USB-202. Valid range is [0, 7]")
return 0.0
try:
voltage = mcculw.v_in(self.board_num, channel, self.range)
self.current_analog_inputs[channel] = voltage
return voltage
except Exception as exc:
print(f"Exception occurred doing voltage read from MCC USB-202: {exc}")
return 0.0
def read_all_analog_channels(self):
'''Read all analog input channels and update the current_analog_inputs list'''
for i in range(8):
self.voltage_read(i)
def digital_read(self, channel: int) -> int:
if self.board_num == -1:
return -1
if channel < 0 or channel > 7:
print(f"Digital channel: {channel} is out of range for the MCC USB-202. Valid range is [0, 7]")
return -1
try:
state = mcculw.d_bit_in(self.board_num, self.digital_type, channel)
self.current_digital_inputs[channel] = state
return state
except Exception as exc:
print(f"Exception occurred doing digital read from MCC USB-202: {exc}")
return -1
def read_all_digital_channels(self):
'''Read all digital input channels and update the current_digital_inputs list'''
for i in range(8):
self.digital_read(i)
def read_all_channels(self):
for i in range(8):
self.voltage_read(i)
self.digital_read(i)
def main():
mcculw.ignore_instacal() # ONLY CALL ONCE
#mcc3114 = MCC3114()
mcc202 = MCC202()
while True:
mcc202.read_all_channels()
mcc202.print_all_channels()
time.sleep(3)
#mcc3114.voltage_write(1, 5)
#mcc3114.voltage_write(7, 5)
#time.sleep(100)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,9 @@
class SharedFloat:
def __init__(self, initial_value:float=0.0):
self.value = initial_value
def get(self):
return self.value
def set(self, new_value):
self.value = new_value

View File

@ -0,0 +1,711 @@
from typing import List, Callable
import numpy as np
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Physics_Elements import StateSpace, Joint, Spring, SharedRigidActuator, SharedSpring
ZERO_VECTOR = np.array([0.0, 0.0, 0.0])
'''
TODO
1. Change spring_force to just force
Include getters and setters, lest important for attributes
2. Add Object_Sharing objects to this file
'''
def stiffness_function_const_then_rigid(length, unstretched_length):
percent_length = 0.9 # For the first bit of length in compression, it follors k*x
k = 1000
if length < (1-percent_length)* unstretched_length:
# It is really compressed, so make k = k *10
k = k + k*10/(1 + np.exp(-10*(unstretched_length - length - percent_length*unstretched_length)))
return k
def damping_force_x(mass: 'Joint', t):
x_vel = mass.statespace.get_vel()[0]
a = 1
b = 0.01
return x_vel*a*-1 + x_vel**2 * b*-1
def damping_force_y(mass: 'Joint', t):
y_vel = mass.statespace.get_vel()[1]
a = 1
b = 0.01
return y_vel*a*-1 + y_vel**2 * b*-1
def damping_force_z(mass: 'Joint', t):
z_vel = mass.statespace.get_vel()[2]
a = 1
b = 0.01
return z_vel*a*-1 + z_vel**2 * b*-1
'''
TODO
'''
class StateSpace():
def __init__(self, pos=np.copy(ZERO_VECTOR), vel=np.copy(ZERO_VECTOR), accel=np.copy(ZERO_VECTOR), force=np.copy(ZERO_VECTOR), integration_method="simple"):
self.pos = pos
self.vel = vel
self.accel = accel
self.force = force
self.integration_method = self.parse_integration_method(integration_method)
self.max_saved = 2
self.previous_positions: list[list[float, float, float]] = []
self.previous_vel: list[list[float, float, float]] = []
self.previous_accel: list[list[float, float, float]] = []
self.verlet_constants = [
[],
[],
[-1, 2, 1],
[0, -1, 2, 1],
[1/11, -4/11, -6/11, 20/11, 12/11],
]
def parse_integration_method(self, method_str):
if method_str == "simple":
return 1
elif method_str == "verlet":
return 2
elif method_str == "adams-bashforth":
return 3
def integrate(self, delta_t):
'''
NOTE: This is run after accel has been updated.
'''
if self.integration_method == 1:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
elif self.integration_method == 2:
# Verlet integration
self.save_last()
if delta_t >= 0.005:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
else:
self.vel = self.vel + self.accel * delta_t
self.verlet_integration(delta_t)
elif self.integration_method == 3: # Adams-Bashforth
self.save_last()
if delta_t >= 0.005:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
else:
self.adams_bashforth_integration(delta_t)
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
def adams_bashforth_integration(self, delta_t):
'''
Testing.
2 points seems to damp the system. 3 does better at maintaining. Above 3 it is worse
'''
num_prev_accel = len(self.previous_accel)
match num_prev_accel:
case 0 | 1:
self.vel = self.vel + self.accel * delta_t
case 2:
self.vel = self.vel + delta_t * (1.5 * self.previous_accel[-1] - 0.5 * self.previous_accel[-2])
case 3:
self.vel = self.vel + delta_t * (23/12 * self.previous_accel[-1] - 4/3 * self.previous_accel[-2] + 5/12 * self.previous_accel[-3])
case 4:
self.vel = self.vel + delta_t * (55/24 * self.previous_accel[-1] - 59/24 * self.previous_accel[-2] + 37/24 * self.previous_accel[-3] - 9/24 * self.previous_accel[-4])
case 5:
self.vel = self.vel + delta_t * ((1901 * self.previous_accel[-1] - 2774 * self.previous_accel[-2] + 2616 * self.previous_accel[-3] - 1274 * self.previous_accel[-4] + 251 * self.previous_accel[-5]) / 720)
case _:
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
def verlet_integration(self, delta_t):
num_prev_positions = len(self.previous_positions)
match num_prev_positions:
case 0 | 1:
self.pos = self.pos + self.vel * delta_t
case 2:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(2):
new_pos += self.verlet_constants[2][i] * self.previous_positions[i]
new_pos += self.verlet_constants[2][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case 3:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(3):
new_pos += self.verlet_constants[3][i] * self.previous_positions[i]
new_pos += self.verlet_constants[3][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case 4:
new_pos = np.array([0.0, 0.0, 0.0])
for i in range(4):
new_pos += self.verlet_constants[4][i] * self.previous_positions[i]
new_pos += self.verlet_constants[4][-1] * delta_t * self.accel * delta_t
self.pos = new_pos
case _:
self.pos = self.pos + self.vel * delta_t
def save_last(self):
self.previous_positions.append(np.copy(self.pos))
self.previous_positions = self.previous_positions[-1*self.max_saved:]
self.previous_vel.append(np.copy(self.vel))
self.previous_vel = self.previous_vel[-1*self.max_saved:]
self.previous_accel.append(np.copy(self.accel))
self.previous_accel = self.previous_accel[-1*self.max_saved:]
def save_last_statespace(self):
if self.last_statespace is None:
self.last_statespace = StateSpace()
self.last_statespace.pos = np.copy(self.pos)
self.last_statespace.vel = np.copy(self.vel)
self.last_statespace.accel = np.copy(self.accel)
def set_pos(self, new_pos):
self.pos = new_pos
def set_vel(self, new_vel):
self.vel = new_vel
def set_accel(self, new_accel):
self.accel = new_accel
def set_force(self, new_force):
self.force = new_force
def get_pos(self):
return self.pos
def get_vel(self):
return self.vel
def get_accel(self):
return self.accel
def get_force(self):
return self.force
class Joint():
def __init__(self, pos=np.copy(ZERO_VECTOR), mass: float=0.1, fixed: bool=False, name: str="Joint", integration_method:str="simple"):
self.name = name
self.statespace = StateSpace(pos, ZERO_VECTOR, ZERO_VECTOR, ZERO_VECTOR, integration_method=integration_method)
self.connected_springs: List[Spring] = []
self.constant_external_forces: List[List[float, float, float]] = []
self.connected_actuators: List[RigidActuator] = []
self.variable_external_forces = [[], [], []]
self.fixed = fixed
self.connected_to_rigid_actuator = False
self.mass = mass
self.parent_joints: List[Joint] = []
self.child_joints: List[Joint] = []
self.damping = False
self.sharing_attributes = False
self.shared_attributes = None
def is_same(self, other_joint: 'Joint'):
if self.name == other_joint.get_name():
return True
return False
def get_statespace(self):
return self.statespace
def get_pos(self):
return self.statespace.get_pos()
def get_vel(self):
return self.statespace.get_vel()
def get_accel(self):
return self.statespace.get_accel()
def set_state_space_pos(self, new_pos):
self.statespace.set_pos(new_pos)
def set_state_space_vel(self, new_vel):
self.statespace.set_vel(new_vel)
def set_connected_to_rigid_actuator(self, state):
self.connected_to_rigid_actuator = state
def get_connected_springs(self):
return self.connected_springs
def get_connected_actuators(self):
return self.connected_actuators
def get_name(self):
return self.name
def add_spring(self, new_spring):
self.connected_springs.append(new_spring)
def add_actuator(self, new_actuator):
self.connected_actuators.append(new_actuator)
def add_constant_force(self, new_force):
self.constant_external_forces.append(new_force)
def integrate_statespace(self, delta_t):
self.statespace.integrate(delta_t)
def add_gravity(self):
gravity_force = np.array([
0,
-9.81 * self.mass,
0
])
self.add_constant_force(gravity_force)
def add_variable_force(self, force_vect: list):
if force_vect[0] is not None:
self.variable_external_forces[0].append(force_vect[0])
if force_vect[1] is not None:
self.variable_external_forces[1].append(force_vect[1])
if force_vect[2] is not None:
self.variable_external_forces[2].append(force_vect[2])
def calc_net_spring_force(self):
net_spring_force = 0
for spring in self.get_connected_springs():
if self.is_same(spring.get_parent_joint()):
spring_force = spring.get_spring_force() # This is the force the spring exerts on the joint ASSUMING self is parent_joint of the spring
else:
spring_force = -1*spring.get_spring_force()
net_spring_force += spring_force
return net_spring_force
def calc_net_constant_external_force(self):
net_external_force = np.copy(ZERO_VECTOR)
for force in self.constant_external_forces:
net_external_force += force
return net_external_force
def calc_net_variable_external_force(self, t):
net_variable_external_force = np.array([0.0, 0.0, 0.0])
for i in range(3):
for func in self.variable_external_forces[i]:
force = func(t)
net_variable_external_force[i] += force
return net_variable_external_force
def calc_net_force(self, t):
net_force = np.copy(ZERO_VECTOR)
if len(self.constant_external_forces) != 0:
net_force += self.calc_net_constant_external_force()
if len(self.variable_external_forces) != 0:
net_force += self.calc_net_variable_external_force(t)
net_spring_force = self.calc_net_spring_force()
if self.damping == True:
net_force += self.calc_damping(net_spring_force)
net_force += net_spring_force
self.statespace.set_force(net_force)
return self.statespace.get_force()
def add_damping(self, vel_ratio=0.25, mom_ratio=None):
self.damping = True
if mom_ratio is not None:
self.damping_vel_ratio = self.mass * mom_ratio
else:
self.damping_vel_ratio = vel_ratio
def calc_damping(self, spring_force):
vel_damping = self.damping_vel_ratio * (self.statespace.get_vel()) * -1
return vel_damping
def calc_accel(self):
if self.fixed == False and self.connected_to_rigid_actuator == False:
self.statespace.set_accel(self.statespace.get_force() / self.mass)
def is_in_list(self, joints: list['Joint']):
for j in joints:
if self.is_same(j):
return True
return False
def find_parent_joints(self):
for spring in self.connected_springs:
if self.is_same(spring.get_child_joint()):
parent_joint = spring.get_parent_joint()
if not parent_joint.is_in_list(self.parent_joints):
self.parent_joints.append(parent_joint)
def find_child_joints(self):
for spring in self.connected_springs:
if self.is_same(spring.get_parent_joint()):
child_joint = spring.get_child_joint()
if not child_joint.is_in_list(self.child_joints):
self.child_joints.append(child_joint)
def print_attributes(self):
print(f"Joint: {self.name}")
print(f"Force: {self.statespace.get_force()}")
print(f"Accel: {self.statespace.get_accel()}")
print(f"Vel: {self.statespace.get_vel()}")
print(f"Pos: {self.statespace.get_pos()}")
def attach_shared_attributes(self, shared_attributes: 'SharedJoint'):
if not isinstance(shared_attributes._getvalue(), SharedJoint):
raise TypeError(f"shared_attributes for Joint {self.name} is not a SharedJoint")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.shared_attributes.set_name(f"{self.name}")
self.update_shared_attributes()
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_statespace(self.statespace)
class Spring():
def __init__(self, parent_joint: 'Joint', child_joint: 'Joint', unstretched_length: float=0, constant_stiffness: float = None, stiffness_func: Callable[[float, float], float]=None, name: str="Spring"):
self.name = name
self.parent_joint = parent_joint
self.child_joint = child_joint
self.unstretched_length = unstretched_length
self.stiffness_func = stiffness_func
self.constant_stiffness = constant_stiffness
self.current_stiffness = 0.0
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector: list[float] = self.calc_r_unit_vector()
self.spring_force_scalar: float = 0.0
self.spring_force: list[float] = self.calc_spring_force()
self.parent_joint.add_spring(self)
self.child_joint.add_spring(self)
self.sharing_attributes = False
self.shared_attributes: 'SharedSpring' = None
def get_name(self):
return self.name
def get_parent_joint(self):
return self.parent_joint
def get_child_joint(self):
return self.child_joint
def get_spring_force(self):
return self.spring_force
def calc_r_vector(self):
return self.child_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
return self.vector / self.length
def calc_stiffness(self):
if self.stiffness_func is not None:
return self.stiffness_func(self.length, self.unstretched_length)
else:
return self.constant_stiffness
def calc_spring_force(self):
'''Positive force is in tension. Aka, the spring PULLS on the other object'''
self.length = self.calc_length()
del_length = self.length - self.unstretched_length
self.current_stiffness = self.calc_stiffness()
self.spring_force_scalar = del_length * self.current_stiffness
self.vector = self.calc_r_vector()
self.r_unit_vector = self.calc_r_unit_vector()
self.spring_force = self.spring_force_scalar * self.r_unit_vector
return self.spring_force
def print_attributes(self):
print(f"Spring: {self.name}")
print(f"Length: {self.length}")
print(f"Spring Force: {self.spring_force}")
def get_pos(self):
return self.parent_statespace.get_pos()
def get_axis_vector(self):
return self.child_statespace.get_pos() - self.parent_statespace.get_pos()
def attach_shared_attributes(self, shared_attributes: 'SharedSpring'):
if not isinstance(shared_attributes._getvalue(), SharedSpring):
raise TypeError(f"shared_attributes for Spring {self.name} is not a SharedSpring")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.update_shared_attributes()
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_child_statespace(self.child_joint.get_statespace())
self.shared_attributes.set_parent_statespace(self.parent_joint.get_statespace())
self.shared_attributes.set_spring_force(self.spring_force)
self.shared_attributes.set_spring_force_scalar(self.spring_force_scalar)
self.shared_attributes.set_length(self.length)
self.shared_attributes.set_unstretched_length(self.unstretched_length)
self.shared_attributes.set_stiffness(self.current_stiffness)
def is_same(self, other_spring: 'Spring'):
if self.name == other_spring.get_name():
return True
return False
def is_in_list(self, springs: list['Spring']):
for j in springs:
if self.is_same(j):
return True
return False
'''
It connects 2 joints.
1 joint is completely fixed. Cannot move. Ever. It will be referred to as "grounded".
The other joint is contrained to the grounded joint. The actuator controls the vector displacement <x,y,z> from the grounded joint
'''
class RigidActuator():
def __init__(self, parent_joint: 'Joint', grounded_joint: 'Joint', name: str = "Rigid Actuator", max_length:float=1, min_length:float=0.1, control_code=0):
self.name = name
self.parent_joint = parent_joint
self.grounded_joint = grounded_joint # Same as child joint
self.parent_joint.add_actuator(self)
self.parent_joint.set_connected_to_rigid_actuator(True)
self.grounded_joint.add_actuator(self)
self.grounded_joint.set_connected_to_rigid_actuator(True)
self.max_length = max_length
self.min_length = min_length
self.control_code = control_code # 0 for pos, 1 for vel, 2 for accel
self.calc_r_unit_vector()
self.shared_attributes: 'SharedRigidActuator' = None
def get_name(self):
return self.name
def get_parent_joint(self):
return self.parent_joint
def get_child_joint(self):
return self.grounded_joint
def is_same(self, other: 'RigidActuator'):
if self.name == other.get_name():
return True
return False
def is_in_list(self, actuators: list['RigidActuator']):
for j in actuators:
if self.is_same(j):
return True
return False
def calc_r_vector(self):
return self.grounded_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
return self.vector / self.length
def get_pos(self):
return self.parent_joint.get_statespace().get_pos()
def get_axis_vector(self):
return self.grounded_joint.get_statespace().get_pos() - self.parent_joint.get_statespace().get_pos()
def get_r_unit_vector(self):
return self.calc_r_unit_vector()
def update_shared_attributes(self):
# These 2 lines are used to make sure we set the internal properties correctly
self.vector = self.calc_r_vector()
self.length = self.calc_length()
# END
if self.shared_attributes is not None:
self.shared_attributes.set_child_statespace(self.grounded_joint.get_statespace())
self.shared_attributes.set_parent_statespace(self.parent_joint.get_statespace())
self.shared_attributes.set_length(self.length)
def attach_shared_attributes(self, shared_attributes: 'SharedRigidActuator'):
if not isinstance(shared_attributes._getvalue(), SharedRigidActuator):
raise TypeError(f"shared_attributes for RigidActuator {self.name} is not a SharedRigidActuator")
self.sharing_attributes = True
self.shared_attributes = shared_attributes
self.update_shared_attributes()
def set_parent_pos(self, new_pos):
'''Set the position of the parent joint's statestpace'''
self.parent_joint.set_state_space_pos(new_pos)
def update_from_controller(self):
if self.control_code == 0: # Controller controls pos
self.set_pos_to_controlled_pos()
elif self.control_code == 1: # Controller controls vel
self.set_vel_to_controlled_vel()
elif self.control_code == 2: # Controls controls accel
pass
def set_vel_to_controlled_vel(self):
controlled_vel = self.shared_attributes.get_vel()
self.parent_joint.set_state_space_vel(controlled_vel)
def set_pos_to_controlled_pos(self):
controlled_pos = self.shared_attributes.get_pos()
self.parent_joint.set_state_space_pos(controlled_pos)
class SharedPhysicsElement():
def __init__(self) -> None:
self.name: str = None
self.statespace: StateSpace = None
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_name(self, name:str) -> None:
self.name = name
def set_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_name(self) -> str:
return self.name
def get_statespace(self) -> StateSpace:
return self.statespace
def get_pos(self) -> List[float]:
return self.statespace.get_pos()
def get_vel(self) -> List[float]:
return self.statespace.get_vel()
def get_accel(self) -> List[float]:
return self.statespace.get_accel()
def get_force(self) -> List[float]:
return self.statespace.get_force()
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
class SharedJoint(SharedPhysicsElement):
def _init__(self) -> None:
super().__init__()
class SharedSpring(SharedPhysicsElement):
'''self.statespace is the statespace of the parent'''
def _init__(self) -> None:
super().__init__()
self.child_statespace: StateSpace = None
self.unstretched_length: float = None
self.length: float = None
self.stiffness: float = None
self.spring_force: float = None
def set_parent_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_child_statespace(self, statespace:StateSpace) -> None:
self.child_statespace = statespace
def set_unstretched_length(self, unstretched_length: float) -> None:
self.unstretched_length = unstretched_length
def set_length(self, length: float) -> None:
self.length = length
def set_stiffness(self, stiffness: float) -> None:
self.stiffness = stiffness
def set_spring_force(self, set_spring_force: float) -> None:
self.spring_force = set_spring_force
def set_spring_force_scalar(self, scalar_force: float) -> None:
self.spring_force_scalar = scalar_force
def get_parent_statespace(self) -> StateSpace:
return self.statespace
def get_child_statespace(self) -> StateSpace:
return self.child_statespace
def get_unstretched_length(self) -> float:
return self.unstretched_length
def get_length(self) -> float:
return self.length
def get_stiffness(self) -> float:
return self.stiffness
def get_spring_force(self) -> list[float]:
return self.spring_force
def get_spring_force_scalar(self) -> float:
return self.spring_force_scalar
def get_axis_vector(self) -> List[float]:
return self.child_statespace.get_pos() - self.statespace.get_pos()
class SharedRigidActuator(SharedPhysicsElement):
'''self.statespace is the statespace of the parent'''
def _init__(self) -> None:
super().__init__()
self.child_statespace: StateSpace = None
self.length: float = None
def set_parent_statespace(self, statespace:StateSpace) -> None:
self.statespace = statespace
def set_parent_statespace_pos(self, new):
self.statespace.set_pos(new)
def set_parent_statespace_vel(self, new):
self.statespace.set_vel(new)
def set_child_statespace(self, statespace:StateSpace) -> None:
self.child_statespace = statespace
def set_length(self, length: float) -> None:
self.length = length
def get_parent_statespace(self) -> StateSpace:
return self.statespace
def get_child_statespace(self) -> StateSpace:
return self.child_statespace
def get_length(self) -> float:
return self.length
def get_axis_vector(self) -> List[float]:
return self.child_statespace.get_pos() - self.statespace.get_pos()
def get_r_unit_vector(self):
return self.calc_r_unit_vector()
def calc_r_vector(self):
return self.child_statespace.get_pos() - self.statespace.get_pos()
def calc_length(self):
return ((self.vector[0])**2 + (self.vector[1])**2 + (self.vector[2])**2) ** 0.5
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
return self.vector / self.length

View File

@ -0,0 +1,451 @@
import numpy as np
from math import log, copysign
from Physics_Elements import SharedRigidActuator, SharedJoint, SharedSpring
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
from Physics_Elements import StateSpace, Spring
'''
TODO
1.
2. Better noise generation
Non-linearity, hysteresis, background noise, AC noise
'''
class LoadCell():
'''Provides net force feedback.'''
def __init__(self, sensor_settings: dict={}):
self.sensor_settings = sensor_settings
self.attached_sim_source = None
self.shared_attributes: SharedLoadCell = None # For plotting if desired
self.true_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.true_force_scalar: float = 0.0
self.last_true_force_scalars: list[float] = [] # Stores the last true force for hysteresis reasons
self.true_voltage_scalar:float = 0.0
self.noisy_force_vector = np.array([0.0, 0.0, 0.0])
self.noisy_force_scalar: float = 0.0
self.noisy_voltage_scalar: float = 0.0
self.parse_settings()
self.calc_noise_parameters()
def parse_settings(self):
self.name = self.sensor_settings["name"]
self.scale = self.sensor_settings["mV/V"] / 1000.0
self.excitation = self.sensor_settings["excitation"]
self.full_scale_force = self.sensor_settings["full_scale_force"]
self.output_channel = self.sensor_settings["output_channel"]
self.noise_settings = self.sensor_settings.get("noise_settings", dict())
self.full_scale_voltage = self.scale * self.excitation
def calc_noise_parameters(self):
# Calculate static error
self.static_error_stdev = self.full_scale_voltage * self.noise_settings.get("static_error_band", 0) / 100
# Non-linearity
self.nonlinearity_exponent = self.calc_nonlinearity_exponent()
# Hysteresis
self.hysteresis = self.noise_settings.get("hysteresis", 0) / 100
# Repeatability
self.repeatability_offset = np.random.normal(0, self.full_scale_voltage * self.noise_settings.get("repeatability", 0) / 100)
# Thermal applied at a given true voltage
self.thermal_error = self.noise_settings.get("thermal_error", 0) / 100 * self.noise_settings.get("temperature_offset", 0)
def calc_nonlinearity_exponent(self):
voltage = 10
if self.full_scale_voltage != 1:
self.full_scale_voltage
error = self.noise_settings.get("non-linearity", 0) / 100
b = log(1 + error, voltage)
return b
def attach_sim_source(self, sim_source):
self.attached_sim_source = sim_source
def attach_shared_attributes(self, shared_attributes):
self.shared_attributes = shared_attributes
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_true_force_vector(self.true_force_vector)
self.shared_attributes.set_true_force_scalar(self.true_force_scalar)
self.shared_attributes.set_true_voltage_scalar(self.true_voltage_scalar)
self.shared_attributes.set_noisy_voltage_scalar(self.noisy_voltage_scalar)
def update_internal_attributes(self):
self.calc_sensor_force_scalar()
self.calc_true_voltage_scalar()
# Calc noise
self.calc_noisy_voltage_scalar()
def pull_from_sim(self):
'''Gets the real net force from the shared attribute updated by the sim'''
# Must be overridden
pass
def calc_sensor_force_scalar(self):
self.last_true_force_scalars.append(self.true_force_scalar)
self.last_true_force_scalars = self.last_true_force_scalars[-5:]
self.true_force_scalar = ((self.true_force_vector[0])**2 + (self.true_force_vector[1])**2 + (self.true_force_vector[2])**2)**0.5
def get_sensor_force_scalar(self):
return self.true_force_scalar
def get_sensor_force_vector(self):
return self.true_force_vector
def get_true_voltage_scalar(self) -> float:
return self.true_voltage_scalar
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage_scalar
def calc_true_voltage_scalar(self):
force_fraction = self.true_force_scalar / self.full_scale_force
full_force_voltage = self.excitation * self.scale
self.true_voltage_scalar = force_fraction * full_force_voltage
def calc_noisy_voltage_scalar(self):
# Calculate static error
static_error = np.random.normal(0, self.static_error_stdev)
# Non-linearity
if self.true_voltage_scalar > 1E-5:
nonlinearity_multiplier = self.true_voltage_scalar**(self.nonlinearity_exponent)
else:
nonlinearity_multiplier = 1
# Hysteresis
average_last = np.average(self.last_true_force_scalars)
#hysteresis_error = (self.true_force_scalar - average_last) / self.true_force_scalar * self.hysteresis * self.full_scale_voltage
hysteresis_error = copysign(1, self.true_force_scalar - average_last) * self.hysteresis * self.full_scale_voltage
# Repeatability
# self.repeatability_offset
# Thermal
thermal_error = self.thermal_error * self.true_voltage_scalar
self.noisy_voltage_scalar = self.true_voltage_scalar*nonlinearity_multiplier + static_error + hysteresis_error + self.repeatability_offset + thermal_error
class LoadCellJoint(LoadCell):
def __init__(self, sensor_settings: dict={}):
super().__init__(sensor_settings=sensor_settings)
self.attached_sim_source: SharedJoint = None
def attach_sim_source(self, sim_source: 'SharedJoint'):
if not isinstance(sim_source._getvalue(), SharedJoint):
raise TypeError(f"sim_source for LoadCellJoint {self.name} is not a SharedJoint")
self.attached_sim_source = sim_source
def pull_from_sim(self):
self.true_force_vector = self.attached_sim_source.get_force()
class LoadCellSpring(LoadCell):
'''Provides feedback on the force along the axis of a spring.'''
def __init__(self, sensor_settings: dict={}):
super().__init__(sensor_settings=sensor_settings)
self.attached_sim_source: SharedSpring = None
self.unstretched_length: float = 0.0
self.true_length: float = 0.0
def attach_sim_source(self, sim_source: 'SharedSpring'):
if not isinstance(sim_source._getvalue(), SharedSpring):
raise TypeError(f"sim_source for LoadCellSpring {self.name} is not a SharedSpring")
self.attached_sim_source = sim_source
self.unstretched_length = sim_source.get_unstretched_length()
def pull_from_sim(self):
self.true_force_vector = self.attached_sim_source.get_spring_force()
self.calc_sensor_force_scalar()
self.true_length = self.attached_sim_source.get_length()
def calc_sensor_force_scalar(self):
self.last_true_force_scalars.append(self.true_force_scalar)
self.last_true_force_scalars = self.last_true_force_scalars[-5:]
self.true_force_scalar = ((self.true_force_vector[0])**2 + (self.true_force_vector[1])**2 + (self.true_force_vector[2])**2)**0.5
if self.true_length >= self.unstretched_length: # Tension is positive
self.true_force_scalar *= 1
else:
self.true_force_scalar*= -1
def calc_true_voltage_scalar(self):
'''Include the possibility to be negative if in tension'''
force_fraction = self.true_force_scalar / self.full_scale_force
full_force_voltage = self.excitation * self.scale
self.true_voltage_scalar = force_fraction * full_force_voltage
class SharedLoadCell():
def __init__(self):
self.true_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.true_force_scalar: float = 0.0
self.true_voltage_scalar: float = 0.0
self.noisy_force_vector = np.array([0.0, 0.0, 0.0]) # Current real force on the joint
self.noisy_force_scalar: float = 0.0
self.noisy_voltage_scalar: float = 0.0
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_true_force_vector(self, true_force_vector):
self.true_force_vector = true_force_vector
def get_true_force_vector(self) -> float:
return self.true_force_vector
def set_true_force_scalar(self, true_force_scalar):
self.true_force_scalar = true_force_scalar
def get_true_force_scalar(self) -> float:
return self.true_force_scalar
def set_true_voltage_scalar(self, true_voltage_scalar):
self.true_voltage_scalar = true_voltage_scalar
def get_true_voltage_scalar(self) -> float:
return self.true_voltage_scalar
def set_noisy_voltage_scalar(self, noisy_voltage_scalar):
self.noisy_voltage_scalar = noisy_voltage_scalar
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage_scalar
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
class SharedLoadCellJoint(SharedLoadCell):
def __init__(self):
super().__init__()
class SharedLoadCellSpring(SharedLoadCell):
def __init__(self):
super().__init__()
class DisplacementSensor():
'''Measures the displacement between 2 joints.
The output is the non-negative scalar distance between parent and child'''
def __init__(self, parent_joint: 'SharedJoint', child_joint: 'SharedJoint', sensor_settings: dict={}):
self.sensor_settings = sensor_settings
self.parse_settings()
self.calc_noise_parameters()
if not isinstance(parent_joint._getvalue(), SharedJoint):
raise TypeError(f"parent_joint for DisplacementSensor {self.name} is not a SharedJoint")
if not isinstance(child_joint._getvalue(), SharedJoint):
raise TypeError(f"child_joint for DisplacementSensor {self.name} is not a SharedJoint")
parent_joint.set_connected_to_sensor(True)
child_joint.set_connected_to_sensor(True)
self.sim_source_parent:SharedJoint = parent_joint
self.sim_source_child:SharedJoint = child_joint
self.shared_attributes: SharedDisplacementSensor = None # For plotting if desired
self.length: float = 0.0 # distance from child to parent
self.true_disp_scalar: float = 0.0 # length - neutral_length
self.last_true_disp_scalars: list[float] = [] # Stores the last true displacements for hysteresis reasons
self.calc_sensor_true_disp()
self.true_voltage: float = 0.0
self.noisy_voltage: float = 0.0
def parse_settings(self):
self.name = self.sensor_settings["name"]
self.scale = self.sensor_settings["volts_per_meter"]
self.output_channel = self.sensor_settings["output_channel"]
self.neutral_length = self.sensor_settings["neutral_length"]
self.neutral_voltage = self.sensor_settings.get("neutral_voltage", 0)
self.noise_settings = self.sensor_settings.get("noise_settings", dict())
self.full_scale_voltage = self.scale * (self.sensor_settings["max_length"] - self.neutral_length)
def calc_noise_parameters(self):
# Calculate static error
self.static_error_stdev = self.full_scale_voltage * self.noise_settings.get("static_error_band", 0) / 100
# Non-linearity
self.nonlinearity_exponent = self.calc_nonlinearity_exponent()
# Hysteresis
self.hysteresis = self.noise_settings.get("hysteresis", 0) / 100
# Repeatability
self.repeatability_offset = np.random.normal(0, self.full_scale_voltage * self.noise_settings.get("repeatability", 0) / 100)
# Thermal applied at a given true voltage
self.thermal_error = self.noise_settings.get("thermal_error", 0) / 100 * self.noise_settings.get("temperature_offset", 0)
def calc_nonlinearity_exponent(self):
voltage = 10
if self.full_scale_voltage != 1:
self.full_scale_voltage
error = self.noise_settings.get("non-linearity", 0) / 100
b = log(1 + error, voltage)
return b
def attach_shared_attributes(self, shared_attributes):
if not isinstance(shared_attributes._getvalue(), SharedDisplacementSensor):
raise TypeError(f"shared_attributes for DisplacementSensor {self.name} is not a SharedDisplacementSensor")
self.shared_attributes = shared_attributes
self.shared_attributes.set_neutral_length(self.neutral_length)
def pull_from_sim(self):
'''Gets the real displacement from the shared attribute updated by the sim'''
parent_pos = self.sim_source_parent.get_pos()
child_pos = self.sim_source_child.get_pos()
axis_vector = child_pos - parent_pos
self.length = ((axis_vector[0])**2 + (axis_vector[1])**2 + (axis_vector[2])**2)**0.5
def calc_sensor_true_disp(self):
self.last_true_disp_scalars.append(self.true_disp_scalar)
self.last_true_disp_scalars = self.last_true_disp_scalars[-5:]
self.true_disp_scalar = self.length - self.neutral_length
def calc_true_voltage(self):
self.true_voltage = self.true_disp_scalar * self.scale
def calc_noisy_voltage(self):
# Calculate static error
static_error = np.random.normal(0, self.static_error_stdev)
# Non-linearity
if self.true_voltage > 1E-5:
nonlinearity_multiplier = self.true_voltage**(self.nonlinearity_exponent)
else:
nonlinearity_multiplier = 1
# Hysteresis
average_last = np.average(self.last_true_disp_scalars)
#hysteresis_error = (self.true_force_scalar - average_last) / self.true_force_scalar * self.hysteresis * self.full_scale_voltage
hysteresis_error = copysign(1, self.true_disp_scalar - average_last) * self.hysteresis * self.full_scale_voltage
# Repeatability
# self.repeatability_offset
# Thermal
thermal_error = self.thermal_error * self.true_voltage
self.noisy_voltage = self.true_voltage*nonlinearity_multiplier + static_error + hysteresis_error + self.repeatability_offset + thermal_error
def update_shared_attributes(self):
if self.shared_attributes is not None:
self.shared_attributes.set_current_length(self.length)
self.shared_attributes.set_true_voltage(self.true_voltage)
self.shared_attributes.set_noisy_voltage(self.noisy_voltage)
self.shared_attributes.set_displacement(self.true_disp_scalar)
def update_internal_attributes(self):
self.calc_sensor_true_disp()
self.calc_true_voltage()
self.calc_noisy_voltage()
def get_true_voltage_scalar(self) -> float:
return self.true_voltage
def get_noisy_voltage_scalar(self) -> float:
return self.noisy_voltage
class SharedDisplacementSensor():
def __init__(self):
self.neutral_length: float = 0.0
self.length: float = 0.0
self.displacement: float = 0.0
self.true_voltage: float = 0.0
self.noisy_voltage: float = 0.0
self.connected_to_plotter: bool = False
self.connected_to_visualization: bool = False
self.connected_to_sensor: bool = False
self.connected_to_controller: bool = False
def set_neutral_length(self, new:float):
self.neutral_length = new
def set_current_length(self, new:float):
self.length = new
def set_displacement(self, new:float):
self.displacement = new
def set_true_voltage(self, new:float):
self.true_voltage = new
def set_noisy_voltage(self, new:float):
self.noisy_voltage = new
def set_connected_to_plotter(self, state: bool) -> None:
self.connected_to_plotter = state
def set_connected_to_visualization(self, state: bool) -> None:
self.connected_to_visualization = state
def set_connected_to_sensor(self, state: bool) -> None:
self.connected_to_sensor = state
def set_connected_to_controller(self, state: bool) -> None:
self.connected_to_controller = state
def get_connected_to_plotter(self) -> bool:
return self.connected_to_plotter
def get_connected_to_visualization(self) -> bool:
return self.connected_to_visualization
def get_connected_to_sensor(self) -> bool:
return self.connected_to_sensor
def get_connected_to_controller(self) -> bool:
return self.connected_to_controller
def get_neutral_length(self) -> float:
return self.neutral_length
def get_displacement(self) -> float:
return self.displacement
def get_current_length(self) -> float:
return self.length
def get_true_voltage(self) -> float:
return self.true_voltage
def get_noisy_voltage(self) -> float:
return self.noisy_voltage

View File

@ -0,0 +1,236 @@
import time
import threading
import numpy as np
from typing import List
from Visualization import Visualization
from Physics_Elements import Joint, Spring, RigidActuator
class BFSSimulation():
def __init__(self, parent_joint: Joint, settings:dict):
if not isinstance(parent_joint, Joint):
raise TypeError(f"parent_joint for BFSSimulation is not a Joint")
self.parent_joint = parent_joint
self.duration = settings["duration"]
self.delta_t = settings["delta_t"]
self.plotting_update_period = settings["plotting_update_period"]
self.sensor_update_period = settings["sensor_update_period"]
self.controller_pull_period = settings["controller_pull_period"]
self.joints: List[Joint] = []
self.springs: List[Spring] = []
self.actuators: List[RigidActuator] = []
self.rigid_actuators: List[RigidActuator] = []
self.get_joints_bfs()
#self.get_parent_joints_for_every_joint()
# Plotting or viz elements are updated slower than sensor
self.plotting_or_viz_only_shared = []
self.sensor_shared = []
self.controller_shared = []
self.sort_shared_attributes_into_frequencies()
self.vis:Visualization = None
self.attached_shared_time:float = None
self.sim_time:float = 0.0
def is_in_joints(self, joint: Joint):
for j in self.joints:
if joint.is_same(j):
return True
return False
def is_in_queue(self, joint: Joint, queue):
for j in queue:
if joint.is_same(j):
return True
return False
def get_joints_bfs(self):
queue: List[Joint] = []
queue.append(self.parent_joint)
while queue:
parent_joint: Joint
parent_joint = queue.pop(0)
if parent_joint is None:
continue
if not parent_joint.is_in_list(self.joints):
self.joints.append(parent_joint)
connected_springs = parent_joint.get_connected_springs()
for spring in connected_springs:
if not spring.is_in_list(self.springs):
self.springs.append(spring)
child_joint = spring.get_child_joint()
if not self.is_in_joints(child_joint) and not self.is_in_queue(child_joint, queue):
queue.append(child_joint)
connected_actuators = parent_joint.get_connected_actuators()
actuator: RigidActuator = None
for actuator in connected_actuators:
if actuator not in self.actuators:
self.rigid_actuators.append(actuator)
if actuator.get_child_joint() not in self.joints and actuator.get_child_joint() not in queue:
queue.append(actuator.get_child_joint())
def get_parent_joints_for_every_joint(self):
for joint in self.joints:
joint.find_parent_joints()
def get_child_joints_for_every_joint(self):
for joint in self.joints:
joint.find_child_joints()
def update_actuators_from_controller(self):
for rigid_actuator in self.rigid_actuators:
rigid_actuator.update_from_controller()
def pull_actuator_positions_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_actuators_from_controller()
time.sleep(self.controller_pull_period)
def update_sensor_attributes(self):
'''Updates the joints, springs, and actuators that are attached to sensors'''
for obj in self.sensor_shared:
obj.update_shared_attributes()
def update_sensor_attributes_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_sensor_attributes()
time.sleep(self.sensor_update_period)
def update_plotting_or_viz_only_attributes(self):
'''Updates the joints, springs, and actuators that are only attached for plotting or visualization'''
for obj in self.plotting_or_viz_only_shared:
obj.update_shared_attributes()
def update_plotting_or_viz_only_attributes_thread(self):
while self.spare_stop_event.is_set() == False:
self.update_plotting_or_viz_only_attributes()
time.sleep(self.plotting_update_period)
def sort_shared_attributes_into_frequencies(self):
'''For each shared attribute connected to anything, put them in lists so we know how fast we need to update them.'''
for joint in self.joints:
shared = joint.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(joint)
elif plot or vis:
self.plotting_or_viz_only_shared.append(joint)
if contr:
self.controller_shared.append(joint)
for spring in self.springs:
shared = spring.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(spring)
elif plot or vis:
self.plotting_or_viz_only_shared.append(spring)
if contr:
self.controller_shared.append(spring)
for actuator in self.rigid_actuators:
shared = actuator.shared_attributes
if shared is None:
continue
plot = shared.get_connected_to_plotter()
vis = shared.get_connected_to_visualization()
sens = shared.get_connected_to_sensor()
contr = shared.get_connected_to_controller()
if sens:
self.sensor_shared.append(actuator)
elif plot or vis:
self.plotting_or_viz_only_shared.append(actuator)
if contr:
self.controller_shared.append(actuator)
def attach_shared_time(self, attached_shared_time):
self.attached_shared_time = attached_shared_time
def run_process(self):
time_values = []
self.sim_time = 0
#time_last = time.perf_counter()
time_last = time.time()
count = 0
self.spare_stop_event = threading.Event()
self.spare_stop_event.clear()
pull_actuator_thread = threading.Thread(target=self.pull_actuator_positions_thread)
pull_actuator_thread.start()
update_plotting_or_viz_only_thread = threading.Thread(target=self.update_plotting_or_viz_only_attributes_thread)
update_plotting_or_viz_only_thread.start()
update_sensor_thread = threading.Thread(target=self.update_sensor_attributes_thread)
update_sensor_thread.start()
while self.sim_time < self.duration:
count += 1
if self.delta_t is None:
#new_time = time.perf_counter()
new_time = time.time()
delta_time = new_time - time_last
time_last = new_time
for spring in self.springs:
spring.calc_spring_force()
joint: Joint
for joint in self.joints:
if joint.fixed == True:
continue
# Get joint forces
joint.calc_net_force(self.sim_time)
# Get joint accels.
joint.calc_accel()
# Integrate joint vel and pos
if self.delta_t is None:
joint.integrate_statespace(delta_time)
else:
joint.integrate_statespace(self.delta_t)
time_values.append(self.sim_time)
if self.delta_t is None:
self.sim_time += delta_time
else:
self.sim_time += self.delta_t
# Update the shared sim time as fast as possible
self.attached_shared_time.set(self.sim_time)
self.spare_stop_event.set()
pull_actuator_thread.join()
update_plotting_or_viz_only_thread.join()
update_sensor_thread.join()
print(f"Average delta_t = {self.sim_time / count}")

View File

@ -0,0 +1,594 @@
import vpython
import time
from copy import deepcopy
import multiprocessing
import threading
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore
from Physics_Elements import Joint, Spring, SharedRigidActuator, SharedSpring, SharedJoint
from Sensor_Elements import SharedLoadCellJoint, SharedLoadCellSpring, SharedDisplacementSensor
from Controller_Input_Elements import SharedRigidActuatorController
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Object_Sharing import SharedFloat
'''
TODO
2. Make this in a separate thread
'''
class Visualization():
def __init__(self, stop_event, scene_settings:dict):
self.stop_event = stop_event
self.scene_settings = scene_settings
self.attached_attributes = []
self.shared_attributes_object_settings = []
self.vpython_objects = []
def draw_scene(self):
vpython.scene = vpython.canvas(width=self.scene_settings["canvas_width"], height=self.scene_settings["canvas_height"])
side = self.scene_settings["cube_size"]
thk = self.scene_settings["wall_thickness"]
wall_length = side + 2*thk
wall_bottom = vpython.box(pos=vpython.vector(0, -thk/2, 0), size=vpython.vector(wall_length, thk, wall_length), color=vpython.color.gray(0.9))
wall_left = vpython.box(pos=vpython.vector(-(side + thk)/2, side/2, 0), size=vpython.vector(thk, side, side), color=vpython.color.gray(0.7))
wall_right = vpython.box(pos=vpython.vector((side + thk)/2, side/2, 0), size=vpython.vector(thk, side, side), color=vpython.color.gray(0.7))
wall_back = vpython.box(pos=vpython.vector(0, (side)/2, -(side+ thk)/2), size=vpython.vector(wall_length, side, thk), color=vpython.color.gray(0.7))
def attach_shared_attributes(self, shared_attributes, object_settings):
self.attached_attributes.append(shared_attributes)
self.shared_attributes_object_settings.append(object_settings)
shared_attributes.set_connected_to_visualization(True)
def generate_vpython_objects(self):
for i in range(len(self.attached_attributes)):
object_settings = self.shared_attributes_object_settings[i]
if object_settings["type"] == "sphere":
shared_attribute:Joint = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.sphere(pos=object_pos, radius=object_settings["radius"], color=object_color))
elif object_settings["type"] == "helix":
shared_attribute:Spring = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_axis = deepcopy(shared_attribute.get_axis_vector())
object_axis = vpython.vector(object_axis[0], object_axis[1], object_axis[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.helix(pos=object_pos, axis=object_axis, radius=object_settings["radius"], thickness=object_settings["thickness"], color=object_color))
elif object_settings["type"] == "cylinder":
shared_attribute:Spring = self.attached_attributes[i]
object_pos = deepcopy(shared_attribute.get_pos())
object_pos = vpython.vector(object_pos[0], object_pos[1], object_pos[2])
object_axis = deepcopy(shared_attribute.get_axis_vector())
object_axis = vpython.vector(object_axis[0], object_axis[1], object_axis[2])
object_color = self.return_vpython_color(object_settings["color"])
self.vpython_objects.append(vpython.cylinder(pos=object_pos, axis=object_axis, radius=object_settings["radius"], color=object_color))
def return_vpython_color(self, color_str):
if color_str == "blue":
return vpython.color.blue
elif color_str == "black":
return vpython.color.black
elif color_str == "red":
return vpython.color.red
elif color_str == "green":
return vpython.color.green
elif color_str == "white":
return vpython.color.white
def update_scene(self):
for i in range(len(self.vpython_objects)):
element = self.vpython_objects[i]
shared_attributes = self.attached_attributes[i]
if isinstance(element, vpython.sphere):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
elif isinstance(element, vpython.helix):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
updated_axis = shared_attributes.get_axis_vector()
element.axis = deepcopy(vpython.vector(*(updated_axis)))
elif isinstance(element, vpython.cylinder):
updated_pos = shared_attributes.get_pos()
element.pos = deepcopy(vpython.vector(*(updated_pos)))
updated_axis = shared_attributes.get_axis_vector()
element.axis = deepcopy(vpython.vector(*(updated_axis)))
def run_process(self):
# Draw scene
self.draw_scene()
# Generate vpython objects from settings
self.generate_vpython_objects()
while self.stop_event.is_set() == False:
# Update pos and axes of all vpython objects
self.update_scene()
class Plotter():
def __init__(self, plot_settings: dict, stop_event: multiprocessing.Event):
self.plot_settings = plot_settings
self.title = plot_settings["title"]
self.window_length = self.plot_settings["window_length"]
self.update_interval = self.plot_settings["update_interval"]
self.pull_interval = self.plot_settings["pull_interval"]
self.stop_event: multiprocessing.Event = stop_event
self.attached_attributes = []
self.shared_attributes_plot_settings = []
self.pull_data_stop_event: threading.Event = None
self.shared_x: SharedFloat = None
self.plot_setup = {}
def attach_shared_attributes(self, shared_attributes, plot_settings):
self.attached_attributes.append(shared_attributes)
self.shared_attributes_plot_settings.append(plot_settings)
shared_attributes.set_connected_to_plotter(True)
def attach_shared_x(self, shared_x):
self.shared_x = shared_x
def pull_data_thread(self):
while self.pull_data_stop_event.is_set() == False:
self.pull_data()
time.sleep(self.pull_interval / 1000)
def pull_data(self):
self.raw_xdata.append(self.shared_x.get())
for i in range(len(self.shared_attributes_plot_settings)):
foo = self.attached_attributes[i]._getvalue()
for key, value in self.shared_attributes_plot_settings[i].items():
if isinstance(foo, SharedLoadCellJoint):
bar: SharedLoadCellJoint = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_true_force_vector()
for subkey, settings in value.items():
if subkey == "x":
ylabel: str = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif key == "true_voltage":
voltage_scalar = bar.get_true_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif key == "noisy_voltage":
voltage_scalar = bar.get_noisy_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif isinstance(foo, SharedLoadCellSpring):
bar: SharedLoadCellSpring = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_true_force_vector()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif subkey == "scalar":
force_scalar = bar.get_true_force_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_scalar)
elif key == "true_voltage":
voltage_scalar = bar.get_true_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif key == "noisy_voltage":
voltage_scalar = bar.get_noisy_voltage_scalar()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif isinstance(foo, SharedRigidActuator):
bar: SharedRigidActuator = self.attached_attributes[i]
if key == "pos":
pos_vector = bar.get_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
if key == "vel":
pos_vector = self.attached_attributes[i].get_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
elif isinstance(foo, SharedRigidActuatorController):
bar: SharedRigidActuatorController = self.attached_attributes[i]
if key == "input_voltage":
input_voltage = bar.get_input_voltage()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(input_voltage)
elif key == "digital_input":
digital_input = bar.get_digital_command()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(digital_input)
elif key == "pos":
controlled_vel = bar.get_controlled_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[2])
elif key == "vel":
controlled_vel = bar.get_controlled_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(controlled_vel[2])
if subkey == "scalar":
vel_scalar = bar.get_controlled_vel_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_scalar)
elif key == "disp":
disp_scalar = bar.get_controlled_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(disp_scalar)
elif isinstance(foo, SharedDisplacementSensor):
bar: SharedDisplacementSensor = self.attached_attributes[i]
if key == "voltage":
for subkey, settings in value.items():
if subkey == "noisy":
voltage_scalar = bar.get_noisy_voltage()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
elif subkey == "true":
voltage_scalar = bar.get_true_voltage()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(voltage_scalar)
if key == "disp":
disp_scalar = bar.get_displacement()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(disp_scalar)
elif key == "length":
length_scalar = bar.get_current_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(length_scalar)
elif isinstance(foo, SharedSpring):
bar: SharedSpring = self.attached_attributes[i]
if key == "force":
force_vector = bar.get_spring_force()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
elif subkey == "scalar":
force_scalar = bar.get_spring_force_scalar()
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_scalar)
elif key == "length":
length = bar.get_length()
for subkey, settings in value.items():
if subkey == "scalar":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(length)
elif isinstance(foo, SharedJoint):
bar: SharedJoint = self.attached_attributes[i]
if key == "accel":
accel_vector = bar.get_accel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(accel_vector[2])
elif key == "vel":
vel_vector = bar.get_vel()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(vel_vector[2])
elif key == "pos":
pos_vector = bar.get_pos()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[1])
if subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(pos_vector[2])
elif key == "force":
force_vector = bar.get_spring_force()
for subkey, settings in value.items():
if subkey == "x":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[0])
elif subkey == "y":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[1])
elif subkey == "z":
ylabel = settings["ylabel"]
(subplot, line) = self.raw_data_map[ylabel]
self.raw_ydata[subplot][line].append(force_vector[2])
def update_live_window(self):
if self.stop_event.is_set():
self.timer.stop()
self.pull_data_stop_event.set()
return
final_time = self.raw_xdata[-1]
target_time = final_time - self.window_length
closest_index = min(range(len(self.raw_xdata)), key=lambda i: abs(self.raw_xdata[i] - target_time))
self.window_xdata = self.raw_xdata[closest_index:]
# Attach the data to the appropriate sublines in the subplots
for i in range(self.num_plots):
for j in range(len(self.subplot_keys[i])):
window_ydata = self.raw_ydata[i][j][closest_index:]
if self.plot_settings["step_or_plot"] == "plot":
if len(self.window_xdata) == len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata, window_ydata)
elif len(self.window_xdata) > len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata[:-1], window_ydata)
elif self.plot_settings["step_or_plot"] == "step":
if len(self.window_xdata) == len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata, window_ydata[:-1])
elif len(self.window_xdata) > len(window_ydata):
self.subplot_curves[i][j].setData(self.window_xdata[:-1], window_ydata[:-1])
def generate_plots(self):
# Create the application
self.app = QtWidgets.QApplication([])
# Create a plot window
self.win = pg.GraphicsLayoutWidget(show=True, title=self.title)
self.win.resize(1000, 600)
self.win.setWindowTitle(self.plot_settings["title"])
self.num_plots = self.plot_settings["num_subplots"]
self.plots = []
for i in range(self.num_plots):
plot = self.win.addPlot(title=self.plot_settings["subplots"][i]["ylabel"])
plot.setLabel('left', self.plot_settings["subplots"][i]["ylabel"])
plot.addLegend()
self.plots.append(plot)
# Move us to the next row for the subplot
if i < self.num_plots - 1:
self.win.nextRow()
def generate_curves(self):
self.subplot_keys = [[] for _ in range(self.num_plots)] # list for each subplot
self.subplot_curves = [[] for _ in range(self.num_plots)] # list for each subplot
self.raw_xdata = []
self.raw_ydata = [[] for _ in range(self.num_plots)] # list for each subplot
self.raw_data_map = {}
for setting in self.shared_attributes_plot_settings:
'''
Each setting looks like {
"accel": {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Accel"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Accel"
}
},
"vel": {
"x": {
"subplot": 2,
"ylabel": "Main Mass x-Vel"
}
},
"pos": {
"x": {
"subplot": 3,
"ylabel": "Main Mass x-Vel"
}
}
}
'''
for key, value in setting.items():
''' key would be like "accel"
value would be like {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Accel"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Accel"
}
}
'''
for line, line_settings in value.items():
''' line would be like "x"
line_settings would be like {
"subplot": 1,
"ylabel": "Main Mass x-Accel",
"color": "w"
}
'''
subplot = line_settings["subplot"]
label = line_settings["ylabel"]
color = line_settings.get("color", "w") # Default to white if no color is specified
self.subplot_keys[subplot].append(label)
if self.plot_settings["step_or_plot"] == "plot":
self.subplot_curves[subplot].append(self.plots[subplot].plot(name=label, pen=pg.mkPen(color=color)))
elif self.plot_settings["step_or_plot"] == "step":
self.subplot_curves[subplot].append(self.plots[subplot].plot(name=label, stepMode=True, pen=pg.mkPen(color=color)))
self.raw_data_map[label] = (subplot, len(self.raw_ydata[subplot]))
self.raw_ydata[subplot].append([])
self.window_xdata = []
self.window_ydata = [[] for _ in range(self.num_plots)] # list for each subplot
def show_plot(self):
pass
def run_process(self):
# Generate the figure and subplots
self.generate_plots()
# Generate the curves
self.generate_curves()
self.pull_data_stop_event = threading.Event()
self.pull_data_stop_event.clear()
pull_data_thread = threading.Thread(target=self.pull_data_thread)
pull_data_thread.start()
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_live_window)
self.timer.start(self.update_interval)
# Start the Qt event loop
QtWidgets.QApplication.instance().exec_()
pull_data_thread.join()

Binary file not shown.

View File

@ -0,0 +1,268 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, SharedJoint, SharedSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting. 1 for the mass and the other for the spring
mass_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Joint Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
}
],
"window_length": 100, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
mass_plotter.attach_shared_x(shared_sim_time)
spring_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Spring Plotter",
"xlabel": "Time (s)",
"num_subplots": 2,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Length (m)"
},
{
"ylabel": "Force (N)"
}
],
"window_length": 100, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 milliseconds
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
spring_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 5, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
wall_joint = Joint(
pos = np.array([-10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "Wall Joint"
)
spring = Spring(
parent_joint = main_mass,
child_joint = wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "Spring"
)
'''Adding items to be plotted'''
# Since we want to plot the physics of the mass and spring, we need shared attributes for them
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
shared_spring: SharedSpring = base_manager.SharedSpring()
spring.attach_shared_attributes(shared_spring)
# Attach the shared elements to the plotter
mass_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
spring_plotter.attach_shared_attributes(
shared_attributes = shared_spring,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": "Spring Length",
"color": "r"
}
},
"force": {
"x": {
"subplot": 1,
"ylabel": "Spring x-Force",
"color": "g"
}
}
}
)
'''Adding items to be visualized'''
# We aready have shared_main_mass and shared_spring from the plotting, so there is no need to make more shared elements
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
mass_plotter_process = multiprocessing.Process(target=mass_plotter.run_process)
spring_plotter_process = multiprocessing.Process(target=spring_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
mass_plotter_process.start()
spring_plotter_process.start()
visualization_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
mass_plotter_process.join()
spring_plotter_process.join()
visualization_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,430 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from HITL_Controller import HITLController
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
from Sensor_Elements import DisplacementSensor, LoadCellSpring, SharedDisplacementSensor, SharedLoadCellSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting.
physics_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
},
{
"ylabel": "Force (N)"
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
physics_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the controller input plotting.
controller_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Pos (m)",
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
controller_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the sensor output plotting
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 2,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Sensor Output (V)",
},
],
"window_length": 10, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Creating an instance for the HITLController'''
hitl_controller = HITLController(
stop_event = stop_event,
settings = {
"pull_from_sim_period": 5,
"plotting_update_period": 10
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 5, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
# Since we want to plot the physics of the mass we need shared attributes for it
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
north_wall_joint = Joint(
pos = np.array([10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "North Wall Joint"
)
north_spring = Spring(
parent_joint = main_mass,
child_joint = north_wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "North Spring"
)
shared_north_spring: SharedSpring = base_manager.SharedSpring()
north_spring.attach_shared_attributes(shared_north_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_north_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# Setting up the actuator. This requires the "grounded_joint" to fix to. We also want a spring in between the actuator and main_mass
actuator_joint = Joint(
pos = np.array([-5, 5, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Actuator Joint"
)
actuator_spring = Spring(
parent_joint = main_mass,
child_joint = actuator_joint,
unstretched_length = 5,
constant_stiffness = 150,
name = "Actuator Spring"
)
shared_actuator_spring: SharedSpring = base_manager.SharedSpring()
actuator_spring.attach_shared_attributes(shared_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Actuator Spring Force",
"color": "r"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
south_wall_joint = Joint(
pos = np.array([-10, 5, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "South Wall Joint"
)
rigid_actuator = RigidActuator(
parent_joint = actuator_joint,
grounded_joint = south_wall_joint,
name = "Rigid Actuator",
max_length = 8,
min_length = 2,
control_code = 0 # Position Control
)
shared_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
rigid_actuator.attach_shared_attributes(shared_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Rigid Actuator Controller",
"analog_input_channel": 0,
"digital_input_channel": 0,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 0, # Controls position
"max_length": 8,
"min_length": 2
}
)
rigid_actuator_controller.attach_sim_target(shared_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(rigid_actuator_controller)
shared_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
rigid_actuator_controller.attach_shared_attributes(shared_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Controller Voltage",
"color": "b"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Actuator Controller CTRL Input",
"color": "b"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Actuator Controller Displacement",
"color": "b"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_south_wall_joint: SharedJoint = base_manager.SharedJoint()
south_wall_joint.attach_shared_attributes(shared_south_wall_joint)
shared_actuator_joint: SharedJoint = base_manager.SharedJoint()
actuator_joint.attach_shared_attributes(shared_actuator_joint)
rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_actuator_joint,
child_joint = shared_south_wall_joint,
sensor_settings = {
"name": f"Actuator Displacement Sensor",
"output_channel": 0,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
rigid_actuator_displacement_sensor.attach_shared_attributes(shared_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Displacement Length",
"color": "b"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 1,
"ylabel": f"Actuator Displacement Voltage",
"color": "b"
}
},
}
)
hitl_controller.attach_displacement_sensor(rigid_actuator_displacement_sensor)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
hitl_controller_process = multiprocessing.Process(target=hitl_controller.run_process)
physics_plotter_process.start()
controller_plotter_process.start()
sensor_plotter_process.start()
visualization_process.start()
hitl_controller_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
physics_plotter_process.join()
controller_plotter_process.join()
sensor_plotter_process.join()
visualization_process.join()
hitl_controller_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,649 @@
import numpy as np
import time
import multiprocessing
from multiprocessing.managers import BaseManager
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from HITL_Controller import HITLController
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
from Sensor_Elements import DisplacementSensor, LoadCellSpring, SharedDisplacementSensor, SharedLoadCellSpring
from Object_Sharing import SharedFloat
def main():
'''Setting up the BaseManager so data can be shared between processes'''
base_manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
base_manager.start()
'''Creating instances for plotter and visualizer'''
# Setup the plotter and visualization processes
stop_event = multiprocessing.Event()
stop_event.clear()
# Create a synchronized time between the processes so the plotter and physics sim are in sync
shared_sim_time:SharedFloat = base_manager.SharedFloat()
shared_sim_time.set(0.0) # Set the initial time = 0.0 seconds
# Create a real-time plotter for physics plotting.
physics_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Position (m)"
},
{
"ylabel": "Velocity (m/s)"
},
{
"ylabel": "Accel (m/s/s)"
},
{
"ylabel": "Force (N)"
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
physics_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the controller input plotting.
controller_plotter = Plotter(
stop_event = stop_event,
plot_settings = {
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Pos (m)",
},
],
"window_length": 10, # Keep 100 seconds visible
"pull_interval": 10, # Pull data every 10 millisecond
"update_interval": 100 # Update the graph every 100 milliseconds
}
)
controller_plotter.attach_shared_x(shared_sim_time)
# Create a real-time plotter for the sensor output plotting
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Load Cell Force (N)",
},
{
"ylabel": "Sensor Output (V)",
},
],
"window_length": 10, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
# Create a 3D visualization for the entire model
visualizer = Visualization(
stop_event = stop_event,
scene_settings = {
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 20
}
)
'''Creating an instance for the HITLController'''
hitl_controller = HITLController(
stop_event = stop_event,
settings = {
"pull_from_sim_period": 5,
"plotting_update_period": 10
}
)
'''Setting up physics simulation'''
# Create the physics elements
main_mass = Joint(
pos = np.array([0, 10, 0]),
mass = 5,
fixed = False,
name = "Main mass",
integration_method = "adams-bashforth"
)
main_mass.add_damping(mom_ratio=0.0)
# Since we want to plot the physics of the mass we need shared attributes for it
shared_main_mass: SharedJoint = base_manager.SharedJoint()
main_mass.attach_shared_attributes(shared_main_mass)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_main_mass,
plot_settings = {
"pos": {
"x": {
"subplot": 0,
"ylabel": "Main Poss x-Pos",
"color": "r"
}
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Poss x-Vel",
"color": "g"
}
},
"accel": {
"x": {
"subplot": 2,
"ylabel": "Main Poss x-Accel",
"color": "b"
}
}
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_main_mass,
object_settings = {
"type": "sphere",
"color": "red",
"radius": 0.5,
}
)
# Before we create a spring, we need to create the Joint on the wall for the Spring to mount to
north_wall_joint = Joint(
pos = np.array([10, 15, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "North Wall Joint"
)
north_spring = Spring(
parent_joint = main_mass,
child_joint = north_wall_joint,
unstretched_length = 13,
constant_stiffness = 100,
name = "North Spring"
)
shared_north_spring: SharedSpring = base_manager.SharedSpring()
north_spring.attach_shared_attributes(shared_north_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_north_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
west_wall_joint = Joint(
pos = np.array([0, 15, -10]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "West Wall Joint"
)
west_spring = Spring(
parent_joint = main_mass,
child_joint = west_wall_joint,
unstretched_length = 9,
constant_stiffness = 100,
name = "West Spring"
)
shared_west_spring: SharedSpring = base_manager.SharedSpring()
west_spring.attach_shared_attributes(shared_west_spring)
visualizer.attach_shared_attributes(
shared_attributes = shared_west_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# Setting up the actuator. This requires the "grounded_joint" to fix to. We also want a spring in between the actuator and main_mass
actuator_joint = Joint(
pos = np.array([-5, 10, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Actuator Joint"
)
actuator_spring = Spring(
parent_joint = main_mass,
child_joint = actuator_joint,
unstretched_length = 7,
constant_stiffness = 150,
name = "Actuator Spring"
)
shared_actuator_spring: SharedSpring = base_manager.SharedSpring()
actuator_spring.attach_shared_attributes(shared_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Actuator Spring Force",
"color": "b"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
south_wall_joint = Joint(
pos = np.array([-10, 10, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "South Wall Joint"
)
rigid_actuator = RigidActuator(
parent_joint = actuator_joint,
grounded_joint = south_wall_joint,
name = "Rigid Actuator",
max_length = 8,
min_length = 2,
control_code = 0 # Position Control
)
shared_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
rigid_actuator.attach_shared_attributes(shared_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Rigid Actuator Controller",
"analog_input_channel": 0,
"digital_input_channel": 0,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 0, # Controls position
"max_length": 8,
"min_length": 2
}
)
rigid_actuator_controller.attach_sim_target(shared_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(rigid_actuator_controller)
shared_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
rigid_actuator_controller.attach_shared_attributes(shared_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Controller Voltage",
"color": "r"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Actuator Controller CTRL Input",
"color": "r"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Actuator Controller Displacement",
"color": "r"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_south_wall_joint: SharedJoint = base_manager.SharedJoint()
south_wall_joint.attach_shared_attributes(shared_south_wall_joint)
shared_actuator_joint: SharedJoint = base_manager.SharedJoint()
actuator_joint.attach_shared_attributes(shared_actuator_joint)
rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_actuator_joint,
child_joint = shared_south_wall_joint,
sensor_settings = {
"name": f"Actuator Displacement Sensor",
"output_channel": 0,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
rigid_actuator_displacement_sensor.attach_shared_attributes(shared_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Actuator Displacement Length",
"color": "r"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 2,
"ylabel": f"Actuator Displacement Voltage",
"color": "r"
}
},
}
)
hitl_controller.attach_displacement_sensor(rigid_actuator_displacement_sensor)
'''Second actuator'''
bottom_actuator_joint = Joint(
pos = np.array([0, 5, 0]),
mass = 1, # Does not matter because it is fixed to an actuator
fixed = False, # We do not want it to ever move
name = "Bottom Actuator Joint"
)
bottom_actuator_spring = Spring(
parent_joint = main_mass,
child_joint = bottom_actuator_joint,
unstretched_length = 7.5,
constant_stiffness = 1000,
name = "Bottom Actuator Spring"
)
shared_bottom_actuator_spring: SharedSpring = base_manager.SharedSpring()
bottom_actuator_spring.attach_shared_attributes(shared_bottom_actuator_spring)
physics_plotter.attach_shared_attributes(
shared_attributes = shared_bottom_actuator_spring,
plot_settings = {
"force": {
"scalar": {
"subplot": 3,
"ylabel": "Bottom Actuator Spring Force",
"color": "r"
}
},
}
)
visualizer.attach_shared_attributes(
shared_attributes = shared_bottom_actuator_spring,
object_settings = {
"type": "helix",
"color": "white",
"radius": 0.5,
"thickness": 0.1
}
)
# LoadCell on the bottom spring
bottom_spring_loadcell = LoadCellSpring(sensor_settings = {
"name": "Bottom Spring LC",
"mV/V": 1000,
"excitation": 10,
"full_scale_force": 5000, # What is the max force on the load cell
"output_channel": 8 # The output channel that the load cell outputs through the MCC3114. Must be between [0, 15]
}
)
bottom_spring_loadcell.attach_sim_source(shared_bottom_actuator_spring) # Point the LoadCellSpring to pull physics values from the SharedSpring
shared_bottom_spring_loadcell: SharedLoadCellSpring = base_manager.SharedLoadCellSpring()
bottom_spring_loadcell.attach_shared_attributes(shared_bottom_spring_loadcell)
hitl_controller.attach_load_cell(bottom_spring_loadcell)
sensor_plotter.attach_shared_attributes(shared_attributes = shared_bottom_spring_loadcell,
plot_settings = {
"force": {
"scalar": {
"subplot": 1, # Must be in range [0, num_subplots-1]
"ylabel": "Bottom Spring LC-Force",
"color": "g" # White is default if not listed. "b", "g", "r", "c", "m", "y", "k", "w"
},
},
"noisy_voltage": {
"scalar": {
"subplot": 2, # Must be in range [0, num_subplots-1]
"ylabel": "Bottom Spring LC-Voltage",
"color": "g" # White is default if not listed. "b", "g", "r", "c", "m", "y", "k", "w"
}
}
}
)
bottom_wall_joint = Joint(
pos = np.array([0, 0, 0]),
mass = 1, # Does not matter because it is fixed
fixed = True, # We do not want it to ever move
name = "Bottom Wall Joint"
)
bottom_rigid_actuator = RigidActuator(
parent_joint = bottom_actuator_joint,
grounded_joint = bottom_wall_joint,
name = "Bottom Rigid Actuator",
max_length = 10,
min_length = 1,
control_code = 1 # Position Control
)
shared_bottom_rigid_actuator: SharedRigidActuator = base_manager.SharedRigidActuator()
bottom_rigid_actuator.attach_shared_attributes(shared_bottom_rigid_actuator)
visualizer.attach_shared_attributes(
shared_attributes = shared_bottom_rigid_actuator,
object_settings = {
"type": "cylinder",
"color": "black",
"radius": 0.5,
}
)
# We have an actuator, but it currently does nothing without the RigidActuatorController
# We will add the Controller and a DisplacementSensor for something in the real-world to control with
bottom_rigid_actuator_controller = RigidActuatorController(
controller_settings = {
"name": "Bottom Rigid Actuator Controller",
"analog_input_channel": 1,
"digital_input_channel": 2,
"units_per_volt": 2, # 2 meters per volt
"neutral_length": 1.5,
"neutral_voltage": 2.5, # At 2.5V, go to neutral_length of 5
"controls_pos/vel/accel": 1, # Controls position
"max_length": 10,
"min_length": 1
}
)
bottom_rigid_actuator_controller.attach_sim_target(shared_bottom_rigid_actuator)
hitl_controller.attach_rigid_actuator_controller(bottom_rigid_actuator_controller)
shared_bottom_rigid_actuator_controller: SharedRigidActuatorController = base_manager.SharedRigidActuatorController()
bottom_rigid_actuator_controller.attach_shared_attributes(shared_bottom_rigid_actuator_controller)
controller_plotter.attach_shared_attributes(
shared_attributes = shared_bottom_rigid_actuator_controller,
plot_settings = {
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"Bottom Actuator Controller Voltage",
"color": "b"
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"Bottom Actuator Controller CTRL Input",
"color": "b"
}
},
"disp": {
"scalar": {
"subplot": 2,
"ylabel": f"Bottom Actuator Controller Displacement",
"color": "b"
}
}
}
)
# For the displacement sensor, we need 2 shared joints. 1 is the grounded joint of the actuator. the other is the parent
shared_bottom_wall_joint: SharedJoint = base_manager.SharedJoint()
bottom_wall_joint.attach_shared_attributes(shared_bottom_wall_joint)
shared_bottom_actuator_joint: SharedJoint = base_manager.SharedJoint()
bottom_actuator_joint.attach_shared_attributes(shared_bottom_actuator_joint)
bottom_rigid_actuator_displacement_sensor = DisplacementSensor(
parent_joint = shared_bottom_actuator_joint,
child_joint = shared_bottom_wall_joint,
sensor_settings = {
"name": f"Bottom Actuator Displacement Sensor",
"output_channel": 1,
"volts_per_meter": 1,
"neutral_length": 0,
"neutral_voltage": 0, # Voltage at neutral length
"max_length": 8,
}
)
shared_bottom_rigid_actuator_displacement_sensor: SharedDisplacementSensor = base_manager.SharedDisplacementSensor()
bottom_rigid_actuator_displacement_sensor.attach_shared_attributes(shared_bottom_rigid_actuator_displacement_sensor)
sensor_plotter.attach_shared_attributes(shared_attributes=shared_bottom_rigid_actuator_displacement_sensor,
plot_settings = {
"length": {
"scalar": {
"subplot": 0,
"ylabel": f"Bottom Actuator Displacement Length",
"color": "b"
}
},
"voltage": {
"true": { # true or noisy because we didn't add noise
"subplot": 2,
"ylabel": f"Bottom Actuator Displacement Voltage",
"color": "b"
}
},
}
)
hitl_controller.attach_displacement_sensor(bottom_rigid_actuator_displacement_sensor)
'''Create the physics simulation'''
simulation = BFSSimulation(
parent_joint = main_mass, # Because spring is a child of main_mass, it will be reached by BFS
settings = {
"duration": 1000, # Run the sim for 1000 seconds
"delta_t": None, # Run in real-time
"plotting_update_period": 0.01, # Update the plottable elements every 0.01 seconds
"sensor_update_period": 0.01, # Update the sensor elements every second (because we have none, this doesn't matter)
"controller_pull_period": 0.01 # Update the controller elements every seconds (again, we have none)
}
)
simulation.attach_shared_time(shared_sim_time)
'''Setup the processes and run them'''
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
visualization_process = multiprocessing.Process(target=visualizer.run_process)
simulation_process = multiprocessing.Process(target=simulation.run_process)
hitl_controller_process = multiprocessing.Process(target=hitl_controller.run_process)
physics_plotter_process.start()
controller_plotter_process.start()
sensor_plotter_process.start()
visualization_process.start()
hitl_controller_process.start()
time.sleep(5) # Give the plotters and visualizer time to startup before the physics sim runs.
# Join the process
simulation_process.start()
simulation_process.join() # This blocks until the simulation finishes
physics_plotter_process.join()
controller_plotter_process.join()
sensor_plotter_process.join()
visualization_process.join()
hitl_controller_process.join()
# Close the manager
base_manager.shutdown()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,514 @@
import numpy as np
import multiprocessing
from multiprocessing.managers import BaseManager
import time
# if TYPE_CHECKING:
from Visualization import Visualization, Plotter
from Simulation import BFSSimulation
from Physics_Elements import Joint, Spring, RigidActuator, SharedRigidActuator, SharedJoint, SharedSpring, SharedRigidActuator
from Object_Sharing import SharedFloat
from HITL_Controller import HITLController
from Sensor_Elements import SharedLoadCellJoint, LoadCellSpring, SharedLoadCellSpring, DisplacementSensor, SharedDisplacementSensor
from Controller_Input_Elements import RigidActuatorController, SharedRigidActuatorController
steel_elastic_modulus = 200E9 # 200GPa
gravity = 9.81 # m/s^2
# Stage setup
stage_mass = 500_000 # Mass in kilograms
stage_weight = stage_mass * gravity
max_thrust = 1.5E7 # Max thrust in Newtons. Currently 15_000_000 N
stage_diameter = 5.5 # meters
stage_height = 60 # Meters
wall_thickness = 0.03 # Meters
# Actuator setup
rope_area = 5E-4 # Area in m^2. Currently 5 cm^2
rope_force_at_neutral_actuator = (max_thrust - stage_weight) / 4.0
lateral_offset = 2 # Meters away from the stage wall
actuator_neutral_length = 2
actuator_min_length = 0.5
actuator_max_length = 2.5
actuator_controller_neutral_voltage = 0
actuator_controller_mps_per_volt = 1 # meters per second per volt for the RMS controller to command the sim
actuator_displacement_sensor_neutral_voltage = 0
actuator_displacement_sensor_neutral_length = 0
actuator_displacement_sensor_volts_per_meter = 3 # Volts per meter for the actuator displacement sensor to output
rope_volts_per_newton = 1E-6 # 1 volt is 1 million newtons
# Stage calculations for stiffness of rope supporting main mass
stage_wall_area = np.pi / 4.0 * (stage_diameter**2 - (stage_diameter - wall_thickness*2)**2)
stage_stiffness = steel_elastic_modulus * stage_wall_area / stage_height # k = EA/l
stage_unstretched_length = stage_height - stage_weight / stage_stiffness
# Actuator calculations
rope_stiffness = steel_elastic_modulus * rope_area / stage_height # Estimate due to stageheight != rope length but it is close enough
rope_unstretched_length = rope_force_at_neutral_actuator / rope_stiffness + 56.45
actuator_lateral_offset = stage_diameter / 2 + lateral_offset
actuator_angle = np.arctan(stage_height/actuator_lateral_offset)
actuator_parent_height = actuator_neutral_length * np.sin(actuator_angle)
actuator_parent_lateral_offset = actuator_neutral_length * np.cos(actuator_angle)
def sinusoid_force1(time):
return 100_000*np.sin(0.1*time)
def sinusoid_force2(time):
return 125_000*np.sin(0.2*time+0.3)
def rope_stiffness_func(length, unstretched_length):
if length > unstretched_length:
return rope_stiffness
else:
return 0
def stage_setup_1(_max_run_time=100):
max_run_time = _max_run_time
# SIM SETUP #############################################
manager = BaseManager()
BaseManager.register('SharedFloat', SharedFloat)
BaseManager.register('SharedJoint', SharedJoint)
BaseManager.register('SharedSpring', SharedSpring)
BaseManager.register('SharedRigidActuator', SharedRigidActuator)
BaseManager.register('SharedLoadCellJoint', SharedLoadCellJoint)
BaseManager.register('SharedLoadCellSpring', SharedLoadCellSpring)
BaseManager.register('SharedRigidActuatorController', SharedRigidActuatorController)
BaseManager.register('SharedDisplacementSensor', SharedDisplacementSensor)
manager.start()
stop_event = multiprocessing.Event()
stop_event.clear()
shared_sim_time = manager.SharedFloat()
shared_sim_time.set(0.0)
physics_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Physics Plotter",
"xlabel": "Time (s)",
"num_subplots": 5,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Accel (m/s/s)",
},
{
"ylabel": "Vel (m/s)",
},
{
"ylabel": "Pos (m)",
},
{
"ylabel": "Pos 2 (m)",
},
{
"ylabel": "Force (N)",
},
],
"window_length": 100,
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds
})
physics_plotter.attach_shared_x(shared_sim_time)
sensor_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Sensor Plotter",
"xlabel": "Time (s)",
"num_subplots": 4,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Spring Force (N)",
},
{
"ylabel": "Actuator Length (m)",
},
{
"ylabel": "Actuator Position Output (V)",
},
{
"ylabel": "Load Cell Output (V)",
},
],
"window_length": 100, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
sensor_plotter.attach_shared_x(shared_sim_time)
controller_plotter = Plotter(stop_event=stop_event, plot_settings={
"title": "Controller Plotter",
"xlabel": "Time (s)",
"num_subplots": 3,
"step_or_plot": "plot",
"subplots": [
{
"ylabel": "Controller Input (V)",
},
{
"ylabel": "Digital Input"
},
{
"ylabel": "Controlled Vel (m/s)",
},
],
"window_length": 100, # Max data points to keep on the plot
"pull_interval": 10, # in milliseconds
"update_interval": 100, # In milliseconds.
})
controller_plotter.attach_shared_x(shared_sim_time)
visualizer = Visualization(stop_event=stop_event, scene_settings={
"canvas_width": 1600,
"canvas_height": 1000,
"wall_thickness": 0.1,
"cube_size": 100
})
hitl_controller = HITLController(stop_event=stop_event, settings={
"pull_from_sim_period": 5, # Pull values for the sensors every XXX milliseconds. Shouldn't be faster than the update_period for the sim
"plotting_update_period": 10, # Update the shared attributes (for plotting) every YYY milliseconds
})
# SIM ELEMENTS SETUP
# Changes height to 59.78258707863 rather than 60m to that it is in equilibrium
main_mass = Joint(np.array([0, 60, 0]), mass=stage_mass, fixed=False, name="Stage Mass", integration_method="adams-bashforth")
main_mass_shared:SharedJoint = manager.SharedJoint()
main_mass.attach_shared_attributes(main_mass_shared)
main_mass.add_damping(mom_ratio=1.5)
main_mass.add_gravity()
main_mass.add_variable_force([sinusoid_force1, None, sinusoid_force2])
physics_plotter.attach_shared_attributes(main_mass_shared, plot_settings={
"accel": {
"x": {
"subplot": 0,
"ylabel": "Main Mass x-Accel",
"color": "r"
},
"y": {
"subplot": 0,
"ylabel": "Main Mass y-Accel",
"color": "g"
},
"z": {
"subplot": 0,
"ylabel": "Main Mass z-Accel",
"color": "b"
},
},
"vel": {
"x": {
"subplot": 1,
"ylabel": "Main Mass x-Vel",
"color": "r"
},
"y": {
"subplot": 1,
"ylabel": "Main Mass y-Vel",
"color": "g"
},
"z": {
"subplot": 1,
"ylabel": "Main Mass z-Vel",
"color": "b"
}
},
"pos": {
"x": {
"subplot": 2,
"ylabel": "Main Mass x-Pos",
"color": "r"
},
"y": {
"subplot": 3,
"ylabel": "Main Mass y-Pos",
"color": "g"
},
"z": {
"subplot": 2,
"ylabel": "Main Mass z-Pos",
"color": "b"
}
}
})
visualizer.attach_shared_attributes(main_mass_shared, object_settings={
"type": "sphere",
"color": "red",
"radius": stage_diameter/2.0
})
support_spring_joint = Joint(np.array([0, stage_height*2, 0]), mass=0, fixed=True, name="Support Spring Joint")
support_spring = Spring(parent_joint=main_mass, child_joint=support_spring_joint, unstretched_length=stage_unstretched_length, constant_stiffness=stage_stiffness, name="Support Spring")
support_spring_shared_attributes:SharedSpring = manager.SharedSpring()
support_spring.attach_shared_attributes(support_spring_shared_attributes)
physics_plotter.attach_shared_attributes(support_spring_shared_attributes, plot_settings={
"force": {
"y": {
"subplot": 4,
"ylabel": "Support Spring y-Force",
"color": "w"
}
}
})
# Setting up the actuators and ropes
for i in range(4):
if i == 0:
floor_x_pos = actuator_lateral_offset
floor_z_pos = 0
parent_x_pos = actuator_lateral_offset - actuator_parent_lateral_offset
parent_z_pos = 0
name = "East Actuator"
name2 = "East Spring"
color = "r"
elif i == 1:
floor_x_pos = 0
floor_z_pos = actuator_lateral_offset
parent_x_pos = 0
parent_z_pos = actuator_lateral_offset - actuator_parent_lateral_offset
name = "South Actuator"
name2 = "South Spring"
color = "g"
elif i == 2:
floor_x_pos = -1*actuator_lateral_offset
floor_z_pos = 0
parent_x_pos = -1*actuator_lateral_offset + actuator_parent_lateral_offset
parent_z_pos = 0
name = "West Actuator"
name2 = "West Spring"
color = "b"
elif i == 3:
floor_x_pos = 0
floor_z_pos = -1*actuator_lateral_offset
parent_x_pos = 0
parent_z_pos = -1*actuator_lateral_offset + actuator_parent_lateral_offset
name = "North Actuator"
name2 = "North Spring"
color = "w"
actuator_spring_joint = Joint(np.array([parent_x_pos, actuator_parent_height, parent_z_pos]), mass=0.001, fixed=False, name=f"{name} Spring Joint")
actuator_floor_joint = Joint(np.array([floor_x_pos, 0, floor_z_pos]), mass=0.001, fixed=False, name=f"{name} Floor Joint")
#rope = Spring(parent_joint=main_mass, child_joint=actuator_spring_joint, unstretched_length=rope_unstretched_length, constant_stiffness=rope_stiffness, name=name2)
rope = Spring(parent_joint=main_mass, child_joint=actuator_spring_joint, unstretched_length=rope_unstretched_length, stiffness_func=rope_stiffness_func, name=name2)
rope_shared:SharedSpring = manager.SharedSpring()
rope.attach_shared_attributes(rope_shared)
visualizer.attach_shared_attributes(rope_shared, object_settings={
"type": "helix",
"color": "white",
"radius": 0.1,
"thickness": 0.1
})
rope_lc = LoadCellSpring(sensor_settings={
"name": f"{name2} LC",
"mV/V": 1000,
"excitation": 10,
"full_scale_force": (1/rope_volts_per_newton * 10),
"output_channel": i*4 + 3,
"noise_settings": {
"static_error_band": 0.15, # % of FSO
"non-linearity": 0.15, # % of FSO
"hysteresis": 0.07, # % of FSO
"repeatability": 0.02, # % of FSO
"thermal_error": 0.005, # % of reading per degree F
"temperature_offset": 10 # Degrees F off from calibration temperature
}
})
rope_lc.attach_sim_source(rope_shared)
rope_lc_shared_attributes = manager.SharedLoadCellSpring()
rope_lc.attach_shared_attributes(rope_lc_shared_attributes)
hitl_controller.attach_load_cell(rope_lc)
sensor_plotter.attach_shared_attributes(rope_lc_shared_attributes, plot_settings={
"force": {
"scalar": {
"subplot": 0,
"ylabel": f"{name2} LC Force",
"color": color
}
},
"noisy_voltage": {
"scalar": {
"subplot": 3,
"ylabel": f"{name2} LC Noisy-Voltage",
"color": color
}
},
})
actuator = RigidActuator(actuator_spring_joint, actuator_floor_joint, name=name, max_length=actuator_max_length, min_length=actuator_min_length, control_code=1)
actuator_shared_attributes:SharedRigidActuator = manager.SharedRigidActuator()
actuator.attach_shared_attributes(actuator_shared_attributes)
visualizer.attach_shared_attributes(actuator_shared_attributes, object_settings={
"type": "cylinder",
"color": "black",
"radius": 0.3
})
shared_actuator_spring_joint: SharedJoint = manager.SharedJoint()
actuator_spring_joint.attach_shared_attributes(shared_actuator_spring_joint)
shared_actuator_floor_joint: SharedJoint = manager.SharedJoint()
actuator_floor_joint.attach_shared_attributes(shared_actuator_floor_joint)
actuator_disp_sensor = DisplacementSensor(parent_joint=shared_actuator_spring_joint, child_joint=shared_actuator_floor_joint, sensor_settings={
"name": f"{name} Sensor",
"output_channel": i*4 + 2,
"volts_per_meter": actuator_displacement_sensor_volts_per_meter,
"neutral_length": actuator_displacement_sensor_neutral_length, # Length at neutral voltage
"neutral_voltage": actuator_displacement_sensor_neutral_voltage,
"max_length": actuator_max_length,
"noise_settings": {
"static_error_band": 0.15, # % of FSO
"non-linearity": 0.15, # % of FSO
"hysteresis": 0.07, # % of FSO
"repeatability": 0.02, # % of FSO
"thermal_error": 0.005, # % of reading per degree F
"temperature_offset": 10 # Degrees F off from calibration temperature
}
})
actuator_disp_sensor_shared:SharedDisplacementSensor = manager.SharedDisplacementSensor()
actuator_disp_sensor.attach_shared_attributes(actuator_disp_sensor_shared)
sensor_plotter.attach_shared_attributes(actuator_disp_sensor_shared, plot_settings={
"length": {
"scalar": {
"subplot": 1,
"ylabel": f"{name} Length",
"color": color
}
},
"voltage": {
"noisy": {
"subplot": 2,
"ylabel": f"{name} Noisy-Voltage",
"color": color
}
},
})
hitl_controller.attach_displacement_sensor(actuator_disp_sensor)
actuator_controller = RigidActuatorController(controller_settings={
"name": "Main Actuator Controller",
"analog_input_channel": i,
"digital_input_channel": i,
"units_per_volt": actuator_controller_mps_per_volt, # How far the actuator moves per input volt (m/s or m/s/s if controlling vel or accel)
"neutral_length": actuator_neutral_length, # Length at neutral voltage
"neutral_voltage": actuator_controller_neutral_voltage, # Voltage to go to neutral length
"controls_pos/vel/accel": 1, # 0 = controls pos, 1 = controls vel, 2 = controls accel,
"max_length": actuator_max_length,
"min_length": actuator_min_length
})
actuator_controller_shared_attributes = manager.SharedRigidActuatorController() # For plotting
actuator_controller.attach_shared_attributes(actuator_controller_shared_attributes)
actuator_controller.attach_sim_target(actuator_shared_attributes) # So the controller can update sim elements
hitl_controller.attach_rigid_actuator_controller(actuator_controller)
controller_plotter.attach_shared_attributes(actuator_controller_shared_attributes, plot_settings={
"input_voltage": {
"scalar": {
"subplot": 0,
"ylabel": f"{name} Controller Voltage",
"color": color
}
},
"digital_input": {
"scalar": {
"subplot": 1,
"ylabel": f"{name} Controller CTRL Input",
"color": color
}
},
"vel": {
"x": {
"subplot": 2,
"ylabel": f"{name} Controlled x-Vel",
"color": color
}
}
})
# PROCESSES #############################################################
# Make the simulation process
simulation = BFSSimulation(parent_joint=main_mass, settings={
"duration": max_run_time,
"delta_t": None,
"shared_update_period": 0.1, # not used
"plotting_update_period": 0.01,
"sensor_update_period": 0.01,
"controller_pull_period": 0.01
})
simulation.attach_shared_time(shared_sim_time)
simulation_process = multiprocessing.Process(target=simulation.run_process)
# Start the processes
# Start the visualization process
visualization_process = multiprocessing.Process(target=visualizer.run_process)
visualization_process.start()
# Start the physics_plotter process
physics_plotter_process = multiprocessing.Process(target=physics_plotter.run_process)
physics_plotter_process.start()
# Start the sensor physics_plotter process
sensor_plotter_process = multiprocessing.Process(target=sensor_plotter.run_process)
sensor_plotter_process.start()
# Start the sensor physics_plotter process
controller_plotter_process = multiprocessing.Process(target=controller_plotter.run_process)
controller_plotter_process.start()
# Start the HITL interface process
hitl_process = multiprocessing.Process(target=hitl_controller.run_process)
hitl_process.start()
time.sleep(5)
simulation_process.start()
# Join the processes
simulation_process.join()
stop_event.set()
visualization_process.join()
physics_plotter_process.join()
sensor_plotter_process.join()
controller_plotter_process.join()
hitl_process.join()
# Close the manager
manager.shutdown()
def main():
max_run_time = 10000
#multiprocessed_double_mass_spring(100)
#single_spring(max_run_time)
stage_setup_1(max_run_time)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,112 @@
import matplotlib.pyplot as plt
from copy import deepcopy
class StateSpace():
def __init__(self, pos, vel, accel):
self.pos = pos
self.vel = vel
self.accel = accel
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
class Block():
def __init__(self):
self.mass = 5
self.statespace = StateSpace(5,0,0)
self.net_force = 0
class Spring():
def __init__(self):
self.mass = 1
self.pos1 = 0
self.pos2 = 10
self.vel = 0
self.accel = 0
self.length = self.pos2 - self.pos1
self.zero_length = 10
self.k = 10
self.del_l = self.length - self.zero_length
self.force = self.del_l * self.k
def get_force(self):
self.length = self.pos2 - self.pos1
self.del_l = self.length - self.zero_length
self.force = self.del_l * self.k
return self.force
applied_force = 0
block = Block()
spring = Spring()
# Initialize lists to store data for plotting
t_values = []
pos_values = []
vel_values = []
accel_values = []
force_net_values = []
spring_force_values = []
del_l_values = []
del_t = 0.1
t = 0
while t < 10:
spring.pos1 = block.statespace.pos
spring.force = spring.get_force()
block.net_force = applied_force + spring.force
block.statespace.accel = block.net_force / block.mass
block.statespace.vel += block.statespace.accel * del_t
block.statespace.pos += block.statespace.vel * del_t
# Store data for plotting
t_values.append(t)
pos_values.append(block.statespace.pos)
vel_values.append(block.statespace.vel)
accel_values.append(block.statespace.accel)
force_net_values.append(block.net_force)
spring_force_values.append(spring.force)
del_l_values.append(spring.del_l)
t += del_t
print(max(pos_values))
# Plot the data
plt.figure(figsize=(12, 8))
plt.subplot(3, 1, 1)
plt.plot(t_values, pos_values)
plt.plot(t_values, del_l_values)
plt.title('Position vs Time')
plt.legend(['Position', 'Delta Length'])
plt.xlabel('Time (s)')
plt.ylabel('Position')
plt.subplot(3, 1, 2)
plt.plot(t_values, vel_values)
plt.title('Velocity vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Velocity')
plt.subplot(3, 1, 3)
plt.plot(t_values, accel_values)
plt.plot(t_values, force_net_values)
plt.plot(t_values, spring_force_values)
plt.legend(['Acceleration', 'Net Force', 'Spring Force'])
plt.title('Acceleration, Net Force, and Spring Force vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Value')
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,436 @@
import math
import numpy as np
from typing import List
import matplotlib.pyplot as plt
ZERO_VECTOR = np.array([0.0, 0.0, 0.0])
class StateSpace():
def __init__(self, pos=np.copy(ZERO_VECTOR), vel=np.copy(ZERO_VECTOR), accel=np.copy(ZERO_VECTOR), force=np.copy(ZERO_VECTOR)):
self.pos = pos
self.vel = vel
self.accel = accel
self.force = force
def get_pos(self):
return self.pos
def get_vel(self):
return self.vel
def get_accel(self):
return self.accel
def get_force(self):
return self.force
def set_pos(self, new_pos):
self.pos = new_pos
def set_vel(self, new_vel):
self.vel = new_vel
def set_accel(self, new_accel):
self.accel = new_accel
def set_force(self, new_force):
self.force = new_force
def integrate(self, delta_t):
self.vel = self.vel + self.accel * delta_t
self.pos = self.pos + self.vel * delta_t
def print(self):
print(f"Pos: {self.pos}, Vel: {self.vel}, Accel: {self.accel}")
class Joint():
def __init__(self, pos=np.copy(ZERO_VECTOR), fixed: bool=False):
self.statespace = StateSpace(pos, ZERO_VECTOR, ZERO_VECTOR, ZERO_VECTOR)
self.connected_springs: List[Spring] = []
self.connected_masses: List[Mass] = []
self.connected_rigid_bodies: List[RigidBody] = []
self.external_forces: List[List[float, float, float]] = []
self.fixed = fixed
self.parent_joints: List[Joint] = []
self.child_joints: List[Joint] = []
def get_pos(self):
return self.statespace.get_pos()
def get_vel(self):
return self.statespace.get_vel()
def get_accel(self):
return self.statespace.get_accel()
def get_force(self):
return self.statespace.get_force()
def get_connected_springs(self):
return self.connected_springs
def get_connected_masses(self):
return self.connected_masses
def get_connected_rigid_bodies(self):
return self.connected_rigid_bodies
def is_fixed(self):
return self.fixed
def add_spring(self, new_spring):
self.connected_springs.append(new_spring)
def add_mass(self, new_mass):
self.connected_masses.append(new_mass)
def add_rigid_body(self, new_rigid_body):
self.connected_rigid_bodies.append(new_rigid_body)
def add_extenal_force(self, new_force = ZERO_VECTOR):
self.external_forces.append(new_force)
def calc_net_spring_force(self):
net_spring_force = 0
for spring in self.connected_springs:
spring_force = spring.calc_spring_force() # This is the force the spring exerts on the joint ASSUMING self is parent_joint of the spring
net_spring_force += spring_force
return net_spring_force
def calc_net_external_force(self):
net_external_force = np.copy(ZERO_VECTOR)
for force in self.external_forces:
net_external_force += force
return net_external_force
def calc_net_force(self):
net_external_force = self.calc_net_external_force()
net_spring_force = self.calc_net_spring_force()
self.statespace.force = net_external_force + net_spring_force
return self.statespace.force
def integrate_accel_vel(self, delta_t: float=0.1):
self.statespace.integrate(delta_t)
def set_accel(self, accel):
self.statespace.set_accel(accel)
def find_parent_joints(self):
for spring in self.connected_springs:
if spring.child_joint == self:
if spring.parent_joint not in self.parent_joints:
self.parent_joints.append(spring.parent_joint)
for mass in self.connected_masses:
if mass.child_joint == self:
if mass.parent_joint not in self.parent_joints:
self.parent_joints.append(mass.parent_joint)
def find_child_joints(self):
for spring in self.connected_springs:
if spring.parent_joint == self:
if spring.child_joint not in self.child_joints:
self.child_joints.append(spring.child_joint)
for mass in self.connected_masses:
if mass.parent_joint == self:
if mass.child_joint not in self.child_joints:
self.child_joints.append(mass.child_joint)
class Spring():
def __init__(self, parent_joint: Joint, child_joint: Joint, zero_length: float=0, stiffness: float=0):
self.parent_joint = parent_joint
self.child_joint = child_joint
self.zero_length = zero_length
self.stiffness = stiffness
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector = self.calc_r_unit_vector()
self.spring_force = self.calc_spring_force()
self.parent_joint.add_spring(self)
self.child_joint.add_spring(self)
def calc_r_vector(self):
self.vector = self.child_joint.get_pos() - self.parent_joint.get_pos()
return self.vector
def calc_length(self):
self.vector = self.calc_r_vector()
return np.linalg.norm(self.vector)
def calc_r_unit_vector(self):
self.vector = self.calc_r_vector()
self.length = self.calc_length()
self.unit_vector = self.vector / self.length
return self.unit_vector
def calc_spring_force(self):
'''Positive force is in tension. Aka, the spring PULLS on the other object'''
self.length = self.calc_length()
del_length = self.length - self.zero_length
spring_force = del_length * self.stiffness
self.r_unit_vector = self.calc_r_unit_vector()
self.spring_force = spring_force * self.r_unit_vector
return self.spring_force
class Mass():
'''
A mass is a point mass located in the center of 2 joints.
It cannot exert a force
'''
def __init__(self, parent_joint: Joint, child_joint: Joint, mass: float=0):
self.parent_joint = parent_joint
self.child_joint = child_joint
self.mass = mass
self.parent_joint.add_mass(self)
self.child_joint.add_mass(self)
self.statespace = StateSpace((child_joint.get_pos() + parent_joint.get_pos()) / 2.0,
(child_joint.get_vel() + parent_joint.get_vel()) / 2.0,
(child_joint.get_accel() + parent_joint.get_accel()) / 2.0,
(child_joint.get_force() + parent_joint.get_force()) / 2.0)
def set_accel(self, accel):
self.statespace.set_accel(accel)
def integrate_accel_vel(self, delta_t: float=0.1):
self.statespace.integrate(delta_t)
class Simulation():
def __init__(self, parent_joint: Joint, duration: float, delta_t: float):
self.parent_joint = parent_joint
self.duration = duration
self.delta_t = delta_t
self.joints: List[Joint] = []
self.masses: List[Mass] = []
self.springs: List[Spring] = []
self.rigid_bodies: List[RigidBody] = []
def get_all_nodes_and_edges(self):
'''
Do a BFS to get all of the joints (nodes) and springs/masses (edges) in the system
'''
queue: List[Joint] = []
queue.append(self.parent_joint)
while queue:
node: Joint
node = queue.pop(0)
if node not in self.joints:
self.joints.append(node)
connected_springs = node.get_connected_springs()
connected_masses = node.get_connected_masses()
connected_rigid_bodies = node.get_connected_rigid_bodies()
for spring in connected_springs:
if spring not in self.springs:
self.springs.append(spring)
if spring.child_joint not in self.joints and spring.child_joint not in queue:
queue.append(spring.child_joint)
for mass in connected_masses:
if mass not in self.masses:
self.masses.append(mass)
if mass.child_joint not in self.joints and mass.child_joint not in queue:
queue.append(mass.child_joint)
for rigid_body in connected_rigid_bodies:
if rigid_body not in self.rigid_bodies:
self.rigid_bodies.append(rigid_body)
if rigid_body.child_joint not in self.joints and rigid_body.child_joint not in queue:
queue.append(rigid_body.child_joint)
def print_components(self):
print("Joints:")
count = 0
for joint in self.joints:
print(f"Accel: {joint.get_accel()}")
print(f"Vel: {joint.get_vel()}")
print(f"Pos: {joint.get_pos()}")
print()
print("Masses:")
count = 0
for mass in self.masses:
print(f"Accel: {mass.statespace.get_accel()}")
print(f"Vel: {mass.statespace.get_vel()}")
print(f"Pos: {mass.statespace.get_pos()}")
print()
print("Springs:")
count = 0
for spring in self.springs:
print(f"Spring Force: {spring.spring_force}")
print(f"Spring Length: {spring.length}")
print()
print("Rigid Bodies:")
count = 0
for rigid_body in self.rigid_bodies:
print(f"Transfer Force: {rigid_body.force}")
print()
def calc_force_at_every_joint(self):
'''
At every joint, calculate the net force at each joint (this accounts for springs and external forces).
'''
for joint in self.joints:
joint.calc_net_force()
def calc_rigid_body_force(self):
for body in self.rigid_bodies:
body.force = 0
def calc_accel_at_every_mass(self):
'''
Using the sum of the forces at the 2 joints on each mass, we calc the accel of the mass
'''
for mass in self.masses:
net_force = mass.parent_joint.get_force() + mass.child_joint.get_force()
accel = net_force / mass.mass
mass.set_accel(accel)
def assign_joint_accel(self):
'''
If the joint is fixed, accel = 0
'''
net_joint_accel = np.copy(ZERO_VECTOR)
for joint in self.joints:
if joint.is_fixed() == True:
continue
for mass in joint.get_connected_masses():
net_joint_accel += mass.statespace.get_accel()
joint.set_accel(net_joint_accel)
def integrate_timestep(self):
self.calc_force_at_every_joint()
self.calc_accel_at_every_mass()
self.assign_joint_accel()
for joint in self.joints:
joint.integrate_accel_vel(self.delta_t)
for mass in self.masses:
mass.integrate_accel_vel(self.delta_t)
def run(self):
mass_pos_values = []
time_values = []
t = 0
while t < self.duration:
self.integrate_timestep()
self.print_components()
mass_pos_values.append(self.masses[0].statespace.get_pos())
time_values.append(t)
print("*"*100)
t += self.delta_t
plt.close()
# Plot the data
plt.figure(figsize=(12, 8))
plt.plot(time_values, mass_pos_values)
plt.title('Position vs Time')
plt.xlabel('Time (s)')
plt.ylabel('Position')
plt.show()
class BFSSimulation():
def __init__(self, parent_joint: Joint, duration: float, delta_t: float):
self.parent_joint = parent_joint
self.duration = duration
self.delta_t = delta_t
self.joints: List[Joint] = []
self.get_joints_bfs()
self.get_parent_joints_for_every_joint()
def get_joints_bfs(self):
queue: List[Joint] = []
queue.append(self.parent_joint)
visited_springs: List[Spring] = []
visited_masses: List[Spring] = []
while queue:
node: Joint
node = queue.pop(0)
if node not in self.joints:
self.joints.append(node)
connected_springs = node.get_connected_springs()
connected_masses = node.get_connected_masses()
for spring in connected_springs:
if spring not in visited_springs:
visited_springs.append(spring)
if spring.child_joint not in self.joints and spring.child_joint not in queue:
queue.append(spring.child_joint)
for mass in connected_masses:
if mass not in visited_masses:
visited_masses.append(mass)
if mass.child_joint not in self.joints and mass.child_joint not in queue:
queue.append(mass.child_joint)
def get_parent_joints_for_every_joint(self):
for joint in self.joints:
joint.find_parent_joints()
def get_child_joints_for_every_joint(self):
for joint in self.joints:
joint.find_child_joints()
def integrate_joints(self):
for joint in self.joints:
joint.calc_net_force()
def main():
joint_left_mass1 = Joint(np.array([0,0,0]), fixed=False)
joint_mass1_spring1 = Joint(np.array([0,0,0]), fixed=False)
joint_spring1_rigidbody1 = Joint(np.array([10,0,0]), fixed=False)
joint_rigidbody1_spring2 = Joint(np.array([10,0,0]), fixed=False)
joint_right_spring2 = Joint(np.array([20,0,0]), fixed=True)
mass1 = Mass(joint_left_mass1, joint_mass1_spring1, 5)
spring1 = Spring(joint_mass1_spring1, joint_spring1_rigidbody1, 10, 10)
#rigidbody1 = RigidBody(joint_spring1_rigidbody1, joint_rigidbody1_spring2, 0.0)
spring2 = Spring(joint_rigidbody1_spring2, joint_right_spring2, 10, 100)
joint_left_mass1.add_extenal_force(np.array([5, 0, 0]))
simulation = BFSSimulation(joint_left_mass1, 1, 0.1)
simulation.get_all_nodes_and_edges()
simulation.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,30 @@
import pickle
from PhysicsElements import StateSpace, ZERO_VECTOR, Joint, Spring
import numpy as np
from Object_Sharing import SharedJointList
def is_picklable(obj):
try:
pickle.dumps(obj)
return True
except pickle.PicklingError:
return False
from multiprocessing.managers import BaseManager
def func(x,y):
return 10
if __name__ == '__main__':
BaseManager.register('Joint', Joint)
BaseManager.register('Spring', Spring)
manager = BaseManager()
manager.start()
north_wall_joint:Joint = manager.Joint(np.array([10,0,0]), mass=0.001, fixed=True, name="North Wall Joint")
main_mass:Joint = manager.Joint(np.array([0,0,0]), mass=3, fixed=False, name="Blue Mass")
middle_mass:Joint = manager.Joint(np.array([5,0,0]), mass=5, fixed=False, name="White Mass")
spring1 = manager.Spring(parent_joint=middle_mass, child_joint=north_wall_joint, unstretched_length=7.5, stiffness_func=func, name="Spring 1")
print(is_picklable(spring1))

View File

@ -0,0 +1,111 @@
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import random
from multiprocessing import Process, Manager
class LivePlot:
def __init__(self, mult=1, max_data_points=100):
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(8, 6))
self.fig = fig
self.ax1 = ax1
self.ax2 = ax2
self.ax3 = ax3
self.mult = mult
self.ax1.set_ylabel('Plot 1')
self.ax2.set_ylabel('Plot 2')
self.ax3.set_ylabel('Plot 3')
self.ax3.set_xlabel('Time')
self.max_data_points = max_data_points
self.t = 0
self.x_data = []
self.y_data1 = []
self.y_data1_2 = []
self.y_data2 = []
self.y_data3 = []
# Initialize lines (empty initially)
self.line1, = self.ax1.plot([], [], label='Plot 1')
self.line1_2, = self.ax1.plot([], [], label='Plot 1.2')
self.line2, = self.ax2.plot([], [], label='Plot 2')
self.line3, = self.ax3.plot([], [], label='Plot 3')
self.ax1.legend()
self.ax2.legend()
self.ax3.legend()
def generate_random_data(self):
return random.randint(1, 100)
def update_data_external(self):
# Simulate external updates (replace with your actual data update mechanism)
new_x = self.t
self.t += 1
new_y1 = self.generate_random_data() * self.mult
new_y2 = self.generate_random_data() * self.mult
new_y3 = self.generate_random_data() * self.mult
self.x_data.append(new_x)
self.y_data1.append(new_y1)
self.y_data1_2.append(new_y1 * -1) # Example modification of data
self.y_data2.append(new_y2)
self.y_data3.append(new_y3)
# Keep only the last `max_data_points` data points
self.x_data = self.x_data[-self.max_data_points:]
self.y_data1 = self.y_data1[-self.max_data_points:]
self.y_data1_2 = self.y_data1_2[-self.max_data_points:]
self.y_data2 = self.y_data2[-self.max_data_points:]
self.y_data3 = self.y_data3[-self.max_data_points:]
def update_plot(self, i):
self.update_data_external()
# Update plot data
self.line1.set_data(self.x_data, self.y_data1)
self.line1_2.set_data(self.x_data, self.y_data1_2)
self.line2.set_data(self.x_data, self.y_data2)
self.line3.set_data(self.x_data, self.y_data3)
# Adjust plot limits (x-axis)
if len(self.x_data) > 1:
self.ax1.set_xlim(self.x_data[0], self.x_data[-1])
self.ax2.set_xlim(self.x_data[0], self.x_data[-1])
self.ax3.set_xlim(self.x_data[0], self.x_data[-1])
# Adjust plot limits (y-axis) - optional
self.ax1.relim()
self.ax1.autoscale_view()
self.ax2.relim()
self.ax2.autoscale_view()
self.ax3.relim()
self.ax3.autoscale_view()
#return self.line1, self.line2, self.line3
def animate(self):
anim = animation.FuncAnimation(self.fig, self.update_plot, frames=1000, interval=10, blit=False)
plt.show()
#anim.save(filename="test.mp4")
# Function to run animation in a separate process
def run_animation(fig, ax1, ax2, ax3):
#live_plot = LivePlot(fig, ax1, ax2, ax3)
#live_plot.animate()
pass
# Example usage with multiprocessing
if __name__ == '__main__':
plot1 = LivePlot(mult=1)
plot2 = LivePlot(mult=5)
process1 = Process(target=plot1.animate)
process2 = Process(target=plot2.animate)
process1.start()
process2.start()
process1.join()
process2.join()

View File

@ -0,0 +1,59 @@
from mcculw import ul
from mcculw.enums import DigitalIODirection
from mcculw.ul import ULError
# Define the board number
BOARD_NUM = 0
# Define the ports and the direction of data flow
PORT_A = 10
PORT_B = 11
PORT_C_LOW = 12
PORT_C_HIGH = 13
# Configure ports as input or output
def configure_ports():
try:
ul.d_config_port(BOARD_NUM, PORT_A, DigitalIODirection.OUT)
ul.d_config_port(BOARD_NUM, PORT_B, DigitalIODirection.IN)
ul.d_config_port(BOARD_NUM, PORT_C_LOW, DigitalIODirection.OUT)
ul.d_config_port(BOARD_NUM, PORT_C_HIGH, DigitalIODirection.IN)
print("Ports configured successfully.")
except ULError as e:
print(f"Error configuring ports: {e}")
# Write data to a port
def write_data(port, data):
try:
ul.d_out(BOARD_NUM, port, data)
print(f"Data {data} written to port {port} successfully.")
except ULError as e:
print(f"Error writing data to port {port}: {e}")
# Read data from a port
def read_data(port):
try:
data = ul.d_in(BOARD_NUM, port)
print(f"Data read from port {port}: {data}")
return data
except ULError as e:
print(f"Error reading data from port {port}: {e}")
return None
def main():
# Configure the ports
configure_ports()
# Write some data to PORT_A and PORT_C_LOW
write_data(PORT_A, 0xFF) # Write 0xFF (255) to PORT_A
write_data(PORT_C_LOW, 0xAA) # Write 0xAA (170) to PORT_C_LOW
# Read data from PORT_B and PORT_C_HIGH
data_b = read_data(PORT_B)
data_c_high = read_data(PORT_C_HIGH)
# Perform additional processing as needed
# For example, you might want to perform some logic based on the input data
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
import cProfile
import pstats
# Load the profiling data from the file
stats = pstats.Stats('profile_results.txt')
# Print the top 10 functions by cumulative time
stats.sort_stats('cumulative').print_stats(25)

View File

@ -0,0 +1,78 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore
import numpy as np
class RealTimePlot:
def __init__(self, update_interval=1, max_display_time=10, num_subplots=2, line_styles=None):
# Create the application
self.app = QtWidgets.QApplication([])
# Create a plot window
self.win = pg.GraphicsLayoutWidget(show=True, title="Real-Time Plot")
self.win.resize(1000, 600)
# Add plots
self.plots = []
self.curves = []
self.line_styles = line_styles if line_styles is not None else ['line'] * num_subplots
for i in range(num_subplots):
plot = self.win.addPlot(title=f"Subplot {i+1}")
plot.setLabel('left', f'Subplot {i+1} Y-Axis')
plot.addLegend() # Add a legend to each plot
self.plots.append(plot)
if self.line_styles[i] == 'step':
curve = plot.plot(pen='y', name=f'Subplot {i+1} Data', stepMode=True)
else:
curve = plot.plot(pen='y', name=f'Subplot {i+1} Data')
self.curves.append(curve)
if i < num_subplots - 1:
self.win.nextRow()
# Data buffers
self.xdata = [np.empty(0) for _ in range(num_subplots)]
self.ydata = [np.empty(0) for _ in range(num_subplots)]
# Parameters
self.update_interval = update_interval
self.max_display_time = max_display_time
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_live_window)
self.timer.start(update_interval)
def update_live_window(self):
for i in range(len(self.plots)):
# Generate new data
t = self.xdata[i][-1] + self.update_interval / 1000.0 if len(self.xdata[i]) > 0 else 0
y = np.sin(2 * np.pi * t + i) # Different phase for each subplot
# Append new data to buffers
self.xdata[i] = np.append(self.xdata[i], t)
self.ydata[i] = np.append(self.ydata[i], y)
# Remove old data to keep the buffer size within max_display_time
if t > self.max_display_time:
self.xdata[i] = self.xdata[i][self.xdata[i] > t - self.max_display_time]
self.ydata[i] = self.ydata[i][-len(self.xdata[i]):]
# Ensure correct lengths for step mode
if self.line_styles[i] == 'step':
xdata_step = np.append(self.xdata[i], self.xdata[i][-1] + self.update_interval / 1000.0)
self.curves[i].setData(xdata_step, self.ydata[i])
else:
self.curves[i].setData(self.xdata[i], self.ydata[i])
def run(self):
# Start the Qt event loop
QtWidgets.QApplication.instance().exec_()
# Parameters
update_interval = 100 # milliseconds
max_display_time = 10 # seconds
num_subplots = 2 # number of subplots
line_styles = ['line', 'step'] # specify 'line' or 'step' for each subplot
# Create and run the real-time plot
rt_plot = RealTimePlot(update_interval, max_display_time, num_subplots, line_styles)
rt_plot.run()

View File

@ -0,0 +1,99 @@
import multiprocessing
from multiprocessing.managers import BaseManager
import numpy as np
import time
import random
from PhysicsElements import Joint, Spring, SpringWithMass, Actuator, StateSpace
class CustomManager(BaseManager):
# nothing
pass
def worker_process(shared_state_space):
while True:
print("Worker Process:")
shared_state_space.print()
time.sleep(0.1)
def worker_process2(shared_joint: Joint):
while True:
print("Worker Process:")
statespace = shared_joint.get_state_space()
print(statespace.print())
time.sleep(0.1)
def worker_process3(shared_joint_list: list):
while True:
print("Worker Process:")
statespace:StateSpace = shared_joint_list.at(0).get_state_space()
statespace.print()
time.sleep(0.1)
def statespace_changer(shared_joint_list: list):
while True:
rand_int = random.randint(0, 10)
shared_joint_list.at(0).set_state_space_pos(np.array([rand_int,0,0]))
print("Changed pos")
time.sleep(1)
class SharedJointList():
def __init__(self):
self.list: list = []
def append(self, new_joint: Joint):
self.list.append(new_joint)
def at(self, index: int):
return self.list[index]
if __name__ == "__main__":
# Create a multiprocessing manager
# CustomManager.register('StateSpace', StateSpace)
# CustomManager.register('Joint', Joint)
# CustomManager.register('SharedJointList', SharedJointList)
#BaseManager.register('StateSpace', StateSpace)
BaseManager.register('Joint', Joint)
BaseManager.register('SharedJointList', SharedJointList)
with BaseManager() as manager:
#shared_statespace = manager.StateSpace()
shared_joint:Joint = manager.Joint(pos=np.array([0,0,-4]), mass=0.001, fixed=True, name="North Actuator Joint")
joint_list:SharedJointList = manager.SharedJointList()
rand_joint = manager.Joint(pos=np.array([0,2,15]),
mass=0.001,
fixed=True,
name="Random Joint")
joint_list.append(rand_joint)
#proc = multiprocessing.Process(target=worker_process, args=(shared_statespace,))
#proc = multiprocessing.Process(target=worker_process2, args=(shared_joint,))
proc = multiprocessing.Process(target=worker_process3, args=(joint_list,))
proc.start()
changer_proc = multiprocessing.Process(target=statespace_changer, args=(joint_list,))
changer_proc.start()
time.sleep(2)
#shared_statespace.set_pos(np.array([-8,-4,-2]))
#shared_joint.set_state_space_pos((np.array([-8,-4,-2])))
while True:
print("Main: ", end="")
joint_list.at(0).get_state_space().print()
time.sleep(1)
proc.join()
changer_proc.join()

View File

@ -0,0 +1,3 @@
{
"python.REPL.enableREPLSmartSend": false
}

View File

@ -0,0 +1,23 @@
import subprocess
import sys
def install_packages(packages):
for package in packages:
print(f"Installing {package}...")
try:
result = subprocess.run([sys.executable, "-m", "pip", "install", package], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(result.stdout)
if result.stderr:
print(result.stderr)
except subprocess.CalledProcessError as e:
print(f"Failed to install {package}: {e}")
sys.exit(1)
if __name__ == "__main__":
packages = ['vpython', 'numpy', 'typing', 'pyqtgraph', 'pyqt5' , 'mcculw']
source_dir = "./mcculw_files" # Relative path to the local directory
dest_dir = "C:/absolute/path/to/directory" # Absolute path to the destination directory
install_packages(packages)
print("Installation and file copy complete.")

Binary file not shown.

View File

@ -0,0 +1,29 @@
import numpy as np
num_points = 7
num_columns = num_points + 1
RHS = np.zeros(num_columns)
RHS[2] = 1
taylor_coef_matrix = np.zeros((num_columns, num_columns))
bases = [j for j in range(-num_points+1, 2)]
for i in range(num_columns):
row = []
for base in bases:
row.append(base**i)
taylor_coef_matrix[i] = row
x = np.linalg.solve(taylor_coef_matrix, RHS)
multiplier = 1 / x[-1]
x = x[0:-1]
x = -1*x
x = np.append(x, [0.5])
x = x * multiplier
print(x)

View File

@ -0,0 +1,23 @@
import subprocess
import sys
def install_packages(packages):
for package in packages:
print(f"Installing {package}...")
try:
result = subprocess.run([sys.executable, "-m", "pip", "install", package], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(result.stdout)
if result.stderr:
print(result.stderr)
except subprocess.CalledProcessError as e:
print(f"Failed to install {package}: {e}")
sys.exit(1)
if __name__ == "__main__":
packages = ['vpython', 'numpy', 'typing', 'pyqtgraph', 'pyqt5' , 'mcculw']
source_dir = "./mcculw_files" # Relative path to the local directory
dest_dir = "C:/absolute/path/to/directory" # Absolute path to the destination directory
install_packages(packages)
print("Installation and file copy complete.")

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,149 @@
<mxfile host="Electron" modified="2024-06-11T19:31:46.378Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.0.4 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="YiW67u71xuPxX2CrrYUi" version="24.0.4" type="device">
<diagram name="Page-1" id="e7e014a7-5840-1c2e-5031-d8a46d1fe8dd">
<mxGraphModel dx="2074" dy="1204" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2" value="Thread 1" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="164.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="5" value="" style="ellipse;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="2" vertex="1">
<mxGeometry x="100" y="40" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="6" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="5" target="7" edge="1">
<mxGeometry x="100" y="40" as="geometry">
<mxPoint x="115" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="7" value="idle" style="" parent="2" vertex="1">
<mxGeometry x="60" y="110" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="8" value="user action" style="" parent="2" vertex="1">
<mxGeometry x="60" y="220" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="9" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="7" target="8" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="10" value="post command" style="" parent="2" vertex="1">
<mxGeometry x="60" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="11" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="8" target="10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="2" source="10" target="7" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="160" y="290" as="sourcePoint" />
<mxPoint x="260" y="190" as="targetPoint" />
<Array as="points">
<mxPoint x="30" y="250" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="Thread 2" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="444.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="13" value="" style="ellipse;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="3" vertex="1">
<mxGeometry x="60" y="40" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="14" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="13" target="15" edge="1">
<mxGeometry x="40" y="20" as="geometry">
<mxPoint x="55" y="90" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="15" value="idle" style="" parent="3" vertex="1">
<mxGeometry x="20" y="110" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="16" value="check for &#xa;new commands" style="" parent="3" vertex="1">
<mxGeometry x="20" y="220" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="17" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="15" target="16" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="18" value="command queue" style="" parent="3" vertex="1">
<mxGeometry x="20" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="19" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="16" target="18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="21" value="queue empty" style="rhombus;fillColor=#ffffc0;strokeColor=#ff0000;" parent="3" vertex="1">
<mxGeometry x="150" y="225" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="22" value="yes" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;align=left;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;exitX=0.5;exitY=0;endFill=1;rounded=0;entryX=0.75;entryY=0.5;entryPerimeter=0" parent="3" source="21" target="25" edge="1">
<mxGeometry x="-1" relative="1" as="geometry">
<mxPoint x="160" y="150" as="targetPoint" />
<Array as="points">
<mxPoint x="190" y="180" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="23" value="no" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;align=left;verticalAlign=top;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="21" target="30" edge="1">
<mxGeometry x="-1" relative="1" as="geometry">
<mxPoint x="190" y="305" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="16" target="21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="25" value="" style="shape=line;strokeWidth=6;strokeColor=#ff0000;rotation=90" parent="3" vertex="1">
<mxGeometry x="130" y="127.5" width="50" height="15" as="geometry" />
</mxCell>
<mxCell id="26" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="25" target="15" edge="1">
<mxGeometry x="130" y="90" as="geometry">
<mxPoint x="230" y="140" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="30" value="dispatch&#xa;command&#xa;worker thread" style="" parent="3" vertex="1">
<mxGeometry x="140" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="31" value="critical&#xa;section" style="shape=note;whiteSpace=wrap;size=17" parent="3" vertex="1">
<mxGeometry x="105" y="490" width="100" height="50" as="geometry" />
</mxCell>
<mxCell id="32" value="" style="endArrow=none;strokeColor=#FF0000;endFill=0;rounded=0;dashed=1" parent="3" source="18" target="31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="34" value="" style="whiteSpace=wrap;strokeColor=#FF0000;fillColor=#FF0000" parent="3" vertex="1">
<mxGeometry x="245" y="395" width="5" height="45" as="geometry" />
</mxCell>
<mxCell id="41" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;entryX=0;entryY=0.5;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="3" source="30" target="34" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="60" y="480" as="sourcePoint" />
<mxPoint x="160" y="380" as="targetPoint" />
<Array as="points">
<mxPoint x="195" y="400" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="4" value="Thread 3" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="724.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="33" value="process&#xa;command" style="" parent="4" vertex="1">
<mxGeometry x="90" y="405" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="35" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0;entryX=0.25;entryY=0.5;entryPerimeter=0" parent="4" target="25" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-30" y="410" as="sourcePoint" />
<mxPoint x="-120" y="120" as="targetPoint" />
<Array as="points">
<mxPoint x="-10" y="135" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="37" value="" style="edgeStyle=none;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="4" target="33" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-30" y="429.5" as="sourcePoint" />
<mxPoint x="90" y="429.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="38" value="" style="ellipse;shape=endState;fillColor=#000000;strokeColor=#ff0000" parent="4" vertex="1">
<mxGeometry x="130" y="500" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="39" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="4" source="33" target="38" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="20" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="1" source="10" target="18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
code/setup/icalsetup.exe Normal file

Binary file not shown.

View File

@ -0,0 +1,29 @@
import numpy as np
num_points = 7
num_columns = num_points + 1
RHS = np.zeros(num_columns)
RHS[2] = 1
taylor_coef_matrix = np.zeros((num_columns, num_columns))
bases = [j for j in range(-num_points+1, 2)]
for i in range(num_columns):
row = []
for base in bases:
row.append(base**i)
taylor_coef_matrix[i] = row
x = np.linalg.solve(taylor_coef_matrix, RHS)
multiplier = 1 / x[-1]
x = x[0:-1]
x = -1*x
x = np.append(x, [0.5])
x = x * multiplier
print(x)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,149 @@
<mxfile host="Electron" modified="2024-06-11T19:31:46.378Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.0.4 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="YiW67u71xuPxX2CrrYUi" version="24.0.4" type="device">
<diagram name="Page-1" id="e7e014a7-5840-1c2e-5031-d8a46d1fe8dd">
<mxGraphModel dx="2074" dy="1204" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2" value="Thread 1" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="164.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="5" value="" style="ellipse;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="2" vertex="1">
<mxGeometry x="100" y="40" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="6" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="5" target="7" edge="1">
<mxGeometry x="100" y="40" as="geometry">
<mxPoint x="115" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="7" value="idle" style="" parent="2" vertex="1">
<mxGeometry x="60" y="110" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="8" value="user action" style="" parent="2" vertex="1">
<mxGeometry x="60" y="220" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="9" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="7" target="8" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="10" value="post command" style="" parent="2" vertex="1">
<mxGeometry x="60" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="11" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="2" source="8" target="10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="2" source="10" target="7" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="160" y="290" as="sourcePoint" />
<mxPoint x="260" y="190" as="targetPoint" />
<Array as="points">
<mxPoint x="30" y="250" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="Thread 2" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="444.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="13" value="" style="ellipse;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="3" vertex="1">
<mxGeometry x="60" y="40" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="14" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="13" target="15" edge="1">
<mxGeometry x="40" y="20" as="geometry">
<mxPoint x="55" y="90" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="15" value="idle" style="" parent="3" vertex="1">
<mxGeometry x="20" y="110" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="16" value="check for &#xa;new commands" style="" parent="3" vertex="1">
<mxGeometry x="20" y="220" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="17" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="15" target="16" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="18" value="command queue" style="" parent="3" vertex="1">
<mxGeometry x="20" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="19" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="16" target="18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="21" value="queue empty" style="rhombus;fillColor=#ffffc0;strokeColor=#ff0000;" parent="3" vertex="1">
<mxGeometry x="150" y="225" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="22" value="yes" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;align=left;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;exitX=0.5;exitY=0;endFill=1;rounded=0;entryX=0.75;entryY=0.5;entryPerimeter=0" parent="3" source="21" target="25" edge="1">
<mxGeometry x="-1" relative="1" as="geometry">
<mxPoint x="160" y="150" as="targetPoint" />
<Array as="points">
<mxPoint x="190" y="180" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="23" value="no" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;align=left;verticalAlign=top;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="21" target="30" edge="1">
<mxGeometry x="-1" relative="1" as="geometry">
<mxPoint x="190" y="305" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="16" target="21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="25" value="" style="shape=line;strokeWidth=6;strokeColor=#ff0000;rotation=90" parent="3" vertex="1">
<mxGeometry x="130" y="127.5" width="50" height="15" as="geometry" />
</mxCell>
<mxCell id="26" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#FF0000;endFill=1;rounded=0" parent="3" source="25" target="15" edge="1">
<mxGeometry x="130" y="90" as="geometry">
<mxPoint x="230" y="140" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="30" value="dispatch&#xa;command&#xa;worker thread" style="" parent="3" vertex="1">
<mxGeometry x="140" y="325" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="31" value="critical&#xa;section" style="shape=note;whiteSpace=wrap;size=17" parent="3" vertex="1">
<mxGeometry x="105" y="490" width="100" height="50" as="geometry" />
</mxCell>
<mxCell id="32" value="" style="endArrow=none;strokeColor=#FF0000;endFill=0;rounded=0;dashed=1" parent="3" source="18" target="31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="34" value="" style="whiteSpace=wrap;strokeColor=#FF0000;fillColor=#FF0000" parent="3" vertex="1">
<mxGeometry x="245" y="395" width="5" height="45" as="geometry" />
</mxCell>
<mxCell id="41" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;entryX=0;entryY=0.5;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="3" source="30" target="34" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="60" y="480" as="sourcePoint" />
<mxPoint x="160" y="380" as="targetPoint" />
<Array as="points">
<mxPoint x="195" y="400" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="4" value="Thread 3" style="swimlane;whiteSpace=wrap" parent="1" vertex="1">
<mxGeometry x="724.5" y="128" width="280" height="570" as="geometry" />
</mxCell>
<mxCell id="33" value="process&#xa;command" style="" parent="4" vertex="1">
<mxGeometry x="90" y="405" width="110" height="50" as="geometry" />
</mxCell>
<mxCell id="35" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0;entryX=0.25;entryY=0.5;entryPerimeter=0" parent="4" target="25" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-30" y="410" as="sourcePoint" />
<mxPoint x="-120" y="120" as="targetPoint" />
<Array as="points">
<mxPoint x="-10" y="135" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="37" value="" style="edgeStyle=none;strokeColor=#FF0000;endArrow=open;endFill=1;rounded=0" parent="4" target="33" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-30" y="429.5" as="sourcePoint" />
<mxPoint x="90" y="429.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="38" value="" style="ellipse;shape=endState;fillColor=#000000;strokeColor=#ff0000" parent="4" vertex="1">
<mxGeometry x="130" y="500" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="39" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="4" source="33" target="38" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="20" value="" style="endArrow=open;strokeColor=#FF0000;endFill=1;rounded=0" parent="1" source="10" target="18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB