1. Введение

Python — python.org

Табакерка

NanoPi NEO2

CPU: Allwinner H5, Quad-core 64-bit high-performance Cortex A53

DDR3 RAM: 1GB

Connectivity: 10/100/1000M Ethernet, RTL8211E-VB-CG chip

USB Host: USB Type A x 1 and USB pin header x 2

MicroSD Slot: MicroSD x 1 for system boot and storage

MicroUSB: Power input (5V/2A) and OTG

OS/Software: Armbian Linux nanopineo2black 6.12.23-current-sunxi64 #2 SMP PREEMPT_RT Thu Apr 10 12:39:41 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux

2. t3s — питон в табакерке

Подробное описание приложения приведено в /docs/t3s.doc.md, которое размещено в репозитории.
Далее приведено краткое описание приложения.

2.1 Введение и назначение

t3s (Time series simulation server, сервер моделирования временны́х рядов) — фоновый Linux-сервис (daemon), предназначенный для моделирования значений технологических параметров, представляющих собой временны́е ряды, установок и промышленных объектов.

Ключевой принцип работы:

  • Ядро получает исходные значения истоков (начальных узлов графа) по gRPC от стороннего шлюза промышленных протоколов передачи данных Elicont.

  • Выполняет расчёт значений графа узлов моделирования.

  • Передаёт результаты стоков (конечных узлов) обратно в шлюз по gRPC.

  • Управление ядром выполняется CLI-конфигуратором через ZMQ.

Приложение состоит из двух частей:

  • моделирующее ядро (daemon-процесс, скрипт CPython);

  • конфигуратор (CLI на базе Click, скрипт CPython).

Полное наименование: Сервер моделирования временны́х рядов.
Рабочее наименование: t3s.


2.2 Термины и определения

Термин

Определение

Решётчатая функция

Функция, определённая в дискретные моменты времени

Шаг решётчатой функции (шаг решётки), hₚ

Временной интервал между двумя соседними значениями решётчатой функции

Решётчатая функция модели (последовательность)

Решётчатая функция, значения которой являются значениями моделирования

Узел моделирования (узел)

Элемент с n ≠ 0 входами и m ≠ 0 выходами. Определяется уникальным идентификатором, типом динамического звена, его параметрами и пятью коэффициентами C1..C5. Значения всех m выходов узла одинаковы

Квант моделирования (шаг моделирования), Q

Временной интервал между двумя соседними значениями последовательности

Итерация моделирования (такт)

Процесс, в ходе которого рассчитываются текущие значения всех узлов моделирования

Скважность кванта, q

Величина, рассчитываемая на каждом такте по фактическому интервалу между обменами данными: q = ⌊Δτᵢ / hₚ⌋ (деление нацело). Шаг hₚ постоянен; квант Q в общем случае непостоянен

Ориентированный граф (орграф)

Граф, в котором рёбра между узлами имеют направления

Дуга

Направленное ребро в ориентированном графе

Смежные узлы

Узлы, имеющие общее ребро

Ациклический орграф (DAG)

Направленный граф без циклов

Степень захода узла

Количество дуг, заходящих в узел

Исток (начальный узел)

Узел с нулевой степенью захода. Получает данные по gRPC от шлюза протоколов

Степень исхода узла

Количество дуг, исходящих из узла

Сток (конечный узел)

Узел с нулевой степенью исхода. Передаёт данные по gRPC к шлюзу протоколов

Граф моделирования

Ациклический орграф, объединяющий узлы моделирования

Входы и выходы узла. Входы узла {e⁺₁, …, e⁺ₙ} (j = 1…n) делятся на внешние e⁺ᵍ (от шлюза протоколов) и внутренние e⁺ᵈ (от других узлов графа); количество как e⁺ᵍ, так и e⁺ᵈ может быть равно нулю. Аналогично выходы узла {e⁻₁, …, e⁻ₘ} (k = 1…m) делятся на внешние e⁻ᵍ (к шлюзу) и внутренние e⁻ᵈ (к другим узлам).
Исток — узел без внутренних входов (e⁺ᵈ пусто), сток — узел без внутренних выходов (e⁻ᵈ пусто).
Шаг решётчатой функции постоянен: hₚ = const; квант Q в общем случае непостоянен (Q ≠ const) из-за задержек в работе вычислительных устройств. Величина q рассчитывается на каждом такте. При q = 1 шаг решётчатой функции совпадает с шагом последовательности; при q > 1 в одном кванте для каждого узла последовательно рассчитываются q значений, последнее считается текущим.


2.3 Общая архитектура

Приложение построено по модульному принципу. Моделирующее ядро взаимодействует со шлюзом протоколов по gRPC, а с конфигуратором — по ZMQ (межскриптовый механизм REQ/REP).

┌───────────────┐   gRPC   ┌──────────────────┐  ZMQ  ┌──────────────┐
│ Протокольный  │ <──────> │ Ядро моделирова- │ <───> │ Конфигуратор │
│ шлюз (Elicont)│ (данные) │ ния (daemon)     │       │    (CLI)     │
└───────────────┘          └──────────────────┘       └──────────────┘

Ядро (модуль src/t3s/) объединяет следующие подсистемы:

  • kernel — моделирование: граф, узлы, коэффициенты, симулятор, планировщик, конечный автомат (FSM), метрики.

  • gateway — gRPC-клиент шлюза Elicont и маппинг типов сигналов.

  • control — ZMQ-сервер команд.


2.4 Модель исполнения, квант и скважность

Модель исполнения — один поток: каждый такт рассчитывается последовательно в топологическом порядке графа. Планировщик работает в отдельном потоке, но расчёт тактов является однопоточным. Такая модель исключает гонки за состояние узлов графа.
Ядро работает в режиме реального времени.
Модель исполнения:

  • Шаг решётчатой функции постоянен: hₚ = const.

  • Квант Q в общем случае непостоянен (Q ≠ const) — фактический интервал между моментами начала вычислений значений узлов (в эти моменты происходит обмен данными) определяется задержками вычислительных устройств.

  • Скважность q — динамическая величина, рассчитываемая на каждом кванте: q = ⌊Δτᵢ / hₚ⌋, где Δτᵢ — фактический интервал между соседними моментами обмена данными (τᵢ₋₁ и τᵢ); деление выполняется нацело.

  • Номинальная скважность конфигурации qₙₒₘ = Qₙₒₘ / hₚ (где Qₙₒₘ — параметр quantum_size) используется как исходное значение по умолчанию; шаг не может быть больше кванта: hₚ ≤ Qₙₒₘ, отношение Qₙₒₘ / hₚ — натуральное число.

Особенности расчёта:

  • При q = 1 шаг решётчатой функции совпадает с шагом последовательности; в кванте выполняется один такт, для каждого узла рассчитывается одно текущее значение.

  • При q > 1 в рамках кванта выполняется q тактов; в каждом такте вычисляется весь граф, все его узлы — для каждого узла одно очередное значение рекуррентной формулы. Значением узла за квант считается результат последнего такта yᵢ.

  • Внутренние входы e⁺ᵈ передаются в узел на каждом такте: используются свежие выходы предшественников, вычисленные в текущем такте (предшественники обрабатываются раньше в топологическом порядке, поэтому их выходы уже пересчитаны к моменту расчёта узла). Внешние входы e⁺ᵍ узел получает только в моменты квантов, поэтому значения на тактах аппроксимируются квадратичной параболой, проходящей через три последние решётчатые точки входа (модуль interpolation.py).

  • Расчёт каждого такта выполняется последовательно в топологическом порядке графа (однопоточная модель).

  • Программный таймер планировщика имеет период quantum_size; по истечении кванта запускается серия из q тактов. При переполнении кванта логируется WARNING, и новая серия тактов не начинается до завершения текущей (soft real-time, пропуск тиков); фактический интервал Δτᵢ при этом увеличивается, и скважность q следующего кванта автоматически растёт (модель «догоняет» реальное время). Данная ситуация, как отмечалось раньше, связана, в первую очередь, с задержками в работе вычислительного устройства.


2.5 Типовые динамические звенья

Все узлы используют общую рекуррентную формулу расчёта:

y_i = C1 * SUM(x_i) + C2 * y_{i-1} - C3 * y_{i-2} + C4 * y_{i-3} - C5 * y_{i-4}

где x_i — значения входов узла.
Начальные условия: значения входов и история y_{i-1}..y_{i-4} равны нулям.
В текущей реализации поддерживаются четыре типа звеньев:

Тип

Звено

Параметры

APD_1

Апериодическое звено 1-го порядка

k — коэффициент усиления, T — время интегрирования

APD_2

Апериодическое звено 2-го порядка

k, T1 — первое время интегрирования, T2 — второе время интегрирования

OSC

Колебательное звено

k, T — время интегрирования, xi — коэффициент колебательности

CV

Консервативное звено

k, T — время интегрирования

Формулы коэффициентов C1..C5 выводятся из передаточных функций типовых динамических звеньев.


2.6 Моделирующее ядро

Ядро реализовано как скрипт CPython, работающий в фоновом режиме (daemon).

2.6.1 Граф моделирования

Модуль graph.py реализует ациклический орграф (DAG):

  • Ацикличность гарантируется на этапе валидации и загрузки (алгоритм Кана).

  • Расчёт ведётся в топологическом порядке (topo_order).

  • Операции добавления/удаления узлов и дуг автоматически пересчитывают топологию.

  • При добавлении дуги, создающей цикл, изменение откатывается и выбрасывается ошибка GraphError.

Структурная валидация (validate):

  • проверка ацикличности и вычисление топологического порядка;

  • исток (степень захода 0) обязан иметь непустой in_tags;

  • сток (степень исхода 0) обязан иметь непустой out_tags.

Граф описывается в .JSON-файле формата GraphConfig. Замена графа целиком выполняется командой graph load <path> только на остановленном ядре; локальная запись графа (из stdin) в файл — командой graph save <path>.

Персистентность: при запуске ядро загружает граф из файла graph_config.json (путь задаётся в [graph] config_file конфига ядра); все hot-update изменения (graph add/update/remove/connect/disconnect) сериализируются в этот же файл. При загрузке графа из другого файла (graph load) загруженный граф также сериализируется в graph_config.json. Файл по умолчанию (для первого запуска) не содержит узлов и дуг. Содержимое JSON-файла должно соответствовать схеме ниже.

Схема JSON-файла графа (GraphConfig), поля верхнего уровня:

Поле

Тип

Описание

graph_id

string

Идентификатор графа.

params

object

Параметры моделирования: quantum_size — номинальный квант Q (сек), hp — шаг решётчатой функции hₚ (сек, постоянная величина), precision — точность mpmath (знаков после запятой, по умолчанию 30). Номинальная скважность qₙₒₘ = Q / hₚ вычисляется автоматически и должна быть натуральным числом; фактическая скважность каждого такта рассчитывается ядром динамически.

nodes

array

Массив узлов моделирования (см. схему узла ниже).

edges

array

Рёбра графа — список двухэлементных списков [[from, to], [from, to], ...], где from — идентификатор узла-источника, to — идентификатор узла-приёмника. Пустой список означает граф без дуг.

Схема элемента массива nodes (узел моделирования):

Поле

Тип

Описание

id

string

Уникальный идентификатор узла.

node_type

string

Тип типового динамического звена: APD_1, APD_2, OSC, CV.

k

number

Коэффициент усиления.

T

number

Время интегрирования (для APD_1, OSC, CV).

T1

number

Первое время интегрирования (для APD_2).

T2

number

Второе время интегрирования (для APD_2).

xi

number

Коэффициент колебательности (для OSC).

coefficients

object

Коэффициенты C1..C5 (строки mpf) — пересчитываются ядром по параметрам и шагу hₚ; в файле заполняются автоматически при сохранении.

in_tags

array

Теги (guid) сигналов Elicont, подаваемых на вход узла (обязателен непустой для истока).

out_tags

array

Теги (guid) сигналов Elicont, передаваемых с выхода узла (обязателен непустой для стока).

Параметры звена записываются плоскими полями k, T, T1, T2, xi на верхнем уровне узла (без вложенного params); выводятся только поля, актуальные для данного типа звена.
Рёбра задаются списком двухэлементных списков [[from, to], ...].

Пример JSON-файла графа

{
  "graph_id": "graph",
  "params": {"quantum_size": 0.1, "hp": 0.1, "precision": 30},
  "nodes": [
    {"id": "sink", "node_type": "APD_1", "k": 1.0, "T": 2.0,
     "in_tags": [], "out_tags": ["gw.out.1"]},
    {"id": "src", "node_type": "APD_1", "k": 2.0, "T": 5.0,
     "in_tags": ["gw.in.1"], "out_tags": []}
  ],
  "edges": [["src", "sink"]]
}

Граф содержит два узла: src (исток — имеет in_tags) и sink (сток — имеет out_tags), соединённые дугой src → sink.

2.6.2 Узел моделирования

Модуль node.py определяет класс Node:

  • Идентификация: уникальный id, node_type.

  • Параметры звена: k, T, T1, T2, xi (по типу нужные).

  • Коэффициенты: C1..C5 (mpf), пересчитываются при изменении параметров или кванта.

  • Теги сигналов: in_tags (для истоков), out_tags (для стоков), соответствуют «Данным пользователя» (userdata) в конфигураторе Эликонт-КС.

  • Дуги: in_edges, out_edges (множества смежных узлов).

  • История: буфер history (deque, maxlen=5) для хранения y_{i-1}..y_{i-4}.

  • Последний выход: last_output.

Вычисление нового значения выполняется методом compute(sum_x) по рекуррентной формуле, фиксация — методом commit(y) (сдвигает историю).

2.6.3 Симулятор

Модуль simulation.py выполняет расчёт одного кванта (q тактов) в топологическом порядке:

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

  • Скважность q кванта принимается извне (от планировщика): run_tick(tick_index, q); при прямом вызове без q используется номинальная скважность графа.

  • В рамках кванта выполняется q тактов; в каждом такте вычисляется весь граф — для каждого узла одно очередное значение (node.run_step). Значением узла за квант считается результат последнего такта (last_output, yᵢ).

  • Внешние входы (от шлюза) на тактах аппроксимируются квадратичной интерполяцией (interpolation.py) по трём последним решётчатым точкам последовательности guid_history. Внутренние входы (от смежных узлов in_edges) передаются на каждом такте — используются свежие выходы предшественников, вычисленные в текущем такте.

  • Метрики замеряются по каждому такту: tick_count увеличивается на q за квант, модельное время — на q · hₚ.

  • Функция compute_skva(delta_tau, hp) вычисляет скважность по фактическому интервалу обмена данными: q = ⌊Δτᵢ / hₚ⌋.

Результат кванта — список объектов ValueUpdate: node_id, value, timestamp_ns, tick_index.

2.6.4 Планировщик

Модуль scheduler.py реализует soft real-time программный таймер тактов:

  • Период таймера равен номинальному кванту quantum_size; шаг hₚ задаётся отдельно.

  • Скважность рассчитывается на каждом кванте: планировщик замеряет фактический интервал Δτᵢ между началами квантов (perf_counter) и вычисляет q = ⌊Δτᵢ / hₚ⌋ (compute_skva); при q < 1 — WARNING и принудительное q = 1.

  • Колбэк кванта принимает (tick_index, q); симулятор в рамках кванта выполняет q тактов (весь граф в каждом такте).

  • Если расчёт не уложился в квант (переполнение такта) — логируется WARNING tick skipped и новый такт не начинается до завершения текущего (пропуск тиков); при этом Δτᵢ следующего такта больше номинала, и q автоматически растёт.

  • Поддерживает смену номинального кванта (set_quantum) и шага hₚ (set_hp) на лету.

  • Последняя фактическая скважность доступна в статусе ядра: kernel status → last_q.

2.6.5 Точность вычислений

Модуль precision.py управляет точностью библиотеки mpmath:

  • По умолчанию 30 знаков после запятой, настраивается через параметр kernel.precision

  • Все вычисления, приём и передача данных между узлами выполняются числами mpf

  • Данные от шлюза (float64) преобразуются в mpf через строковый маршрут float64 → str → mpf; обратное преобразование — mpf → str → float64 (функции to_mpf / to_float). Использование mpmath снижает эффект накопления ошибки чисел с плавающей точкой.


2.7 Модуль связи со шлюзом Elicont

Модуль gateway/ обеспечивает взаимодействие ядра со шлюзом промышленных протоколов Elicont.
Базовый механизм получения данных — поллинг один раз за квант (в момент обмена данными τᵢ); при q > 1 на подтактах кванта внешние входы аппроксимируются квадратичной параболой по трём последним решёточным значениям (см. § 2.4). Потоковые методы остаются в интерфейсе как расширение.
Модуль mapping.py реализует валидацию маппинга типов сигналов:

  • На вход/выход узла допустимы только сигналы типа FLOAT64; любые иные типы (в т. ч. числовые FLOAT32, INT8..INT64 и беззнаковые, а также Boolean, строки, bitfield, OCTET, ENUM) — ошибка валидации.

  • validate_mapping проверяет наличие и допустимость типов всех требуемых guid.

При запуске ядра выполняется отложенная валидация маппинга. Если шлюз недоступен, ядро всё равно переходит в RUNNING, но стоки не передаются до установления связи и успешной проверки.


2.8 Модуль управления ядром

Модуль control/ предоставляет единый ZMQ-интерфейс управления ядром.
Модуль zmq_server.py реализует REQ/REP сервер:

  • Привязка к loopback, порт по умолчанию 5555.

  • Принимает JSON-команды от конфигуратора и делегирует их в подсистемы ядра.

  • Класс CommandHandler обрабатывает команды групп: control, config, logging, elicont.


2.9 Конфигуратор (CLI)

Конфигуратор — CLI-скрипт на базе Click (cli/t3s_cli/cli.py).
Используется для настройки, пуска и останова ядра. Все операции над графом завершаются проверкой на ацикличность и топологической сортировкой.

Группы команд:

  • graph — операции с графом (в т. ч. hot-update на запущенном ядре);

  • kernel — управление ядром;

  • config — параметры конфигурации;

  • elicont — шлюз Elicont;

  • logging — логирование.

2.9.1 Группа graph

Подкоманды группы выполняют действия над элементами графа, загруженного в ядро. Команды могут выполняться как на остановленном, так и на запущенном ядре моделирования (hot-update); при изменении узлов пересчитываются коэффициенты C1..C5 и применяются на следующем такте, учитываются новые дуги. История y_{i-1}..y_{i-4} изменённого узла не обнуляется. Все изменения сериализируются в graph_config.json.

Команда

Описание

stdin

stdout

graph add <node_id> <node_type> --k --T --T1 --T2 --xi --in-tag --out-tag

Добавление узла в граф ядра (hot-update)

—

JSON

graph update <id> --k --T --xi ...

Корректировка параметров узла (hot-update)

—

JSON

graph remove <node_id>

Удаление узла (hot-update)

—

JSON

graph connect <from> <to> [tag]

Добавление дуги с проверкой на цикл (hot-update). Если <from> — узел ecs (шлюз), tag добавляется в in_tags узла <to>; если <to> — ecs, tag добавляется в out_tags узла <from>

—

JSON

graph disconnect <from> <to> [tag]

Удаление дуги (hot-update). Если <from> — узел ecs, tag удаляется из in_tags узла <to>; если <to> — ecs, tag удаляется из out_tags узла <from>

—

JSON

graph show

Вывод всего графа в терминал (GraphConfig ядра + топопорядок)

—

JSON

graph status

Вывод текущего графа ядра (параметры, узлы, дуги, топопорядок)

—

Таблица

graph save <path>

Сохранение графа в JSON-файл

JSON

—

graph load <path>

Загрузка графа из JSON-файла (только остановленное ядро)

—

JSON

graph save-context <path>

Сохранение состояния графа

—

JSON

graph load-context <path>

Загрузка состояния графа

—

JSON

graph validate

Проверка ацикличности (топологическая сортировка)

JSON

Порядок узлов

Замена графа целиком из JSON-файла (graph load) и загрузка состояния (graph load-context) выполняются только на остановленном ядре.

Контекст графа (graph save-context / graph load-context) — это текущее состояние моделирования, а не сам граф: структура (узлы, дуги, параметры, коэффициенты) сохраняется отдельно (graph save/load), контекст опирается на те же id узлов.

Сохраняется (метод Graph.context_to_dict()):

  • справочно: graph_id, quantum_size, hp, precision (при загрузке не применяются);

  • для каждого узла: history — рекуррентная история y_{i-1}..y_{i-4} (deque maxlen=5, новое первым, включая под-значения тактов при q > 1) и last_output — последний рассчитанный выход yᵢ; именно эти данные восстанавливаются в узле Node.set_state();

  • source_inputs — история значений входных guid истоков из кэша симулятора guidhistory (до 3 последних решёточных значений, новое первым); по ней восстанавливается квадратичная интерполяция внешних входов при q > 1 и кэш «замороженных» входов sourcecache (при недоступности шлюза входы замирают на последних значениях, а не на нулях).

Числовые значения хранятся строками (mpf-строки): точность вычислений (30+ знаков) не помещается в float64; при загрузке история узлов конвертируется через to_mpf, история guid — через float/to_mpf.

Не сохраняется: структура графа и in_tags/out_tags, дуги, параметры и коэффициенты C1..C5 (это конфигурация — graph save/load), состояние планировщика, метрики, слепок конфигурации КС (GetAllObjectsData) и кэш пула сигналов шлюза.

Пример файла контекста:

{
  "graph_id": "graph",
  "quantum_size": 0.1,
  "hp": 0.1,
  "precision": 30,
  "nodes": {
    "src": {"history": ["0.9", "0.8", "0.7"], "last_output": "0.95"},
    "sink": {"history": ["0.85"], "last_output": "0.85"}
  },
  "source_inputs": {"gw.in.1": ["10.0", "9.5", "9.0"]}
}

Загрузка (App.load_context()): для каждого узла из nodes вызывается set_state(history, last_output); узел из контекста, отсутствующий в текущем графе, → GraphError; узел графа, отсутствующий в контексте, остаётся с нулевым состоянием. Незнакомые ключи контекста игнорируются.

2.9.2 Группа kernel

Команды группы (порт ZMQ 5555, REQ/REP):

Команда

Описание

kernel start

Запуск ядра

kernel stop

Остановка ядра

kernel restart

Перезапуск ядра: последовательно выполняет kernel stop и kernel start; доступен только в состоянии RUNNING

kernel status

Проверка статуса

kernel node-stats

Статистика узлов графа (диагностика)

2.9.3 Группа config

Команда

Описание

config get

Просмотр эффективной конфигурации (t3s.toml + params графа)

config set --zmq-control-port --quantum-size --hp --log-level --log-file --elicont-host --elicont-port --api-gateway-host --api-gateway-port --precision N

Живое обновление параметров: применяется мгновенно, пересчитывает все узлы. Параметры моделирования (quantum-size, hp, precision) записываются в params файла graph_config.json, остальные — в t3s.toml (api_gateway — адрес сервиса управления каналами; пусто/0 — как [elicont])

2.9.4 Группа logging

Команда

Описание

logging enable [--level INFO]

Включение логирования

logging disable

Отключение логирования

logging toggle [--level INFO]

Переключение логирования

logging level DEBUG\|INFO\|WARNING\|ERROR\|CRITICAL

Установка уровня логирования

logging status

Статус логирования (enabled/disabled, level, file)

2.9.5 Группа elicont

Команда

Описание

elicont status

Статус подключения к шлюзу (connected, running, signal_count, connected_channels)

elicont validate-mapping

Отложенная проверка маппинга guid и типов

elicont signals-show

Объекты конфигурации КС (gRPC GetAllObjectsData, Эликонт-КС 2.X.X). По каждому объекту выводятся: guid (8-4-4-4-12), family (RX_SIGNAL/TX_SIGNAL/RX_COMMAND/TX_COMMAND), direction (client — принимаемый (RX) / server — передаваемый (TX)), type значения и user_data. Сигналы и команды выводятся отдельными списками. Слепок перечитывается принудительно (без кэша ядра) — вывод отражает актуальную конфигурацию КС. Вывод: connected, format (objects), signal_count, signals[], command_count, commands[]


2.10 Конфигурация приложения

Параметры разделены на два файла:

  1. Конфиг ядра (config/t3s.toml) — путь к файлу графа, ZMQ, Elicont, логирование.

  2. Файл графа (GraphConfig, JSON) — узлы, дуги и параметры моделирования ядра (quantum_size, hp, precision).

Параметры моделирования указываются в params файла графа graph_config.json, а не в конфиге ядра (секция [kernel] отсутствует). Команда config set пишет изменения и применяет их мгновенно (персистентность после перезапуска): параметры моделирования — в graph_config.json, остальные — в конфиг ядра.

Основные секции конфига ядра (t3s.toml):

[graph]
# Файл графа GraphConfig (JSON): загружается при запуске ядра, hot-update
# сериализуется в этот файл. В params файла задаются quantum_size,
# hp (шаг решётчатой функции h_P) и precision (точность mpmath).
config_file = "/var/lib/t3s/graph_config.json"

[zmq]
control_port = 5555     # ZMQ REQ/REP команд

[elicont]
# Данные разделены на два gRPC-канала:
#   port         — серверный канал: приём данных от шлюза (чтение):
#                  GetState, GetAllSignals, GetSignalsByGuid, GetAllObjectsData;
#   write_port   — клиентский канал: передача данных в шлюз (запись): SetSignals.
# Пустой write_host = "" — использовать host; write_port = 0 — использовать port.
host = "127.0.0.1"
port = 29040            # серверный канал (чтение, gRPC)
write_host = ""         # пусто — хост [elicont]
write_port = 29041      # клиентский канал (запись, gRPC)
tls = false
auth_token = ""

[api_gateway]
host = ""               # пусто — хост [elicont]
port = 8080             # порт сервиса управления каналами (ApiGateway)

[logging]
enabled = true
level = "INFO"          # DEBUG|INFO|WARNING|ERROR|CRITICAL
file = "/var/log/t3s/t3s.log"
max_bytes = 10485760
backup_count = 5

Логика загрузки/сохранения и карта ключей config set описаны в src/t3s/config.py.


2.11 Жизненный цикл ядра

Жизненный цикл управляется конечным автоматом (FSM) — модуль state_machine.py.
Состояния:

STOPPED ──load-graph──▶ CONFIGURING ──start──▶ RUNNING
   ▲                        │                    │
   └─────────stop───────────┴────begin/finish────┘
                                        │
                                  PENDING_UPDATE (hot-update)
                                        │
                                   авария → ERROR

Состояние

Описание

STOPPED

Начальное состояние; ядро остановлено

CONFIGURING

Граф загружен, ядро готово к запуску

RUNNING

Идёт моделирование

PENDING_UPDATE

Промежуточное состояние при применении hot-update

ERROR

Аварийное состояние

Допустимые переходы:

  • start — из CONFIGURING (после валидации) или STOPPED при загруженном графе;

  • stop — из RUNNING, PENDING_UPDATE, ERROR;

  • restart — только из RUNNING (последовательно выполняет stop и start);

  • load-graph — только из STOPPED;

  • begin_update/finish_update — только из RUNNING (hot-update).

Graceful shutdown выполняется по сигналу SIGTERM/SIGINT в KernelApplication.


2.12 Метрики ядра

Модуль metrics.py накапливает экспериментально определяемые величины, характеризующие мощность и производительность ядра. Метрики собираются по каждому такту моделирования и возвращаются командой t3s kernel status (поле metrics, zmq_server.py).

Базовые понятия:

  • Такт (tick) — один шаг расчёта решётчатой функции по шагу hₚ. За каждый такт пересчитывается весь граф — все узлы в топологическом порядке (число рассчитанных узлов равно длине topo_order, см. simulation.py).

  • Квант (quantum_size) — номинальный период запуска расчёта. При скважности q = quantum_size / hₚ в рамках одного кванта выполняется q тактов; метрики обновляются в конце каждого такта (metrics.py).

  • Все метрики — накопительные с момента запуска ядра (не сбрасываются при stop/start моделирования).

Перечень метрик:

Метрика

Тип

Описание

tick_count

int

Число выполненных тактов; увеличивается на q за каждый квант

skipped_ticks

int

Число пропущенных тактов из-за переполнения кванта (расчёт не уложился в номинал)

total_compute_time

float

Суммарное «чистое» время расчётов, сек (без пауз ожидания следующего кванта)

last_tick_duration

float

Длительность последнего такта, сек

avg_tick_duration

float

Средняя длительность такта: total_compute_time / tick_count

max_tick_duration

float

Максимальная длительность такта за сессию, сек

simulated_time

float

Накопленное модельное время, сек (увеличивается на q · hₚ за квант)

peak_nodes_per_tick

int

Пиковое (максимальное) число узлов, рассчитанных за один такт

throughput

float

Производительность: оценка числа узлов в секунду (см. ниже)

Мощность (peak_nodes_per_tick):

  • Пиковое число узлов графа, рассчитанных за такт, накопленное за всю сессию (не среднее и не мгновенное значение). Начальное значение — 0; в конце каждого такта выполняется peak_nodes_per_tick = max(peak_nodes_per_tick, nodes_computed) (metrics.py).

  • Поскольку за каждый такт пересчитывается весь граф, для рабочего графа этот параметр обычно равен суммарному числу узлов графа и отражает его «мощность». Например, для цепочки из 500 узлов (k1…k500) значение будет 500 уже после первого такта.

  • Используется как индикатор масштаба моделирования, а не скорости расчёта.

Производительность:

  • Время такта — фактическая длительность расчёта всех узлов: last_tick_duration (последний такт), avg_tick_duration (среднее за сессию), max_tick_duration (худший случай).

  • throughput — пропускная способность в узлах/сек: throughput = peak_nodes_per_tick · tick_count / total_compute_time (metrics.py). Показывает, сколько узлов ядро могло бы пересчитать за секунду, если бы всё время занимал только расчёт (без ожидания кванта). Возвращает null, если расчётов ещё не было (total_compute_time == 0).

  • На уровне лога DEBUG длительность и число узлов каждого такта пишутся в журнал (tick %d computed: %d nodes, q=%d, см. simulation.py).

Пропуск тактов (skipped_ticks) — если расчёт не уложился в номинальный квант, такт помечается пропущенным (переполнение кванта): логируется WARNING tick skipped: compute overran quantum (scheduler.py), счётчик skipped_ticks увеличивается (metrics.py), а следующий запуск планируется с учётом фактического переполнения — номинальная скважность q автоматически растёт (см. § «Планировщик»). Рост skipped_ticks при постоянной нагрузке — признак того, что ядро не успевает моделировать в реальном времени.


2.13 Логирование

Система логирования построена на стандартном модуле logging (Python) и пишет записи в файл через RotatingFileHandler с ротацией по размеру. Дополнительно ведётся консольный обработчик (StreamHandler) для диагностики. Настройка выполняется при запуске ядра в logging_setup.py и может изменяться на лету через команды группы logging CLI.
Поддерживаются пять стандартных уровней логирования (по возрастанию серьёзности): DEBUG < INFO < WARNING < ERROR < CRITICAL. Обработчики пропускают записи ниже текущего установленного уровня, поэтому выбор уровня определяет детальность журнала и его объём.

Уровень

Назначение

Примеры событий в t3s

DEBUG

Подробная диагностика для разработки и отладки; самый большой объём записей

Завершение каждого такта: число рассчитанных узлов и q (tick %d computed: %d nodes, q=%d, см. simulation.py)

INFO

Штатные события жизненного цикла; рекомендуемый уровень для обычной эксплуатации

Запуск и остановка ядра, загрузка графа, применение конфигурации, установка связи со шлюзом, старт ZMQ- и gRPC-серверов управления (t3s kernel starting…, kernel started, connected to Elicont…)

WARNING

Нештатные, но не фатальные ситуации, требующие внимания; расчёт продолжается

Пропуск такта из-за переполнения кванта (tick skipped: compute overran quantum, см. scheduler.py), принудительное q = 1 при q < 1, недоступность шлюза при старте (истоки получают нули), невозможность получить входы, откат на конфиг по умолчанию, невозможность открыть файл лога

ERROR

Ошибки, нарушающие штатную работу, но не останавливающие процесс

Сбой расчёта такта (tick %d failed), таймаут/отказ подключения к Elicont, ошибка публикации значений стоков

CRITICAL

Критические сбои, после которых ядро не может корректно работать

Зарезервировано для фатальных ошибок процесса (в текущей версии не выдаётся)

Примечание о производительности: на уровне DEBUG журнал растёт максимально быстро из-за записи каждого такта; для длительной промышленной эксплуатации рекомендуется INFO, а DEBUG — включать временно для разбора проблем.

3. Тестирование

3.1 Тестирование с визуализацией

Начальное тестирование проводилось на графе из трёх последовательно соединенных узлов, первый из которых выполнял функции истока — получал данные по gRPC из шлюза, при этом все узлы передавали данные по gRPC в шлюз:

        ┌─────────────┐
        │ Elicont-CS  │
        └──┬───────▲──┘
    gRPC   │       │  gRPC (значения k1, k2, k3)
  (источник│       │
   данных) ▼       │
        ┌─────────────────────────────┐
        │            t3s              │
        │   k1 ────▶ k2 ────▶ k3      │
        └─────────────────────────────┘

Шлюз по MQTT был подключен к MQTT Explorer:

┌───────────────┐   MQTT   ┌─────────────┐   gRPC   ┌──────┐
│ MQTT Explorer │ ◀──────▶ │ Elicont-CS  │ ◀──────▶ │ t3s  │
└───────────────┘          └─────────────┘          └──────┘

Окно конфигуратора Эликонт-КС:

Результаты тестирования с визуализацией в MQTT Explorer:

Метрики ядра для 3 узлов:

3.2 Тестирование производительности

Метрики ядра для 10 узлов:

Метрики ядра для 100 узлов:

Метрики ядра для 500 узлов:

Результаты приведённых тестов показывают, что табакерка уверенно просчитывает 500 узлов моделирования в пределах 100 мс, т. е. десять раз в секунду рассчитывает значения для пятисот технологических параметров.
Учитывая возможности Эликонт-КС по преобразованию протоколов передачи данных, моделируемые t3s данные могут передаваться приёмникам по какому-нибудь промышленному протоколу — Modbus RTU/TCP, OPC UA и т. д.

4. Заключение

Цель разработки t3s — показать, что применение математического аппарата, описанного в предыдущей статье, даёт хороший результат по скорости при моделировании временных рядов технологических параметров в режиме реального времени на вычислительных устройствах скромной мощности. Поставленная цель, на мой взгляд, достигнута.
Однако самым поразительным для меня в этой небольшой работе стал и остаётся факт того, что разработку приложения выполнил ИИ-агент от Яндекса — SourceCraft Assistant. Моя работа свелась к подготовке ТЗ на разработку t3s. ТЗ прошло несколько итераций корректировок и дополнений, поскольку SC Assistant как дотошный душнила задавал вопросы, ответы на которые расширяли ТЗ чёткими и нужными требованиями, а противоречия исключались. При этом была пара моментов, когда мы с ним дискутировали и мне требовалось аргументированно убеждать его в своей правоте. Всю основную работу — начиная от разработки архитектуры приложения, дерева файлов, репозитория, схемы импорта и до написания самих текстов файлов, подробного описания приложения, прогонки автоматических юнит- и интеграционных тестов — выполнил SC Assistant. Будущее наступило, просто мы пока этого ещё не заметили.

Комментарии (3)


  1. Imaginarium
    04.11.2026 08:55

    Издевательство с этим нейрослопом, конечно. О чем статья – совершенно непонятно. Почему мы должны читать собранную в один документ кучу выхлопа ИИ, с помощью которого он пытается рассуждать – тоже неясно.


    1. StasTukalo
      04.11.2026 08:55

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


  1. slavic-voronezh
    04.11.2026 08:55

    Почему используются такие глобальные термины как ядро, узлы и т.д. Метрики узлов ни о чём не говорят. Почему зациклились только на определённом формате вещественных чисел. Как временной ряд соотносится с графом узлов сколько заняла работа по времени и сколько токенов утилизировано. Внутри графа используются математические функции в слоях, если используются то зачем? Зачем вообще чего то там моделировать? Смысл вашей работы получить фиктивный результат (от виртуального срабатывания датчика) вместо аппаратного сигнала?