koios-model-utils 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,60 @@
1
+ """koios-model-utils — utilities for exporting and annotating ONNX models for the Koios IoT platform."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from koios_model_utils.enums import (
6
+ Algorithm,
7
+ ModelType,
8
+ NormalizationSource,
9
+ NormalizationType,
10
+ OutputMode,
11
+ )
12
+ from koios_model_utils.metadata import (
13
+ InputBinding,
14
+ OutputBinding,
15
+ TrainingMeta,
16
+ build_input_bindings,
17
+ build_output_bindings,
18
+ embed_koios_metadata,
19
+ )
20
+
21
+ # SB3-dependent exports — only available with koios-model-utils[sb3]
22
+ try:
23
+ from koios_model_utils.callbacks import (
24
+ ObsRangeCallback,
25
+ VecNormalizeSyncCallback,
26
+ )
27
+ from koios_model_utils.export import (
28
+ OnnxActorWrapper,
29
+ OnnxDQNWrapper,
30
+ detect_algorithm,
31
+ export_onnx,
32
+ load_vec_normalize_stats,
33
+ )
34
+ except ImportError:
35
+ pass
36
+
37
+ __all__ = [
38
+ # enums (always available)
39
+ "Algorithm",
40
+ "ModelType",
41
+ "NormalizationSource",
42
+ "NormalizationType",
43
+ "OutputMode",
44
+ # metadata (always available)
45
+ "InputBinding",
46
+ "OutputBinding",
47
+ "TrainingMeta",
48
+ "embed_koios_metadata",
49
+ "build_input_bindings",
50
+ "build_output_bindings",
51
+ # export (requires sb3 extra)
52
+ "export_onnx",
53
+ "detect_algorithm",
54
+ "load_vec_normalize_stats",
55
+ "OnnxActorWrapper",
56
+ "OnnxDQNWrapper",
57
+ # callbacks (requires sb3 extra)
58
+ "ObsRangeCallback",
59
+ "VecNormalizeSyncCallback",
60
+ ]
@@ -0,0 +1,147 @@
1
+ """SB3 training callbacks for Koios model export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+ from stable_baselines3.common.callbacks import BaseCallback
11
+
12
+ if TYPE_CHECKING:
13
+ from stable_baselines3.common.callbacks import EvalCallback
14
+
15
+
16
+ class ObsRangeCallback(BaseCallback):
17
+ """Track per-feature min/max of raw (pre-normalization) observations.
18
+
19
+ Saves ``obs_range.json`` to the run directory on training end, containing
20
+ the observed min/max per feature across all training steps. These are used
21
+ as failure bounds in Koios — if a live value falls outside the range the
22
+ model was trained on, the binding is flagged as untrusted.
23
+
24
+ Works with both raw envs and VecNormalize-wrapped envs. When VecNormalize
25
+ is present, the original (un-normalized) observations are tracked.
26
+ """
27
+
28
+ def __init__(self, run_dir: str | Path, verbose: int = 0) -> None:
29
+ super().__init__(verbose)
30
+ self._run_dir = Path(run_dir)
31
+ self._obs_min: np.ndarray | None = None
32
+ self._obs_max: np.ndarray | None = None
33
+
34
+ def _on_step(self) -> bool:
35
+ # Get raw observations — use original obs when normalization is active
36
+ obs = None
37
+
38
+ # Path 1: VecNormalize (z_score)
39
+ vec_norm = self.model._vec_normalize_env # type: ignore[union-attr]
40
+ if vec_norm is not None:
41
+ obs = vec_norm.get_original_obs()
42
+
43
+ # Path 2: MinMaxObsWrapper — walk wrapper chain for _last_original_obs
44
+ if obs is None:
45
+ env = self.training_env
46
+ # Unwrap through VecEnv layers to find the underlying env
47
+ while hasattr(env, "envs"):
48
+ env = env.envs[0] # type: ignore[union-attr]
49
+ # Walk Gymnasium wrapper chain
50
+ current = env
51
+ while current is not None:
52
+ if hasattr(current, "_last_original_obs"):
53
+ obs = current._last_original_obs
54
+ break
55
+ current = getattr(current, "env", None)
56
+
57
+ # Path 3: fallback to raw obs
58
+ if obs is None:
59
+ obs = self.locals.get("new_obs")
60
+
61
+ if obs is None:
62
+ return True
63
+
64
+ obs = np.asarray(obs, dtype=np.float32)
65
+
66
+ # Flatten to (n_envs, num_features) — collapse obs_depth if present
67
+ if obs.ndim == 3:
68
+ # (n_envs, obs_depth, num_features) → min/max across envs and depth
69
+ flat = obs.reshape(-1, obs.shape[-1])
70
+ elif obs.ndim == 2:
71
+ flat = obs
72
+ else:
73
+ flat = obs.reshape(1, -1)
74
+
75
+ step_min = flat.min(axis=0)
76
+ step_max = flat.max(axis=0)
77
+
78
+ if self._obs_min is None:
79
+ self._obs_min = step_min.copy()
80
+ self._obs_max = step_max.copy()
81
+ else:
82
+ np.minimum(self._obs_min, step_min, out=self._obs_min)
83
+ np.maximum(self._obs_max, step_max, out=self._obs_max)
84
+
85
+ return True
86
+
87
+ def _on_training_end(self) -> None:
88
+ if self._obs_min is None or self._obs_max is None:
89
+ return
90
+
91
+ out_path = self._run_dir / "obs_range.json"
92
+ data = {
93
+ "obs_min": self._obs_min.tolist(),
94
+ "obs_max": self._obs_max.tolist(),
95
+ }
96
+ with open(out_path, "w") as f:
97
+ json.dump(data, f, indent=2)
98
+
99
+ if self.verbose >= 1:
100
+ print(f"Observation ranges saved to {out_path}")
101
+ for i, (lo, hi) in enumerate(zip(self._obs_min, self._obs_max)):
102
+ print(f" feature {i}: [{lo:.4f}, {hi:.4f}]")
103
+
104
+
105
+ class VecNormalizeSyncCallback(BaseCallback):
106
+ """Save VecNormalize stats alongside every new best model.
107
+
108
+ SB3's ``EvalCallback`` saves ``best_model.zip`` when eval reward improves,
109
+ but doesn't save the VecNormalize running statistics. Without the stats
110
+ file, an early ``koios-export`` on ``best_model.zip`` produces an ONNX with
111
+ no z-score metadata — the PE then feeds raw observations to a model that
112
+ expects normalized ones.
113
+
114
+ This callback checks after each eval whether a new best model was saved
115
+ and, if so, writes ``vec_normalize.pkl`` to the same directory.
116
+ """
117
+
118
+ def __init__(self, eval_callback: EvalCallback, verbose: int = 0) -> None:
119
+ super().__init__(verbose)
120
+ self._eval_cb = eval_callback
121
+ self._last_best_timestep: int = -1
122
+
123
+ def _on_step(self) -> bool:
124
+ # EvalCallback sets best_mean_reward and n_calls when a new best is saved
125
+ best_ts = getattr(self._eval_cb, "best_mean_reward_timestep", None)
126
+ if best_ts is None:
127
+ # Fallback: check if best_model.zip was updated by comparing n_calls
128
+ best_ts = getattr(self._eval_cb, "n_calls", 0)
129
+
130
+ # Detect new best model by checking if EvalCallback updated its best
131
+ vec_norm = self.model._vec_normalize_env # type: ignore[union-attr]
132
+ if vec_norm is None:
133
+ return True
134
+
135
+ best_path = Path(self._eval_cb.best_model_save_path) / "best_model.zip"
136
+ stats_path = Path(self._eval_cb.best_model_save_path) / "vec_normalize.pkl"
137
+
138
+ # Save if best_model.zip exists and is newer than our last save
139
+ if best_path.exists():
140
+ mtime = best_path.stat().st_mtime_ns
141
+ if mtime != self._last_best_timestep:
142
+ self._last_best_timestep = mtime
143
+ vec_norm.save(str(stats_path))
144
+ if self.verbose >= 1:
145
+ print(f"[VecNormSync] Saved {stats_path}")
146
+
147
+ return True
@@ -0,0 +1,67 @@
1
+ """String enums for Koios metadata values.
2
+
3
+ Every enum here is a :class:`enum.StrEnum` — members are real strings and
4
+ serialize transparently through ``json.dumps``, so users may pass either
5
+ the enum member or the raw string literal wherever metadata values are
6
+ expected. The server consumes strings (case-insensitive for
7
+ normalization keys, exact-match for training keys), so both forms flow
8
+ through identically.
9
+
10
+ The string values match the names of the corresponding Django
11
+ ``IntegerChoices`` members in
12
+ ``koios-webapp-py/app/AiOpsInterface/apps/aiops/choices.py`` — renaming
13
+ a member on the server requires updating the matching member here (the
14
+ contract tests in ``tests/test_metadata.py`` pin this).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from enum import StrEnum
20
+
21
+
22
+ class NormalizationType(StrEnum):
23
+ """How a binding's value is mapped into / out of model space."""
24
+
25
+ NONE = "none"
26
+ MIN_MAX = "min_max"
27
+ SYMMETRIC = "symmetric"
28
+ Z_SCORE = "z_score"
29
+
30
+
31
+ class NormalizationSource(StrEnum):
32
+ """Where normalization parameters come from at runtime."""
33
+
34
+ # The tag's engineering range (range_min / range_max on the bound tag).
35
+ TAG_RANGE = "tag_range"
36
+ # Explicit ``custom_minimum`` / ``custom_maximum`` / ``custom_mean`` /
37
+ # ``custom_std`` values embedded in the ONNX metadata.
38
+ CUSTOM = "custom"
39
+
40
+
41
+ class ModelType(StrEnum):
42
+ """How the predict engine applies the model's output to the bound tag.
43
+
44
+ Maps to the server's ``OutputApplicationChoices``. The legacy
45
+ ``POSITIONAL`` / ``VELOCITY`` aliases are still accepted by the
46
+ server but should not be used in new code.
47
+ """
48
+
49
+ ABSOLUTE = "ABSOLUTE"
50
+ RELATIVE = "RELATIVE"
51
+
52
+
53
+ class OutputMode(StrEnum):
54
+ """Whether the model emits a continuous value or a categorical action."""
55
+
56
+ CONTINUOUS = "CONTINUOUS"
57
+ DISCRETE = "DISCRETE"
58
+
59
+
60
+ class Algorithm(StrEnum):
61
+ """Stable-Baselines3 algorithm identifiers recognized by the exporter."""
62
+
63
+ PPO = "PPO"
64
+ SAC = "SAC"
65
+ TD3 = "TD3"
66
+ A2C = "A2C"
67
+ DQN = "DQN"