Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update FixedJoints: restoring spring-damped-torques, initial rotation offset #135

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0ccba62
Fix #131 for FixedJoints by applying restoring torques on rotation er…
mstoelzle Jul 14, 2022
e7d1b22
Use JointCasesCallback also for remaining joint cases examples
mstoelzle Jul 14, 2022
b72bdc9
Formatting using black
mstoelzle Jul 14, 2022
a27ee99
Scipy as_rotvec has issue in documentation about degrees parameter
mstoelzle Jul 14, 2022
6c8ac25
Adjust test for FixedJoint
mstoelzle Jul 14, 2022
187bb09
Fix direction of compensation damping torque for FixedJoint
mstoelzle Jul 14, 2022
368be52
Improve plotting for `fixed_joint_torsion` example
mstoelzle Jul 15, 2022
2107805
Enable the FixedJoint to automatically configure a static rotation of…
mstoelzle Jul 15, 2022
5ddeaa1
Fix final time of `fixed_joint_torsion` example
mstoelzle Jul 15, 2022
1b21120
Fix test of FixedJoint
mstoelzle Jul 15, 2022
5b37950
Replace JointCasesCallback with MyCallback
mstoelzle Jul 15, 2022
d8d7be8
Remove branching for self.nut > 0.0
mstoelzle Jul 15, 2022
00ecf2f
Fix unclear docstrings
mstoelzle Jul 15, 2022
cfae6d3
Rename rot_vec in inertial frame to rot_vec_inertial_frame
mstoelzle Jul 15, 2022
b4ae266
Rename static to rest in docstring
mstoelzle Jul 16, 2022
e3792f7
rename error_rot to dev_rot
mstoelzle Jul 16, 2022
95f4f8b
Rename error_omega to dev_omega
mstoelzle Jul 16, 2022
3b7a1fd
Finish renaming error_rot to dev_rot and error_omega to dev_omega
mstoelzle Jul 16, 2022
9a5cf1c
Some more renaming of variables
mstoelzle Jul 16, 2022
cb84464
Make `rest_rotation_matrix` initialization parameter of FixedJoint class
mstoelzle Jul 16, 2022
63c8399
Fix utilization of diagnostic directors data in `fixed_joint_torsion`
mstoelzle Jul 16, 2022
8fd8976
Remove initialization of `rest_rotation_matrix` during main loop
mstoelzle Jul 16, 2022
c97c9b4
Update elastica/joint.py
mstoelzle Jul 16, 2022
dc59e29
Update elastica/joint.py
mstoelzle Jul 16, 2022
91d1817
Replace numpy default argument with None
mstoelzle Jul 16, 2022
7b351c5
Fix sign error of dev_omega
mstoelzle Jul 16, 2022
e334b09
Also rotate omega to inertial frame in test_fixedjoint
mstoelzle Jul 16, 2022
08a34d8
Merge branch 'bugfix/fixed-joint-torsional-torque' of https://github.…
mstoelzle Jul 16, 2022
9d48697
Reformatting with black
mstoelzle Jul 16, 2022
5608760
Merge remote-tracking branch 'upstream/update-0.3.0' into bugfix/fixe…
mstoelzle Jul 16, 2022
6d360f6
Remove `joint_cases_callback`
mstoelzle Jul 16, 2022
d31173e
Fix missing transpose in `get_relative_rotation_two_systems`
mstoelzle Jul 16, 2022
f157413
Align rod2 in `fixed_joint_torsion` along (0,1,0), enabling example t…
mstoelzle Jul 16, 2022
cf368be
Add example to docstring of get_relative_rotation_two_systems
mstoelzle Jul 16, 2022
4868dc5
Finish examples of `get_relative_rotation_two_systems`
mstoelzle Jul 16, 2022
f36cf35
Some reformatting
mstoelzle Jul 16, 2022
9397d00
Update elastica/joint.py
mstoelzle Jul 17, 2022
035e0ed
Replace scipy rotation vector with custom numba _inv_rotate
mstoelzle Jul 17, 2022
34c6b2b
Merge branch 'bugfix/fixed-joint-torsional-torque' of https://github.…
mstoelzle Jul 17, 2022
d84f5f1
Test fixed joint for a variety of rest rotation matrices
mstoelzle Jul 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 51 additions & 31 deletions elastica/joint.py
Expand Up @@ -5,6 +5,7 @@
from elastica.utils import Tolerance, MaxDimension
from elastica._linalg import _batch_product_k_ik_to_ik
from math import sqrt
from scipy.spatial.transform import Rotation


class FreeJoint:
Expand Down Expand Up @@ -209,9 +210,14 @@ class FixedJoint(FreeJoint):
Damping coefficient of the joint.
kt: float
Rotational stiffness coefficient of the joint.
nut: float
Rotational damping coefficient of the joint.
static_rotation: np.array
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
Static 3x3 rotation matrix from system one to system two.
Instead of aligning the directors of both systems directly, a desired rotational offset C_12* is enforced.
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
"""

def __init__(self, k, nu, kt):
def __init__(self, k, nu, kt, nut=0.0, use_static_rotation=True):
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
"""

Parameters
Expand All @@ -222,51 +228,65 @@ def __init__(self, k, nu, kt):
Damping coefficient of the joint.
kt: float
Rotational stiffness coefficient of the joint.
nut: float = 0.
Rotational damping coefficient of the joint.
use_static_rotation: bool = True
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
If True, the initial rotation offset between the two systems is recorded
and enforced throughout the entire simulation.
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
"""
super().__init__(k, nu)
# additional in-plane constraint through restoring torque
# stiffness of the restoring constraint -- tuned empirically
self.kt = kt
self.nut = nut

# if a static rotation offset between the two systems should not be enforced,
# we set the relative rotation to the identity matrix. Otherwise
if use_static_rotation:
self.static_rotation = None
else:
self.static_rotation = np.eye(3)
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved

# Apply force is same as free joint
def apply_forces(self, rod_one, index_one, rod_two, index_two):
return super().apply_forces(rod_one, index_one, rod_two, index_two)

def apply_torques(self, rod_one, index_one, rod_two, index_two):
# current direction of the first element of link two
# also NOTE: - rod two is fixed at first element
link_direction = (
rod_two.position_collection[..., index_two + 1]
- rod_two.position_collection[..., index_two]
)
def apply_torques(self, system_one, index_one, system_two, index_two):
# collect directors of systems one and two
# note that systems can be either rods or rigid bodies
system_one_director = system_one.director_collection[..., index_one]
system_two_director = system_two.director_collection[..., index_two]

mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
# To constrain the orientation of link two, the second node of link two should align with
# the direction of link one. Thus, we compute the desired position of the second node of link two
# as check1, and the current position of the second node of link two as check2. Check1 and check2
# should overlap.
if self.static_rotation is None:
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
# this if clause should be active during the first timestep for the case use_static_rotation==True
self.static_rotation = system_one_director @ system_two_director.T
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved

tgt_destination = (
rod_one.position_collection[..., index_one]
+ rod_two.rest_lengths[index_two] * rod_one.tangents[..., index_one]
) # dl of rod 2 can be different than rod 1 so use rest length of rod 2
# relative rotation matrix from system 1 to system 2: C_12 = C_1I @ C_I2 where I denotes the inertial frame
rel_rot = system_one_director @ system_two_director.T
# C_22* = C_21 @ C_12* where C_12* is the desired rotation between systems one and two
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
error_rot = rel_rot.T @ self.static_rotation
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved

curr_destination = rod_two.position_collection[
..., index_two + 1
] # second element of rod2
# compute rotation vectors based on C_22*
rot_vec = Rotation.from_matrix(error_rot).as_rotvec()
skim0119 marked this conversation as resolved.
Show resolved Hide resolved

# Compute the restoring torque
forcedirection = -self.kt * (
curr_destination - tgt_destination
) # force direction is between rod2 2nd element and rod1
torque = np.cross(link_direction, forcedirection)
# rotate rotation vector into inertial frame
rot_vec = system_two_director.T @ rot_vec
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved

# The opposite torque will be applied on link one
rod_one.external_torques[..., index_one] -= (
rod_one.director_collection[..., index_one] @ torque
)
rod_two.external_torques[..., index_two] += (
rod_two.director_collection[..., index_two] @ torque
)
# we compute the constraining torque using a rotational spring - damper system in the inertial frame
torque = self.kt * rot_vec

# add damping torque
if self.nut > 0.0:
mstoelzle marked this conversation as resolved.
Show resolved Hide resolved
# error in rotation velocity between system 1 and system 2
error_omega = (
system_one.omega_collection[..., index_one]
- system_two.omega_collection[..., index_two]
)
torque -= self.nut * error_omega

# The opposite torques will be applied to system one and two after rotating the torques into the local frame
system_one.external_torques[..., index_one] -= system_one_director @ torque
system_two.external_torques[..., index_two] += system_two_director @ torque


@numba.njit(cache=True)
Expand Down
27 changes: 3 additions & 24 deletions examples/JointCases/fixed_joint.py
Expand Up @@ -9,6 +9,7 @@
from examples.JointCases.external_force_class_for_joint_test import (
EndpointForcesSinusoidal,
)
from examples.JointCases.joint_cases_callback import JointCasesCallback
from examples.JointCases.joint_cases_postprocessing import (
plot_position,
plot_video,
Expand Down Expand Up @@ -104,36 +105,14 @@ class FixedJointSimulator(
time_step=dt,
)

# Callback functions
# Add call backs
class TestJoints(CallBackBaseClass):
"""
Call back function for testing joints
"""

def __init__(self, step_skip: int, callback_params: dict):
CallBackBaseClass.__init__(self)
self.every = step_skip
self.callback_params = callback_params

def make_callback(self, system, time, current_step: int):
if current_step % self.every == 0:
self.callback_params["time"].append(time)
self.callback_params["step"].append(current_step)
self.callback_params["position"].append(system.position_collection.copy())
self.callback_params["velocity"].append(system.velocity_collection.copy())
return


pp_list_rod1 = defaultdict(list)
pp_list_rod2 = defaultdict(list)


fixed_joint_sim.collect_diagnostics(rod1).using(
TestJoints, step_skip=1000, callback_params=pp_list_rod1
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod1
)
fixed_joint_sim.collect_diagnostics(rod2).using(
TestJoints, step_skip=1000, callback_params=pp_list_rod2
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod2
)

fixed_joint_sim.finalize()
Expand Down
171 changes: 171 additions & 0 deletions examples/JointCases/fixed_joint_torsion.py
@@ -0,0 +1,171 @@
__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section."""

import matplotlib.pyplot as plt
import numpy as np
import sys

# FIXME without appending sys.path make it more generic
sys.path.append("../../")
from elastica import *
from examples.JointCases.external_force_class_for_joint_test import (
EndpointForcesSinusoidal,
)
from examples.JointCases.joint_cases_callback import JointCasesCallback
from examples.JointCases.joint_cases_postprocessing import (
plot_position,
plot_orientation,
plot_video,
plot_video_xy,
plot_video_xz,
)


class FixedJointSimulator(
BaseSystemCollection, Constraints, Connections, Forcing, Damping, CallBacks
):
pass


fixed_joint_sim = FixedJointSimulator()

# setting up test params
n_elem = 10
direction = np.array([0.0, 0.0, 1.0])
normal = np.array([0.0, 1.0, 0.0])
roll_direction = np.cross(direction, normal)
base_length = 0.2
base_radius = 0.007
base_area = np.pi * base_radius ** 2
density = 1750
E = 3e7
poisson_ratio = 0.5
shear_modulus = E / (poisson_ratio + 1.0)

start_rod_1 = np.zeros((3,))
start_rod_2 = start_rod_1 + direction * base_length

# Create rod 1
rod1 = CosseratRod.straight_rod(
n_elem,
start_rod_1,
direction,
normal,
base_length,
base_radius,
density,
0.0, # internal damping constant, deprecated in v0.3.0
E,
shear_modulus=shear_modulus,
)
fixed_joint_sim.append(rod1)
# Create rod 2
rod2 = CosseratRod.straight_rod(
n_elem,
start_rod_2,
direction,
normal,
base_length,
base_radius,
density,
0.0, # internal damping constant, deprecated in v0.3.0
E,
shear_modulus=shear_modulus,
)
fixed_joint_sim.append(rod2)

# Apply boundary conditions to rod1.
fixed_joint_sim.constrain(rod1).using(
OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,)
)

# Connect rod 1 and rod 2
fixed_joint_sim.connect(
first_rod=rod1, second_rod=rod2, first_connect_idx=-1, second_connect_idx=0
).using(FixedJoint, k=1e5, nu=1.0, kt=1e3, nut=1e-3)

# Add forces to rod2
fixed_joint_sim.add_forcing_to(rod2).using(
UniformTorques, torque=5e-3, direction=np.array([0.0, 0.0, 1.0])
)

# add damping
damping_constant = 0.4
dt = 1e-5
fixed_joint_sim.dampen(rod1).using(
ExponentialDamper,
damping_constant=damping_constant,
time_step=dt,
)
fixed_joint_sim.dampen(rod2).using(
ExponentialDamper,
damping_constant=damping_constant,
time_step=dt,
)


pp_list_rod1 = defaultdict(list)
pp_list_rod2 = defaultdict(list)


fixed_joint_sim.collect_diagnostics(rod1).using(
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod1
)
fixed_joint_sim.collect_diagnostics(rod2).using(
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod2
)

fixed_joint_sim.finalize()
timestepper = PositionVerlet()

final_time = 10
dl = base_length / n_elem
dt = 1e-5
total_steps = int(final_time / dt)
print("Total steps", total_steps)
integrate(timestepper, fixed_joint_sim, final_time, total_steps)


plot_orientation(
"Orientation of last node of rod 1",
pp_list_rod1["time"],
np.array(pp_list_rod1["director"])[..., -1],
)
plot_orientation(
"Orientation of last node of rod 2",
pp_list_rod2["time"],
np.array(pp_list_rod2["director"])[..., -1],
)

PLOT_FIGURE = True
SAVE_FIGURE = True
PLOT_VIDEO = True

# plotting results
if PLOT_FIGURE:
filename = "fixed_joint_torsion_test.png"
plot_position(pp_list_rod1, pp_list_rod2, filename, SAVE_FIGURE)

if PLOT_VIDEO:
filename = "fixed_joint_torsion_test"
fps = 100 # Hz
plot_video(
pp_list_rod1,
pp_list_rod2,
video_name=filename + ".mp4",
margin=0.2,
fps=fps,
)
plot_video_xy(
pp_list_rod1,
pp_list_rod2,
video_name=filename + "_xy.mp4",
margin=0.2,
fps=fps,
)
plot_video_xz(
pp_list_rod1,
pp_list_rod2,
video_name=filename + "_xz.mp4",
margin=0.2,
fps=fps,
)
28 changes: 3 additions & 25 deletions examples/JointCases/hinge_joint.py
Expand Up @@ -9,6 +9,7 @@
from examples.JointCases.external_force_class_for_joint_test import (
EndpointForcesSinusoidal,
)
from examples.JointCases.joint_cases_callback import JointCasesCallback
from examples.JointCases.joint_cases_postprocessing import (
plot_position,
plot_video,
Expand Down Expand Up @@ -106,39 +107,16 @@ class HingeJointSimulator(
time_step=dt,
)

# Callback functions
# Add call backs
class TestJoints(CallBackBaseClass):
"""
Call back function for testing joints
"""

def __init__(self, step_skip: int, callback_params: dict):
CallBackBaseClass.__init__(self)
self.every = step_skip
self.callback_params = callback_params

def make_callback(self, system, time, current_step: int):
if current_step % self.every == 0:
self.callback_params["time"].append(time)
self.callback_params["step"].append(current_step)
self.callback_params["position"].append(system.position_collection.copy())
self.callback_params["velocity"].append(system.velocity_collection.copy())
return


pp_list_rod1 = defaultdict(list)
pp_list_rod2 = defaultdict(list)


hinge_joint_sim.collect_diagnostics(rod1).using(
TestJoints, step_skip=1000, callback_params=pp_list_rod1
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod1
)
hinge_joint_sim.collect_diagnostics(rod2).using(
TestJoints, step_skip=1000, callback_params=pp_list_rod2
JointCasesCallback, step_skip=1000, callback_params=pp_list_rod2
)


hinge_joint_sim.finalize()
timestepper = PositionVerlet()
# timestepper = PEFRL()
Expand Down