15
15
16
16
from spatialmath import SE2 , base
17
17
from roboticstoolbox .mobile .drivers import VehicleDriver
18
- from roboticstoolbox .mobile .animations import VehiclePolygon
18
+ from roboticstoolbox .mobile .Animations import VehiclePolygon
19
19
20
20
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 ):
24
25
r"""
25
26
Superclass for vehicle kinematic models
26
27
@@ -62,13 +63,14 @@ def __init__(self, covar=None, speed_max=np.inf, accel_max=np.inf, x0=None, dt=0
62
63
if len (x0 ) not in (2 ,3 ):
63
64
raise ValueError ('x0 must be length 2 or 3' )
64
65
self ._x0 = x0
65
- self ._x = x0
66
+ self ._x = x0 . copy ()
66
67
67
68
self ._random = np .random .default_rng (seed )
68
69
self ._seed = seed
69
70
self ._speed_max = speed_max
70
71
self ._accel_max = accel_max
71
72
self ._v_prev = 0
73
+ self ._polygon = polygon
72
74
73
75
if isinstance (animation , str ):
74
76
animation = VehiclePolygon (animation )
@@ -85,6 +87,9 @@ def __init__(self, covar=None, speed_max=np.inf, accel_max=np.inf, x0=None, dt=0
85
87
self ._verbose = verbose
86
88
self ._plot = False
87
89
90
+ self ._control = None
91
+ self ._x_hist = []
92
+
88
93
if workspace :
89
94
self ._workspace = base .expand_dims (workspace )
90
95
else :
@@ -111,7 +116,12 @@ def workspace(self):
111
116
Returns the bounds of the workspace as specified by constructor
112
117
option ``workspace``
113
118
"""
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
115
125
116
126
@property
117
127
def x (self ):
@@ -123,6 +133,16 @@ def x(self):
123
133
"""
124
134
return self ._x
125
135
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
+
126
146
@property
127
147
def x0 (self ):
128
148
"""
@@ -140,7 +160,7 @@ def x0(self):
140
160
"""
141
161
return self ._x0
142
162
143
- @property
163
+ @x0 . setter
144
164
def x0 (self , x0 ):
145
165
"""
146
166
Set vehicle initial state/configuration (superclass method)
@@ -288,9 +308,7 @@ def control(self, control):
288
308
Control can be:
289
309
290
310
* 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
+
294
312
* a driver agent, subclass of :func:`VehicleDriver`
295
313
296
314
Example:
@@ -303,11 +321,17 @@ def control(self, control):
303
321
304
322
:seealso: :func:`eval_control`, :func:`RandomPath`, :func:`PurePursuit`
305
323
"""
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
+
306
328
self ._control = control
307
329
if isinstance (control , VehicleDriver ):
308
330
# if this is a driver agent, connect it to the vehicle
309
331
control .vehicle = self
310
332
333
+ def polygon (self , q ):
334
+ return self ._polygon .transformed (SE2 (q ))
311
335
312
336
# This function is overridden by the child class
313
337
@abstractmethod
@@ -399,14 +423,15 @@ def init(self, x0=None, control=None):
399
423
if x0 is not None :
400
424
self ._x = base .getvector (x0 , 3 )
401
425
else :
402
- self ._x = self ._x0
426
+ self ._x = self ._x0 . copy ()
403
427
404
428
self ._x_hist = []
405
429
406
430
if self ._seed is not None :
407
431
self ._random = np .random .default_rng (self ._seed )
408
432
409
433
if control is not None :
434
+ # override control
410
435
self ._control = control
411
436
412
437
if self ._control is not None :
@@ -418,12 +443,12 @@ def init(self, x0=None, control=None):
418
443
if self ._animation is not None :
419
444
420
445
# setup the plot
421
- self ._ax = base .plotvol2 (self ._workspace )
446
+ self ._ax = base .plotvol2 (self .workspace )
422
447
423
448
self ._ax .set_xlabel ('x' )
424
449
self ._ax .set_ylabel ('y' )
425
450
self ._ax .set_aspect ('equal' )
426
- self ._ax .figure .canvas .set_window_title (
451
+ self ._ax .figure .canvas .manager . set_window_title (
427
452
f"Robotics Toolbox for Python (Figure { self ._ax .figure .number } )" )
428
453
429
454
self ._animation .add (ax = self ._ax ) # add vehicle animation to axis
@@ -433,16 +458,15 @@ def init(self, x0=None, control=None):
433
458
if isinstance (self ._control , VehicleDriver ):
434
459
self ._control .init (ax = self ._ax )
435
460
436
- def step (self , u1 = None , u2 = None ):
461
+ def step (self , u = None , animate = False ):
437
462
"""
438
463
Step simulator by one time step (superclass method)
439
464
440
465
:return: odometry :math:`(\delta_d, \delta_\t heta)`
441
466
:rtype: ndarray(2)
442
467
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
446
470
- ``veh.step()`` as above but control is taken from the ``control``
447
471
attribute which might be a function or driver agent.
448
472
@@ -466,11 +490,8 @@ def step(self, u1=None, u2=None):
466
490
:seealso: :func:`control`, :func:`update`, :func:`run`
467
491
"""
468
492
# 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 )
474
495
else :
475
496
u = self .eval_control (self ._control , self ._x )
476
497
@@ -490,7 +511,7 @@ def step(self, u1=None, u2=None):
490
511
odo += self .random .multivariate_normal ((0 , 0 ), self ._V )
491
512
492
513
# do the graphics
493
- if self ._animation :
514
+ if animate and self ._animation :
494
515
self ._animation .update (self ._x )
495
516
if self ._timer is not None :
496
517
self ._timer .set_text (f"t = { self ._t :.2f} " )
@@ -502,7 +523,6 @@ def step(self, u1=None, u2=None):
502
523
if self ._verbose :
503
524
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} )" )
504
525
505
-
506
526
return odo
507
527
508
528
@@ -595,6 +615,8 @@ def plot(self, x=None, shape='box', block=False, size=True, **kwargs):
595
615
base .plot_poly (SE2 (x ) * vertices , close = True , ** kwargs )
596
616
597
617
def plot_xy (self , * args , block = False , ** kwargs ):
618
+ if args is None and 'color' not in kwargs :
619
+ kwargs ['color' ] = 'b'
598
620
xyt = self .x_hist
599
621
plt .plot (xyt [:, 0 ], xyt [:, 1 ], * args , ** kwargs )
600
622
plt .show (block = block )
@@ -620,14 +642,17 @@ def limits_va(self, v):
620
642
is reset at the start of each simulation.
621
643
"""
622
644
# 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 ;
627
650
self ._v_prev = v
628
651
629
652
# 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
631
656
632
657
633
658
def path (self , t = 10 , u = None , x0 = None ):
@@ -681,18 +706,18 @@ def dynamics(t, x, vehicle, u):
681
706
return (sol .t , sol .y .T )
682
707
# ========================================================================= #
683
708
684
- class Bicycle (Vehicle ):
709
+ class Bicycle (VehicleBase ):
685
710
686
711
def __init__ (self ,
687
- l = 1 ,
712
+ L = 1 ,
688
713
steer_max = 0.45 * pi ,
689
714
** kwargs
690
715
):
691
716
r"""
692
717
Create new bicycle kinematic model
693
718
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
696
721
:param steer_max: [description], defaults to :math:`0.45\pi`
697
722
:type steer_max: float, optional
698
723
:param **kwargs: additional arguments passed to :class:`Vehicle`
@@ -702,7 +727,7 @@ def __init__(self,
702
727
"""
703
728
super ().__init__ (** kwargs )
704
729
705
- self ._l = l
730
+ self ._l = L
706
731
self ._steer_max = steer_max
707
732
708
733
def __str__ (self ):
@@ -867,7 +892,7 @@ def Fv(self, x, odo):
867
892
# fmt: on
868
893
return J
869
894
870
- def deriv (self , x , u ):
895
+ def deriv (self , x , u , limits = True ):
871
896
r"""
872
897
Time derivative of state
873
898
@@ -880,12 +905,13 @@ def deriv(self, x, u):
880
905
881
906
Returns the time derivative of state (3x1) at the state ``x`` with
882
907
inputs ``u``.
883
-
884
- .. note:: Vehicle speed and steering limits are not applied here
885
908
"""
886
909
887
910
# unpack some variables
888
911
theta = x [2 ]
912
+
913
+ if limits :
914
+ u = self .u_limited (u )
889
915
v = u [0 ]
890
916
gamma = u [1 ]
891
917
@@ -900,29 +926,29 @@ def u_limited(self, u):
900
926
# limit speed and steer angle
901
927
ulim = np .array (u )
902
928
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 )
904
930
905
931
return ulim
906
932
907
933
# ========================================================================= #
908
934
909
- class Unicycle (Vehicle ):
935
+ class Unicycle (VehicleBase ):
910
936
911
937
def __init__ (self ,
912
- w = 1 ,
938
+ W = 1 ,
913
939
** kwargs ):
914
940
r"""
915
941
Create new unicycle kinematic model
916
942
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
919
945
:param **kwargs: additional arguments passed to :class:`Vehicle`
920
946
constructor
921
947
922
948
:seealso: :class:`.Vehicle`
923
949
"""
924
950
super ().__init__ (** kwargs )
925
- self ._w = w
951
+ self ._w = W
926
952
927
953
def __str__ (self ):
928
954
@@ -1070,39 +1096,52 @@ def u_limited(self, u):
1070
1096
1071
1097
return ulim
1072
1098
1099
+ class DiffSteer (Unicycle ):
1100
+ pass
1073
1101
1074
1102
if __name__ == "__main__" :
1075
- from math import pi
1076
1103
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)
1078
1117
1079
- v = VehiclePolygon ()
1080
- # v = VehicleIcon('greycar2', scale=2, rotation=90)
1118
+ # v = VehiclePolygon()
1119
+ # # v = VehicleIcon('greycar2', scale=2, rotation=90)
1081
1120
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)
1084
1123
1085
- odo = veh .step (1 , 0.3 )
1086
- print (odo )
1124
+ # odo = veh.step(1, 0.3)
1125
+ # print(odo)
1087
1126
1088
- print (veh .x )
1127
+ # print(veh.x)
1089
1128
1090
- print (veh .f ([0 , 0 , 0 ], odo ))
1129
+ # print(veh.f([0, 0, 0], odo))
1091
1130
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)
1097
1136
1098
- return (1 , d_heading )
1137
+ # return (1, d_heading)
1099
1138
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)
1104
1143
1105
- veh .plot_xyt_t ()
1144
+ # veh.plot_xyt_t()
1106
1145
# veh.plot(p)
1107
1146
1108
1147
# t, x = veh.path(5, u=control)
0 commit comments