22
33from abc import ABC , abstractmethod
44from dataclasses import dataclass , field
5- from typing import List , Tuple
5+ from typing import List , Tuple , TYPE_CHECKING
66
77import numpy as np
88
1414from .types import NpMatrix4x4
1515from .world_entity import Connection
1616
17+ if TYPE_CHECKING :
18+ from .world import World
1719
1820
1921class Has1DOFState :
@@ -22,6 +24,7 @@ class Has1DOFState:
2224 """
2325
2426 dof : DegreeOfFreedom
27+ _world : World
2528
2629 @property
2730 def position (self ) -> float :
@@ -60,6 +63,24 @@ def jerk(self, value: float) -> None:
6063 self ._world .notify_state_change ()
6164
6265
66+ class HasUpdateState (ABC ):
67+ """
68+ Mixin class for connections that need state updated which are not trivial integrations.
69+ Typically needed for connections that use active and passive degrees of freedom.
70+ Look at OmniDrive for an example usage.
71+ """
72+
73+ @abstractmethod
74+ def update_state (self , dt : float ) -> None :
75+ """
76+ Allows the connection to update the state of its dofs.
77+ An integration update for active dofs will have happened before this method is called.
78+ Write directly into self._world.state, but don't touch dofs that don't belong to this connection.
79+ :param dt: Time passed since last update.
80+ """
81+ pass
82+
83+
6384@dataclass
6485class FixedConnection (Connection ):
6586 """
@@ -72,7 +93,10 @@ class ActiveConnection(Connection):
7293 """
7394 Has one or more degrees of freedom that can be actively controlled, e.g., robot joints.
7495 """
75- active_dofs : List [DegreeOfFreedom ] = field (default_factory = list , init = False )
96+
97+ @property
98+ def active_dofs (self ) -> List [DegreeOfFreedom ]:
99+ return []
76100
77101
78102@dataclass
@@ -81,7 +105,10 @@ class PassiveConnection(Connection):
81105 Has one or more degrees of freedom that cannot be actively controlled.
82106 Useful if a transformation is only tracked, e.g., the robot's localization.
83107 """
84- passive_dofs : List [DegreeOfFreedom ] = field (default_factory = list , init = False )
108+
109+ @property
110+ def passive_dofs (self ) -> List [DegreeOfFreedom ]:
111+ return []
85112
86113
87114@dataclass
@@ -170,8 +197,7 @@ def __post_init__(self):
170197 else :
171198 self .offset = self .offset
172199 self .axis = self .axis
173- self .dof = self .dof or self ._world .create_degree_of_freedom (name = PrefixedName (str (self .name )))
174- self .active_dofs = [self .dof ]
200+ self ._post_init_world_part ()
175201
176202 motor_expression = self .dof .symbols .position * self .multiplier + self .offset
177203 translation_axis = cas .Vector3 (self .axis ) * motor_expression
@@ -182,6 +208,18 @@ def __post_init__(self):
182208 self .origin_expression .reference_frame = self .parent
183209 self .origin_expression .child_frame = self .child
184210
211+ def _post_init_with_world (self ):
212+ self .dof = self .dof or self ._world .create_degree_of_freedom (name = PrefixedName (str (self .name )))
213+
214+ def _post_init_without_world (self ):
215+ if self .dof is None :
216+ raise ValueError ("PrismaticConnection cannot be created without a world "
217+ "if the dof is not provided." )
218+
219+ @property
220+ def active_dofs (self ) -> List [DegreeOfFreedom ]:
221+ return [self .dof ]
222+
185223 def __hash__ (self ):
186224 return hash ((self .parent , self .child ))
187225
@@ -224,8 +262,7 @@ def __post_init__(self):
224262 else :
225263 self .offset = self .offset
226264 self .axis = self .axis
227- self .dof = self .dof or self ._world .create_degree_of_freedom (name = PrefixedName (str (self .name )))
228- self .active_dofs = [self .dof ]
265+ self ._post_init_world_part ()
229266
230267 motor_expression = self .dof .symbols .position * self .multiplier + self .offset
231268 rotation_axis = cas .Vector3 (self .axis )
@@ -234,6 +271,18 @@ def __post_init__(self):
234271 self .origin_expression .reference_frame = self .parent
235272 self .origin_expression .child_frame = self .child
236273
274+ def _post_init_with_world (self ):
275+ self .dof = self .dof or self ._world .create_degree_of_freedom (name = PrefixedName (str (self .name )))
276+
277+ def _post_init_without_world (self ):
278+ if self .dof is None :
279+ raise ValueError ("RevoluteConnection cannot be created without a world "
280+ "if the dof is not provided." )
281+
282+ @property
283+ def active_dofs (self ) -> List [DegreeOfFreedom ]:
284+ return [self .dof ]
285+
237286 def __hash__ (self ):
238287 return hash ((self .parent , self .child ))
239288
@@ -268,16 +317,7 @@ class Connection6DoF(PassiveConnection):
268317
269318 def __post_init__ (self ):
270319 super ().__post_init__ ()
271- self .x = self .x or self ._world .create_degree_of_freedom (name = PrefixedName ('x' , str (self .name )))
272- self .y = self .y or self ._world .create_degree_of_freedom (name = PrefixedName ('y' , str (self .name )))
273- self .z = self .z or self ._world .create_degree_of_freedom (name = PrefixedName ('z' , str (self .name )))
274- self .qx = self .qx or self ._world .create_degree_of_freedom (name = PrefixedName ('qx' , str (self .name )))
275- self .qy = self .qy or self ._world .create_degree_of_freedom (name = PrefixedName ('qy' , str (self .name )))
276- self .qz = self .qz or self ._world .create_degree_of_freedom (name = PrefixedName ('qz' , str (self .name )))
277- self .qw = self .qw or self ._world .create_degree_of_freedom (name = PrefixedName ('qw' , str (self .name )))
278- self .passive_dofs = [self .x , self .y , self .z , self .qx , self .qy , self .qz , self .qw ]
279-
280- self ._world .state [self .qw .name ].position = 1.
320+ self ._post_init_world_part ()
281321 parent_P_child = cas .Point3 ((self .x .symbols .position ,
282322 self .y .symbols .position ,
283323 self .z .symbols .position ))
@@ -290,6 +330,29 @@ def __post_init__(self):
290330 reference_frame = self .parent ,
291331 child_frame = self .child )
292332
333+ def _post_init_with_world (self ):
334+ if all (dof is None for dof in self .passive_dofs ):
335+ self .x = self ._world .create_degree_of_freedom (name = PrefixedName ('x' , str (self .name )))
336+ self .y = self ._world .create_degree_of_freedom (name = PrefixedName ('y' , str (self .name )))
337+ self .z = self ._world .create_degree_of_freedom (name = PrefixedName ('z' , str (self .name )))
338+ self .qx = self ._world .create_degree_of_freedom (name = PrefixedName ('qx' , str (self .name )))
339+ self .qy = self ._world .create_degree_of_freedom (name = PrefixedName ('qy' , str (self .name )))
340+ self .qz = self ._world .create_degree_of_freedom (name = PrefixedName ('qz' , str (self .name )))
341+ self .qw = self ._world .create_degree_of_freedom (name = PrefixedName ('qw' , str (self .name )))
342+ self ._world .state [self .qw .name ].position = 1.
343+ elif any (dof is None for dof in self .passive_dofs ):
344+ raise ValueError ("Connection6DoF can only be created "
345+ "if you provide all or none of the passive degrees of freedom" )
346+
347+ def _post_init_without_world (self ):
348+ if any (dof is None for dof in self .passive_dofs ):
349+ raise ValueError ("Connection6DoF cannot be created without a world "
350+ "if some passive degrees of freedom are not provided." )
351+
352+ @property
353+ def passive_dofs (self ) -> List [DegreeOfFreedom ]:
354+ return [self .x , self .y , self .z , self .qx , self .qy , self .qz , self .qw ]
355+
293356 @property
294357 def origin (self ) -> NpMatrix4x4 :
295358 return super ().origin
@@ -307,24 +370,6 @@ def origin(self, transformation: NpMatrix4x4) -> None:
307370 self ._world .notify_state_change ()
308371
309372
310- class HasUpdateState (ABC ):
311- """
312- Mixin class for connections that need state updated which are not trivial integrations.
313- Typically needed for connections that use active and passive degrees of freedom.
314- Look at OmniDrive for an example usage.
315- """
316-
317- @abstractmethod
318- def update_state (self , dt : float ) -> None :
319- """
320- Allows the connection to update the state of its dofs.
321- An integration update for active dofs will have happened before this method is called.
322- Write directly into self._world.state, but don't touch dofs that don't belong to this connection.
323- :param dt: Time passed since last update.
324- """
325- pass
326-
327-
328373@dataclass
329374class OmniDrive (ActiveConnection , PassiveConnection , HasUpdateState ):
330375 x : DegreeOfFreedom = field (default = None )
@@ -341,38 +386,7 @@ class OmniDrive(ActiveConnection, PassiveConnection, HasUpdateState):
341386
342387 def __post_init__ (self ):
343388 super ().__post_init__ ()
344- stringified_name = str (self .name )
345- lower_translation_limits = DerivativeMap ()
346- lower_translation_limits .velocity = - self .translation_velocity_limits
347- upper_translation_limits = DerivativeMap ()
348- upper_translation_limits .velocity = self .translation_velocity_limits
349- lower_rotation_limits = DerivativeMap ()
350- lower_rotation_limits .velocity = - self .rotation_velocity_limits
351- upper_rotation_limits = DerivativeMap ()
352- upper_rotation_limits .velocity = self .rotation_velocity_limits
353-
354- self .x = self .x or self ._world .create_degree_of_freedom (name = PrefixedName ('x' , stringified_name ))
355- self .y = self .y or self ._world .create_degree_of_freedom (name = PrefixedName ('y' , stringified_name ))
356- self .z = self .z or self ._world .create_degree_of_freedom (name = PrefixedName ('z' , stringified_name ))
357-
358- self .roll = self .roll or self ._world .create_degree_of_freedom (name = PrefixedName ('roll' , stringified_name ))
359- self .pitch = self .pitch or self ._world .create_degree_of_freedom (name = PrefixedName ('pitch' , stringified_name ))
360- self .yaw = self .yaw or self ._world .create_degree_of_freedom (
361- name = PrefixedName ('yaw' , stringified_name ),
362- lower_limits = lower_rotation_limits ,
363- upper_limits = upper_rotation_limits )
364-
365- self .x_vel = self .x_vel or self ._world .create_degree_of_freedom (
366- name = PrefixedName ('x_vel' , stringified_name ),
367- lower_limits = lower_translation_limits ,
368- upper_limits = upper_translation_limits )
369- self .y_vel = self .y_vel or self ._world .create_degree_of_freedom (
370- name = PrefixedName ('y_vel' , stringified_name ),
371- lower_limits = lower_translation_limits ,
372- upper_limits = upper_translation_limits )
373- self .active_dofs = [self .x_vel , self .y_vel , self .yaw ]
374- self .passive_dofs = [self .x , self .y , self .z , self .roll , self .pitch ]
375-
389+ self ._post_init_world_part ()
376390 odom_T_bf = cas .TransformationMatrix .from_xyz_rpy (x = self .x .symbols .position ,
377391 y = self .y .symbols .position ,
378392 yaw = self .yaw .symbols .position )
@@ -388,6 +402,58 @@ def __post_init__(self):
388402 self .origin_expression .reference_frame = self .parent
389403 self .origin_expression .child_frame = self .child
390404
405+ def _post_init_with_world (self ):
406+ if all (dof is None for dof in self .dofs ):
407+ stringified_name = str (self .name )
408+ lower_translation_limits = DerivativeMap ()
409+ lower_translation_limits .velocity = - self .translation_velocity_limits
410+ upper_translation_limits = DerivativeMap ()
411+ upper_translation_limits .velocity = self .translation_velocity_limits
412+ lower_rotation_limits = DerivativeMap ()
413+ lower_rotation_limits .velocity = - self .rotation_velocity_limits
414+ upper_rotation_limits = DerivativeMap ()
415+ upper_rotation_limits .velocity = self .rotation_velocity_limits
416+
417+ self .x = self ._world .create_degree_of_freedom (name = PrefixedName ('x' , stringified_name ))
418+ self .y = self ._world .create_degree_of_freedom (name = PrefixedName ('y' , stringified_name ))
419+ self .z = self ._world .create_degree_of_freedom (name = PrefixedName ('z' , stringified_name ))
420+
421+ self .roll = self ._world .create_degree_of_freedom (name = PrefixedName ('roll' , stringified_name ))
422+ self .pitch = self ._world .create_degree_of_freedom (name = PrefixedName ('pitch' , stringified_name ))
423+ self .yaw = self ._world .create_degree_of_freedom (
424+ name = PrefixedName ('yaw' , stringified_name ),
425+ lower_limits = lower_rotation_limits ,
426+ upper_limits = upper_rotation_limits )
427+
428+ self .x_vel = self ._world .create_degree_of_freedom (
429+ name = PrefixedName ('x_vel' , stringified_name ),
430+ lower_limits = lower_translation_limits ,
431+ upper_limits = upper_translation_limits )
432+ self .y_vel = self ._world .create_degree_of_freedom (
433+ name = PrefixedName ('y_vel' , stringified_name ),
434+ lower_limits = lower_translation_limits ,
435+ upper_limits = upper_translation_limits )
436+ elif any (dof is None for dof in self .passive_dofs ):
437+ raise ValueError ("OmniDrive can only be created "
438+ "if you provide all or none of the passive degrees of freedom" )
439+
440+ def _post_init_without_world (self ):
441+ if any (dof is None for dof in self .dofs ):
442+ raise ValueError ("OmniDrive cannot be created without a world "
443+ "if some passive degrees of freedom are not provided." )
444+
445+ @property
446+ def active_dofs (self ) -> List [DegreeOfFreedom ]:
447+ return [self .x_vel , self .y_vel , self .yaw ]
448+
449+ @property
450+ def passive_dofs (self ) -> List [DegreeOfFreedom ]:
451+ return [self .x , self .y , self .z , self .roll , self .pitch ]
452+
453+ @property
454+ def dofs (self ) -> List [DegreeOfFreedom ]:
455+ return self .active_dofs + self .passive_dofs
456+
391457 def update_state (self , dt : float ) -> None :
392458 state = self ._world .state
393459 state [self .x_vel .name ].position = 0
0 commit comments