Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CODE_WALKTHROUGH.md
Original file line number Diff line number Diff line change
Expand Up @@ -1831,3 +1831,64 @@ Stage 3 специально держит это правило жёстким:
- cooldown не скрывает нарушение качества;
- journal пишет summary причин отбраковки;
- selected повторно проходит constraints.

## 22. Что добавил Stage 4

Stage 4 показывает связанную цепочку от прогноза гидроочистки к модельному
блендингу:

```text
Hydrotreater forecast
-> apply_hydrotreater_forecast()
-> calculate_mass_blend()
-> sulfur_constraint_status()
-> rank_feasible_blends()
```

### `source/ml/blending.py`

`HybridComponentForecast` описывает прогноз серы гидроочищенного компонента. В нём
есть point sulfur, upper sulfur, `source_state_id`, ссылка на входное качество и
диапазон транспортного лага.

`apply_hydrotreater_forecast()` заменяет только один компонент смеси. Сейчас это
компонент `A` в `hybrid_blend`.

`calculate_mass_blend()` считает серу смеси по массовым долям:

```text
S_mix = sum(w_i * S_i)
```

Эта же формула применяется к `upper`, но это не статистический доверительный
интервал смеси. Это консервативная сценарная граница.

`sulfur_constraint_status()` возвращает:

- `pass`, если upper sulfur доступна и не выше лимита;
- `fail`, если нарушена сера или запас компонента;
- `unknown`, если upper sulfur отсутствует.

`rank_feasible_blends()` сначала отбрасывает infeasible рецептуры, потом сортирует
оставшиеся по `(risk, -throughput, cost, change_size, id)`.

### Gas Context

`collect_gas_context()` берёт из `config/tags.csv` газовые теги `ht:F9`, `ht:F22`,
`ht:Q21` и возвращает их как `GasContextSignal`.

Важное ограничение: `GasContextSignal.action_enabled` всегда `False`. Эти сигналы
можно показывать как технологический контекст, но нельзя рекомендовать менять как
уставки, пока не подтверждены единицы, диапазоны и модель эффекта.

### `global_tests/test_stage4_blending.py`

Тесты Stage 4 проверяют:

- замену только нужного компонента прогнозом гидроочистки;
- массовый баланс point/upper sulfur;
- изменение feasible рецептур при изменении качества компонента;
- `UNKNOWN`, если нет upper sulfur;
- stock shortfall;
- запрет невалидных долей;
- context-only статус газовых тегов.
24 changes: 24 additions & 0 deletions PROJECT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1649,3 +1649,27 @@ Stage 3 делает backend-cycle строже. Он не добавляет ML
Если следующий этап будет добавлять ML, он должен встроиться в этот контур, а не
обходить его. ML может дать прогноз или оценку эффекта действия, но финальное
решение всё равно должно пройти `check_constraints()` и журналирование.

---

## 35. Что добавляет Stage 4

Stage 4 связывает прогноз гидроочистки с модельным блендингом.

Практически это означает:

- прогноз серы гидроочищенного компонента заменяет компонент `A`;
- рецептуры пересчитываются по массовому балансу;
- верхняя граница серы смешивается как консервативная сценарная граница;
- рецепты с нарушением серы, отсутствующей uncertainty или нехваткой запаса не
попадают в ranking;
- `T95`, цетановое число и полный паспорт товарного продукта остаются
`not_assessed`.

Газовые теги `ht:F9`, `ht:F22`, `ht:Q21` теперь зафиксированы как gas context.
Это наблюдаемые технологические сигналы, а не безопасные controls. Их нельзя
рекомендовать менять, пока нет подтверждённых единиц, инженерных диапазонов, max step
и модели эффекта действия.

В коде Stage 4 живёт в `source/ml/blending.py`, а regression-тесты - в
`global_tests/test_stage4_blending.py`.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Stage 1](STAGE1.md) — первый сквозной backend-цикл, demo-сценарии, журнал и ограничения этапа.
- [Stage 2](STAGE2.md) — как оригинальные материалы превращаются в prepared dataset и `ProcessState`.
- [Stage 3](STAGE3.md) — hard constraints, причины отбраковки кандидатов, materiality и cooldown.
- [Stage 4](STAGE4.md) — hybrid chain, model blending и газовые теги как context-only сигналы.
- [Code walkthrough](CODE_WALKTHROUGH.md) — папки, файлы и хронология вызовов почти построчно.
- [ML system design](DESIGN.md#8-ml-неопределённость-и-модель-последствий) — обучение, метрики, анализ ошибок и жизненный цикл модели; общие контракты и данные описаны в том же документе.
- [Материалы задания](materials/README.md) — ТЗ, схемы и исходные данные.
Expand All @@ -27,12 +28,19 @@
`ProcessState` для сценария `history`;
- backend-срез stage 3: более строгий выбор `hold`/`recommend`/`abstain`, единые
hard checks, причины отбраковки кандидатов, повторная проверка selected и trace
в журнале.
в журнале;
- stage 4: модельная связка гидроочистка -> блендинг, массовый баланс рецептур и
gas context для `ht:F9`, `ht:F22`, `ht:Q21` без включения реального управления газом.

Полноценной ML-модели, промышленного управления реальными уставками и Streamlit UI пока
нет. Текущий backend уже умеет готовить данные и запускать безопасный demo-каркас с
guardrails, но не выдаёт промышленную рекомендацию на реальной истории.

Stage 4 показывает связанную цепочку и модельный блендинг. Газовые теги видны как
технологический контекст, но не становятся action controls: нет подтверждённых единиц,
диапазонов и модели эффекта. Полная товарная спецификация также не заявлена:
`T95`, цетановое число и весь паспорт продукта пока `not_assessed`.

## Проверка

Нужны Python 3.11, Git LFS и `tar` с поддержкой RAR.
Expand Down
83 changes: 60 additions & 23 deletions STAGE4.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,82 @@
# Stage 4: Гибридная цепочка и блендинг
# Stage 4: Hybrid Chain, Blending, Gas Context

## Результат

Добавлен изменяемый hybrid-сценарий: прогноз серы гидроочищенного компонента
заменяет компонент `A`, после чего рецептуры пересчитываются по массовому балансу.
Изменение входного прогноза меняет множество допустимых рецептур.
Stage 4 демонстрирует связанную цепочку:

```text
history / hydrotreater forecast -> blend component A -> mass-balance recipes -> ranked feasible blends
```

Прогноз серы гидроочищенного компонента заменяет компонент `A` в hybrid/blend
сценарии. После этого рецептуры пересчитываются по массовому балансу. Изменение
point/upper sulfur входного прогноза меняет множество допустимых рецептур.

Газовые теги гидроочистки теперь явно фиксируются как технологический контекст. Это
важно для понимания процесса, но Stage 4 не включает реальное управление газом:
нет подтверждённых единиц, инженерных диапазонов и модели эффекта действия.

## Реализовано

- `HybridComponentForecast` хранит point/upper sulfur, `source_state_id`, ссылку на
входное качество и явный диапазон транспортного лага.
- Сера смеси рассчитывается как `sum(w_i * S_i)`. Верхние границы смешиваются так
же, но помечены как консервативная сценарная граница без заявления о совместном
статистическом покрытии.
- Доли неотрицательны, обязаны содержать все компоненты и суммироваться в единицу.
- `apply_hydrotreater_forecast()` заменяет только соответствующий компонент смеси.
- Сера смеси считается как `sum(w_i * S_i)`.
- Верхние оценки серы смешиваются тем же массовым балансом, но помечаются как
консервативная сценарная граница без заявления о совместном статистическом покрытии.
- Доли рецептуры неотрицательные, содержат все компоненты и суммируются в единицу.
- Расход компонента проверяется против доступного запаса.
- Двухкомпонентный grid имеет изменяемый шаг, детерминированный порядок и явный
предел размера; усечение запрещено.
- Недопустимые по сере, uncertainty или запасу рецептуры отбрасываются до
ранжирования `(risk, -throughput, cost, change_size, id)`.
- В `config/scenarios/hybrid_blend.json` компонент `A` оставлен пустым до runtime
замены прогнозом — отсутствие данных не превращается в ноль.
- Недопустимые по сере, uncertainty или запасу рецептуры отбрасываются до ranking.
- Ranking использует только feasible варианты и стабильный ключ
`(risk, -throughput, cost, change_size, id)`.
- Grid рецептур детерминированный, с явным пределом размера; silent truncation запрещён.
- `GasContextSignal` и `collect_gas_context()` возвращают газовые теги как context-only,
а не как action controls.

## Gas Context

Stage 4 отслеживает следующие газовые сигналы из `config/tags.csv`:

| Signal | Meaning | Stage 4 status |
| --- | --- | --- |
| `ht:F9` | газ поддува / массовый расход | context only |
| `ht:F22` | газ поддува / связанный технологический сигнал | context only |
| `ht:Q21` | газ поддува | context only |

Для всех этих сигналов:

- `action_enabled=false`;
- причина фиксируется как `context_only`;
- они не попадают в `ScenarioConfig.controls`;
- backend не рекомендует менять их значения.

Если позже эксперт подтвердит единицы, диапазоны, max step и модель эффекта, газ можно
будет рассматривать как отдельный action/control этап. Сейчас это только наблюдаемый
контекст процесса.

## Честные границы
## Честные Границы

- Связь качества АВТ с подачей на гидроочистку и лаг `0–180` минут пока являются
- Связь качества АВТ с подачей на гидроочистку и лаг `0-180` минут остаются
модельным предположением: подтверждённого flow mapping нет.
- Реальная история рецептур и запасов не предоставлена, поэтому блендинг остаётся
- Реальная история рецептур и запасов не предоставлена, поэтому blending остаётся
модельным сценарием.
- Проверяются только сера и запас. `T95` и цетановое число обязательны для товарного
продукта, но не смешиваются линейно без физического или эмпирического основания.
Результат всегда имеет `full_specification_status="not_assessed"`.
- Проверяются только сера и запас компонента.
- `T95`, цетановое число и полная товарная спецификация не оценены:
`full_specification_status="not_assessed"`.
- Отсутствующая верхняя граница серы даёт `UNKNOWN`, а не `PASS`.
- Газовые сигналы не являются безопасными уставками.

## Проверка

```powershell
.\.venv\Scripts\python.exe -m pytest global_tests\test_stage4_blending.py -q
.\.venv\Scripts\ruff.exe check source\ml\blending.py global_tests\test_stage4_blending.py
.\.venv\Scripts\mypy.exe source\ml\blending.py
.\.venv\Scripts\python.exe -m pytest global_tests\test_stage3_guardrails.py -q
.\.venv\Scripts\python.exe -m pytest
.\.venv\Scripts\python.exe -m ruff check .
.\.venv\Scripts\python.exe -m mypy source
.\.venv\Scripts\python.exe -m source.main validate-stage0
```

Acceptance-тесты покрывают provenance, массовый баланс point/upper, изменение
допустимого множества рецептур, `UNKNOWN` при отсутствующей upper-границе, запасы,
невалидные доли и отсутствие ложного заявления о полной спецификации.
невалидные доли, ranking только feasible вариантов и context-only статус газовых тегов.
17 changes: 16 additions & 1 deletion global_tests/test_stage4_blending.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

import pytest

from source.config import load_scenario
from source.config import load_scenario, load_tag_dictionary
from source.contracts import ConstraintStatus, EstimateBasis, IntervalKind, MetricEstimate
from source.ml.blending import (
GAS_CONTEXT_REASON,
GAS_CONTEXT_SIGNAL_IDS,
HybridComponentForecast,
apply_hydrotreater_forecast,
calculate_mass_blend,
collect_gas_context,
enumerate_two_component_recipes,
rank_feasible_blends,
sulfur_constraint_status,
Expand Down Expand Up @@ -117,3 +120,15 @@ def test_stock_and_recipe_validation_are_enforced() -> None:
calculate_mass_blend({"A": 0.8, "B": 0.1}, components, 100.0)
with pytest.raises(ValueError, match="nonnegative"):
calculate_mass_blend({"A": 1.1, "B": -0.1}, components, 100.0)


def test_gas_tags_are_context_only_not_action_controls() -> None:
tags = load_tag_dictionary("config/tags.csv")

context = collect_gas_context(tags)

assert tuple(item.signal_id for item in context) == GAS_CONTEXT_SIGNAL_IDS
assert all(not item.action_enabled for item in context)
assert all(item.reason == GAS_CONTEXT_REASON for item in context)
assert all(tags[item.signal_id].controllable is False for item in context)
assert {item.unit for item in context} == {"unknown"}
Loading
Loading