Давным-давно мне пришла в голову идея попробовать создать собственный симулятор для развлечения. Но на самом деле я никогда не начинал этим заниматься, потому что меня пугала задача и я думал, что это слишком сложный проект, чтобы я мог его создать в свободное время. Затем, несколько недель назад, я обнаружил simpy. Хотя simpy был не совсем тем, чем я хотел заниматься, прочитав его документацию и примеры, а также познакомившись с концепциями моделирования дискретных событий, я почувствовал, что задача создания симулятор, может быть, не был таким сложным, как я думал раньше.

Моя первая мысль была «Хорошо, все сводится к вычислению набора значений (для каждой моделируемой вещи) внутри большого for цикла». С этой идеей я решил создать свой собственный инструмент моделирования. Основная идея была настолько проста, что я решил обойтись без каких-либо внешних пакетов, кроме стандартных пакетов библиотек и matplotlib для построения графиков.

Вот как выглядит мой большой for цикл (без кода, связанного с созданием графиков):

def run(self):
    for i in range(self.cycles):
        # run models according to position on execution list
        for model_definition in self.execution_list:
            model_inputs = self.get_model_inputs(model_definition)
odel_definition["name"]].calculate(**model_inputs)
        # update time
        self.t.append(self.t[-1] + self.dt)

Вот и все, это монстр, которого я боялся вначале! Не так уж и плохо, не правда ли? Как я уже упоминал, все сводится к вычислению состояний модели и результатов внутри цикла. Конечно, есть намного больше кода, помимо того, что показано выше. И, конечно, это действительно, очень, очень простой симулятор с почти нулевым количеством функций. Он находится в нескольких световых годах от того, чтобы быть похожим на OpenModelica или MATLAB.

По правде говоря, моей главной целью в этом небольшом проекте было фактически создать образец проекта Python, который я мог бы использовать в качестве стандарта для других будущих проектов Python. Следовательно, меня больше интересовали такие вещи, как:

  • Как лучше всего структурировать код.
  • Использование базовых стандартных библиотек (os, warnings, unittest и т. Д.).
  • Настройка и запуск тестов.
  • Отладка кода Python.
  • Документация по коду.
  • Кодирование на Python в целом.

Тем не менее, всего через несколько дней я уже выпустил то, что не стыдно публиковать! Итак, давайте лучше посмотрим, как это работает!

Симулятор класса

У класса симулятора есть только несколько обязанностей:

  • Загружайте, запускайте и отслеживайте все моделируемые модели.
  • Следите за временем.
  • Инициализируйте графики.

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

{
    "name": "rc",
    "class": "RC",
    "order": 1,
    "params": {
        "resistance": 2,
        "capacitance": 0.4,
        "charge": 0.0
    },
    "inputs": {
        "Vin": {
            "model": "sig",
            "variable": "current_value"
        }
    },
    "plot": {
        "Vc": {}
    }
}

После загрузки моделей класс Simulator запускает большой for цикл для вычисления смоделированных значений и обновления графиков.

Классы моделей

Подпакет модели тренажера содержит модули, используемые для моделирования и моделирования различных компонентов. У этих модулей есть две обязанности:

  • Инициализируйте модель.
  • Сохраните переменные ввода, состояния и вывода.
  • Рассчитайте состояние компонента на основе состояния и входных переменных.

Пример для модели RC показан ниже.

class RC:
    """ A class used to model an RC (resistor-capacitor) circuit """
    def __init__(self, dt, resistance, capacitance, charge):
        # constants
        self.dt = dt
        self.R = [resistance]
        self.C = [capacitance]
        # state variable
        self.Q = [charge]
        self.Q1 = [charge]
        # inputs
        self.Vin = [None]
        # other
        self.Vr = [None]
        self.Vc = [None]
        self.i = [None]

    def calculate(self, Vin):
        """ Calculates the next value and adds it to the memory """
        self.Vc.append(self.Q[-1]/self.C[-1])
        self.Vr.append(Vin - self.Vc[-1])
        self.i.append(self.Vr[-1]/self.R[-1])
        # update state variables
        self.Q.append(self.dt*((1/self.R[-1])*(Vin-(1/self.C[-1])*self.Q1[-1]))+self.Q1[-1])
        self.Q1.append(self.Q[-1])
        # save input just for recording
        self.Vin.append(Vin)
        # return the charge
        return self.Q[-1]

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

Пример моделирования

Загрузив три файла JSON в папку конфигурации, я могу смоделировать три разных компонента:

  • Генератор сигналов (специальный класс, который выводит значения на основе своего файла настроек).
  • RC-цепочка (резистор-конденсатор).
  • Схема RL (резистор-индуктор).

И эти компоненты подключаются (через файл настроек), как показано ниже:

Схема нарисована с помощью circuitlab.

Результаты моделирования для 20-секундного прогона показаны ниже. Фиолетовый сигнал - Vin - представляет входное напряжение в цепи, и я специально сделал его по странной кривой, чтобы увидеть реакцию каждого компонента.

Что дальше

В будущем я хотел бы добавить к этому проекту несколько вещей:

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

Репозиторий

Этот до тупо простой код симулятора можно найти здесь.

Первоначально опубликовано на сайте fabiomolinar.com 24 июля 2019 г.