diff --git a/bindings/generated_docstrings/geometry.h b/bindings/generated_docstrings/geometry.h index cc508d65ec62..c6ebd24ae396 100644 --- a/bindings/generated_docstrings/geometry.h +++ b/bindings/generated_docstrings/geometry.h @@ -75,21 +75,22 @@ Parameter ``resolution_hint``: guides the level of mesh refinement. It has length units (in meters) and roughly corresponds to a typical edge length in the resulting mesh. See hug_properties. This will be ignored for - geometry types that don't require tessellation. + geometry types that don't require tessellation. Must satisfy 0 < + ``resolution_hint`` < ∞ (NaN and ±∞ rejected). Parameter ``hydroelastic_modulus``: A multiplier that maps penetration to pressure. See - hug_properties. + hug_properties. Must be > 0 (+∞ allowed; NaN rejected). Parameter ``properties``: The properties will be added to this property set. Raises: - RuntimeError If ``properties`` already has properties with the + RuntimeError If ``resolution_hint`` or ``hydroelastic_modulus`` is + invalid, or if ``properties`` already has properties with the names that this function would need to add. Precondition: - 0 < ``resolution_hint`` < ∞, 0 < ``hydroelastic_modulus``, and ``properties`` is not nullptr.)"""; } AddCompliantHydroelasticProperties; // Symbol: drake::geometry::AddCompliantHydroelasticPropertiesForHalfSpace @@ -102,21 +103,22 @@ thickness. This variant is required for hydroelastic half spaces. Parameter ``slab_thickness``: The distance from the half space boundary to its rigid core (this - helps define the extent field of the half space). + helps define the extent field of the half space). Must satisfy 0 < + ``slab_thickness`` < ∞ (NaN and ±∞ rejected). Parameter ``hydroelastic_modulus``: A multiplier that maps penetration to pressure. See - hug_properties. + hug_properties. Must be > 0 (+∞ allowed; NaN rejected). Parameter ``properties``: The properties will be added to this property set. Raises: - RuntimeError If ``properties`` already has properties with the + RuntimeError If ``slab_thickness`` or ``hydroelastic_modulus`` is + invalid, or if ``properties`` already has properties with the names that this function would need to add. Precondition: - 0 < ``slab_thickness`` < ∞, 0 < ``hydroelastic_modulus``, and ``properties`` is not nullptr.)"""; } AddCompliantHydroelasticPropertiesForHalfSpace; // Symbol: drake::geometry::AddContactMaterial @@ -132,9 +134,10 @@ default values will be provided. Downstream consumers of the contact materials can optionally provide defaults for missing properties. Raises: - RuntimeError if ``dissipation`` is negative, ``point_stiffness`` - is not positive, of any of the contact material properties have - already been defined in ``properties``. + RuntimeError if ``dissipation`` is present but not ≥ 0 (NaN is + rejected; +∞ is allowed), if ``point_stiffness`` is present but + not > 0 (NaN is rejected; +∞ is allowed), or if any of the contact + material properties have already been defined in ``properties``. Precondition: ``properties`` is not nullptr.)"""; @@ -152,17 +155,19 @@ Parameter ``resolution_hint``: guides the level of mesh refinement. It has length units (in meters) and roughly corresponds to a typical edge length in the resulting mesh. See hug_properties. This will be ignored for - geometry types that don't require tessellation. + geometry types that don't require tessellation. Must satisfy 0 < + ``resolution_hint`` < ∞ (NaN and ±∞ rejected). Parameter ``properties``: The properties will be added to this property set. Raises: - RuntimeError If ``properties`` already has properties with the - names that this function would need to add. + RuntimeError If ``resolution_hint`` is invalid or if + ``properties`` already has properties with the names that this + function would need to add. Precondition: - 0 < ``resolution_hint`` < ∞ and ``properties`` is not nullptr.)"""; + ``properties`` is not nullptr.)"""; // Source: drake/geometry/proximity_properties.h const char* doc_1args = R"""(Overload, intended for shapes that don't get tessellated in their @@ -890,17 +895,14 @@ Serialization" for background.)"""; const char* doc = R"""(@name Hydroelastic Contact Properties -These properties affect hydroelastic contact only. For more detail, -including limits of the numeric parameters, +These properties affect hydroelastic contact only. Valid ranges for +each numeric parameter are documented on the corresponding field +below. See also: geometry∷AddRigidHydroelasticProperties, geometry∷AddCompliantHydroelasticProperties, - geometry∷AddCompliantHydroelasticPropertiesForHalfSpace. - -For more context, - -See also: + geometry∷AddCompliantHydroelasticPropertiesForHalfSpace, hug_properties. There are three valid options for ``compliance_type``: - "undefined": hydroelastic contact will not be used. - "rigid": the default hydroelastic compliance type will @@ -916,14 +918,18 @@ See also: const char* doc = R"""(@name General Contact Properties -These properties affect contact in general. For more detail, including -limits of the numeric parameters, +These properties affect contact in general. Valid ranges for each +numeric parameter are documented on the corresponding field below. See also: geometry∷AddContactMaterial, multibody∷CoulombFriction, - mbp_contact_modeling, mbp_dissipation_model. To be valid, either - both friction values must be populated, or neither. Friction - quantities are unitless.)"""; + mbp_contact_modeling, mbp_dissipation_model. Dynamic Coulomb + friction coefficient (unitless). + +To be valid, either both friction values must be populated, or +neither. When present, the value must be non-negative (``≥ 0``). +∞ is +allowed; NaN is not. Additional relationship constraints with +``static_friction`` are enforced by multibody∷CoulombFriction.)"""; } dynamic_friction; // Symbol: drake::geometry::DefaultProximityProperties::hunt_crossley_dissipation struct /* hunt_crossley_dissipation */ { @@ -933,6 +939,9 @@ R"""(Controls energy dissipation from contact, for contact approximations other than* multibody∷DiscreteContactApproximation∷kSap. Units are seconds per meter. +When present, the value must be non-negative (``≥ 0``). +∞ is allowed; +NaN is not. + If a non-deformable geometry is missing a value for dissipation, MultibodyPlant will generate a default value (based on multibody∷MultibodyPlantConfig∷penetration_allowance). However, this @@ -949,7 +958,11 @@ parameters specific to your model.)"""; struct /* hydroelastic_modulus */ { // Source: drake/geometry/scene_graph_config.h const char* doc = -R"""(A measure of material stiffness, in units of Pascals.)"""; +R"""(A measure of material stiffness, in units of Pascals. + +When present, the value must be strictly positive (``> 0``). +∞ is +allowed (it is mathematically equivalent to a rigid object); NaN is +not.)"""; } hydroelastic_modulus; // Symbol: drake::geometry::DefaultProximityProperties::margin struct /* margin */ { @@ -961,6 +974,9 @@ for contact resolution whenever their distance is within δ₁ + δ₂. That is, (speculative) contact constraints are added for objects at a distance smaller than δ₁+δ₂. +When present, the value must satisfy ``0 ≤ margin < ∞`` (finite and +non-negative). NaN and ±∞ are not allowed. + Refer to hydro_margin for further details, including theory, examples, recommended margin values and limitations. @@ -996,6 +1012,9 @@ Collision Detection. const char* doc = R"""(A measure of material stiffness, in units of Newtons per meter. +When present, the value must be strictly positive (``> 0``). +∞ is +allowed; NaN is not. + If a non-deformable geometry is missing a value for stiffness, MultibodyPlant will generate a default value (based on multibody∷MultibodyPlantConfig∷penetration_allowance). However, this @@ -1012,7 +1031,10 @@ parameters specific to your model.)"""; // Source: drake/geometry/scene_graph_config.h const char* doc = R"""(Controls energy damping from contact, *only for* -multibody∷DiscreteContactApproximation∷kSap. Units are seconds.)"""; +multibody∷DiscreteContactApproximation∷kSap. Units are seconds. + +When present, the value must satisfy ``0 ≤ relaxation_time < ∞`` +(finite and non-negative). NaN and ±∞ are not allowed.)"""; } relaxation_time; // Symbol: drake::geometry::DefaultProximityProperties::resolution_hint struct /* resolution_hint */ { @@ -1021,6 +1043,9 @@ multibody∷DiscreteContactApproximation∷kSap. Units are seconds.)"""; R"""(Controls how finely primitive geometries are tessellated, units of meters. +When present, the value must satisfy ``0 < resolution_hint < ∞`` +(finite and positive). NaN and ±∞ are not allowed. + While no single value is universally appropriate, this value was selected based on the following idea. We're attempting to make introducing novel manipulands as easy as possible. Considering a @@ -1033,13 +1058,18 @@ appropriate for contact with a compliant gripper.)"""; // Source: drake/geometry/scene_graph_config.h const char* doc = R"""(For a halfspace, the thickness of compliant material to model, in -units of meters.)"""; +units of meters. + +When present, the value must satisfy ``0 < slab_thickness < ∞`` +(finite and positive). NaN and ±∞ are not allowed.)"""; } slab_thickness; // Symbol: drake::geometry::DefaultProximityProperties::static_friction struct /* static_friction */ { // Source: drake/geometry/scene_graph_config.h const char* doc = -R"""(See also: +R"""(Static Coulomb friction coefficient (unitless). + +See also: dynamic_friction.)"""; } static_friction; auto Serialize__fields() const { diff --git a/geometry/proximity_engine.cc b/geometry/proximity_engine.cc index 73b63e0a9250..bcb2559149c9 100644 --- a/geometry/proximity_engine.cc +++ b/geometry/proximity_engine.cc @@ -1219,9 +1219,7 @@ class ProximityEngine::Impl : public ShapeReifier { unordered_map>* objects) { const double margin = props.GetPropertyOrDefault(kHydroGroup, kMargin, 0.0); - if (!(margin >= 0 && std::isfinite(margin))) { - throw std::logic_error("Margin must be non-negative and finite."); - } + ThrowIfInvalidMargin(margin); ReifyData data{nullptr, id, props, X_WG, margin}; shape.Reify(this, &data); diff --git a/geometry/proximity_properties.cc b/geometry/proximity_properties.cc index bea386c734c1..2549d4673983 100644 --- a/geometry/proximity_properties.cc +++ b/geometry/proximity_properties.cc @@ -1,6 +1,7 @@ #include "drake/geometry/proximity_properties.h" #include +#include #include namespace drake { @@ -79,6 +80,72 @@ std::string_view to_string(const HydroelasticType& type) { return EnumToChars(type); } +void ThrowIfInvalidHydroelasticModulus(double hydroelastic_modulus) { + // +∞ is allowed (equivalent to rigid); NaN is not (`!(nan > 0)`). + if (!(hydroelastic_modulus > 0)) { + throw std::logic_error( + fmt::format("The hydroelastic modulus must be positive; given {}", + hydroelastic_modulus)); + } +} + +void ThrowIfInvalidResolutionHint(double resolution_hint) { + if (!(std::isfinite(resolution_hint) && resolution_hint > 0)) { + throw std::logic_error( + fmt::format("The resolution_hint must be positive and finite; given {}", + resolution_hint)); + } +} + +void ThrowIfInvalidSlabThickness(double slab_thickness) { + if (!(std::isfinite(slab_thickness) && slab_thickness > 0)) { + throw std::logic_error( + fmt::format("The slab_thickness must be positive and finite; given {}", + slab_thickness)); + } +} + +void ThrowIfInvalidMargin(double margin) { + if (!(std::isfinite(margin) && margin >= 0)) { + throw std::logic_error(fmt::format( + "The margin must be non-negative and finite; given {}", margin)); + } +} + +void ThrowIfInvalidHuntCrossleyDissipation(double dissipation) { + // +∞ is allowed; NaN is not (`!(nan >= 0)`). + if (!(dissipation >= 0)) { + throw std::logic_error(fmt::format( + "The dissipation can't be negative; given {}", dissipation)); + } +} + +void ThrowIfInvalidRelaxationTime(double relaxation_time) { + if (!(std::isfinite(relaxation_time) && relaxation_time >= 0)) { + throw std::logic_error(fmt::format( + "The relaxation_time must be non-negative and finite; given {}", + relaxation_time)); + } +} + +void ThrowIfInvalidPointStiffness(double point_stiffness) { + // +∞ is allowed; NaN is not (`!(nan > 0)`). + if (!(point_stiffness > 0)) { + throw std::logic_error(fmt::format( + "The point_contact_stiffness must be strictly positive; given {}", + point_stiffness)); + } +} + +void ThrowIfInvalidFrictionCoefficient(double friction_coefficient) { + // +∞ is allowed; NaN is not (`!(nan >= 0)`). + if (!(friction_coefficient >= 0)) { + throw std::logic_error( + fmt::format("The friction coefficient can't be negative; given {}", + friction_coefficient)); + } +} + } // namespace internal void AddContactMaterial( @@ -87,20 +154,13 @@ void AddContactMaterial( ProximityProperties* properties) { DRAKE_DEMAND(properties != nullptr); if (dissipation.has_value()) { - if (*dissipation < 0) { - throw std::logic_error(fmt::format( - "The dissipation can't be negative; given {}", *dissipation)); - } + internal::ThrowIfInvalidHuntCrossleyDissipation(*dissipation); properties->AddProperty(internal::kMaterialGroup, internal::kHcDissipation, *dissipation); } if (point_stiffness.has_value()) { - if (*point_stiffness <= 0) { - throw std::logic_error(fmt::format( - "The point_contact_stiffness must be strictly positive; given {}", - *point_stiffness)); - } + internal::ThrowIfInvalidPointStiffness(*point_stiffness); properties->AddProperty(internal::kMaterialGroup, internal::kPointStiffness, *point_stiffness); } @@ -117,6 +177,7 @@ void AddContactMaterial( void AddRigidHydroelasticProperties(double resolution_hint, ProximityProperties* properties) { DRAKE_DEMAND(properties != nullptr); + internal::ThrowIfInvalidResolutionHint(resolution_hint); properties->AddProperty(internal::kHydroGroup, internal::kRezHint, resolution_hint); AddRigidHydroelasticProperties(properties); @@ -138,11 +199,7 @@ void AddCompliantHydroelasticProperties(double hydroelastic_modulus, // The bare minimum of defining a compliant geometry is to declare its // compliance type. Downstream consumers (ProximityEngine) will determine // if this is sufficient. - if (hydroelastic_modulus <= 0) { - throw std::logic_error( - fmt::format("The hydroelastic modulus must be positive; given {}", - hydroelastic_modulus)); - } + internal::ThrowIfInvalidHydroelasticModulus(hydroelastic_modulus); properties->AddProperty(internal::kHydroGroup, internal::kElastic, hydroelastic_modulus); properties->AddProperty(internal::kHydroGroup, internal::kComplianceType, @@ -154,6 +211,7 @@ void AddCompliantHydroelasticProperties(double resolution_hint, double hydroelastic_modulus, ProximityProperties* properties) { DRAKE_DEMAND(properties != nullptr); + internal::ThrowIfInvalidResolutionHint(resolution_hint); properties->AddProperty(internal::kHydroGroup, internal::kRezHint, resolution_hint); AddCompliantHydroelasticProperties(hydroelastic_modulus, properties); @@ -163,6 +221,7 @@ void AddCompliantHydroelasticPropertiesForHalfSpace( double slab_thickness, double hydroelastic_modulus, ProximityProperties* properties) { DRAKE_DEMAND(properties != nullptr); + internal::ThrowIfInvalidSlabThickness(slab_thickness); properties->AddProperty(internal::kHydroGroup, internal::kSlabThickness, slab_thickness); AddCompliantHydroelasticProperties(hydroelastic_modulus, properties); diff --git a/geometry/proximity_properties.h b/geometry/proximity_properties.h index bf4954ebef39..9fdce78ac09c 100644 --- a/geometry/proximity_properties.h +++ b/geometry/proximity_properties.h @@ -95,6 +95,50 @@ std::string GetStringFromHydroelasticType(HydroelasticType hydroelastic_type); /* String conversion for debug-printing hydroelastic type. */ std::string_view to_string(const HydroelasticType& type); +/* @name Validating proximity property numeric values + + These helpers are the single enforcement point for the numeric ranges of + proximity properties. They are used by DefaultProximityProperties:: + ValidateOrThrow() and by the public Add* helpers below. Valid ranges + (including NaN/∞ disposition) are documented on the corresponding fields of + DefaultProximityProperties and on the Add* APIs that accept these values. + */ +//@{ + +/* @throws std::exception unless `hydroelastic_modulus` > 0. + +∞ is allowed; NaN is not. */ +void ThrowIfInvalidHydroelasticModulus(double hydroelastic_modulus); + +/* @throws std::exception unless 0 < `resolution_hint` < ∞. + NaN and ±∞ are not allowed. */ +void ThrowIfInvalidResolutionHint(double resolution_hint); + +/* @throws std::exception unless 0 < `slab_thickness` < ∞. + NaN and ±∞ are not allowed. */ +void ThrowIfInvalidSlabThickness(double slab_thickness); + +/* @throws std::exception unless 0 ≤ `margin` < ∞. + NaN and ±∞ are not allowed. */ +void ThrowIfInvalidMargin(double margin); + +/* @throws std::exception unless `dissipation` ≥ 0. + +∞ is allowed; NaN is not. */ +void ThrowIfInvalidHuntCrossleyDissipation(double dissipation); + +/* @throws std::exception unless 0 ≤ `relaxation_time` < ∞. + NaN and ±∞ are not allowed. */ +void ThrowIfInvalidRelaxationTime(double relaxation_time); + +/* @throws std::exception unless `point_stiffness` > 0. + +∞ is allowed; NaN is not. */ +void ThrowIfInvalidPointStiffness(double point_stiffness); + +/* @throws std::exception unless `friction_coefficient` ≥ 0. + +∞ is allowed; NaN is not. */ +void ThrowIfInvalidFrictionCoefficient(double friction_coefficient); + +//@} + } // namespace internal /** @@ -106,9 +150,10 @@ std::string_view to_string(const HydroelasticType& type); * Downstream consumers of the contact materials can optionally provide * defaults for missing properties. * - * @throws std::exception if `dissipation` is negative, `point_stiffness` is - * not positive, of any of the contact material properties have already been - * defined in `properties`. + * @throws std::exception if `dissipation` is present but not ≥ 0 (NaN is + * rejected; +∞ is allowed), if `point_stiffness` is present but not > 0 (NaN + * is rejected; +∞ is allowed), or if any of the contact material properties + * have already been defined in `properties`. * @pre `properties` is not nullptr. */ void AddContactMaterial( @@ -125,11 +170,13 @@ void AddContactMaterial( roughly corresponds to a typical edge length in the resulting mesh. See @ref hug_properties. This will be ignored for geometry types that don't - require tessellation. + require tessellation. Must satisfy + 0 < `resolution_hint` < ∞ (NaN and ±∞ rejected). @param[in,out] properties The properties will be added to this property set. - @throws std::exception If `properties` already has properties with the + @throws std::exception If `resolution_hint` is invalid or if + `properties` already has properties with the names that this function would need to add. - @pre 0 < `resolution_hint` < ∞ and `properties` is not nullptr. */ + @pre `properties` is not nullptr. */ void AddRigidHydroelasticProperties(double resolution_hint, ProximityProperties* properties); @@ -149,14 +196,17 @@ void AddRigidHydroelasticProperties(ProximityProperties* properties); roughly corresponds to a typical edge length in the resulting mesh. See @ref hug_properties. This will be ignored for geometry types that don't - require tessellation. + require tessellation. Must satisfy + 0 < `resolution_hint` < ∞ (NaN and ±∞ rejected). @param hydroelastic_modulus A multiplier that maps penetration to pressure. See - @ref hug_properties. + @ref hug_properties. Must be > 0 (+∞ allowed; NaN + rejected). @param[in,out] properties The properties will be added to this property set. - @throws std::exception If `properties` already has properties with the - names that this function would need to add. - @pre 0 < `resolution_hint` < ∞, 0 < `hydroelastic_modulus`, and `properties` - is not nullptr. */ + @throws std::exception If `resolution_hint` or `hydroelastic_modulus` is + invalid, or if `properties` already has properties + with the names that this function would need to + add. + @pre `properties` is not nullptr. */ void AddCompliantHydroelasticProperties(double resolution_hint, double hydroelastic_modulus, ProximityProperties* properties); @@ -167,14 +217,16 @@ void AddCompliantHydroelasticProperties(double resolution_hint, @param slab_thickness The distance from the half space boundary to its rigid core (this helps define the extent field of - the half space). + the half space). Must satisfy + 0 < `slab_thickness` < ∞ (NaN and ±∞ rejected). @param hydroelastic_modulus A multiplier that maps penetration to pressure. See - @ref hug_properties. + @ref hug_properties. Must be > 0 (+∞ allowed; NaN + rejected). @param[out] properties The properties will be added to this property set. - @throws std::exception If `properties` already has properties with the names - that this function would need to add. - @pre 0 < `slab_thickness` < ∞, 0 < `hydroelastic_modulus`, and `properties` - is not nullptr. */ + @throws std::exception If `slab_thickness` or `hydroelastic_modulus` is + invalid, or if `properties` already has properties with + the names that this function would need to add. + @pre `properties` is not nullptr. */ void AddCompliantHydroelasticPropertiesForHalfSpace( double slab_thickness, double hydroelastic_modulus, ProximityProperties* properties); diff --git a/geometry/scene_graph_config.cc b/geometry/scene_graph_config.cc index cea78e455729..05acad942c1e 100644 --- a/geometry/scene_graph_config.cc +++ b/geometry/scene_graph_config.cc @@ -1,6 +1,7 @@ #include "drake/geometry/scene_graph_config.h" -#include +#include +#include #include "drake/geometry/proximity_properties.h" #include "drake/multibody/plant/coulomb_friction.h" @@ -10,52 +11,20 @@ namespace geometry { namespace { -// Conditions, that if not met, could trigger an exception. -// TODO(#21167) NaN values are not consistently accounted for. -enum Condition { - kPositiveFinite, - kNonNegativeFinite, - kPositive, - kNonNegative, -}; - -// Check the value (if present) of `name`d `property` for `condition`. If the -// value is present and the condition is not met, throw an exception with a -// nice message. +// Check the value (if present) of `name`d `property` using `validate`. If the +// value is present and invalid, rethrow with a SceneGraphConfig-prefixed +// message that includes the property name. void ThrowUnlessAbsentOr(std::string_view name, std::optional property, - Condition condition) { + void (*validate)(double)) { if (!property.has_value()) { return; } - double value = *property; - std::string_view condition_name; - bool should_throw{false}; - switch (condition) { - case kPositiveFinite: { - should_throw = (!std::isfinite(value) || value <= 0.0); - condition_name = "positive, finite"; - break; - } - case kNonNegativeFinite: { - should_throw = (!std::isfinite(value) || value < 0.0); - condition_name = "non-negative, finite"; - break; - } - case kPositive: { - should_throw = (value <= 0.0); - condition_name = "positive"; - break; - } - case kNonNegative: { - should_throw = (value < 0.0); - condition_name = "non-negative"; - break; - } - } - if (should_throw) { + try { + validate(*property); + } catch (const std::exception& e) { throw std::logic_error(fmt::format( - "Invalid scene graph configuration: '{}' ({}) must be a {} value.", - name, value, condition_name)); + "Invalid scene graph configuration: '{}' ({}) is invalid. {}", name, + *property, e.what())); } } @@ -66,17 +35,19 @@ void DefaultProximityProperties::ValidateOrThrow() const { internal::GetHydroelasticTypeFromString(compliance_type); // Use a macro to capture both property name and value. -#define DRAKE_ENFORCE(prop, cond) ThrowUnlessAbsentOr(#prop, prop, cond) - DRAKE_ENFORCE(hydroelastic_modulus, kPositive); - DRAKE_ENFORCE(resolution_hint, kPositiveFinite); - DRAKE_ENFORCE(slab_thickness, kPositiveFinite); - DRAKE_ENFORCE(margin, kNonNegativeFinite); +#define DRAKE_ENFORCE(prop, validate) \ + ThrowUnlessAbsentOr(#prop, prop, &internal::validate) + DRAKE_ENFORCE(hydroelastic_modulus, ThrowIfInvalidHydroelasticModulus); + DRAKE_ENFORCE(resolution_hint, ThrowIfInvalidResolutionHint); + DRAKE_ENFORCE(slab_thickness, ThrowIfInvalidSlabThickness); + DRAKE_ENFORCE(margin, ThrowIfInvalidMargin); - DRAKE_ENFORCE(dynamic_friction, kNonNegative); - DRAKE_ENFORCE(static_friction, kNonNegative); - DRAKE_ENFORCE(hunt_crossley_dissipation, kNonNegative); - DRAKE_ENFORCE(relaxation_time, kNonNegativeFinite); - DRAKE_ENFORCE(point_stiffness, kPositive); + DRAKE_ENFORCE(dynamic_friction, ThrowIfInvalidFrictionCoefficient); + DRAKE_ENFORCE(static_friction, ThrowIfInvalidFrictionCoefficient); + DRAKE_ENFORCE(hunt_crossley_dissipation, + ThrowIfInvalidHuntCrossleyDissipation); + DRAKE_ENFORCE(relaxation_time, ThrowIfInvalidRelaxationTime); + DRAKE_ENFORCE(point_stiffness, ThrowIfInvalidPointStiffness); #undef DRAKE_ENFORCE // Require either both friction quantities or neither. diff --git a/geometry/scene_graph_config.h b/geometry/scene_graph_config.h index 98172388b044..6fe78ceede56 100644 --- a/geometry/scene_graph_config.h +++ b/geometry/scene_graph_config.h @@ -31,13 +31,12 @@ struct DefaultProximityProperties { } /** @name Hydroelastic Contact Properties - These properties affect hydroelastic contact only. For more detail, including - limits of the numeric parameters, @see - geometry::AddRigidHydroelasticProperties, + These properties affect hydroelastic contact only. Valid ranges for each + numeric parameter are documented on the corresponding field below. + @see geometry::AddRigidHydroelasticProperties, geometry::AddCompliantHydroelasticProperties, - geometry::AddCompliantHydroelasticPropertiesForHalfSpace. - - For more context, @see @ref hug_properties. */ + geometry::AddCompliantHydroelasticPropertiesForHalfSpace, + @ref hug_properties. */ /// @{ /** There are three valid options for `compliance_type`: - "undefined": hydroelastic contact will not be used. @@ -48,12 +47,18 @@ struct DefaultProximityProperties { - "compliant": the default hydroelastic compliance type will be compliant. */ std::string compliance_type{"undefined"}; - /** A measure of material stiffness, in units of Pascals. */ + /** A measure of material stiffness, in units of Pascals. + + When present, the value must be strictly positive (`> 0`). +∞ is allowed (it + is mathematically equivalent to a rigid object); NaN is not. */ std::optional hydroelastic_modulus{1e7}; /** Controls how finely primitive geometries are tessellated, units of meters. + When present, the value must satisfy `0 < resolution_hint < ∞` (finite and + positive). NaN and ±∞ are not allowed. + While no single value is universally appropriate, this value was selected based on the following idea. We're attempting to make introducing novel manipulands as easy as possible. Considering a simple soup can as a @@ -63,7 +68,10 @@ struct DefaultProximityProperties { std::optional resolution_hint{0.02}; /** For a halfspace, the thickness of compliant material to model, in units - of meters. */ + of meters. + + When present, the value must satisfy `0 < slab_thickness < ∞` (finite and + positive). NaN and ±∞ are not allowed. */ std::optional slab_thickness; /** (Advanced) Specifies a thin layer of thickness "margin" (in meters) around @@ -71,6 +79,9 @@ struct DefaultProximityProperties { resolution whenever their distance is within δ₁ + δ₂. That is, (speculative) contact constraints are added for objects at a distance smaller than δ₁+δ₂. + When present, the value must satisfy `0 ≤ margin < ∞` (finite and + non-negative). NaN and ±∞ are not allowed. + Refer to @ref hydro_margin for further details, including theory, examples, recommended margin values and limitations. @@ -100,21 +111,29 @@ struct DefaultProximityProperties { /** @name General Contact Properties - These properties affect contact in general. For more detail, including limits - of the numeric parameters, @see geometry::AddContactMaterial, - multibody::CoulombFriction, @ref mbp_contact_modeling, @ref - mbp_dissipation_model. */ + These properties affect contact in general. Valid ranges for each numeric + parameter are documented on the corresponding field below. + @see geometry::AddContactMaterial, multibody::CoulombFriction, + @ref mbp_contact_modeling, @ref mbp_dissipation_model. */ /// @{ - /** To be valid, either both friction values must be populated, or - neither. Friction quantities are unitless. */ + /** Dynamic Coulomb friction coefficient (unitless). + + To be valid, either both friction values must be populated, or neither. When + present, the value must be non-negative (`≥ 0`). +∞ is allowed; NaN is not. + Additional relationship constraints with `static_friction` are enforced by + multibody::CoulombFriction. */ std::optional dynamic_friction{0.5}; - /** @see dynamic_friction. */ + /** Static Coulomb friction coefficient (unitless). + @see dynamic_friction. */ std::optional static_friction{0.5}; /** Controls energy dissipation from contact, for contact approximations *other than* multibody::DiscreteContactApproximation::kSap. Units are seconds per meter. + When present, the value must be non-negative (`≥ 0`). +∞ is allowed; NaN is + not. + If a non-deformable geometry is missing a value for dissipation, MultibodyPlant will generate a default value (based on multibody::MultibodyPlantConfig::penetration_allowance). However, this @@ -128,20 +147,26 @@ struct DefaultProximityProperties { std::optional hunt_crossley_dissipation{50.0}; /** Controls energy damping from contact, *only for* - multibody::DiscreteContactApproximation::kSap. Units are seconds. */ + multibody::DiscreteContactApproximation::kSap. Units are seconds. + + When present, the value must satisfy `0 ≤ relaxation_time < ∞` (finite and + non-negative). NaN and ±∞ are not allowed. */ std::optional relaxation_time{0.1}; /// @} /** @name Point Contact Properties - These properties affect point contact only. For complete descriptions of - the numeric parameters, See - @ref point_forces_modeling "Compliant Point Contact Forces", + These properties affect point contact only. Valid ranges for each numeric + parameter are documented on the corresponding field below. + @see @ref point_forces_modeling "Compliant Point Contact Forces", geometry::AddContactMaterial. */ /// @{ /** A measure of material stiffness, in units of Newtons per meter. + When present, the value must be strictly positive (`> 0`). +∞ is allowed; NaN + is not. + If a non-deformable geometry is missing a value for stiffness, MultibodyPlant will generate a default value (based on multibody::MultibodyPlantConfig::penetration_allowance). However, this diff --git a/geometry/test/proximity_properties_test.cc b/geometry/test/proximity_properties_test.cc index 22b6e31a03b7..aa5d4a72a6c6 100644 --- a/geometry/test/proximity_properties_test.cc +++ b/geometry/test/proximity_properties_test.cc @@ -1,5 +1,7 @@ #include "drake/geometry/proximity_properties.h" +#include +#include #include #include @@ -24,6 +26,9 @@ using internal::kRezHint; using internal::kSlabThickness; using CoulombFrictiond = multibody::CoulombFriction; +constexpr double kInf = std::numeric_limits::infinity(); +constexpr double kNan = std::numeric_limits::quiet_NaN(); + GTEST_TEST(ProximityPropertiesTest, AddContactMaterial) { const double d = 0.1; const double ps = 250.0; @@ -75,6 +80,13 @@ GTEST_TEST(ProximityPropertiesTest, AddContactMaterial) { ".+dissipation can't be negative.+"); } + // Error case: NaN dissipation. + { + ProximityProperties p; + DRAKE_EXPECT_THROWS_MESSAGE(AddContactMaterial(kNan, ps, mu, &p), + ".+dissipation can't be negative.+"); + } + // Error case: negative stiffness. { ProximityProperties p; @@ -88,6 +100,13 @@ GTEST_TEST(ProximityPropertiesTest, AddContactMaterial) { DRAKE_EXPECT_THROWS_MESSAGE(AddContactMaterial(d, 0, mu, &p), ".+stiffness must be strictly positive.+"); } + + // Error case: NaN stiffness. + { + ProximityProperties p; + DRAKE_EXPECT_THROWS_MESSAGE(AddContactMaterial(d, kNan, mu, &p), + ".+stiffness must be strictly positive.+"); + } } GTEST_TEST(ProximityPropertiesTest, AddRigidProperties) { @@ -101,6 +120,22 @@ GTEST_TEST(ProximityPropertiesTest, AddRigidProperties) { EXPECT_EQ(props.GetProperty(kHydroGroup, kRezHint), length); } + { + ProximityProperties props; + DRAKE_EXPECT_THROWS_MESSAGE(AddRigidHydroelasticProperties(0.0, &props), + ".*resolution_hint must be positive and " + "finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE(AddRigidHydroelasticProperties(-1.0, &props), + ".*resolution_hint must be positive and " + "finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE(AddRigidHydroelasticProperties(kNan, &props), + ".*resolution_hint must be positive and " + "finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE(AddRigidHydroelasticProperties(kInf, &props), + ".*resolution_hint must be positive and " + "finite.*"); + } + ProximityProperties props; AddRigidHydroelasticProperties(&props); EXPECT_TRUE(props.HasProperty(kHydroGroup, kComplianceType)); @@ -124,6 +159,12 @@ void CheckDisallowedModulusValues( DRAKE_EXPECT_THROWS_MESSAGE(function_to_test(-1.3, &p), ".+elastic modulus must be positive.+"); } + // Error case: NaN hydroelastic modulus. + { + ProximityProperties p; + DRAKE_EXPECT_THROWS_MESSAGE(function_to_test(kNan, &p), + ".+elastic modulus must be positive.+"); + } } // Tests the variant where the static pressure is given explicitly. This doesn't @@ -144,6 +185,25 @@ GTEST_TEST(ProximityPropertiesTest, AddCompliantProperties) { EXPECT_EQ(props.GetProperty(kHydroGroup, kElastic), E); } + // +∞ modulus is allowed. + { + ProximityProperties props; + EXPECT_NO_THROW(AddCompliantHydroelasticProperties(1.0, kInf, &props)); + } + + { + ProximityProperties props; + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticProperties(0.0, E, &props), + ".*resolution_hint must be positive and finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticProperties(kNan, E, &props), + ".*resolution_hint must be positive and finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticProperties(kInf, E, &props), + ".*resolution_hint must be positive and finite.*"); + } + CheckDisallowedModulusValues("AddCompliantHydroelasticProperties", [](double modulus, ProximityProperties* p) { AddCompliantHydroelasticProperties(1., modulus, @@ -167,6 +227,19 @@ GTEST_TEST(ProximityPropertiesTest, AddHalfSpaceCompliantProperties) { HydroelasticType::kCompliant); } + { + ProximityProperties props; + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticPropertiesForHalfSpace(0.0, E, &props), + ".*slab_thickness must be positive and finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticPropertiesForHalfSpace(kNan, E, &props), + ".*slab_thickness must be positive and finite.*"); + DRAKE_EXPECT_THROWS_MESSAGE( + AddCompliantHydroelasticPropertiesForHalfSpace(kInf, E, &props), + ".*slab_thickness must be positive and finite.*"); + } + CheckDisallowedModulusValues("AddCompliantHydroelasticPropertiesForHalfSpace", [](double modulus, ProximityProperties* p) { AddCompliantHydroelasticPropertiesForHalfSpace( diff --git a/geometry/test/scene_graph_config_test.cc b/geometry/test/scene_graph_config_test.cc index 07a059381b06..2812c872631c 100644 --- a/geometry/test/scene_graph_config_test.cc +++ b/geometry/test/scene_graph_config_test.cc @@ -14,6 +14,9 @@ namespace { using yaml::LoadYamlString; using yaml::SaveYamlString; +constexpr double kInf = std::numeric_limits::infinity(); +constexpr double kNan = std::numeric_limits::quiet_NaN(); + const char* const kExampleConfig = R"""( default_proximity_properties: compliance_type: compliant @@ -61,10 +64,21 @@ GTEST_TEST(SceneGraphConfigTest, ValidateModulus) { props.hydroelastic_modulus = 0; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'hydroelastic_modulus' \\(0\\) must be a positive value."); - props.hydroelastic_modulus = std::numeric_limits::quiet_NaN(); - // TODO(#21167) document a disposition for NaN. + "Invalid scene graph configuration: 'hydroelastic_modulus' \\(0\\) is " + "invalid. The hydroelastic modulus must be positive; given 0"); + props.hydroelastic_modulus = -1; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'hydroelastic_modulus' \\(-1\\) is " + "invalid. The hydroelastic modulus must be positive; given -1"); + props.hydroelastic_modulus = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'hydroelastic_modulus' \\(nan\\) is " + "invalid. The hydroelastic modulus must be positive; given nan"); + // +∞ is intentionally allowed for hydroelastic modulus. + props.hydroelastic_modulus = kInf; + EXPECT_NO_THROW(config.ValidateOrThrow()); } GTEST_TEST(SceneGraphConfigTest, ValidateRezHint) { @@ -73,13 +87,18 @@ GTEST_TEST(SceneGraphConfigTest, ValidateRezHint) { props.resolution_hint = 0; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'resolution_hint' \\(0\\) must be a positive, finite value."); - props.resolution_hint = std::numeric_limits::quiet_NaN(); + "Invalid scene graph configuration: 'resolution_hint' \\(0\\) is " + "invalid. The resolution_hint must be positive and finite; given 0"); + props.resolution_hint = kNan; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'resolution_hint' \\(nan\\) must be a positive, finite value."); + "Invalid scene graph configuration: 'resolution_hint' \\(nan\\) is " + "invalid. The resolution_hint must be positive and finite; given nan"); + props.resolution_hint = kInf; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'resolution_hint' \\(inf\\) is " + "invalid. The resolution_hint must be positive and finite; given inf"); } GTEST_TEST(SceneGraphConfigTest, ValidateSlabThickness) { @@ -88,25 +107,57 @@ GTEST_TEST(SceneGraphConfigTest, ValidateSlabThickness) { props.slab_thickness = 0; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'slab_thickness' \\(0\\) must be a positive, finite value."); - props.slab_thickness = std::numeric_limits::quiet_NaN(); + "Invalid scene graph configuration: 'slab_thickness' \\(0\\) is " + "invalid. The slab_thickness must be positive and finite; given 0"); + props.slab_thickness = kNan; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'slab_thickness' \\(nan\\) must be a positive, finite value."); + "Invalid scene graph configuration: 'slab_thickness' \\(nan\\) is " + "invalid. The slab_thickness must be positive and finite; given nan"); + props.slab_thickness = kInf; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'slab_thickness' \\(inf\\) is " + "invalid. The slab_thickness must be positive and finite; given inf"); +} + +GTEST_TEST(SceneGraphConfigTest, ValidateMargin) { + SceneGraphConfig config; + auto& props = config.default_proximity_properties; + props.margin = -1; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'margin' \\(-1\\) is " + "invalid. The margin must be non-negative and finite; given -1"); + props.margin = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'margin' \\(nan\\) is " + "invalid. The margin must be non-negative and finite; given nan"); + props.margin = kInf; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'margin' \\(inf\\) is " + "invalid. The margin must be non-negative and finite; given inf"); + props.margin = 0; + EXPECT_NO_THROW(config.ValidateOrThrow()); } GTEST_TEST(SceneGraphConfigTest, ValidateDynamicFriction) { SceneGraphConfig config; auto& props = config.default_proximity_properties; + // Keep static >= dynamic so CoulombFriction isn't the failure mode. + props.static_friction = 10; props.dynamic_friction = -1; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'dynamic_friction' \\(-1\\) must be a non-negative value."); - props.dynamic_friction = std::numeric_limits::quiet_NaN(); - // TODO(#21167) document a disposition for NaN. + "Invalid scene graph configuration: 'dynamic_friction' \\(-1\\) is " + "invalid. The friction coefficient can't be negative; given -1"); + props.dynamic_friction = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'dynamic_friction' \\(nan\\) is " + "invalid. The friction coefficient can't be negative; given nan"); } GTEST_TEST(SceneGraphConfigTest, ValidateStaticFriction) { @@ -115,10 +166,13 @@ GTEST_TEST(SceneGraphConfigTest, ValidateStaticFriction) { props.static_friction = -1; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'static_friction' \\(-1\\) must be a non-negative value."); - props.static_friction = std::numeric_limits::quiet_NaN(); - // TODO(#21167) document a disposition for NaN. + "Invalid scene graph configuration: 'static_friction' \\(-1\\) is " + "invalid. The friction coefficient can't be negative; given -1"); + props.static_friction = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'static_friction' \\(nan\\) is " + "invalid. The friction coefficient can't be negative; given nan"); } GTEST_TEST(SceneGraphConfigTest, ValidateHuntCrossley) { @@ -127,11 +181,13 @@ GTEST_TEST(SceneGraphConfigTest, ValidateHuntCrossley) { props.hunt_crossley_dissipation = -1; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'hunt_crossley_dissipation' \\(-1\\) must be a non-negative" - " value."); - props.hunt_crossley_dissipation = std::numeric_limits::quiet_NaN(); - // TODO(#21167) document a disposition for NaN. + "Invalid scene graph configuration: 'hunt_crossley_dissipation' " + "\\(-1\\) is invalid. The dissipation can't be negative; given -1"); + props.hunt_crossley_dissipation = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'hunt_crossley_dissipation' " + "\\(nan\\) is invalid. The dissipation can't be negative; given nan"); } GTEST_TEST(SceneGraphConfigTest, ValidateRelaxationTime) { @@ -140,13 +196,21 @@ GTEST_TEST(SceneGraphConfigTest, ValidateRelaxationTime) { props.relaxation_time = -1; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'relaxation_time' \\(-1\\) must be a non-negative, finite value."); - props.relaxation_time = std::numeric_limits::quiet_NaN(); + "Invalid scene graph configuration: 'relaxation_time' \\(-1\\) is " + "invalid. The relaxation_time must be non-negative and finite; given " + "-1"); + props.relaxation_time = kNan; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'relaxation_time' \\(nan\\) must be a non-negative, finite value."); + "Invalid scene graph configuration: 'relaxation_time' \\(nan\\) is " + "invalid. The relaxation_time must be non-negative and finite; given " + "nan"); + props.relaxation_time = kInf; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'relaxation_time' \\(inf\\) is " + "invalid. The relaxation_time must be non-negative and finite; given " + "inf"); } GTEST_TEST(SceneGraphConfigTest, ValidatePointStiffness) { @@ -155,10 +219,24 @@ GTEST_TEST(SceneGraphConfigTest, ValidatePointStiffness) { props.point_stiffness = -1; DRAKE_EXPECT_THROWS_MESSAGE( config.ValidateOrThrow(), - "Invalid scene graph configuration:" - " 'point_stiffness' \\(-1\\) must be a positive value."); - props.point_stiffness = std::numeric_limits::quiet_NaN(); - // TODO(#21167) document a disposition for NaN. + "Invalid scene graph configuration: 'point_stiffness' \\(-1\\) is " + "invalid. The point_contact_stiffness must be strictly positive; given " + "-1"); + props.point_stiffness = 0; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'point_stiffness' \\(0\\) is " + "invalid. The point_contact_stiffness must be strictly positive; given " + "0"); + props.point_stiffness = kNan; + DRAKE_EXPECT_THROWS_MESSAGE( + config.ValidateOrThrow(), + "Invalid scene graph configuration: 'point_stiffness' \\(nan\\) is " + "invalid. The point_contact_stiffness must be strictly positive; given " + "nan"); + // +∞ is intentionally allowed for point stiffness. + props.point_stiffness = kInf; + EXPECT_NO_THROW(config.ValidateOrThrow()); } GTEST_TEST(SceneGraphConfigTest, ValidateCoulombFriction) {