codex/unilateral-contact-load-release.py

unilateral-contact-load-release.py
"""Original contact-complementarity set and initial block lift-off response."""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    mass, gravity = 2.0, 9.81
    pull = np.linspace(0, 30, 1501)
    reaction = np.maximum(mass * gravity - pull, 0)
    acceleration = np.maximum((pull - mass * gravity) / mass, 0)
    assert np.allclose(reaction + pull - mass * gravity, mass * acceleration)
    assert np.allclose(reaction * acceleration, 0)
    assert np.isclose(mass * gravity - 5, 14.62)
    assert np.isclose((25 - mass * gravity) / mass, 2.69)

    fig, (allowed, release) = plt.subplots(1, 2, figsize=(10, 4.3), dpi=100,
                                         layout="constrained")
    fig.set_facecolor("white")
    allowed.axvspan(0, 2.2, color="#eeeeee", zorder=0)
    allowed.plot([0, 0], [0, 25], color="#286f99", linewidth=3)
    allowed.plot([0, 2.2], [0, 0], color="#286f99", linewidth=3)
    allowed.scatter([0, 0, 1.6], [0, 15, 0], color="#286f99", s=38, zorder=4)
    allowed.scatter([1.0], [10], color="#9f4b27", marker="x", s=65, linewidth=2, zorder=5)
    allowed.annotate("Loaded contact", (0, 15), xytext=(0.32, 18), fontsize=9,
                     arrowprops={"arrowstyle": "->", "color": "#286f99"})
    allowed.annotate("Unloaded touch", (0, 0), xytext=(0.25, 4), fontsize=9,
                     arrowprops={"arrowstyle": "->", "color": "#286f99"})
    allowed.annotate("Open contact", (1.6, 0), xytext=(1.27, 3), fontsize=9,
                     arrowprops={"arrowstyle": "->", "color": "#286f99"})
    allowed.text(0.75, 12, "Positive gap and force:\nexcluded", fontsize=9, color="#9f4b27")
    allowed.set(xlim=(-0.08, 2.2), ylim=(-1, 25), xticks=[0, 1, 2],
                xlabel="Normal gap (mm)", ylabel="Normal force (N)",
                title="Complementarity allows only the blue axes")
    force_line, = release.plot(pull, reaction, color="#286f99", linewidth=2,
                               label="Initial normal reaction")
    release.set(xlim=(0, 30), ylim=(-1, 21), xlabel="Upward applied force (N)",
                ylabel="Initial normal reaction (N)", title="Lift-off when the upward pull exceeds mg")
    release.tick_params(axis="y", labelcolor="#286f99")
    accel_axes = release.twinx()
    accel_line, = accel_axes.plot(pull, acceleration, "--", color="#9f4b27", linewidth=2,
                                  label="Initial upward acceleration")
    accel_axes.set(ylim=(-0.28, 5.88), ylabel="Initial upward acceleration (m/s²)")
    accel_axes.tick_params(axis="y", labelcolor="#9f4b27")
    release.axvline(mass * gravity, color="#777777", linestyle=":", linewidth=1)
    release.scatter([5], [14.62], color="#286f99", zorder=5)
    accel_axes.scatter([25], [2.69], color="#9f4b27", zorder=5)
    release.annotate("5 N pull: 14.62 N reaction", (5, 14.62), xytext=(2, 10.5), fontsize=8.5,
                     arrowprops={"arrowstyle": "->", "color": "#286f99"})
    release.legend(handles=[force_line, accel_line], loc="upper right", fontsize=8)
    for ax in (allowed, release):
        ax.grid(alpha=0.12)
    fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()