Skip to main content

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(&microstate);
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(&microstate);
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(&microstate);
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(&microstate);
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(&microstate_a, &microstate_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    ///     &microstate,
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    ///         &microstate,
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(&microstate, &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(&microstate, &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(&microstate, 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(&microstate, 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(&microstate) == 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(&microstate.sites()[0].properties) == 1.0);
624            check!(single.0.site_energy(&microstate.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(&microstate, 0, &final_body) == f64::INFINITY);
662            check!(energy.delta_energy_insert(&microstate, &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(&microstate, 0, &final_body) == 2.0);
695            check!(energy.delta_energy_insert(&microstate, &final_body) == 6.0);
696            check!(energy.delta_energy_remove(&microstate, 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(&microstate, &microstate_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(&microstate) == 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(&microstate.sites()[0].properties) == 0.0);
811            check!(single.0.site_energy(&microstate.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(&microstate, 0, &final_body) == f64::INFINITY);
878            check!(energy.delta_energy_insert(&microstate, &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(&microstate, 0, &final_body_0) == 0.0);
908            check!(energy.delta_energy_one(&microstate, 0, &final_body_inf) == f64::INFINITY);
909            check!(energy.delta_energy_insert(&microstate, &final_body_0) == 0.0);
910            check!(energy.delta_energy_insert(&microstate, &final_body_inf) == f64::INFINITY);
911            check!(energy.delta_energy_remove(&microstate, 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(&microstate_0, &microstate_0) == 0.0);
920            check!(energy.delta_energy_total(&microstate_0, &microstate_inf) == f64::INFINITY);
921            check!(energy.delta_energy_total(&microstate_inf, &microstate_0) == 0.0);
922            check!(energy.delta_energy_total(&microstate_inf, &microstate_inf) == f64::INFINITY);
923
924            Ok(())
925        }
926    }
927}