Metadata-Version: 2.3
Name: trosna-gisteresis
Version: 0.1.0
Summary: Hysteresis decision rule with dwell time and cooldown period for streaming resource-imbalance indices
Keywords: hysteresis,dwell time,cooldown,debounce,virtualization,memory ballooning,KVM
Author: Andrey Ferubko
Author-email: Andrey Ferubko <ferubko1999@yandex.ru>
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Topic :: System :: Distributed Computing
Requires-Dist: numpy>=1.26
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# trosna-gisteresis

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

## Постановка

Пусть `I_t` — индекс неравномерности в точке мониторинга `t` (размах, коэффициент вариации,
индекс Джини долей свободной памяти; для индекса Джейна берут `1 − J`), `Δt_t` — интервал
между точками мониторинга. Параметры: верхний и нижний пороги `θ⁺ > θ⁻`, выдержка `T_d`,
период охлаждения `T_c`. Тогда

    d_t = (d_{t−1} + Δt_t) · [I_t > θ⁺]                         (время непрерывного превышения θ⁺)
    g_t = max{g_{t−1}, [I_t < θ⁻]}                               (флаг готовности)
    δ_t = 1  ⇔  I_t > θ⁺ ∧ d_t ≥ T_d ∧ g_t = 1 ∧ t − τ ≥ T_c     (решение)

где `[·]` равно 1 при истинном условии и 0 иначе, `τ` — момент последнего срабатывания.
После срабатывания `τ = t`, `g_t = 0`, `d_t = 0`: повторное решение возможно только после
снижения индекса ниже `θ⁻` и не раньше, чем через `T_c`.

* **Выдержка** отсекает кратковременные выбросы: решение принимается, только если дисбаланс
  держится не меньше `T_d`.
* **Два порога** не дают решению «дребезжать», пока индекс колеблется около одного порога.
* **Флаг готовности** не даёт повторять перераспределение, если ограничения не позволяют
  устранить дисбаланс (индекс остаётся выше `θ⁺`).
* **Период охлаждения** ограничивает частоту перераспределений сверху.

Необязательное расширение `rearm_after = T_r` взводит правило и по истечении времени `T_r`
после срабатывания:

    g_t = max{g_{t−1}, [I_t < θ⁻], [t − τ ≥ T_r]}

Без него правило блокируется, если перераспределение не опустило индекс ниже `θ⁻`
(например, решение было принято во время кратковременного выброса и после его окончания
распределение памяти оказалось перекошенным).

Время задаётся в секундах (или любых единицах, одинаковых для `t`, `T_d`, `T_c`), поэтому
правило корректно работает при переменном интервале мониторинга. Сложность — `O(1)` на точку.

## Использование

```python
import numpy as np
from trosna_gisteresis import Hysteresis, hysteresis

# Ряд целиком: индекс и отметки времени (здесь — мониторинг раз в 5 с).
t = np.arange(0.0, 600.0, 5.0)
index = np.where((t >= 100) & (t < 300), 0.5, 0.1)
trace = hysteresis(index, t, upper=0.35, lower=0.25, dwell=30.0, cooldown=120.0)
# Выдержка отсчитывается от последней точки ниже θ⁺ (t = 95), поэтому решение — в t = 125.
print(trace.fire_times)  # [125.]

# Поток: по одной точке мониторинга.
rule = Hysteresis(upper=0.35, lower=0.25, dwell=30.0, cooldown=120.0)
for t_i, index_i in [(0, 0.5), (10, 0.5), (20, 0.5), (30, 0.5)]:
    step = rule.step(index_i, t_i)
    if step.fired:
        print(f"перераспределить в t = {step.t}")  # перераспределить в t = 30.0

# Состояние можно сохранить и продолжить после перезапуска управляющей системы.
saved = rule.state
rule = Hysteresis(rule.params, state=saved)
```

`hysteresis` возвращает `Trace` с массивами `fired` (`δ_t`), `dwell` (`d_t`), `armed` (`g_t`),
`since_fired` (`t − τ`) — значения, по которым проверялось условие решения, — и итоговым
состоянием `state`. Без `t` точки считаются идущими с шагом 1.

### Параметры

| Параметр | Обозначение | Смысл |
|---|---|---|
| `upper` | `θ⁺` | порог значимого дисбаланса |
| `lower` | `θ⁻` | порог, ниже которого правило снова взводится, `θ⁻ < θ⁺` |
| `dwell` | `T_d` | выдержка, в единицах времени |
| `cooldown` | `T_c` | период охлаждения, в единицах времени |
| `dwell_from` | | `"interval"` (по умолчанию, как в формуле) — первая точка выше `θ⁺` засчитывает весь интервал от предыдущей; `"sample"` — отсчёт от первой точки выше `θ⁺` |
| `missing` | | пропуск (`NaN`): `"reset"` (по умолчанию) — выдержка обнуляется; `"hold"` — продолжает идти |
| `armed` | `g_0` | начальное значение флага готовности, по умолчанию `True` |
| `rearm_after` | `T_r` | взведение по времени после срабатывания; `None` (по умолчанию) — только по `θ⁻` |

Подробное обоснование доопределений, которых нет в исходной формулировке (начальные условия,
отсчёт выдержки, пропуски, `T_d = 0`, округление времени), — в [docs/design.md](docs/design.md).

## Разработка

```bash
uv sync                      # окружение
uv run pytest                # тесты (детерминированные, ~1 мин)
uv run ruff check . && uv run ruff format --check .
uv run python examples/demo.py
```

Тесты сравнивают реализацию с буквальной записью формул в точной арифметике на случайных
рядах (hypothesis с фиксированным порядком генерации), проверяют инварианты правила
(разнесение срабатываний не меньше `T_c`, обязательное снижение ниже `θ⁻` между
срабатываниями, непрерывность превышения на протяжении `T_d`, инвариантность к сдвигу и
масштабу времени), эквивалентность потоковой и пакетной обработки и сценарии с дребезгом,
короткоживущими процессами и переменным интервалом мониторинга.

Поддерживаются Python 3.11–3.14.
