1use ocas_atom::tensor::{
23 Contracted, IndexPosition, IndexSlot, Symmetry, Tensor, contract, symmetrise_sign,
24};
25use ocas_atom::{AtomArena, Symbol};
26use ocas_core::arena::Arena;
27use pyo3::exceptions::PyValueError;
28use pyo3::prelude::*;
29use pyo3::types::PyList;
30use std::collections::HashMap;
31
32struct TensorInner {
39 arena_ptr: *mut Arena,
40 ctx_ptr: *mut AtomArena<'static>,
41 tensor: Tensor<'static>,
42}
43
44unsafe impl Send for TensorInner {}
47unsafe impl Sync for TensorInner {}
48
49impl Drop for TensorInner {
50 fn drop(&mut self) {
51 unsafe {
54 let _ = Box::from_raw(self.ctx_ptr);
55 let _ = Box::from_raw(self.arena_ptr);
56 }
57 }
58}
59
60impl TensorInner {
61 fn ctx(&self) -> &'static AtomArena<'static> {
63 unsafe { &*self.ctx_ptr }
65 }
66
67 fn new_pair() -> (*mut Arena, *mut AtomArena<'static>) {
69 let arena_box: Box<Arena> = Box::new(Arena::new());
70 let arena_ptr = Box::into_raw(arena_box);
71 let arena_ref: &'static Arena = unsafe { &*arena_ptr };
73 let ctx = AtomArena::new(arena_ref);
74 let ctx_ptr = Box::into_raw(Box::new(ctx));
75 (arena_ptr, ctx_ptr)
76 }
77
78 fn build<F>(f: F) -> PyResult<Box<Self>>
81 where
82 F: FnOnce(&'static AtomArena<'static>) -> Tensor<'static>,
83 {
84 let (arena_ptr, ctx_ptr) = Self::new_pair();
85 struct Guard {
87 arena_ptr: *mut Arena,
88 ctx_ptr: *mut AtomArena<'static>,
89 armed: bool,
90 }
91 impl Drop for Guard {
92 fn drop(&mut self) {
93 if self.armed {
94 unsafe {
95 let _ = Box::from_raw(self.ctx_ptr);
96 let _ = Box::from_raw(self.arena_ptr);
97 }
98 }
99 }
100 }
101 let mut g = Guard {
102 arena_ptr,
103 ctx_ptr,
104 armed: true,
105 };
106 let ctx = unsafe { &*ctx_ptr };
107 let tensor = f(ctx);
108 g.armed = false;
109 Ok(Box::new(TensorInner {
110 arena_ptr,
111 ctx_ptr,
112 tensor,
113 }))
114 }
115}
116
117fn parse_position(s: &str) -> PyResult<IndexPosition> {
123 match s.to_ascii_lowercase().as_str() {
124 "upper" | "up" | "contravariant" => Ok(IndexPosition::Upper),
125 "lower" | "down" | "covariant" => Ok(IndexPosition::Lower),
126 _ => Err(PyValueError::new_err(format!(
127 "position must be 'upper' or 'lower', got {s:?}"
128 ))),
129 }
130}
131
132fn position_str(p: IndexPosition) -> &'static str {
133 match p {
134 IndexPosition::Upper => "upper",
135 IndexPosition::Lower => "lower",
136 }
137}
138
139fn parse_symmetry(s: &str) -> PyResult<Symmetry> {
140 match s.to_ascii_lowercase().as_str() {
141 "none" | "" => Ok(Symmetry::None),
142 "symmetric" | "sym" => Ok(Symmetry::Symmetric),
143 "antisymmetric" | "antisym" | "skew" => Ok(Symmetry::Antisymmetric),
144 _ => Err(PyValueError::new_err(format!(
145 "symmetry must be 'none', 'symmetric', or 'antisymmetric', got {s:?}"
146 ))),
147 }
148}
149
150fn symmetry_str(s: Symmetry) -> &'static str {
151 match s {
152 Symmetry::None => "none",
153 Symmetry::Symmetric => "symmetric",
154 Symmetry::Antisymmetric => "antisymmetric",
155 }
156}
157
158#[pyclass(name = "Tensor")]
162pub struct PyTensor {
163 inner: Box<TensorInner>,
164}
165
166#[pymethods]
167impl PyTensor {
168 #[new]
175 #[pyo3(signature = (name, slots, symmetry="none"))]
176 fn new(name: &str, slots: &Bound<'_, PyAny>, symmetry: &str) -> PyResult<Self> {
177 let sym = parse_symmetry(symmetry)?;
178 let parsed: Vec<(String, IndexPosition)> = slots
179 .try_iter()
180 .map_err(|_| PyValueError::new_err("slots must be a list of (label, position) pairs"))?
181 .map(|item| -> PyResult<(String, IndexPosition)> {
182 let item = item?;
183 let (label, pos): (String, String) = item.extract().map_err(|_| {
184 PyValueError::new_err("each slot must be a (label, position) pair")
185 })?;
186 Ok((label, parse_position(&pos)?))
187 })
188 .collect::<PyResult<_>>()?;
189 let symbol = Symbol::new(name);
190 let inner = TensorInner::build(|ctx| {
191 let slots: Vec<IndexSlot<'static>> = parsed
192 .iter()
193 .map(|(label, pos)| IndexSlot::new(ctx.var(label), *pos))
194 .collect();
195 Tensor::new(symbol, slots).with_symmetry(sym)
196 })?;
197 Ok(PyTensor { inner })
198 }
199
200 #[getter]
202 fn name(&self) -> String {
203 self.inner.tensor.name().as_str().to_string()
204 }
205
206 #[getter]
208 fn rank(&self) -> usize {
209 self.inner.tensor.rank()
210 }
211
212 #[getter]
214 fn symmetry(&self) -> &'static str {
215 symmetry_str(self.inner.tensor.symmetry())
216 }
217
218 fn slots(&self) -> Vec<(String, &'static str)> {
220 self.inner
221 .tensor
222 .slots()
223 .iter()
224 .map(|s| (s.label().to_string(), position_str(s.position())))
225 .collect()
226 }
227
228 fn dummy_labels(&self) -> Vec<String> {
231 self.inner
232 .tensor
233 .dummy_labels()
234 .into_iter()
235 .map(|a| a.to_string())
236 .collect()
237 }
238
239 fn to_string_atom(&self) -> String {
241 self.inner.tensor.to_atom(self.inner.ctx()).to_string()
242 }
243
244 fn __repr__(&self) -> String {
245 format!(
246 "Tensor({:?}, rank={}, symmetry={:?})",
247 self.name(),
248 self.rank(),
249 self.symmetry()
250 )
251 }
252}
253
254fn rebuild_tensor(
260 name: &str,
261 sym: Symmetry,
262 slots: &[(String, IndexPosition)],
263) -> PyResult<PyTensor> {
264 let inner = TensorInner::build(|ctx| {
265 let slots: Vec<IndexSlot<'static>> = slots
266 .iter()
267 .map(|(label, pos)| IndexSlot::new(ctx.var(label), *pos))
268 .collect();
269 Tensor::new(Symbol::new(name), slots).with_symmetry(sym)
270 })?;
271 Ok(PyTensor { inner })
272}
273
274fn snapshot(tensor: &Tensor<'_>) -> (String, Symmetry, Vec<(String, IndexPosition)>) {
277 let name = tensor.name().as_str().to_string();
278 let sym = tensor.symmetry();
279 let slots: Vec<(String, IndexPosition)> = tensor
280 .slots()
281 .iter()
282 .map(|s| (s.label().to_string(), s.position()))
283 .collect();
284 (name, sym, slots)
285}
286
287#[pyfunction]
295pub fn contract_tensors<'py>(
296 py: Python<'py>,
297 a: &PyTensor,
298 b: &PyTensor,
299) -> PyResult<Bound<'py, PyAny>> {
300 let (arena_ptr, ctx_ptr) = TensorInner::new_pair();
304 struct DropGuard {
305 arena_ptr: *mut Arena,
306 ctx_ptr: *mut AtomArena<'static>,
307 }
308 impl Drop for DropGuard {
309 fn drop(&mut self) {
310 unsafe {
311 let _ = Box::from_raw(self.ctx_ptr);
312 let _ = Box::from_raw(self.arena_ptr);
313 }
314 }
315 }
316 let _guard = DropGuard { arena_ptr, ctx_ptr };
317 let ctx: &'static AtomArena<'static> = unsafe { &*ctx_ptr };
318
319 let (a_name, a_sym, a_slots_data) = snapshot(&a.inner.tensor);
321 let (b_name, b_sym, b_slots_data) = snapshot(&b.inner.tensor);
322 let a_slots: Vec<IndexSlot<'static>> = a_slots_data
323 .iter()
324 .map(|(label, pos)| IndexSlot::new(ctx.var(label), *pos))
325 .collect();
326 let b_slots: Vec<IndexSlot<'static>> = b_slots_data
327 .iter()
328 .map(|(label, pos)| IndexSlot::new(ctx.var(label), *pos))
329 .collect();
330 let a_rebuilt = Tensor::new(Symbol::new(&a_name), a_slots).with_symmetry(a_sym);
331 let b_rebuilt = Tensor::new(Symbol::new(&b_name), b_slots).with_symmetry(b_sym);
332
333 let result = contract(ctx, &a_rebuilt, &b_rebuilt);
334 match result {
335 Contracted::Product(p) => {
336 let mut out: Vec<PyTensor> = Vec::with_capacity(p.factors.len());
337 for factor in &p.factors {
338 let (name, sym, slots) = snapshot(factor);
339 out.push(rebuild_tensor(&name, sym, &slots)?);
340 }
341 let list = PyList::new(py, out)?;
342 let tuple = ("product", list.into_any()).into_pyobject(py)?;
343 Ok(tuple.into_any())
344 }
345 Contracted::Scalar(atom) => {
346 let s = atom.to_string();
347 let tuple = ("scalar", s).into_pyobject(py)?;
348 Ok(tuple.into_any())
349 }
350 }
351}
352
353#[pyfunction]
358pub fn tensor_symmetrise_sign(tensor: &PyTensor) -> i64 {
359 symmetrise_sign(&tensor.inner.tensor)
360}
361
362#[pyfunction]
367#[pyo3(signature = (expr, specs, index_groups=None))]
368pub fn canonicalize_tensors(
369 expr: &str,
370 specs: HashMap<String, String>,
371 index_groups: Option<HashMap<String, u64>>,
372) -> PyResult<String> {
373 use ocas_atom::tensor::canon::canonicalize_tensors as canon;
374 use ocas_atom::tensor::spec::TensorRegistry;
375 use ocas_parse;
376
377 let arena = Arena::new();
378 let ctx = AtomArena::new(&arena);
379 let parsed = ocas_parse::parse(&ctx, expr)
380 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
381
382 let mut reg = TensorRegistry::new();
383 for (name, spec_str) in &specs {
384 let spec = parse_symmetry_spec(spec_str);
385 reg.register(Symbol::new(name), spec);
386 }
387 if let Some(groups) = &index_groups {
388 for (label, group) in groups {
389 reg.set_index_group(Symbol::new(label), *group);
390 }
391 }
392
393 let ct = canon(&ctx, parsed, ®)
394 .map_err(|e| PyValueError::new_err(format!("canonicalisation error: {e:?}")))?;
395
396 Ok(ct.canonical_form.to_string())
397}
398
399fn parse_symmetry_spec(s: &str) -> ocas_atom::tensor::spec::SymmetrySpec {
400 use ocas_atom::tensor::spec::SymmetrySpec;
401 match s {
402 "none" => SymmetrySpec::none(),
403 "symmetric" => SymmetrySpec::fully_symmetric(64),
406 "antisymmetric" => SymmetrySpec::fully_antisymmetric(64),
407 _ => SymmetrySpec::none(),
408 }
409}
410
411#[pyfunction]
415pub fn young_project(expr: &str, tableau: Vec<usize>) -> PyResult<String> {
416 use ocas_atom::tensor::young::{YoungTableau, young_project as yp};
417 use ocas_parse;
418
419 let arena = Arena::new();
420 let ctx = AtomArena::new(&arena);
421 let parsed = ocas_parse::parse(&ctx, expr)
422 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
423
424 let t = YoungTableau::new(tableau);
425 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| yp(&ctx, parsed, &t)))
429 .map_err(|e| {
430 PyValueError::new_err(format!(
431 "young projection panicked: {}",
432 if let Some(s) = e.downcast_ref::<String>() {
433 s.clone()
434 } else if let Some(s) = e.downcast_ref::<&str>() {
435 s.to_string()
436 } else {
437 "unknown panic".to_string()
438 }
439 ))
440 })?;
441 Ok(result.to_string())
442}
443
444#[pyfunction]
446pub fn refresh_dummies(expr: &str, specs: HashMap<String, String>) -> PyResult<String> {
447 use ocas_atom::tensor::dummy::refresh_dummies as rd;
448 use ocas_atom::tensor::spec::TensorRegistry;
449 use ocas_parse;
450
451 let arena = Arena::new();
452 let ctx = AtomArena::new(&arena);
453 let parsed = ocas_parse::parse(&ctx, expr)
454 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
455
456 let mut reg = TensorRegistry::new();
457 for (name, spec_str) in &specs {
458 let spec = parse_symmetry_spec(spec_str);
459 reg.register(Symbol::new(name), spec);
460 }
461
462 let result =
463 rd(&ctx, parsed, ®).map_err(|e| PyValueError::new_err(format!("dummy error: {e:?}")))?;
464 Ok(result.to_string())
465}