muthuk1 commited on
Commit
2c41426
·
verified ·
1 Parent(s): 5199d5c

Upload inference_server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. inference_server.py +483 -0
inference_server.py ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ALWAS ML Inference Server
3
+ FastAPI-based inference API for all 4 ALWAS ML models.
4
+ Designed to replace the Groq API dependency and add predictive capabilities.
5
+ """
6
+ import os
7
+ import json
8
+ import numpy as np
9
+ import joblib
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional, List
12
+ from pydantic import BaseModel, Field
13
+
14
+ # === Load Models & Configs ===
15
+ MODEL_DIR = os.environ.get('MODEL_DIR', './models')
16
+
17
+ # Load all models
18
+ hours_model = joblib.load(f'{MODEL_DIR}/hours_estimator.joblib')
19
+ complexity_xgb = joblib.load(f'{MODEL_DIR}/complexity_xgb.joblib')
20
+ complexity_lgb = joblib.load(f'{MODEL_DIR}/complexity_lgb.joblib')
21
+ bottleneck_model = joblib.load(f'{MODEL_DIR}/bottleneck_predictor.joblib')
22
+ completion_model = joblib.load(f'{MODEL_DIR}/completion_predictor.joblib')
23
+
24
+ # Load encoders
25
+ tech_node_encoder = joblib.load(f'{MODEL_DIR}/tech_node_encoder.joblib')
26
+ block_type_encoder = joblib.load(f'{MODEL_DIR}/block_type_encoder.joblib')
27
+ priority_encoder = joblib.load(f'{MODEL_DIR}/priority_encoder.joblib')
28
+ complexity_encoder = joblib.load(f'{MODEL_DIR}/complexity_encoder.joblib')
29
+ bottleneck_encoder = joblib.load(f'{MODEL_DIR}/bottleneck_encoder.joblib')
30
+
31
+ # Load config
32
+ with open(f'{MODEL_DIR}/feature_config.json', 'r') as f:
33
+ feature_config = json.load(f)
34
+
35
+ with open(f'{MODEL_DIR}/metrics.json', 'r') as f:
36
+ model_metrics = json.load(f)
37
+
38
+ # === Pydantic Models ===
39
+ class BlockEstimateRequest(BaseModel):
40
+ """Request for complexity & hours estimation (replaces Groq API)."""
41
+ block_type: str = Field(..., description="Block type (e.g., 'ADC', 'PLL', 'LDO')")
42
+ tech_node: str = Field(..., description="Technology node (e.g., '7nm', '28nm')")
43
+ priority: str = Field(default='P3-Medium', description="Priority level")
44
+ transistor_count: Optional[int] = Field(default=None, description="Estimated transistor count")
45
+ has_dependencies: bool = Field(default=False)
46
+ num_dependencies: int = Field(default=0)
47
+ constraint_complexity: float = Field(default=1.0, ge=0, le=3.0)
48
+ drc_iterations: int = Field(default=2)
49
+ engineer_skill_factor: float = Field(default=1.0, ge=0.5, le=1.5)
50
+
51
+ class BottleneckRequest(BaseModel):
52
+ """Request for bottleneck risk prediction."""
53
+ block_type: str
54
+ tech_node: str
55
+ priority: str = 'P3-Medium'
56
+ transistor_count: Optional[int] = None
57
+ has_dependencies: bool = False
58
+ num_dependencies: int = 0
59
+ constraint_complexity: float = 1.0
60
+ estimated_hours: float = 20.0
61
+ hours_logged: float = 0.0
62
+ drc_iterations: int = 2
63
+ drc_violations_total: int = 0
64
+ lvs_mismatches_total: int = 0
65
+ current_stage: str = 'In Progress'
66
+ days_in_current_stage: float = 0.0
67
+ engineer_skill_factor: float = 1.0
68
+ is_overdue: bool = False
69
+
70
+ class CompletionRequest(BaseModel):
71
+ """Request for completion time prediction."""
72
+ block_type: str
73
+ tech_node: str
74
+ priority: str = 'P3-Medium'
75
+ transistor_count: Optional[int] = None
76
+ has_dependencies: bool = False
77
+ num_dependencies: int = 0
78
+ constraint_complexity: float = 1.0
79
+ estimated_hours: float = 20.0
80
+ engineer_skill_factor: float = 1.0
81
+ drc_iterations: int = 2
82
+ current_stage: str = 'In Progress'
83
+ cumulative_hours: float = 0.0
84
+ cumulative_days: float = 0.0
85
+ cumulative_drc_violations: int = 0
86
+ cumulative_lvs_mismatches: int = 0
87
+
88
+ class BulkBlockRequest(BaseModel):
89
+ """Bulk estimation for multiple blocks."""
90
+ blocks: List[BlockEstimateRequest]
91
+
92
+ # === Helper Functions ===
93
+ STAGES = ['Not Started', 'In Progress', 'DRC', 'LVS', 'ERC', 'Review', 'Completed']
94
+ STAGE_IDX = {s: i for i, s in enumerate(STAGES)}
95
+ PRIORITY_MAP = {'P1-Critical': 1, 'P2-High': 2, 'P3-Medium': 3, 'P4-Low': 4}
96
+
97
+ # Default transistor counts by block type
98
+ DEFAULT_TRANSISTOR_COUNTS = {
99
+ 'ADC': 50000, 'DAC': 35000, 'PLL': 80000, 'LDO': 8000, 'BGR': 5000,
100
+ 'OTA': 3000, 'Comparator': 2000, 'SerDes': 120000, 'VCO': 15000,
101
+ 'Mixer': 10000, 'LNA': 6000, 'PA': 20000, 'TIA': 4000, 'SampleHold': 3500,
102
+ 'LVDS_Driver': 8000, 'BandgapRef': 3000, 'CurrentMirror': 1500,
103
+ 'DiffAmp': 2500, 'Oscillator': 12000, 'PowerDetector': 5000
104
+ }
105
+
106
+ def safe_encode(encoder, value, default=0):
107
+ """Safely encode a value, returning default if unknown."""
108
+ try:
109
+ return encoder.transform([value])[0]
110
+ except (ValueError, KeyError):
111
+ return default
112
+
113
+ def safe_priority_encode(priority):
114
+ """Encode priority safely."""
115
+ try:
116
+ return priority_encoder.transform([[priority]])[0][0]
117
+ except:
118
+ return 2 # default to medium
119
+
120
+ def get_transistor_count(block_type, provided_count):
121
+ """Get transistor count, using default if not provided."""
122
+ if provided_count and provided_count > 0:
123
+ return provided_count
124
+ return DEFAULT_TRANSISTOR_COUNTS.get(block_type, 10000)
125
+
126
+
127
+ # === Prediction Functions ===
128
+ def predict_complexity_and_hours(req: BlockEstimateRequest):
129
+ """Predict complexity class and estimated hours for a new block."""
130
+ tc = get_transistor_count(req.block_type, req.transistor_count)
131
+ tc_log = np.log1p(tc)
132
+
133
+ tech_enc = safe_encode(tech_node_encoder, req.tech_node)
134
+ type_enc = safe_encode(block_type_encoder, req.block_type)
135
+ priority_enc = safe_priority_encode(req.priority)
136
+ priority_num = PRIORITY_MAP.get(req.priority, 3)
137
+
138
+ type_node_int = tech_enc * 10 + type_enc
139
+ complexity_score = req.constraint_complexity * tc_log
140
+ size_priority_int = tc_log * priority_num
141
+
142
+ # Hours estimation features
143
+ hours_features = np.array([[
144
+ tech_enc, type_enc, priority_enc, tc, tc_log,
145
+ int(req.has_dependencies), req.num_dependencies,
146
+ req.constraint_complexity, req.drc_iterations,
147
+ req.engineer_skill_factor, type_node_int,
148
+ complexity_score, size_priority_int
149
+ ]])
150
+
151
+ estimated_hours = float(hours_model.predict(hours_features)[0])
152
+ estimated_hours = max(4.0, round(estimated_hours, 1))
153
+
154
+ # Complexity classification features
155
+ complexity_features = np.array([[
156
+ tech_enc, type_enc, priority_enc, tc, tc_log,
157
+ int(req.has_dependencies), req.num_dependencies,
158
+ req.constraint_complexity, req.drc_iterations,
159
+ type_node_int, complexity_score, size_priority_int
160
+ ]])
161
+
162
+ xgb_proba = complexity_xgb.predict_proba(complexity_features)[0]
163
+ lgb_proba = complexity_lgb.predict_proba(complexity_features)[0]
164
+ ensemble_proba = (xgb_proba + lgb_proba) / 2
165
+
166
+ complexity_idx = int(np.argmax(ensemble_proba))
167
+ complexity_label = complexity_encoder.classes_[complexity_idx]
168
+ confidence = float(ensemble_proba[complexity_idx])
169
+
170
+ # Generate reasoning
171
+ reasoning = generate_reasoning(req, complexity_label, estimated_hours, tc)
172
+
173
+ # Risk assessment
174
+ risk_level = 'low' if complexity_label == 'Low' else ('medium' if complexity_label == 'Medium' else 'high')
175
+
176
+ # Suggested skill level
177
+ skill_needed = 'senior' if complexity_label == 'High' else ('mid' if complexity_label == 'Medium' else 'junior')
178
+
179
+ return {
180
+ 'complexity': complexity_label,
181
+ 'estimated_hours': estimated_hours,
182
+ 'confidence': round(confidence, 3),
183
+ 'risk_level': risk_level,
184
+ 'reasoning': reasoning,
185
+ 'recommended_drc_iterations': max(req.drc_iterations, 2 if complexity_label == 'High' else 1),
186
+ 'suggested_engineer_skill_level': skill_needed,
187
+ 'complexity_probabilities': {
188
+ cls: round(float(p), 3)
189
+ for cls, p in zip(complexity_encoder.classes_, ensemble_proba)
190
+ },
191
+ 'estimated_days': round(estimated_hours / 8, 1),
192
+ 'model_version': '1.0.0',
193
+ }
194
+
195
+ def generate_reasoning(req, complexity, hours, tc):
196
+ """Generate human-readable reasoning for the estimate."""
197
+ parts = []
198
+
199
+ if complexity == 'High':
200
+ if req.tech_node in ['5nm', '7nm', '12nm']:
201
+ parts.append(f"Advanced {req.tech_node} node requires extensive DRC/LVS iterations with tight design rules")
202
+ if tc > 50000:
203
+ parts.append(f"Large transistor count (~{tc:,}) significantly increases layout complexity and verification time")
204
+ if req.block_type in ['PLL', 'SerDes', 'ADC', 'PA']:
205
+ parts.append(f"{req.block_type} blocks require precision analog matching and careful signal routing")
206
+ if req.has_dependencies:
207
+ parts.append(f"Inter-block dependencies ({req.num_dependencies}) add integration and timing closure overhead")
208
+ if req.constraint_complexity > 2.0:
209
+ parts.append(f"High constraint complexity ({req.constraint_complexity:.1f}/3.0) demands extensive floor planning")
210
+ elif complexity == 'Medium':
211
+ parts.append(f"{req.block_type} at {req.tech_node} presents moderate layout challenges")
212
+ if req.constraint_complexity > 1.5:
213
+ parts.append("Analog constraints require careful floor planning and routing")
214
+ if tc > 20000:
215
+ parts.append(f"Moderate transistor count (~{tc:,}) requires systematic verification")
216
+ else:
217
+ parts.append(f"{req.block_type} at {req.tech_node} is a well-characterized block with established layout patterns")
218
+ if tc < 10000:
219
+ parts.append("Small transistor count allows straightforward layout")
220
+
221
+ if not parts:
222
+ parts.append(f"Standard {req.block_type} layout at {req.tech_node} technology node")
223
+
224
+ parts.append(f"Estimated {hours:.0f} hours ({hours/8:.1f} working days) for completion")
225
+
226
+ return "; ".join(parts) + "."
227
+
228
+
229
+ def predict_bottleneck_risk(req: BottleneckRequest):
230
+ """Predict bottleneck risk for a block."""
231
+ tc = get_transistor_count(req.block_type, req.transistor_count)
232
+ tc_log = np.log1p(tc)
233
+
234
+ tech_enc = safe_encode(tech_node_encoder, req.tech_node)
235
+ type_enc = safe_encode(block_type_encoder, req.block_type)
236
+ priority_enc = safe_priority_encode(req.priority)
237
+
238
+ complexity_score = req.constraint_complexity * tc_log
239
+ hours_ratio = req.hours_logged / max(req.estimated_hours, 1)
240
+ stage_idx = STAGE_IDX.get(req.current_stage, 1)
241
+
242
+ features = np.array([[
243
+ tech_enc, type_enc, priority_enc, tc_log,
244
+ int(req.has_dependencies), req.num_dependencies,
245
+ req.constraint_complexity, req.estimated_hours, req.hours_logged,
246
+ hours_ratio, req.drc_iterations, req.drc_violations_total,
247
+ req.lvs_mismatches_total, stage_idx, req.days_in_current_stage,
248
+ req.engineer_skill_factor, int(req.is_overdue), complexity_score
249
+ ]])
250
+
251
+ risk_idx = bottleneck_model.predict(features)[0]
252
+ risk_proba = bottleneck_model.predict_proba(features)[0]
253
+ risk_label = bottleneck_encoder.classes_[risk_idx]
254
+
255
+ # Generate actionable recommendations
256
+ recommendations = []
257
+ if risk_label == 'High':
258
+ if hours_ratio > 1.3:
259
+ recommendations.append("Block is significantly over budget — consider reassignment or scope review")
260
+ if req.days_in_current_stage > 5:
261
+ recommendations.append(f"Block stuck in {req.current_stage} for {req.days_in_current_stage:.0f} days — escalate to manager")
262
+ if req.drc_violations_total > 10:
263
+ recommendations.append(f"High DRC violations ({req.drc_violations_total}) — review design rule compliance")
264
+ if req.is_overdue:
265
+ recommendations.append("Block is past due date — prioritize or adjust timeline")
266
+ elif risk_label == 'Medium':
267
+ if hours_ratio > 1.0:
268
+ recommendations.append("Hours approaching estimate — monitor closely")
269
+ if req.days_in_current_stage > 3:
270
+ recommendations.append(f"Consider checking progress — {req.days_in_current_stage:.0f} days in {req.current_stage}")
271
+
272
+ return {
273
+ 'risk_level': risk_label,
274
+ 'confidence': round(float(risk_proba[risk_idx]), 3),
275
+ 'risk_probabilities': {
276
+ cls: round(float(p), 3)
277
+ for cls, p in zip(bottleneck_encoder.classes_, risk_proba)
278
+ },
279
+ 'hours_over_budget_ratio': round(hours_ratio, 2),
280
+ 'recommendations': recommendations if recommendations else ['Block progressing normally'],
281
+ 'should_alert': risk_label == 'High',
282
+ 'model_version': '1.0.0',
283
+ }
284
+
285
+
286
+ def predict_completion_time(req: CompletionRequest):
287
+ """Predict remaining hours to completion."""
288
+ tc = get_transistor_count(req.block_type, req.transistor_count)
289
+ tc_log = np.log1p(tc)
290
+
291
+ tech_enc = safe_encode(tech_node_encoder, req.tech_node)
292
+ type_enc = safe_encode(block_type_encoder, req.block_type)
293
+ priority_num = PRIORITY_MAP.get(req.priority, 3)
294
+ stage_idx = STAGE_IDX.get(req.current_stage, 1)
295
+ stages_completed = stage_idx
296
+
297
+ hours_ratio = req.cumulative_hours / max(req.estimated_hours, 1)
298
+ avg_hours_per_stage = req.cumulative_hours / max(stages_completed, 1)
299
+ avg_days_per_stage = req.cumulative_days / max(stages_completed, 1)
300
+
301
+ features = np.array([[
302
+ tech_enc, type_enc, priority_num, tc_log,
303
+ int(req.has_dependencies), req.num_dependencies,
304
+ req.constraint_complexity, req.estimated_hours,
305
+ req.engineer_skill_factor, req.drc_iterations,
306
+ stage_idx, req.cumulative_hours, req.cumulative_days,
307
+ req.cumulative_drc_violations, req.cumulative_lvs_mismatches,
308
+ hours_ratio, stages_completed, avg_hours_per_stage, avg_days_per_stage
309
+ ]])
310
+
311
+ remaining_hours = float(completion_model.predict(features)[0])
312
+ remaining_hours = max(0, round(remaining_hours, 1))
313
+ remaining_days = remaining_hours / 8
314
+
315
+ # Project completion date
316
+ now = datetime.now()
317
+ estimated_completion = now + timedelta(days=remaining_days)
318
+
319
+ return {
320
+ 'remaining_hours': remaining_hours,
321
+ 'remaining_days': round(remaining_days, 1),
322
+ 'estimated_completion_date': estimated_completion.strftime('%Y-%m-%d'),
323
+ 'total_estimated_hours': round(req.cumulative_hours + remaining_hours, 1),
324
+ 'progress_percent': round(req.cumulative_hours / max(req.cumulative_hours + remaining_hours, 1) * 100, 1),
325
+ 'current_stage': req.current_stage,
326
+ 'model_version': '1.0.0',
327
+ }
328
+
329
+
330
+ # === FastAPI App ===
331
+ try:
332
+ from fastapi import FastAPI, HTTPException
333
+ from fastapi.middleware.cors import CORSMiddleware
334
+ import uvicorn
335
+
336
+ app = FastAPI(
337
+ title="ALWAS ML API",
338
+ description="Machine Learning models for the Analog Layout Workflow Automation System",
339
+ version="1.0.0",
340
+ )
341
+
342
+ app.add_middleware(
343
+ CORSMiddleware,
344
+ allow_origins=["*"],
345
+ allow_credentials=True,
346
+ allow_methods=["*"],
347
+ allow_headers=["*"],
348
+ )
349
+
350
+ @app.get("/")
351
+ def root():
352
+ return {
353
+ "service": "ALWAS ML API",
354
+ "version": "1.0.0",
355
+ "models": {
356
+ "hours_estimation": model_metrics.get('hours_estimation', {}),
357
+ "complexity_classification": model_metrics.get('complexity_classification', {}),
358
+ "bottleneck_prediction": model_metrics.get('bottleneck_prediction', {}),
359
+ "completion_prediction": model_metrics.get('completion_prediction', {}),
360
+ },
361
+ "endpoints": [
362
+ "/predict/estimate",
363
+ "/predict/bottleneck",
364
+ "/predict/completion",
365
+ "/predict/bulk-estimate",
366
+ "/health",
367
+ ]
368
+ }
369
+
370
+ @app.get("/health")
371
+ def health():
372
+ return {"status": "healthy", "models_loaded": 5, "timestamp": datetime.now().isoformat()}
373
+
374
+ @app.post("/predict/estimate")
375
+ def estimate_block(req: BlockEstimateRequest):
376
+ """Estimate complexity and hours for a new block. Direct replacement for Groq AI estimation."""
377
+ try:
378
+ return predict_complexity_and_hours(req)
379
+ except Exception as e:
380
+ raise HTTPException(status_code=500, detail=str(e))
381
+
382
+ @app.post("/predict/bottleneck")
383
+ def predict_bottleneck(req: BottleneckRequest):
384
+ """Predict bottleneck risk for an in-progress block."""
385
+ try:
386
+ return predict_bottleneck_risk(req)
387
+ except Exception as e:
388
+ raise HTTPException(status_code=500, detail=str(e))
389
+
390
+ @app.post("/predict/completion")
391
+ def predict_completion(req: CompletionRequest):
392
+ """Predict remaining time to completion."""
393
+ try:
394
+ return predict_completion_time(req)
395
+ except Exception as e:
396
+ raise HTTPException(status_code=500, detail=str(e))
397
+
398
+ @app.post("/predict/bulk-estimate")
399
+ def bulk_estimate(req: BulkBlockRequest):
400
+ """Bulk estimation for multiple blocks at once."""
401
+ try:
402
+ results = [predict_complexity_and_hours(block) for block in req.blocks]
403
+ return {
404
+ "count": len(results),
405
+ "estimates": results,
406
+ "total_estimated_hours": round(sum(r['estimated_hours'] for r in results), 1),
407
+ }
408
+ except Exception as e:
409
+ raise HTTPException(status_code=500, detail=str(e))
410
+
411
+ @app.get("/model/metrics")
412
+ def get_metrics():
413
+ """Get model performance metrics."""
414
+ return model_metrics
415
+
416
+ @app.get("/model/supported-values")
417
+ def get_supported_values():
418
+ """Get list of supported block types, tech nodes, etc."""
419
+ return {
420
+ "tech_nodes": feature_config['tech_nodes'],
421
+ "block_types": feature_config['block_types'],
422
+ "priorities": feature_config['priorities'],
423
+ "stages": STAGES,
424
+ "complexity_classes": feature_config['complexity_classes'],
425
+ "bottleneck_classes": feature_config['bottleneck_classes'],
426
+ }
427
+
428
+ HAS_FASTAPI = True
429
+
430
+ except ImportError:
431
+ HAS_FASTAPI = False
432
+ print("FastAPI not installed — running in library mode only")
433
+
434
+
435
+ # === Standalone Test ===
436
+ if __name__ == '__main__':
437
+ print("=" * 60)
438
+ print("ALWAS ML Inference Server — Test Mode")
439
+ print("=" * 60)
440
+
441
+ # Test 1: Complexity & Hours estimation
442
+ print("\n--- Test: PLL at 7nm ---")
443
+ result = predict_complexity_and_hours(BlockEstimateRequest(
444
+ block_type='PLL', tech_node='7nm', priority='P1-Critical',
445
+ transistor_count=80000, has_dependencies=True, num_dependencies=3,
446
+ constraint_complexity=2.5, drc_iterations=4, engineer_skill_factor=0.8
447
+ ))
448
+ print(json.dumps(result, indent=2))
449
+
450
+ # Test 2: Simple block
451
+ print("\n--- Test: CurrentMirror at 45nm ---")
452
+ result = predict_complexity_and_hours(BlockEstimateRequest(
453
+ block_type='CurrentMirror', tech_node='45nm', priority='P4-Low',
454
+ transistor_count=1500, constraint_complexity=0.5
455
+ ))
456
+ print(json.dumps(result, indent=2))
457
+
458
+ # Test 3: Bottleneck risk
459
+ print("\n--- Test: Bottleneck Risk ---")
460
+ result = predict_bottleneck_risk(BottleneckRequest(
461
+ block_type='ADC', tech_node='7nm', priority='P1-Critical',
462
+ estimated_hours=60, hours_logged=80,
463
+ drc_violations_total=15, current_stage='DRC',
464
+ days_in_current_stage=7, is_overdue=True
465
+ ))
466
+ print(json.dumps(result, indent=2))
467
+
468
+ # Test 4: Completion time
469
+ print("\n--- Test: Completion Prediction ---")
470
+ result = predict_completion_time(CompletionRequest(
471
+ block_type='DAC', tech_node='12nm', priority='P2-High',
472
+ estimated_hours=40, current_stage='LVS',
473
+ cumulative_hours=25, cumulative_days=4,
474
+ cumulative_drc_violations=5
475
+ ))
476
+ print(json.dumps(result, indent=2))
477
+
478
+ # Start server if FastAPI available
479
+ if HAS_FASTAPI:
480
+ print(f"\n{'=' * 60}")
481
+ print("Starting ALWAS ML API server on http://0.0.0.0:7860")
482
+ print("=" * 60)
483
+ uvicorn.run(app, host="0.0.0.0", port=7860)