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