1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use crate::common::DtypeArg;
5use anyhow::Context;
6use clap::Subcommand;
7
8use zsfm_gguf::GGMLType;
9use zsfm_tirex::config::TiRexConfig;
10use zsfm_tirex::convert::{convert, ConvertOptions};
11
12#[derive(Subcommand)]
13pub enum Command {
14 Upload {
16 #[arg(short, long, default_value = "amaye15/tirex-gguf")]
17 repo: String,
18 #[arg(long, env = "HF_TOKEN")]
19 token: String,
20 #[arg(long, default_value = ".")]
22 root: PathBuf,
23 },
24 Convert {
35 #[arg(short, long, default_value = "NX-AI/TiRex")]
36 model: String,
37 #[arg(short, long, default_value = "gguf/tirex-f32.gguf")]
38 output: PathBuf,
39 #[arg(long, default_value = "f32")]
40 dtype: DtypeArg,
41 #[arg(long, default_value = "models")]
42 model_dir: PathBuf,
43 #[arg(long, env = "HF_TOKEN")]
44 token: Option<String>,
45 #[arg(long)]
47 redownload: bool,
48 },
49 InspectTensors { path: PathBuf },
52 Infer {
60 #[arg(short, long, default_value = "gguf/tirex-f32.gguf")]
61 gguf: PathBuf,
62 },
63
64 Delete {
71 #[arg(short, long, default_value = "NX-AI/TiRex")]
73 model: String,
74 #[arg(long, default_value = "models")]
76 model_dir: PathBuf,
77 #[arg(short, long)]
79 output: Option<PathBuf>,
80 },
81}
82
83pub async fn run(command: Command) -> anyhow::Result<()> {
84 match command {
85 Command::Delete {
86 model,
87 model_dir,
88 output,
89 } => {
90 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
91 crate::common::delete_cached_model(&canonical, output.as_deref())?;
92 }
93 Command::Upload { repo, token, root } => {
94 zsfm_hub::upload_repo(&repo, &token, &root).await?;
95 }
96
97 Command::Convert {
98 model,
99 output,
100 dtype,
101 model_dir,
102 token,
103 redownload,
104 } => {
105 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
106 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
107 {
108 return Ok(());
109 }
110 if let Some(parent) = canonical.parent() {
114 std::fs::create_dir_all(parent).context("create canonical GGUF dir")?;
115 }
116
117 eprintln!("Downloading {model} into {} …", model_dir.display());
118 let ckpt_path =
119 zsfm_hub::download_file(&model, "model.ckpt", token.as_deref(), &model_dir)
120 .await
121 .context("download failed")?;
122
123 let config = TiRexConfig::default_from_ckpt();
124 let f32_opts = ConvertOptions {
125 output_dtype: GGMLType::F32,
126 };
127 convert(&ckpt_path, &config, &f32_opts, &canonical)?;
128 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
129 let _ = std::fs::remove_file(&ckpt_path);
130
131 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
132 eprintln!("Wrote {}", output.display());
133 }
134
135 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
136
137 Command::Infer { gguf } => {
138 return run_infer_burn(&gguf).await;
139 }
140 }
141
142 Ok(())
143}
144
145async fn run_infer_burn(gguf: &std::path::PathBuf) -> anyhow::Result<()> {
146 use std::collections::BTreeMap;
147 use zsfm_tirex::infer::BurnTiRexModel;
148
149 let buf = zsfm_core::read_stdin_limited()?;
150 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
151 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
152 let horizon = zsfm_core::parse_horizon(&req)?;
153 let config = TiRexConfig::default_from_ckpt();
154
155 eprintln!("Loading Burn model from {} …", gguf.display());
156 let model = BurnTiRexModel::load(gguf, config.clone()).context("load model")?;
157
158 let mut fc_outputs = Vec::new();
159 let mut total_ctx = 0usize;
160 for raw_variates in &contexts {
161 anyhow::ensure!(
162 raw_variates.len() == 1,
163 "TiRex only supports univariate forecasting (1 variate per context)"
164 );
165 let ctx = &raw_variates[0];
166 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
167 total_ctx += ctx.len();
168 let (quantiles, median) = model.forecast(ctx, horizon).context("forecast")?;
169 let mut q_map: BTreeMap<String, Vec<f32>> = BTreeMap::new();
170 for (i, q) in config.quantiles.iter().enumerate() {
171 q_map.insert(format!("{q:.2}"), quantiles[i].clone());
172 }
173 fc_outputs.push(zsfm_core::ForecastOutput::Univariate {
174 point: median,
175 quantiles: q_map,
176 });
177 }
178 println!(
179 "{}",
180 zsfm_core::forecast_response_json("tirex", total_ctx, horizon, fc_outputs)?
181 );
182 Ok(())
183}