Scissor Meachanism¶
This notebook models a scissor mechansim made from four rods, six hinges and two supports. as two indipendent rods joined by a hinge treated as constraint. The functions shown in the double pendulum notebook are used within the bar_joint_model class, which simplifies the construction of such mechanisms.
[1]:
# Project and symbolic tools
import heyoka as hy
import quivira as qv
# Numerics and visualization
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
from _animation import animate_bar_model
Initialization¶
Initialize the model and set the gravity constant.
[2]:
model = qv.bar_joint_model()
model.set_g(9.81)
Building the model¶
The mechanism is being built. There are different ways the bars are added to the model, all highlighted here. `add_bar <../api.html#quivira.bar_joint_model.add_bar>`__ just adds the minimal information, length and mass. Theoretically, the equations of motion could be built from just this information. Eventually, it needs to be followed by `set_init_bar_attitude <../api.html#quivira.bar_joint_model.set_init_bar_attitude>`__ to add an initial position and rotation.
`add_bar_at_attitude <../api.html#quivira.bar_joint_model.add_bar_at_attitude>`__ combines the two for simplicity. `add_bar_from_point_pair <../api.html#quivira.bar_joint_model.add_bar_from_point_pair>`__ uses the coordinates of a start and an end point.
Joints only require the indices of the bars they join together and the respective position along the bars. For a bar with midpoint position \((x, y)\) and angle \(\theta\), a point on the bar is described by
where \(L\) is the bar length and \(s \in [-\tfrac{1}{2}, \tfrac{1}{2}]\) is the normalized position along the bar measured from its center. The endpoints correspond to \(s = -\tfrac{1}{2}\) and \(s = \tfrac{1}{2}\), while intermediate locations use any value in that interval.
Supports tie a point on a bar, defined with the \(s\) formulation from above, to a global position, and lock the dof in x and/or y. Thereofre, they are the only instance where both the topology and the initial state strictly have to be defined at the same time.
[3]:
# First two bars
model.add_bar(length=np.sqrt(4**2 + 4**2), mass=1)
model.add_bar_at_attitude(x0=2, y0=2, theta0=-np.pi/4, length=np.sqrt(4**2 + 4**2), mass=1)
model.add_joint(bar_index=[0, 1], s=[0.0, 0.0])
# Second two bars
model.add_bar_from_point_pair(start=(4, 0), end=(8, 4), mass_per_length=(1/np.sqrt(4**2 + 4**2)))
model.add_bar_from_point_pair(start=(4, 4), end=(8, 0), mass_per_length=(1/np.sqrt(4**2 + 4**2)))
model.add_joint(bar_index=[2, 3], s=[0.0, 0.0])
# Connect them
model.add_joint(bar_index=[0, 3], s=[0.5, -0.5])
model.add_joint(bar_index=[1, 2], s=[0.5, -0.5])
# Make supports
model.add_support(bar_index=0, s=-0.5, dof_in_x=False, dof_in_y=False, x=0, y=0)
model.add_support(bar_index=1, s=-0.5, dof_in_x=False, dof_in_y=True, x=0, y=4)
Taylor integrator¶
The equations of motion are found, which we then use to instantiate heyoka Taylor adaptive integrator. We keep the Delassus matrix \(W\) to find singularities later.
[4]:
ode_system, W = model.build_lagrange_eom(return_W=True)
ta = hy.taylor_adaptive(ode_system, compact_mode=True)
Initial state¶
Before we can integrate, we need to complete the setup of the initial state. While we set the initial positions and rotations of most bars when we added them to the model, we still need to assign an attitude to bar 0. All velocities are set to zero by default, which we can change for each bar with `set_init_bar_velocities <../api.html#quivira.bar_joint_model.set_init_bar_velocities>`__. Alternatively, we could also use
`set_initial_state <../api.html#quivira.bar_joint_model.set_initial_state>`__ to set all initial states at the same time and disregard what we have initialized before, as we will see later in this notebook.
To check if we are satisfied with our model, we can print it. There should be no None values.
[5]:
model.set_init_bar_attitude(bar_index=0, x0=2, y0=2, theta0=np.pi/4)
model.print_model()
Bars:
Bar 0: length=5.656854249492381, mass=1
Bar 1: length=5.656854249492381, mass=1
Bar 2: length=5.656854249492381, mass=1.0
Bar 3: length=5.656854249492381, mass=1.0
Joints:
Joint 0: bar 0 at s=0.0, bar 1 at s=0.0
Joint 1: bar 2 at s=0.0, bar 3 at s=0.0
Joint 2: bar 0 at s=0.5, bar 3 at s=-0.5
Joint 3: bar 1 at s=0.5, bar 2 at s=-0.5
Supports:
Support 0: bar 0 at s=-0.5, position=(0, 0), dof_in_x=False, dof_in_y=False
Support 1: bar 1 at s=-0.5, position=(0, 4), dof_in_x=False, dof_in_y=True
Initial state:
Bar 0: x=2, y=2, theta=0.7853981633974483, xd=0, yd=0, thetad=0
Bar 1: x=2, y=2, theta=-0.7853981633974483, xd=0, yd=0, thetad=0
Bar 2: x=6.0, y=2.0, theta=0.7853981633974483, xd=0, yd=0, thetad=0
Bar 3: x=6.0, y=2.0, theta=-0.7853981633974483, xd=0, yd=0, thetad=0
We can now extract that initial state as a list that will be passed to the integrator. We then use `check_constraint_fulfillment <../api.html#quivira.bar_joint_model.check_constraint_fulfillment>`__ to ensure that the initial state is possible with the topological constraints we defined in the beginning.
[6]:
state0 = model.get_initial_state()
model.check_constraint_fulfillment(state0)
Simulation¶
Now we can integrate, sampling 401 times and for a duration of 1.2 seconds.
[7]:
ta.time = 0.0
ta.state[:] = state0
t_end = 1
tgrid = np.linspace(0.0, t_end, 401)
solution = ta.propagate_grid(tgrid)[-1]
Animate the solution
[8]:
animate_bar_model(model, solution)