1use std::fmt;
9
10use crate::component::capability::{CapabilityKind, CapabilityRole, StructuralKind};
11use crate::identity::{
12 CapabilityId, CapabilityRef, ComponentInstanceId, ComponentTypeId, JointId, LinkId,
13 MODULE_INSTANCE_SEPARATOR,
14};
15use crate::robot::KinematicKind;
16use crate::structure::JointKind;
17
18#[derive(Debug, thiserror::Error)]
20pub enum ModelError {
21 #[error("{kind} '{value}' is not normalized")]
23 NotNormalized { kind: IdentifierKind, value: String },
24
25 #[error("{kind} '{value}' must not contain reserved separator '{MODULE_INSTANCE_SEPARATOR}'")]
28 ReservedSeparator { kind: IdentifierKind, value: String },
29
30 #[error("capability reference '{value}' must use component.capability")]
32 MalformedCapabilityReference { value: String },
33
34 #[error("invalid logical asset id '{value}'")]
36 MalformedAssetId { value: String },
37
38 #[error("unknown capability '{reference}'")]
40 UnknownCapability { reference: CapabilityRef },
41
42 #[error("capability '{reference}' must reference a {expected}, found {actual}")]
44 CapabilityKindMismatch {
45 reference: CapabilityRef,
46 expected: CapabilityKind,
47 actual: CapabilityKind,
48 },
49
50 #[error("capability '{reference}' must target a {expected}")]
53 CapabilityTargetKind {
54 reference: CapabilityRef,
55 expected: StructuralKind,
56 },
57
58 #[error("{kind} target '{id}' for capability '{reference}' not found")]
61 UnknownBoundTarget {
62 reference: CapabilityRef,
63 kind: StructuralKind,
64 id: String,
65 },
66
67 #[error(
70 "component type '{component_type}' capability '{capability_id}' references unknown {kind} '{id}'"
71 )]
72 UnknownDeclaredTarget {
73 component_type: ComponentTypeId,
74 capability_id: CapabilityId,
75 kind: StructuralKind,
76 id: String,
77 },
78
79 #[error("component map identity '{key}' does not match embedded id '{embedded}'")]
81 ComponentIdentityMismatch {
82 key: ComponentInstanceId,
83 embedded: ComponentInstanceId,
84 },
85
86 #[error("component '{instance}' references unknown component type '{component_type}'")]
88 UnknownComponentType {
89 instance: ComponentInstanceId,
90 component_type: ComponentTypeId,
91 },
92
93 #[error("component '{instance}' references unknown mount link '{link}'")]
95 UnknownMountLink {
96 instance: ComponentInstanceId,
97 link: LinkId,
98 },
99
100 #[error("component '{instance}' capability '{capability_id}' direction sign must be -1 or 1")]
102 DirectionSign {
103 instance: ComponentInstanceId,
104 capability_id: CapabilityId,
105 value: i8,
106 },
107
108 #[error(
110 "component '{instance}' direction sign references unknown capability '{capability_id}'"
111 )]
112 UnknownDirectionSignCapability {
113 instance: ComponentInstanceId,
114 capability_id: CapabilityId,
115 },
116
117 #[error(
120 "component '{instance}' role assignment references unknown capability '{capability_id}'"
121 )]
122 UnknownRoleCapability {
123 instance: ComponentInstanceId,
124 capability_id: CapabilityId,
125 },
126
127 #[error("component '{instance}' capability '{capability_id}' has an empty role assignment")]
129 EmptyCapabilityRoles {
130 instance: ComponentInstanceId,
131 capability_id: CapabilityId,
132 },
133
134 #[error("component '{instance}' capability '{capability_id}' repeats role '{role}'")]
136 DuplicateCapabilityRole {
137 instance: ComponentInstanceId,
138 capability_id: CapabilityId,
139 role: CapabilityRole,
140 },
141
142 #[error("simulation type '{component_type}' has no matching component type")]
144 SimulationWithoutComponentType { component_type: ComponentTypeId },
145
146 #[error("simulation capability '{component_type}.{capability_id}' has no component capability")]
148 SimulationWithoutCapability {
149 component_type: ComponentTypeId,
150 capability_id: CapabilityId,
151 },
152
153 #[error(
156 "simulation capability '{component_type}.{capability_id}' kind does not match component"
157 )]
158 SimulationCapabilityKindMismatch {
159 component_type: ComponentTypeId,
160 capability_id: CapabilityId,
161 simulated: CapabilityKind,
162 declared: CapabilityKind,
163 },
164
165 #[error("{owner} joint '{joint}' uses unsupported runtime kind '{kind:?}'")]
167 UnsupportedJointKind {
168 owner: JointOwner,
169 joint: JointId,
170 kind: JointKind,
171 },
172
173 #[error("motion {field} must be finite, positive, and fit in f32")]
178 MotionLimit { field: MotionLimitField },
179
180 #[error("{kinematics} {field} must be finite and positive")]
182 KinematicScalar {
183 kinematics: KinematicKind,
184 field: KinematicScalarField,
185 },
186
187 #[error("stock safety footprint cannot include movable joint '{joint}'")]
190 FootprintMovableJoint { joint: JointId },
191
192 #[error("stock safety footprint does not support mesh collision on link '{link}'")]
195 FootprintMesh { link: LinkId },
196
197 #[error("stock safety footprint contains a non-finite value")]
199 FootprintNonFinite,
200
201 #[error("stock safety footprint radius must be finite and positive")]
204 FootprintRadius,
205
206 #[error(transparent)]
208 Structure(#[from] StructureError),
209}
210
211impl From<phoxal_runtime_contract::identity::TopologyIdError> for ModelError {
212 fn from(error: phoxal_runtime_contract::identity::TopologyIdError) -> Self {
213 use phoxal_runtime_contract::identity::TopologyIdError;
214 match error {
215 TopologyIdError::Robot(value) => Self::NotNormalized {
216 kind: IdentifierKind::RobotId,
217 value,
218 },
219 TopologyIdError::ComponentInstance(value) => Self::NotNormalized {
220 kind: IdentifierKind::ComponentInstance,
221 value,
222 },
223 }
224 }
225}
226
227#[derive(Debug, thiserror::Error)]
229pub enum StructureError {
230 #[error("duplicate {kind} identity '{name}'")]
232 DuplicateIdentity { kind: StructuralKind, name: String },
233
234 #[error("joint '{joint}' references unknown {role} link '{link}'")]
236 UnknownJointLink {
237 joint: JointId,
238 role: LinkRole,
239 link: LinkId,
240 },
241
242 #[error("link '{link}' is the child of multiple joints")]
244 MultipleParentJoints { link: LinkId },
245
246 #[error("joint '{joint}' cannot use '{link}' as both parent and child")]
248 SelfReferentialJoint { joint: JointId, link: LinkId },
249
250 #[error("structure must have exactly one root link, found {found}")]
252 RootLinkCount { found: usize },
253
254 #[error("structure contains a joint cycle involving '{link}'")]
256 JointCycle { link: LinkId },
257
258 #[error("{owner} pose must be finite")]
260 Pose { owner: PoseOwner },
261
262 #[error("link '{link}' mass must be finite and non-negative")]
264 Mass { link: LinkId },
265
266 #[error("link '{link}' inertia must be finite and positive semidefinite")]
269 Inertia { link: LinkId },
270
271 #[error("link '{link}' geometry dimensions must be finite and positive")]
273 Geometry { link: LinkId },
274
275 #[error("joint '{joint}' axis must be finite")]
277 AxisNotFinite { joint: JointId },
278
279 #[error("joint '{joint}' axis must be non-zero")]
281 AxisNotOriented { joint: JointId },
282
283 #[error("joint '{joint}' limits must be finite with lower <= upper")]
285 JointLimits { joint: JointId },
286
287 #[error("joint '{joint}' dynamics must be finite and non-negative")]
289 JointDynamics { joint: JointId },
290
291 #[error("joint '{joint}' mimics unknown joint '{mimicked}'")]
293 UnknownMimicJoint { joint: JointId, mimicked: JointId },
294
295 #[error("joint '{joint}' safety limits must be finite with lower <= upper")]
297 JointSafety { joint: JointId },
298
299 #[error("structure root link must be '{expected}', found '{found}'")]
301 RootLinkName { expected: LinkId, found: LinkId },
302
303 #[error("structure must attach 'base_link' under 'base_footprint' with a fixed joint")]
305 MissingBaseLink,
306
307 #[error("structure must attach 'base_link' directly under 'base_footprint' with a fixed joint")]
310 MisattachedBaseLink,
311
312 #[error("canonical structure document is invalid: {0}")]
314 Document(#[from] serde_json::Error),
315}
316
317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub enum IdentifierKind {
320 RobotId,
321 RobotLink,
322 RobotJoint,
323 ComponentType,
324 ComponentInstance,
325 Capability,
326}
327
328impl fmt::Display for IdentifierKind {
329 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
330 formatter.write_str(match self {
331 Self::RobotId => "robot id",
332 Self::RobotLink => "robot link",
333 Self::RobotJoint => "robot joint",
334 Self::ComponentType => "component type",
335 Self::ComponentInstance => "component instance id",
336 Self::Capability => "capability id",
337 })
338 }
339}
340
341#[derive(Clone, Debug, PartialEq, Eq)]
343pub enum JointOwner {
344 Robot,
346 ComponentType(ComponentTypeId),
348}
349
350impl fmt::Display for JointOwner {
351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352 match self {
353 Self::Robot => formatter.write_str("robot"),
354 Self::ComponentType(component_type) => write!(formatter, "{component_type}"),
355 }
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum LinkRole {
362 Parent,
363 Child,
364}
365
366impl fmt::Display for LinkRole {
367 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368 formatter.write_str(match self {
369 Self::Parent => "parent",
370 Self::Child => "child",
371 })
372 }
373}
374
375#[derive(Clone, Debug, PartialEq, Eq)]
377pub enum PoseOwner {
378 LinkInertial(LinkId),
379 LinkVisual(LinkId),
380 LinkCollision(LinkId),
381 Joint(JointId),
382}
383
384impl fmt::Display for PoseOwner {
385 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386 match self {
387 Self::LinkInertial(link) => write!(formatter, "link '{link}' inertial"),
388 Self::LinkVisual(link) => write!(formatter, "link '{link}' visual"),
389 Self::LinkCollision(link) => write!(formatter, "link '{link}' collision"),
390 Self::Joint(joint) => write!(formatter, "joint '{joint}'"),
391 }
392 }
393}
394
395#[derive(Clone, Copy, Debug, PartialEq, Eq)]
397pub enum MotionLimitField {
398 MaxLinearSpeedMps,
399 MaxAngularSpeedRadps,
400}
401
402impl fmt::Display for MotionLimitField {
403 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
404 formatter.write_str(match self {
405 Self::MaxLinearSpeedMps => "max_linear_speed_mps",
406 Self::MaxAngularSpeedRadps => "max_angular_speed_radps",
407 })
408 }
409}
410
411#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub enum KinematicScalarField {
414 WheelRadiusM,
415 WheelBaseM,
416 TrackM,
417 MaxSteeringAngleRad,
418}
419
420impl fmt::Display for KinematicScalarField {
421 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
422 formatter.write_str(match self {
423 Self::WheelRadiusM => "wheel_radius_m",
424 Self::WheelBaseM => "wheel_base_m",
425 Self::TrackM => "track_m",
426 Self::MaxSteeringAngleRad => "max_steering_angle_rad",
427 })
428 }
429}