irsim.world.sensors.contact2d 源代码
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
from matplotlib.collections import LineCollection
from mpl_toolkits.mplot3d import Axes3D
from irsim.config import palette_param
if TYPE_CHECKING:
from irsim.lib.algorithm.contact import Contact, ContactReport
from irsim.world.object_base import ObjectBase
[文档]
class Contact2D:
"""
Contact sensor of one object: what the contact step recorded about it
(``collision_mode: contact`` only), as a contact sensor mounted on the
object reports.
Every object carries one as ``obj.contact``, written by the environment
and the contact solver, so its readings are always available:
``obj.contact.in_contact``, ``partners``, ``reports``, ``force``,
``contact_time``, ``air_time``, ``started`` and ``ended``.
Listing it under the object's ``sensors`` as ``type: 'contact2d'`` makes
that one the object's sensor and draws it: a marker at each contact
point and a line along the contact normal whose length is the contact
force times ``force_scale``.
Args:
state (np.ndarray): Initial state of the object ``[x, y, theta]``,
kept for the common sensor interface; the readings do not
depend on it.
obj_id (int): ID of the associated object.
force_scale (float): Length of the drawn force line per newton, in
meters. Default 0.05.
marker_size (float): Size of the contact point markers. Default 6.
linewidth (float): Width of the force lines. Default 1.5.
alpha (float): Transparency of the drawing. Default 0.9.
**kwargs: ``plot`` sub-dict (preferred) with the visualization options
above and ``color`` (default: the palette ``marker`` color), plus
the ``name`` / ``type`` key of the YAML entry.
Attributes:
sensor_type (str): ``"contact2d"``.
parent (ObjectBase): The object the sensor is mounted on.
in_contact (bool): Whether a contact was resolved against ``parent``
in the last step.
partners (list): The objects ``parent`` touched in the last step,
each once.
records (list[Contact]): The contact records of the last step, in
the solver's order.
force (np.ndarray): Net world-frame contact force ``(2, 1)`` on
``parent`` in the last step, in newtons.
contact_time (float): Seconds ``parent`` has been touching something.
air_time (float): Seconds ``parent`` has been free; one of the two
timers runs while the other is held at zero.
started (bool): Whether the last step began a contact.
ended (bool): Whether the last step ended one.
"""
def __init__(
self,
state: np.ndarray | None = None,
obj_id: int = 0,
force_scale: float = 0.05,
marker_size: float = 6.0,
linewidth: float = 1.5,
alpha: float = 0.9,
**kwargs,
) -> None:
self.sensor_type = "contact2d"
self.obj_id = obj_id
self._state = state
self.parent: ObjectBase | None = None
# Visualization params may be given under a `plot:` sub-dict
# (preferred) or as flat top-level keys.
_plot = kwargs.get("plot") or {}
self.force_scale = _plot.get("force_scale", force_scale)
self.marker_size = _plot.get("marker_size", marker_size)
self.linewidth = _plot.get("linewidth", linewidth)
self.alpha = _plot.get("alpha", alpha)
self.color = _plot.get("color", kwargs.get("color", palette_param.marker))
self.in_contact = False
self.partners: list[Any] = []
self.records: list[Contact] = []
self.force = np.zeros((2, 1))
self.contact_time = 0.0
self.air_time = 0.0
self.started = False
self.ended = False
self._was_in_contact = False
self._point_artist = None
self._force_artist = None
# -- readings -----------------------------------------------------------
@property
def reports(self) -> list[ContactReport]:
"""Last step's contacts seen from ``parent``'s side."""
return [contact.report(self.parent) for contact in self.records]
# -- writes by the environment and the contact solver ------------------
[文档]
def clear(self) -> None:
"""Forget last step's contacts: flag, partners, records and force."""
self.in_contact = False
self.partners = []
self.records = []
self.force = np.zeros((2, 1))
[文档]
def reset(self) -> None:
"""Forget everything, the timers included, as at the start of a run."""
self.clear()
self.contact_time = 0.0
self.air_time = 0.0
self.started = False
self.ended = False
self._was_in_contact = False
[文档]
def add(self, contact: Contact) -> None:
"""Record a contact the solver resolved against ``parent``."""
self.in_contact = True
other = contact.b if contact.a is self.parent else contact.a
if other not in self.partners:
self.partners.append(other)
self.records.append(contact)
[文档]
def add_force(self, force_xy: np.ndarray) -> None:
"""Accumulate a world-frame force ``(2,)`` or ``(2, 1)`` on ``parent``."""
self.force = self.force + np.asarray(force_xy, dtype=float).reshape(2, 1)
[文档]
def tick(self, step_time: float) -> None:
"""Advance ``contact_time`` or ``air_time`` by one step and raise
``started`` or ``ended`` for a step that switched between them."""
self.started = self.in_contact and not self._was_in_contact
self.ended = self._was_in_contact and not self.in_contact
self._was_in_contact = self.in_contact
if self.in_contact:
self.contact_time += step_time
self.air_time = 0.0
else:
self.air_time += step_time
self.contact_time = 0.0
# -- sensor interface ----------------------------------------------------
[文档]
def step(self, state: np.ndarray) -> None:
"""Keep the sensor state; the readings are written by the contact step."""
self._state = state
[文档]
def plot(self, ax, state: np.ndarray | None = None, **kwargs) -> None:
"""
Draw the contact points and force lines on a given axis.
Nothing is drawn on a 3D axis, since contacts are planar.
"""
if isinstance(ax, Axes3D) or self._point_artist is not None:
return
(self._point_artist,) = ax.plot(
[],
[],
linestyle="",
marker="o",
markersize=self.marker_size,
color=self.color,
alpha=self.alpha,
zorder=4,
)
self._force_artist = LineCollection(
[],
colors=self.color,
linewidths=self.linewidth,
alpha=self.alpha,
zorder=4,
)
ax.add_collection(self._force_artist)
self.step_plot()
[文档]
def step_plot(self) -> None:
"""Move the markers and force lines to the last step's contacts."""
if self._point_artist is None:
return
reports = self.reports
self._point_artist.set_data(
[r.point[0] for r in reports], [r.point[1] for r in reports]
)
self._force_artist.set_segments(
[
[r.point, r.point + r.normal * (r.force * self.force_scale)]
for r in reports
]
)
[文档]
def plot_clear(self) -> None:
"""Remove the drawing from the axis."""
for artist in (self._point_artist, self._force_artist):
if artist is not None:
artist.remove()
self._point_artist = None
self._force_artist = None