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
32unsafe fn extend_str_lifetime(s: &str) -> &'static str {
39 unsafe { std::mem::transmute::<&str, &'static str>(s) }
40}
41
42struct TensorInner {
45 arena_ptr: *mut Arena,
46 ctx_ptr: *mut AtomArena<'static>,
47 tensor: Tensor<'static>,
48}
49
50unsafe impl Send for TensorInner {}
53unsafe impl Sync for TensorInner {}
54
55impl Drop for TensorInner {
56 fn drop(&mut self) {
57 unsafe {
60 let _ = Box::from_raw(self.ctx_ptr);
61 let _ = Box::from_raw(self.arena_ptr);
62 }
63 }
64}
65
66impl TensorInner {
67 fn ctx(&self) -> &'static AtomArena<'static> {
69 unsafe { &*self.ctx_ptr }
71 }
72
73 fn new_pair() -> (*mut Arena, *mut AtomArena<'static>) {
75 let arena_box: Box<Arena> = Box::new(Arena::new());
76 let arena_ptr = Box::into_raw(arena_box);
77 let arena_ref: &'static Arena = unsafe { &*arena_ptr };
79 let ctx = AtomArena::new(arena_ref);
80 let ctx_ptr = Box::into_raw(Box::new(ctx));
81 (arena_ptr, ctx_ptr)
82 }
83
84 fn build<F>(f: F) -> PyResult<Box<Self>>
87 where
88 F: FnOnce(&'static AtomArena<'static>) -> Tensor<'static>,
89 {
90 let (arena_ptr, ctx_ptr) = Self::new_pair();
91 struct Guard {
93 arena_ptr: *mut Arena,
94 ctx_ptr: *mut AtomArena<'static>,
95 armed: bool,
96 }
97 impl Drop for Guard {
98 fn drop(&mut self) {
99 if self.armed {
100 unsafe {
101 let _ = Box::from_raw(self.ctx_ptr);
102 let _ = Box::from_raw(self.arena_ptr);
103 }
104 }
105 }
106 }
107 let mut g = Guard {
108 arena_ptr,
109 ctx_ptr,
110 armed: true,
111 };
112 let ctx = unsafe { &*ctx_ptr };
113 let tensor = f(ctx);
114 g.armed = false;
115 Ok(Box::new(TensorInner {
116 arena_ptr,
117 ctx_ptr,
118 tensor,
119 }))
120 }
121}
122
123fn parse_position(s: &str) -> PyResult<IndexPosition> {
129 match s.to_ascii_lowercase().as_str() {
130 "upper" | "up" | "contravariant" => Ok(IndexPosition::Upper),
131 "lower" | "down" | "covariant" => Ok(IndexPosition::Lower),
132 _ => Err(PyValueError::new_err(format!(
133 "position must be 'upper' or 'lower', got {s:?}"
134 ))),
135 }
136}
137
138fn position_str(p: IndexPosition) -> &'static str {
139 match p {
140 IndexPosition::Upper => "upper",
141 IndexPosition::Lower => "lower",
142 }
143}
144
145fn parse_symmetry(s: &str) -> PyResult<Symmetry> {
146 match s.to_ascii_lowercase().as_str() {
147 "none" | "" => Ok(Symmetry::None),
148 "symmetric" | "sym" => Ok(Symmetry::Symmetric),
149 "antisymmetric" | "antisym" | "skew" => Ok(Symmetry::Antisymmetric),
150 _ => Err(PyValueError::new_err(format!(
151 "symmetry must be 'none', 'symmetric', or 'antisymmetric', got {s:?}"
152 ))),
153 }
154}
155
156fn symmetry_str(s: Symmetry) -> &'static str {
157 match s {
158 Symmetry::None => "none",
159 Symmetry::Symmetric => "symmetric",
160 Symmetry::Antisymmetric => "antisymmetric",
161 }
162}
163
164#[pyclass(name = "Tensor")]
168pub struct PyTensor {
169 inner: Box<TensorInner>,
170}
171
172#[pymethods]
173impl PyTensor {
174 #[new]
181 #[pyo3(signature = (name, slots, symmetry="none"))]
182 fn new(name: &str, slots: &Bound<'_, PyAny>, symmetry: &str) -> PyResult<Self> {
183 let sym = parse_symmetry(symmetry)?;
184 let parsed: Vec<(String, IndexPosition)> = slots
185 .try_iter()
186 .map_err(|_| PyValueError::new_err("slots must be a list of (label, position) pairs"))?
187 .map(|item| -> PyResult<(String, IndexPosition)> {
188 let item = item?;
189 let (label, pos): (String, String) = item.extract().map_err(|_| {
190 PyValueError::new_err("each slot must be a (label, position) pair")
191 })?;
192 Ok((label, parse_position(&pos)?))
193 })
194 .collect::<PyResult<_>>()?;
195 let static_name = unsafe { extend_str_lifetime(name) };
196 let symbol = Symbol::new(static_name);
197 let inner = TensorInner::build(|ctx| {
198 let slots: Vec<IndexSlot<'static>> = parsed
199 .iter()
200 .map(|(label, pos)| {
201 let static_label = unsafe { extend_str_lifetime(label) };
202 IndexSlot::new(ctx.var(static_label), *pos)
203 })
204 .collect();
205 Tensor::new(symbol, slots).with_symmetry(sym)
206 })?;
207 Ok(PyTensor { inner })
208 }
209
210 #[getter]
212 fn name(&self) -> String {
213 self.inner.tensor.name().as_str().to_string()
214 }
215
216 #[getter]
218 fn rank(&self) -> usize {
219 self.inner.tensor.rank()
220 }
221
222 #[getter]
224 fn symmetry(&self) -> &'static str {
225 symmetry_str(self.inner.tensor.symmetry())
226 }
227
228 fn slots(&self) -> Vec<(String, &'static str)> {
230 self.inner
231 .tensor
232 .slots()
233 .iter()
234 .map(|s| (s.label().to_string(), position_str(s.position())))
235 .collect()
236 }
237
238 fn dummy_labels(&self) -> Vec<String> {
241 self.inner
242 .tensor
243 .dummy_labels()
244 .into_iter()
245 .map(|a| a.to_string())
246 .collect()
247 }
248
249 fn to_string_atom(&self) -> String {
251 self.inner.tensor.to_atom(self.inner.ctx()).to_string()
252 }
253
254 fn __repr__(&self) -> String {
255 format!(
256 "Tensor({:?}, rank={}, symmetry={:?})",
257 self.name(),
258 self.rank(),
259 self.symmetry()
260 )
261 }
262}
263
264fn rebuild_tensor(
270 name: &str,
271 sym: Symmetry,
272 slots: &[(String, IndexPosition)],
273) -> PyResult<PyTensor> {
274 let static_name = unsafe { extend_str_lifetime(name) };
275 let inner = TensorInner::build(|ctx| {
276 let slots: Vec<IndexSlot<'static>> = slots
277 .iter()
278 .map(|(label, pos)| {
279 let static_label = unsafe { extend_str_lifetime(label) };
280 IndexSlot::new(ctx.var(static_label), *pos)
281 })
282 .collect();
283 Tensor::new(Symbol::new(static_name), slots).with_symmetry(sym)
284 })?;
285 Ok(PyTensor { inner })
286}
287
288fn snapshot(tensor: &Tensor<'_>) -> (String, Symmetry, Vec<(String, IndexPosition)>) {
291 let name = tensor.name().as_str().to_string();
292 let sym = tensor.symmetry();
293 let slots: Vec<(String, IndexPosition)> = tensor
294 .slots()
295 .iter()
296 .map(|s| (s.label().to_string(), s.position()))
297 .collect();
298 (name, sym, slots)
299}
300
301#[pyfunction]
309pub fn contract_tensors<'py>(
310 py: Python<'py>,
311 a: &PyTensor,
312 b: &PyTensor,
313) -> PyResult<Bound<'py, PyAny>> {
314 let (arena_ptr, ctx_ptr) = TensorInner::new_pair();
318 struct DropGuard {
319 arena_ptr: *mut Arena,
320 ctx_ptr: *mut AtomArena<'static>,
321 }
322 impl Drop for DropGuard {
323 fn drop(&mut self) {
324 unsafe {
325 let _ = Box::from_raw(self.ctx_ptr);
326 let _ = Box::from_raw(self.arena_ptr);
327 }
328 }
329 }
330 let _guard = DropGuard { arena_ptr, ctx_ptr };
331 let ctx: &'static AtomArena<'static> = unsafe { &*ctx_ptr };
332
333 let (a_name, a_sym, a_slots_data) = snapshot(&a.inner.tensor);
335 let (b_name, b_sym, b_slots_data) = snapshot(&b.inner.tensor);
336 let a_slots: Vec<IndexSlot<'static>> = a_slots_data
337 .iter()
338 .map(|(label, pos)| {
339 let static_label = unsafe { extend_str_lifetime(label) };
340 IndexSlot::new(ctx.var(static_label), *pos)
341 })
342 .collect();
343 let b_slots: Vec<IndexSlot<'static>> = b_slots_data
344 .iter()
345 .map(|(label, pos)| {
346 let static_label = unsafe { extend_str_lifetime(label) };
347 IndexSlot::new(ctx.var(static_label), *pos)
348 })
349 .collect();
350 let a_rebuilt = Tensor::new(
351 Symbol::new(unsafe { extend_str_lifetime(&a_name) }),
352 a_slots,
353 )
354 .with_symmetry(a_sym);
355 let b_rebuilt = Tensor::new(
356 Symbol::new(unsafe { extend_str_lifetime(&b_name) }),
357 b_slots,
358 )
359 .with_symmetry(b_sym);
360
361 let result = contract(ctx, &a_rebuilt, &b_rebuilt);
362 match result {
363 Contracted::Product(p) => {
364 let mut out: Vec<PyTensor> = Vec::with_capacity(p.factors.len());
365 for factor in &p.factors {
366 let (name, sym, slots) = snapshot(factor);
367 out.push(rebuild_tensor(&name, sym, &slots)?);
368 }
369 let list = PyList::new(py, out)?;
370 let tuple = ("product", list.into_any()).into_pyobject(py)?;
371 Ok(tuple.into_any())
372 }
373 Contracted::Scalar(atom) => {
374 let s = atom.to_string();
375 let tuple = ("scalar", s).into_pyobject(py)?;
376 Ok(tuple.into_any())
377 }
378 }
379}
380
381#[pyfunction]
386pub fn tensor_symmetrise_sign(tensor: &PyTensor) -> i64 {
387 symmetrise_sign(&tensor.inner.tensor)
388}
389
390#[pyfunction]
395#[pyo3(signature = (expr, specs, index_groups=None))]
396pub fn canonicalize_tensors(
397 expr: &str,
398 specs: HashMap<String, String>,
399 index_groups: Option<HashMap<String, u64>>,
400) -> PyResult<String> {
401 use ocas_atom::tensor::canon::canonicalize_tensors as canon;
402 use ocas_atom::tensor::spec::TensorRegistry;
403 use ocas_parse;
404
405 let arena = Arena::new();
406 let ctx = AtomArena::new(&arena);
407 let parsed = ocas_parse::parse(&ctx, expr)
408 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
409
410 let mut reg = TensorRegistry::new();
411 for (name, spec_str) in &specs {
412 let spec = parse_symmetry_spec(spec_str);
413 reg.register(Symbol::new(name), spec);
414 }
415 if let Some(groups) = &index_groups {
416 for (label, group) in groups {
417 reg.set_index_group(Symbol::new(label), *group);
418 }
419 }
420
421 let ct = canon(&ctx, parsed, ®)
422 .map_err(|e| PyValueError::new_err(format!("canonicalisation error: {e:?}")))?;
423
424 Ok(ct.canonical_form.to_string())
425}
426
427fn parse_symmetry_spec(s: &str) -> ocas_atom::tensor::spec::SymmetrySpec {
428 use ocas_atom::tensor::spec::SymmetrySpec;
429 match s {
430 "none" => SymmetrySpec::none(),
431 "symmetric" => SymmetrySpec::fully_symmetric(0),
432 "antisymmetric" => SymmetrySpec::fully_antisymmetric(0),
433 _ => SymmetrySpec::none(),
434 }
435}
436
437#[pyfunction]
441pub fn young_project(expr: &str, tableau: Vec<usize>) -> PyResult<String> {
442 use ocas_atom::tensor::young::{YoungTableau, young_project as yp};
443 use ocas_parse;
444
445 let arena = Arena::new();
446 let ctx = AtomArena::new(&arena);
447 let parsed = ocas_parse::parse(&ctx, expr)
448 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
449
450 let t = YoungTableau::new(tableau);
451 let result = yp(&ctx, parsed, &t);
452 Ok(result.to_string())
453}
454
455#[pyfunction]
457pub fn refresh_dummies(expr: &str, specs: HashMap<String, String>) -> PyResult<String> {
458 use ocas_atom::tensor::dummy::refresh_dummies as rd;
459 use ocas_atom::tensor::spec::TensorRegistry;
460 use ocas_parse;
461
462 let arena = Arena::new();
463 let ctx = AtomArena::new(&arena);
464 let parsed = ocas_parse::parse(&ctx, expr)
465 .map_err(|e| PyValueError::new_err(format!("parse error: {e}")))?;
466
467 let mut reg = TensorRegistry::new();
468 for (name, spec_str) in &specs {
469 let spec = parse_symmetry_spec(spec_str);
470 reg.register(Symbol::new(name), spec);
471 }
472
473 let result =
474 rd(&ctx, parsed, ®).map_err(|e| PyValueError::new_err(format!("dummy error: {e:?}")))?;
475 Ok(result.to_string())
476}