spacexerq 1 日 前
コミット
f301eb0ca6

+ 76 - 13
apps/gui/src/core/synchronizer.py

@@ -1,3 +1,5 @@
+import math
+
 from src.hardware.constraints import HardwareConstraints
 
 
@@ -13,7 +15,16 @@ class Synchronizer:
         synchro_block_timer = self.hw.MIN_BLOCK_DURATION
         tr_delay = self.hw.TR_DELAY
         rf_delay = self.hw.RF_DELAY
-        start_delay = max(self.hw.START_DELAY, self.hw.RF_DELAY)
+        # Стартовый блок (индекс 0) не эмитится в XML — его представляет
+        # заголовок — но синхронизатору он нужен как ненулевой буфер для
+        # вычитаний задержек и как смещение начала ADC. Поэтому выключенный
+        # START_DELAY = минимальный блок (20 тактов), а не ноль (синхронизатор
+        # не умеет работать с нулевыми длительностями).
+        min_event_duration = 20 * synchro_block_timer
+        if self.hw.START_DELAY_ENABLED:
+            start_delay = max(self.hw.START_DELAY, self.hw.RF_DELAY)
+        else:
+            start_delay = min_event_duration
 
         min_block_time = 800e-9
 
@@ -35,12 +46,15 @@ class Synchronizer:
             if sync_sequence.block_events[block_counter + 1][5]:
                 is_not_adc_block = False
 
-                gate_adc.append(0)
-                gate_rf.append(gate_rf[-1])
-                blocks_duration[-1] -= tr_delay
-                blocks_duration.append(tr_delay)
-                gate_tr_switch.append(0)
-                added_blocks += 1
+                # Блок задержки TR вставляется только если включён (иначе
+                # давал артефакт в 1 такт при TR_DELAY = 20 нс).
+                if self.hw.TR_DELAY_ENABLED:
+                    gate_adc.append(0)
+                    gate_rf.append(gate_rf[-1])
+                    blocks_duration[-1] -= tr_delay
+                    blocks_duration.append(tr_delay)
+                    gate_tr_switch.append(0)
+                    added_blocks += 1
 
                 gate_adc.append(1)
                 gate_tr_switch.append(0)
@@ -49,12 +63,15 @@ class Synchronizer:
                 gate_adc.append(0)
 
             if sync_sequence.block_events[block_counter + 1][1] and is_not_adc_block:
-                gate_rf.append(1)
-                gate_adc.append(gate_adc[-1])
-                blocks_duration[-1] -= rf_delay
-                blocks_duration.append(rf_delay)
-                gate_tr_switch.append(gate_tr_switch[-1])
-                added_blocks += 1
+                # Блок задержки RF вставляется только если включён (иначе
+                # вычитание rf_delay могло обнулить стартовый блок -> 0 тактов).
+                if self.hw.RF_DELAY_ENABLED:
+                    gate_rf.append(1)
+                    gate_adc.append(gate_adc[-1])
+                    blocks_duration[-1] -= rf_delay
+                    blocks_duration.append(rf_delay)
+                    gate_tr_switch.append(gate_tr_switch[-1])
+                    added_blocks += 1
 
                 gate_rf.append(1)
             else:
@@ -65,6 +82,52 @@ class Synchronizer:
 
         number_of_blocks += added_blocks
 
+        # Минимальная длительность события в синхро-последовательности
+        # ограничена 20 тактами (влияет на реальную длительность блоков).
+        # Удлинение коротких блоков компенсируется за счёт соседнего блока,
+        # чтобы сохранить общую длительность последовательности (TR).
+        # Донором предпочитаем блок без ADC, чтобы не урезать окно ADC.
+        n_dur = len(blocks_duration)
+        for i in range(n_dur):
+            if blocks_duration[i] < min_event_duration:
+                deficit = min_event_duration - blocks_duration[i]
+                blocks_duration[i] = min_event_duration
+                neighbors = [j for j in (i - 1, i + 1) if 0 <= j < n_dur]
+                non_adc = [j for j in neighbors if gate_adc[j] == 0]
+                pool = non_adc if non_adc else neighbors
+                donor = max(pool, key=lambda j: blocks_duration[j])
+                blocks_duration[donor] -= deficit
+
+        # Максимальная длительность блока ограничена 999999 тактами.
+        # Блоки длиннее разбиваются на равные части (уровни гейтов
+        # дублируются на каждую часть, суммарная длительность сохраняется).
+        max_event_duration = 999999 * synchro_block_timer
+        split_blocks_duration = []
+        split_gate_adc = []
+        split_gate_rf = []
+        split_gate_tr_switch = []
+        for i in range(len(blocks_duration)):
+            dur = blocks_duration[i]
+            if dur > max_event_duration:
+                n_parts = math.ceil(dur / max_event_duration)
+                part_dur = dur / n_parts
+            else:
+                n_parts = 1
+                part_dur = dur
+            for _ in range(n_parts):
+                split_blocks_duration.append(part_dur)
+                # Уровни гейтов (в т.ч. ADC HIGH) дублируются на все части:
+                # разбитый блок остаётся непрерывным HIGH, триггер не дёргается.
+                split_gate_adc.append(gate_adc[i])
+                split_gate_rf.append(gate_rf[i])
+                split_gate_tr_switch.append(gate_tr_switch[i])
+
+        number_of_blocks += len(split_blocks_duration) - len(blocks_duration)
+        blocks_duration = split_blocks_duration
+        gate_adc = split_gate_adc
+        gate_rf = split_gate_rf
+        gate_tr_switch = split_gate_tr_switch
+
         return {
             "number_of_blocks": number_of_blocks,
             "gate_adc": gate_adc,

+ 57 - 4
apps/gui/src/gui/controls_panel.py

@@ -11,6 +11,7 @@ from src.gui.tr_widgets import TrGroupBox, TrPushButton
 from PySide6.QtWidgets import (
     QWidget, QVBoxLayout, QFormLayout,
     QDoubleSpinBox, QGridLayout, QFileDialog,
+    QCheckBox, QHBoxLayout,
 )
 
 
@@ -26,6 +27,14 @@ _FIELDS = [
     ("block_duration_raster","Block Raster",     "uss",  1e6,  0.001, 100.0, 0.01),
 ]
 
+# Поля задержек с галочкой "вставлять блок": {attr поля: attr флага в hw}.
+# Снятая галочка убирает вставку блока задержки (нет артефактов 1/0 тактов).
+_ENABLE_FLAGS = {
+    "RF_DELAY":    "RF_DELAY_ENABLED",
+    "TR_DELAY":    "TR_DELAY_ENABLED",
+    "START_DELAY": "START_DELAY_ENABLED",
+}
+
 
 class DelayControlsPanel(QWidget):
     rerun = Signal()        # user clicked "Apply & Rerun"
@@ -43,6 +52,8 @@ class DelayControlsPanel(QWidget):
 
         self._spinboxes: dict[str, tuple[QDoubleSpinBox, float]] = {}
         self._defaults: dict[str, float] = {}
+        self._checkboxes: dict[str, tuple[QCheckBox, str]] = {}
+        self._enable_defaults: dict[str, bool] = {}
 
         for attr, label, unit, scale, mn, mx, step in _FIELDS:
             sb = QDoubleSpinBox()
@@ -52,9 +63,28 @@ class DelayControlsPanel(QWidget):
             sb.setSingleStep(step)
             sb.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
             sb.valueChanged.connect(self._mark_modified)
-            form.addRow(f"{label}:", sb)
             self._spinboxes[attr] = (sb, scale)
 
+            flag_attr = _ENABLE_FLAGS.get(attr)
+            if flag_attr:
+                cb = QCheckBox()
+                cb.setChecked(True)
+                cb.setToolTip(
+                    "Вставлять блок этой задержки. Снято — блок не вставляется "
+                    "(убирает артефакты 1/0 тактов)."
+                )
+                cb.toggled.connect(self._on_enable_toggled)
+                self._checkboxes[attr] = (cb, flag_attr)
+                row = QWidget()
+                row_lay = QHBoxLayout(row)
+                row_lay.setContentsMargins(0, 0, 0, 0)
+                row_lay.setSpacing(4)
+                row_lay.addWidget(sb, stretch=1)
+                row_lay.addWidget(cb)
+                form.addRow(f"{label}:", row)
+            else:
+                form.addRow(f"{label}:", sb)
+
         outer.addWidget(self._grp)
 
         # 2x2 grid: prevents text overflow on narrow left panel (min 220 px)
@@ -88,11 +118,21 @@ class DelayControlsPanel(QWidget):
             sb.setValue(val * scale)
             sb.blockSignals(False)
             sb.setStyleSheet("")
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            enabled = bool(getattr(hw, flag_attr, True))
+            self._enable_defaults[attr] = enabled
+            cb.blockSignals(True)
+            cb.setChecked(enabled)
+            cb.blockSignals(False)
+            self._spinboxes[attr][0].setEnabled(enabled)
 
     def get_overrides(self) -> dict:
-        """Return dict of {attr: value_in_SI_units}."""
-        return {attr: sb.value() / scale
-                for attr, (sb, scale) in self._spinboxes.items()}
+        """Return dict of {attr: value} in SI units plus *_ENABLED bool flags."""
+        overrides = {attr: sb.value() / scale
+                     for attr, (sb, scale) in self._spinboxes.items()}
+        for cb, flag_attr in self._checkboxes.values():
+            overrides[flag_attr] = cb.isChecked()
+        return overrides
 
     # ------------------------------------------------------------------
     # Private helpers
@@ -112,6 +152,13 @@ class DelayControlsPanel(QWidget):
                     sb.setStyleSheet("")
                 break
 
+    def _on_enable_toggled(self) -> None:
+        sender = self.sender()
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            if cb is sender:
+                self._spinboxes[attr][0].setEnabled(cb.isChecked())
+                break
+
     def _on_reset(self) -> None:
         for attr, (sb, scale) in self._spinboxes.items():
             default = self._defaults.get(attr)
@@ -120,6 +167,12 @@ class DelayControlsPanel(QWidget):
                 sb.setValue(default * scale)
                 sb.blockSignals(False)
                 sb.setStyleSheet("")
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            default = self._enable_defaults.get(attr, True)
+            cb.blockSignals(True)
+            cb.setChecked(default)
+            cb.blockSignals(False)
+            self._spinboxes[attr][0].setEnabled(default)
 
     def _on_save(self) -> None:
         path, _ = QFileDialog.getSaveFileName(

+ 11 - 0
apps/gui/src/hardware/constraints.py

@@ -27,6 +27,13 @@ class HardwareConstraints:
         self.MIN_BLOCK_DURATION = 20e-9  # сек, минимальная длительность блока (квант времени последовательности)
         self.GRAD_DELAY = 1000e-9
 
+        # Флаги вставки задержек в синхро-последовательность.
+        # Если флаг False — соответствующий блок задержки не вставляется
+        # (и не вычитается из соседнего), что убирает артефакты 1/0 тактов.
+        self.TR_DELAY_ENABLED = True
+        self.RF_DELAY_ENABLED = True
+        self.START_DELAY_ENABLED = True
+
         # Максимальные амплитуды
         self.RF_MAX = 1.0  # относительная макс. амплитуда RF (нормирована на 1.0)
         self.GRAD_MAX = 9e-3 * self.gamma  # макс. градиент (Гц/м) по умолчанию 9 mT/m * gamma
@@ -60,6 +67,10 @@ class HardwareConstraints:
         else:
             self.START_DELAY = self.MIN_BLOCK_DURATION * 10
         self.MIN_BLOCK_DURATION = data.get("MIN_BLOCK_DURATION", self.MIN_BLOCK_DURATION)
+        # Флаги вставки задержек
+        self.TR_DELAY_ENABLED = data.get("TR_DELAY_ENABLED", self.TR_DELAY_ENABLED)
+        self.RF_DELAY_ENABLED = data.get("RF_DELAY_ENABLED", self.RF_DELAY_ENABLED)
+        self.START_DELAY_ENABLED = data.get("START_DELAY_ENABLED", self.START_DELAY_ENABLED)
         # Обновление максимальных амплитуд (если указаны)
         self.RF_MAX = data.get("RF_MAX", self.RF_MAX)
         self.GRAD_MAX = data.get("GRAD_MAX", self.GRAD_MAX)

+ 30 - 8
apps/gui/src/interfaces/xml_generator.py

@@ -24,43 +24,65 @@ class XMLGenerator:
             adc_times_values = []
             adc_times_starts = []
 
+            # Массивы синхронизатора имеют ведущий seed-элемент на индексе 0
+            # (стартовый блок), значимые блоки идут по индексам 1..nb. Seed
+            # представлен заголовочным тегом (*1), поэтому в цикле эмитим
+            # элементы 1..nb: block_iter k -> индекс массива k + 1.
+            # (Совпадает с эталонной логикой srv_interp.synchronization.)
+
             with tag("RF"):
                 with tag("RF1"):
                     text(0)
                 for rf_iter in range(number_of_blocks):
                     with tag("RF" + str(rf_iter + 2)):
-                        text(gate_rf[rf_iter])
+                        text(gate_rf[rf_iter + 1])
 
             with tag("SW"):
                 with tag("SW1"):
                     text(1)
                 for sw_iter in range(number_of_blocks):
                     with tag("SW" + str(sw_iter + 2)):
-                        text(gate_tr_switch[sw_iter])
+                        text(gate_tr_switch[sw_iter + 1])
 
             with tag("ADC"):
                 with tag("ADC1"):
                     text(0)
                 for adc_iter in range(number_of_blocks):
-                    if gate_adc[adc_iter] == 1:
-                        adc_times_values.append(blocks_duration[adc_iter])
-                        adc_times_starts.append(sum(blocks_duration[0:adc_iter]))
+                    idx = adc_iter + 1
+                    # Непрерывный участок HIGH (в т.ч. собранный из разбитых
+                    # блоков) считается одним событием ADC: засекаем фронт и
+                    # суммируем длительность всего участка для points-окна.
+                    is_rising_edge = (
+                        gate_adc[idx] == 1 and gate_adc[idx - 1] == 0
+                    )
+                    if is_rising_edge:
+                        adc_times_starts.append(sum(blocks_duration[0:idx]))
+                        run_duration = 0
+                        run_iter = idx
+                        while (run_iter < len(gate_adc)
+                               and gate_adc[run_iter] == 1):
+                            run_duration += blocks_duration[run_iter]
+                            run_iter += 1
+                        adc_times_values.append(run_duration)
                     with tag("ADC" + str(adc_iter + 2)):
-                        text(gate_adc[adc_iter])
+                        text(gate_adc[idx])
 
             with tag("GR"):
                 with tag("GR1"):
                     text(1)
                 for gr_iter in range(number_of_blocks):
+                    # Последнее событие импульсной последовательности
+                    # переводим из LOW в HIGH (включение градиентной системы)
+                    gr_value = 1 if gr_iter == number_of_blocks - 1 else 0
                     with tag("GR" + str(gr_iter + 2)):
-                        text(0)
+                        text(gr_value)
 
             with tag("CL"):
                 with tag("CL1"):
                     text(int(min_block_time / synchro_block_timer))
                 for cl_iter in range(number_of_blocks):
                     with tag("CL" + str(cl_iter + 2)):
-                        text(int(blocks_duration[cl_iter] / synchro_block_timer))
+                        text(int(blocks_duration[cl_iter + 1] / synchro_block_timer))
 
         xml_string = indent(doc.getvalue(), indentation=" " * 4, newline="\r")
         with open(path, "w", encoding="utf-8") as f:

+ 76 - 13
services/seq-interp/src/core/synchronizer.py

@@ -1,3 +1,5 @@
+import math
+
 from seq_interp.src.hardware.constraints import HardwareConstraints
 
 
@@ -13,7 +15,16 @@ class Synchronizer:
         synchro_block_timer = self.hw.MIN_BLOCK_DURATION
         tr_delay = self.hw.TR_DELAY
         rf_delay = self.hw.RF_DELAY
-        start_delay = max(self.hw.START_DELAY, self.hw.RF_DELAY)
+        # Стартовый блок (индекс 0) не эмитится в XML — его представляет
+        # заголовок — но синхронизатору он нужен как ненулевой буфер для
+        # вычитаний задержек и как смещение начала ADC. Поэтому выключенный
+        # START_DELAY = минимальный блок (20 тактов), а не ноль (синхронизатор
+        # не умеет работать с нулевыми длительностями).
+        min_event_duration = 20 * synchro_block_timer
+        if self.hw.START_DELAY_ENABLED:
+            start_delay = max(self.hw.START_DELAY, self.hw.RF_DELAY)
+        else:
+            start_delay = min_event_duration
 
         min_block_time = 800e-9
 
@@ -35,12 +46,15 @@ class Synchronizer:
             if sync_sequence.block_events[block_counter + 1][5]:
                 is_not_adc_block = False
 
-                gate_adc.append(0)
-                gate_rf.append(gate_rf[-1])
-                blocks_duration[-1] -= tr_delay
-                blocks_duration.append(tr_delay)
-                gate_tr_switch.append(0)
-                added_blocks += 1
+                # Блок задержки TR вставляется только если включён (иначе
+                # давал артефакт в 1 такт при TR_DELAY = 20 нс).
+                if self.hw.TR_DELAY_ENABLED:
+                    gate_adc.append(0)
+                    gate_rf.append(gate_rf[-1])
+                    blocks_duration[-1] -= tr_delay
+                    blocks_duration.append(tr_delay)
+                    gate_tr_switch.append(0)
+                    added_blocks += 1
 
                 gate_adc.append(1)
                 gate_tr_switch.append(0)
@@ -49,12 +63,15 @@ class Synchronizer:
                 gate_adc.append(0)
 
             if sync_sequence.block_events[block_counter + 1][1] and is_not_adc_block:
-                gate_rf.append(1)
-                gate_adc.append(gate_adc[-1])
-                blocks_duration[-1] -= rf_delay
-                blocks_duration.append(rf_delay)
-                gate_tr_switch.append(gate_tr_switch[-1])
-                added_blocks += 1
+                # Блок задержки RF вставляется только если включён (иначе
+                # вычитание rf_delay могло обнулить стартовый блок -> 0 тактов).
+                if self.hw.RF_DELAY_ENABLED:
+                    gate_rf.append(1)
+                    gate_adc.append(gate_adc[-1])
+                    blocks_duration[-1] -= rf_delay
+                    blocks_duration.append(rf_delay)
+                    gate_tr_switch.append(gate_tr_switch[-1])
+                    added_blocks += 1
 
                 gate_rf.append(1)
             else:
@@ -65,6 +82,52 @@ class Synchronizer:
 
         number_of_blocks += added_blocks
 
+        # Минимальная длительность события в синхро-последовательности
+        # ограничена 20 тактами (влияет на реальную длительность блоков).
+        # Удлинение коротких блоков компенсируется за счёт соседнего блока,
+        # чтобы сохранить общую длительность последовательности (TR).
+        # Донором предпочитаем блок без ADC, чтобы не урезать окно ADC.
+        n_dur = len(blocks_duration)
+        for i in range(n_dur):
+            if blocks_duration[i] < min_event_duration:
+                deficit = min_event_duration - blocks_duration[i]
+                blocks_duration[i] = min_event_duration
+                neighbors = [j for j in (i - 1, i + 1) if 0 <= j < n_dur]
+                non_adc = [j for j in neighbors if gate_adc[j] == 0]
+                pool = non_adc if non_adc else neighbors
+                donor = max(pool, key=lambda j: blocks_duration[j])
+                blocks_duration[donor] -= deficit
+
+        # Максимальная длительность блока ограничена 999999 тактами.
+        # Блоки длиннее разбиваются на равные части (уровни гейтов
+        # дублируются на каждую часть, суммарная длительность сохраняется).
+        max_event_duration = 999999 * synchro_block_timer
+        split_blocks_duration = []
+        split_gate_adc = []
+        split_gate_rf = []
+        split_gate_tr_switch = []
+        for i in range(len(blocks_duration)):
+            dur = blocks_duration[i]
+            if dur > max_event_duration:
+                n_parts = math.ceil(dur / max_event_duration)
+                part_dur = dur / n_parts
+            else:
+                n_parts = 1
+                part_dur = dur
+            for _ in range(n_parts):
+                split_blocks_duration.append(part_dur)
+                # Уровни гейтов (в т.ч. ADC HIGH) дублируются на все части:
+                # разбитый блок остаётся непрерывным HIGH, триггер не дёргается.
+                split_gate_adc.append(gate_adc[i])
+                split_gate_rf.append(gate_rf[i])
+                split_gate_tr_switch.append(gate_tr_switch[i])
+
+        number_of_blocks += len(split_blocks_duration) - len(blocks_duration)
+        blocks_duration = split_blocks_duration
+        gate_adc = split_gate_adc
+        gate_rf = split_gate_rf
+        gate_tr_switch = split_gate_tr_switch
+
         return {
             "number_of_blocks": number_of_blocks,
             "gate_adc": gate_adc,

+ 57 - 4
services/seq-interp/src/gui/controls_panel.py

@@ -10,6 +10,7 @@ from seq_interp.src.gui.scheme_panel import system_is_dark
 from PySide6.QtWidgets import (
     QWidget, QVBoxLayout, QGroupBox, QFormLayout,
     QDoubleSpinBox, QPushButton, QGridLayout, QFileDialog,
+    QCheckBox, QHBoxLayout,
 )
 
 
@@ -25,6 +26,14 @@ _FIELDS = [
     ("block_duration_raster","Block Raster",     "uss",  1e6,  0.001, 100.0, 0.01),
 ]
 
+# Поля задержек с галочкой "вставлять блок": {attr поля: attr флага в hw}.
+# Снятая галочка убирает вставку блока задержки (нет артефактов 1/0 тактов).
+_ENABLE_FLAGS = {
+    "RF_DELAY":    "RF_DELAY_ENABLED",
+    "TR_DELAY":    "TR_DELAY_ENABLED",
+    "START_DELAY": "START_DELAY_ENABLED",
+}
+
 
 class DelayControlsPanel(QWidget):
     rerun = Signal()        # user clicked "Apply & Rerun"
@@ -42,6 +51,8 @@ class DelayControlsPanel(QWidget):
 
         self._spinboxes: dict[str, tuple[QDoubleSpinBox, float]] = {}
         self._defaults: dict[str, float] = {}
+        self._checkboxes: dict[str, tuple[QCheckBox, str]] = {}
+        self._enable_defaults: dict[str, bool] = {}
 
         for attr, label, unit, scale, mn, mx, step in _FIELDS:
             sb = QDoubleSpinBox()
@@ -51,9 +62,28 @@ class DelayControlsPanel(QWidget):
             sb.setSingleStep(step)
             sb.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
             sb.valueChanged.connect(self._mark_modified)
-            form.addRow(f"{label}:", sb)
             self._spinboxes[attr] = (sb, scale)
 
+            flag_attr = _ENABLE_FLAGS.get(attr)
+            if flag_attr:
+                cb = QCheckBox()
+                cb.setChecked(True)
+                cb.setToolTip(
+                    "Вставлять блок этой задержки. Снято — блок не вставляется "
+                    "(убирает артефакты 1/0 тактов)."
+                )
+                cb.toggled.connect(self._on_enable_toggled)
+                self._checkboxes[attr] = (cb, flag_attr)
+                row = QWidget()
+                row_lay = QHBoxLayout(row)
+                row_lay.setContentsMargins(0, 0, 0, 0)
+                row_lay.setSpacing(4)
+                row_lay.addWidget(sb, stretch=1)
+                row_lay.addWidget(cb)
+                form.addRow(f"{label}:", row)
+            else:
+                form.addRow(f"{label}:", sb)
+
         outer.addWidget(grp)
 
         # 2x2 grid: prevents text overflow on narrow left panel (min 220 px)
@@ -87,11 +117,21 @@ class DelayControlsPanel(QWidget):
             sb.setValue(val * scale)
             sb.blockSignals(False)
             sb.setStyleSheet("")
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            enabled = bool(getattr(hw, flag_attr, True))
+            self._enable_defaults[attr] = enabled
+            cb.blockSignals(True)
+            cb.setChecked(enabled)
+            cb.blockSignals(False)
+            self._spinboxes[attr][0].setEnabled(enabled)
 
     def get_overrides(self) -> dict:
-        """Return dict of {attr: value_in_SI_units}."""
-        return {attr: sb.value() / scale
-                for attr, (sb, scale) in self._spinboxes.items()}
+        """Return dict of {attr: value} in SI units plus *_ENABLED bool flags."""
+        overrides = {attr: sb.value() / scale
+                     for attr, (sb, scale) in self._spinboxes.items()}
+        for cb, flag_attr in self._checkboxes.values():
+            overrides[flag_attr] = cb.isChecked()
+        return overrides
 
     # ------------------------------------------------------------------
     # Private helpers
@@ -111,6 +151,13 @@ class DelayControlsPanel(QWidget):
                     sb.setStyleSheet("")
                 break
 
+    def _on_enable_toggled(self) -> None:
+        sender = self.sender()
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            if cb is sender:
+                self._spinboxes[attr][0].setEnabled(cb.isChecked())
+                break
+
     def _on_reset(self) -> None:
         for attr, (sb, scale) in self._spinboxes.items():
             default = self._defaults.get(attr)
@@ -119,6 +166,12 @@ class DelayControlsPanel(QWidget):
                 sb.setValue(default * scale)
                 sb.blockSignals(False)
                 sb.setStyleSheet("")
+        for attr, (cb, flag_attr) in self._checkboxes.items():
+            default = self._enable_defaults.get(attr, True)
+            cb.blockSignals(True)
+            cb.setChecked(default)
+            cb.blockSignals(False)
+            self._spinboxes[attr][0].setEnabled(default)
 
     def _on_save(self) -> None:
         path, _ = QFileDialog.getSaveFileName(

+ 11 - 0
services/seq-interp/src/hardware/constraints.py

@@ -27,6 +27,13 @@ class HardwareConstraints:
         self.MIN_BLOCK_DURATION = 20e-9  # сек, минимальная длительность блока (квант времени последовательности)
         self.GRAD_DELAY = 1000e-9
 
+        # Флаги вставки задержек в синхро-последовательность.
+        # Если флаг False — соответствующий блок задержки не вставляется
+        # (и не вычитается из соседнего), что убирает артефакты 1/0 тактов.
+        self.TR_DELAY_ENABLED = True
+        self.RF_DELAY_ENABLED = True
+        self.START_DELAY_ENABLED = True
+
         # Максимальные амплитуды
         self.RF_MAX = 1.0  # относительная макс. амплитуда RF (нормирована на 1.0)
         self.GRAD_MAX = 9e-3 * self.gamma  # макс. градиент (Гц/м) по умолчанию 9 mT/m * gamma
@@ -60,6 +67,10 @@ class HardwareConstraints:
         else:
             self.START_DELAY = self.MIN_BLOCK_DURATION * 10
         self.MIN_BLOCK_DURATION = data.get("MIN_BLOCK_DURATION", self.MIN_BLOCK_DURATION)
+        # Флаги вставки задержек
+        self.TR_DELAY_ENABLED = data.get("TR_DELAY_ENABLED", self.TR_DELAY_ENABLED)
+        self.RF_DELAY_ENABLED = data.get("RF_DELAY_ENABLED", self.RF_DELAY_ENABLED)
+        self.START_DELAY_ENABLED = data.get("START_DELAY_ENABLED", self.START_DELAY_ENABLED)
         # Обновление максимальных амплитуд (если указаны)
         self.RF_MAX = data.get("RF_MAX", self.RF_MAX)
         self.GRAD_MAX = data.get("GRAD_MAX", self.GRAD_MAX)

+ 30 - 8
services/seq-interp/src/interfaces/xml_generator.py

@@ -24,43 +24,65 @@ class XMLGenerator:
             adc_times_values = []
             adc_times_starts = []
 
+            # Массивы синхронизатора имеют ведущий seed-элемент на индексе 0
+            # (стартовый блок), значимые блоки идут по индексам 1..nb. Seed
+            # представлен заголовочным тегом (*1), поэтому в цикле эмитим
+            # элементы 1..nb: block_iter k -> индекс массива k + 1.
+            # (Совпадает с эталонной логикой srv_interp.synchronization.)
+
             with tag("RF"):
                 with tag("RF1"):
                     text(0)
                 for rf_iter in range(number_of_blocks):
                     with tag("RF" + str(rf_iter + 2)):
-                        text(gate_rf[rf_iter])
+                        text(gate_rf[rf_iter + 1])
 
             with tag("SW"):
                 with tag("SW1"):
                     text(1)
                 for sw_iter in range(number_of_blocks):
                     with tag("SW" + str(sw_iter + 2)):
-                        text(gate_tr_switch[sw_iter])
+                        text(gate_tr_switch[sw_iter + 1])
 
             with tag("ADC"):
                 with tag("ADC1"):
                     text(0)
                 for adc_iter in range(number_of_blocks):
-                    if gate_adc[adc_iter] == 1:
-                        adc_times_values.append(blocks_duration[adc_iter])
-                        adc_times_starts.append(sum(blocks_duration[0:adc_iter]))
+                    idx = adc_iter + 1
+                    # Непрерывный участок HIGH (в т.ч. собранный из разбитых
+                    # блоков) считается одним событием ADC: засекаем фронт и
+                    # суммируем длительность всего участка для points-окна.
+                    is_rising_edge = (
+                        gate_adc[idx] == 1 and gate_adc[idx - 1] == 0
+                    )
+                    if is_rising_edge:
+                        adc_times_starts.append(sum(blocks_duration[0:idx]))
+                        run_duration = 0
+                        run_iter = idx
+                        while (run_iter < len(gate_adc)
+                               and gate_adc[run_iter] == 1):
+                            run_duration += blocks_duration[run_iter]
+                            run_iter += 1
+                        adc_times_values.append(run_duration)
                     with tag("ADC" + str(adc_iter + 2)):
-                        text(gate_adc[adc_iter])
+                        text(gate_adc[idx])
 
             with tag("GR"):
                 with tag("GR1"):
                     text(1)
                 for gr_iter in range(number_of_blocks):
+                    # Последнее событие импульсной последовательности
+                    # переводим из LOW в HIGH (включение градиентной системы)
+                    gr_value = 1 if gr_iter == number_of_blocks - 1 else 0
                     with tag("GR" + str(gr_iter + 2)):
-                        text(0)
+                        text(gr_value)
 
             with tag("CL"):
                 with tag("CL1"):
                     text(int(min_block_time / synchro_block_timer))
                 for cl_iter in range(number_of_blocks):
                     with tag("CL" + str(cl_iter + 2)):
-                        text(int(blocks_duration[cl_iter] / synchro_block_timer))
+                        text(int(blocks_duration[cl_iter + 1] / synchro_block_timer))
 
         xml_string = indent(doc.getvalue(), indentation=" " * 4, newline="\r")
         with open(path, "w", encoding="utf-8") as f: