1use pyo3::prelude::*;
2use std::path::PathBuf;
3
4#[pymodule]
11fn zsfm(m: &Bound<'_, PyModule>) -> PyResult<()> {
12 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
13 m.add_function(wrap_pyfunction!(list_models, m)?)?;
14 m.add_function(wrap_pyfunction!(list_forecasters, m)?)?;
15 m.add_function(wrap_pyfunction!(list_tabular, m)?)?;
16 m.add_function(wrap_pyfunction!(convert, m)?)?;
17 m.add_function(wrap_pyfunction!(delete, m)?)?;
18
19 m.add_class::<TotoModel>()?;
21 m.add_class::<ChronosModel>()?;
22 m.add_class::<TimesFmModel>()?;
23 m.add_class::<SundialModel>()?;
24 m.add_class::<TtmModel>()?;
25 m.add_class::<LagLlamaModel>()?;
26 m.add_class::<MomentModel>()?;
27 m.add_class::<MoiraiModel>()?;
28 m.add_class::<Moirai2Model>()?;
29 m.add_class::<FlowStateModel>()?;
30 m.add_class::<TirexModel>()?;
31
32 m.add_class::<MitraModel>()?;
34 m.add_class::<TabDptModel>()?;
35 m.add_class::<TabIclModel>()?;
36 m.add_class::<TabPfnModel>()?;
37 m.add_class::<TabFmModel>()?;
38 Ok(())
39}
40
41#[pyfunction]
42fn list_models() -> Vec<&'static str> {
43 vec![
44 "toto", "chronos", "timesfm", "sundial", "ttm", "lag_llama", "moment", "moirai",
45 "moirai2", "flowstate", "tirex", "mitra", "tabdpt", "tabicl", "tabpfn", "tabfm",
46 ]
47}
48#[pyfunction]
49fn list_forecasters() -> Vec<&'static str> {
50 vec![
51 "toto", "chronos", "timesfm", "sundial", "ttm", "lag_llama", "moment", "moirai",
52 "moirai2", "flowstate", "tirex",
53 ]
54}
55#[pyfunction]
56fn list_tabular() -> Vec<&'static str> {
57 vec!["mitra", "tabdpt", "tabicl", "tabpfn", "tabfm"]
58}
59
60fn dtype_from_str(s: &str) -> PyResult<zsfm_gguf::GGMLType> {
65 match s {
66 "f32" => Ok(zsfm_gguf::GGMLType::F32),
67 "f16" => Ok(zsfm_gguf::GGMLType::F16),
68 "q8" => Ok(zsfm_gguf::GGMLType::Q8_0),
69 "bf16" => Ok(zsfm_gguf::GGMLType::BF16),
70 _ => Err(pyo3::exceptions::PyValueError::new_err(format!(
71 "unknown dtype {s:?}: expected one of f32, f16, q8, bf16"
72 ))),
73 }
74}
75fn delete_cached_model(canonical: &std::path::Path, output: Option<&std::path::Path>) -> anyhow::Result<()> {
76 let mut deleted_any = false;
77 if let Some(cache_dir) = canonical.parent() {
78 if cache_dir.exists() {
79 let count = walk_file_count(cache_dir);
80 std::fs::remove_dir_all(cache_dir)?;
81 println!("Deleted cache directory {} ({} files)", cache_dir.display(), count);
82 deleted_any = true;
83 } else if canonical.exists() {
84 std::fs::remove_file(canonical)?;
85 println!("Deleted {}", canonical.display());
86 deleted_any = true;
87 } else {
88 println!("No cache found at {} (already deleted?)", cache_dir.display());
89 }
90 } else if canonical.exists() {
91 std::fs::remove_file(canonical)?;
92 println!("Deleted {}", canonical.display());
93 deleted_any = true;
94 }
95 if let Some(out) = output {
96 if out.exists() {
97 std::fs::remove_file(out)?;
98 println!("Deleted output {}", out.display());
99 deleted_any = true;
100 } else {
101 println!("Output file not found: {} (already deleted?)", out.display());
102 }
103 }
104 if !deleted_any {
105 println!("Nothing to delete.");
106 }
107 Ok(())
108}
109
110fn walk_file_count(dir: &std::path::Path) -> usize {
111 let mut count = 0;
112 if let Ok(entries) = std::fs::read_dir(dir) {
113 for entry in entries.flatten() {
114 let path = entry.path();
115 if path.is_dir() {
116 count += walk_file_count(&path);
117 } else {
118 count += 1;
119 }
120 }
121 }
122 count
123}
124
125
126#[pyfunction]
128#[pyo3(signature = (model, output=None, dtype="f16", model_dir="models", token=None, redownload=false, task=None, filename=None))]
129fn convert(
130 model: &str,
131 output: Option<String>,
132 dtype: &str,
133 model_dir: &str,
134 token: Option<String>,
135 redownload: bool,
136 task: Option<String>,
137 filename: Option<String>,
138) -> PyResult<()> {
139 let dtype_ty = dtype_from_str(dtype)?;
140 let model_dir = PathBuf::from(model_dir);
141 let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
142 rt.block_on(async {
143 match model {
144 "toto" => {
145 let repo = "Datadog/Toto-2.0-2.5B";
146 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/toto-2.5b-f16.gguf"));
147 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
148 if canonical.exists() && !redownload {
149 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
150 return Ok(());
151 }
152 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
153 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
154 let cfg = zsfm_toto::config::TotoConfig::from_json(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
155 zsfm_toto::convert::convert(repo, &files, &cfg, &zsfm_toto::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
156 files.cleanup_weights();
157 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
158 Ok::<(), anyhow::Error>(())
159 }
160 "chronos" => {
161 let repo = "amazon/chronos-2";
162 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/chronos-f16.gguf"));
163 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
164 if canonical.exists() && !redownload {
165 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
166 return Ok(());
167 }
168 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
169 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
170 let cfg = zsfm_chronos::config::Chronos2Config::from_json(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
171 zsfm_chronos::convert::convert(repo, &files, &cfg, &zsfm_chronos::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
172 files.cleanup_weights();
173 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
174 Ok(())
175 }
176 "timesfm" => {
177 let repo = "google/timesfm-2.5-200m-pytorch";
178 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/timesfm.gguf"));
179 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
180 if canonical.exists() && !redownload {
181 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
182 return Ok(());
183 }
184 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
185 let cfg = zsfm_timesfm::config::TimesFMConfig::new();
186 zsfm_timesfm::convert::convert(repo, &files, &cfg, &zsfm_timesfm::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
187 files.cleanup_weights();
188 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
189 Ok(())
190 }
191 "sundial" => {
192 let repo = "thuml/sundial-base-128m";
193 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/sundial-f16.gguf"));
194 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
195 if canonical.exists() && !redownload {
196 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
197 return Ok(());
198 }
199 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
200 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
201 let cfg: zsfm_sundial::config::SundialConfig = serde_json::from_str(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
202 zsfm_sundial::convert::convert(repo, &files, &cfg, &zsfm_sundial::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
203 files.cleanup_weights();
204 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
205 Ok(())
206 }
207 "ttm" => {
208 let repo = "ibm-granite/granite-timeseries-ttm-r2";
209 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/ttm-f32.gguf"));
210 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
211 if canonical.exists() && !redownload {
212 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
213 return Ok(());
214 }
215 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
216 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
217 let cfg = zsfm_ttm::config::TtmConfig::from_json(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
218 zsfm_ttm::convert::convert(repo, &files, &cfg, &zsfm_ttm::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
219 files.cleanup_weights();
220 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
221 Ok(())
222 }
223 "lag_llama" | "lag-llama" => {
224 let repo = "time-series-foundation-models/Lag-Llama";
225 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/lag_llama-f32.gguf"));
226 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
227 if canonical.exists() && !redownload {
228 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
229 return Ok(());
230 }
231 if let Some(p) = canonical.parent() { std::fs::create_dir_all(p).map_err(|e| anyhow::anyhow!(e.to_string()))?; }
232 let ckpt = zsfm_hub::download_file(repo, "lag-llama.ckpt", token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
233 let cfg = zsfm_lag_llama::config::LagLlamaConfig::default_from_ckpt();
234 zsfm_lag_llama::convert::convert(&ckpt, &cfg, &zsfm_lag_llama::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
235 let _ = std::fs::remove_file(&ckpt);
236 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
237 Ok(())
238 }
239 "moment" => {
240 let repo = "AutonLab/MOMENT-1-large";
241 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/moment-f32.gguf"));
242 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
243 if canonical.exists() && !redownload {
244 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
245 return Ok(());
246 }
247 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
248 let cfg = zsfm_moment::config::MomentConfig::default();
249 zsfm_moment::convert::convert(&files.safetensors_shards, &cfg, &zsfm_moment::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
250 files.cleanup_weights();
251 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
252 Ok(())
253 }
254 "moirai" => {
255 let repo = "Salesforce/moirai-1.0-R-large";
256 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/moirai-f32.gguf"));
257 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
258 if canonical.exists() && !redownload {
259 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
260 return Ok(());
261 }
262 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
263 let cfg = zsfm_moirai::config::MoiraiConfig::default();
264 zsfm_moirai::convert::convert(&files.safetensors_shards, &cfg, &zsfm_moirai::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
265 files.cleanup_weights();
266 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
267 Ok(())
268 }
269 "moirai2" | "moirai-2" => {
270 let repo = "Salesforce/moirai-2.0-R-small";
271 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/moirai2-f32.gguf"));
272 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
273 if canonical.exists() && !redownload {
274 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
275 return Ok(());
276 }
277 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
278 let cfg = zsfm_moirai2::config::Moirai2Config::default();
279 zsfm_moirai2::convert::convert(&files.safetensors_shards, &cfg, &zsfm_moirai2::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
280 files.cleanup_weights();
281 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
282 Ok(())
283 }
284 "flowstate" | "flowstate-r1" => {
285 let repo = "ibm-granite/granite-timeseries-flowstate-r1";
286 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/flowstate-r1-f16.gguf"));
287 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
288 if canonical.exists() && !redownload {
289 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
290 return Ok(());
291 }
292 let files = zsfm_hub::download_model(repo, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
293 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
294 let cfg = zsfm_flowstate::config::FlowStateConfig::from_json(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
295 zsfm_flowstate::convert::convert(repo, &files, &cfg, &zsfm_flowstate::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
296 files.cleanup_weights();
297 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
298 Ok(())
299 }
300 "tirex" => {
301 let repo = "NX-AI/TiRex";
302 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("gguf/tirex-f32.gguf"));
303 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
304 if canonical.exists() && !redownload {
305 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
306 return Ok(());
307 }
308 if let Some(p) = canonical.parent() { std::fs::create_dir_all(p).map_err(|e| anyhow::anyhow!(e.to_string()))?; }
309 let ckpt = zsfm_hub::download_file(repo, "model.ckpt", token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
310 let cfg = zsfm_tirex::config::TiRexConfig::default_from_ckpt();
311 zsfm_tirex::convert::convert(&ckpt, &cfg, &zsfm_tirex::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
312 let _ = std::fs::remove_file(&ckpt);
313 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
314 Ok(())
315 }
316 "mitra" => {
317 let task_str = task.as_deref().unwrap_or("classification");
318 let repo = if task_str == "regression" { "autogluon/mitra-regressor" } else { "autogluon/mitra-classifier" };
319 let variant_dir = model_dir.join(format!("mitra-{task_str}"));
320 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(format!("gguf/mitra-{task_str}-{dtype}.gguf")));
321 let canonical = zsfm_hub::canonical_gguf_path(&variant_dir, repo);
322 if canonical.exists() && !redownload {
323 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
324 return Ok(());
325 }
326 let files = zsfm_hub::download_model(repo, token.as_deref(), &variant_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
327 let cfg = if task_str == "regression" { zsfm_mitra::config::MitraConfig::regressor() } else { zsfm_mitra::config::MitraConfig::classifier() };
328 let p = files.safetensors_shards.first().ok_or_else(|| anyhow::anyhow!("no shard"))?;
329 zsfm_mitra::convert::convert(std::slice::from_ref(p), &cfg, &zsfm_mitra::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
330 files.cleanup_weights();
331 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
332 Ok(())
333 }
334 "tabdpt" => {
335 let repo = "Layer6/TabDPT";
336 let fname = filename.as_deref().unwrap_or("tabdpt1_2.safetensors");
337 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(format!("gguf/tabdpt-{dtype}.gguf")));
338 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, repo);
339 if canonical.exists() && !redownload {
340 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
341 return Ok(());
342 }
343 if let Some(p) = canonical.parent() { std::fs::create_dir_all(p).map_err(|e| anyhow::anyhow!(e.to_string()))?; }
344 let p = zsfm_hub::download_file(repo, fname, token.as_deref(), &model_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
345 let cfg = zsfm_tabdpt::config::TabDptConfig::default_v1_2();
346 zsfm_tabdpt::convert::convert(std::slice::from_ref(&p), &cfg, &zsfm_tabdpt::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
347 let _ = std::fs::remove_file(&p);
348 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
349 Ok(())
350 }
351 "tabfm" => {
352 let task_str = task.as_deref().unwrap_or("classification");
353 let repo = "google/tabfm-1.0.0-pytorch";
354 let variant_dir = model_dir.join(format!("tabfm-{task_str}"));
355 let out = output.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(format!("gguf/tabfm-{task_str}-{dtype}.gguf")));
356 let canonical = zsfm_hub::canonical_gguf_path(&variant_dir, repo);
357 if canonical.exists() && !redownload {
358 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
359 return Ok(());
360 }
361 let files = zsfm_hub::download_model_prefixed(repo, task_str, token.as_deref(), &variant_dir).await.map_err(|e| anyhow::anyhow!(e.to_string()))?;
362 let s = std::fs::read_to_string(&files.config_json).map_err(|e| anyhow::anyhow!(e.to_string()))?;
363 let cfg = zsfm_tabfm::config::TabFMConfig::from_json(&s).map_err(|e| anyhow::anyhow!(e.to_string()))?;
364 let p = files.safetensors_shards.first().ok_or_else(|| anyhow::anyhow!("no shard"))?;
365 zsfm_tabfm::convert::convert(repo, p, &cfg, &zsfm_tabfm::convert::ConvertOptions { output_dtype: zsfm_gguf::GGMLType::F32 }, &canonical).map_err(|e| anyhow::anyhow!(e.to_string()))?;
366 files.cleanup_weights();
367 zsfm_checkpoint::recast(&canonical, &out, dtype_ty).map_err(|e| anyhow::anyhow!(e.to_string()))?;
368 Ok(())
369 }
370 other => Err(anyhow::anyhow!("unknown model {other:?}; expected one of {}", list_models().join(", ")))
371 }
372 }).map_err(|e: anyhow::Error| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
373 Ok(())
374}
375
376#[pyfunction]
377#[pyo3(signature = (model, model_dir="models", output=None))]
378fn delete(model: &str, model_dir: &str, output: Option<String>) -> PyResult<()> {
379 let (repo, variant_dir): (String, Option<String>) = match model {
381 "toto" => ("Datadog/Toto-2.0-2.5B".into(), None),
382 "chronos" => ("amazon/chronos-2".into(), None),
383 "timesfm" => ("google/timesfm-2.5-200m-pytorch".into(), None),
384 "sundial" => ("thuml/sundial-base-128m".into(), None),
385 "ttm" => ("ibm-granite/granite-timeseries-ttm-r2".into(), None),
386 "lag_llama" | "lag-llama" => ("time-series-foundation-models/Lag-Llama".into(), None),
387 "moment" => ("AutonLab/MOMENT-1-large".into(), None),
388 "moirai" => ("Salesforce/moirai-1.0-R-large".into(), None),
389 "moirai2" | "moirai-2" => ("Salesforce/moirai-2.0-R-small".into(), None),
390 "flowstate" | "flowstate-r1" => ("ibm-granite/granite-timeseries-flowstate-r1".into(), None),
391 "tirex" => ("NX-AI/TiRex".into(), None),
392 "mitra" | "mitra-classification" => ("autogluon/mitra-classifier".into(), Some("mitra-classification".into())),
393 "mitra-regression" => ("autogluon/mitra-regressor".into(), Some("mitra-regression".into())),
394 "tabdpt" => ("Layer6/TabDPT".into(), None),
395 "tabicl" => ("jingang/TabICL".into(), None),
396 "tabpfn" => ("Prior-Labs/tabpfn_3".into(), None),
397 "tabfm" | "tabfm-classification" => ("google/tabfm-1.0.0-pytorch".into(), Some("tabfm-classification".into())),
398 "tabfm-regression" => ("google/tabfm-1.0.0-pytorch".into(), Some("tabfm-regression".into())),
399 _ if model.contains('/') => (model.to_string(), None),
401 _ => return Err(pyo3::exceptions::PyValueError::new_err(format!("unknown model {model:?}"))),
402 };
403 let model_dir = PathBuf::from(model_dir);
404 let canonical = if let Some(v) = variant_dir {
405 let vd = model_dir.join(v);
406 zsfm_hub::canonical_gguf_path(&vd, &repo)
407 } else {
408 zsfm_hub::canonical_gguf_path(&model_dir, &repo)
409 };
410 let out = output.map(PathBuf::from);
411 delete_cached_model(&canonical, out.as_deref()).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
412 Ok(())
413}
414
415use zsfm_toto::config::TotoConfig;
420use zsfm_toto::infer::TotoModel as RustTotoModel;
421
422#[pyclass]
423struct TotoModel {
424 inner: RustTotoModel,
425 _gguf: String,
426}
427
428#[pymethods]
429impl TotoModel {
430 #[new]
431 #[pyo3(signature = (gguf, config=None, context_length=None, use_f64=false))]
432 fn new(gguf: String, config: Option<String>, context_length: Option<usize>, use_f64: bool) -> PyResult<Self> {
433 let gguf_path = PathBuf::from(&gguf);
434 let cfg_path = config.unwrap_or_else(|| "models/Datadog__Toto-2.0-2.5B/config.json".into());
435 let s = std::fs::read_to_string(&cfg_path).map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{cfg_path}: {e}")))?;
436 let v: serde_json::Value = serde_json::from_str(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
437 let max_ctx = context_length.unwrap_or(4096);
438 let inner = RustTotoModel::builder(&gguf_path)
439 .config_json(&v)
440 .with_compute_f64(use_f64)
441 .build()
442 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Toto {gguf}: {e}")))?;
443 let _ = max_ctx;
445 Ok(Self { inner, _gguf: gguf })
446 }
447 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
450 let data = vec![context.clone()];
451 let mask = vec![vec![true; context.len()]];
452 let qmat = self.inner.forecast(&data, &mask, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
453 let median_idx = 4; Ok(qmat[median_idx][0].clone())
455 }
456
457 fn forecast_batch(&self, contexts: Vec<Vec<f32>>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
459 let mut out = Vec::new();
460 for ctx in contexts {
461 let data = vec![ctx.clone()];
462 let mask = vec![vec![true; ctx.len()]];
463 let qmat = self.inner.forecast(&data, &mask, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
464 out.push(qmat[4][0].clone());
465 }
466 Ok(out)
467 }
468
469 fn forecast_quantiles(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
472 let data = vec![context.clone()];
473 let mask = vec![vec![true; context.len()]];
474 let qmat = self.inner.forecast(&data, &mask, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
475 Ok(qmat.into_iter().map(|q| q[0].clone()).collect())
476 }
477
478 fn quantiles(&self) -> Vec<f32> {
480 vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
481 }
482}
483
484use zsfm_timesfm::infer::TimesFMModel as RustTimesFmModel;
485#[pyclass]
486struct TimesFmModel { inner: RustTimesFmModel }
487#[pymethods]
488impl TimesFmModel {
489 #[new]
490 fn new(gguf: String) -> PyResult<Self> {
491 let p = PathBuf::from(&gguf);
492 let inner = RustTimesFmModel::load(&p).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TimesFM {gguf}: {e}")))?;
493 Ok(Self { inner })
494 }
495 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
496 let out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
497 Ok(out.into_iter().next().unwrap_or_default())
498 }
499
500 fn forecast_quantiles(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
504 let mut out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
505 if !out.is_empty() {
506 out.remove(0); }
508 Ok(out)
509 }
510
511 fn quantiles(&self) -> Vec<f32> {
513 vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
514 }
515}
516
517use zsfm_sundial::infer::SundialModel as RustSundialModel;
518#[pyclass]
519struct SundialModel { inner: RustSundialModel }
520#[pymethods]
521impl SundialModel {
522 #[new]
523 fn new(gguf: String) -> PyResult<Self> {
524 let p = PathBuf::from(&gguf);
525 let inner = RustSundialModel::builder(&p).build().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Sundial {gguf}: {e}")))?;
526 Ok(Self { inner })
527 }
528 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
529 let raw = self.inner.forecast(&context, &candle_core::Device::Cpu).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
530 Ok(raw.into_iter().take(horizon).collect())
531 }
532}
533
534use zsfm_ttm::config::TtmConfig;
535use zsfm_ttm::infer::TtmModel as RustTtmModel;
536
537#[pyclass]
538struct TtmModel {
539 inner: RustTtmModel,
540 _config_path: Option<String>,
541}
542
543#[pymethods]
544impl TtmModel {
545 #[new]
546 #[pyo3(signature = (gguf, config=None))]
547 fn new(gguf: String, config: Option<String>) -> PyResult<Self> {
548 let gguf_path = PathBuf::from(&gguf);
549 let ttm_config = if let Some(cfg_path) = &config {
550 let s = std::fs::read_to_string(cfg_path).map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("read config {cfg_path}: {e}")))?;
551 TtmConfig::from_json(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?
552 } else {
553 let canonical = PathBuf::from("models/ibm-granite__granite-timeseries-ttm-r2/config.json");
554 if canonical.exists() {
555 let s = std::fs::read_to_string(&canonical).map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
556 TtmConfig::from_json(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?
557 } else {
558 return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!("config not found at {canonical:?} and no `config` arg given")));
559 }
560 };
561 let inner = RustTtmModel::builder(&gguf_path).config(ttm_config).build().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TTM {gguf}: {e}")))?;
562 Ok(Self { inner, _config_path: config })
563 }
564 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
565 let out = self.inner.forecast(&context).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
566 Ok(out.into_iter().take(horizon).collect())
567 }
568}
569
570use zsfm_lag_llama::config::LagLlamaConfig;
571use zsfm_lag_llama::infer::LagLlamaModel as RustLagLlamaModel;
572#[pyclass]
573struct LagLlamaModel { inner: RustLagLlamaModel }
574#[pymethods]
575impl LagLlamaModel {
576 #[new]
577 fn new(gguf: String) -> PyResult<Self> {
578 let p = PathBuf::from(&gguf);
579 let cfg = LagLlamaConfig::default_from_ckpt();
580 let inner = RustLagLlamaModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load LagLlama {gguf}: {e}")))?;
581 Ok(Self { inner })
582 }
583 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
584 let out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
585 Ok(out)
586 }
587}
588
589use zsfm_moment::config::MomentConfig;
590use zsfm_moment::infer::MomentModel as RustMomentModel;
591#[pyclass]
592struct MomentModel { inner: RustMomentModel }
593#[pymethods]
594impl MomentModel {
595 #[new]
596 fn new(gguf: String) -> PyResult<Self> {
597 let p = PathBuf::from(&gguf);
598 let cfg = MomentConfig::default();
599 let inner = RustMomentModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Moment {gguf}: {e}")))?;
600 Ok(Self { inner })
601 }
602 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
603 let out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
604 Ok(out)
605 }
606}
607
608use zsfm_moirai::config::MoiraiConfig;
609use zsfm_moirai::infer::MoiraiModel as RustMoiraiModel;
610#[pyclass]
611struct MoiraiModel { inner: RustMoiraiModel }
612#[pymethods]
613impl MoiraiModel {
614 #[new]
615 fn new(gguf: String) -> PyResult<Self> {
616 let p = PathBuf::from(&gguf);
617 let cfg = MoiraiConfig::default();
618 let inner = RustMoiraiModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Moirai {gguf}: {e}")))?;
619 Ok(Self { inner })
620 }
621 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
622 let out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
623 Ok(out)
624 }
625}
626
627use zsfm_moirai2::config::Moirai2Config;
628use zsfm_moirai2::infer::Moirai2Model as RustMoirai2Model;
629#[pyclass]
630struct Moirai2Model { inner: RustMoirai2Model }
631#[pymethods]
632impl Moirai2Model {
633 #[new]
634 fn new(gguf: String) -> PyResult<Self> {
635 let p = PathBuf::from(&gguf);
636 let cfg = Moirai2Config::default();
637 let inner = RustMoirai2Model::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Moirai2 {gguf}: {e}")))?;
638 Ok(Self { inner })
639 }
640 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
641 let out = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
642 Ok(out)
643 }
644}
645
646use zsfm_flowstate::config::FlowStateConfig;
647use zsfm_flowstate::infer::FlowStateModel as RustFlowStateModel;
648#[pyclass]
649struct FlowStateModel { inner: RustFlowStateModel }
650#[pymethods]
651impl FlowStateModel {
652 #[new]
653 #[pyo3(signature = (gguf, config=None))]
654 fn new(gguf: String, config: Option<String>) -> PyResult<Self> {
655 let p = PathBuf::from(&gguf);
656 let cfg_path = config.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("models/ibm-granite__granite-timeseries-flowstate-r1/config.json"));
657 let s = std::fs::read_to_string(&cfg_path).map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("read {}: {e}", cfg_path.display())))?;
658 let cfg = FlowStateConfig::from_json(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
659 let inner = RustFlowStateModel::builder(&p).config_from(&cfg).build().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load FlowState {gguf}: {e}")))?;
660 Ok(Self { inner })
661 }
662 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
663 let qmat = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
664 let median_idx = self.inner.config.median_index();
665 Ok(qmat[median_idx].clone())
666 }
667
668 fn forecast_quantiles(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
670 self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
671 }
672
673 fn quantiles(&self) -> Vec<f32> {
675 self.inner.config.quantiles().to_vec()
676 }
677}
678
679use zsfm_tirex::config::TiRexConfig;
680use zsfm_tirex::infer::TiRexModel as RustTirexModel;
681#[pyclass]
682struct TirexModel { inner: RustTirexModel }
683#[pymethods]
684impl TirexModel {
685 #[new]
686 fn new(gguf: String) -> PyResult<Self> {
687 let p = PathBuf::from(&gguf);
688 let cfg = TiRexConfig::default_from_ckpt();
689 let inner = RustTirexModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TiRex {gguf}: {e}")))?;
690 Ok(Self { inner })
691 }
692 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
697 let (_quantiles, median) = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
698 Ok(median)
699 }
700
701 fn forecast_quantiles(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
703 let (quantiles, _median) = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
704 Ok(quantiles)
705 }
706
707 fn quantiles(&self) -> Vec<f32> {
709 TiRexConfig::default_from_ckpt().quantiles
710 }
711}
712
713use zsfm_chronos::config::Chronos2Config;
714use zsfm_chronos::infer::ChronosModel as RustChronosModel;
715
716#[pyclass]
717struct ChronosModel { inner: RustChronosModel }
718#[pymethods]
719impl ChronosModel {
720 #[new]
721 #[pyo3(signature = (gguf, config=None))]
722 fn new(gguf: String, config: Option<String>) -> PyResult<Self> {
723 let gguf_path = PathBuf::from(&gguf);
724 let cfg_path = config.map(PathBuf::from).unwrap_or_else(|| PathBuf::from("models/amazon__chronos-2/config.json"));
725 let s = std::fs::read_to_string(&cfg_path).map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("read {}: {e}", cfg_path.display())))?;
726 let cfg = Chronos2Config::from_json(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
727 let inner = RustChronosModel::builder(&gguf_path).config_from(&cfg).build().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Chronos {gguf}: {e}")))?;
728 Ok(Self { inner })
729 }
730 fn forecast(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<f32>> {
731 let qmat = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
732 let levels = self.inner.config.quantiles();
733 let median_idx = levels.iter().position(|&q| (q - 0.5).abs() < 1e-6).unwrap_or(levels.len()/2);
734 Ok(qmat[median_idx].clone())
735 }
736 fn forecast_quantiles(&self, context: Vec<f32>, horizon: usize) -> PyResult<Vec<Vec<f32>>> {
737 let qmat = self.inner.forecast(&context, horizon).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
738 Ok(qmat)
739 }
740 fn quantiles(&self) -> Vec<f32> { self.inner.config.quantiles().to_vec() }
741}
742
743use zsfm_mitra::config::MitraConfig;
748use zsfm_mitra::MitraModel as RustMitraModel;
749#[pyclass]
750struct MitraModel { inner: RustMitraModel, is_classifier: bool }
751#[pymethods]
752impl MitraModel {
753 #[new]
754 #[pyo3(signature = (gguf, task="classification"))]
755 fn new(gguf: String, task: &str) -> PyResult<Self> {
756 let p = PathBuf::from(&gguf);
757 let cfg = match task {
758 "classification" => MitraConfig::classifier(),
759 "regression" => MitraConfig::regressor(),
760 _ => return Err(pyo3::exceptions::PyValueError::new_err("task must be 'classification' or 'regression'"))
761 };
762 let is_classifier = task == "classification";
763 let inner = RustMitraModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load Mitra {gguf}: {e}")))?;
764 Ok(Self { inner, is_classifier })
765 }
766 fn predict_classification(&self, x_support: Vec<Vec<f32>>, y_support: Vec<usize>, x_query: Vec<Vec<f32>>, n_classes: usize) -> PyResult<Vec<Vec<f32>>> {
767 if !self.is_classifier { return Err(pyo3::exceptions::PyValueError::new_err("model was loaded as regressor, not classifier")) }
768 let logits = self.inner.predict_classification(&x_support, &y_support, &x_query, n_classes).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
769 Ok(logits)
770 }
771 fn predict_regression(&self, x_support: Vec<Vec<f32>>, y_support: Vec<f32>, x_query: Vec<Vec<f32>>) -> PyResult<Vec<f32>> {
772 if self.is_classifier { return Err(pyo3::exceptions::PyValueError::new_err("model was loaded as classifier, not regressor")) }
773 let out = self.inner.predict_regression(&x_support, &y_support, &x_query).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
774 Ok(out)
775 }
776}
777
778use zsfm_tabdpt::config::TabDptConfig;
779use zsfm_tabdpt::TabDptModel as RustTabDptModel;
780#[pyclass]
781struct TabDptModel { inner: RustTabDptModel }
782#[pymethods]
783impl TabDptModel {
784 #[new]
785 fn new(gguf: String) -> PyResult<Self> {
786 let p = PathBuf::from(&gguf);
787 let cfg = TabDptConfig::default_v1_2();
788 let inner = RustTabDptModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TabDPT {gguf}: {e}")))?;
789 Ok(Self { inner })
790 }
791 fn predict_classification(&self, x_support: Vec<Vec<f32>>, y_support: Vec<usize>, x_query: Vec<Vec<f32>>, n_classes: usize) -> PyResult<Vec<Vec<f32>>> {
792 let out = self.inner.predict_classification(&x_support, &y_support, &x_query, n_classes).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
793 Ok(out)
794 }
795 fn predict_regression(&self, x_support: Vec<Vec<f32>>, y_support: Vec<f32>, x_query: Vec<Vec<f32>>) -> PyResult<Vec<f32>> {
796 let out = self.inner.predict_regression(&x_support, &y_support, &x_query).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
797 Ok(out)
798 }
799}
800
801use zsfm_tabicl::config::TabIclConfig;
802use zsfm_tabicl::TabIclModel as RustTabIclModel;
803#[pyclass]
804struct TabIclModel { inner: RustTabIclModel }
805#[pymethods]
806impl TabIclModel {
807 #[new]
808 fn new(gguf: String) -> PyResult<Self> {
809 let p = PathBuf::from(&gguf);
810 let cfg = TabIclConfig::v2();
811 let inner = RustTabIclModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TabICL {gguf}: {e}")))?;
812 Ok(Self { inner })
813 }
814 fn predict_classification(&self, x_support: Vec<Vec<f32>>, y_support: Vec<usize>, x_query: Vec<Vec<f32>>, n_classes: usize) -> PyResult<Vec<Vec<f32>>> {
815 let out = self.inner.predict_classification(&x_support, &y_support, &x_query, n_classes).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
816 Ok(out)
817 }
818}
819
820use zsfm_tabpfn::config::TabPfnConfig;
821use zsfm_tabpfn::TabPfnModel as RustTabPfnModel;
822#[pyclass]
823struct TabPfnModel { inner: RustTabPfnModel }
824#[pymethods]
825impl TabPfnModel {
826 #[new]
827 fn new(gguf: String) -> PyResult<Self> {
828 let p = PathBuf::from(&gguf);
829 let cfg = TabPfnConfig::v3_default();
830 let inner = RustTabPfnModel::load(&p, cfg).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TabPFN {gguf}: {e}")))?;
831 Ok(Self { inner })
832 }
833 fn predict_classification(&self, x_support: Vec<Vec<f32>>, y_support: Vec<usize>, x_query: Vec<Vec<f32>>, n_classes: usize) -> PyResult<Vec<Vec<f32>>> {
834 let out = self.inner.predict_classification(&x_support, &y_support, &x_query, n_classes).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
835 Ok(out)
836 }
837}
838
839use zsfm_tabfm::config::TabFMConfig;
840use zsfm_tabfm::TabFMModel as RustTabFMModel;
841#[pyclass]
842struct TabFmModel { inner: RustTabFMModel, is_classifier: bool }
843#[pymethods]
844impl TabFmModel {
845 #[new]
846 #[pyo3(signature = (gguf, config=None))]
847 fn new(gguf: String, config: Option<String>) -> PyResult<Self> {
848 let p = PathBuf::from(&gguf);
849 let cfg_path = if let Some(c) = config { PathBuf::from(c) } else {
850 let cand = PathBuf::from("models/tabfm-classification/google__tabfm-1.0.0-pytorch/classification_config.json");
852 if cand.exists() { cand } else { PathBuf::from("models/tabfm-regression/google__tabfm-1.0.0-pytorch/regression_config.json") }
853 };
854 let s = std::fs::read_to_string(&cfg_path).map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("read {}: {e}", cfg_path.display())))?;
855 let cfg = TabFMConfig::from_json(&s).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
856 let is_classifier = cfg.is_classifier;
857 let inner = RustTabFMModel::builder(&p).config_from(&cfg).build().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("load TabFM {gguf}: {e}")))?;
858 Ok(Self { inner, is_classifier })
859 }
860 fn predict(&self, x: Vec<Vec<f32>>, y: Vec<f32>, train_size: usize) -> PyResult<Vec<Vec<f32>>> {
861 let out = self.inner.predict(&x, &y, train_size, None, None).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
862 Ok(out)
863 }
864}