AnoushkaVyas commited on
Commit
23c792e
·
verified ·
1 Parent(s): 96a28ec

Restructure model card: pitch, access, skill, notebooks, GIFT-Eval, paper

Browse files
Files changed (1) hide show
  1. README.md +95 -703
README.md CHANGED
@@ -6,759 +6,151 @@ tags:
6
  - foundation-model
7
  - zero-shot
8
  - gift-eval
 
9
  pipeline_tag: time-series-forecasting
10
  library_name: pytorch
11
  ---
12
 
13
  # CHARM
14
 
15
- CHARM is a zero-shot probabilistic time-series foundation model from **C3 AI**.
16
- It produces full quantile forecasts (99 quantile levels) for arbitrary horizons and
17
- is evaluated zero-shot on the [GIFT-Eval](https://huggingface.co/spaces/Salesforce/GIFT-Eval)
18
- benchmark — no GIFT-Eval data is used in training.
19
 
20
- > **Availability:** CHARM model weights and training/replication code are
21
- > **not publicly released**. The model is served behind an API and consumed
22
- > through the open-source **`c3-charm` Python SDK** (see [Using CHARM](#using-charm--the-c3-charm-sdk) below).
23
- > This page serves as the model card and benchmark record.
 
24
 
25
- ## Model summary
26
-
27
- | | |
28
- |---|---|
29
- | Parameters | ~63.3M (encoder ~59M) |
30
- | Hidden size (d_model) | 384 |
31
- | Projection | TCN-pool (patch size 16, causal conv stack + residual gate) |
32
- | Backbone | Transformer encoder with RoPE attention |
33
- | Decoder | Quantile decoder, 99 levels (0.01–0.99) |
34
- | Max context length | 8192 |
35
- | Output | Probabilistic (quantile) forecasts, multivariate-capable |
36
- | Precision | float32 |
37
-
38
- ## Intended use
39
-
40
- Zero-shot probabilistic forecasting of univariate and multivariate time series
41
- across domains (energy, transport, sales, healthcare, nature, web/cloud-ops,
42
- econ/finance). The model is applied without any per-dataset fine-tuning.
43
 
44
- ## Using CHARM — the `c3-charm` SDK
 
45
 
46
- CHARM is served behind an API and consumed through the open-source [`c3-charm`](https://pypi.org/project/c3-charm/) Python SDK. The SDK provides **embeddings** (multivariate time series → vectors), **forecast/backcast** (quantile predictions), and an optional **toolkit** for downstream tasks (anomaly detection, retrieval, classification, reconstruction, forecasting). The reference below is the full SDK documentation.
47
-
48
- > **Dual-model serving.** A CHARM server can be backed by two independent checkpoints — one for embeddings (`/predict`, `client.embeddings`) and a separate one for forecasting (`/forecast`, `client.prediction`). They may differ in architecture, patch size, and embedding dimension, so read per-model properties from `client.model_info()` rather than hardcoding.
49
-
50
- ### Example notebooks
51
-
52
- Runnable notebooks live in [`notebooks/`](./notebooks) in this repo. Open them on the Hub, or download the folder and run locally after `pip install c3-charm[toolkit]` (set `CHARM_BASE_URL` / `CHARM_API_KEY` first). The classification and reconstruction/forecasting demos read the small sample datasets in [`notebooks/data/`](./notebooks/data).
53
-
54
- | Notebook | What it covers |
55
- |---|---|
56
- | [`getting_started.ipynb`](./notebooks/getting_started.ipynb) | Client setup, first embeddings call, inspecting `model_info()`. |
57
- | [`charm_toolkit_demo.ipynb`](./notebooks/charm_toolkit_demo.ipynb) | Tour of the `charm_toolkit` — datasets, precompute, trainer, and each task head. |
58
- | [`demo_forecasting.ipynb`](./notebooks/demo_forecasting.ipynb) | Zero-shot quantile forecasting and the embedding-based `ForecastingModel` head (weather data). |
59
- | [`demo_reconstruction.ipynb`](./notebooks/demo_reconstruction.ipynb) | Backcast reconstruction and the `ReconstructionModel` head for anomaly detection (weather data). |
60
- | [`demo_classification.ipynb`](./notebooks/demo_classification.ipynb) | Time-series classification with the `ClassificationModel` head (BasicMotions data). |
61
- | [`demo_retrieval_anomaly_detection.ipynb`](./notebooks/demo_retrieval_anomaly_detection.ipynb) | Embedding retrieval + kNN / zero-shot anomaly-detection recipes. |
62
-
63
- ### Installation
64
 
65
  ```bash
66
- pip install c3-charm # core SDK only (embeddings + forecast)
67
- pip install c3-charm[toolkit] # includes PyTorch models, datasets, trainers
68
  ```
69
 
70
- Or from source:
71
-
72
- ```bash
73
- git clone https://github.com/c3ai/c3-charm.git
74
- cd c3-charm
75
- poetry install # core SDK only
76
- poetry install --with toolkit # include toolkit dependencies
77
- ```
78
 
79
- #### CPU vs CUDA torch
80
 
81
- `pip install c3-charm` pulls the **default torch wheel from PyPI**, which on
82
- Linux is the full **CUDA** build (~4 GB). The SDK itself never requires a
83
- GPU — the model runs server-side, and toolkit training heads are small
84
- enough to fit on CPU — so if you want the smaller CPU-only wheel, install
85
- torch from the CPU index **before** `c3-charm`:
86
 
87
- ```bash
88
- pip install torch --index-url https://download.pytorch.org/whl/cpu
89
- pip install c3-charm
90
- ```
91
 
92
- Or from source, use `bash setup.sh`, which forces the CPU wheel.
93
 
94
- If you instead want a specific CUDA build, install it manually **after**
95
- `c3-charm`:
96
 
97
  ```bash
98
- pip install --force-reinstall --no-deps torch \
99
- --index-url https://download.pytorch.org/whl/cu124
100
- ```
101
-
102
- Swap `cu124` for the CUDA version that matches your driver (`cu121`,
103
- `cu126`, `cu128`, …). `--no-deps` is important — it stops pip from
104
- re-resolving your torch install.
105
-
106
- ---
107
-
108
- ### Core SDK
109
-
110
- #### Client initialization
111
-
112
- ```python
113
- from charm import CharmClient
114
-
115
- client = CharmClient(
116
- base_url="http://your-server:8080",
117
- api_key="your-api-key", # or set CHARM_API_KEY env var
118
- timeout=300, # override; SDK default is 15s — raise it for /forecast (server allows up to ~220s)
119
- max_retries=3,
120
- )
121
- ```
122
-
123
- #### Embeddings — `client.embeddings.create()`
124
-
125
- Converts time series windows into dense vectors.
126
-
127
- ```python
128
- response = client.embeddings.create(
129
- descriptions=[["sensor_A", "sensor_B"]], # (N, C) channel names
130
- ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]], # (N, T, C) values
131
- batch_size=32,
132
- return_tensors="np", # "list", "np", or "torch"
133
- aggregate=True, # True → (N, D); False → (N, T_, C, D)
134
- progress=True,
135
- )
136
- embeddings = response.embeds # shape (N, D) when aggregate=True
137
- ```
138
-
139
- **`aggregate` parameter:**
140
- - `True` (default): Returns flattened embeddings `(N, D)` — one vector per series. Best for retrieval, classification, clustering.
141
- - `False`: Returns per-patch, per-channel embeddings `(N, T_, C, D)` where `T_ = ceil(T / patch_size)`. Best for fine-grained tasks or custom heads.
142
-
143
- **Async** (faster for large datasets):
144
-
145
- ```python
146
- response = await client.embeddings.async_create(
147
- descriptions=descriptions,
148
- ts_array=ts_array,
149
- max_B_per_request=32,
150
- concurrency_per_call=8,
151
- return_tensors="np",
152
- aggregate=True,
153
- )
154
- ```
155
-
156
- #### Forecast / Backcast — `client.prediction.create()`
157
-
158
- Zero-shot quantile predictions — no training required.
159
-
160
- ```python
161
- response = client.prediction.create(
162
- descriptions=[["sensor_A", "sensor_B"]],
163
- ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]],
164
- target_len=10, # positive = forecast, negative = backcast
165
- return_tensors="np",
166
- )
167
- forecast = response.denormalized_predictions # (N, target_len=10, C, Q) — Q quantiles (model-dependent, e.g. 21)
168
- median = response.median # (N, 10, C) — point forecast (median quantile)
169
- ```
170
-
171
- **Backcast** (reconstruct past values):
172
-
173
- ```python
174
- response = client.prediction.create(
175
- descriptions=descriptions,
176
- ts_array=ts_array,
177
- target_len=-8, # reconstruct last 8 steps
178
- return_tensors="np",
179
- )
180
- ```
181
-
182
- #### Input constraints
183
-
184
- | Constraint | Limit |
185
- |---|---|
186
- | Timesteps per series | 1 ≤ T ≤ 8192 (the model's training window) |
187
- | Channels per series | No hard limit (large C grows memory ~O(C²) under cross-channel attention) |
188
- | Per-request size | N × C × T ≤ 500,000 — SDK client-side batching guard, not a server limit |
189
- | Batch consistency | All series in a request must share the same T and C |
190
- | Best accuracy | T ≤ 8192 (the model's training window); T need not be a multiple of the patch size |
191
-
192
- The model was trained on windows of up to 8192 timesteps (patch size 16).
193
- Inputs are not required to be a multiple of the patch size — they are padded
194
- to a patch boundary internally. The architecture can technically accept more
195
- than 8192 (up to 1500 patches), but longer inputs rely on positions seen only
196
- in pretraining, so quality degrades. Query `client.model_info()` for the
197
- served model's actual patch size and embedding dimension.
198
-
199
- The SDK handles client-side batching automatically when you set `batch_size` (sync) or `max_B_per_request` (async).
200
-
201
- #### Output shapes
202
-
203
- | Method | Output field | Shape |
204
- |---|---|---|
205
- | `embeddings.create(aggregate=True)` | `response.embeds` | (N, D) |
206
- | `embeddings.create(aggregate=False)` | `response.embeds` | (N, T_, C, D), T_ = ceil(T / patch_size) |
207
- | `prediction.create(target_len > 0)` | `response.denormalized_predictions` | (N, target_len, C, Q) |
208
- | `prediction.create(target_len < 0)` | `response.denormalized_predictions` | (N, abs(target_len), C, Q) |
209
- | `prediction.create(...)` | `response.median` | (N, abs(target_len), C) |
210
-
211
- #### Channel descriptions
212
-
213
- Descriptions are **required** and affect embedding quality. They tell the model what each channel represents.
214
-
215
- **Good descriptions** — use meaningful, consistent names:
216
- ```python
217
- descriptions = [["engine_temperature", "oil_pressure", "rpm"]]
218
- ```
219
-
220
- **Acceptable** — short but informative:
221
- ```python
222
- descriptions = [["temp", "pressure", "speed"]]
223
- ```
224
-
225
- **Avoid** — generic or positional names reduce model effectiveness:
226
- ```python
227
- descriptions = [["col_0", "col_1", "col_2"]] # works but suboptimal
228
- ```
229
-
230
- When working with pandas DataFrames, use column names directly:
231
- ```python
232
- descriptions = [df.columns.tolist()] * N
233
- ```
234
-
235
- #### Scaling
236
-
237
- **No pre-processing needed.** CHARM normalizes internally. Send raw data as-is. Do not apply StandardScaler, MinMaxScaler, or log transforms before calling the API.
238
-
239
- #### Error handling
240
-
241
- ```python
242
- from charm import CharmError, AuthenticationError, InvalidRequestError, RateLimitError
243
-
244
- try:
245
- response = client.embeddings.create(...)
246
- except AuthenticationError:
247
- # bad API key
248
- except InvalidRequestError as e:
249
- # shape violations, empty input
250
- except RateLimitError:
251
- # back off and retry
252
- except CharmError as e:
253
- # catch-all for other SDK errors
254
- ```
255
-
256
- ---
257
-
258
- ### Toolkit — Downstream Tasks
259
-
260
- The toolkit (`pip install c3-charm[toolkit]`) provides PyTorch models, dataset utilities, and training infrastructure for fine-tuning on top of CHARM embeddings.
261
-
262
- #### Retrieval — `charm_toolkit.retrieval`
263
-
264
- Find similar time series by embedding similarity.
265
-
266
- ```python
267
- from charm_toolkit.retrieval import (
268
- l2_normalize,
269
- cosine_similarity_matrix,
270
- knn_search,
271
- retrieval_metrics,
272
- )
273
-
274
- # Embed your data
275
- response = client.embeddings.create(
276
- descriptions=descriptions,
277
- ts_array=windows_list,
278
- return_tensors="np",
279
- )
280
- embeddings = response.embeds # (N, D)
281
-
282
- # Similarity search
283
- sim = cosine_similarity_matrix(embeddings, embeddings)
284
-
285
- # kNN search
286
- indices, scores = knn_search(query_emb, corpus_emb, k=5)
287
-
288
- # Evaluation metrics
289
- metrics = retrieval_metrics(
290
- query_emb=query_emb,
291
- corpus_emb=corpus_emb,
292
- query_labels=query_labels,
293
- corpus_labels=corpus_labels,
294
- k_values=[1, 3, 5, 10],
295
- exclude_self=True,
296
- query_ids=query_dataset_names,
297
- corpus_ids=corpus_dataset_names,
298
- )
299
- # Returns: precision@k, ndcg@k, hit_rate@k
300
- ```
301
-
302
- #### Anomaly Detection — `charm_toolkit.anomaly_detection`
303
-
304
- Detect anomalies via kNN distance scoring on windowed CHARM embeddings.
305
-
306
- ```python
307
- from charm_toolkit.anomaly_detection import (
308
- sliding_window_embeddings,
309
- knn_anomaly_scores,
310
- window_scores_to_pointwise,
311
- )
312
-
313
- # 1. Embed sliding windows
314
- train_emb = sliding_window_embeddings(
315
- client, train_data, descriptions,
316
- window_size=128, stride=1, batch_size=64,
317
- )
318
- test_emb = sliding_window_embeddings(
319
- client, test_data, descriptions,
320
- window_size=128, stride=1, batch_size=64,
321
- )
322
-
323
- # 2. Score test windows by distance to train
324
- window_scores = knn_anomaly_scores(
325
- test_emb=test_emb,
326
- reference_emb=train_emb,
327
- k=5,
328
- distance="cosine", # "cosine", "l2", "l1"
329
- aggregation="mean", # "mean", "max"
330
- )
331
-
332
- # 3. Aggregate to per-timestep scores
333
- pointwise_scores = window_scores_to_pointwise(
334
- window_scores=window_scores,
335
- window_size=128,
336
- stride=1,
337
- total_length=len(test_data),
338
- method="mean", # "mean", "max", "last", "center"
339
- )
340
  ```
341
 
342
- **Pointwise aggregation methods:**
343
-
344
- Each timestep is covered by multiple overlapping windows. The `method` parameter controls how to assign a single score per timestep:
345
-
346
- | Method | Behavior | Use case |
347
- |--------|----------|----------|
348
- | `"mean"` | Average of all windows covering the point | Smooth, best for offline evaluation |
349
- | `"max"` | Max score among covering windows | Conservative, catches isolated spikes |
350
- | `"last"` | Score of the most recently *completed* window | Online/streaming — score only updates when a window finishes processing |
351
- | `"center"` | Score of the window centered on each point | Minimal time-shift, tightest temporal alignment |
352
-
353
- **Zero-shot recipes (no clean reference set required) — recommended methods, in order of strength:**
354
-
355
- 1. **Bootstrap k-NN** (best). Two steps: run `sklearn.ensemble.IsolationForest` on the embedding matrix and take the bottom ~70% by score as a presumed-clean reference; then call `knn_anomaly_scores` against that reference.
356
- 2. **CBLOF**: `pyod.models.cblof.CBLOF` on the embedding matrix.
357
- 3. **IsolationForest**: `sklearn.ensemble.IsolationForest` directly on the embedding matrix.
358
-
359
- L2-normalize embeddings beforehand to use cosine geometry. See `demo_retrieval_anomaly_detection.ipynb`.
360
-
361
- **Best practices (validated on TSB-AD, metric VUS-PR):**
362
-
363
- 1. **Ensemble the embedding score with per-window statistics.** The encoder
364
- instance-normalizes each window, erasing amplitude/level-shift anomalies (spikes,
365
- steps — the most common kind) from the embedding. Recover them with
366
- `window_statistics` (no extra model call) and combine via `ensemble_scores`
367
- (`method="zscore"` — z-score each detector and sum; parameter-free and as strong as a
368
- tuned weighted ensemble). This is worth **~+7 pp VUS-PR** supervised.
369
-
370
- ```python
371
- from charm_toolkit.anomaly_detection import window_statistics, ensemble_scores
372
-
373
- emb_score = knn_anomaly_scores(test_emb, train_emb, k=3, distance="cosine")
374
- S_tr = window_statistics(train_data, 128, 1)
375
- mu, sd = S_tr.mean(0, keepdims=True), S_tr.std(0, keepdims=True) + 1e-8
376
- stats_score = knn_anomaly_scores(
377
- (window_statistics(test_data, 128, 1) - mu) / sd, (S_tr - mu) / sd,
378
- k=3, distance="l2")
379
- window_scores = ensemble_scores([emb_score, stats_score], method="zscore")
380
- # zero-shot: build the reference by bootstrap (above) and use method="rank"
381
- ```
382
-
383
- 2. **Multivariate: do NOT pool channels when the channel count is high.** Channel-mean
384
- pooling dilutes an anomaly confined to a few channels across all of them. Keep channels
385
- separate with `sliding_window_channel_embeddings` + `per_channel_knn_scores`
386
- (`channel_pool="adaptive"`). The benefit grows with the number of channels — negligible
387
- at C≤3, sizeable at C≥20, large at C>60 — so it is **advisable whenever C is high**. For
388
- few channels, plain mean-pooling is equally good and cheaper.
389
-
390
- ```python
391
- from charm_toolkit.anomaly_detection import (
392
- sliding_window_channel_embeddings, per_channel_knn_scores)
393
-
394
- train_pc = sliding_window_channel_embeddings(client, train_data, descriptions) # (N, C, D)
395
- test_pc = sliding_window_channel_embeddings(client, test_data, descriptions)
396
- emb_score = per_channel_knn_scores(test_pc, train_pc, k=3, channel_pool="adaptive")
397
- ```
398
-
399
- #### ReconstructionModel — anomaly detection via learned head
400
-
401
- ```python
402
- from charm_toolkit import (
403
- ReconstructionModel, create_reconstruction_datasets,
404
- collator, TrainerClass,
405
- )
406
- from torch.utils.data import DataLoader
407
- import torch.nn as nn
408
-
409
- train_ds, val_ds, test_ds = create_reconstruction_datasets(
410
- raw_data, # (T, C) numpy array or torch tensor
411
- descriptions=channel_names,
412
- window_size=256,
413
- stride=1,
414
- train_ratio=0.7,
415
- val_ratio=0.15,
416
- sequential=True,
417
- scale=True,
418
- )
419
-
420
- model = ReconstructionModel(
421
- embedding_client=client,
422
- reconstructor="linear", # "linear", "mlp", or custom nn.Module
423
- hidden_dim=128,
424
- dropout=0.1,
425
- )
426
-
427
- trainer = TrainerClass(
428
- model=model,
429
- train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
430
- val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
431
- epochs=1000,
432
- patience=5,
433
- lr=1e-3,
434
- criterion=nn.HuberLoss(),
435
- )
436
- trainer.fit()
437
- ```
438
-
439
- #### ForecastingModel — embedding-based forecasting
440
-
441
- ```python
442
- from charm_toolkit import ForecastingModel, create_forecasting_datasets, collator, TrainerClass
443
- from torch.utils.data import DataLoader
444
-
445
- train_ds, val_ds, test_ds = create_forecasting_datasets(
446
- raw_data,
447
- descriptions=channel_names,
448
- train_horizon=96,
449
- test_horizon=96,
450
- train_ratio=0.7,
451
- val_ratio=0.15,
452
- sequential=True,
453
- scale=True,
454
- )
455
-
456
- model = ForecastingModel(
457
- embedding_client=client,
458
- horizon=96,
459
- input_size=96,
460
- head="linear",
461
- hidden_dim=128,
462
- mode="last", # "last", "avg", "none"
463
- per_channel=True,
464
- num_channels=len(channel_names),
465
- )
466
-
467
- trainer = TrainerClass(
468
- model=model,
469
- train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
470
- val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
471
- epochs=1000,
472
- patience=10,
473
- lr=1e-2,
474
- )
475
- trainer.fit()
476
- ```
477
-
478
- #### ClassificationModel — time series classification
479
-
480
- ```python
481
- from charm_toolkit import ClassificationModel, create_classification_datasets, collator, TrainerClass
482
- from torch.utils.data import DataLoader
483
- import torch.nn as nn
484
-
485
- train_ds, val_ds, test_ds = create_classification_datasets(
486
- raw_data, # (N, T, C)
487
- labels=labels, # list of N integer labels
488
- descriptions=channel_names,
489
- train_ratio=0.7,
490
- val_ratio=0.15,
491
- )
492
-
493
- model = ClassificationModel(
494
- embedding_client=client,
495
- num_classes=num_classes,
496
- hidden_dim=128,
497
- pooling_over_t="mean",
498
- pooling_over_channels="mean",
499
- classifier_type="mlp",
500
- )
501
-
502
- trainer = TrainerClass(
503
- model=model,
504
- train_loader=DataLoader(train_ds, batch_size=32, collate_fn=collator),
505
- val_loader=DataLoader(val_ds, batch_size=32, collate_fn=collator),
506
- epochs=100,
507
- patience=10,
508
- lr=1e-3,
509
- criterion=nn.CrossEntropyLoss(),
510
- )
511
- trainer.fit()
512
- ```
513
-
514
- #### Precomputing embeddings (critical for training)
515
-
516
- Toolkit models call the API every forward pass. For training with hundreds of windows per epoch, **precompute embeddings once**:
517
-
518
- ```python
519
- from charm_toolkit import precompute_dataset_embeddings, PrecomputedEmbeddingsDataset
520
-
521
- # Compute once, save to disk as memmap
522
- train_shape = precompute_dataset_embeddings(
523
- client=client, dataset=train_ds,
524
- output_path="./outputs/train_embeddings.pt", memory_batch_size=8192
525
- )
526
- val_shape = precompute_dataset_embeddings(
527
- client=client, dataset=val_ds,
528
- output_path="./outputs/val_embeddings.pt", memory_batch_size=8192
529
- )
530
-
531
- # Wrap datasets — model skips API calls when "embeds" key present
532
- train_ds = PrecomputedEmbeddingsDataset(train_ds, "./outputs/train_embeddings.pt", train_shape)
533
- val_ds = PrecomputedEmbeddingsDataset(val_ds, "./outputs/val_embeddings.pt", val_shape)
534
-
535
- # Training now uses cached embeddings — orders of magnitude faster
536
- train_loader = DataLoader(train_ds, batch_size=512, shuffle=True, collate_fn=collator)
537
- ```
538
-
539
- #### Trainer API
540
-
541
- ```python
542
- from charm_toolkit import TrainerClass
543
-
544
- trainer = TrainerClass(
545
- model=model,
546
- train_loader=train_loader,
547
- val_loader=val_loader,
548
- test_loader=test_loader, # optional
549
- lr=1e-3,
550
- weight_decay=1e-4,
551
- epochs=1000,
552
- patience=5,
553
- min_delta=1e-4,
554
- max_grad_norm=5.0,
555
- criterion=None, # defaults to MSELoss
556
- )
557
- trainer.fit()
558
- test_loss = trainer.evaluate(test_loader)
559
- ```
560
-
561
- #### Dataset factory functions
562
-
563
- All return `(train_dataset, val_dataset, test_dataset)`:
564
-
565
- | Function | Input shape | Key args |
566
- |---|---|---|
567
- | `create_reconstruction_datasets(raw_data, ...)` | (T, C) | `window_size`, `stride`, `train_ratio`, `val_ratio` |
568
- | `create_forecasting_datasets(raw_data, ...)` | (T, C) | `train_horizon`, `test_horizon`, `stride`, `train_ratio`, `val_ratio` |
569
- | `create_classification_datasets(raw_data, labels, ...)` | (N, T, C) | `train_ratio`, `val_ratio` |
570
 
571
- Reconstruction and forecasting expect a single long time series `(T, C)` split temporally. Classification expects pre-windowed `(N, T, C)`.
572
-
573
- #### collator
574
-
575
- All DataLoaders using toolkit datasets require `collator` as the `collate_fn`:
576
-
577
- ```python
578
- from charm_toolkit import collator
579
- # or equivalently:
580
- from charm_toolkit.Datasets import collator
581
- ```
582
-
583
- ---
584
-
585
- ### Embeddings as features
586
-
587
- CHARM embeddings work as drop-in feature vectors for any sklearn model:
588
-
589
- ```python
590
- import numpy as np
591
- from sklearn.ensemble import IsolationForest
592
- from sklearn.linear_model import LogisticRegression
593
- from charm_toolkit.retrieval import cosine_similarity_matrix
594
-
595
- response = client.embeddings.create(
596
- descriptions=descriptions,
597
- ts_array=windows_list,
598
- return_tensors="np",
599
- )
600
- X = response.embeds # (N, D)
601
-
602
- # Anomaly detection with isolation forest
603
- clf = IsolationForest(contamination=0.05)
604
- anomaly_labels = clf.fit_predict(X)
605
-
606
- # Similarity search
607
- sim = cosine_similarity_matrix(X, X)
608
-
609
- # As features for any classifier
610
- clf = LogisticRegression().fit(X_train, y_train)
611
- ```
612
-
613
- ---
614
 
615
- ### Local Deployment
 
616
 
617
- Deploy models locally from GitHub releases — no remote server needed:
618
-
619
- ```python
620
- with CharmClient(tag="experiment-2026-03-15_10-30-00") as client:
621
- response = client.embeddings.create(...)
622
- # Server shuts down automatically
623
- ```
624
-
625
- When `tag` is provided:
626
- 1. Checks for GPU availability (falls back to CPU)
627
- 2. Clones repo at the specified tag (shallow clone)
628
- 3. Downloads model weights from the GitHub release
629
- 4. Launches the serving stack locally
630
- 5. Polls health endpoint until ready
631
-
632
- Files cached at `~/.charm/models/<tag>/` for fast subsequent runs.
633
-
634
- ```python
635
- CharmClient(
636
- tag="experiment-tag", # required for local mode
637
- repo_url="https://...", # default: c3-e/research
638
- cache_dir="/path/to/cache", # default: ~/.charm/models
639
- port=8080, # 0 = auto-select
640
- )
641
- ```
642
-
643
- ---
644
-
645
- ### Best practices
646
-
647
- #### Input & preprocessing
648
- - Send data as `(N, T, C)` — N series, each T timesteps × C channels; all series in one request must share the same T and C.
649
- - Keep `T ≤ 8192` (the training window). The model can accept more, but quality degrades on lengths it wasn't trained on. T does **not** need to be a multiple of the patch size — inputs are padded to a patch boundary internally.
650
- - **Do not pre-scale** your data. CHARM normalizes internally (asinh z-score); applying StandardScaler/MinMaxScaler/log yourself hurts results.
651
-
652
- #### Channel descriptions (a real quality lever)
653
- - Use meaningful, consistent channel names (`"engine_temperature"`, not `"col_0"`) — the model is channel-aware and descriptions materially affect embeddings.
654
- - Reuse the same names across requests so embeddings stay comparable (retrieval, clustering).
655
-
656
- #### Batching & throughput
657
- - Respect the per-request budget: `batch_size × C × T ≤ 500,000` (SDK-enforced client-side).
658
- - Use `async_create` for large N — it batches concurrently; the sync client is sequential and slow past ~100 series. Tune `max_B_per_request` / `concurrency_per_call` instead of one giant request.
659
-
660
- #### Timeouts & retries
661
- - Raise `timeout` for forecasting — the SDK default is 15s, but the server allows forecasts up to ~220s. Use `timeout ≈ 220+` for `prediction.create`.
662
- - Keep the built-in retries (exponential backoff on 429/5xx) rather than hand-rolling.
663
-
664
- #### Embeddings
665
- - `aggregate=True` (default) → `(N, D)` for retrieval / classification / clustering. `aggregate=False` → `(N, T_, C, D)` only when you need per-patch/per-channel detail for a custom head.
666
- - L2-normalize before cosine similarity (embeddings are unit-normed by the encoder, but normalize again after any pooling you do).
667
- - Discover `D` at runtime via `client.model_info()` — it's model-dependent; don't hardcode.
668
-
669
- #### Forecasting
670
- - `target_len > 0` = forecast, `< 0` = backcast; `0` is invalid.
671
- - Keep `T + abs(target_len) ≤ 8192` (the training window) for best forecast/backcast quality.
672
- - Use `response.median` for a point forecast, or the full quantile axis for intervals. `Q` is model-dependent (e.g. 21 or 99) — read `denormalized_predictions.shape[-1]`.
673
-
674
- #### Classification
675
- - For classification heads, **don't pool over channels** — keeping the per-channel embeddings flat (rather than averaging them) boosts accuracy, at a modest cost in head size/complexity. In the toolkit `ClassificationModel`, set `pooling_over_channels="flatten"` (and pass `num_channels=C`, required for flatten) instead of the default `"mean"`.
676
-
677
- #### Dual-model awareness
678
- - Treat `/predict` (embeddings) and `/forecast` as **separate models** — they may have different patch sizes / embedding dims. Read the per-role `models` map from `client.model_info()` instead of assuming they match.
679
-
680
- #### Training on top of CHARM
681
- - Precompute embeddings once (`precompute_dataset_embeddings` + `PrecomputedEmbeddingsDataset`) — toolkit models otherwise call the API every forward pass.
682
-
683
- #### Reliability & ops
684
- - Catch specific errors (`InvalidRequestError`, `AuthenticationError`, `RateLimitError`, or the base `CharmError`) and back off on rate limits.
685
- - Use the context manager (`with CharmClient(...) as client:`) so local deployments shut down cleanly.
686
- - Set credentials via env (`CHARM_API_KEY`, `CHARM_BASE_URL`) rather than hardcoding.
687
-
688
- ---
689
-
690
- ### Decision guide
691
-
692
- #### When to use CHARM
693
-
694
- - Multivariate time series (multiple channels measured over time)
695
- - Each window has at least a few patches (patch size is 16, so ~48+ timesteps is a good floor)
696
- - You want a strong starting point without feature engineering
697
-
698
- #### When to use classical methods instead
699
 
700
- - Tabular data without a time dimension — use LightGBM, XGBoost
701
- - Very short series (< 10 timesteps)
702
- - Single scalar features — still works but may not outperform ARIMA/ETS
703
 
704
- #### Zero-shot vs fine-tuned
 
 
 
 
 
 
 
 
705
 
706
- | Approach | When | Effort |
707
- |---|---|---|
708
- | `prediction.create(target_len=H)` | Quick forecast baseline, no labeled data | None — one API call |
709
- | Embeddings + sklearn | Moderate data, combine with other features | Minutes |
710
- | Embeddings + kNN (retrieval/AD) | Unlabeled anomaly detection or search | Minutes |
711
- | Toolkit model (Reconstruction/Forecasting/Classification) | Have labeled data, want best performance | Train a small head (~minutes on CPU) |
712
 
713
- ---
 
 
714
 
715
  ## GIFT-Eval results
716
 
717
- Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term
718
- configurations) using the standard 11-metric protocol. Aggregate scores
719
- (geometric mean of per-config metrics normalized to the Seasonal Naive baseline;
720
- lower is better):
721
 
722
- | Metric | Score (rel. Seasonal Naive) |
723
- |---|---|
724
- | MASE | 0.7582 |
725
- | CRPS (mean weighted sum quantile loss) | 0.4776 |
726
 
727
  Per-term (geometric mean, normalized to Seasonal Naive):
728
 
729
- | Term | MASE | CRPS |
730
- |---|---|---|
731
- | short | 0.7463 | 0.5036 |
732
  | medium | 0.7577 | 0.4452 |
733
- | long | 0.7911 | 0.4460 |
734
 
735
- Scores are the geometric mean of per-config metric / Seasonal Naive across all
736
- 97 GIFT-Eval configurations (lower is better; < 1.0 beats Seasonal Naive).
737
- Full per-config results are in `all_results.csv`.
738
 
739
  ## Evaluation protocol
740
 
741
  - Benchmark: GIFT-Eval, 97 configs (short / medium / long terms).
742
- - Metrics: MSE[mean], MSE[0.5], MAE[0.5], MASE[0.5], MAPE[0.5], sMAPE[0.5],
743
- MSIS, RMSE[mean], NRMSE[mean], ND[0.5], mean_weighted_sum_quantile_loss
744
- (computed with gluonts `evaluate_forecasts`).
745
  - Context length: 8192; forecasts are full quantile distributions.
746
- - Zero-shot: no GIFT-Eval train/test data is seen during pretraining
747
- (`testdata_leakage = No`).
748
 
749
  ## Limitations
750
 
751
- - Forecast quality varies by domain and horizon; very long horizons and highly
752
- non-stationary series remain challenging.
753
  - Quantile calibration is learned and may drift on out-of-distribution scales.
754
 
 
 
 
 
 
 
 
755
  ## Citation
756
 
757
- ```
758
- @misc{charm,
759
- title = {CHARM: A Zero-Shot Time-Series Foundation Model},
760
- author = {C3 AI},
761
- year = {2026},
762
- url = {https://huggingface.co/c3aiia3c/CHARM}
 
 
 
763
  }
764
  ```
 
 
 
 
 
 
6
  - foundation-model
7
  - zero-shot
8
  - gift-eval
9
+ - arxiv:2605.31580
10
  pipeline_tag: time-series-forecasting
11
  library_name: pytorch
12
  ---
13
 
14
  # CHARM
15
 
16
+ CHARM (**CH**annel-**A**ware **R**epresentation **M**odel) is C3 AI's foundation model for
17
+ **multivariate time series**. It is served behind an API and consumed through the open-source
18
+ `c3-charm` Python SDK:
 
19
 
20
+ - **Embeddings**: turn `(T, C)` windows into dense vectors for search, clustering, anomaly
21
+ detection and features.
22
+ - **Zero-shot forecasting**: quantile forecasts and backcasts with no training data.
23
+ - **Toolkit** (`charm_toolkit`): anomaly-detection recipes, retrieval metrics, and small
24
+ trainable heads for forecasting, reconstruction and classification.
25
 
26
+ The model is channel-aware (channel names are part of the input) and normalizes internally, so you
27
+ send raw values. It runs server-side; the SDK is the client.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ > **Availability:** CHARM model weights and training/replication code are **not publicly
30
+ > released**. The model is served behind an API and consumed through the `c3-charm` SDK.
31
 
32
+ ## Use it
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  ```bash
35
+ pip install c3-charm # Python >= 3.10
 
36
  ```
37
 
38
+ Full SDK documentation (install, data format, client, embeddings, forecasting, anomaly detection,
39
+ trained heads, HTTP API) ships inside the package — find it with
40
+ `python -c "import charm.docs as d; print(d.get_docs_path())"` and start at `index.md`.
41
+ Package: [c3-charm on PyPI](https://pypi.org/project/c3-charm/).
 
 
 
 
42
 
43
+ ## Getting access
44
 
45
+ CHARM is served behind an API. Set `CHARM_BASE_URL` and `CHARM_API_KEY` to point the SDK at an
46
+ endpoint. To obtain access, contact **gerardo.pastrana@c3.ai** and **dhruv.mehta@c3.ai**.
 
 
 
47
 
48
+ The model will also be available on **Azure AI Foundry** — deployment instructions will be added
49
+ here.
 
 
50
 
51
+ ## Using an AI coding agent?
52
 
53
+ The **CHARM agent skill** teaches Claude Code, Cursor, Codex and others when and how to apply
54
+ CHARM. Install it:
55
 
56
  ```bash
57
+ claude plugin marketplace add https://huggingface.co/c3aiia3c/CHARM
58
+ claude plugin install charm@c3-charm
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  ```
60
 
61
+ Other agents, or manual install: see [`skills/charm/README.md`](skills/charm/README.md).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ ## Example notebooks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ Runnable notebooks (with sample data) live in [`notebooks/`](notebooks). Run them locally after
66
+ `pip install "c3-charm[toolkit]"` (set `CHARM_BASE_URL` / `CHARM_API_KEY` first).
67
 
68
+ | Notebook | What it covers |
69
+ | --- | --- |
70
+ | [getting_started.ipynb](notebooks/getting_started.ipynb) | Client setup, first embeddings call, inspecting `model_info()` |
71
+ | [charm_toolkit_demo.ipynb](notebooks/charm_toolkit_demo.ipynb) | Tour of the toolkit: datasets, precompute, trainer, each task head |
72
+ | [demo_forecasting.ipynb](notebooks/demo_forecasting.ipynb) | Zero-shot forecasting and the embedding-based ForecastingModel head |
73
+ | [demo_reconstruction.ipynb](notebooks/demo_reconstruction.ipynb) | Backcast reconstruction and the ReconstructionModel head |
74
+ | [demo_classification.ipynb](notebooks/demo_classification.ipynb) | Time-series classification with the ClassificationModel head |
75
+ | [demo_retrieval_anomaly_detection.ipynb](notebooks/demo_retrieval_anomaly_detection.ipynb) | Embedding retrieval + kNN / zero-shot anomaly-detection recipes |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ ## Model summary
 
 
78
 
79
+ | Parameters | ~63.3M (encoder ~59M) |
80
+ | ---------------------- | ----------------------------------------------------------- |
81
+ | Hidden size (d_model) | 384 |
82
+ | Projection | TCN-pool (patch size 16, causal conv stack + residual gate) |
83
+ | Backbone | Transformer encoder with RoPE attention |
84
+ | Decoder | Quantile decoder, 99 levels (0.01–0.99) |
85
+ | Max context length | 8192 |
86
+ | Output | Probabilistic (quantile) forecasts, multivariate-capable |
87
+ | Precision | float32 |
88
 
89
+ ## Intended use
 
 
 
 
 
90
 
91
+ Zero-shot probabilistic forecasting of univariate and multivariate time series across domains
92
+ (energy, transport, sales, healthcare, nature, web/cloud-ops, econ/finance). The model is applied
93
+ without any per-dataset fine-tuning.
94
 
95
  ## GIFT-Eval results
96
 
97
+ Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term configurations)
98
+ using the standard 11-metric protocol. Aggregate scores (geometric mean of per-config metrics
99
+ normalized to the Seasonal Naive baseline; lower is better):
 
100
 
101
+ | Metric | Score (rel. Seasonal Naive) |
102
+ | -------------------------------------- | --------------------------- |
103
+ | MASE | 0.7582 |
104
+ | CRPS (mean weighted sum quantile loss) | 0.4776 |
105
 
106
  Per-term (geometric mean, normalized to Seasonal Naive):
107
 
108
+ | Term | MASE | CRPS |
109
+ | ------ | ------ | ------ |
110
+ | short | 0.7463 | 0.5036 |
111
  | medium | 0.7577 | 0.4452 |
112
+ | long | 0.7911 | 0.4460 |
113
 
114
+ Scores are the geometric mean of per-config metric / Seasonal Naive across all 97 GIFT-Eval
115
+ configurations (lower is better; < 1.0 beats Seasonal Naive).
 
116
 
117
  ## Evaluation protocol
118
 
119
  - Benchmark: GIFT-Eval, 97 configs (short / medium / long terms).
120
+ - Metrics: MSE[mean], MSE[0.5], MAE[0.5], MASE[0.5], MAPE[0.5], sMAPE[0.5], MSIS, RMSE[mean],
121
+ NRMSE[mean], ND[0.5], mean_weighted_sum_quantile_loss (computed with gluonts
122
+ `evaluate_forecasts`).
123
  - Context length: 8192; forecasts are full quantile distributions.
124
+ - Zero-shot: no GIFT-Eval train/test data is seen during pretraining (`testdata_leakage = No`).
 
125
 
126
  ## Limitations
127
 
128
+ - Forecast quality varies by domain and horizon; very long horizons and highly non-stationary
129
+ series remain challenging.
130
  - Quantile calibration is learned and may drift on out-of-distribution scales.
131
 
132
+ ## Paper
133
+
134
+ Utsav Dutta, Gerardo Pastrana, Sina Khoshfetrat Pakazad, Henrik Ohlsson.
135
+ *Giving Sensors a Voice: Multimodal JEPA for Semantic Time-Series Embeddings.* ICML 2026.
136
+ [arXiv:2605.31580](https://arxiv.org/abs/2605.31580) ·
137
+ [Hugging Face Papers](https://huggingface.co/papers/2605.31580)
138
+
139
  ## Citation
140
 
141
+ ```bibtex
142
+ @inproceedings{dutta2026charm,
143
+ title = {Giving Sensors a Voice: Multimodal JEPA for Semantic Time-Series Embeddings},
144
+ author = {Dutta, Utsav and Pastrana, Gerardo and Khoshfetrat Pakazad, Sina and Ohlsson, Henrik},
145
+ booktitle = {Proceedings of the 43rd International Conference on Machine Learning (ICML)},
146
+ series = {Proceedings of Machine Learning Research},
147
+ volume = {306},
148
+ year = {2026},
149
+ url = {https://arxiv.org/abs/2605.31580},
150
  }
151
  ```
152
+
153
+ ## License
154
+
155
+ Apache License 2.0 for the SDK and skill. See [c3-charm on PyPI](https://pypi.org/project/c3-charm/).
156
+ Model weights are not released.