1515
1616from spatialmath import SE2 , base
1717from roboticstoolbox .mobile .drivers import VehicleDriver
18- from roboticstoolbox .mobile .animations import VehiclePolygon
18+ from roboticstoolbox .mobile .Animations import VehiclePolygon
1919
2020
21- class Vehicle (ABC ):
22- def __init__ (self , covar = None , speed_max = np .inf , accel_max = np .inf , x0 = None , dt = 0.1 ,
23- control = None , seed = 0 , animation = None , verbose = False , plot = False , workspace = None ):
21+ class VehicleBase (ABC ):
22+ def __init__ (self , covar = None , speed_max = np .inf , accel_max = np .inf , x0 = [0 , 0 , 0 ], dt = 0.1 ,
23+ control = None , seed = 0 , animation = None , verbose = False , plot = False , workspace = None ,
24+ polygon = None ):
2425 r"""
2526 Superclass for vehicle kinematic models
2627
@@ -62,13 +63,14 @@ def __init__(self, covar=None, speed_max=np.inf, accel_max=np.inf, x0=None, dt=0
6263 if len (x0 ) not in (2 ,3 ):
6364 raise ValueError ('x0 must be length 2 or 3' )
6465 self ._x0 = x0
65- self ._x = x0
66+ self ._x = x0 . copy ()
6667
6768 self ._random = np .random .default_rng (seed )
6869 self ._seed = seed
6970 self ._speed_max = speed_max
7071 self ._accel_max = accel_max
7172 self ._v_prev = 0
73+ self ._polygon = polygon
7274
7375 if isinstance (animation , str ):
7476 animation = VehiclePolygon (animation )
@@ -85,6 +87,9 @@ def __init__(self, covar=None, speed_max=np.inf, accel_max=np.inf, x0=None, dt=0
8587 self ._verbose = verbose
8688 self ._plot = False
8789
90+ self ._control = None
91+ self ._x_hist = []
92+
8893 if workspace :
8994 self ._workspace = base .expand_dims (workspace )
9095 else :
@@ -111,7 +116,12 @@ def workspace(self):
111116 Returns the bounds of the workspace as specified by constructor
112117 option ``workspace``
113118 """
114- return self ._workspace
119+
120+ # get workspace specified for Vehicle or from its driver
121+ if self ._workspace is not None :
122+ return self ._workspace
123+ if self ._control is not None :
124+ return self ._control ._workspace
115125
116126 @property
117127 def x (self ):
@@ -123,6 +133,16 @@ def x(self):
123133 """
124134 return self ._x
125135
136+ @property
137+ def q (self ):
138+ """
139+ Get vehicle state/configuration (superclass method)
140+
141+ :return: Vehicle state :math:`(x, y, \t heta)`
142+ :rtype: ndarray(3)
143+ """
144+ return self ._x
145+
126146 @property
127147 def x0 (self ):
128148 """
@@ -140,7 +160,7 @@ def x0(self):
140160 """
141161 return self ._x0
142162
143- @property
163+ @x0 . setter
144164 def x0 (self , x0 ):
145165 """
146166 Set vehicle initial state/configuration (superclass method)
@@ -288,9 +308,7 @@ def control(self, control):
288308 Control can be:
289309
290310 * a constant tuple as the control inputs to the vehicle
291- * a function called as ``f(vehicle, t, x)`` that returns a tuple
292- * an interpolator called as f(t) that returns a tuple, see
293- SciPy interp1d
311+
294312 * a driver agent, subclass of :func:`VehicleDriver`
295313
296314 Example:
@@ -303,11 +321,17 @@ def control(self, control):
303321
304322 :seealso: :func:`eval_control`, :func:`RandomPath`, :func:`PurePursuit`
305323 """
324+ # * a function called as ``f(vehicle, t, x)`` that returns a tuple
325+ # * an interpolator called as f(t) that returns a tuple, see
326+ # SciPy interp1d
327+
306328 self ._control = control
307329 if isinstance (control , VehicleDriver ):
308330 # if this is a driver agent, connect it to the vehicle
309331 control .vehicle = self
310332
333+ def polygon (self , q ):
334+ return self ._polygon .transformed (SE2 (q ))
311335
312336 # This function is overridden by the child class
313337 @abstractmethod
@@ -399,14 +423,15 @@ def init(self, x0=None, control=None):
399423 if x0 is not None :
400424 self ._x = base .getvector (x0 , 3 )
401425 else :
402- self ._x = self ._x0
426+ self ._x = self ._x0 . copy ()
403427
404428 self ._x_hist = []
405429
406430 if self ._seed is not None :
407431 self ._random = np .random .default_rng (self ._seed )
408432
409433 if control is not None :
434+ # override control
410435 self ._control = control
411436
412437 if self ._control is not None :
@@ -418,12 +443,12 @@ def init(self, x0=None, control=None):
418443 if self ._animation is not None :
419444
420445 # setup the plot
421- self ._ax = base .plotvol2 (self ._workspace )
446+ self ._ax = base .plotvol2 (self .workspace )
422447
423448 self ._ax .set_xlabel ('x' )
424449 self ._ax .set_ylabel ('y' )
425450 self ._ax .set_aspect ('equal' )
426- self ._ax .figure .canvas .set_window_title (
451+ self ._ax .figure .canvas .manager . set_window_title (
427452 f"Robotics Toolbox for Python (Figure { self ._ax .figure .number } )" )
428453
429454 self ._animation .add (ax = self ._ax ) # add vehicle animation to axis
@@ -433,16 +458,15 @@ def init(self, x0=None, control=None):
433458 if isinstance (self ._control , VehicleDriver ):
434459 self ._control .init (ax = self ._ax )
435460
436- def step (self , u1 = None , u2 = None ):
461+ def step (self , u = None , animate = False ):
437462 """
438463 Step simulator by one time step (superclass method)
439464
440465 :return: odometry :math:`(\delta_d, \delta_\t heta)`
441466 :rtype: ndarray(2)
442467
443- - ``veh.step(vel, steer)`` for a Bicycle vehicle model
444- - ``veh.step((vel, steer))`` as above but control is a tuple
445- - ``veh.step(vel, vel_diff)`` for a Unicycle vehicle model
468+ - ``veh.step((vel, steer))`` for a Bicycle vehicle model
469+ - ``veh.step((vel, vel_diff))`` for a Unicycle vehicle model
446470 - ``veh.step()`` as above but control is taken from the ``control``
447471 attribute which might be a function or driver agent.
448472
@@ -466,11 +490,8 @@ def step(self, u1=None, u2=None):
466490 :seealso: :func:`control`, :func:`update`, :func:`run`
467491 """
468492 # determine vehicle control
469- if u1 is not None :
470- if u2 is not None :
471- u = self .eval_control ((u1 , u2 ), self ._x )
472- else :
473- u = self .eval_control (u1 , self ._x )
493+ if u is not None :
494+ u = self .eval_control (u , self ._x )
474495 else :
475496 u = self .eval_control (self ._control , self ._x )
476497
@@ -490,7 +511,7 @@ def step(self, u1=None, u2=None):
490511 odo += self .random .multivariate_normal ((0 , 0 ), self ._V )
491512
492513 # do the graphics
493- if self ._animation :
514+ if animate and self ._animation :
494515 self ._animation .update (self ._x )
495516 if self ._timer is not None :
496517 self ._timer .set_text (f"t = { self ._t :.2f} " )
@@ -502,7 +523,6 @@ def step(self, u1=None, u2=None):
502523 if self ._verbose :
503524 print (f"{ self ._t :8.2f} : u=({ u [0 ]:8.2f} , { u [1 ]:8.2f} ), x=({ self ._x [0 ]:8.2f} , { self ._x [1 ]:8.2f} , { self ._x [2 ]:8.2f} )" )
504525
505-
506526 return odo
507527
508528
@@ -595,6 +615,8 @@ def plot(self, x=None, shape='box', block=False, size=True, **kwargs):
595615 base .plot_poly (SE2 (x ) * vertices , close = True , ** kwargs )
596616
597617 def plot_xy (self , * args , block = False , ** kwargs ):
618+ if args is None and 'color' not in kwargs :
619+ kwargs ['color' ] = 'b'
598620 xyt = self .x_hist
599621 plt .plot (xyt [:, 0 ], xyt [:, 1 ], * args , ** kwargs )
600622 plt .show (block = block )
@@ -620,14 +642,17 @@ def limits_va(self, v):
620642 is reset at the start of each simulation.
621643 """
622644 # acceleration limit
623- if (v - self ._v_prev ) / self ._dt > self ._accel_max :
624- v = self ._v_prev + self ._accelmax * self ._dt ;
625- elif (v - self ._v_prev ) / self ._dt < - self ._accel_max :
626- v = self ._v_prev - self ._accel_max * self ._dt ;
645+ if self ._accel_max is not None :
646+ if (v - self ._v_prev ) / self ._dt > self ._accel_max :
647+ v = self ._v_prev + self ._accelmax * self ._dt ;
648+ elif (v - self ._v_prev ) / self ._dt < - self ._accel_max :
649+ v = self ._v_prev - self ._accel_max * self ._dt ;
627650 self ._v_prev = v
628651
629652 # speed limit
630- return min (self ._speed_max , max (v , - self ._speed_max ));
653+ if self ._speed_max is not None :
654+ v = np .clip (v , - self ._speed_max , self ._speed_max )
655+ return v
631656
632657
633658 def path (self , t = 10 , u = None , x0 = None ):
@@ -681,18 +706,18 @@ def dynamics(t, x, vehicle, u):
681706 return (sol .t , sol .y .T )
682707# ========================================================================= #
683708
684- class Bicycle (Vehicle ):
709+ class Bicycle (VehicleBase ):
685710
686711 def __init__ (self ,
687- l = 1 ,
712+ L = 1 ,
688713 steer_max = 0.45 * pi ,
689714 ** kwargs
690715 ):
691716 r"""
692717 Create new bicycle kinematic model
693718
694- :param l : wheel base, defaults to 1
695- :type l : float, optional
719+ :param L : wheel base, defaults to 1
720+ :type L : float, optional
696721 :param steer_max: [description], defaults to :math:`0.45\pi`
697722 :type steer_max: float, optional
698723 :param **kwargs: additional arguments passed to :class:`Vehicle`
@@ -702,7 +727,7 @@ def __init__(self,
702727 """
703728 super ().__init__ (** kwargs )
704729
705- self ._l = l
730+ self ._l = L
706731 self ._steer_max = steer_max
707732
708733 def __str__ (self ):
@@ -867,7 +892,7 @@ def Fv(self, x, odo):
867892 # fmt: on
868893 return J
869894
870- def deriv (self , x , u ):
895+ def deriv (self , x , u , limits = True ):
871896 r"""
872897 Time derivative of state
873898
@@ -880,12 +905,13 @@ def deriv(self, x, u):
880905
881906 Returns the time derivative of state (3x1) at the state ``x`` with
882907 inputs ``u``.
883-
884- .. note:: Vehicle speed and steering limits are not applied here
885908 """
886909
887910 # unpack some variables
888911 theta = x [2 ]
912+
913+ if limits :
914+ u = self .u_limited (u )
889915 v = u [0 ]
890916 gamma = u [1 ]
891917
@@ -900,29 +926,29 @@ def u_limited(self, u):
900926 # limit speed and steer angle
901927 ulim = np .array (u )
902928 ulim [0 ] = self .limits_va (u [0 ])
903- ulim [1 ] = np .maximum ( - self ._steer_max , np . minimum ( self ._steer_max , u [ 1 ]) )
929+ ulim [1 ] = np .clip ( u [ 1 ], - self ._steer_max , self ._steer_max )
904930
905931 return ulim
906932
907933# ========================================================================= #
908934
909- class Unicycle (Vehicle ):
935+ class Unicycle (VehicleBase ):
910936
911937 def __init__ (self ,
912- w = 1 ,
938+ W = 1 ,
913939 ** kwargs ):
914940 r"""
915941 Create new unicycle kinematic model
916942
917- :param w : vehicle width, defaults to 1
918- :type w : float, optional
943+ :param W : vehicle width, defaults to 1
944+ :type W : float, optional
919945 :param **kwargs: additional arguments passed to :class:`Vehicle`
920946 constructor
921947
922948 :seealso: :class:`.Vehicle`
923949 """
924950 super ().__init__ (** kwargs )
925- self ._w = w
951+ self ._w = W
926952
927953 def __str__ (self ):
928954
@@ -1070,39 +1096,52 @@ def u_limited(self, u):
10701096
10711097 return ulim
10721098
1099+ class DiffSteer (Unicycle ):
1100+ pass
10731101
10741102if __name__ == "__main__" :
1075- from math import pi
10761103
1077- V = np .diag (np .r_ [0.02 , 0.5 * pi / 180 ] ** 2 )
1104+ from roboticstoolbox import RandomPath
1105+
1106+ V = np .eye (2 ) * 0.001
1107+ robot = Bicycle (covar = V , animation = "car" )
1108+ odo = robot .step ((1 , 0.3 ), animate = False )
1109+
1110+ robot .control = RandomPath (workspace = 10 )
1111+
1112+ robot .run (T = 10 )
1113+
1114+ # from math import pi
1115+
1116+ # V = np.diag(np.r_[0.02, 0.5 * pi / 180] ** 2)
10781117
1079- v = VehiclePolygon ()
1080- # v = VehicleIcon('greycar2', scale=2, rotation=90)
1118+ # v = VehiclePolygon()
1119+ # # v = VehicleIcon('greycar2', scale=2, rotation=90)
10811120
1082- veh = Bicycle (covar = V , animation = v , control = RandomPath (10 ), verbose = False )
1083- print (veh )
1121+ # veh = Bicycle(covar=V, animation=v, control=RandomPath(10), verbose=False)
1122+ # print(veh)
10841123
1085- odo = veh .step (1 , 0.3 )
1086- print (odo )
1124+ # odo = veh.step(1, 0.3)
1125+ # print(odo)
10871126
1088- print (veh .x )
1127+ # print(veh.x)
10891128
1090- print (veh .f ([0 , 0 , 0 ], odo ))
1129+ # print(veh.f([0, 0, 0], odo))
10911130
1092- def control (v , t , x ):
1093- goal = (6 ,6 )
1094- goal_heading = atan2 (goal [1 ]- x [1 ], goal [0 ]- x [0 ])
1095- d_heading = base .angdiff (goal_heading , x [2 ])
1096- v .stopif (base .norm (x [0 :2 ] - goal ) < 0.1 )
1131+ # def control(v, t, x):
1132+ # goal = (6,6)
1133+ # goal_heading = atan2(goal[1]-x[1], goal[0]-x[0])
1134+ # d_heading = base.angdiff(goal_heading, x[2])
1135+ # v.stopif(base.norm(x[0:2] - goal) < 0.1)
10971136
1098- return (1 , d_heading )
1137+ # return (1, d_heading)
10991138
1100- veh .control = RandomPath (10 )
1101- p = veh .run (1000 , plot = True )
1102- # plt.show()
1103- print (p )
1139+ # veh.control=RandomPath(10)
1140+ # p = veh.run(1000, plot=True)
1141+ # # plt.show()
1142+ # print(p)
11041143
1105- veh .plot_xyt_t ()
1144+ # veh.plot_xyt_t()
11061145 # veh.plot(p)
11071146
11081147 # t, x = veh.path(5, u=control)
0 commit comments