File size: 12,906 Bytes
a162e39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
from dataclasses import dataclass
import numpy as np
from dronemodel import DroneModel

import param_ as param_
from settings import Settings


@dataclass
class Drone:
    """
    Creates a drone (it is either red or blue / foe or friend
    """
    is_blue: bool = True
    position: np.ndarray((3,)) = np.zeros((3,))
    position_noise: np.ndarray((3,)) = np.zeros((3,))
    drone_model: DroneModel = None
    max_speeds: np.ndarray((3,)) = None
    min_speeds: np.ndarray((3,)) = None
    init_position: np.ndarray((3,)) = None
    init_speed: np.ndarray((3,)) = None
    color = np.ndarray((3,))
    is_alive: bool = True
    is_fired: int = 0
    fires = 0
    step_ = 0
    id_: int = -1
    ttl: float = param_.DURATION  # ttl = Time To Live expressed in seconds
    speed: np.ndarray((3,)) = np.zeros((3,))
    min_positions = np.zeros((3,))
    max_positions = np.array([Settings.perimeter, 2*np.pi, Settings.perimeter_z])

    def __post_init__(self):
        self.drone_model = DroneModel(self.is_blue)
        self.max_speeds = np.array([self.drone_model.max_speed,
                                    2*np.pi,
                                    self.drone_model.max_up_speed])
        self.min_speeds = np.array([0,
                                    0,
                                    -self.drone_model.max_down_speed])

        self.init_position = np.copy(self.position)
        self.init_position_noise = np.copy(self.position_noise)
        self.init_speed = np.copy(self.speed)
        self.color = param_.BLUE_COLOR if self.is_blue else param_.RED_COLOR
        self.ttl = (self.position[0] / self.max_speeds[0]) * param_.TTL_RATIO + param_.TTL_MIN

    def reset(self):
        self.is_alive = True
        self.speed = self.init_speed
        self.color = param_.BLUE_COLOR if self.is_blue else param_.RED_COLOR
        distance_factor = Settings.blue_distance_factor if self.is_blue else Settings.red_distance_factor
        self.position[0] = self.init_position[0]*distance_factor
        self.position[2] = self.init_position[2]*distance_factor
        self.position_noise *= distance_factor

        self.position[0] = np.clip(self.position[0] + np.random.rand() * self.position_noise[0],
                                   param_.GROUNDZONE * 1.1,
                                   param_.PERIMETER * 0.9)
        self.position[1] = self.position[1] + np.random.rand() * self.position_noise[1]
        self.position[2] = np.clip(self.position[2] + np.random.rand() * self.position_noise[2],
                                   param_.GROUNDZONE * 1.1,
                                   param_.PERIMETER_Z * 0.9)

        self.ttl = (self.position[0] / self.max_speeds[0]) * param_.TTL_RATIO + param_.TTL_MIN


    def step(self, action):
        self.step_ = self.step_ + 1  # for debug purposes
        reward = 0
        info = {'ttl': param_.DURATION}
        if self.is_alive:  # if the drone is dead, it no longer moves :)
            pos_xyz, speed_xyz = self.to_xyz(self.position), self.to_xyz(self.speed)
            pos_s, speed_s = \
                self.drone_model.get_trajectory(pos_xyz, speed_xyz, action, np.linspace(0, param_.STEP, 10))
            pos, speed = pos_s.T[-1], speed_s.T[-1]
            self.position, self.speed = self.from_xyz(pos), self.from_xyz(speed)
            self.ttl -= param_.STEP
            info['ttl'] = self.ttl

            # evaluate the distance compared to the greedy action

            if self.is_blue:
                '''
                for further usage
                straight_action, time_to_catch = self.simple_blue()
                tolerance = 0.05 if 4 < time_to_catch else 1 if 2 < time_to_catch else 3
                distance = 1 if tolerance < np.linalg.norm(straight_action - action) else 0
                '''
                distance = 1
            else:
                straight_action = self.simple_red()
                distance = 1 if 0.1 < np.linalg.norm(straight_action - action) else 0
            info['distance_to_straight_action'] = distance


            if self._hits_target():
                info['hits_target'] = 1
                reward = -param_.TARGET_HIT_COST
                self.color = param_.RED_SUCCESS_COLOR
                self.is_alive = False  # the red has done its job ...

            if self._out_of_bounds():
                coef = -1 if self.is_blue else 1
                reward = coef * param_.OOB_COST
                self.is_alive = False
                info['oob'] = 1

        obs = self.get_observation()
        done = not self.is_alive

        return obs, reward, done, info

    def _out_of_bounds(self):
        return not (0 < self.position[2] < Settings.perimeter_z and self.position[0] < Settings.perimeter)

    def _hits_target(self):
        if self.is_blue:
            return False
        else:
            distance_to_zero = np.sqrt(self.position[0]**2 + self.position[2]**2)
            return distance_to_zero < Settings.groundzone

    def fires_(self, foe) -> bool:
        """
        checks if the foe drone is hit by self
        :param foe: a foe drone
        :return: True= yes, got you
        """
        # deads don't kill nor die
        if not (self.is_alive and foe.is_alive):
            return False

        # lets see if foe is in the "fire cone"
        pos_xyz = - self.to_xyz(self.position) + self.to_xyz(foe.position)
        distance = np.linalg.norm(pos_xyz)
        pos_xyz /= distance

        if distance < self.drone_model.distance_to_neutralisation:
            return self.is_in_the_cone(foe)

        return False


    def is_in_the_cone(self, foe) -> bool:
        '''
        verifies if foe is in the cone (without any regard to distance)
        :param foe:
        :return:
        '''
        pos_xyz = - self.to_xyz(self.position) + self.to_xyz(foe.position)
        pos_xyz /= np.linalg.norm(pos_xyz)
        vit_xyz = self.to_xyz(self.speed)
        vit_xyz /= np.linalg.norm(vit_xyz)
        cos_theta = np.dot(pos_xyz, vit_xyz)
        in_the_cone = False
        if 0 < cos_theta:
            theta = np.arccos(cos_theta)
            in_the_cone = theta < self.drone_model.angle_to_neutralisation
        return in_the_cone


    # tell the drones that they are dead
    def is_killed(self, is_blue=True):
        self.is_alive = False
        self.position[2] = 0
        self.color = param_.BLUE_DEAD_COLOR if is_blue else param_.RED_DEAD_COLOR

    def to_xyz(self, rho_theta_z: np.ndarray(shape=(3,))) -> np.ndarray(shape=(3,)):
        """
        allows to get the 3D xyz coordinates from a polar representation
        :param rho_theta_z: array (3,) with rho in meter, theta in rad, zed in meter for positions, /s for speeds, etc.
        :return: float array (3,) with x, y, z in meter, /s for speeds, etc.
        """
        xy_ = rho_theta_z[0] * np.exp(1j * rho_theta_z[1])
        return np.array([np.real(xy_), np.imag(xy_), rho_theta_z[2]])

    def from_xyz(self, xyz: np.ndarray(shape=(3,))) -> np.ndarray(shape=(3,)):
        """
        """
        z_complex = xyz[0] + 1j*xyz[1]
        rho = np.abs(z_complex)
        theta = np.angle(z_complex)
        return np.array([rho, theta, xyz[2]], dtype='float32')

    def to_norm(self,
                rho_theta_z: np.ndarray(shape=(3,)),
                max_vector: np.ndarray(shape=(3,)),
                min_vector: np.ndarray(shape=(3,)) = np.array([0, 0, 0]))\
            -> np.ndarray(shape=(3,), dtype='float32'):
        """
        normalises the position/speed in order to have all space in a [0;1]**3 space
        :return: rho, theta, zed in a [0;1]**3 space
        """
        rho = rho_theta_z[0] / max_vector[0]
        theta = (rho_theta_z[1] / (2 * np.pi)) % 1
        zed = (rho_theta_z[2] - min_vector[2]) / (max_vector[2] - min_vector[2])
        return np.array([rho, theta, zed], dtype='float32')

    def from_norm(self,
                  norm: np.ndarray(shape=(3,)),
                  max_vector: np.ndarray(shape=(3,)),
                  min_vector: np.ndarray(shape=(3,)) = np.array([0, 0, 0]))\
            -> np.ndarray(shape=(3,), dtype='float32'):
        """
        denormalises and renders into cylindric coordinates
        :param norm:
        :param max_vector:
        :param min_vector:
        :return:
        """
        rho = norm[0] * max_vector[0]
        theta = norm[1] * 2*np.pi
        zed = norm[2] * (max_vector[2] - min_vector[2]) + min_vector[2]
        return np.array([rho, theta, zed], dtype='float32')

    def to_lat_lon_zed(self, lat, lon):
        z = self.position[0] * np.exp(1j * self.position[1])
        lat = np.imag(z) * 360 / (40075 * 1000) + lat
        lon = np.real(z) * 360 / (40075 * 1000 * np.cos(np.pi / 180 * lat)) + lon
        return lat, lon, self.position[2]

    def distance(self, other_drone=None):

        if other_drone:
            distance = np.sqrt(np.abs(self.position[0] * np.exp(1j * self.position[1]) -
                                      other_drone.position[0] * np.exp(1j * other_drone.position[1])) ** 2 +
                               (self.position[2] - other_drone.position[2]) ** 2)
        else:
            distance = np.sqrt((self.position[0] ** 2) + self.position[2] ** 2)
        return distance

    def get_observation(self):  # -> np.array(shape=(6,), dtype='float32'):
        """
        get normalised and transformed position and speed
        :return:
        """
        # calculates transformed normalised position
        normalised_position = self.to_norm(self.position, self.max_positions, self.min_positions)

        # calculates transformed normalised speed
        normalised_speed = self.to_norm(self.speed, self.max_speeds, self.min_speeds)

        return np.append(normalised_position, normalised_speed)


    def simple_red(self, target: np.ndarray(3,)=np.zeros(3), z_margin: float=50) -> np.ndarray(shape=(3,)):
        '''
        defines the actions for a trajectory targeting zero
        :return:
        '''

        self_z = self.position[0] * np.exp(1j * self.position[1])
        target_z = target[0] * np.exp(1j * target[1])

        direction = np.zeros(3)
        direction[0] = np.abs(self_z - target_z)
        direction[1] = np.angle(self_z - target_z)
        direction[2] = self.position[2] - target[2] - z_margin

        theta = (direction[1] + np.pi) / (2*np.pi) % 1

        # slope of drone given its position
        tan_phi = np.sign(direction[2]) * np.inf if direction[0] == 0 else direction[2]/direction[0]
        # slope of drone speed
        tan_phi_point = np.sign(self.speed[2]) * np.inf if self.speed[0] == 0 else self.speed[2]/self.speed[0]
        # slope of forces
        f_ratio = self.drone_model.Fxy / self.drone_model.Fz_minus
        # go up if speed slope is too steep and vertical speed < 0 else take the position angle for forces angle
        psy = -np.arctan(tan_phi * f_ratio) / np.pi + 0.5

        action = np.array([1, theta, psy])

        return action

    def simple_target(self, target: (np.ndarray(shape=(3,)))) -> np.ndarray(shape=(3,)):
        '''
        defines the actions for a trajectory targeting ... the given target
        :return:
        '''

        self_z = self.position[0] * np.exp(1j * self.position[1])
        target_z = target[0] * np.exp(1j * target[1])

        direction = np.zeros(3)
        direction[0] = np.abs(self_z - target_z)
        direction[1] = np.angle(self_z - target_z)
        direction[2] = self.position[2] - target[2]

        theta = (direction[1] + np.pi) / (2*np.pi) % 1

        # slope of drone given its position
        tan_phi = np.sign(direction[2]) * np.inf if direction[0] == 0 else direction[2]/direction[0]
        # slope of drone speed
        tan_phi_point = np.sign(self.speed[2]) * np.inf if self.speed[0] == 0 else self.speed[2]/self.speed[0]
        # slope of forces
        f_ratio = self.drone_model.Fxy / self.drone_model.Fz_minus
        # go up if speed slope is too steep and vertical speed < 0 else take the position angle for forces angle
        psy = 0.5 if tan_phi_point < -tan_phi else -np.arctan(tan_phi * f_ratio) / np.pi + 0.5

        if Settings.perimeter_z / 2 < direction[2]:
            psy = min(0.2, psy)

        if self.position[0] < 1.5 * Settings.groundzone:
            psy = min(0.2, psy)


        action = np.array([1, theta, psy])



        return action

    def next_simple_pos(self):
        next_pos = np.zeros(3)
        simple_next = self.pos[0] * np.exp(1j * self.pos[1]) + self.speed[0] * np.exp(1j * self.speed[1]) * param_.step
        next_pos[0] = np.abs(simple_next)
        next_pos[1] = np.arg(simple_next)
        next_pos[2] = self.pos[2] + self.speed[2] * param_.step
        return next_pos

    def copy_pos_speed(self, drone_to_copy):
        self.position = drone_to_copy.position
        self.speed = drone_to_copy.speed