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

Type-dependent Interactions

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

Overview

Some models label each site with a site type and apply interactions as a function of the site type. For example, you can model coarse-grained phase separation with attractive A-A and B-B interactions and purely repulsive A-B interactions. This tutorial shows you how to define site types and compute type-dependent pairwise interactions.

  • Objectives:
    • Show how to use an enum to name the possible site types.
    • Define a custom site properties struct that includes the site type.
    • Show how to compute type-dependent pairwise interactions.
    • Demonstrate phase separation of A and B site types.
  • File: hoomd-rs/examples/mc-tutorial/type-dependent-interactions.rs
  • Run (interactively):
    cargo run --release --features "bevy" --example type-dependent-interactions
    
  • Run (in batch mode):
    cargo run --release --example type-dependent-interactions
    

Use Declarations

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


#[cfg(feature = "bevy")]
mod type_dependent_interactions_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use type_dependent_interactions_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 strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


#[cfg(feature = "bevy")]
mod type_dependent_interactions_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use type_dependent_interactions_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

You might be familiar with simulation tools where you assign types to sites based on a numerical index or string name. In hoomd-rs, you can assign the site type (and any other site-specific parameter(s)) using any Rust datatype.

SiteType enum

Using an enum ensures that every site always has a well-defined site type. If you fail to assign a type or set an invalid one, Rust will issue an error at compile time. In this example, SiteType enumerates all the site types:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Define SiteProperties

Previous tutorials used one of the built-in structures (Point or OrientedPoint) to represent the site properties. These types are limited as they represent only a site’s position (or position and orientation). This tutorial defines a new SiteProperties struct that gives each site a position in space and a given site type:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Initialization, MC, and MD methods operate on a generic site properties type S with trait bounds set as needed. The derive macro implements the listed traits for SiteProperties automatically. All the types listed in the above code block are required by at least one method in this example. Rust provides the Clone, Copy, Default traits (and their derive macros). hoomd_microstate defines Position and Orientation along with their corresponding derive macros.

Note

You can add any fields to your SiteProperties type and use those fields when computing interactions. position is the only required field.

Transforming Sites

Body stores each of its sites in the body frame of reference. The Transform trait (implemented for the body properties type) transforms site properties from the body frame to the system frame. You must implement Transform<SiteProperties> for the body properties type to use Microstate:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

This implementation is suitable for point bodies with point sites in Euclidean space as it transforms from one frame to the other by vector addition and copies all other fields. See the hoomd_microstate::property module documentation for code that works with oriented bodies and/or sites.

Site-site Interactions

For demonstration purposes, this tutorial shows you how to implement a coarse-grained model where A-A interactions attract via the Lennard-Jones potential, B-B interact via the sum of a power law and a Gaussian, and A-B interact with the Weeks-Chandler-Anderson potential.

Define SitePairInteraction

Define a new struct to hold the interaction parameters. Use the provided types for the LennardJones and WeeksChandlerAnderson interactions:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


#[cfg(feature = "bevy")]
mod type_dependent_interactions_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use type_dependent_interactions_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 type-dependent interactions:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

First, compute the distance between the two sites. Then use Rust’s match expression to compute the desired potential as a function of the two site types. Rust will produce a compile error should you miss one or more cases. You can compute interactions that are not included in hoomd-interaction by writing the expression directly, as demonstrated in the B-B case.

Important

Ensure that site_pair_energy(i,j) computes the same energy as site_pair_energy(j,i). Rust does not enforce this symmetry and hoomd-rs cannot detect when there is a problem.

Construct the Simulation Model

The new() method constructs a new simulation model:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Parameters

Assign all the model parameters in one code block:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

initial_packing_fraction is the volume of the disks divided by the volume 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), sigma is the disk diameter, and macrostate holds the current temperature set point (in units of energy).

Hamiltonian

Construct the SitePairInteraction type and use it with PairwiseCutoff to form the system’s Hamiltonian:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Overlap Penalty Hamiltonian

Even though SitePairInteraction treats all sites as points with well-defined potentials at all distances , it is helpful to place sites where as it can take many steps to relax high energy states. Use the hard disk OverlapPenalty as a Hamiltonian when initializing:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Construct the Boundary

Place all bodies in periodic square boundary conditions at the chosen packing fraction:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Place A and B bodies with QuickInsert

Use one QuickInsert to place half of the bodies with site type A and a second to place the remainder with site type B:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

Initialization and Simulation

The remaining initialization and simulation code is very similar to that in the Hard Ellipse Self-Assembly tutorial. The differences are that rotation moves are not present here and there are two quick_insert methods to apply instead of one (see also the complete code below).

Execute the Simulation in Batch Mode

Implement AppendMicrostate

Patchy Particle Self-assembly and previous tutorials could call: hoomd_gsd_file.append_microstate(&simulation.microstate) and it just worked. hoomd-rs has implemented the AppendMicrostate trait for typical combinations of Point and OrientedPoint site properties (in 2D and 3D) and various boundary conditions.

When you customize the site properties and/or boundary types, you also need to implement AppendMicrostate to write GSD files. Your implementation must to append a frame at the current step, transform your boundary to a GSD box definition, write particle positions projected to Cartesian 3-vectors, and can also write any other data chunks you would like in the frame:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

append_frame returns a Frame type. You write data to the current frame by adding methods to the call chain. Notice how the code above calls

.configuration_box()?
.configuration_dimensions()?
.particles_position()?...`)

The frame ends once the call chain completes and the Frame is dropped. The arguments of each method pass the data to write. For example, configuration_dimensions takes the number of dimensions (2 or 3). The particles_position method takes an IntoIterator that produces Cartesian<3> vectors. Use Microstate’s iter_sites_tag_order to ensure that all sites are written to the file in increasing tag order.

Enumerations in Rust internally number the variants starting at 0 (by default). This conveniently allows us to convert a site’s type to a 0-based type id with the cast: s.properties.site_type as u32. We also need to write the string type names to the file. Rust itself doesn’t provide any mechanism to do so. This example uses the strum crate’s #[derive(VariantNames)] macro on the SiteType type to create the SiteType::VARIANTS array of string type names.

Tip

Use a built-in implementation in hoomd-microstate/src/append.rs as a starting point when you need to implement AppendMicrostate for your custom site and/or boundary types.

Implement main()

To run the simulation, construct the TypeDependentInteractions simulation model. Then call advance() many times:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

See Applying Interactions for a step-by-step explanation of this code.

Note

This main() function runs in batch mode. There is a different main() (not shown here) used in the interactive example. The interactive example does not write the GSD file.

Log to the GSD File

To log custom quantities to the GSD file, add .log_scalar, .log_scalars and/or .log_arrays to the call chain after .append_microstate:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

The given quantities are stored in the frame alongside the particle properties. Log names that start with “particles/” store per-particle values. This tutorial logs the energy each site contributes to the total computed by the site_energy helper method:

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


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

The method computes the negative of the energy delta that results when removing the site’s parent body. The bodies in this tutorial are all single-site bodies, so this is equivalent to the site’s contribution. You can use any of the APIs in hoomd-rs to compute and log any site-specific quantity you are interested in.

Note

Use GSD to log per-particle quantities. Parquet and other column-oriented formats cannot easily store thousands of scalar columns.

Prefer parquet when logging a few scalar quantities over large numbers of frames. GSD uses 32 bytes of overhead per data chunk per frame, so a GSD file with only scalar log quantities will be about 5 times larger than an equivalent Parquet file.

Run the Simulation

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

cargo run --release --example type-dependent-interactions

Visualize the Simulation Results

Open the generated type-dependent-interactions.gsd in Ovito or another visualization tool to see the simulation results. By default. Ovito displays the A particles with an appropriate size, but the B particles are too large. To fix this and color the sites by the logged site_energy quantity, import this modifier snippet (use the copy button to copy the entire text to your clipboard):

{"description": "OVITO Modifier Snippet: Edit types (Particle Type) | Color coding (site_energy)", "payload": "AAAHinjafRTLThNR9LS0pS2FykOQRwxqTNyIIrJiAQKNYUFAwC7YkMvMZTownWnmAcGY6ELjwo3xB/wA+QCWbtzAzi/AjQtd6AeY6DkzZ9pbGJjkzL33vN/lo6+fi39PNgGCOQDII/TBClRhCTbwHIdVcMFBbAdCGaKvBFMwAZMwjf+HjEsjpBAy0ZH+wO/bEQ5GEAbBAxN8kLCFYCO4YMChIh2aCS8ZBqJkybPZf4/fGbJUxXuO3l/uvX91/7RwChe/To4kFcsivGFdL/ksEX7VdRrS9cmDCWiAQH989FADC33z4AFG344bx6wcIqdEiUcXqB6M3/oY5uxyqfgrkG+xg0VCpObufprRN8eO8NFFXqaA/Uyn6NrdxPXEsp1cFJRtab7GiIEXZxGil1OZDYuLYSuG+sN3S3ggTPVZ8329aX4QYTg1+yfCz/44fvbr7c3j+ETUEDkU1h7m8X8DYYYtT3HSKYTCqnB9U7OkF3bFVbkaJjVXWBzhXogMjrIxcmGsSZlsxUZhpH8zR4/nBK4mlQ6IeCijIvCdJ/pu4PlrwjakSuvek7KxLi2p+aZjt1E0x3Lcp67QTWn7oLQfnXlp61VhBW3Kip6PuWhDU/93Sd30pb5x2AhzFNY8x7RL3Ka4h7SarJuasCqWrKMLW1teTejOATdRyFOqC89LIgwjqiGfe3JZerUFiiSJ607INS+0vR2hyYXAskzbqNhi25J6Ev9ozTRqFoK/ToIV3ZCJ1rtDvUmU3n39YA1TGiTK9biXksrnsqFmKkNZUBGFphUVm3PbUFSCskwONtdsgCRKyRZ1mSjiHNjSPV9MomRDZXh5HWMypERlKdhBXbqmtqSre69o6hituWNKV93M09xHNCT9YX0XHB2Lt4x/5s0zC23sEYWlarqmbnpKZ2eZk+azt4LNGraqoqrADLRNSvG8E1PkeqIniv4is9By6uL6sXQnk2iPFReFL1a2d3EWFSFaCn2LOKGG8NsDjL2iBZlX8LEk7cnCmtzZEK4h/dY8whzPa4ozuslZTfP7hBumg9/fePAzPOr7XIifvJmI/J3XPsV0k3j/A200N3Q="}

Alternately, you can:

  1. Add an Edit types modification to the Ovito pipeline.
  2. Select type B.
  3. Set the Display radius to 0.5.
  4. Add a Color coding modification to the pipeline.
  5. Set the site_energy Input property and choose the Viridis Color gradient

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

Conclusion

This tutorial showed you how to implement custom site properties and site-site interaction types. Specifically, it demonstrated the addition of a site type field and showed how you can use that when computing the interaction energies.

Navigate to the top of the page and refresh to see the simulation in action again. Notice that the sites quickly phase separate. Wait long enough and the system should form two stripes. Due to the differently shaped potentials, the A and B domains are distinct. The A sites form a hexagonal solid and B form a lower density fluid.

Complete Code

use anyhow::{Context, anyhow};
use strum::VariantNames;
use strum_macros::VariantNames;

use hoomd_geometry::{
    Volume,
    shape::{Circle, Rectangle},
};
use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
use hoomd_interaction::{
    MaximumInteractionRange, PairwiseCutoff, SitePairEnergy,
    pairwise::Isotropic,
    univariate::{
        Expanded, LennardJones, OverlapPenalty, UnivariateEnergy,
        WeeksChandlerAnderson,
    },
};
use hoomd_mc::{
    QuickCompress, QuickInsert, Sweep, Translate, Trial, Tune, UniformIn,
};
use hoomd_microstate::{AppendMicrostate, Site};
use hoomd_microstate::{
    Microstate, SiteKey, Transform,
    boundary::Periodic,
    property::{Point, Position},
};
use hoomd_simulation::{Simulation, macrostate::Isothermal};
use hoomd_spatial::VecCell;
use hoomd_vector::{Cartesian, Metric};

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

#[derive(Clone, Copy, Default, PartialEq, VariantNames)]
enum SiteType {
    #[default]
    A,
    B,
}

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

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 {
    lj_aa: LennardJones<12, 6>,
    wca_ab: WeeksChandlerAnderson,
    maximum_interaction_range: f64,
}

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

        match (site_properties_i.site_type, site_properties_j.site_type) {
            (SiteType::A, SiteType::A) => self.lj_aa.energy(r),
            (SiteType::A, SiteType::B) | (SiteType::B, SiteType::A) => {
                self.wca_ab.energy(r)
            }
            (SiteType::B, SiteType::B) => {
                1.0 / r.powi(12) - f64::exp(-1.0 / 2.0 * r.powi(2))
            }
        }
    }
}

impl TypeDependentInteractions {
    /// Construct a new type-dependent interactions simulation.
    fn new() -> anyhow::Result<TypeDependentInteractions> {
        let initial_packing_fraction = 0.3;
        let target_packing_fraction = 0.5;
        let n_disks = 512;
        let maximum_distance = 0.07;
        let sigma = 1.0;
        let macrostate = Isothermal { temperature: 1.0 };

        let lj_aa = LennardJones {
            epsilon: 2.0,
            sigma: 1.0,
        };
        let wca_ab = WeeksChandlerAnderson {
            epsilon: 1.0,
            sigma: 1.0,
        };
        let hamiltonian = PairwiseCutoff(SitePairInteraction {
            lj_aa,
            wca_ab,
            maximum_interaction_range: 2.5,
        });

        let overlap_penalty = Isotropic {
            interaction: Expanded {
                delta: sigma,
                f: OverlapPenalty::default(),
            },
            r_cut: sigma,
        };

        let overlap_penalty_hamiltonian = PairwiseCutoff(overlap_penalty);

        let circle = Circle {
            radius: (sigma / 2.0).try_into()?,
        };
        let initial_box_volume =
            n_disks as f64 * circle.volume() / 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 distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::A,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_a = QuickInsert::new(distribution, n_disks / 2);

        let distribution = UniformIn {
            boundary: periodic_square.clone(),
            template_sites: vec![SiteProperties {
                site_type: SiteType::B,
                ..SiteProperties::default()
            }],
        };
        let quick_insert_b =
            QuickInsert::new(distribution, n_disks - quick_insert_a.target());

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

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

        let target_box_volume =
            n_disks as f64 * circle.volume() / target_packing_fraction;
        let quick_compress =
            QuickCompress::with_target_volume(target_box_volume.try_into()?);

        Ok(TypeDependentInteractions {
            microstate,
            overlap_penalty_hamiltonian,
            hamiltonian,
            translate_sweep,
            quick_insert_a,
            quick_insert_b,
            quick_compress,
            macrostate,
            phase: Phase::Initialize,
        })
    }
}

#[cfg_attr(feature = "bevy", derive(Resource))]
struct TypeDependentInteractions {
    /// 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 insert algorithm for A bodies.
    quick_insert_a: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick insert algorithm for B bodies.
    quick_insert_b: QuickInsert<UniformIn<SiteProperties, Periodic<Rectangle>>>,
    /// Quick compress algorithm
    quick_compress: QuickCompress<Periodic<Rectangle>>,
    /// How sites interact during compression.
    overlap_penalty_hamiltonian:
        PairwiseCutoff<Isotropic<Expanded<OverlapPenalty>>>,
    /// The current phase of the simulation.
    phase: Phase,
}

enum Phase {
    Initialize,
    Equilibrate,
}

impl Simulation for TypeDependentInteractions {
    /// 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 TypeDependentInteractions {
    fn initialize(&mut self) -> anyhow::Result<()> {
        if self.quick_insert_a.is_complete()
            && self.quick_insert_b.is_complete()
        {
            self.quick_compress.apply(
                &mut self.microstate,
                &self.overlap_penalty_hamiltonian,
                |_| true,
            );
        } else {
            self.quick_insert_a
                .apply(&mut self.microstate, &self.overlap_penalty_hamiltonian);
            self.quick_insert_b
                .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_a.target() + self.quick_insert_b.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,
        );
    }
}

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_type_id(
                microstate
                    .iter_sites_tag_order()
                    .map(|s| s.properties.site_type as u32),
            )?
            .particles_types(SiteType::VARIANTS.iter().copied())
    }
}

// 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_interaction::TotalEnergy;

    let mut simulation = TypeDependentInteractions::new()?;
    let mut hoomd_gsd_file =
        HoomdGsdFile::create("type-dependent-interactions.gsd")?;

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

        if simulation.step().is_multiple_of(10_000) {
            hoomd_gsd_file
                .append_microstate(&simulation.microstate)?
                .log_scalar(
                    "potential_energy",
                    simulation.hamiltonian.total_energy(&simulation.microstate),
                )?
                .log_scalars(
                    "particles/site_energy",
                    simulation
                        .microstate
                        .iter_sites_tag_order()
                        .map(|s| site_energy(&simulation, s)),
                )?
                .end()?;
        }
    }

    Ok(())
}

#[allow(dead_code, reason = "site_energy is used in the batch mode main()")]
fn site_energy(
    simulation: &TypeDependentInteractions,
    site: &Site<SiteProperties>,
) -> f64 {
    use hoomd_interaction::DeltaEnergyRemove;

    let body_index = simulation.microstate.body_indices()[site.body_tag]
        .expect("site's parent body should be in the microstate");

    -simulation
        .hamiltonian
        .delta_energy_remove(&simulation.microstate, body_index)
}


#[cfg(feature = "bevy")]
mod type_dependent_interactions_interactive;
#[cfg(feature = "bevy")]
use bevy::prelude::Resource;
#[cfg(feature = "bevy")]
use type_dependent_interactions_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.