Skip to content

Конструктор ROI и наборов нуклидов / ROI and nuclide set builder - #32

Draft
Verter73 wants to merge 7 commits into
Am6er:masterfrom
VibeEngineering-LLC:feat/roi-wizard
Draft

Конструктор ROI и наборов нуклидов / ROI and nuclide set builder#32
Verter73 wants to merge 7 commits into
Am6er:masterfrom
VibeEngineering-LLC:feat/roi-wizard

Conversation

@Verter73

@Verter73 Verter73 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

English (short): adds Tools → ROI and nuclide set builder — a three-step panel that
turns a choice of nuclides into a ready ROI configuration (ROIConfigManager) or a
NuclideSet library entry (NuclideDefinitionManager). The window is a DockContent,
so it docks, groups and auto-hides like every other BecqMoni panel and is painted by the
app's own VS2015BlueTheme. One menu item, no behaviour changes anywhere else, no new
dependencies. Labels use the project's own .resx + satellite mechanism. Draft: opened
for review, questions to the author are at the bottom.


Зачем

Набор ROI и библиотека нуклидов сейчас набираются вручную: линия за линией, с оглядкой на
справочник. Модуль делает то же самое за три шага — выбрать изотопы, получить линии,
выгрузить результат туда, куда приложение и так умеет их принимать.

Что делает

Шаг 1 — источники. 121 нуклид из снимка IAEA Live Chart / ENSDF (γ + X, T½,
ветвления), коды семейств по ANSI N42.34 (NORM / MED / IND / SNM плюс три группы вне
стандарта), три ряда распада (U-238, Th-232, U-235) — одним нуклидом или цепочкой, и
линии ХРИ материалов защиты и детектора (Fe, Cu, Cd, Sn, Te, I, Ba, W, Pb).

Шаг 2 — линии. Фильтры по интенсивности, энергии и периоду полураспада; слияние
линий, которые детектор не разрешит (порог k·FWHM, сцинтилляторная модель √E от R₆₆₂);
вторичные пики — обратное рассеяние, комптон-край, вылеты 511 и 1022, вылет K-рентгена
иода, аннигиляция, каскадное суммирование, наложение; равновесие ряда (интенсивности на
распад родителя); поиск по всей базе «кто ещё светит рядом с этой энергией».

Шаг 3 — оформление и экспорт. Маркеры или зоны, ширина зоны в процентах от энергии
либо k·FWHM, цвета по цепочке или по нуклиду; проверки перед записью; предпросмотр XML
тем же XmlSerializer, каким пишет ROIConfigManager.SaveConfig.

Как встроено

Один пункт меню — Tools → RoiWizardToolStripMenuItem. Ни одна существующая строка не
меняет поведения: правки хоста это +100 строк на пять файлов (MainForm, его ресурсы,
.csproj), только добавления.

Окно — родная док-панель. RoiWizardForm наследует DockContent, поэтому полоску
заголовка, кнопки и обводку рисует та же VS2015BlueTheme, которую MainForm ставит в
InitializeDockPanelTheme, а панель стыкуется к краям, группируется вкладкой с другими
панелями и убирается в автоскрытие булавкой. Открывается плавающей поверх главного окна,
немодально — спектр остаётся доступным. Экземпляр один: закрытие панель прячет
(HideOnClose), повторный вызов из меню возвращает её со всеми настройками.

Всего три точки сцепления с приложением:

Точка Зачем Если делать иначе
MainForm.dockPanel1 показ: Show(dockPanel1, bounds) DockContent — это обычный Form, поэтому ShowDialog(this) работает без правок в модуле; теряется только стыковка
ROIConfigManager, NuclideDefinitionManager запись результата других способов принять результат у приложения нет
EnergyResolutionCalculator / FwhmCalibration Func<double> в конструкторе конструктор без аргументов: кнопка «из спектра» выключается, форма работает автономно

Новых зависимостей нет: XmlSerializer, WinForms, XPTable и DockPanelSuite уже
в проекте.

Подписи — механизмом проекта: RoiWizard/RoiWizardStrings.resx (нейтральная, английская)
и RoiWizardStrings.ru.resx, сателлит собирает MSBuild — как для остальных форм и для
Properties/Resources.resx. Новый язык добавляется файлом RoiWizardStrings.<culture>.resx,
код при этом не трогают. Расчётное ядро языка не знает вовсе: проверки возвращают код
замечания и подстановки, фразу собирает форма.

Данные лежат снимком в RoiWizard/nuclides.xml (101 КБ), пересобирается скриптом; текст
справки — в help.xml, выгружается из эталонной веб-страницы, чтобы две версии не
разъехались после первой же правки.

Как проверялось

  • Чистый клон master (d1eab74) + integration/host-patch/apply_patch.py +
    restore + Rebuild Releaseноль ошибок. Единственное предупреждение
    (MSB3327, нет сертификата подписи ClickOnce) воспроизводится и на дереве без модуля.
    Скрипт идемпотентен: повторный запуск ничего не дублирует.
  • 100 проверок расчётного ядра (integration/tests/run_tests.cmd, код возврата 0/1,
    без тестового фреймворка в решении) — зелёные.
  • Живой прогон окна под ru-RU и en-US: все состояния трёх шагов и справка,
    отдельно — обход раскладки на вылет контролов за пределы родителя и наложения соседей.
  • Прогон на реальном спектре: ROI-конфигурация и набор нуклидов создаются и читаются
    приложением обратно.

Как выглядит

Пункт в меню Инструменты, следом за «Редактировать наборы изотопов…» — рядом с
окнами, чей результат конструктор и собирает. Больше правок в интерфейсе нет.

Пункт меню

Панель поверх открытого спектра: слева спектр главного окна, справа за панелью —
«Обнаружение пиков», внизу чипы выбранных нуклидов.

Панель в приложении

Шаг 1 · Изотопы

Шаг 2 · Линии

Шаг 3 · Оформление и экспорт

Справка

Все снимки сделаны в живом приложении (свежий клон master, сборка Release, панель
открыта своим пунктом меню). Полный набор экранов с пояснениями —
docs/SCREENSHOTS.md,
история правок — CHANGELOG.md.
Интерфейс обкатывался на веб-странице, она же служит эталоном раскладки и палитры:
https://vibeengineering-llc.github.io/becqmoni-roi-wizard/

Вопросы к вам

  1. Якоря — главный вопрос. Модуль помечает несколько якорных линий (по умолчанию
    три, поле 1–9). LibraryPeakFitter перебирает все записи с IsAnchor, берёт сдвиг
    калибровки с сильнейшей по SNR и требует совпадения с найденным пиком хотя бы одной
    (допуск 0,5·FWHM): при единственном якоре достаточно не найтись линии 2614,5 — и
    молчит весь набор. Так ли задуман гейт, и не мешает ли несколько якорей чему-то, что
    видно изнутри приложения?
  2. Снимок данных в репозитории. 101 КБ XML внутри сборки — приемлемо, или каталог
    должен подтягиваться из NucBase, который в BecqMoni уже есть?
  3. Обновление каталога. Пересборка снимка — python-скрипт по IAEA Live Chart. Нужен
    ли он в дереве проекта или достаточно готового снимка?
  4. Место панели по умолчанию. Сейчас панель открывается плавающей по центру экрана.
    Может, ей уместнее сразу стыковаться к какому-то краю или в группу к существующей
    панели — вам виднее, как устроена привычная раскладка.

Готов доводить по замечаниям — правки удобно вносить в ветку форка, PR обновится сам.

Verter73 and others added 3 commits July 26, 2026 03:49
Adds Tools -> "ROI and nuclide set builder": a three-step window that turns a
choice of nuclides into a ready ROI configuration or a NuclideSet library entry,
so neither has to be typed in by hand line by line.

What it does. Step 1 picks sources: 121 nuclides from an IAEA Live Chart / ENSDF
snapshot with family codes (ANSI N42.34), three decay chains, and XRF lines of
shielding and detector materials. Step 2 turns them into lines: filters by
intensity, energy and half-life; merging of lines the detector cannot resolve
(k*FWHM, scintillator model); secondary peaks (backscatter, Compton edge, escape
511/1022, iodine K-escape, annihilation, cascade sum, pile-up); and a search for
what else emits near a given energy. Step 3 exports: an ROI configuration through
ROIConfigManager, or a nuclide set through NuclideDefinitionManager with anchors
set for the library fit.

How it is wired in. One menu item; nothing existing changes behaviour. The
resolution comes from the active spectrum's FWHM calibration through a delegate,
so the window stays usable and testable with no spectrum open. No new
dependencies: XmlSerializer, WinForms and XPTable are already in the project.

Labels follow the project's own mechanism: RoiWizard/RoiWizardStrings.resx
(neutral, English) plus .ru.resx, satellite built by MSBuild, exactly as for the
other forms. The calculation core knows no language at all - the checker returns
an issue code and its arguments, the form composes the phrase.

Data is a snapshot in RoiWizard/nuclides.xml (101 KB), rebuilt by
tools/export_catalog.py; the help text lives in help.xml, exported from the
reference web page so the two cannot drift apart.

Checked: the solution builds with the module (Release, .NET Framework 4.8, zero
errors); 100 core tests pass; the window was driven under ru-RU and en-US.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The markup walker called its handler Tag, which shadows the Tag property every
Control carries; the compiler warned (CS0108) and a reader would have to stop and
work out which of the two a call meant. Renamed to ApplyTag — nothing else changes.

With this the module compiles warning-free; what remains for the solution is the
project's own MSB3327 about a missing ClickOnce signing certificate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RoiWizardForm inherits DockContent, so the window is a native BecqMoni
panel: the caption strip and buttons come from VS2015BlueTheme, it docks,
groups into tabs and auto-hides like the peak detection panel. Non-modal,
single instance, HideOnClose keeps the selection across reopenings; the
menu handler shows it via Show(dockPanel1, bounds).

Checked rows in the lines table get the web tint (#CDE4F7); the step-1
presets block measures its wrapped height so the last row is not clipped
in narrow columns. The help window (outside the dock system) gets a
panel-style caption drawn from the same theme palette; the app icon is
removed from both captions.

Core tests 100/100; clean Release build against master d1eab74.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Verter73

Copy link
Copy Markdown
Collaborator Author

English (short): update after a live round of UI polishing — the wizard window is now a real DockContent, so it docks, groups and auto-hides like every other BecqMoni panel and is painted by the app's own VS2015BlueTheme. Checked rows in the lines table get the selection tint; the step-1 presets row no longer clips in narrow layouts. Clean Release build against master (d1eab74), core tests 100/100.


Обновление ветки (940a53b) — итог обкатки окна вживую, поверх двух коммитов драфта:

Окно мастера — родная док-панель. RoiWizardForm теперь наследует DockContent: панель открывается плавающей, пристыковывается к краям, группируется вкладкой с «Обнаружением пиков» и другими панелями, убирается в автоскрытие булавкой. Полоску заголовка, кнопки и обводку рисует ваша же VS2015BlueTheme — совпадение с остальными окнами по построению, самодельной стилизации в мастере не осталось. Окно немодальное: спектр доступен, пока мастер открыт.

Показ из меню — вместо ShowDialog теперь Show(this.dockPanel1, bounds) (см. обновлённый host-patch/README.md). Экземпляр один: закрытие панели прячет её (HideOnClose), повторное открытие возвращает выбранные источники и настройки нетронутыми.

Паритет с веб-версией: отмеченная строка в таблице линий тонируется цветом выделения (#CDE4F7), как tr.selrow на странице-эталоне.

Раскладка шага 1: блок пресетов пересчитывает свою высоту под фактический перенос строк — в узкой колонке (например, у пристыкованной панели) нижняя строка больше не срезается, место отдаёт таблица каталога.

Окно справки в док-систему не входит, поэтому ему полоска в стиле панелей рисуется вручную — метрики VS2012DockPaneCaption (24 px, кнопки 18×18) и цвета из ColorPalette темы (ToolWindowCaptionInactive, ховер #FFFCF4/#E5C365, окантовка ToolWindowBorder). У обоих окон убрана пиктограмма приложения из заголовка.

Проверка: свежий клон master (d1eab74) + apply_patch.py + restore + Rebuild Release — 0 ошибок (единственное предупреждение MSB3327 про сертификат ClickOnce воспроизводится и на голом master); живой запуск со стыковкой и группировкой; тесты ядра 100/100.

Подробный ченжлог: https://github.com/VibeEngineering-LLC/becqmoni-roi-wizard/blob/main/CHANGELOG.md

The help window had its own hand-drawn caption which did not match the strip
the theme paints for the wizard's float window. It is now a DockContent shown
as a floating panel, so the theme draws both windows and they agree by
construction; it is also no longer modal. The custom chrome is retired
entirely (PanelChrome, ApplyCaption, DWM calls, the WM_NCCALCSIZE hook) along
with the now-unused pinTip string.

ShowRoiWizardForm sized the float window with hard numbers while the form needs
1591x1001 - AutoScaleMode.Font grows the layout to the theme font, so the panel
opened clipped. The size now comes from the form and is clamped to the working
area of the screen it opens on.

Release build clean; core tests 100/100.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Verter73

Copy link
Copy Markdown
Collaborator Author

English (short): the PR description is rewritten to the current state and every screenshot was retaken inside the running application. The help window is now a dock panel as well, so both module windows are painted by the app's own theme. One real bug fixed on the way: the float window was sized with hard numbers and opened clipped — the size now comes from the form itself.


Описание PR переписано под актуальное состояние, и все снимки переделаны в живом приложении — прежние снимались с формы, поднятой отдельно, а вне док-системы у неё системный заголовок Windows, которого внутри приложения нет.

Что изменилось с прошлого комментария:

Окно справки — тоже док-панель. У него был свой нарисованный заголовок, и он не совпал с полоской, которой тема рисует плавающую панель мастера. Подбирать цвета руками оказалось тупиковым путём: они подходили к одному состоянию темы и расходились с другим. Теперь справка — такой же DockContent, её заголовок рисует та же тема, и два окна модуля совпадают по построению. Побочно: справка перестала быть модальной, её можно держать открытой, продолжая работу. Самодельная отрисовка заголовка удалена целиком (около 200 строк: PanelChrome, вызовы DWM, перехват WM_NCCALCSIZE) — в диффе это −338 строк.

Исправлен дефект размера. ShowRoiWizardForm задавал плавающему окну 1200×700 числами, а форме нужно 1591×1001: AutoScaleMode.Font укрупняет разметку под шрифт темы. Панель открывалась сжатой, содержимое обрезалось. Теперь размер читается у самой формы и ограничивается рабочей областью того экрана, на котором открывается.

В описание добавлена таблица трёх точек сцепления с приложением — dockPanel1, менеджеры конфигураций и делегат разрешения, — и для каждой сказано, что будет, если подключить иначе. В частности: DockContent наследует обычный Form, поэтому если стыковка не нужна, ShowDialog(this) работает без единой правки в модуле.

Проверка та же: чистый клон master (d1eab74) + apply_patch.py + restore + Rebuild Release — ноль ошибок, тесты ядра 100/100, живой прогон под ru-RU и en-US.

Подробности по датам — CHANGELOG.md.

@Am6er Am6er self-assigned this Jul 26, 2026
@Am6er
Am6er marked this pull request as ready for review July 26, 2026 12:18
Verter73 and others added 3 commits July 26, 2026 16:46
After writing a set, the "Nuclide set" combo of the peak detection window kept
its old contents until BecqMoni was restarted.

ROI configs have a notification - ROIConfigManager raises ROIConfigListChanged
and the combos refresh themselves. Nuclide sets have no event at all, and
DCPeakDetectionView.RefreshNuclideSets() is only called from the constructor,
so no code path could refresh the list after a write. This affected
NuclideSetForm's own saves too, not just the wizard.

NuclideDefinitionManager now raises NuclideDefinitionListChanged at the end of
a successful SaveDefinitionFile, and DCPeakDetectionView subscribes to it. The
current selection survives - RefreshNuclideSets restores it via
IndexOf(selectedNuclideSet). Additions only.

Verified by reading both combo boxes with CB_GETLBTEXT before and after an
export: the set list gains "IAEA set" immediately, the ROI list "IAEA lines".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tint

Two defects found while testing the table.

Numeric columns sorted as text: in "E, keV" 39.86 sat between 340.96 and 409.46.
The columns are TextColumn, whose default comparer reads Cell.Text. The numbers
were in Cell.Data all along, which is what NumberComparer reads - ten numeric
columns now declare it. Nearby hits also gained a numeric half-life; that cell
used to carry only the caption.

Clicking a header wiped the row highlight in that column: XPTable fills the
sorted column with Table.SortedColumnBackColor (WhiteSmoke by default) on top of
the row background. The module's tables now set it to Color.Transparent - the
fill is only drawn when A != 0, and the sort direction is still shown by the
header arrow.

Verified in the live application; core tests 100/100.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Verter73

Copy link
Copy Markdown
Collaborator Author

English (short): the nuclide-library notification is reverted — with it in place, peak detection started misbehaving during testing. RefreshNuclideSets() also assigns SelectedIndex, so calling it on every write of the definition file can reset the chosen set. The missing notification is still real, but it is now a question for you rather than a patch from us. The sorting fixes stay.


Откат: правка «новый набор виден без перезапуска» снята (коммит 4347e2f), правки хоста снова только пять файлов — пункт меню и его ресурсы.

Причина: на испытаниях после неё сломалось поведение поиска пиков. Механизм, судя по коду, такой — RefreshNuclideSets() не только пересобирает список, но и присваивает comboBoxNuclSet.SelectedIndex. Вызов его в произвольный момент (при любой записи файла определений) поднимает SelectedIndexChanged у окна обнаружения пиков и способен сбросить выбранный набор: если объект набора заменён, IndexOf(selectedNuclideSet) даёт −1 и выбор уезжает на «--- All Nuclides ---». Точка расширения выбрана неудачно, и подбирать её со стороны я не буду.

Сама нехватка уведомления остаётся фактом: у ROIConfigManager событие ROIConfigListChanged есть, у NuclideDefinitionManager события нет, а RefreshNuclideSets() вызывается только из конструктора — поэтому набор, записанный что мастером, что штатным NuclideSetForm, появляется в списке только после перезапуска. Как правильно уведомлять интерфейс — вам виднее; вынес это отдельным вопросом в описании PR.

Исправления сортировки остаются — они целиком в модуле и хоста не касаются:

  • числовые столбцы сравнивались как текст (39,86 попадало между 340,96 и 409,46): у TextColumn сравнение читает Cell.Text, а числа лежат в Cell.Data, который читает NumberComparer — десяти столбцам он и назначен;
  • щелчок по заголовку гасил подсветку строки в этом столбце: Table.SortedColumnBackColor (по умолчанию WhiteSmoke) заливается поверх фона строки — у таблиц модуля он теперь Color.Transparent.

@Verter73

Copy link
Copy Markdown
Collaborator Author

English (short): summary of what changed since you took the PR at 15:18 MSK. Net effect: two sorting fixes inside the module. The host patch is back to exactly what it was — five files, the menu item and its resources. One commit pair (b2713834347e2f) is an experiment that was added and rolled back the same evening; details below so the history does not look odd.


Сводка по ветке с того момента, как вы взяли PR (15:18 МСК) — три коммита, но по сути изменение одно.

Что осталось в коде — два исправления сортировки (102e778), оба в модуле, хоста не касаются:

  1. Числовые столбцы сравнивались как текст: в «E, кэВ» значение 39,86 оказывалось между 340,96 и 409,46. Столбцы — TextColumn, её сравнение по умолчанию читает Cell.Text; числа лежали в Cell.Data, который читает NumberComparer. Десяти числовым столбцам он и назначен (каталог, линии, находки). Заодно у находок появился числовой период полураспада — в ячейке T½ раньше была только подпись, сортировать было не по чему.
  2. Щелчок по заголовку гасил подсветку строки в этом столбце: Table.SortedColumnBackColor (по умолчанию WhiteSmoke) заливается поверх фона строки (Renderers/CellRenderer.cs:621,667). У таблиц модуля он теперь Color.Transparent — направление сортировки и так показано стрелкой.

Что появилось и было убрано — уведомление об изменении библиотеки (b271383, откат 4347e2f). Проблема настоящая: набор, записанный что мастером, что штатным NuclideSetForm, не появляется в списке «Набор» до перезапуска — у ROIConfigManager событие ROIConfigListChanged есть, у NuclideDefinitionManager события нет, а DCPeakDetectionView.RefreshNuclideSets() вызывается только из конструктора. Я добавил событие в менеджер и подписку в окне; список действительно обновлялся, но на испытаниях после этого разладилось поведение поиска пиков. Причина, судя по коду: RefreshNuclideSets() не только пересобирает список, но и присваивает comboBoxNuclSet.SelectedIndex, то есть вызов в произвольный момент поднимает SelectedIndexChanged и может сбросить выбранный набор. Точка расширения выбрана неудачно — правку я снял и подбирать её со стороны не буду. Сама нехватка уведомления вынесена вопросом в описание PR.

Проверка (на чистом клоне master + apply_patch.py + Rebuild Release): ноль ошибок, единственное предупреждение MSB3327 про сертификат ClickOnce есть и без модуля; тесты ядра 100/100; сортировка и обе таблицы проверены живьём кадрами до/после.

Открытые вопросы прежние и все в описании PR: число якорных линий, снимок данных против встроенного nucdb.sqlite, и теперь — как правильно уведомлять интерфейс об изменении библиотеки.

@Am6er

Am6er commented Jul 26, 2026

Copy link
Copy Markdown
Owner

English (short): the PR has been taken over, extended and reworked on the maintainer's side. The module itself is kept — what changed is where its data comes from and how the set for the library fit is composed. Everything is on branch roi-wizard-reworked; please review there. Full report below: what was accepted as is, what was replaced and why, and the test results.


Спасибо за работу — модуль взят, доработан и переработан. Всё лежит в ветке
roi-wizard-reworked
(один коммит поверх master), сравнить с текущим состоянием PR удобно
так.
Приглашаю смотреть и возражать.

Что принято без изменений

Архитектура модуля, разбиение на шаги, разделение «расчётное ядро не знает языка», выбор
DockContent вместо диалога, механизм .resx + сателлит, SetChecker с кодами замечаний,
предпросмотр тем же XmlSerializer, AnchorPicker с правилом «сильная И одинокая», и обе
ваши правки сортировки из 102e778 (числовые компараторы и прозрачный
SortedColumnBackColor). Правило «якорем не может быть ХРИ или вторичный маркер» уже
работало — проверено, не трогал.

Что заменено и почему

1. Свой снимок nuclides.xml — удалён. 2429 строк, 121 нуклид. Всё, что в нём было по
линиям, уже лежит в nucdb.sqlite: decay_radiations содержит 40 216 записей G и 10 928
X с оболочками KA1/KA2/KB/KpB1/L — ровно та же структура. Два источника одних и тех же
ядерных данных расходятся после первой правки любого из них. NuclideCatalog теперь читает
базу: 2142 нуклида вместо 121, и поиск «кто ещё светит рядом» идёт по всей базе.

2. Зашитая EquilibriumFactors — удалена. Шесть ветвлений константами в коде. Ветвления
есть в decay_chain; ряды считаются обходом от корня с накоплением доли. Учтена ловушка
l_seqno: следуются только переходы с минимальным уровнем, иначе у Bi-212 наряду с
настоящими 35.94 / 64.06 % приезжают 67 / 33 % с уровня 5, а линия Pa-234m 1001.03 кэВ
теряется совсем (она при parent_l_seqno = 2).

3. BuildFullSet — заменён измеренным оптимумом. Это главное расхождение. Профиль брал
все линии ≥ 0.05 % без единого фильтра — то есть точку k = 0, I_min = 0 сетки из
tools/LibraryFitLab, худшую из измеренных и по recall, и по фантомам. Комментарий
обосновывал его итерацией 9, но сет итерации 9 сам был отфильтрован.

Заменён на: разнос ≥ 0.7·FWHM до более сильной линии, интенсивность ≥ 1 % на распад
родителя цепочки
, якорь входит всегда мимо обоих фильтров (якорь U-238 — Pa-234m,
0.842 %, порог не проходит, а без якоря фит не запускается). Это не слияние: центроид не
двигается, сильная линия остаётся на табличной позиции — сдвинутая позиция сломала бы
посадку компонент, на которой фит и держится.

4. Ответ на ваш вопрос про якоря. Несколько якорей — да, ровно то, подо что писан
LibraryPeakFitter: он перебирает все IsAnchor, берёт сдвиг калибровки с сильнейшего по
SNR и требует совпадения хотя бы одного. Три по умолчанию — нормально. Но по прогону (ниже)
видно, что интенсивность — не лучший критерий: ториевый якорь 2614 кэВ ложно срабатывает на
40 % файлов, а урановый Pa-234m 1001 кэВ — на 4 %, потому что слабый и одинокий.

5. Ответ на вопрос про снимок против nucdb. См. п. 1 — база. В неё добавлено шесть
таблиц для того, чему в ней места не было: families + nuclide_families (классификация
ANSI N42.34, 160 привязок), xrf_elements + xrf_lines (10 элементов защиты и детектора,
46 линий — это не излучение распада), chains (выбор разбираемых рядов), catalog_meta.
Плюс пять индексов: в базе не было ни одного, и сборка каталога занимала 216 секунд
против 255 мс сейчас. Выигрывают и запросы NucBase — они ходят по тем же колонкам.

6. Ответ на вопрос про уведомление об изменении библиотеки (то, что вы добавили в
b271383 и откатили в 4347e2f). Проблема настоящая, и она чинится. RefreshNuclideSets()
начинается с Items.Clear() — это само по себе ставит SelectedIndex = -1 и поднимает
SelectedIndexChanged; обработчик обнуляет selectedNuclideSet и запускает полный
перерасчёт пиков. К строке восстановления выбора восстанавливать уже нечего. Уведомление
оставлено, но выбор снимается в локальную переменную до очистки, а обработчик на время
пересборки отключён флагом. Откат 4347e2f в ветку не брал.

7. Вторичные пики — пущены в набор, но с Intencity = 0. Обратное рассеяние,
комптоновский край, вылеты, сумм-пики и ХРИ в спектре есть и ловятся финдером; именованный
аппаратный пик лучше фантомной линии нуклида на том же месте. Нулевая интенсивность —
механизм, а не потеря данных: LibraryPeakFitter собирает bound-группу только из линий с
Intencity > 0, поэтому такая запись остаётся одиночным компонентом и не портит веса
BR-связки. Якорем стать по-прежнему не может.

8. Поправки комптоновского края и обратного рассеяния. Числа сняты на одном комплексе
(Gamma-1C, NaI 63×63, Pb 50 мм), полагаться на специфику одного детектора нельзя. Разделены
по переносимости: сдвиг края задаётся методом поиска (аналитика даёт край ступеньки,
вторая производная — центроид размытой особенности), алгоритм везде один, доля FWHM
переносима — 0.8 оставлен. Сдвиг обратного рассеяния определяется геометрией защиты; был
абсолютным +10 кэВ, что на 1024-канальном детекторе означало не то же, что на
8192-канальном — переведён в доли FWHM (0.35, те же измеренные 9.9 кэВ на 200–250 кэВ).
Обе вынесены в SecondaryPeakTuning.

9. Скрипты вне дерева. integration/host-patch/apply_patch.py, integration/tests/ и
tools/export_catalog.py в диффе PR отсутствовали — «100 проверок ядра» из описания по PR
не воспроизводились. С удалением снимка пересобирать нечего; вместо этого в дерево добавлен
tools/RoiWizardCheck (см. ниже).

Исправленные дефекты

дефект следствие
LineMerger не копировал OwnerLabel слитая линия получала имя Th-232 965–969 (Ac-228)ChainOf читал цепочку «Ac-228» и линия выпадала из BR-связки ряда
импорт NucBase писал интенсивность на распад нуклида BR-связка берёт веса прямо из Intencity: 583.19 кэВ Tl-208 шёл как 85 % вместо 30.5 % — правка хоста, нужна независимо от модуля
предпросмотр XML ≠ файл пустой FormatVersion, чужой Guid
перезапись ROI-конфигурации в ROIConfigList оставались две записи на один файл, побеждала сохранённая последней
панель не восстанавливалась из раскладки нет ветки в GetContentFromPersistString
шрифты WizardTheme — свойства новый Font на каждое обращение, Walk раздавал их сотне контролов
SafeFileName не знал про CON/NUL/COM1-9 и хвостовые точки
EqualEnergies с фиксированным 1 кэВ на 2614 кэВ это сотая доля FWHM — вырожденные пары не замечались; теперь 0.1·FWHM
три статусные строки и подписи рядов не локализованы

Отдельно — два дефекта, которые нашлись только живым запуском:

  • PrettyName съедал вторую букву символа: 241AM → «A-241m». Изомер в базе всегда со
    строчной m, а на «m» кончаются шесть символов элементов — Am, Cm, Fm, Pm, Sm, Tm.
    Am-241 — первый нуклид любого калибровочного набора.
  • Подпись галки обрезалась фиксированной шириной, в русской раскладке сильнее.

Новое

Редактор семейств в NucBase — раз классификация лежит в базе, править её должен штатный
интерфейс приложения к этой базе, а не внешний скрипт. Кнопка «Семейства…», восемь групп,
подписи и пояснения берутся из самой базы (двуязычные). После записи каталог конструктора
сбрасывается и панель пересобирает списки — правка видна без перезапуска.

Критериев слияния теперь два, по решению мейнтейнера: предел Sparrow 0.85 (физическая
разрешимость, для маркеров ROI) и измеренный оптимум 0.7 (состав сета). Разные вопросы —
выбор за пользователем.

Результаты тестирования

1. tools/RoiWizardCheck — 33 утверждения, все зелёные. Консольный харнесс против
собранной сборки (юнит-тестового проекта в решении нет по устройству). Проверяет линии
против ENSDF, ловушку parent_l_seqno, ветвления рядов, порядок членов, регистр в nucid,
пересчёт при импорте NucBase, порядок критериев слияния и состав рекомендованного сета.

2. Приёмочный прогон recall — сошёлся с отчётом лаборатории в точку. Свип прогнан
заново целиком на девяти эталонных спектрах:

детектор k = 0.7, I_min = 1 % было в отчёте база финдера
AS80x80 89.87 % 89.9 % 46.8 %
ASN16 87.23 % 87.2 % 53.9 %
RC103 95.00 % 95.0 % 45.0 %

Вся сетка 9×7 воспроизвелась, включая провал без фильтра разноса (AS80x80 78.48 % при
k = 0). LibraryPeakFitter не менялся — числа и обязаны были совпасть.

Состав рекомендованного сета сверен с data/recommended_sets.csv: все 20 эталонных линий
ASN16 Th-232 воспроизведены
. Сверх эталона две — Ra-228 13.5 (1.6 %) и Bi-212 39.9
(1.06 %); обе настоящие, эталон обрезан снизу 46.54 кэВ по диапазону детекторов.

3. Прогон по всем спектрам трёх каталогов — 121 файл, сбоев ноль. Отработал 91 файл;
оставшиеся 30 не берёт сам харнесс (Device config not found — GUID устройства из спектра
нет среди конфигов рабочего каталога), не приложение. 819 прогонов.

детектор цепочка якорь сработал ср. LIB на обманке фантомов
ASN16 Th-232 40 % 13.6 40 % 15.5
ASN16 Ra-226 29 % 14.9 29 % 17.8
ASN16 U-235 20 % 10.9 20 % 7.9
ASN16 U-238 4 % 14.7 4 % 14.3
AS80x80 Th-232 33 % 15.3 33 % 9.0
RC103 Th-232 14 % 13.0 14 % 11.0

Медиана прогона с сетом 44 / 43 / 1 мс, максимум 581 мс.

Это подтверждает главный вывод лаборатории на реальных файлах, а не только на девяти
отобранных
: сет-обманка (якорь настоящий, остальные линии сдвинуты на пустые энергии)
принимается ровно так же часто, как настоящий, и даёт сопоставимое число линий. Гейт
SignificanceZ = 4 не отличает цепочку, которая в образце есть, от сдвинутой. Это свойство
LibraryPeakFitter, а не модуля, и следующая задача — заменить тест значимости амплитуды на
сравнение моделей (ΔD с поправкой Уилкса) или matched-filter по остатку.

4. Живой прогон приложения под ru-RU и en-US: панель открывается и восстанавливается из
раскладки, каталог читается из базы, набор создаётся и сразу виден в «Обнаружении пиков»,
предпросмотр совпадает с файлом, редактор семейств пишет и правка подхватывается на лету
(группа MED 15 → 16 без перезапуска).

Сборка Release чистая — 0 ошибок, 0 предупреждений; сателлит ru собирается.

Что осталось открытым

  • Гейт значимости (см. выше) — задача не модуля.
  • Пресет «ЕРН-фон» смешивает Th-232 и U-238: один сработавший якорь включает обе цепочки, и
    на ториевом образце урановая половина даёт ложные отождествления.
  • Рекомендованный состав берёт только γ. Рентген распада исключён намеренно — оптимум
    мерялся без него; но на NaI K-серия Pb/Bi 72…88 кэВ реально неразделима с γ 84.4 и 99.5,
    и эту конфигурацию никто не мерял.
  • В tools/LibraryFitLab/scripts не хватает модуля gainscan.pycalibrate.py без него
    не запускается. Обошёл через apply_calibration.py (применяет сохранённые коэффициенты).

@Am6er Am6er added enhancement New feature or request duplicate This issue or pull request already exists labels Jul 26, 2026

@Am6er Am6er left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Переработано на стороне мейнтейнера: ветка roi-wizard-reworked. Разбор принятого и заменённого, а также результаты тестирования — в комментарии выше. Этот PR в текущем виде не мержится: его содержимое вошло в ветку с правками (единый источник данных вместо снимка, измеренный фильтр состава сета, правка импорта NucBase). Прошу смотреть и возражать там.

@Verter73

Copy link
Copy Markdown
Collaborator Author

English (short): reviewed roi-wizard-reworked as asked. Everything checkable on our side checks out: Release builds with 0 errors, your RoiWizardCheck runs green here, and the database numbers, the Intencity = 0 reasoning and the backscatter conversion all reproduce. Six findings below, one of which is a startup crash rather than a window error. At the end: two questions — where you disagree, and who takes the fixes we agree on.


Спасибо, разбор посмотрел целиком. Ветку развернул рядом с проверочным деревом, собрал и прогнал.

Что проверил и подтверждаю

проверка результат
Rebuild Release из чистого дерева ветки 0 ошибок; предупреждение одно — MSB3327 (сертификат ClickOnce), оно есть и на голом master
tools/RoiWizardCheck — собрал csc по вашей инструкции, запустил всё зелёное, код возврата 0
«2142 нуклида, 40 216 G, 10 928 X» сходится запрос в запрос
«пять индексов, в базе не было ни одного» ровно пять, плюс прогнан ANALYZE (есть sqlite_stat1)
8 семейств, 160 привязок, 10 элементов ХРИ / 46 линий, 4 ряда, 3 записи меты сходится
обоснование Intencity = 0 подтверждается: LibraryPeakFitter.cs:255members.Count >= 2 && members.All(m => m.Intensity > 0.0); второй гейт — EnergySpectrumView.cs:722
перевод сдвига обратного рассеяния 10 кэВ → 0.35·FWHM арифметика верна: 9.9 кэВ при R = 7.5 % на 225 кэВ — это 0.34·FWHM по вашей же ResolutionModel
локализация модуля 160 ключей, наборы en и ru совпадают, пустых значений нет, все ключи есть в Designer

Отдельно: ваша правка RefreshNuclideSets точнее нашей. Мы остановились на «SelectedIndex присваивается», вы нашли причину глубже — Items.Clear() сам поднимает SelectedIndexChanged, и восстанавливать выбор в конце было уже нечего. Флаг + снятие выбора до очистки — это правильно, снимаем свой откат.

Пересчёт интенсивности на распад корня цепочки при импорте NucBase — настоящий дефект хоста, к модулю отношения не имеющий; хорошо, что нашёлся.

Замечание про скрипты вне дерева принимаем: в диффе PR действительно 24 файла, apply_patch.py, integration/tests/ и export_catalog.py в них нет, и проверить «100 тестов» по PR было нельзя.

Замечания

1. Нет базы или база старая — падает не окно, а приложение

NuclideCatalog.Load (BecquerelMonitor/RoiWizard/NuclideCatalog.cs:110) бросает FileNotFoundException, а запросы к catalog_meta / families / chains на базе без новых таблиц дадут SqliteException. Ловли нет ни в конструкторе формы (RoiWizardForm.cs:61), ни в ShowRoiWizardForm.

Существеннее другое: GetContentFromPersistString (MainForm.cs:350) зовёт то же свойство при восстановлении раскладки, а LoadFromXml на старте (MainForm.cs:216) не обёрнут try. То есть у пользователя, у которого панель осталась в сохранённой раскладке, а база по любой причине не та, приложение не запустится вовсе — и он даже не свяжет это с конструктором ROI.

До перехода на базу каталог был встроенным ресурсом и исчезнуть не мог; это регрессия, созданная переходом. Проверку схемы вы уже написали — NuclideFamilies.IsAvailable; каталогу нужна такая же плюс try вокруг обоих мест показа.

2. Обход decay_chain недосчитывает всё, что ниже точки слияния ветвей

DecayChains.Fill раскрывает узел один раз — с долей, накопленной к моменту раскрытия (NucBase/DecayChains.cs:98-145). Если основной вклад приходит позже слабого, потомки остаются с долей от слабого. Прогон вашего же алгоритма на вашей базе, ряд U-238:

214BI  frac=0.998400 -> 210PB 0.003%, 214PO 99.98%, 210TL 0.021%
210PB  frac=0.000030 -> 210BI 100%      <- раскрыт по прямой слабой ветке 0.003 %
214PO  frac=0.998190 -> 210PB 100%      <- основной вклад приходит ПОЗЖЕ
210BI  frac=0.000030 -> 210PO 100%

Итог: 210BI и 210PO получают 0.00003 вместо 0.9984 — занижение в 33 000 раз. То же в ряду Ra-226. Сам Pb-210 в порядке: его доля накапливается и в конце верна, страдают только потомки.

На сегодняшних данных это никого не задевает — у Bi-210 гамма-линий в базе нет, у Po-210 одна 803.06 кэВ с 0.00103 %, обе ниже любого порога. Но это свойство алгоритма, а не удача данных: любая цепочка со слабым «коротким замыканием» в обход сильной ветки даст то же самое. Лечится раскрытием узла заново, когда его доля выросла (или обходом в топологическом порядке).

Попутно: ваш харнесс печатает u238: ветвление Ra-226 0.9984 (ждали 1) и засчитывает по допуску. Эти 0.16 % — переход 234mPa → 234Pa → 234U, которого в decay_chain нет с процентом. Мелочь, но раз тест сам это показывает, лучше или зафиксировать ожидание, или добить обход.

3. Изомерные состояния схлопываются в одно имя

PrettyName (NuclideCatalog.cs:567) режет хвост по первой строчной m, поэтому 152EUm1 и 152EUm2 дают одно и то же «Eu-152m». В каталоге таких групп 52; крайний случай — 129INm1, 129INm2, 129INm3, 129INm4: четыре разных нуклида под одним именем.

Имя — это идентичность записи в наборе (NuclideDefinition.NuclideName), и обратное преобразование вы сами называете неоднозначным в комментарии к ToNucid (NuclideCatalog.cs:598). Пока в наборе один из пары — всё равно; как только пользователь возьмёт обоих, различить их будет нечем.

4. Дубли строк в списке каталога

144TBm лежит в nuclides тремя строками — уникального ключа по nucid у таблицы нет, — и ReadNuclides их не сворачивает: «Tb-144m» показывается в списке трижды. Единственный такой случай во всей выборке 2142, лечится добавлением group by nucid.

5. База стала изменяемой, но осталась файлом поставки

nucdb.sqlite подключён как <Content> (BecquerelMonitor.csproj:1237) и не значится ни в одном из 17 PublishFile как файл данных. При обновлении ClickOnce файл заменяется целиком — классификация, которую пользователь правил новым редактором семейств, пропадёт вместе с ним. Это не возражение против редактора: сама идея держать классификацию там же, где ядерные данные, разумна. Это вопрос о том, что считать пользовательскими данными: либо база помечается файлом данных развёртывания, либо пользовательские привязки живут отдельным файлом рядом и накладываются поверх поставочных.

Побочно: каждая правка базы кладёт в историю репозитория новый бинарник на 6,7 МБ.

6. Мелкое

  • В SecondaryPeaks.cs над новым абзацем комментария остался старый — они дублируют друг друга.
  • В tools/LibraryFitLab/scripts/__pycache__ закоммичены четыре .pyc.
  • Подписи периода полураспада ms и ns остались машинными кодами: в switch есть s/m/h/d/y/us, а миллисекунды и наносекунды выпадают как есть.
  • И симметрично вашему замечанию про наши скрипты: gainscan.py в дереве заменён заглушкой gainscan_stub.py, а девяти эталонных спектров в репозитории нет — измеренный оптимум по репозиторию воспроизвести нельзя ровно в том же смысле, в каком нельзя было проверить наши тесты по PR. Вы, в отличие от нас, выложили калибровки и все сводные таблицы, так что замечание слабее нашего, но упомянуть честно.

Чего в замечаниях нет

Возражений по существу ваших пяти замен у нас нет.

Отказ от снимка в пользу базы принимаем: аргумент «два источника одних и тех же ядерных данных разъедутся после первой правки» сильнее нашего аргумента про воспроизводимость, а шесть добавленных таблиц закрывают то, чего в базе не было. Замену BuildFullSet измеренным оптимумом принимаем тем более — у нас за профилем стояла ссылка на чужую итерацию, у вас измерение на девяти спектрах и трёх детекторах. Разделение поправок вторичных пиков по переносимости — по-нашему правильнее исходного.

Про гейт значимости (SignificanceZ = 4 не отличает настоящую цепочку от сдвинутой) — вывод серьёзный, и он действительно не про модуль. Если возьмётесь за сравнение моделей или matched-filter, скажите: это как раз та задача, где можно помочь измерением, а не кодом.

Два вопроса

  1. По замечаниям 1–6 — где не согласны? Особенно интересует ваше мнение по пятому: где, по-вашему, правильное место пользовательской классификации, если база — файл поставки. И по второму: считать ли недосчёт потомков после слияния ветвей дефектом сейчас, раз на нынешних данных он ничего не меняет, или это «починим, когда появится цепочка, где это видно».

  2. Кто правит согласованное? Замечания 1, 3, 4 и 6 — правки на несколько строк каждая, второе — с десяток строк в Fill. Мы можем сделать их прямо в roi-wizard-reworked (вы написали, что ветка общая) и отдельным коммитом с описанием, что и почему; можем прислать патчи сюда, а можем просто оставить вам. Скажите, как вам удобнее — и держать ли этот PR открытым как место обсуждения или закрыть в пользу ветки.

@Am6er
Am6er marked this pull request as draft July 26, 2026 19:08
@Am6er

Am6er commented Jul 26, 2026

Copy link
Copy Markdown
Owner

English (short): all six findings accepted — five are fixed in 3c71fbd, the sixth (a shipped database that the user can now edit) is a design question and I take it. Two of your findings were worse than reported: three duplicated rows, not one, and 163 collapsing isomer groups, not 52. Answers to both questions at the end, plus what I would ask of you next.


Разбор сильный — спасибо. Возражений по существу ни к одному замечанию нет; проверил каждое на данных, прежде чем править.

Замечания 1–4 и 6 — исправлены, коммит 3c71fbd

1. Падение приложения при отсутствующей или старой базе

Принято целиком, включая главное: падало бы не окно, а запуск. Вы правы и в диагнозе регрессии — до перехода на базу каталог был встроенным ресурсом и исчезнуть не мог.

Сделано: NuclideCatalog.IsAvailable (файл + все девять таблиц), геттер панели возвращает null вместо исключения, GetContentFromPersistString отдаёт null — DockPanelSuite подставляет DummyContent и закрывает его, раскладка грузится. Пункт меню показывает сообщение.

2. Недосчёт потомков за точкой слияния ветвей

Считаю дефектом сейчас, а не «когда появится цепочка, где это видно» — отвечаю на ваш прямой вопрос. Ваш трейс воспроизвёлся один в один: 210BI и 210PO получали 0.00003 вместо 0.9984.

Довод не в сегодняшних данных, а в том, что величина уходит в NuclideDefinition.Intencity, откуда BR‑связка берёт веса компонент. Ошибка в весах не отбрасывает линию, а тихо перераспределяет амплитуду внутри бленда — то есть проявилась бы не отсутствием находки, а неверным ответом, который нечем поймать. Такое чинят до того, как оно понадобится.

Исправлено сменой схемы обхода: распространяется приращение доли, узел раскрывается заново на каждый новый вклад. Проверки добавлены в харнесс — Pb‑210, Bi‑210, Po‑210 теперь 0.99843.

Про 0.9984 (ждали 1) — приняли ваш упрёк: ожидание зафиксировано явно как 0.9984 с допуском 0.0005 и с причиной в комментарии (переход 234mPa → 234Pa → 234U без процента в decay_chain). Прятать расхождение за допуском было неправильно.

3. Схлопывание изомерных состояний

Принято, и хуже, чем вы насчитали: групп не 52, а 163; максимум — 98Ym1…98Ym6, шесть состояний под одним именем. Ваш аргумент про идентичность записи набора решающий.

Номер состояния сохраняется: 234PAm1Pa-234m1. Побочно ToNucid стал однозначным — перебор суффиксов убран, комментарий про неоднозначность снят. Это заодно приводит имена к тому же виду, что у pretty() в LibraryFitLab/scripts/chains.py, где номер и не терялся.

4. Дубли строк

Принято, и тоже хуже: дублей три, а не один144TBm трижды, 161PM и 35NA дважды. Дедуп по min(rowid); в списке стало 2140 нуклидов вместо 2142.

6. Мелкое

Дублирующийся абзац в SecondaryPeaks слит; четыре .pyc из __pycache__ убраны из индекса и закрыты .gitignore; ms и ns выписаны в switch явно (возвращают те же символы СИ — они одинаковы во всех языках, в отличие от us, который пишется микро‑знаком).

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

Замечание 5 — беру на себя, вот позиция

Вы задали правильный вопрос, и ответ, по-моему, такой: база остаётся файлом поставки и остаётся read-only, а пользовательская классификация переезжает в отдельный файл рядом с конфигом и накладывается поверх.

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

Почему не «оставить как есть»: правки теряются при первом же обновлении, о чём пользователь не узнает.

Наложение решает обе задачи и попутно снимает вашу приписку про 6,7 МБ бинарника в истории на каждую правку: приложение перестаёт писать в базу вообще, и она меняется только когда обновляются ядерные данные.

Это уже не правка на несколько строк, а смена пути записи в редакторе семейств плюс слой наложения в каталоге, поэтому отдельным коммитом. Делаю сам.

Ответы на два вопроса

1. Где не согласен. Нигде. Все шесть — по делу, два оказались масштабнее заявленного.

2. Кто правит. Уже поправил 1–4 и 6, пятое беру. Не потому, что не хочу делиться — а потому, что все шесть трогают код, который я на этой неделе переписывал, и мне дешевле не объяснять контекст, а сделать.

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

Что предложу вам вместо правок — и это важнее

Вы написали: «если возьмётесь за сравнение моделей или matched-filter, скажите: это как раз та задача, где можно помочь измерением, а не кодом». Беру гейт значимости на себя и ловлю вас на слове.

Напомню, что там: SignificanceZ = 4 не отличает настоящую цепочку от сдвинутой на пустое место. На сетах‑обманках фит принимает 63–79 % несуществующих линий, одинаково на девяти спектрах, трёх детекторах и статистике от 0.43 М до 61 М отсчётов; распределения Fisher z у фантомов и настоящих линий совпадают, подъём порога режет тех и других поровну, улучшение модели фона проверено прямым экспериментом и не помогает. На прогоне по 121 реальному спектру это подтвердилось вне выборки: сет‑обманка срабатывает с той же частотой, что настоящий, и даёт сопоставимое число линий.

Замена — не порог, а статистика: ΔD с поправкой Уилкса вместо теста амплитуды, либо matched‑filter по остатку с переоптимизированным фоном (в RJMCMC‑ветке это уже считается как ResidualSnr).

Чем вы поможете сильнее всего: спектрами других детекторов и проверками на них в tools/.

Сейчас вся доказательная база — три детектора (ASN16, AS80x80, RC‑103) и девять отобранных спектров. Этого хватило, чтобы показать проблему, но мало, чтобы принять её решение: новый критерий надо проверять там, где он может сломаться, а не там, где ломался старый.

Конкретно нужно:

  • спектры детекторов другого класса — прежде всего HPGe, где FWHM на порядок у́же и вырождение бленда совсем другое; LaBr₃ с собственной активностью La‑138; CdZnTe. На них ResolutionModel (√E от R₆₆₂) заведомо неточен — уже это стоит померить;
  • спектры с измеренным фоном. Сейчас таких пар всего две из девяти, а именно они позволили опровергнуть гипотезу «виновата модель континуума». Для нового критерия фон будет ключевым, и двух пар мало;
  • прогоны на сетах‑обманках по вашей же схеме (якорь настоящий, остальные линии сдвинуты на 2–4 FWHM) — это единственная метрика, которая ловит фантомы честно, и её надо иметь на каждом новом детекторе до того, как менять критерий;
  • воспроизводимый конвейер: gainscan.py в дерево, и, если позволят объёмы, сами спектры или ссылку на них — чтобы прогон повторялся не только у нас.

Формат, который лягет в tools/LibraryFitLab без переделок: каталог спектров + запись в SPECTRA в calibrate.py + строка в calibration.json. Дальше mkconfig.pyrun_sets.ps1 / run_decoy.ps1analyze.py подхватят их сами.

Если сделаете это — у смены критерия появится база, на которой её можно принимать или отвергать по числам, а не по соображениям. Это ровно та помощь, которой мне сейчас не хватает.

@Am6er

Am6er commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Два коммита в ветке: корпус спектров и продолжение работы над гейтом значимости. Полный журнал — tools/LibraryFitLab/README.md.

Корпус спектров (0d39b1f)

Девять спектров, на которых держались все числа отчёта, — одно семейство: три близких сцинтиллятора, 6.7–8.5 % на 662 кэВ, 8192 или 1024 канала. «Сравнение детекторов» на таком наборе сравнивало три прибора, у которых различались сразу и кристалл, и разрядность, и образец, так что и k·FWHM, и I_min, и связь roi-radius с числом каналов на полуширину проверялись внутри одного узкого коридора.

tools/LibraryFitLab/corpus/46 рабочих копий с 18 конфигураций детекторов, плюс сами конфигурации и манифест на каждую строку. Оригиналы в библиотеке спектров не тронуты.

Что появилось из осей, которых раньше не было:

  • Разрешение от 0.22 % (HPGe) до 15 % (Obsidian) — почти в семьдесят раз. Плюс CZT, LaBr3, SrI2, Gamma-Spectra, Atom Spectra 1 Pro, Nano 3, RadiaCode-101/103g.
  • Число каналов при неизменном всём остальном: один ториевый КИ, один Atom Spectra Nano 8, пять настроек MCA — 1024/2048/3000/4096/8192.
  • Радий из поверочного источника и спектр на 24.5 М отсчётов. Раньше радий был только в чароите — природном образце, где его количество неизвестно, и самом слабом файле набора (0.43 М).
  • Статистика от 0.14 М до 251 М отсчётов против 0.43–61 М.
  • Одиннадцать негативов, где цепочки заведомо нет: Cs-137, Am-241, K-40, Lu-176, Co-60, I-131 и пять фонов помещения. RC101_I131 — прямая ловушка: 364.5 кэВ иода лежит в полуширине от 351.9 кэВ Bi-214.
  • Измеренный фон у 33 спектров из 46 против двух из девяти.

Отбор скриптован: library_inventory.py обходит библиотеку (581 XML, 346 ResultData), library_screen.py меряет, что в спектре на самом деле есть — по именам файлов судить нельзя. build_corpus.py вынимает нужный ResultData, выбрасывает списки импульсов (пять поверочных файлов весили 168 МБ, их копии — 1.2 МБ), подшивает фон из соседнего файла, где своего нет, и проверяет энергокалибровку, сравнивая три гипотезы — оставить, поправить усиление, перефитить — на одном и том же наборе найденных пар. check_corpus.py перечитывает записанные копии и меряет, насколько пик отстоит от табличной энергии в долях FWHM — в той шкале, в которой фит и опознаёт линии: хуже 0.34 FWHM нет ничего, 45 из 46 укладываются в 0.27. Девятка скопирована байт-в-байт.

Найден и исправлен дефект харнесса. LibraryFitLab никогда не брал конфигурацию устройства: ResultData.PeakDetectionMethodConfig помечен [XmlIgnore] и инициализирован новым объектом, поэтому проверка «взять из файла, если есть, иначе из устройства» всегда выбирала первое. Все прогоны молча шли на умолчаниях класса. Для 8192-канальных сцинтилляторов это близко к правде и потому было невидимо, но германий пересыпался с 16384 каналов до 1024, полуширина становилась меньше канала, и финдер не находил ни одного пика. Трём детекторам девятки записаны ровно те умолчания, при которых считался отчёт, — число найденных пиков у них не изменилось.

Одна ложная тревога, которую я снимаю. По дороге была выдвинута гипотеза, что CDATA.ReadXml съедает следующий XML-элемент и непустой Note обнуляет настройки устройства. Проверено отдельной пробой (probes/DeviceConfigProbe.cs) на всех вариантах Note, включая записанный самим приложением, — бага нет, конфигурация читается целиком. Обе пробы оставлены в дереве, чтобы вопрос не поднимался второй раз.

Гейт значимости (8adb329)

Продолжение раздела про ΔD. Обе прежние проверки задают свой вопрос относительно одной и той же подложки — огибающей SNIP плюс фон прибора: Fisher z спрашивает, отличается ли там амплитуда от нуля, ΔD — становится ли там модель хуже без компоненты. Если структура появилась из-за того, как проведён континуум, обе отвечают «да». Отсюда и то, что подъём порога z резал настоящие и ложные линии поровну, и то, что ΔD пропускал половину обманок.

Новый тест спрашивает другое: переживёт ли линия смену модели фона. Чистая площадь гауссианы калиброванной ширины меряется дважды — над линейной и над квадратичной подложкой, подогнанной МНК только по крыльям окна и не связанной ни со SNIP, ни с фоном прибора. Амплитуда в обеих линейна, поэтому оценка всегда даёт число с честной пуассоновской ошибкой, а не упирается в нуль снизу, как координатный спуск. Соседние компоненты перед измерением вычитаются — офлайновая проверка этого не умела и именно на этом теряла настоящие линии в блендах.

Померено на всём корпусе одним свипом: 1016 сильных линий в знаменателе recall, 1285 сдвинутых линий, предъявленных сработавшему фиту.

критерий recall фантомы ложных на настоящую сверх финдера
только финдер 41.2 %
z ≥ 4 (как было) 75.3 % 68.2 % 2.53
ΔD ≥ 16 69.5 % 51.5 % 2.31
ΔD + устойчивость 59.4 % 24.7 % 1.71

Последняя колонка — то, ради чего всё считалось: сколько несуществующих линий фит приносит на каждую настоящую, которой не нашёл финдер. Без неё цифры обманывают — критерий, отвергающий всё подряд, показывает прекрасные 10 % фантомов и не приносит ничего.

Главное: тест сдвигает всю кривую, а не движется по ней. При любой геометрии соотношение лучше, чем у обоих прежних критериев. По времени — незаметно (медиана прогона 82 мс против 83 у ΔD и 85 у z).

Попутно выяснилось, что порог значимости почти ничего не решает (z от 2 до 6 двигает recall на 4 п.п.), а геометрия окна — решает: 4.5σ с крыльями от 2.5σ лучше, чем 2.6/1.5 из офлайновой проверки, где квадратика на коротком окне успевает частично повторить сам пик.

Чего это не решает

Задача не закрыта. Фит по-прежнему принимает четверть предъявленных ему несуществующих линий и приносит почти две ложных на каждую настоящую. Улучшение по соотношению — примерно четверть, не порядок.

Куда идти дальше, видно из тех же чисел:

  1. Согласованность сета целиком. Оба оставшихся критерия смотрят на компоненту по отдельности. Ни один не спрашивает, согласуются ли принятые линии между собой: у настоящей цепочки отношения площадей обязаны отвечать табличным интенсивностям с точностью до эффективности, у набора фантомов — нет. Это единственный из оставшихся критериев, который смотрит на то, чего у фантомов быть не может в принципе.
  2. Ширина. Тест меряет площадь гауссианы заданной ширины; компонента, севшая на плечо чужого пика, её наберёт. Свободная ширина с проверкой согласия с FWHM-калибровкой — дешёвое дополнение.
  3. Единая рабочая точка на восемнадцать групп — компромисс. На германии тест снимает фантомы почти нацело (85.4 % → 4.2 %), но и recall роняет до базы финдера: библиотечный фит там фактически выключается. На Obsidian и RC-101 recall вообще не зависит от критерия — он равен базе финдера, ограничение там в разрешении. Порог, зависящий от числа каналов на полуширину, напрашивается, но на одном германиевом спектре его не откалибровать.

И отдельно, из журнала корпуса: mkconfig.py научился строить сеты для всех восемнадцати групп, но полная сетка k × I_min по-прежнему гонялась только на трёх детекторах девятки — рекомендации k = 0.7 и I_min = 1 % на германии, CZT и LaBr3 никто не перепроверял.

@Am6er

Am6er commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Ничего не менял и не гонял — только разбор. Проверил локальный RAG (77 документов, 13.7k чанков, включая Lsrm-Алгоритмические основы.md, FRMAC, Рейлли, Currie) и академический поиск.

Сразу оговорка: RAG, на который ссылается присланный файл (knowledge_index.json, references/REFERENCES.md, _extracted_corpus), в этой машине недоступен — его нет ни в одном репозитории. У меня другой RAG, на D:\RAG. Поэтому дефект карточки [Vartanov-6-MultipletResolvability], о котором пишет файл, я подтвердить или опровергнуть не могу — документа Вартанова в моём индексе нет.

Что уже закрыто измерением и повторно проверять не нужно

Это важно, потому что половина предложений из обоих текстов лежит на оси, которую я вчера измерил и исчерпал.

Ось «амплитуда против своей погрешности относительно фиксированной подложки» мертва. Порог по ней ничего не решает: сканирование z от 2 до 6 двигает recall на 4 п.п., фантомы на 8 п.п., соотношение — почти не меняется. Раньше в отчёте то же показано для z от 4 до 100. Любой критерий этой формы — Currie L_c, ISO 11929, «S₁ > q√S₂» из Спектрометрии ионизирующих излучений — даст ровно то же, что уже стоит.

Поправка Уилкса на границу уже реализована. §2 присланного файла описывает то, что в коде с прошлого раза: SignificanceDeltaDeviance = SignificanceZ²= 16, и в комментарии написано именно про смесь ½·χ²₀ + ½·χ²₁ и односторонний уровень. Разбор случаев A/B верен и совпадает с тем, что сделано: библиотечный фит — случай A, финдер — случай B, и порог туда не переносится.

Улучшение модели континуума не помогает — проверено прямым экспериментом с измеренным фоном 192 ks (доля фантомов сдвинулась на ±2–6 п.п.).

Что реально сработало — смена оси на устойчивость к модели фона: 2.53 → 1.71 ложной линии на настоящую. И там геометрия окна оказалась важнее порога.

Ранжирование гипотез

1. Согласованность набора по кривой относительной эффективности — сильнейшая

Это то, на что независимо указали и мои измерения, и §3.3 файла, и оно есть в RAG в готовом виде.

LSRM §5.2.3 даёт формулу прямо: S₁/S₂ ≈ (I₁·ε₁)/(I₂·ε₂), и там же — ключевая оговорка, снимающая главное возражение: «при отсутствии данных об эффективности регистрации могут быть учтены соотношения интенсивностей в предположении, что внутри одного участка эффективность мало меняется». Именно на этом уже стоит BR-связка (0.85·FWHM). Расширение — на весь спектр.

Возражение «нужна калибровка по эффективности, а её в корпусе нет» снимается техникой относительной эффективности: RE(E) ∝ C(E)/BR, важна только форма кривой, и строится она по самим данным [5], [6]. У настоящей цепочки в вековом равновесии все линии делят одну активность, значит точки S_i/I_i обязаны лечь на одну гладкую кривую. У сета-обманки амплитуды набраны из шума — лечь не могут по построению.

Почему это сильнее всего остального:

  • Другая ось. Не «велика ли амплитуда», а «согласуются ли линии между собой». Ни один из проверенных критериев на неё не смотрит.
  • Уровень набора, а не линии. Метрика, которую я меряю (доля принятых фантомов), — это свойство набора. Критерий той же размерности может забраковать весь фантомный сет сразу.
  • Ровно эту схему применяют работающие алгоритмы идентификации — Kaissas et al. [1] описывают исключение ложных идентификаций как двойную проверку: присутствуют ли все самые интенсивные линии нуклида и согласуются ли высоты пиков с выходами.
  • Наши цели — цепочки на ~20 линий, идеальный случай для такого теста.

Чего это стоит и что может сломать:

  • Каскадное суммирование. FRMAC прямо предупреждает, что кривая относительной эффективности его не компенсирует, а для Th-232 и Ra-226 в близкой геометрии оно реальное. Настоящая цепочка может завалить тест. [4] показывает, как корреляции эффективности и неопределённости интенсивностей раздувают неопределённость именно у нуклидов с многими линиями.
  • Нужно ≥4 принятых линии, чтобы осталась степень свободы после свободной RE-кривой (2–3 параметра).
  • Меняется семантика гейта: сейчас он на линию, а тут — на набор, и нужно правило, что делать при провале.

2. Индекс доверия LSRM — не тест, а число для отчёта

RAG подтверждает формулу и Таблицу 14-1 дословно, включая NaI: Cs-137 = 1.8, Th-232 = 16.6, Eu-152 = 18.3. Но там же авторы пишут: «введенный таким образом индекс доверия носит эвристический характер». δI в нём — та же невязка интенсивностей, что и в пункте 1, только свёрнутая в логарифм произведения без калиброванного нулевого распределения.

Вывод: CI — отличная пользовательская величина (порядок «Th-232 = 16.6 против Cs-137 = 1.8» ровно то, чего ждёшь от цепочки), но как решающее правило он слабее честного χ² по RE-кривой. Считать обе, показывать CI, решать по χ².

3. FDR — правильная рамка, но строго после калибровки

Наблюдение, которого нет ни в одном из присланных текстов: доля фантомов, которую я меряю, и есть FDR — ожидаемая доля ложных среди отвергнутых нулевых гипотез [1]. Библиотечный фит проверяет ~20 линий одновременно, то есть это буквально задача множественной проверки, и «обнаружение пиков как множественная проверка» — established постановка [9], [6].

Но BH требует калиброванных p-значений при H₀, а мои измерения показывают, что per-line статистика откалибрована плохо: распределения z у фантомов и настоящих линий совпадают. BH — поправка на множественность, а не на калибровку, поверх сломанного p она даст сломанный FDR.

Отдельно: BH корректен при независимости или положительной зависимости [2], а компоненты в бленде делят амплитуду и потому зависимы отрицательно. Понадобится Бенджамини–Иекутиели или e-value-замыкание [8].

Ценность высокая, но отложенная: это ручка, которую на самом деле хочет пользователь («принимаю не больше 10 % ложных линий») вместо произвольного z.

4. Уточнение порога ΔD: соседи тоже на границе

Это то, чего в присланном файле нет, и оно прямо следует из литературы. В DevianceGain соседи при выключении компоненты перефитываются с амплитудой, зажатой нулём снизу, — то есть это мешающие параметры, тоже лежащие на границе. Kopylev et al. [6] специально разбирают эту конфигурацию и предупреждают: вопреки распространённому мнению, при мешающих параметрах на границе наивный подход может оказаться не консервативным, а анти-консервативным. Общая характеризация весов смеси при произвольном числе мешающих параметров на границе получена только недавно [1].

Практический смысл: порог z² = 16 мог быть занижен, и часть фантомов ΔD пропускал именно поэтому. Проверяется дёшево — параметрическим бутстрапом нулевого распределения на реальных спектрах [8], без всякой асимптотики.

5. Ширина как отдельная ось — дёшево, но узко

Мой тест формы меряет площадь гауссианы заданной ширины: компонента, севшая на плечо чужого пика, её наберёт. Свободная ширина с проверкой согласия с FWHM(E) закрывает эту дыру. RAG поддерживает: без физической привязки ширины «алгоритмы с неизвестным числом компонент начинают объяснять асимметрию или фон лишними пиками».

Оговорка: в конфигурациях корпуса Min_FWHM_Tol/Max_FWHM_Tol = [1, 199], то есть ширинный фильтр финдера фактически выключен, и это унаследованное умолчание. На 1024 каналах ширина определяется плохо.

6. Окна — дёшево, частично подтверждено, но не главное

Аргумент §4 файла ослабляется тем, что LSRM §6 даёт окно идентификации как ΔE = Δ₀E/√661 · √E, то есть с той же √E-зависимостью, что и FWHM. Мои константы — доли FWHM, значит масштабирование уже правильное; под вопросом только множитель, единый для диапазона разрешения в 70 раз.

Косвенное подтверждение, что это не пусто: в моём свипе геометрия окна оказалась важнее порога (2.6σ против 4.5σ — 4.5 п.п. recall), и разбивка по детекторам показывает, что единая точка плохо сидит на краях (HPGe: recall 87→38 %; Obsidian и RC-101: recall не меняется вовсе ни от какого критерия). Утверждение про ORTEC (2.0 FWHM для NaI против 3.5 для HPGe, выключенный по умолчанию тест критического уровня) в моём RAG не подтверждается — источника нет, считаю непроверенным.

7–9. Слабые для этой задачи

  • RJMCMC как критерий. Уже частично в дереве, и отчёт уже показал: деконволюция поверх сета даёт ноль прироста, на RC-103 вредит. Эталон 9999/10000 из ISMA2014 получен на данных, порождённых самой моделью — это проверка сходимости сэмплера, а не устойчивости к неверной модели. Наши фантомы живут именно за счёт неверной модели континуума, поэтому цифра не переносится.
  • AIC/BIC. Та же правдоподобностная ось с другим штрафом; AIC уже используется в замене бленда. Байесовский фактор — то же плюс Occam-множитель, который перефит соседей в ΔD уже приближает, но дороже на порядок.
  • Нейросетевой адаптивный порог. 46 спектров, 18 типов детекторов, разметки нет. Непроверяемо и необъяснимо; ANN-идентификация [3] обучается на симуляции, а у нас как раз симуляции нет.
  • Mariscotti, вейвлеты, вторые разности, SNIP. Это про финдер (случай B), а не про библиотечный гейт. SNIP уже стоит, и его улучшение проверено — не помогает.

Что я предлагаю считать планом

Порядок такой, и он не про «попробовать всё», а про то, что каждый следующий шаг имеет смысл только после предыдущего:

  1. Сначала — согласованность по RE-кривой. Единственный кандидат на новой оси с прямым обоснованием в LSRM и в практике изотопного анализа. До кода нужен один расчёт на бумаге: сколько степеней свободы остаётся у типичного корпусного сета после свободной RE-кривой на 2–3 параметра, и не съедает ли каскадное суммирование весь бюджет на Th-232 в близкой геометрии.
  2. Параллельно и дёшево — бутстрап нулевого распределения ΔD на реальных спектрах вместо асимптотики. Отвечает на вопрос, не занижен ли текущий порог из-за соседей на границе [6].
  3. Потом — FDR поверх того, что получится. Только когда per-line статистика откалибрована; иначе это косметика.
  4. Ширина — как дешёвое дополнение, не как основной критерий.
  5. Окна — не как гипотеза о фантомах, а как отдельная работа про зависимость рабочей точки от числа каналов на полуширину. Для неё в корпусе не хватает второго германиевого спектра: сейчас HPGe представлен одним файлом, и калибровать по нему зависимость нельзя.

И одно замечание про критерий приёмки из §5 файла. Планка «выше нескольких процентов на обманке — критерий не решает задачу» взята из ISMA2014, то есть с модельных данных. На реальных спектрах с неверной моделью континуума она недостижима: мой самый агрессивный вариант даёт 10.3 % фантомов и при этом добавляет 18 настоящих линий на весь корпус, то есть не работает вовсе. Осмысленная планка — ложных на настоящую сверх финдера, а не доля фантомов сама по себе.

Ссылки

[1] Asymptotic distribution of the likelihood ratio test statistic with inequality-constrained nuisance parameters (Salucci, 2026, 0 citations)
[2] THE CONTROL OF THE FALSE DISCOVERY RATE IN MULTIPLE TESTING UNDER DEPENDENCY (Benjamini & Yekutieli, 2001, 10776 citations, Annals of Statistics)
[3] Radionuclide identification method for NaI low-count gamma-ray spectra using artificial neural network (Qi et al., 2021, 27 citations, Nuclear Engineering and Technology)
[4] Effects of efficiency correlations and intensity uncertainties on interference correction for gamma spectrometry (Persson et al., 2022, 0 citations, J. Radioanal. Nucl. Chem.)
[5] Relative efficiency calibration for determining isotopic composition and age of HEU items by passive non-destructive gamma spectrometry (Le et al., 2018, 3 citations, Applied Radiation and Isotopes)
[6] On the asymptotic distribution of likelihood ratio test when parameters lie on the boundary (Kopylev et al., 2011, 20 citations, Sankhya B)
[7] Controlling the false discovery rate: a practical and powerful approach to multiple testing (Benjamini & Hochberg, 1995, 106398 citations, JRSS-B)
[8] Bringing Closure to False Discovery Rate Control: A General Principle for Multiple Testing (Xu et al., 2025, 16 citations)
[9] Peak Detection as Multiple Testing (Schwartzman et al., 2010, 6 citations, arXiv: Methodology)

Из локального RAG: Lsrm-Алгоритмические основы.md §5.2.3 (соотношения интенсивностей), §6 (окно идентификации), §14.3 (индекс доверия, Табл. 14-1); FRMAC_GammaSpec_KnowledgeGuide_2019-08 §7.2–7.3 (L_c, L_d) и §14.5 (каскадное суммирование против RE-кривой); Duglas_Raylli_gamma_neytrony.md §8.4.1 (кривая относительной эффективности по самим данным).

@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Отчёт по проверке намеченного плана. Коммит c63f1ff. Все прогоны — по полному корпусу (46 спектров, 18 групп детекторов); это теперь зафиксировано правилом в README проекта.

Итог

Из пяти гипотез сработала первая, но не в той форме, в какой предлагалась. Проверка согласованности по кривой относительной эффективности как критерий для ОТДЕЛЬНОЙ линии не даёт ничего сверх уже имеющегося. Она же, применённая к НАБОРУ целиком, — лучший результат за всю работу над гейтом.

критерий recall фантомы ложных на настоящую
только финдер 41.2 %
z ≥ 4 (исходный) 75.3 % 68.2 % 2.53
ΔD ≥ 16 69.5 % 51.5 % 2.31
устойчивость к фону 62.8 % 32.6 % 1.91
ΔD + устойчивость (прежнее умолчание) 59.4 % 24.7 % 1.72
z + вето по набору, 1.25 59.4 % 5.7 % 0.40
z + вето по набору, 1.5 66.7 % 15.4 % 0.76
ΔD + устойчивость + вето, 1.25 55.4 % 13.1 % 0.92

Вето при 1.25 даёт ровно тот же recall, что прежнее умолчание, при вчетверо меньшей доле фантомов. При 1.5 оно превосходит его по обеим осям сразу. Доля принятых несуществующих линий упала с 68.2 % у исходного критерия до 5.7 % — в двенадцать раз; соотношение ложных к настоящим улучшилось в шесть раз. По времени бесплатно: 80 мс против 84.

Что проверяется

У настоящей цепочки в вековом равновесии все линии делят одну активность, поэтому площадь каждой обязана равняться A · I(E) · ε(E), и точки S/I ложатся на одну гладкую кривую. Форма ε(E) заранее не нужна — фитится вместе с активностью, важна только гладкость. Тот же приём, что в изотопном анализе (Рейлли, гл. 8: RE(E) ∝ C(E)/BR), и та же формула, что в ЛСРМ §5.2.3.

У сета-обманки интенсивности табличные, а площади набраны из того, что случайно оказалось на сдвинутой энергии. На общую кривую они лечь не обязаны — и не ложатся.

Две неудачные постановки до кода

Гипотеза сначала проверялась на корпусе напрямую (scripts/re_curve_check.py), и обе первые постановки провалились по причинам, к гипотезе отношения не имеющим.

χ²/dof кривой по всем линиям набора дал 1172 у настоящих цепочек против 1944 у обманок — бессмыслица. Разбор: линейная подложка на крутом комптоновском континууме давала отрицательные площади у половины линий; логарифм такие точки выбрасывал, и у обманки оставались только случайные попадания с огромными погрешностями (χ²/dof = 0.2 у обманки ASN8_8192 — «обманка согласована лучше настоящей цепочки»); а при 10⁸ отсчётов пуассоновская погрешность — доли процента, так что 10 % систематики дают χ²/dof в сотни. Тест проверял «точна ли модель», а не «согласован ли набор».

Кривая по сильным линиям, слабые проверяются против неё — разделение есть, но не лучше того, что уже стояло в production, и кривую не удалось построить для 26 пар «спектр × цепочка» из 54, в том числе ни для одной у HPGe, Obsidian, RC-101 и RC-103.

Попутно измерен систематический пол: 24 % (медиана; худший случай 58 %). Даже сильные одиночные линии настоящей цепочки настолько расходятся с гладкой кривой — каскадное суммирование, интерференция, погрешности табличных интенсивностей. Ниже этого порог опускать нельзя.

Что сработало

Разброс, посчитанный для набора целиком: настоящие цепочки — медиана 74 % (квартили 51…99 %), обманки — 156 % (116…237 %). Отдельная слабая линия измеряется плохо и почти ничего не говорит; десяток таких линий, взятых вместе, говорит много.

Неожиданное: строгий отсев мешает вету

Комбинация «ΔD + устойчивость + вето» хуже, чем «z + вето»: 13.1 % фантомов против 5.7 % при более низком recall. Вету нужно не меньше четырёх принятых линий, чтобы построить кривую, а пофайловые критерии лишают его точек — и на части наборов оно просто не может судить. Строгие критерии выключают голодом тот, что работает лучше их обоих.

Поэтому в production идёт «z по линии + вето по набору», а ΔD и тест устойчивости выключены умолчанием. Они не ошибочны — они проигрывают.

По детекторам

Вето превосходит прежнее умолчание по обеим осям на 11 группах из 17 и даёт ноль фантомов на десяти. Примеры: CZT 100 % / 0 % против 82.8 % / 15.5 %; SrI2 96.2 % / 0 % против 84.6 % / 35.7 %; AS80x80 46.2 % / 5.1 % против 40.9 % / 26.1 %.

Где хуже — и это интереснее: HPGe, ASN8 1024 и ASN8 3000 откатываются ровно к базе финдера, вето срабатывает и на настоящем наборе. У германия причина понятна (головы рядов U-238 и U-235 дают мало сильных линий). А вот у ASN8 странно: 1024 и 3000 вето заваливает, а 2048 и 4096 на том же источнике, том же приборе и том же образце дают 95.5 %. Разница только в пересыпке MCA. Это прямое указание, что порог зависит от числа каналов на полуширину — ровно тот вопрос, ради которого лестница ASN8 в корпус и бралась.

Остальные шаги плана

Шаг 2, калибровка порога ΔD — закрыт данными, бутстрап не понадобился. Сет-обманка и есть эмпирическая H₀ на реальных данных: при пороге ΔD ≥ 16 (номинальный односторонний z = 4, α ≈ 3·10⁻⁵) проходит 51.5 % линий обманки. Расхождение с номиналом — четыре порядка, на таком фоне поправка на границу области (смесь ½·χ²₀ + ½·χ²₁, разница вдвое) не имеет значения. Неверна не поправка и не эталонное распределение, а сама нулевая гипотеза: «амплитуда равна нулю ПРИ ПРАВИЛЬНОМ континууме», а континуум неправилен.

Шаг 3, FDR — отложен обоснованно. Он и планировался после калибровки per-line статистики; она не откалибрована и не станет — вето работает не через p-значение линии, а через свойство набора. Осмысленная постановка теперь другая: контролировать долю ложных НАБОРОВ, и вернуться к ней, когда вето станет выдавать величину, а не бинарное решение.

Шаги 4 и 5 (ширина, окна) — не проверялись, приоритет сместился. У шага 5 теперь есть конкретный адрес: расхождение внутри лестницы ASN8.

Чего это не решает

  • 5.7 % несуществующих линий всё ещё принимаются; 0.40 ложной на каждую настоящую сверх финдера. В шесть раз лучше, но не ноль.
  • Вето бинарно: снимает набор целиком либо пропускает целиком. Набор, где девять линий согласованы, а одна — фантом, проходит целиком. Продолжение — исключать линии по одной с платой за каждую.
  • Когда вето не может судить (меньше четырёх линий), не судит никто. Пофайловые критерии выключены, а включать их обратно нельзя — они же и лишают вето точек. Правильная конструкция: тест устойчивости как ЗАПАСНОЙ, включающийся только там, где вето воздержалось. Не измерено.
  • Порог 1.25 подобран, а не выведен: систематический пол в 24 % измерен, но связь с рабочим порогом эмпирическая.

Отдельно исправлено по дороге: при срабатывании вето фит обязан отменять и замену пиков финдера линиями bound-групп — иначе он уносит центроид бленда, ничего не давая взамен, и recall проваливается ниже базы финдера (на HPGe было 23.1 % против 28.2 %).

@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Спектры spectravibe-toolkit приняты в корпус

Спасибо за отзыв и за комплект — забрал 23 спектра из
VibeEngineering-LLC/spectravibe-toolkit.
Корпус: 46 → 69 спектров, 18 → 23 группы детекторов. Коммит c115289.

Брал не файлы, а оси, которых у корпуса не было.

Геометрия — впервые. Комплект поверки Гамма-1С даёт один и тот же аттестованный
ториевый источник в трёх сосудах (Дента 120 мл, маринелли 1 л, Петри 60 мл) и точечные
на 5 и 25 см. Это ровно то, чего не хватало: каскадное суммирование держит
систематический пол вето по согласованности набора (24 % разброса вокруг кривой
эффективности), а проверить это было не на чем — в корпусе не существовало ни одной
пары «то же самое, но в другой геометрии».

Паспортные активности — впервые. Th-232 1940 Бк/кг ±6 %, Ra-226 1780, K-40 2530,
точечные в беккерелях. Раньше корпус знал «что в образце есть», но не «сколько».

Eu-152 — четырнадцать линий одного нуклида от 122 до 1408 кэВ, на трёх детекторах.
Не цепочка, но объект того же типа: одна активность на все линии. Предельный случай для
критерия согласованности.

Th-228 — укороченный ториевый ряд. Ac-228 в аттестованном источнике нет, линий
911/969/338 кэВ в спектре нет. Завёл отдельной цепочкой: считать его рядом Th-232 значит
записать в знаменатель recall линии, которых там быть не может.

Второй и третий германий, второй LaBr3, второй CZT. Журнал прямо просил второй HPGe:
прежний несёт головы рядов U-238/U-235, где сильных линий почти нет и вету не из чего
строить кривую. У новых есть ториевая цепочка.

Приёмка: прошли все 23, медианная невязка от 0.006 до 0.15 FWHM — заметно лучше среднего
по корпусу. Аттестованные источники на поверочном комплексе, калибровки честные.

Про полином 5-й степени — дефект подтвердился и оказался хуже, чем в отзыве

PolynomialEnergyCalibration.ChannelToEnergy разбирал только степени 4, 3 и 2, всё
остальное молча уходило в линейную ветку:

if (this.polynomialOrder == 4) { ... }
if (this.polynomialOrder == 3) { ... }
if (this.polynomialOrder == 2) { ... }
return this.coefficients[1] * n + this.coefficients[0];   // сюда попадала 5-я степень

На Y88-SRC-05-25cm.xml шкала уезжала до 5.6 кэВ на верхнем конце — на германии это
пятнадцать полуширин, и ни одного сообщения об ошибке.

Вторая половина серьёзнее и в отзыве не названа: обратное преобразование
EnrgToChannel выше 4-й степени бросало NotImplementedException, а DetectPeak
вызывается под catch-all в DCPeakDetectionView — то есть на таком спектре поиск пиков
молча не работал совсем. Не «энергии смещены», а «пиков нет».

Исправлено обоими концами: прямое — схемой Горнера по всем имеющимся коэффициентам,
обратное — тем же поиском корня, что для 3-й и 4-й. Ветки 4/3/2/1 оставлены как были,
чтобы не сдвинулись уже посчитанные числа. Регрессия — probes/CalibrationProbe.cs,
сверяет шкалу приложения с независимо посчитанным полиномом и проверяет замыкание
обратного преобразования. Файл Y-88 в корпус не взят (две линии, мерить нечего), оставлен
в библиотеке как фикстура к этой правке.

Два других замечания подтвердились и правки не потребовали: наш scripts/spectrum.py
строит полином по длине массива коэффициентов и 5-ю степень читает верно — дефект был
только в приложении; SqrtFwhmCalibration в этих файлах не записана, её и так считает
сборщик корпуса по модели разрешения группы. Вшитый <BackgroundEnergySpectrum>
подхватывается: у 21 из 23 новых спектров фон свой.

Прогон на расширенном корпусе

По полному корпусу из 69 спектров: 1371 сильная линия в знаменателе recall, 2201
сдвинутая линия обманок.

критерий recall фантомы ложных на настоящую
только финдер 44.1 %
z ≥ 4 77.2 % 63.7 % 3.10
ΔD + устойчивость к фону 60.9 % 22.4 % 2.14
z + вето/1.25 64.0 % 9.7 % 0.78
z + вето/1.0 58.4 % 6.4 % 0.72
z + вето/1.5 69.4 % 18.5 % 1.17

Вывод стал крепче: вето превосходит связку «ΔD + устойчивость» по обеим осям сразу.

Но пороги сместились. На 46 спектрах то же вето при 1.25 давало 5.7 % фантомов,
теперь 9.7 %, и лучшее соотношение переехало с 1.25 на 1.0. Критерий не менялся —
изменился корпус. Это довод в пользу правила «всякое измерение — по полному корпусу»,
которое теперь записано в README проекта. Умолчание оставил 1.25: оно единственное
строго превосходит прежнее производственное по обеим осям.

Самое интересное — что показали новые группы

детектор финдер z ΔD + устойчивость z + вето/1.25
G1S Гамма-1С 61.1 % 94.7 % / 60.0 % 80.9 % / 25.7 % 94.7 % / 19.2 %
G1S (вето 1.0) 94.7 % / 6.5 %
HPGE_GMX 32.4 % 59.8 % / 42.7 % 41.2 % / 1.8 % 59.8 % / 7.3 %
HPGE_GEM 89.7 % 94.8 % / 31.7 % 91.4 % / 2.9 % 94.8 % / 21.2 %
LABR_BRIL 32.8 % 82.8 % / 57.5 % 48.4 % / 15.0 % 53.1 % / 5.3 %

На аттестованных источниках вето не стоит ничего. У Гамма-1С 94.7 % — ровно столько
же, сколько у самого разрешающего критерия, при вдесятеро меньшем числе фантомов (6.5 %
против 60.0 % при пороге 1.0). То же у обоих германиев: recall не отличается от z.

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

Что теперь проверяемо, а раньше не было

  • Порог вето против каскадного суммирования. Th-228 и Eu-152 на 5 и на 25 см — один
    источник, один детектор, разное суммирование. Если систематический пол на 5 см растёт,
    а на 25 см падает, порог обязан зависеть от геометрии, а не быть константой.
  • Порог против числа каналов на полуширину. Ториевая цепочка теперь есть на 1024
    каналах (Гамма-1С, LaBr3), на 4095 (CZT) и на 8192 (два германия). Аномалия лестницы
    ASN8 — 1024 и 3000 вето заваливает, 2048 и 4096 дают 95.5 % на том же источнике —
    проверяема на независимом материале. Это же отвечает на предложение про ребиннинг.
  • Абсолютная активность — по паспорту.
  • Многолинейчатый нуклид против цепочки — Eu-152 и Ba-133.

Про предложение прогнать обманки у себя: генератор и метрика лежат в
tools/LibraryFitLab/scripts/gate_study.py, состав корпуса — в corpus_def.py,
import_vibe.py держит таблицу «файл вверху по течению → имя у нас», так что происхождение
каждого из 23 спектров прослеживается. Буду рад сверить числа.

Подробности со всеми промежуточными результатами — в журнале
tools/LibraryFitLab/README.md, раздел «Пополнение корпуса: аттестованные источники».

@Verter73

Copy link
Copy Markdown
Collaborator Author

Гейт значимости на расширенном корпусе: спектры аттестованных источников, лестница по числу каналов

Отчёт о прогоне, сделанном по прямой просьбе из комментария к PR #32 от 26.07:
«Чем вы поможете сильнее всего: спектрами других детекторов и проверками на них
в tools/». Сделаны обе части — и спектры, и проверки на них тем же
инструментом, что и всё остальное в отчёте LibraryFitLab.

Дата прогона: 27.07.2026. Ветка roi-wizard-reworked, коммит c63f1ff.
Все числа получены харнессом LibraryFitLab.exe, собранным из этой ветки;
никакой собственной реализации критериев не писалось.


1. Коротко

  1. База воспроизведена точь-в-точь. Все восемь строк таблицы из отчёта о
    согласованности набора сошлись до десятой доли процента. Значит числа ниже
    сопоставимы с вашими, а не «похожи».

  2. Корпус расширен с 46 до 63 спектров и с 18 до 24 групп детекторов.
    Выводы устояли.
    Ни один критерий не сдвинулся больше чем на три пункта,
    порядок критериев тот же, вето по-прежнему лучшее.

  3. Провал вето на германии объяснён и измерен. Ваше предположение верно:
    дело не в германии, а в оборванной цепочке. На германии с полным рядом
    Th-232 вето держится выше базы финдера и снимает почти все фантомы.

  4. Расхождение первое: на германии выключенный вами shape лучше вето.
    При том же recall фантомов вдвенадцатеро меньше — 2,0 % против 25,5 %.

  5. Расхождение второе: порог 1.25 — функция разрешения, а не константа.
    На германии переход 1.0 → 1.25 роняет качество вчетверо, на сцинтилляторах
    почти безразличен.

  6. Лестница по числу каналов пройдена на одном измерении. Вето перестаёт
    работать при ~2 каналах на полуширину — но только там, где сетка перестаёт
    разделять линии. На германии при той же плотности каналов оно работает
    идеально. Немонотонность вашего ряда ASN8 плотностью каналов не объясняется.


2. Воспроизведение вашей базы

Прогон gate_study.py --run + --report на неизменённом корпусе из 46 спектров:

критерий recall фантомы у вас
финдер 41,2 % 41,2 %
z 75,3 % 68,2 % 75,3 / 68,2
dd 69,5 % 51,5 % 69,5 / 51,5
shape 62,8 % 32,6 % 62,8 / 32,6
dd+shape 59,4 % 24,7 % 59,4 / 24,7
z+chain/1.0 54,6 % 5,1 % 54,6 / 5,1
z+chain/1.25 59,4 % 5,7 % 59,4 / 5,7
z+chain/1.5 66,7 % 15,4 % 66,7 / 15,4
всё/1.25 55,4 % 13,1 % 55,4 / 13,1

Полный вывод — приложение logs/01_report_total_46_baseline.txt.

Две вещи пришлось поправить, чтобы прогон вообще запустился у постороннего.
Они не влияют на результат, но без них конвейер не воспроизводится:

  • scripts/chains.py:19 держит абсолютный путь к nucdb.sqlite на вашей
    машине. Без правки падает всё, что строит сеты.
  • mkconfig.py ждёт scripts/calibration.json, а тот в .gitignore как
    промежуточный. Оригинал лежит в data/calibration.json — достаточно
    скопировать, но из репозитория это не видно.

3. Что добавлено в корпус

Источник — публичный набор VibeEngineering-LLC/spectravibe-toolkit: спектры
аттестованных источников ЛСРМ, уже в формате BecqMoni XML, с паспортными
активностями в описях. Взято 17 спектров в шести новых группах.

группа детектор R(662) спектры зачем
GEM20 HPGe 20 % коаксиальный, 8192 к. 0,20 % Th-232 и Ra-226 в Маринелли, Th-228 точечный, Cs-137 негатив аттестованный объёмный торий в равновесии: полный ряд, включая 911/969 Ac-228
HandyHPGe HPGe портативный, 8192 к. 0,21 % Th-232, Th-228, Co-60 негатив тот же торий на втором германии другой модели
SimpleHPGe HPGe планарный, 8191 к. 0,21 % Th-228, Eu-152 негатив третий класс германия
HandyLaBr LaBr₃ портативный, 1024 к. 3,39 % Th-228, фон, Ba-133 негатив LaBr₃ с аттестованным источником; у вас LaBr₃ только руда без паспорта
HandyNaI NaI портативный, 1024 к. 5,31 % Th-232 цепочка на 1024 каналах сцинтиллятора
G1S NaI 63×63, 1024 к. 7,34 % Th-232, Ra-226, Cs-137 и K-40 негативы фон той же геометрии измерен и лежит в том же файле

Отдельно про последнюю строку: вы писали, что пар с измеренным фоном всего две
из девяти и этого мало. В наборе Гамма-1С фон измерен для каждой геометрии и
подшит в файл образца, так что здесь таких пар сорок.

Ось, которой не было

Одна и та же цепочка Th-232 на шести классах детекторов при разрешении,
различающемся в тридцать раз — от 0,20 % до 7,34 % на 662 кэВ. Плюс зеркало к
урановому стеклу: спектры Th-228, где ряд оборван сверху (нет линий Ac-228),
в той же геометрии, что и полный Th-232.

Сборка и приёмка

Спектры прошли штатный конвейер: corpus_def.pybuild_corpus.py
check_corpus.py, никаких обходных путей. Приёмка (logs/02_check_corpus.txt):

группа линий медиана невязки, FWHM
GEM20 1…16 0,045…0,217
HandyHPGe 3…10 0,009…0,099
SimpleHPGe 6…9 0,009…0,056
HandyLaBr 4…7 0,132…0,571
G1S 2…5 0,043…0,199

Слабое место — LaBr₃: медиана 0,571 FWHM у фона и 0,333 у Th-228. Это выше
вашего порога приёмки, числа по этой группе стоит считать ориентировочными.

Один спектр отвергнут. Th-228 на портативном NaI: калибровка не легла ни
одним из трёх кандидатов, ноль принятых линий, невязка 1,18 FWHM. Источник в
свинцовом контейнере, низ спектра поглощён, а верх не описывается ни поправкой
масштаба, ни полиномом по двум точкам. Причина записана в corpus_def.py рядом
с местом, где стояла запись, — в вашем же стиле.


4. Результат A: выводы устойчивы при расширении корпуса

63 спектра, 24 группы, 1289 опорных линий против 1016.

критерий 63 спектра 46 спектров
финдер 43,4 % 41,2 %
z 75,0 / 65,0 75,3 / 68,2
dd 70,6 / 51,9 69,5 / 51,5
shape 62,4 / 28,3 62,8 / 32,6
dd+shape 59,5 / 21,5 59,4 / 24,7
z+chain/1.0 56,2 / 4,8 54,6 / 5,1
z+chain/1.25 60,2 / 8,5 59,4 / 5,7
z+chain/1.5 66,0 / 16,0 66,7 / 15,4
всё/1.25 56,4 / 11,8 55,4 / 13,1

Это ровно то, чего вы хотели от расширения: критерий проверен там, где мог
сломаться — на полупроводниках, на другом классе сцинтилляторов, на
аттестованных источниках вместо природных образцов, — и не сломался.

Полный вывод — logs/03_report_total_63.txt, разбивка по группам —
logs/04_report_per_det_63.txt.


5. Результат B: провал вето на германии — из-за оборванной цепочки

Вы писали: «У германия причина понятна (головы рядов U-238 и U-235 дают мало
сильных линий)». Проверка прямым сравнением внутри одного класса детектора:

группа что в образце база финдера z z+chain/1.0
HPGE (ваш) урановые пуговицы, ряд оборван снизу 28,2 % 87,2 / 85,4 28,2 / 0,0
GEM20 аттестованный Th-232, полный ряд 74,2 % 92,1 / 47,0 77,5 / 5,4
HandyHPGe Th-232 и Th-228 38,7 % 82,3 / 46,7 56,5 / 0,0

На германии с полной цепочкой вето не откатывается к базе финдера: 77,5 %
против 74,2 % и 56,5 % против 38,7 %. Разваливает его не германий, а число
сильных линий, доступных для построения кривой.

Это же объясняет, почему на вашем HPGe вето снимает набор целиком: у головы
ряда U-238 линий для кривой просто не набирается, и разброс мерить не по чему.


6. Результат C: на германии выключенный вами критерий лучше вето

группа shape (выключен умолчанием) dd+shape z+chain/1.25 (в production)
GEM20 76,4 / 2,0 76,4 / 1,3 77,5 / 25,5
HandyHPGe 54,8 / 1,7 54,8 / 1,7 56,5 / 23,3
HPGE (ваш) 38,5 / 4,2 38,5 / 3,1 28,2 / 0,0

Вы выключили тест устойчивости к модели фона умолчанием с формулировкой «они не
ошибочны — они проигрывают». На корпусе, где почти всё — сцинтилляторы, это
верно. На полупроводнике картина обратная: при одинаковом recall фантомов у
shape в двенадцать раз меньше.

Ваша собственная заметка — «правильная конструкция: тест устойчивости как
ЗАПАСНОЙ, включающийся только там, где вето воздержалось» — недооценивает
случай. На германии он не запасной, а лучший из имеющихся.

Практическое следствие: выбор критерия разумно сделать зависящим от класса
детектора, а не глобальным.


7. Результат D: порог 1.25 — функция разрешения

Вы отметили: «Порог 1.25 подобран, а не выведен». Видно, откуда это берётся:

группа R(662) фантомы при 1.0 при 1.25 при 1.5
GEM20 0,20 % 5,4 % 25,5 % 25,5 %
HandyHPGe 0,21 % 0,0 % 23,3 % 23,3 %
HandyLaBr 3,39 % 0,0 % 0,0 % 0,0 %
G1S 7,34 % 7,8 % 17,8 % 17,8 %
CZT (ваш) 1,4 % 0,0 % 0,0 % 0,0 %

На германии шаг от 1.0 к 1.25 роняет качество вчетверо-впятеро, на
сцинтилляторах почти безразличен.

Физически это ожидаемо. Разброс точек S/I вокруг гладкой кривой у настоящей
цепочки складывается из статистики, вычитания континуума и систематики
(каскадное суммирование, интерференция, погрешности табличных интенсивностей).
Чем лучше разрешение, тем меньше первые два вклада и тем плотнее настоящая
цепочка ложится на кривую — значит и допуск на разброс должен быть у́же.

Ваш измеренный систематический пол в 24 % (медиана, худший случай 58 %) снят на
корпусе, где германий представлен одним спектром. Корпус теперь позволяет
измерить этот пол отдельно по классам разрешения и связать порог с ним, а не
подбирать.


8. Результат E: лестница по числу каналов на одном измерении

Ваша аномалия: на лестнице ASN8 вето заваливает 1024 и 3000 каналов и даёт
95,5 % на 2048 и 4096 при том же приборе, источнике и образце. Вывод, который
вы из неё сделали: рабочая точка зависит от числа каналов на полуширину.

Слабое место такого ряда в том, что его ступени — разные измерения: вместе с
числом каналов менялись статистика на канал, мёртвое время и момент времени.
Здесь ступени получены арифметической пересыпкой одного измерения, поэтому
меняется только сетка.

Как сделано

Отсчёты группируются по N подряд и суммируются; энергетическая калибровка
пересчитывается подстановкой n → N·n (k-й коэффициент умножается на N^k
для полинома это точно, без подгонки); FWHM-калибровка, заданная в каналах,
пересчитывается как c0/N², c1/N, c2. Фон пересыпается тем же множителем.

Проверка на Th-232 Гамма-1С: полное число отсчётов сохранено (702 463 → 702 463),
положения линий 238, 585 и 2602 кэВ совпали до 0,00 кэВ, ширина в каналах ровно
вдвое меньше.

Каждая ступень дальше прошла штатный build_corpus.py — то есть калибровалась
и принималась тем же кодом, что и остальной корпус.

Лестница NaI 63×63

Знаменатель recall одинаков на всех ступенях (42 линии), сравнение чистое.

каналов каналов на FWHM финдер z z+chain/1.0 z+chain/1.25
1024 17,2 61,9 % 97,6 / 66,7 97,6 / 7,8 97,6 / 17,8
512 8,0 64,3 % 97,6 / 72,3 81,0 / 0,0 81,0 / 0,0
256 3,8 48,8 % 83,7 / 62,5 67,4 / 0,0 83,7 / 0,0
128 2,1 45,2 % 90,5 / 70,5 90,5 / 18,2 90,5 / 70,5

При 2,1 канала на полуширину вето с рабочим порогом 1.25 перестаёт работать
полностью
: 70,5 % фантомов — ровно столько же, сколько даёт голый z. Оно не
отсекает ничего.

При этом порог 1.0 на той же сетке ещё держится — 18,2 %. То есть грубая сетка
не просто сдвигает оптимум, а меняет, какой порог вообще жизнеспособен.

Лестница германия

каналов каналов на FWHM z z+chain/1.0
GEM20 8192 4,0 92,1 / 47,0 77,5 / 5,4
L20_4096 4096 2,1 100 / 26,9 100 / 0,0

На германии при тех же 2,1 канала на полуширину вето работает идеально: recall
100 %, ноль фантомов. У NaI на этой же плотности оно мертво.

Оговорка к сравнению: в группе GEM20 три спектра с цепочкой (включая Th-228 с
оборванным рядом), в ступени — два, поэтому знаменатели разные, 89 и 27 линий.
Чистая часть эксперимента — лестница NaI.

Что из этого следует

Определяет не плотность каналов сама по себе, а то, разделяет ли сетка
соседние линии цепочки.
У германия при 4096 каналах линии по-прежнему
раздельны — разрешение 0,20 %, ширина линии 1,3 кэВ при шаге 0,7 кэВ. У NaI при
128 каналах цепочка превращается в бленды: точки S/I перестают принадлежать
отдельным линиям, и разбросу нечего мерить.

Отсюда же — сомнение в исходном объяснении аномалии ASN8. Наша лестница
монотонна: 17,2 → 8,0 → 3,8 работают, 2,1 ломается. Ваш ряд немонотонен —
1024 и 3000 плохо, 2048 и 4096 хорошо. Плотностью каналов такая
немонотонность не объясняется. Искать причину стоит в том, что у вас менялось
вместе с настройкой MCA, а у нас нет: статистика на канал, мёртвое время,
момент измерения, пересыпка Ch_Concat в конфигурации устройства.

Предел пересыпки

Германий ниже 4096 каналов не собирается вовсе: на 2048 и 1024 калибровка не
строится, ноль опорных линий, измеренное разрешение вылетает до 10–23 % вместо
0,2 %. Пик становится у́же одного канала, и мерить нечего. Четыре ступени
отвергнуты на сборке — это физический предел, а не дефект.


9. Что не дало результата и почему

Две группы в отчёте показывают recall на уровне базы финдера при нулевом числе
предъявлений. Это не результат, а отсутствие запуска фита:

  • SimpleHPGe — верх энергетической шкалы 1978 кэВ (спектр снят в диапазоне
    2 МэВ), ториевый якорь 2614 кэВ за её пределом. Якорь не срабатывает никогда.
  • HandyNaI — 297 с живого времени, якорь не найден финдером. Это ровно тот
    случай, о котором вы предупреждали в описании метрики: «прогон, где якорь не
    совпал ни с одним пиком, даёт ноль фантомов и полный набор линий в
    знаменатель, отчего слабый спектр выглядит чистым».

10. Побочная находка про формат

При разборе, почему группа германия падала целиком, обнаружилось следующее.

PolynomialEnergyCalibration.CheckCalibration() отвергает любой порядок выше
четвёртого. DocumentManager.CheckDocument() проверяет <EnergySpectrum> и
<BackgroundEnergySpectrum> как две независимые калибровки. Поэтому фон с
полиномом 5-й степени делает непригодным весь документ — даже когда у самого
спектра калибровка линейная.

Последствия зависят от режима, и оба жёстче, чем «шкала едет»:

  • в харнессе все прогоны группы падают с AggregateException — ноль строк
    в CSV;
  • в GUI CreateDocument (строки 89–91) показывает диалог «ошибка открытия
    файла… сбросить калибровку?»; при «нет» файл не открывается вовсе, при «да»
    калибровка заменяется на дефолтную.

Отдельно: EnergyToChannel реализован для порядков 1–3, для 4 и выше бросает
NotImplementedException, а EnergySpectrumView.TryMapPixelToChannel ловит
только OutofChannelException. В харнессе это не проявляется (он не рисует), в
GUI путь исполняется при шкале по энергии, которая стоит по умолчанию.

В наборе spectravibe-toolkit это уже исправлено: фон вынесен из 51 файла в
отдельное дерево, ссылка <BackgroundSpectrumFile> сохранена, оба конвертера
больше не вшивают фон со степенью выше четвёртой. Сообщается на случай, если
приложение получит такие файлы из других источников.


11. Как воспроизвести

Спектры: публичный репозиторий VibeEngineering-LLC/spectravibe-toolkit,
каталог detectors/. 251 файл BecqMoni XML по 11 классам детекторов, описи с
паспортными активностями — в detectors/<класс>/reference_spectra/INDEX.json.

Порядок:

# 1. состав корпуса: записи наших спектров
scripts/corpus_def.py          -> список OURS (17 записей) и LADDER (12 ступеней)

# 2. группы устройств
scripts/build_corpus.py        -> NEW_DEVICE пополнен шестью группами + ступени

# 3. сборка и приёмка
python scripts/build_corpus.py --only=<ключи>
python scripts/check_corpus.py

# 4. прогон и отчёт
python scripts/gate_study.py --run  --dets=<группы>
python scripts/gate_study.py --report --per-det --skip-dets=<ступени>

Правки в дереве LibraryFitLab

Четыре штуки, все в logs/07_local_patches.diff. Первая обязательна для любого
постороннего, остальные три — следствие того, что полная пересборка корпуса вне
вашей машины невозможна.

файл что зачем
chains.py путь к nucdb.sqlite ищется рядом с решением, переопределяется LFL_NUCDB, исходный литерал остался запасным абсолютный путь на вашу машину; без правки не работает ничего, что строит сеты
build_corpus.py merge_manifest() и merge_detectors(): при --only манифест и detectors.csv сливаются по ключу, а не игнорируются раньше при частичной сборке пересобранные спектры в таблицы не попадали, а полная сборка требует вашей библиотеки спектров
gate_study.py --dets= для прогона, --skip-dets= для отчёта; сеты при этом строятся всегда на весь корпус полный прогон идёт больше двух часов; сеты нельзя строить по части групп — sets_manifest.json общий, и отчёт потеряет обманки у остальных
corpus_def.py, build_corpus.py вторая библиотека спектров и шесть групп устройств собственно добавление

Ребиннинг сделан отдельным скриптом, в дерево не входит: он правит документ на
месте, а не пересобирает через ридер и райтер, потому что round-trip через
сторонние библиотеки теряет SqrtFwhmCalibration и PeakType — то есть ровно
модель разрешения и форму пика, на которых стоит корпус.


12. Оговорки

  1. LaBr₃ подтверждён слабо. Медиана невязки 0,571 FWHM у фона и 0,333 у
    Th-228 — выше порога приёмки. Числа по группе ориентировочные.

  2. Сравнение германиевых ступеней неточно из-за разного состава групп
    (89 линий против 27). Чистая часть лестницы — NaI.

  3. GEM20 собран с фоном, у которого полином понижен с 5-й степени до 4-й
    методом наименьших квадратов по узлам сетки. Максимальное отклонение по всему
    диапазону — 0,212 кэВ, шестая часть полуширины на 662 кэВ. Фон при этом тот
    же самый, что указан в шапке измерения; подмены на другой фон не делалось.
    Числа проверены дважды — до и после перехода набора на отдельное дерево
    фонов, совпали до последней цифры.

  4. Th-228 засчитан как Th-232. В спектрах Th-228 ряд оборван сверху: нет
    линий Ac-228 (911, 969 кэВ), которые в знаменатель recall входят. Поэтому
    recall на таких спектрах занижен по построению — они брались ради проверки
    поведения вето на неполном наборе, а не ради абсолютных значений.

  5. Одна точка сетки. Всё считано при k = 0.7, I_min = 1 % — так же, как
    в вашем прогоне. Сетку k × I_min на новых классах детекторов мы не гоняли.


13. Приложения

файл что внутри
logs/01_report_total_46_baseline.txt воспроизведение вашей базы, 46 спектров
logs/02_check_corpus.txt приёмка корпуса целиком, невязки в долях FWHM
logs/03_report_total_63.txt итог по расширенному корпусу без ступеней лестницы
logs/04_report_per_det_63.txt разбивка по всем 24 группам
logs/05_report_ladder.txt то же со ступенями лестницы
logs/06_detectors_ours.csv модели разрешения и диапазоны наших групп
logs/07_local_patches.diff все правки в дереве LibraryFitLab
logs/05_manifest_ours.csv строки манифеста по нашим спектрам

14. Что можем сделать дальше

  • Сетка k × I_min на новых классах. У вас она снята на трёх детекторах
    девятки; теперь есть германий, LaBr₃ и NaI 63×63 с аттестованными
    источниками.
  • Систематический пол по классам разрешения. Ваши 24 % медианы сняты
    преимущественно на сцинтилляторах. Раздельная оценка даёт шанс вывести порог
    вето, а не подбирать.
  • Расчёт степеней свободы кривой относительной эффективности и бюджета
    каскадного суммирования для Th-232 в близкой геометрии — тот самый, что стоял
    первым пунктом вашего плана и был пропущен ради кода.

Приложения — часть 1

logs/01 — воспроизведение вашей базы, 46 спектров
детектор   гейт             recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
ИТОГО                       recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
финдер                       41.2%     1016        -        0        0
z                            75.3%     1016    68.2%     1285      105
dd                           69.5%     1016    51.5%     1285      102
shape                        62.8%     1016    32.6%     1285      102
dd+shape                     59.4%     1016    24.7%     1285      102
z+chain/1.0                  54.6%     1016     5.1%     1285       96
z+chain/1.25                 59.4%     1016     5.7%     1285      107
z+chain/1.5                  66.7%     1016    15.4%     1285      105
всё/1.25                     55.4%     1016    13.1%     1285      102
logs/03 — итог по расширенному корпусу, 63 спектра
детектор   гейт             recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
ИТОГО                       recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
финдер                       43.4%     1289        -        0        0
z                            75.0%     1289    65.0%     1664       96
dd                           70.6%     1289    51.9%     1664       90
shape                        62.4%     1289    28.3%     1664       89
dd+shape                     59.5%     1289    21.5%     1664       97
z+chain/1.0                  56.2%     1289     4.8%     1664       87
z+chain/1.25                 60.2%     1289     8.5%     1664       90
z+chain/1.5                  66.0%     1289    16.0%     1664       94
всё/1.25                     56.4%     1289    11.8%     1664      100
logs/06 — модели разрешения наших групп (detectors.csv)
det,channels,spectra,e_lo,e_hi,res_c0,res_c1,res_c2,fwhm_662_pct
G1S,1024,4,30.0,2900.0,0.0,3.336602218048564,0.0003467499530004568,7.34
GEM20,8192,4,20.0,2800.0,0.0,0.0026725671741362673,1.2341673620423536e-07,0.2
HandyHPGe,8192,3,20.0,2800.0,0.0,0.0029188854074655244,-1.3466065948513076e-07,0.21
HandyLaBr,1024,3,20.0,2800.0,0.0,0.804071658529424,-6.552496879252723e-05,3.39
HandyNaI,1024,1,20.0,2800.0,0.0,1.8638704459732884,0.0,5.31
L1S_128,128,2,30.0,2900.0,0.0,2.944248776239956,0.0007567845473682039,7.21
L1S_256,256,2,30.0,2900.0,0.0,1.7827691327191761,0.0014379383715808608,6.43
L1S_512,512,2,30.0,2900.0,0.0,3.046805946103481,8.768715617468948e-05,6.85
L20_4096,4096,2,20.0,2800.0,0.0,0.0031363510216923765,-5.67395272634839e-08,0.22
SimpleHPGe,8191,2,20.0,2800.0,0.0,0.0035237715056662348,-1.0090645065366619e-06,0.21
logs/02 — приёмка корпуса (check_corpus.py)
спектр               детектор   лин  медиана      p90     макс   ширина  вердикт
ASN16_Th232          ASN16        4    0.008    0.015    0.016     0.89  ок
ASN16_Charoite       ASN16        7    0.028    0.065    0.077     0.88  ок
ASN16_UGlass         ASN16        3    0.048    0.073    0.079     0.95  ок
ASN16_Granite        ASN16        4    0.011    0.065    0.085     0.93  ок
AS80_Th232WT20       AS80x80      3    0.013    0.026    0.029     0.86  ок
AS80_Th232_v2        AS80x80      4    0.035    0.154    0.193     0.86  ок
AS80_UGlass          AS80x80      5    0.117    0.701    0.811     1.02  ок
AS80_Charoite        AS80x80      3    0.335    0.376    0.386     0.80  на грани
RC103_Th232WT20      RC103        3    0.036    0.056    0.062     0.92  ок
HPGE_Uranium         HPGE         9    0.074    0.226    0.374     1.11  ок
LaBr3_Ore            LaBr3        6    0.039    0.093    0.114     1.04  ок
CZT_Th232            CZT          8    0.068    0.118    0.148     0.95  ок
CZT_Cs137            CZT          3    0.266    0.684    0.788     1.00  на грани
SrI2_Th232           SrI2         6    0.017    0.032    0.044     1.01  ок
GS4000_Ra226         GS4000       5    0.113    0.228    0.291     1.05  ок
GS4000_Th232         GS4000       3    0.023    0.223    0.272     0.97  ок
GS4000_U             GS4000       3    0.057    0.397    0.482     1.35  ок
GS4000_Lu176         GS4000       3    0.140    0.362    0.418     1.06  ок
ASN3_Tile            ASN3         5    0.064    0.114    0.121     1.11  ок
ASN8_Th232_1024      ASN8_1024    6    0.048    0.100    0.101     0.99  ок
ASN8_Th232_2048      ASN8_2048    5    0.062    0.091    0.102     1.04  ок
ASN8_Th232_3000      ASN8_3000    6    0.025    0.077    0.086     0.93  ок
ASN8_Th232_4096      ASN8_4096    6    0.068    0.095    0.100     1.02  ок
ASN8_Th232_8192      ASN8_8192    5    0.020    0.074    0.081     1.01  ок
ASN8_UGlass          ASN8_8192    6    0.149    0.624    0.736     1.15  ок
ASN8_Am241           ASN8_8192    2    0.056    0.071    0.075     1.31  ок
ASN8_Background      ASN8_8192    3    0.064    0.074    0.076     1.19  ок
OBS_Th232WT20        OBS          5    0.116    0.437    0.468     0.93  ок
OBS_UGlass           OBS          1    0.131    0.131    0.131     0.47  ок
OBS_Background       OBS          1    0.197    0.197    0.197     0.79  ок
RC103_K40            RC103        2    0.217    0.383    0.425     1.25  ок
RC103_Background     RC103        1    0.031    0.031    0.031     1.00  ок
RC103_Co60           RC103g       4    0.056    0.152    0.189     1.12  ок
RC101_Th232          RC101        4    0.123    0.213    0.225     0.70  ок
RC101_I131           RC101        2    0.043    0.046    0.047     1.12  ок
AS1Pro_Ra226         AS1PRO       6    0.102    0.329    0.513     0.99  ок
ASN16_Charoite_v2    ASN16        3    0.064    0.070    0.071     1.22  ок
ASN16_Th232_Am241    ASN16        6    0.070    0.199    0.265     0.90  ок
ASN16_Am241          ASN16        5    0.036    0.052    0.054     0.89  ок
ASN16_Cs137          ASN16        6    0.058    0.071    0.077     0.94  ок
ASN16_Background     ASN16        4    0.044    0.094    0.108     0.97  ок
ASN16_BrazilNuts     ASN16        5    0.078    0.092    0.095     1.05  ок
AS80_Th232Medal      AS80x80      3    0.148    0.219    0.236     0.56  ок
AS80_Onyx            AS80x80      3    0.098    0.219    0.250     0.75  ок
AS80_K40             AS80x80      3    0.058    0.289    0.347     0.60  ок
AS80_Background      AS80x80      1    0.055    0.055    0.055     0.61  ок
G20_Th232_Mar        GEM20       16    0.009    0.058    0.081     1.10  ок
G20_Ra226_Mar        GEM20       13    0.021    0.047    0.788     0.96  ок
G20_Cs137_Mar        GEM20        1    0.217    0.217    0.217     0.95  ок
G20_Th228_P25        GEM20        8    0.016    0.045    0.048     0.97  ок
HHP_Th232            HandyHPGe   10    0.035    0.077    0.122     0.97  ок
HHP_Th228            HandyHPGe    4    0.034    0.099    0.123     1.05  ок
HHP_Co60             HandyHPGe    3    0.008    0.009    0.010     0.96  ок
SHP_Th228            SimpleHPGe    6    0.018    0.056    0.073     1.02  ок
SHP_Eu152            SimpleHPGe    9    0.004    0.009    0.012     1.04  ок
LB_Th228             HandyLaBr    4    0.038    0.333    0.456     0.86  ок
LB_Background        HandyLaBr    5    0.122    0.571    0.707     0.93  ок
LB_Ba133             HandyLaBr    7    0.044    0.132    0.248     0.85  ок
HN_Th232             HandyNaI     4    0.147    0.341    0.362     1.30  ок
G1S_Th232_Denta      G1S          4    0.021    0.075    0.095     0.84  ок
G1S_Ra226_Denta      G1S          5    0.011    0.043    0.059     0.84  ок
G1S_Cs137_Denta      G1S          2    0.036    0.049    0.052     0.94  ок
G1S_K40_Denta        G1S          5    0.125    0.199    0.248     1.05  ок
G1S_Th232_512        L1S_512      3    0.017    0.070    0.083     1.00  ок
G1S_Ra226_512        L1S_512      6    0.024    0.028    0.029     0.93  ок
G1S_Th232_256        L1S_256      3    0.107    0.119    0.122     0.97  ок
G1S_Ra226_256        L1S_256      3    0.063    0.219    0.258     1.05  ок
G1S_Th232_128        L1S_128      3    0.004    0.060    0.074     0.93  ок
G1S_Ra226_128        L1S_128      4    0.048    0.053    0.055     1.05  ок
G20_Th232_4096       L20_4096    15    0.008    0.037    0.060     1.05  ок
G20_Ra226_4096       L20_4096    12    0.017    0.031    0.036     0.97  ок
G20_Th232_2048       L20_2048   --  нет файла
G20_Ra226_2048       L20_2048   --  нет файла
G20_Th232_1024       L20_1024   --  нет файла
G20_Ra226_1024       L20_1024   --  нет файла

плохих: 0 
спорных/непроверенных: 6 AS80_Charoite, CZT_Cs137, G20_Th232_2048, G20_Ra226_2048, G20_Th232_1024, G20_Ra226_1024

@Verter73

Copy link
Copy Markdown
Collaborator Author

Приложения — часть 2: разбивка по группам

Продолжение отчёта выше.

logs/04 — recall и фантомы по всем 24 группам (без ступеней лестницы)
детектор   гейт             recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
AS1PRO     финдер            70.0%       20     nan%        0        0
AS1PRO     z                100.0%       20    69.0%       29      280
AS1PRO     dd                90.0%       20    58.6%       29      301
AS1PRO     shape             95.0%       20    44.8%       29      240
AS1PRO     dd+shape          85.0%       20    37.9%       29      265
AS1PRO     z+chain/1.0      100.0%       20     0.0%       29      215
AS1PRO     z+chain/1.25     100.0%       20     0.0%       29      228
AS1PRO     z+chain/1.5      100.0%       20    24.1%       29      238
AS1PRO     всё/1.25          85.0%       20    17.2%       29      269

AS80x80    финдер            30.1%      186     nan%        0        0
AS80x80    z                 50.0%      186    50.6%      176      206
AS80x80    dd                45.7%      186    41.5%      176      228
AS80x80    shape             44.1%      186    33.5%      176      236
AS80x80    dd+shape          40.9%      186    26.1%      176      196
AS80x80    z+chain/1.0       40.9%      186     5.1%      176      214
AS80x80    z+chain/1.25      46.2%      186     5.1%      176      232
AS80x80    z+chain/1.5       46.2%      186     5.1%      176      222
AS80x80    всё/1.25          37.6%      186    17.0%      176      220

ASN16      финдер            52.2%      251     nan%        0        0
ASN16      z                 92.4%      251    67.5%      385      480
ASN16      dd                87.3%      251    49.1%      385      517
ASN16      shape             75.7%      251    33.8%      385      448
ASN16      dd+shape          72.9%      251    24.4%      385      561
ASN16      z+chain/1.0       64.5%      251     5.5%      385      402
ASN16      z+chain/1.25      72.1%      251     7.5%      385      435
ASN16      z+chain/1.5       81.7%      251    17.9%      385      392
ASN16      всё/1.25          72.9%      251    14.3%      385      432

ASN3       финдер            30.6%       49     nan%        0        0
ASN3       z                 75.5%       49    80.0%       50      142
ASN3       dd                63.3%       49    66.0%       50      166
ASN3       shape             53.1%       49    46.0%       50      154
ASN3       dd+shape          42.9%       49    42.0%       50      168
ASN3       z+chain/1.0       65.3%       49     0.0%       50      141
ASN3       z+chain/1.25      65.3%       49     0.0%       50      148
ASN3       z+chain/1.5       75.5%       49     0.0%       50      146
ASN3       всё/1.25          42.9%       49    22.0%       50      170

ASN8_1024  финдер            54.5%       22     nan%        0        0
ASN8_1024  z                 95.5%       22    61.9%       21       16
ASN8_1024  dd                86.4%       22    52.4%       21       16
ASN8_1024  shape             81.8%       22    28.6%       21       16
ASN8_1024  dd+shape          81.8%       22    28.6%       21       16
ASN8_1024  z+chain/1.0       54.5%       22     0.0%       21       15
ASN8_1024  z+chain/1.25      54.5%       22     0.0%       21       18
ASN8_1024  z+chain/1.5       54.5%       22     0.0%       21       16
ASN8_1024  всё/1.25          81.8%       22    28.6%       21       16

ASN8_2048  финдер            50.0%       22     nan%        0        0
ASN8_2048  z                 95.5%       22    57.1%       21       38
ASN8_2048  dd                86.4%       22    47.6%       21       38
ASN8_2048  shape             77.3%       22     4.8%       21       40
ASN8_2048  dd+shape          77.3%       22     4.8%       21       36
ASN8_2048  z+chain/1.0       95.5%       22     0.0%       21       38
ASN8_2048  z+chain/1.25      95.5%       22     0.0%       21       39
ASN8_2048  z+chain/1.5       95.5%       22     0.0%       21       34
ASN8_2048  всё/1.25          77.3%       22     4.8%       21       37

ASN8_3000  финдер            52.4%       21     nan%        0        0
ASN8_3000  z                100.0%       21    73.7%       19       54
ASN8_3000  dd                90.5%       21    42.1%       19       56
ASN8_3000  shape             81.0%       21    36.8%       19       55
ASN8_3000  dd+shape          81.0%       21    21.1%       19       59
ASN8_3000  z+chain/1.0       52.4%       21     0.0%       19       56
ASN8_3000  z+chain/1.25      52.4%       21     0.0%       19       58
ASN8_3000  z+chain/1.5      100.0%       21     0.0%       19       54
ASN8_3000  всё/1.25          81.0%       21     0.0%       19       52

ASN8_4096  финдер            50.0%       22     nan%        0        0
ASN8_4096  z                 95.5%       22    70.0%       20       82
ASN8_4096  dd                90.9%       22    50.0%       20       80
ASN8_4096  shape             77.3%       22    30.0%       20       79
ASN8_4096  dd+shape          77.3%       22    25.0%       20       82
ASN8_4096  z+chain/1.0       95.5%       22     0.0%       20       78
ASN8_4096  z+chain/1.25      95.5%       22     0.0%       20       78
ASN8_4096  z+chain/1.5       95.5%       22     0.0%       20       74
ASN8_4096  всё/1.25          77.3%       22     0.0%       20       74

ASN8_8192  финдер            23.2%       82     nan%        0        0
ASN8_8192  z                 64.6%       82    82.8%       99      204
ASN8_8192  dd                58.5%       82    67.7%       99      227
ASN8_8192  shape             57.3%       82    47.5%       99      202
ASN8_8192  dd+shape          52.4%       82    37.4%       99      261
ASN8_8192  z+chain/1.0       35.4%       82    20.2%       99      220
ASN8_8192  z+chain/1.25      35.4%       82    20.2%       99      246
ASN8_8192  z+chain/1.5       64.6%       82    20.2%       99      274
ASN8_8192  всё/1.25          31.7%       82    19.2%       99      278

CZT        финдер            69.0%       29     nan%        0        0
CZT        z                100.0%       29    69.0%       58       15
CZT        dd                86.2%       29    29.3%       58       14
CZT        shape             89.7%       29    29.3%       58       14
CZT        dd+shape          82.8%       29    15.5%       58       14
CZT        z+chain/1.0       69.0%       29     0.0%       58       14
CZT        z+chain/1.25     100.0%       29     0.0%       58       15
CZT        z+chain/1.5      100.0%       29     0.0%       58       14
CZT        всё/1.25          82.8%       29     3.4%       58       14

G1S        финдер            61.9%       42     nan%        0        0
G1S        z                 97.6%       42    66.7%       90       18
G1S        dd                90.5%       42    50.0%       90       18
G1S        shape             88.1%       42    27.8%       90       16
G1S        dd+shape          83.3%       42    24.4%       90       16
G1S        z+chain/1.0       97.6%       42     7.8%       90       15
G1S        z+chain/1.25      97.6%       42    17.8%       90       14
G1S        z+chain/1.5       97.6%       42    17.8%       90       16
G1S        всё/1.25          83.3%       42    24.4%       90       16

GEM20      финдер            74.2%       89     nan%        0        0
GEM20      z                 92.1%       89    47.0%      149      244
GEM20      dd                94.4%       89    49.7%      149      282
GEM20      shape             76.4%       89     2.0%      149      314
GEM20      dd+shape          76.4%       89     1.3%      149      279
GEM20      z+chain/1.0       77.5%       89     5.4%      149      272
GEM20      z+chain/1.25      77.5%       89    25.5%      149      294
GEM20      z+chain/1.5       77.5%       89    25.5%      149      265
GEM20      всё/1.25          76.4%       89     1.3%      149      269

GS4000     финдер            46.8%       62     nan%        0        0
GS4000     z                 77.4%       62    57.9%      121      131
GS4000     dd                72.6%       62    44.6%      121      139
GS4000     shape             71.0%       62    28.9%      121      145
GS4000     dd+shape          67.7%       62    22.3%      121      143
GS4000     z+chain/1.0       67.7%       62     9.1%      121      144
GS4000     z+chain/1.25      67.7%       62     9.1%      121      166
GS4000     z+chain/1.5       67.7%       62    17.4%      121      152
GS4000     всё/1.25          51.6%       62    18.2%      121      166

HPGE       финдер            28.2%       39     nan%        0        0
HPGE       z                 87.2%       39    85.4%       96      142
HPGE       dd                89.7%       39    78.1%       96      130
HPGE       shape             38.5%       39     4.2%       96      130
HPGE       dd+shape          38.5%       39     3.1%       96      144
HPGE       z+chain/1.0       28.2%       39     0.0%       96      129
HPGE       z+chain/1.25      28.2%       39     0.0%       96      137
HPGE       z+chain/1.5       28.2%       39    34.4%       96      130
HPGE       всё/1.25          38.5%       39     3.1%       96      132

HandyHPGe  финдер            38.7%       62     nan%        0        0
HandyHPGe  z                 82.3%       62    46.7%       60      130
HandyHPGe  dd                87.1%       62    68.3%       60      118
HandyHPGe  shape             54.8%       62     1.7%       60      114
HandyHPGe  dd+shape          54.8%       62     1.7%       60      110
HandyHPGe  z+chain/1.0       56.5%       62     0.0%       60      109
HandyHPGe  z+chain/1.25      56.5%       62    23.3%       60      104
HandyHPGe  z+chain/1.5       56.5%       62    23.3%       60      106
HandyHPGe  всё/1.25          54.8%       62     1.7%       60      103

HandyLaBr  финдер            34.6%       26     nan%        0        0
HandyLaBr  z                 46.2%       26    58.8%       80        4
HandyLaBr  dd                46.2%       26    51.2%       80        4
HandyLaBr  shape             42.3%       26    28.8%       80        3
HandyLaBr  dd+shape          42.3%       26    18.8%       80        4
HandyLaBr  z+chain/1.0       34.6%       26     0.0%       80        3
HandyLaBr  z+chain/1.25      46.2%       26     0.0%       80        3
HandyLaBr  z+chain/1.5       46.2%       26     0.0%       80        4
HandyLaBr  всё/1.25          42.3%       26     5.0%       80        4

HandyNaI   финдер            21.7%       23     nan%        0        0
HandyNaI   z                 21.7%       23     nan%        0       14
HandyNaI   dd                21.7%       23     nan%        0       12
HandyNaI   shape             21.7%       23     nan%        0       13
HandyNaI   dd+shape          21.7%       23     nan%        0       13
HandyNaI   z+chain/1.0       21.7%       23     nan%        0       12
HandyNaI   z+chain/1.25      21.7%       23     nan%        0       13
HandyNaI   z+chain/1.5       21.7%       23     nan%        0       13
HandyNaI   всё/1.25          21.7%       23     nan%        0       12





LaBr3      финдер            53.8%       52     nan%        0        0
LaBr3      z                 98.1%       52    75.8%       99     1368
LaBr3      dd                84.6%       52    54.5%       99     1388
LaBr3      shape             88.5%       52    37.4%       99     1295
LaBr3      dd+shape          80.8%       52    30.3%       99     1362
LaBr3      z+chain/1.0       69.2%       52     0.0%       99     1300
LaBr3      z+chain/1.25      69.2%       52     0.0%       99     1532
LaBr3      z+chain/1.5       69.2%       52    15.2%       99     1606
LaBr3      всё/1.25          67.3%       52     0.0%       99     1582

OBS        финдер            21.6%       51     nan%        0        0
OBS        z                 21.6%       51    50.0%       10        3
OBS        dd                21.6%       51    40.0%       10        2
OBS        shape             21.6%       51    20.0%       10        2
OBS        dd+shape          21.6%       51    20.0%       10        2
OBS        z+chain/1.0       21.6%       51     0.0%       10        3
OBS        z+chain/1.25      21.6%       51     0.0%       10        4
OBS        z+chain/1.5       21.6%       51     0.0%       10        2
OBS        всё/1.25          21.6%       51    20.0%       10        3

RC101      финдер            35.0%       20     nan%        0        0
RC101      z                 35.0%       20    50.0%       20        6
RC101      dd                35.0%       20    30.0%       20        6
RC101      shape             35.0%       20    25.0%       20        6
RC101      dd+shape          35.0%       20    15.0%       20        6
RC101      z+chain/1.0       35.0%       20    20.0%       20        6
RC101      z+chain/1.25      35.0%       20    20.0%       20        6
RC101      z+chain/1.5       35.0%       20    50.0%       20        6
RC101      всё/1.25          35.0%       20    15.0%       20        6

RC103      финдер            29.0%       62     nan%        0        0
RC103      z                 66.1%       62    81.8%       33        6
RC103      dd                61.3%       62    45.5%       33        6
RC103      shape             53.2%       62    45.5%       33        6
RC103      dd+shape          50.0%       62    27.3%       33        5
RC103      z+chain/1.0       46.8%       62     0.0%       33        6
RC103      z+chain/1.25      46.8%       62     0.0%       33        5
RC103      z+chain/1.5       66.1%       62    42.4%       33        6
RC103      всё/1.25          50.0%       62    27.3%       33        6


SimpleHPGe финдер            35.5%       31     nan%        0        0
SimpleHPGe z                 35.5%       31     nan%        0      180
SimpleHPGe dd                35.5%       31     nan%        0      186
SimpleHPGe shape             35.5%       31     nan%        0      188
SimpleHPGe dd+shape          35.5%       31     nan%        0      188
SimpleHPGe z+chain/1.0       35.5%       31     nan%        0      223
SimpleHPGe z+chain/1.25      35.5%       31     nan%        0      194
SimpleHPGe z+chain/1.5       35.5%       31     nan%        0      202
SimpleHPGe всё/1.25          35.5%       31     nan%        0      196

SrI2       финдер            57.7%       26     nan%        0        0
SrI2       z                 96.2%       26    85.7%       28       10
SrI2       dd                88.5%       26    67.9%       28        9
SrI2       shape             88.5%       26    42.9%       28        9
SrI2       dd+shape          84.6%       26    35.7%       28       10
SrI2       z+chain/1.0       57.7%       26     0.0%       28       10
SrI2       z+chain/1.25      96.2%       26     0.0%       28        9
SrI2       z+chain/1.5       96.2%       26     0.0%       28        9
SrI2       всё/1.25          84.6%       26     0.0%       28       10

ИТОГО                       recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
финдер                       43.4%     1289        -        0        0
z                            75.0%     1289    65.0%     1664       96
dd                           70.6%     1289    51.9%     1664       90
shape                        62.4%     1289    28.3%     1664       89
dd+shape                     59.5%     1289    21.5%     1664       97
z+chain/1.0                  56.2%     1289     4.8%     1664       87
z+chain/1.25                 60.2%     1289     8.5%     1664       90
z+chain/1.5                  66.0%     1289    16.0%     1664       94
всё/1.25                     56.4%     1289    11.8%     1664      100
logs/05 — то же со ступенями лестницы по числу каналов
детектор   гейт             recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
AS1PRO     финдер            70.0%       20     nan%        0        0
AS1PRO     z                100.0%       20    69.0%       29      280
AS1PRO     dd                90.0%       20    58.6%       29      301
AS1PRO     shape             95.0%       20    44.8%       29      240
AS1PRO     dd+shape          85.0%       20    37.9%       29      265
AS1PRO     z+chain/1.0      100.0%       20     0.0%       29      215
AS1PRO     z+chain/1.25     100.0%       20     0.0%       29      228
AS1PRO     z+chain/1.5      100.0%       20    24.1%       29      238
AS1PRO     всё/1.25          85.0%       20    17.2%       29      269

AS80x80    финдер            30.1%      186     nan%        0        0
AS80x80    z                 50.0%      186    50.6%      176      206
AS80x80    dd                45.7%      186    41.5%      176      228
AS80x80    shape             44.1%      186    33.5%      176      236
AS80x80    dd+shape          40.9%      186    26.1%      176      196
AS80x80    z+chain/1.0       40.9%      186     5.1%      176      214
AS80x80    z+chain/1.25      46.2%      186     5.1%      176      232
AS80x80    z+chain/1.5       46.2%      186     5.1%      176      222
AS80x80    всё/1.25          37.6%      186    17.0%      176      220

ASN16      финдер            52.2%      251     nan%        0        0
ASN16      z                 92.4%      251    67.5%      385      480
ASN16      dd                87.3%      251    49.1%      385      517
ASN16      shape             75.7%      251    33.8%      385      448
ASN16      dd+shape          72.9%      251    24.4%      385      561
ASN16      z+chain/1.0       64.5%      251     5.5%      385      402
ASN16      z+chain/1.25      72.1%      251     7.5%      385      435
ASN16      z+chain/1.5       81.7%      251    17.9%      385      392
ASN16      всё/1.25          72.9%      251    14.3%      385      432

ASN3       финдер            30.6%       49     nan%        0        0
ASN3       z                 75.5%       49    80.0%       50      142
ASN3       dd                63.3%       49    66.0%       50      166
ASN3       shape             53.1%       49    46.0%       50      154
ASN3       dd+shape          42.9%       49    42.0%       50      168
ASN3       z+chain/1.0       65.3%       49     0.0%       50      141
ASN3       z+chain/1.25      65.3%       49     0.0%       50      148
ASN3       z+chain/1.5       75.5%       49     0.0%       50      146
ASN3       всё/1.25          42.9%       49    22.0%       50      170

ASN8_1024  финдер            54.5%       22     nan%        0        0
ASN8_1024  z                 95.5%       22    61.9%       21       16
ASN8_1024  dd                86.4%       22    52.4%       21       16
ASN8_1024  shape             81.8%       22    28.6%       21       16
ASN8_1024  dd+shape          81.8%       22    28.6%       21       16
ASN8_1024  z+chain/1.0       54.5%       22     0.0%       21       15
ASN8_1024  z+chain/1.25      54.5%       22     0.0%       21       18
ASN8_1024  z+chain/1.5       54.5%       22     0.0%       21       16
ASN8_1024  всё/1.25          81.8%       22    28.6%       21       16

ASN8_2048  финдер            50.0%       22     nan%        0        0
ASN8_2048  z                 95.5%       22    57.1%       21       38
ASN8_2048  dd                86.4%       22    47.6%       21       38
ASN8_2048  shape             77.3%       22     4.8%       21       40
ASN8_2048  dd+shape          77.3%       22     4.8%       21       36
ASN8_2048  z+chain/1.0       95.5%       22     0.0%       21       38
ASN8_2048  z+chain/1.25      95.5%       22     0.0%       21       39
ASN8_2048  z+chain/1.5       95.5%       22     0.0%       21       34
ASN8_2048  всё/1.25          77.3%       22     4.8%       21       37

ASN8_3000  финдер            52.4%       21     nan%        0        0
ASN8_3000  z                100.0%       21    73.7%       19       54
ASN8_3000  dd                90.5%       21    42.1%       19       56
ASN8_3000  shape             81.0%       21    36.8%       19       55
ASN8_3000  dd+shape          81.0%       21    21.1%       19       59
ASN8_3000  z+chain/1.0       52.4%       21     0.0%       19       56
ASN8_3000  z+chain/1.25      52.4%       21     0.0%       19       58
ASN8_3000  z+chain/1.5      100.0%       21     0.0%       19       54
ASN8_3000  всё/1.25          81.0%       21     0.0%       19       52

ASN8_4096  финдер            50.0%       22     nan%        0        0
ASN8_4096  z                 95.5%       22    70.0%       20       82
ASN8_4096  dd                90.9%       22    50.0%       20       80
ASN8_4096  shape             77.3%       22    30.0%       20       79
ASN8_4096  dd+shape          77.3%       22    25.0%       20       82
ASN8_4096  z+chain/1.0       95.5%       22     0.0%       20       78
ASN8_4096  z+chain/1.25      95.5%       22     0.0%       20       78
ASN8_4096  z+chain/1.5       95.5%       22     0.0%       20       74
ASN8_4096  всё/1.25          77.3%       22     0.0%       20       74

ASN8_8192  финдер            23.2%       82     nan%        0        0
ASN8_8192  z                 64.6%       82    82.8%       99      204
ASN8_8192  dd                58.5%       82    67.7%       99      227
ASN8_8192  shape             57.3%       82    47.5%       99      202
ASN8_8192  dd+shape          52.4%       82    37.4%       99      261
ASN8_8192  z+chain/1.0       35.4%       82    20.2%       99      220
ASN8_8192  z+chain/1.25      35.4%       82    20.2%       99      246
ASN8_8192  z+chain/1.5       64.6%       82    20.2%       99      274
ASN8_8192  всё/1.25          31.7%       82    19.2%       99      278

CZT        финдер            69.0%       29     nan%        0        0
CZT        z                100.0%       29    69.0%       58       15
CZT        dd                86.2%       29    29.3%       58       14
CZT        shape             89.7%       29    29.3%       58       14
CZT        dd+shape          82.8%       29    15.5%       58       14
CZT        z+chain/1.0       69.0%       29     0.0%       58       14
CZT        z+chain/1.25     100.0%       29     0.0%       58       15
CZT        z+chain/1.5      100.0%       29     0.0%       58       14
CZT        всё/1.25          82.8%       29     3.4%       58       14

G1S        финдер            61.9%       42     nan%        0        0
G1S        z                 97.6%       42    66.7%       90       18
G1S        dd                90.5%       42    50.0%       90       18
G1S        shape             88.1%       42    27.8%       90       16
G1S        dd+shape          83.3%       42    24.4%       90       16
G1S        z+chain/1.0       97.6%       42     7.8%       90       15
G1S        z+chain/1.25      97.6%       42    17.8%       90       14
G1S        z+chain/1.5       97.6%       42    17.8%       90       16
G1S        всё/1.25          83.3%       42    24.4%       90       16

GEM20      финдер            74.2%       89     nan%        0        0
GEM20      z                 92.1%       89    47.0%      149      244
GEM20      dd                94.4%       89    49.7%      149      282
GEM20      shape             76.4%       89     2.0%      149      314
GEM20      dd+shape          76.4%       89     1.3%      149      279
GEM20      z+chain/1.0       77.5%       89     5.4%      149      272
GEM20      z+chain/1.25      77.5%       89    25.5%      149      294
GEM20      z+chain/1.5       77.5%       89    25.5%      149      265
GEM20      всё/1.25          76.4%       89     1.3%      149      269

GS4000     финдер            46.8%       62     nan%        0        0
GS4000     z                 77.4%       62    57.9%      121      131
GS4000     dd                72.6%       62    44.6%      121      139
GS4000     shape             71.0%       62    28.9%      121      145
GS4000     dd+shape          67.7%       62    22.3%      121      143
GS4000     z+chain/1.0       67.7%       62     9.1%      121      144
GS4000     z+chain/1.25      67.7%       62     9.1%      121      166
GS4000     z+chain/1.5       67.7%       62    17.4%      121      152
GS4000     всё/1.25          51.6%       62    18.2%      121      166

HPGE       финдер            28.2%       39     nan%        0        0
HPGE       z                 87.2%       39    85.4%       96      142
HPGE       dd                89.7%       39    78.1%       96      130
HPGE       shape             38.5%       39     4.2%       96      130
HPGE       dd+shape          38.5%       39     3.1%       96      144
HPGE       z+chain/1.0       28.2%       39     0.0%       96      129
HPGE       z+chain/1.25      28.2%       39     0.0%       96      137
HPGE       z+chain/1.5       28.2%       39    34.4%       96      130
HPGE       всё/1.25          38.5%       39     3.1%       96      132

HandyHPGe  финдер            38.7%       62     nan%        0        0
HandyHPGe  z                 82.3%       62    46.7%       60      130
HandyHPGe  dd                87.1%       62    68.3%       60      118
HandyHPGe  shape             54.8%       62     1.7%       60      114
HandyHPGe  dd+shape          54.8%       62     1.7%       60      110
HandyHPGe  z+chain/1.0       56.5%       62     0.0%       60      109
HandyHPGe  z+chain/1.25      56.5%       62    23.3%       60      104
HandyHPGe  z+chain/1.5       56.5%       62    23.3%       60      106
HandyHPGe  всё/1.25          54.8%       62     1.7%       60      103

HandyLaBr  финдер            34.6%       26     nan%        0        0
HandyLaBr  z                 46.2%       26    58.8%       80        4
HandyLaBr  dd                46.2%       26    51.2%       80        4
HandyLaBr  shape             42.3%       26    28.8%       80        3
HandyLaBr  dd+shape          42.3%       26    18.8%       80        4
HandyLaBr  z+chain/1.0       34.6%       26     0.0%       80        3
HandyLaBr  z+chain/1.25      46.2%       26     0.0%       80        3
HandyLaBr  z+chain/1.5       46.2%       26     0.0%       80        4
HandyLaBr  всё/1.25          42.3%       26     5.0%       80        4

HandyNaI   финдер            21.7%       23     nan%        0        0
HandyNaI   z                 21.7%       23     nan%        0       14
HandyNaI   dd                21.7%       23     nan%        0       12
HandyNaI   shape             21.7%       23     nan%        0       13
HandyNaI   dd+shape          21.7%       23     nan%        0       13
HandyNaI   z+chain/1.0       21.7%       23     nan%        0       12
HandyNaI   z+chain/1.25      21.7%       23     nan%        0       13
HandyNaI   z+chain/1.5       21.7%       23     nan%        0       13
HandyNaI   всё/1.25          21.7%       23     nan%        0       12

L1S_128    финдер            45.2%       42     nan%        0        0
L1S_128    z                 90.5%       42    70.5%       44        1
L1S_128    dd                78.6%       42    56.8%       44        1
L1S_128    shape             83.3%       42    45.5%       44        1
L1S_128    dd+shape          71.4%       42    38.6%       44        1
L1S_128    z+chain/1.0       90.5%       42    18.2%       44        1
L1S_128    z+chain/1.25      90.5%       42    70.5%       44        2
L1S_128    z+chain/1.5       90.5%       42    70.5%       44        1
L1S_128    всё/1.25          71.4%       42    38.6%       44        2

L1S_256    финдер            48.8%       43     nan%        0        0
L1S_256    z                 83.7%       43    62.5%       40        5
L1S_256    dd                74.4%       43    47.5%       40        4
L1S_256    shape             72.1%       43    45.0%       40        4
L1S_256    dd+shape          67.4%       43    32.5%       40        2
L1S_256    z+chain/1.0       67.4%       43     0.0%       51        4
L1S_256    z+chain/1.25      83.7%       43     0.0%       51        4
L1S_256    z+chain/1.5       83.7%       43     0.0%       51        2
L1S_256    всё/1.25          67.4%       43    12.5%       40        4

L1S_512    финдер            64.3%       42     nan%        0        0
L1S_512    z                 97.6%       42    72.3%       47        7
L1S_512    dd                85.7%       42    51.1%       47        8
L1S_512    shape             85.7%       42    40.4%       47        6
L1S_512    dd+shape          78.6%       42    29.8%       47       10
L1S_512    z+chain/1.0       81.0%       42     0.0%       47        7
L1S_512    z+chain/1.25      81.0%       42     0.0%       47        6
L1S_512    z+chain/1.5       97.6%       42    17.0%       47        7
L1S_512    всё/1.25          78.6%       42    29.8%       47        6

L20_4096   финдер            85.2%       27     nan%        0        0
L20_4096   z                100.0%       27    26.9%       26      122
L20_4096   dd               100.0%       27    38.5%       26      128
L20_4096   shape            100.0%       27     3.8%       26      155
L20_4096   dd+shape         100.0%       27     0.0%       26      146
L20_4096   z+chain/1.0      100.0%       27     0.0%       26      130
L20_4096   z+chain/1.25     100.0%       27     0.0%       26      122
L20_4096   z+chain/1.5      100.0%       27    26.9%       26      136
L20_4096   всё/1.25         100.0%       27     0.0%       26      118

LaBr3      финдер            53.8%       52     nan%        0        0
LaBr3      z                 98.1%       52    75.8%       99     1368
LaBr3      dd                84.6%       52    54.5%       99     1388
LaBr3      shape             88.5%       52    37.4%       99     1295
LaBr3      dd+shape          80.8%       52    30.3%       99     1362
LaBr3      z+chain/1.0       69.2%       52     0.0%       99     1300
LaBr3      z+chain/1.25      69.2%       52     0.0%       99     1532
LaBr3      z+chain/1.5       69.2%       52    15.2%       99     1606
LaBr3      всё/1.25          67.3%       52     0.0%       99     1582

OBS        финдер            21.6%       51     nan%        0        0
OBS        z                 21.6%       51    50.0%       10        3
OBS        dd                21.6%       51    40.0%       10        2
OBS        shape             21.6%       51    20.0%       10        2
OBS        dd+shape          21.6%       51    20.0%       10        2
OBS        z+chain/1.0       21.6%       51     0.0%       10        3
OBS        z+chain/1.25      21.6%       51     0.0%       10        4
OBS        z+chain/1.5       21.6%       51     0.0%       10        2
OBS        всё/1.25          21.6%       51    20.0%       10        3

RC101      финдер            35.0%       20     nan%        0        0
RC101      z                 35.0%       20    50.0%       20        6
RC101      dd                35.0%       20    30.0%       20        6
RC101      shape             35.0%       20    25.0%       20        6
RC101      dd+shape          35.0%       20    15.0%       20        6
RC101      z+chain/1.0       35.0%       20    20.0%       20        6
RC101      z+chain/1.25      35.0%       20    20.0%       20        6
RC101      z+chain/1.5       35.0%       20    50.0%       20        6
RC101      всё/1.25          35.0%       20    15.0%       20        6

RC103      финдер            29.0%       62     nan%        0        0
RC103      z                 66.1%       62    81.8%       33        6
RC103      dd                61.3%       62    45.5%       33        6
RC103      shape             53.2%       62    45.5%       33        6
RC103      dd+shape          50.0%       62    27.3%       33        5
RC103      z+chain/1.0       46.8%       62     0.0%       33        6
RC103      z+chain/1.25      46.8%       62     0.0%       33        5
RC103      z+chain/1.5       66.1%       62    42.4%       33        6
RC103      всё/1.25          50.0%       62    27.3%       33        6


SimpleHPGe финдер            35.5%       31     nan%        0        0
SimpleHPGe z                 35.5%       31     nan%        0      180
SimpleHPGe dd                35.5%       31     nan%        0      186
SimpleHPGe shape             35.5%       31     nan%        0      188
SimpleHPGe dd+shape          35.5%       31     nan%        0      188
SimpleHPGe z+chain/1.0       35.5%       31     nan%        0      223
SimpleHPGe z+chain/1.25      35.5%       31     nan%        0      194
SimpleHPGe z+chain/1.5       35.5%       31     nan%        0      202
SimpleHPGe всё/1.25          35.5%       31     nan%        0      196

SrI2       финдер            57.7%       26     nan%        0        0
SrI2       z                 96.2%       26    85.7%       28       10
SrI2       dd                88.5%       26    67.9%       28        9
SrI2       shape             88.5%       26    42.9%       28        9
SrI2       dd+shape          84.6%       26    35.7%       28       10
SrI2       z+chain/1.0       57.7%       26     0.0%       28       10
SrI2       z+chain/1.25      96.2%       26     0.0%       28        9
SrI2       z+chain/1.5       96.2%       26     0.0%       28        9
SrI2       всё/1.25          84.6%       26     0.0%       28       10

ИТОГО                       recall    линий  фантомы  предъяв       мс
----------------------------------------------------------------------
финдер                       45.0%     1443        -        0        0
z                            76.9%     1443    64.7%     1821       78
dd                           71.9%     1443    51.7%     1821       79
shape                        64.7%     1443    29.0%     1821       76
dd+shape                     61.4%     1443    22.1%     1821       78
z+chain/1.0                  59.1%     1443     4.8%     1832       78
z+chain/1.25                 63.1%     1443     9.4%     1832       75
z+chain/1.5                  68.8%     1443    17.0%     1832       76
всё/1.25                     58.6%     1443    12.8%     1821       76

@Verter73

Copy link
Copy Markdown
Collaborator Author

Приложения — часть 3: правки в дереве LibraryFitLab

Четыре правки, без которых прогон не воспроизводится у постороннего. Локальные пути заменены на плейсхолдеры.

logs/07 — diff по scripts/
diff --git a/tools/LibraryFitLab/scripts/build_corpus.py b/tools/LibraryFitLab/scripts/build_corpus.py
index 072f442..814e844 100644
--- a/tools/LibraryFitLab/scripts/build_corpus.py
+++ b/tools/LibraryFitLab/scripts/build_corpus.py
@@ -100,8 +100,38 @@ NEW_DEVICE = {
                     channels=8192, dtype='AtomSpectraVCP', lo=20.0, hi=2800.0),
     'RC103g':  dict(guid='17280748-6c91-49e6-a001-3c074db3750e', name='RC-103g 1024 (corpus)',
                     channels=1024, dtype='RadiaCode', lo=15.0, hi=2800.0),
+
+    # Группы набора spectravibe-toolkit (аттестованные источники ЛСРМ).
+    'GEM20':      dict(guid='9e5a1c00-0011-4a00-9000-11c0de000011',
+                       name='HPGe 20% coax 8192 (corpus)',
+                       channels=8192, dtype='AudioInput', lo=20.0, hi=2800.0),
+    'HandyHPGe':  dict(guid='9e5a1c00-0012-4a00-9000-11c0de000012',
+                       name='HPGe portable 8192 (corpus)',
+                       channels=8192, dtype='AudioInput', lo=20.0, hi=2800.0),
+    'SimpleHPGe': dict(guid='9e5a1c00-0013-4a00-9000-11c0de000013',
+                       name='HPGe planar 8191 (corpus)',
+                       channels=8191, dtype='AudioInput', lo=20.0, hi=2800.0),
+    'HandyLaBr':  dict(guid='9e5a1c00-0014-4a00-9000-11c0de000014',
+                       name='LaBr3 portable 1024 (corpus)',
+                       channels=1024, dtype='AudioInput', lo=20.0, hi=2800.0),
+    'HandyNaI':   dict(guid='9e5a1c00-0015-4a00-9000-11c0de000015',
+                       name='NaI portable 1024 (corpus)',
+                       channels=1024, dtype='AudioInput', lo=20.0, hi=2800.0),
+    'G1S':        dict(guid='9e5a1c00-0016-4a00-9000-11c0de000016',
+                       name='NaI 63x63 1024 (corpus)',
+                       channels=1024, dtype='AudioInput', lo=30.0, hi=2900.0),
 }
 
+# Ступени лестницы по числу каналов: тот же прибор, пересыпанная сетка.
+for _i, (_pfx, _base, _lo, _hi) in enumerate(
+        (('L1S', 1024, 30.0, 2900.0), ('L20', 8192, 20.0, 2800.0))):
+    for _j, _div in enumerate((2, 4, 8)):
+        _n = _base // _div
+        NEW_DEVICE['%s_%d' % (_pfx, _n)] = dict(
+            guid='9e5a1c00-00%d%d-4a00-9000-11c0de0000%d%d' % (2 + _i, _j, 2 + _i, _j),
+            name='%s %d ch (corpus)' % ('NaI 63x63' if _pfx == 'L1S' else 'HPGe 20%', _n),
+            channels=_n, dtype='AudioInput', lo=_lo, hi=_hi)
+
 def device_guid(det):
     if det in NEW_DEVICE:
         return NEW_DEVICE[det]['guid']
@@ -564,6 +594,57 @@ MANIFEST_COLUMNS = [
 ]
 
 
+def merge_manifest(new_rows):
+    """Слить новые строки манифеста с уже записанными, по ключу key."""
+    path = os.path.join(CORPUS, 'manifest.csv')
+    merged = {}
+    if os.path.isfile(path):
+        with open(path, encoding='utf-8-sig', newline='') as fh:
+            for r in csv.DictReader(fh):
+                r['live'] = r.pop('live_s', '')
+                merged[r['key']] = r
+    for r in new_rows:
+        merged[r['key']] = r
+    rows = list(merged.values())
+    write_manifest(rows)
+    return rows
+
+
+def merge_detectors(state, res_coef, rows):
+    """То же для detectors.csv: строки чужих групп сохраняются как есть."""
+    path = os.path.join(CORPUS, 'detectors.csv')
+    header = ['det', 'channels', 'spectra', 'e_lo', 'e_hi',
+              'res_c0', 'res_c1', 'res_c2', 'fwhm_662_pct']
+    old = {}
+    if os.path.isfile(path):
+        with open(path, encoding='utf-8-sig', newline='') as fh:
+            for r in csv.DictReader(fh):
+                old[r['det']] = [r.get(c, '') for c in header]
+
+    channels, ranges = {}, {}
+    for st in state.values():
+        channels.setdefault(st['det'], st['sp'].n)
+        cfg = st.get('peak_config') or PEAK_CONFIG_DEFAULTS
+        ranges.setdefault(st['det'], (cfg['Min_Range'], cfg['Max_Range']))
+
+    for det in sorted(res_coef):
+        if det not in channels:      # группа не пересобиралась — не трогаем
+            continue
+        coef = res_coef[det]
+        rf = corpus_calib.resolution_fn(coef)
+        lo, hi = ranges.get(det, (30.0, 2800.0))
+        old[det] = [det, channels[det], sum(1 for r in rows if r['det'] == det),
+                    lo, hi, repr(float(coef[0])), repr(float(coef[1])),
+                    repr(float(coef[2])), round(100 * rf(662.0) / 662.0, 2)]
+
+    with open(path, 'w', encoding='utf-8-sig', newline='') as fh:
+        w = csv.writer(fh)
+        w.writerow(header)
+        for det in sorted(old):
+            w.writerow(old[det])
+    print('детекторы: %s (%d групп)' % (path, len(old)))
+
+
 def legacy_manifest(entries):
     """Строки манифеста для девятки — из data/calibration.json, где лежат те же
     коэффициенты, которыми сделаны её рабочие копии."""
@@ -839,6 +920,13 @@ def main():
         rows = legacy_manifest(legacy_rows) + rows
         write_manifest(rows)
         write_detectors(state, res_coef, rows)
+    else:
+        # Частичная пересборка. Раньше при --only манифест и detectors.csv не
+        # трогались вовсе, и пересобранные спектры в них не появлялись. Полная
+        # пересборка тут невозможна: исходников остальных записей на этой
+        # машине нет. Поэтому обе таблицы сливаются по ключу.
+        rows = merge_manifest(rows)
+        merge_detectors(state, res_coef, rows)
 
     with open(os.path.join(HERE, 'corpus_state.json'), 'w', encoding='utf-8') as fh:
         json.dump({k: dict(det=v['det'], ecal=[float(c) for c in v['ecal'].coef],
diff --git a/tools/LibraryFitLab/scripts/chains.py b/tools/LibraryFitLab/scripts/chains.py
index d751dcb..f3156b5 100644
--- a/tools/LibraryFitLab/scripts/chains.py
+++ b/tools/LibraryFitLab/scripts/chains.py
@@ -13,10 +13,19 @@ but with two corrections that matter for a library fit:
     factor of the 212BI branch, otherwise a bound group mixing 208TL and 212BI
     lines gets wrong weights.
 """
+import os
 import sqlite3
 import re
 
-DB = r'C:\Users\moroz\source\repos\BQ Eng res .NET 4.8\BecquerelMonitor\nucdb.sqlite'
+# Путь к базе был жёстко задан на машину автора. Здесь он ищется рядом с
+# решением (../../../BecquerelMonitor/nucdb.sqlite), переопределяется
+# переменной LFL_NUCDB, и только потом падает на исходный литерал.
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_HERE)))  # scripts -> LibraryFitLab -> tools -> repo
+_LOCAL = os.path.join(_ROOT, 'BecquerelMonitor', 'nucdb.sqlite')
+DB = os.environ.get('LFL_NUCDB') or (
+    _LOCAL if os.path.isfile(_LOCAL)
+    else r'C:\Users\moroz\source\repos\BQ Eng res .NET 4.8\BecquerelMonitor\nucdb.sqlite')
 
 
 def conn():
diff --git a/tools/LibraryFitLab/scripts/corpus_def.py b/tools/LibraryFitLab/scripts/corpus_def.py
index 0933465..0537eae 100644
--- a/tools/LibraryFitLab/scripts/corpus_def.py
+++ b/tools/LibraryFitLab/scripts/corpus_def.py
@@ -26,6 +26,28 @@ def p(*parts):
     return os.path.join(LIB, *parts)
 
 
+# Вторая библиотека: опубликованный набор spectravibe-toolkit. Спектры лежат
+# в BecqMoni XML уже готовыми, поэтому путь ведёт прямо к копии.
+LIB2 = os.environ.get(
+    'LFL_LIB2', r'<SPECTRAVIBE>\detectors')
+
+
+def q(cls, *parts):
+    return os.path.join(LIB2, cls, 'reference_spectra', 'becqmoni', *parts)
+
+
+def qbg(cls, *parts):
+    """Фон. С 27.07 набор держит фоны отдельным деревом: вшивание переносило
+    калибровку фона в файл образца, и полином 5-й степени у фона делал
+    непригодным весь документ."""
+    return os.path.join(LIB2, cls, 'reference_spectra', 'background', 'becqmoni', *parts)
+
+
+def kit(*parts):
+    return os.path.join(LIB2, 'Gamma-1S', 'reference_spectra',
+                        'reference_kits_becqmoni', *parts)
+
+
 # ---------------------------------------------------------------------------
 # Девять спектров исходного исследования. Их рабочие копии уже посчитаны и
 # лежат в scripts/spectra; корпус берёт их БАЙТ-В-БАЙТ, иначе числа отчёта
@@ -240,4 +262,134 @@ NEW = [
              'сам по себе, как спектр'),
 ]
 
+# ---------------------------------------------------------------------------
+# Пополнение из набора spectravibe-toolkit (аттестованные источники ЛСРМ).
+# Ось, которой не было ни в девятке, ни в пополнении: ОДНА И ТА ЖЕ цепочка
+# Th-232 на шести классах детекторов от HPGe 8192 до NaI 1024 — то есть при
+# разрешении, различающемся в тридцать раз, и при неизменном содержимом.
+# Отдельно: спектры Th-228, где ряд оборван СВЕРХУ (нет 228Ac 911/969), —
+# зеркало к урановому стеклу, где он оборван снизу.
+# ---------------------------------------------------------------------------
+OURS = [
+    # --- HPGe 20 % общего назначения, объёмная геометрия Маринелли ----------
+    dict(key='G20_Th232_Mar', det='GEM20', channels=8192, chains=['Th-232'],
+         path=q('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Marinelli', 'm_th06.xml'),
+         bg=qbg('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Background', 'Bckg_1.xml'),
+         why='аттестованный объёмный Th-232 в равновесии на германии: полный '
+             'ряд разрешён, включая 911/969 Ac-228. У HPGe корпуса ряд оборван '
+             '(урановые пуговицы), и вето по набору там падает до базы финдера'),
+    dict(key='G20_Ra226_Mar', det='GEM20', channels=8192, chains=['Ra-226'],
+         path=q('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Marinelli', 'm_ra06.xml'),
+         bg=qbg('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Background', 'Bckg_1.xml'),
+         why='аттестованный Ra-226 на германии — единственный в корпусе '
+             'радий с паспортом, остальные природные'),
+    dict(key='G20_Cs137_Mar', det='GEM20', channels=8192, nuclides=['137CS'],
+         path=q('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Marinelli', 'm_cs06.xml'),
+         bg=qbg('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Background', 'Bckg_1.xml'),
+         why='негатив на германии в той же геометрии: цепочки нет вовсе'),
+    dict(key='G20_Th228_P25', det='GEM20', channels=8192, chains=['Th-232'],
+         path=q('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Point25', 'Th228-SRC-05-25cm.xml'),
+         bg=qbg('GP_HPGe20', 'Work', 'GP', 'HPGe(20_)', 'Spe', 'Background', 'Bckg_1.xml'),
+         why='ряд оборван сверху: Th-228 даёт всё ниже Ra-224 и ни одной '
+             'линии Ac-228 — проверка, снимет ли вето набор с настоящими '
+             'линиями, но неполный'),
+
+    # --- HPGe портативный ---------------------------------------------------
+    dict(key='HHP_Th232', det='HandyHPGe', channels=8192, chains=['Th-232'],
+         path=q('Handy_HPGe', 'Work', 'Handy', 'Handy(HPGe)', 'Spe', 'Th-232  17 kBq.xml'),
+         why='тот же Th-232 на втором германии другой модели'),
+    dict(key='HHP_Th228', det='HandyHPGe', channels=8192, chains=['Th-232'],
+         path=q('Handy_HPGe', 'Work', 'Handy', 'Handy(HPGe)', 'Spe', 'Th-228  68 kBq.xml'),
+         why='пара к HHP_Th232: то же место, оборванный сверху ряд'),
+    dict(key='HHP_Co60', det='HandyHPGe', channels=8192, nuclides=['60CO'],
+         path=q('Handy_HPGe', 'Work', 'Handy', 'Handy(HPGe)', 'Spe', 'Co-60  198 kBq.xml'),
+         why='негатив: две линии 1173/1332 и ничего больше'),
+
+    # --- HPGe планарный, демонстрационный -----------------------------------
+    dict(key='SHP_Th228', det='SimpleHPGe', channels=8191, chains=['Th-232'],
+         path=q('Simple_HPGe', 'Work', 'Simple', 'HPGe(Demo)', 'SPE', '10cm', 'Th228_10cm.xml'),
+         why='третий класс германия'),
+    dict(key='SHP_Eu152', det='SimpleHPGe', channels=8191, nuclides=['152EU'],
+         path=q('Simple_HPGe', 'Work', 'Simple', 'HPGe(Demo)', 'SPE', '10cm', 'Eu152_10cm.xml'),
+         why='негатив с богатым спектром: Eu-152 даёт больше десятка сильных '
+             'линий, часть рядом с линиями ториевого ряда — ловушка на якорь'),
+
+    # --- LaBr3 --------------------------------------------------------------
+    dict(key='LB_Th228', det='HandyLaBr', channels=1024, chains=['Th-232'],
+         path=q('Handy_LaBr', 'Work', 'Handy', 'Handy(LaBr)', 'Spe', 'Th228_#SRC-17_24sm.xml'),
+         bg=q('Handy_LaBr', 'Work', 'Handy', 'Handy(LaBr)', 'Spe', 'Background', 'Background_1.xml'),
+         why='LaBr3 с аттестованным источником — у корпуса LaBr3 только руда '
+             'без паспорта'),
+    dict(key='LB_Background', det='HandyLaBr', channels=1024, nuclides=['40K'],
+         path=q('Handy_LaBr', 'Work', 'Handy', 'Handy(LaBr)', 'Spe', 'Background', 'Background_1.xml'),
+         why='фон LaBr3: собственная активность La-138 и ряда Ac-227 без '
+             'образца — негатив, где якорь обязан не сработать'),
+    dict(key='LB_Ba133', det='HandyLaBr', channels=1024, nuclides=['133BA'],
+         path=q('Handy_LaBr', 'Work', 'Handy', 'Handy(LaBr)', 'Spe', 'Ba133_#SRC-13_24sm.xml'),
+         bg=q('Handy_LaBr', 'Work', 'Handy', 'Handy(LaBr)', 'Spe', 'Background', 'Background_1.xml'),
+         why='негатив LaBr3 и опорные линии для модели разрешения группы'),
+
+    # --- NaI портативный, 1024 канала ---------------------------------------
+    dict(key='HN_Th232', det='HandyNaI', channels=1024, chains=['Th-232'],
+         path=q('Handy_NaI', 'Work', 'Handy', 'Handy(NaI)', 'Spe', 'Th-232  A=16700 Bq.xml'),
+         why='та же цепочка на 1024 каналах сцинтиллятора'),
+    # HN_Th228 (Handy_NaI, 'Th-228  A=67700 Bq  in KT1-5.xml') отвергнут на
+    # приёмке: калибровка не легла ни одним из трёх кандидатов (0 принятых
+    # линий, невязка 1.18 FWHM при пороге 0.25). Источник в контейнере КТ1-5,
+    # линии ниже 300 кэВ поглощены, а выше — сдвиг усиления, который не
+    # описывается ни поправкой масштаба, ни полиномом по двум точкам.
+
+    # --- NaI 63x63 Гамма-1С: фон измерен и вшит в тот же файл ---------------
+    dict(key='G1S_Th232_Denta', det='G1S', channels=1024, chains=['Th-232'],
+         path=kit('Denta_120mL', 'Th-232', 'sample_Th232_420-7-17_Дента-120мл_0cm.xml'),
+         why='аттестованный объёмный Th-232, фон той же геометрии измерен и '
+             'лежит в том же файле — таких пар в корпусе было две из сорока шести'),
+    dict(key='G1S_Ra226_Denta', det='G1S', channels=1024, chains=['Ra-226'],
+         path=kit('Denta_120mL', 'Ra-226', 'sample_Ra226_420-7-18_Дента-120мл_0cm.xml'),
+         why='аттестованный Ra-226 с измеренным фоном той же геометрии'),
+    dict(key='G1S_Cs137_Denta', det='G1S', channels=1024, nuclides=['137CS'],
+         path=kit('Denta_120mL', 'Cs-137', 'sample_Cs137_420-7-14_Дента-120мл_0cm.xml'),
+         why='негатив в той же геометрии и с тем же фоном'),
+    dict(key='G1S_K40_Denta', det='G1S', channels=1024, nuclides=['40K'],
+         path=kit('Denta_120mL', 'K-40', 'sample_K40_420-7-20_Дента-120мл_0cm.xml'),
+         why='негатив: одна линия 1460 кэВ, ближайший сосед ториевого ряда — '
+             '1495 Ac-228, то есть проверка разноса на 1024 каналах'),
+]
+
+# ---------------------------------------------------------------------------
+# Лестница по числу каналов. Вопрос из отчёта о согласованности набора: вето
+# заваливает ASN8 на 1024 и 3000 каналах и даёт 95.5 % на 2048 и 4096 при том
+# же приборе и образце. Там ступени — РАЗНЫЕ измерения: вместе с числом каналов
+# менялись статистика на канал, мёртвое время и момент времени. Здесь ступени
+# получены арифметической пересыпкой ОДНОГО измерения, поэтому меняется только
+# сетка. Две цепочки на класс, чтобы модель разрешения ступени строилась не по
+# одному спектру.
+# ---------------------------------------------------------------------------
+LADDER_DIR = os.environ.get(
+    'LFL_LADDER',
+    r'<LADDER_DIR>'
+    r'\C--Users----------claude-Service\4834f3cb-58ae-439d-9f1f-498a22617384'
+    r'\scratchpad\ladder')
+
+
+def _ladder(prefix, det_prefix, base_channels):
+    out = []
+    for div in (2, 4, 8):
+        nch = base_channels // div
+        for chain, tag in (('Th-232', 'Th232'), ('Ra-226', 'Ra226')):
+            out.append(dict(
+                key='%s_%s_%d' % (prefix, tag, nch),
+                det='%s_%d' % (det_prefix, nch),
+                channels=nch, chains=[chain],
+                path=os.path.join(LADDER_DIR, '%s_%s_div%d.xml' % (prefix, tag, div)),
+                why='ступень лестницы: тот же спектр, пересыпанный в %d каналов' % nch))
+    return out
+
+
+LADDER = _ladder('G1S', 'L1S', 1024) + _ladder('G20', 'L20', 8192)
+
+# build_corpus.py собирает по списку NEW, поэтому наши записи вливаются в него,
+# а не живут отдельным списком.
+NEW = NEW + OURS + LADDER
+
 ALL = LEGACY + NEW
diff --git a/tools/LibraryFitLab/scripts/gate_study.py b/tools/LibraryFitLab/scripts/gate_study.py
index b7c60e5..807f420 100644
--- a/tools/LibraryFitLab/scripts/gate_study.py
+++ b/tools/LibraryFitLab/scripts/gate_study.py
@@ -178,16 +178,25 @@ def set_names(det):
     return names
 
 
-def run():
+def run(only_dets=None):
     rows = manifest()
     by_det = defaultdict(list)
     for r in rows:
         by_det[r['det']].append(r['key'])
     dets = sorted(by_det)
 
+    # Сеты строятся ВСЕГДА на весь корпус: sets_manifest.json — общий файл, и
+    # если собрать его по части групп, отчёт потеряет линии обманок у всех
+    # остальных и покажет у них пустые фантомы.
     print('сеты для %d групп...' % len(dets))
     build_sets(dets)
 
+    # --dets=A,B — прогнать харнесс только по этим группам. Полный прогон идёт
+    # больше двух часов, а после починки одной группы пересчитывать остальные
+    # незачем: результаты лежат в out_gate пофайлово и дополняются на месте.
+    if only_dets:
+        dets = [d for d in dets if d in only_dets]
+
     os.makedirs(OUT, exist_ok=True)
     for det in dets:
         wd = prepare_workdir(det, by_det[det])
@@ -344,6 +353,10 @@ def report():
 
 if __name__ == '__main__':
     if '--run' in sys.argv:
-        run()
+        sel = None
+        for a in sys.argv[1:]:
+            if a.startswith('--dets='):
+                sel = set(a.split('=', 1)[1].split(','))
+        run(sel)
     if '--report' in sys.argv or '--run' not in sys.argv:
         report()
logs/05 — строки манифеста по нашим спектрам
key,det,channels,live_s,counts,chains,nuclides,background,ecal_mode,ecal_lines,ecal_rms_kev,ecal_rms_fwhm,bg_ecal_mode,fwhm_662_pct,result_data,why
G20_Th232_Mar,GEM20,8192,3585.2,690674,Th-232,,Bckg_1.xml,affine/grp,18,0.1,0.088,stored,0.2,0,"аттестованный объёмный Th-232 в равновесии на германии: полный ряд разрешён, включая 911/969 Ac-228. У HPGe корпуса ряд оборван (урановые пуговицы), и вето по набору там падает до базы финдера"
G20_Ra226_Mar,GEM20,8192,3600.0,459140,Ra-226,,Bckg_1.xml,affine/grp,12,0.04,0.024,stored,0.2,0,"аттестованный Ra-226 на германии — единственный в корпусе радий с паспортом, остальные природные"
G20_Cs137_Mar,GEM20,8192,1800.0,67391,,137CS,Bckg_1.xml,ref-cal(G20_Th232_Mar),1,0.3,0.201,stored,0.2,0,негатив на германии в той же геометрии: цепочки нет вовсе
G20_Th228_P25,GEM20,8192,2700.0,536999,Th-232,,Bckg_1.xml,poly2/grp,11,0.12,0.067,stored,0.2,0,"ряд оборван сверху: Th-228 даёт всё ниже Ra-224 и ни одной линии Ac-228 — проверка, снимет ли вето набор с настоящими линиями, но неполный"
HHP_Th232,HandyHPGe,8192,300.0,94237,Th-232,,встроен,affine,12,0.17,0.124,affine,0.21,0,тот же Th-232 на втором германии другой модели
HHP_Th228,HandyHPGe,8192,300.0,174257,Th-232,,встроен,poly2/grp,6,0.09,0.057,affine,0.21,0,"пара к HHP_Th232: то же место, оборванный сверху ряд"
HHP_Co60,HandyHPGe,8192,300.0,486346,,60CO,встроен,poly1/grp,3,0.02,0.011,affine,0.21,0,негатив: две линии 1173/1332 и ничего больше
SHP_Th228,SimpleHPGe,8191,3566.9,949114,Th-232,,встроен,affine/grp,7,0.04,0.033,gain,0.21,0,третий класс германия
SHP_Eu152,SimpleHPGe,8191,3518.1,2326412,,152EU,встроен,affine,9,0.01,0.005,gain,0.21,0,"негатив с богатым спектром: Eu-152 даёт больше десятка сильных линий, часть рядом с линиями ториевого ряда — ловушка на якорь"
LB_Th228,HandyLaBr,1024,3600.0,648015,Th-232,,встроен,poly2/robust,11,5.82,0.169,как передний план,3.39,0,LaBr3 с аттестованным источником — у корпуса LaBr3 только руда без паспорта
LB_Background,HandyLaBr,1024,3600.0,416667,,40K,нет,poly2,5,5.85,0.211,-,3.39,0,"фон LaBr3: собственная активность La-138 и ряда Ac-227 без образца — негатив, где якорь обязан не сработать"
LB_Ba133,HandyLaBr,1024,3600.0,1060053,,133BA,встроен,affine/robust,9,4.03,0.141,как передний план,3.39,0,негатив LaBr3 и опорные линии для модели разрешения группы
HN_Th232,HandyNaI,1024,297.0,57565,Th-232,,встроен,affine/grp,4,12.53,0.151,stored,5.31,0,та же цепочка на 1024 каналах сцинтиллятора
G1S_Th232_Denta,G1S,1024,6309.1,702463,Th-232,,встроен,affine/robust/grp,11,7.33,0.139,gain,7.34,0,"аттестованный объёмный Th-232, фон той же геометрии измерен и лежит в том же файле — таких пар в корпусе было две из сорока шести"
G1S_Ra226_Denta,G1S,1024,6937.7,232899,Ra-226,,встроен,affine,12,4.56,0.112,gain,7.34,0,аттестованный Ra-226 с измеренным фоном той же геометрии
G1S_Cs137_Denta,G1S,1024,4819.6,59058,,137CS,встроен,gain/grp,2,2.78,0.044,gain,7.34,0,негатив в той же геометрии и с тем же фоном
G1S_K40_Denta,G1S,1024,57055.2,522328,,40K,встроен,affine/grp,9,11.22,0.154,gain,7.34,0,"негатив: одна линия 1460 кэВ, ближайший сосед ториевого ряда — 1495 Ac-228, то есть проверка разноса на 1024 каналах"
G1S_Th232_512,L1S_512,512,6309.1,702463,Th-232,,встроен,gain/robust/grp,11,8.36,0.161,affine,6.85,0,"ступень лестницы: тот же спектр, пересыпанный в 512 каналов"
G1S_Ra226_512,L1S_512,512,6937.7,232899,Ra-226,,встроен,gain,10,3.4,0.06,affine,6.85,0,"ступень лестницы: тот же спектр, пересыпанный в 512 каналов"
G1S_Th232_256,L1S_256,256,6309.1,702463,Th-232,,встроен,gain,10,7.1,0.126,gain,6.43,0,"ступень лестницы: тот же спектр, пересыпанный в 256 каналов"
G1S_Ra226_256,L1S_256,256,6937.7,232899,Ra-226,,встроен,poly2,10,8.21,0.173,gain,6.43,0,"ступень лестницы: тот же спектр, пересыпанный в 256 каналов"
G1S_Th232_128,L1S_128,128,6309.1,702463,Th-232,,встроен,affine/grp,4,0.39,0.008,stored,7.21,0,"ступень лестницы: тот же спектр, пересыпанный в 128 каналов"
G1S_Ra226_128,L1S_128,128,6937.7,232899,Ra-226,,встроен,poly2,5,4.38,0.066,stored,7.21,0,"ступень лестницы: тот же спектр, пересыпанный в 128 каналов"
G20_Th232_4096,L20_4096,4096,3585.2,690674,Th-232,,встроен,poly2/robust,17,0.28,0.126,stored,0.22,0,"ступень лестницы: тот же спектр, пересыпанный в 4096 каналов"
G20_Ra226_4096,L20_4096,4096,3600.0,459140,Ra-226,,встроен,affine/grp,13,0.11,0.057,stored,0.22,0,"ступень лестницы: тот же спектр, пересыпанный в 4096 каналов"

@Verter73

Copy link
Copy Markdown
Collaborator Author

Приписка: наши комментарии разошлись во времени

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

Числа сошлись

критерий у вас, 69 спектров у нас, 63 спектра
финдер 44,1 % 43,4 %
z 77,2 / 63,7 75,0 / 65,0
вето/1.0 58,4 / 6,4 56,2 / 4,8
вето/1.25 64,0 / 9,7 60,2 / 8,5

Корпуса пересекаются, но не совпадают: у вас 23 спектра из набора, у нас 17 плюс восемь ступеней пересыпки, состав групп разный. При этом числа согласуются, и обе стороны независимо получили смещение оптимума с 1.25 к 1.0 при расширении корпуса. Как перекрёстная проверка это, по-моему, ценнее самих чисел.

Совпал и вывод про аттестованные источники: у вас Гамма-1С даёт 94,7 % recall при 6,5 % фантомов, у нас 97,6 % при 7,8 %. Вето на чистой одиночной цепочке действительно не стоит ничего.

Th-228 — ваше решение правильнее нашего

Вы завели его отдельной цепочкой, потому что записывать в знаменатель recall линии Ac-228, которых в источнике нет, нельзя. Мы засчитывали Th-228 как Th-232 и оговорили это пятым пунктом в разделе оговорок — но оговорка не исправляет метрику. Наши числа по спектрам Th-228 занижены по построению, это касается HHP_Th228, SHP_Th228, G20_Th228_P25 и всей группы SimpleHPGe. Выводы по германию это не меняет — они опираются на спектры с полным рядом, — но при сверке имейте в виду.

Ребиннинг отвечает не на тот вопрос, на который вы его закрыли

Вы пишете, что ториевая цепочка теперь есть на 1024, 4095 и 8192 каналах и это же отвечает на предложение про ребиннинг. Это другой опыт: там вместе с числом каналов меняются кристалл, разрешение, статистика и геометрия — то есть та же слабость, что у лестницы ASN8, где ступени были разными измерениями.

Наша лестница — одно измерение под четырьмя сетками, полученными арифметической пересыпкой: отсчёты суммируются по N подряд, калибровка пересчитывается подстановкой n → N·n точно, без подгонки. Больше не меняется ничего. Результат (раздел 8 отчёта):

каналов каналов на FWHM z вето/1.0 вето/1.25
1024 17,2 97,6 / 66,7 97,6 / 7,8 97,6 / 17,8
512 8,0 97,6 / 72,3 81,0 / 0,0 81,0 / 0,0
256 3,8 83,7 / 62,5 67,4 / 0,0 83,7 / 0,0
128 2,1 90,5 / 70,5 90,5 / 18,2 90,5 / 70,5

При 2,1 канала на полуширину вето с порогом 1.25 не отсекает ничего — 70,5 % фантомов, ровно как без вето. При этом на германии при той же плотности каналов (ступень 4096) оно даёт 100 % recall и ноль фантомов.

Отсюда вывод, который из вашего материала не следует: определяет не плотность каналов, а то, разделяет ли сетка соседние линии цепочки. У германия при 4096 линии по-прежнему раздельны, у NaI при 128 каналах цепочка становится блендом, и разбросу вокруг кривой нечего мерить.

Это же ставит под сомнение исходное объяснение аномалии ASN8: наша лестница монотонна (17,2 → 8,0 → 3,8 работают, 2,1 ломается), а ваш ряд немонотонен — 1024 и 3000 плохо, 2048 и 4096 хорошо. Монотонная зависимость немонотонного ряда не объясняет. Искать стоит в том, что у вас менялось вместе с настройкой MCA, а при пересыпке не менялось: статистика на канал, мёртвое время, Ch_Concat в конфигурации устройства.

На германии устойчивость к модели фона обыгрывает вето — по вашим же числам

В вашей таблице новых групп:

детектор ΔD + устойчивость z + вето/1.25
HPGE_GMX 41,2 % / 1,8 % 59,8 % / 7,3 %
HPGE_GEM 91,4 % / 2,9 % 94,8 % / 21,2 %

У нас на своих группах то же самое и резче: shape даёт 2,0 % и 1,7 % фантомов против 25,5 % и 23,3 % у вето при одинаковом recall.

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

Про дефект полинома

Вы нашли то, чего мы не нашли: DetectPeak под catch-all в DCPeakDetectionView, поэтому поиск пиков молча не работал совсем. Мы дошли только до отрисовки и до отказа загрузки в харнессе. Ваш разбор полнее, возражений нет.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Довесок: ещё одно место, где ошибка неотличима от результата

Прошли расчётный тракт той же линзой, которой вы нашли catch-all в DCPeakDetectionView — 15 файлов, которые считают спектр. Ниже только то, что стоит внимания; остальные молчаливые обработчики оказались либо осознанными (OutofChannelException в отрисовке, глотание координат NaN при рисовании), либо уже вами исправленными. По коммиту c115289.

EnergyToChannel при неудаче возвращает канал 0

PolynomialEnergyCalibration.cs, четыре ветки, все одинаково: строка 284 (порядок 2), 317 (3), 300 (4) и 344 — ваша новая ветка для степеней выше четвёртой.

Ноль — валидный номер канала. Вызывающий не отличит «не смог посчитать» от «энергия приходится на левый край шкалы». Рядом видно, что вопрос вы уже обдумывали: throw и MessageBox стоят закомментированными.

Что с этим делают потребители.

MeasurementResultManager.cs:234-235 — границы ROI:

lowerLimitChannel = (int)Math.Ceiling(this.energyCalibration.EnergyToChannel(lowerLimit, ...));
upperLimitChannel = (int)Math.Floor(this.energyCalibration.EnergyToChannel(upperLimit, ...));

Ловится только OutofChannelException — возврат нуля исключением не является. Дальше for (int i = lowerLimitChannel; i <= upperLimitChannel; i++): если ноль вернулся у верхней границы, цикл не выполнится ни разу и площадь окажется нулевой; если у обеих — окно схлопнется в канал 0. Те же строки повторяются на 291–292 и 308–311 — фоновое окно и боковые области.

DoseRateManager.cs:39-40 — границы мощности дозы:

int startch = (int)calibration.EnergyToChannel(point.LowerBound, ...);
int endch   = (int)calibration.EnergyToChannel(point.UpperBound, ...);
if (startch < 0) startch = 0;
if (endch >= energySpectrum.Spectrum.Length) endch = ...;

Клампы только по размеру массива, нулевой возврат проходит. При endch = 0 цикл не выполняется, counts == 0, и точка калибровки дозы молча выпадает из расчёта.

От вашего случая это отличается тем, что там пропадали пики целиком — и это видно, — а здесь число выводится, просто неправильное.

Лечится тем же приёмом, что вы применили к 5-й степени: либо OutofChannelException, которую потребители уже ловят и обрабатывают осмысленно, либо double.NaN с проверкой на стороне вызова. Возврат нуля — единственный вариант, при котором ошибка выглядит как результат.

Два мелких, на ваше усмотрение

  • PeakStabilizer.cs:102catch (Exception) { return; }: стабилизатор молча выключается при неудаче решения СЛАУ. Для фонового процесса молчание разумно, но нет ни счётчика, ни флага, поэтому «не сработало» неотличимо от «не потребовалось».
  • EnergySpectrumView.cs:459, 467 — пустые catch вокруг получения синглтонов: поле остаётся null, а падение случится позже и в другом месте.

Am6er added a commit that referenced this pull request Jul 27, 2026
A hypothesis rejected, a half-fix finished, and three corpus spectra rebuilt on
a background that was paired at random.

The gate. Every per-line criterion measures significance against one fixed
continuum, and z and dD divide by a Poisson error alone. Decoys pass z>=4 at
64% against a nominal 3e-5, so the null hypothesis is wrong, not the threshold.
The proposal was to measure the continuum-model error in situ, the way high
energy physics measures a spurious signal: fit the same profile at K offset
positions, take the robust spread as the local systematic, divide by it.

The first run said 0.14 false lines per true against 0.44 for plain z. It was
wrong. Offsets landing within 1.5 FWHM of another chain line were discarded,
and next to a chain line on a scintillator there is no empty space: 4305 of
7030 offsets went, and 62% of lines got no estimate at all. What survived was
mostly germanium. That is the subset trap the full-corpus rule exists to catch,
sprung inside a single script rather than between runs; only a coverage counter
makes it visible, so the script now always prints one.

Contamination by a foreign line is one sided, so the scale can be taken from
the lower half and nothing needs masking. Coverage went to 644 real and 539
decoy lines, one line short. The advantage vanished with the masking. At
matched recall all four robust estimators are indistinguishable from plain z
from 40% recall upward - 24.1% decoys for z against 23.6 to 26.0% - and the
only gain is at 25 to 30% recall, the region where a criterion that rejects
everything looks excellent and delivers nothing. The reason is that the local
continuum error at a real line and at a decoy a few FWHM away is the same
quantity: dividing by it rescales both numerators alike. My reading of the
literature took the variance half of the spurious-signal method; the half that
works is about bias, which is what the background-shape test already does.

Two things did come out of it. shape+chain had never been measured - the
journal blamed dD for starving the veto - and it turns out shape starves it by
itself: same recall as z+chain, 10.4% phantoms against 6.4%. And the veto's
fallback, noted in the journal and never built, is now built and measured. It
must fire only where the veto abstains: letting it also replace the veto's
verdict on an inconsistent set returns a third of the decoy lines the veto used
to kill, and phantoms on Gamma-1S go from 6.5% to 33.9% at unchanged recall.
Restricted to the abstain branch it closes the hole where nothing judged at
all: 63.5% recall at 8.6% phantoms against 64.0/9.7, free in time.
ChainConsistencyMinLines is no longer const - a curve that can be fitted and a
verdict that can be trusted are different things, and 6 measures better than 4.

The calibration fix from c115289 was half of one. CheckCalibration still
rejected every order above four, and CheckDocument repairs a rejected
calibration by dropping trailing zero coefficients - of which a real
fifth-degree polynomial has none, so it fell through to the branch that
substitutes defaults. A fifth-degree background therefore destroyed the scale
of the whole document even when the spectrum itself was linear. Order above
four is now checked like orders 1 to 4 and the monotonicity pass decides.

Upstream published detectors/CHANGELOG.md: backgrounds were paired by counting
shared path components, and a Marinelli of distilled water ended up attached to
point sources and barrels. Three of our GEM20 spectra carried such a pairing,
with a fifth-degree calibration on top. Re-paired explicitly with Bckg_5 - same
Marinelli geometry, distilled water, second-degree calibration, five hours -
which for three Marinelli samples is the proper blank. The LaBr3 pairing is
uncurated but is a background of the same detector under point sources; kept,
with the reason written next to the entry. All 24 files re-imported, corpus
rebuilt, nothing above fourth order left, acceptance unmoved.

Reproducibility, from Verter73's report on PR #32: chains.py held an absolute
path to nucdb.sqlite on one machine, and mkconfig.py wanted a gitignored
intermediate. Both fixed - the pipeline could not start for anyone else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@Verter73 — спасибо за отчёт, он оказался полезнее спектров. Прогнал всё по вашим замечаниям, коммит 2158004. По пунктам: что исправил, что подтвердилось, и одна гипотеза, которую пришлось отвергнуть.

Ваши две правки для постороннего — сделаны

chains.py держал абсолютный путь к nucdb.sqlite на моей машине; теперь база ищется относительно дерева решения и переопределяется LFL_NUCDB. mkconfig.py ждал scripts/calibration.json, который в .gitignore; теперь при его отсутствии берётся data/calibration.json. Обе — мой недосмотр, конвейер у вас не должен был запускаться вообще.

Про полином 5-й степени: вы правы, вчерашняя правка была половинчатой

Я чинил ChannelToEnergy и EnrgToChannel, а вы указали на CheckCalibration() и DocumentManager.CheckDocument(). Проверил — вы точны, и следствие жёстче, чем я думал.

CheckCalibration отвергала любой порядок выше четвёртого, а ветка починки в CheckDocument понижает степень только за счёт хвостовых нулевых коэффициентов. У настоящего полинома 5-й степени нулей нет, срабатывает zerosCount == 0, и калибровка заменяется на дефолтную. То есть фон 5-й степени убивал шкалу всего документа даже когда у самого спектра она линейная — ровно ваша формулировка.

Исправлено: порядок выше четвёртого проверяется теперь так же, как 1–4 (длина массива, ненулевой старший член), а решает проход по монотонности, который там и был. probes/CalibrationProbe.cs дополнен проверкой CheckCalibration — на Y88-SRC-05-25cm.xml печатает «степень 5: принята», шкала сходится до 0.000 кэВ на всех контрольных каналах.

Про фоны: забрал ваш CHANGELOG, три спектра пересобраны

detectors/CHANGELOG.md про случайные пары ударил прямо по нашим файлам. Три спектра GP_HPGe20 в корпусе (HPGeGEM_Th232, Ra226, K40) несли автоматически подобранный фон, да ещё с полиномом 5-й степени.

Пару задал заново и явно: Bckg_5 — та же геометрия маринелли, дистиллированная вода, калибровка 2-й степени, 5 часов набора. Для трёх образцов в маринелли это штатный бланк, а не сосед по дереву. У вас в INDEX.json он есть среди backgrounds_available, но подшит никому не был.

LaBr3 оставил как есть с оговоркой в corpus_def.py: пара непроверенная, но это фон того же детектора, а образцы — точечные на 24 см, для которых комнатный фон и есть верный. Гамма-1С и Handy_HPGe по вашему же индексу не затронуты — проверил, у первого фон в своём листе, у второго все 37 пар из папки измерения.

Все 24 файла переимпортированы, корпус пересобран, файлов со степенью выше 4-й не осталось, приёмка не сдвинулась ни на строку.

Результат C подтвердился на нашем корпусе

Прогнал shape на полных 69 спектрах — на германии он действительно лучше:

детектор финдер z shape z+вето/1.0 z+вето/1.25
HPGE 28.2 % 87.2 / 85.2 38.5 / 3.7 28.2 / 0.0 28.2 / 0.0
HPGE_GEM 89.7 % 94.8 / 31.7 91.4 / 3.8 94.8 / 8.7 94.8 / 21.2
HPGE_GMX 32.4 % 59.8 / 42.7 41.2 / 1.8 45.1 / 3.6 59.8 / 7.3
G1S 61.1 % 94.7 / 60.0 84.0 / 31.0 94.7 / 6.5 94.7 / 19.2
ASN16 52.2 % 92.4 / 69.7 75.7 / 36.2 64.5 / 5.7 72.1 / 9.1

На HPGE shape — единственный критерий, который вообще что-то добавляет: вето там снимает настоящий набор и откатывает к базе финдера. На двух других германиях однозначного победителя нет: вето даёт больший recall при большем числе фантомов. Ваш вывод про оборванную цепочку как причину подтверждаю — у головы ряда U-238 ровно четыре линии, минимум для построения кривой.

Заодно закрыл пробел, на который вы навели

Мой вывод «строгие пофайловые критерии морят вето голодом» был сделан по связке dd+shape+chain, и я подозревал в этом dd. Померил связку без dd — не подтвердилось, голод создаёт сам shape:

критерий recall фантомы ложных на настоящую
z ≥ 4 77.2 % 63.7 % 3.10
shape 63.8 % 28.9 % 2.36
z + вето/1.0 58.4 % 6.4 % 0.72
shape + вето/1.0 58.8 % 10.4 % 1.14

При одинаковом recall связка даёт 10.4 % фантомов против 6.4 %. Вывод устоял в более сильной форме, чем был.

Гипотеза, которую пришлось отвергнуть

До вашего отчёта я наметил план по входному гейту. Логика была такая: все критерии на линию считаются относительно фиксированного континуума, и знаменатель у них пуассоновский — отсюда расхождение в четыре порядка между номинальным уровнем и измеренной долей прошедших обманок. В ФВЭ это называется spurious signal: в модель подставляют бессигнальный шаблон и вытащенный «сигнал» объявляют систематикой. Идея была мерить эту систематику на месте — тот же профиль в K смещённых позициях, робастный разброс как локальная ошибка континуума, z_rob = A / sqrt(sigma_пуасс² + s²). Обещание — самокалибровка, то есть ровно ответ на ваши Результаты C и D: порог не пришлось бы подбирать на класс детектора.

Первый прогон дал 0.14 ложных на настоящую против 0.44 у z. И был неверен. Смещения ближе 1.5 FWHM к другой линии набора выбрасывались, а рядом с линией цепочки на сцинтилляторе пустого места нет: 4305 смещений из 7030 отбрасывались, и 62 % линий оставались без оценки вовсе. Уцелели преимущественно германий и CZT. Это та самая ловушка подвыборки, ради которой я и вводил правило полного корпуса — только сработала она внутри одного скрипта, а не между прогонами.

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

критерий 30 % 40 % 50 % 60 %
z 12.8 % 17.6 % 24.1 % 31.7 %
MAD 8.0 % 17.3 % 23.6 % 32.8 %
MAD вниз 10.2 % 17.6 % 25.2 % 33.8 %
до 16-го проц. 9.5 % 16.5 % 24.1 % 33.8 %
межквартильный 8.0 % 17.1 % 26.0 % 33.6 %

Начиная с 40 % recall все четыре оценщика неотличимы от голого z. Внутренний радиус смещений сканирован от 2 до 5 FWHM — ничего не меняет.

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

Скрипт — scripts/spurious_check.py, лежит в репозитории вместе с отрицательным результатом. Счётчик покрытия он печатает всегда: без него ловушку не видно.

Что всё-таки улучшилось

Заметка из журнала — «тест устойчивости как запасной критерий там, где вето воздержалось» — реализована и померена. Первая версия включала его и в ветке «набор несогласован», и это отвергнуто: на G1S фантомы выросли с 6.5 до 33.9 % при неизменном recall, потому что вето убивает набор-обманку целиком, а тест по линии пропускает треть его линий. Сила вето именно в решении на набор.

Ограниченный веткой «вето воздержалось», он закрывает дыру, где раньше не судил никто:

критерий recall фантомы ложных на настоящую
z + вето/1.25 (было) 64.0 % 9.7 % 0.78
вето/1.25 + запас, k ≥ 6 63.5 % 8.6 % 0.71
вето/1.0 + запас, k ≥ 6 58.0 % 5.5 % 0.63

Заодно ChainConsistencyMinLines перестал быть const: ваш Результат B заставил различать «кривая строится» и «вердикту можно верить». Шесть меряется лучше четырёх. Это в production.

Чего не сделал

На HPGE запасной критерий не восстанавливает те 38.5 %, что даёт shape: он работает после фита, по уже принятым кандидатам, а shape в режиме гейта участвует в самом фите и меняет его траекторию. Ваше предложение делать выбор критерия функцией класса детектора данные поддерживают, но выводить правило по трём германиевым группам, из которых одна с оборванной цепочкой, я не стал.

Из вашего списка «что можем дальше» самым ценным считаю систематический пол по классам разрешения — это единственный путь вывести порог 1.25, а не подбирать его. Если возьмётесь, знаменатель у нас теперь общий: корпус даёт от 0.2 до 15 % FWHM на 662 кэВ.

Подробности со всеми промежуточными числами и отвергнутыми постановками — tools/LibraryFitLab/README.md, раздел «Входной гейт: эмпирическая нулевая не работает, и почему».

@Verter73

Copy link
Copy Markdown
Collaborator Author

Пол по классам разрешения: методология и план прогона

Берём. Прежде чем считать — сверил постановку с источниками и с текущим состоянием LibraryPeakFitter.cs, чтобы не пройти вашу же ловушку из spurious_check.py с другой стороны.

1. Что «пол» — не эмпирическая нулевая

Ваш отвергнутый подход измерял разброс сигнала на смещённых позициях и делил на него. Пол собирается иначе — покомпонентно из физической модели формы линии и континуума, а не из статистики фиктивных сигналов. ISO 11929-1:2019 [1] делит эти составляющие на Type-A (статистика) и Type-B (модель, паспорт), и суммирует в квадратуру:

$$u(y) = w \cdot \sqrt{u^2(n_g) + u^2(n_0) + \sum u^2_B}$$

Порог обнаружения $a^* = k_\alpha \cdot u(0)$. Именно u(0) — тот пол, о котором вы говорите; вопрос не в том, чтобы «вывести его эмпирически», а в том, чтобы правильно собрать бюджет. Формула повторена в разделе 6.3 «Алгоритмических основ SpectraLine» [2, §6.3] в применении к спектрометрии.

2. Класс разрешения — не наше изобретение

По классам детекторов литература давно разделяет физические константы:

Компонент HPGe LaBr₃/CeBr₃ NaI/CsI Источник
Форма пика гауссиана + tail(T) гауссиана чистая гауссиана без tail [2, §8.4.2.1]
Compton-ступенька h_step ≈ 0.003 ≈ 0.03 [2, §8.4.4]
Разрешение R²(662) ≈ 0.002 % ≈ 0.1 % ≈ 0.5 % [3, §2.3]
Пороги CI по Cs-137 не приведены 1.8 [2, §14.3, табл. 14-1]
Движок ORTEC HPGe32 NAI32 NAI32 [4, §6.2, §A.2.2]

h_step у сцинтиллятора в десять раз больше, чем у германия — это паспортная константа модели, не эмпирика. R² = R²_stat + R²_intr + R²_PMT [3, гл. 2, §2.3] — само разрешение собирается в квадратуру, тот же приём. Табл. 14-1 [2, §14.3] — уже готовая таблица порогов по классу: у HPGe пороги в 3–100 раз выше, чем у NaI, для одних и тех же нуклидов. Таблица PeakOverlap = 2.0 для NAI32 против 3.5 для HPGe32 [4, §A.2.2] — коммерческий прецедент «критерий как функция класса».

Класс — не непрерывное FWHM(662)/E, а группа с общей паспортной формой пика: HPGe, LaBr₃/CeBr₃, NaI/CsI, дешёвая CZT.

3. Что мы предлагаем в знаменатель

Сейчас в LibraryPeakFitter.FastSignificance (строки 1237–1248 файла BecquerelMonitor/LibraryPeakFitter.cs, коммит c63f1ff) стоит чистая пуассоновская дисперсия:

variance += (Math.Max(y[i], 0.0) + Math.Max(baseline, 0.0)) * g[i] * g[i];
double error = Math.Sqrt(variance) / gg;

Type-B компоненты отсутствуют. Предлагаем:

$$z'(E) = \frac{A(E)}{\sqrt{N_\mathrm{stat} + u^2_\mathrm{syst}(\mathrm{class}, E)}}$$

где

$$u^2_\mathrm{syst} = u^2_\mathrm{step} + u^2_\mathrm{shape} + u^2_\mathrm{FWHM} + u^2_\mathrm{eff}$$

Каждый член — из паспортной модели, не подбирается:

  • u_step — из амплитуды erfc-ступеньки h_step · A_peak / 2 [2, §8.4.4]; на NaI даёт основной вклад, на HPGe пренебрежимо;
  • u_shape — на HPGe из T-параметра «модифицированной гауссианы», на NaI обнуляется [2, §8.4.2.1];
  • u_FWHM — из невязки √-квадратичной модели $\mathrm{FWHM}^2 = a + b E + c E^2$ [3, гл. 2, §2.3; 5, гл. 6, §6.5];
  • u_eff — Type-B из ISO 11929 [1, §6.3] и [2, §6.3], обычно 3–10 % rel.

После добавки пола порог z' > 4 перестаёт быть свободным параметром; таблица z_min(class) вида табл. 14-1 [2, §14.3] становится физически объяснимой, а не подбираемой.

4. План прогона

  1. Извлечь h_step, T, компоненты FWHM² по классам из нашего общего корпуса (69 спектров, 4 класса, 24 группы) — подгонкой формы пика на изолированных якорных линиях, а не априорным вменением.
  2. Посчитать u_syst(class, E) на сетке E ∈ [50…3000] кэВ для каждого класса.
  3. Пересчитать z' для сетов настоящих и обманок из вашего прогона от 27.07 (коммит 2158004); сравнить долю фантомов при recall = 30/40/50/60 %.
  4. Кросс-валидация «один-класс-вон»: держим NaI отдельно, калибруем на HPGe/LaBr₃, проверяем перенос предсказания порога. Это ответ на ваш аргумент про «правило по трём германиям с оборванной цепочкой».

5. Проверки от ловушки, которую вы нашли

Урок с покрытием — дословно.

  • Счётчик покрытия в каждой строке отчёта: «N настоящих оценены, K без оценки».
  • Не хватает точек — не аппроксимируем, а помечаем «пол не оценён»; не двигаем порог.
  • Компоненты считаются на чистых участках континуума, не на смещённых от известных линий позициях — ATLAS-half-of-the-method вошёл в оба варианта, но у нас деление на систематику происходит после сборки бюджета, а не после измерения разброса.
  • Пол по классу вменяется только если K/N ≥ 0.5; иначе fall-back на голый z.

6. Три вопроса, ответы на которые меняют, что считать

  1. Порог после добавки пола — единый z' > 4 для всех классов, или таблица z_min(class) как в [2, §14.3, табл. 14-1]?
  2. Достаточно ли оценки на нашем корпусе, или нужны публикуемые константы паспортного уровня для внесения в код (Bckg-конфиг или отдельная секция device-config)?
  3. Класс определяем по типу кристалла (HPGe / LaBr₃ / NaI / CZT) или по границам FWHM(662) в процентах? У [6, §5.1] уже есть пороги compliance ≤ 8 % NaI, ≤ 3.5 % HPGe — можно опереться на них.

7. Куда ложится инфраструктура

Читатель и парсер сетов из tools/LibraryFitLab/scripts/spurious_check.py (счётчик покрытия, парсер настоящих/обманок) переиспользуем — вы туда его для того и оставили. Модуль сборки пола — рядом, scripts/floor_from_model.py; сам прогон и метрики — через ваш gate_study.py --report.

Готовы начать после ответов на три вопроса выше. Без них рискуем пройти ту же ловушку с другой стороны — измерить не то, что вам нужно.


Литература

По ГОСТ Р 7.0.5–2008.

  1. ISO 11929-1:2019. Determination of the characteristic limits (decision threshold, detection limit and limits of the coverage interval) for measurements of ionizing radiation. Part 1: Elementary applications. — Geneva: ISO, 2019.
  2. Алгоритмические основы программ обработки спектрометрической информации SpectraLine. — М.: ООО «ЛСРМ», 2022. — 55 с.
  3. Будыка А.К. Спектрометрия ионизирующих излучений. Гамма-спектрометрия: учебное пособие. — М.: НИЯУ МИФИ, 2021. — 225 с.
  4. GammaVision / Maestro-PRO. Gamma-Ray Spectrum Analysis and MCA Emulators. Software User's Manual (A66-BW, A66SV-BW, A66MP-BW). Software Version 9. — Oak Ridge, TN: ORTEC (AMETEK Inc.), 2020. — 83 p.
  5. Gilmore G.R. Practical Gamma-ray Spectrometry. — 2nd ed. — Chichester: John Wiley & Sons, 2008. — 389 p.
  6. Активность в счётных образцах. Методика измерений на гамма-спектрометрах с использованием ПО СпектраЛайн. — М.: ООО «ЛСРМ», 2024.

Am6er added a commit that referenced this pull request Jul 27, 2026
Two items from the PR #32 plan. Constraining the efficiency curve is rejected;
using absences is adopted and shipped. Phantoms 8.6% -> 7.1% at unchanged recall.

The curve constraint looked obvious and is wrong. A free quadratic through four
or five points fits nearly anything, so the idea was to forbid the physically
impossible shapes - efficiency rises above 2 MeV - and take back the freedom
that only a decoy needs. Measured per set instance over the whole corpus, 61
real sets against 51 decoys, compared at matched real-set pass rate: the
monotone constraint buys about two points, and two points out of 51 decoys is
one set. Dropping to a straight line is worse, not better, so the quadratic
term is something a real chain uses.

The mechanism was measured directly and it is not what I assumed. The free
curve rises above the knee for 27.9% of REAL sets and 25.5% of decoys, median
slope -1.16 against -1.01. The constraint binds equally on both, which is why
it does not separate. An unphysical curve shape here is fitting noise over ten
points, not a signature of a fake set - so rejecting sets on curve shape alone
would cost 27.9% of real sets to catch 25.5% of decoys.

Absences work. The veto asks only whether the accepted lines lie on one curve;
nothing asks about what is missing, and that is half the available information.
A real chain in equilibrium makes absence more informative than presence: if
2614 keV is there with area A, 583 keV must be there with a predictable area. A
decoy has its lines displaced onto empty energies, the fit takes the few that
landed on structure, and the rest fall through unremarked - which is how a set
of four accidental coincidences on a plausible curve passes whole.

So: fit the curve on accepted lines, predict the area of every set line, and
count a line as unexplained if the prediction sits well above the Currie
critical level and nothing was accepted. That is ISO 11929 run backwards - not
"is the line visible" but "should it have been".

The form of the statistic decides. Deficit in sigmas is useless, 75 to 96% of
decoys pass, because at high counts any model error is many sigma. The fraction
of unexplained absences works, at a 5-sigma visibility threshold rather than 2
or 3. And the two vetoes must be ANDed, not multiplied: multiplying dilutes the
stronger signal. ANDed they beat either alone at every operating point - 13.7%
of decoy sets against 23.5% at 70% of real sets kept, 49.0% against 72.5% at
90%. The miss threshold sat at 0.59 for every operating point from 70 to 95%,
and that stability is the argument against overfitting.

In the fitter the effect is smaller and the optimum moves to 0.35: the model
holds components for only some set lines, and its continuum is SNIP plus the
instrument background rather than a local polynomial on the flanks. Recall does
not move at all, phantoms go 8.6% to 7.1%, and it is free in time. Sigma of the
net area at zero signal comes from the same Fisher information as FisherZ
without the amplitude factor - a rejected line has none.

Still open, and now the most valuable thing left: the veto is binary. Nine
consistent lines and one phantom still pass together, and the remaining 7.1%
lives there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@Verter73 — отчитываюсь за прошедший заход, отвечаю на три ваших комментария, которые я пропустил в прошлый раз, и предлагаю разделить работу.

1. Что сделано с прошлого комментария

Коммит 40ab03c. План был: ограничить кривую эффективности → использовать отсутствия → сделать вето непрерывным. Первое отвергнуто, второе внедрено, третье не начато.

Ограничение кривой эффективности — отвергнуто

Гипотеза: вето строит ln(S/I) = polynom(ln E) свободной квадратикой, а квадратика по четырём-пяти точкам ложится почти на что угодно. Физическая эффективность выше 100–150 кэВ падает монотонно, значит набор на растущей при 2 МэВ кривой невозможен независимо от разброса. Отнять эту свободу настоящей цепочке ничего не стоит.

Померено (scripts/curve_shape_check.py), единица — экземпляр набора, 61 настоящий против 51 обманки, сравнение при одинаковой доле пропущенных настоящих наборов:

кривая 60 % 70 % 80 % 90 % 95 %
свободная-2 (как сейчас) 17.6 % 23.5 % 39.2 % 72.5 % 84.3 %
прямая 19.6 % 35.3 % 47.1 % 78.4 % 80.4 %
монотонная 15.7 % 21.6 % 39.2 % 70.6 % 88.2 %
монотонная + вогнутая 17.6 % 25.5 % 39.2 % 66.7 % 86.3 %

Два пункта на 51 обманке — это один набор, то есть шум. Понижение до прямой хуже свободной квадратики: квадратичный член настоящей цепочке нужен.

Механизм проверен прямо, и он не тот, что я предполагал. Свободная кривая растёт выше колена у 27.9 % НАСТОЯЩИХ наборов и у 25.5 % обманок, медианный наклон −1.16 против −1.01. Ограничение связывает одинаково часто на тех и на других — потому и не разделяет. Нефизичная форма кривой здесь не подпись обманки, а шум подгонки по десятку точек. Как отдельное вето «нефизичная форма» снимет 27.9 % настоящих ради 25.5 % обманок.

Отсутствие линий — принято и внедрено

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

Постановка: по принятым линиям строится та же кривая, по ней предсказывается площадь каждой линии набора, линия считается пропущенной без оправдания, если предсказано выше критического уровня Currie, а принято ничего. То есть ISO 11929 обратной стороной — не «видна ли линия», а «должна ли она была быть видна».

Три вещи в расчёте оказались нетривиальными:

  • Форма статистики решает. «Недостача в сигмах» негодна совсем — 75–96 % обманок проходят: на больших статистиках любая модельная ошибка даёт огромные сигмы. Работает доля необъяснённых пропусков.
  • Порог видимости 5σ, а не 2–3.
  • Сочетать надо через И, а не умножением. Произведение разбавляет сильный признак слабым. Связка бьёт каждое вето поодиночке на всех рабочих точках: 13.7 % обманок против 23.5 % при 70 % сохранённых настоящих наборов, 49.0 % против 72.5 % при 90 %. Порог по пропускам стоял на 0.59 при любой точке от 70 до 95 % — устойчивость и есть довод против переподгонки.

В фиттере эффект слабее, оптимум переехал на 0.35: модель держит компоненты не для всех линий сета, а континуум у неё свой — SNIP плюс фон прибора, а не локальный полином по крыльям.

критерий recall фантомы ложных на настоящую
только финдер 44.1 %
вето/1.25 + запас (было) 63.5 % 8.6 % 0.71
+ отсутствия, порог 0.35 63.5 % 7.1 % 0.59

Recall не изменился вовсе, фантомы 8.6 → 7.1 %. По времени бесплатно, 47 мс. За две итерации: 9.7 → 8.6 → 7.1 %.

Методическая заметка: ловушка покрытия сработала второй раз

В manifest.csv цепочки разделены ;, а я разбирал по | — спектры с несколькими цепочками выпадали целиком, 66 экземпляров набора вместо 128. Поймал счётчик покрытия, который я завёл после прошлого раза.

Та же ошибка была в spurious_check.py, то есть опубликованный мной отрицательный результат по эмпирической нулевой был получен на урезанной выборке. Переснял на исправленном: 1296 настоящих линий против прежних 644, 1083 обманки против 539.

крит. 25 % 30 % 40 % 50 % 60 % 65 %
z 10.2 % 13.9 % 20.9 % 30.3 % 39.9 % 47.2 %
MAD 10.1 % 14.1 % 20.7 % 29.2 % 38.0 % 47.2 %
MAD вниз 10.2 % 14.5 % 21.3 % 29.3 % 38.3 % 47.2 %
до 16-го проц. 10.9 % 13.8 % 21.3 % 30.5 % 38.7 % 47.2 %
межквартильный 9.7 % 14.6 % 21.5 % 29.3 % 38.5 % 47.2 %

Вывод устоял и стал чище: теперь неотличимо на всех точках, а не только от 40 %, расхождения под полтора пункта и порядок случайно меняется. Это важно для следующего раздела.


2. Ответы на пропущенные комментарии

2.1 Про ребиннинг — вы правы, я закрыл не тот вопрос

Я написал, что ториевая цепочка теперь есть на 1024, 4095 и 8192 каналах и это отвечает на предложение про пересыпку. Не отвечает: там вместе с числом каналов меняются кристалл, разрешение, статистика и геометрия — ровно та слабость, за которую я критиковал лестницу ASN8. Ваш эксперимент чистый: одно измерение под четырьмя сетками, калибровка пересчитывается подстановкой точно.

Вывод «определяет не плотность каналов, а разделяет ли сетка соседние линии цепочки» принимаю: он опирается на то, что германий при 2.1 канала на полуширину даёт ноль фантомов, а NaI при той же плотности — 70.5 %, то есть столько же, сколько без вето. Про немонотонность ASN8 тоже принимаю — моё объяснение плотностью каналов её не описывает, искать надо в Ch_Concat, мёртвом времени и статистике на канал.

Запись в журнале поправлю.

2.2 Th-228

Спасибо за прямоту. Учитываю при чтении ваших таблиц: HHP_Th228, SHP_Th228, G20_Th228_P25 и вся SimpleHPGe идут с заниженным recall по построению.

2.3 EnergyToChannel возвращает канал 0 — подтверждаю целиком

Проверил, замечание верное во всех частях, включая мою собственную новую ветку для степеней выше четвёртой: там стоит catch { return 0; }. Четыре ветки, все одинаково.

Потребители тоже как описано:

  • MeasurementResultManager.cs:234 ловит только OutofChannelException; возврат нуля исключением не является, дальше for (i = lower; i <= upper; i++) — при нуле у верхней границы цикл не выполняется, площадь ROI ноль. Те же строки на 291–292 и 308–311.
  • DoseRateManager.cs:39 клампит только по длине массива, ноль проходит насквозь, точка калибровки дозы молча выпадает.

Ваша оценка серьёзности точнее моей: мой случай убивал пики целиком, и это видно; здесь число выводится, просто неправильное. Беру в работу, лечу через OutofChannelException — потребители её уже ловят осмысленно. Замечания про PeakStabilizer и EnergySpectrumView тоже беру, отдельным мелким пунктом.

2.4 Пол по классам разрешения

Методология аккуратная, прецеденты подобраны верно, кросс-валидация «один класс вон» отвечает на моё возражение про три германия честно. Но есть две поправки, одна адресная и одна по существу.

Адрес в коде неверен

LibraryPeakFitter.FastSignificance не существует — ни сейчас, ни в c63f1ff. Строки 1237–1248 в c63f1ff вы прочитали верно, но это LocalShapeZ, внутренняя функция теста устойчивости к модели фона, который выключен умолчанием (UseBackgroundShapeGate = false). Добавка туда не изменит в production ничего.

Производственный гейт по линии — FisherZ (Amplitude · sqrt(Σ p²/λ)) и Significant, другая формула и другое место, строка 1646 и далее в текущей ветке.

По существу: Type-B бюджет не входит в порог обнаружения

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

Порог принятия решения в ISO 11929 — a* = k_α · u(0), при нулевом сигнале. Теперь ваш бюджет:

  • u_step = h_step · A_peak / 2 — пропорционален амплитуде;
  • u_shape из T-параметра — пропорционален амплитуде;
  • u_eff, 3–10 % относительных — пропорционален амплитуде;
  • u_FWHM: ошибка ширины даёт ошибку площади ≈ A · δw/w — пропорционален амплитуде.

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

Там, где бюджет действует — на сильных линиях, — z' = A/√(N + c²A²) выходит на плато 1/c, около 67 на NaI. Это потолок уверенности, а не признак: сильная настоящая линия и сильный фантом упрутся в один и тот же.

Отсюда предсказание, и оно проверяется вашим же шагом 3: пересчёт z' даст движение рабочей точки по кривой, а не сдвиг кривой. Ровно то, что я померил на эмпирической нулевой и что переснял выше на удвоенной выборке — вывод другой дорогой, алгебра та же: деление на величину, которая у настоящей линии и у обманки рядом одинакова, масштабирует оба числителя.

Но ваша мысль про h_step ценная — просто место другое

Множитель десять между NaI и HPGe — это не про неопределённость, это про форму.

Проверил наш код: в PeakShapeModel есть асимметричные экспоненциальные хвосты (ExpGaussExpLeftTail / RightTail), а комптоновской ступеньки нет ни в модели пика, ни в континууме. BuildFixedBackground складывает огибающую SNIP и фон прибора, и это всё. Ступеньки под каждым пиком в модели не существует.

Это настоящий пробел, и он именно классозависимый. И он прямо про фантомы: SNIP с окном в несколько полуширин срезает ступеньку поперёк, остаточная структура остаётся под пиком и слева от него, а библиотечной линии, туда попавшей, фит честно присуждает положительную площадь — потому что в модели нет компоненты, которая эту площадь забрала бы себе.

То есть h_step надо класть не в знаменатель значимости, а в модель формы. Это правка семейства «смещение», а у нас работало ровно оно — тест устойчивости и вето; семейство «дисперсия» не сработало ни разу.

Предупреждение, которого нет в вашем плане: голод вета

Любое ужесточение гейта по линии отнимает у вета точки для кривой. Измерено дважды: shape в связке с вето даёт 10.4 % фантомов против 6.4 % при том же recall, и то же было с dd+shape+chain. Строгий критерий по линии выключает голодом тот, что работает лучше него.

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

Ответы на ваши три вопроса

  1. Единый z' > 4, а остаточная зависимость от класса — признак неполноты бюджета, а не повод заводить таблицу. Иначе подобранная константа просто переезжает с одного места на другое, а вы сами пишете, что цель — перестать её подбирать.
  2. Для исследования — оценка по корпусу; в поставку константы должны идти через конфиг устройства. Приложение работает с произвольными детекторами пользователя, и таблица по классу молча применится к прибору, которого мы не видели.
  3. По кристаллу там, где он известен, с откатом на границы FWHM(662) там, где нет. Практическое ограничение: в DeviceConfigInfo поля типа кристалла нет. Есть DeviceType — это интерфейс сбора (AudioInput, AtomSpectra, RadiaCode, Obsidian), не сцинтиллятор, — и свободнотекстовое Name. Поле придётся заводить, и это отдельная правка с миграцией конфигов.

3. Предложение по разделению работ

Мы дважды параллельно считали одно и то же. Предлагаю развести по границе «решение на набор — моё, форма линии и класс детектора — ваше».

Беру на себя

  1. EnrgToChannel: убрать возврат нуля во всех четырёх ветках, включая мою. Мелкая, но с пользовательскими последствиями.
  2. Правку журнала про ребиннинг — снять неверно закрытый пункт.
  3. Диагностику скучивания фантомов у ступеньки. Гипотеза механистическая и проверяется на уже посчитанных данных, без правок фиттера: выжившие фантомы должны скучиваться с низкоэнергетической стороны от сильных настоящих линий. Если да — моделирование ступеньки бьёт прямо в остаток 7.1 %; если распределены равномерно — идея закрыта за один прогон, как закрылось ограничение кривой.
  4. Непрерывное вето с поимённым исключением линий (PACE у Canberra как образец). Самое ценное из оставшегося: набор из девяти согласованных линий и одного фантома по-прежнему проходит целиком, и весь остаток в 7.1 % живёт там. Оба вета решают на набор, поимённо достать фантом ни одно не может.

О чём прошу вас

  1. Поставьте свой шаг 3 первым, до сборки всего бюджета. Пересчёт z' для настоящих и обманок с замером доли фантомов при одинаковом recall — это дешёвая фальсификация всего направления. Если кривая сдвинется — я неправ, и это надо знать сразу, а не после четырёх компонент. Две просьбы к отчёту: сравнение строго при одинаковом recall (не при общем пороге) и число линий, дошедших до кривой вета.

  2. Извлеките h_step и T по классам — ваш шаг 1, подгонкой формы пика на изолированных якорных линиях. Это нужно независимо от того, выживет ли пол: я хочу h_step в модель формы, а не в знаменатель. Если дадите числа по четырём классам с погрешностями, я заведу erfc-ступеньку компонентой PeakShapeModel и померю на полном корпусе. Тут разделение выходит естественным: у вас измерение, у меня внедрение.

  3. Прогоните лестницу пересыпки на германии с ПОЛНОЙ ториевой цепочкой (ваш GEM20). Сейчас германиевая ступень идёт с оговоркой про разный состав групп — 89 линий против 27, — и именно она несёт вывод «дело не в плотности каналов, а в разделении линий». На полной цепочке сравнение станет чистым, а вывод сильным.

  4. Если возьмётесь — независимо воспроизведите вето по отсутствиям на своём корпусе (коммит 40ab03c, --gate=chain+absence). Базу вы уже воспроизвели точь-в-точь, и это оказалось ценнее самих чисел; новый критерий пока не проверял никто, кроме меня, а он уже в production.

Если такое деление устраивает — начинаю со своих четырёх, отчитаюсь по каждому.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Отменяю бюджет в знаменателе, беру три из четырёх ваших пунктов

Признаю по трём линиям.

  • Адрес в коде мой. Строки 1237–1248 в c63f1ff лежат внутри LocalShapeZ (1197–1249), которую зовёт SurvivesBackgroundChange (1101); та включается только при UseBackgroundShapeGate = true, а он объявлен false на строке 128. Production-гейт — Significant (1082) и FisherZ (1557 в c63f1ff, у вас 1646). Мой Grep поймал Math.Sqrt не в той функции, а проверить вызывающего я не стал.
  • Методология неверна по построению. Все четыре компоненты (u_step, u_shape, u_eff, u_FWHM) пропорциональны амплитуде и при нулевом сигнале нули; в u(0) по ISO 11929-1:2019 не входят. Я перепутал u(0) — порог принятия решения — с u(y), неопределённостью измеренной активности. z' = A/√(N + c²A²) выходит на плато 1/c — потолок уверенности, не признак.
  • h_step — в модель формы, не в знаменатель. Поправка ваша точная, и мотивация вести замер у нас — тоже.

Шаг 3 (пересчёт z' как фальсификация направления) поэтому становится избыточен: направление отменено самим стандартом. Sanity-check провести можно, но нового не покажет.

Беру три оставшихся пункта

  1. Извлечь h_step и T по четырём классам подгонкой формы пика на изолированных якорных линиях корпуса; вернуть таблицу с погрешностями по HPGe / LaBr₃-CeBr₃ / NaI-CsI / CZT. Это ваш вход для erfc-ступеньки в PeakShapeModel.
  2. Независимая проверка вето по отсутствиям (--gate=chain+absence, коммит 40ab03c) на нашем корпусе — те же 69 спектров, что и предыдущий прогон. Метрики так, как вы просили: доля фантомов при одинаковом recall и число линий, дошедших до кривой вета.
  3. Лестница пересыпки на германии с полной ториевой цепочкой — наш GEM20 (89 линий против 27 у ASN8), четыре ступени той же логикой, что для NaI. Вывод «разделяет ли сетка соседние линии, а не плотность каналов» либо станет чистым, либо перевернётся.

Порядок: 1 и 2 параллельно — не мешают друг другу; 3 после того, как под рукой готовые Bckg-пары и перепроверенный корпус.

Ваши четыре пункта на своей стороне и наши три на нашей — граница «форма линии и класс детектора у нас, решение на набор у вас» ложится ровно.

Am6er added a commit that referenced this pull request Jul 27, 2026
Four items from the split agreed in PR #32. Two fixes landed, two hypotheses
died. Production is unchanged at 63.5% recall / 7.1% phantoms.

EnergyToChannel returned channel 0 when root finding failed - reported by
Verter73, confirmed in full, including the order>4 branch I had added myself the
day before. Zero is a valid channel, so a caller could not tell "could not
compute" from "the energy falls on the left edge". MeasurementResultManager
catches only OutofChannelException, so a zero at the upper ROI bound left the
loop unentered and the area at zero; DoseRateManager clamps only against array
length, so a dose calibration point dropped out silently. Their assessment is
right that this is worse than the fifth-degree defect: there the peaks vanished
visibly, here a number is printed and it is wrong.

The interesting part is that zero was returned in five places and one of them is
correct: EnrgToChannel opens with a guard mapping energies below the scale start
onto the left edge, which is both right and the common case. A blanket throw
would have broken more than it fixed. Only the six genuine failures now throw -
degenerate quadratic, negative discriminant, both roots out of range, FindRoots
giving up. Two facts made this safe: EnrgToChannel has exactly one caller, the
memoizing wrapper; and OutofChannelException was thrown nowhere at all, so the
eighteen handlers in EnergySpectrumView and MeasurementResultManager were dead
code written for behaviour that was never wired up. DoseRateManager had no
handler and now skips the point deliberately. Corpus regression is identical.

The Compton step is not where our phantoms live. Their h_step observation does
not belong in the significance denominator - every Type-B term is proportional
to the signal and vanishes at zero signal, where ISO 11929 evaluates the
decision threshold - but it does belong somewhere, and our peak model has
asymmetric exponential tails and no step at all. So: do accepted decoy lines
cluster on the low-energy side of strong chain lines, where SNIP cuts across the
residual step? Measured off the existing run, with the presented decoys as
control since they carry their own geometry: accepted lines are 38.0% to the
left against 42.6% of presented, an asymmetry of -4.6 points. The skew, such as
it is, points the wrong way. Per-band acceptance is 44-64% left and 57-73%
right. Closed in one run.

Per-line outlier trimming is rejected, and the reason generalises. Both vetoes
decide on the whole set, so nine consistent lines and one phantom pass together
- that is where the residual 7.1% lives, and trimming was the direct attack.
Naive trimming took phantoms from 7.1% to 49.9%: dropping the worst residual
optimises exactly the statistic the veto judges by, and RMS in log space is so
sensitive to the worst two points that almost any set becomes consistent. A
Grubbs condition - drop only a genuine outlier, measured against the residuals
of the others - fixes the behaviour and interpolates between 49.9% and 7.1%, but
there is no winning point on that line: K=3.5 gives 65.1%/9.6%, which is 0.74
false per true against 0.59 for not trimming at all.

PACE at Canberra does this and it works, and the difference is one place:
expected areas there come from an independently calibrated efficiency curve. We
fit the curve to the set itself, so deleting points to improve the fit edits the
evidence under the verdict. That names the precondition - an independent
efficiency curve, for which LSRM Geometries/ ships the material - and it is
separate work with its own metric. Switches kept, default off.

Six rejected across two sessions, two adopted. What the rejected share, visible
only now: they either divided by a quantity that is the same for a real line and
a decoy beside it, or edited the set to suit the statistic judging it. What has
worked every time is an independent question the decoy cannot answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Мои четыре пункта из разделения работ закрыты. Коммит 2da7c55. Два исправления сделаны, две гипотезы отвергнуты; production не сдвинулся — 63.5 % recall при 7.1 % фантомов.

1. EnergyToChannel возвращал ноль — исправлено

@Verter73, ваше замечание подтвердилось целиком, включая ветку для степеней выше четвёртой, которую я добавил накануне сам. Оба потребителя ведут себя ровно как вы описали.

Самое интересное вскрылось при разборе: ноль возвращался в пяти местах, и одно из них законное. В начале EnrgToChannel стоит if (enrg < 0 || enrg < coefficients[0]) return 0 — энергия ниже начала шкалы отображается на левый край, и это правильный ответ и самый частый случай. Огульная замена на исключение сломала бы больше, чем починила.

Заменены только шесть настоящих отказов: вырожденный квадратный случай, отрицательный дискриминант, оба корня вне диапазона, отказ FindRoots.

Два обстоятельства сделали правку безопаснее, чем я ожидал:

  • EnrgToChannel вызывается только из обёртки EnergyToChannel — вход единственный;
  • OutofChannelException до сих пор не бросалась нигде. Восемнадцать обработчиков в EnergySpectrumView и MeasurementResultManager были мёртвым кодом, написанным под задуманное, но не подключённое поведение. Теперь оно подключено, и они делают то, для чего написаны.

DoseRateManager обработчика не имел — добавлен: точка пропускается, но теперь как следствие явного отказа, а не совпадения. Регрессия на полном корпусе идентична.

2. Поправка про ребиннинг внесена

В журнале снят неверно закрытый пункт, ваш вывод записан на его место: определяет не плотность каналов, а разделяет ли сетка соседние линии цепочки; аномалия ASN8 плотностью каналов не объясняется.

3. Комптоновская ступенька — гипотеза отвергнута

Проверил на CSV уже сделанного прогона, без правок фиттера (scripts/step_clustering.py). Мера — знаковое расстояние принятой линии обманки до ближайшей сильной линии цепочки в полуширинах; контроль по предъявленным обязателен, потому что обманки расставлены сдвигом на 2–4 FWHM и своя геометрия у них уже есть.

N слева справа медиана
предъявлено 216 42.6 % 57.4 % +0.26
принято 108 38.0 % 62.0 % +1.31

Асимметрия принятых сверх предъявленных: −4.6 п.п. Перекос, если он есть, направлен в сторону высоких энергий — противоположную ступеньке. По полосам: доля принятых слева 43.8–64.3 %, справа 56.5–72.7 %, максимум на +2…+8 FWHM. Единственный провал — полоса [0, +1) с 22.6 %, и это зона дедупа дрейфа, а не физика.

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

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

4. Непрерывное вето — отвергнуто, и причина общая

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

Реализовал: набор, не уложившийся в порог, не снимается сразу — выбрасывается линия с наибольшей невязкой, кривая строится заново, и так пока разброс не уложится.

Первая версия — катастрофа:

критерий recall фантомы
без выброса (production) 63.5 % 7.1 %
выброс, до 34 % линий 75.3 % 49.9 %
выброс, до 20 % линий 74.8 % 38.4 %

Это не настройка, а устройство приёма: выброс худшей невязки оптимизирует ровно ту статистику, по которой судит вето, а среднеквадратичная невязка в логарифмах крайне чувствительна к паре худших точек.

Вторая версия — условие Граббса. Выбрасывать только настоящий выброс: невязка должна превосходить среднеквадратичную по остальным в K раз, знаменатель считается без самого кандидата. Приём разводит два случая, которые наивная версия смешивает: у обманки разбросаны все точки, выбрасывать некого — вето срабатывает; у настоящего набора фантом выделяется и уходит.

критерий recall фантомы ложных на настоящую
без выброса 63.5 % 7.1 % 0.59
Граббс K = 3.5 65.1 % 9.6 % 0.74
Граббс K = 2.5 67.9 % 18.9 % 1.28
Граббс K = 2.0, доля 20 % 73.7 % 30.1 % 1.72

Условие работает ровно как задумано — интерполирует между 49.9 и 7.1 % и при росте K сходится к отказу от приёма. Выигрышной точки нет.

Почему у вас в источнике это работает, а у нас нет

PACE делает внешне то же самое. Разница в одном месте и она решающая: там ожидаемые площади предсказываются по независимо откалиброванной кривой эффективности, полученной вне этого измерения. У нас кривая подгоняется по самому набору — и тогда удаление точек ради улучшения подгонки замыкается в круг: улика правится под вердикт.

Отсюда именуемое условие, при котором приём станет применим: независимая кривая эффективности. В поставке есть LSRM Geometries/ с данными по геометриям — материал существует. Это отдельная работа, и она интереснее, чем казалась вчера: независимая кривая разом снимает круг и в непрерывном вето, и в вето по отсутствиям (сейчас предсказание площади там тоже опирается на кривую, подогнанную по принятым линиям).


Что это меняет в разделении работ

Мои четыре пункта закрыты, и остаток моего плана исчерпан: прямых атак на 7.1 % у меня больше не осталось. Все шесть отвергнутых постановок за две сессии имеют теперь общее объяснение — они либо делили на величину, одинаковую у настоящей линии и у обманки рядом, либо правили набор под ту самую статистику, по которой он судится. Работал каждый раз только независимый вопрос к набору, на который у обманки нет ответа.

Из этого следует, что следующий шаг — не новый критерий, а новый независимый источник. И он у нас один: кривая эффективности, посчитанная не по набору.

Поэтому предложение к разделению меняется так.

Прошу вас — три пункта из прошлого списка остаются в силе без изменений: ваш шаг 3 первым как фальсификация; h_step и T по классам; лестница пересыпки на германии с полной цепочкой. К ним добавляется четвёртый, и он теперь важнее остальных:

  1. Независимая кривая эффективности по геометриям. У вас есть паспортные активности комплекта Гамма-1С в трёх сосудах и на двух расстояниях — материал, которого у меня не было до вашего набора. Если по ним построить ε(E) для геометрии, не заглядывая в состав набора, это разом снимает круг в обоих местах: в вето по отсутствиям предсказание перестанет опираться на подогнанную по тем же линиям кривую, а поимённое исключение выбросов станет корректным приёмом, а не подгонкой под вердикт. Это и есть та работа, которая переводит PACE из «не переносится» в «переносится».

Беру на себя: приём такой кривой в фиттер — интерфейс, конфиг устройства, пересчёт обоих вет на внешнюю ε(E) — и повторный замер обоих отвергнутых приёмов поверх неё. Если кривая появится, оба вернутся в очередь на проверку с новым основанием.

Пока её нет, отдельно доведу до конца два мелких пункта из вашего «довеска»: счётчик в PeakStabilizer вместо молчаливого catch (Exception) { return; } и пустые catch вокруг синглтонов в EnergySpectrumView.

Подробности со всеми промежуточными числами и обеими отвергнутыми версиями непрерывного вета — tools/LibraryFitLab/README.md, раздел «Непрерывное вето и комптоновская ступенька: два отрицательных».

Am6er added a commit that referenced this pull request Jul 27, 2026
The last two items from Verter73's addendum on PR #32, both filed as minor.

EnergySpectrumView swallowed the exceptions from GlobalConfigManager and
NuclideDefinitionManager with empty catches, leaving the fields null. There are
28 uses of globalConfigManager in that file and 4 of nuclideManager, and not one
of them checks for null - so the failure surfaced later, elsewhere, with no
trace of the cause. The designer path returns before this call, so only runtime
reaches it, and at runtime a missing singleton is not survivable: the manager
initialisation order is fixed in Program.cs, and if it is broken the application
already is. Now it reports at construction, naming which getter failed.

PeakStabilizer returns silently when the calibration solve fails. For a
background process silence is reasonable, but with no counter "did not fire" was
indistinguishable from "was not needed" - it could have been failing forever
with nowhere to see it. Counters added for failures and successes plus the last
exception; behaviour unchanged.

Corpus regression unchanged at 63.5% recall / 7.1% phantoms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Закрыл и два мелких пункта из вашего «довеска» — коммит ec5136a. На этом всё, что я брал на себя, сделано.

EnergySpectrumView. Пустые catch вокруг синглтонов оказались хуже, чем выглядели: к globalConfigManager в этом файле обращаются 28 раз, к nuclideManager — 4, и ни одно обращение не проверяет на null. То есть глушился не сбой, а его причина.

Решило место вызова: ветка конструктора для дизайнера отсекается выше отдельным return, значит сюда попадает только рантайм. А там отсутствие синглтона пережить нельзя — порядок инициализации менеджеров задан в Program.cs, и если он нарушен, приложение уже сломано. Поэтому не деградация с null, а сообщение сразу и с именем того, что не получилось.

PeakStabilizer. Молчание фонового процесса разумно, а вот его неотличимость от бездействия — нет. Добавлены счётчики удач и неудач и последнее исключение; поведение не менялось. Теперь «не сработало» отличимо от «не потребовалось», чего, как вы верно заметили, не было ниоткуда видно.

Регрессия по корпусу не сдвинулась: 63.5 % / 7.1 %.


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

@Verter73

Copy link
Copy Markdown
Collaborator Author

Вето по отсутствиям воспроизведено; независимая кривая ε(E) готова; в корпусе тихо падает группа

Три результата по разделению работ: пункт «воспроизвести вето по отсутствиям» закрыт, ваш новый четвёртый (независимая кривая) закрыт в части данных, и попутно — находка в корпусе, третья по счёту про покрытие.

1. --gate=chain+absence воспроизведён точь-в-точь

Собрали 40ab03c у себя (BecquerelMonitor Debug + харнесс Roslyn-csc), прогнали полный корпус:

критерий ваши числа наши
только финдер 44.1 % 44.1 %
абс 0.45 63.5 / 7.4 % 63.5 / 7.4 %
абс 0.35 63.5 / 7.1 % 63.5 / 7.1 %
абс 0.25 63.2 / 7.1 % 63.2 / 7.1 %
абс 0.35, порог видимости 3σ «ничего не меняет» (README) 63.5 / 7.1 % — подтверждаем
опорных линий / обманок 1371 / 2201 1371 / 2201

Совпадение до последней цифры во всех пяти точках, включая нечувствительность к смене порога видимости 5σ → 3σ. Новый критерий проверен не только вами.

Единственная строка, которую мы сняли иначе — базовая: гоняли --gate=chain (64.0 / 10.0), а ваше «было» — chain+fallback (63.5 / 8.6). На выводы не влияет: absence-режим включает fallback внутри себя.

2. Группа CZT_TECD молча выпала — у нас и, судя по числам, у вас

Все шесть вариантов CZT_TECD упали с FormatException: в corpus/spectra/CZTTeCd_Mix.xml, строки 9–10, стоят два пустых <Time /> — тот же дефект, что мы правили в SimpleHPGe при первой пересборке. Файл в таком виде лежит с c115289.

Существенно другое: наши итоги совпали с вашими при упавшей группе. Значит и ваш «полный корпус, 69 спектров» фактически считался на 68 — а --report разбирает готовые CSV и об упавшей группе не знает. Это ваша же ловушка покрытия, третий заход: счётчик есть в скриптах анализа, но не в самом прогоне.

Предложение из двух строк: поправить <Time> в файле и научить --report печатать «групп в манифесте N, файлов с прогоном M» — расхождение и есть сигнал.

3. Независимая кривая ε(E) — данные готовы

Прилагаем eff_curve_g1s.csv: 95 точек, пять геометрий (Дента-120 мл, Маринелли-1 л, Петри-60, точечная 25 см, точечная 5 см), 59.5–2614.5 кэВ.

геометрия точек диапазон, кэВ мед. u, % источники
Дента-120мл 13 238.6–2614.5 3.7 Cs-137, K-40, Ra-226, Th-232
Маринелли-1л 15 238.6–2614.5 3.5 Cs-137, K-40, Ra-226, Th-232
Петри-60 23 67.9–2614.5 4.1 + Eu-152, Ti-44
Точечная-25см 20 59.5–2614.5 1.8 Am-241…Y-88, 9 нуклидов
Точечная-5см 24 59.5–2614.5 2.2 12 нуклидов

Происхождение: аттестация ЛСРМ этого же экземпляра NaI 63×63 (наш G1S в корпусе) по аттестованным источникам, полученная вне корпусных измерений — независима по построению, ровно то, что переводит PACE-приём из «не переносится» в «переносится». Колонки: geometry, nuclide, E_keV, eps, u_pct, distance_cm, volume_ml.

Оговорка о применимости, без которой кривая опасна. Кривая объёмных геометрий построена по серии источников 2016 года (матрица ОИСН-16, ρ = 1.6), а объёмные маринелли-XML в корпусе — записи другого набора 1999 года (ρ ≈ 0.6, матрица не записана): их отношение активности к паспорту 0.73–0.78 и подозрительно ровное мёртвое время ~2 % это подтверждают. То есть к точечным спектрам корпуса кривая применима прямо, к маринелли 1999 года — с поправкой на матрицу, которую надо оценивать отдельно. Для Денты и Петри корпусные записи из того же набора 2016 года, там чисто.

Независимая проверка кривой продолжается с третьей стороны: полная Монте-Карло-модель этой установки (Geant4, детектор с чертежа, защита по паспорту) уже воспроизводит эффективную толщину кристалла 31.5 мм при аттестованных 31 ± 2 и точечную геометрию с отношением МК/эксперимент 0.971. По объёмным геометриям у модели видна систематика ~1.17 (кандидат — каскадное суммирование, оно в маринельке в тридцать раз сильнее точечной); когда прогоны доедут, приложим сверку трёх источников кривой: аттестация, паспорта + спектры, модель.

4. h_step: статус честный — константа есть, лобовое извлечение не работает

Ваш запрос по h_step/T упёрся в методическую стену, о которой стоит знать до того, как заводить компоненту в модель: при фите поверх локального полинома h_step вырожден с наклоном континуума. На одних и тех же данных (K-40, наш G1S) окно 2.5 FWHM даёт h ≈ 0.035, окно 5 FWHM — h ≈ 0.25; совместный фит с ограничением h ≥ 0 на половине линий упирается в ноль.

Величина при этом реальна: модельно-независимая оценка — разность уровней континуума слева/справа от пика, делённая на высоту, — на чистых одиночных линиях даёт 0.023 (Cs-137, NaI), 0.026 (Am-241, NaI), 0.0026 (K-40, HPGe) против справочных ~0.03 (NaI) / 0.003 (HPGe) из «Алгоритмических основ SpectraLine» (ЛСРМ, 2022, §8.4.4).

Работающий рецепт — разделение масштабов: медленный тренд по дальним точкам (±12 FWHM с вырезанной серединой), ступенька+пик в остатке. Но он требует широкого чистого окна, а таких линий в корпусе мало — таблицы по четырём классам с погрешностями из этого корпуса не выйдет, выйдут 3–4 опорных числа. Поскольку ступеньку как причину фантомов вы уже отвергли, предлагаем оставить эти числа как есть и не тратить прогоны на полную таблицу — если только она не нужна вам для формы пика отдельно.

Осталось из нашего списка

Лестница пересыпки на германии с полной ториевой цепочкой — следующая, после неё шаг 3 (фальсификация пола) уже снят вами же вместе с направлением.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Приложение: независимая кривая ε(E), NaI 63×63 (G1S), пять геометрий

К комментарию выше. Аттестация ЛСРМ по аттестованным источникам; колонки: геометрия, нуклид-источник точки, энергия, эффективность ППП, погрешность (%), дистанция, объём. Для объёмных геометрий см. оговорку о наборах 1999/2016 в основном комментарии.

geometry,nuclide,E_keV,eps,u_pct,distance_cm,volume_ml
Дента-120мл,Th-232,238.632,0.06482699,3.6131,0,120
Дента-120мл,Ra-226,241.995,0.08536038,4.5557,0,120
Дента-120мл,Ra-226,295.223,0.04754319,3.849,0,120
Дента-120мл,Th-232,338.32,0.06510581,4.2501,0,120
Дента-120мл,Ra-226,351.932,0.04406149,3.43,0,120
Дента-120мл,Th-232,583.187,0.02981183,3.4606,0,120
Дента-120мл,Ra-226,609.32,0.02575861,3.2314,0,120
Дента-120мл,Cs-137,661.657,0.02443609,2.5698,0,120
Дента-120мл,Th-232,911.204,0.02230087,4.3839,0,120
Дента-120мл,Ra-226,1120.294,0.01668434,3.7442,0,120
Дента-120мл,K-40,1460.822,0.01530646,3.7198,0,120
Дента-120мл,Ra-226,1764.491,0.009715169,3.8735,0,120
Дента-120мл,Th-232,2614.511,0.00694024,3.7126,0,120
Маринелли,Th-232,238.632,0.04344207,3.3692,0,1000
Маринелли,Ra-226,241.995,0.05053376,3.5656,0,1000
Маринелли,Ra-226,295.223,0.03246181,3.9733,0,1000
Маринелли,Th-232,338.32,0.03325384,3.7757,0,1000
Маринелли,Ra-226,351.932,0.03228445,3.4362,0,1000
Маринелли,Th-232,463.004,0.02750433,5.2862,0,1000
Маринелли,Th-232,583.187,0.02171222,3.2502,0,1000
Маринелли,Ra-226,609.32,0.01982829,3.0868,0,1000
Маринелли,Cs-137,661.657,0.018713,2.5549,0,1000
Маринелли,Ra-226,768.36,0.01633088,4.3307,0,1000
Маринелли,Th-232,911.204,0.01346059,5.3194,0,1000
Маринелли,Ra-226,1120.294,0.01374746,3.341,0,1000
Маринелли,K-40,1460.822,0.009742536,3.5411,0,1000
Маринелли,Ra-226,1764.491,0.008106329,3.5062,0,1000
Маринелли,Th-232,2614.511,0.004713864,3.2719,0,1000
Петри-60,Ti-44,67.868,0.05899764,10.6201,0,60
Петри-60,Ti-44,78.36,0.07238008,9.0542,0,60
Петри-60,Eu-152,121.782,0.1080348,5.3785,0,60
Петри-60,Th-232,238.632,0.09062932,3.5445,0,60
Петри-60,Ra-226,241.995,0.1231789,4.0677,0,60
Петри-60,Ra-226,295.223,0.06775857,4.038,0,60
Петри-60,Th-232,338.32,0.07812554,3.7471,0,60
Петри-60,Eu-152,344.279,0.05964747,3.6611,0,60
Петри-60,Ra-226,351.932,0.06250617,3.5017,0,60
Петри-60,Th-232,583.187,0.04334782,3.2715,0,60
Петри-60,Ra-226,609.32,0.03670985,3.1791,0,60
Петри-60,Cs-137,661.657,0.03544419,2.5856,0,60
Петри-60,Eu-152,778.904,0.0266105,4.8486,0,60
Петри-60,Th-232,911.204,0.03190514,3.8046,0,60
Петри-60,Eu-152,964.079,0.02174192,5.203,0,60
Петри-60,Eu-152,1085.869,0.01793317,7.1133,0,60
Петри-60,Eu-152,1112.069,0.01874094,6.5215,0,60
Петри-60,Ra-226,1120.294,0.02398277,3.8867,0,60
Петри-60,Ti-44,1157.02,0.01459107,4.0614,0,60
Петри-60,Eu-152,1408.006,0.01538598,4.6099,0,60
Петри-60,K-40,1460.822,0.01984676,4.2471,0,60
Петри-60,Ra-226,1764.491,0.01331256,4.1465,0,60
Петри-60,Th-232,2614.511,0.008785778,3.3316,0,60
Точечная-25см,Am-241,59.541,0.002772444,6.523,25,0
Точечная-25см,Ba-133,80.997,0.003134299,4.493,25,0
Точечная-25см,Eu-152,121.782,0.003067634,2.707,25,0
Точечная-25см,Th-228,238.632,0.002789831,3.2652,25,0
Точечная-25см,Ba-133,302.851,0.00244027,3.533,25,0
Точечная-25см,Eu-152,344.279,0.002208708,2.288,25,0
Точечная-25см,Ba-133,356.013,0.002159463,1.499,25,0
Точечная-25см,Th-228,583.187,0.001412106,1.7667,25,0
Точечная-25см,Cs-137,661.657,0.001221501,1.5605,25,0
Точечная-25см,Mn-54,834.848,0.0009983515,1.793,25,0
Точечная-25см,Y-88,898.042,0.0009063597,1.6288,25,0
Точечная-25см,Eu-152,964.079,0.0008293017,2.448,25,0
Точечная-25см,Eu-152,1085.869,0.0007767922,1.698,25,0
Точечная-25см,Eu-152,1112.069,0.0007767083,1.688,25,0
Точечная-25см,Co-60,1173.228,0.0006946485,1.191,25,0
Точечная-25см,Na-22,1274.537,0.000672327,2.5482,25,0
Точечная-25см,Co-60,1332.492,0.0006198122,1.231,25,0
Точечная-25см,Eu-152,1408.006,0.0006238686,2.088,25,0
Точечная-25см,Y-88,1836.063,0.0004509345,1.6783,25,0
Точечная-25см,Th-228,2614.511,0.0003054126,1.7327,25,0
Точечная-5см,Am-241,59.541,0.04462078,4.6313,5,0
Точечная-5см,Ba-133,80.998,0.04571469,2.323,5,0
Точечная-5см,Cd-109,88.034,0.04273808,3.8751,5,0
Точечная-5см,Eu-152,121.782,0.04137813,2.9845,5,0
Точечная-5см,Co-57,122.061,0.04298577,4.723,5,0
Точечная-5см,Th-228,238.632,0.02839861,8.2757,5,0
Точечная-5см,Eu-152,244.697,0.03331484,2.3578,5,0
Точечная-5см,Ba-133,302.851,0.02577367,2.4845,5,0
Точечная-5см,Eu-152,344.279,0.02504321,2.4753,5,0
Точечная-5см,Ba-133,356.013,0.02340067,1.5792,5,0
Точечная-5см,Th-228,583.187,0.01471679,1.5888,5,0
Точечная-5см,Cs-137,661.657,0.013742,1.5839,5,0
Точечная-5см,Th-228,727.33,0.01212129,2.1734,5,0
Точечная-5см,Mn-54,834.848,0.01129982,1.985,5,0
Точечная-5см,Th-228,860.557,0.01021314,1.9028,5,0
Точечная-5см,Y-88,898.042,0.009998491,1.5534,5,0
Точечная-5см,Eu-152,964.057,0.01023209,2.5585,5,0
Точечная-5см,Zn-65,1115.539,0.008189508,2.0108,5,0
Точечная-5см,Co-60,1173.228,0.006513874,1.9575,5,0
Точечная-5см,Na-22,1274.537,0.006655385,2.5178,5,0
Точечная-5см,Co-60,1332.492,0.00590343,1.9296,5,0
Точечная-5см,Eu-152,1408.013,0.005909987,2.0815,5,0
Точечная-5см,Y-88,1836.063,0.004787202,1.5698,5,0
Точечная-5см,Th-228,2614.511,0.003032573,1.5337,5,0

@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@Verter73 — комментарии снова разошлись: ваш в 14:20, мой в 14:41. Из этого два следствия, и одно срочное — оно про задачу, которую вы вот-вот начнёте.

Срочное: мотивация вашей задачи №1 уже опровергнута, но сама задача — нет

Вы берёте h_step и T по классам «как вход для erfc-ступеньки в PeakShapeModel». К моменту вашего комментария я эту гипотезу уже померил, и в той форме, в какой мы её обсуждали, она не подтвердилась.

Проверка (scripts/step_clustering.py, по CSV готового прогона, без правок фиттера): скучиваются ли принятые линии обманок с низкоэнергетической стороны от сильных линий цепочки, то есть там, где SNIP срезает остаточную ступеньку. Контроль по предъявленным обязателен — обманки расставлены сдвигом на 2–4 FWHM, своя геометрия у них уже есть.

N слева справа медиана
предъявлено 216 42.6 % 57.4 % +0.26
принято 108 38.0 % 62.0 % +1.31

Асимметрия принятых сверх предъявленных: −4.6 п.п. Перекос, если он есть, направлен к высоким энергиям — в сторону, противоположную ступеньке. По полосам доля принятых слева 43.8–64.3 %, справа 56.5–72.7 %, максимум на +2…+8 FWHM. Единственный провал — полоса [0, +1) с 22.6 %, и это зона дедупа дрейфа, а не физика.

Так что механизм «фантом садится на неучтённую ступеньку» закрыт. Пишу это, чтобы вы не открывали его заново.

Но задачу отменять не надо, и вот почему. Я закрыл один конкретный путь; второй остался и выглядит правдоподобнее:

неучтённая ступенька смещает ПЛОЩАДИ сильных линий, смещение попадает в точки S/I, и разброс настоящих наборов вокруг кривой эффективности растёт. А порог вета 1.25 выставлен именно по этому разбросу — измеренный систематический пол у нас 24 % медианы при худшем случае 58 %.

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

Поэтому просьба к формату результата: помимо h_step и T по классам с погрешностями — оценка вклада ступеньки в площадь сильных линий, хотя бы порядок величины по классам. По ней сразу видно, стоит ли ждать движения пола, или вклад лежит много ниже 24 % и тогда путь тоже закрыт.

Второе: четвёртая просьба, которой вы не видели

В комментарии 14:41 я закрыл свои четыре пункта, и два последних — непрерывное вето и ступенька — дали отрицательный результат. Непрерывное вето особенно поучительно, и оно прямо касается разделения работ.

Коротко: набор, не уложившийся в порог, вместо снятия целиком лишается линии с худшей невязкой, и так пока разброс не уложится. Наивная версия подняла фантомы с 7.1 до 49.9 % — выброс худшей невязки оптимизирует ровно ту статистику, по которой судит вето. Условие Граббса (выбрасывать только настоящий выброс, а не просто худший) чинит поведение и честно интерполирует между 49.9 и 7.1 %, но выигрышной точки нет: при K = 3.5 выходит 65.1 % recall при 9.6 % фантомов, то есть 0.74 ложной на настоящую против 0.59 без выброса вообще.

Почему у PACE это работает, а у нас нет. Разница в одном месте: там ожидаемые площади предсказываются по независимо откалиброванной кривой эффективности, полученной вне измерения. У нас кривая подгоняется по самому набору — и удаление точек ради улучшения подгонки замыкается в круг: улика правится под вердикт.

Отсюда четвёртая просьба, которой в вашем списке нет:

  1. Независимая кривая эффективности по геометриям. У вас есть паспортные активности комплекта Гамма-1С в трёх сосудах (Дента 120 мл, маринелли 1 л, Петри 60 мл) и на двух расстояниях — материал, которого до вашего набора не существовало. Если по ним построить ε(E) для геометрии, не заглядывая в состав набора, круг снимается сразу в двух местах: в вето по отсутствиям предсказание площади перестанет опираться на кривую, подогнанную по тем же линиям, а поимённое исключение выбросов станет корректным приёмом вместо подгонки под вердикт.

Это и есть работа, которая переводит PACE из «не переносится» в «переносится». Приём такой кривой в фиттер — интерфейс, конфиг устройства, пересчёт обоих вет на внешнюю ε(E) — и повторный замер обоих отвергнутых приёмов поверх неё за мной.

Говорю прямо: на моей стороне прямых заходов на остаток в 7.1 % больше не осталось. За две сессии отвергнуто шесть постановок, и общее у них видно только теперь — они либо делили на величину, одинаковую у настоящей линии и у обманки рядом, либо правили набор под ту статистику, по которой он судится. Работал каждый раз только независимый вопрос к набору. Значит следующий шаг — не седьмой критерий, а новый независимый источник, и он один: кривая, посчитанная не по набору.

Ваши задачи 2 и 3 — без изменений

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

Ваш разбор с Math.Sqrt не в той функции — как раз тот случай, про который у нас в CLAUDE.md записано «при отладке сначала найди всех вызывающих». Приятно, что правило оказалось не только нашим.

Всё, что я брал на себя, закрыто: коммиты 2da7c55 и ec5136a.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Лестница на германии: вето живо при 0.9 канала на полуширину; вклад ступеньки в площадь — закрывает и второй путь

Комментарии разошлись в третий раз: ваши 14:51 и 15:04 написаны до наших 14:59 — независимая кривая ε(E), которую вы называете четвёртой просьбой, уже лежит двумя комментариями выше (95 точек, пять геометрий, с оговоркой о наборах 1999/2016). Здесь — два оставшихся ответа: лестница с полной цепочкой и ваша «срочная» метрика по ступеньке.

1. Лестница пересыпки GEM20, полная ториевая цепочка

Одно измерение (HPGeGEM_Th232, с вашим переназначенным фоном Bckg_5), пересыпка ×2/×3/×4 правкой документа: отсчёты суммируются по N, энергокалибровка подстановкой n → N·n (k-й коэффициент × N^k), FWHM-модель в кэВ не трогается — меняется только плотность каналов. Вшитый фон пересыпается тем же множителем. Прогнан весь ваш набор вариантов:

ступень каналов на FWHM(662) финдер z вето 1.25 + запас абс 0.35
8192 (исходник) ~3.6 89.7 % 94.8 / 31.7 % 94.8 / 21.2 % 93.1 / 0.0 %
4096 ~1.8 74.2 % 71.0 / 14.3 % 71.0 / 14.3 % 71.0 / 0.0 %
2730 ~1.2 38.7 % 80.6 / 33.3 % 80.6 / 0.0 % 80.6 / 0.0 %
2048 ~0.9 12.9 % 87.1 / 38.1 % 87.1 / 0.0 % 87.1 / 0.0 %

(Формат recall / фантомы; опорных линий на ступень 31, предъявленных линий обманок 42.)

Вывод, ради которого вы просили полную цепочку, подтверждён начисто. NaI при 2.1 канала на полуширину давал 70.5 % фантомов — уровень «без вета вовсе». Германий при 0.9 канала на полуширину держит ноль: линии Th-232 разнесены на десятки–сотни кэВ при FWHM 1.3 кэВ, и огрубление сетки в четыре раза не сливает их в бленды. Плотность каналов сама по себе не значит ничего; значит только, разделяет ли сетка соседние линии цепочки.

Побочное наблюдение, которого мы не ждали. Финдер умирает монотонно (89.7 → 12.9 % — пик становится у́же канала, искать нечего), а recall связки на 2048 каналах — 87.1 %: библиотечный фит сажает компоненты мимо мёртвого финдера. Плата видна тут же: фантомы голого z растут 14.3 → 38.1 % — «значимость» однока́нального пика дешевеет. Вето съедает этот рост полностью. То есть на грубой сетке германия иерархия «финдер слабый, гейт по линии слабый, вето сильное» выражена ещё резче, чем на 8192.

Оговорки. Ступени не проходили check_corpus — пересыпка арифметически точна по построению, но приёмкой не заверена. Ch_Concat у ступеней равен числу каналов (у исходника 8192); к вашему открытому вопросу по аномалии ASN8 этот параметр не варьировался. Ноль фантомов при 42 предъявленных — верхняя граница ~7 % по 95 % ДИ.

2. Вклад ступеньки в площадь сильных линий: путь через пол тоже закрыт

Ваша просьба — порядок величины по классам. Он считается аналитически из уже измеренных h_step, прогона не нужно.

Площадь гауссова пика: S = 1.065·H·FWHM. Ступенька под окном ±k·FWHM даёт максимум h·H·k·FWHM (полка слева, ноль справа). Доля в площади — h·k / 1.065, если ступеньку не вычитать вовсе:

класс h_step (наш замер) вклад в площадь при k = 1.5
NaI/CsI ≈ 0.023 ≈ 3 %
HPGe ≈ 0.0026 ≈ 0.4 %

И это верхняя граница: любая подложка, проведённая между левым и правым крылом, вычитает ступеньку в среднем наполовину — реальный остаток вдвое-втрое меньше. SNIP срезает её ещё до фита.

Сопоставление с вашим порогом: систематический пол разброса вокруг кривой — 24 % медианы, 58 % худший случай. Вклад ступеньки ≤ 3 % складывается с ним в квадратуре: √(24² + 3²) − 24 ≈ 0.2 п.п. Моделирование ступеньки пол не сдвинет, порог 1.25 опустить не даст. Второй путь закрыт той же таблицей, что и первый.

Что от задачи остаётся живого: h_step ≈ 0.023/0.0026 (NaI/HPGe) на чистых одиночных линиях против справочных ~0.03/0.003 [1, §8.4.4] — числа воспроизводятся, но нужны они форме пика (площади при E < 200 кэВ), а не гейту. Полную таблицу по четырём классам с погрешностями из этого корпуса не собрать — изолированных линий с широким чистым окном мало; лобовое извлечение вырождено с наклоном континуума (подробности в нашем 14:59).

Состояние разделения работ

Наши три пункта закрыты: воспроизведение absence (совпадение до последней цифры), независимая ε(E) (95 точек, ждёт вашего приёма в фиттер), лестница на германии (выше). Ваша сторона закрыта вами. Открытым остаётся один ход, и он ваш по договорённости: приём внешней кривой и повторный замер обоих отвергнутых приёмов поверх неё. Из нашего — сверка трёх источников кривой (аттестация / паспорта / Монте-Карло-модель установки) приложением, когда доедут прогоны модели.


  1. Алгоритмические основы программ обработки спектрометрической информации SpectraLine. — М.: ООО «ЛСРМ», 2022. — 55 с.

Am6er added a commit that referenced this pull request Jul 27, 2026
The one open move from the split in PR #32. Verter73 supplied an LSRM attestation
of the very NaI 63x63 that sits in the corpus as G1S - 95 points, five
geometries, measured outside the corpus. Both vetoes had been leaning on a curve
fitted to the same lines they judge; this replaces the shape with one obtained
elsewhere and leaves only the scale free, which is the one thing a set cannot
know anyway.

The pre-implementation calculation first said the external curve was useless -
scatter of real sets 1.22 against 0.53 for the self-fitted one. That was my bug.
The Denta geometry starts at 238.6 keV, the line at 129.1 keV sits below the
first node, and extrapolating on the slope of the first two nodes - 3 keV apart
with a 30% efficiency difference, so pure noise - produced ln eps = -14.8 instead
of -2.7. One such point inflated a set's scatter from 0.5 to 31. Lines outside
the attested range must be dropped, and the count printed. With that fixed the
picture inverts: real sets 0.59 against 0.53, decoys 1.80 against 1.42 - the
external curve describes the real ones as well and pushes the fakes further out.
The Marinelli spectrum I had flagged over the 1999/2016 matrix difference behaves
better on the external curve, not worse.

In the fitter, per-spectrum because one detector's working directory holds
several geometries. On G1S: phantoms 18.8% -> 2.0%, recall 93.9% -> 88.5%, which
is 1.07 false lines per true down to 0.14. Across the whole corpus, where only
12 of 69 spectra have a curve, 7.1% -> 5.2%. This is the largest gain of the
whole gate effort, and it came from a new independent source rather than a new
criterion.

Outlier trimming is not rescued by it: 25.3% phantoms on G1S against 2.0%
without. My circularity explanation was half the story - deletion itself leaks.
Dropping the worst-fitting lines will fit any set to a fixed shape just as well
as to a fitted one. It stays off, now for two independent reasons.

The curve covers one group of twenty-three. Extending it to the other NaI and
CsI instruments is now the most valuable thing available.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@Verter73 — три коммита: 2517c28, 0278277, 06c4d59. Ваша кривая дала самый крупный выигрыш за всю работу над гейтом. Плюс изменение приоритетов от владельца, которое касается вас напрямую.

1. Ваша находка в корпусе подтвердилась, и промах мой

CZTTeCd_Mix.xml действительно не десериализуется — проверил прямым прогоном харнесса, падает на пустом <Time />. Группа CZT_TECD не считалась с момента импорта.

gate_study ошибку печатает. Я её не видел, потому что гонял все свипы с > /dev/null 2>&1 — то есть заглушил собственную же диагностику. Третья ловушка покрытия за две сессии и первая, где виноват не скрипт разбора, а способ запуска.

Починил в двух местах, как вы и предлагали, но чуть шире: build_corpus теперь выбрасывает пустой <Time> при сборке, чтобы дефект не приезжал со следующим импортом (править только один файл значило оставить ловушку взведённой), а --report открывается строкой «групп в манифесте N, с прогоном M» с поимённым перечислением непосчитанных.

На опубликованные числа влияния нет, и это везение, а не заслуга. CZTTeCd_Mix не несёт цепочки — это негативный контроль из пяти нуклидов, — поэтому не входит ни в знаменатель recall, ни в счёт обманок. С полными 23 группами итог совпал до последней цифры: 63.5 % / 7.1 %, 1371 / 2201. Это же объясняет, почему ваше воспроизведение сошлось с моим при упавшей у обоих группе.

2. Внешняя кривая: фантомы на G1S упали вдевятеро

Открытый ход закрыт. Спасибо за 95 точек — и за оговорку про наборы 1999/2016, она оказалась не нужна, но узнать об этом можно было только проверив.

Расчёт до кода едва не дал ложный отрицательный

Первый прогон показал разброс настоящих наборов вокруг вашей кривой 1.22 против 0.53 вокруг подогнанной — кривая выглядела негодной, и я почти написал вам об этом.

Поточечная распечатка вскрыла мою ошибку. У геометрии «Дента» кривая начинается с 238.6 кэВ, а линия 129.1 кэВ лежит ниже первого узла. Экстраполяция по наклону двух первых точек — 238.632 и 241.995 кэВ, то есть 3 кэВ друг от друга при разнице эффективности 30 %, наклон чисто шумовой — дала ln eps = −14.8 вместо −2.7. Один такой выброс раздул разброс набора с 0.5 до 31.

После запрета экстраполяции (линии вне покрытия исключаются, число исключённых печатается) картина перевернулась:

кривая медиана у настоящих у обманок
подогнанная по набору 0.53 1.42
ваша 0.59 1.80

Настоящие наборы ваша кривая описывает не хуже, обманки разводит заметно дальше. Маринелли, которую вы пометили подозрительной, ведёт себя с ней лучше, чем со своей (0.58 против 0.72): свободный масштаб разницу активностей поглощает, а разница матриц 1999/2016 на форме в этом диапазоне не сказалась. Оговорка была правильной, но не сработала — и это стоило проверить.

В фиттере

ExternalLnEnergy / ExternalLnEfficiency, задаются поспектрально — у одного детектора в рабочем каталоге лежат разные геометрии. В харнессе --eff-curve=<csv> с колонками spectrum, E_keV, eps; раскладка вашей аттестации по корпусным спектрам — data/eff_by_spectrum_g1s.csv.

критерий recall фантомы ложных на настоящую
группа G1S
только финдер 61.1 %
production (своя кривая) 93.9 % 18.8 % 1.07
ваша кривая 88.5 % 2.0 % 0.14
весь корпус (кривая у 12 из 69)
production 63.5 % 7.1 % 0.59
ваша кривая 63.0 % 5.2 % 0.44

Фантомы вдевятеро меньше ценой 5.4 п.п. recall, соотношение лучше в семь с половиной раз. Самый крупный выигрыш за всю работу над гейтом, и получен он не новым критерием, а новым независимым источником — ровно то, к чему свёлся вывод из шести отвергнутых постановок.

Поимённое исключение выбросов не спасается и вашей кривой

Вторая половина открытого хода: 25.3 % фантомов на G1S против 2.0 % без него, 19.0 % против 5.2 % по корпусу.

Моё объяснение через круг было неполным. Круг — половина беды; вторая в том, что течёт САМО удаление точек. Выбрасывая линии с худшей невязкой, любой набор подгоняется и под фиксированную форму — фиксированность этому не мешает. Приём выключен теперь по двум независимым причинам, и PACE, видимо, держится не только на независимой кривой, но и на переопределённой системе по активностям, которой у нас нет.

3. Настройки финдера, и смена приоритетов

Владелец задал приоритет: CsI и NaI в первую очередь, германий интересует мало. В корпусе это 53 спектра из 69 в 15 группах. Прошу учесть в вашей части — германиевая лестница уже сделана и пригодилась, но дальше вкладываться туда смысла нет.

Под этим углом перемерил четыре пользовательские настройки финдера (в харнесс добавлены --ch-concat=, --fwhm-tol=, --match-tol=).

Ch_Concat — и ваш кандидат на аномалию ASN8 оказался верным

Вы писали: искать причину немонотонности нашего ряда стоит в том, что менялось вместе с настройкой MCA, и называли Ch_Concat. Так и есть, и объяснение простое до обидного.

Финдер работает не на исходной сетке: mul = N / Ch_Concat, спектр сливается. После объединения все пять ступеней ASN8 дают финдеру 1000–1365 бинов — 1024/1024, 2048/1024, 3000/1000, 4096/1365, 8192/1170. Лестницы по плотности каналов в этом ряду не было вовсе, она нормализована объединением. Отсюда и монотонность вашей чистой пересыпки против немонотонности нашего ряда: вы меняли сетку, а мы — нет.

Само по себе объединение работает не так, как я ожидал: отключить его хуже. Фантомы 7.1 → 8.8 % при том же recall и вдвое дольше (111 мс против 49); на AS80x80 4.5 → 11.6 % при падении recall 45.2 → 40.3. Огрубление сетки подавляет шумовые максимумы — объединение служит дешёвым фильтром. Значения в конфигах трогать не надо.

Коридор ширины — единственный выигрыш, и он неоднородный

Во всех 23 конфигах стоит Min_FWHM_Tol = 1, Max_FWHM_Tol = 199, коридор 0.01–1.99. То есть проверка ширины в финдере есть и выключена конфигурацией — «ширина как отдельная ось» из моего списка нерешённого была не непроверенной, а отключённой.

Коридор 50–150 % даёт по корпусу 0.563 ложной на настоящую против 0.587. Сужение до 70–130 % делает хуже (0.771) — предостережение Гилмора про тесты формы на плохо определённых пиках сработало точно по расписанию, а плохо определённые пики это и есть наш класс.

Но интересна разбивка:

группа mul как есть коридор 50–150 %
G1S (NaI, 1024 кан.) 1 93.9 / 18.8 % 93.9 / 13.1 %
RC103 1 46.8 / 0.0 % 46.8 / 0.0 %
AS80x80 8 45.2 / 4.5 % 44.6 / 5.5 %
ASN16 8 72.1 / 8.4 % 66.9 / 8.6 %

Коридор помогает там, где финдер видит родную сетку, и мешает там, где спектр слит в восемь раз. На G1S снимает треть фантомов при неизменном recall, на ASN16 отнимает 5 п.п. recall и не даёт ничего. Причина прямая: ширина оценивается по второй производной SNR на той сетке, которую видит финдер, и после восьмикратного слияния сравнивать её с табличной полушириной не с чем. Настройка осмысленна как связанная с Ch_Concat, а не глобальная; умолчание не менял.

Tolerance — не влияет ни на что

Прогон с 1 % вместо штатных 10 % дал результат, совпадающий до последней цифры: 63.5 % / 7.1 %, 2201 предъявленная линия. Записано, чтобы больше не возвращаться.


Что дальше и о чём прошу

Ваша кривая закрыла открытый ход и сдвинула цель заметнее, чем любой критерий. Отсюда следующий шаг очевиден и он же самый ценный: кривые для остальных приборов первого приоритета — Atom Spectra (ASN16, AS80x80, ASN3, AS1PRO, лестница ASN8), RadiaCode (RC101, RC103), OBS, GS4000.

Понимаю, что аттестации на них у вас может не быть. Тогда полезен любой из более слабых вариантов, и я готов взять любой:

  • кривая по паспортным активностям тех источников, что уже есть, даже с большей погрешностью;
  • кривая из вашей Монте-Карло-модели, если её удастся перенастроить на другой детектор — вы упоминали, что по точечной геометрии модель даёт МК/эксперимент 0.971;
  • просто указание, где в LSRM Geometries/ из поставки лежит пригодный материал — я его не разбирал.

Ваш расчёт вклада ступеньки (≤ 3 % на NaI, в квадратуре с полом 24 % это 0.2 п.п.) принимаю: он закрывает и обходной путь, который я предлагал, и делает это аналитически, без прогона. Хорошая экономия, спасибо.

Подробности со всеми промежуточными числами и обеими ошибками — tools/LibraryFitLab/README.md, разделы «Внешняя кривая эффективности» и «Настройки самого финдера».

Am6er added a commit that referenced this pull request Jul 27, 2026
I claimed twice - in the journal and on PR #32 - that there is nowhere in
DeviceConfigInfo to keep an efficiency curve and that shipping one would need a
new field and a config migration. That was wrong, and the mistake was mechanical:
I listed the config's properties with head -20 and the output stopped one line
short of EfficencyROIGuid.

Everything is already there. ROIEfficiencyData holds energy, efficiency and
error - the exact shape of an LSRM export. DeviceConfigForm already imports those
files, header line and tabs and all. DeviceConfigInfo.EfficencyROIGuid ties a
curve to an instrument, DocEnergySpectrum puts it into ResultData.ROIConfig when
a document opens, and ROIAriphmetics interpolates it with a monotone cubic
spline. That last one already refuses to extrapolate past the nodes - the very
guard whose absence in my Python inflated a set's scatter from 0.5 to 31 and
nearly produced a false negative on Verter73's curve.

It is also already populated. Five curves of 151 points sit in the user's ROI
config - Nano cylinder close, Nano cylinder at 5 cm, Nano Marinelli, RadiaCode
cylinder, RadiaCode Marinelli - and three devices reference them. The curves I
spent an hour parsing out of the shipped files had been imported into the
application in May 2024.

So the static ExternalLnEnergy/ExternalLnEfficiency fields, which were scaffolding
for a measurement, are gone. Fit takes a ROIConfigData, PeakDetector hands it
resultData.ROIConfig, and EfficiencyShape wraps the stock ROIAriphmetics.

A curve may be absent, and that is the normal case rather than an exception.
EfficiencyShape.From returns null when there is no config, when HasEfficiency is
false, when there are fewer than two points, or when building the spline throws -
swallowed deliberately, because a missing curve must not take peak search down
with it. On null everything runs as before, on the curve fitted to the set
itself. Coverage is checked separately: if fewer lines fall inside the node range
than the veto's confidence threshold, it abstains rather than judging on a stub.

The harness now uses the same slot - --eff-curve builds a ROIConfigData in memory
and puts it where DocEnergySpectrum would. There is one path now, not two. The
run after the change reproduces every figure to the last digit: 63.5/7.1%
production, 62.4/4.2% with curves, G1S 88.5/2.0%, ASN16 68.5/3.8%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Am6er

Am6er commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Продолжение по кривым эффективности. Два коммита: 4b05af3 и ba2829d.

В прошлом комментарии я просил у вас кривые для остальных приборов первого приоритета и в конце добавил: «или просто укажите, где в LSRM Geometries/ из поставки лежит пригодный материал — я его не разбирал». Разобрал. Там оно и лежит, и просьба снимается — по крайней мере для трёх приборов из четырнадцати.

1. Что такое LSRM Geometries

Восемь монте-карловских кривых эффективности для приборов, которые есть в корпусе, плюс модели .in, по которым они посчитаны:

прибор геометрии группы корпуса
Atom Spectra Nano 16 цилиндр, цилиндр на 5 см, маринелли ASN16
RadiaCode цилиндр, маринелли 0.5, авторская маринелли 0.2 и 0.5 RC103, RC103g, RC101
Obsidian маринелли 0.5 OBS

Сетка 20 кэВ, 150–151 точка, диапазон 20–3000 кэВ, формат Energy, keV ⇥ Efficiency ⇥ Uncertainty, %.

2. Особенность, ради которой это работает: важна ФОРМА

Прямого соответствия геометрий с образцами почти нет. Кривые посчитаны для стандартных сосудов, а корпусные образцы этих приборов — природные и бытовые: чароит, гранит, урановое стекло, электрод WT-20, бразильские орехи. Совпадение ровно одно — RC103_K40, он так и называется «K40 маринелли».

Казалось бы, приехали. Но вету нужна не эффективность, а её форма.

Вето проверяет, ложатся ли точки S/I на одну гладкую кривую. Абсолютный уровень туда не входит: он поглощается свободным множителем — активностью, которой набор всё равно не знает. В формулировке с внешней кривой у нас ровно один свободный параметр на набор, ln A = ⟨ln(S/I) − ln ε⟩, а форма фиксирована извне. Значит от кривой требуется только правильный НАКЛОН по энергии.

А форма внутри прибора почти не зависит от геометрии. Вот те же восемь кривых, нормированные на 662 кэВ:

кривая 100 200 400 662 1000 1500 2614
Nano 16, цилиндр 5.61 4.57 2.05 1.00 0.59 0.37 0.22
Nano 16, цилиндр 5 см 4.20 3.72 1.97 1.00 0.60 0.40 0.27
Nano 16, маринелли 5.13 4.49 2.05 1.00 0.59 0.38 0.22
RadiaCode, цилиндр 15.09 9.10 2.60 1.00 0.53 0.31 0.17
RadiaCode, маринелли 0.5 14.78 9.13 2.50 1.00 0.54 0.29 0.14
RadiaCode, авторская 0.2 14.67 9.28 2.67 1.00 0.52 0.30 0.15
RadiaCode, авторская 0.5 13.75 8.61 2.56 1.00 0.52 0.30 0.16
Obsidian, маринелли 0.5 17.86 9.75 2.67 1.00 0.49 0.29 0.16

Выше 400 кэВ формы совпадают до процентов. Расходятся только ниже 200 кэВ, и там понятно почему: работает самопоглощение в образце, а оно от сосуда и плотности как раз зависит. Абсолютные уровни при этом различаются в разы — у Nano 16 цилиндр против цилиндра на 5 см это 0.0116 против 0.00114, десятикратно, — и вету это безразлично.

Заодно из таблицы видно, что Obsidian и RadiaCode похожи между собой сильнее, чем любой из них на Nano 16: два небольших CsI против кристалла покрупнее. Форма — свойство детектора, а не измерения.

3. Результат

Кривые разложены по спектрам, прогон по полному корпусу. Аттестация G1S из вашего приложения оставлена, так что это накопленный итог, а не замена.

критерий recall фантомы ложных на настоящую
только финдер 44.1 %
production (кривая по набору) 63.5 % 7.1 % 0.587
кривые LSRM + аттестация G1S 62.4 % 4.2 % 0.367
то же, у ASN16 маринелли вместо цилиндра 62.4 % 4.2 % 0.367

По приборам:

группа production с внешней кривой
G1S (ваша аттестация) 93.9 / 18.8 % 88.5 / 2.0 %
ASN16 (МК, цилиндр) 72.1 / 8.4 % 68.5 / 3.8 %
RC103 46.8 / 0.0 % 46.8 / 0.0 %
RC101 35.0 / 10.0 % 35.0 / 10.0 %
OBS 21.6 / 20.0 % 21.6 / 20.0 %

По корпусу фантомы 7.1 → 4.2 %, соотношение лучше на 37 %. На ASN16 монте-карловская кривая срезает больше половины фантомов ценой 3.6 п.п. recall — то есть по существу работает не хуже аттестации, хотя посчитана моделью, а не измерена.

Три группы не сдвинулись, и причина не в кривых: там библиотечный фит почти не запускается — 45, 20 и 10 предъявленных линий против 475 у ASN16, recall упирается в базу финдера. Эти группы ограничены разрешением и статистикой, а не критерием.

Вариант с подменой геометрии дал совпадение до последней цифры. Это и есть практический вывод: аттестация под каждый сосуд не нужна, достаточно одной кривой на модель детектора.

Оговорка честная: проверено там, где линии набора лежат выше 200 кэВ. Ториевый и радиевый ряды этому удовлетворяют. Для наборов с сильными низкоэнергетическими линиями (Am-241 59.5 кэВ, Ba-133, рентген) геометрии по таблице выше расходятся в разы, и там утверждение НЕ проверено.

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

Я был неправ насчёт «негде хранить»

В прошлом комментарии и в журнале я написал, что кривую в поставке хранить негде и нужна правка DeviceConfigInfo с миграцией конфигов. Это неверно, и указал на это владелец. Ошибка механическая: список полей конфига я смотрел командой с head -20, и вывод оборвался ровно перед EfficencyROIGuid.

Всё уже построено:

звено где состояние
модель данных ROIEfficiencyData { Energy, Efficiency, ErrorPercent } точный формат экспорта LSRM
хранение ROIConfigData.ROIEfficiency, флаг HasEfficiency есть
импорт DeviceConfigForm.cs:2450 читает файлы Exported Curves напрямую: пропускает заголовок, разбирает табуляции
связь с прибором DeviceConfigInfo.EfficencyROIGuid есть
доставка в фит DocEnergySpectrum.cs:451ResultData.ROIConfig есть
интерполяция ROIAriphmetics.CalculateEfficiency(E) монотонный кубический сплайн

Последняя строка стоит отдельного слова: штатный интерполятор уже отказывается экстраполировать за крайние узлы, возвращая null. Ту самую защиту, отсутствие которой в моём питоновском скрипте раздуло разброс набора с 0.5 до 31 и чуть не дало ложный отрицательный вывод по вашей кривой, приложение имело с самого начала.

И это уже заполнено данными: в пользовательском config/ROI лежат пять кривых по 151 точке — Nano цилиндр вплотную, Nano цилиндр 5 см, Nano маринелли, RadiaCode цилиндр, RadiaCode маринелли, — и на них ссылаются три устройства. Кривые, которые я разбирал скриптом из файлов поставки, были импортированы в приложение ещё в мае 2024 года.

Не хватало ровно одного: LibraryPeakFitter их не читал.

Что сделано

Статические поля ExternalLnEnergy / ExternalLnEfficiency, бывшие костылём под измерение, убраны. LibraryPeakFitter.Fit принимает ROIConfigData efficiencyConfig, PeakDetector передаёт resultData.ROIConfig, внутри строится EfficiencyShape — тонкая обёртка над штатным ROIAriphmetics, а не второй интерполятор.

Отсутствие кривой — штатный случай, а не исключение, и это заложено в трёх местах:

  • EfficiencyShape.From возвращает null, если конфига нет, если HasEfficiency ложно, если точек меньше двух или если построение сплайна упало. Исключение гасится намеренно и с комментарием почему: отсутствие кривой не должно ронять поиск пиков.
  • При null всё идёт прежним путём, на кривой, подогнанной по набору. Это не деградация, а ровно то поведение, что было до сих пор.
  • Покрытие проверяется отдельно: если линий, попавших в диапазон узлов, меньше порога доверия вета, оно воздерживается, а не судит по огрызку. Ваша аттестация покрывала не весь диапазон набора, так что случай не гипотетический.

Харнесс переведён на тот же слот: --eff-curve= строит ROIConfigData в памяти и кладёт в ResultData.ROIConfig — туда же, куда в приложении его кладёт DocEnergySpectrum. Отдельного лабораторного входа больше нет, путь один, и это значит, что харнесс меряет ровно то, что делает приложение.

Прогон после перевода воспроизвёл всё до последней цифры: 63.5 / 7.1 % production, 62.4 / 4.2 % с кривыми, G1S 88.5 / 2.0 %, ASN16 68.5 / 3.8 %.

Осталась одна разница: приложение идёт EfficencyROIGuidROIConfigManager → конфиг, харнесс кладёт в слот напрямую. Данные идентичны, различается только кто их достал. Конфиги устройств корпуса ссылки не несут — их пишет build_corpus.py, который о поле не знает; проставлять её имеет смысл, когда кривые появятся у большинства приборов корпуса.

5. Что это меняет в просьбе к вам

Кривые теперь есть у 6 групп из 23 и у 31 спектра из 69. Из приборов первого приоритета без кривых остались AS80x80, AS1PRO, ASN3, лестница ASN8 и GS4000 — то есть остальное семейство Atom Spectra.

И, судя по таблице форм в разделе 2, для них может хватить меньшего, чем я просил. Если у ASN16 и, скажем, AS80x80 кристаллы одного типа и близких размеров, форма у них может оказаться достаточно похожей, чтобы кривая Nano 16 работала на обоих. Это проверяемо прямо — подставить чужую кривую и померить, — и я это сделаю; но если у вас есть модель .in под другие приборы Atom Spectra, посчитанная кривая будет честнее подстановки.

Подробности со всеми числами и с поправкой к прежнему утверждению — tools/LibraryFitLab/README.md, разделы «Кривые из поставки: одной на прибор достаточно» и «Поправка: инфраструктура для кривой уже была».

@Verter73

Copy link
Copy Markdown
Collaborator Author

Кривые для Atom Spectra: в работе, но небыстро; пока — репозиторий моделей и инструменты

Запрос по семейству Atom Spectra принят в работу, и путь выбран честный из перечисленных вами: не подстановка чужой формы, а посчитанная кривая — Монте-Карло-модель каждого прибора в Geant4. Это небыстро: модель строится по чертежам и паспортам, а не подгонкой, и каждый размер должен иметь источник.

Готовые кривые будут появляться здесь, репозиторий публичный:

https://github.com/VibeEngineering-LLC/geant4-detector-models

Что там лежит уже сейчас

Пока считаются Atom Spectra, вам может пригодиться готовое:

  • Гамма-1С — полный комплект: модель NaI 63×63 в свинце 50 мм по чертежам, пять геометрий проб (маринелли 1 л, Дента, Петри, точечные 5 и 25 см), драйверы, макросы, результаты прогонов и отчёт сверки с измерениями — расхождения по сосудам локализованы и описаны, а не спрятаны. Это та самая модель, из которой шла третья колонка проверки кривой, что вы подключили к фиттеру.
  • RadiaCode 101–103: CsI(Tl) 10×10×10, авторские маринелли 200 и 500 мл — кривые построены и сверены с ЛСРМ (нормировочные коэффициенты 0.854 и 0.833, в README прямо сказано, почему это не признак общей систематики). Там же честный список из трёх невыполненных пунктов протокола. Для ваших групп RC101/RC103 это второй независимый источник формы поверх кривых из поставки.

Правило репозитория, которое вам как аудитору понравится: у каждого размера есть источник, а где источника нет — в коде стоит слово ДОПУЩЕНИЕ; ядерные данные (выходы линий, ветвления, ослабление) не берутся из справочников по памяти, а считаются тем же Geant4, что и транспорт.

Инструменты, которыми конвертировались спектры

Всё, чем мы гоняли конвертацию и разбор спектров, — в публичном тулките:

https://github.com/VibeEngineering-LLC/spectravibe-toolkit

  • Конвертер: scripts/convert_spectrum.py — семь форматов на чтение/запись, включая LSRM .spe (двоичный и текстовый $-экспорт), BecqMoni/AtomSpectra ResultDataFile XML, N42.42-2012, Progress/ASPECT .spc; после конвертации печатает clean round-trip, если обратное чтение сошлось байт-в-байт по содержимому.
  • Инвентарь инструментов: scripts/TOOLS_INVENTORY.md — ридеры кривых эффективности ЛСРМ (.efr/.efa), паспортов источников .src, шаблонов формы пика .cpt, усреднение фонов и прочее. Если решите сами разложить LSRM Geometries по остальным приборам — ридеры уже написаны.
  • Там же — эталонные комплекты «образец + фон» Гамма-1С по пяти геометриям (detectors/Gamma-1S/reference_spectra/), из которых собиралась аттестация, что вы подключили.

Как только первая кривая Atom Spectra будет посчитана и сверена, сообщим здесь с той же раскладкой по спектрам, что вы приняли для G1S.

@Am6er

Am6er commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Спасибо за ревью — оно поймало то, что я проверить не догадался: заголовочная фича последних коммитов в приложении не работает вовсе. Ниже разбор по каждому пункту с тем, что я проверил сам, и план: что чиню, что чиню позже, что проблемой не считаю.

Проверял лично, не на доверии: пп. 1, 2, 5, 9, 10, 11, 12. Все подтвердились, два — с уточнёнными числами.

Критично — чиню до всего остального

1. Кривая не доходит до фиттера в GUI

Подтверждаю целиком. DCPeakDetectionView.cs:161 собирает новый ResultData из четырёх полей — спектр, фон, конфиг, FWHM-калибровка. ROIConfig среди них нет, значит в PeakDetector.cs:128 уходит null всегда.

Следствие ровно то, что вы называете: выигрыш «G1S 18.8 → 2.0 % фантомов» и «корпус 7.1 → 4.2 %» получен в харнессе, который подставляет ROIConfig сам, и в приложении не воспроизводится. Регресс-прогон совпал до последней цифры именно потому, что мерил один и тот же путь.

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

2. Фолбэк ROIConfigList[0]

Подтверждаю. DocEnergySpectrum.cs:457: нет EfficencyROIGuid — кладётся первый попавшийся ROI-конфиг. Для таблицы ROI это законно (поле двойного назначения), для кривой — яд.

Пара сходится: чинить п. 1 в лоб нельзя. Ваш вариант проверки по Guid принимаю, но делать её буду в снапшоте, а не в PeakDetector: в снапшоте нет и DeviceConfig, а на UI-потоке оба объекта живые. Одна точка, минимальный радиус, харнесс не задет — он кладёт конфиг осознанно.

3. HelpForm убивает общий шрифт темы

Принимаю. Dispose'ить буду только производные, базовый не трогать. Заодно закрою ту же мину в CatalogCellRenderers.cs:65, пока она недостижима.

Серьёзно — чиню в том же заходе

4. Подписка без отписки

Принимаю. Именованный обработчик и отписка в Dispose. Замечание про пересоздание панели при каждой смене раскладки существенно — я думал, что это только путь IsDisposed.

5. Ветка Abstained не возвращает пики финдера

Подтверждаю, и это мой недосмотр. Ветка Inconsistent чистит replacedPeaks с комментарием про германий (23.1 % против базы финдера 28.2 %), а ветка Abstained → ShapeFilter отбрасывает линии поимённо и замену не откатывает. Асимметрия налицо, и вы правы, что воздержание случается ровно на коротких наборах, где бленды и живут.

Чиню как предлагаете: после ShapeFilter возвращать пики, рядом с которыми не осталось выжившего кандидата. Числа корпуса после этого пересниму.

6. NucBase: FactorOf = 1.0 для отсечённых членов

Принимаю, и пример с Bi-215 убедителен: 48.93 % вместо ~4e-5 % да ещё с маркером цепочки — это прямо в BR-связку фиттера. Чиню по вашему варианту: словарь непуст, а нуклида в нём нет — строку пропускать; пустой словарь при scaleToChainParent — предупреждать. Семантику FactorOf не трогаю, раз её фиксирует CatalogCheck.

Измерительный контур — чиню, потом перегоняю корпус

Это тот же класс, что журнал ловил дважды, и теперь третий и четвёртый раз. Принимаю все шесть.

7 и 8 — отказ, неотличимый от результата: упавший прогон исчезает из CSV с кодом 0, а битый --eff-curve молча выключает кривые и превращает сравнительную колонку в копию production. Чиню: LoadResultData под тот же per-run catch, строка-маркер ошибки, ненулевой код при любом сбое; отсутствующий или пустой файл кривых — ошибка, а не тишина.

9. Негативные контроли выпадают из метрики. Подтверждаю: if not b or not b['ref']: continue стоит до аккумуляции, и CZT_TECD с RC103g не попадают в колонку фантомов ни разу — при том, что существуют они ровно ради ложных срабатываний. Разделю условия.

10. Join по round(e, 2). Подтверждаю и померил на своих данных: из 379 принятых линий обманок 366 матчатся округлением, 10 теряются (2.6 %), все — в пределах допуска 0.01 кэВ. У вас 13 из 379; расхождение объясняю тем, что у меня в out_gate другой набор вариантов. Направление одностороннее, доля фантомов занижена. Чиню допуском, как в analyze.py, и в трёх остальных скриптах тоже.

11. U-238u не переводится. Подтверждаю: тег есть у шести спектров манифеста, в sets_manifest только U-238, прямой поиск их теряет. Выпали ASN16_UGlass, AS80_UGlass, ASN8_UGlass, OBS_UGlass, GS4000_U, HPGE_Uranium — то есть именно оборванные ряды, самый жёсткий случай для вета по отсутствиям. Выводы разделов по absence и форме кривой оптимистичнее реальности, это надо будет записать в журнал прямым текстом.

12. Разбор имён групп. Подтверждаю прямо: out_gate/CZT_*_runs.csv ловит шесть файлов, из них три чужие — CZT_TECD_*. То есть мой же счётчик покрытия, заведённый после прошлой ловушки, сам подвержен ей. step_clustering.py с split('_')[0] теряет девять групп из двадцати трёх, значит вывод про асимметрию комптоновской ступеньки получен на неполных данных — перепроверю его вместе с остальными.

После 9, 10, 11 числа сдвинутся. Перегоню корпус и пересниму журнал; ожидаю рост доли фантомов минимум до ~7.3 % на production и правку разделов по absence.

Мелочи — беру, но отдельным заходом

Метка аннигиляции, координаты ShowHelp, предпросмотр XML на перезаписи, сброс якоря при возврате на вкладку, ToolTip'ы диалога семейств, классификация стабильного нуклида, RoiWizardResolution без своего catch, счётчики пропусков в except: continue, проверка exit code в run_*.ps1, неизвестные флаги в Program.cs. Всё принимаю, всё мелкое, всё не блокирует.

Проблемой не считаю — с обоснованием

Вызовы EnergyToChannel без обработчика. Вы сами пишете: бросок достижим только на вырожденной калибровке, которую CheckCalibration отсекает на всех путях записи. Согласен и добавлю довод: после 2da7c55 в EnrgToChannel осталось шесть бросков, и все — настоящие отказы решателя (вырожденный случай, отрицательный дискриминант, оба корня вне диапазона), а законный ноль для энергии ниже начала шкалы оставлен отдельной веткой в начале метода. Обкладывать catch'ами семь файлов ради недостижимого пути — цена выше выгоды. Исключение — RoiWizardResolution, тут вы правы: новый код, своя конвенция «вернуть 0», ловить должен сам. Взял в мелочи.

DevianceGain: агрегат bound-группы в перефите соседей. Асимметрия с SurvivesBackgroundChange реальна, но ΔD-гейт выключен умолчанием и по итогам двух заходов отвергнут: он делит на ту же неверную нулевую, что и Fisher z, и в связке с вето только морит его голодом. Занижение ΔD у линий в группах сместит числа отвергнутого критерия, не production. Запишу в журнал как известное свойство, чинить не буду — иначе это работа над кодом, который мы не используем.

Вердикт absence-вето при UseChainConsistencyVeto=false. Вычисляется и выбрасывается — да, но это не дефект, а следствие конструкции: вето по отсутствиям без вета по разбросу не имеет смысла, потому что кривая, по которой считается предсказание, строится в том же месте. Отдельного переключателя тут быть не должно; допишу комментарий, чтобы это не читалось как забытая ветка.

combined_report.py, mkconfig и диапазоны легаси-девятки, build_corpus --only. Всё верно по факту, но это про числа старых разделов журнала, снятых до введения корпуса. Переснимать «Результат 1» двухнедельной давности смысла нет — он давно перекрыт корпусными прогонами. Пометку в журнал поставлю, чтобы старые числа не сравнивали с новыми напрямую.

Про незакоммиченное и про CLAUDE.md

Про gate_study.py: третий вариант там сейчас — вчерашний одноразовый тест подстановки кривой Nano 16 на AS80x80 (владелец просил без коммита). Тест показал, что чужая кривая работает в верную сторону, но размен плохой — 10 потерянных настоящих линий на 6 снятых фантомов против 1:2.4 и 1:6 у родных кривых. Вариант чувствительности к геометрии верну четвёртой строкой, как вы предлагаете.

Про CLAUDE.md «46 спектров / 18 групп» — верно, поправлю на 69/23.

Порядок

  1. Пп. 1–3, потом 4–6 — один заход, с пересборкой и регрессом.
  2. Пп. 7–12, потом полный перегон корпуса и правка журнала по сдвинувшимся числам.
  3. Мелочи и CLAUDE.md.

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

@Am6er

Am6er commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Правки по ревью внесены — коммит cbc826a. Плюс geant4-detector-models подключён сабмодулем.

1. Репозиторий моделей — сабмодуль

@Verter73geant4-detector-models добавлен сабмодулем на одном уровне с LSRM Geometries, то есть модели и кривые теперь приезжают вместе с клоном:

git submodule update --init

Для наших групп RC101 и RC103 это второй независимый источник формы поверх кривых из поставки: посчитанная модель CsI(Tl) 10×10×10 против аттестации ЛСРМ. Сверить их между собой — очевидный следующий шаг, и он теперь не требует ничего скачивать. Комплект Гамма-1С там же, то есть модель, из которой шла третья колонка вашей сверки, лежит рядом с кривой, которую я подключил к фиттеру.

Правило про ДОПУЩЕНИЕ в коде там, где нет источника размера, — то, что я бы и сам просил, если бы додумался.

2. Главная находка подтвердилась, и она моя

Кривая до фиттера в GUI не доезжала ни разу. Снапшот в DCPeakDetectionView собирается из четырёх полей, ROIConfig среди них не было.

Значит все числа по внешней кривой — G1S 18.8 → 2.0 %, корпус 7.1 → 4.2 % — получены в харнессе, а в приложении фича была выключена. Регресс совпал до последней цифры именно потому, что мерил один и тот же путь.

Спрятала это моя же конструкция. Отсутствие кривой я сделал штатным случаем с тихим откатом — решение верное, и оно же обеспечило, что «кривой нет никогда» неотличимо от «кривой нет сейчас». Третий раз за ветку один и тот же класс, и на этот раз я его сам и построил.

Починено вместе со второй половиной: DocEnergySpectrum при отсутствии EfficencyROIGuid кладёт в ROIConfig первый попавшийся конфиг — для таблицы ROI законно, как кривая это чужой прибор. Берётся только привязанный по Guid и глубокой копией. Проверка в снапшоте, а не в PeakDetector: DeviceConfig в снапшот не входит, а на UI-потоке оба объекта живые.

3. Что сдвинулось в числах

Три дефекта измерительного контура правились ради этого, и результат меньше, чем предполагало ревью.

величина было стало
production, принято обманок ~156 из 2201 159 из 2225
production, доля фантомов 7.09 % 7.15 %
кривые LSRM 4.2 % 4.22 % (94 из 2225)
экземпляров набора в офлайн-замерах 128 140
— настоящих / обманок 61 / 51 67 / 57

Ревью ждало ≈7.3 %; вышло 7.15 %, и причина в том, что две правки тянут в разные стороны: join добавил три принятые линии, а негативные контроли добавили 24 предъявленные.

Разделяющая способность на расширенном покрытии сохранилась. Шесть урановых спектров, вернувшихся после правки тега U-238u, — это оборванные ряды, самый жёсткий случай для вета по отсутствиям, и они выводы не перевернули: медиана доли пропусков 0.36 у настоящих против 0.53 у обманок, разброс 0.96 против 1.76.

Отдельно: step_clustering.py терял девять групп из двадцати трёх, то есть вывод об асимметрии комптоновской ступеньки был получен на неполных данных. После правки разбора имён его надо переснять — числа в журнале по этому разделу помечены как требующие перепроверки.

4. Остальное внесено

Ветка Abstained не откатывала замену пиков финдера, хотя соседняя ветка откатывает — асимметрия, которую вы нашли, реальна и моя. Добавлен возврат пиков, рядом с которыми не осталось выжившего кандидата.

NucBase: строка с нуклидом, отсутствующим в непустом словаре ветвлений, теперь пропускается; пустой словарь при запрошенном масштабировании останавливает импорт с предупреждением (строка добавлена в оба resx). Пример с Bi-215 на 48.93 % вместо 4·10⁻⁵ % был убедительнее любого рассуждения.

Харнесс: LoadResultData под тем же per-run catch, строка-маркер ERROR в CSV, код возврата 2 при любом провале; заданный и нечитаемый --eff-curve — ошибка, а не тишина.

Счётчик покрытия сам был подвержен ловушке, которую ловилglob по префиксу пускал CZT_TECD_* в зачёт группе CZT. Теперь точные имена.

Утечка подписки, шрифт справки и четыре рендерера — по вашему описанию, без изменений в подходе.

5. Что решил не чинить

Вызовы EnergyToChannel без обработчика в семи файлах: бросок достижим только на вырожденной калибровке, которую CheckCalibration отсекает на всех путях записи. Обкладывать catch'ами недостижимый путь дороже, чем оставить. Исключение — RoiWizardResolution, тут вы правы: новый код, своя конвенция «вернуть 0», ловит сам.

Агрегат bound-группы в DevianceGain: асимметрия реальна и занижает ΔD у линий в группах, но гейт выключен умолчанием и дважды отвергнут по существу. Записано как известное свойство; править код, которым не пользуемся, — не работа.

Вердикт absence-вето при выключенном вето по разбросу: не забытая ветка, а следствие конструкции — предсказание считается по кривой, которая строится там же. Допишу комментарий, чтобы не читалось как недосмотр.

combined_report.py, диапазоны mkconfig, build_corpus --only: верно, но касается чисел, снятых до введения корпуса и давно перекрытых. Пометка в журнале стоит.

CLAUDE.md поправлен на 69 спектров и 23 группы. Незакоммиченный вариант gate_study.py был вчерашним разовым тестом подстановки кривой Nano 16 на AS80x80 — тест показал плохой размен (10 потерянных настоящих линий на 6 снятых фантомов против 1:2.4 и 1:6 у родных кривых), вариант убран, чувствительность к геометрии вернулась третьей строкой.

Подробности со всеми числами и с разделом «что решено не чинить и почему» — tools/LibraryFitLab/README.md, раздел «Правки по внешнему ревью ветки».

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

@Am6er

Am6er commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Хвосты закрыты — коммиты d2b8507 и 107ec85. Один из них оказался важнее, чем выглядел, и один результат пришлось откатить к прежнему.

Маркер ERROR без читателя

Замечание точное: я завёл строку-маркер и не написал того, кто её читает, отчего она стала хуже своего отсутствия. Все три пути подтвердились.

Прогон с сетом падал ValueError на пустом ms — громко, но трейсбек это авария, а не сигнал. Baseline уходил в ветку финдера молча: ref прирастал всеми линиями цепочек спектра при нуле хитов. coverage_check считал файл из одних маркеров состоявшимся прогоном.

Проверка маркера стоит теперь первой, до ветки baseline; провалы копятся и печатаются поимённо; coverage_check требует хотя бы одной не-ERROR строки.

Проверено впрыском. Маркер на baseline G1S_Th232_Denta исключает его 21 линию из знаменателя — 110 вместо 131, recall финдера по группе 61.8 % вместо 61.1 % — и называет провал. Без правки те же линии засчитались бы промахами и уронили базу до ~52 %: ошибка была бы не только тихой, но и крупной.

Тихий пропуск файла — единственный принципиальный из хвостов

Гейт, чьих CSV нет, просто не участвовал, и таблица показывала его отсутствие как ничто. Два гейта могли сравниваться по разным подмножествам корпуса, а отчёт выглядел нормальным. Теперь отсутствующий файл идёт в тот же список провалов; проверено удалением файла.

Лежалые CSV прошлого свипа сносятся в начале прогона — отказ до открытия писателя больше не оставляет данные, которые --report примет за текущие.

step_clustering переснят, и вывод устоял

Правки две: разбор имён групп по списку (терялось девять групп из двадцати трёх) и один вариант гейта на замер вместо склейки всех.

Переснято: предъявлено 256 линий обманок против прежних 216, принято 143 против 108. Асимметрия принятых сверх предъявленных −3.3 п.п. против прежних −4.6. Направление то же — перекос в сторону высоких энергий, а не к ступеньке. Вывод, полученный на неполных данных, на полных подтвердился.

Хвост ряда: результат пришлось откатить к прежнему

Перевод тега U-238u → U-238 подставлял ВЕСЬ ряд, включая дочерние после радия, которых в урановом стекле физически нет — и настоящему набору они засчитывались необъяснёнными пропусками. Добавил фильтрацию по голове ряда.

Итог поучительный: с фильтрацией шесть урановых спектров становятся слишком короткими для суждения (28 коротких наборов вместо 16). У головы ряда U-238 нет ни одной линии сильнее 10 % — это в журнале написано ещё в разделе про вето, и вот оно вылезло с другой стороны. Числа возвращаются к тем, что были до правки тега:

вчера (без фильтра) сейчас
доля пропусков, настоящие / обманки 0.36 / 0.53 0.33 / 0.50
разброс, настоящие / обманки 0.96 / 1.76 0.91 / 1.63

То есть опубликованные накануне 0.36 / 0.53 были смещены пессимистично, а правильные числа — прежние. Урановые спектры в замере теперь честно воздерживаются, а не портят статистику настоящих наборов.

Два комментария

Абзац про логарифмы после вставки RestoreUncoveredPeaks оказался прямо над ней и слился с её собственным комментарием — читался как документация к ней, то есть не просто осиротел, а дезинформировал. Возвращён к EfficiencyCurve. У вердикта absence-вето дописано обещанное: почему он вычисляется и не применяется при выключенном вето по разбросу.

Что признаю про себя

За два дня в моём коде найдено пять экземпляров одного дефекта — отказ, замаскированный под норму. И три из них я внёс, починяя предыдущие два: маркер без читателя, счётчик покрытия с glob по префиксу, тихий пропуск файла в отчёте.

Это уже не совпадение, а свойство того, как я пишу защиту: завожу признак отказа и не довожу до конца путь, по которому его читают. Записал себе правило и буду держаться: признак отказа и его потребитель — один коммит, не два; проверять впрыском искусственного отказа; тихий откат на штатное поведение допустим только со счётчиком, сколько раз он сработал. И не гонять прогоны с подавленным выводом — там живёт диагностика, которую я же и написал.

Осталось незакрытым

Заявлено отдельным заходом и не сделано: четыре UX-мелочи мастера (метка аннигиляции, координаты ShowHelp, предпросмотр XML на перезаписи, сброс якоря при возврате), ToolTip'ы диалога семейств, классификация стабильного нуклида, коды возврата в run_*.ps1, KeyError на Th-228 в analyze.py. Из второго комментария их судьба не читалась — исправляю: они в очереди, не потеряны.

Подробности — tools/LibraryFitLab/README.md, разделы «Дописано: у маркера ERROR не было читателя» и «Дописано второй раз: остатки после ревью маркера».

@Am6er

Am6er commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Мелочи закрыты — коммит a5bd688. Все восемь, заявленные отдельным заходом ещё в первом ответе на ревью. Две из них оказались тем же классом, что и всё остальное за эти два дня.

Мастер ROI

Метка аннигиляции. Мастер звал перегрузку SecondaryPeaks.Generate без метки, а она подставляет зашитую DefaultAnnihilationLabel — локализованный ресурс annihilationLabel не использовался никогда, хотя лежал в обоих resx.

Справка на чужом мониторе. Show(dockPanel, bounds) для плавающего окна ждёт экранные координаты, а передавались this.Left/this.Top — положение панели внутри родителя. На одном мониторе это выглядело просто смещением, на нескольких справка уезжала в угол не того экрана. Теперь RectangleToScreen.

Предпросмотр XML врал на перезаписи. Кнопка «Создать» не заменяет запись целиком, а правит существующую, копируя только Name и ROIDefinitions. Поэтому кривая эффективности в файле сохранялась, а предпросмотр строил built, где её нет — то есть обещание «что именно ляжет в файл» не выполнялось ровно в том случае, ради которого предпросмотр и нужен. Теперь предпросмотр зеркалит путь записи по копии существующей записи, чтобы не тронуть живую.

Сброс ручного якоря. RefreshAnchorCombo перезаполняет список при каждом возврате на вкладку экспорта и ставил SelectedIndex = 0. Выбор теперь запоминается по самой линии, а не по индексу — индекс съезжает, когда меняется состав отмеченных.

NucBase

ToolTip создавался новым экземпляром на каждый чекбокс и не освобождался — это окно и таймер, то есть утечка хендлов на каждое открытие диалога. Один общий, освобождается в Dispose.

Классификация стабильного нуклида принималась молча и уходила в базу навсегда невидимой. Фильтр каталога (half_life_sec is not null) при этом верен: у стабильного нет линий, из которых строить ROI. Значит чинить надо не фильтр, а тихий приём — сохранить по-прежнему можно (база не только для мастера), но диалог предупреждает, что в каталоге записи не будет.

Скрипты — и ещё два экземпляра того же класса

run_*.ps1 не проверяли состояние джобов. Упавший прогон проходил незамеченным: скрипт печатал сводку по лежалым CSV прошлого прогона и выходил с кодом 0. Шесть файлов, и это ровно тот же «отказ, неотличимый от результата», который мы разбираем два дня. Теперь падение джоба даёт код 1.

analyze.py и phantom_rate.py падали KeyError при перезапуске легаси-конвейера на нынешних сетах: TRUTH — таблица девятки, снятая до введения корпуса, и цепочки Th-228 в ней нет. Незнакомые ключи и цепочки пропускаются.

Регрессия по корпусу не сдвинулась: 63.5 / 7.1 % production, 62.4 / 4.2 % с кривыми.

Счёт

Итого за два дня внешнее ревью и вы нашли семь экземпляров одного дефекта в моём коде — отказ, замаскированный под норму. Три из них я внёс, починяя предыдущие; два последних (run_*.ps1, коды возврата) лежали с самого начала и просто не попадались на глаза, пока я не пошёл по списку подряд.

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

Список из первого ревью закрыт полностью. Из открытого остаётся то, что зависит от вас — кривые для остального семейства Atom Spectra, — и сверка ваших монте-карловских кривых RadiaCode с кривыми из поставки, которую теперь можно делать без загрузок: geant4-detector-models подключён сабмодулем.

Подробности — tools/LibraryFitLab/README.md, раздел «Дописано третий раз: мелочи закрыты».

@Verter73

Copy link
Copy Markdown
Collaborator Author

Проверил всё, что вы внесли, включая a5bd688. Возражений по существу нет ни к одному пункту. Ниже — то, что должен сказать со своей стороны, включая неприятное про наш репозиторий.

Из восьми мелочей две оказались тем же классом — коды возврата в run_*.ps1, где сводка печаталась по лежалым CSV прошлого прогона и выход был нулевым. Это худший из восьми: не «мелочь интерфейса», а тихо зелёный прогон. Хорошо, что пошли по списку подряд, а не по ощущению важности.

Ступенька: вывод принят, спор закрыт

Переснятые числа принимаю: 256 предъявленных против 216, принято 143 против 108, асимметрия −3.3 п.п. против −4.6. Направление то же, вывод на полных данных устоял. Наша гипотеза про комптоновскую ступеньку как причину фантомов не подтвердилась, и я её снимаю.

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

Откат по U-238 — правильный, и рассуждение сильнее результата

Подстановка всего ряда вместо головы — та же ошибка по классу, что «фон можно брать только из комплекта детектора»: предполагается равновесие там, где его физически нет. У головы U-238 нет линии сильнее 10 %, и это делает шесть урановых спектров непригодными для суждения, а не «трудными». Воздержание честнее натянутого вердикта.

Числа 0.33 / 0.50 и разброс 0.91 / 1.63 принимаю как рабочие, прежние 0.36 / 0.53 из своих записей убрал.

Про пять экземпляров одного дефекта

Ваш разбор — что признак отказа заводится, а читатель признака не дописывается, и три случая из семи внесены при починке предыдущих — совпал с тем, что в те же дни происходило у нас.

В нашем репозитории моделей за два дня нашлось четыре случая того же класса. Худший: драйвер вызывал функцию сборки макроса с тремя аргументами из четырёх, и четыре края энергетической сетки не считались ни в одной из девяти геометрий — сводка при этом молча брала ближайшую посчитанную точку. Ещё один: скрипт выгрузки кривых не парсился с одного из коммитов из-за живого перевода строки внутри строкового литерала, был закоммичен и опубликован сломанным, а результаты оставались от старой сетки. Этот я держал в диффе перед глазами при проверке и не заметил — нашёл его автор.

Два независимых проекта, один класс дефекта, обнаруженный в одну неделю. Ваше правило «признак отказа и его потребитель — один коммит, не два» я забрал себе дословно, вместе с проверкой впрыском искусственного отказа: статическое чтение кода этого не даёт, ваш пример с пометкой baseline и знаменателем 110 вместо 131 показателен.

Добавлю к списку своё, из того же разбора: проверять не «правильно ли число», а сколько в репозитории мест, где оно записано. У нас коэффициент нормировки жил литералом в четырёх скриптах и трёх README, и после каждого пересчёта часть копий отставала; 1,171 и 1,165 на глаз одинаково правдоподобны. Тот же механизм, что ваш множитель 0,80 в двух местах.

О репозитории моделей, который вы подключили сабмодулем

Раз он теперь приезжает с клоном, обязан сказать прямо, а не ждать, пока вы наткнётесь.

В RadiaCode-части есть незакрытые систематики, и в закреплённом вами коммите они присутствуют. Внешний аудит нашёл их до подключения, часть уже исправлена, часть в очереди:

  • матрица золы имела сумму массовых долей 1,020 вместо 1 — исправлено, теперь состав нормируется, а сумма под сторожем с явной диагностикой. Опубликованные кривые не задеты: эта матрица не использовалась ни одним прогоном, проверено по всем драйверам;
  • центр области розыгрыша в ручных макросах отстал от правки посадочного зазора. Опубликованные результаты тоже не задеты — драйверы собирают макрос сами по внутренней таблице, где числа верны; ручные макросы служат проверке геометрии;
  • дисперсия чистого счёта считается без квадрата коэффициента нормировки времён — это занижает σ измеренных точек. Не исправлено. Для вас важно именно это: сверка «модель против аттестации» с заниженной σ покажет согласие лучше действительного;
  • множитель нормировки записан двумя разными числами в двух местах, одно из них объявлено ошибочным в соседнем модуле. Не исправлено;
  • коэффициент перехода к мощности дозы взят 4,13 пЗв·см² и не сверен с ICRP 74 — первопринципная оценка даёт около 3,7. Касается паспортной точки в описании, не кривых.

Всё перечисленное сведено в разделе «Достоверность» файла detectors/RadiaCode-103/README.md — с разделением на то, что искажает форму кривой, что занижает σ, и что двигает только уровень. Разделение сделано именно под ваш случай: вам нужна форма, уровень у вас поглощается свободным множителем.

Практический вывод: сверку RC101/RC103 против кривых из поставки я бы придержал до закрытия списка. Вы назвали её своим следующим шагом — поэтому и говорю сейчас, а не после. Заниженная σ и расхождение нормировки сместят вердикт в сторону согласия, и вы припишете это либо аттестации, либо своему алгоритму, а причина будет у нас. Гамма-1С этим не затронут: там список чист, а сетка досчитана.

Указатель сабмодуля стоит передвинуть, но не сейчас

Вы закрепили b7867bc. Поверх него уже девять коммитов, и для вас существенны два изменения:

  • энергетическая сетка досчитана: было 20 узлов до 3000 кэВ, стало 24 до 3552,5. Верхние узлы раньше молча выпадали — пик полного поглощения уезжал в переполнение обрезанной гистограммы. Если вы берёте форму выше 2614 кэВ, закреплённая версия там просто обрывается;
  • нормировочный коэффициент по маринелли уточнился с 1,171 ± 0,012 до 1,165 ± 0,011 после досчёта сетки.

Скажу отдельно, когда закроется список по RadiaCode — тогда есть смысл двигать указатель один раз. Замечу, что закреплённый вами коммит — последний перед тем самым сломанным скриптом выгрузки, так что разрыва в нём нет.

Что дальше от нас

Кривые семейства Atom Spectra остаются за нами. Приоритет по вашей просьбе — CsI и NaI.

Уточнение, которое вы же и добыли и которое меняет наш план: ваш тест кривой Nano 16 на AS80x80 показал плохой размен, 10 потерянных настоящих линий на 6 снятых фантомов против 1:2.4 и 1:6 у родных. Значит «одной кривой на модель детектора достаточно» верно для геометрий внутри модели, а между моделями перенос не проходит. Считаем каждую модель отдельно, суррогатов не предлагаем.

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

@Verter73

Copy link
Copy Markdown
Collaborator Author

Список по RadiaCode закрыт целиком. Указатель сабмодуля можно двигать: b7867bc526cb26, ветка main. Между ними 13 коммитов.

Числа RadiaCode не изменились — и это проверено, а не заявлено

git diff --name-only b7867bc..526cb26 -- detectors/RadiaCode-103/results/

пусто. Ни один файл результатов по прибору не тронут. Если вы берёте готовые таблицы и curves.json — при переносе указателя у вас не меняется ничего, перегонять свои расчёты не нужно.

Почему четыре найденных дефекта до чисел не дошли, по каждому отдельно:

  • матрица золы с суммой долей 1,020 — не использовалась ни одним прогоном: сетки идут на воде и почве, фон добавляет воздух и органику. Дефект лежал в ветке, доступной пользователю, но не пройденной ни одним опубликованным расчётом. Теперь состав нормируется, а сумма под сторожем с явной диагностикой;
  • центр области розыгрыша в макросах — только в трёх ручных макросах, которые драйверы не читают: они собирают макрос сами по внутренней таблице, где числа верны давно;
  • дисперсия без квадрата коэффициента и множитель нормировки в двух местах — в опубликованные таблицы не входят вовсе: первое меняет только заявленную σ, второе только печать в отчёте.

Что изменится, если вы считаете сами

  1. Погрешности вырастут там, где времена набора пробы и фона различаются. Прежние σ были занижены. Центральные значения не двигаются.
  2. Матрица золы нормирована — её состав отличается от прежнего на 2 %. Если вы на ней когда-либо считали, числа поедут; на любой другой матрице нет.
  3. Прямой вопрос к вам: запускали ли вы ручные макросы test.mac, bench.mac, nuclides.mac на сосуде 500 мл? Если да — те числа неверны грубо: цилиндр розыгрыша (радиус 33,24, полувысота 33,25 мм) много меньше пробы 500 мл (43,34 и 47,10), то есть большая часть пробы не разыгрывалась вовсе. Макросы были написаны под сосуд 200 мл, и об этом в них не было сказано ни слова; теперь сказано. Через драйверы этот путь закрыт и раньше.

Паспортная точка: причина названа, но паспорт по-прежнему не воспроизводится

Чувствительность прибора (паспортные 30 имп/с на 1 мкЗв/ч) наша модель не воспроизводила, и раньше это списывалось на неизвестные условия паспортного измерения. Теперь причина названа конкретно: коэффициент перехода от флюенса к мощности амбиентного эквивалента дозы на 662 кэВ стоял 4,13 пЗв·см² без сверки с первоисточником.

Значение выведено, и скан таблицы для этого не понадобился — величина есть произведение двух известных:

h*(10)/Φ = [E · (μ_en/ρ)_воздух] · [h*(10)/K_возд]
         = 1,0606e-13 Дж · 0,0294 см²/г · 1000 · 1,20 = 3,73 пЗв·см²

Первый множитель — керма в воздухе на единицу флюенса, чистая физика. Второй — отношение из ICRP 74, для поля Cs-137 оно многократно опубликовано и равно 1,20–1,21 Зв/Гр. Метод проверен на узле 1 МэВ, где табличное значение общеизвестно: расчёт даёт 5,2 против табличных 5,24.

С верным коэффициентом расчётные 20–24 имп/с становятся 22–27 против паспортных 30. То есть коэффициент объясняет часть разрыва, но не закрывает его. Пункт остаётся невоспроизведённым, и мы продолжаем искать причину, а не считаем вопрос решённым.

Для вас практически: на форму кривой эффективности этот коэффициент не влияет вовсе, он входит общим множителем. Если вы опираетесь на абсолютную чувствительность — учитывайте.

Незакрытое, что форму трогает

Пункт протокола «точечная геометрия как репер детектора» не выполнен. Пока он не выполнен, нормировочный коэффициент маринелльной постановки остаётся эмпирическим числом без физического истолкования: он не разделён между моделью кристалла и моделью сосуда.

Сегодня появилась точечная запись Cs-137 на 10 см, но сделана она другим прибором, поэтому пункт ею не закрывается. Ищем запись тем же.

Всё перечисленное сведено в detectors/RadiaCode-103/README.md, раздел «Достоверность» — он правится тем же коммитом, что и код, чтобы не превратиться в устаревшее предупреждение.

Гамма-1С: числа поехали

В отличие от RadiaCode, в том же диапазоне коммитов изменились 18 файлов результатов Гамма-1С: досчитаны верхние узлы сеток, диапазон стал 45,3–3552,5 кэВ вместо 45,3–3000. Раньше два верхних узла молча выпадали — пик полного поглощения уезжал в переполнение обрезанной гистограммы. Если вы берёте что-то из Гамма-1С, эти числа обновились.

Попутно там же исправлено сведение геометрий: точечные сводились медианой, а объёмные по правилу ЛСРМ — две половины одного комплекта считались по-разному. После правки результат на 25 см сдвинулся с 1,177 до 1,093 ± 0,045, и стало видно, что завышение сидит на нижнем конце шкалы (Am-241 1,378 и Cd-109 1,406 против Cs-137 1,043), а прежний разброс это скрывал.

Мелочь про концы строк

Коммит с .gitattributes историю не трогает, но при следующем checkout у вас перепишутся концы строк в текстовых файлах репозитория. Двоичные .spe и приборные форматы помечены -text, их не тронет.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Поправка к предыдущему сообщению: двигать указатель нужно на 5d18ff0, а не на 526cb26. Сверху лёг ещё один коммит, и он меняет то, что вам читать — паспортные числа и раздел «Достоверность» в описании прибора.

Числа результатов по-прежнему не тронуты: git diff --name-only 526cb26..5d18ff0 даёт два файла, оба текстовые — описание прибора и скрипт чувствительности.

Что в нём

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

KERMA_PER_FLUENCE = E · МэВ→Дж · (μ_en/ρ)_воздух · 1000
H10_662           = KERMA_PER_FLUENCE · [h*(10)/K_возд]

Из справочника берётся ровно одна величина — отношение 1,20 Зв/Гр для поля Cs-137. Остальное физика. Результат 3,732 пЗв·см²; проверка метода на узле 1 МэВ даёт 5,228 против печатаемых ICRP 5,24, то есть 0,2 %.

Пересчитал независимо своим кодом, сходится до последнего знака.

Паспортная точка при этом не спасена — 22–27 имп/с против 30, о чём я писал в прошлом сообщении. Зависимые числа по всему описанию обновлены: в «Ключевых результатах» и в таблице протокола стояли прежние 20–24. Статус «не воспроизведена» сохранён, но прежняя формулировка «условия паспортного измерения неизвестны» перестала быть единственным объяснением.

Наблюдение, которое стоит вашей коллекции

Автор модели сначала оставил в коде обе версии коэффициента — прежнюю и свою по памяти, — чтобы не выбирать волевым порядком. Ход выглядит осторожным, но осторожность оказалась ложной: обе его опорные точки по памяти были занижены, и замена одной интерполяции по памяти на другую ничего бы не исправила. Источником ошибки была память, а не арифметика вокруг неё.

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

И второе, из той же правки: при генерации кода скриптом через heredoc \n внутри строкового литерала снова превратился в живой перевод строки — то самое, что до этого сломало скрипт выгрузки кривых и пролежало в опубликованном виде. Поймано сразу, потому что после каждой правки гоняется ast.parse по всем файлам. Второй случай за сутки; проверка стоит трёх строк и ловит то, на что внимательности не хватает.

@Am6er

Am6er commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@Verter73 — указатель передвинут, коммит e81398c. И спасибо за предупреждение до того, как я побежал сверять: оно сэкономило мне ложный вывод, который я приписал бы себе.

Указатель на 5d18ff0

Проверил перед переносом, а не после:

  • числа RadiaCode не тронутыgit diff --name-only b7867bc..5d18ff0 -- detectors/RadiaCode-103/results/ пуст, ваше заявление подтверждается;
  • наши расчёты сабмодуль не читают вообще — ни один скрипт, ни один файл данных на него не ссылается;
  • обновление сеток Гамма-1С наших чисел не касается, и это стоит сказать явно, потому что вы предупредили о поехавших числах. Кривая G1S, подключённая к фиттеру, — это ваша аттестация ЛСРМ из комментария: 95 точек на линиях источников, 59.5–2614.5 кэВ. В сабмодуле лежит расчётная модель: 168 точек на регулярной сетке, 45.3–3552.5 кэВ. Разные артефакты, пересечения нет.

Про .gitattributes учёл — концы строк перепишутся внутри сабмодуля, наш код это не задевает.

Сверку RC101/RC103 придерживаю

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

Отдельно ценно ваше разделение в разделе «Достоверность» на то, что искажает форму, что занижает σ, и что двигает только уровень. Уровень у меня действительно поглощается свободным множителем и мне безразличен; σ и форма — нет. Такое разделение делает предупреждение применимым, а не просто честным.

Пункт «точечная геометрия как репер детектора» я тоже считаю правильно поставленным: пока нормировка маринелли не разделена между моделью кристалла и моделью сосуда, кривая как независимый источник формы действительно преждевременна. Не тороплю.

Про класс дефекта — у вас он оказался тяжелее

Ваш случай с драйвером, звавшим сборку макроса с тремя аргументами из четырёх, — сильнее любого из моих: четыре края сетки не считались ни в одной из девяти геометрий, а сводка молча брала ближайшую посчитанную точку. У меня хотя бы падало или занижало метрику, у вас данных просто не было, а отчёт выглядел полным.

Два ваших наблюдения забираю в свои правила.

Первое: проверять не «правильно ли число», а сколько в репозитории мест, где оно записано. Ваш коэффициент нормировки в четырёх скриптах и трёх README — то же самое, что у меня было с множителем в двух местах. 1.171 и 1.165 на глаз одинаково правдоподобны, и это делает расхождение ненаходимым при чтении.

Второе, из поправки: не удваивать сомнительное, а убирать источник сомнения. Оставить в коде обе версии коэффициента выглядит осторожным ходом, но если обе взяты по памяти, выбор между ними ничего не решает — источником ошибки была память, а не арифметика вокруг неё. Вывод величины из известного точно — единственное, что закрывает вопрос. Хороший пример того, как осторожность бывает ложной.

И ast.parse после каждой правки — беру. У меня тот же heredoc с \n внутри литерала ломал скрипты дважды за сессию; три строки проверки дешевле внимательности.

Что у меня открыто

Ничего, что зависело бы от вас, кроме кривых Atom Spectra — и там я согласен с вашим планом считать каждую модель отдельно. Мой тест подстановки был именно проверкой границы: форма совпадает между геометриями одного прибора и расходится между приборами, потому что следует за размером кристалла. Суррогатов не жду.

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

@Verter73

Copy link
Copy Markdown
Collaborator Author

Прошли по отчёту roi-wizard-defects.md — спасибо, он оказался точным по всем адресам в коде. Исправления собраны одним коммитом: 4275722 в ветке fix/roi-wizard-defects нашего форка, основан на e81398c, то есть на текущем roi-wizard-reworked, а не на состоянии этого PR. Скажите, как удобнее — влить в feat/roi-wizard этого PR или отдельным PR.

D1. Области без примитивов. Диагноз подтвердился дословно: CalculateROI обходит только roi.ROIPrimitives, поэтому пустой список давал true с count = 0, а границы на графике при этом рисовались — со стороны выглядит как «нет сигнала», хотя не задан способ подсчёта. Теперь BuildRoiConfig создаёт примитив BG difference / Addition тем же способом, что ROIConfigForm.button3_Click, границы примитив берёт из самой области. На живом прогоне: 38 областей из 38 с примитивом, границы совпадают с зоной во всех.

Ваша оговорка про штатную кнопку «Nuclide → Added» верна, и мы её не трогали: ROIConfigForm.button9_Click тоже не создаёт примитивов, так что мастер эту дыру наследовал, а не внёс. Если решите закрыть и там — рецепт тот же, InitFromDefinition подтягивает границы сам.

Отдельно: первая версия этой правки была неверной, и её поймал живой прогон. В режиме маркеров границы области равны -10 (признак «зоны нет»), и примитив с таким интервалом не считает ничего, а EnergyToChannel(-10) способен выбросить OutofChannelException. То есть честное «примитива нет» подменялось на «примитив есть, но пустой» — стало хуже, чем было. Сейчас в маркерах примитив не создаётся вовсе, а строка состояния прямо говорит, сколько областей не измеряют площадь, вместо голого числа областей.

D4. Дубликаты в библиотеке. AddRange заменён на переиспользование существующей записи по (имя, энергия): Sets — это HashSet<Guid>, одна запись законно принадлежит нескольким наборам, чем мы и пользуемся. Якорный флаг только поднимается, никогда не снимается, иначе сломался бы чужой набор, которому та же запись служит якорем; цвет, видимость и интенсивность существующей записи не трогаются. Проверено семью прогонами подряд: 138 записей неизменно, дублей ноль, наборов 7. До правки пятый прогон дал бы под 300 записей с копиями по четыре штуки.

D6. Галки типов работали как видимость. Ваш пример воспроизвёлся полностью, включая побочный эффект: именно поэтому проверка данных ругалась на Pb-212 X KpB1 — на линию, которой на экране не было. Сделали «вижу = получаю»: скрытый тип не уходит ни в ROI, ни в набор, ни в предпросмотр, ни в проверки, а счётчик считает видимые линии и в числителе, и в знаменателе. Живой прогон: с снятыми галками в файле 38 гамма-областей, ни одной X, ни одной XRF — и то же в наборе.

D9. Смешанный пресет. Добавили замечание MixedChains: цепочки читаются ровно так, как их читает ChainOf — по тексту в последних скобках, — поэтому набор из независимых нуклидов (Cs-137 + Co-60) под него не попадает, а Th-232 + U-238 в одном наборе попадает. Пресеты теперь заменяют выбор, а не доливают к нему.

Чтобы это замечание вообще было видно, пришлось закрыть и D5: RunChecks фильтровал замечания набора по Error, и диалог поднимался только на ошибках, так что равные энергии, рентгеновский якорь и смешанные цепочки не доходили до оператора, хотя SetChecker выдаёт их предупреждением намеренно. Проверка, которой не видно, — не проверка.

Плюс дефект, которого в отчёте не было, нашёлся при живом прогоне: объединение близких линий было режимом, притворявшимся разовым действием. Rebuild обнулял beforeMerge, поэтому любая галка фильтра молча отменяла слияние, а кнопка выглядела как прежде. Теперь объединение переживает пересборку, переприменяется при смене критерия или множителя, и снимается только кнопкой «Вернуть исходные».

Проверки. tools/RoiWizardCheck/CatalogCheck.cs подрос на 14 утверждений: примитивы и их границы, режим маркеров (примитивов быть не должно), смешанные цепочки в обе стороны — и что ложной тревоги на независимых нуклидах нет, — и слияние библиотеки на повторных прогонах. Всё зелёное, код возврата 0.

По D3 (справка) согласны — она описывает веб-версию по каждому второму абзацу. Это наш след: help.xml выгружался из страницы и после переработки под WinForms не перевыгружался. Перепишем отдельно, вместе с оставшейся мелочью из отчёта по UX.

@Verter73

Copy link
Copy Markdown
Collaborator Author

Закрыли D3 — справку: коммит d80c7ee в той же ветке fix/roi-wizard-defects.

Ваш разбор оказался верен по всем девяти строкам таблицы, и причина ровно та, что вы назвали: help.xml выгружался из index.html веб-версии скриптом tools/export_help.py и после переработки под WinForms не перевыгружался. Общий источник ставился как раз для того, чтобы два текста не разъехались, — а вышло наоборот: разъехались всё равно, и никто этого не замечал, пока выгрузку не повторяли. Так что справка модуля теперь ведётся отдельно, генератором рядом с gen_strings.py, и её надо править вместе с поведением.

Переписана под фактическое состояние, включая то, что изменилось предыдущим коммитом:

  • источник линий — встроенная nucdb.sqlite, только для чтения, обращений в сеть нет (было «снимок data/nuclides.js, обновляется update_nuclides.py, API не отдаёт CORS»);
  • «Создать конфигурацию ROI» пишет через ROIConfigManager и запись сразу видна в списке, файл никуда не «скачивается»; имя конфигурации становится именем файла, совпадение имён перезапишет чужой;
  • «Добавить набор в библиотеку» дописывает на месте — ни диалога выбора файла, ни резервной копии, как справка обещала;
  • якорей несколько, сколько задаёт счётчик, а не «ровно один»: фиту достаточно совпадения с любым, единственный якорь делает набор хрупким;
  • скрытый галкой тип линий не попадает ни в конфигурацию, ни в набор — старая формулировка «галки типов управляют только видимостью» стала неверной после правки D6;
  • объединение близких линий описано как режим, который переживает смену фильтров;
  • отдельный абзац о том, что именно измеряет область: в зонах примитив BG difference с границами зоны, в маркерах зоны нет по определению и площадь не считается, о чём говорит и строка состояния;
  • проверки: ошибки блокируют сохранение, предупреждения спрашивают; про смешанные цепочки сказано прямо, вместе с правилом «один набор — одна цепочка»;
  • убрано «инструмент неофициальный и частью BecqMoni не является» — он в меню приложения.

Заодно три места из отчёта по интерфейсу, где смысл не читался с подписи:

  • поле R принимает два знака вместо одного. У HPGe с R = 0,22 % округление до 0,2 занижало ширину пика на 9 % — модель разрешения и без того приблизительная, поле не должно добавлять к её погрешности своей;
  • галка полный набор получила подсказку, что относится только к набору, а ROI-конфигурация всегда строится по таблице (в общей рамке с кнопкой создания это неочевидно);
  • пара «выбор якоря / счётчик якорей»: вы отметили, что гашение выглядит наоборот. Поведение верное — при полном наборе гаснет ручной выбор, потому что якоря там выбираются автоматически, а счётчик как раз действует, — но по виду не отличить, поэтому у обоих теперь подсказки.

Остальное из отчёта по UX (разметка окна, сплиттеры, предпросмотр по вкладке, порядок в списке конфигураций, Dirty при выборе конфигурации) не трогали: там часть про хост, часть — вкусовое, и делать это без вас смысла нет. Скажите, что из этого стоит взять, — либо закроем всё скопом, либо оставим как есть.

@Am6er

Am6er commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Обе ваши правки забрал: 4275722 и d80c7ee влиты fast-forward'ом в roi-wizard-reworked, авторство сохранено. Проверил перед этим не только текст — собрал ветку в отдельном worktree (Debug/AnyCPU, 0 ошибок) и прогнал CatalogCheck (всё зелёное, код возврата 0, включая 14 новых утверждений). Диагнозы по D1/D4/D5/D6/D9 сверил по адресам: CalculateROI действительно обходит только roi.ROIPrimitives, примитив собирается ровно как в ROIConfigForm.button3_Click (поиск по имени, а не по индексу — это лучше оригинала), имена BG difference/Addition совпадают с ROIPrimitiveDefinition/ROIPrimitiveOperation, и выбор именно BG difference согласуется с поставкой: в config/ROI/* таких примитивов 56 из 70. Отказ создавать примитив в маркерах обоснован — CalculateROI ловит OutofChannelException через return false, то есть пустышка дала бы «невалидно» вместо честного «не настроено».

Одно возражение по существу — и оно оказалось входом в разговор побольше.

SetChecker.ChainOfLibraryPeakFitter.ChainOf. Комментарий обещает «читается ровно так, как её читает ChainOf», но настоящий ChainOf (LibraryPeakFitter.cs:949-962) при отсутствии скобок возвращает имя целиком, а ваша копия — null, и вдобавок требует ) последним символом. Для фиттера «Cs-137 + Co-60» — тоже две цепочки. Важнее следствие: якорный гейт — setLines.Any(n => n.IsAnchor) на весь набор (LibraryPeakFitter.cs:483), безотносительно цепочек, поэтому риск «один якорь сажает компоненты на всё» существует и для независимых нуклидов, а предупреждение его не покрывает. Решение не ругаться на «Cs-137 + Co-60» я считаю правильным — это нормальный способ пользоваться набором, — но это продуктовое решение, а не верность ChainOf, и подавать его надо так.

И ещё живой ложный срабатыш той же природы: SecondaryPeaks.cs:265 строит метки вида BS (Cs-137 662), которые кончаются на ). Обе функции прочитают из них «цепочку» Cs-137 662 — при том что комментарий в SetChecker прямо говорит, что вторичные в проверке не участвуют.


Дальше — мысли Amber

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

Общий диагноз у всех трёх один: величина, первичная по физике, хранится неявно — цепочка размазана по строке имени, вертикальные линии живут в неиспользуемых полях измерительной записи, эффективность свёрнута внутрь дозового коэффициента.

1. NuclideDefinition — отдельный атрибут идентификатора цепочки

Сейчас имя цепочки лежит в свободном формате в Name и никак не формализовано. Отсюда и разошедшиеся ChainOf.

Предлагаю необязательное поле, значение — корень ряда в каноничном виде (U-238, Th-232), а не машинный слаг. Причина: ровно это возвращает сегодняшний ChainOf(Name), поэтому новое поле и fallback дают одно и то же значение — их можно сверять между собой и валидировать, а не гадать, какое главнее. И такое поле человек может ввести руками в NuclideDefinitionForm без таблицы соответствий.

Появляется при формировании из roi-wizard/nucdb (там готовый ответ уже есть — CatalogNuclide.Chain и ChainRoot()), либо пользовательскими движениями в NuclideDefinitionForm. Используется везде, где логика требует привязки к имени цепочки.

Четыре условия, без которых поле сделает хуже, а не лучше:

  1. Скобки из Name не убирать. NuclideDefinitionFile не имеет FormatVersion, обработчиков UnknownElement нет — старая сборка новый файл прочитает молча, а при первом же сохранении из «Nuclide definitions» перепишет его и сотрёт цепочки у всех записей. Пока в ходу выпущенные сборки, поле обязано быть формализацией поверх имени, а не заменой ему: пусто ⇒ fallback на разбор скобок.
  2. ChainOf должен стать одной общей функциейChain ?? ChainOf(Name) — и использоваться из LibraryPeakFitter, SetChecker и мастера. Это и закрывает возражение выше.
  3. Ключ дедупликации. SetExporter.FindSameLine матчит по (Name, Energy ± 0.001). Как только цепочка перестанет быть частью имени, Bi-214 из U-238 и Bi-214 из Ra-226 схлопнутся в одну запись. Цепочка обязана войти в ключ.
  4. LineSetBuilder.cs:206 держит признак «линия входит в ряд» на сравнении label != nuclide.Name, и на этой строке висит пересчёт равновесия. Новое поле должно забрать эту роль, иначе конвенция останется несущей, просто в другом месте.

Заодно предлагаю добавить FormatVersion в NuclideDefinitionFile — его там нет вовсе, а он понадобится и здесь, и в миграции из пункта 2. Только с семантикой «отсутствует = legacy», а не жёстким гейтом, как в DeviceConfigManager.cs:82, иначе все существующие файлы уедут в ветку разбора старого формата.

2. Отрисовку одиночных вертикальных линий — из ROI в NuclideSet

Аргумент: ROI задуман как функционал расчёта активности, в нём всё заведено именно для этого. А вертикальным линиям с учётом интенсивности место как раз в NuclideSet.

Данные это подтверждают полностью. ShowROIReferencePeak читает ровно четыре поля — Enabled, PeakEnergy, Intencity, Color — и не трогает ни границы, ни примитивы. У всех четырёх есть близнец в NuclideDefinition (Visible, Energy, Intencity, NuclideColor), а Sets/NuclideSet уже дают группировку, которую сегодня изображает ROI-файл. Рендерер — сорок строк, фильтр буквально тот же, что в LibraryPeakFitter.cs:480. Не хватает только переключателя «рисовать этот набор» на NuclideSet рядом с HideUnknownPeaks.

Кривая эффективности при этом остаётся в ROI и никуда не мигрирует. Она нужна там для автозаполнения коэффициента пересчёта в активность, и привязана она к геометрии, а не к прибору: детектор один, а геометрий (и кривых) много. Из ROI уезжает только логика отрисовки.

Мины в миграции, которые надо назвать до начала работы:

  • ROI-конфиги нельзя удалять. ROIConfigReference.Guid зашит в сохранённые спектры (DocumentManager.PrepareROIConfig), и на Guid конфига же ссылается DeviceConfigInfo.EfficencyROIGuid. Удаление осиротит .bmn, а DCResultView разыменовывает ROIConfig без проверки на null. Миграция должна вычищать маркерные записи, оставляя файл, Guid, имя и ROIEfficiency.
  • Предикат «это маркер» по литералу -10 не годится. В поставке Th-232 Intensities.xml использует -100. Надёжный признак — ROIPrimitives.Count == 0 && Intencity > 0: ровно то, что RoiWizardForm уже считает для новой строки состояния.
  • Свежая установка — лёгкий случай, обновление — тяжёлый. MainForm.cs:96-124 копирует поставочные конфиги, только если каталога %AppData%\BecqMoni\config нет вовсе; на обновлении не копируется ничего и пользовательские файлы 2022 года живут вечно. Значит миграция — это рантайм-код, идемпотентный и привязанный к чему-то долговечному. То есть к FormatVersion из пункта 1.
  • В ROI нет IsAnchor. Мигрированный набор без якоря никогда не запустит LibraryPeakFitter (:483) — будет рисовать линии и молча не фититься. Якоря придётся проставлять AnchorPicker'ом. Это ровно тот класс дефекта, который мы весь этот PR и вылавливаем: работает на вид, не работает по делу.
  • Мелочь по дороге: ShowROIBorderLine на OutofChannelException делает break — то есть перестаёт рисовать все оставшиеся области, а не только проблемную. Вынос маркеров это чинит побочно, но break надо менять на continue независимо.
  • И вопрос UX: сегодня интенсивность и цвет маркера правятся в ROIConfigForm. После переноса это должно правиться в NuclideDefinitionForm/NuclideSetForm не хуже, иначе пользователь теряет рабочий сценарий.

3. Кривая эффективности в настройках Device — в явном виде, и смена главенства

Тезис: эффективность первична, доза — следствие. И, по итогам последних опытов, кривая эффективности теперь влияет на поиск пиков — как и должно быть.

Здесь самое интересное: арифметика уже такая, просто она спрятана. DeviceConfigForm.CalculateDoseRateConfig (:2522-2577) считает

Sensitivity = doseRateCoeff · µ_en(E) · (R→Sv)(E) · E / ε(E),   при CPS = 1

то есть хранимая дозовая калибровка и есть кривая эффективности, свёрнутая с ядром µ_en и отнормированная на одно эталонное измерение. Речь не о новой модели, а о том, чтобы перестать её прятать.

При этом в одной форме сейчас живут два независимых загрузчика LSRM-файла: транзиентный IInterpolation на вкладке Dose (private IInterpolation efficiencyCurve, нигде не сохраняется) и EfficencyROIGuid на вкладке References. Рядом висит // TODO: create shared method for LSRM file read. Одну и ту же кривую можно загрузить дважды в одну форму, и эти два пути ничего друг о друге не знают.

Что предлагаю по существу:

  • вкладка Dose перестаёт грузить свой одноразовый файл и берёт кривую, уже привязанную к устройству (EfficencyROIGuid), — то есть выбор геометрии становится входом, а дозовая таблица выходом;
  • константа нормировки doseRateCoeff становится явным хранимым полем. Сейчас её нет нигде: она размазана по каждому EtalonDoseRateValue. Без неё «доза как следствие» невоспроизводима задним числом;
  • вручную настроенные DoseRateCalibrationPoints должны продолжать побеждать расчётные, иначе показания тихо поедут на обновлении;
  • ядро (16 точек µ_en и R→Sv) переезжает из файла WinForms-формы куда-то, откуда его можно переиспользовать.

Отдельно про влияние на поиск пиков. Проводка уже есть: LibraryPeakFitter.Fit(..., ROIConfigData efficiencyConfig)EfficiencyShape → veto по согласованности цепочки, и BoundEfficiencyConfig защищает от того, чтобы в фит уехала чужая геометрия. Но привязка стала несущей, а её почти ни у кого нет: EfficencyROIGuid не проставлен ни в одном из 9 поставочных конфигов устройств, а DocEnergySpectrum при отсутствии привязки откатывается на ROIConfigList[0] — произвольную чужую геометрию. Раз кривая теперь влияет на результат, «привязки нет» должно быть видно в форме устройства, а не быть молчаливой нормой. Это тот же урок, что уже записан в журнале LibraryFitLab: молчаливая терпимость к отсутствующей кривой скрывала то, что она отсутствовала всегда.

Два ограничения, о которые тут легко споткнуться:

  • DeviceConfigInfo.FormatVersion не бампать. DeviceConfigManager.cs:82 любое значение кроме "120920" уводит в _097b-десериализатор. Необязательный элемент добавляется без бампа — так в своё время и добавили EfficencyROIGuid.
  • Опечатку Efficency не чинить. Это имя XML-элемента; переименование молча оборвёт связь у тех устройств, где он уже проставлен.

Что хочу решить совместно

  1. Цепочка: согласны ли на «необязательное поле + fallback на скобки, скобки в Name остаются»? Или видите вариант, где имя можно чистить сразу — с учётом того, что старая сборка при сохранении файл перепишет?
  2. FormatVersion в NuclideDefinitionFile: вводим сейчас, вместе с полем цепочки? Мне он нужен как ключ идемпотентности для миграции маркеров.
  3. Маркеры: переключатель отрисовки — на NuclideSet (набор целиком) или на NuclideDefinitionVisible уже есть такой смысл)? И согласны ли с предикатом ROIPrimitives.Count == 0 && Intencity > 0 вместо литерала границ?
  4. Dose: согласны, что вкладка Dose должна брать привязанную кривую вместо своего загрузчика, и что doseRateCoeff надо хранить явно? Здесь у вас больше опыта с аттестацией, чем у меня, и если я упускаю причину, по которой одноразовая загрузка на вкладке Dose лучше — скажите.

Порядок предлагаю такой: цепочка → Dose/эффективность → маркеры. Маркеры последними, потому что их миграция трогает пользовательские файлы сильнее всех и выигрывает от FormatVersion, введённого первым шагом.

Каждый пункт — отдельная ветка и отдельный PR: этот и так вырос сверх всякой меры. И на каждый — свои утверждения в CatalogCheck, в первую очередь на миграцию: она из тех вещей, которые ломаются ровно один раз и у пользователя.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants