use std::path::PathBuf;
use ndarray::{Array2, ArrayView4};
use crate::{
config::RuntimeConfig,
error::{RapidOcrError, Result},
formula::contract::validate_formula_contract,
model_verify::VerifiedModel,
runtime::{provider::ProviderResolution, session::OrtSession},
};
#[derive(Debug)]
pub struct FormulaSession {
inner: OrtSession,
input_name: String,
output_name: String,
model_path: PathBuf,
}
impl FormulaSession {
pub fn new(model: &VerifiedModel, runtime_cfg: &RuntimeConfig) -> Result<Self> {
let model_path = model.path();
let inner = OrtSession::open_verified(model, runtime_cfg, &[])?;
let probe = inner.probe_io()?;
let contract = validate_formula_contract(&probe, model_path)?;
Ok(Self {
inner,
input_name: contract.input_name,
output_name: contract.output_name,
model_path: model_path.to_path_buf(),
})
}
pub fn input_name(&self) -> &str {
&self.input_name
}
pub fn output_name(&self) -> &str {
&self.output_name
}
pub fn provider_resolution(&self) -> ProviderResolution {
self.inner.provider_resolution()
}
pub fn session_threads(&self) -> (Option<usize>, Option<usize>) {
self.inner.session_threads()
}
pub fn character_metadata(&self) -> Result<Option<String>> {
self.inner.metadata_custom("character")
}
pub fn run(&mut self, input: ArrayView4<'_, f32>) -> Result<Array2<i64>> {
let shape = input.shape();
if shape[1] != 1 || shape[2] != 384 || shape[3] != 384 {
return Err(RapidOcrError::InvalidInput(format!(
"formula input must be [N,1,384,384], got {:?} (model={})",
shape,
self.model_path.display()
)));
}
self.inner.run_i64_2d(&self.input_name, input)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use ndarray::Array4;
use super::*;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/formula-onnx")
.join(name)
}
fn cred(name: &str) -> VerifiedModel {
VerifiedModel::verify(&fixture(name), None).expect("fixture credential")
}
fn rt() -> RuntimeConfig {
RuntimeConfig::default()
}
#[test]
fn accepts_formula_contract_and_runs() {
let mut session = FormulaSession::new(&cred("formula_ok.onnx"), &rt())
.expect("formula fixture should load");
assert_eq!(session.input_name(), "x");
assert_eq!(session.output_name(), "fetch_name_0");
let input = Array4::<f32>::zeros((1, 1, 384, 384));
let output = session
.run(input.view())
.expect("formula fixture should run");
assert_eq!(output.nrows(), 1);
assert!(output.ncols() >= 1);
let resolution = session.provider_resolution();
assert_eq!(
resolution.requested,
RuntimeConfig::default().provider_preference
);
if resolution.selected_ep == crate::runtime::provider::ResolvedExecutionProvider::Cpu {
assert!(
resolution.fallback_used
|| resolution.requested == crate::config::ProviderPreference::Cpu,
"a CPU session requested through the default DirectML path must report fallback"
);
}
}
#[test]
fn rejects_input_rank() {
let error = FormulaSession::new(&cred("formula_input_rank3.onnx"), &rt())
.expect_err("rank3 input must fail");
assert!(error.to_string().contains("rank"), "error: {error}");
}
#[test]
fn rejects_input_dtype() {
let error = FormulaSession::new(&cred("formula_input_int64.onnx"), &rt())
.expect_err("int64 input must fail");
assert!(error.to_string().contains("FLOAT32"), "error: {error}");
}
#[test]
fn rejects_input_spatial_dims() {
let error = FormulaSession::new(&cred("formula_input_512.onnx"), &rt())
.expect_err("512 input must fail");
assert!(error.to_string().contains("384"), "error: {error}");
}
#[test]
fn rejects_output_rank() {
let error = FormulaSession::new(&cred("formula_output_rank1.onnx"), &rt())
.expect_err("rank1 output must fail");
assert!(error.to_string().contains("rank"), "error: {error}");
}
#[test]
fn rejects_output_dtype() {
let error = FormulaSession::new(&cred("formula_output_f32.onnx"), &rt())
.expect_err("float output must fail");
assert!(error.to_string().contains("INT64"), "error: {error}");
}
#[test]
fn rejects_multi_input_and_output() {
let input_error = FormulaSession::new(&cred("formula_multi_input.onnx"), &rt())
.expect_err("multi input must fail");
assert!(
input_error.to_string().contains("exactly one input"),
"error: {input_error}"
);
let output_error = FormulaSession::new(&cred("formula_multi_output.onnx"), &rt())
.expect_err("multi output must fail");
assert!(
output_error.to_string().contains("exactly one output"),
"error: {output_error}"
);
}
#[test]
fn wrong_input_shape_is_rejected_before_run() {
let mut session = FormulaSession::new(&cred("formula_ok.onnx"), &rt())
.expect("formula fixture should load");
let input = Array4::<f32>::zeros((1, 1, 512, 384));
let error = session
.run(input.view())
.expect_err("bad input shape must fail");
assert!(
error.to_string().contains("[N,1,384,384]"),
"error: {error}"
);
}
#[test]
#[cfg(not(feature = "cuda-provider"))]
fn unavailable_provider_is_rejected_even_without_strict_flag() {
let runtime = RuntimeConfig {
provider_preference: crate::config::ProviderPreference::Cuda { device_id: 0 },
fail_if_provider_unavailable: true,
..RuntimeConfig::default()
};
let error = FormulaSession::new(&cred("formula_ok.onnx"), &runtime)
.expect_err("unavailable accelerator must not fall back to CPU");
assert!(
matches!(error, RapidOcrError::UnsupportedProvider(_)),
"error: {error}"
);
}
#[test]
fn explicit_cpu_preference_is_accepted() {
let runtime = RuntimeConfig {
provider_preference: crate::config::ProviderPreference::Cpu,
fail_if_provider_unavailable: true,
..RuntimeConfig::default()
};
let session = FormulaSession::new(&cred("formula_ok.onnx"), &runtime)
.expect("explicit CPU must be accepted");
assert_eq!(
session.provider_resolution().selected_ep,
crate::runtime::provider::ResolvedExecutionProvider::Cpu
);
}
#[test]
fn standalone_session_uses_the_shared_thread_policy() {
let runtime = RuntimeConfig::default();
let expected = runtime.effective_session_threads();
assert_eq!(
expected,
(
Some(crate::runtime::profile::auto_tuned_thread_budget()),
Some(1)
),
"a default `RuntimeConfig` derives intra = budget and inter = 1"
);
let session = FormulaSession::new(&cred("formula_ok.onnx"), &runtime)
.expect("formula fixture should load");
assert_eq!(
session.session_threads(),
expected,
"the standalone formula path must ask ORT for the derived thread counts"
);
let plan = crate::runtime::profile::RuntimeProfile::plan(&runtime, false);
assert_eq!(
(plan.threads.ort_intra, plan.threads.ort_inter),
expected,
"engine plan and standalone session must agree by construction"
);
}
#[test]
fn standalone_session_honours_explicit_threads() {
let runtime = RuntimeConfig {
intra_threads: Some(3),
inter_threads: None,
auto_tune_threads: false,
..RuntimeConfig::default()
};
assert_eq!(runtime.effective_session_threads(), (Some(3), None));
let session = FormulaSession::new(&cred("formula_ok.onnx"), &runtime)
.expect("formula fixture should load");
assert_eq!(session.session_threads(), (Some(3), None));
}
}