Skip to content
LIBRARY
tests.GyroscopicEffectsTest.md

tests.GyroscopicEffectsTest

Gyroscopic Effects Test

Validates Rotor1D's gyroscopic reaction against an explicit 3D model of the same rotor.

Both branches are a spherically-supported arm carrying a rotor at 45 degrees to the arm, released with the rotor spinning at 10 rad/s:

  • Branch A builds the rotor explicitly: a Revolute joint lets a BodyCylinder spin relative to the arm.

  • Branch B attaches the very same BodyCylinder rigidly and adds a Rotor1D on the same frame to supply the spin momentum.

The two must move identically. That equivalence is exactly what the -e*dot(nJ, z_a) term in Rotor1D buys: it hands the rotor's axial-inertia demand back to the flange path, so the momenta add up without double counting the axial inertia that the rigid BodyCylinder already contributes.

Usage

MultibodyComponents.tests.GyroscopicEffectsTest(d_rotor=0.1, l_rotor=0.2, density=7700, m_rotor=density * 3.141592653589793 * l_rotor * (d_rotor / 2) ^ 2, J_rotor=m_rotor * (d_rotor / 2) ^ 2 / 2)

Parameters:

NameDescriptionUnitsDefault value
exacttrue
d_rotorm0.1
l_rotorm0.2
density7700

Behavior

Source

dyad
"""
# Gyroscopic Effects Test

Validates `Rotor1D`'s gyroscopic reaction against an explicit 3D model of the
same rotor.

Both branches are a spherically-supported arm carrying a rotor at 45 degrees to
the arm, released with the rotor spinning at 10 rad/s:

- Branch A builds the rotor explicitly: a `Revolute` joint lets a `BodyCylinder`
  spin relative to the arm.
- Branch B attaches the very same `BodyCylinder` *rigidly* and adds a `Rotor1D`
  on the same frame to supply the spin momentum.

The two must move identically. That equivalence is exactly what the
`-e*dot(nJ, z_a)` term in `Rotor1D` buys: it hands the rotor's axial-inertia
demand back to the flange path, so the momenta add up without double counting
the axial inertia that the rigid `BodyCylinder` already contributes.
"""
example component GyroscopicEffectsTest
  world = MultibodyComponents.World() {
    "Dyad": {"placement": {"diagram": {"x1": 20, "x2": 120, "y1": 120, "y2": 220}}}
  }
  # Branch A: explicit 3D rotor on a revolute joint
  spherical1 = MultibodyComponents.Spherical() {
    "Dyad": {"placement": {"diagram": {"x1": 145, "x2": 245, "y1": 120, "y2": 220}}}
  }
  arm1 = MultibodyComponents.BodyCylinder(r = [0.25, 0, 0], diameter = 0.05) {
    "Dyad": {"placement": {"diagram": {"x1": 290, "x2": 390, "y1": 120, "y2": 220}}}
  }
  rot1 = MultibodyComponents.FixedRotation(n = [0, 1, 0], angle = 45) {
    "Dyad": {"placement": {"diagram": {"x1": 410, "x2": 510, "y1": 120, "y2": 220}}}
  }
  revolute = MultibodyComponents.Revolute(n = [1, 0, 0], phi(initial = 0), w(initial = 10)) {
    "Dyad": {"placement": {"diagram": {"x1": 540, "x2": 640, "y1": 120, "y2": 220}}}
  }
  rotorbody1 = MultibodyComponents.BodyCylinder(r = [l_rotor, 0, 0], diameter = d_rotor) {
    "Dyad": {"placement": {"diagram": {"x1": 660, "x2": 760, "y1": 120, "y2": 220}}}
  }
  # Branch B: rigid rotor body plus Rotor1D for the spin momentum
  anchor = MultibodyComponents.Fixed(r = [0, -0.6, 0]) {
    "Dyad": {
      "placement": {"diagram": {"x1": -10, "x2": 90, "y1": 490, "y2": 590, "rot": 270}}
    }
  }
  spherical2 = MultibodyComponents.Spherical() {
    "Dyad": {
      "placement": {"diagram": {"x1": -10, "x2": 90, "y1": 380, "y2": 480, "rot": 270}}
    }
  }
  arm2 = MultibodyComponents.BodyCylinder(r = [0.25, 0, 0], diameter = 0.05) {
    "Dyad": {"placement": {"diagram": {"x1": 30, "x2": 130, "y1": 255, "y2": 355}}}
  }
  rot2 = MultibodyComponents.FixedRotation(n = [0, 1, 0], angle = 45) {
    "Dyad": {"placement": {"diagram": {"x1": 150, "x2": 250, "y1": 255, "y2": 355}}}
  }
  rotorbody2 = MultibodyComponents.BodyCylinder(r = [l_rotor, 0, 0], diameter = d_rotor) {
    "Dyad": {"placement": {"diagram": {"x1": 270, "x2": 370, "y1": 255, "y2": 355}}}
  }
  rotor1D = MultibodyComponents.Rotor1D(J = J_rotor, n = [1, 0, 0], exact = exact, phi(initial = 0), w(initial = 10)) {
    "Dyad": {"placement": {"diagram": {"x1": 395, "x2": 495, "y1": 240, "y2": 340}}}
  }
  structural parameter exact::Boolean = true
  parameter d_rotor::Length = 0.1
  parameter l_rotor::Length = 0.2
  parameter density::Real = 7700
  # Must match BodyCylinder's own axial inertia for the two branches to agree:
  # m = density*pi*length*radius^2, I_axial = m*radius^2/2. A float literal is
  # used for pi because a symbolic pi cannot be evaluated in a parameter binding.
  final parameter m_rotor::Mass = density * 3.141592653589793 * l_rotor * (d_rotor / 2) ^ 2
  final parameter J_rotor::Inertia = m_rotor * (d_rotor / 2) ^ 2 / 2
relations
  # Branch A
  connect(world.frame_b, spherical1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(spherical1.frame_b, arm1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(arm1.frame_b, rot1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(rot1.frame_b, revolute.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(revolute.frame_b, rotorbody1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  # Branch B. `anchor` is its own ground -- it must NOT also be connected to
  # `world.frame_b`, or the orientation would be rooted twice.
  connect(anchor.frame_b, spherical2.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 40.00000000000001, "y": 540}], "E": 2}],
      "path": {}
    }
  }
  connect(spherical2.frame_b, arm2.frame_a) {
    "Dyad": {
      "edges": [
        {"S": 1, "M": [{"x": 39.99999999999999, "y": 360}, {"x": 40, "y": 360}], "E": 2}
      ],
      "path": {}
    }
  }
  connect(arm2.frame_b, rot2.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(rot2.frame_b, rotorbody2.frame_a, rotor1D.frame_a) {
    "Dyad": {
      "edges": [
        {"S": 1, "M": [], "E": -1},
        {"S": -1, "M": [], "E": 2},
        {"S": -1, "M": [{"x": 260, "y": 395}, {"x": 445, "y": 395}], "E": 3}
      ],
      "junctions": [{"x": 260, "y": 305}],
      "path": {}
    }
  }
end
Flattened Source
dyad
"""
# Gyroscopic Effects Test

Validates `Rotor1D`'s gyroscopic reaction against an explicit 3D model of the
same rotor.

Both branches are a spherically-supported arm carrying a rotor at 45 degrees to
the arm, released with the rotor spinning at 10 rad/s:

- Branch A builds the rotor explicitly: a `Revolute` joint lets a `BodyCylinder`
  spin relative to the arm.
- Branch B attaches the very same `BodyCylinder` *rigidly* and adds a `Rotor1D`
  on the same frame to supply the spin momentum.

The two must move identically. That equivalence is exactly what the
`-e*dot(nJ, z_a)` term in `Rotor1D` buys: it hands the rotor's axial-inertia
demand back to the flange path, so the momenta add up without double counting
the axial inertia that the rigid `BodyCylinder` already contributes.
"""
example component GyroscopicEffectsTest
  world = MultibodyComponents.World() {
    "Dyad": {"placement": {"diagram": {"x1": 20, "x2": 120, "y1": 120, "y2": 220}}}
  }
  # Branch A: explicit 3D rotor on a revolute joint
  spherical1 = MultibodyComponents.Spherical() {
    "Dyad": {"placement": {"diagram": {"x1": 145, "x2": 245, "y1": 120, "y2": 220}}}
  }
  arm1 = MultibodyComponents.BodyCylinder(r = [0.25, 0, 0], diameter = 0.05) {
    "Dyad": {"placement": {"diagram": {"x1": 290, "x2": 390, "y1": 120, "y2": 220}}}
  }
  rot1 = MultibodyComponents.FixedRotation(n = [0, 1, 0], angle = 45) {
    "Dyad": {"placement": {"diagram": {"x1": 410, "x2": 510, "y1": 120, "y2": 220}}}
  }
  revolute = MultibodyComponents.Revolute(n = [1, 0, 0], phi(initial = 0), w(initial = 10)) {
    "Dyad": {"placement": {"diagram": {"x1": 540, "x2": 640, "y1": 120, "y2": 220}}}
  }
  rotorbody1 = MultibodyComponents.BodyCylinder(r = [l_rotor, 0, 0], diameter = d_rotor) {
    "Dyad": {"placement": {"diagram": {"x1": 660, "x2": 760, "y1": 120, "y2": 220}}}
  }
  # Branch B: rigid rotor body plus Rotor1D for the spin momentum
  anchor = MultibodyComponents.Fixed(r = [0, -0.6, 0]) {
    "Dyad": {
      "placement": {"diagram": {"x1": -10, "x2": 90, "y1": 490, "y2": 590, "rot": 270}}
    }
  }
  spherical2 = MultibodyComponents.Spherical() {
    "Dyad": {
      "placement": {"diagram": {"x1": -10, "x2": 90, "y1": 380, "y2": 480, "rot": 270}}
    }
  }
  arm2 = MultibodyComponents.BodyCylinder(r = [0.25, 0, 0], diameter = 0.05) {
    "Dyad": {"placement": {"diagram": {"x1": 30, "x2": 130, "y1": 255, "y2": 355}}}
  }
  rot2 = MultibodyComponents.FixedRotation(n = [0, 1, 0], angle = 45) {
    "Dyad": {"placement": {"diagram": {"x1": 150, "x2": 250, "y1": 255, "y2": 355}}}
  }
  rotorbody2 = MultibodyComponents.BodyCylinder(r = [l_rotor, 0, 0], diameter = d_rotor) {
    "Dyad": {"placement": {"diagram": {"x1": 270, "x2": 370, "y1": 255, "y2": 355}}}
  }
  rotor1D = MultibodyComponents.Rotor1D(J = J_rotor, n = [1, 0, 0], exact = exact, phi(initial = 0), w(initial = 10)) {
    "Dyad": {"placement": {"diagram": {"x1": 395, "x2": 495, "y1": 240, "y2": 340}}}
  }
  structural parameter exact::Boolean = true
  parameter d_rotor::Length = 0.1
  parameter l_rotor::Length = 0.2
  parameter density::Real = 7700
  # Must match BodyCylinder's own axial inertia for the two branches to agree:
  # m = density*pi*length*radius^2, I_axial = m*radius^2/2. A float literal is
  # used for pi because a symbolic pi cannot be evaluated in a parameter binding.
  final parameter m_rotor::Mass = density * 3.141592653589793 * l_rotor * (d_rotor / 2) ^ 2
  final parameter J_rotor::Inertia = m_rotor * (d_rotor / 2) ^ 2 / 2
relations
  # Branch A
  connect(world.frame_b, spherical1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(spherical1.frame_b, arm1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(arm1.frame_b, rot1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(rot1.frame_b, revolute.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(revolute.frame_b, rotorbody1.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  # Branch B. `anchor` is its own ground -- it must NOT also be connected to
  # `world.frame_b`, or the orientation would be rooted twice.
  connect(anchor.frame_b, spherical2.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 40.00000000000001, "y": 540}], "E": 2}],
      "path": {}
    }
  }
  connect(spherical2.frame_b, arm2.frame_a) {
    "Dyad": {
      "edges": [
        {"S": 1, "M": [{"x": 39.99999999999999, "y": 360}, {"x": 40, "y": 360}], "E": 2}
      ],
      "path": {}
    }
  }
  connect(arm2.frame_b, rot2.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "path": {}}}
  connect(rot2.frame_b, rotorbody2.frame_a, rotor1D.frame_a) {
    "Dyad": {
      "edges": [
        {"S": 1, "M": [], "E": -1},
        {"S": -1, "M": [], "E": 2},
        {"S": -1, "M": [{"x": 260, "y": 395}, {"x": 445, "y": 395}], "E": 3}
      ],
      "junctions": [{"x": 260, "y": 305}],
      "path": {}
    }
  }
metadata {}
end


Test Cases

No test cases defined.

  • Examples

  • Experiments

  • Analyses