Модель роя БПЛА
Модель роя БПЛА
В данном примере рассматривается комплексная агент-ориентированная модель управления роем беспилотных летательных аппаратов (БПЛА) для выполнения задачи поиска и уничтожения мобильных целей на ограниченном полигоне. Код инкапсулирует ключевые аспекты поведения роя: децентрализованное управление, совместное построение карты обстановки, коммуникацию между агентами в пределах ограниченного радиуса действия и кооперативное принятие решений для атаки целей, требующих для нейтрализации одновременного применения нескольких дронов.
В данном проекте применяются следующие библиотеки и инструменты:
using Random
— предоставляет функции для генерации псевдослучайных чисел, что критично для моделирования стохастического поведения дронов и целей.
using LinearAlgebra
— предлагает инструменты для линейных алгебраических операций, таких как вычисление нормы вектора (norm
), используемой для расчёта расстояний между агентами.
using Statistics
— содержит базовые статистические функции, включая вычисление среднего значения (mean
) для анализа результатов симуляции.
gr()
— активирует бэкенд GR для библиотеки Plots.jl, который обеспечивает высокоскоростную и качественную визуализацию анимации работы роя.
using Random
using LinearAlgebra
using Statistics
gr()
Теперь рассмотрим структуры объектов применяемых в проекте.
Структура: SimulationConfig
Назначение: Хранение всех конфигурационных параметров симуляции для обеспечения гибкости и удобства управления экспериментом.
world_size::Tuple{Int, Int}
— Размеры виртуального полигона (ширина и высота) в условных единицах, ограничивающие область движения агентов.n_drones::Int
— Общее количество дронов в рое.n_targets::Int
— Общее количество целей на полигоне.dt::Float64
— Величина временного шага (дельта времени) для интегрирования уравнений движения, определяющая дискретность симуляции.total_time::Int
— Общая продолжительность моделирования, измеряемая в количестве временных шагов (кадрах).comm_range::Float64
— Максимальная дальность, на которой два дрона могут обмениваться данными (радиус связи).sensor_range::Float64
— Максимальная дальность обнаружения цели сенсорами дрона (радиус обзора).drone_speed::Float64
— Базовая скорость перемещения дронов в режиме поиска.target_speed::Float64
— Базовая скорость перемещения целей.seed::Int
— Seed (зерно) для генератора псевдослучайных чисел, обеспечивающее воспроизводимость результатов эксперимента.
Структура: Drone
Назначение: Описание состояния и поведения одного агента-дрона в рое. Является изменяемой (mutable), так как её поля активно обновляются в процессе симуляции.
id::Int
— Уникальный идентификатор дрона.pos::Vector{Float64}
— Вектор с текущими координатами дрона на плоскости[x, y]
.vel::Vector{Float64}
— Вектор с текущими компонентами скорости дрона[vx, vy]
.state::Symbol
— Текущее состояние дрона (:searching
- поиск,:diving
- атака), определяющее его поведение.map::Matrix{Float64}
— Локальная карта известных целей, представляющая собой матрицу, где1.0
отмечает координаты обнаруженных целей.target_id::Union{Int, Nothing}
— ID цели, которую дрон атакует в данный момент;nothing
, если дрон в режиме поиска.comm_range::Float64
— Радиус связи конкретного дрона (может быть использован для моделирования гетерогенного роя).sensor_range::Float64
— Радиус обзора конкретного дрона.failed_attack::Bool
— Флаг, указывающий на неудачную попытку атаки (нужен для визуального выделения такого дрона и смены его логики).target_destroyed::Bool
— Флаг, указывающий, был ли дрон уничтожен.
Структура: Target
Назначение: Описание состояния цели. Также является изменяемой, так её статус и список атакующих дронов меняются в ходе симуляции.
id::Int
— Уникальный идентификатор цели.pos::Vector{Float64}
— Вектор с текущими координатами цели[x, y]
.vel::Vector{Float64}
— Вектор с текущими компонентами скорости цели[vx, vy]
.required_drones::Int
— Количество дронов, необходимое для одновременной атаки, чтобы уничтожить цель (порог кооперации).locked_by::Vector{Int}
— Вектор, содержащийid
дронов, которые в данный момент атакуют ("захватили") данную цель.destroyed::Bool
— Флаг, указывающий, была ли цель уничтожена.
struct SimulationConfig
world_size::Tuple{Int, Int}
n_drones::Int
n_targets::Int
dt::Float64
total_time::Int
comm_range::Float64
sensor_range::Float64
drone_speed::Float64
target_speed::Float64
seed::Int
end
mutable struct Drone
id::Int
pos::Vector{Float64}
vel::Vector{Float64}
state::Symbol
map::Matrix{Float64}
target_id::Union{Int, Nothing}
comm_range::Float64
sensor_range::Float64
failed_attack::Bool
target_destroyed::Bool
end
mutable struct Target
id::Int
pos::Vector{Float64}
vel::Vector{Float64}
required_drones::Int
locked_by::Vector{Int}
destroyed::Bool
end
Код представленный ниже инициализирует глобальные константы с параметрами симуляции, создает пустую карту и выводит в консоль основные настройки эксперимента для визуального контроля.
const config = SimulationConfig((200, 200), 10, 7, 0.5, 700, 100.0, 20.0, 3.0, 0.5, 123)
const empty_map = zeros(config.world_size...)
println("Конфигурация:")
println(" Дроны: $(config.n_drones)")
println(" Цели: $(config.n_targets)")
println(" Размер мира: $(config.world_size)")
println(" Диапазон связи: $(config.comm_range)")
println(" Диапазон сенсора: $(config.sensor_range)")
Далее по порядку рассмотрим вспомогательные функции используемые в этом примере.
Функция initialize_drones
создает и возвращает вектор из n
дронов, каждый из которых случайно размещается на одной из четырех границ игрового мира и получает начальную скорость, направленную внутрь полигона, а также инициализирует все стартовые параметры агентов, включая их уникальный идентификатор, пустую карту, состояние поиска и отсутствие цели.
function initialize_drones(n, world_size, comm_range, sensor_range)
drones = Vector{Drone}(undef, n)
sides = rand(1:4, n)
for i in 1:n
pos, vel = if sides[i] == 1
([rand() * world_size[1], world_size[2]], [0.0, -rand(1.0:config.drone_speed)])
elseif sides[i] == 2
([world_size[1], rand() * world_size[2]], [-rand(1.0:config.drone_speed), 0.0])
elseif sides[i] == 3
([rand() * world_size[1], 0.0], [0.0, rand(1.0:config.drone_speed)])
else
([0.0, rand() * world_size[2]], [rand(1.0:config.drone_speed), 0.0])
end
drones[i] = Drone(i, pos, vel, :searching, copy(empty_map), nothing, comm_range, sensor_range, false, false)
end
return drones
end
Функция initialize_targets
создает и возвращает вектор из n
целей, каждая из которых размещается в псевдослучайной позиции в центральной области мира (с отступом от границ) и получает случайный вектор скорости, а также инициализирует их параметры, включая идентификатор, необходимое для уничтожения количество дронов (по умолчанию 1), пустой список атакующих и статус "не уничтожен".
function initialize_targets(n, world_size)
targets = Vector{Target}(undef, n)
for i in 1:n
pos = [rand(10:world_size[1]-10), rand(10:world_size[2]-10)]
vel = [rand(-1.0:0.1:1.0), rand(-1.0:0.1:1.0)] * config.target_speed
targets[i] = Target(i, pos, vel, 1, Int[], false)
end
return targets
end
Функция update_position!
обновляет позицию агента (дрона или цели) на основе его скорости и временного шага, обеспечивает корректное отражение от границ путем помещения агента внутрь полигона и расчета нового случайного вектора скорости, направленного под углом в пределах ±60 градусов от нормали к стене, но только если агент является активной, не уничтоженной целью.
function update_position!(agent, dt, world_size)
if !(isa(agent, Target) && agent.destroyed)
agent.pos .+= agent.vel .* dt
hit_wall = false
for i in 1:2
if agent.pos[i] < 0
agent.pos[i] = 0
hit_wall = true
elseif agent.pos[i] > world_size[i]
agent.pos[i] = world_size[i]
hit_wall = true
end
end
if hit_wall
θ = rand(-π/3:0.1:π/3)
speed = max(norm(agent.vel), 1.0)
agent.vel = [cos(θ) -sin(θ); sin(θ) cos(θ)] * [speed, 0.0]
for i in 1:2
if agent.pos[i] == 0
agent.vel[i] = abs(agent.vel[i])
elseif agent.pos[i] == world_size[i]
agent.vel[i] = -abs(agent.vel[i])
end
end
end
end
end
Функция sense_and_act!
реализует логику обнаружения целей и принятия решений для одного дрона: если дрон не находится в режиме атаки, не провалил предыдущую атаку и не уничтожил ранее цель, он сканирует пространство в пределах своего радиуса обзора, обновляет свою карту координатами найденных целей и, если цель требует больше дронов для атаки, переходит в режим атаки (:diving
), блокируя цель и добавляя свой ID в её список атакующих.
function sense_and_act!(drone, targets, world_size)
(drone.state == :diving || drone.failed_attack || drone.target_destroyed) && return nothing
for target in targets
target.destroyed && continue
dist = norm(drone.pos - target.pos)
if dist < drone.sensor_range
x_idx = Int(clamp(round(target.pos[1]), 1, world_size[1]))
y_idx = Int(clamp(round(target.pos[2]), 1, world_size[2]))
drone.map[x_idx, y_idx] = 1.0
if length(target.locked_by) < target.required_drones && drone.target_id === nothing
drone.state = :diving
drone.target_id = target.id
push!(target.locked_by, drone.id)
break
end
end
end
end
Функция communicate!
реализует механизм обмена информацией между дронами: для каждой пары дронов проверяется расстояние между ними, и если оно меньше радиуса связи обоих, то происходит синхронизация их карт целей путем объединения известных координат (если цель известна хотя бы одному дрону, она становится известна обоим).
function communicate!(drones)
n = length(drones)
for i in 1:n
for j in i+1:n
dist = norm(drones[i].pos - drones[j].pos)
if dist < min(drones[i].comm_range, drones[j].comm_range)
for x in 1:size(drones[i].map, 1)
for y in 1:size(drones[i].map, 2)
if drones[i].map[x, y] == 1.0 || drones[j].map[x, y] == 1.0
drones[i].map[x, y] = 1.0
drones[j].map[x, y] = 1.0
end
end
end
end
end
end
end
Функция dive_to_target!
управляет процессом атаки дрона: если цель уже уничтожена, дрон переходит в режим поиска, в противном случае дрон движется к цели и при сближении на критическое расстояние с вероятностью 80% уничтожает цель (если достигнуто необходимое количество атакующих дронов).
function dive_to_target!(drone, targets, dt, world_size)
if drone.state == :diving && drone.target_id !== nothing
target = targets[drone.target_id]
if target.destroyed
drone.state = :searching
drone.target_id = nothing
drone.target_destroyed = true
return
end
direction = target.pos - drone.pos
norm_dir = norm(direction)
if norm_dir > 1.0
drone.vel = direction / norm_dir * 5.0
else
drone.vel .= 0.0
if rand() < 0.8 && length(target.locked_by) >= target.required_drones
target.destroyed = true
target.vel .= 0.0
drone.target_destroyed = true
else
drone.failed_attack = true
drone.state = :searching
drone.vel .= 0.0
filter!(x -> x != drone.id, target.locked_by)
end
end
update_position!(drone, dt, world_size)
end
end
Функция create_plot
визуализирует текущее состояние симуляции: отображает цели (красные - активные, серые - уничтоженные) с информацией о количестве атакующих дронов, дроны в различных состояниях (синие - поиск, зеленые - атака, серые - неактивные после выполнения задачи или неудачи), зоны обзора дронов в режиме поиска, а также выводит статистику по количеству уничтоженных целей, атакующих дронов и номер текущего кадра.
function create_plot(drones, targets, frame, world_size, destroyed_count, attacking_count)
p = plot(size=(800, 800), xlim=(0, world_size[1]), ylim=(0, world_size[2]), legend=false, title="Рой дронов - Кадр: $frame/$(config.total_time)")
for target in targets
color = target.destroyed ? :gray : :red
scatter!([target.pos[1]], [target.pos[2]], color=color, markersize=6, marker=:square, alpha=0.8)
if !target.destroyed
annotate!(target.pos[1] + 3, target.pos[2] + 3, text("Ц$(target.id)($(length(target.locked_by))/$(target.required_drones))", 8))
end
end
for drone in drones
color = if drone.failed_attack || drone.target_destroyed # внесены изменения
:gray
elseif drone.state == :searching
:blue
elseif drone.state == :diving
if drone.target_id !== nothing && targets[drone.target_id].destroyed
:gray
else
:green
end
else
:purple
end
scatter!([drone.pos[1]], [drone.pos[2]], color=color, markersize=7, marker=:utriangle, alpha=0.9)
if drone.state == :searching && !drone.failed_attack && !drone.target_destroyed # внесены изменения
plot!([drone.pos[1] + drone.sensor_range * cos(θ) for θ in 0:0.2:2π],
[drone.pos[2] + drone.sensor_range * sin(θ) for θ in 0:0.2:2π],
linecolor=color, linealpha=0.1, linewidth=1, seriestype=:path)
end
annotate!(drone.pos[1] - 6, drone.pos[2] - 6, text("Д$(drone.id)", 8, color))
end
annotate!(5, world_size[2] - 5, text("Уничтожено: $(destroyed_count)/$(config.n_targets)", 10, :black))
annotate!(5, world_size[2] - 15, text("Атакующие: $(attacking_count)", 10, :black))
annotate!(5, world_size[2] - 25, text("Кадр: $frame/$(config.total_time)", 10, :black))
return p
end
Функция run_simulation
является основным циклом симуляции: инициализирует дронов и цели, затем на каждом временном шаге обновляет позиции целей, выполняет обнаружение целей дронами, организует коммуникацию между дронами, обновляет позиции активных дронов, визуализирует состояние системы, отслеживает время уничтожения целей и выводит итоговую статистику после завершения симуляции.
function run_simulation()
Random.seed!(config.seed)
drones = initialize_drones(config.n_drones, config.world_size, config.comm_range, config.sensor_range)
targets = initialize_targets(config.n_targets, config.world_size)
frames_to_destroy = Int[]
target_destroy_times = Dict{Int, Int}()
destroyed_count = 0
anim = @animate for frame in 1:config.total_time
attacking_count = count(d -> d.state == :diving, drones)
current_destroyed = count(t -> t.destroyed, targets)
if current_destroyed > destroyed_count
for target in targets
if target.destroyed && !haskey(target_destroy_times, target.id)
target_destroy_times[target.id] = frame
push!(frames_to_destroy, frame)
end
end
destroyed_count = current_destroyed
end
if destroyed_count == config.n_targets
println("Все цели уничтожены на кадре $frame")
break
end
for target in targets
update_position!(target, config.dt, config.world_size)
end
for drone in drones
sense_and_act!(drone, targets, config.world_size)
end
communicate!(drones)
for drone in drones
if drone.state == :searching && !drone.failed_attack && !drone.target_destroyed # внесены изменения
update_position!(drone, config.dt, config.world_size)
elseif drone.state == :diving
dive_to_target!(drone, targets, config.dt, config.world_size)
end
end
p = create_plot(drones, targets, frame, config.world_size, destroyed_count, attacking_count)
end every 5
println("\n" * "="^50)
println("ИТОГИ СИМУЛЯЦИИ")
println("="^50)
println("Уничтожено целей: $(destroyed_count)/$(config.n_targets)")
if !isempty(frames_to_destroy)
println("Среднее время уничтожения: $(round(mean(frames_to_destroy), digits=1)) кадров")
println("Максимальное время: $(maximum(frames_to_destroy)) кадров")
println("Минимальное время: $(minimum(frames_to_destroy)) кадров")
end
return anim
end
Теперь запустим симуляцию и посмотрим на её результаты.
println("Запуск симуляции...")
animation = run_simulation()
gif(animation, "drone_swarm.gif", fps=10)
Вывод
Этот проект представляет собой отличный пример имитационного моделирования сложных систем с децентрализованным управлением. Он учит принципам агент-ориентированного моделирования, где каждый дрон обладает собственной логикой поведения и локальной информацией, но при этом способен к кооперативному взаимодействию через ограниченную коммуникацию.
Наиболее интересные аспекты:
- Реализация механизма коллективного принятия решений без центрального контроллера
- Моделирование ограниченной сенсорной области и радиуса связи
- Кооперативная атака целей, требующих одновременного участия нескольких дронов
- Динамическая адаптация к изменяющейся обстановке и обработка неудачных атак
- Визуализация в реальном времени с цветовой индикацией состояний агентов
Код демонстрирует, как простая набор правил на индивидуальном уровне может порождать сложное групповое поведение на системном уровне, что является ключевым принципом в роевом интеллекте и мультиагентных системах.