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

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