imbue2025 commited on
Commit
6d41b6a
·
verified ·
1 Parent(s): 44c687d

Create infra.py

Browse files
Files changed (1) hide show
  1. infra.py +124 -0
infra.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import numpy as np
6
+ import pandas as pd
7
+ from safetensors.torch import load_file
8
+ from typing import List, Tuple, Optional
9
+
10
+ class ServiceConfig:
11
+ MODEL_PATH = os.path.join(os.path.abspath(os.getcwd()), "lpr_oracle_ensemble.safetensors")
12
+ DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
13
+ NUM_MODELS = 5
14
+ LOOKBACK_DAYS = 60
15
+ DECISION_THRESHOLD = 0.9957
16
+ IN_CHANNELS = 2
17
+
18
+ class InceptionModule(nn.Module):
19
+ def __init__(self, in_channels: int, out_channels: int, kernel_sizes=[9, 19, 39], bottleneck_channels=32):
20
+ super().__init__()
21
+ self.use_bottleneck = in_channels > 1
22
+ if self.use_bottleneck:
23
+ self.bottleneck = nn.Conv1d(in_channels, bottleneck_channels, kernel_size=1, bias=False)
24
+ in_channels = bottleneck_channels
25
+ self.conv_layers = nn.ModuleList()
26
+ for k in kernel_sizes:
27
+ self.conv_layers.append(nn.Conv1d(in_channels, out_channels, kernel_size=k, padding=k//2, bias=False))
28
+ self.maxpool = nn.MaxPool1d(kernel_size=3, stride=1, padding=1)
29
+ self.conv_pool = nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False)
30
+ self.bn = nn.BatchNorm1d(out_channels * (len(kernel_sizes) + 1))
31
+ self.act = nn.ReLU()
32
+
33
+ def forward(self, x):
34
+ input_tensor = x
35
+ if self.use_bottleneck: x = self.bottleneck(x)
36
+ outputs = [layer(x)[:, :, :input_tensor.shape[2]] for layer in self.conv_layers]
37
+ pool_out = self.conv_pool(self.maxpool(x))[:, :, :input_tensor.shape[2]]
38
+ outputs.append(pool_out)
39
+ return self.act(self.bn(torch.cat(outputs, dim=1)))
40
+
41
+ class InceptionTimeNet(nn.Module):
42
+ def __init__(self, in_channels=2, num_classes=1):
43
+ super().__init__()
44
+ self.block1 = InceptionModule(in_channels, 32)
45
+ self.block2 = InceptionModule(32 * 4, 32)
46
+ self.shortcut = nn.Conv1d(in_channels, 32 * 4, kernel_size=1, bias=False)
47
+ self.bn_sc = nn.BatchNorm1d(32 * 4)
48
+ self.gap = nn.AdaptiveAvgPool1d(1)
49
+ self.fc = nn.Linear(32 * 4, num_classes)
50
+
51
+ def forward(self, x):
52
+ x = x.permute(0, 2, 1)
53
+ res = self.bn_sc(self.shortcut(x))
54
+ out = self.block2(self.block1(x))
55
+ out = F.relu(out + res)
56
+ return self.fc(self.gap(out).squeeze(-1))
57
+
58
+ class LPROracleService:
59
+ def __init__(self, model_path: str = ServiceConfig.MODEL_PATH):
60
+ self.device = ServiceConfig.DEVICE
61
+ self.models = self._load_ensemble(model_path)
62
+ print(f"LPR-Oracle Service Online. Device: {self.device}. Threshold: {ServiceConfig.DECISION_THRESHOLD}")
63
+
64
+ def _load_ensemble(self, path: str) -> List[nn.Module]:
65
+ if not os.path.exists(path):
66
+ raise FileNotFoundError(f"Weights not found at {path}")
67
+
68
+ print(f"Loading weights from {path}...")
69
+ full_state = load_file(path)
70
+ models = []
71
+
72
+ for i in range(ServiceConfig.NUM_MODELS):
73
+ model = InceptionTimeNet(in_channels=ServiceConfig.IN_CHANNELS).to(self.device)
74
+ prefix = f"m{i}."
75
+ model_state = {k[len(prefix):]: v for k, v in full_state.items() if k.startswith(prefix)}
76
+ model.load_state_dict(model_state)
77
+ model.eval()
78
+ models.append(model)
79
+
80
+ return models
81
+
82
+ def preprocess(self, sequence: np.ndarray) -> torch.Tensor:
83
+ if sequence.ndim == 2:
84
+ sequence = sequence[np.newaxis, ...]
85
+
86
+ _, L, C = sequence.shape
87
+ if L != ServiceConfig.LOOKBACK_DAYS or C != ServiceConfig.IN_CHANNELS:
88
+ raise ValueError(f"Input shape mismatch. Expected (N, {ServiceConfig.LOOKBACK_DAYS}, {ServiceConfig.IN_CHANNELS}), got {sequence.shape}")
89
+
90
+ return torch.FloatTensor(sequence).to(self.device)
91
+
92
+ def predict(self, sequence: np.ndarray) -> dict:
93
+ x = self.preprocess(sequence)
94
+ ensemble_probs = []
95
+
96
+ with torch.no_grad():
97
+ for model in self.models:
98
+ logits = model(x)
99
+ probs = torch.sigmoid(logits).cpu().numpy()
100
+ probs = 1.0 - probs
101
+ ensemble_probs.append(probs)
102
+
103
+ avg_prob = np.mean(ensemble_probs, axis=0)
104
+
105
+ is_change = avg_prob > ServiceConfig.DECISION_THRESHOLD
106
+
107
+ return {
108
+ "probability": float(avg_prob[0]),
109
+ "threshold": ServiceConfig.DECISION_THRESHOLD,
110
+ "alert": bool(is_change[0])
111
+ }
112
+
113
+ if __name__ == "__main__":
114
+ try:
115
+ service = LPROracleService()
116
+
117
+ print("\nSimulating Incoming Data Stream")
118
+
119
+ mock_data_stable = np.random.randn(60, 2)
120
+ result_stable = service.predict(mock_data_stable)
121
+ print(f"Input: {result_stable}")
122
+
123
+ except Exception as e:
124
+ print(f"Service Error: {e}")