Skip to main content

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

1use crate::{
2    math::{
3        Current, Quantity, Scalar, SquareMatrix, Tensor, TensorArray, TensorRank1, Vector,
4        optimize::{
5            Direct, EqualityConstraint, LineSearch, NewtonRaphson, SecondOrderOptimization,
6            Tolerances,
7        },
8    },
9    mechanics::Vectors,
10    physics::molecular::single_chain::{Extensible, Inextensible, SingleChain, SingleChainError},
11    units::Temperature,
12};
13use std::{f64::consts::PI, thread::scope};
14
15pub type Configuration = Vectors<Current>;
16
17#[derive(Clone, Copy, Debug)]
18pub enum Ensemble {
19    Isometric(Scalar),
20    Isotensional(Scalar),
21}
22
23pub trait Thermodynamics
24where
25    Self: Isometric + Isotensional + Legendre + SingleChain,
26{
27    fn ensemble(&self) -> Ensemble;
28    fn temperature(&self) -> Quantity<Temperature> {
29        match self.ensemble() {
30            Ensemble::Isometric(temperature) => Quantity::new(temperature),
31            Ensemble::Isotensional(temperature) => Quantity::new(temperature),
32        }
33    }
34    fn nondimensional_helmholtz_free_energy(
35        &self,
36        nondimensional_extension: Scalar,
37    ) -> Result<Scalar, SingleChainError> {
38        match self.ensemble() {
39            Ensemble::Isometric(_) => {
40                Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)
41            }
42            Ensemble::Isotensional(_) => {
43                Legendre::nondimensional_helmholtz_free_energy(self, nondimensional_extension)
44            }
45        }
46    }
47    fn nondimensional_helmholtz_free_energy_per_link(
48        &self,
49        nondimensional_extension: Scalar,
50    ) -> Result<Scalar, SingleChainError> {
51        match self.ensemble() {
52            Ensemble::Isometric(_) => Isometric::nondimensional_helmholtz_free_energy_per_link(
53                self,
54                nondimensional_extension,
55            ),
56            Ensemble::Isotensional(_) => Legendre::nondimensional_helmholtz_free_energy_per_link(
57                self,
58                nondimensional_extension,
59            ),
60        }
61    }
62    fn nondimensional_force(
63        &self,
64        nondimensional_extension: Scalar,
65    ) -> Result<Scalar, SingleChainError> {
66        match self.ensemble() {
67            Ensemble::Isometric(_) => {
68                Isometric::nondimensional_force(self, nondimensional_extension)
69            }
70            Ensemble::Isotensional(_) => {
71                Legendre::nondimensional_force(self, nondimensional_extension)
72            }
73        }
74    }
75    fn nondimensional_stiffness(
76        &self,
77        nondimensional_extension: Scalar,
78    ) -> Result<Scalar, SingleChainError> {
79        match self.ensemble() {
80            Ensemble::Isometric(_) => {
81                Isometric::nondimensional_stiffness(self, nondimensional_extension)
82            }
83            Ensemble::Isotensional(_) => {
84                Legendre::nondimensional_stiffness(self, nondimensional_extension)
85            }
86        }
87    }
88    fn nondimensional_radial_distribution(
89        &self,
90        nondimensional_extension: Scalar,
91    ) -> Result<Scalar, SingleChainError> {
92        match self.ensemble() {
93            Ensemble::Isometric(_) => {
94                Isometric::nondimensional_radial_distribution(self, nondimensional_extension)
95            }
96            Ensemble::Isotensional(_) => {
97                Legendre::nondimensional_radial_distribution(self, nondimensional_extension)
98            }
99        }
100    }
101    fn nondimensional_spherical_distribution(
102        &self,
103        nondimensional_extension: Scalar,
104    ) -> Result<Scalar, SingleChainError> {
105        match self.ensemble() {
106            Ensemble::Isometric(_) => {
107                Isometric::nondimensional_spherical_distribution(self, nondimensional_extension)
108            }
109            Ensemble::Isotensional(_) => {
110                Legendre::nondimensional_spherical_distribution(self, nondimensional_extension)
111            }
112        }
113    }
114    fn nondimensional_gibbs_free_energy(
115        &self,
116        nondimensional_force: Scalar,
117    ) -> Result<Scalar, SingleChainError> {
118        match self.ensemble() {
119            Ensemble::Isometric(_) => {
120                Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)
121            }
122            Ensemble::Isotensional(_) => {
123                Isotensional::nondimensional_gibbs_free_energy(self, nondimensional_force)
124            }
125        }
126    }
127    fn nondimensional_gibbs_free_energy_per_link(
128        &self,
129        nondimensional_force: Scalar,
130    ) -> Result<Scalar, SingleChainError> {
131        match self.ensemble() {
132            Ensemble::Isometric(_) => {
133                Legendre::nondimensional_gibbs_free_energy_per_link(self, nondimensional_force)
134            }
135            Ensemble::Isotensional(_) => {
136                Isotensional::nondimensional_gibbs_free_energy_per_link(self, nondimensional_force)
137            }
138        }
139    }
140    fn nondimensional_extension(
141        &self,
142        nondimensional_force: Scalar,
143    ) -> Result<Scalar, SingleChainError> {
144        match self.ensemble() {
145            Ensemble::Isometric(_) => {
146                Legendre::nondimensional_extension(self, nondimensional_force)
147            }
148            Ensemble::Isotensional(_) => {
149                Isotensional::nondimensional_extension(self, nondimensional_force)
150            }
151        }
152    }
153    fn nondimensional_compliance(
154        &self,
155        nondimensional_force: Scalar,
156    ) -> Result<Scalar, SingleChainError> {
157        match self.ensemble() {
158            Ensemble::Isometric(_) => {
159                Legendre::nondimensional_compliance(self, nondimensional_force)
160            }
161            Ensemble::Isotensional(_) => {
162                Isotensional::nondimensional_compliance(self, nondimensional_force)
163            }
164        }
165    }
166}
167
168pub trait ThermodynamicsExtensible
169where
170    Self: IsotensionalExtensible + Thermodynamics,
171{
172    fn nondimensional_link_energy_average(
173        &self,
174        nondimensional_force: Scalar,
175    ) -> Result<Scalar, SingleChainError> {
176        match self.ensemble() {
177            Ensemble::Isometric(_) => {
178                unimplemented!()
179            }
180            Ensemble::Isotensional(_) => {
181                IsotensionalExtensible::nondimensional_link_energy_average(
182                    self,
183                    nondimensional_force,
184                )
185            }
186        }
187    }
188    fn nondimensional_link_energy_variance(
189        &self,
190        nondimensional_force: Scalar,
191    ) -> Result<Scalar, SingleChainError> {
192        match self.ensemble() {
193            Ensemble::Isometric(_) => {
194                unimplemented!()
195            }
196            Ensemble::Isotensional(_) => {
197                IsotensionalExtensible::nondimensional_link_energy_variance(
198                    self,
199                    nondimensional_force,
200                )
201            }
202        }
203    }
204    fn nondimensional_link_energy_probability(
205        &self,
206        nondimensional_energy: Scalar,
207        nondimensional_force: Scalar,
208    ) -> Result<Scalar, SingleChainError> {
209        match self.ensemble() {
210            Ensemble::Isometric(_) => {
211                unimplemented!()
212            }
213            Ensemble::Isotensional(_) => {
214                IsotensionalExtensible::nondimensional_link_energy_probability(
215                    self,
216                    nondimensional_energy,
217                    nondimensional_force,
218                )
219            }
220        }
221    }
222    fn nondimensional_link_length_average(
223        &self,
224        nondimensional_force: Scalar,
225    ) -> Result<Scalar, SingleChainError> {
226        match self.ensemble() {
227            Ensemble::Isometric(_) => {
228                unimplemented!()
229            }
230            Ensemble::Isotensional(_) => {
231                IsotensionalExtensible::nondimensional_link_length_average(
232                    self,
233                    nondimensional_force,
234                )
235            }
236        }
237    }
238    fn nondimensional_link_length_variance(
239        &self,
240        nondimensional_force: Scalar,
241    ) -> Result<Scalar, SingleChainError> {
242        match self.ensemble() {
243            Ensemble::Isometric(_) => {
244                unimplemented!()
245            }
246            Ensemble::Isotensional(_) => {
247                IsotensionalExtensible::nondimensional_link_length_variance(
248                    self,
249                    nondimensional_force,
250                )
251            }
252        }
253    }
254    fn nondimensional_link_length_probability(
255        &self,
256        nondimensional_length: Scalar,
257        nondimensional_force: Scalar,
258    ) -> Result<Scalar, SingleChainError> {
259        match self.ensemble() {
260            Ensemble::Isometric(_) => {
261                unimplemented!()
262            }
263            Ensemble::Isotensional(_) => {
264                IsotensionalExtensible::nondimensional_link_length_probability(
265                    self,
266                    nondimensional_length,
267                    nondimensional_force,
268                )
269            }
270        }
271    }
272}
273
274pub trait Isometric
275where
276    Self: SingleChain,
277{
278    /// ```math
279    /// \beta\psi(\gamma) = -\ln Q(\gamma)
280    /// ```
281    fn nondimensional_helmholtz_free_energy(
282        &self,
283        nondimensional_extension: Scalar,
284    ) -> Result<Scalar, SingleChainError> {
285        Ok(
286            self.nondimensional_helmholtz_free_energy_per_link(nondimensional_extension)?
287                * (self.number_of_links() as Scalar),
288        )
289    }
290    /// ```math
291    /// \vartheta(\gamma) = \beta\psi(\gamma) / N_b
292    /// ```
293    fn nondimensional_helmholtz_free_energy_per_link(
294        &self,
295        nondimensional_extension: Scalar,
296    ) -> Result<Scalar, SingleChainError> {
297        Ok(
298            self.nondimensional_helmholtz_free_energy(nondimensional_extension)?
299                / (self.number_of_links() as Scalar),
300        )
301    }
302    /// ```math
303    /// \eta(\gamma) = \frac{\partial\vartheta}{\partial\gamma}
304    /// ```
305    fn nondimensional_force(
306        &self,
307        nondimensional_extension: Scalar,
308    ) -> Result<Scalar, SingleChainError>;
309    /// ```math
310    /// k(\gamma) = \frac{\partial\eta}{\partial\gamma}
311    /// ```
312    fn nondimensional_stiffness(
313        &self,
314        nondimensional_extension: Scalar,
315    ) -> Result<Scalar, SingleChainError>;
316    /// ```math
317    /// \mathcal{g}(\gamma) = 4\pi\gamma^2\mathcal{P}(\gamma)
318    /// ```
319    fn nondimensional_radial_distribution(
320        &self,
321        nondimensional_extension: Scalar,
322    ) -> Result<Scalar, SingleChainError> {
323        Ok(
324            self.nondimensional_spherical_distribution(nondimensional_extension)?
325                * (4.0 * PI * nondimensional_extension.powi(2)),
326        )
327    }
328    /// ```math
329    /// \mathcal{P}(\gamma) \propto e^{-\beta\psi(\gamma)}
330    /// ```
331    fn nondimensional_spherical_distribution(
332        &self,
333        nondimensional_extension: Scalar,
334    ) -> Result<Scalar, SingleChainError>;
335}
336
337pub trait Isotensional
338where
339    Self: SingleChain,
340{
341    /// ```math
342    /// \beta\varphi(\eta) = -\ln Z(\eta)
343    /// ```
344    fn nondimensional_gibbs_free_energy(
345        &self,
346        nondimensional_force: Scalar,
347    ) -> Result<Scalar, SingleChainError> {
348        Ok(
349            self.nondimensional_gibbs_free_energy_per_link(nondimensional_force)?
350                * (self.number_of_links() as Scalar),
351        )
352    }
353    /// ```math
354    /// \varrho(\eta) = \beta\varphi(\eta) / N_b
355    /// ```
356    fn nondimensional_gibbs_free_energy_per_link(
357        &self,
358        nondimensional_force: Scalar,
359    ) -> Result<Scalar, SingleChainError> {
360        Ok(self.nondimensional_gibbs_free_energy(nondimensional_force)?
361            / (self.number_of_links() as Scalar))
362    }
363    /// ```math
364    /// \gamma(\eta) = -\frac{\partial\varrho}{\partial\eta}
365    /// ```
366    fn nondimensional_extension(
367        &self,
368        nondimensional_force: Scalar,
369    ) -> Result<Scalar, SingleChainError>;
370    /// ```math
371    /// c(\eta) = \frac{\partial\gamma}{\partial\eta}
372    /// ```
373    fn nondimensional_compliance(
374        &self,
375        nondimensional_force: Scalar,
376    ) -> Result<Scalar, SingleChainError>;
377}
378
379pub trait IsotensionalExtensible
380where
381    Self: Extensible + Isotensional,
382{
383    /// ```math
384    /// \langle\upsilon\rangle = \varepsilon\,\frac{\partial\varrho}{\partial\varepsilon}
385    /// ```
386    fn nondimensional_link_energy_average(
387        &self,
388        nondimensional_force: Scalar,
389    ) -> Result<Scalar, SingleChainError>;
390    /// ```math
391    /// \sigma_\upsilon^2 = -\varepsilon^2\frac{\partial^2\varrho}{\partial\varepsilon^2}
392    /// ```
393    fn nondimensional_link_energy_variance(
394        &self,
395        nondimensional_force: Scalar,
396    ) -> Result<Scalar, SingleChainError>;
397    /// ```math
398    /// p(\upsilon\,|\,\eta) = \int p(\lambda\,|\,\eta)\,\delta[\upsilon - \upsilon(\lambda)]\,d\lambda
399    /// ```
400    fn nondimensional_link_energy_probability(
401        &self,
402        nondimensional_energy: Scalar,
403        nondimensional_force: Scalar,
404    ) -> Result<Scalar, SingleChainError>;
405    /// ```math
406    /// \langle\lambda\rangle = \int_0^\infty p(\lambda\,|\,\eta)\,\lambda\,d\lambda
407    /// ```
408    fn nondimensional_link_length_average(
409        &self,
410        nondimensional_force: Scalar,
411    ) -> Result<Scalar, SingleChainError>;
412    /// ```math
413    /// \sigma_\lambda^2 = \langle\lambda^2\rangle - \langle\lambda\rangle^2
414    /// ```
415    fn nondimensional_link_length_variance(
416        &self,
417        nondimensional_force: Scalar,
418    ) -> Result<Scalar, SingleChainError>;
419    /// ```math
420    /// p(\lambda\,|\,\eta) = \frac{z_0(\eta,\lambda)}{z(\eta)}\,e^{-\upsilon(\lambda)}
421    /// ```
422    fn nondimensional_link_length_probability(
423        &self,
424        nondimensional_length: Scalar,
425        nondimensional_force: Scalar,
426    ) -> Result<Scalar, SingleChainError>;
427}
428
429pub trait Legendre
430where
431    Self: Isometric + Isotensional + SingleChain,
432{
433    /// ```math
434    /// \beta\psi(\gamma) = \beta\varphi(\eta) + N_b\eta(\gamma)\gamma
435    /// ```
436    fn nondimensional_helmholtz_free_energy(
437        &self,
438        nondimensional_extension: Scalar,
439    ) -> Result<Scalar, SingleChainError> {
440        let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
441        Ok(
442            Isotensional::nondimensional_gibbs_free_energy(self, nondimensional_force)?
443                + self.number_of_links() as Scalar
444                    * nondimensional_force
445                    * nondimensional_extension,
446        )
447    }
448    /// ```math
449    /// \vartheta(\gamma) = \varrho(\eta) + \eta(\gamma)\gamma
450    /// ```
451    fn nondimensional_helmholtz_free_energy_per_link(
452        &self,
453        nondimensional_extension: Scalar,
454    ) -> Result<Scalar, SingleChainError> {
455        Ok(
456            Legendre::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
457                / (self.number_of_links() as Scalar),
458        )
459    }
460    /// ```math
461    /// \eta(\gamma) = \gamma^{-1}(\gamma)
462    /// ```
463    fn nondimensional_force(
464        &self,
465        nondimensional_extension: Scalar,
466    ) -> Result<Scalar, SingleChainError> {
467        (NewtonRaphson {
468            abs_tol: Tolerances {
469                constraint: 1e-10,
470                residual: 1e-10,
471            },
472            line_search: LineSearch::Error {
473                cut_back: 5e-1,
474                max_steps: 10,
475            },
476            linear_solver: Direct,
477            ..Default::default()
478        }
479        .minimize(
480            |&nondimensional_force| {
481                Ok(Isotensional::nondimensional_gibbs_free_energy_per_link(
482                    self,
483                    nondimensional_force,
484                )? - nondimensional_force * nondimensional_extension)
485            },
486            |&nondimensional_force| {
487                Ok(
488                    Isotensional::nondimensional_extension(self, nondimensional_force)?
489                        - nondimensional_extension,
490                )
491            },
492            |&nondimensional_force| {
493                Ok(Isotensional::nondimensional_compliance(
494                    self,
495                    nondimensional_force,
496                )?)
497            },
498            nondimensional_extension,
499            EqualityConstraint::None,
500            None,
501        ))
502        .map_err(|error| SingleChainError::upstream(error, self))
503    }
504    /// ```math
505    /// k(\gamma) = \left(\frac{\partial\gamma}{\partial\eta}\right)^{-1}
506    /// ```
507    fn nondimensional_stiffness(
508        &self,
509        nondimensional_extension: Scalar,
510    ) -> Result<Scalar, SingleChainError> {
511        let nondimensional_force = Legendre::nondimensional_force(self, nondimensional_extension)?;
512        Ok(1.0 / Isotensional::nondimensional_compliance(self, nondimensional_force)?)
513    }
514    /// ```math
515    /// \mathcal{g}(\gamma) = 4\pi\gamma^2\mathcal{P}(\gamma)
516    /// ```
517    fn nondimensional_radial_distribution(
518        &self,
519        nondimensional_extension: Scalar,
520    ) -> Result<Scalar, SingleChainError> {
521        Ok(
522            Legendre::nondimensional_spherical_distribution(self, nondimensional_extension)?
523                * (4.0 * PI * nondimensional_extension.powi(2)),
524        )
525    }
526    /// ```math
527    /// \mathcal{P}(\gamma) \propto e^{-\beta\psi(\gamma)}
528    /// ```
529    fn nondimensional_spherical_distribution(
530        &self,
531        nondimensional_extension: Scalar,
532    ) -> Result<Scalar, SingleChainError> {
533        Ok(
534            Legendre::nondimensional_radial_distribution(self, nondimensional_extension)?
535                / (4.0 * PI * nondimensional_extension.powi(2)),
536        )
537    }
538    /// ```math
539    /// \beta\varphi(\eta) = \beta\psi(\gamma) - N_b\eta\gamma(\eta)
540    /// ```
541    fn nondimensional_gibbs_free_energy(
542        &self,
543        nondimensional_force: Scalar,
544    ) -> Result<Scalar, SingleChainError> {
545        let nondimensional_extension =
546            Legendre::nondimensional_extension(self, nondimensional_force)?;
547        Ok(
548            Isometric::nondimensional_helmholtz_free_energy(self, nondimensional_extension)?
549                - self.number_of_links() as Scalar
550                    * nondimensional_force
551                    * nondimensional_extension,
552        )
553    }
554    /// ```math
555    /// \varrho(\eta) = \vartheta(\gamma) - \eta\gamma(\eta)
556    /// ```
557    fn nondimensional_gibbs_free_energy_per_link(
558        &self,
559        nondimensional_force: Scalar,
560    ) -> Result<Scalar, SingleChainError> {
561        Ok(
562            Legendre::nondimensional_gibbs_free_energy(self, nondimensional_force)?
563                / (self.number_of_links() as Scalar),
564        )
565    }
566    /// ```math
567    /// \gamma(\eta) = \eta^{-1}(\eta)
568    /// ```
569    fn nondimensional_extension(
570        &self,
571        nondimensional_force: Scalar,
572    ) -> Result<Scalar, SingleChainError> {
573        (NewtonRaphson {
574            abs_tol: Tolerances {
575                constraint: 1e-10,
576                residual: 1e-10,
577            },
578            line_search: LineSearch::Error {
579                cut_back: 5e-1,
580                max_steps: 10,
581            },
582            linear_solver: Direct,
583            ..Default::default()
584        }
585        .minimize(
586            |&nondimensional_extension| {
587                Ok(Isometric::nondimensional_helmholtz_free_energy_per_link(
588                    self,
589                    nondimensional_extension,
590                )? - nondimensional_force * nondimensional_extension)
591            },
592            |&nondimensional_extension| {
593                Ok(
594                    Isometric::nondimensional_force(self, nondimensional_extension)?
595                        - nondimensional_force,
596                )
597            },
598            |&nondimensional_extension| {
599                Ok(Isometric::nondimensional_stiffness(
600                    self,
601                    nondimensional_extension,
602                )?)
603            },
604            nondimensional_force,
605            EqualityConstraint::None,
606            None,
607        ))
608        .map_err(|error| SingleChainError::upstream(error, self))
609    }
610    /// ```math
611    /// c(\eta) = \left(\frac{\partial\eta}{\partial\gamma}\right)^{-1}
612    /// ```
613    fn nondimensional_compliance(
614        &self,
615        nondimensional_force: Scalar,
616    ) -> Result<Scalar, SingleChainError> {
617        let nondimensional_extension =
618            Legendre::nondimensional_extension(self, nondimensional_force)?;
619        Ok(1.0 / Isometric::nondimensional_stiffness(self, nondimensional_extension)?)
620    }
621}
622
623pub trait MonteCarlo
624where
625    Self: SingleChain + Sync,
626{
627    fn cosine_moments(
628        &self,
629        nondimensional_force: Scalar,
630        number_of_samples: usize,
631        number_of_threads: usize,
632    ) -> (Vector, SquareMatrix, Vector, SquareMatrix) {
633        cosine_moments_reweighted(
634            self,
635            nondimensional_force,
636            number_of_samples,
637            number_of_threads,
638        )
639    }
640    fn nondimensional_longitudinal_extension(
641        &self,
642        nondimensional_force: Scalar,
643        number_of_samples: usize,
644        number_of_threads: usize,
645    ) -> Scalar {
646        //
647        // Does not work well above a certain force (see EFRC for bias method).
648        //
649        // Should set up to use random_nondimensional_link_vectors when implemented to work for a given force,
650        // and use the re-weighting (possibly with the above bias method) when unimplemented under force.
651        //
652        nondimensional_longitudinal_extension_reweighted(
653            self,
654            nondimensional_force,
655            number_of_samples,
656            number_of_threads,
657        )
658    }
659    fn random_nondimensional_link_vectors(&self, nondimensional_force: Scalar) -> Configuration;
660    fn random_configuration(&self, nondimensional_force: Scalar) -> Configuration {
661        let mut position = TensorRank1::<3, Current>::zero();
662        self.random_nondimensional_link_vectors(nondimensional_force)
663            .into_iter()
664            .map(|displacement| {
665                position += displacement;
666                position.clone()
667            })
668            .collect()
669    }
670}
671
672pub trait MonteCarloExtensible
673where
674    Self: Extensible + MonteCarlo,
675{
676    fn nondimensional_lateral_distribution(
677        &self,
678        nondimensional_force: Scalar,
679        num_bins: usize,
680        number_of_samples: usize,
681        number_of_threads: usize,
682        maximum_nondimensional_extension: Scalar,
683    ) -> (Vector, Vector) {
684        nondimensional_lateral_distribution(
685            self,
686            nondimensional_force,
687            num_bins,
688            number_of_samples,
689            number_of_threads,
690            maximum_nondimensional_extension,
691        )
692    }
693    fn nondimensional_longitudinal_distribution(
694        &self,
695        nondimensional_force: Scalar,
696        num_bins: usize,
697        number_of_samples: usize,
698        number_of_threads: usize,
699        maximum_nondimensional_extension: Scalar,
700    ) -> (Vector, Vector) {
701        nondimensional_longitudinal_distribution(
702            self,
703            nondimensional_force,
704            num_bins,
705            number_of_samples,
706            number_of_threads,
707            maximum_nondimensional_extension,
708        )
709    }
710    fn nondimensional_radial_distribution(
711        &self,
712        nondimensional_force: Scalar,
713        num_bins: usize,
714        number_of_samples: usize,
715        number_of_threads: usize,
716        maximum_nondimensional_extension: Scalar,
717    ) -> (Vector, Vector) {
718        nondimensional_radial_distribution(
719            self,
720            nondimensional_force,
721            num_bins,
722            number_of_samples,
723            number_of_threads,
724            maximum_nondimensional_extension,
725        )
726    }
727    fn nondimensional_transverse_distribution(
728        &self,
729        nondimensional_force: Scalar,
730        num_bins: usize,
731        number_of_samples: usize,
732        number_of_threads: usize,
733        maximum_nondimensional_extension: Scalar,
734    ) -> (Vector, Vector) {
735        nondimensional_transverse_distribution(
736            self,
737            nondimensional_force,
738            num_bins,
739            number_of_samples,
740            number_of_threads,
741            maximum_nondimensional_extension,
742        )
743    }
744}
745
746impl<T> MonteCarloExtensible for T where T: Extensible + MonteCarlo {}
747
748pub trait MonteCarloInextensible
749where
750    Self: Inextensible + MonteCarlo,
751{
752    fn nondimensional_angular_distribution(
753        &self,
754        nondimensional_force: Scalar,
755        num_bins: usize,
756        number_of_samples: usize,
757        number_of_threads: usize,
758    ) -> (Vector, Vector) {
759        nondimensional_angular_distribution(
760            self,
761            nondimensional_force,
762            num_bins,
763            number_of_samples,
764            number_of_threads,
765            self.maximum_nondimensional_extension(),
766        )
767    }
768    fn nondimensional_lateral_distribution(
769        &self,
770        nondimensional_force: Scalar,
771        num_bins: usize,
772        number_of_samples: usize,
773        number_of_threads: usize,
774    ) -> (Vector, Vector) {
775        nondimensional_lateral_distribution(
776            self,
777            nondimensional_force,
778            num_bins,
779            number_of_samples,
780            number_of_threads,
781            self.maximum_nondimensional_extension(),
782        )
783    }
784    fn nondimensional_longitudinal_distribution(
785        &self,
786        nondimensional_force: Scalar,
787        num_bins: usize,
788        number_of_samples: usize,
789        number_of_threads: usize,
790    ) -> (Vector, Vector) {
791        nondimensional_longitudinal_distribution(
792            self,
793            nondimensional_force,
794            num_bins,
795            number_of_samples,
796            number_of_threads,
797            self.maximum_nondimensional_extension(),
798        )
799    }
800    fn nondimensional_radial_distribution(
801        &self,
802        nondimensional_force: Scalar,
803        num_bins: usize,
804        number_of_samples: usize,
805        number_of_threads: usize,
806    ) -> (Vector, Vector) {
807        nondimensional_radial_distribution(
808            self,
809            nondimensional_force,
810            num_bins,
811            number_of_samples,
812            number_of_threads,
813            self.maximum_nondimensional_extension(),
814        )
815    }
816    fn nondimensional_transverse_distribution(
817        &self,
818        nondimensional_force: Scalar,
819        num_bins: usize,
820        number_of_samples: usize,
821        number_of_threads: usize,
822    ) -> (Vector, Vector) {
823        nondimensional_transverse_distribution(
824            self,
825            nondimensional_force,
826            num_bins,
827            number_of_samples,
828            number_of_threads,
829            self.maximum_nondimensional_extension(),
830        )
831    }
832}
833
834impl<T> MonteCarloInextensible for T where T: Inextensible + MonteCarlo {}
835
836pub(super) fn cosine_moments_reweighted<T: MonteCarlo>(
837    model: &T,
838    nondimensional_force: Scalar,
839    number_of_samples: usize,
840    number_of_threads: usize,
841) -> (Vector, SquareMatrix, Vector, SquareMatrix) {
842    let base = number_of_samples / number_of_threads;
843    let remainder = number_of_samples % number_of_threads;
844    scope(|s| {
845        (0..number_of_threads)
846            .map(|t| {
847                s.spawn(move || {
848                    cosine_moments_reweighted_inner(
849                        model,
850                        nondimensional_force,
851                        base + usize::from(t < remainder),
852                    )
853                })
854            })
855            .collect::<Vec<_>>()
856            .into_iter()
857            .map(|handle| handle.join().unwrap())
858            .reduce(|mut acc, (x_max, z_scaled, cos1_scaled, cos1cos1_scaled, cos2_scaled, cos2cos1_scaled)| {
859                let x_max_new = acc.0.max(x_max);
860                let scale_acc = (acc.0 - x_max_new).exp();
861                let scale_new = (x_max - x_max_new).exp();
862                acc.1 = acc.1 * scale_acc + z_scaled * scale_new;
863                acc.2 = acc.2 * scale_acc + cos1_scaled * scale_new;
864                acc.3 = acc.3 * scale_acc + cos1cos1_scaled * scale_new;
865                acc.4 = acc.4 * scale_acc + cos2_scaled * scale_new;
866                acc.5 = acc.5 * scale_acc + cos2cos1_scaled * scale_new;
867                acc.0 = x_max_new;
868                acc
869            })
870            .map(|(_x_max, z_scaled, cos1_scaled, cos1cos1_scaled, cos2_scaled, cos2cos1_scaled)| {
871                let z_inv = 1.0 / z_scaled;
872                (
873                    cos1_scaled * z_inv,
874                    cos1cos1_scaled * z_inv,
875                    cos2_scaled * z_inv,
876                    cos2cos1_scaled * z_inv,
877                )
878            })
879            .unwrap()
880    })
881}
882
883fn cosine_moments_reweighted_inner<T: MonteCarlo>(
884    model: &T,
885    nondimensional_force: Scalar,
886    number_of_samples: usize,
887) -> (Scalar, Scalar, Vector, SquareMatrix, Vector, SquareMatrix) {
888    let num_links = model.number_of_links() as usize;
889
890    let mut x_max = Scalar::NEG_INFINITY;
891    let mut z_scaled = 0.0;
892    let mut cos1_scaled = Vector::zero(num_links);
893    let mut cos1cos1_scaled = SquareMatrix::zero(num_links);
894    let mut cos2_scaled = Vector::zero(num_links);
895    let mut cos2cos1_scaled = SquareMatrix::zero(num_links);
896
897    for _ in 0..number_of_samples {
898        let links = model.random_nondimensional_link_vectors(0.0);
899
900        let cosines: Vec<Scalar> = links
901            .iter()
902            .map(|link| link[2].value() / link.norm().value())
903            .collect();
904
905        let sum_cos: Scalar = cosines.iter().sum();
906        let x = nondimensional_force * sum_cos;
907
908        if x > x_max {
909            let scale = if x_max.is_finite() {
910                (x_max - x).exp()
911            } else {
912                0.0
913            };
914            z_scaled *= scale;
915            cos1_scaled *= scale;
916            cos1cos1_scaled *= scale;
917            cos2_scaled *= scale;
918            cos2cos1_scaled *= scale;
919            x_max = x;
920        }
921
922        let w = (x - x_max).exp();
923
924        z_scaled += w;
925
926        for i in 0..num_links {
927            let ci = cosines[i];
928            let ci2 = ci * ci;
929
930            cos1_scaled[i] += ci * w;
931            cos2_scaled[i] += ci2 * w;
932
933            for j in 0..num_links {
934                let cj = cosines[j];
935                cos1cos1_scaled[i][j] += ci * cj * w;
936                cos2cos1_scaled[i][j] += ci2 * cj * w;
937            }
938        }
939    }
940    (
941        x_max,
942        z_scaled,
943        cos1_scaled,
944        cos1cos1_scaled,
945        cos2_scaled,
946        cos2cos1_scaled,
947    )
948}
949
950fn nondimensional_longitudinal_extension_reweighted<T: MonteCarlo>(
951    model: &T,
952    nondimensional_force: Scalar,
953    number_of_samples: usize,
954    number_of_threads: usize,
955) -> Scalar {
956    let base = number_of_samples / number_of_threads;
957    let remainder = number_of_samples % number_of_threads;
958
959    scope(|s| {
960        (0..number_of_threads)
961            .map(|t| {
962                s.spawn(move || {
963                    nondimensional_longitudinal_extension_reweighted_inner(
964                        model,
965                        nondimensional_force,
966                        base + usize::from(t < remainder),
967                    )
968                })
969            })
970            .collect::<Vec<_>>()
971            .into_iter()
972            .map(|handle| handle.join().unwrap())
973            .reduce(|mut acc, (x_max, z_scaled, ext_scaled)| {
974                let x_max_new = acc.0.max(x_max);
975                let scale_acc = (acc.0 - x_max_new).exp();
976                let scale_new = (x_max - x_max_new).exp();
977
978                acc.1 = acc.1 * scale_acc + z_scaled * scale_new;
979                acc.2 = acc.2 * scale_acc + ext_scaled * scale_new;
980                acc.0 = x_max_new;
981                acc
982            })
983            .map(|(_x_max, z_scaled, ext_scaled)| {
984                ext_scaled / z_scaled / model.number_of_links() as Scalar
985            })
986            .unwrap()
987    })
988}
989
990fn nondimensional_longitudinal_extension_reweighted_inner<T: MonteCarlo>(
991    model: &T,
992    nondimensional_force: Scalar,
993    number_of_samples: usize,
994) -> (Scalar, Scalar, Scalar) {
995    let mut x_max = Scalar::NEG_INFINITY;
996    let mut z_scaled = 0.0;
997    let mut ext_scaled = 0.0;
998
999    for _ in 0..number_of_samples {
1000        let links = model.random_nondimensional_link_vectors(0.0);
1001
1002        let extension_sum: Scalar = links.iter().map(|link| link[2].value()).sum();
1003        let x = nondimensional_force * extension_sum;
1004
1005        if x > x_max {
1006            let scale = if x_max.is_finite() {
1007                (x_max - x).exp()
1008            } else {
1009                0.0
1010            };
1011            z_scaled *= scale;
1012            ext_scaled *= scale;
1013            x_max = x;
1014        }
1015
1016        let w = (x - x_max).exp();
1017        z_scaled += w;
1018        ext_scaled += extension_sum * w;
1019    }
1020
1021    (x_max, z_scaled, ext_scaled)
1022}
1023
1024fn nondimensional_angular_distribution<T: MonteCarlo>(
1025    model: &T,
1026    nondimensional_force: Scalar,
1027    number_of_bins: usize,
1028    number_of_samples: usize,
1029    number_of_threads: usize,
1030    maximum_nondimensional_extension: Scalar,
1031) -> (Vector, Vector) {
1032    let base = number_of_samples / number_of_threads;
1033    let remainder = number_of_samples % number_of_threads;
1034    scope(|s| {
1035        let mut total_counts = vec![0; number_of_bins];
1036        (0..number_of_threads)
1037            .map(|t| {
1038                s.spawn(move || {
1039                    nondimensional_angular_distribution_inner(
1040                        model,
1041                        nondimensional_force,
1042                        number_of_bins,
1043                        base + usize::from(t < remainder),
1044                        maximum_nondimensional_extension,
1045                    )
1046                })
1047            })
1048            .collect::<Vec<_>>()
1049            .into_iter()
1050            .for_each(|handle| {
1051                total_counts
1052                    .iter_mut()
1053                    .zip(handle.join().unwrap())
1054                    .for_each(|(tot, c)| *tot += c)
1055            });
1056        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1057        let bin_centers = (0..number_of_bins)
1058            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1059            .collect();
1060        let total_samples = number_of_samples as Scalar;
1061        let bin_values = total_counts
1062            .into_iter()
1063            .map(|count| count as Scalar / total_samples / bin_width)
1064            .collect();
1065        (bin_centers, bin_values)
1066    })
1067}
1068
1069fn nondimensional_lateral_distribution<T: MonteCarlo>(
1070    model: &T,
1071    nondimensional_force: Scalar,
1072    number_of_bins: usize,
1073    number_of_samples: usize,
1074    number_of_threads: usize,
1075    maximum_nondimensional_extension: Scalar,
1076) -> (Vector, Vector) {
1077    let base = number_of_samples / number_of_threads;
1078    let remainder = number_of_samples % number_of_threads;
1079    scope(|s| {
1080        let mut total_counts = vec![0; number_of_bins];
1081        (0..number_of_threads)
1082            .map(|t| {
1083                s.spawn(move || {
1084                    nondimensional_lateral_distribution_inner(
1085                        model,
1086                        nondimensional_force,
1087                        number_of_bins,
1088                        base + usize::from(t < remainder),
1089                        maximum_nondimensional_extension,
1090                    )
1091                })
1092            })
1093            .collect::<Vec<_>>()
1094            .into_iter()
1095            .for_each(|handle| {
1096                total_counts
1097                    .iter_mut()
1098                    .zip(handle.join().unwrap())
1099                    .for_each(|(tot, c)| *tot += c)
1100            });
1101        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1102        let bin_centers = (0..number_of_bins)
1103            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1104            .collect();
1105        let total_samples = number_of_samples as Scalar;
1106        let bin_values = total_counts
1107            .into_iter()
1108            .map(|count| count as Scalar / total_samples / bin_width)
1109            .collect();
1110        (bin_centers, bin_values)
1111    })
1112}
1113
1114fn nondimensional_longitudinal_distribution<T: MonteCarlo>(
1115    model: &T,
1116    nondimensional_force: Scalar,
1117    number_of_bins: usize,
1118    number_of_samples: usize,
1119    number_of_threads: usize,
1120    maximum_nondimensional_extension: Scalar,
1121) -> (Vector, Vector) {
1122    let base = number_of_samples / number_of_threads;
1123    let remainder = number_of_samples % number_of_threads;
1124    scope(|s| {
1125        let mut total_counts = vec![0; number_of_bins];
1126        (0..number_of_threads)
1127            .map(|t| {
1128                s.spawn(move || {
1129                    nondimensional_longitudinal_distribution_inner(
1130                        model,
1131                        nondimensional_force,
1132                        number_of_bins,
1133                        base + usize::from(t < remainder),
1134                        maximum_nondimensional_extension,
1135                    )
1136                })
1137            })
1138            .collect::<Vec<_>>()
1139            .into_iter()
1140            .for_each(|handle| {
1141                total_counts
1142                    .iter_mut()
1143                    .zip(handle.join().unwrap())
1144                    .for_each(|(tot, c)| *tot += c)
1145            });
1146        let bin_width = 2.0 * maximum_nondimensional_extension / (number_of_bins as Scalar);
1147        let bin_centers = (0..number_of_bins)
1148            .map(|i| -maximum_nondimensional_extension + (i as Scalar + 0.5) * bin_width)
1149            .collect();
1150        let total_samples = number_of_samples as Scalar;
1151        let bin_values = total_counts
1152            .into_iter()
1153            .map(|count| count as Scalar / total_samples / bin_width)
1154            .collect();
1155        (bin_centers, bin_values)
1156    })
1157}
1158
1159fn nondimensional_radial_distribution<T: MonteCarlo>(
1160    model: &T,
1161    nondimensional_force: Scalar,
1162    number_of_bins: usize,
1163    number_of_samples: usize,
1164    number_of_threads: usize,
1165    maximum_nondimensional_extension: Scalar,
1166) -> (Vector, Vector) {
1167    let base = number_of_samples / number_of_threads;
1168    let remainder = number_of_samples % number_of_threads;
1169    scope(|s| {
1170        let mut total_counts = vec![0; number_of_bins];
1171        (0..number_of_threads)
1172            .map(|t| {
1173                s.spawn(move || {
1174                    nondimensional_radial_distribution_inner(
1175                        model,
1176                        nondimensional_force,
1177                        number_of_bins,
1178                        base + usize::from(t < remainder),
1179                        maximum_nondimensional_extension,
1180                    )
1181                })
1182            })
1183            .collect::<Vec<_>>()
1184            .into_iter()
1185            .for_each(|handle| {
1186                total_counts
1187                    .iter_mut()
1188                    .zip(handle.join().unwrap())
1189                    .for_each(|(tot, c)| *tot += c)
1190            });
1191        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1192        let bin_centers = (0..number_of_bins)
1193            .map(|i| (i as Scalar + 0.5) * bin_width)
1194            .collect();
1195        let total_samples = number_of_samples as Scalar;
1196        let bin_values = total_counts
1197            .into_iter()
1198            .map(|count| count as Scalar / total_samples / bin_width)
1199            .collect();
1200        (bin_centers, bin_values)
1201    })
1202}
1203
1204fn nondimensional_transverse_distribution<T: MonteCarlo>(
1205    model: &T,
1206    nondimensional_force: Scalar,
1207    number_of_bins: usize,
1208    number_of_samples: usize,
1209    number_of_threads: usize,
1210    maximum_nondimensional_extension: Scalar,
1211) -> (Vector, Vector) {
1212    let base = number_of_samples / number_of_threads;
1213    let remainder = number_of_samples % number_of_threads;
1214    scope(|s| {
1215        let mut total_counts = vec![0; number_of_bins];
1216        (0..number_of_threads)
1217            .map(|t| {
1218                s.spawn(move || {
1219                    nondimensional_transverse_distribution_inner(
1220                        model,
1221                        nondimensional_force,
1222                        number_of_bins,
1223                        base + usize::from(t < remainder),
1224                        maximum_nondimensional_extension,
1225                    )
1226                })
1227            })
1228            .collect::<Vec<_>>()
1229            .into_iter()
1230            .for_each(|handle| {
1231                total_counts
1232                    .iter_mut()
1233                    .zip(handle.join().unwrap())
1234                    .for_each(|(tot, c)| *tot += c)
1235            });
1236        let bin_width = maximum_nondimensional_extension / (number_of_bins as Scalar);
1237        let bin_centers = (0..number_of_bins)
1238            .map(|i| (i as Scalar + 0.5) * bin_width)
1239            .collect();
1240        let total_samples = number_of_samples as Scalar;
1241        let bin_values = total_counts
1242            .into_iter()
1243            .map(|count| count as Scalar / total_samples / bin_width)
1244            .collect();
1245        (bin_centers, bin_values)
1246    })
1247}
1248
1249fn nondimensional_angular_distribution_inner<T: MonteCarlo>(
1250    model: &T,
1251    nondimensional_force: Scalar,
1252    num_bins: usize,
1253    number_of_samples: usize,
1254    maximum_nondimensional_extension: Scalar,
1255) -> Vec<usize> {
1256    let mut bin_counts = vec![0; num_bins];
1257    let end_index = model.number_of_links() as usize - 1;
1258    for _ in 0..number_of_samples {
1259        let configuration = model.random_configuration(nondimensional_force);
1260        let gamma = configuration[end_index].norm().value();
1261        let nondimensional_extension = if gamma == 0.0 {
1262            0.0
1263        } else {
1264            configuration[end_index][2].value() / gamma
1265        };
1266        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1267            panic!(
1268                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1269            )
1270        }
1271        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1272            / (2.0 * maximum_nondimensional_extension)
1273            * num_bins as Scalar) as usize;
1274        bin_counts[bin_index] += 1;
1275    }
1276    bin_counts
1277}
1278
1279fn nondimensional_lateral_distribution_inner<T: MonteCarlo>(
1280    model: &T,
1281    nondimensional_force: Scalar,
1282    num_bins: usize,
1283    number_of_samples: usize,
1284    maximum_nondimensional_extension: Scalar,
1285) -> Vec<usize> {
1286    let mut bin_counts = vec![0; num_bins];
1287    let num_links = model.number_of_links() as Scalar;
1288    let end_index = model.number_of_links() as usize - 1;
1289    for _ in 0..number_of_samples {
1290        let configuration = model.random_configuration(nondimensional_force);
1291        let nondimensional_extension = configuration[end_index][1].value() / num_links;
1292        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1293            panic!(
1294                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1295            )
1296        }
1297        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1298            / (2.0 * maximum_nondimensional_extension)
1299            * num_bins as Scalar) as usize;
1300        bin_counts[bin_index] += 1;
1301    }
1302    bin_counts
1303}
1304
1305fn nondimensional_longitudinal_distribution_inner<T: MonteCarlo>(
1306    model: &T,
1307    nondimensional_force: Scalar,
1308    num_bins: usize,
1309    number_of_samples: usize,
1310    maximum_nondimensional_extension: Scalar,
1311) -> Vec<usize> {
1312    let mut bin_counts = vec![0; num_bins];
1313    let num_links = model.number_of_links() as Scalar;
1314    let end_index = model.number_of_links() as usize - 1;
1315    for _ in 0..number_of_samples {
1316        let configuration = model.random_configuration(nondimensional_force);
1317        let nondimensional_extension = configuration[end_index][2].value() / num_links;
1318        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1319            panic!(
1320                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1321            )
1322        }
1323        let bin_index = ((nondimensional_extension + maximum_nondimensional_extension)
1324            / (2.0 * maximum_nondimensional_extension)
1325            * num_bins as Scalar) as usize;
1326        bin_counts[bin_index] += 1;
1327    }
1328    bin_counts
1329}
1330
1331fn nondimensional_radial_distribution_inner<T: MonteCarlo>(
1332    model: &T,
1333    nondimensional_force: Scalar,
1334    num_bins: usize,
1335    number_of_samples: usize,
1336    maximum_nondimensional_extension: Scalar,
1337) -> Vec<usize> {
1338    let mut bin_counts = vec![0; num_bins];
1339    let num_links = model.number_of_links() as Scalar;
1340    let end_index = model.number_of_links() as usize - 1;
1341    for _ in 0..number_of_samples {
1342        let configuration = model.random_configuration(nondimensional_force);
1343        let nondimensional_extension = configuration[end_index].norm().value() / num_links;
1344        if nondimensional_extension > maximum_nondimensional_extension {
1345            panic!(
1346                "Sample {nondimensional_extension} above maximum {maximum_nondimensional_extension}"
1347            )
1348        }
1349        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1350            * num_bins as Scalar) as usize;
1351        bin_counts[bin_index] += 1;
1352    }
1353    bin_counts
1354}
1355
1356fn nondimensional_transverse_distribution_inner<T: MonteCarlo>(
1357    model: &T,
1358    nondimensional_force: Scalar,
1359    num_bins: usize,
1360    number_of_samples: usize,
1361    maximum_nondimensional_extension: Scalar,
1362) -> Vec<usize> {
1363    let mut bin_counts = vec![0; num_bins];
1364    let num_links = model.number_of_links() as Scalar;
1365    let end_index = model.number_of_links() as usize - 1;
1366    for _ in 0..number_of_samples {
1367        let configuration = model.random_configuration(nondimensional_force);
1368        let nondimensional_extension = (configuration[end_index][0].value().powi(2)
1369            + configuration[end_index][1].value().powi(2))
1370        .sqrt()
1371            / num_links;
1372        if nondimensional_extension.abs() > maximum_nondimensional_extension {
1373            panic!(
1374                "Sample {nondimensional_extension} outside [-{maximum_nondimensional_extension}, {maximum_nondimensional_extension}]"
1375            )
1376        }
1377        let bin_index = (nondimensional_extension / maximum_nondimensional_extension
1378            * num_bins as Scalar) as usize;
1379        bin_counts[bin_index] += 1;
1380    }
1381    bin_counts
1382}