Skip to main content

conspire/physics/molecular/single_chain/thermodynamics/
mod.rs

1use crate::{
2    math::{
3        Matrix, Scalar, Tensor, TensorArray, Vector,
4        optimize::{EqualityConstraint, LineSearch, NewtonRaphson, SecondOrderOptimization},
5    },
6    mechanics::{Coordinates, CurrentCoordinate},
7    physics::molecular::single_chain::{Extensible, Inextensible, SingleChain, SingleChainError},
8};
9use std::{f64::consts::PI, thread::scope};
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 ThermodynamicsExtensible
165where
166    Self: IsotensionalExtensible + Thermodynamics,
167{
168    fn nondimensional_link_energy_average(
169        &self,
170        nondimensional_force: Scalar,
171    ) -> Result<Scalar, SingleChainError> {
172        match self.ensemble() {
173            Ensemble::Isometric(_) => {
174                unimplemented!()
175            }
176            Ensemble::Isotensional(_) => {
177                IsotensionalExtensible::nondimensional_link_energy_average(
178                    self,
179                    nondimensional_force,
180                )
181            }
182        }
183    }
184    fn nondimensional_link_energy_variance(
185        &self,
186        nondimensional_force: Scalar,
187    ) -> Result<Scalar, SingleChainError> {
188        match self.ensemble() {
189            Ensemble::Isometric(_) => {
190                unimplemented!()
191            }
192            Ensemble::Isotensional(_) => {
193                IsotensionalExtensible::nondimensional_link_energy_variance(
194                    self,
195                    nondimensional_force,
196                )
197            }
198        }
199    }
200    fn nondimensional_link_energy_probability(
201        &self,
202        nondimensional_energy: Scalar,
203        nondimensional_force: Scalar,
204    ) -> Result<Scalar, SingleChainError> {
205        match self.ensemble() {
206            Ensemble::Isometric(_) => {
207                unimplemented!()
208            }
209            Ensemble::Isotensional(_) => {
210                IsotensionalExtensible::nondimensional_link_energy_probability(
211                    self,
212                    nondimensional_energy,
213                    nondimensional_force,
214                )
215            }
216        }
217    }
218    fn nondimensional_link_length_average(
219        &self,
220        nondimensional_force: Scalar,
221    ) -> Result<Scalar, SingleChainError> {
222        match self.ensemble() {
223            Ensemble::Isometric(_) => {
224                unimplemented!()
225            }
226            Ensemble::Isotensional(_) => {
227                IsotensionalExtensible::nondimensional_link_length_average(
228                    self,
229                    nondimensional_force,
230                )
231            }
232        }
233    }
234    fn nondimensional_link_length_variance(
235        &self,
236        nondimensional_force: Scalar,
237    ) -> Result<Scalar, SingleChainError> {
238        match self.ensemble() {
239            Ensemble::Isometric(_) => {
240                unimplemented!()
241            }
242            Ensemble::Isotensional(_) => {
243                IsotensionalExtensible::nondimensional_link_length_variance(
244                    self,
245                    nondimensional_force,
246                )
247            }
248        }
249    }
250    fn nondimensional_link_length_probability(
251        &self,
252        nondimensional_length: Scalar,
253        nondimensional_force: Scalar,
254    ) -> Result<Scalar, SingleChainError> {
255        match self.ensemble() {
256            Ensemble::Isometric(_) => {
257                unimplemented!()
258            }
259            Ensemble::Isotensional(_) => {
260                IsotensionalExtensible::nondimensional_link_length_probability(
261                    self,
262                    nondimensional_length,
263                    nondimensional_force,
264                )
265            }
266        }
267    }
268}
269
270pub trait Isometric
271where
272    Self: SingleChain,
273{
274    /// ```math
275    /// \beta\psi(\gamma) = -\ln Q(\gamma)
276    /// ```
277    fn nondimensional_helmholtz_free_energy(
278        &self,
279        nondimensional_extension: Scalar,
280    ) -> Result<Scalar, SingleChainError> {
281        Ok(
282            self.nondimensional_helmholtz_free_energy_per_link(nondimensional_extension)?
283                * (self.number_of_links() as Scalar),
284        )
285    }
286    /// ```math
287    /// \vartheta(\gamma) = \beta\psi(\gamma) / N_b
288    /// ```
289    fn nondimensional_helmholtz_free_energy_per_link(
290        &self,
291        nondimensional_extension: Scalar,
292    ) -> Result<Scalar, SingleChainError> {
293        Ok(
294            self.nondimensional_helmholtz_free_energy(nondimensional_extension)?
295                / (self.number_of_links() as Scalar),
296        )
297    }
298    /// ```math
299    /// \eta(\gamma) = \frac{\partial\vartheta}{\partial\gamma}
300    /// ```
301    fn nondimensional_force(
302        &self,
303        nondimensional_extension: Scalar,
304    ) -> Result<Scalar, SingleChainError>;
305    /// ```math
306    /// k(\gamma) = \frac{\partial\eta}{\partial\gamma}
307    /// ```
308    fn nondimensional_stiffness(
309        &self,
310        nondimensional_extension: Scalar,
311    ) -> Result<Scalar, SingleChainError>;
312    /// ```math
313    /// \mathcal{g}(\gamma) = 4\pi\gamma^2\mathcal{P}(\gamma)
314    /// ```
315    fn nondimensional_radial_distribution(
316        &self,
317        nondimensional_extension: Scalar,
318    ) -> Result<Scalar, SingleChainError> {
319        Ok(
320            self.nondimensional_spherical_distribution(nondimensional_extension)?
321                * (4.0 * PI * nondimensional_extension.powi(2)),
322        )
323    }
324    /// ```math
325    /// \mathcal{P}(\gamma) \propto e^{-\beta\psi(\gamma)}
326    /// ```
327    fn nondimensional_spherical_distribution(
328        &self,
329        nondimensional_extension: Scalar,
330    ) -> Result<Scalar, SingleChainError>;
331}
332
333pub trait Isotensional
334where
335    Self: SingleChain,
336{
337    /// ```math
338    /// \beta\varphi(\eta) = -\ln Z(\eta)
339    /// ```
340    fn nondimensional_gibbs_free_energy(
341        &self,
342        nondimensional_force: Scalar,
343    ) -> Result<Scalar, SingleChainError> {
344        Ok(
345            self.nondimensional_gibbs_free_energy_per_link(nondimensional_force)?
346                * (self.number_of_links() as Scalar),
347        )
348    }
349    /// ```math
350    /// \varrho(\eta) = \beta\varphi(\eta) / N_b
351    /// ```
352    fn nondimensional_gibbs_free_energy_per_link(
353        &self,
354        nondimensional_force: Scalar,
355    ) -> Result<Scalar, SingleChainError> {
356        Ok(self.nondimensional_gibbs_free_energy(nondimensional_force)?
357            / (self.number_of_links() as Scalar))
358    }
359    /// ```math
360    /// \gamma(\eta) = -\frac{\partial\varrho}{\partial\eta}
361    /// ```
362    fn nondimensional_extension(
363        &self,
364        nondimensional_force: Scalar,
365    ) -> Result<Scalar, SingleChainError>;
366    /// ```math
367    /// c(\eta) = \frac{\partial\gamma}{\partial\eta}
368    /// ```
369    fn nondimensional_compliance(
370        &self,
371        nondimensional_force: Scalar,
372    ) -> Result<Scalar, SingleChainError>;
373}
374
375pub trait IsotensionalExtensible
376where
377    Self: Extensible + Isotensional,
378{
379    /// ```math
380    /// \langle\upsilon\rangle = \varepsilon\,\frac{\partial\varrho}{\partial\varepsilon}
381    /// ```
382    fn nondimensional_link_energy_average(
383        &self,
384        nondimensional_force: Scalar,
385    ) -> Result<Scalar, SingleChainError>;
386    /// ```math
387    /// \sigma_\upsilon^2 = -\varepsilon^2\frac{\partial^2\varrho}{\partial\varepsilon^2}
388    /// ```
389    fn nondimensional_link_energy_variance(
390        &self,
391        nondimensional_force: Scalar,
392    ) -> Result<Scalar, SingleChainError>;
393    /// ```math
394    /// p(\upsilon\,|\,\eta) = \int p(\lambda\,|\,\eta)\,\delta[\upsilon - \upsilon(\lambda)]\,d\lambda
395    /// ```
396    fn nondimensional_link_energy_probability(
397        &self,
398        nondimensional_energy: Scalar,
399        nondimensional_force: Scalar,
400    ) -> Result<Scalar, SingleChainError>;
401    /// ```math
402    /// \langle\lambda\rangle = \int_0^\infty p(\lambda\,|\,\eta)\,\lambda\,d\lambda
403    /// ```
404    fn nondimensional_link_length_average(
405        &self,
406        nondimensional_force: Scalar,
407    ) -> Result<Scalar, SingleChainError>;
408    /// ```math
409    /// \sigma_\lambda^2 = \langle\lambda^2\rangle - \langle\lambda\rangle^2
410    /// ```
411    fn nondimensional_link_length_variance(
412        &self,
413        nondimensional_force: Scalar,
414    ) -> Result<Scalar, SingleChainError>;
415    /// ```math
416    /// p(\lambda\,|\,\eta) = \frac{z_0(\eta,\lambda)}{z(\eta)}\,e^{-\upsilon(\lambda)}
417    /// ```
418    fn nondimensional_link_length_probability(
419        &self,
420        nondimensional_length: Scalar,
421        nondimensional_force: Scalar,
422    ) -> Result<Scalar, SingleChainError>;
423}
424
425pub trait Legendre
426where
427    Self: Isometric + Isotensional + SingleChain,
428{
429    /// ```math
430    /// \beta\psi(\gamma) = \beta\varphi(\eta) + N_b\eta(\gamma)\gamma
431    /// ```
432    fn nondimensional_helmholtz_free_energy(
433        &self,
434        nondimensional_extension: Scalar,
435    ) -> Result<Scalar, SingleChainError> {
436        let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
437        Ok(
438            Isotensional::nondimensional_gibbs_free_energy(self, nondimensional_force)?
439                + self.number_of_links() as Scalar
440                    * nondimensional_force
441                    * nondimensional_extension,
442        )
443    }
444    /// ```math
445    /// \vartheta(\gamma) = \varrho(\eta) + \eta(\gamma)\gamma
446    /// ```
447    fn nondimensional_helmholtz_free_energy_per_link(
448        &self,
449        nondimensional_extension: Scalar,
450    ) -> Result<Scalar, SingleChainError> {
451        Ok(
452            Legendre::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
453                / (self.number_of_links() as Scalar),
454        )
455    }
456    /// ```math
457    /// \eta(\gamma) = \gamma^{-1}(\gamma)
458    /// ```
459    fn nondimensional_force(
460        &self,
461        nondimensional_extension: Scalar,
462    ) -> Result<Scalar, SingleChainError> {
463        match (NewtonRaphson {
464            abs_tol: 1e-10,
465            line_search: LineSearch::Error {
466                cut_back: 5e-1,
467                max_steps: 10,
468            },
469            ..Default::default()
470        }
471        .minimize(
472            |&nondimensional_force| {
473                Ok(Isotensional::nondimensional_gibbs_free_energy_per_link(
474                    self,
475                    nondimensional_force,
476                )? - nondimensional_force * nondimensional_extension)
477            },
478            |&nondimensional_force| {
479                Ok(
480                    Isotensional::nondimensional_extension(self, nondimensional_force)?
481                        - nondimensional_extension,
482                )
483            },
484            |&nondimensional_force| {
485                Ok(Isotensional::nondimensional_compliance(
486                    self,
487                    nondimensional_force,
488                )?)
489            },
490            nondimensional_extension,
491            EqualityConstraint::None,
492            None,
493        )) {
494            Ok(nondimensional_force) => Ok(nondimensional_force),
495            Err(error) => Err(SingleChainError::Upstream(
496                format!("{error}"),
497                format!("{self:?}"),
498            )),
499        }
500    }
501    /// ```math
502    /// k(\gamma) = \left(\frac{\partial\gamma}{\partial\eta}\right)^{-1}
503    /// ```
504    fn nondimensional_stiffness(
505        &self,
506        nondimensional_extension: Scalar,
507    ) -> Result<Scalar, SingleChainError> {
508        let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
509        Ok(1.0 / Isotensional::nondimensional_compliance(self, nondimensional_force)?)
510    }
511    /// ```math
512    /// \mathcal{g}(\gamma) = 4\pi\gamma^2\mathcal{P}(\gamma)
513    /// ```
514    fn nondimensional_radial_distribution(
515        &self,
516        nondimensional_extension: Scalar,
517    ) -> Result<Scalar, SingleChainError> {
518        Ok(
519            Legendre::nondimensional_spherical_distribution(self, nondimensional_extension)?
520                * (4.0 * PI * nondimensional_extension.powi(2)),
521        )
522    }
523    /// ```math
524    /// \mathcal{P}(\gamma) \propto e^{-\beta\psi(\gamma)}
525    /// ```
526    fn nondimensional_spherical_distribution(
527        &self,
528        nondimensional_extension: Scalar,
529    ) -> Result<Scalar, SingleChainError>;
530    /// ```math
531    /// \beta\varphi(\eta) = \beta\psi(\gamma) - N_b\eta\gamma(\eta)
532    /// ```
533    fn nondimensional_gibbs_free_energy(
534        &self,
535        nondimensional_force: Scalar,
536    ) -> Result<Scalar, SingleChainError> {
537        let nondimensional_extension =
538            Legendre::nondimensional_extension(self, nondimensional_force)?;
539        Ok(
540            Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
541                - self.number_of_links() as Scalar
542                    * nondimensional_force
543                    * nondimensional_extension,
544        )
545    }
546    /// ```math
547    /// \varrho(\eta) = \vartheta(\gamma) - \eta\gamma(\eta)
548    /// ```
549    fn nondimensional_gibbs_free_energy_per_link(
550        &self,
551        nondimensional_force: Scalar,
552    ) -> Result<Scalar, SingleChainError> {
553        Ok(
554            Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)?
555                / (self.number_of_links() as Scalar),
556        )
557    }
558    /// ```math
559    /// \gamma(\eta) = \eta^{-1}(\eta)
560    /// ```
561    fn nondimensional_extension(
562        &self,
563        nondimensional_force: Scalar,
564    ) -> Result<Scalar, SingleChainError> {
565        match (NewtonRaphson {
566            abs_tol: 1e-10,
567            line_search: LineSearch::Error {
568                cut_back: 5e-1,
569                max_steps: 10,
570            },
571            ..Default::default()
572        }
573        .minimize(
574            |&nondimensional_extension| {
575                Ok(Isometric::nondimensional_helmholtz_free_energy_per_link(
576                    self,
577                    nondimensional_extension,
578                )? - nondimensional_force * nondimensional_extension)
579            },
580            |&nondimensional_extension| {
581                Ok(
582                    Isometric::nondimensional_force(self, nondimensional_extension)?
583                        - nondimensional_force,
584                )
585            },
586            |&nondimensional_extension| {
587                Ok(Isometric::nondimensional_stiffness(
588                    self,
589                    nondimensional_extension,
590                )?)
591            },
592            nondimensional_force,
593            EqualityConstraint::None,
594            None,
595        )) {
596            Ok(nondimensional_extension) => Ok(nondimensional_extension),
597            Err(error) => Err(SingleChainError::Upstream(
598                format!("{error}"),
599                format!("{self:?}"),
600            )),
601        }
602    }
603    /// ```math
604    /// c(\eta) = \left(\frac{\partial\eta}{\partial\gamma}\right)^{-1}
605    /// ```
606    fn nondimensional_compliance(
607        &self,
608        nondimensional_force: Scalar,
609    ) -> Result<Scalar, SingleChainError> {
610        let nondimensional_extension =
611            Legendre::nondimensional_extension(self, nondimensional_force)?;
612        Ok(1.0 / Isometric::nondimensional_stiffness(self, nondimensional_extension)?)
613    }
614}
615
616pub trait MonteCarlo
617where
618    Self: SingleChain + Sync,
619{
620    fn random_nondimensional_link_vectors(&self, nondimensional_force: Scalar) -> Configuration;
621    fn random_configuration(&self, nondimensional_force: Scalar) -> Configuration {
622        let mut position = CurrentCoordinate::zero();
623        self.random_nondimensional_link_vectors(nondimensional_force)
624            .into_iter()
625            .map(|displacement| {
626                position += displacement;
627                position.clone()
628            })
629            .collect()
630    }
631}
632
633pub trait MonteCarloExtensible
634where
635    Self: Extensible + MonteCarlo,
636{
637    fn cosine_powers(
638        &self,
639        nondimensional_force: Scalar,
640        number_of_powers: usize,
641        number_of_samples: usize,
642        number_of_threads: usize,
643    ) -> Matrix {
644        cosine_powers(
645            self,
646            nondimensional_force,
647            number_of_powers,
648            number_of_samples,
649            number_of_threads,
650            true,
651        )
652    }
653    fn nondimensional_extension(
654        &self,
655        nondimensional_force: Scalar,
656        num_samples: usize,
657        num_threads: usize,
658    ) -> Scalar {
659        self.cosine_powers(nondimensional_force, 0, num_samples, num_threads)
660            .into_iter()
661            .flatten()
662            .sum::<Scalar>()
663            / self.number_of_links() as Scalar
664    }
665    fn nondimensional_lateral_distribution(
666        &self,
667        nondimensional_force: Scalar,
668        num_bins: usize,
669        num_samples: usize,
670        num_threads: usize,
671        maximum_nondimensional_extension: Scalar,
672    ) -> (Vector, Vector) {
673        nondimensional_lateral_distribution(
674            self,
675            nondimensional_force,
676            num_bins,
677            num_samples,
678            num_threads,
679            maximum_nondimensional_extension,
680        )
681    }
682    fn nondimensional_longitudinal_distribution(
683        &self,
684        nondimensional_force: Scalar,
685        num_bins: usize,
686        num_samples: usize,
687        num_threads: usize,
688        maximum_nondimensional_extension: Scalar,
689    ) -> (Vector, Vector) {
690        nondimensional_longitudinal_distribution(
691            self,
692            nondimensional_force,
693            num_bins,
694            num_samples,
695            num_threads,
696            maximum_nondimensional_extension,
697        )
698    }
699    fn nondimensional_radial_distribution(
700        &self,
701        nondimensional_force: Scalar,
702        num_bins: usize,
703        num_samples: usize,
704        num_threads: usize,
705        maximum_nondimensional_extension: Scalar,
706    ) -> (Vector, Vector) {
707        nondimensional_radial_distribution(
708            self,
709            nondimensional_force,
710            num_bins,
711            num_samples,
712            num_threads,
713            maximum_nondimensional_extension,
714        )
715    }
716    fn nondimensional_transverse_distribution(
717        &self,
718        nondimensional_force: Scalar,
719        num_bins: usize,
720        num_samples: usize,
721        num_threads: usize,
722        maximum_nondimensional_extension: Scalar,
723    ) -> (Vector, Vector) {
724        nondimensional_transverse_distribution(
725            self,
726            nondimensional_force,
727            num_bins,
728            num_samples,
729            num_threads,
730            maximum_nondimensional_extension,
731        )
732    }
733}
734
735impl<T> MonteCarloExtensible for T where T: Extensible + MonteCarlo {}
736
737pub trait MonteCarloInextensible
738where
739    Self: Inextensible + MonteCarlo,
740{
741    fn cosine_powers(
742        &self,
743        nondimensional_force: Scalar,
744        number_of_powers: usize,
745        number_of_samples: usize,
746        number_of_threads: usize,
747    ) -> Matrix {
748        cosine_powers(
749            self,
750            nondimensional_force,
751            number_of_powers,
752            number_of_samples,
753            number_of_threads,
754            false,
755        )
756    }
757    fn nondimensional_extension(
758        &self,
759        nondimensional_force: Scalar,
760        num_samples: usize,
761        num_threads: usize,
762    ) -> Scalar {
763        self.cosine_powers(nondimensional_force, 1, num_samples, num_threads)
764            .into_iter()
765            .flatten()
766            .sum::<Scalar>()
767            / self.number_of_links() as Scalar
768    }
769    fn nondimensional_angular_distribution(
770        &self,
771        nondimensional_force: Scalar,
772        num_bins: usize,
773        num_samples: usize,
774        num_threads: usize,
775    ) -> (Vector, Vector) {
776        nondimensional_angular_distribution(
777            self,
778            nondimensional_force,
779            num_bins,
780            num_samples,
781            num_threads,
782            self.maximum_nondimensional_extension(),
783        )
784    }
785    fn nondimensional_lateral_distribution(
786        &self,
787        nondimensional_force: Scalar,
788        num_bins: usize,
789        num_samples: usize,
790        num_threads: usize,
791    ) -> (Vector, Vector) {
792        nondimensional_lateral_distribution(
793            self,
794            nondimensional_force,
795            num_bins,
796            num_samples,
797            num_threads,
798            self.maximum_nondimensional_extension(),
799        )
800    }
801    fn nondimensional_longitudinal_distribution(
802        &self,
803        nondimensional_force: Scalar,
804        num_bins: usize,
805        num_samples: usize,
806        num_threads: usize,
807    ) -> (Vector, Vector) {
808        nondimensional_longitudinal_distribution(
809            self,
810            nondimensional_force,
811            num_bins,
812            num_samples,
813            num_threads,
814            self.maximum_nondimensional_extension(),
815        )
816    }
817    fn nondimensional_radial_distribution(
818        &self,
819        nondimensional_force: Scalar,
820        num_bins: usize,
821        num_samples: usize,
822        num_threads: usize,
823    ) -> (Vector, Vector) {
824        nondimensional_radial_distribution(
825            self,
826            nondimensional_force,
827            num_bins,
828            num_samples,
829            num_threads,
830            self.maximum_nondimensional_extension(),
831        )
832    }
833    fn nondimensional_transverse_distribution(
834        &self,
835        nondimensional_force: Scalar,
836        num_bins: usize,
837        num_samples: usize,
838        num_threads: usize,
839    ) -> (Vector, Vector) {
840        nondimensional_transverse_distribution(
841            self,
842            nondimensional_force,
843            num_bins,
844            num_samples,
845            num_threads,
846            self.maximum_nondimensional_extension(),
847        )
848    }
849}
850
851impl<T> MonteCarloInextensible for T where T: Inextensible + MonteCarlo {}
852
853fn cosine_powers<T: MonteCarlo>(
854    model: &T,
855    nondimensional_force: Scalar,
856    number_of_powers: usize,
857    number_of_samples: usize,
858    number_of_threads: usize,
859    is_extensible: bool,
860) -> Matrix {
861    let base = number_of_samples / number_of_threads;
862    let remainder = number_of_samples % number_of_threads;
863    scope(|s| {
864        (0..number_of_threads)
865            .map(|t| {
866                s.spawn(move || {
867                    cosine_powers_inner(
868                        model,
869                        nondimensional_force,
870                        number_of_powers,
871                        base + usize::from(t < remainder),
872                        is_extensible,
873                    )
874                })
875            })
876            .collect::<Vec<_>>()
877            .into_iter()
878            .map(|handle| handle.join().unwrap() / number_of_samples as Scalar)
879            .sum()
880    })
881}
882
883fn cosine_powers_inner<T: MonteCarlo>(
884    model: &T,
885    nondimensional_force: Scalar,
886    number_of_powers: usize,
887    number_of_samples: usize,
888    is_extensible: bool,
889) -> Matrix {
890    let mut cosines = Matrix::zero(
891        model.number_of_links() as usize,
892        number_of_powers + is_extensible as usize,
893    );
894    for _ in 0..number_of_samples {
895        cosines
896            .iter_mut()
897            .zip(model.random_nondimensional_link_vectors(nondimensional_force))
898            .for_each(|(powers, link)| {
899                if is_extensible {
900                    powers[0] += link[2];
901                    let unit = link.normalized();
902                    powers
903                        .iter_mut()
904                        .skip(1)
905                        .enumerate()
906                        .for_each(|(power, entry)| *entry += unit[2].powi(power as i32 + 1))
907                } else {
908                    powers
909                        .iter_mut()
910                        .enumerate()
911                        .for_each(|(power, entry)| *entry += link[2].powi(power as i32 + 1))
912                }
913            })
914    }
915    cosines
916}
917
918fn nondimensional_angular_distribution<T: MonteCarlo>(
919    model: &T,
920    nondimensional_force: Scalar,
921    number_of_bins: usize,
922    number_of_samples: usize,
923    number_of_threads: usize,
924    maximum_nondimensional_extension: Scalar,
925) -> (Vector, Vector) {
926    let base = number_of_samples / number_of_threads;
927    let remainder = number_of_samples % number_of_threads;
928    scope(|s| {
929        let mut total_counts = vec![0; number_of_bins];
930        (0..number_of_threads)
931            .map(|t| {
932                s.spawn(move || {
933                    nondimensional_angular_distribution_inner(
934                        model,
935                        nondimensional_force,
936                        number_of_bins,
937                        base + usize::from(t < remainder),
938                        maximum_nondimensional_extension,
939                    )
940                })
941            })
942            .collect::<Vec<_>>()
943            .into_iter()
944            .for_each(|handle| {
945                total_counts
946                    .iter_mut()
947                    .zip(handle.join().unwrap())
948                    .for_each(|(tot, c)| *tot += c)
949            });
950        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
951        let bin_centers = (0..number_of_bins)
952            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
953            .collect();
954        let total_samples = number_of_samples as Scalar;
955        let bin_values = total_counts
956            .into_iter()
957            .map(|count| count as Scalar / total_samples / bin_width)
958            .collect();
959        (bin_centers, bin_values)
960    })
961}
962
963fn nondimensional_lateral_distribution<T: MonteCarlo>(
964    model: &T,
965    nondimensional_force: Scalar,
966    number_of_bins: usize,
967    number_of_samples: usize,
968    number_of_threads: usize,
969    maximum_nondimensional_extension: Scalar,
970) -> (Vector, Vector) {
971    let base = number_of_samples / number_of_threads;
972    let remainder = number_of_samples % number_of_threads;
973    scope(|s| {
974        let mut total_counts = vec![0; number_of_bins];
975        (0..number_of_threads)
976            .map(|t| {
977                s.spawn(move || {
978                    nondimensional_lateral_distribution_inner(
979                        model,
980                        nondimensional_force,
981                        number_of_bins,
982                        base + usize::from(t < remainder),
983                        maximum_nondimensional_extension,
984                    )
985                })
986            })
987            .collect::<Vec<_>>()
988            .into_iter()
989            .for_each(|handle| {
990                total_counts
991                    .iter_mut()
992                    .zip(handle.join().unwrap())
993                    .for_each(|(tot, c)| *tot += c)
994            });
995        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
996        let bin_centers = (0..number_of_bins)
997            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
998            .collect();
999        let total_samples = number_of_samples as Scalar;
1000        let bin_values = total_counts
1001            .into_iter()
1002            .map(|count| count as Scalar / total_samples / bin_width)
1003            .collect();
1004        (bin_centers, bin_values)
1005    })
1006}
1007
1008fn nondimensional_longitudinal_distribution<T: MonteCarlo>(
1009    model: &T,
1010    nondimensional_force: Scalar,
1011    number_of_bins: usize,
1012    number_of_samples: usize,
1013    number_of_threads: usize,
1014    maximum_nondimensional_extension: Scalar,
1015) -> (Vector, Vector) {
1016    let base = number_of_samples / number_of_threads;
1017    let remainder = number_of_samples % number_of_threads;
1018    scope(|s| {
1019        let mut total_counts = vec![0; number_of_bins];
1020        (0..number_of_threads)
1021            .map(|t| {
1022                s.spawn(move || {
1023                    nondimensional_longitudinal_distribution_inner(
1024                        model,
1025                        nondimensional_force,
1026                        number_of_bins,
1027                        base + usize::from(t < remainder),
1028                        maximum_nondimensional_extension,
1029                    )
1030                })
1031            })
1032            .collect::<Vec<_>>()
1033            .into_iter()
1034            .for_each(|handle| {
1035                total_counts
1036                    .iter_mut()
1037                    .zip(handle.join().unwrap())
1038                    .for_each(|(tot, c)| *tot += c)
1039            });
1040        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1041        let bin_centers = (0..number_of_bins)
1042            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1043            .collect();
1044        let total_samples = number_of_samples as Scalar;
1045        let bin_values = total_counts
1046            .into_iter()
1047            .map(|count| count as Scalar / total_samples / bin_width)
1048            .collect();
1049        (bin_centers, bin_values)
1050    })
1051}
1052
1053fn nondimensional_radial_distribution<T: MonteCarlo>(
1054    model: &T,
1055    nondimensional_force: Scalar,
1056    number_of_bins: usize,
1057    number_of_samples: usize,
1058    number_of_threads: usize,
1059    maximum_nondimensional_extension: Scalar,
1060) -> (Vector, Vector) {
1061    let base = number_of_samples / number_of_threads;
1062    let remainder = number_of_samples % number_of_threads;
1063    scope(|s| {
1064        let mut total_counts = vec![0; number_of_bins];
1065        (0..number_of_threads)
1066            .map(|t| {
1067                s.spawn(move || {
1068                    nondimensional_radial_distribution_inner(
1069                        model,
1070                        nondimensional_force,
1071                        number_of_bins,
1072                        base + usize::from(t < remainder),
1073                        maximum_nondimensional_extension,
1074                    )
1075                })
1076            })
1077            .collect::<Vec<_>>()
1078            .into_iter()
1079            .for_each(|handle| {
1080                total_counts
1081                    .iter_mut()
1082                    .zip(handle.join().unwrap())
1083                    .for_each(|(tot, c)| *tot += c)
1084            });
1085        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1086        let bin_centers = (0..number_of_bins)
1087            .map(|i| (i as Scalar + 0.5) * bin_width)
1088            .collect();
1089        let total_samples = number_of_samples as Scalar;
1090        let bin_values = total_counts
1091            .into_iter()
1092            .map(|count| count as Scalar / total_samples / bin_width)
1093            .collect();
1094        (bin_centers, bin_values)
1095    })
1096}
1097
1098fn nondimensional_transverse_distribution<T: MonteCarlo>(
1099    model: &T,
1100    nondimensional_force: Scalar,
1101    number_of_bins: usize,
1102    number_of_samples: usize,
1103    number_of_threads: usize,
1104    maximum_nondimensional_extension: Scalar,
1105) -> (Vector, Vector) {
1106    let base = number_of_samples / number_of_threads;
1107    let remainder = number_of_samples % number_of_threads;
1108    scope(|s| {
1109        let mut total_counts = vec![0; number_of_bins];
1110        (0..number_of_threads)
1111            .map(|t| {
1112                s.spawn(move || {
1113                    nondimensional_transverse_distribution_inner(
1114                        model,
1115                        nondimensional_force,
1116                        number_of_bins,
1117                        base + usize::from(t < remainder),
1118                        maximum_nondimensional_extension,
1119                    )
1120                })
1121            })
1122            .collect::<Vec<_>>()
1123            .into_iter()
1124            .for_each(|handle| {
1125                total_counts
1126                    .iter_mut()
1127                    .zip(handle.join().unwrap())
1128                    .for_each(|(tot, c)| *tot += c)
1129            });
1130        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1131        let bin_centers = (0..number_of_bins)
1132            .map(|i| (i as Scalar + 0.5) * bin_width)
1133            .collect();
1134        let total_samples = number_of_samples as Scalar;
1135        let bin_values = total_counts
1136            .into_iter()
1137            .map(|count| count as Scalar / total_samples / bin_width)
1138            .collect();
1139        (bin_centers, bin_values)
1140    })
1141}
1142
1143fn nondimensional_angular_distribution_inner<T: MonteCarlo>(
1144    model: &T,
1145    nondimensional_force: Scalar,
1146    num_bins: usize,
1147    num_samples: usize,
1148    maximum_nondimensional_extension: Scalar,
1149) -> Vec<usize> {
1150    let mut bin_counts = vec![0; num_bins];
1151    let end_index = model.number_of_links() as usize - 1;
1152    for _ in 0..num_samples {
1153        let configuration = model.random_configuration(nondimensional_force);
1154        let gamma = configuration[end_index].norm();
1155        let nondimensional_extension = if gamma == 0.0 {
1156            0.0
1157        } else {
1158            configuration[end_index][2] / gamma
1159        };
1160        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1161            panic!(
1162                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1163            )
1164        }
1165        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1166            / (2.0 * maximum_nondimensional_extension)
1167            * num_bins as Scalar) as usize;
1168        bin_counts[bin_index] += 1;
1169    }
1170    bin_counts
1171}
1172
1173fn nondimensional_lateral_distribution_inner<T: MonteCarlo>(
1174    model: &T,
1175    nondimensional_force: Scalar,
1176    num_bins: usize,
1177    num_samples: usize,
1178    maximum_nondimensional_extension: Scalar,
1179) -> Vec<usize> {
1180    let mut bin_counts = vec![0; num_bins];
1181    let num_links = model.number_of_links() as Scalar;
1182    let end_index = model.number_of_links() as usize - 1;
1183    for _ in 0..num_samples {
1184        let configuration = model.random_configuration(nondimensional_force);
1185        let nondimensional_extension = configuration[end_index][1] / num_links;
1186        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1187            panic!(
1188                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1189            )
1190        }
1191        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1192            / (2.0 * maximum_nondimensional_extension)
1193            * num_bins as Scalar) as usize;
1194        bin_counts[bin_index] += 1;
1195    }
1196    bin_counts
1197}
1198
1199fn nondimensional_longitudinal_distribution_inner<T: MonteCarlo>(
1200    model: &T,
1201    nondimensional_force: Scalar,
1202    num_bins: usize,
1203    num_samples: usize,
1204    maximum_nondimensional_extension: Scalar,
1205) -> Vec<usize> {
1206    let mut bin_counts = vec![0; num_bins];
1207    let num_links = model.number_of_links() as Scalar;
1208    let end_index = model.number_of_links() as usize - 1;
1209    for _ in 0..num_samples {
1210        let configuration = model.random_configuration(nondimensional_force);
1211        let nondimensional_extension = configuration[end_index][2] / num_links;
1212        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1213            panic!(
1214                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1215            )
1216        }
1217        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1218            / (2.0 * maximum_nondimensional_extension)
1219            * num_bins as Scalar) as usize;
1220        bin_counts[bin_index] += 1;
1221    }
1222    bin_counts
1223}
1224
1225fn nondimensional_radial_distribution_inner<T: MonteCarlo>(
1226    model: &T,
1227    nondimensional_force: Scalar,
1228    num_bins: usize,
1229    num_samples: usize,
1230    maximum_nondimensional_extension: Scalar,
1231) -> Vec<usize> {
1232    let mut bin_counts = vec![0; num_bins];
1233    let num_links = model.number_of_links() as Scalar;
1234    let end_index = model.number_of_links() as usize - 1;
1235    for _ in 0..num_samples {
1236        let configuration = model.random_configuration(nondimensional_force);
1237        let nondimensional_extension = configuration[end_index].norm() / num_links;
1238        if nondimensional_extension > maximum_nondimensional_extension {
1239            panic!(
1240                "Sample {nondimensional_extension} above maximum {maximum_nondimensional_extension}"
1241            )
1242        }
1243        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1244            * num_bins as Scalar) as usize;
1245        bin_counts[bin_index] += 1;
1246    }
1247    bin_counts
1248}
1249
1250fn nondimensional_transverse_distribution_inner<T: MonteCarlo>(
1251    model: &T,
1252    nondimensional_force: Scalar,
1253    num_bins: usize,
1254    num_samples: usize,
1255    maximum_nondimensional_extension: Scalar,
1256) -> Vec<usize> {
1257    let mut bin_counts = vec![0; num_bins];
1258    let num_links = model.number_of_links() as Scalar;
1259    let end_index = model.number_of_links() as usize - 1;
1260    for _ in 0..num_samples {
1261        let configuration = model.random_configuration(nondimensional_force);
1262        let nondimensional_extension =
1263            (configuration[end_index][0].powi(2) + configuration[end_index][1].powi(2)).sqrt()
1264                / num_links;
1265        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1266            panic!(
1267                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1268            )
1269        }
1270        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1271            * num_bins as Scalar) as usize;
1272        bin_counts[bin_index] += 1;
1273    }
1274    bin_counts
1275}