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 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 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 fn nondimensional_force(
302 &self,
303 nondimensional_extension: Scalar,
304 ) -> Result<Scalar, SingleChainError>;
305 fn nondimensional_stiffness(
309 &self,
310 nondimensional_extension: Scalar,
311 ) -> Result<Scalar, SingleChainError>;
312 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 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 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 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 fn nondimensional_extension(
363 &self,
364 nondimensional_force: Scalar,
365 ) -> Result<Scalar, SingleChainError>;
366 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 fn nondimensional_link_energy_average(
383 &self,
384 nondimensional_force: Scalar,
385 ) -> Result<Scalar, SingleChainError>;
386 fn nondimensional_link_energy_variance(
390 &self,
391 nondimensional_force: Scalar,
392 ) -> Result<Scalar, SingleChainError>;
393 fn nondimensional_link_energy_probability(
397 &self,
398 nondimensional_energy: Scalar,
399 nondimensional_force: Scalar,
400 ) -> Result<Scalar, SingleChainError>;
401 fn nondimensional_link_length_average(
405 &self,
406 nondimensional_force: Scalar,
407 ) -> Result<Scalar, SingleChainError>;
408 fn nondimensional_link_length_variance(
412 &self,
413 nondimensional_force: Scalar,
414 ) -> Result<Scalar, SingleChainError>;
415 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 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 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 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 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 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 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 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 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 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 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 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}