abhaypratapsingh111 commited on
Commit
7697331
Β·
verified Β·
1 Parent(s): f357acb

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +649 -0
app.py ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main Dash application for Chronos 2 Time Series Forecasting
3
+ """
4
+
5
+ import base64
6
+ import io
7
+ import logging
8
+ from pathlib import Path
9
+ from dash import Dash, html, dcc, Input, Output, State, callback_context
10
+ import dash_bootstrap_components as dbc
11
+ import pandas as pd
12
+
13
+ # Import components
14
+ from components.upload import (
15
+ create_upload_component,
16
+ create_column_selector,
17
+ create_sample_data_loader,
18
+ format_upload_status,
19
+ create_data_preview_table,
20
+ create_quality_report
21
+ )
22
+ from components.chart import (
23
+ create_forecast_chart,
24
+ create_empty_chart,
25
+ create_metrics_display,
26
+ create_backtest_metrics_display,
27
+ decimate_data
28
+ )
29
+ from components.controls import (
30
+ create_forecast_controls,
31
+ create_model_status_bar,
32
+ create_results_section,
33
+ create_app_header,
34
+ create_footer
35
+ )
36
+
37
+ # Import services
38
+ from services.model_service import model_service
39
+ from services.data_processor import data_processor
40
+ from services.cache_manager import cache_manager
41
+
42
+ # Import utilities
43
+ from utils.validators import (
44
+ validate_file_upload,
45
+ validate_column_selection,
46
+ validate_forecast_parameters
47
+ )
48
+ from utils.metrics import calculate_metrics
49
+
50
+ # Import configuration
51
+ from config.settings import CONFIG, APP_METADATA, LOG_LEVEL, LOG_FORMAT, LOG_FILE, setup_directories
52
+ from config.constants import MAX_CHART_POINTS
53
+
54
+ # Setup logging with both file and console handlers
55
+ def setup_logging():
56
+ """Configure logging to write to both file and console"""
57
+ # Create logs directory first
58
+ Path(LOG_FILE).parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ # Get root logger
61
+ root_logger = logging.getLogger()
62
+ root_logger.setLevel(LOG_LEVEL)
63
+
64
+ # Remove any existing handlers
65
+ root_logger.handlers = []
66
+
67
+ # Create formatters
68
+ formatter = logging.Formatter(LOG_FORMAT)
69
+
70
+ # File handler - writes all logs to file
71
+ file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8')
72
+ file_handler.setLevel(LOG_LEVEL)
73
+ file_handler.setFormatter(formatter)
74
+ root_logger.addHandler(file_handler)
75
+
76
+ # Console handler - writes to stderr
77
+ console_handler = logging.StreamHandler()
78
+ console_handler.setLevel(LOG_LEVEL)
79
+ console_handler.setFormatter(formatter)
80
+ root_logger.addHandler(console_handler)
81
+
82
+ logger = logging.getLogger(__name__)
83
+ logger.info(f"Logging configured - writing to {LOG_FILE}")
84
+ return logger
85
+
86
+ logger = setup_logging()
87
+
88
+ # Initialize Dash app
89
+ app = Dash(
90
+ __name__,
91
+ external_stylesheets=[
92
+ dbc.themes.BOOTSTRAP,
93
+ 'https://use.fontawesome.com/releases/v5.15.4/css/all.css'
94
+ ],
95
+ suppress_callback_exceptions=True,
96
+ title=APP_METADATA['title']
97
+ )
98
+
99
+ # App layout
100
+ app.layout = dbc.Container([
101
+ # Header
102
+ create_app_header(),
103
+
104
+ # Model status
105
+ html.Div(id='model-status-bar'),
106
+
107
+ # Stores for data
108
+ dcc.Store(id='uploaded-data-store'),
109
+ dcc.Store(id='processed-data-store'),
110
+ dcc.Store(id='forecast-results-store'),
111
+
112
+ # Sample data loader
113
+ create_sample_data_loader(),
114
+
115
+ # Upload section
116
+ create_upload_component(),
117
+
118
+ # Column selector (hidden initially)
119
+ create_column_selector(),
120
+
121
+ # Forecast controls
122
+ create_forecast_controls(),
123
+
124
+ # Results section (hidden initially)
125
+ create_results_section(),
126
+
127
+ # Footer
128
+ create_footer()
129
+
130
+ ], fluid=True, className="py-4")
131
+
132
+
133
+ # Callback: Load model on startup
134
+ @app.callback(
135
+ Output('model-status-bar', 'children'),
136
+ Input('model-status-bar', 'id')
137
+ )
138
+ def load_model_on_startup(_):
139
+ """Load the model when the app starts"""
140
+ logger.info("=" * 80)
141
+ logger.info("CALLBACK: load_model_on_startup - ENTRY")
142
+ logger.info("=" * 80)
143
+
144
+ try:
145
+ logger.info("Attempting to load Chronos-2 model...")
146
+ result = model_service.load_model()
147
+
148
+ logger.info(f"Model loading result: {result}")
149
+
150
+ if result['status'] == 'success':
151
+ logger.info("βœ“ Model loaded successfully - returning 'ready' status bar")
152
+ status_bar = create_model_status_bar('ready')
153
+ logger.info(f"Status bar created: {type(status_bar)}")
154
+ return status_bar
155
+ else:
156
+ logger.error(f"βœ— Model loading failed: {result.get('error')}")
157
+ return create_model_status_bar('error')
158
+ except Exception as e:
159
+ logger.error(f"βœ— EXCEPTION in load_model_on_startup: {str(e)}", exc_info=True)
160
+ return create_model_status_bar('error')
161
+ finally:
162
+ logger.info("CALLBACK: load_model_on_startup - EXIT")
163
+ logger.info("=" * 80)
164
+
165
+
166
+ # Callback: Handle file upload
167
+ @app.callback(
168
+ [Output('uploaded-data-store', 'data'),
169
+ Output('upload-status', 'children'),
170
+ Output('column-selector-card', 'style'),
171
+ Output('date-column-dropdown', 'options'),
172
+ Output('target-column-dropdown', 'options'),
173
+ Output('id-column-dropdown', 'options'),
174
+ Output('covariate-columns-dropdown', 'options')],
175
+ Input('upload-data', 'contents'),
176
+ State('upload-data', 'filename')
177
+ )
178
+ def handle_file_upload(contents, filename):
179
+ """Handle file upload and extract column information"""
180
+ logger.info("=" * 80)
181
+ logger.info("CALLBACK: handle_file_upload - ENTRY")
182
+ logger.info(f"Filename: {filename}")
183
+ logger.info(f"Contents received: {contents is not None}")
184
+ logger.info("=" * 80)
185
+
186
+ if contents is None:
187
+ logger.warning("No contents provided - returning empty response")
188
+ return None, '', {'display': 'none'}, [], [], [], []
189
+
190
+ try:
191
+ # Parse uploaded file
192
+ content_type, content_string = contents.split(',')
193
+ decoded = base64.b64decode(content_string)
194
+
195
+ # Server-side validation
196
+ validation = validate_file_upload(filename, len(decoded))
197
+ if not validation['valid']:
198
+ error_msg = ' '.join(validation['issues'])
199
+ logger.warning(f"File upload validation failed: {error_msg}")
200
+ return None, format_upload_status('error', error_msg, True), {'display': 'none'}, [], [], [], []
201
+
202
+ # Additional security: Sanitize filename
203
+ import re
204
+ safe_filename = re.sub(r'[^\w\-\.]', '_', filename)
205
+ if safe_filename != filename:
206
+ logger.info(f"Sanitized filename from '{filename}' to '{safe_filename}'")
207
+
208
+ # Load file
209
+ logger.info(f"Loading file with data_processor: {len(decoded)} bytes")
210
+ result = data_processor.load_file(decoded, filename)
211
+ logger.info(f"Load result status: {result['status']}")
212
+
213
+ if result['status'] == 'error':
214
+ logger.error(f"βœ— File loading error: {result['error']}")
215
+ return None, format_upload_status('error', result['error'], True), {'display': 'none'}, [], [], [], []
216
+
217
+ # Get column information
218
+ logger.info("Getting column information from data_processor")
219
+ col_info = data_processor.get_column_info()
220
+ logger.info(f"Column info: date_cols={col_info['date_columns']}, numeric_cols={col_info['numeric_columns'][:5]}...")
221
+
222
+ # Create dropdown options
223
+ date_options = [{'label': col, 'value': col} for col in col_info['date_columns']]
224
+ target_options = [{'label': col, 'value': col} for col in col_info['numeric_columns']]
225
+ id_options = [{'label': col, 'value': col} for col in col_info['all_columns']]
226
+ # Covariates can be any numeric column
227
+ covariate_options = [{'label': col, 'value': col} for col in col_info['numeric_columns']]
228
+
229
+ logger.info(f"Created dropdown options: {len(date_options)} date, {len(target_options)} target, {len(id_options)} id, {len(covariate_options)} covariate")
230
+
231
+ success_msg = f"Successfully loaded {filename} ({len(result['data'])} rows, {len(result['data'].columns)} columns)"
232
+ logger.info(f"βœ“ {success_msg}")
233
+
234
+ logger.info("CALLBACK: handle_file_upload - EXIT (success)")
235
+ logger.info("=" * 80)
236
+
237
+ return (
238
+ result['metadata'],
239
+ format_upload_status('success', success_msg),
240
+ {'display': 'block'},
241
+ date_options,
242
+ target_options,
243
+ id_options,
244
+ covariate_options
245
+ )
246
+
247
+ except Exception as e:
248
+ logger.error(f"βœ— EXCEPTION in handle_file_upload: {str(e)}", exc_info=True)
249
+ logger.info("CALLBACK: handle_file_upload - EXIT (exception)")
250
+ logger.info("=" * 80)
251
+ return None, format_upload_status('error', f"Error: {str(e)}", True), {'display': 'none'}, [], [], [], []
252
+
253
+
254
+ # Callback: Load sample data
255
+ @app.callback(
256
+ [Output('uploaded-data-store', 'data', allow_duplicate=True),
257
+ Output('upload-status', 'children', allow_duplicate=True),
258
+ Output('column-selector-card', 'style', allow_duplicate=True),
259
+ Output('date-column-dropdown', 'options', allow_duplicate=True),
260
+ Output('target-column-dropdown', 'options', allow_duplicate=True),
261
+ Output('id-column-dropdown', 'options', allow_duplicate=True),
262
+ Output('covariate-columns-dropdown', 'options', allow_duplicate=True)],
263
+ [Input('load-weather', 'n_clicks'),
264
+ Input('load-airquality', 'n_clicks'),
265
+ Input('load-bitcoin', 'n_clicks'),
266
+ Input('load-stock', 'n_clicks'),
267
+ Input('load-traffic', 'n_clicks'),
268
+ Input('load-electricity', 'n_clicks')],
269
+ prevent_initial_call=True
270
+ )
271
+ def load_sample_data(weather_clicks, airquality_clicks, bitcoin_clicks, stock_clicks, traffic_clicks, electricity_clicks):
272
+ """Load sample datasets"""
273
+ ctx = callback_context
274
+ if not ctx.triggered:
275
+ return None, '', {'display': 'none'}, [], [], [], []
276
+
277
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
278
+
279
+ # Map button to filename
280
+ sample_files = {
281
+ 'load-weather': 'weather_stations.csv',
282
+ 'load-airquality': 'air_quality_uci.csv',
283
+ 'load-bitcoin': 'bitcoin_price.csv',
284
+ 'load-stock': 'stock_sp500.csv',
285
+ 'load-traffic': 'traffic_speeds.csv',
286
+ 'load-electricity': 'electricity_consumption.csv'
287
+ }
288
+
289
+ filename = sample_files.get(button_id)
290
+ if not filename:
291
+ return None, '', {'display': 'none'}, [], [], [], []
292
+
293
+ try:
294
+ # Load sample file
295
+ filepath = f"{CONFIG['datasets_folder']}/{filename}"
296
+ with open(filepath, 'rb') as f:
297
+ contents = f.read()
298
+
299
+ result = data_processor.load_file(contents, filename)
300
+
301
+ if result['status'] == 'error':
302
+ return None, format_upload_status('error', result['error'], True), {'display': 'none'}, [], [], [], []
303
+
304
+ # Get column information
305
+ col_info = data_processor.get_column_info()
306
+
307
+ date_options = [{'label': col, 'value': col} for col in col_info['date_columns']]
308
+ target_options = [{'label': col, 'value': col} for col in col_info['numeric_columns']]
309
+ id_options = [{'label': col, 'value': col} for col in col_info['all_columns']]
310
+ covariate_options = [{'label': col, 'value': col} for col in col_info['numeric_columns']]
311
+
312
+ success_msg = f"Loaded sample dataset: {filename}"
313
+
314
+ return (
315
+ result['metadata'],
316
+ format_upload_status('success', success_msg),
317
+ {'display': 'block'},
318
+ date_options,
319
+ target_options,
320
+ id_options,
321
+ covariate_options
322
+ )
323
+
324
+ except Exception as e:
325
+ logger.error(f"Error loading sample data: {str(e)}", exc_info=True)
326
+ error_msg = f"Sample data not found. Please ensure datasets folder exists: {CONFIG['datasets_folder']}"
327
+ return None, format_upload_status('warning', error_msg), {'display': 'none'}, [], [], [], []
328
+
329
+
330
+ # Callback: Handle forecasting mode changes
331
+ @app.callback(
332
+ [Output('covariate-section', 'style'),
333
+ Output('target-help-text', 'children')],
334
+ Input('forecasting-mode', 'value')
335
+ )
336
+ def update_forecasting_mode(mode):
337
+ """Update UI based on selected forecasting mode"""
338
+ if mode == 'univariate':
339
+ return (
340
+ {'display': 'none'},
341
+ 'Select ONE target variable (multi-select available, but use only one for univariate)'
342
+ )
343
+ elif mode == 'multivariate':
344
+ return (
345
+ {'display': 'none'},
346
+ 'Select MULTIPLE target variables to forecast together'
347
+ )
348
+ else: # covariate-informed
349
+ return (
350
+ {'display': 'block'},
351
+ 'Select target variable(s) to forecast (can select multiple)'
352
+ )
353
+
354
+
355
+ # Callback: Handle backtest enable/disable
356
+ @app.callback(
357
+ Output('backtest-controls', 'style'),
358
+ Input('backtest-enable', 'value')
359
+ )
360
+ def toggle_backtest_controls(backtest_enabled):
361
+ """Show/hide backtest controls based on checkbox"""
362
+ if 'enabled' in backtest_enabled:
363
+ return {'display': 'block'}
364
+ return {'display': 'none'}
365
+
366
+
367
+ # Callback: Update data preview and quality report
368
+ @app.callback(
369
+ [Output('data-preview-container', 'children'),
370
+ Output('data-quality-report', 'children'),
371
+ Output('processed-data-store', 'data'),
372
+ Output('generate-forecast-btn', 'disabled')],
373
+ [Input('date-column-dropdown', 'value'),
374
+ Input('target-column-dropdown', 'value'),
375
+ Input('forecasting-mode', 'value'),
376
+ Input('covariate-columns-dropdown', 'value')],
377
+ State('id-column-dropdown', 'value')
378
+ )
379
+ def update_preview_and_process(date_col, target_col, mode, covariate_cols, id_col):
380
+ """Update data preview and process data when columns are selected"""
381
+ logger.info("=" * 80)
382
+ logger.info("CALLBACK: update_preview_and_process - ENTRY")
383
+ logger.info(f"date_col: {date_col}")
384
+ logger.info(f"target_col: {target_col}")
385
+ logger.info(f"mode: {mode}")
386
+ logger.info(f"covariate_cols: {covariate_cols}")
387
+ logger.info(f"id_col: {id_col}")
388
+ logger.info("=" * 80)
389
+
390
+ if not date_col or not target_col:
391
+ logger.warning(f"Missing required columns - date_col: {date_col}, target_col: {target_col}")
392
+ return '', '', None, True
393
+
394
+ try:
395
+ # Ensure target_col is a list for consistency
396
+ if not isinstance(target_col, list):
397
+ target_col = [target_col] if target_col else []
398
+
399
+ # Ensure covariate_cols is a list
400
+ if covariate_cols and not isinstance(covariate_cols, list):
401
+ covariate_cols = [covariate_cols]
402
+
403
+ # Validate column selection
404
+ # For multivariate, validate each target column
405
+ for t_col in target_col:
406
+ validation = validate_column_selection(data_processor.data, date_col, t_col)
407
+ if not validation['valid']:
408
+ error_msg = ' '.join(validation['issues'])
409
+ return format_upload_status('error', error_msg, True), '', None, True
410
+
411
+ # Show preview
412
+ preview = create_data_preview_table(data_processor.data)
413
+
414
+ # Process data - pass target columns based on mode
415
+ # For univariate: single target, for multivariate: list of targets
416
+ if mode == 'univariate':
417
+ target_to_process = target_col[0] # Single target string
418
+ else:
419
+ target_to_process = target_col # List of targets for multivariate
420
+
421
+ result = data_processor.preprocess(
422
+ date_column=date_col,
423
+ target_column=target_to_process,
424
+ id_column=id_col,
425
+ forecast_horizon=30
426
+ )
427
+
428
+ if result['status'] == 'error':
429
+ return preview, format_upload_status('error', result['error'], True), None, True
430
+
431
+ # Show quality report
432
+ quality_report = create_quality_report(result['quality_report'])
433
+
434
+ # Store processed data with forecasting mode and columns
435
+ processed_data = {
436
+ 'data': result['data'].to_json(date_format='iso'),
437
+ 'quality_report': result['quality_report'],
438
+ 'forecasting_mode': mode,
439
+ 'target_columns': target_col,
440
+ 'covariate_columns': covariate_cols if covariate_cols else [],
441
+ 'date_column': date_col,
442
+ 'id_column': id_col
443
+ }
444
+
445
+ return preview, quality_report, processed_data, False
446
+
447
+ except Exception as e:
448
+ logger.error(f"Error in preview/process: {str(e)}", exc_info=True)
449
+ return '', format_upload_status('error', f"Error: {str(e)}", True), None, True
450
+
451
+
452
+ # Callback: Generate forecast
453
+ @app.callback(
454
+ [Output('forecast-chart', 'figure'),
455
+ Output('metrics-display', 'children'),
456
+ Output('results-card', 'style'),
457
+ Output('loading-output', 'children')],
458
+ Input('generate-forecast-btn', 'n_clicks'),
459
+ [State('processed-data-store', 'data'),
460
+ State('horizon-slider', 'value'),
461
+ State('confidence-checklist', 'value'),
462
+ State('backtest-enable', 'value'),
463
+ State('backtest-size-slider', 'value')],
464
+ prevent_initial_call=True
465
+ )
466
+ def generate_forecast(n_clicks, processed_data, horizon, confidence_levels, backtest_enabled, backtest_size):
467
+ """Generate forecast using the Chronos model, optionally with backtesting"""
468
+ logger.info("=" * 80)
469
+ logger.info("CALLBACK: generate_forecast - ENTRY")
470
+ logger.info(f"n_clicks: {n_clicks}")
471
+ logger.info(f"horizon: {horizon}")
472
+ logger.info(f"confidence_levels: {confidence_levels}")
473
+ logger.info(f"processed_data is None: {processed_data is None}")
474
+ logger.info("=" * 80)
475
+
476
+ if not processed_data or not n_clicks:
477
+ logger.warning(f"Early return - processed_data exists: {processed_data is not None}, n_clicks: {n_clicks}")
478
+ return create_empty_chart(), '', {'display': 'none'}, ''
479
+
480
+ try:
481
+ # Load processed data
482
+ logger.info("Loading processed data from JSON...")
483
+ df = pd.read_json(processed_data['data'])
484
+ logger.info(f"Loaded DataFrame: shape={df.shape}, columns={df.columns.tolist()}")
485
+
486
+ # Get forecasting mode and metadata
487
+ mode = processed_data.get('forecasting_mode', 'univariate')
488
+ target_columns = processed_data.get('target_columns', [])
489
+ covariate_columns = processed_data.get('covariate_columns', [])
490
+
491
+ logger.info(f"Forecasting mode: {mode}")
492
+ logger.info(f"Target columns: {target_columns}")
493
+ logger.info(f"Covariate columns: {covariate_columns}")
494
+
495
+ # Validate parameters
496
+ logger.info("Validating forecast parameters...")
497
+ validation = validate_forecast_parameters(horizon, confidence_levels, len(df))
498
+ logger.info(f"Validation result: {validation}")
499
+
500
+ if not validation['valid']:
501
+ error_msg = ' '.join(validation['issues'])
502
+ logger.error(f"βœ— Validation failed: {error_msg}")
503
+ return create_empty_chart(error_msg), '', {'display': 'none'}, ''
504
+
505
+ # Perform backtesting if enabled
506
+ backtest_df = None
507
+ backtest_metrics = None
508
+
509
+ if backtest_enabled and 'enabled' in backtest_enabled:
510
+ logger.info(f"Backtesting enabled with test_size={backtest_size}")
511
+
512
+ backtest_result = model_service.backtest(
513
+ data=df,
514
+ test_size=min(backtest_size, len(df) // 3), # Ensure we have enough training data
515
+ forecast_horizon=horizon,
516
+ confidence_levels=confidence_levels
517
+ )
518
+
519
+ if backtest_result['status'] == 'success':
520
+ backtest_df = backtest_result['backtest_data']
521
+ backtest_metrics = backtest_result['metrics']
522
+ logger.info(f"βœ“ Backtest completed: {backtest_metrics}")
523
+ else:
524
+ logger.warning(f"Backtest failed: {backtest_result.get('error', 'Unknown error')}")
525
+
526
+ # Generate forecast
527
+ logger.info(f"Calling model_service.predict() - horizon={horizon}, confidence={confidence_levels}, mode={mode}")
528
+ logger.info(f"Model service state: is_loaded={model_service.is_loaded}, variant={model_service.model_variant}")
529
+
530
+ forecast_result = model_service.predict(
531
+ data=df,
532
+ horizon=horizon,
533
+ confidence_levels=confidence_levels
534
+ )
535
+
536
+ logger.info(f"Forecast result status: {forecast_result['status']}")
537
+
538
+ if forecast_result['status'] == 'error':
539
+ logger.error(f"βœ— Forecast generation failed: {forecast_result['error']}")
540
+ return create_empty_chart(f"Forecast failed: {forecast_result['error']}"), '', {'display': 'none'}, ''
541
+
542
+ # Get forecast data
543
+ forecast_df = forecast_result['forecast']
544
+ logger.info(f"Forecast DataFrame shape: {forecast_df.shape}, columns: {forecast_df.columns.tolist()}")
545
+
546
+ # Decimate data if too large
547
+ logger.info("Decimating data for chart...")
548
+ historical_decimated = decimate_data(df, MAX_CHART_POINTS // 2)
549
+ forecast_decimated = decimate_data(forecast_df, MAX_CHART_POINTS // 2)
550
+ logger.info(f"Decimated - historical: {len(historical_decimated)}, forecast: {len(forecast_decimated)}")
551
+
552
+ # Prepare data for chart (rename Chronos 2 columns to chart format)
553
+ logger.info("Renaming columns for chart...")
554
+ historical_for_chart = historical_decimated.rename(columns={
555
+ 'timestamp': 'ds',
556
+ 'target': 'y'
557
+ })
558
+ logger.info(f"Historical chart data columns: {historical_for_chart.columns.tolist()}")
559
+
560
+ # Create chart title and labels based on target columns
561
+ logger.info("Creating forecast chart...")
562
+ primary_target = target_columns[0] if target_columns else 'Target'
563
+
564
+ if mode == 'multivariate' and len(target_columns) > 1:
565
+ chart_title = f"Forecast: {primary_target} (with {', '.join(target_columns[1:])} as covariates)"
566
+ y_label = primary_target
567
+ elif covariate_columns:
568
+ chart_title = f"Forecast: {primary_target} (with covariates)"
569
+ y_label = primary_target
570
+ else:
571
+ chart_title = f"Forecast: {primary_target}"
572
+ y_label = primary_target
573
+
574
+ fig = create_forecast_chart(
575
+ historical_data=historical_for_chart,
576
+ forecast_data=forecast_decimated,
577
+ confidence_levels=confidence_levels,
578
+ title=chart_title,
579
+ y_axis_label=y_label,
580
+ backtest_data=backtest_df
581
+ )
582
+ logger.info(f"Chart created: {type(fig)}")
583
+
584
+ # Create metrics display
585
+ metrics = {
586
+ 'inference_time': forecast_result['inference_time'],
587
+ 'data_points': len(df),
588
+ 'horizon': horizon
589
+ }
590
+ logger.info(f"Creating metrics display: {metrics}")
591
+
592
+ # Add backtest metrics if available
593
+ if backtest_metrics:
594
+ metrics_components = dbc.Row([
595
+ dbc.Col(create_metrics_display(metrics, forecast_result['inference_time']), md=6),
596
+ dbc.Col(create_backtest_metrics_display(backtest_metrics), md=6)
597
+ ])
598
+ else:
599
+ metrics_components = dbc.Row(create_metrics_display(
600
+ metrics,
601
+ forecast_result['inference_time']
602
+ ))
603
+
604
+ logger.info("βœ“ Forecast generation successful - returning chart and metrics")
605
+ logger.info("CALLBACK: generate_forecast - EXIT (success)")
606
+ logger.info("=" * 80)
607
+
608
+ return fig, metrics_components, {'display': 'block'}, ''
609
+
610
+ except Exception as e:
611
+ logger.error(f"βœ— EXCEPTION in generate_forecast: {str(e)}", exc_info=True)
612
+ logger.info("CALLBACK: generate_forecast - EXIT (exception)")
613
+ logger.info("=" * 80)
614
+ return create_empty_chart(f"Error: {str(e)}"), '', {'display': 'none'}, ''
615
+
616
+
617
+ # Health check endpoint
618
+ @app.server.route('/health')
619
+ def health_check():
620
+ """Health check endpoint for deployment monitoring"""
621
+ status = {
622
+ 'status': 'healthy' if model_service.is_loaded else 'degraded',
623
+ 'model_loaded': model_service.is_loaded,
624
+ 'model_variant': model_service.model_variant,
625
+ 'device': model_service.device
626
+ }
627
+ return status
628
+
629
+
630
+ # Run the app
631
+ if __name__ == '__main__':
632
+ # Setup directories
633
+ setup_directories()
634
+
635
+ logger.info(f"Starting Chronos 2 Forecasting App")
636
+ logger.info(f"Configuration: {CONFIG}")
637
+
638
+ # Get host and port from environment variables (for HuggingFace Spaces, Render, etc.)
639
+ import os
640
+ host = os.getenv('HOST', '127.0.0.1')
641
+ port = int(os.getenv('PORT', '7860')) # 7860 is HuggingFace Spaces default
642
+ debug = os.getenv('DEBUG', 'True').lower() == 'true'
643
+
644
+ # Run the app
645
+ app.run_server(
646
+ host=host,
647
+ port=port,
648
+ debug=debug
649
+ )