conspire/physics/molecular/single_chain/efjc/
mod.rs1#[cfg(test)]
2mod test;
3
4use crate::{
5 math::{
6 Scalar,
7 random::{random_uniform, random_x2_normal},
8 special::{erf, erfc},
9 },
10 mechanics::CurrentCoordinate,
11 physics::{
12 BOLTZMANN_CONSTANT,
13 molecular::single_chain::{
14 Configuration, Ensemble, Extensible, Isometric, Isotensional, IsotensionalExtensible,
15 Legendre, MonteCarlo, SingleChain, SingleChainError, Thermodynamics,
16 ThermodynamicsExtensible,
17 ufjc::{
18 nondimensional_extension as nondimensional_extension_asymptotic,
20 nondimensional_gibbs_free_energy_per_link as nondimensional_gibbs_free_energy_per_link_asymptotic,
21 },
22 },
23 },
24};
25use std::f64::consts::{PI, TAU};
26
27#[derive(Clone, Debug)]
31pub struct ExtensibleFreelyJointedChain {
32 pub link_length: Scalar,
34 pub link_stiffness: Scalar,
36 pub number_of_links: u8,
38 pub ensemble: Ensemble,
40}
41
42impl ExtensibleFreelyJointedChain {
43 fn nondimensional_link_stiffness(&self) -> Scalar {
44 self.link_stiffness * self.link_length().powi(2) / BOLTZMANN_CONSTANT / self.temperature()
45 }
46}
47
48impl SingleChain for ExtensibleFreelyJointedChain {
49 fn link_length(&self) -> Scalar {
50 self.link_length
51 }
52 fn number_of_links(&self) -> u8 {
53 self.number_of_links
54 }
55}
56
57impl Extensible for ExtensibleFreelyJointedChain {}
58
59impl Thermodynamics for ExtensibleFreelyJointedChain {
60 fn ensemble(&self) -> Ensemble {
61 self.ensemble
62 }
63}
64
65impl ThermodynamicsExtensible for ExtensibleFreelyJointedChain {}
66
67impl Isometric for ExtensibleFreelyJointedChain {
68 fn nondimensional_helmholtz_free_energy(
69 &self,
70 _nondimensional_extension: Scalar,
71 ) -> Result<Scalar, SingleChainError> {
72 unimplemented!()
73 }
74 fn nondimensional_force(
75 &self,
76 _nondimensional_extension: Scalar,
77 ) -> Result<Scalar, SingleChainError> {
78 unimplemented!()
79 }
80 fn nondimensional_stiffness(
81 &self,
82 _nondimensional_extension: Scalar,
83 ) -> Result<Scalar, SingleChainError> {
84 unimplemented!()
85 }
86 fn nondimensional_spherical_distribution(
87 &self,
88 _nondimensional_extension: Scalar,
89 ) -> Result<Scalar, SingleChainError> {
90 unimplemented!()
91 }
92}
93
94impl Isotensional for ExtensibleFreelyJointedChain {
95 fn nondimensional_gibbs_free_energy_per_link(
99 &self,
100 nondimensional_force: Scalar,
101 ) -> Result<Scalar, SingleChainError> {
102 let eta = nondimensional_force;
103 let kappa = self.nondimensional_link_stiffness();
104 let eta_over_kappa = eta / kappa;
105 let neg_2_eta_exp = (-2.0 * eta).exp();
106 Ok(nondimensional_gibbs_free_energy_per_link_asymptotic(
107 eta,
108 kappa,
109 -0.5 * eta.powi(2) / kappa,
110 1.0,
111 )? - (0.5
112 + ((eta_over_kappa + 1.0) * erf((eta + kappa) / (2.0 * kappa).sqrt())
113 - (eta_over_kappa - 1.0)
114 * neg_2_eta_exp
115 * erf((eta - kappa) / (2.0 * kappa).sqrt()))
116 / (2.0 * (1.0 - neg_2_eta_exp) * (1.0 + eta / eta.tanh() / kappa)))
117 .ln())
118 }
119 fn nondimensional_extension(
123 &self,
124 nondimensional_force: Scalar,
125 ) -> Result<Scalar, SingleChainError> {
126 let eta = nondimensional_force;
127 let kappa = self.nondimensional_link_stiffness();
128 let eta_over_kappa = eta / kappa;
129 let neg_2_eta_exp = (-2.0 * eta).exp();
130 let denominator = 2.0 * (1.0 - neg_2_eta_exp) * (1.0 + eta / eta.tanh() / kappa);
131 let fraction = ((eta_over_kappa + 1.0) * erf((eta + kappa) / (2.0 * kappa).sqrt())
132 - (eta_over_kappa - 1.0) * neg_2_eta_exp * erf((eta - kappa) / (2.0 * kappa).sqrt()))
133 / denominator;
134 Ok(
135 nondimensional_extension_asymptotic(eta, kappa, eta_over_kappa, 1.0)?
136 + (((2.0 / PI / kappa).sqrt()
137 * (eta_over_kappa + 1.0)
138 * (-(eta + kappa).powi(2) / 2.0 / kappa).exp()
139 + (1.0 + (1.0 + eta) / kappa))
140 - 1.0
141 * neg_2_eta_exp
142 * ((2.0 / PI / kappa).sqrt()
143 * (eta_over_kappa - 1.0)
144 * (-(eta - kappa).powi(2) / 2.0 / kappa).exp()
145 + (1.0 + (1.0 - eta) / kappa)
146 * erf((eta - kappa) / (2.0 * kappa).sqrt()))
147 - fraction
148 * (2.0
149 * ((1.0 + neg_2_eta_exp) * (1.0 + (1.0 + eta / eta.tanh()) / kappa)
150 - 4.0 * eta_over_kappa / (1.0 / neg_2_eta_exp - 1.0))))
151 / denominator
152 / (1.0 + fraction),
153 )
154 }
155 fn nondimensional_compliance(
159 &self,
160 _nondimensional_force: Scalar,
161 ) -> Result<Scalar, SingleChainError> {
162 unimplemented!()
163 }
164}
165
166impl IsotensionalExtensible for ExtensibleFreelyJointedChain {
167 fn nondimensional_link_energy_average(
171 &self,
172 nondimensional_force: Scalar,
173 ) -> Result<Scalar, SingleChainError> {
174 Ok(0.5
175 * self.nondimensional_link_stiffness()
176 * (nondimensional_link_length_squared_average(
177 self.nondimensional_link_stiffness(),
178 nondimensional_force,
179 )? - 2.0
180 * ThermodynamicsExtensible::nondimensional_link_length_average(
181 self,
182 nondimensional_force,
183 )?
184 + 1.0))
185 }
186 fn nondimensional_link_energy_variance(
190 &self,
191 nondimensional_force: Scalar,
192 ) -> Result<Scalar, SingleChainError> {
193 Ok(0.25
194 * self.nondimensional_link_stiffness().powi(2)
195 * (nondimensional_link_length_quad_average(
196 self.nondimensional_link_stiffness(),
197 nondimensional_force,
198 )? - 4.0
199 * nondimensional_link_length_cubed_average(
200 self.nondimensional_link_stiffness(),
201 nondimensional_force,
202 )?
203 + 6.0
204 * nondimensional_link_length_squared_average(
205 self.nondimensional_link_stiffness(),
206 nondimensional_force,
207 )?
208 - 4.0
209 * ThermodynamicsExtensible::nondimensional_link_length_average(
210 self,
211 nondimensional_force,
212 )?
213 + 1.0)
214 - ThermodynamicsExtensible::nondimensional_link_energy_average(
215 self,
216 nondimensional_force,
217 )?
218 .powi(2))
219 }
220 fn nondimensional_link_energy_probability(
224 &self,
225 nondimensional_energy: Scalar,
226 nondimensional_force: Scalar,
227 ) -> Result<Scalar, SingleChainError> {
228 let kappa = self.nondimensional_link_stiffness();
229 let eta = (2.0 * kappa * nondimensional_energy).sqrt();
230 let delta_lambda = (2.0 * nondimensional_energy / kappa).sqrt();
231 [eta, -eta]
232 .into_iter()
233 .zip([1.0 + delta_lambda, 1.0 - delta_lambda])
234 .map(|(eta, nondimensional_length)| {
235 Ok(
236 IsotensionalExtensible::nondimensional_link_length_probability(
237 self,
238 nondimensional_length,
239 nondimensional_force,
240 )? / eta.abs(),
241 )
242 })
243 .sum()
244 }
245 fn nondimensional_link_length_average(
249 &self,
250 nondimensional_force: Scalar,
251 ) -> Result<Scalar, SingleChainError> {
252 let eta = nondimensional_force;
253 let kappa = self.nondimensional_link_stiffness();
254 let eta_over_kappa = eta / kappa;
255 let erfd_p = 1.0 + erf((eta + kappa) / (2.0 * kappa).sqrt());
256 let exp_n2_eta_erfc_m = (-2.0 * eta).exp() * erfc((eta - kappa) / (2.0 * kappa).sqrt());
257 Ok(
258 (4.0 * (-0.5 * (eta.powi(2) / kappa + kappa) - eta).exp() / (TAU * kappa).sqrt()
259 * eta_over_kappa
260 + (1.0 / kappa + (eta_over_kappa + 1.0).powi(2)) * erfd_p
261 - (1.0 / kappa + (eta_over_kappa - 1.0).powi(2)) * exp_n2_eta_erfc_m)
262 / ((eta / kappa + 1.0) * erfd_p + (eta / kappa - 1.0) * exp_n2_eta_erfc_m),
263 )
264 }
265 fn nondimensional_link_length_variance(
269 &self,
270 nondimensional_force: Scalar,
271 ) -> Result<Scalar, SingleChainError> {
272 Ok(nondimensional_link_length_squared_average(
273 self.nondimensional_link_stiffness(),
274 nondimensional_force,
275 )? - ThermodynamicsExtensible::nondimensional_link_length_average(
276 self,
277 nondimensional_force,
278 )?
279 .powi(2))
280 }
281 fn nondimensional_link_length_probability(
285 &self,
286 nondimensional_length: Scalar,
287 nondimensional_force: Scalar,
288 ) -> Result<Scalar, SingleChainError> {
289 let eta = nondimensional_force;
290 let lambda = nondimensional_length;
291 let kappa = self.nondimensional_link_stiffness();
292 let eta_over_kappa = eta / kappa;
293 let upsilon_twice = 0.5 * kappa * ((lambda - 1.0).powi(2) + eta_over_kappa.powi(2));
294 Ok((kappa / TAU).sqrt()
295 * 2.0
296 * lambda
297 * ((eta * (lambda - 1.0) - upsilon_twice).exp()
298 - (-eta * (lambda + 1.0) - upsilon_twice).exp())
299 / ((1.0 + eta_over_kappa) * (1.0 + erf((eta + kappa) / (2.0 * kappa).sqrt()))
300 - (1.0 - eta_over_kappa)
301 * (-2.0 * eta).exp()
302 * erfc((eta - kappa) / (2.0 * kappa).sqrt())))
303 }
304}
305
306impl Legendre for ExtensibleFreelyJointedChain {
307 fn nondimensional_spherical_distribution(
308 &self,
309 _nondimensional_extension: Scalar,
310 ) -> Result<Scalar, SingleChainError> {
311 unimplemented!()
312 }
313}
314
315impl MonteCarlo for ExtensibleFreelyJointedChain {
316 fn random_nondimensional_link_vectors(&self, nondimensional_force: Scalar) -> Configuration {
317 let sigma = 1.0 / self.nondimensional_link_stiffness().sqrt();
318 (0..self.number_of_links())
319 .map(|_| {
320 let cos_theta = if nondimensional_force == 0.0 {
321 2.0 * random_uniform() - 1.0
322 } else {
323 todo!("Force biases the link stretch too.")
324 };
325 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
326 let phi = TAU * random_uniform();
327 let (sin_phi, cos_phi) = phi.sin_cos();
328 let lambda = random_x2_normal(1.0, sigma);
329 CurrentCoordinate::from([
330 lambda * sin_theta * cos_phi,
331 lambda * sin_theta * sin_phi,
332 lambda * cos_theta,
333 ])
334 })
335 .collect()
336 }
337}
338
339fn nondimensional_link_length_squared_average(
340 kappa: Scalar,
341 eta: Scalar,
342) -> Result<Scalar, SingleChainError> {
343 let eta_over_kappa = eta / kappa;
344 let erfd_p_pre = (eta / kappa + 1.0) * (1.0 + erf((eta + kappa) / (2.0 * kappa).sqrt()));
345 let exp_n2_eta_erfc_m_pre =
346 (eta / kappa - 1.0) * (-2.0 * eta).exp() * erfc((eta - kappa) / (2.0 * kappa).sqrt());
347 Ok(
348 (2.0 * (-0.5 * (eta.powi(2) / kappa + kappa) - eta).exp() / (TAU * kappa).sqrt()
349 * ((2.0 / kappa + (eta / kappa + 1.0).powi(2))
350 - (2.0 / kappa + (eta / kappa - 1.0).powi(2)))
351 + (3.0 / kappa + (eta_over_kappa + 1.0).powi(2)) * erfd_p_pre
352 + (3.0 / kappa + (eta_over_kappa - 1.0).powi(2)) * exp_n2_eta_erfc_m_pre)
353 / (erfd_p_pre + exp_n2_eta_erfc_m_pre),
354 )
355}
356
357fn nondimensional_link_length_cubed_average(
358 kappa: Scalar,
359 eta: Scalar,
360) -> Result<Scalar, SingleChainError> {
361 let eta_over_kappa = eta / kappa;
362 let x_p = (eta + kappa) / (2.0 * kappa).sqrt();
363 let x_m = (eta - kappa) / (2.0 * kappa).sqrt();
364 let one_plus_erf_p = 1.0 + erf(x_p);
365 let erfc_m = erfc(x_m);
366 let exp_n2_eta = (-2.0 * eta).exp();
367 let denominator =
368 (eta_over_kappa + 1.0) * one_plus_erf_p + exp_n2_eta * (eta_over_kappa - 1.0) * erfc_m;
369 let p_p = eta.powi(4)
370 + 4.0 * eta.powi(3) * kappa
371 + 6.0 * eta.powi(2) * kappa * (1.0 + kappa)
372 + 4.0 * eta * kappa.powi(2) * (3.0 + kappa)
373 + kappa.powi(2) * (3.0 + 6.0 * kappa + kappa.powi(2));
374 let p_m = eta.powi(4) - 4.0 * eta.powi(3) * kappa + 6.0 * eta.powi(2) * kappa * (1.0 + kappa)
375 - 4.0 * eta * kappa.powi(2) * (3.0 + kappa)
376 + kappa.powi(2) * (3.0 + 6.0 * kappa + kappa.powi(2));
377 let boundary =
378 2.0 * eta * (eta.powi(2) + 5.0 * kappa + 3.0 * kappa.powi(2)) * (2.0 / PI).sqrt()
379 / kappa.powf(3.5)
380 * (-(eta.powi(2) / (2.0 * kappa) + eta + 0.5 * kappa)).exp();
381 let branch_terms = (p_p * one_plus_erf_p - exp_n2_eta * p_m * erfc_m) / kappa.powi(4);
382 Ok((boundary + branch_terms) / denominator)
383}
384
385fn nondimensional_link_length_quad_average(
386 kappa: Scalar,
387 eta: Scalar,
388) -> Result<Scalar, SingleChainError> {
389 let sqrt_kappa = kappa.sqrt();
390 let sqrt_2 = 2.0_f64.sqrt();
391 let sqrt_pi = PI.sqrt();
392 let sqrt_2_pi = (2.0 * PI).sqrt();
393 let x_p = (eta + kappa) / (2.0 * kappa).sqrt();
394 let x_m = (eta - kappa) / (2.0 * kappa).sqrt();
395 let erf_p = erf(x_p);
396 let erfc_m = erfc(x_m);
397 let exp_p = ((eta + kappa).powi(2) / (2.0 * kappa)).exp();
398 let exp_m = ((eta - kappa).powi(2) / (2.0 * kappa)).exp();
399 let exp_n2_eta = (-2.0 * eta).exp();
400 let denominator =
401 (eta / kappa + 1.0) * (1.0 + erf_p) + exp_n2_eta * (eta / kappa - 1.0) * erfc_m;
402 let poly_p = eta.powi(5)
403 + 5.0 * eta.powi(4) * kappa
404 + 10.0 * eta.powi(3) * kappa * (1.0 + kappa)
405 + 10.0 * eta.powi(2) * kappa.powi(2) * (3.0 + kappa)
406 + 5.0 * eta * kappa.powi(2) * (3.0 + 6.0 * kappa + kappa.powi(2))
407 + kappa.powi(3) * (15.0 + 10.0 * kappa + kappa.powi(2));
408 let inner = -2.0 * (eta - kappa).powi(4) * sqrt_kappa
409 - 18.0 * (eta - kappa).powi(2) * kappa.powf(1.5)
410 + 18.0 * kappa.powf(3.5)
411 + 2.0 * kappa.powf(4.5)
412 + 2.0
413 * sqrt_2
414 * eta.powi(3)
415 * kappa
416 * (2.0 * sqrt_2 * sqrt_kappa + 5.0 * exp_p * sqrt_pi + 5.0 * exp_p * kappa * sqrt_pi)
417 + eta.powi(5) * exp_p * sqrt_2_pi
418 + 15.0 * exp_p * kappa.powi(3) * sqrt_2_pi
419 + 10.0 * exp_p * kappa.powi(4) * sqrt_2_pi
420 + exp_p * kappa.powi(5) * sqrt_2_pi
421 + eta.powi(4) * (2.0 * sqrt_kappa + 5.0 * exp_p * kappa * sqrt_2_pi)
422 + 2.0
423 * eta.powi(2)
424 * kappa.powf(1.5)
425 * (9.0
426 + 6.0 * kappa
427 + 15.0 * exp_p * sqrt_kappa * sqrt_2_pi
428 + 5.0 * exp_p * kappa.powf(1.5) * sqrt_2_pi)
429 + eta
430 * kappa.powi(2)
431 * (36.0 * sqrt_kappa
432 + 8.0 * kappa.powf(1.5)
433 + 15.0 * exp_p * sqrt_2_pi
434 + 30.0 * exp_p * kappa * sqrt_2_pi
435 + 5.0 * exp_p * kappa.powi(2) * sqrt_2_pi)
436 + exp_m * (eta - kappa).powi(5) * sqrt_2_pi * erfc_m
437 + 10.0 * exp_m * (eta - kappa).powi(3) * kappa * sqrt_2_pi * erfc_m
438 + 15.0 * exp_m * (eta - kappa) * kappa.powi(2) * sqrt_2_pi * erfc_m
439 + exp_p * poly_p * sqrt_2_pi * erf_p;
440 let numerator = (-(eta.powi(2) / (2.0 * kappa) + eta + 0.5 * kappa)).exp()
441 / (TAU.sqrt() * kappa.powi(5))
442 * inner;
443 Ok(numerator / denominator)
444}