Skip to main content

sparse_ir_capi/
types.rs

1//! Opaque types for C API
2//!
3//! All Rust objects are wrapped in opaque pointers to hide implementation
4//! details from C code.
5
6use crate::{SPIR_STATISTICS_BOSONIC, SPIR_STATISTICS_FERMIONIC};
7use mdarray::{DynRank, Slice, ViewMut};
8use num_complex::Complex;
9use sparse_ir::basis::FiniteTempBasis;
10use sparse_ir::fitters::InplaceFitter;
11use sparse_ir::freq::MatsubaraFreq;
12use sparse_ir::gemm::GemmBackendHandle;
13use sparse_ir::kernel::{AbstractKernel, CentrosymmKernel, LogisticKernel, RegularizedBoseKernel};
14use sparse_ir::poly::PiecewiseLegendrePolyVector;
15use sparse_ir::polyfourier::PiecewiseLegendreFTVector;
16use sparse_ir::sve::SVEResult;
17use sparse_ir::taufuncs::normalize_tau;
18use sparse_ir::traits::Statistics;
19use sparse_ir::{Bosonic, Fermionic};
20use std::sync::Arc;
21
22/// Convert Statistics enum to C-API integer
23#[inline]
24#[allow(dead_code)]
25pub(crate) fn statistics_to_c(stats: Statistics) -> i32 {
26    match stats {
27        Statistics::Fermionic => SPIR_STATISTICS_FERMIONIC,
28        Statistics::Bosonic => SPIR_STATISTICS_BOSONIC,
29    }
30}
31
32/// Convert C-API integer to Statistics enum
33#[inline]
34#[allow(dead_code)]
35pub(crate) fn statistics_from_c(value: i32) -> Result<Statistics, i32> {
36    match value {
37        SPIR_STATISTICS_FERMIONIC => Ok(Statistics::Fermionic),
38        SPIR_STATISTICS_BOSONIC => Ok(Statistics::Bosonic),
39        _ => Err(value),
40    }
41}
42
43/// Function domain type for continuous functions
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum FunctionDomain {
46    /// Tau domain with periodicity (statistics-dependent)
47    Tau(Statistics),
48    /// Omega (frequency) domain without periodicity
49    Omega,
50}
51
52impl FunctionDomain {
53    /// Check if this is a tau function with the given statistics
54    #[allow(dead_code)]
55    pub(crate) fn is_tau_with_statistics(&self, stats: Statistics) -> bool {
56        matches!(self, FunctionDomain::Tau(s) if *s == stats)
57    }
58
59    /// Check if this is an omega function
60    #[allow(dead_code)]
61    pub(crate) fn is_omega(&self) -> bool {
62        matches!(self, FunctionDomain::Omega)
63    }
64}
65
66/// Opaque kernel type for C API (compatible with libsparseir)
67///
68/// This is a tagged union that can hold either LogisticKernel or RegularizedBoseKernel.
69/// The actual type is determined by which constructor was used.
70///
71/// Note: Named `spir_kernel` to match libsparseir C++ API exactly.
72/// The internal structure is hidden using a void pointer to prevent exposing KernelType to C.
73#[repr(C)]
74pub struct spir_kernel {
75    pub(crate) _private: *const std::ffi::c_void,
76}
77
78/// Opaque SVE result type for C API (compatible with libsparseir)
79///
80/// Contains singular values and singular functions from SVE computation.
81///
82/// Note: Named `spir_sve_result` to match libsparseir C++ API exactly.
83/// The internal structure is hidden using a void pointer to prevent exposing `Arc<SVEResult>` to C.
84#[repr(C)]
85pub struct spir_sve_result {
86    pub(crate) _private: *const std::ffi::c_void,
87}
88
89/// Opaque basis type for C API (compatible with libsparseir)
90///
91/// Represents a finite temperature basis (IR or DLR).
92///
93/// Note: Named `spir_basis` to match libsparseir C++ API exactly.
94/// The internal structure is hidden using a void pointer to prevent exposing BasisType to C.
95#[repr(C)]
96pub struct spir_basis {
97    pub(crate) _private: *const std::ffi::c_void,
98}
99
100/// Internal basis type (not exposed to C)
101#[derive(Clone)]
102pub(crate) enum BasisType {
103    LogisticFermionic(Arc<FiniteTempBasis<LogisticKernel, Fermionic>>),
104    LogisticBosonic(Arc<FiniteTempBasis<LogisticKernel, Bosonic>>),
105    RegularizedBoseFermionic(Arc<FiniteTempBasis<RegularizedBoseKernel, Fermionic>>),
106    RegularizedBoseBosonic(Arc<FiniteTempBasis<RegularizedBoseKernel, Bosonic>>),
107    // DLR (Discrete Lehmann Representation) variants
108    // Note: DLR always uses LogisticKernel internally, regardless of input kernel type
109    DLRFermionic(Arc<sparse_ir::dlr::DiscreteLehmannRepresentation<Fermionic>>),
110    DLRBosonic(Arc<sparse_ir::dlr::DiscreteLehmannRepresentation<Bosonic>>),
111}
112
113/// Internal kernel type (not exposed to C)
114#[derive(Clone)]
115pub(crate) enum KernelType {
116    Logistic(Arc<LogisticKernel>),
117    RegularizedBose(Arc<RegularizedBoseKernel>),
118}
119
120impl spir_kernel {
121    /// Get a reference to the inner KernelType
122    pub(crate) fn inner(&self) -> &KernelType {
123        unsafe { &*(self._private as *const KernelType) }
124    }
125
126    pub(crate) fn new_logistic(lambda: f64) -> Self {
127        let inner = KernelType::Logistic(Arc::new(LogisticKernel::new(lambda)));
128        Self {
129            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
130        }
131    }
132
133    pub(crate) fn new_regularized_bose(lambda: f64) -> Self {
134        let inner = KernelType::RegularizedBose(Arc::new(RegularizedBoseKernel::new(lambda)));
135        Self {
136            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
137        }
138    }
139
140    pub(crate) fn lambda(&self) -> f64 {
141        match self.inner() {
142            KernelType::Logistic(k) => k.lambda(),
143            KernelType::RegularizedBose(k) => k.lambda(),
144        }
145    }
146
147    pub(crate) fn compute(&self, x: f64, y: f64) -> f64 {
148        match self.inner() {
149            KernelType::Logistic(k) => k.compute(x, y),
150            KernelType::RegularizedBose(k) => k.compute(x, y),
151        }
152    }
153
154    /// Get the inner kernel for SVE computation
155    pub(crate) fn as_logistic(&self) -> Option<&Arc<LogisticKernel>> {
156        match self.inner() {
157            KernelType::Logistic(k) => Some(k),
158            _ => None,
159        }
160    }
161
162    pub(crate) fn as_regularized_bose(&self) -> Option<&Arc<RegularizedBoseKernel>> {
163        match self.inner() {
164            KernelType::RegularizedBose(k) => Some(k),
165            _ => None,
166        }
167    }
168
169    /// Get kernel domain boundaries (xmin, xmax, ymin, ymax)
170    pub(crate) fn domain(&self) -> (f64, f64, f64, f64) {
171        // Both kernel types have domain [-1, 1] × [-1, 1]
172        (-1.0, 1.0, -1.0, 1.0)
173    }
174}
175
176impl Clone for spir_kernel {
177    fn clone(&self) -> Self {
178        // Cheap clone: KernelType::clone internally uses Arc::clone which is cheap
179        let inner = self.inner().clone();
180        Self {
181            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
182        }
183    }
184}
185
186impl Drop for spir_kernel {
187    fn drop(&mut self) {
188        unsafe {
189            if !self._private.is_null() {
190                let _ = Box::from_raw(self._private as *const KernelType as *mut KernelType);
191            }
192        }
193    }
194}
195
196impl spir_sve_result {
197    /// Get a reference to the inner Arc<SVEResult>
198    fn inner_arc(&self) -> &Arc<SVEResult> {
199        unsafe { &*(self._private as *const Arc<SVEResult>) }
200    }
201
202    pub(crate) fn new(sve_result: SVEResult) -> Self {
203        let inner = Arc::new(sve_result);
204        Self {
205            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
206        }
207    }
208
209    pub(crate) fn size(&self) -> usize {
210        self.inner_arc().s.len()
211    }
212
213    pub(crate) fn svals(&self) -> &[f64] {
214        &self.inner_arc().s
215    }
216
217    #[allow(dead_code)]
218    pub(crate) fn epsilon(&self) -> f64 {
219        self.inner_arc().epsilon
220    }
221
222    #[allow(dead_code)]
223    pub(crate) fn truncate(&self, epsilon: f64, max_size: Option<usize>) -> Self {
224        let (u_part, s_part, v_part) = self.inner_arc().part(Some(epsilon), max_size);
225        let truncated = SVEResult::new(u_part, s_part, v_part, epsilon);
226        Self::new(truncated)
227    }
228
229    /// Get inner SVEResult for basis construction
230    pub(crate) fn inner(&self) -> &Arc<SVEResult> {
231        self.inner_arc()
232    }
233}
234
235impl Clone for spir_sve_result {
236    fn clone(&self) -> Self {
237        // Cheap clone: Arc::clone just increments reference count
238        let inner = self.inner_arc().clone();
239        Self {
240            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
241        }
242    }
243}
244
245impl Drop for spir_sve_result {
246    fn drop(&mut self) {
247        unsafe {
248            if !self._private.is_null() {
249                let _ =
250                    Box::from_raw(self._private as *const Arc<SVEResult> as *mut Arc<SVEResult>);
251            }
252        }
253    }
254}
255
256impl spir_basis {
257    /// Get a reference to the inner BasisType (for internal use by other modules)
258    pub(crate) fn inner(&self) -> &BasisType {
259        unsafe { &*(self._private as *const BasisType) }
260    }
261
262    fn inner_type(&self) -> &BasisType {
263        self.inner()
264    }
265
266    pub(crate) fn new_logistic_fermionic(
267        basis: FiniteTempBasis<LogisticKernel, Fermionic>,
268    ) -> Self {
269        let inner = BasisType::LogisticFermionic(Arc::new(basis));
270        Self {
271            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
272        }
273    }
274
275    pub(crate) fn new_logistic_bosonic(basis: FiniteTempBasis<LogisticKernel, Bosonic>) -> Self {
276        let inner = BasisType::LogisticBosonic(Arc::new(basis));
277        Self {
278            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
279        }
280    }
281
282    pub(crate) fn new_regularized_bose_fermionic(
283        basis: FiniteTempBasis<RegularizedBoseKernel, Fermionic>,
284    ) -> Self {
285        let inner = BasisType::RegularizedBoseFermionic(Arc::new(basis));
286        Self {
287            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
288        }
289    }
290
291    pub(crate) fn new_regularized_bose_bosonic(
292        basis: FiniteTempBasis<RegularizedBoseKernel, Bosonic>,
293    ) -> Self {
294        let inner = BasisType::RegularizedBoseBosonic(Arc::new(basis));
295        Self {
296            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
297        }
298    }
299
300    pub(crate) fn new_dlr_fermionic(
301        dlr: Arc<sparse_ir::dlr::DiscreteLehmannRepresentation<Fermionic>>,
302    ) -> Self {
303        let inner = BasisType::DLRFermionic(dlr);
304        Self {
305            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
306        }
307    }
308
309    pub(crate) fn new_dlr_bosonic(
310        dlr: Arc<sparse_ir::dlr::DiscreteLehmannRepresentation<Bosonic>>,
311    ) -> Self {
312        let inner = BasisType::DLRBosonic(dlr);
313        Self {
314            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
315        }
316    }
317
318    pub(crate) fn size(&self) -> usize {
319        match self.inner_type() {
320            BasisType::LogisticFermionic(b) => b.size(),
321            BasisType::LogisticBosonic(b) => b.size(),
322            BasisType::RegularizedBoseFermionic(b) => b.size(),
323            BasisType::RegularizedBoseBosonic(b) => b.size(),
324            BasisType::DLRFermionic(dlr) => dlr.poles.len(),
325            BasisType::DLRBosonic(dlr) => dlr.poles.len(),
326        }
327    }
328
329    pub(crate) fn svals(&self) -> Vec<f64> {
330        match self.inner_type() {
331            BasisType::LogisticFermionic(b) => b.s().to_vec(),
332            BasisType::LogisticBosonic(b) => b.s().to_vec(),
333            BasisType::RegularizedBoseFermionic(b) => b.s().to_vec(),
334            BasisType::RegularizedBoseBosonic(b) => b.s().to_vec(),
335            // DLR: no singular values, return empty
336            BasisType::DLRFermionic(_) | BasisType::DLRBosonic(_) => vec![],
337        }
338    }
339
340    pub(crate) fn statistics(&self) -> i32 {
341        // 0 = Bosonic, 1 = Fermionic (matching libsparseir)
342        match self.inner_type() {
343            BasisType::LogisticFermionic(_) => 1,
344            BasisType::LogisticBosonic(_) => 0,
345            BasisType::RegularizedBoseFermionic(_) => 1,
346            BasisType::RegularizedBoseBosonic(_) => 0,
347            BasisType::DLRFermionic(_) => 1,
348            BasisType::DLRBosonic(_) => 0,
349        }
350    }
351
352    pub(crate) fn beta(&self) -> f64 {
353        match self.inner_type() {
354            BasisType::LogisticFermionic(b) => b.beta(),
355            BasisType::LogisticBosonic(b) => b.beta(),
356            BasisType::RegularizedBoseFermionic(b) => b.beta(),
357            BasisType::RegularizedBoseBosonic(b) => b.beta(),
358            BasisType::DLRFermionic(dlr) => dlr.beta,
359            BasisType::DLRBosonic(dlr) => dlr.beta,
360        }
361    }
362
363    #[allow(dead_code)]
364    pub(crate) fn wmax(&self) -> f64 {
365        match self.inner_type() {
366            BasisType::LogisticFermionic(b) => b.wmax(),
367            BasisType::LogisticBosonic(b) => b.wmax(),
368            BasisType::RegularizedBoseFermionic(b) => b.wmax(),
369            BasisType::RegularizedBoseBosonic(b) => b.wmax(),
370            BasisType::DLRFermionic(dlr) => dlr.wmax,
371            BasisType::DLRBosonic(dlr) => dlr.wmax,
372        }
373    }
374
375    pub(crate) fn default_tau_sampling_points(&self) -> Vec<f64> {
376        match self.inner_type() {
377            BasisType::LogisticFermionic(b) => b.default_tau_sampling_points(),
378            BasisType::LogisticBosonic(b) => b.default_tau_sampling_points(),
379            BasisType::RegularizedBoseFermionic(b) => b.default_tau_sampling_points(),
380            BasisType::RegularizedBoseBosonic(b) => b.default_tau_sampling_points(),
381            // DLR: no default tau sampling points
382            BasisType::DLRFermionic(_) | BasisType::DLRBosonic(_) => vec![],
383        }
384    }
385
386    pub(crate) fn default_tau_sampling_points_size_requested(
387        &self,
388        size_requested: usize,
389    ) -> Vec<f64> {
390        match self.inner_type() {
391            BasisType::LogisticFermionic(b) => {
392                b.default_tau_sampling_points_size_requested(size_requested)
393            }
394            BasisType::LogisticBosonic(b) => {
395                b.default_tau_sampling_points_size_requested(size_requested)
396            }
397            BasisType::RegularizedBoseFermionic(b) => {
398                b.default_tau_sampling_points_size_requested(size_requested)
399            }
400            BasisType::RegularizedBoseBosonic(b) => {
401                b.default_tau_sampling_points_size_requested(size_requested)
402            }
403            // DLR: no default tau sampling points
404            BasisType::DLRFermionic(_) | BasisType::DLRBosonic(_) => vec![],
405        }
406    }
407
408    pub(crate) fn default_matsubara_sampling_points(&self, positive_only: bool) -> Vec<i64> {
409        match self.inner_type() {
410            BasisType::LogisticFermionic(b) => {
411                b.default_matsubara_sampling_points_i64(positive_only)
412            }
413            BasisType::LogisticBosonic(b) => b.default_matsubara_sampling_points_i64(positive_only),
414            BasisType::RegularizedBoseFermionic(b) => {
415                b.default_matsubara_sampling_points_i64(positive_only)
416            }
417            BasisType::RegularizedBoseBosonic(b) => {
418                b.default_matsubara_sampling_points_i64(positive_only)
419            }
420            // DLR: no default Matsubara sampling points
421            BasisType::DLRFermionic(_) | BasisType::DLRBosonic(_) => vec![],
422        }
423    }
424
425    pub(crate) fn default_matsubara_sampling_points_with_mitigate(
426        &self,
427        positive_only: bool,
428        mitigate: bool,
429        n_points: usize,
430    ) -> Vec<i64> {
431        match self.inner_type() {
432            BasisType::LogisticFermionic(b) => b
433                .default_matsubara_sampling_points_i64_with_mitigate(
434                    positive_only,
435                    mitigate,
436                    n_points,
437                ),
438            BasisType::LogisticBosonic(b) => b.default_matsubara_sampling_points_i64_with_mitigate(
439                positive_only,
440                mitigate,
441                n_points,
442            ),
443            BasisType::RegularizedBoseFermionic(b) => b
444                .default_matsubara_sampling_points_i64_with_mitigate(
445                    positive_only,
446                    mitigate,
447                    n_points,
448                ),
449            BasisType::RegularizedBoseBosonic(b) => b
450                .default_matsubara_sampling_points_i64_with_mitigate(
451                    positive_only,
452                    mitigate,
453                    n_points,
454                ),
455            // DLR: no default Matsubara sampling points
456            BasisType::DLRFermionic(_) | BasisType::DLRBosonic(_) => vec![],
457        }
458    }
459
460    pub(crate) fn default_omega_sampling_points(&self) -> Vec<f64> {
461        match self.inner_type() {
462            BasisType::LogisticFermionic(b) => b.default_omega_sampling_points(),
463            BasisType::LogisticBosonic(b) => b.default_omega_sampling_points(),
464            BasisType::RegularizedBoseFermionic(b) => b.default_omega_sampling_points(),
465            BasisType::RegularizedBoseBosonic(b) => b.default_omega_sampling_points(),
466            // DLR: return poles as omega sampling points
467            BasisType::DLRFermionic(dlr) => dlr.poles.clone(),
468            BasisType::DLRBosonic(dlr) => dlr.poles.clone(),
469        }
470    }
471}
472
473impl Clone for spir_basis {
474    fn clone(&self) -> Self {
475        // Cheap clone: BasisType::clone internally uses Arc::clone which is cheap
476        let inner = self.inner_type().clone();
477        Self {
478            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
479        }
480    }
481}
482
483impl Drop for spir_basis {
484    fn drop(&mut self) {
485        unsafe {
486            if !self._private.is_null() {
487                let _ = Box::from_raw(self._private as *const BasisType as *mut BasisType);
488            }
489        }
490    }
491}
492
493// ============================================================================
494// Wrapper types for different function representations
495// ============================================================================
496
497/// Wrapper for PiecewiseLegendrePolyVector with domain information
498#[derive(Clone)]
499pub(crate) struct PolyVectorFuncs {
500    pub poly: Arc<PiecewiseLegendrePolyVector>,
501    pub domain: FunctionDomain,
502}
503
504impl PolyVectorFuncs {
505    /// Evaluate all functions at a single point
506    pub fn evaluate_at(&self, x: f64, beta: f64) -> Vec<f64> {
507        // Normalize x based on domain
508        let (x_reg, sign) = match self.domain {
509            FunctionDomain::Tau(Statistics::Fermionic) => {
510                // u functions (fermionic): normalize tau to [0, beta]
511                normalize_tau::<Fermionic>(x, beta)
512            }
513            FunctionDomain::Tau(Statistics::Bosonic) => {
514                // u functions (bosonic): normalize tau to [0, beta]
515                normalize_tau::<Bosonic>(x, beta)
516            }
517            FunctionDomain::Omega => {
518                // v functions: no normalization needed
519                (x, 1.0)
520            }
521        };
522
523        // Evaluate all polynomials at the normalized point
524        self.poly
525            .polyvec
526            .iter()
527            .map(|p| sign * p.evaluate(x_reg))
528            .collect()
529    }
530
531    /// Batch evaluate all functions at multiple points
532    /// Returns Vec<Vec<f64>> where result[i][j] is function i evaluated at point j
533    pub fn batch_evaluate_at(&self, xs: &[f64], beta: f64) -> Vec<Vec<f64>> {
534        let n_funcs = self.poly.polyvec.len();
535        let n_points = xs.len();
536        let mut result = vec![vec![0.0; n_points]; n_funcs];
537
538        // Normalize all points based on domain
539        let normalized: Vec<(f64, f64)> = xs
540            .iter()
541            .map(|&x| match self.domain {
542                FunctionDomain::Tau(Statistics::Fermionic) => normalize_tau::<Fermionic>(x, beta),
543                FunctionDomain::Tau(Statistics::Bosonic) => normalize_tau::<Bosonic>(x, beta),
544                FunctionDomain::Omega => (x, 1.0),
545            })
546            .collect();
547
548        // Extract normalized x values and signs
549        let xs_reg: Vec<f64> = normalized.iter().map(|(x, _)| *x).collect();
550        let signs: Vec<f64> = normalized.iter().map(|(_, s)| *s).collect();
551
552        // Evaluate each polynomial at all regularized points using evaluate_many
553        for (i, p) in self.poly.polyvec.iter().enumerate() {
554            let values = p.evaluate_many(&xs_reg);
555            for (j, &val) in values.iter().enumerate() {
556                result[i][j] = signs[j] * val;
557            }
558        }
559
560        result
561    }
562}
563
564/// Wrapper for Fourier-transformed functions (PiecewiseLegendreFTVector)
565#[derive(Clone)]
566pub(crate) struct FTVectorFuncs {
567    pub ft_fermionic: Option<Arc<PiecewiseLegendreFTVector<Fermionic>>>,
568    pub ft_bosonic: Option<Arc<PiecewiseLegendreFTVector<Bosonic>>>,
569    pub statistics: Statistics,
570}
571
572/// Wrapper for DLR functions in tau domain
573#[derive(Clone)]
574pub(crate) struct DLRTauFuncs {
575    pub poles: Vec<f64>,
576    pub beta: f64,
577    pub wmax: f64,
578    pub pole_weights: Vec<f64>,
579    pub kernel_ypower: i32,
580    pub statistics: Statistics,
581}
582
583impl DLRTauFuncs {
584    fn zero_pole_tau_limit(&self) -> f64 {
585        match self.kernel_ypower {
586            0 => -0.5,
587            1 => -1.0 / (self.beta * self.wmax * self.wmax),
588            _ => panic!(
589                "DLR tau evaluation does not support kernel ypower = {}",
590                self.kernel_ypower
591            ),
592        }
593    }
594
595    fn evaluate_single(&self, tau: f64, pole: f64, pole_weight: f64) -> f64 {
596        match self.statistics {
597            Statistics::Fermionic => {
598                let (tau_reg, sign) = normalize_tau::<Fermionic>(tau, self.beta);
599                let value = if pole >= 0.0 {
600                    -(-pole * tau_reg).exp() / (1.0 + (-self.beta * pole).exp())
601                } else {
602                    -(pole * (self.beta - tau_reg)).exp() / (1.0 + (self.beta * pole).exp())
603                };
604                sign * value * pole_weight
605            }
606            Statistics::Bosonic => {
607                let (tau_reg, sign) = normalize_tau::<Bosonic>(tau, self.beta);
608                if pole == 0.0 {
609                    sign * self.zero_pole_tau_limit()
610                } else if pole > 0.0 {
611                    let denominator = -(-self.beta * pole).exp_m1();
612                    sign * (-(-tau_reg * pole).exp() * pole_weight / denominator)
613                } else {
614                    let denominator = -(self.beta * pole).exp_m1();
615                    sign * ((pole * (self.beta - tau_reg)).exp() * pole_weight / denominator)
616                }
617            }
618        }
619    }
620
621    /// Evaluate all DLR tau functions at a single point
622    pub fn evaluate_at(&self, tau: f64) -> Vec<f64> {
623        self.poles
624            .iter()
625            .zip(self.pole_weights.iter())
626            .map(|(&pole, &pole_weight)| self.evaluate_single(tau, pole, pole_weight))
627            .collect()
628    }
629
630    /// Batch evaluate all DLR tau functions at multiple points
631    /// Returns Vec<Vec<f64>> where result[i][j] is function i evaluated at point j
632    pub fn batch_evaluate_at(&self, taus: &[f64]) -> Vec<Vec<f64>> {
633        let n_funcs = self.poles.len();
634        let n_points = taus.len();
635        let mut result = vec![vec![0.0; n_points]; n_funcs];
636
637        // Evaluate at each point
638        for (j, &tau) in taus.iter().enumerate() {
639            for (i, (&pole, &pole_weight)) in
640                self.poles.iter().zip(self.pole_weights.iter()).enumerate()
641            {
642                result[i][j] = self.evaluate_single(tau, pole, pole_weight);
643            }
644        }
645
646        result
647    }
648}
649
650/// Wrapper for DLR functions in Matsubara domain
651#[derive(Clone)]
652pub(crate) struct DLRMatsubaraFuncs {
653    pub poles: Vec<f64>,
654    pub beta: f64,
655    pub wmax: f64,
656    pub pole_weights: Vec<f64>,
657    pub kernel_ypower: i32,
658    pub statistics: Statistics,
659}
660
661impl DLRMatsubaraFuncs {
662    fn zero_pole_matsubara_limit(&self) -> f64 {
663        match self.kernel_ypower {
664            0 => -0.5 * self.beta,
665            1 => -1.0 / (self.wmax * self.wmax),
666            _ => panic!(
667                "DLR Matsubara evaluation does not support kernel ypower = {}",
668                self.kernel_ypower
669            ),
670        }
671    }
672}
673
674// ============================================================================
675// Internal enum to hold different function types
676// ============================================================================
677
678/// Internal enum to hold different function types
679#[derive(Clone)]
680pub(crate) enum FuncsType {
681    /// Continuous functions (u or v): PiecewiseLegendrePolyVector
682    PolyVector(PolyVectorFuncs),
683
684    /// Fourier-transformed functions (uhat): PiecewiseLegendreFTVector
685    FTVector(FTVectorFuncs),
686
687    /// DLR functions in tau domain (discrete poles)
688    DLRTau(DLRTauFuncs),
689
690    /// DLR functions in Matsubara domain (discrete poles)
691    DLRMatsubara(DLRMatsubaraFuncs),
692}
693
694/// Opaque funcs type for C API (compatible with libsparseir)
695///
696/// Wraps piecewise Legendre polynomial representations:
697/// - PiecewiseLegendrePolyVector for u and v
698/// - PiecewiseLegendreFTVector for uhat
699///
700/// Note: Named `spir_funcs` to match libsparseir C++ API exactly.
701/// The internal FuncsType is hidden using a void pointer, but beta is kept as a public field.
702#[repr(C)]
703pub struct spir_funcs {
704    pub(crate) _private: *const std::ffi::c_void,
705    pub(crate) beta: f64,
706}
707
708impl spir_funcs {
709    /// Get a reference to the inner FuncsType
710    pub(crate) fn inner_type(&self) -> &FuncsType {
711        unsafe { &*(self._private as *const FuncsType) }
712    }
713
714    /// Create u funcs (tau-domain, Fermionic)
715    pub(crate) fn from_u_fermionic(poly: Arc<PiecewiseLegendrePolyVector>, beta: f64) -> Self {
716        let inner = FuncsType::PolyVector(PolyVectorFuncs {
717            poly,
718            domain: FunctionDomain::Tau(Statistics::Fermionic),
719        });
720        Self {
721            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
722            beta,
723        }
724    }
725
726    /// Create u funcs (tau-domain, Bosonic)
727    pub(crate) fn from_u_bosonic(poly: Arc<PiecewiseLegendrePolyVector>, beta: f64) -> Self {
728        let inner = FuncsType::PolyVector(PolyVectorFuncs {
729            poly,
730            domain: FunctionDomain::Tau(Statistics::Bosonic),
731        });
732        Self {
733            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
734            beta,
735        }
736    }
737
738    /// Create v funcs (omega-domain, no statistics)
739    pub(crate) fn from_v(poly: Arc<PiecewiseLegendrePolyVector>, beta: f64) -> Self {
740        let inner = FuncsType::PolyVector(PolyVectorFuncs {
741            poly,
742            domain: FunctionDomain::Omega,
743        });
744        Self {
745            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
746            beta,
747        }
748    }
749
750    /// Create uhat funcs (Matsubara-domain, Fermionic, truncated)
751    pub(crate) fn from_uhat_fermionic(
752        ft: Arc<PiecewiseLegendreFTVector<Fermionic>>,
753        beta: f64,
754    ) -> Self {
755        let inner = FuncsType::FTVector(FTVectorFuncs {
756            ft_fermionic: Some(ft),
757            ft_bosonic: None,
758            statistics: Statistics::Fermionic,
759        });
760        Self {
761            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
762            beta,
763        }
764    }
765
766    /// Create uhat funcs (Matsubara-domain, Bosonic, truncated)
767    pub(crate) fn from_uhat_bosonic(
768        ft: Arc<PiecewiseLegendreFTVector<Bosonic>>,
769        beta: f64,
770    ) -> Self {
771        let inner = FuncsType::FTVector(FTVectorFuncs {
772            ft_fermionic: None,
773            ft_bosonic: Some(ft),
774            statistics: Statistics::Bosonic,
775        });
776        Self {
777            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
778            beta,
779        }
780    }
781
782    /// Create uhat_full funcs (Matsubara-domain, Fermionic, untruncated)
783    ///
784    /// Creates funcs from the full (untruncated) basis functions `uhat_full`.
785    /// This accesses `basis.uhat_full` which contains all basis functions
786    /// from the SVE result, not just the truncated ones.
787    pub(crate) fn from_uhat_full_fermionic(
788        ft: Arc<PiecewiseLegendreFTVector<Fermionic>>,
789        beta: f64,
790    ) -> Self {
791        let inner = FuncsType::FTVector(FTVectorFuncs {
792            ft_fermionic: Some(ft),
793            ft_bosonic: None,
794            statistics: Statistics::Fermionic,
795        });
796        Self {
797            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
798            beta,
799        }
800    }
801
802    /// Create uhat_full funcs (Matsubara-domain, Bosonic, untruncated)
803    ///
804    /// Creates funcs from the full (untruncated) basis functions `uhat_full`.
805    /// This accesses `basis.uhat_full` which contains all basis functions
806    /// from the SVE result, not just the truncated ones.
807    pub(crate) fn from_uhat_full_bosonic(
808        ft: Arc<PiecewiseLegendreFTVector<Bosonic>>,
809        beta: f64,
810    ) -> Self {
811        let inner = FuncsType::FTVector(FTVectorFuncs {
812            ft_fermionic: None,
813            ft_bosonic: Some(ft),
814            statistics: Statistics::Bosonic,
815        });
816        Self {
817            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
818            beta,
819        }
820    }
821
822    /// Create DLR tau funcs (tau-domain, Fermionic)
823    /// Note: DLR always uses LogisticKernel regardless of the IR basis kernel type
824    pub(crate) fn from_dlr_tau_fermionic(
825        poles: Vec<f64>,
826        beta: f64,
827        wmax: f64,
828        pole_weights: Vec<f64>,
829        kernel_ypower: i32,
830    ) -> Self {
831        let inner = FuncsType::DLRTau(DLRTauFuncs {
832            poles,
833            beta,
834            wmax,
835            pole_weights,
836            kernel_ypower,
837            statistics: Statistics::Fermionic,
838        });
839        Self {
840            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
841            beta,
842        }
843    }
844
845    /// Create DLR tau funcs (tau-domain, Bosonic)
846    /// Note: DLR always uses LogisticKernel regardless of the IR basis kernel type
847    pub(crate) fn from_dlr_tau_bosonic(
848        poles: Vec<f64>,
849        beta: f64,
850        wmax: f64,
851        pole_weights: Vec<f64>,
852        kernel_ypower: i32,
853    ) -> Self {
854        let inner = FuncsType::DLRTau(DLRTauFuncs {
855            poles,
856            beta,
857            wmax,
858            pole_weights,
859            kernel_ypower,
860            statistics: Statistics::Bosonic,
861        });
862        Self {
863            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
864            beta,
865        }
866    }
867
868    /// Create DLR Matsubara funcs (Matsubara-domain, Fermionic)
869    pub(crate) fn from_dlr_matsubara_fermionic(
870        poles: Vec<f64>,
871        beta: f64,
872        wmax: f64,
873        pole_weights: Vec<f64>,
874        kernel_ypower: i32,
875    ) -> Self {
876        let inner = FuncsType::DLRMatsubara(DLRMatsubaraFuncs {
877            poles,
878            beta,
879            wmax,
880            pole_weights,
881            kernel_ypower,
882            statistics: Statistics::Fermionic,
883        });
884        Self {
885            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
886            beta,
887        }
888    }
889
890    /// Create DLR Matsubara funcs (Matsubara-domain, Bosonic)
891    pub(crate) fn from_dlr_matsubara_bosonic(
892        poles: Vec<f64>,
893        beta: f64,
894        wmax: f64,
895        pole_weights: Vec<f64>,
896        kernel_ypower: i32,
897    ) -> Self {
898        let inner = FuncsType::DLRMatsubara(DLRMatsubaraFuncs {
899            poles,
900            beta,
901            wmax,
902            pole_weights,
903            kernel_ypower,
904            statistics: Statistics::Bosonic,
905        });
906        Self {
907            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
908            beta,
909        }
910    }
911
912    /// Get the number of basis functions
913    pub(crate) fn size(&self) -> usize {
914        match self.inner_type() {
915            FuncsType::PolyVector(pv) => pv.poly.polyvec.len(),
916            FuncsType::FTVector(ftv) => {
917                if let Some(ft) = &ftv.ft_fermionic {
918                    ft.polyvec.len()
919                } else if let Some(ft) = &ftv.ft_bosonic {
920                    ft.polyvec.len()
921                } else {
922                    0
923                }
924            }
925            FuncsType::DLRTau(dlr) => dlr.poles.len(),
926            FuncsType::DLRMatsubara(dlr) => dlr.poles.len(),
927        }
928    }
929
930    /// Get knots for continuous functions (PolyVector only)
931    pub(crate) fn knots(&self) -> Option<Vec<f64>> {
932        match self.inner_type() {
933            FuncsType::PolyVector(pv) => {
934                // Get unique knots from all polynomials
935                let mut all_knots = Vec::new();
936                for p in &pv.poly.polyvec {
937                    for &knot in &p.knots {
938                        if !all_knots.iter().any(|&k: &f64| (k - knot).abs() < 1e-14) {
939                            all_knots.push(knot);
940                        }
941                    }
942                }
943                all_knots.sort_by(|a, b| a.partial_cmp(b).unwrap());
944                Some(all_knots)
945            }
946            _ => None, // FT vectors don't have knots in the traditional sense
947        }
948    }
949
950    /// Evaluate at a single tau/omega point (for continuous functions only)
951    ///
952    /// # Arguments
953    /// * `x` - For u: tau ∈ [-beta, beta], For v: omega ∈ [-omega_max, omega_max]
954    ///
955    /// # Returns
956    /// Vector of function values, or None if not continuous
957    pub(crate) fn eval_continuous(&self, x: f64) -> Option<Vec<f64>> {
958        match self.inner_type() {
959            FuncsType::PolyVector(pv) => Some(pv.evaluate_at(x, self.beta)),
960            FuncsType::DLRTau(dlr) => Some(dlr.evaluate_at(x)),
961            _ => None,
962        }
963    }
964
965    /// Evaluate at a single Matsubara frequency (for FT functions only)
966    ///
967    /// # Arguments
968    /// * `n` - Matsubara frequency index
969    ///
970    /// # Returns
971    /// Vector of complex function values, or None if not FT type
972    pub(crate) fn eval_matsubara(&self, n: i64) -> Option<Vec<num_complex::Complex64>> {
973        match self.inner_type() {
974            FuncsType::FTVector(ftv) => {
975                if ftv.statistics == Statistics::Fermionic {
976                    // Fermionic
977                    let ft = ftv.ft_fermionic.as_ref()?;
978                    let freq = MatsubaraFreq::<Fermionic>::new(n).ok()?;
979                    let mut result = Vec::with_capacity(ft.polyvec.len());
980                    for p in &ft.polyvec {
981                        result.push(p.evaluate(&freq));
982                    }
983                    Some(result)
984                } else {
985                    // Bosonic
986                    let ft = ftv.ft_bosonic.as_ref()?;
987                    let freq = MatsubaraFreq::<Bosonic>::new(n).ok()?;
988                    let mut result = Vec::with_capacity(ft.polyvec.len());
989                    for p in &ft.polyvec {
990                        result.push(p.evaluate(&freq));
991                    }
992                    Some(result)
993                }
994            }
995            FuncsType::DLRMatsubara(dlr) => {
996                // Evaluate DLR Matsubara functions using the kernel-aware pole weights.
997                use num_complex::Complex;
998
999                let mut result = Vec::with_capacity(dlr.poles.len());
1000                if dlr.statistics == Statistics::Fermionic {
1001                    let freq = MatsubaraFreq::<Fermionic>::new(n).ok()?;
1002                    let iv = freq.value_imaginary(dlr.beta);
1003                    for (i, &pole) in dlr.poles.iter().enumerate() {
1004                        let pole_weight = dlr.pole_weights[i];
1005                        result
1006                            .push(Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0)));
1007                    }
1008                } else {
1009                    let freq = MatsubaraFreq::<Bosonic>::new(n).ok()?;
1010                    let iv = freq.value_imaginary(dlr.beta);
1011                    for (i, &pole) in dlr.poles.iter().enumerate() {
1012                        if pole == 0.0 {
1013                            if freq.n() == 0 {
1014                                result.push(Complex::new(dlr.zero_pole_matsubara_limit(), 0.0));
1015                            } else {
1016                                result.push(Complex::new(0.0, 0.0));
1017                            }
1018                        } else {
1019                            let pole_weight = dlr.pole_weights[i];
1020                            result.push(
1021                                Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0)),
1022                            );
1023                        }
1024                    }
1025                }
1026                Some(result)
1027            }
1028            _ => None,
1029        }
1030    }
1031
1032    /// Batch evaluate at multiple tau/omega points
1033    pub(crate) fn batch_eval_continuous(&self, xs: &[f64]) -> Option<Vec<Vec<f64>>> {
1034        match self.inner_type() {
1035            FuncsType::PolyVector(pv) => Some(pv.batch_evaluate_at(xs, self.beta)),
1036            FuncsType::DLRTau(dlr) => Some(dlr.batch_evaluate_at(xs)),
1037            _ => None,
1038        }
1039    }
1040
1041    /// Batch evaluate at multiple Matsubara frequencies (for FT functions only)
1042    ///
1043    /// # Arguments
1044    /// * `ns` - Matsubara frequency indices
1045    ///
1046    /// # Returns
1047    /// Matrix of complex function values (size = `[n_funcs, n_freqs]`), or None if not FT type
1048    pub(crate) fn batch_eval_matsubara(
1049        &self,
1050        ns: &[i64],
1051    ) -> Option<Vec<Vec<num_complex::Complex64>>> {
1052        match self.inner_type() {
1053            FuncsType::FTVector(ftv) => {
1054                if ftv.statistics == Statistics::Fermionic {
1055                    // Fermionic
1056                    let ft = ftv.ft_fermionic.as_ref()?;
1057                    let n_funcs = ft.polyvec.len();
1058                    let n_points = ns.len();
1059                    let mut result =
1060                        vec![vec![num_complex::Complex64::new(0.0, 0.0); n_points]; n_funcs];
1061
1062                    for (j, &n) in ns.iter().enumerate() {
1063                        let freq = MatsubaraFreq::<Fermionic>::new(n).ok()?;
1064                        for (i, p) in ft.polyvec.iter().enumerate() {
1065                            result[i][j] = p.evaluate(&freq);
1066                        }
1067                    }
1068                    Some(result)
1069                } else {
1070                    // Bosonic
1071                    let ft = ftv.ft_bosonic.as_ref()?;
1072                    let n_funcs = ft.polyvec.len();
1073                    let n_points = ns.len();
1074                    let mut result =
1075                        vec![vec![num_complex::Complex64::new(0.0, 0.0); n_points]; n_funcs];
1076
1077                    for (j, &n) in ns.iter().enumerate() {
1078                        let freq = MatsubaraFreq::<Bosonic>::new(n).ok()?;
1079                        for (i, p) in ft.polyvec.iter().enumerate() {
1080                            result[i][j] = p.evaluate(&freq);
1081                        }
1082                    }
1083                    Some(result)
1084                }
1085            }
1086            FuncsType::DLRMatsubara(dlr) => {
1087                // Batch evaluate DLR Matsubara functions using the kernel-aware pole weights.
1088                use num_complex::Complex;
1089
1090                let n_funcs = dlr.poles.len();
1091                let n_points = ns.len();
1092                let mut result = vec![vec![Complex::new(0.0, 0.0); n_points]; n_funcs];
1093
1094                for (j, &n) in ns.iter().enumerate() {
1095                    if dlr.statistics == Statistics::Fermionic {
1096                        let freq = MatsubaraFreq::<Fermionic>::new(n).ok()?;
1097                        let iv = freq.value_imaginary(dlr.beta);
1098                        for (i, &pole) in dlr.poles.iter().enumerate() {
1099                            let pole_weight = dlr.pole_weights[i];
1100                            result[i][j] =
1101                                Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0));
1102                        }
1103                    } else {
1104                        let freq = MatsubaraFreq::<Bosonic>::new(n).ok()?;
1105                        let iv = freq.value_imaginary(dlr.beta);
1106                        for (i, &pole) in dlr.poles.iter().enumerate() {
1107                            if pole == 0.0 {
1108                                if freq.n() == 0 {
1109                                    result[i][j] =
1110                                        Complex::new(dlr.zero_pole_matsubara_limit(), 0.0);
1111                                } else {
1112                                    result[i][j] = Complex::new(0.0, 0.0);
1113                                }
1114                            } else {
1115                                let pole_weight = dlr.pole_weights[i];
1116                                result[i][j] =
1117                                    Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0));
1118                            }
1119                        }
1120                    }
1121                }
1122
1123                Some(result)
1124            }
1125            FuncsType::DLRTau(_) => {
1126                // DLRTau is for tau, not Matsubara frequencies
1127                None
1128            }
1129            _ => None,
1130        }
1131    }
1132
1133    /// Extract a slice of functions by indices (creates a new subset)
1134    ///
1135    /// # Arguments
1136    /// * `indices` - Indices of functions to extract
1137    ///
1138    /// # Returns
1139    /// New funcs object with the selected subset, or None if operation not supported
1140    pub(crate) fn get_slice(&self, indices: &[usize]) -> Option<Self> {
1141        match self.inner_type() {
1142            FuncsType::PolyVector(pv) => {
1143                let mut new_polys = Vec::with_capacity(indices.len());
1144                for &idx in indices {
1145                    if idx >= pv.poly.polyvec.len() {
1146                        return None;
1147                    }
1148                    new_polys.push(pv.poly.polyvec[idx].clone());
1149                }
1150                let new_poly_vec = PiecewiseLegendrePolyVector::new(new_polys);
1151                Some(Self {
1152                    _private: Box::into_raw(Box::new(FuncsType::PolyVector(PolyVectorFuncs {
1153                        poly: Arc::new(new_poly_vec),
1154                        domain: pv.domain,
1155                    }))) as *mut std::ffi::c_void,
1156                    beta: self.beta,
1157                })
1158            }
1159            FuncsType::FTVector(ftv) => {
1160                // Extract slice from PiecewiseLegendreFTVector
1161                if ftv.statistics == Statistics::Fermionic {
1162                    let ft = ftv.ft_fermionic.as_ref()?;
1163                    let mut new_polyvec = Vec::with_capacity(indices.len());
1164                    for &idx in indices {
1165                        if idx >= ft.polyvec.len() {
1166                            return None;
1167                        }
1168                        new_polyvec.push(ft.polyvec[idx].clone());
1169                    }
1170                    let new_ft_vector =
1171                        Arc::new(PiecewiseLegendreFTVector::from_vector(new_polyvec));
1172                    Some(Self {
1173                        _private: Box::into_raw(Box::new(FuncsType::FTVector(FTVectorFuncs {
1174                            ft_fermionic: Some(new_ft_vector),
1175                            ft_bosonic: None,
1176                            statistics: ftv.statistics,
1177                        }))) as *mut std::ffi::c_void,
1178                        beta: self.beta,
1179                    })
1180                } else {
1181                    let ft = ftv.ft_bosonic.as_ref()?;
1182                    let mut new_polyvec = Vec::with_capacity(indices.len());
1183                    for &idx in indices {
1184                        if idx >= ft.polyvec.len() {
1185                            return None;
1186                        }
1187                        new_polyvec.push(ft.polyvec[idx].clone());
1188                    }
1189                    let new_ft_vector =
1190                        Arc::new(PiecewiseLegendreFTVector::from_vector(new_polyvec));
1191                    Some(Self {
1192                        _private: Box::into_raw(Box::new(FuncsType::FTVector(FTVectorFuncs {
1193                            ft_fermionic: None,
1194                            ft_bosonic: Some(new_ft_vector),
1195                            statistics: ftv.statistics,
1196                        }))) as *mut std::ffi::c_void,
1197                        beta: self.beta,
1198                    })
1199                }
1200            }
1201            FuncsType::DLRTau(dlr) => {
1202                // Select subset of poles
1203                let mut new_poles = Vec::with_capacity(indices.len());
1204                let mut new_pole_weights = Vec::with_capacity(indices.len());
1205                for &idx in indices {
1206                    if idx >= dlr.poles.len() {
1207                        return None;
1208                    }
1209                    new_poles.push(dlr.poles[idx]);
1210                    new_pole_weights.push(dlr.pole_weights[idx]);
1211                }
1212                Some(Self {
1213                    _private: Box::into_raw(Box::new(FuncsType::DLRTau(DLRTauFuncs {
1214                        poles: new_poles,
1215                        beta: dlr.beta,
1216                        wmax: dlr.wmax,
1217                        pole_weights: new_pole_weights,
1218                        kernel_ypower: dlr.kernel_ypower,
1219                        statistics: dlr.statistics,
1220                    }))) as *mut std::ffi::c_void,
1221                    beta: dlr.beta,
1222                })
1223            }
1224            FuncsType::DLRMatsubara(dlr) => {
1225                // Select subset of poles
1226                let mut new_poles = Vec::with_capacity(indices.len());
1227                let mut new_pole_weights = Vec::with_capacity(indices.len());
1228                for &idx in indices {
1229                    if idx >= dlr.poles.len() {
1230                        return None;
1231                    }
1232                    new_poles.push(dlr.poles[idx]);
1233                    new_pole_weights.push(dlr.pole_weights[idx]);
1234                }
1235                Some(Self {
1236                    _private: Box::into_raw(Box::new(FuncsType::DLRMatsubara(DLRMatsubaraFuncs {
1237                        poles: new_poles,
1238                        beta: dlr.beta,
1239                        wmax: dlr.wmax,
1240                        pole_weights: new_pole_weights,
1241                        kernel_ypower: dlr.kernel_ypower,
1242                        statistics: dlr.statistics,
1243                    }))) as *mut std::ffi::c_void,
1244                    beta: dlr.beta,
1245                })
1246            }
1247        }
1248    }
1249}
1250
1251impl Clone for spir_funcs {
1252    fn clone(&self) -> Self {
1253        // Cheap clone: FuncsType::clone internally uses Arc::clone which is cheap
1254        let inner = self.inner_type().clone();
1255        Self {
1256            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
1257            beta: self.beta,
1258        }
1259    }
1260}
1261
1262impl Drop for spir_funcs {
1263    fn drop(&mut self) {
1264        unsafe {
1265            if !self._private.is_null() {
1266                let _ = Box::from_raw(self._private as *const FuncsType as *mut FuncsType);
1267            }
1268        }
1269    }
1270}
1271
1272// Helper function for tau normalization is now provided by sparse_ir::taufuncs::normalize_tau
1273
1274#[cfg(test)]
1275mod tests {
1276
1277    #[test]
1278    fn test_funcs_creation() {
1279        // Basic test that funcs types can be created
1280        // More comprehensive tests should be in integration tests
1281    }
1282}
1283/// Sampling type for C API (unified type for all domains)
1284///
1285/// This wraps different sampling implementations:
1286/// - TauSampling (for tau-domain)
1287/// - MatsubaraSampling (for Matsubara frequencies, full range or positive-only)
1288/// The internal structure is hidden using a void pointer to prevent exposing SamplingType to C.
1289#[repr(C)]
1290pub struct spir_sampling {
1291    pub(crate) _private: *const std::ffi::c_void,
1292}
1293
1294impl spir_sampling {
1295    /// Get a reference to the inner SamplingType (for internal use by other modules)
1296    pub(crate) fn inner(&self) -> &SamplingType {
1297        unsafe { &*(self._private as *const SamplingType) }
1298    }
1299}
1300
1301impl Clone for spir_sampling {
1302    fn clone(&self) -> Self {
1303        // Cheap clone: SamplingType::clone internally uses Arc::clone which is cheap
1304        let inner = self.inner().clone();
1305        Self {
1306            _private: Box::into_raw(Box::new(inner)) as *const std::ffi::c_void,
1307        }
1308    }
1309}
1310
1311impl Drop for spir_sampling {
1312    fn drop(&mut self) {
1313        unsafe {
1314            if !self._private.is_null() {
1315                let _ = Box::from_raw(self._private as *const SamplingType as *mut SamplingType);
1316            }
1317        }
1318    }
1319}
1320
1321/// Internal enum to distinguish between different sampling types
1322#[derive(Clone)]
1323pub(crate) enum SamplingType {
1324    TauFermionic(Arc<sparse_ir::sampling::TauSampling<Fermionic>>),
1325    TauBosonic(Arc<sparse_ir::sampling::TauSampling<Bosonic>>),
1326    MatsubaraFermionic(Arc<sparse_ir::matsubara_sampling::MatsubaraSampling<Fermionic>>),
1327    MatsubaraBosonic(Arc<sparse_ir::matsubara_sampling::MatsubaraSampling<Bosonic>>),
1328    MatsubaraPositiveOnlyFermionic(
1329        Arc<sparse_ir::matsubara_sampling::MatsubaraSamplingPositiveOnly<Fermionic>>,
1330    ),
1331    MatsubaraPositiveOnlyBosonic(
1332        Arc<sparse_ir::matsubara_sampling::MatsubaraSamplingPositiveOnly<Bosonic>>,
1333    ),
1334}
1335
1336/// InplaceFitter implementation for SamplingType
1337///
1338/// Delegates to the underlying sampling type's InplaceFitter implementation.
1339/// Returns false for unsupported operations based on sampling type.
1340impl InplaceFitter for SamplingType {
1341    fn n_points(&self) -> usize {
1342        match self {
1343            SamplingType::TauFermionic(s) => s.n_sampling_points(),
1344            SamplingType::TauBosonic(s) => s.n_sampling_points(),
1345            SamplingType::MatsubaraFermionic(s) => s.n_sampling_points(),
1346            SamplingType::MatsubaraBosonic(s) => s.n_sampling_points(),
1347            SamplingType::MatsubaraPositiveOnlyFermionic(s) => s.n_sampling_points(),
1348            SamplingType::MatsubaraPositiveOnlyBosonic(s) => s.n_sampling_points(),
1349        }
1350    }
1351
1352    fn basis_size(&self) -> usize {
1353        match self {
1354            SamplingType::TauFermionic(s) => s.basis_size(),
1355            SamplingType::TauBosonic(s) => s.basis_size(),
1356            SamplingType::MatsubaraFermionic(s) => s.basis_size(),
1357            SamplingType::MatsubaraBosonic(s) => s.basis_size(),
1358            SamplingType::MatsubaraPositiveOnlyFermionic(s) => s.basis_size(),
1359            SamplingType::MatsubaraPositiveOnlyBosonic(s) => s.basis_size(),
1360        }
1361    }
1362
1363    fn evaluate_nd_dd_to(
1364        &self,
1365        backend: Option<&GemmBackendHandle>,
1366        coeffs: &Slice<f64, DynRank>,
1367        dim: usize,
1368        out: &mut ViewMut<'_, f64, DynRank>,
1369    ) -> bool {
1370        match self {
1371            SamplingType::TauFermionic(s) => {
1372                InplaceFitter::evaluate_nd_dd_to(s.as_ref(), backend, coeffs, dim, out)
1373            }
1374            SamplingType::TauBosonic(s) => {
1375                InplaceFitter::evaluate_nd_dd_to(s.as_ref(), backend, coeffs, dim, out)
1376            }
1377            // Matsubara doesn't support dd (real → real)
1378            _ => false,
1379        }
1380    }
1381
1382    fn evaluate_nd_dz_to(
1383        &self,
1384        backend: Option<&GemmBackendHandle>,
1385        coeffs: &Slice<f64, DynRank>,
1386        dim: usize,
1387        out: &mut ViewMut<'_, Complex<f64>, DynRank>,
1388    ) -> bool {
1389        match self {
1390            SamplingType::MatsubaraFermionic(s) => {
1391                InplaceFitter::evaluate_nd_dz_to(s.as_ref(), backend, coeffs, dim, out)
1392            }
1393            SamplingType::MatsubaraBosonic(s) => {
1394                InplaceFitter::evaluate_nd_dz_to(s.as_ref(), backend, coeffs, dim, out)
1395            }
1396            SamplingType::MatsubaraPositiveOnlyFermionic(s) => {
1397                InplaceFitter::evaluate_nd_dz_to(s.as_ref(), backend, coeffs, dim, out)
1398            }
1399            SamplingType::MatsubaraPositiveOnlyBosonic(s) => {
1400                InplaceFitter::evaluate_nd_dz_to(s.as_ref(), backend, coeffs, dim, out)
1401            }
1402            // Tau doesn't support dz (real → complex)
1403            _ => false,
1404        }
1405    }
1406
1407    fn evaluate_nd_zz_to(
1408        &self,
1409        backend: Option<&GemmBackendHandle>,
1410        coeffs: &Slice<Complex<f64>, DynRank>,
1411        dim: usize,
1412        out: &mut ViewMut<'_, Complex<f64>, DynRank>,
1413    ) -> bool {
1414        match self {
1415            SamplingType::TauFermionic(s) => {
1416                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1417            }
1418            SamplingType::TauBosonic(s) => {
1419                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1420            }
1421            SamplingType::MatsubaraFermionic(s) => {
1422                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1423            }
1424            SamplingType::MatsubaraBosonic(s) => {
1425                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1426            }
1427            SamplingType::MatsubaraPositiveOnlyFermionic(s) => {
1428                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1429            }
1430            SamplingType::MatsubaraPositiveOnlyBosonic(s) => {
1431                InplaceFitter::evaluate_nd_zz_to(s.as_ref(), backend, coeffs, dim, out)
1432            }
1433        }
1434    }
1435
1436    fn fit_nd_dd_to(
1437        &self,
1438        backend: Option<&GemmBackendHandle>,
1439        values: &Slice<f64, DynRank>,
1440        dim: usize,
1441        out: &mut ViewMut<'_, f64, DynRank>,
1442    ) -> bool {
1443        match self {
1444            SamplingType::TauFermionic(s) => {
1445                InplaceFitter::fit_nd_dd_to(s.as_ref(), backend, values, dim, out)
1446            }
1447            SamplingType::TauBosonic(s) => {
1448                InplaceFitter::fit_nd_dd_to(s.as_ref(), backend, values, dim, out)
1449            }
1450            // Matsubara doesn't support dd (real → real)
1451            _ => false,
1452        }
1453    }
1454
1455    fn fit_nd_zd_to(
1456        &self,
1457        backend: Option<&GemmBackendHandle>,
1458        values: &Slice<Complex<f64>, DynRank>,
1459        dim: usize,
1460        out: &mut ViewMut<'_, f64, DynRank>,
1461    ) -> bool {
1462        match self {
1463            SamplingType::MatsubaraFermionic(s) => {
1464                InplaceFitter::fit_nd_zd_to(s.as_ref(), backend, values, dim, out)
1465            }
1466            SamplingType::MatsubaraBosonic(s) => {
1467                InplaceFitter::fit_nd_zd_to(s.as_ref(), backend, values, dim, out)
1468            }
1469            SamplingType::MatsubaraPositiveOnlyFermionic(s) => {
1470                InplaceFitter::fit_nd_zd_to(s.as_ref(), backend, values, dim, out)
1471            }
1472            SamplingType::MatsubaraPositiveOnlyBosonic(s) => {
1473                InplaceFitter::fit_nd_zd_to(s.as_ref(), backend, values, dim, out)
1474            }
1475            // Tau doesn't support zd (complex → real)
1476            _ => false,
1477        }
1478    }
1479
1480    fn fit_nd_zz_to(
1481        &self,
1482        backend: Option<&GemmBackendHandle>,
1483        values: &Slice<Complex<f64>, DynRank>,
1484        dim: usize,
1485        out: &mut ViewMut<'_, Complex<f64>, DynRank>,
1486    ) -> bool {
1487        match self {
1488            SamplingType::TauFermionic(s) => {
1489                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1490            }
1491            SamplingType::TauBosonic(s) => {
1492                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1493            }
1494            SamplingType::MatsubaraFermionic(s) => {
1495                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1496            }
1497            SamplingType::MatsubaraBosonic(s) => {
1498                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1499            }
1500            SamplingType::MatsubaraPositiveOnlyFermionic(s) => {
1501                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1502            }
1503            SamplingType::MatsubaraPositiveOnlyBosonic(s) => {
1504                InplaceFitter::fit_nd_zz_to(s.as_ref(), backend, values, dim, out)
1505            }
1506        }
1507    }
1508}
1509
1510#[cfg(test)]
1511mod sampling_tests {
1512
1513    #[test]
1514    fn test_sampling_creation() {
1515        // Basic test that sampling types can be created
1516        // More comprehensive tests should be in integration tests
1517    }
1518}
1519// Re-export status codes from lib.rs to avoid duplication
1520// (StatusCode and constants are defined in lib.rs)