© Copyright, 2025 G. Schaer.
SPDX-License-Identifier: GPL-3.0-only
Project Example 2: The Gyroscope
Set up the Visualizer and Physics Environment
Import necessary condynsate modules
[1]:
from condynsate import Project
from condynsate import __assets__ as assets
Create a instance of condynsate.Project that uses the visualizaer and keyboard but does use the animator.
[2]:
proj = Project(keyboard = True,
visualizer = True,
animator = False)
You can open the visualizer by visiting the following URL:
http://127.0.0.1:7000/static/
Load a medium plane into the simulator to represent the ground. Set fixed = True so it are fixed in space (0 degerees of freedom) and adjust give it a texture so it is pretty.
[3]:
# Load a plane with a carpet texture for the ground
ground = proj.load_urdf(assets['plane_medium.urdf'], fixed=True)
ground.links['plane'].set_texture(assets['carpet.png'])
Load the gyro into the simulator. Set fixed = True so that the base link of the gyro has 0 degrees of freedom.
[4]:
# Load and orient a 2 gimbal gyroscope.
gyro = proj.load_urdf(assets['gyro.urdf'], fixed=True)
Note that the gryo’s position update is not yet reflected in the visualizer. To update this manually, we can call the proj.refresh_visualizer function. Note, however, this is also done automatically every time proj.load_urdf, proj.reset, or proj.step is called. Therefore, calling this now is purely an aesthetic choice as it will be done automatically when the simulation starts running.
[5]:
proj.refresh_visualizer() # This returns 0 on success
[5]:
0
Now we will adjust the visualizer scene. First we turn off the axes and grid plane visualization.
[6]:
# Turn off the axes and grid visualization.
proj.visualizer.set_axes(False) # This returns 0 on success
proj.visualizer.set_grid(False) # This returns 0 on success
[6]:
0
Then we will move the camera’s position up just a slight amount from its default position and then tell it to look directly at the center of mass of the gyro.
[7]:
# Set the camera's position
proj.visualizer.set_cam_position((2.0, -4.0, 3.75)) # This returns 0 on success
# Focus the camera on the gyro
proj.visualizer.set_cam_target(gyro.center_of_mass) # This returns 0 on success
[7]:
0
Set the "base_to_ring3", "ring3_to_ring2", and "ring2_to_ring1" joint damping (rotational friction) to some small value. This helps with numerical stability. The "base_to_ring3" joint is the rotational joint between the base of the pendulum and the outer gimbal, "ring3_to_ring2" joint is the rotational joint between the outer gimbal and the middle gimbal, and "ring2_to_ring1" joint is the rotational joint between the middle gimbal and the inner gimbal.
[8]:
# Set joint damping
gyro.joints['base_to_ring3'].set_dynamics(damping=0.025) # This returns 0 on success
gyro.joints['ring3_to_ring2'].set_dynamics(damping=0.025) # This returns 0 on success
gyro.joints['ring2_to_ring1'].set_dynamics(damping=0.025) # This returns 0 on success
[8]:
0
Set the damping to 0 and limit the maximum magnitude of the angular velocity of the "ring1_to_core" joint to 10 rad/sec, then set the its initial angular velocity to to 5 rad/sec. The "ring1_to_core" joint is the rotational joint between the inner gimbal and the gyroscope’s wheel.
[9]:
gyro.joints['ring1_to_core'].set_dynamics(damping=0.0, # Set friction to 0
max_omega=10.0 # Limit maximum speed to 10 rad/sec
) # This returns 0 on success
gyro.joints['ring1_to_core'].set_initial_state(omega=5.0) # This returns 0 on success
[9]:
0
Define Functions Used in Simulation Loop
In this simulation, we want the user to be able to apply torques to each gimbal joint through keypresses. To enable this, we will create a function that listens for keypresses, converts them to gimbal joint torque commands, and sets those torque values
[10]:
def apply_torques(project, gryoscope):
# Determine what torques to apply based on key presses
tau0 = 0.0 # Torque applied to outermost ring
tau0 -= 0.5 * float(project.keyboard.is_pressed('q')) # Convert bool return value to float (0.0 or 1.0)
tau0 += 0.5 * float(project.keyboard.is_pressed('e'))
tau1 = 0.0 # Torque applied to the middle ring
tau1 -= 0.5 * float(project.keyboard.is_pressed('w'))
tau1 += 0.5 * float(project.keyboard.is_pressed('s'))
tau2 = 0.0 # Torque applied to the inner ring
tau2 -= 0.5 * float(project.keyboard.is_pressed('a'))
tau2 += 0.5 * float(project.keyboard.is_pressed('d'))
# Apply the torques
gryoscope.joints['base_to_ring3'].apply_torque(tau0, # Set the torque value
draw_arrow=True, # Draw an arrow to visualize the applied torque
arrow_scale=1.5, # Set the scaling of the arrow
arrow_offset=0.5, # Move the arrow away from the center of the
) # This returns 0 on success
gryoscope.joints['ring3_to_ring2'].apply_torque(tau1, draw_arrow=True, arrow_scale=1.5, arrow_offset=0.8,)
gryoscope.joints['ring2_to_ring1'].apply_torque(tau2, draw_arrow=True, arrow_scale=1.5, arrow_offset=0.5,)
Now we want to color the gyroscope’s wheel based on its rotation speed. To do so we define a function that
Reads the gyroscope’s wheel’s angular velocity
Changes the color of the wheel based on the velocity
[11]:
def set_color(gryoscope, max_omega=10.0):
# Get the angular velocity of the wheel in its body coordinates
omega = gryoscope.links['core'].state.omega_in_body
# In the case of the wheel, the +z body axis is the rotational axis, so isolate that
omega = omega[2]
# Get a color based on the rotation rate
r = min(max(omega / max_omega + 1., 0.), 1.)
g = 1. - abs(omega) / max_omega
b = min(max(1. - omega / max_omega, 0.), 1.)
# Set the color of the core link
gryoscope.links['core'].set_color((r, g, b)) # This returns 0 on success
Important Note: When we call set_color, this visualizer will not update. We must remember to either manually update the visualizer or call a functional that automatically updates the visualizer (proj.load_urdf, proj.reset, or proj.step) after we call set_color to see the color change
Finally, we would also like to allow the user the adjust the wheel joint speed based on keyboard inputs. We do this similarly as in the apply_torque.
[12]:
def set_omega(project, gryoscope):
# The amount by which to iterate the wheel speed
iter_val = 0.0
iter_val -= 0.1 * float(project.keyboard.is_pressed('f')) # Convert bool return value to float (0.0 or 1.0)
iter_val += 0.1 * float(project.keyboard.is_pressed('r'))
# Read the current wheel joint speed, iterate it, and set the new value
old_omega = gryoscope.joints['ring1_to_core'].state.omega
new_omega = old_omega + iter_val
gryoscope.joints['ring1_to_core'].set_state(omega = new_omega) # This returns 0 on success
Running the Simulation
Before running the simulation, we reset the project. This ensure that everything is started, updated, and in the desired initial state. proj.reset should be called before a simulation every time you run one.
[13]:
# Reset the project to its initial state. This is required to
# reset the simulation, reset the visualizer, and reset/start the
# animator.
proj.reset() # This returns 0 on success
[13]:
0
Next we create and run a simulation loop. In this loop, on every step we
Set the torques based on keyboard presses
Set the wheel speed based on keyboard presses
Set the wheel’s color based on its speed
Take a simulation step in real time
[14]:
# Run a 10 second simulation loop
while proj.simtime <= 10.:
apply_torques(proj, gyro)
set_omega(proj, gyro)
set_color(gyro)
proj.step(real_time=True, stable_step=False) # This returns 0 on success
Finally, we terminate the project. This is required to save any videos that are recorded and gracefully exit all the children threads (including the animator window). proj.terminate() should be called when done with any member of the condynsate.Project class.
[15]:
proj.terminate() # This returns 0 on success
[15]:
0