1use std::sync::Arc;
26
27use ocas_domain::Rational;
28use ocas_domain::dual::{DualShape, HyperDual, new_first_order};
29use pyo3::exceptions::{PyTypeError, PyValueError};
30use pyo3::prelude::*;
31
32fn py_to_rational(obj: &Bound<'_, PyAny>) -> PyResult<Rational> {
34 if let Ok(n) = obj.extract::<i64>() {
35 Ok(Rational::new(n, 1))
36 } else if let Ok((num, den)) = obj.extract::<(i64, i64)>() {
37 if den == 0 {
38 Err(PyValueError::new_err("rational denominator cannot be zero"))
39 } else {
40 Ok(Rational::new(num, den))
41 }
42 } else {
43 Err(PyTypeError::new_err("expected int or (num, denom) tuple"))
44 }
45}
46
47fn rational_to_string(r: &Rational) -> String {
50 let n = r.numer().to_i64().unwrap_or(0);
51 let d = r.denom().to_i64().unwrap_or(0);
52 if d == 1 {
53 n.to_string()
54 } else {
55 format!("{n}/{d}")
56 }
57}
58
59#[pyclass(name = "DualShape")]
63pub struct PyDualShape {
64 shape: Arc<DualShape>,
65}
66
67#[pymethods]
68impl PyDualShape {
69 #[staticmethod]
72 fn first_order(n_vars: usize) -> PyResult<Self> {
73 if n_vars == 0 {
74 return Err(PyValueError::new_err("n_vars must be >= 1"));
75 }
76 Ok(PyDualShape {
77 shape: new_first_order::<Rational>(n_vars),
78 })
79 }
80
81 #[getter]
83 fn n_vars(&self) -> usize {
84 self.shape.n_vars()
85 }
86
87 #[getter]
89 fn n_components(&self) -> usize {
90 self.shape.n_components()
91 }
92
93 fn __repr__(&self) -> String {
94 format!(
95 "DualShape(n_vars={}, n_components={})",
96 self.n_vars(),
97 self.n_components()
98 )
99 }
100}
101
102#[pyclass(name = "HyperDual")]
108pub struct PyHyperDual {
109 inner: HyperDual<Rational>,
110 shape: Arc<DualShape>,
111}
112
113impl PyHyperDual {
114 fn new(inner: HyperDual<Rational>) -> Self {
115 let shape = inner.shape().clone();
116 PyHyperDual { inner, shape }
117 }
118}
119
120#[pymethods]
121impl PyHyperDual {
122 #[staticmethod]
124 fn variable(shape: &PyDualShape, i: usize, value: &Bound<'_, PyAny>) -> PyResult<Self> {
125 if i >= shape.shape.n_vars() {
126 return Err(PyValueError::new_err(format!(
127 "variable index {i} out of range (n_vars = {})",
128 shape.shape.n_vars()
129 )));
130 }
131 let v = py_to_rational(value)?;
132 Ok(PyHyperDual::new(HyperDual::variable(&shape.shape, i, v)))
133 }
134
135 #[staticmethod]
137 fn constant(shape: &PyDualShape, value: &Bound<'_, PyAny>) -> PyResult<Self> {
138 let v = py_to_rational(value)?;
139 Ok(PyHyperDual::new(HyperDual::constant(&shape.shape, v)))
140 }
141
142 fn value(&self) -> String {
144 rational_to_string(self.inner.value())
145 }
146
147 fn deriv(&self, i: usize) -> Option<String> {
150 self.inner.deriv(i).map(rational_to_string)
151 }
152
153 #[getter]
155 fn n_vars(&self) -> usize {
156 self.shape.n_vars()
157 }
158
159 fn __repr__(&self) -> String {
160 format!(
161 "HyperDual(value={}, n_vars={})",
162 self.value(),
163 self.n_vars()
164 )
165 }
166
167 fn __add__(&self, other: &PyHyperDual) -> PyResult<PyHyperDual> {
168 if !Arc::ptr_eq(&self.shape, &other.shape) {
169 return Err(PyValueError::new_err(
170 "cannot add HyperDuals with different shapes",
171 ));
172 }
173 Ok(PyHyperDual::new(self.inner.clone() + other.inner.clone()))
174 }
175
176 fn __sub__(&self, other: &PyHyperDual) -> PyResult<PyHyperDual> {
177 if !Arc::ptr_eq(&self.shape, &other.shape) {
178 return Err(PyValueError::new_err(
179 "cannot subtract HyperDuals with different shapes",
180 ));
181 }
182 Ok(PyHyperDual::new(self.inner.clone() - other.inner.clone()))
183 }
184
185 fn __mul__(&self, other: &PyHyperDual) -> PyResult<PyHyperDual> {
186 if !Arc::ptr_eq(&self.shape, &other.shape) {
187 return Err(PyValueError::new_err(
188 "cannot multiply HyperDuals with different shapes",
189 ));
190 }
191 Ok(PyHyperDual::new(self.inner.clone() * other.inner.clone()))
192 }
193
194 fn __truediv__(&self, other: &PyHyperDual) -> PyResult<PyHyperDual> {
195 if !Arc::ptr_eq(&self.shape, &other.shape) {
196 return Err(PyValueError::new_err(
197 "cannot divide HyperDuals with different shapes",
198 ));
199 }
200 if other.inner.value() == &Rational::new(0, 1) {
202 return Err(PyValueError::new_err(
203 "division by zero (value component is zero)",
204 ));
205 }
206 Ok(PyHyperDual::new(self.inner.clone() / other.inner.clone()))
207 }
208
209 fn __neg__(&self) -> PyHyperDual {
210 PyHyperDual::new(-self.inner.clone())
211 }
212}