diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ec3b9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +__pycache__ +TODO.md +script.py diff --git a/MainWindow_map.py b/MainWindow_map.py new file mode 100644 index 0000000..9a06b55 --- /dev/null +++ b/MainWindow_map.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'MainWindow_map.ui' +# +# Created by: PyQt5 UI code generator 5.15.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1297, 900) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setContentsMargins(-1, -1, -1, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setContentsMargins(-1, -1, 0, -1) + self.horizontalLayout.setObjectName("horizontalLayout") + self.formLayout = QtWidgets.QFormLayout() + self.formLayout.setObjectName("formLayout") + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) + self.comboBox_map = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_map.setObjectName("comboBox_map") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.comboBox_map) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setObjectName("label_2") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setObjectName("label_3") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3) + self.label_8 = QtWidgets.QLabel(self.centralwidget) + self.label_8.setObjectName("label_8") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_8) + self.comboBox_axis = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_axis.setObjectName("comboBox_axis") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.comboBox_axis) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.lineEdit_coverr = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit_coverr.setAlignment(QtCore.Qt.AlignCenter) + self.lineEdit_coverr.setObjectName("lineEdit_coverr") + self.horizontalLayout_2.addWidget(self.lineEdit_coverr) + self.label_10 = QtWidgets.QLabel(self.centralwidget) + self.label_10.setObjectName("label_10") + self.horizontalLayout_2.addWidget(self.label_10) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2) + self.lineEdit_shipn = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit_shipn.setAlignment(QtCore.Qt.AlignCenter) + self.lineEdit_shipn.setObjectName("lineEdit_shipn") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.lineEdit_shipn) + self.horizontalLayout.addLayout(self.formLayout) + self.formLayout_2 = QtWidgets.QFormLayout() + self.formLayout_2.setObjectName("formLayout_2") + self.label_6 = QtWidgets.QLabel(self.centralwidget) + self.label_6.setObjectName("label_6") + self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_6) + self.comboBox_render = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_render.setObjectName("comboBox_render") + self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.comboBox_render) + self.label_7 = QtWidgets.QLabel(self.centralwidget) + self.label_7.setObjectName("label_7") + self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_7) + self.comboBox_around = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_around.setObjectName("comboBox_around") + self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.comboBox_around) + self.label_11 = QtWidgets.QLabel(self.centralwidget) + self.label_11.setObjectName("label_11") + self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_11) + self.comboBox_bettery = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_bettery.setObjectName("comboBox_bettery") + self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.comboBox_bettery) + self.label_9 = QtWidgets.QLabel(self.centralwidget) + self.label_9.setObjectName("label_9") + self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_9) + self.lineEdit_bettery = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit_bettery.setAlignment(QtCore.Qt.AlignCenter) + self.lineEdit_bettery.setObjectName("lineEdit_bettery") + self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.lineEdit_bettery) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setObjectName("label_4") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_4) + self.comboBox_algo = QtWidgets.QComboBox(self.centralwidget) + self.comboBox_algo.setObjectName("comboBox_algo") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.comboBox_algo) + self.horizontalLayout.addLayout(self.formLayout_2) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.pushButton_run = QtWidgets.QPushButton(self.centralwidget) + font = QtGui.QFont() + font.setFamily("宋体") + font.setPointSize(12) + self.pushButton_run.setFont(font) + self.pushButton_run.setObjectName("pushButton_run") + self.verticalLayout_2.addWidget(self.pushButton_run) + self.pushButton_stop = QtWidgets.QPushButton(self.centralwidget) + font = QtGui.QFont() + font.setFamily("宋体") + font.setPointSize(12) + self.pushButton_stop.setFont(font) + self.pushButton_stop.setObjectName("pushButton_stop") + self.verticalLayout_2.addWidget(self.pushButton_stop) + self.horizontalLayout.addLayout(self.verticalLayout_2) + self.horizontalLayout.setStretch(0, 1) + self.horizontalLayout.setStretch(1, 1) + self.horizontalLayout.setStretch(2, 1) + self.verticalLayout.addLayout(self.horizontalLayout) + self.label_show = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("微软雅黑") + font.setPointSize(12) + self.label_show.setFont(font) + self.label_show.setObjectName("label_show") + self.verticalLayout.addWidget(self.label_show) + self.verticalLayout.setStretch(0, 1) + self.verticalLayout.setStretch(1, 10) + self.horizontalLayout_3.addLayout(self.verticalLayout) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.pushButton_run.clicked.connect(MainWindow.run) + self.pushButton_stop.clicked.connect(MainWindow.stop) + self.comboBox_bettery.currentTextChanged['QString'].connect(MainWindow.slot_bettery) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "SeAI Palette集智调色板")) + self.label.setText(_translate("MainWindow", "地图选择")) + self.label_2.setText(_translate("MainWindow", "最低覆盖率")) + self.label_3.setText(_translate("MainWindow", "移动节点数量")) + self.label_8.setText(_translate("MainWindow", "区域划分方向")) + self.lineEdit_coverr.setText(_translate("MainWindow", "100")) + self.label_10.setText(_translate("MainWindow", "%")) + self.lineEdit_shipn.setText(_translate("MainWindow", "1")) + self.label_6.setText(_translate("MainWindow", "是否渲染")) + self.label_7.setText(_translate("MainWindow", "考虑固定节点")) + self.label_11.setText(_translate("MainWindow", "是否需要充电")) + self.label_9.setText(_translate("MainWindow", "移动节点电池容量")) + self.lineEdit_bettery.setText(_translate("MainWindow", "100")) + self.label_4.setText(_translate("MainWindow", "算法选择")) + self.pushButton_run.setText(_translate("MainWindow", "运行")) + self.pushButton_stop.setText(_translate("MainWindow", "停止")) + self.label_show.setText(_translate("MainWindow", "


")) diff --git a/MainWindow_map.ui b/MainWindow_map.ui new file mode 100644 index 0000000..0db2ba6 --- /dev/null +++ b/MainWindow_map.ui @@ -0,0 +1,264 @@ + + + MainWindow + + + + 0 + 0 + 1297 + 900 + + + + SeAI Palette集智调色板 + + + + + + + 0 + + + + + 0 + + + + + + + 地图选择 + + + + + + + + + + 最低覆盖率 + + + + + + + 移动节点数量 + + + + + + + 区域划分方向 + + + + + + + + + + + + 100 + + + Qt::AlignCenter + + + + + + + % + + + + + + + + + 1 + + + Qt::AlignCenter + + + + + + + + + + + 是否渲染 + + + + + + + + + + 考虑固定节点 + + + + + + + + + + 是否需要充电 + + + + + + + + + + 移动节点电池容量 + + + + + + + 100 + + + Qt::AlignCenter + + + + + + + 算法选择 + + + + + + + + + + + + + + + 宋体 + 12 + + + + 运行 + + + + + + + + 宋体 + 12 + + + + 停止 + + + + + + + + + + + + 微软雅黑 + 12 + + + + <html><head/><body><p><br/></p></body></html> + + + + + + + + + + + + pushButton_run + clicked() + MainWindow + run() + + + 1186 + 39 + + + 1287 + 49 + + + + + pushButton_stop + clicked() + MainWindow + stop() + + + 1275 + 160 + + + 1273 + 133 + + + + + comboBox_bettery + currentTextChanged(QString) + MainWindow + slot_bettery() + + + 792 + 120 + + + 899 + -8 + + + + + + run() + stop() + slot_bettery() + + diff --git a/Palette/algos/__init__.py b/Palette/algos/__init__.py new file mode 100644 index 0000000..3337788 --- /dev/null +++ b/Palette/algos/__init__.py @@ -0,0 +1,23 @@ +from .boustrophedon import Boustrophedon +from .spiral import Spiral +from .wildfire import WildFire +from .split_area import split_area +from .multi_boustrophedon import MultiBoustrophedon +from .multi_greedy import MultiGreedy +from .multi_wildfire import MultiWildFire +from .charge import Charge +from .multi_charge import MultiCharge +from .multi_spiral import MultiSpiral + +__all__ = [ + "Boustrophedon", + "WildFire", + "Spiral", + "split_area", + "MultiBoustrophedon", + "MultiWildFire", + "Charge", + "MultiCharge", + "MultiSpiral", + "MultiGreedy" +] diff --git a/Palette/algos/boustrophedon.py b/Palette/algos/boustrophedon.py new file mode 100644 index 0000000..b9c74db --- /dev/null +++ b/Palette/algos/boustrophedon.py @@ -0,0 +1,139 @@ +import numpy as np +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN + + +class Boustrophedon(object): + def __init__(self): + super().__init__() + self.dest_direction = [None] + + @ staticmethod + def _right_index(x, y): + return x+1, y + + @ staticmethod + def _left_index(x, y): + return x-1, y + + @ staticmethod + def _up_index(x, y): + return x, y-1 + + @ staticmethod + def _down_index(x, y): + return x, y+1 + + @ staticmethod + def _is_valid_index(index, an_array): + x, y = index + return 0 <= x < an_array.shape[0] and 0 <= y < an_array.shape[1] + + def _count_valid_steps(self, index, direction, fields): + if direction == UP: + update_index = self._up_index + elif direction == DOWN: + update_index = self._down_index + elif direction == LEFT: + update_index = self._left_index + elif direction == RIGHT: + update_index = self._right_index + else: + raise ValueError(f'invalid direction: {direction}') + + valid_steps = 0 + cur_x, cur_y = index + while True: + cur_x, cur_y = update_index(cur_x, cur_y) + if not self._is_valid_index((cur_x, cur_y), fields) \ + or fields[cur_x, cur_y] != 0: + break + else: + valid_steps += 1 + return valid_steps + + def _act(self, direction, fields, index, ship_id): + x, y = index + if direction == UP: + continue_update_index = self._up_index + reverse_update_index = self._down_index + possible_turn = [LEFT, RIGHT] + reverse_direction = DOWN + elif direction == DOWN: + continue_update_index = self._down_index + reverse_update_index = self._up_index + possible_turn = [LEFT, RIGHT] + reverse_direction = UP + elif direction == LEFT: + continue_update_index = self._left_index + reverse_update_index = self._right_index + possible_turn = [UP, DOWN] + reverse_direction = RIGHT + elif direction == RIGHT: + continue_update_index = self._right_index + reverse_update_index = self._left_index + possible_turn = [UP, DOWN] + reverse_direction = LEFT + else: + raise ValueError(f'invalid direction: {direction}') + + if not self._is_valid_index(continue_update_index(x, y), fields) \ + or fields[continue_update_index(x, y)] != 0: + + steps0 = self._count_valid_steps((x, y), possible_turn[0], fields) + steps1 = self._count_valid_steps((x, y), possible_turn[1], fields) + + if steps0 > 0 or steps1 > 0: + self.dest_direction[ship_id] = reverse_direction + if steps0 > 0 and steps1 > 0: + if steps0 < steps1: + return possible_turn[1] + else: + return possible_turn[0] + elif steps0 > 0: + return possible_turn[0] + else: + return possible_turn[1] + else: + search_x, search_y = reverse_update_index(x, y) + can_continue = False + while self._is_valid_index((search_x, search_y), fields) and fields[search_x, search_y] == 0: + search_steps0 = self._count_valid_steps( + (search_x, search_y), possible_turn[0], fields) + search_steps1 = self._count_valid_steps( + (search_x, search_y), possible_turn[1], fields) + if search_steps0 or search_steps1: + can_continue = True + break + search_x, search_y = reverse_update_index( + search_x, search_y) + if can_continue: + return reverse_direction + else: + return BROKEN + else: + return direction + + def step(self, state, info): + x, y, direction = state + fields = info['fields'] + + if np.sum(fields == 0) == 0: + return [FINISHED] # finished + + if info['redecide_direction']: + up_steps = self._count_valid_steps((x, y), UP, fields) + down_steps = self._count_valid_steps((x, y), DOWN, fields) + left_steps = self._count_valid_steps((x, y), LEFT, fields) + right_steps = self._count_valid_steps((x, y), RIGHT, fields) + + direction = [UP, DOWN, LEFT, RIGHT][np.argmax( + [up_steps, down_steps, left_steps, right_steps])] + + if self.dest_direction[0] is not None: + d = self.dest_direction[0] + self.dest_direction = [None] + if self._count_valid_steps((x, y), d, fields) > 0: + return [d] + else: + return [BROKEN] + return [self._act(direction, fields, (x, y), 0)] diff --git a/Palette/algos/charge.py b/Palette/algos/charge.py new file mode 100644 index 0000000..135af98 --- /dev/null +++ b/Palette/algos/charge.py @@ -0,0 +1,60 @@ +import numpy as np + +from Palette.algos.wildfire import WildFire +from Palette.algos.utils import Node +from Palette.constants import FILLING_UP, FILLING_DOWN, FILLING_LEFT, FILLING_RIGHT, FINISHED, BROKEN, FILLING, FILLING_BACK + + +class Charge(WildFire): + def __init__(self, filling_time: int): + super().__init__() + self.filling_time = filling_time + + def step(self, state, info): + if not self.empty(): + # if there're actions to do + action = self.actions_to_do[0] + del self.actions_to_do[0] + return action, None + + x, y, _ = state + fields = info['fields'] + + if np.sum(fields == 0) == 0: + return FINISHED, None + + root_node = Node(x, y, None, energy=True) + buffer = [root_node] + visited = np.zeros_like(fields) + visited[x, y] = True + while len(buffer) > 0: + cur_node = buffer[0] + del buffer[0] + for dx, dy in zip(self.delta_x, self.delta_y): + cur_x, cur_y = cur_node.x, cur_node.y + next_x, next_y = cur_x+dx, cur_y+dy + if self._is_valid_index((next_x, next_y), fields) and not visited[next_x, next_y]: + visited[next_x, next_y] = True + next_node = Node(next_x, next_y, cur_node, energy=True) + if fields[next_x, next_y] == -1: + # not visited + node = next_node + while node.direction is not None: + self.actions_to_do.append(node.direction) + node = node.father + self.actions_to_do.reverse() + charge_len = len(self.actions_to_do) + + self.actions_to_do.extend( + [FILLING for _ in range(self.filling_time)]) + self.actions_to_do.extend( + [FILLING_BACK for _ in range(charge_len)]) + + action = self.actions_to_do[0] + del self.actions_to_do[0] + + return action, charge_len + else: + buffer.append(next_node) + + raise RuntimeError(f'Charge runtime error!') diff --git a/Palette/algos/multi_boustrophedon.py b/Palette/algos/multi_boustrophedon.py new file mode 100644 index 0000000..ebffc84 --- /dev/null +++ b/Palette/algos/multi_boustrophedon.py @@ -0,0 +1,102 @@ +import numpy as np +from Palette.algos.boustrophedon import Boustrophedon +from Palette.algos.multi_wildfire import MultiWildFire +from Palette.algos.multi_charge import MultiCharge +from Palette.algos.utils import Node +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN, \ + FILLING_UP, FILLING_DOWN, FILLING_LEFT, FILLING_RIGHT + +from typing import List + + +class MultiBoustrophedon(Boustrophedon): + def __init__( + self, + ship_num: int, + wildfires: MultiWildFire, + charges: MultiCharge + ): + super().__init__() + self.ship_num = ship_num + self.dest_direction = [None for _ in range(ship_num)] + self.wildfires = wildfires + self.charges = charges + self.finished_idx = [False for _ in range(ship_num)] + + #self.ship_energy = ship_energy + #self.current_ship_energy = [ship_energy for _ in range(self.ship_num)] + + self.delta_x = (0, 0, 1, -1, ) # 1, 1, -1, -1) + self.delta_y = (1, -1, 0, 0, ) # 1, -1, 1, -1) + + def step(self, state, info): + ret_directions = [] + redecide_directions = [False for _ in range(self.ship_num)] + + for i, (x, y, direction) in enumerate(state): + if self.finished_idx[i]: + ret_directions.append(FINISHED) + continue + + if not self.charges[i].empty(): + ret_directions.append(self.charges[i].step(state, info)[0]) + if self.charges[i].empty(): + redecide_directions[i] = True + continue + + # Decide whether charge or not + first_charge_act, charge_len = self.charges.step(state, info, i) + if first_charge_act == FINISHED: + self.finished_idx[i] = True + ret_directions.append(FINISHED) + continue + + if info['batteries'][i] < charge_len + 2: + ret_directions.append(first_charge_act) + redecide_directions[i] = True + continue + else: + self.charges[i].clear() + self.wildfires[i].clear() + + if not self.wildfires[i].empty(): + ret_directions.append(self.wildfires[i].step(state, info)[0]) + if self.wildfires[i].empty(): + redecide_directions[i] = True + continue + + fields = info['fields'][i] + + if np.sum(fields == 0) == 0: + self.finished_idx[i] = True + ret_directions.append(FINISHED) # finished + continue + + if info['redecide_direction'][i]: + up_steps = self._count_valid_steps((x, y), UP, fields) + down_steps = self._count_valid_steps((x, y), DOWN, fields) + left_steps = self._count_valid_steps((x, y), LEFT, fields) + right_steps = self._count_valid_steps((x, y), RIGHT, fields) + + direction = [UP, DOWN, LEFT, RIGHT][np.argmax( + [up_steps, down_steps, left_steps, right_steps])] + + if self.dest_direction[i] is not None: + d = self.dest_direction[i] + self.dest_direction[i] = None + + if self._count_valid_steps((x, y), d, fields) > 0: + ret_directions.append(d) + else: + ret_directions.append( + self.wildfires.step(state, info, i)[0]) + continue + + cur_act = self._act(direction, fields, (x, y), i) + if cur_act == BROKEN: + ret_directions.append(self.wildfires.step(state, info, i)[0]) + redecide_directions[i] = True + else: + ret_directions.append(cur_act) + + return np.asarray(ret_directions), redecide_directions diff --git a/Palette/algos/multi_charge.py b/Palette/algos/multi_charge.py new file mode 100644 index 0000000..dde1b13 --- /dev/null +++ b/Palette/algos/multi_charge.py @@ -0,0 +1,23 @@ +from Palette.algos.charge import Charge + + +class MultiCharge(object): + def __init__(self, n_ships: int, filling_time: int): + super().__init__() + self._n_ships = n_ships + self._filling_time = filling_time + self._charges = [Charge(self._filling_time) + for _ in range(self._n_ships)] + + def __len__(self): + return self._n_ships + + def __getitem__(self, idx): + return self._charges[idx] + + def step(self, state, info, idx): + agent_info = {k: info[k] + for k in (info.keys()-{'fields', 'redecide_direction'})} + agent_info['fields'] = info['fields'][idx] + agent_info['redecide_direction'] = info['redecide_direction'][idx] + return self._charges[idx].step(state[idx], agent_info) diff --git a/Palette/algos/multi_greedy.py b/Palette/algos/multi_greedy.py new file mode 100644 index 0000000..24c8688 --- /dev/null +++ b/Palette/algos/multi_greedy.py @@ -0,0 +1,96 @@ +import numpy as np +from Palette.algos.boustrophedon import Boustrophedon +from Palette.algos.multi_wildfire import MultiWildFire +from Palette.algos.multi_charge import MultiCharge +from Palette.algos.utils import Node +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN, \ + FILLING_UP, FILLING_DOWN, FILLING_LEFT, FILLING_RIGHT + +from typing import List + + +class MultiGreedy(Boustrophedon): + def __init__( + self, + ship_num: int, + wildfires: MultiWildFire, + charges: MultiCharge + ): + super().__init__() + self.ship_num = ship_num + self.dest_direction = [None for _ in range(ship_num)] + self.wildfires = wildfires + self.charges = charges + self.finished_idx = [False for _ in range(ship_num)] + + #self.ship_energy = ship_energy + #self.current_ship_energy = [ship_energy for _ in range(self.ship_num)] + + self.delta_x = (0, 0, 1, -1, ) # 1, 1, -1, -1) + self.delta_y = (1, -1, 0, 0, ) # 1, -1, 1, -1) + + def step(self, state, info): + ret_directions = [] + redecide_directions = [False for _ in range(self.ship_num)] + + for i, (x, y, direction) in enumerate(state): + if self.finished_idx[i]: + ret_directions.append(FINISHED) + continue + + if not self.charges[i].empty(): + ret_directions.append(self.charges[i].step(state, info)[0]) + if self.charges[i].empty(): + redecide_directions[i] = True + continue + + # Decide whether charge or not + first_charge_act, charge_len = self.charges.step(state, info, i) + if first_charge_act == FINISHED: + self.finished_idx[i] = True + ret_directions.append(FINISHED) + continue + + if info['batteries'][i] < charge_len + 2: + ret_directions.append(first_charge_act) + redecide_directions[i] = True + continue + else: + self.charges[i].clear() + self.wildfires[i].clear() + + if not self.wildfires[i].empty(): + ret_directions.append(self.wildfires[i].step(state, info)[0]) + if self.wildfires[i].empty(): + redecide_directions[i] = True + continue + + fields = info['fields'][i] + + if np.sum(fields == 0) == 0: + self.finished_idx[i] = True + ret_directions.append(FINISHED) # finished + continue + + up_steps = self._count_valid_steps((x, y), UP, fields) + down_steps = self._count_valid_steps((x, y), DOWN, fields) + left_steps = self._count_valid_steps((x, y), LEFT, fields) + right_steps = self._count_valid_steps((x, y), RIGHT, fields) + + if up_steps == 0 and down_steps == 0 and left_steps == 0 and right_steps == 0: + cur_act = BROKEN + else: + ds, ss = [], [] + for d, s in zip((UP, DOWN, LEFT, RIGHT), (up_steps, down_steps, left_steps, right_steps)): + if s > 0: + ds.append(d) + ss.append(s) + cur_act = ds[np.argmin(ss)] + + if cur_act == BROKEN: + ret_directions.append(self.wildfires.step(state, info, i)[0]) + redecide_directions[i] = True + else: + ret_directions.append(cur_act) + + return np.asarray(ret_directions), redecide_directions diff --git a/Palette/algos/multi_spiral.py b/Palette/algos/multi_spiral.py new file mode 100644 index 0000000..90d4c2a --- /dev/null +++ b/Palette/algos/multi_spiral.py @@ -0,0 +1,93 @@ +import numpy as np +from Palette.algos.spiral import Spiral +from Palette.algos.multi_wildfire import MultiWildFire +from Palette.algos.multi_charge import MultiCharge +from Palette.algos.utils import Node +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN, \ + FILLING_UP, FILLING_DOWN, FILLING_LEFT, FILLING_RIGHT + +from typing import List + + +class MultiSpiral(Spiral): + def __init__( + self, + ship_num: int, + wildfires: MultiWildFire, + charges: MultiCharge + ): + super().__init__() + self.ship_num = ship_num + # self.dest_direction = [None for _ in range(ship_num)] + self.wildfires = wildfires + self.charges = charges + self.finished_idx = [False for _ in range(ship_num)] + + #self.ship_energy = ship_energy + #self.current_ship_energy = [ship_energy for _ in range(self.ship_num)] + + self.delta_x = [0, 0, 1, -1, 1, 1, -1, -1] + self.delta_y = [1, -1, 0, 0, 1, -1, 1, -1] + + def step(self, state, info): + ret_directions = [] + redecide_directions = [False for _ in range(self.ship_num)] + + for i, (x, y, direction) in enumerate(state): + if self.finished_idx[i]: + ret_directions.append(FINISHED) + continue + + if not self.charges[i].empty(): + ret_directions.append(self.charges[i].step(state, info)[0]) + if self.charges[i].empty(): + redecide_directions[i] = True + continue + + # Decide whether charge or not + first_charge_act, charge_len = self.charges.step(state, info, i) + if first_charge_act == FINISHED: + self.finished_idx[i] = True + ret_directions.append(FINISHED) + continue + + if info['batteries'][i] < charge_len + 2: + ret_directions.append(first_charge_act) + redecide_directions[i] = True + continue + else: + self.charges[i].clear() + self.wildfires[i].clear() + + if not self.wildfires[i].empty(): + ret_directions.append(self.wildfires[i].step(state, info)[0]) + if self.wildfires[i].empty(): + redecide_directions[i] = True + continue + + fields = info['fields'][i] + + if np.sum(fields == 0) == 0: + self.finished_idx[i] = True + ret_directions.append(FINISHED) # finished + continue + + if info['redecide_direction'][i]: + all_directions = (UP, DOWN, LEFT, RIGHT) + all_update_fn = (self._up_index, self._down_index, + self._left_index, self._right_index) + edge_directions = [self._is_edge( + update_fn(x, y), fields, direction) for update_fn in all_update_fn] + valid_steps = [self._count_valid_steps( + (x, y), d, fields) for d in all_directions] + direction = np.max([Spiral.MyTuple(e, v, d) for e, v, d in zip( + edge_directions, valid_steps, all_directions)]).direction + + cur_act = self._act(direction, fields, (x, y)) + if cur_act == BROKEN: + ret_directions.append(self.wildfires.step(state, info, i)[0]) + redecide_directions[i] = True + else: + ret_directions.append(cur_act) + + return np.asarray(ret_directions), redecide_directions diff --git a/Palette/algos/multi_split_area.py b/Palette/algos/multi_split_area.py new file mode 100644 index 0000000..222fc13 --- /dev/null +++ b/Palette/algos/multi_split_area.py @@ -0,0 +1,54 @@ +import numpy as np + + +def multi_split_area( + fields: np.ndarray, + field_size: int, + n_areas: int, + axis: str +): + assert axis in ['x', 'y'], f"Invalid axis: {axis}" + splited_fields = np.zeros_like(fields) + blank_spaces_num = np.sum(fields == 0) + + last_pos = 0 + area_spaces = [] + start_points = [] + for area_id in range(1, n_areas+1): + p = last_pos + cur_area_spaces = 0 + start_point_assigned = False + while True: + cur_spaces = np.sum(fields[p, :] == 0) if axis == 'x' else np.sum( + fields[:, p] == 0) + cur_area_spaces += cur_spaces + if axis == 'x': + for i in range(fields.shape[1]): + if fields[p, i] != -1: + splited_fields[p, i] = area_id + if not start_point_assigned and fields[p, i] == 0: + start_points.append((p, i)) + start_point_assigned = True + else: + splited_fields[p, i] = -1 + else: + for i in range(fields.shape[0]): + if fields[i, p] != -1: + splited_fields[i, p] = area_id + if not start_point_assigned and fields[i, p] == 0: + start_points.append((i, p)) + start_point_assigned = True + else: + splited_fields[i, p] = -1 + p += 1 + if (axis == 'x' and p >= fields.shape[0]) \ + or (axis == 'y' and p >= fields.shape[1]) \ + or np.sum(area_spaces) + cur_area_spaces >= blank_spaces_num * area_id / n_areas: + break + last_pos = p + area_spaces.append(cur_area_spaces) + start_points = np.asarray(start_points) + start_points[:, 1] = fields.shape[1] - start_points[:, 1] - 1 + start_points = start_points * field_size + field_size / 2 + assert np.sum(splited_fields == 0) == 0 + return splited_fields, np.asarray(start_points) diff --git a/Palette/algos/multi_wildfire.py b/Palette/algos/multi_wildfire.py new file mode 100644 index 0000000..68c73f3 --- /dev/null +++ b/Palette/algos/multi_wildfire.py @@ -0,0 +1,21 @@ +from Palette.algos.wildfire import WildFire + + +class MultiWildFire(object): + def __init__(self, n_ships: int): + super().__init__() + self._n_ships = n_ships + self._wildfires = [WildFire() for _ in range(self._n_ships)] + + def __len__(self): + return self._n_ships + + def __getitem__(self, idx): + return self._wildfires[idx] + + def step(self, state, info, idx): + agent_info = {k: info[k] + for k in (info.keys()-{'fields', 'redecide_direction'})} + agent_info['fields'] = info['fields'][idx] + agent_info['redecide_direction'] = info['redecide_direction'][idx] + return self._wildfires[idx].step(state[idx], agent_info) diff --git a/Palette/algos/spiral.py b/Palette/algos/spiral.py new file mode 100644 index 0000000..2e9a7fa --- /dev/null +++ b/Palette/algos/spiral.py @@ -0,0 +1,172 @@ +import numpy as np +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN + + +class Spiral(object): + class MyTuple: + def __init__(self, x1, x2, direction): + self.data = (x1, x2) + self.direction = direction + + def __lt__(self, other): + return self.data < other.data + + def __le__(self, other): + return self.data <= other.data + + def __gt__(self, other): + return self.data > other.data + + def __ge__(self, other): + return self.data >= other.data + + def __eq__(self, other): + return self.data == other.data + + def __ne__(self, other): + return self.data != other.data + + def __init__(self,): + super().__init__() + self.delta_x = [0, 0, 1, -1, 1, 1, -1, -1] + self.delta_y = [1, -1, 0, 0, 1, -1, 1, -1] + + @ staticmethod + def _right_index(x, y): + return x+1, y + + @ staticmethod + def _left_index(x, y): + return x-1, y + + @ staticmethod + def _up_index(x, y): + return x, y-1 + + @ staticmethod + def _down_index(x, y): + return x, y+1 + + @ staticmethod + def _is_valid_index(index, an_array): + x, y = index + return 0 <= x < an_array.shape[0] and 0 <= y < an_array.shape[1] + + def _count_valid_steps(self, index, direction, fields): + if direction == UP: + update_index = self._up_index + elif direction == DOWN: + update_index = self._down_index + elif direction == LEFT: + update_index = self._left_index + elif direction == RIGHT: + update_index = self._right_index + else: + raise ValueError(f'invalid direction: {direction}') + + valid_steps = 0 + cur_x, cur_y = index + while True: + cur_x, cur_y = update_index(cur_x, cur_y) + if not self._is_valid_index((cur_x, cur_y), fields) \ + or fields[cur_x, cur_y] != 0: + break + else: + valid_steps += 1 + return valid_steps + + def _is_edge(self, index, fields, direction=None): + if direction is not None: + if direction == UP: + delta_x, delta_y = [self.delta_x[0]] + \ + self.delta_x[2:], [self.delta_y[0]]+self.delta_y[2:] + elif direction == DOWN: + delta_x, delta_y = self.delta_x[1:], self.delta_y[1:] + elif direction == RIGHT: + delta_x, delta_y = self.delta_x[0:1] + \ + self.delta_x[3:], self.delta_y[0:1] + self.delta_y[3:] + elif direction == LEFT: + delta_x, delta_y = self.delta_x[0:2] + \ + self.delta_x[4:], self.delta_y[0:2] + self.delta_y[4:] + else: + delta_x, delta_y = self.delta_x, self.delta_y + x, y = index + for dx, dy in zip(delta_x, delta_y): + if self._is_valid_index((x+dx, y+dy), fields) and fields[x+dx, y+dy] != 0: + return True + return False + + def _act(self, direction, fields, index): + x, y = index + if direction == UP: + continue_update_index = self._up_index + reverse_update_index = self._down_index + possible_turn = [LEFT, RIGHT] + possible_turn_update_index = [self._left_index, self._right_index] + reverse_direction = DOWN + elif direction == DOWN: + continue_update_index = self._down_index + reverse_update_index = self._up_index + possible_turn = [LEFT, RIGHT] + reverse_direction = UP + possible_turn_update_index = [self._left_index, self._right_index] + elif direction == LEFT: + continue_update_index = self._left_index + reverse_update_index = self._right_index + possible_turn = [UP, DOWN] + reverse_direction = RIGHT + possible_turn_update_index = [self._up_index, self._down_index] + elif direction == RIGHT: + continue_update_index = self._right_index + reverse_update_index = self._left_index + possible_turn = [UP, DOWN] + reverse_direction = LEFT + possible_turn_update_index = [self._up_index, self._down_index] + else: + raise ValueError(f'invalid direction: {direction}') + + def _turn(cur_direction): + edge_directions = [self._is_edge( + update_fn(x, y), fields, cur_direction) for update_fn in possible_turn_update_index] + valid_steps = [self._count_valid_steps( + (x, y), d, fields) for d in possible_turn] + steps0, steps1 = valid_steps + + if steps0 > 0 or steps1 > 0: + direction = np.max( + [Spiral.MyTuple(e, v, d) for e, v, d in zip(edge_directions, valid_steps, possible_turn)]).direction + return direction + else: + return BROKEN + + if not self._is_valid_index(continue_update_index(x, y), fields) \ + or fields[continue_update_index(x, y)] != 0: + """ + if not self._is_edge(continue_update_index(x, y), fields, direction): + print('turn') + else: + print('not turn') + """ + return _turn(direction) + else: + # print(f'not turn') + return direction + + def step(self, state, info): + x, y, direction = state[0] + fields = info['fields'] + + if np.sum(fields == 0) == 0: + return [FINISHED] + + if info['redecide_direction']: + all_directions = (UP, DOWN, LEFT, RIGHT) + all_update_fn = (self._up_index, self._down_index, + self._left_index, self._right_index) + edge_directions = [self._is_edge( + update_fn(x, y), fields, direction) for update_fn in all_update_fn] + valid_steps = [self._count_valid_steps( + (x, y), d, fields) for d in all_directions] + direction = np.max([Spiral.MyTuple(e, v, d) for e, v, d in zip( + edge_directions, valid_steps, all_directions)]).direction + return [self._act(direction, fields, (x, y))] diff --git a/Palette/algos/split_area.py b/Palette/algos/split_area.py new file mode 100644 index 0000000..f259346 --- /dev/null +++ b/Palette/algos/split_area.py @@ -0,0 +1,54 @@ +import numpy as np + + +def split_area( + fields: np.ndarray, +): + x_array = [0] + last_array = fields[0, :] + for i in range(1, fields.shape[0]): + cur_array = fields[i, :] + if (cur_array != last_array).any(): + x_array.append(i) + last_array = cur_array + x_array.append(fields.shape[0]) + + """ + y_array = [0] + last_array = fields[:, 0] + for i in range(1, fields.shape[1]): + cur_array = fields[:, i] + if (cur_array != last_array).any(): + y_array.append(i) + last_array = cur_array + y_array.append(fields.shape[1]) + """ + + splited_fields = np.zeros_like(fields) + cur_field_id = 1 + """ + for i in range(len(x_array)-1): + for j in range(len(y_array)-1): + if fields[x_array[i], y_array[j]] != -1: + cur_id = cur_field_id + cur_field_id += 1 + else: + cur_id = -1 + print('obstacle') + splited_fields[x_array[i]:x_array[i+1], + y_array[j]:y_array[j+1]] = cur_id + """ + for i in range(len(x_array)-1): + field_id_used = False + for x in range(x_array[i], x_array[i+1]): + for y in range(fields.shape[1]): + if fields[x, y] != -1: + splited_fields[x, y] = cur_field_id + field_id_used = True + else: + splited_fields[x, y] = -1 + if field_id_used: + cur_field_id += 1 + + assert np.sum(splited_fields == 0) == 0 + return splited_fields diff --git a/Palette/algos/utils.py b/Palette/algos/utils.py new file mode 100644 index 0000000..812cdd5 --- /dev/null +++ b/Palette/algos/utils.py @@ -0,0 +1,45 @@ +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN,\ + FILLING_UP, FILLING_DOWN, FILLING_LEFT, FILLING_RIGHT + + +class Node: + def __init__(self, x, y, father, energy: bool = False): + self.x, self.y = x, y + self._energy = energy + self.direction = None + self.father = father + if self.father is not None: + father_x, father_y = self.father.x, self.father.y + delta_x, delta_y = x-father_x, y-father_y + if delta_x == 0: + if delta_y == 1: + if not self.energy: + self.direction = DOWN + else: + self.direction = FILLING_DOWN + elif delta_y == -1: + if not self.energy: + self.direction = UP + else: + self.direction = FILLING_UP + elif delta_y == 0: + if delta_x == 1: + if not self.energy: + self.direction = RIGHT + else: + self.direction = FILLING_RIGHT + elif delta_x == -1: + if not self.energy: + self.direction = LEFT + else: + self.direction = FILLING_LEFT + + if self.direction is None: + raise ValueError(f'invalid delta: ({delta_x}, {delta_y})') + + def is_root(self) -> bool: + return self.father is None + + @property + def energy(self): + return self._energy diff --git a/Palette/algos/wildfire.py b/Palette/algos/wildfire.py new file mode 100644 index 0000000..be17717 --- /dev/null +++ b/Palette/algos/wildfire.py @@ -0,0 +1,64 @@ +import numpy as np +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos.utils import Node + + +class WildFire(object): + def __init__(self): + super().__init__() + self.actions_to_do = [] + self.delta_x = (0, 0, 1, -1, ) # 1, 1, -1, -1) + self.delta_y = (1, -1, 0, 0, ) # 1, -1, 1, -1) + + @ staticmethod + def _is_valid_index(index, an_array): + x, y = index + return 0 <= x < an_array.shape[0] and 0 <= y < an_array.shape[1] + + def empty(self): + return len(self.actions_to_do) == 0 + + def clear(self): + self.actions_to_do.clear() + + def step(self, state, info): + if not self.empty(): + # if there're actions to do + action = self.actions_to_do[0] + del self.actions_to_do[0] + return [action] + + # print(state) + x, y, _ = state + fields = info['fields'] + + if np.sum(fields == 0) == 0: + return [FINISHED] + + root_node = Node(x, y, None) + buffer = [root_node] + visited = np.zeros_like(fields) + visited[x, y] = True + while len(buffer) > 0: + cur_node = buffer[0] + del buffer[0] + for dx, dy in zip(self.delta_x, self.delta_y): + cur_x, cur_y = cur_node.x, cur_node.y + next_x, next_y = cur_x+dx, cur_y+dy + if self._is_valid_index((next_x, next_y), fields) and fields[next_x, next_y] >= 0 and not visited[next_x, next_y]: + visited[next_x, next_y] = True + next_node = Node(next_x, next_y, cur_node) + if fields[next_x, next_y] == 0: + # not visited + node = next_node + while node.direction is not None: + self.actions_to_do.append(node.direction) + node = node.father + self.actions_to_do.reverse() + action = self.actions_to_do[0] + del self.actions_to_do[0] + return [action] + else: + buffer.append(next_node) + + raise RuntimeError(f'WildFire runtime error!') diff --git a/Palette/constants.py b/Palette/constants.py new file mode 100644 index 0000000..936c418 --- /dev/null +++ b/Palette/constants.py @@ -0,0 +1,37 @@ +UP = 0 +DOWN = 1 +LEFT = 2 +RIGHT = 3 + +FILLING_UP = 4 +FILLING_DOWN = 5 +FILLING_LEFT = 6 +FILLING_RIGHT = 7 +FILLING = 8 +FILLING_BACK = 9 + +FINISHED = -1 +BROKEN = -2 + +COLORS = ( + (138, 43, 226), + (156, 102, 31), + (255, 64, 64), + (100, 0, 205), + (255, 97, 3), + (0, 255, 0), + (255, 20, 147), + (105, 105, 105), + (252, 230, 201), + (255, 215, 0), + (255, 106, 106), + (230, 230, 250), + (139, 137, 112), + (255, 160, 122), + (32, 178, 170), + (132, 112, 255), + (93, 71, 139), + (238, 64, 0), + (255, 192, 203), + (255, 100, 255), +) diff --git a/Palette/env/__init__.py b/Palette/env/__init__.py new file mode 100644 index 0000000..0253d8c --- /dev/null +++ b/Palette/env/__init__.py @@ -0,0 +1,6 @@ +from .sea_env import make, SeaEnv + +__all__ = [ + 'make', + 'SeaEnv' +] diff --git a/Palette/env/collide_types.py b/Palette/env/collide_types.py new file mode 100644 index 0000000..d203d3e --- /dev/null +++ b/Palette/env/collide_types.py @@ -0,0 +1,3 @@ +COLLISION_SHIP = 1 +COLLISION_OBSTACLE = 2 +COLLISION_DESTINATION = 3 \ No newline at end of file diff --git a/Palette/env/engine.py b/Palette/env/engine.py new file mode 100644 index 0000000..0bd9c8c --- /dev/null +++ b/Palette/env/engine.py @@ -0,0 +1,562 @@ +import pymunk +import pymunk.pygame_util +import pygame +import os +import numpy as np +import math + +from copy import deepcopy +from Palette.env.utils import process_angle, asincos +from typing import List, Tuple, Union +from pygame.color import THECOLORS +from pymunk.vec2d import Vec2d + +from Palette.env.collide_types import COLLISION_DESTINATION, COLLISION_OBSTACLE, COLLISION_SHIP +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN, \ + FILLING_LEFT, FILLING_RIGHT, FILLING_UP, FILLING_DOWN, FILLING, FILLING_BACK,\ + COLORS + + +class SeaEnvEngine(object): + def __init__( + self, + + window_width: int, + window_height: int, + + field_size: Tuple[int], + + ship_radius: int, + ship_velocity: int, + + sonar_spread_times: int, # the number of sonar detection points + + obstacles: dict, + + show_sensors: bool = False, + draw_screen: bool = True, + show_fields: bool = True, + + init_ships: bool = True, + start_position=None, # [start_x, start_y] + start_r=None, + + time_click: float = 1.0/10.0, + filling_time: int = 1, + + cover_ratio: float = 1.0, + + actual_background: bool = False, + actual_around_area: int = 2, + + *args, + **kwargs + ) -> None: + + super().__init__() + + self.max_angle, self.min_angle = None, None + + # basic params + self.crashed = False # whether or not crash into an obstacle or bound + self.finished = False + self.terminate = False + + self.show_sensors = show_sensors + self.draw_screen = draw_screen + self.show_fields = show_fields + self.num_steps = 0 + self.window_width = window_width + self.window_height = window_height + self.sonar_spread_times = sonar_spread_times + self.ship_radius = ship_radius + + self.time_click = time_click + self.filling_time = filling_time + self.cover_ratio = cover_ratio + self.actual_background = actual_background + self.actual_around_area = actual_around_area + + """ 1. init pygame """ + if self.draw_screen: + pygame.init() + self.screen = pygame.display.set_mode( + (window_width, window_height)) + self.clock = pygame.time.Clock() + self.screen.set_alpha(None) + pygame.display.set_caption('NODE Simulation') + self.draw_options = pymunk.pygame_util.DrawOptions(self.screen) + + if not actual_background: + self.ss_img = pygame.image.load( + os.path.join("pics", "sea.jpg")) + else: + self.ss_img = pygame.image.load( + os.path.join("pics", 'sea.png')) + self.ss_img = pygame.transform.scale( + self.ss_img, (self.window_width, self.window_height)) + + self.ship_img = pygame.image.load(os.path.join("pics", "ship.png")) + self.ship_img = pygame.transform.scale( + self.ship_img, (self.ship_radius * 4, self.ship_radius * 4)) + """ 2. init gymunk """ + # physics stuff + self.space = pymunk.Space() + self.space.gravity = pymunk.Vec2d(0., 0.) + # create bounds + # self.create_bounds(window_height, window_width) + # create ship + self.finished_idx = None + self.init_ships = init_ships + self.battery_capacity = None + self._ship_batteries = None + self._ship_charging = None + self._ship_charging_positions = None + self._ship_charging_time_left = None + + if self.init_ships: + self.create_ship(start_position=start_position, + start_r=start_r) + self.ship_velocity = ship_velocity + + assert self.window_width % field_size[0] == 0 and self.window_height % field_size[1] == 0 + self.field_size = field_size + self.fields = np.zeros( + shape=(self.window_width//field_size[0], self.window_height//field_size[1])) + + # create obstacles + self.obstacles = [] + self.obstacles_w_h = [] + for obstacle_key in obstacles.keys(): + x, y, width, height = obstacles[obstacle_key] + cur_obstacle = self.create_rectangle_obstacle(x, y, width, height) + self.obstacles.append(cur_obstacle) + self.obstacles_w_h.append((width, height)) + # label the position of obstacles as -1 + for i in range(x//self.field_size[0], (x+width)//self.field_size[0]): + for j in range(y//self.field_size[1], (y+height)//self.field_size[1]): + self.fields[i, self.fields.shape[1]-j-1] = -1 + + if self.actual_background: + for i in range( + max(0, x//self.field_size[0]-self.actual_around_area), + min((x+width)//self.field_size[0] + self.actual_around_area, + self.fields.shape[0])): + for j in range( + max(0, y//self.field_size[1]-self.actual_around_area), + min((y+height)//self.field_size[1]+self.actual_around_area, + self.fields.shape[1])): + if self.fields[i, self.fields.shape[1]-j-1] != -1: + self.fields[i, self.fields.shape[1]-j-1] = 1 + """ + self.obstacles_radius = [] + for _ in range(n_obstacles): + cur_radius = np.random.randint( + obstacle_radius_bound[0], obstacle_radius_bound[1]) + cur_init_x = np.random.randint(cur_radius, window_width-cur_radius) + cur_init_y = np.random.randint( + cur_radius, window_height-cur_radius) + cur_obstacle = self.create_obstacle( + init_x=cur_init_x, init_y=cur_init_y, obstacle_radius=cur_radius) + self.obstacles.append(cur_obstacle) + self.obstacles_radius.append(cur_radius) + """ + self.setup_collision_handler() + + @property + def splited_areas(self): + return deepcopy(self._splited_areas) + + @splited_areas.setter + def splited_areas(self, splited_areas): + self._splited_areas = deepcopy(splited_areas) + + @property + def ship_num(self): + return len(self.ship_body) + + def setup_collision_handler(self): + def post_solve_ship_obstacle(arbiter, space, data): + self.crashed = True + + self.space.add_collision_handler( + COLLISION_SHIP, COLLISION_OBSTACLE).post_solve = post_solve_ship_obstacle + + def create_bounds(self, window_height: float, window_width: float): + static = [ + pymunk.Segment( + self.space.static_body, + (0, self.window_height-1), (0, self.window_height-window_height), 1), + pymunk.Segment( + self.space.static_body, + (1, self.window_height-window_height), (window_width, self.window_height-window_height), 1), + pymunk.Segment( + self.space.static_body, + (window_width-1, self.window_height-window_height), (window_width-1, self.window_height-1), 1), + pymunk.Segment( + self.space.static_body, + (1, self.window_height-1), (window_width, self.window_height-1), 1) + ] + for s in static: + s.friction = 1. + s.group = 1 + # bound is the same as obstacle for collision + s.collision_type = COLLISION_OBSTACLE + s.color = THECOLORS['blue'] + self.space.add(*static) + + def create_ship(self, start_position, start_r: float, battery_capacity: int): + self.ship_body, self.ship_shape = [], [] + for init_xy, init_r in zip(start_position, start_r): + init_x, init_y = init_xy + inertia = pymunk.moment_for_circle( + mass=1, inner_radius=0, outer_radius=self.ship_radius, offset=(0, 0)) + self.ship_body.append(pymunk.Body(1, inertia)) + self.ship_body[-1].position = init_x, self.window_height - init_y + self.ship_shape.append(pymunk.Circle( + body=self.ship_body[-1], radius=self.ship_radius, offset=(0, 0))) + self.ship_shape[-1].color = THECOLORS["green"] + self.ship_shape[-1].elasticity = 1.0 + self.ship_body[-1].angle = init_r + self.ship_shape[-1].collision_type = COLLISION_SHIP + self.space.add(self.ship_body[-1], self.ship_shape[-1]) + self.init_ships = True + self.finished_idx = [False for _ in range(self.ship_num)] + self.battery_capacity = battery_capacity + self._ship_batteries = [ + self.battery_capacity for _ in range(len(start_position))] + self._ship_charging = [False for _ in range(self.ship_num)] + self._ship_charging_positions = [[] for _ in range(self.ship_num)] + self._ship_charging_time_left = [0 for _ in range(self.ship_num)] + + def create_rectangle_obstacle(self, x, y, width, height): + # points = [(0, 0), (0, height), (width, height), (width, 0)] + points = [(-width/2, -height/2), (-width/2, height/2), + (width/2, height/2), (width/2, -height/2)] + # points = [(-width, -height), (-width, 0), (0, 0), (0, -height)] + mass = 9999999999 + moment = pymunk.moment_for_poly(mass, points, (0, 0)) + obstacle_body = pymunk.Body(mass=mass, moment=moment) + obstacle_body.position = (x+width/2, self.window_height-y-height/2) + obstacle_shape = pymunk.Poly(obstacle_body, points) + obstacle_shape.color = THECOLORS["blue"] + obstacle_shape.collision_type = COLLISION_OBSTACLE + self.space.add(obstacle_body, obstacle_shape) + return obstacle_body + + def _filling_get_last_position(self, ship_id: int): + if self._ship_charging[ship_id]: + last_pos = self._ship_charging_positions[ship_id][-1] + else: + last_pos = self.ship_body[ship_id].position + return last_pos + + def get_battery(self, ship_id: int): + return self._ship_batteries[ship_id] + + def frame_step(self, actions: List[int]): + """frame update for one step + + Args: + action (int): the next direction of the ship + 0: up + 1: down + 2: left + 3: right + """ + assert self.init_ships, 'Have not initialized ships!' + + if self.terminate: + raise AssertionError("Cannot continue running after crash!") + + for ship_id, action in enumerate(actions): + if action == UP: # up + self._ship_charging[ship_id] = False + self._ship_charging_time_left[ship_id] = 0 + self.ship_body[ship_id].angle = 1.5*np.pi + elif action == DOWN: # down + self._ship_charging[ship_id] = False + self._ship_charging_time_left[ship_id] = 0 + self.ship_body[ship_id].angle = 0.5*np.pi + elif action == LEFT: # left + self._ship_charging[ship_id] = False + self._ship_charging_time_left[ship_id] = 0 + self.ship_body[ship_id].angle = np.pi + elif action == RIGHT: # right + self._ship_charging[ship_id] = False + self._ship_charging_time_left[ship_id] = 0 + self.ship_body[ship_id].angle = 0.0 + + elif action == FINISHED: + self._ship_charging[ship_id] = False + self._ship_charging_time_left[ship_id] = 0 + driving_direction = Vec2d(1, 0).rotated( + self.ship_body[ship_id].angle) + self.ship_body[ship_id].velocity = 0.0 * driving_direction + if not self.finished_idx[ship_id]: + self.space.remove(self.ship_body[ship_id]) + self.finished_idx[ship_id] = True + + elif action == FILLING_UP: + last_pos = self._filling_get_last_position(ship_id) + self._ship_charging_positions[ship_id].append( + (last_pos[0], last_pos[1]-self.ship_velocity*self.time_click)) + self._ship_charging[ship_id] = True + self._ship_charging_time_left[ship_id] = self.filling_time + recent_x, recent_y = self._ship_charging_positions[ship_id][-1] + rx, ry = int( + recent_x//self.field_size[0]), int(recent_y//self.field_size[1]) + if self.fields[rx, ry] >= 0: + self.fields[rx, ry] += 1 + elif action == FILLING_DOWN: + last_pos = self._filling_get_last_position(ship_id) + self._ship_charging_positions[ship_id].append( + (last_pos[0], last_pos[1]+self.ship_velocity*self.time_click)) + self._ship_charging[ship_id] = True + self._ship_charging_time_left[ship_id] = self.filling_time + recent_x, recent_y = self._ship_charging_positions[ship_id][-1] + rx, ry = int( + recent_x//self.field_size[0]), int(recent_y//self.field_size[1]) + if self.fields[rx, ry] >= 0: + self.fields[rx, ry] += 1 + elif action == FILLING_LEFT: + last_pos = self._filling_get_last_position(ship_id) + self._ship_charging_positions[ship_id].append( + (last_pos[0]-self.ship_velocity*self.time_click, last_pos[1])) + self._ship_charging[ship_id] = True + self._ship_charging_time_left[ship_id] = self.filling_time + recent_x, recent_y = self._ship_charging_positions[ship_id][-1] + rx, ry = int( + recent_x//self.field_size[0]), int(recent_y//self.field_size[1]) + if self.fields[rx, ry] >= 0: + self.fields[rx, ry] += 1 + elif action == FILLING_RIGHT: + last_pos = self._filling_get_last_position(ship_id) + self._ship_charging_positions[ship_id].append( + (last_pos[0]+self.ship_velocity*self.time_click, last_pos[1])) + self._ship_charging[ship_id] = True + self._ship_charging_time_left[ship_id] = self.filling_time + recent_x, recent_y = self._ship_charging_positions[ship_id][-1] + rx, ry = int( + recent_x//self.field_size[0]), int(recent_y//self.field_size[1]) + if self.fields[rx, ry] >= 0: + self.fields[rx, ry] += 1 + elif action == FILLING_BACK: + assert self._ship_charging[ship_id] == True + assert self._ship_charging_time_left[ship_id] == 0 + self._ship_charging_positions[ship_id].pop() + if len(self._ship_charging_positions[ship_id]): + recent_x, recent_y = self._ship_charging_positions[ship_id][-1] + rx, ry = int( + recent_x//self.field_size[0]), int(recent_y//self.field_size[1]) + if self.fields[rx, ry] >= 0: + self.fields[rx, ry] += 1 + elif action == FILLING: + assert self._ship_charging[ship_id] == True + self._ship_charging_time_left[ship_id] -= 1 + if self._ship_charging_time_left[ship_id] == 0: + self._ship_batteries[ship_id] = self.battery_capacity + + else: + raise ValueError(f"invalid action: {action}") + + self.ship_body[ship_id].angle = process_angle( + self.ship_body[ship_id].angle) + # calculate velocity with direction + driving_direction = Vec2d(1, 0).rotated( + self.ship_body[ship_id].angle) + + if not self._ship_charging[ship_id]: + self.ship_body[ship_id].velocity = self.ship_velocity * \ + driving_direction + else: + self.ship_body[ship_id].velocity = 0.0 * driving_direction + + # Update the screen and stuff. + if self.draw_screen: + # self.screen.fill(THECOLORS["black"]) + self.screen.blit(self.ss_img, (0, 0)) + # draw(self.screen, self.space) + + if self.show_fields: + for i in range(self.fields.shape[0]): + for j in range(self.fields.shape[1]): + if self.fields[i, j] > 0: + pos_x, pos_y = self.field_size[0] * i,\ + self.field_size[1] * j + # the size of your rect + s = pygame.Surface(self.field_size) + # alpha level + s.set_alpha(int(96 * self.fields[i, j])) + # this fills the entire surface + color = COLORS[int(self._splited_areas[i, j] - 1)] + s.fill(color) + # (0,0) are the top-left coordinates + self.screen.blit(s, (pos_x, pos_y)) + elif self.fields[i, j] == -1: + pos_x, pos_y = self.field_size[0] * i,\ + self.field_size[1] * j + # the size of your rect + s = pygame.Surface(self.field_size) + # alpha level + # this fills the entire surface + if self.actual_background: + color = (0, 0, 0) + s.set_alpha(100) + else: + color = (0, 0, 255) + s.set_alpha(255) + + s.fill(color) + # (0,0) are the top-left coordinates + self.screen.blit(s, (pos_x, pos_y)) + for i in range(self.ship_num): + self.screen.blit( + self.ship_img, (self.ship_body[i].position[0]-2.0*self.ship_radius, self.ship_body[i].position[1]-2.0*self.ship_radius)) + # draw charging trajectory + for ship_id in range(self.ship_num): + if self._ship_charging[ship_id]: + for cx, cy in self._ship_charging_positions[ship_id]: + pygame.draw.circle( + self.screen, (255, 255, 255), (cx, cy), 2) + + pygame.display.flip() + self.clock.tick() + self.space.step(self.time_click) + + for i in range(self.ship_num): + if not self.finished_idx[i]: + if actions[i] != FILLING: + self._ship_batteries[i] -= 1 + if self._ship_batteries[i] < 0: + self.crashed = True + + if self.crashed: + print('crashed!!!') + self.terminate = True + self.num_steps += 1 + + return self.current() + + def current(self): + state = None + for ship_id in range(len(self.ship_body)): + x, y = self.ship_body[ship_id].position + if self.finished_idx[ship_id]: + direction = UP + elif self.ship_body[ship_id].angle == 1.5 * np.pi: + direction = UP + elif self.ship_body[ship_id].angle == 0.0: + direction = RIGHT + elif self.ship_body[ship_id].angle == 0.5 * np.pi: + direction = DOWN + elif self.ship_body[ship_id].angle == np.pi: + direction = LEFT + else: + raise ValueError( + f'invalid angle: {self.ship_body[ship_id].angle}') + cur_state = np.asarray([ + int(x//self.field_size[0]), + int(y//self.field_size[1]), + direction])[None] + if state is None: + state = cur_state + else: + state = np.concatenate([state, cur_state], axis=0) + if not self.crashed and not self.finished_idx[ship_id] and not self._ship_charging[ship_id]: + cx, cy = int(x//self.field_size[0]), int(y//self.field_size[1]) + if self.fields[cx, cy] >= 0 and 0 <= x < self.window_width \ + and 0 <= y < self.window_height: + self.fields[cx, cy] += 1 + else: + print(f'crash') + self.crashed = True + if np.sum(self.fields > 0) / np.sum(self.fields >= 0) >= self.cover_ratio: + self.finished = True + print('finished!!!') + self.terminate = True + info = {'fields': self.fields, 'finished': self.finished} + return state, self.terminate, info + + def get_rotated_point(self, x_1, y_1, x_2, y_2, radians): + # Rotate x_2, y_2 around x_1, y_1 by angle. + x_change = (x_2 - x_1) * math.cos(radians) + \ + (y_2 - y_1) * math.sin(radians) + y_change = (y_1 - y_2) * math.cos(radians) - \ + (x_1 - x_2) * math.sin(radians) + new_x = x_change + x_1 + # new_y = self.window_height - (y_change + y_1) + new_y = y_change + y_1 + return int(new_x), int(new_y) + + def get_sonar_readings(self, x, y, angle): + """ + Instead of using a grid of boolean(ish) sensors, sonar readings + simply return N "distance" readings, one for each sonar + we're simulating. The distance is a count of the first non-zero + reading starting at the object. For instance, if the fifth sensor + in a sonar "arm" is non-zero, then that arm returns a distance of 5. + """ + # Make our arms. + arm_left = self.make_sonar_arm(x, y) + # arm_middle = arm_left + # arm_right = arm_left + + # Rotate them and get readings. + readings = [] + + for shift_angle in np.arange(-0.75, 0.75, 0.05): + readings.append(self.get_arm_distance( + arm_left, x, y, angle, shift_angle)) + # print(len(readings)) + + # readings.append(self.get_arm_distance(arm_left, x, y, angle, 0.75)) + # readings.append(self.get_arm_distance(arm_middle, x, y, angle, 0)) + # readings.append(self.get_arm_distance(arm_right, x, y, angle, -0.75)) + + if self.draw_screen and self.show_sensors: + pygame.display.update() + + return readings + + def make_sonar_arm(self, x, y): + spread = 10 # Default spread. + distance = 10 # Gap before first sensor. + arm_points = [] + # Make an arm. We build it flat because we'll rotate it about the + # center later. + for i in range(1, self.sonar_spread_times): + arm_points.append((distance + x + (spread * i), y)) + + return arm_points + + def get_arm_distance(self, arm, x, y, angle, offset) -> int: + # Used to count the distance. + i = 0 + + # Look at each point and see if we've hit something. + for point in arm: + i += 1 + + # Move the point to the right spot. + rotated_p = self.get_rotated_point( + x, y, point[0], point[1], angle + offset + ) + + # Check if we've hit something. Return the current i (distance) + # if we did. + if rotated_p[0] <= 0 or rotated_p[1] <= 0 \ + or rotated_p[0] >= self.window_width or rotated_p[1] >= self.window_height: + return i # Sensor is off the screen. + else: + for obstacle, obstacle_w_h in zip(self.obstacles, self.obstacles_w_h): + obstacle_position = obstacle.position + if 0 <= rotated_p[0] - obstacle_position[0] <= obstacle_w_h[0] \ + and 0 <= rotated_p[1] - obstacle_position[1] <= obstacle_w_h[1]: + return i + if self.draw_screen and self.show_sensors: + pygame.draw.circle( + self.screen, (255, 255, 255), (rotated_p), 2) + + # Return the distance for the arm. + return i diff --git a/Palette/env/sea_env.py b/Palette/env/sea_env.py new file mode 100644 index 0000000..6dfbd9e --- /dev/null +++ b/Palette/env/sea_env.py @@ -0,0 +1,164 @@ +import os +import yaml +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy +from easydict import EasyDict +from typing import List + +from Palette.algos.multi_split_area import multi_split_area +from Palette.env.engine import SeaEnvEngine +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN + + +class SeaEnv(object): + def __init__( + self, + cfg: EasyDict, + draw: bool, + n_ships: int, + battery_capacity: int, + split_axis: str = 'x', + filling_time: int = 1, + cover_ratio: float = 1.0 + ) -> None: + super().__init__() + self.draw = draw + self.cfg = cfg + self.split_axis = split_axis + if "max_step" in self.cfg: + self.max_step = self.cfg["max_step"] + else: + self.max_step = None + self.cur_step = 0 + self.init_ships = False + self.n_ships = n_ships + self.battery_capacity = battery_capacity + self.filling_time = filling_time + self.cover_ratio = cover_ratio + + @property + def ship_num(self): + return self.n_ships + + @property + def fields(self): + return deepcopy(self.sea_env_engine.fields) + + @property + def splited_areas(self): + return self.sea_env_engine.splited_areas + + def step(self, action: List[int]): + cur_frame, done, info = self.sea_env_engine.frame_step(action) + self.cur_step += 1 + if self.max_step is not None and self.cur_step >= self.max_step: + done = True + return cur_frame, done, info + + def reset(self): + done = True + self.cur_step = 0 + while done: + self.sea_env_engine = SeaEnvEngine( + draw_screen=self.draw, + init_ships=self.init_ships, + filling_time=self.filling_time, + cover_ratio=self.cover_ratio, + **self.cfg + ) + self.sea_env_engine.splited_areas, start_points = multi_split_area( + fields=self.sea_env_engine.fields, + n_areas=self.n_ships, + axis=self.split_axis, + field_size=self.cfg.field_size[0], + ) + self.sea_env_engine.create_ship( + start_position=start_points, + start_r=[1.0*np.pi for _ in range(self.n_ships)], + battery_capacity=self.battery_capacity + ) + state, done, info = self.sea_env_engine.current() + return state, info + + def seed(self, seed: int = None) -> List[int]: + np.random.seed(seed) + return [seed] + + def render(self, mode='human'): + pass + + def close(self): + pass + + def create_ships(self, start_position, start_r): + self.sea_env_engine.create_ship( + start_position=start_position, start_r=start_r) + + def get_battery(self, ship_id: int): + return self.sea_env_engine.get_battery(ship_id) + + +def make( + env_config: str, + render: bool, + split_axis: str, + n_ships: int, + battery_capacity: int = np.inf, + filling_time: int = 1, + cover_ratio: float = 1.0 +): + """ + if not render: + os.environ["SDL_VIDEODRIVER"] = "dummy" + else: + os.environ["DISPLAY"] = os.popen( + 'printenv grep DISPLAY').read().strip() + """ + + with open(os.path.join('Palette', 'maps', f'{env_config}.yaml'), 'r') as f: + config = yaml.safe_load(f) + config = EasyDict(config) + env_config = config.env + env = SeaEnv(cfg=env_config, draw=render, + split_axis=split_axis, n_ships=n_ships, + battery_capacity=battery_capacity, filling_time=filling_time, + cover_ratio=cover_ratio) + return env + + +if __name__ == '__main__': + def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--axis', type=str, + default='x', help="The axis for splitting areas.") + parser.add_argument('--render', action='store_true', + help='render or not') + return parser.parse_args() + + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, split_axis=args.axis) + + import random + steps = 0 + acts = [] + acts.extend([[UP, DOWN] for _ in range(49)]) + acts.extend([[RIGHT, LEFT] for _ in range(5)]) + acts.extend([[LEFT, RIGHT] for _ in range(5)]) + acts.extend([[DOWN, UP] for _ in range(20)]) + acts.extend([[RIGHT, LEFT] for _ in range(200)]) + while True: + s, t, _ = sea_env.step(acts[steps]) + for _ in range(99999999999): + pass + steps += 1 + print(s) + + if t: + break + # sea_env.reset() + print(steps) diff --git a/Palette/env/utils.py b/Palette/env/utils.py new file mode 100644 index 0000000..7bb4276 --- /dev/null +++ b/Palette/env/utils.py @@ -0,0 +1,29 @@ +import numpy as np +import random + +from math import acos, asin + + +def process_angle(_angle): + ret = _angle + if ret < 0: + ret += (abs(ret) // (2*np.pi) + 1) * 2*np.pi + if ret >= 2 * np.pi: + ret -= (abs(ret) // (2*np.pi)) * 2*np.pi + assert 0 <= ret < 2 * np.pi, f'invalid angle: {ret}' + assert abs(np.sin(ret) - np.sin(_angle) + ) < 1e-5 and (np.cos(ret) - np.cos(_angle)) < 1e-5 + return ret + + +def asincos(sin_value, cos_value): + if sin_value >= 0: + # 第一象限和第二象限 + return acos(cos_value) + else: + if cos_value >= 0: + # 第四象限 + return asin(sin_value) + else: + # 第三象限 + return 2*np.pi - acos(cos_value) diff --git a/Palette/examples/charge_multi_split_wildfire_boustrophedon.py b/Palette/examples/charge_multi_split_wildfire_boustrophedon.py new file mode 100644 index 0000000..7bd3be2 --- /dev/null +++ b/Palette/examples/charge_multi_split_wildfire_boustrophedon.py @@ -0,0 +1,90 @@ +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import MultiGreedy, MultiWildFire, MultiCharge, multi_split_area + + +def calculate_repeat_steps(fields): + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def generate_agent_fields(fields, ship_num, splited_areas): + agents_fields = np.stack([deepcopy(fields) + for _ in range(ship_num)], axis=0) + for ship_id in range(1, ship_num+1): + cur_fields = agents_fields[ship_id-1] + # print((splited_areas != ship_id) * (splited_areas != -1)) + cur_fields[(splited_areas != ship_id) * (splited_areas != -1)] += 1 + return agents_fields + + +def main(env: SeaEnv, agent: MultiGreedy): + state, info = env.reset() + redecide_direction = [True for _ in range(env.ship_num)] + steps = 0 + while True: + steps += 1 + info['redecide_direction'] = redecide_direction + agents_fields = generate_agent_fields( + info['fields'], env.ship_num, env.splited_areas) + agent_info = deepcopy(info) + agent_info['fields'] = agents_fields + agent_info['batteries'] = [env.get_battery( + ship_id) for ship_id in range(env.ship_num)] + acts, redecide_direction = agent.step(state, agent_info) + + assert (acts != BROKEN).all(), f'Invalid: ships output BROKEN step.' + state, terminate, info = env.step(acts) + + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'Used steps: {steps}\nrepeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--axis', type=str, default='x', + help='The axis for splitting areas') + parser.add_argument('-n', '--n-ships', type=int, + default=3, help='The number of ships.') + parser.add_argument('-b', '--battery-capacity', type=int, default=np.inf, + help='The num of steps a full battery can support.') + parser.add_argument('-f', '--filling-time', type=int, default=1, + help='The num of time steps needed for charging.') + parser.add_argument('--render', action='store_true', + help='render or not') + parser.add_argument('--cover-ratio', type=float, default=1.0) + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, + split_axis=args.axis, + n_ships=args.n_ships, + filling_time=args.filling_time, + battery_capacity=args.battery_capacity, + cover_ratio=args.cover_ratio) + wildfires = MultiWildFire(n_ships=sea_env.ship_num) + charges = MultiCharge(n_ships=args.n_ships, filling_time=args.filling_time) + algo = MultiGreedy( + ship_num=sea_env.ship_num, wildfires=wildfires, charges=charges) + main(sea_env, algo) + + for _ in range(int(1e8)): + continue diff --git a/Palette/examples/charge_multi_split_wildfire_greedy.py b/Palette/examples/charge_multi_split_wildfire_greedy.py new file mode 100644 index 0000000..6eb0f2c --- /dev/null +++ b/Palette/examples/charge_multi_split_wildfire_greedy.py @@ -0,0 +1,91 @@ +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import MultiGreedy, MultiWildFire, MultiCharge, multi_split_area + + +def calculate_repeat_steps(fields): + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def generate_agent_fields(fields, ship_num, splited_areas): + agents_fields = np.stack([deepcopy(fields) + for _ in range(ship_num)], axis=0) + for ship_id in range(1, ship_num+1): + cur_fields = agents_fields[ship_id-1] + # print((splited_areas != ship_id) * (splited_areas != -1)) + cur_fields[(splited_areas != ship_id) * (splited_areas != -1)] += 1 + return agents_fields + + +def main(env: SeaEnv, agent: MultiGreedy): + state, info = env.reset() + redecide_direction = [True for _ in range(env.ship_num)] + steps = 0 + while True: + steps += 1 + info['redecide_direction'] = redecide_direction + # print(np.sum(info['fields'] == 0)) + agents_fields = generate_agent_fields( + info['fields'], env.ship_num, env.splited_areas) + agent_info = deepcopy(info) + agent_info['fields'] = agents_fields + agent_info['batteries'] = [env.get_battery( + ship_id) for ship_id in range(env.ship_num)] + acts, redecide_direction = agent.step(state, agent_info) + + assert (acts != BROKEN).all(), f'Invalid: ships output BROKEN step.' + state, terminate, info = env.step(acts) + + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'Used steps: {steps}\nrepeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--axis', type=str, default='x', + help='The axis for splitting areas') + parser.add_argument('-n', '--n-ships', type=int, + default=3, help='The number of ships.') + parser.add_argument('-b', '--battery-capacity', type=int, default=np.inf, + help='The num of steps a full battery can support.') + parser.add_argument('-f', '--filling-time', type=int, default=1, + help='The num of time steps needed for charging.') + parser.add_argument('--render', action='store_true', + help='render or not') + parser.add_argument('--cover-ratio', type=float, default=1.0) + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, + split_axis=args.axis, + n_ships=args.n_ships, + filling_time=args.filling_time, + battery_capacity=args.battery_capacity, + cover_ratio=args.cover_ratio) + wildfires = MultiWildFire(n_ships=sea_env.ship_num) + charges = MultiCharge(n_ships=args.n_ships, filling_time=args.filling_time) + algo = MultiGreedy( + ship_num=sea_env.ship_num, wildfires=wildfires, charges=charges) + main(sea_env, algo) + + for _ in range(int(1e8)): + continue diff --git a/Palette/examples/charge_multi_split_wildfire_spiral.py b/Palette/examples/charge_multi_split_wildfire_spiral.py new file mode 100644 index 0000000..c02f53b --- /dev/null +++ b/Palette/examples/charge_multi_split_wildfire_spiral.py @@ -0,0 +1,90 @@ +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import MultiSpiral, MultiWildFire, MultiCharge, multi_split_area + + +def calculate_repeat_steps(fields): + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def generate_agent_fields(fields, ship_num, splited_areas): + agents_fields = np.stack([deepcopy(fields) + for _ in range(ship_num)], axis=0) + for ship_id in range(1, ship_num+1): + cur_fields = agents_fields[ship_id-1] + # print((splited_areas != ship_id) * (splited_areas != -1)) + cur_fields[(splited_areas != ship_id) * (splited_areas != -1)] += 1 + return agents_fields + + +def main(env: SeaEnv, agent: MultiSpiral): + state, info = env.reset() + redecide_direction = [True for _ in range(env.ship_num)] + steps = 0 + while True: + steps += 1 + info['redecide_direction'] = redecide_direction + agents_fields = generate_agent_fields( + info['fields'], env.ship_num, env.splited_areas) + agent_info = deepcopy(info) + agent_info['fields'] = agents_fields + agent_info['batteries'] = [env.get_battery( + ship_id) for ship_id in range(env.ship_num)] + acts, redecide_direction = agent.step(state, agent_info) + + assert (acts != BROKEN).all(), f'Invalid: ships output BROKEN step.' + state, terminate, info = env.step(acts) + + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'Used steps: {steps}\nrepeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--axis', type=str, default='x', + help='The axis for splitting areas') + parser.add_argument('-n', '--n-ships', type=int, + default=3, help='The number of ships.') + parser.add_argument('-b', '--battery-capacity', type=int, default=np.inf, + help='The num of steps a full battery can support.') + parser.add_argument('-f', '--filling-time', type=int, default=1, + help='The num of time steps needed for charging.') + parser.add_argument('--render', action='store_true', + help='render or not') + parser.add_argument('--cover-ratio', type=float, default=1.0) + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, + split_axis=args.axis, + n_ships=args.n_ships, + filling_time=args.filling_time, + battery_capacity=args.battery_capacity, + cover_ratio=args.cover_ratio) + wildfires = MultiWildFire(n_ships=sea_env.ship_num) + charges = MultiCharge(n_ships=args.n_ships, filling_time=args.filling_time) + algo = MultiSpiral( + ship_num=sea_env.ship_num, wildfires=wildfires, charges=charges) + main(sea_env, algo) + + for _ in range(int(1e8)): + continue diff --git a/Palette/examples/multi_pure_boustrophedon.py b/Palette/examples/multi_pure_boustrophedon.py new file mode 100644 index 0000000..348d3cc --- /dev/null +++ b/Palette/examples/multi_pure_boustrophedon.py @@ -0,0 +1,45 @@ +import numpy as np + +from argparse import ArgumentParser + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import MultiGreedy + + +def calculate_repeat_steps(fields): + assert np.sum(fields == 0) == 0, f'Not finish!' + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def main(env: SeaEnv, agent: MultiGreedy): + state, info = env.reset() + info['redecide_direction'] = [False for _ in range(env.ship_num())] + while True: + act = agent.step(state, info) + print(f'act: {act}') + if act == FINISHED: + print(f'Congrates! Covered all the places!') + break + elif act == BROKEN: + print('Broken!') + state, terminate, info = env.step(act) + info['redecide_direction'] = [False for _ in range(env.ship_num())] + if terminate: + print('Terminate!') + break + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, default='multi_map0', + help="The name of config file, e.g., multi_map0") + parser.add_argument('--render', action='store_true', + help='render or not') + return parser.parse_args() + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, render=args.render) + algo = MultiGreedy(ship_num=sea_env.ship_num()) + main(sea_env, algo) diff --git a/Palette/examples/multi_split_wildfire_boustrophedon.py b/Palette/examples/multi_split_wildfire_boustrophedon.py new file mode 100644 index 0000000..c9a30ad --- /dev/null +++ b/Palette/examples/multi_split_wildfire_boustrophedon.py @@ -0,0 +1,75 @@ +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import MultiGreedy, MultiWildFire, multi_split_area + + +def calculate_repeat_steps(fields): + assert np.sum(fields == 0) == 0, f'Not finish!' + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def generate_agent_fields(fields, ship_num, splited_areas): + agents_fields = np.stack([deepcopy(fields) + for _ in range(ship_num)], axis=0) + for ship_id in range(1, ship_num+1): + cur_fields = agents_fields[ship_id-1] + # print((splited_areas != ship_id) * (splited_areas != -1)) + cur_fields[(splited_areas != ship_id) * (splited_areas != -1)] += 1 + return agents_fields + + +def main(env: SeaEnv, agent: MultiGreedy): + state, info = env.reset() + redecide_direction = [True for _ in range(env.ship_num)] + steps = 0 + while True: + steps += 1 + info['redecide_direction'] = redecide_direction + agents_fields = generate_agent_fields( + info['fields'], env.ship_num, env.splited_areas) + agent_info = deepcopy(info) + agent_info['fields'] = agents_fields + acts, redecide_direction = agent.step(state, agent_info) + + assert (acts != BROKEN).all(), f'Invalid: ships output BROKEN step.' + state, terminate, info = env.step(acts) + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'Used steps: {steps}\nrepeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--axis', type=str, default='x', + help='The axis for splitting areas') + parser.add_argument('-n', '--n-ships', type=int, + default=3, help='The number of ships.') + parser.add_argument('--render', action='store_true', + help='render or not') + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, + split_axis=args.axis, + n_ships=args.n_ships) + wildfires = MultiWildFire(n_ships=sea_env.ship_num) + algo = MultiGreedy(ship_num=sea_env.ship_num, wildfires=wildfires) + main(sea_env, algo) diff --git a/Palette/examples/pure_boustrophedon.py b/Palette/examples/pure_boustrophedon.py new file mode 100644 index 0000000..d06857b --- /dev/null +++ b/Palette/examples/pure_boustrophedon.py @@ -0,0 +1,37 @@ +from argparse import ArgumentParser + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import Boustrophedon + + +def main(env: SeaEnv, agent: Boustrophedon): + state, info = env.reset() + while True: + act = agent.step(state, info) + if act == FINISHED: + print(f'Congrates! Covered all the places!') + break + elif act == BROKEN: + print(f'Broken!') + break + state, terminate, info = env.step(act) + if terminate: + print(f'Terminate!') + break + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('--render', action='store_true', + help='render or not') + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, render=args.render) + algo = Boustrophedon() + main(sea_env, algo) diff --git a/Palette/examples/split.py b/Palette/examples/split.py new file mode 100644 index 0000000..c43268e --- /dev/null +++ b/Palette/examples/split.py @@ -0,0 +1,89 @@ +import numpy as np + +from argparse import ArgumentParser +from copy import deepcopy + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import split_area, Boustrophedon, Spiral, WildFire +from typing import Union + + +def calculate_repeat_steps(fields): + assert np.sum(fields == 0) == 0, f'Not finish!' + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def main(env: SeaEnv, agent: Union[Boustrophedon, Spiral], wildfire: WildFire): + state, info = env.reset() + fields_to_split = deepcopy(info['fields']) + fields_to_split[fields_to_split > 0] = 0 + splited_area = split_area(fields_to_split) + n_areas = int(np.max(splited_area)) + + for area_id in range(1, n_areas+1): + redecide_direction = True + cur_area_terminate = False + while True: + # for _ in range(9999999): + # continue + info['redecide_direction'] = redecide_direction + modified_fields = deepcopy(info['fields']) + modified_fields += ((splited_area != area_id) + * (splited_area != -1)) + info_for_agent = deepcopy(info) + info_for_agent['fields'] = modified_fields + act = agent.step(state, info_for_agent) + redecide_direction = False + if act == FINISHED: + cur_area_terminate = True + break + elif act == BROKEN: + print(f'Broken!') + while True: + act = wildfire.step(state, info_for_agent) + state, cur_area_terminate, info = env.step(act) + if cur_area_terminate: + break + if wildfire.empty(): + redecide_direction = True + break + else: + state, terminate, info = env.step(act) + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'repeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + if cur_area_terminate: + break + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('-a', '--algo', type=str, choices=[ + 'b', 's'], default='b', help='The algorithm is Boustrophedon or Spiral.') + parser.add_argument('--render', action='store_true', + help='render or not') + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, render=args.render) + if args.algo == 'b': + algo = Boustrophedon() + elif args.algo == 's': + algo = Spiral() + else: + raise ValueError(f'invalid args.algo: {args.algo}') + wildfire = WildFire() + main(sea_env, algo, wildfire) diff --git a/Palette/examples/wildfire_boustrophedon.py b/Palette/examples/wildfire_boustrophedon.py new file mode 100644 index 0000000..a9bbf56 --- /dev/null +++ b/Palette/examples/wildfire_boustrophedon.py @@ -0,0 +1,77 @@ +import numpy as np + +from argparse import ArgumentParser + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import Boustrophedon, WildFire + + +def calculate_repeat_steps(fields): + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def main(env: SeaEnv, agent: Boustrophedon, wildfire: WildFire): + state, info = env.reset() + redecide_direction = True + all_steps = 0 + while True: + all_steps += 1 + state = state[0] + info['redecide_direction'] = redecide_direction + act = agent.step(state, info) + redecide_direction = False + if act[0] == FINISHED: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + print(f'{repeat_steps} steps repeated!') + return + elif act[0] == BROKEN: + print(f'Broken!') + while True: + all_steps += 1 + act = wildfire.step(state, info) + state, terminate, info = env.step(act) + if terminate: + return + if wildfire.empty(): + redecide_direction = True + break + all_steps -= 1 + else: + state, terminate, info = env.step(act) + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + print(f'steps: {all_steps}') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'repeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('--render', action='store_true', + help='render or not') + parser.add_argument('--cover-ratio', type=float, default=1.0) + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, n_ships=1, split_axis='x', + cover_ratio=args.cover_ratio) + algo = Boustrophedon() + wildfire = WildFire() + main(sea_env, algo, wildfire) + + for _ in range(int(1e8)): + continue \ No newline at end of file diff --git a/Palette/examples/wildfire_spiral.py b/Palette/examples/wildfire_spiral.py new file mode 100644 index 0000000..9cb40db --- /dev/null +++ b/Palette/examples/wildfire_spiral.py @@ -0,0 +1,77 @@ +import numpy as np + +from argparse import ArgumentParser + +from Palette.env import make, SeaEnv +from Palette.constants import UP, DOWN, LEFT, RIGHT, FINISHED, BROKEN +from Palette.algos import Spiral, WildFire + + +def calculate_repeat_steps(fields): + return int(np.sum(fields[fields > 0]) - np.sum(fields > 0)) + + +def main(env: SeaEnv, agent: Spiral, wildfire: WildFire): + state, info = env.reset() + redecide_direction = True + all_steps = 0 + while True: + all_steps += 1 + info['redecide_direction'] = redecide_direction + act = agent.step(state, info) + redecide_direction = False + if act[0] == FINISHED: + print(f'Congrates! covered all the places!') + repeat_steps = calculate_repeat_steps(info['fields']) + print(f'{repeat_steps} steps repeated!') + break + elif act[0] == BROKEN: + print(f'Broken!') + while True: + all_steps += 1 + act = wildfire.step(state[0], info) + state, terminate, info = env.step(act) + all_steps += 1 + if terminate: + return + if wildfire.empty(): + redecide_direction = True + break + all_steps -= 1 + else: + state, terminate, info = env.step(act) + if terminate: + if info['finished']: + print(f'Congrates! covered all the places!') + print(f'steps: {all_steps}') + repeat_steps = calculate_repeat_steps(info['fields']) + units_num = info['fields'].size + repeat_ratio = repeat_steps / units_num + print( + f'repeat steps: {repeat_steps}\nrepeat ratio: {repeat_steps}/{units_num}={repeat_ratio*100:.2f}%') + else: + print(f'Failed!') + return + + +def get_args(): + parser = ArgumentParser() + parser.add_argument('-c', "--config_name", type=str, required=True, + help="The name of config file, e.g., map0") + parser.add_argument('--render', action='store_true', + help='render or not') + parser.add_argument('--cover-ratio', type=float, default=1.0) + return parser.parse_args() + + +if __name__ == '__main__': + args = get_args() + sea_env = make(env_config=args.config_name, + render=args.render, n_ships=1, split_axis='x', + cover_ratio=args.cover_ratio) + algo = Spiral() + wildfire = WildFire() + main(sea_env, algo, wildfire) + + for _ in range(int(1e8)): + continue diff --git a/Palette/maps/map0.yaml b/Palette/maps/map0.yaml new file mode 100644 index 0000000..9711eca --- /dev/null +++ b/Palette/maps/map0.yaml @@ -0,0 +1,14 @@ +map_name: map0 +env: + window_width: !!int 1000 + window_height: !!int 1000 + field_size: [20, 20] + start_position: [10, 10] + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [100, 100, 100, 200] + obstacle1: [300, 160, 120, 120] + obstacle2: [460, 460, 120, 120] + obstacle3: [700, 700, 160, 160] diff --git a/Palette/maps/map1.yaml b/Palette/maps/map1.yaml new file mode 100644 index 0000000..3763e9f --- /dev/null +++ b/Palette/maps/map1.yaml @@ -0,0 +1,15 @@ +map_name: map1 +env: + window_width: !!int 1000 + window_height: !!int 1000 + field_size: [20, 20] + start_position: [10, 10] + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [100, 600, 100, 200] + obstacle1: [300, 360, 120, 120] + obstacle2: [660, 460, 120, 120] + obstacle3: [400, 700, 160, 160] + obstacle4: [460, 460, 200, 40] diff --git a/Palette/maps/map2.yaml b/Palette/maps/map2.yaml new file mode 100644 index 0000000..e395bfd --- /dev/null +++ b/Palette/maps/map2.yaml @@ -0,0 +1,17 @@ +map_name: map2 +env: + window_width: !!int 1000 + window_height: !!int 1000 + field_size: [20, 20] + start_position: [10, 10] + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [100, 600, 100, 200] + obstacle1: [300, 260, 120, 320] + obstacle2: [660, 460, 120, 120] + obstacle3: [400, 700, 160, 160] + obstacle4: [420, 260, 200, 80] + obstacle5: [420, 500, 200, 80] + obstacle6: [580, 380, 40, 120] diff --git a/Palette/maps/map_actual.yaml b/Palette/maps/map_actual.yaml new file mode 100644 index 0000000..db61a54 --- /dev/null +++ b/Palette/maps/map_actual.yaml @@ -0,0 +1,22 @@ +map_name: map_actual +env: + window_width: !!int 1300 + window_height: !!int 1200 + field_size: [4, 4] + start_position: [2, 2] + ship_radius: !!int 1 + ship_velocity: 40 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [40, 440, 340, 220] + obstacle1: [100, 160, 300, 180] + obstacle2: [300, 40, 100, 60] + obstacle3: [500, 340, 160, 80] + obstacle4: [980, 20, 180, 80] + obstacle5: [860, 300, 260, 160] + obstacle6: [1160, 440, 60, 40] + obstacle7: [740, 980, 180, 120] + obstacle8: [0, 1140, 140, 60] + obstacle9: [1240, 700, 60, 60] + actual_background: !!bool true + actual_around_area: !!int 0 diff --git a/Palette/maps/map_actual_show.yaml b/Palette/maps/map_actual_show.yaml new file mode 100644 index 0000000..8538b72 --- /dev/null +++ b/Palette/maps/map_actual_show.yaml @@ -0,0 +1,22 @@ +map_name: map_actual_show +env: + window_width: !!int 1300 + window_height: !!int 1200 + field_size: [20, 20] + start_position: [10, 10] + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [40, 440, 340, 220] + obstacle1: [100, 160, 300, 180] + obstacle2: [300, 40, 100, 60] + obstacle3: [500, 340, 160, 80] + obstacle4: [980, 20, 180, 80] + obstacle5: [860, 300, 260, 160] + obstacle6: [1160, 440, 60, 40] + obstacle7: [740, 980, 180, 120] + obstacle8: [0, 1140, 140, 60] + obstacle9: [1240, 700, 60, 60] + actual_background: !!bool true + actual_around_area: !!int 0 diff --git a/Palette/maps/map_actual_with_around.yaml b/Palette/maps/map_actual_with_around.yaml new file mode 100644 index 0000000..84c1cff --- /dev/null +++ b/Palette/maps/map_actual_with_around.yaml @@ -0,0 +1,22 @@ +map_name: map_actual_with_around +env: + window_width: !!int 1300 + window_height: !!int 1200 + field_size: [4, 4] + start_position: [2, 2] + ship_radius: !!int 1 + ship_velocity: 40 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [40, 440, 340, 220] + obstacle1: [100, 160, 300, 180] + obstacle2: [300, 40, 100, 60] + obstacle3: [500, 340, 160, 80] + obstacle4: [980, 20, 180, 80] + obstacle5: [860, 300, 260, 160] + obstacle6: [1160, 440, 60, 40] + obstacle7: [740, 980, 180, 120] + obstacle8: [0, 1140, 140, 60] + obstacle9: [1240, 700, 60, 60] + actual_background: !!bool true + actual_around_area: !!int 50 diff --git a/Palette/maps/map_actual_with_around_show.yaml b/Palette/maps/map_actual_with_around_show.yaml new file mode 100644 index 0000000..0e62210 --- /dev/null +++ b/Palette/maps/map_actual_with_around_show.yaml @@ -0,0 +1,22 @@ +map_name: map_actual_with_around +env: + window_width: !!int 1300 + window_height: !!int 1200 + field_size: [20, 20] + start_position: [10, 10] + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [40, 440, 340, 220] + obstacle1: [100, 160, 300, 180] + obstacle2: [300, 40, 100, 60] + obstacle3: [500, 340, 160, 80] + obstacle4: [980, 20, 180, 80] + obstacle5: [860, 300, 260, 160] + obstacle6: [1160, 440, 60, 40] + obstacle7: [740, 980, 180, 120] + obstacle8: [0, 1140, 140, 60] + obstacle9: [1240, 700, 60, 60] + actual_background: !!bool true + actual_around_area: !!int 5 diff --git a/Palette/maps/multi_map0.yaml b/Palette/maps/multi_map0.yaml new file mode 100644 index 0000000..be8567a --- /dev/null +++ b/Palette/maps/multi_map0.yaml @@ -0,0 +1,19 @@ +map_name: multi_map0 +env: + window_width: !!int 1000 + window_height: !!int 1000 + field_size: [20, 20] + #start_position: + # ship0: [10, 10] + # ship1: [990, 990] + #start_r: + # ship0: !!float 0.0 # pi + # ship1: !!float 1.0 # pi + ship_radius: !!int 5 + ship_velocity: 200 + sonar_spread_times: !!int 40 + obstacles: # name: [x of the left bottom, y of the left bottom, width, height] + obstacle0: [100, 100, 100, 200] + obstacle1: [300, 160, 120, 120] + obstacle2: [460, 460, 120, 120] + obstacle3: [700, 700, 160, 160] diff --git a/README.md b/README.md index ae08536..42504f4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# SeAIPalette +# SeAI Palette集智调色板 +## 0. 软件介绍 +SeAI Palette集智调色板是面向集群网络的多节点智能协同路径规划软件。软件以面向对象的设计理念,采用Python语言编程,基于pyqt、pygame、pyyaml、pymunk、easydict以及ppdet等技术开发,内置3个虚拟地图和多种算法(牛耕法、内螺旋法、贪心法等,持续更新),并提供扩展接口,支持地图和算法自定义。软件安装简单,运行方便,可选参数丰富,扩展性高,非常适用于相关研究领域的工程技术人员和学生掌握学习集群智能规划方法。 +集智调色板软件设计了参数输入模块、算法运行模块及信息输出模块,在不同节点数量要求的前提下划分区域方向,按区域进行算法的运行。并可以综合考虑固定节点、覆盖率、电池容量等条件下,给出运行步数、重复步数和重复率等等性能指标。 + +软件界面简单,易学已用,包含参数的输入选择,程序的运行,算法结果的展示等,源代码公开,算法可修改。 +开发人员:王凯、于化鹏、李晶、王兆琦、李慧涛、赵志允、张乐飞、陈光 + + +## 1. 开发环境配置 +运行以下命令: +```bash +conda env create -f create_env.yaml +``` +该命令会创建一个名为`Palette`的conda虚拟环境,用`conda activate Palette`即可激活该虚拟环境。 + + +## 2. 软件运行 +运行以下命令运行软件: +```python +python main_tt.py +``` + +## 3. 一些说明 +1. 程序输出的说明 + + 程序运行结束后会在命令行输出类似于下面的结果: + ``` + finished!!! + Congrates! covered all the places! + Used steps: 79 + repeat steps: 187 + repeat ratio: 187/3900=4.79% + ``` + 分别为使用的步数,重复的步数和重复率。 + +2. 关于渲染结果的说明 + + 渲染中不同移动节点负责的区域用不同颜色标记,每个区域颜色越深表示该区域被重复走的次数越多。 + + 为了能够让人看清最后遍历的结果,我们在程序最后加了一个循环(空循环$10^8$次)以防止渲染结果立刻消失。 + + 此外,渲染结果最后会留一格小区域没有覆盖,这个是渲染结果滞后仿真程序内核一个时间单位导致的,不会对实际测试结果造成影响。 \ No newline at end of file diff --git a/condaenv.6gnh6mi1.requirements.txt b/condaenv.6gnh6mi1.requirements.txt new file mode 100644 index 0000000..8e46c12 --- /dev/null +++ b/condaenv.6gnh6mi1.requirements.txt @@ -0,0 +1,5 @@ +pygame==2.0.1 +pymunk==6.0.0 +pyyaml==5.4.1 +numpy==1.20.3 +easydict==1.9 \ No newline at end of file diff --git a/create_env.yaml b/create_env.yaml new file mode 100644 index 0000000..d734233 --- /dev/null +++ b/create_env.yaml @@ -0,0 +1,11 @@ +name: Palette +dependencies: + - python=3.7.3 + - anaconda + - pip + - pip: + - pygame==2.0.1 + - pymunk==6.0.0 + - pyyaml==5.4.1 + - numpy==1.20.3 + - easydict==1.9 diff --git a/create_env.yaml.bak b/create_env.yaml.bak new file mode 100644 index 0000000..3a1e29e --- /dev/null +++ b/create_env.yaml.bak @@ -0,0 +1,11 @@ +name: Palette +dependencies: + - python=3.7.6 + - anaconda + - pip + - pip: + - pygame==2.0.1 + - pymunk==6.0.0 + - pyyaml==5.4.1 + - numpy==1.20.3 + - easydict==1.9 diff --git a/main_tt.py b/main_tt.py new file mode 100644 index 0000000..e228c26 --- /dev/null +++ b/main_tt.py @@ -0,0 +1,127 @@ +import sys, os +from MainWindow_map import Ui_MainWindow +from PyQt5 import QtWidgets, QtCore, QtSql, QtGui +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from PyQt5.QtGui import QPixmap + +import threading, time, subprocess + +class mywindow(QtWidgets.QMainWindow, Ui_MainWindow): + def __init__(self): + super(mywindow, self).__init__() + self.setupUi(self) + + self.comboBox_map.addItems(['地图0', '地图1', '地图2']) + + self.comboBox_algo.addItems(['野火法+牛耕法', '野火法+内螺旋法', '野火法+贪心法']) + + self.comboBox_axis.addItems(['x', 'y']) + + self.comboBox_render.addItems(['是', '否']) + + self.comboBox_around.addItems(['不考虑', '考虑']) + + self.comboBox_bettery.addItems(['否', '是']) + + if self.comboBox_bettery.currentText() == '否': + self.lineEdit_bettery.setEnabled(False) + + def slot_bettery(self): + if self.comboBox_bettery.currentText() == '否': + self.lineEdit_bettery.setEnabled(False) + if self.comboBox_bettery.currentText() == '是': + self.lineEdit_bettery.setEnabled(True) + + def thread_run(self): + if self.comboBox_bettery.currentText() == '否': + if self.ships_num == '1': + if self.comboBox_algo.currentText() == '野火法+牛耕法': + cmd = 'python -u -m Palette.examples.wildfire_boustrophedon -c '+self.map+self.render+'--cover-ratio '+self.cover_ratio + result = subprocess.Popen( + cmd, shell=True,stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(),encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+内螺旋法': + cmd = 'python -u -m Palette.examples.wildfire_spiral -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+贪心法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_greedy -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio + ' -n 1' + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + elif self.ships_num != '1': + if self.comboBox_algo.currentText() == '野火法+牛耕法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_boustrophedon -c '+self.map+self.render+'--cover-ratio '+self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis + result = subprocess.Popen( + cmd, shell=True,stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(),encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+内螺旋法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_spiral -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+贪心法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_greedy -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + + elif self.comboBox_bettery.currentText() == '是': + if self.ships_num != '1': + if self.comboBox_algo.currentText() == '野火法+牛耕法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_boustrophedon -c '+self.map+self.render+'--cover-ratio '+self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis+' -b '+self.lineEdit_bettery.text() + result = subprocess.Popen( + cmd, shell=True,stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(),encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+内螺旋法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_spiral -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis+' -b '+self.lineEdit_bettery.text() + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + elif self.comboBox_algo.currentText() == '野火法+贪心法': + cmd = 'python -u -m Palette.examples.charge_multi_split_wildfire_greedy -c ' + self.map + self.render + '--cover-ratio ' + self.cover_ratio+' -n '+str(self.ships_num)+' -a '+self.axis+' -b '+self.lineEdit_bettery.text() + result = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE).stdout + self.label_show.setText(str(result.read(), encoding="utf-8")) + + elif self.ships_num == '1': + self.label_show.setText('请设置多移动节点') + + + def run(self): + self.cover_ratio = str(float(self.lineEdit_coverr.text()) / 100.0) + self.ships_num = self.lineEdit_shipn.text() + self.axis = self.comboBox_axis.currentText() + if self.comboBox_map.currentText() == '地图0': + self.map = 'map0' + elif self.comboBox_map.currentText() == '地图1': + self.map = 'map1' + elif self.comboBox_map.currentText() == '地图2': + self.map = 'map2' + elif self.comboBox_map.currentText() == '自定义地图': + if self.comboBox_around.currentText() == '不考虑': + self.map = 'map_actual_show' + elif self.comboBox_around.currentText() == '考虑': + self.map = 'map_actual_with_around_show' + + if self.comboBox_render.currentText() == '是': + self.render = ' --render ' + elif self.comboBox_render.currentText() == '否': + self.render = ' ' + + threading.Thread(target=self.thread_run).start() + self.label_show.setText('程序正在运行,请稍后!') + + def stop(self): + print('2') + + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = mywindow() + # window.showMaximized() + window.show() + sys.exit(app.exec_()) diff --git a/pics/sea.jpg b/pics/sea.jpg new file mode 100644 index 0000000..a7f1d98 Binary files /dev/null and b/pics/sea.jpg differ diff --git a/pics/ship.png b/pics/ship.png new file mode 100644 index 0000000..f450b72 Binary files /dev/null and b/pics/ship.png differ