triage4 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- triage4-1.0.0/LICENSE +21 -0
- triage4-1.0.0/MANIFEST.in +20 -0
- triage4-1.0.0/PKG-INFO +64 -0
- triage4-1.0.0/README.md +35 -0
- triage4-1.0.0/pyproject.toml +48 -0
- triage4-1.0.0/setup.cfg +4 -0
- triage4-1.0.0/setup.py +8 -0
- triage4-1.0.0/src/triage4/__init__.py +70 -0
- triage4-1.0.0/src/triage4/adaptive_token_bucket.py +61 -0
- triage4-1.0.0/src/triage4/alarm_rate_monitor.py +96 -0
- triage4-1.0.0/src/triage4/band_classifier.py +110 -0
- triage4-1.0.0/src/triage4/device_fair_queue.py +184 -0
- triage4-1.0.0/src/triage4/results.py +203 -0
- triage4-1.0.0/src/triage4/source_aware_queue.py +105 -0
- triage4-1.0.0/src/triage4/token_bucket.py +70 -0
- triage4-1.0.0/src/triage4/triage4_config.py +192 -0
- triage4-1.0.0/src/triage4/triage4_scheduler.py +502 -0
- triage4-1.0.0/src/triage4.egg-info/PKG-INFO +64 -0
- triage4-1.0.0/src/triage4.egg-info/SOURCES.txt +20 -0
- triage4-1.0.0/src/triage4.egg-info/dependency_links.txt +1 -0
- triage4-1.0.0/src/triage4.egg-info/requires.txt +13 -0
- triage4-1.0.0/src/triage4.egg-info/top_level.txt +1 -0
triage4-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laboratory of Emerging Smart Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
include setup.py
|
|
5
|
+
|
|
6
|
+
# Only ship core package sources
|
|
7
|
+
recursive-include src/triage4 *.py
|
|
8
|
+
|
|
9
|
+
# Never ship research/benchmark/test trees in sdist
|
|
10
|
+
prune tests
|
|
11
|
+
prune benchmarks
|
|
12
|
+
prune assessment
|
|
13
|
+
prune results
|
|
14
|
+
prune scripts
|
|
15
|
+
prune docs
|
|
16
|
+
prune tmp
|
|
17
|
+
|
|
18
|
+
global-exclude __pycache__
|
|
19
|
+
global-exclude *.py[cod]
|
|
20
|
+
global-exclude .DS_Store
|
triage4-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triage4
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: TRIAGE/4 core scheduling package for priority-aware IoT message serving
|
|
5
|
+
Author: TRIAGE/4 Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy>=1.24.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
21
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
23
|
+
Provides-Extra: research
|
|
24
|
+
Requires-Dist: matplotlib>=3.7.0; extra == "research"
|
|
25
|
+
Requires-Dist: scipy>=1.10.0; extra == "research"
|
|
26
|
+
Requires-Dist: pandas>=2.0.0; extra == "research"
|
|
27
|
+
Requires-Dist: tqdm>=4.67.1; extra == "research"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# triage4
|
|
31
|
+
|
|
32
|
+
Core TRIAGE/4 scheduling package for priority-aware IoT message serving.
|
|
33
|
+
|
|
34
|
+
## Package Scope
|
|
35
|
+
|
|
36
|
+
- Distributable package code lives in `src/triage4`.
|
|
37
|
+
- Research and benchmarking code lives in `assessment/` and is intentionally non-distribution.
|
|
38
|
+
- Compatibility modules under `src/schedulers` and `src/vanilla` support repository workflows and are excluded from wheel packaging.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Install core package for runtime use:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Install research dependencies for local assessment/benchmark execution:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e ".[research]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Install development + research dependencies:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e ".[dev,research]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Packaging Guarantees
|
|
61
|
+
|
|
62
|
+
- Project distribution name: `triage4`
|
|
63
|
+
- Published wheel content: `triage4/*` modules and package metadata only
|
|
64
|
+
- `assessment/`, `tests/`, and benchmark artifacts are not shipped in distribution files
|
triage4-1.0.0/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# triage4
|
|
2
|
+
|
|
3
|
+
Core TRIAGE/4 scheduling package for priority-aware IoT message serving.
|
|
4
|
+
|
|
5
|
+
## Package Scope
|
|
6
|
+
|
|
7
|
+
- Distributable package code lives in `src/triage4`.
|
|
8
|
+
- Research and benchmarking code lives in `assessment/` and is intentionally non-distribution.
|
|
9
|
+
- Compatibility modules under `src/schedulers` and `src/vanilla` support repository workflows and are excluded from wheel packaging.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Install core package for runtime use:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Install research dependencies for local assessment/benchmark execution:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -e ".[research]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install development + research dependencies:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev,research]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Packaging Guarantees
|
|
32
|
+
|
|
33
|
+
- Project distribution name: `triage4`
|
|
34
|
+
- Published wheel content: `triage4/*` modules and package metadata only
|
|
35
|
+
- `assessment/`, `tests/`, and benchmark artifacts are not shipped in distribution files
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "triage4"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "TRIAGE/4 core scheduling package for priority-aware IoT message serving"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "TRIAGE/4 Contributors" }
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.24.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.4.0",
|
|
31
|
+
"pytest-cov>=4.1.0",
|
|
32
|
+
"black>=23.0.0",
|
|
33
|
+
"mypy>=1.5.0",
|
|
34
|
+
]
|
|
35
|
+
research = [
|
|
36
|
+
"matplotlib>=3.7.0",
|
|
37
|
+
"scipy>=1.10.0",
|
|
38
|
+
"pandas>=2.0.0",
|
|
39
|
+
"tqdm>=4.67.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
include-package-data = false
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
include = ["triage4*"]
|
|
48
|
+
exclude = ["tests*", "benchmarks*", "assessment*"]
|
triage4-1.0.0/setup.cfg
ADDED
triage4-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tiered Resource Allocation for IoT Alarm and Geographic-priority Emergency (TRIAGE/4).
|
|
3
|
+
|
|
4
|
+
Four-band hierarchical scheduler that resolves priority inversion by
|
|
5
|
+
separating semantic urgency (alarms) from geographic priority (zones).
|
|
6
|
+
|
|
7
|
+
Main components:
|
|
8
|
+
- TRIAGE4Scheduler: Main scheduler with discrete-event simulation
|
|
9
|
+
- TRIAGE4Config: Configuration dataclass with band thresholds and token parameters
|
|
10
|
+
- DeviceFairQueue: Per-device round-robin queue for fairness
|
|
11
|
+
- BandClassifier: Message-to-band classification logic
|
|
12
|
+
|
|
13
|
+
Band hierarchy:
|
|
14
|
+
0. ALARM: Emergency messages, strict priority
|
|
15
|
+
1. HIGH: High-priority zones, token-constrained
|
|
16
|
+
2. STANDARD: Standard zones, token-constrained
|
|
17
|
+
3. BACKGROUND: Low-priority zones, token-constrained
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> from triage4 import TRIAGE4Scheduler, TRIAGE4Config
|
|
21
|
+
>>> config = TRIAGE4Config(
|
|
22
|
+
... high_zone_max=1,
|
|
23
|
+
... standard_zone_max=3,
|
|
24
|
+
... high_token_budget=10,
|
|
25
|
+
... service_rate=20.0
|
|
26
|
+
... )
|
|
27
|
+
>>> scheduler = TRIAGE4Scheduler(config)
|
|
28
|
+
>>> result = scheduler.schedule(
|
|
29
|
+
... arrival_times=[0.0, 0.1, 0.2],
|
|
30
|
+
... device_ids=["sensor_1", "sensor_2", "sensor_1"],
|
|
31
|
+
... zone_priorities=[0, 5, 2],
|
|
32
|
+
... is_alarm=[False, True, False]
|
|
33
|
+
... )
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from .band_classifier import (
|
|
37
|
+
BAND_ALARM,
|
|
38
|
+
BAND_BACKGROUND,
|
|
39
|
+
BAND_HIGH,
|
|
40
|
+
BAND_STANDARD,
|
|
41
|
+
BandClassifier,
|
|
42
|
+
)
|
|
43
|
+
from .adaptive_token_bucket import AdaptiveTokenBucket
|
|
44
|
+
from .alarm_rate_monitor import AlarmRateMonitor
|
|
45
|
+
from .device_fair_queue import DeviceFairQueue
|
|
46
|
+
from .triage4_config import TRIAGE4Config, create_triage4_custom, create_triage4_default
|
|
47
|
+
from .triage4_scheduler import TRIAGE4Scheduler
|
|
48
|
+
from .source_aware_queue import SourceAwareQueue
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Main scheduler
|
|
52
|
+
"TRIAGE4Scheduler",
|
|
53
|
+
# Configuration
|
|
54
|
+
"TRIAGE4Config",
|
|
55
|
+
"create_triage4_default",
|
|
56
|
+
"create_triage4_custom",
|
|
57
|
+
# Components
|
|
58
|
+
"BandClassifier",
|
|
59
|
+
"DeviceFairQueue",
|
|
60
|
+
"AdaptiveTokenBucket",
|
|
61
|
+
"AlarmRateMonitor",
|
|
62
|
+
"SourceAwareQueue",
|
|
63
|
+
# Band constants
|
|
64
|
+
"BAND_ALARM",
|
|
65
|
+
"BAND_HIGH",
|
|
66
|
+
"BAND_STANDARD",
|
|
67
|
+
"BAND_BACKGROUND",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptive token bucket that activates only when alarm protection is enabled.
|
|
3
|
+
|
|
4
|
+
Wraps the standard TokenBucket and exposes activation/deactivation hooks with
|
|
5
|
+
hysteresis controlled by the caller (e.g., AlarmRateMonitor).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .token_bucket import TIME_TOLERANCE, TokenBucket
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AdaptiveTokenBucket:
|
|
12
|
+
"""Token bucket that can be activated/deactivated at runtime."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, budget: int, period: float, burst_capacity: int | None = None):
|
|
15
|
+
if budget <= 0:
|
|
16
|
+
raise ValueError("budget must be positive")
|
|
17
|
+
if period <= 0:
|
|
18
|
+
raise ValueError("period must be positive")
|
|
19
|
+
if burst_capacity is not None and burst_capacity < budget:
|
|
20
|
+
raise ValueError("burst_capacity must be >= budget")
|
|
21
|
+
|
|
22
|
+
self.bucket = TokenBucket(
|
|
23
|
+
budget=budget, period=period, burst_capacity=burst_capacity
|
|
24
|
+
)
|
|
25
|
+
self.active = False
|
|
26
|
+
|
|
27
|
+
def activate(self, current_time: float) -> None:
|
|
28
|
+
"""Enable rate limiting and realign refill schedule."""
|
|
29
|
+
if not self.active:
|
|
30
|
+
self.active = True
|
|
31
|
+
# Align next refill to current_time to avoid retroactive refills.
|
|
32
|
+
self.bucket.next_refill = current_time + self.bucket.period
|
|
33
|
+
self.bucket.tokens = self.bucket.max_capacity
|
|
34
|
+
|
|
35
|
+
def deactivate(self) -> None:
|
|
36
|
+
"""Disable rate limiting (bucket becomes pass-through)."""
|
|
37
|
+
self.active = False
|
|
38
|
+
|
|
39
|
+
def consume(self, current_time: float, amount: int = 1) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Consume tokens if active. Returns True if the request is allowed.
|
|
42
|
+
When inactive, always returns True (no limiting).
|
|
43
|
+
"""
|
|
44
|
+
if not self.active:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
self.bucket.refill(current_time)
|
|
48
|
+
return self.bucket.consume(amount)
|
|
49
|
+
|
|
50
|
+
def get_next_refill_time(self) -> float:
|
|
51
|
+
"""Expose next refill time for event scheduling."""
|
|
52
|
+
if not self.active:
|
|
53
|
+
# If inactive, never schedule on bucket events
|
|
54
|
+
return float("inf")
|
|
55
|
+
return self.bucket.get_next_refill_time()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def tokens(self) -> int:
|
|
59
|
+
"""Current token count (0 when inactive implies unlimited)."""
|
|
60
|
+
return self.bucket.tokens if self.active else -1
|
|
61
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sliding-window alarm rate monitor for adaptive protection.
|
|
3
|
+
|
|
4
|
+
Detects abnormal alarm rates using a simple windowed counter with hysteresis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections import deque
|
|
8
|
+
from typing import Deque, Tuple
|
|
9
|
+
|
|
10
|
+
from .token_bucket import TIME_TOLERANCE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AlarmRateMonitor:
|
|
14
|
+
"""Track alarm arrivals and detect abnormal rates."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
window_duration: float,
|
|
19
|
+
abnormal_threshold: float,
|
|
20
|
+
deactivation_threshold: float,
|
|
21
|
+
min_observations: int = 1,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Args:
|
|
25
|
+
window_duration: Sliding window size in seconds
|
|
26
|
+
abnormal_threshold: Alarms/sec that triggers protection
|
|
27
|
+
deactivation_threshold: Alarms/sec below which protection deactivates
|
|
28
|
+
min_observations: Minimum alarms before detection can trigger
|
|
29
|
+
"""
|
|
30
|
+
if window_duration <= 0:
|
|
31
|
+
raise ValueError("window_duration must be positive")
|
|
32
|
+
if abnormal_threshold <= 0:
|
|
33
|
+
raise ValueError("abnormal_threshold must be positive")
|
|
34
|
+
if deactivation_threshold < 0:
|
|
35
|
+
raise ValueError("deactivation_threshold must be non-negative")
|
|
36
|
+
if deactivation_threshold > abnormal_threshold:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"deactivation_threshold must be <= abnormal_threshold for hysteresis"
|
|
39
|
+
)
|
|
40
|
+
if min_observations <= 0:
|
|
41
|
+
raise ValueError("min_observations must be positive")
|
|
42
|
+
|
|
43
|
+
self.window_duration = float(window_duration)
|
|
44
|
+
self.abnormal_threshold = float(abnormal_threshold)
|
|
45
|
+
self.deactivation_threshold = float(deactivation_threshold)
|
|
46
|
+
self.min_observations = int(min_observations)
|
|
47
|
+
|
|
48
|
+
self._arrivals: Deque[Tuple[float, str]] = deque()
|
|
49
|
+
|
|
50
|
+
def record_arrival(self, timestamp: float, alarm_source: str) -> None:
|
|
51
|
+
"""Add an alarm arrival to the window."""
|
|
52
|
+
self._arrivals.append((timestamp, alarm_source))
|
|
53
|
+
self._prune(timestamp)
|
|
54
|
+
|
|
55
|
+
def get_rate(self, now: float | None = None) -> float:
|
|
56
|
+
"""Return alarms/sec over the current window."""
|
|
57
|
+
if now is None and self._arrivals:
|
|
58
|
+
now = self._arrivals[-1][0]
|
|
59
|
+
elif now is None:
|
|
60
|
+
return 0.0
|
|
61
|
+
|
|
62
|
+
self._prune(now)
|
|
63
|
+
if not self._arrivals:
|
|
64
|
+
return 0.0
|
|
65
|
+
|
|
66
|
+
window_span = max(self.window_duration, TIME_TOLERANCE)
|
|
67
|
+
return len(self._arrivals) / window_span
|
|
68
|
+
|
|
69
|
+
def is_abnormal(self, now: float | None = None) -> bool:
|
|
70
|
+
"""True if rate exceeds abnormal threshold with enough observations."""
|
|
71
|
+
if now is None and self._arrivals:
|
|
72
|
+
now = self._arrivals[-1][0]
|
|
73
|
+
elif now is None:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
self._prune(now)
|
|
77
|
+
if len(self._arrivals) < self.min_observations:
|
|
78
|
+
return False
|
|
79
|
+
return self.get_rate(now) >= self.abnormal_threshold - TIME_TOLERANCE
|
|
80
|
+
|
|
81
|
+
def is_recovered(self, now: float | None = None) -> bool:
|
|
82
|
+
"""True if rate has fallen below deactivation threshold."""
|
|
83
|
+
if now is None and self._arrivals:
|
|
84
|
+
now = self._arrivals[-1][0]
|
|
85
|
+
elif now is None:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
self._prune(now)
|
|
89
|
+
return self.get_rate(now) <= self.deactivation_threshold + TIME_TOLERANCE
|
|
90
|
+
|
|
91
|
+
def _prune(self, now: float) -> None:
|
|
92
|
+
"""Drop arrivals outside the sliding window."""
|
|
93
|
+
cutoff = now - self.window_duration - TIME_TOLERANCE
|
|
94
|
+
while self._arrivals and self._arrivals[0][0] < cutoff:
|
|
95
|
+
self._arrivals.popleft()
|
|
96
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Band classification logic for TRIAGE/4.
|
|
3
|
+
|
|
4
|
+
Maps messages to one of four bands based on semantic urgency (is_alarm)
|
|
5
|
+
and geographic priority (zone_priority).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Band constants
|
|
9
|
+
BAND_ALARM = 0 # Emergency messages, always served first
|
|
10
|
+
BAND_HIGH = 1 # High-priority zone telemetry, token-constrained
|
|
11
|
+
BAND_STANDARD = 2 # Standard zone telemetry, token-constrained
|
|
12
|
+
BAND_BACKGROUND = 3 # Low-priority zone data, best-effort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BandClassifier:
|
|
16
|
+
"""
|
|
17
|
+
Classifies messages into TRIAGE/4 bands.
|
|
18
|
+
|
|
19
|
+
Classification hierarchy:
|
|
20
|
+
1. Semantic urgency (is_alarm) overrides geographic priority
|
|
21
|
+
2. Geographic priority (zone_priority) determines band for non-alarms
|
|
22
|
+
|
|
23
|
+
Band assignment rules:
|
|
24
|
+
- is_alarm=True → ALARM (0) - regardless of zone priority
|
|
25
|
+
- zone_priority <= high_zone_max → HIGH (1)
|
|
26
|
+
- zone_priority <= standard_zone_max → STANDARD (2)
|
|
27
|
+
- zone_priority > standard_zone_max → BACKGROUND (3)
|
|
28
|
+
|
|
29
|
+
This separation resolves priority inversion where routine telemetry
|
|
30
|
+
from high-priority zones delays critical alarms from low-priority zones.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, high_zone_max: int, standard_zone_max: int):
|
|
34
|
+
"""
|
|
35
|
+
Initialize band classifier with zone thresholds.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
high_zone_max: Maximum zone priority for HIGH band (inclusive)
|
|
39
|
+
standard_zone_max: Maximum zone priority for STANDARD band (inclusive)
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> classifier = BandClassifier(high_zone_max=1, standard_zone_max=3)
|
|
43
|
+
>>> classifier.classify(zone_priority=0, is_alarm=False)
|
|
44
|
+
1 # HIGH band
|
|
45
|
+
>>> classifier.classify(zone_priority=5, is_alarm=True)
|
|
46
|
+
0 # ALARM band (semantic override)
|
|
47
|
+
"""
|
|
48
|
+
if high_zone_max < 0:
|
|
49
|
+
raise ValueError(f"high_zone_max must be non-negative, got {high_zone_max}")
|
|
50
|
+
if standard_zone_max < high_zone_max:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"standard_zone_max ({standard_zone_max}) must be >= "
|
|
53
|
+
f"high_zone_max ({high_zone_max})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.high_zone_max = high_zone_max
|
|
57
|
+
self.standard_zone_max = standard_zone_max
|
|
58
|
+
|
|
59
|
+
def classify(self, zone_priority: int, is_alarm: bool) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Classify message into TRIAGE/4 band.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
zone_priority: Geographic zone priority (0=highest priority zone)
|
|
65
|
+
is_alarm: Semantic urgency flag (True for emergency messages)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Band number: 0=ALARM, 1=HIGH, 2=STANDARD, 3=BACKGROUND
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If zone_priority is negative
|
|
72
|
+
"""
|
|
73
|
+
if zone_priority < 0:
|
|
74
|
+
raise ValueError(f"zone_priority must be non-negative, got {zone_priority}")
|
|
75
|
+
|
|
76
|
+
# Semantic urgency overrides geographic priority
|
|
77
|
+
if is_alarm:
|
|
78
|
+
return BAND_ALARM
|
|
79
|
+
|
|
80
|
+
# Geographic priority determines band for non-alarms
|
|
81
|
+
if zone_priority <= self.high_zone_max:
|
|
82
|
+
return BAND_HIGH
|
|
83
|
+
elif zone_priority <= self.standard_zone_max:
|
|
84
|
+
return BAND_STANDARD
|
|
85
|
+
else:
|
|
86
|
+
return BAND_BACKGROUND
|
|
87
|
+
|
|
88
|
+
def get_band_name(self, band: int) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Get human-readable band name.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
band: Band number (0-3)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Band name string
|
|
97
|
+
"""
|
|
98
|
+
names = {
|
|
99
|
+
BAND_ALARM: "ALARM",
|
|
100
|
+
BAND_HIGH: "HIGH",
|
|
101
|
+
BAND_STANDARD: "STANDARD",
|
|
102
|
+
BAND_BACKGROUND: "BACKGROUND",
|
|
103
|
+
}
|
|
104
|
+
return names.get(band, f"UNKNOWN({band})")
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return (
|
|
108
|
+
f"BandClassifier(high_zone_max={self.high_zone_max}, "
|
|
109
|
+
f"standard_zone_max={self.standard_zone_max})"
|
|
110
|
+
)
|