hoomd_interaction/external_type.rs
1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement `External`
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 DeltaEnergyInsert, DeltaEnergyOne, DeltaEnergyRemove, MaximumInteractionRange, SiteEnergy,
10 TotalEnergy,
11};
12use hoomd_microstate::{Body, Microstate, Transform, boundary::Wrap, property::Position};
13
14/// Interactions between sites and external fields.
15///
16/// Given an inner type that implements [`SiteEnergy`], [`External`] represents:
17///
18/// ```math
19/// U_\mathrm{total} = \sum_{i=0}^{N-1} U\left( s_i \right)
20/// ```
21/// where $`s_i`$ is the full set of site properties for site i.
22///
23/// For the inner type, use one from [`external`] or your own custom type.
24///
25/// [`external`]: crate::external
26///
27/// # Examples
28///
29/// A linear external potential:
30/// ```
31/// use hoomd_interaction::{External, TotalEnergy, external::Linear};
32/// use hoomd_microstate::{Body, Microstate, property::Point};
33/// use hoomd_vector::Cartesian;
34///
35/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
36/// let mut microstate = Microstate::new();
37/// microstate.extend_bodies([
38/// Body::point(Cartesian::from([1.0, 0.0])),
39/// Body::point(Cartesian::from([-1.0, 2.0])),
40/// ])?;
41///
42/// let linear = External(Linear {
43/// alpha: 1.0,
44/// plane_origin: Cartesian::default(),
45/// plane_normal: [0.0, 1.0].try_into()?,
46/// });
47///
48/// let total_energy = linear.total_energy(µstate);
49/// assert_eq!(total_energy, 2.0);
50/// # Ok(())
51/// # }
52/// ```
53///
54/// Infinite interaction with a wall:
55/// ```
56/// use hoomd_interaction::{External, SiteEnergy, TotalEnergy};
57/// use hoomd_microstate::{Body, Microstate, property::Point};
58/// use hoomd_vector::Cartesian;
59///
60/// struct Wall;
61///
62/// impl SiteEnergy<Point<Cartesian<2>>> for Wall {
63/// fn site_energy(&self, site_properties: &Point<Cartesian<2>>) -> f64 {
64/// if site_properties.position[1].abs() < 1.0 {
65/// f64::INFINITY
66/// } else {
67/// 0.0
68/// }
69/// }
70///
71/// fn is_only_infinite_or_zero() -> bool {
72/// true
73/// }
74/// }
75///
76/// fn main() -> Result<(), Box<dyn std::error::Error>> {
77/// let mut microstate = Microstate::new();
78/// microstate.extend_bodies([
79/// Body::point(Cartesian::from([1.0, 1.25])),
80/// Body::point(Cartesian::from([-1.0, 2.0])),
81/// ])?;
82///
83/// let wall = External(Wall);
84///
85/// let total_energy = wall.total_energy(µstate);
86/// assert_eq!(total_energy, 0.0);
87/// Ok(())
88/// }
89/// ```
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91pub struct External<E>(pub E);
92
93impl<B, S, X, C, E> TotalEnergy<Microstate<B, S, X, C>> for External<E>
94where
95 E: SiteEnergy<S>,
96{
97 /// Compute the total energy of the microstate contributed by functions of a single site.
98 ///
99 /// The sum over sites differs from HOOMD-blue where external energies are
100 /// evaluated only at the body centers. In general, hoomd-rs interactions apply
101 /// to sites. Use a custom implementation to compute energies over body centers.
102 ///
103 /// # Examples
104 ///
105 /// A linear external potential:
106 /// ```
107 /// use hoomd_interaction::{External, TotalEnergy, external::Linear};
108 /// use hoomd_microstate::{Body, Microstate, property::Point};
109 /// use hoomd_vector::Cartesian;
110 ///
111 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
112 /// let mut microstate = Microstate::new();
113 /// microstate.extend_bodies([
114 /// Body::point(Cartesian::from([1.0, 0.0])),
115 /// Body::point(Cartesian::from([-1.0, 2.0])),
116 /// ])?;
117 ///
118 /// let linear = External(Linear {
119 /// alpha: 1.0,
120 /// plane_origin: Cartesian::default(),
121 /// plane_normal: [0.0, 1.0].try_into()?,
122 /// });
123 ///
124 /// let total_energy = linear.total_energy(µstate);
125 /// assert_eq!(total_energy, 2.0);
126 /// # Ok(())
127 /// # }
128 /// ```
129 ///
130 /// Infinite interaction with a wall:
131 /// ```
132 /// use hoomd_interaction::{External, SiteEnergy, TotalEnergy};
133 /// use hoomd_microstate::{Body, Microstate, property::Point};
134 /// use hoomd_vector::Cartesian;
135 ///
136 /// struct Wall;
137 ///
138 /// impl SiteEnergy<Point<Cartesian<2>>> for Wall {
139 /// fn site_energy(&self, site_properties: &Point<Cartesian<2>>) -> f64 {
140 /// if site_properties.position[1].abs() < 1.0 {
141 /// f64::INFINITY
142 /// } else {
143 /// 0.0
144 /// }
145 /// }
146 ///
147 /// fn is_only_infinite_or_zero() -> bool {
148 /// true
149 /// }
150 /// }
151 ///
152 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
153 /// let mut microstate = Microstate::new();
154 /// microstate.extend_bodies([
155 /// Body::point(Cartesian::from([1.0, 1.25])),
156 /// Body::point(Cartesian::from([-1.0, 2.0])),
157 /// ])?;
158 ///
159 /// let wall = External(Wall);
160 ///
161 /// let total_energy = wall.total_energy(µstate);
162 /// assert_eq!(total_energy, 0.0);
163 /// Ok(())
164 /// }
165 /// ```
166 #[inline]
167 fn total_energy(&self, microstate: &Microstate<B, S, X, C>) -> f64 {
168 let mut total = 0.0;
169 for site in microstate.sites() {
170 let one = self.0.site_energy(&site.properties);
171 if one == f64::INFINITY {
172 return one;
173 }
174 total += one;
175 }
176
177 total
178 }
179
180 /// Compute the difference in energy between two microstates.
181 ///
182 /// Returns $` E_\mathrm{final} - E_\mathrm{initial} `$.
183 ///
184 /// # Example
185 ///
186 /// ```
187 /// use hoomd_interaction::{External, TotalEnergy, external::Linear};
188 /// use hoomd_microstate::{Body, Microstate, property::Point};
189 /// use hoomd_vector::Cartesian;
190 ///
191 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
192 /// let mut microstate_a = Microstate::new();
193 /// microstate_a.extend_bodies([
194 /// Body::point(Cartesian::from([1.0, 0.0])),
195 /// Body::point(Cartesian::from([-1.0, 2.0])),
196 /// ])?;
197 ///
198 /// let mut microstate_b = Microstate::new();
199 /// microstate_b.extend_bodies([
200 /// Body::point(Cartesian::from([1.0, 1.0])),
201 /// Body::point(Cartesian::from([-1.0, 2.0])),
202 /// ])?;
203 ///
204 /// let linear = External(Linear {
205 /// alpha: 1.0,
206 /// plane_origin: Cartesian::default(),
207 /// plane_normal: [0.0, 1.0].try_into()?,
208 /// });
209 ///
210 /// let delta_energy_total =
211 /// linear.delta_energy_total(µstate_a, µstate_b);
212 /// assert_eq!(delta_energy_total, 1.0);
213 /// # Ok(())
214 /// # }
215 /// ```
216 #[inline]
217 fn delta_energy_total(
218 &self,
219 initial_microstate: &Microstate<B, S, X, C>,
220 final_microstate: &Microstate<B, S, X, C>,
221 ) -> f64 {
222 let mut energy_final = 0.0;
223 for site in final_microstate.sites() {
224 let one = self.0.site_energy(&site.properties);
225 if one == f64::INFINITY {
226 return one;
227 }
228 energy_final += one;
229 }
230
231 let mut energy_initial = 0.0;
232 if !E::is_only_infinite_or_zero() {
233 for site in initial_microstate.sites() {
234 let one = self.0.site_energy_initial(&site.properties);
235 if one == f64::INFINITY {
236 return -one;
237 }
238 energy_initial += one;
239 }
240 }
241
242 energy_final - energy_initial
243 }
244}
245
246impl<P, B, S, X, C, E> DeltaEnergyOne<B, S, X, C> for External<E>
247where
248 E: SiteEnergy<S>,
249 B: Transform<S>,
250 S: Position<Position = P>,
251 C: Wrap<B> + Wrap<S>,
252{
253 /// Evaluate the change in energy contributed by `External` when a single body is updated.
254 ///
255 /// # Examples
256 ///
257 /// A linear external potential:
258 /// ```
259 /// use hoomd_interaction::{DeltaEnergyOne, External, external::Linear};
260 /// use hoomd_microstate::{Body, Microstate, property::Point};
261 /// use hoomd_vector::Cartesian;
262 ///
263 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
264 /// let mut microstate = Microstate::new();
265 /// microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])))?;
266 ///
267 /// let linear = External(Linear {
268 /// alpha: 1.0,
269 /// plane_origin: Cartesian::default(),
270 /// plane_normal: [0.0, 1.0].try_into()?,
271 /// });
272 ///
273 /// let delta_energy = linear.delta_energy_one(
274 /// µstate,
275 /// 0,
276 /// &Body::point([0.0, -1.0].into()),
277 /// );
278 /// assert_eq!(delta_energy, -1.0);
279 /// # Ok(())
280 /// # }
281 /// ```
282 ///
283 /// Infinite interaction with a wall:
284 /// ```
285 /// use hoomd_interaction::{DeltaEnergyOne, External, SiteEnergy};
286 /// use hoomd_microstate::{Body, Microstate, property::Point};
287 /// use hoomd_vector::Cartesian;
288 ///
289 /// struct Wall;
290 ///
291 /// impl SiteEnergy<Point<Cartesian<2>>> for Wall {
292 /// fn site_energy(&self, site_properties: &Point<Cartesian<2>>) -> f64 {
293 /// if site_properties.position[1].abs() < 1.0 {
294 /// f64::INFINITY
295 /// } else {
296 /// 0.0
297 /// }
298 /// }
299 ///
300 /// fn is_only_infinite_or_zero() -> bool {
301 /// true
302 /// }
303 /// }
304 ///
305 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
306 /// let mut microstate = Microstate::new();
307 /// microstate.extend_bodies([
308 /// Body::point(Cartesian::from([1.0, 1.25])),
309 /// Body::point(Cartesian::from([-1.0, 2.0])),
310 /// ])?;
311 ///
312 /// let wall = External(Wall);
313 ///
314 /// let delta_energy = wall.delta_energy_one(
315 /// µstate,
316 /// 0,
317 /// &Body::point([0.0, -0.5].into()),
318 /// );
319 /// assert_eq!(delta_energy, f64::INFINITY);
320 /// Ok(())
321 /// }
322 /// ```
323 #[inline]
324 fn delta_energy_one(
325 &self,
326 initial_microstate: &Microstate<B, S, X, C>,
327 body_index: usize,
328 final_body: &Body<B, S>,
329 ) -> f64 {
330 let mut energy_final = 0.0;
331 for s in &final_body.sites {
332 match initial_microstate
333 .boundary()
334 .wrap(final_body.properties.transform(s))
335 {
336 Ok(wrapped_site) => {
337 let one = self.0.site_energy(&wrapped_site);
338 if one == f64::INFINITY {
339 return one;
340 }
341 energy_final += one;
342 }
343 Err(_) => return f64::INFINITY,
344 }
345 }
346
347 let energy_initial = if E::is_only_infinite_or_zero() {
348 0.0
349 } else {
350 initial_microstate
351 .iter_body_sites(body_index)
352 .fold(0.0, |total, s| {
353 total + self.0.site_energy_initial(&s.properties)
354 })
355 };
356
357 energy_final - energy_initial
358 }
359}
360
361impl<P, B, S, X, C, E> DeltaEnergyInsert<B, S, X, C> for External<E>
362where
363 E: SiteEnergy<S>,
364 B: Transform<S>,
365 S: Position<Position = P>,
366 C: Wrap<B> + Wrap<S>,
367{
368 /// Evaluate the change in energy contributed by `External` when a single body is inserted.
369 ///
370 /// # Examples
371 ///
372 /// A linear external potential:
373 /// ```
374 /// use hoomd_interaction::{DeltaEnergyInsert, External, external::Linear};
375 /// use hoomd_microstate::{Body, Microstate, property::Point};
376 /// use hoomd_vector::Cartesian;
377 ///
378 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
379 /// let mut microstate = Microstate::new();
380 /// microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])))?;
381 ///
382 /// let linear = External(Linear {
383 /// alpha: 1.0,
384 /// plane_origin: Cartesian::default(),
385 /// plane_normal: [0.0, 1.0].try_into()?,
386 /// });
387 ///
388 /// let delta_energy = linear
389 /// .delta_energy_insert(µstate, &Body::point([0.0, -1.0].into()));
390 /// assert_eq!(delta_energy, -1.0);
391 /// # Ok(())
392 /// # }
393 /// ```
394 ///
395 /// Infinite interaction with a wall:
396 /// ```
397 /// use hoomd_interaction::{DeltaEnergyInsert, External, SiteEnergy};
398 /// use hoomd_microstate::{Body, Microstate, property::Point};
399 /// use hoomd_vector::Cartesian;
400 ///
401 /// struct Wall;
402 ///
403 /// impl SiteEnergy<Point<Cartesian<2>>> for Wall {
404 /// fn site_energy(&self, site_properties: &Point<Cartesian<2>>) -> f64 {
405 /// if site_properties.position[1].abs() < 1.0 {
406 /// f64::INFINITY
407 /// } else {
408 /// 0.0
409 /// }
410 /// }
411 ///
412 /// fn is_only_infinite_or_zero() -> bool {
413 /// true
414 /// }
415 /// }
416 ///
417 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
418 /// let mut microstate = Microstate::new();
419 /// microstate.extend_bodies([
420 /// Body::point(Cartesian::from([1.0, 1.25])),
421 /// Body::point(Cartesian::from([-1.0, 2.0])),
422 /// ])?;
423 ///
424 /// let wall = External(Wall);
425 ///
426 /// let delta_energy = wall
427 /// .delta_energy_insert(µstate, &Body::point([0.0, -0.5].into()));
428 /// assert_eq!(delta_energy, f64::INFINITY);
429 /// Ok(())
430 /// }
431 /// ```
432 #[inline]
433 fn delta_energy_insert(
434 &self,
435 initial_microstate: &Microstate<B, S, X, C>,
436 new_body: &Body<B, S>,
437 ) -> f64 {
438 let mut energy_final = 0.0;
439 for s in &new_body.sites {
440 match initial_microstate
441 .boundary()
442 .wrap(new_body.properties.transform(s))
443 {
444 Ok(wrapped_site) => {
445 let one = self.0.site_energy(&wrapped_site);
446 if one == f64::INFINITY {
447 return one;
448 }
449 energy_final += one;
450 }
451 Err(_) => return f64::INFINITY,
452 }
453 }
454
455 energy_final
456 }
457}
458
459impl<B, S, X, C, E> DeltaEnergyRemove<B, S, X, C> for External<E>
460where
461 E: SiteEnergy<S>,
462{
463 /// Evaluate the change in energy contributed by `External` when a single body is removed.
464 ///
465 /// # Examples
466 ///
467 /// A linear external potential:
468 /// ```
469 /// use hoomd_interaction::{DeltaEnergyRemove, External, external::Linear};
470 /// use hoomd_microstate::{Body, Microstate, property::Point};
471 /// use hoomd_vector::Cartesian;
472 ///
473 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
474 /// let mut microstate = Microstate::new();
475 /// microstate.add_body(Body::point(Cartesian::from([0.0, 1.0])))?;
476 ///
477 /// let linear = External(Linear {
478 /// alpha: 1.0,
479 /// plane_origin: Cartesian::default(),
480 /// plane_normal: [0.0, 1.0].try_into()?,
481 /// });
482 ///
483 /// let delta_energy = linear.delta_energy_remove(µstate, 0);
484 /// assert_eq!(delta_energy, -1.0);
485 /// # Ok(())
486 /// # }
487 /// ```
488 ///
489 /// Infinite interaction with a wall:
490 /// ```
491 /// use hoomd_interaction::{DeltaEnergyRemove, External, SiteEnergy};
492 /// use hoomd_microstate::{Body, Microstate, property::Point};
493 /// use hoomd_vector::Cartesian;
494 ///
495 /// struct Wall;
496 ///
497 /// impl SiteEnergy<Point<Cartesian<2>>> for Wall {
498 /// fn site_energy(&self, site_properties: &Point<Cartesian<2>>) -> f64 {
499 /// if site_properties.position[1].abs() < 1.0 {
500 /// f64::INFINITY
501 /// } else {
502 /// 0.0
503 /// }
504 /// }
505 ///
506 /// fn is_only_infinite_or_zero() -> bool {
507 /// true
508 /// }
509 /// }
510 ///
511 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
512 /// let mut microstate = Microstate::new();
513 /// microstate.extend_bodies([
514 /// Body::point(Cartesian::from([1.0, 1.25])),
515 /// Body::point(Cartesian::from([-1.0, 2.0])),
516 /// ])?;
517 ///
518 /// let wall = External(Wall);
519 ///
520 /// let delta_energy = wall.delta_energy_remove(µstate, 0);
521 /// assert_eq!(delta_energy, 0.0);
522 /// Ok(())
523 /// }
524 /// ```
525 #[inline]
526 fn delta_energy_remove(
527 &self,
528 initial_microstate: &Microstate<B, S, X, C>,
529 body_index: usize,
530 ) -> f64 {
531 if E::is_only_infinite_or_zero() {
532 return 0.0;
533 }
534
535 let energy_initial = initial_microstate
536 .iter_body_sites(body_index)
537 .fold(0.0, |total, s| {
538 total + self.0.site_energy_initial(&s.properties)
539 });
540
541 -energy_initial
542 }
543}
544
545impl<E> MaximumInteractionRange for External<E> {
546 #[inline]
547 fn maximum_interaction_range(&self) -> f64 {
548 // External interactions are not applied between pairs of particles.
549 0.0
550 }
551}
552
553#[cfg(test)]
554mod test_finite {
555 use super::*;
556 use crate::external::Linear;
557 use assert2::check;
558 use hoomd_geometry::shape::Rectangle;
559 use hoomd_microstate::{
560 Body, Microstate,
561 boundary::{Closed, Open},
562 property::{Point, Position},
563 };
564 use hoomd_vector::{Cartesian, Unit};
565 use rstest::*;
566
567 struct TestSE;
568
569 impl<S> SiteEnergy<S> for TestSE
570 where
571 S: Position<Position = Cartesian<2>>,
572 {
573 fn site_energy(&self, site_properties: &S) -> f64 {
574 site_properties.position()[0] + site_properties.position()[1]
575 }
576 }
577
578 mod site_energy {
579 use super::*;
580 use hoomd_microstate::SiteKey;
581 use hoomd_spatial::AllPairs;
582
583 #[fixture]
584 fn microstate()
585 -> Microstate<Point<Cartesian<2>>, Point<Cartesian<2>>, AllPairs<SiteKey>, Open> {
586 let mut microstate = Microstate::new();
587 microstate
588 .extend_bodies([
589 Body::point(Cartesian::from([1.0, 0.0])),
590 Body::point(Cartesian::from([-1.0, 3.0])),
591 ])
592 .expect("hard-coded bodies should be in the boundary");
593 microstate
594 }
595
596 #[rstest]
597 fn single_total(
598 microstate: Microstate<
599 Point<Cartesian<2>>,
600 Point<Cartesian<2>>,
601 AllPairs<SiteKey>,
602 Open,
603 >,
604 ) {
605 let test_se = TestSE;
606 let single = External(test_se);
607
608 check!(single.total_energy(µstate) == 3.0);
609 }
610
611 #[rstest]
612 fn single_site(
613 microstate: Microstate<
614 Point<Cartesian<2>>,
615 Point<Cartesian<2>>,
616 AllPairs<SiteKey>,
617 Open,
618 >,
619 ) {
620 let test_se = TestSE;
621 let single = External(test_se);
622
623 check!(single.0.site_energy(µstate.sites()[0].properties) == 1.0);
624 check!(single.0.site_energy(µstate.sites()[1].properties) == 2.0);
625 }
626 }
627 mod delta_energy {
628 use super::*;
629
630 struct Zero;
631
632 impl SiteEnergy<Point<Cartesian<2>>> for Zero {
633 fn site_energy(&self, _site_properties: &Point<Cartesian<2>>) -> f64 {
634 0.0
635 }
636 }
637
638 #[test]
639 fn site_outside() {
640 let cuboid = Rectangle::with_equal_edges(
641 4.0.try_into()
642 .expect("hard-coded constant should be positive"),
643 );
644 let square = Closed(cuboid);
645
646 let body = Body {
647 properties: Point::new(Cartesian::from([0.0, 0.0])),
648 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
649 };
650 let mut final_body = body.clone();
651 final_body.properties.position[0] = 1.0;
652
653 let microstate = Microstate::builder()
654 .boundary(square)
655 .bodies([body])
656 .try_build()
657 .expect("the hard-coded bodies should be in the boundary");
658
659 let energy = External(Zero);
660
661 check!(energy.delta_energy_one(µstate, 0, &final_body) == f64::INFINITY);
662 check!(energy.delta_energy_insert(µstate, &final_body) == f64::INFINITY);
663 }
664
665 #[test]
666 fn delta_energy() -> anyhow::Result<()> {
667 let cuboid = Rectangle::with_equal_edges(
668 4.0.try_into()
669 .expect("hard-coded constant should be positive"),
670 );
671 let square = Closed(cuboid);
672
673 let body = Body {
674 properties: Point::new(Cartesian::from([0.0, 0.0])),
675 sites: [Point::new(Cartesian::from([0.0, 0.0]))].into(),
676 };
677 let mut final_body = body.clone();
678 final_body.properties.position[1] = 0.5;
679
680 let microstate = Microstate::builder()
681 .boundary(square)
682 .bodies([body])
683 .try_build()
684 .expect("the hard-coded bodies should be in the boundary");
685
686 let plane_normal = Unit::<Cartesian<2>>::try_from([0.0, 1.0])
687 .expect("the hard-coded vector is not zero");
688 let energy = External(Linear {
689 plane_origin: [0.0, -1.0].into(),
690 plane_normal,
691 alpha: 4.0,
692 });
693
694 check!(energy.delta_energy_one(µstate, 0, &final_body) == 2.0);
695 check!(energy.delta_energy_insert(µstate, &final_body) == 6.0);
696 check!(energy.delta_energy_remove(µstate, 0) == -4.0);
697
698 let mut microstate_final = microstate.clone();
699 microstate_final.update_body_properties(0, final_body.properties)?;
700
701 check!(energy.delta_energy_total(µstate, µstate_final) == 2.0);
702
703 Ok(())
704 }
705 }
706}
707
708#[cfg(test)]
709mod test_infinite {
710 use super::*;
711 use assert2::check;
712 use hoomd_geometry::shape::Rectangle;
713 use hoomd_microstate::{
714 Body, Microstate,
715 boundary::{Closed, Open},
716 property::{Point, Position},
717 };
718 use hoomd_vector::Cartesian;
719 use rstest::*;
720
721 struct TestSO;
722
723 impl<S> SiteEnergy<S> for TestSO
724 where
725 S: Position<Position = Cartesian<2>>,
726 {
727 fn site_energy(&self, site_properties: &S) -> f64 {
728 if site_properties.position()[1].abs() < 1.0 {
729 f64::INFINITY
730 } else {
731 0.0
732 }
733 }
734
735 fn is_only_infinite_or_zero() -> bool {
736 true
737 }
738 }
739
740 mod site_energy {
741 use super::*;
742 use hoomd_microstate::SiteKey;
743 use hoomd_spatial::AllPairs;
744
745 #[fixture]
746 fn microstate()
747 -> Microstate<Point<Cartesian<2>>, Point<Cartesian<2>>, AllPairs<SiteKey>, Open> {
748 let mut microstate = Microstate::new();
749 microstate
750 .extend_bodies([
751 Body::point(Cartesian::from([1.0, -2.0])),
752 Body::point(Cartesian::from([-1.0, 3.0])),
753 ])
754 .expect("hard-coded bodies should be in the boundary");
755 microstate
756 }
757
758 #[fixture]
759 fn overlapping_microstate()
760 -> Microstate<Point<Cartesian<2>>, Point<Cartesian<2>>, AllPairs<SiteKey>, Open> {
761 let mut microstate = Microstate::new();
762 microstate
763 .extend_bodies([
764 Body::point(Cartesian::from([1.0, 0.75])),
765 Body::point(Cartesian::from([-1.0, 3.0])),
766 ])
767 .expect("hard-coded bodies should be in the boundary");
768 microstate
769 }
770
771 #[rstest]
772 fn single_total_0(
773 microstate: Microstate<
774 Point<Cartesian<2>>,
775 Point<Cartesian<2>>,
776 AllPairs<SiteKey>,
777 Open,
778 >,
779 ) {
780 let single = External(TestSO);
781
782 check!(single.total_energy(µstate) == 0.0);
783 }
784
785 #[rstest]
786 fn single_total_inf(
787 overlapping_microstate: Microstate<
788 Point<Cartesian<2>>,
789 Point<Cartesian<2>>,
790 AllPairs<SiteKey>,
791 Open,
792 >,
793 ) {
794 let single = External(TestSO);
795
796 check!(single.total_energy(&overlapping_microstate) == f64::INFINITY);
797 }
798
799 #[rstest]
800 fn single_site_0(
801 microstate: Microstate<
802 Point<Cartesian<2>>,
803 Point<Cartesian<2>>,
804 AllPairs<SiteKey>,
805 Open,
806 >,
807 ) {
808 let single = External(TestSO);
809
810 check!(single.0.site_energy(µstate.sites()[0].properties) == 0.0);
811 check!(single.0.site_energy(µstate.sites()[1].properties) == 0.0);
812 }
813
814 #[rstest]
815 fn single_site_inf(
816 overlapping_microstate: Microstate<
817 Point<Cartesian<2>>,
818 Point<Cartesian<2>>,
819 AllPairs<SiteKey>,
820 Open,
821 >,
822 ) {
823 let single = External(TestSO);
824
825 check!(
826 single
827 .0
828 .site_energy(&overlapping_microstate.sites()[0].properties)
829 == f64::INFINITY
830 );
831 check!(
832 single
833 .0
834 .site_energy(&overlapping_microstate.sites()[1].properties)
835 == 0.0
836 );
837 }
838 }
839 mod delta_energy {
840 use super::*;
841
842 struct Zero;
843
844 impl SiteEnergy<Point<Cartesian<2>>> for Zero {
845 fn site_energy(&self, _site_properties: &Point<Cartesian<2>>) -> f64 {
846 0.0
847 }
848
849 fn is_only_infinite_or_zero() -> bool {
850 true
851 }
852 }
853
854 #[test]
855 fn site_outside() {
856 let cuboid = Rectangle::with_equal_edges(
857 4.0.try_into()
858 .expect("hard-coded constant should be positive"),
859 );
860 let square = Closed(cuboid);
861
862 let body = Body {
863 properties: Point::new(Cartesian::from([0.0, 0.0])),
864 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
865 };
866 let mut final_body = body.clone();
867 final_body.properties.position[0] = 1.0;
868
869 let microstate = Microstate::builder()
870 .boundary(square)
871 .bodies([body])
872 .try_build()
873 .expect("the hard-coded bodies should be in the boundary");
874
875 let energy = External(Zero);
876
877 check!(energy.delta_energy_one(µstate, 0, &final_body) == f64::INFINITY);
878 check!(energy.delta_energy_insert(µstate, &final_body) == f64::INFINITY);
879 }
880
881 #[test]
882 fn delta_energy() -> anyhow::Result<()> {
883 let cuboid = Rectangle::with_equal_edges(
884 4.0.try_into()
885 .expect("hard-coded constant should be positive"),
886 );
887 let square = Closed(cuboid);
888
889 let body = Body {
890 properties: Point::new(Cartesian::from([1.5, 1.5])),
891 sites: [Point::new(Cartesian::from([0.0, 0.0]))].into(),
892 };
893 let mut final_body_inf = body.clone();
894 final_body_inf.properties.position[1] = 0.5;
895
896 let mut final_body_0 = body.clone();
897 final_body_0.properties.position[1] = -1.5;
898
899 let microstate = Microstate::builder()
900 .boundary(square)
901 .bodies([body])
902 .try_build()
903 .expect("the hard-coded bodies should be in the boundary");
904
905 let energy = External(TestSO);
906
907 check!(energy.delta_energy_one(µstate, 0, &final_body_0) == 0.0);
908 check!(energy.delta_energy_one(µstate, 0, &final_body_inf) == f64::INFINITY);
909 check!(energy.delta_energy_insert(µstate, &final_body_0) == 0.0);
910 check!(energy.delta_energy_insert(µstate, &final_body_inf) == f64::INFINITY);
911 check!(energy.delta_energy_remove(µstate, 0) == 0.0);
912
913 let mut microstate_inf = microstate.clone();
914 microstate_inf.update_body_properties(0, final_body_inf.properties)?;
915
916 let mut microstate_0 = microstate.clone();
917 microstate_0.update_body_properties(0, final_body_0.properties)?;
918
919 check!(energy.delta_energy_total(µstate_0, µstate_0) == 0.0);
920 check!(energy.delta_energy_total(µstate_0, µstate_inf) == f64::INFINITY);
921 check!(energy.delta_energy_total(µstate_inf, µstate_0) == 0.0);
922 check!(energy.delta_energy_total(µstate_inf, µstate_inf) == f64::INFINITY);
923
924 Ok(())
925 }
926 }
927}