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