Простая модель сражения на Modelica

в 1:06, , рубрики: Без рубрики

Простая модель сражения на Modelica
Доброго времени суток! Недавно узнав о таком инструменте моделирования, как язык Modelica и его свободной реализации OpenModelica, был удивлен тому, что на Хабре по этому поводу всего одна статья. Поскольку тема несколько необычна, детали пришлось постигать на собственной шкуре некотором взятом из головы примере. В этой статье пойдет речь о том, как построить простую модель сражения (для примера), попутно разобравшись с некоторыми концепциями языка (основное).

Примечание: для подготовки статьи использовался последний ночной билд OpenModelica (rev18625). При инсталляции, обратите внимание, требуется указывать путь без пробелов.

Задача

Представим себе войну как процесс, в котором два противника, обладая различными силами (по соотношению, но не по силе), добиваются одной и той же цели – захвата как можно большей вражеской территории путем уничтожения сил противника на ней. Для определенности, будем считать, что территория завоевана, когда на неё ступила пехота (сухопутные войска). Море в территорию не входит. Победа засчитывается, когда завоевана вся территория врага.

Вообще говоря, войны включают в себя еще и экономику, но моделирование подобного заняло бы слишком много места. Рассмотрим пока три типа сил: морские, сухопутные, и авиацию (ядерное вооружение также рассматривать не будем). У каждой есть сопротивление атаке, или health (одной единицы), а также собственно сила атаки (будем измерять скажем в количестве пуль). И пусть каждые несколько единиц времени, поступают новые единицы в бой.
Простая модель сражения на Modelica
Боевые действия происходят в квадрате, поперек которого горизонтально проходит линия фронта, а справа – море, где действия осуществляют корабли. Длину стороны квадрата обозначим L (для определенности возьмем как 100 километров). Тогда площадь, завоеванная стороной А, будет равна A * L, завоеванная противником – (L-A) * L. Изменение А же будет зависеть от соотношения сил. Чем большие силы на фронте преобладают, тем больше А изменится (в пользу тех сил, что больше). Скажем для определенности, единица пехоты одной стороны продвигает фронт (если без боя) на 1 километр за единицу времени.

Концепции Modelica

Основой моделирования в Modelica являются классы (и их разновидности по вариантам объявления/применения, но об этом чуть-чуть позже). Класс в Modelica несколько отличается от привычных нам классов, так как содержит не только поля и методы (в Modelica — функции), но также и уравнения, которые связывают переменные друг с другом. Также поля могут иметь разный тип «изменчивости» — константа (и есть константа), параметр (не меняется в текущем моделировании), и, собственно, переменная. Полем в классе может быть как объект встроенных типов (Boolean, Real и т.п.), так и пользовательских типов. Более сложной концепцией является возможность «шаблонизации» — замены типа поля, его переопределения.

Классы могут наследоваться (в том числе множественно, причем только diamond). Для Modelica это означает, что всё содержимое наследуемого копируется в наследуемый класс, включая уравнения. И тут следует упомянуть об основном правиле Modelica — количество уравнений для переменных должно соответствовать количеству переменных. Не больше, и не меньше.

Пример решения поставленной задачи

Самый простой тип класса — запись — не может содержать уравнений.
Определим для начала абстрактный тип боевой единицы.

UnitData.mo
record UnitData "Abstract army unit record"

  parameter Real unitHealth;
  parameter Real unitAttack;

  parameter Real supplyTime;
  parameter Real supplyNumber;

end UnitData;

Данный код объявляет четыре непроинициализированных параметра. В кавычках после объявления классов и переменных можно добавлять документирующие строки. В целом же комментарии в Modelica — С++-style, то есть // или /* */ (комментарии такого вида игнорируются средой).

Следующим шагом будет объявление класса с уравнениями, объекты которого будут собой представлять некоторые военные силы, характеризующиеся тем же, что и боевые единицы + количеством.

Forces.mo

class Forces "Abstract army forces class" extends UnitData;

  parameter Real startNumber = 0;

  Real number (start = startNumber);

  Boolean destroyed;

equation

  destroyed = number < 0;

  when destroyed then 
    reinit(number, 0); 
  end when; 

  when sample(supplyTime, supplyTime) then
    reinit(number, pre(number) + supplyNumber);
  end when;                      

end Forces;

На примере этого файла можно проследить наследование, а также работу с событиями Modelica.
Если уравнение destroyed = number < 0; вопросов не должно вызывать, то внутри первого блока when используется особый оператор reinit, который устанавливает значение переменной в результат выражения вторым параметром. reinit, как и pre (значение предыдущего шага), можно использовать только внутри блоков when. pre необходимо использовать для того, чтобы уравнение (именно уравнение) не было таким: x = x + 1.

Встроенная функция sample возвращает true в случае если время моделирования (time) равно первому параметру (первый вызов), а дальше через промежутки, равные второму параметру (в нашем случае они равны — подмога прибывает через равные промежутки времени начиная с момента supplyTime).

Далее нам пригодится класс, представляющий армию противника:

EnemyForces.mo

class EnemyForces "Enemy forces references"

  replaceable Forces enemyAir;
  replaceable Forces enemySea;
  replaceable Forces enemyLand;

end EnemyForces;

Указание переменной как replaceable позволяет в дальнейшем при объявлении объекта EnemyForces переопределить тип переменной.

Теперь подходим к самому главному. Собственно класс, представляющий армию.

Army.mo

class Army

  class LandArmy = Forces(unitHealth = 100, unitAttack = 400, supplyTime = 0.25, supplyNumber = 800);
  class AirArmy = Forces(unitHealth = 400, unitAttack = 750, supplyTime = 0.5, supplyNumber = 400);
  class SeaArmy = Forces(unitHealth = 3000, unitAttack = 7000, supplyTime = 1, supplyNumber = 20);

  SeaArmy seaArmy;
  AirArmy airArmy;
  LandArmy landArmy;

  EnemyForces enemyForces (redeclare Army.AirArmy enemyAir, redeclare Army.SeaArmy enemySea, redeclare Army.LandArmy enemyLand);

  Real armyForce;

equation
  
  armyForce = seaArmy.unitHealth * seaArmy.number + airArmy.unitHealth * airArmy.number + landArmy.unitHealth * landArmy.number;

  der(seaArmy.number) = if not seaArmy.destroyed then 
    -(enemyForces.enemyAir.number * enemyForces.enemyAir.unitAttack + 
      enemyForces.enemySea.number * enemyForces.enemySea.unitAttack) / seaArmy.unitHealth
  else 0;
  
  der(airArmy.number) = if not airArmy.destroyed then 
     -(enemyForces.enemyAir.number * enemyForces.enemyAir.unitAttack + 
       enemyForces.enemySea.number * enemyForces.enemySea.unitAttack + 
       enemyForces.enemyLand.number * enemyForces.enemyLand.unitAttack) / airArmy.unitHealth
  else 0;

  der(landArmy.number) = if not landArmy.destroyed then 
    -(enemyForces.enemyAir.number * enemyForces.enemyAir.unitAttack + 
      enemyForces.enemyLand.number * enemyForces.enemyLand.unitAttack) / landArmy.unitHealth
  else 0;

end Army;

Что тут происходит. Во-первых, мы создаем конкретные классы родов войск, просто приравнивая их к существующему классу Forces и инициализируя его параметры. Тут же объявляем объекты. Следующим делом — создается объект, который будет использоваться как ссылка на вражескую армию, при этом ключевым словом redeclare подменяем replaceable-переменные. Переменная armyForce нам нужна будет чисто для «аналитических умозаключений».
Об уравнениях — первое уравнение означает, что сила армии — это броня всех юнитов какие бывают.
Следующие три уравнения означают:

  1. Если морская армия не уничтожена, то в каждый момент времени из всей её брони убывает урон, наносимый воздушными и морскими силами,
  2. Если воздушная армия не уничтожена, то в каждый момент времени из всей её брони убывает урон, наносимый любыми вражескими войсками,
  3. Аналогично с наземными войсками, за исключением того, что морские силы им не наносят урон

Обозначение der (var) предствляет собой производную по времени от var (переменной).

И, наконец, опишем сам вооруженный конфликт.

Conflict.mo

model Conflict "Conflict of two armies model"

  parameter Real armyASea = 200;
  parameter Real armyAAir = 1000;
  parameter Real armyALand = 4000;

  parameter Real armyBSea = 100;
  parameter Real armyBAir = 1800;
  parameter Real armyBLand = 3600;

  Army armyA (
    seaArmy.startNumber = armyASea, 
    airArmy.startNumber = armyAAir,
    landArmy.startNumber = armyALand,
    enemyForces.enemyAir.startNumber = armyBAir,
    enemyForces.enemySea.startNumber = armyBSea,
    enemyForces.enemyLand.startNumber = armyBLand
  );

  Army armyB (
    seaArmy.startNumber = armyBSea, 
    airArmy.startNumber = armyBAir,
    landArmy.startNumber = armyBLand,
    enemyForces.enemyAir.startNumber = armyAAir,
    enemyForces.enemySea.startNumber = armyASea,
    enemyForces.enemyLand.startNumber = armyALand
  );

  parameter Real L = 100;
  parameter Real landArmySpeed = 1;
  Real A (start = L / 2);

equation

  armyA.enemyForces.enemyAir.number = armyB.airArmy.number;
  armyA.enemyForces.enemySea.number = armyB.seaArmy.number;
  armyA.enemyForces.enemyLand.number = armyB.landArmy.number;

  armyB.enemyForces.enemyAir.number = armyA.airArmy.number;
  armyB.enemyForces.enemySea.number = armyA.seaArmy.number;
  armyB.enemyForces.enemyLand.number = armyA.landArmy.number;

  when A > L then terminate("Army A wins"); end when;
  when A < 0 then terminate("Army B wins"); end when;

  der (A) = (armyA.landArmy.number - armyB.landArmy.number) * landArmySpeed / L;

end Conflict;

Здесь использовано ключевое слово model. Это тоже класс, но для оптимизации (о чем позже) могут использоваться только модели.
Помимо объявления враждующих сторон, на чем не будем останавливаться, есть уравнения, которые устанавливают, что враг A — это B, и наоборот. Два when-условия означают завершение (terminate) моделирования с пометкой того, что произошло.

Запуск и результаты

Для того, чтобы запустить модель, можно воспользоваться shell OpenModelica, набрав в нём следующее:

Команды shell OpenModelica

loadModel(Modelica)
loadFile("d:/path/UnitData.mo")
loadFile("d:/path/Forces.mo")
loadFile("d:/path/EnemyForces.mo")
loadFile("d:/path/Army.mo")
loadFile("d:/path/Conflict.mo")
simulate(Conflict, stopTime = 2)
plot({armyA.armyForce, armyB.armyForce}, xRange = {0, 2})
plot({armyA.landArmy.number, armyB.landArmy.number, armyA.seaArmy.number, armyB.seaArmy.number, armyA.airArmy.number, armyB.airArmy.number}, xRange = {0, 2})
plot(A, xRange = {0, 2})

Соответствующие построения приведены на картинках ниже.
Простая модель сражения на Modelica
Так выглядит соотношение сил по времени.
Простая модель сражения на Modelica
Так выглядит соотношение сил по времени в разрезе типов войск.
Простая модель сражения на Modelica
А так — линия фронта. Из графиков видно, что сначала побеждала армия А, но затем армия В её победила.

Оптимизация с помощью OpenModelica

Помимо того, что можно промоделировать, можно также найти ответы на заданные модели вопросы. Например, если задаться вопросом, при каком минимальном начальном количестве сухопутных войск армия В победит, то с помощью входящего в OpenModelica инструмента OMOptim можно получить следующую картину:
Простая модель сражения на Modelica

Вместо заключения

OpenModelica неплохо документирована, для тех кто знаком с английским. В целом, инструмент многообещающий. Пока сообщения об ошибках не очень говорящие, и отладочная информация не сказать что легко интерпретируется. Но в целом, при достаточном интересе и времени — все победимо.

Еще вместо заключения

Понятно, что модель сражения очень простая, и реальность если и отражает, то только в том, что (в нашем случае минимальный) перевес в силах может не гарантировать победы, и конечный результат определяется структурой системы (кто кого бьет) в не меньшей степени, чем начальные условия.

Полезные ссылки

Отличный туториал (на английском).
Web Reference (на английском).

Автор: S_A

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js