1use cartan_core::Manifold;
16use cartan_dec::{Mesh, Operators};
17use volterra_core::NematicParams;
18use volterra_dec::connection_laplacian::{ConnectionLaplacian, molecular_field_conn};
19use volterra_dec::curved_stokes::{CurvedStokesSolver, nematic_vorticity_source};
20use volterra_dec::semi_lagrangian::SemiLagrangian;
21use volterra_dec::stokes_dec::VelocityFieldDec;
22use volterra_dec::QFieldDec;
23
24#[derive(Debug, Clone)]
26pub struct EngineStats {
27 pub time: f64,
29 pub mean_s: f64,
31 pub velocity_rms: f64,
33 pub n_vertices: usize,
35}
36
37pub struct NematicEngine {
39 params: NematicParams,
41 conn_lap: ConnectionLaplacian,
43 stokes: CurvedStokesSolver,
45 semi_lag: SemiLagrangian,
47 coords: Vec<[f64; 3]>,
49 dual_areas: Vec<f64>,
51 simplices: Vec<[usize; 3]>,
53 boundaries: Vec<[usize; 2]>,
54 vertex_boundaries: Vec<Vec<usize>>,
55 dt: f64,
57 n_vertices: usize,
59}
60
61impl NematicEngine {
62 pub fn new<M: Manifold>(
68 mesh: Mesh<M, 3, 2>,
69 manifold: M,
70 params: NematicParams,
71 gaussian_curvature: Vec<f64>,
72 ) -> Result<Self, String> {
73 params.validate()?;
74
75 let ops = Operators::from_mesh_generic(&mesh, &manifold)
76 .map_err(|e| format!("DEC operator assembly: {e:?}"))?;
77
78 let nv = mesh.n_vertices();
79
80 let coords: Vec<[f64; 3]> = volterra_dec::stokes_dec::extract_coords(&mesh);
82
83 let star0: Vec<f64> = (0..ops.hodge.star0().len())
85 .map(|i| ops.hodge.star0()[i]).collect();
86 let star1: Vec<f64> = (0..ops.hodge.star1().len())
87 .map(|i| ops.hodge.star1()[i]).collect();
88
89 let conn_lap = ConnectionLaplacian::new(&mesh, &coords, &star0, &star1);
91
92 let stokes = CurvedStokesSolver::new(&ops, &mesh, &gaussian_curvature)?;
94
95 let semi_lag = SemiLagrangian::new(coords.clone(), mesh.simplices.clone());
97
98 let ne = mesh.n_boundaries();
100 let mean_edge_len = if ne > 0 {
101 let total: f64 = (0..ne).map(|e| {
102 let [v0, v1] = mesh.boundaries[e];
103 let d = [
104 coords[v1][0] - coords[v0][0],
105 coords[v1][1] - coords[v0][1],
106 coords[v1][2] - coords[v0][2],
107 ];
108 (d[0]*d[0] + d[1]*d[1] + d[2]*d[2]).sqrt()
109 }).sum();
110 total / ne as f64
111 } else {
112 0.01
113 };
114 let dt = params.dt_diffusive(mean_edge_len);
115
116 Ok(Self {
117 params,
118 conn_lap,
119 stokes,
120 semi_lag,
121 coords,
122 dual_areas: star0,
123 simplices: mesh.simplices.clone(),
124 boundaries: mesh.boundaries.clone(),
125 vertex_boundaries: mesh.vertex_boundaries.clone(),
126 dt,
127 n_vertices: nv,
128 })
129 }
130
131 pub fn dt(&self) -> f64 { self.dt }
133
134 pub fn set_dt(&mut self, dt: f64) { self.dt = dt; }
136
137 pub fn n_vertices(&self) -> usize { self.n_vertices }
139
140 pub fn params(&self) -> &NematicParams { &self.params }
142
143 pub fn step(&self, q: &mut QFieldDec) -> VelocityFieldDec {
149 let nv = self.n_vertices;
150 let dt = self.dt;
151
152 let source = nematic_vorticity_source(
154 q, self.params.pe,
155 &self.simplices, &self.coords, &self.dual_areas,
157 );
158 let (_psi, _vel) = self.stokes.solve(&source, self.params.er);
159
160 let vel = self.compute_velocity_from_source(q);
164
165 let q_adv = self.semi_lag.advect(q, &vel, dt);
167
168 let h = molecular_field_conn(
175 &q_adv,
176 1.0, -self.params.la, self.params.lc / 4.0, &self.conn_lap,
180 );
181
182 for i in 0..nv {
184 q.q1[i] = q_adv.q1[i] + dt * h.q1[i];
185 q.q2[i] = q_adv.q2[i] + dt * h.q2[i];
186 }
187
188 vel
189 }
190
191 fn compute_velocity_from_source(&self, q: &QFieldDec) -> VelocityFieldDec {
193 let nv = self.n_vertices;
195 let pe = self.params.pe;
196 let er = self.params.er;
197
198 let mut omega = vec![0.0_f64; nv];
199 let mut areas = vec![0.0_f64; nv];
200
201 for &[i0, i1, i2] in &self.simplices {
202 let p0 = self.coords[i0];
203 let p1 = self.coords[i1];
204 let p2 = self.coords[i2];
205 let e01 = sub3(p1, p0);
206 let e02 = sub3(p2, p0);
207 let e12 = sub3(p2, p1);
208 let e20 = sub3(p0, p2);
209
210 let fn_vec = cross3(e01, e02);
211 let area2 = norm3(fn_vec);
212 if area2 < 1e-30 { continue; }
213 let fn_hat = scale3(fn_vec, 1.0 / area2);
214 let inv_2a = 1.0 / area2;
215
216 let rot_e12 = cross3(fn_hat, e12);
217 let rot_e20 = cross3(fn_hat, e20);
218 let rot_e01 = cross3(fn_hat, e01);
219
220 let gq1 = scale3(add3(add3(
221 scale3(rot_e12, q.q1[i0]),
222 scale3(rot_e20, q.q1[i1])),
223 scale3(rot_e01, q.q1[i2])), inv_2a);
224 let gq2 = scale3(add3(add3(
225 scale3(rot_e12, q.q2[i0]),
226 scale3(rot_e20, q.q2[i1])),
227 scale3(rot_e01, q.q2[i2])), inv_2a);
228
229 let fx = -pe * (gq1[0] + gq2[1]);
230 let fy = -pe * (gq2[0] - gq1[1]);
231
232 let circ_01 = fx * e01[0] + fy * e01[1];
233 let circ_12 = fx * e12[0] + fy * e12[1];
234 let circ_20 = fx * e20[0] + fy * e20[1];
235
236 let face_area = 0.5 * area2;
237 let third = face_area / 3.0;
238 areas[i0] += third;
239 areas[i1] += third;
240 areas[i2] += third;
241
242 omega[i0] += 0.5 * (circ_01 - circ_20);
243 omega[i1] += 0.5 * (circ_12 - circ_01);
244 omega[i2] += 0.5 * (circ_20 - circ_12);
245 }
246
247 for i in 0..nv {
248 if areas[i] > 1e-30 {
249 omega[i] /= er * areas[i];
250 }
251 }
252
253 let _ne = self.boundaries.len();
257 let mut vel = vec![[0.0_f64; 3]; nv];
258
259 for &[i0, i1, i2] in &self.simplices {
264 let p0 = self.coords[i0];
265 let p1 = self.coords[i1];
266 let p2 = self.coords[i2];
267 let e01 = sub3(p1, p0);
268 let e02 = sub3(p2, p0);
269
270 let fn_vec = cross3(e01, e02);
271 let area2 = norm3(fn_vec);
272 if area2 < 1e-30 { continue; }
273 let fn_hat = scale3(fn_vec, 1.0 / area2);
274 let inv_2a = 1.0 / area2;
275
276 let e12 = sub3(p2, p1);
277 let e20 = sub3(p0, p2);
278 let rot_e12 = cross3(fn_hat, e12);
279 let rot_e20 = cross3(fn_hat, e20);
280 let rot_e01 = cross3(fn_hat, e01);
281
282 let gq1 = scale3(add3(add3(
283 scale3(rot_e12, q.q1[i0]),
284 scale3(rot_e20, q.q1[i1])),
285 scale3(rot_e01, q.q1[i2])), inv_2a);
286 let gq2 = scale3(add3(add3(
287 scale3(rot_e12, q.q2[i0]),
288 scale3(rot_e20, q.q2[i1])),
289 scale3(rot_e01, q.q2[i2])), inv_2a);
290
291 let fx = -pe / er * (gq1[0] + gq2[1]);
293 let fy = -pe / er * (gq2[0] - gq1[1]);
294 let fz = 0.0;
295
296 let f_tang = [fx - fn_hat[0] * (fx * fn_hat[0] + fy * fn_hat[1]),
298 fy - fn_hat[1] * (fx * fn_hat[0] + fy * fn_hat[1]),
299 fz - fn_hat[2] * (fx * fn_hat[0] + fy * fn_hat[1])];
300
301 for &vi in &[i0, i1, i2] {
302 vel[vi] = add3(vel[vi], scale3(f_tang, 1.0 / 3.0));
303 }
304 }
305
306 for (v, boundaries) in vel.iter_mut().zip(&self.vertex_boundaries) {
308 let valence = boundaries.len().max(1) as f64;
309 *v = scale3(*v, 1.0 / valence);
310 }
311
312 VelocityFieldDec { v: vel, n_vertices: nv }
313 }
314
315 pub fn run(
317 &self,
318 q: &mut QFieldDec,
319 n_steps: usize,
320 snap_every: usize,
321 mut callback: impl FnMut(usize, &QFieldDec, &VelocityFieldDec, &EngineStats),
322 ) {
323 for step in 0..=n_steps {
324 if step % snap_every == 0 {
325 let vel = if step > 0 {
326 self.compute_velocity_from_source(q)
327 } else {
328 VelocityFieldDec::zeros(self.n_vertices)
329 };
330 let v_rms = (vel.v.iter()
331 .map(|[x, y, z]| x*x + y*y + z*z)
332 .sum::<f64>() / self.n_vertices as f64).sqrt();
333 let stats = EngineStats {
334 time: step as f64 * self.dt,
335 mean_s: q.mean_order_param(),
336 velocity_rms: v_rms,
337 n_vertices: self.n_vertices,
338 };
339 callback(step, q, &vel, &stats);
340 }
341 if step < n_steps {
342 self.step(q);
343 }
344 }
345 }
346}
347
348fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]-b[0], a[1]-b[1], a[2]-b[2]] }
349fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]+b[0], a[1]+b[1], a[2]+b[2]] }
350fn scale3(a: [f64; 3], s: f64) -> [f64; 3] { [a[0]*s, a[1]*s, a[2]*s] }
351fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 { a[0]*b[0] + a[1]*b[1] + a[2]*b[2] }
352fn norm3(a: [f64; 3]) -> f64 { dot3(a, a).sqrt() }
353fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
354 [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]
355}