Skip to main content

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

1use crate::{
2    math::{
3        Scalar, SquareMatrix, 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        Ok(
531            Legendre::nondimensional_radial_distribution(self, nondimensional_extension)?
532                / (4.0 * PI * nondimensional_extension.powi(2)),
533        )
534    }
535    /// ```math
536    /// \beta\varphi(\eta) = \beta\psi(\gamma) - N_b\eta\gamma(\eta)
537    /// ```
538    fn nondimensional_gibbs_free_energy(
539        &self,
540        nondimensional_force: Scalar,
541    ) -> Result<Scalar, SingleChainError> {
542        let nondimensional_extension =
543            Legendre::nondimensional_extension(self, nondimensional_force)?;
544        Ok(
545            Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
546                - self.number_of_links() as Scalar
547                    * nondimensional_force
548                    * nondimensional_extension,
549        )
550    }
551    /// ```math
552    /// \varrho(\eta) = \vartheta(\gamma) - \eta\gamma(\eta)
553    /// ```
554    fn nondimensional_gibbs_free_energy_per_link(
555        &self,
556        nondimensional_force: Scalar,
557    ) -> Result<Scalar, SingleChainError> {
558        Ok(
559            Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)?
560                / (self.number_of_links() as Scalar),
561        )
562    }
563    /// ```math
564    /// \gamma(\eta) = \eta^{-1}(\eta)
565    /// ```
566    fn nondimensional_extension(
567        &self,
568        nondimensional_force: Scalar,
569    ) -> Result<Scalar, SingleChainError> {
570        match (NewtonRaphson {
571            abs_tol: 1e-10,
572            line_search: LineSearch::Error {
573                cut_back: 5e-1,
574                max_steps: 10,
575            },
576            ..Default::default()
577        }
578        .minimize(
579            |&nondimensional_extension| {
580                Ok(Isometric::nondimensional_helmholtz_free_energy_per_link(
581                    self,
582                    nondimensional_extension,
583                )? - nondimensional_force * nondimensional_extension)
584            },
585            |&nondimensional_extension| {
586                Ok(
587                    Isometric::nondimensional_force(self, nondimensional_extension)?
588                        - nondimensional_force,
589                )
590            },
591            |&nondimensional_extension| {
592                Ok(Isometric::nondimensional_stiffness(
593                    self,
594                    nondimensional_extension,
595                )?)
596            },
597            nondimensional_force,
598            EqualityConstraint::None,
599            None,
600        )) {
601            Ok(nondimensional_extension) => Ok(nondimensional_extension),
602            Err(error) => Err(SingleChainError::Upstream(
603                format!("{error}"),
604                format!("{self:?}"),
605            )),
606        }
607    }
608    /// ```math
609    /// c(\eta) = \left(\frac{\partial\eta}{\partial\gamma}\right)^{-1}
610    /// ```
611    fn nondimensional_compliance(
612        &self,
613        nondimensional_force: Scalar,
614    ) -> Result<Scalar, SingleChainError> {
615        let nondimensional_extension =
616            Legendre::nondimensional_extension(self, nondimensional_force)?;
617        Ok(1.0 / Isometric::nondimensional_stiffness(self, nondimensional_extension)?)
618    }
619}
620
621pub trait MonteCarlo
622where
623    Self: SingleChain + Sync,
624{
625    fn cosine_moments(
626        &self,
627        nondimensional_force: Scalar,
628        number_of_samples: usize,
629        number_of_threads: usize,
630    ) -> (Vector, SquareMatrix, Vector, SquareMatrix) {
631        cosine_moments_reweighted(
632            self,
633            nondimensional_force,
634            number_of_samples,
635            number_of_threads,
636        )
637    }
638    fn nondimensional_longitudinal_extension(
639        &self,
640        nondimensional_force: Scalar,
641        number_of_samples: usize,
642        number_of_threads: usize,
643    ) -> Scalar {
644        //
645        // Does not work well above a certain force (see EFRC for bias method).
646        //
647        // Should set up to use random_nondimensional_link_vectors when implemented to work for a given force,
648        // and use the re-weighting (possibly with the above bias method) when unimplemented under force.
649        //
650        nondimensional_longitudinal_extension_reweighted(
651            self,
652            nondimensional_force,
653            number_of_samples,
654            number_of_threads,
655        )
656    }
657    fn random_nondimensional_link_vectors(&self, nondimensional_force: Scalar) -> Configuration;
658    fn random_configuration(&self, nondimensional_force: Scalar) -> Configuration {
659        let mut position = CurrentCoordinate::zero();
660        self.random_nondimensional_link_vectors(nondimensional_force)
661            .into_iter()
662            .map(|displacement| {
663                position += displacement;
664                position.clone()
665            })
666            .collect()
667    }
668}
669
670pub trait MonteCarloExtensible
671where
672    Self: Extensible + MonteCarlo,
673{
674    fn nondimensional_lateral_distribution(
675        &self,
676        nondimensional_force: Scalar,
677        num_bins: usize,
678        number_of_samples: usize,
679        number_of_threads: usize,
680        maximum_nondimensional_extension: Scalar,
681    ) -> (Vector, Vector) {
682        nondimensional_lateral_distribution(
683            self,
684            nondimensional_force,
685            num_bins,
686            number_of_samples,
687            number_of_threads,
688            maximum_nondimensional_extension,
689        )
690    }
691    fn nondimensional_longitudinal_distribution(
692        &self,
693        nondimensional_force: Scalar,
694        num_bins: usize,
695        number_of_samples: usize,
696        number_of_threads: usize,
697        maximum_nondimensional_extension: Scalar,
698    ) -> (Vector, Vector) {
699        nondimensional_longitudinal_distribution(
700            self,
701            nondimensional_force,
702            num_bins,
703            number_of_samples,
704            number_of_threads,
705            maximum_nondimensional_extension,
706        )
707    }
708    fn nondimensional_radial_distribution(
709        &self,
710        nondimensional_force: Scalar,
711        num_bins: usize,
712        number_of_samples: usize,
713        number_of_threads: usize,
714        maximum_nondimensional_extension: Scalar,
715    ) -> (Vector, Vector) {
716        nondimensional_radial_distribution(
717            self,
718            nondimensional_force,
719            num_bins,
720            number_of_samples,
721            number_of_threads,
722            maximum_nondimensional_extension,
723        )
724    }
725    fn nondimensional_transverse_distribution(
726        &self,
727        nondimensional_force: Scalar,
728        num_bins: usize,
729        number_of_samples: usize,
730        number_of_threads: usize,
731        maximum_nondimensional_extension: Scalar,
732    ) -> (Vector, Vector) {
733        nondimensional_transverse_distribution(
734            self,
735            nondimensional_force,
736            num_bins,
737            number_of_samples,
738            number_of_threads,
739            maximum_nondimensional_extension,
740        )
741    }
742}
743
744impl<T> MonteCarloExtensible for T where T: Extensible + MonteCarlo {}
745
746pub trait MonteCarloInextensible
747where
748    Self: Inextensible + MonteCarlo,
749{
750    fn nondimensional_angular_distribution(
751        &self,
752        nondimensional_force: Scalar,
753        num_bins: usize,
754        number_of_samples: usize,
755        number_of_threads: usize,
756    ) -> (Vector, Vector) {
757        nondimensional_angular_distribution(
758            self,
759            nondimensional_force,
760            num_bins,
761            number_of_samples,
762            number_of_threads,
763            self.maximum_nondimensional_extension(),
764        )
765    }
766    fn nondimensional_lateral_distribution(
767        &self,
768        nondimensional_force: Scalar,
769        num_bins: usize,
770        number_of_samples: usize,
771        number_of_threads: usize,
772    ) -> (Vector, Vector) {
773        nondimensional_lateral_distribution(
774            self,
775            nondimensional_force,
776            num_bins,
777            number_of_samples,
778            number_of_threads,
779            self.maximum_nondimensional_extension(),
780        )
781    }
782    fn nondimensional_longitudinal_distribution(
783        &self,
784        nondimensional_force: Scalar,
785        num_bins: usize,
786        number_of_samples: usize,
787        number_of_threads: usize,
788    ) -> (Vector, Vector) {
789        nondimensional_longitudinal_distribution(
790            self,
791            nondimensional_force,
792            num_bins,
793            number_of_samples,
794            number_of_threads,
795            self.maximum_nondimensional_extension(),
796        )
797    }
798    fn nondimensional_radial_distribution(
799        &self,
800        nondimensional_force: Scalar,
801        num_bins: usize,
802        number_of_samples: usize,
803        number_of_threads: usize,
804    ) -> (Vector, Vector) {
805        nondimensional_radial_distribution(
806            self,
807            nondimensional_force,
808            num_bins,
809            number_of_samples,
810            number_of_threads,
811            self.maximum_nondimensional_extension(),
812        )
813    }
814    fn nondimensional_transverse_distribution(
815        &self,
816        nondimensional_force: Scalar,
817        num_bins: usize,
818        number_of_samples: usize,
819        number_of_threads: usize,
820    ) -> (Vector, Vector) {
821        nondimensional_transverse_distribution(
822            self,
823            nondimensional_force,
824            num_bins,
825            number_of_samples,
826            number_of_threads,
827            self.maximum_nondimensional_extension(),
828        )
829    }
830}
831
832impl<T> MonteCarloInextensible for T where T: Inextensible + MonteCarlo {}
833
834pub fn cosine_moments_reweighted<T: MonteCarlo>(
835    model: &T,
836    nondimensional_force: Scalar,
837    number_of_samples: usize,
838    number_of_threads: usize,
839) -> (Vector, SquareMatrix, Vector, SquareMatrix) {
840    let base = number_of_samples / number_of_threads;
841    let remainder = number_of_samples % number_of_threads;
842    scope(|s| {
843        (0..number_of_threads)
844            .map(|t| {
845                s.spawn(move || {
846                    cosine_moments_reweighted_inner(
847                        model,
848                        nondimensional_force,
849                        base + usize::from(t < remainder),
850                    )
851                })
852            })
853            .collect::<Vec<_>>()
854            .into_iter()
855            .map(|handle| handle.join().unwrap())
856            .reduce(|mut acc, (x_max, z_scaled, cos1_scaled, cos1cos1_scaled, cos2_scaled, cos2cos1_scaled)| {
857                let x_max_new = acc.0.max(x_max);
858                let scale_acc = (acc.0 - x_max_new).exp();
859                let scale_new = (x_max - x_max_new).exp();
860                acc.1 = acc.1 * scale_acc + z_scaled * scale_new;
861                acc.2 = acc.2 * scale_acc + cos1_scaled * scale_new;
862                acc.3 = acc.3 * scale_acc + cos1cos1_scaled * scale_new;
863                acc.4 = acc.4 * scale_acc + cos2_scaled * scale_new;
864                acc.5 = acc.5 * scale_acc + cos2cos1_scaled * scale_new;
865                acc.0 = x_max_new;
866                acc
867            })
868            .map(|(_x_max, z_scaled, cos1_scaled, cos1cos1_scaled, cos2_scaled, cos2cos1_scaled)| {
869                let z_inv = 1.0 / z_scaled;
870                (
871                    cos1_scaled * z_inv,
872                    cos1cos1_scaled * z_inv,
873                    cos2_scaled * z_inv,
874                    cos2cos1_scaled * z_inv,
875                )
876            })
877            .unwrap()
878    })
879}
880
881fn cosine_moments_reweighted_inner<T: MonteCarlo>(
882    model: &T,
883    nondimensional_force: Scalar,
884    number_of_samples: usize,
885) -> (Scalar, Scalar, Vector, SquareMatrix, Vector, SquareMatrix) {
886    let num_links = model.number_of_links() as usize;
887
888    let mut x_max = Scalar::NEG_INFINITY;
889    let mut z_scaled = 0.0;
890    let mut cos1_scaled = Vector::zero(num_links);
891    let mut cos1cos1_scaled = SquareMatrix::zero(num_links);
892    let mut cos2_scaled = Vector::zero(num_links);
893    let mut cos2cos1_scaled = SquareMatrix::zero(num_links);
894
895    for _ in 0..number_of_samples {
896        let links = model.random_nondimensional_link_vectors(0.0);
897
898        let cosines: Vec<Scalar> = links.iter().map(|link| link[2] / link.norm()).collect();
899
900        let sum_cos: Scalar = cosines.iter().sum();
901        let x = nondimensional_force * sum_cos;
902
903        if x > x_max {
904            let scale = if x_max.is_finite() {
905                (x_max - x).exp()
906            } else {
907                0.0
908            };
909            z_scaled *= scale;
910            cos1_scaled *= scale;
911            cos1cos1_scaled *= scale;
912            cos2_scaled *= scale;
913            cos2cos1_scaled *= scale;
914            x_max = x;
915        }
916
917        let w = (x - x_max).exp();
918
919        z_scaled += w;
920
921        for i in 0..num_links {
922            let ci = cosines[i];
923            let ci2 = ci * ci;
924
925            cos1_scaled[i] += ci * w;
926            cos2_scaled[i] += ci2 * w;
927
928            for j in 0..num_links {
929                let cj = cosines[j];
930                cos1cos1_scaled[i][j] += ci * cj * w;
931                cos2cos1_scaled[i][j] += ci2 * cj * w;
932            }
933        }
934    }
935    (
936        x_max,
937        z_scaled,
938        cos1_scaled,
939        cos1cos1_scaled,
940        cos2_scaled,
941        cos2cos1_scaled,
942    )
943}
944
945fn nondimensional_longitudinal_extension_reweighted<T: MonteCarlo>(
946    model: &T,
947    nondimensional_force: Scalar,
948    number_of_samples: usize,
949    number_of_threads: usize,
950) -> Scalar {
951    let base = number_of_samples / number_of_threads;
952    let remainder = number_of_samples % number_of_threads;
953
954    scope(|s| {
955        (0..number_of_threads)
956            .map(|t| {
957                s.spawn(move || {
958                    nondimensional_longitudinal_extension_reweighted_inner(
959                        model,
960                        nondimensional_force,
961                        base + usize::from(t < remainder),
962                    )
963                })
964            })
965            .collect::<Vec<_>>()
966            .into_iter()
967            .map(|handle| handle.join().unwrap())
968            .reduce(|mut acc, (x_max, z_scaled, ext_scaled)| {
969                let x_max_new = acc.0.max(x_max);
970                let scale_acc = (acc.0 - x_max_new).exp();
971                let scale_new = (x_max - x_max_new).exp();
972
973                acc.1 = acc.1 * scale_acc + z_scaled * scale_new;
974                acc.2 = acc.2 * scale_acc + ext_scaled * scale_new;
975                acc.0 = x_max_new;
976                acc
977            })
978            .map(|(_x_max, z_scaled, ext_scaled)| {
979                ext_scaled / z_scaled / model.number_of_links() as Scalar
980            })
981            .unwrap()
982    })
983}
984
985fn nondimensional_longitudinal_extension_reweighted_inner<T: MonteCarlo>(
986    model: &T,
987    nondimensional_force: Scalar,
988    number_of_samples: usize,
989) -> (Scalar, Scalar, Scalar) {
990    let mut x_max = Scalar::NEG_INFINITY;
991    let mut z_scaled = 0.0;
992    let mut ext_scaled = 0.0;
993
994    for _ in 0..number_of_samples {
995        let links = model.random_nondimensional_link_vectors(0.0);
996
997        let extension_sum: Scalar = links.iter().map(|link| link[2]).sum();
998        let x = nondimensional_force * extension_sum;
999
1000        if x > x_max {
1001            let scale = if x_max.is_finite() {
1002                (x_max - x).exp()
1003            } else {
1004                0.0
1005            };
1006            z_scaled *= scale;
1007            ext_scaled *= scale;
1008            x_max = x;
1009        }
1010
1011        let w = (x - x_max).exp();
1012        z_scaled += w;
1013        ext_scaled += extension_sum * w;
1014    }
1015
1016    (x_max, z_scaled, ext_scaled)
1017}
1018
1019fn nondimensional_angular_distribution<T: MonteCarlo>(
1020    model: &T,
1021    nondimensional_force: Scalar,
1022    number_of_bins: usize,
1023    number_of_samples: usize,
1024    number_of_threads: usize,
1025    maximum_nondimensional_extension: Scalar,
1026) -> (Vector, Vector) {
1027    let base = number_of_samples / number_of_threads;
1028    let remainder = number_of_samples % number_of_threads;
1029    scope(|s| {
1030        let mut total_counts = vec![0; number_of_bins];
1031        (0..number_of_threads)
1032            .map(|t| {
1033                s.spawn(move || {
1034                    nondimensional_angular_distribution_inner(
1035                        model,
1036                        nondimensional_force,
1037                        number_of_bins,
1038                        base + usize::from(t < remainder),
1039                        maximum_nondimensional_extension,
1040                    )
1041                })
1042            })
1043            .collect::<Vec<_>>()
1044            .into_iter()
1045            .for_each(|handle| {
1046                total_counts
1047                    .iter_mut()
1048                    .zip(handle.join().unwrap())
1049                    .for_each(|(tot, c)| *tot += c)
1050            });
1051        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1052        let bin_centers = (0..number_of_bins)
1053            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1054            .collect();
1055        let total_samples = number_of_samples as Scalar;
1056        let bin_values = total_counts
1057            .into_iter()
1058            .map(|count| count as Scalar / total_samples / bin_width)
1059            .collect();
1060        (bin_centers, bin_values)
1061    })
1062}
1063
1064fn nondimensional_lateral_distribution<T: MonteCarlo>(
1065    model: &T,
1066    nondimensional_force: Scalar,
1067    number_of_bins: usize,
1068    number_of_samples: usize,
1069    number_of_threads: usize,
1070    maximum_nondimensional_extension: Scalar,
1071) -> (Vector, Vector) {
1072    let base = number_of_samples / number_of_threads;
1073    let remainder = number_of_samples % number_of_threads;
1074    scope(|s| {
1075        let mut total_counts = vec![0; number_of_bins];
1076        (0..number_of_threads)
1077            .map(|t| {
1078                s.spawn(move || {
1079                    nondimensional_lateral_distribution_inner(
1080                        model,
1081                        nondimensional_force,
1082                        number_of_bins,
1083                        base + usize::from(t < remainder),
1084                        maximum_nondimensional_extension,
1085                    )
1086                })
1087            })
1088            .collect::<Vec<_>>()
1089            .into_iter()
1090            .for_each(|handle| {
1091                total_counts
1092                    .iter_mut()
1093                    .zip(handle.join().unwrap())
1094                    .for_each(|(tot, c)| *tot += c)
1095            });
1096        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1097        let bin_centers = (0..number_of_bins)
1098            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1099            .collect();
1100        let total_samples = number_of_samples as Scalar;
1101        let bin_values = total_counts
1102            .into_iter()
1103            .map(|count| count as Scalar / total_samples / bin_width)
1104            .collect();
1105        (bin_centers, bin_values)
1106    })
1107}
1108
1109fn nondimensional_longitudinal_distribution<T: MonteCarlo>(
1110    model: &T,
1111    nondimensional_force: Scalar,
1112    number_of_bins: usize,
1113    number_of_samples: usize,
1114    number_of_threads: usize,
1115    maximum_nondimensional_extension: Scalar,
1116) -> (Vector, Vector) {
1117    let base = number_of_samples / number_of_threads;
1118    let remainder = number_of_samples % number_of_threads;
1119    scope(|s| {
1120        let mut total_counts = vec![0; number_of_bins];
1121        (0..number_of_threads)
1122            .map(|t| {
1123                s.spawn(move || {
1124                    nondimensional_longitudinal_distribution_inner(
1125                        model,
1126                        nondimensional_force,
1127                        number_of_bins,
1128                        base + usize::from(t < remainder),
1129                        maximum_nondimensional_extension,
1130                    )
1131                })
1132            })
1133            .collect::<Vec<_>>()
1134            .into_iter()
1135            .for_each(|handle| {
1136                total_counts
1137                    .iter_mut()
1138                    .zip(handle.join().unwrap())
1139                    .for_each(|(tot, c)| *tot += c)
1140            });
1141        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1142        let bin_centers = (0..number_of_bins)
1143            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1144            .collect();
1145        let total_samples = number_of_samples as Scalar;
1146        let bin_values = total_counts
1147            .into_iter()
1148            .map(|count| count as Scalar / total_samples / bin_width)
1149            .collect();
1150        (bin_centers, bin_values)
1151    })
1152}
1153
1154fn nondimensional_radial_distribution<T: MonteCarlo>(
1155    model: &T,
1156    nondimensional_force: Scalar,
1157    number_of_bins: usize,
1158    number_of_samples: usize,
1159    number_of_threads: usize,
1160    maximum_nondimensional_extension: Scalar,
1161) -> (Vector, Vector) {
1162    let base = number_of_samples / number_of_threads;
1163    let remainder = number_of_samples % number_of_threads;
1164    scope(|s| {
1165        let mut total_counts = vec![0; number_of_bins];
1166        (0..number_of_threads)
1167            .map(|t| {
1168                s.spawn(move || {
1169                    nondimensional_radial_distribution_inner(
1170                        model,
1171                        nondimensional_force,
1172                        number_of_bins,
1173                        base + usize::from(t < remainder),
1174                        maximum_nondimensional_extension,
1175                    )
1176                })
1177            })
1178            .collect::<Vec<_>>()
1179            .into_iter()
1180            .for_each(|handle| {
1181                total_counts
1182                    .iter_mut()
1183                    .zip(handle.join().unwrap())
1184                    .for_each(|(tot, c)| *tot += c)
1185            });
1186        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1187        let bin_centers = (0..number_of_bins)
1188            .map(|i| (i as Scalar + 0.5) * bin_width)
1189            .collect();
1190        let total_samples = number_of_samples as Scalar;
1191        let bin_values = total_counts
1192            .into_iter()
1193            .map(|count| count as Scalar / total_samples / bin_width)
1194            .collect();
1195        (bin_centers, bin_values)
1196    })
1197}
1198
1199fn nondimensional_transverse_distribution<T: MonteCarlo>(
1200    model: &T,
1201    nondimensional_force: Scalar,
1202    number_of_bins: usize,
1203    number_of_samples: usize,
1204    number_of_threads: usize,
1205    maximum_nondimensional_extension: Scalar,
1206) -> (Vector, Vector) {
1207    let base = number_of_samples / number_of_threads;
1208    let remainder = number_of_samples % number_of_threads;
1209    scope(|s| {
1210        let mut total_counts = vec![0; number_of_bins];
1211        (0..number_of_threads)
1212            .map(|t| {
1213                s.spawn(move || {
1214                    nondimensional_transverse_distribution_inner(
1215                        model,
1216                        nondimensional_force,
1217                        number_of_bins,
1218                        base + usize::from(t < remainder),
1219                        maximum_nondimensional_extension,
1220                    )
1221                })
1222            })
1223            .collect::<Vec<_>>()
1224            .into_iter()
1225            .for_each(|handle| {
1226                total_counts
1227                    .iter_mut()
1228                    .zip(handle.join().unwrap())
1229                    .for_each(|(tot, c)| *tot += c)
1230            });
1231        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1232        let bin_centers = (0..number_of_bins)
1233            .map(|i| (i as Scalar + 0.5) * bin_width)
1234            .collect();
1235        let total_samples = number_of_samples as Scalar;
1236        let bin_values = total_counts
1237            .into_iter()
1238            .map(|count| count as Scalar / total_samples / bin_width)
1239            .collect();
1240        (bin_centers, bin_values)
1241    })
1242}
1243
1244fn nondimensional_angular_distribution_inner<T: MonteCarlo>(
1245    model: &T,
1246    nondimensional_force: Scalar,
1247    num_bins: usize,
1248    number_of_samples: usize,
1249    maximum_nondimensional_extension: Scalar,
1250) -> Vec<usize> {
1251    let mut bin_counts = vec![0; num_bins];
1252    let end_index = model.number_of_links() as usize - 1;
1253    for _ in 0..number_of_samples {
1254        let configuration = model.random_configuration(nondimensional_force);
1255        let gamma = configuration[end_index].norm();
1256        let nondimensional_extension = if gamma == 0.0 {
1257            0.0
1258        } else {
1259            configuration[end_index][2] / gamma
1260        };
1261        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1262            panic!(
1263                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1264            )
1265        }
1266        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1267            / (2.0 * maximum_nondimensional_extension)
1268            * num_bins as Scalar) as usize;
1269        bin_counts[bin_index] += 1;
1270    }
1271    bin_counts
1272}
1273
1274fn nondimensional_lateral_distribution_inner<T: MonteCarlo>(
1275    model: &T,
1276    nondimensional_force: Scalar,
1277    num_bins: usize,
1278    number_of_samples: usize,
1279    maximum_nondimensional_extension: Scalar,
1280) -> Vec<usize> {
1281    let mut bin_counts = vec![0; num_bins];
1282    let num_links = model.number_of_links() as Scalar;
1283    let end_index = model.number_of_links() as usize - 1;
1284    for _ in 0..number_of_samples {
1285        let configuration = model.random_configuration(nondimensional_force);
1286        let nondimensional_extension = configuration[end_index][1] / num_links;
1287        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1288            panic!(
1289                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1290            )
1291        }
1292        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1293            / (2.0 * maximum_nondimensional_extension)
1294            * num_bins as Scalar) as usize;
1295        bin_counts[bin_index] += 1;
1296    }
1297    bin_counts
1298}
1299
1300fn nondimensional_longitudinal_distribution_inner<T: MonteCarlo>(
1301    model: &T,
1302    nondimensional_force: Scalar,
1303    num_bins: usize,
1304    number_of_samples: usize,
1305    maximum_nondimensional_extension: Scalar,
1306) -> Vec<usize> {
1307    let mut bin_counts = vec![0; num_bins];
1308    let num_links = model.number_of_links() as Scalar;
1309    let end_index = model.number_of_links() as usize - 1;
1310    for _ in 0..number_of_samples {
1311        let configuration = model.random_configuration(nondimensional_force);
1312        let nondimensional_extension = configuration[end_index][2] / num_links;
1313        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1314            panic!(
1315                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1316            )
1317        }
1318        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1319            / (2.0 * maximum_nondimensional_extension)
1320            * num_bins as Scalar) as usize;
1321        bin_counts[bin_index] += 1;
1322    }
1323    bin_counts
1324}
1325
1326fn nondimensional_radial_distribution_inner<T: MonteCarlo>(
1327    model: &T,
1328    nondimensional_force: Scalar,
1329    num_bins: usize,
1330    number_of_samples: usize,
1331    maximum_nondimensional_extension: Scalar,
1332) -> Vec<usize> {
1333    let mut bin_counts = vec![0; num_bins];
1334    let num_links = model.number_of_links() as Scalar;
1335    let end_index = model.number_of_links() as usize - 1;
1336    for _ in 0..number_of_samples {
1337        let configuration = model.random_configuration(nondimensional_force);
1338        let nondimensional_extension = configuration[end_index].norm() / num_links;
1339        if nondimensional_extension > maximum_nondimensional_extension {
1340            panic!(
1341                "Sample {nondimensional_extension} above maximum {maximum_nondimensional_extension}"
1342            )
1343        }
1344        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1345            * num_bins as Scalar) as usize;
1346        bin_counts[bin_index] += 1;
1347    }
1348    bin_counts
1349}
1350
1351fn nondimensional_transverse_distribution_inner<T: MonteCarlo>(
1352    model: &T,
1353    nondimensional_force: Scalar,
1354    num_bins: usize,
1355    number_of_samples: usize,
1356    maximum_nondimensional_extension: Scalar,
1357) -> Vec<usize> {
1358    let mut bin_counts = vec![0; num_bins];
1359    let num_links = model.number_of_links() as Scalar;
1360    let end_index = model.number_of_links() as usize - 1;
1361    for _ in 0..number_of_samples {
1362        let configuration = model.random_configuration(nondimensional_force);
1363        let nondimensional_extension =
1364            (configuration[end_index][0].powi(2) + configuration[end_index][1].powi(2)).sqrt()
1365                / num_links;
1366        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1367            panic!(
1368                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1369            )
1370        }
1371        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1372            * num_bins as Scalar) as usize;
1373        bin_counts[bin_index] += 1;
1374    }
1375    bin_counts
1376}