1use crate::{
2 math::{
3 Scalar, Tensor, Vector,
4 optimize::{EqualityConstraint, LineSearch, NewtonRaphson, SecondOrderOptimization},
5 },
6 mechanics::Coordinates,
7 physics::molecular::single_chain::{Extensible, Inextensible, SingleChain, SingleChainError},
8};
9use std::{f64::consts::PI, thread};
10
11pub type Configuration = Coordinates<1>;
12
13#[derive(Clone, Copy, Debug)]
14pub enum Ensemble {
15 Isometric(Scalar),
16 Isotensional(Scalar),
17}
18
19pub trait Thermodynamics
20where
21 Self: Isometric + Isotensional + Legendre + SingleChain,
22{
23 fn ensemble(&self) -> Ensemble;
24 fn temperature(&self) -> Scalar {
25 match self.ensemble() {
26 Ensemble::Isometric(temperature) => temperature,
27 Ensemble::Isotensional(temperature) => temperature,
28 }
29 }
30 fn nondimensional_helmholtz_free_energy(
31 &self,
32 nondimensional_extension: Scalar,
33 ) -> Result<Scalar, SingleChainError> {
34 match self.ensemble() {
35 Ensemble::Isometric(_) => {
36 Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)
37 }
38 Ensemble::Isotensional(_) => {
39 Legendre::nondimensional_helmholtz_free_energy(self, nondimensional_extension)
40 }
41 }
42 }
43 fn nondimensional_helmholtz_free_energy_per_link(
44 &self,
45 nondimensional_extension: Scalar,
46 ) -> Result<Scalar, SingleChainError> {
47 match self.ensemble() {
48 Ensemble::Isometric(_) => Isometric::nondimensional_helmholtz_free_energy_per_link(
49 self,
50 nondimensional_extension,
51 ),
52 Ensemble::Isotensional(_) => Legendre::nondimensional_helmholtz_free_energy_per_link(
53 self,
54 nondimensional_extension,
55 ),
56 }
57 }
58 fn nondimensional_force(
59 &self,
60 nondimensional_extension: Scalar,
61 ) -> Result<Scalar, SingleChainError> {
62 match self.ensemble() {
63 Ensemble::Isometric(_) => {
64 Isometric::nondimensional_force(self, nondimensional_extension)
65 }
66 Ensemble::Isotensional(_) => {
67 Legendre::nondimensional_force(self, nondimensional_extension)
68 }
69 }
70 }
71 fn nondimensional_stiffness(
72 &self,
73 nondimensional_extension: Scalar,
74 ) -> Result<Scalar, SingleChainError> {
75 match self.ensemble() {
76 Ensemble::Isometric(_) => {
77 Isometric::nondimensional_stiffness(self, nondimensional_extension)
78 }
79 Ensemble::Isotensional(_) => {
80 Legendre::nondimensional_stiffness(self, nondimensional_extension)
81 }
82 }
83 }
84 fn nondimensional_radial_distribution(
85 &self,
86 nondimensional_extension: Scalar,
87 ) -> Result<Scalar, SingleChainError> {
88 match self.ensemble() {
89 Ensemble::Isometric(_) => {
90 Isometric::nondimensional_radial_distribution(self, nondimensional_extension)
91 }
92 Ensemble::Isotensional(_) => {
93 Legendre::nondimensional_radial_distribution(self, nondimensional_extension)
94 }
95 }
96 }
97 fn nondimensional_spherical_distribution(
98 &self,
99 nondimensional_extension: Scalar,
100 ) -> Result<Scalar, SingleChainError> {
101 match self.ensemble() {
102 Ensemble::Isometric(_) => {
103 Isometric::nondimensional_spherical_distribution(self, nondimensional_extension)
104 }
105 Ensemble::Isotensional(_) => {
106 Legendre::nondimensional_spherical_distribution(self, nondimensional_extension)
107 }
108 }
109 }
110 fn nondimensional_gibbs_free_energy(
111 &self,
112 nondimensional_force: Scalar,
113 ) -> Result<Scalar, SingleChainError> {
114 match self.ensemble() {
115 Ensemble::Isometric(_) => {
116 Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)
117 }
118 Ensemble::Isotensional(_) => {
119 Isotensional::nondimensional_gibbs_free_energy(self, nondimensional_force)
120 }
121 }
122 }
123 fn nondimensional_gibbs_free_energy_per_link(
124 &self,
125 nondimensional_force: Scalar,
126 ) -> Result<Scalar, SingleChainError> {
127 match self.ensemble() {
128 Ensemble::Isometric(_) => {
129 Legendre::nondimensional_gibbs_free_energy_per_link(self, nondimensional_force)
130 }
131 Ensemble::Isotensional(_) => {
132 Isotensional::nondimensional_gibbs_free_energy_per_link(self, nondimensional_force)
133 }
134 }
135 }
136 fn nondimensional_extension(
137 &self,
138 nondimensional_force: Scalar,
139 ) -> Result<Scalar, SingleChainError> {
140 match self.ensemble() {
141 Ensemble::Isometric(_) => {
142 Legendre::nondimensional_extension(self, nondimensional_force)
143 }
144 Ensemble::Isotensional(_) => {
145 Isotensional::nondimensional_extension(self, nondimensional_force)
146 }
147 }
148 }
149 fn nondimensional_compliance(
150 &self,
151 nondimensional_force: Scalar,
152 ) -> Result<Scalar, SingleChainError> {
153 match self.ensemble() {
154 Ensemble::Isometric(_) => {
155 Legendre::nondimensional_compliance(self, nondimensional_force)
156 }
157 Ensemble::Isotensional(_) => {
158 Isotensional::nondimensional_compliance(self, nondimensional_force)
159 }
160 }
161 }
162}
163
164pub trait Isometric
165where
166 Self: SingleChain,
167{
168 fn nondimensional_helmholtz_free_energy(
172 &self,
173 nondimensional_extension: Scalar,
174 ) -> Result<Scalar, SingleChainError> {
175 Ok(
176 self.nondimensional_helmholtz_free_energy_per_link(nondimensional_extension)?
177 * (self.number_of_links() as Scalar),
178 )
179 }
180 fn nondimensional_helmholtz_free_energy_per_link(
184 &self,
185 nondimensional_extension: Scalar,
186 ) -> Result<Scalar, SingleChainError> {
187 Ok(
188 self.nondimensional_helmholtz_free_energy(nondimensional_extension)?
189 / (self.number_of_links() as Scalar),
190 )
191 }
192 fn nondimensional_force(
196 &self,
197 nondimensional_extension: Scalar,
198 ) -> Result<Scalar, SingleChainError>;
199 fn nondimensional_stiffness(
203 &self,
204 nondimensional_extension: Scalar,
205 ) -> Result<Scalar, SingleChainError>;
206 fn nondimensional_radial_distribution(
210 &self,
211 nondimensional_extension: Scalar,
212 ) -> Result<Scalar, SingleChainError> {
213 Ok(
214 self.nondimensional_spherical_distribution(nondimensional_extension)?
215 * (4.0 * PI * nondimensional_extension.powi(2)),
216 )
217 }
218 fn nondimensional_spherical_distribution(
222 &self,
223 nondimensional_extension: Scalar,
224 ) -> Result<Scalar, SingleChainError>;
225}
226
227pub trait Isotensional
228where
229 Self: SingleChain,
230{
231 fn nondimensional_gibbs_free_energy(
235 &self,
236 nondimensional_force: Scalar,
237 ) -> Result<Scalar, SingleChainError> {
238 Ok(
239 self.nondimensional_gibbs_free_energy_per_link(nondimensional_force)?
240 * (self.number_of_links() as Scalar),
241 )
242 }
243 fn nondimensional_gibbs_free_energy_per_link(
247 &self,
248 nondimensional_force: Scalar,
249 ) -> Result<Scalar, SingleChainError> {
250 Ok(self.nondimensional_gibbs_free_energy(nondimensional_force)?
251 / (self.number_of_links() as Scalar))
252 }
253 fn nondimensional_extension(
257 &self,
258 nondimensional_force: Scalar,
259 ) -> Result<Scalar, SingleChainError>;
260 fn nondimensional_compliance(
264 &self,
265 nondimensional_force: Scalar,
266 ) -> Result<Scalar, SingleChainError>;
267}
268
269pub trait Legendre
270where
271 Self: Isometric + Isotensional + SingleChain,
272{
273 fn nondimensional_helmholtz_free_energy(
277 &self,
278 nondimensional_extension: Scalar,
279 ) -> Result<Scalar, SingleChainError> {
280 let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
281 Ok(
282 Isotensional::nondimensional_gibbs_free_energy(self, nondimensional_force)?
283 + self.number_of_links() as Scalar
284 * nondimensional_force
285 * nondimensional_extension,
286 )
287 }
288 fn nondimensional_helmholtz_free_energy_per_link(
292 &self,
293 nondimensional_extension: Scalar,
294 ) -> Result<Scalar, SingleChainError> {
295 Ok(
296 Legendre::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
297 / (self.number_of_links() as Scalar),
298 )
299 }
300 fn nondimensional_force(
304 &self,
305 nondimensional_extension: Scalar,
306 ) -> Result<Scalar, SingleChainError> {
307 match (NewtonRaphson {
308 abs_tol: 1e-10,
309 line_search: LineSearch::Error {
310 cut_back: 5e-1,
311 max_steps: 10,
312 },
313 ..Default::default()
314 }
315 .minimize(
316 |&nondimensional_force| {
317 Ok(Isotensional::nondimensional_gibbs_free_energy_per_link(
318 self,
319 nondimensional_force,
320 )? - nondimensional_force * nondimensional_extension)
321 },
322 |&nondimensional_force| {
323 Ok(
324 Isotensional::nondimensional_extension(self, nondimensional_force)?
325 - nondimensional_extension,
326 )
327 },
328 |&nondimensional_force| {
329 Ok(Isotensional::nondimensional_compliance(
330 self,
331 nondimensional_force,
332 )?)
333 },
334 nondimensional_extension,
335 EqualityConstraint::None,
336 None,
337 )) {
338 Ok(nondimensional_force) => Ok(nondimensional_force),
339 Err(error) => Err(SingleChainError::Upstream(
340 format!("{error}"),
341 format!("{self:?}"),
342 )),
343 }
344 }
345 fn nondimensional_stiffness(
349 &self,
350 nondimensional_extension: Scalar,
351 ) -> Result<Scalar, SingleChainError> {
352 let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
353 Ok(1.0 / Isotensional::nondimensional_compliance(self, nondimensional_force)?)
354 }
355 fn nondimensional_radial_distribution(
359 &self,
360 nondimensional_extension: Scalar,
361 ) -> Result<Scalar, SingleChainError> {
362 Ok(
363 Legendre::nondimensional_spherical_distribution(self, nondimensional_extension)?
364 * (4.0 * PI * nondimensional_extension.powi(2)),
365 )
366 }
367 fn nondimensional_spherical_distribution(
371 &self,
372 nondimensional_extension: Scalar,
373 ) -> Result<Scalar, SingleChainError>;
374 fn nondimensional_gibbs_free_energy(
378 &self,
379 nondimensional_force: Scalar,
380 ) -> Result<Scalar, SingleChainError> {
381 let nondimensional_extension =
382 Legendre::nondimensional_extension(self, nondimensional_force)?;
383 Ok(
384 Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
385 - self.number_of_links() as Scalar
386 * nondimensional_force
387 * nondimensional_extension,
388 )
389 }
390 fn nondimensional_gibbs_free_energy_per_link(
394 &self,
395 nondimensional_force: Scalar,
396 ) -> Result<Scalar, SingleChainError> {
397 Ok(
398 Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)?
399 / (self.number_of_links() as Scalar),
400 )
401 }
402 fn nondimensional_extension(
406 &self,
407 nondimensional_force: Scalar,
408 ) -> Result<Scalar, SingleChainError> {
409 match (NewtonRaphson {
410 abs_tol: 1e-10,
411 line_search: LineSearch::Error {
412 cut_back: 5e-1,
413 max_steps: 10,
414 },
415 ..Default::default()
416 }
417 .minimize(
418 |&nondimensional_extension| {
419 Ok(Isometric::nondimensional_helmholtz_free_energy_per_link(
420 self,
421 nondimensional_extension,
422 )? - nondimensional_force * nondimensional_extension)
423 },
424 |&nondimensional_extension| {
425 Ok(
426 Isometric::nondimensional_force(self, nondimensional_extension)?
427 - nondimensional_force,
428 )
429 },
430 |&nondimensional_extension| {
431 Ok(Isometric::nondimensional_stiffness(
432 self,
433 nondimensional_extension,
434 )?)
435 },
436 nondimensional_force,
437 EqualityConstraint::None,
438 None,
439 )) {
440 Ok(nondimensional_extension) => Ok(nondimensional_extension),
441 Err(error) => Err(SingleChainError::Upstream(
442 format!("{error}"),
443 format!("{self:?}"),
444 )),
445 }
446 }
447 fn nondimensional_compliance(
451 &self,
452 nondimensional_force: Scalar,
453 ) -> Result<Scalar, SingleChainError> {
454 let nondimensional_extension =
455 Legendre::nondimensional_extension(self, nondimensional_force)?;
456 Ok(1.0 / Isometric::nondimensional_stiffness(self, nondimensional_extension)?)
457 }
458}
459
460pub trait MonteCarlo
461where
462 Self: SingleChain + Sync,
463{
464 fn random_configuration(&self) -> Configuration;
465}
466
467pub trait MonteCarloExtensible
468where
469 Self: Extensible + MonteCarlo,
470{
471 fn nondimensional_radial_distribution(
472 &self,
473 num_bins: usize,
474 num_samples: usize,
475 num_threads: usize,
476 maximum_nondimensional_extension: Scalar,
477 ) -> (Vector, Vector) {
478 nondimensional_radial_distribution(
479 self,
480 num_bins,
481 num_samples,
482 num_threads,
483 maximum_nondimensional_extension,
484 )
485 }
486}
487
488impl<T> MonteCarloExtensible for T where T: Extensible + MonteCarlo {}
489
490pub trait MonteCarloInextensible
491where
492 Self: Inextensible + MonteCarlo,
493{
494 fn nondimensional_radial_distribution(
495 &self,
496 num_bins: usize,
497 num_samples: usize,
498 num_threads: usize,
499 ) -> (Vector, Vector) {
500 nondimensional_radial_distribution(
501 self,
502 num_bins,
503 num_samples,
504 num_threads,
505 self.maximum_nondimensional_extension(),
506 )
507 }
508}
509
510impl<T> MonteCarloInextensible for T where T: Inextensible + MonteCarlo {}
511
512fn nondimensional_radial_distribution<T: MonteCarlo>(
513 model: &T,
514 num_bins: usize,
515 num_samples: usize,
516 num_threads: usize,
517 maximum_nondimensional_extension: Scalar,
518) -> (Vector, Vector) {
519 let base = num_samples / num_threads;
520 let remainder = num_samples % num_threads;
521 thread::scope(|s| {
522 let mut handles = Vec::with_capacity(num_threads);
523 for t in 0..num_threads {
524 let samples_t = base + usize::from(t < remainder);
525 handles.push(s.spawn(move || {
526 nondimensional_radial_distribution_inner(
527 model,
528 num_bins,
529 samples_t,
530 maximum_nondimensional_extension,
531 )
532 }));
533 }
534 let mut total_counts = vec![0; num_bins];
535 for h in handles {
536 let counts = h.join().expect("thread done goofed");
537 for (tot, c) in total_counts.iter_mut().zip(counts) {
538 *tot += c;
539 }
540 }
541 let bin_width = maximum_nondimensional_extension / (num_bins as Scalar);
542 let bin_centers = (0..num_bins)
543 .map(|i| (i as Scalar + 0.5) * bin_width)
544 .collect();
545 let total_samples = num_samples as Scalar;
546 let bin_values = total_counts
547 .into_iter()
548 .map(|count| count as Scalar / total_samples / bin_width)
549 .collect();
550 (bin_centers, bin_values)
551 })
552}
553
554fn nondimensional_radial_distribution_inner<T: MonteCarlo>(
555 model: &T,
556 num_bins: usize,
557 num_samples: usize,
558 maximum_nondimensional_extension: Scalar,
559) -> Vec<usize> {
560 let mut bin_counts = vec![0; num_bins];
561 let num_links = model.number_of_links() as Scalar;
562 let end_index = model.number_of_links() as usize - 1;
563 for _ in 0..num_samples {
564 let configuration = model.random_configuration();
565 let nondimensional_extension = configuration[end_index].norm() / num_links;
566 if nondimensional_extension > maximum_nondimensional_extension {
567 panic!(
568 "Sample {nondimensional_extension} above maximum {maximum_nondimensional_extension}"
569 )
570 }
571 let bin_index = (nondimensional_extension / maximum_nondimensional_extension
572 * num_bins as Scalar) as usize;
573 bin_counts[bin_index] += 1;
574 }
575 bin_counts
576}