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} равны нулям.
В текущей реализации поддерживаются четыре типа звеньев:
Тип |
Звено |
Параметры |
|---|---|---|
|
Апериодическое звено 1-го порядка |
k — коэффициент усиления, T — время интегрирования |
|
Апериодическое звено 2-го порядка |
k, T1 — первое время интегрирования, T2 — второе время интегрирования |
|
Колебательное звено |
k, T — время интегрирования, xi — коэффициент колебательности |
|
Консервативное звено |
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), поля верхнего уровня:
Поле |
Тип |
Описание |
|---|---|---|
|
|
Идентификатор графа. |
|
|
Параметры моделирования: |
|
|
Массив узлов моделирования (см. схему узла ниже). |
|
|
Рёбра графа — список двухэлементных списков |
Схема элемента массива nodes (узел моделирования):
Поле |
Тип |
Описание |
|---|---|---|
|
|
Уникальный идентификатор узла. |
|
|
Тип типового динамического звена: |
|
|
Коэффициент усиления. |
|
|
Время интегрирования (для |
|
|
Первое время интегрирования (для |
|
|
Второе время интегрирования (для |
|
|
Коэффициент колебательности (для |
|
|
Коэффициенты |
|
|
Теги (guid) сигналов Elicont, подаваемых на вход узла (обязателен непустой для истока). |
|
|
Теги (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 |
|---|---|---|---|
|
Добавление узла в граф ядра (hot-update) |
— |
JSON |
|
Корректировка параметров узла (hot-update) |
— |
JSON |
|
Удаление узла (hot-update) |
— |
JSON |
|
Добавление дуги с проверкой на цикл (hot-update). Если |
— |
JSON |
|
Удаление дуги (hot-update). Если |
— |
JSON |
|
Вывод всего графа в терминал (GraphConfig ядра + топопорядок) |
— |
JSON |
|
Вывод текущего графа ядра (параметры, узлы, дуги, топопорядок) |
— |
Таблица |
|
Сохранение графа в JSON-файл |
JSON |
— |
|
Загрузка графа из JSON-файла (только остановленное ядро) |
— |
JSON |
|
Сохранение состояния графа |
— |
JSON |
|
Загрузка состояния графа |
— |
JSON |
|
Проверка ацикличности (топологическая сортировка) |
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):
Команда |
Описание |
|---|---|
|
Запуск ядра |
|
Остановка ядра |
|
Перезапуск ядра: последовательно выполняет |
|
Проверка статуса |
|
Статистика узлов графа (диагностика) |
2.9.3 Группа config
Команда |
Описание |
|---|---|
|
Просмотр эффективной конфигурации (t3s.toml + params графа) |
|
Живое обновление параметров: применяется мгновенно, пересчитывает все узлы. Параметры моделирования ( |
2.9.4 Группа logging
Команда |
Описание |
|---|---|
|
Включение логирования |
|
Отключение логирования |
|
Переключение логирования |
|
Установка уровня логирования |
|
Статус логирования (enabled/disabled, level, file) |
2.9.5 Группа elicont
Команда |
Описание |
|---|---|
|
Статус подключения к шлюзу (connected, running, signal_count, connected_channels) |
|
Отложенная проверка маппинга guid и типов |
|
Объекты конфигурации КС (gRPC |
2.10 Конфигурация приложения
Параметры разделены на два файла:
Конфиг ядра (
config/t3s.toml) — путь к файлу графа, ZMQ, Elicont, логирование.Файл графа (
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
Состояние |
Описание |
|---|---|
|
Начальное состояние; ядро остановлено |
|
Граф загружен, ядро готово к запуску |
|
Идёт моделирование |
|
Промежуточное состояние при применении hot-update |
|
Аварийное состояние |
Допустимые переходы:
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моделирования).
Перечень метрик:
Метрика |
Тип |
Описание |
|---|---|---|
|
int |
Число выполненных тактов; увеличивается на q за каждый квант |
|
int |
Число пропущенных тактов из-за переполнения кванта (расчёт не уложился в номинал) |
|
float |
Суммарное «чистое» время расчётов, сек (без пауз ожидания следующего кванта) |
|
float |
Длительность последнего такта, сек |
|
float |
Средняя длительность такта: total_compute_time / tick_count |
|
float |
Максимальная длительность такта за сессию, сек |
|
float |
Накопленное модельное время, сек (увеличивается на q · hₚ за квант) |
|
int |
Пиковое (максимальное) число узлов, рассчитанных за один такт |
|
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 ( |
INFO |
Штатные события жизненного цикла; рекомендуемый уровень для обычной эксплуатации |
Запуск и остановка ядра, загрузка графа, применение конфигурации, установка связи со шлюзом, старт ZMQ- и gRPC-серверов управления ( |
WARNING |
Нештатные, но не фатальные ситуации, требующие внимания; расчёт продолжается |
Пропуск такта из-за переполнения кванта ( |
ERROR |
Ошибки, нарушающие штатную работу, но не останавливающие процесс |
Сбой расчёта такта ( |
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)

slavic-voronezh
04.11.2026 08:55Почему используются такие глобальные термины как ядро, узлы и т.д. Метрики узлов ни о чём не говорят. Почему зациклились только на определённом формате вещественных чисел. Как временной ряд соотносится с графом узлов сколько заняла работа по времени и сколько токенов утилизировано. Внутри графа используются математические функции в слоях, если используются то зачем? Зачем вообще чего то там моделировать? Смысл вашей работы получить фиктивный результат (от виртуального срабатывания датчика) вместо аппаратного сигнала?
Imaginarium
Издевательство с этим нейрослопом, конечно. О чем статья – совершенно непонятно. Почему мы должны читать собранную в один документ кучу выхлопа ИИ, с помощью которого он пытается рассуждать – тоже неясно.
StasTukalo
полностью согласен. неуважение к читателям- публиковать подобные мега-портянки ни о чем.