Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Polydisperse Hard Disk Model

Click to focus. When focused, press m to open the options menu. Refresh the page to restart the simulation.

Overview

In some models, every site is different from every other site. For example, you can model polydisperse hard disks where each site has its own radius. This tutorial shows you how to define a custom site type and compute a radius-dependent pairwise interactions.

  • Objectives:
    • Add a radius field to a custom site properties type.
    • Show how to compute radius-dependent pairwise interactions.
    • Execute a sample simulation where every site has a randomly chosen radius.
  • File: hoomd-rs/examples/mc-tutorial/polydisperse-hard-disk-model.rs
  • Run (interactively):
    cargo run --release --features "bevy" --example polydisperse-hard-disk-model
    
  • Run (in batch mode):
    cargo run --release --example polydisperse-hard-disk-model
    

Use Declarations

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Type Aliases

Create type aliases for your model’s vector and body properties so that you don’t need to repeat the full generic type names throughout the code:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

The sites are in this tutorial are placed at points in space and interact via an isotropic interaction. Therefore, use Point for the body properties.

Site Properties

Define SiteProperties

No built-in hoomd-rs site type has a radius field. Define a custom type to hold it along with the site’s position:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

As in Type-dependent Interactions, #[derive] the traits that are needed.

Transforming Sites

Implement Transform<SiteProperties> for the body properties type:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Site-site Interactions

The site_pair_energy method is a function only of two sites. It does not reference any particular microstate. At the same time, the interaction type must be aware of the maximum radius it can expect so that it can be used with PairwiseCutoff. You can meet these requirements by storing the maximum interaction range in a field of the site pair interaction type.

Define the Hard Shape SitePairInteraction

Define the SitePairInteraction type:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Every site-site interaction type must implement MaximumInteractionRange which sets the distance above which the interactions go to zero. You can implement this trait directly, or add the maximum_interaction_range field and #derive[(MaximumInteractionRange)] as shown here.

Implement SitePairEnergy

PairwiseCutoff uses the SitePairEnergy trait to calculate the interaction energy that each pair of sites contributes to the total. Implement the trait and compute the radius-dependent interactions:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

First, compute the distance between the two sites. Return infinity when the disks overlap and 0 when they do not.

Special Methods for Hard Shapes

Hard potentials (potentials that always result of a value of 0 or infinity) should implement two additional methods. site_pair_energy_initial should return 0 and is_only_infinite_or_zero should return true:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Note

Your simulations will run correctly without these methods (the default site_pair_energy_initial calls site_pair_energy and the default is_only_infinite_or_zero returns false). However, these implementations will make hard shape simulations execute faster. There is no need to compute the energy of the initial state before a trial move as it will always be zero for a hard potential. Even if the initial energy were infinity, a trial move with an initial infinite energy and a finite final energy would always be accepted.

Site-site Overlap Penalty

Define SitePairOverlapPenalty

To use QuickInsert and QuickCompress we need to define another site-site interaction potential. This one uses OverlapPenalty to allow partially overlapping sites during initialization:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

As with SitePairInteraction, this type also needs to have a maximum interaction range.

Implement the Overlap Penalty Computation

As in Hard Disk Self-Assembly, you can use an Expanded<OverlapPenalty> to compute the overlap penalty shifted to the surface of the disks. When the disks are polydisperse, you need to set delta to the sum of the radii unique to each pair interaction:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Body Distribution

QuickInsert randomly draws bodies from a distribution and places them in the microstate. Hard Ellipse Self-Assembly and similar tutorials use the UniformIn distribution which randomizes the body’s position (and orientation when present) but keeps all the other body and site properties fixed.

Define PolydisperseBodyDistribution

Define a custom distribution type that samples bodies whose sites have random radii:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Warning

You cannot simply draw a new random radius for each sample. If you did, the larger sites would be more likely to overlap with existing ones and QuickInsert would strongly bias toward placing smaller sites.

To ensure that the inserted sites have an unbiased distribution of radii, precompute the radii and store them in the distribution type.

Implement BodyDistribution

Implement the BodyDistribution trait:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

QuickInsert calls this sample method each time it attempts to place a new body. It increments the index argument after each successfully placed site. This implementation of sample chooses a position randomly in the simulation boundary and chooses a unique (but predetermined) radius based on index.

Implement AppendMicrostate

As in Type-dependent Interactions, implement the AppendMicrostate trait to write the custom SiteProperties type. In this case, use the particles_diameter method to write the unique diameter of each site:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Construct the Simulation Model

The new() method constructs a new simulation model:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Parameters

Assign all the model parameters in one code block:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

seed is the random number seed. Change this value to select different random distribution of radii. minimum_radius and maximum_radius set the limits of the radii distribution. initial_packing_fraction is the area of the disks divided by the area of the simulation boundary in the initial state. Choose this value so that disks can be placed easily in the microstate. During the Initialize phase, the microstate will be compressed until it reaches the packing fraction target_packing_fraction. n_disks is the number of disks to add, maximum_distance is the largest distance a translation trial move can take (initially), and macrostate holds the current temperature set point (in units of energy).

Precompute the Radii

Use the rand crate to randomly sample radii from a uniform distribution:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Place the sampled radii in a Vec.

Tip

This example uses a uniform distribution for simplicity. A normal distribution would likely be more appropriate for production use.

Particle Area

Sum the area of all sites:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

This value will later be used to compute the total area of the simulation boundary from the packing fraction.

Hamiltonian

Construct the SitePairInteraction type and use it with PairwiseCutoff to form the system’s Hamiltonian. Similarly construct the overlap penalty Hamiltonian using the SitePairOverlapPenalty implemented above:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

With a uniform distribution, the maximum interaction range between any two sites is twice the maximum radius of any individual site.

Tip

If you use a normal distribution, you need to compute this maximum.

Warning

If you set the maximum interaction range too small, hoomd-rs will miss some interactions that should be computed. hoomd-rs has no idea what your custom site_pair_energy computes, so it cannot validate or provide an error when you choose a maximum_interaction_range that is incommensurate with your implementation.

Construct the Microstate

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Place Polydisperse Bodies with QuickInsert

Construct the custom PolydisperseBodyDistribution, pass it the predetermined radii. Then make a new QuickInsert with this distribution:

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Initialization and Simulation

The remaining initialization and simulation code is very similar to that in the Hard Ellipse Self-Assembly tutorial. The only difference is that rotation moves are not present here (see the complete code below).

Execute the Simulation in Batch Mode

In a terminal, execute the following command to run the simulation in batch mode:

cargo run --release --example polydisperse-hard-disk-model

Visualize the Simulation Results

Open the generated polydisperse-hard-disk-model.gsd in Ovito or another visualization tool to see the simulation results. By default. Ovito displays the sites with the diameter set in the GSD file.

Render with Tachyon, and you should see something like: Type-dependent Interactions rendered with Ovito

Note

The diameters in the GSD file take precedence in OVITO. When they are set, there is no way to override the display radius. For example, you can no longer set the radius smaller to more easily see self-assembled crystal planes.

Conclusion

This tutorial showed you how to add a site radius field, how to use that when computing custom site-site interactions, and how to write each site’s diameter to a GSD file.

Navigate to the top of the page and refresh to see the simulation in action again. Notice how the sites have different sizes and are randomly distributed in the simulation by QuickInsert.

Complete Code

use anyhow::{Context, anyhow};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use rand::{
    SeedableRng,
    distr::{Distribution, Uniform},
    rngs::StdRng,
};

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    univariate::{Expanded, OverlapPenalty, UnivariateEnergy},
};
use hoomd_mc::{
    BodyDistribution, QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune,
};
use hoomd_microstate::{
    AppendMicrostate, Body, Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, Metric};

type PositionVector = Cartesian<2>;
type BodyProperties = Point<PositionVector>;

#[derive(Clone, Copy, Default, Position)]
struct SiteProperties {
    /// The site's position.
    position: PositionVector,
    /// The site's radius.
    radius: PositiveReal,
}

impl Transform<SiteProperties> for BodyProperties {
    fn transform(&self, site_properties: &SiteProperties) -> SiteProperties {
        SiteProperties {
            position: self.position + site_properties.position,
            ..*site_properties
        }
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairInteraction {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairInteraction {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());

        if r < a.radius.get() + b.radius.get() {
            f64::INFINITY
        } else {
            0.0
        }
    }

    fn site_pair_energy_initial(
        &self,
        _a: &SiteProperties,
        _b: &SiteProperties,
    ) -> f64 {
        0.0
    }

    fn is_only_infinite_or_zero() -> bool {
        true
    }
}

#[derive(MaximumInteractionRange)]
struct SitePairOverlapPenalty {
    maximum_interaction_range: f64,
}

impl SitePairEnergy<SiteProperties> for SitePairOverlapPenalty {
    fn site_pair_energy(&self, a: &SiteProperties, b: &SiteProperties) -> f64 {
        let r = a.position().distance(b.position());
        let pair_interaction = Expanded {
            delta: a.radius.get() + b.radius.get(),
            f: OverlapPenalty::default(),
        };
        pair_interaction.energy(r)
    }
}

struct PolydisperseBodyDistribution {
    /// Radius of each disk to insert into the microstate.
    radii: Vec<PositiveReal>,
    /// Simulation boundary.
    boundary: Periodic<Rectangle>,
}

impl BodyDistribution<Body<BodyProperties, SiteProperties>>
    for PolydisperseBodyDistribution
{
    fn sample<R: rand::Rng + ?Sized>(
        &self,
        index: usize,
        rng: &mut R,
    ) -> Body<BodyProperties, SiteProperties> {
        let properties = Point {
            position: self.boundary.sample(rng),
        };
        let sites = vec![SiteProperties {
            position: Cartesian::default(),
            radius: self.radii[index],
        }];
        Body { properties, sites }
    }
}

impl<X> AppendMicrostate<BodyProperties, SiteProperties, X, Periodic<Rectangle>>
    for HoomdGsdFile
{
    #[inline]
    fn append_microstate(
        &mut self,
        microstate: &Microstate<
            BodyProperties,
            SiteProperties,
            X,
            Periodic<Rectangle>,
        >,
    ) -> Result<hoomd_gsd::hoomd::Frame<'_>, hoomd_gsd::hoomd::AppendError>
    {
        self.append_frame(microstate.step())?
            .configuration_box(microstate.boundary().shape().to_gsd_box())?
            .configuration_dimensions(Dimensions::Two)?
            .particles_position(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.position)
                    .map(|p| [p[0], p[1], 0.0].into()),
            )?
            .particles_diameter(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.radius.get() * 2.0),
            )
    }
}

impl PolydisperseHardDiskModel {
    /// Construct a new hard disk self-assembly simulation.
    fn new() -> anyhow::Result<PolydisperseHardDiskModel> {
        let seed = 1;
        let minimum_radius = 0.1;
        let maximum_radius = 0.8;
        let initial_packing_fraction = 0.6;
        let target_packing_fraction = 0.72;
        let n_disks = 64_usize.pow(2);
        let maximum_distance = 0.07;
        let macrostate = Isothermal { temperature: 1.0 };

        let radius_distribution = Uniform::new(minimum_radius, maximum_radius)?;
        let mut rng = StdRng::seed_from_u64(seed);
        let mut radii = Vec::with_capacity(n_disks);
        for r in radius_distribution.sample_iter(&mut rng).take(n_disks) {
            radii.push(r.try_into()?);
        }

        let total_particle_area = radii.iter().fold(0.0, |total, r| {
            let circle = Circle { radius: *r };
            total + circle.volume()
        });

        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            maximum_interaction_range: maximum_radius * 2.0,
        });
        let overlap_penalty_hamiltonian =
            PairwiseCutoff(SitePairOverlapPenalty {
                maximum_interaction_range: maximum_radius * 2.0,
            });

        let initial_box_volume = total_particle_area / initial_packing_fraction;
        let initial_box_edge_length = initial_box_volume.sqrt();
        let square =
            Rectangle::with_equal_edges(initial_box_edge_length.try_into()?);
        let periodic_square =
            Periodic::new(hamiltonian.maximum_interaction_range(), square)?;

        let vec_cell = VecCell::builder()
            .nominal_search_radius(
                hamiltonian.maximum_interaction_range().try_into()?,
            )
            .build();
        let microstate = Microstate::builder()
            .seed(seed as u32)
            .boundary(periodic_square)
            .spatial_data(vec_cell)
            .try_build()?;

        let distribution = PolydisperseBodyDistribution {
            boundary: microstate.boundary().clone(),
            radii,
        };
        let quick_insert = QuickInsert::new(distribution, n_disks);

        let translate =
            Translate::with_maximum_distance(maximum_distance.try_into()?);
        let translate_sweep = Sweep(translate);

        let target_box_volume = total_particle_area / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(PolydisperseHardDiskModel {
            microstate,
            hamiltonian,
            translate_sweep,
            quick_insert,
            quick_compress,
            overlap_penalty_hamiltonian,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct PolydisperseHardDiskModel {
    /// Positions of all the bodies in the simulation.
    microstate: Microstate<
        BodyProperties,
        SiteProperties,
        VecCell<SiteKey, 2>,
        Periodic<Rectangle>,
    >,
    /// How sites interact with other sites and fields.
    hamiltonian: PairwiseCutoff<SitePairInteraction>,
    /// Trial moves to apply.
    translate_sweep: Sweep<Translate<PositionVector>>,
    /// Temperature set point.
    macrostate: Isothermal,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// Quick insert algorithm.
    quick_insert: QuickInsert<PolydisperseBodyDistribution>,
    /// The current phase of the simulation.
    /// How sites interact when inserted and compressed.
    overlap_penalty_hamiltonian: PairwiseCutoff<SitePairOverlapPenalty>,
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for PolydisperseHardDiskModel {
    /// Advance the simulation forward one step.
    fn advance(&mut self) -> anyhow::Result<()> {
        match self.phase {
            Phase::Initialize => {
                self.initialize().context("failed to initialize")?
            }
            Phase::Equilibrate => self.equilibrate(),
        }

        self.microstate.increment_step();

        Ok(())
    }

    /// Get the current simulation step.
    fn step(&self) -> u64 {
        self.microstate.step()
    }
}

impl PolydisperseHardDiskModel {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert.is_complete() {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
        }

        self.translate_sweep.apply(
            &mut self.microstate,
            &self.overlap_penalty_hamiltonian,
            &Isothermal { temperature: 1.0 },
        );

        if self.quick_compress.is_complete() {
            self.translate_sweep.tune_default(
                &self.microstate,
                &self.hamiltonian,
                &self.macrostate,
            );

            self.phase = Phase::Equilibrate;
            println!(
                "Initialization complete at step {}.",
                self.microstate.step()
            );
        }

        if self.step() >= 20_000 {
            let n = self.microstate.bodies().len();
            let target_n = self.quick_insert.target();
            let volume = self.microstate.boundary().volume();
            let target_volume = self.quick_compress.target_volume();
            return Err(anyhow!(
                "inserted {n}/{target_n} bodies and compressed to {volume} / {target_volume}"
            ));
        }

        Ok(())
    }

    fn equilibrate(&mut self) {
        self.translate_sweep.apply(
            &mut self.microstate,
            &self.hamiltonian,
            &self.macrostate,
        );
    }
}

// Remove the cfg(not(...)) line when using this code outside the hoomd-rs/examples directory.
#[cfg(not(feature = "bevy"))]
fn main() -> anyhow::Result<()> {
    use hoomd_gsd::hoomd::HoomdGsdFile;
    use hoomd_microstate::AppendMicrostate;

    let mut simulation = PolydisperseHardDiskModel::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("polydisperse-hard-disk-model.gsd")?;

    for _ in 0..100_000 {
        simulation.advance()?;

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .end()?;
        }
    }

    Ok(())
}

#[cfg(feature = "bevy")]
mod polydisperse_hard_disk_model_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use polydisperse_hard_disk_model_interactive::main;

Development of hoomd-rs is led by the Glotzer Group at the University of Michigan.

Copyright © 2024-2026 The Regents of the University of Michigan.