Сообщество Engee

Модель роя БПЛА

Автор
avatar-yurevyurev
Notebook

Модель роя БПЛА

В данном примере рассматривается комплексная агент-ориентированная модель управления роем беспилотных летательных аппаратов (БПЛА) для выполнения задачи поиска и уничтожения мобильных целей на ограниченном полигоне. Код инкапсулирует ключевые аспекты поведения роя: децентрализованное управление, совместное построение карты обстановки, коммуникацию между агентами в пределах ограниченного радиуса действия и кооперативное принятие решений для атаки целей, требующих для нейтрализации одновременного применения нескольких дронов.

В данном проекте применяются следующие библиотеки и инструменты:

using Random — предоставляет функции для генерации псевдослучайных чисел, что критично для моделирования стохастического поведения дронов и целей.

using LinearAlgebra — предлагает инструменты для линейных алгебраических операций, таких как вычисление нормы вектора (norm), используемой для расчёта расстояний между агентами.

using Statistics — содержит базовые статистические функции, включая вычисление среднего значения (mean) для анализа результатов симуляции.

gr() — активирует бэкенд GR для библиотеки Plots.jl, который обеспечивает высокоскоростную и качественную визуализацию анимации работы роя.

In [ ]:
using Random
using LinearAlgebra
using Statistics
gr()
Out[0]:
Plots.GRBackend()

Теперь рассмотрим структуры объектов применяемых в проекте.

Структура: SimulationConfig

Назначение: Хранение всех конфигурационных параметров симуляции для обеспечения гибкости и удобства управления экспериментом.

  1. world_size::Tuple{Int, Int} — Размеры виртуального полигона (ширина и высота) в условных единицах, ограничивающие область движения агентов.
  2. n_drones::Int — Общее количество дронов в рое.
  3. n_targets::Int — Общее количество целей на полигоне.
  4. dt::Float64 — Величина временного шага (дельта времени) для интегрирования уравнений движения, определяющая дискретность симуляции.
  5. total_time::Int — Общая продолжительность моделирования, измеряемая в количестве временных шагов (кадрах).
  6. comm_range::Float64 — Максимальная дальность, на которой два дрона могут обмениваться данными (радиус связи).
  7. sensor_range::Float64 — Максимальная дальность обнаружения цели сенсорами дрона (радиус обзора).
  8. drone_speed::Float64 — Базовая скорость перемещения дронов в режиме поиска.
  9. target_speed::Float64 — Базовая скорость перемещения целей.
  10. seed::Int — Seed (зерно) для генератора псевдослучайных чисел, обеспечивающее воспроизводимость результатов эксперимента.

Структура: Drone

Назначение: Описание состояния и поведения одного агента-дрона в рое. Является изменяемой (mutable), так как её поля активно обновляются в процессе симуляции.

  1. id::Int — Уникальный идентификатор дрона.
  2. pos::Vector{Float64} — Вектор с текущими координатами дрона на плоскости [x, y].
  3. vel::Vector{Float64} — Вектор с текущими компонентами скорости дрона [vx, vy].
  4. state::Symbol — Текущее состояние дрона (:searching - поиск, :diving - атака), определяющее его поведение.
  5. map::Matrix{Float64} — Локальная карта известных целей, представляющая собой матрицу, где 1.0 отмечает координаты обнаруженных целей.
  6. target_id::Union{Int, Nothing} — ID цели, которую дрон атакует в данный момент; nothing, если дрон в режиме поиска.
  7. comm_range::Float64 — Радиус связи конкретного дрона (может быть использован для моделирования гетерогенного роя).
  8. sensor_range::Float64 — Радиус обзора конкретного дрона.
  9. failed_attack::Bool — Флаг, указывающий на неудачную попытку атаки (нужен для визуального выделения такого дрона и смены его логики).
  10. target_destroyed::Bool — Флаг, указывающий, был ли дрон уничтожен.

Структура: Target

Назначение: Описание состояния цели. Также является изменяемой, так её статус и список атакующих дронов меняются в ходе симуляции.

  1. id::Int — Уникальный идентификатор цели.
  2. pos::Vector{Float64} — Вектор с текущими координатами цели [x, y].
  3. vel::Vector{Float64} — Вектор с текущими компонентами скорости цели [vx, vy].
  4. required_drones::Int — Количество дронов, необходимое для одновременной атаки, чтобы уничтожить цель (порог кооперации).
  5. locked_by::Vector{Int} — Вектор, содержащий id дронов, которые в данный момент атакуют ("захватили") данную цель.
  6. destroyed::Bool — Флаг, указывающий, была ли цель уничтожена.
In [ ]:
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

Код представленный ниже инициализирует глобальные константы с параметрами симуляции, создает пустую карту и выводит в консоль основные настройки эксперимента для визуального контроля.

In [ ]:
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)")
Конфигурация:
  Дроны: 10
  Цели: 7
  Размер мира: (200, 200)
  Диапазон связи: 100.0
  Диапазон сенсора: 20.0

Далее по порядку рассмотрим вспомогательные функции используемые в этом примере.

Функция initialize_drones создает и возвращает вектор из n дронов, каждый из которых случайно размещается на одной из четырех границ игрового мира и получает начальную скорость, направленную внутрь полигона, а также инициализирует все стартовые параметры агентов, включая их уникальный идентификатор, пустую карту, состояние поиска и отсутствие цели.

In [ ]:
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
Out[0]:
initialize_drones (generic function with 1 method)

Функция initialize_targets создает и возвращает вектор из n целей, каждая из которых размещается в псевдослучайной позиции в центральной области мира (с отступом от границ) и получает случайный вектор скорости, а также инициализирует их параметры, включая идентификатор, необходимое для уничтожения количество дронов (по умолчанию 1), пустой список атакующих и статус "не уничтожен".

In [ ]:
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
Out[0]:
initialize_targets (generic function with 1 method)

Функция update_position! обновляет позицию агента (дрона или цели) на основе его скорости и временного шага, обеспечивает корректное отражение от границ путем помещения агента внутрь полигона и расчета нового случайного вектора скорости, направленного под углом в пределах ±60 градусов от нормали к стене, но только если агент является активной, не уничтоженной целью.

In [ ]:
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
Out[0]:
update_position! (generic function with 1 method)

Функция sense_and_act! реализует логику обнаружения целей и принятия решений для одного дрона: если дрон не находится в режиме атаки, не провалил предыдущую атаку и не уничтожил ранее цель, он сканирует пространство в пределах своего радиуса обзора, обновляет свою карту координатами найденных целей и, если цель требует больше дронов для атаки, переходит в режим атаки (:diving), блокируя цель и добавляя свой ID в её список атакующих.

In [ ]:
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
Out[0]:
sense_and_act! (generic function with 1 method)

Функция communicate! реализует механизм обмена информацией между дронами: для каждой пары дронов проверяется расстояние между ними, и если оно меньше радиуса связи обоих, то происходит синхронизация их карт целей путем объединения известных координат (если цель известна хотя бы одному дрону, она становится известна обоим).

In [ ]:
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
Out[0]:
communicate! (generic function with 1 method)

Функция dive_to_target! управляет процессом атаки дрона: если цель уже уничтожена, дрон переходит в режим поиска, в противном случае дрон движется к цели и при сближении на критическое расстояние с вероятностью 80% уничтожает цель (если достигнуто необходимое количество атакующих дронов).

In [ ]:
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
Out[0]:
dive_to_target! (generic function with 1 method)

Функция create_plot визуализирует текущее состояние симуляции: отображает цели (красные - активные, серые - уничтоженные) с информацией о количестве атакующих дронов, дроны в различных состояниях (синие - поиск, зеленые - атака, серые - неактивные после выполнения задачи или неудачи), зоны обзора дронов в режиме поиска, а также выводит статистику по количеству уничтоженных целей, атакующих дронов и номер текущего кадра.

In [ ]:
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
Out[0]:
create_plot (generic function with 1 method)

Функция run_simulation является основным циклом симуляции: инициализирует дронов и цели, затем на каждом временном шаге обновляет позиции целей, выполняет обнаружение целей дронами, организует коммуникацию между дронами, обновляет позиции активных дронов, визуализирует состояние системы, отслеживает время уничтожения целей и выводит итоговую статистику после завершения симуляции.

In [ ]:
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
Out[0]:
run_simulation (generic function with 1 method)

Теперь запустим симуляцию и посмотрим на её результаты.

In [ ]:
println("Запуск симуляции...")
animation = run_simulation()
gif(animation, "drone_swarm.gif", fps=10)
Запуск симуляции...

==================================================
ИТОГИ СИМУЛЯЦИИ
==================================================
Уничтожено целей: 5/7
Среднее время уничтожения: 177.4 кадров
Максимальное время: 457 кадров
Минимальное время: 24 кадров
[ Info: Saved animation to /user/drone_swarm.gif
Out[0]:
No description has been provided for this image

Вывод

Этот проект представляет собой отличный пример имитационного моделирования сложных систем с децентрализованным управлением. Он учит принципам агент-ориентированного моделирования, где каждый дрон обладает собственной логикой поведения и локальной информацией, но при этом способен к кооперативному взаимодействию через ограниченную коммуникацию.

Наиболее интересные аспекты:

  • Реализация механизма коллективного принятия решений без центрального контроллера
  • Моделирование ограниченной сенсорной области и радиуса связи
  • Кооперативная атака целей, требующих одновременного участия нескольких дронов
  • Динамическая адаптация к изменяющейся обстановке и обработка неудачных атак
  • Визуализация в реальном времени с цветовой индикацией состояний агентов

Код демонстрирует, как простая набор правил на индивидуальном уровне может порождать сложное групповое поведение на системном уровне, что является ключевым принципом в роевом интеллекте и мультиагентных системах.