1use std::path::PathBuf;
2
3use crate::common::DtypeArg;
4use anyhow::Context;
5use clap::Subcommand;
6
7use zsfm_flowstate::config::FlowStateConfig;
8use zsfm_flowstate::convert::{convert, ConvertOptions};
9use zsfm_gguf::GGMLType;
10
11#[derive(Subcommand)]
12pub enum Command {
13 Convert {
21 #[arg(
22 short,
23 long,
24 default_value = "ibm-granite/granite-timeseries-flowstate-r1"
25 )]
26 model: String,
27 #[arg(short, long, default_value = "gguf/flowstate-r1-f16.gguf")]
28 output: PathBuf,
29 #[arg(long, default_value = "f16")]
30 dtype: DtypeArg,
31 #[arg(long, default_value = "models")]
32 model_dir: PathBuf,
33 #[arg(long, env = "HF_TOKEN")]
34 token: Option<String>,
35 #[arg(long)]
37 redownload: bool,
38 },
39 InspectTensors { path: PathBuf },
42 Infer {
48 #[arg(short, long, default_value = "gguf/flowstate-r1-f16.gguf")]
49 gguf: PathBuf,
50 #[arg(
51 long,
52 default_value = "models/ibm-granite__granite-timeseries-flowstate-r1/config.json"
53 )]
54 config: PathBuf,
55 },
56 Upload {
58 #[arg(short, long, default_value = "amaye15/flowstate-r1-gguf")]
59 repo: String,
60 #[arg(long, env = "HF_TOKEN")]
61 token: String,
62 #[arg(long, default_value = ".")]
64 root: PathBuf,
65 },
66
67 Delete {
74 #[arg(
76 short,
77 long,
78 default_value = "ibm-granite/granite-timeseries-flowstate-r1"
79 )]
80 model: String,
81 #[arg(long, default_value = "models")]
83 model_dir: PathBuf,
84 #[arg(short, long)]
86 output: Option<PathBuf>,
87 },
88}
89
90pub async fn run(command: Command) -> anyhow::Result<()> {
91 match command {
92 Command::Delete {
93 model,
94 model_dir,
95 output,
96 } => {
97 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
98 crate::common::delete_cached_model(&canonical, output.as_deref())?;
99 }
100 Command::Upload { repo, token, root } => {
101 zsfm_hub::upload_repo(&repo, &token, &root).await?;
102 }
103
104 Command::Convert {
105 model,
106 output,
107 dtype,
108 model_dir,
109 token,
110 redownload,
111 } => {
112 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
113 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
114 {
115 return Ok(());
116 }
117
118 eprintln!("Downloading {model} into {} …", model_dir.display());
119 let files = zsfm_hub::download_model(&model, token.as_deref(), &model_dir)
120 .await
121 .context("download failed")?;
122
123 let config_str =
124 std::fs::read_to_string(&files.config_json).context("read config.json")?;
125 let config = FlowStateConfig::from_json(&config_str).context("parse config.json")?;
126
127 println!(
128 "Config: {} encoder layers, embed_dim={}, state_dim={}, decoder_dim={}, {} quantiles",
129 config.encoder_num_layers,
130 config.embedding_feature_dim,
131 config.encoder_state_dim,
132 config.decoder_dim,
133 config.n_quantiles(),
134 );
135
136 let f32_opts = ConvertOptions {
137 output_dtype: GGMLType::F32,
138 };
139 convert(&model, &files, &config, &f32_opts, &canonical)?;
140 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
141 files.cleanup_weights();
142
143 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
144 eprintln!("Wrote {}", output.display());
145 }
146
147 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
148
149 Command::Infer { gguf, config } => {
150 return run_infer_burn(&gguf, &config).await;
151 }
152 }
153
154 Ok(())
155}
156
157async fn run_infer_burn(
158 gguf: &std::path::PathBuf,
159 config: &std::path::PathBuf,
160) -> anyhow::Result<()> {
161 use zsfm_flowstate::infer::BurnFlowStateModel;
162
163 let buf = zsfm_core::read_stdin_limited()?;
164 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
165
166 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
167 let horizon = zsfm_core::parse_horizon(&req)?;
168
169 let config_str =
170 std::fs::read_to_string(config).with_context(|| format!("read {}", config.display()))?;
171 let fs_config = FlowStateConfig::from_json(&config_str).context("parse config.json")?;
172
173 eprintln!("Loading Burn model from {} …", gguf.display());
174 let model =
175 BurnFlowStateModel::load(gguf, zsfm_flowstate::infer::InferConfig::from(&fs_config))
176 .context("load model")?;
177 let quantile_levels = model.quantiles().to_vec();
178 let median_idx = model.median_index();
179
180 let mut fc_outputs = Vec::new();
181 let mut total_ctx: usize = 0;
182 for raw_variates in &contexts {
183 anyhow::ensure!(
184 raw_variates.len() == 1,
185 "FlowState only supports univariate forecasting (1 variate per context)"
186 );
187 let ctx = &raw_variates[0];
188 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
189 total_ctx += ctx.len();
190
191 eprintln!(
192 "Running forecast ({} context steps → {horizon} future steps) …",
193 ctx.len()
194 );
195 let quantile_mat = model.forecast(ctx, horizon).context("forecast")?;
196 let qmat: zsfm_core::QuantileMatrix =
197 quantile_mat.into_iter().map(|row| vec![row]).collect();
198 fc_outputs.push(zsfm_core::quantile_matrix_to_output(
199 &qmat,
200 &quantile_levels,
201 median_idx,
202 ));
203 }
204
205 println!(
206 "{}",
207 zsfm_core::forecast_response_json("flowstate", total_ctx, horizon, fc_outputs)?
208 );
209 Ok(())
210}