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 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
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
triage4-1.0.0/setup.py ADDED
@@ -0,0 +1,8 @@
1
+ """Compatibility shim for legacy setuptools workflows.
2
+
3
+ Packaging metadata is defined in pyproject.toml.
4
+ """
5
+
6
+ from setuptools import setup
7
+
8
+ setup()
@@ -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
+ )