moyo 0.10.0

Library for Crystal Symmetry in Rust
Documentation
use serde::Serialize;

use super::layer_hall_symbol_database::{LayerHallNumber, LayerNumber};

/// Preference for the Hall setting of a layer group.
///
/// Mirrors [`super::Setting`] for the bulk space-group case: callers either
/// pin a specific [`LayerHallNumber`] or pick a per-LG default (Standard or
/// Spglib).
///
/// **Standard** is the BCS / ITE convention for layer groups, with the
/// per-system rules:
///
/// - **Monoclinic-oblique** (LGs 3-7): unique axis `c` (the aperiodic
///   axis), which is the only LG-monoclinic-oblique convention in ITE
///   and in Fu *et al.* (paper Table 5). For LGs 5 and 7 with multiple
///   ITE cell choices, Standard takes **cell choice 1** (`setting =
///   "c1"`) per de la Flor *et al.* §2 (i).
/// - **Monoclinic-rectangular** (LGs 8-18): unique axis `a` (`setting =
///   "a"`). Follows Fu *et al.* (paper Table 5) and ITE, which only
///   shows the `:a` diagram for these LGs.
/// - **Orthorhombic** (LGs 19-48): `abc` axis labelling (`setting = ""`,
///   no `b-ac` swap), the ITE default.
/// - **Centrosymmetric** with two ITE origins (LGs 52, 62, 64): **origin
///   choice 2** (inversion at the origin) per de la Flor *et al.* §2
///   (ii). ITE's other entry for each is origin choice 1.
///
/// Reference: de la Flor, Souvignier, Madariaga & Aroyo, *Acta Cryst.*
/// **A77**, 559-571 (2021), §2. For the monoclinic-rectangular axis
/// labelling -- which the BCS paper does not call out explicitly --
/// Fu *et al.* 2024 Table 5 and ITE provide the convention.
///
/// **Spglib** picks the smallest [`LayerHallNumber`] for each LG -- i.e.
/// the first row spglib's `database/layer_spg.csv` lists for that LG.
/// This matches Standard for every LG **except** the centrosymmetric
/// trio (52, 62, 64), where Spglib falls back to origin choice 1.
#[derive(Debug, Copy, Clone, PartialEq, Serialize)]
pub enum LayerSetting {
    /// A specific Hall number (1 - 116).
    HallNumber(LayerHallNumber),
    /// Smallest Hall number for each LG (origin choice 1 for the
    /// centrosymmetric LGs 52, 62, 64).
    Spglib,
    /// BCS standard / default choice (de la Flor *et al.*, *Acta Cryst.*
    /// A77, 559-571 (2021), §2) -- see the type-level docs.
    Standard,
}

impl Default for LayerSetting {
    fn default() -> Self {
        Self::Standard
    }
}

// Smallest LayerHallNumber per LayerNumber (1..=80). Generated by walking
// `LAYER_HALL_SYMBOL_DATABASE` in hall_number order and recording the first
// occurrence of each LG; the test `test_layer_spglib_hall_numbers_match_db`
// keeps this in sync.
#[rustfmt::skip]
const LAYER_SPGLIB_HALL_NUMBERS: [LayerHallNumber; 80] = [
    1,   2,   3,   4,   5,   8,   9,   12,  14,  16,
    18,  20,  22,  24,  26,  28,  30,  32,  34,  35,
    37,  38,  39,  40,  42,  43,  44,  46,  48,  50,
    52,  54,  56,  58,  60,  62,  64,  65,  67,  68,
    70,  72,  74,  76,  77,  79,  80,  81,  82,  83,
    84,  85,  87,  88,  89,  90,  91,  92,  93,  94,
    95,  96,  98,  99,  101, 102, 103, 104, 105, 106,
    107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
];

// Standard Hall numbers per LG, per BCS Subperiodic Groups paper
// (de la Flor et al., Acta Cryst. A77, 559-571 (2021), §2). Identical to
// LAYER_SPGLIB_HALL_NUMBERS except for the three centrosymmetric LGs
// 52 / 62 / 64, where Standard takes origin choice 2 (Hall numbers
// 86 / 97 / 100) over origin choice 1 (85 / 96 / 99). The cell-choice-1
// rule for LGs 5 and 7 is already implemented by smallest-Hall-per-LG
// (rows c1 < c2 < c3 in the database). See
// `test_layer_standard_hall_numbers_match_conventions`.
#[rustfmt::skip]
const LAYER_STANDARD_HALL_NUMBERS: [LayerHallNumber; 80] = [
    1,   2,   3,   4,   5,   8,   9,   12,  14,  16,
    18,  20,  22,  24,  26,  28,  30,  32,  34,  35,
    37,  38,  39,  40,  42,  43,  44,  46,  48,  50,
    52,  54,  56,  58,  60,  62,  64,  65,  67,  68,
    70,  72,  74,  76,  77,  79,  80,  81,  82,  83,
    84,  86,  87,  88,  89,  90,  91,  92,  93,  94,
    95,  97,  98,  100, 101, 102, 103, 104, 105, 106,
    107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
];

impl LayerSetting {
    /// Hall numbers to try when identifying a layer group from operations.
    /// For per-LG defaults, returns one Hall number per LG (80 entries);
    /// for [`LayerSetting::HallNumber`], returns just that one.
    pub fn hall_numbers(&self) -> Vec<LayerHallNumber> {
        match self {
            LayerSetting::HallNumber(hn) => vec![*hn],
            LayerSetting::Spglib => LAYER_SPGLIB_HALL_NUMBERS.to_vec(),
            LayerSetting::Standard => LAYER_STANDARD_HALL_NUMBERS.to_vec(),
        }
    }

    /// Return the Hall number representing layer group `number` under this
    /// setting. Returns `None` for [`LayerSetting::HallNumber`] (callers
    /// already know the Hall number).
    pub fn hall_number(&self, number: LayerNumber) -> Option<LayerHallNumber> {
        let idx = (number as usize).checked_sub(1)?;
        match self {
            LayerSetting::HallNumber(_) => None,
            LayerSetting::Spglib => LAYER_SPGLIB_HALL_NUMBERS.get(idx).copied(),
            LayerSetting::Standard => LAYER_STANDARD_HALL_NUMBERS.get(idx).copied(),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use crate::data::{iter_layer_hall_symbol_entry, layer_hall_symbol_entry};

    #[test]
    fn test_layer_spglib_hall_numbers_match_db() {
        // Spglib variant := smallest Hall number per LG.
        let mut first_seen: HashMap<LayerNumber, LayerHallNumber> = HashMap::new();
        for entry in iter_layer_hall_symbol_entry() {
            first_seen.entry(entry.number).or_insert(entry.hall_number);
        }
        for lg in 1..=80 {
            let expected = *first_seen
                .get(&lg)
                .unwrap_or_else(|| panic!("LG {} missing from database", lg));
            assert_eq!(
                LAYER_SPGLIB_HALL_NUMBERS[(lg - 1) as usize],
                expected,
                "spglib Hall number for LG {} drifted from database",
                lg
            );
        }
    }

    /// Standard differs from Spglib only at the centrosymmetric LGs 52, 62,
    /// 64, where Standard takes origin choice 2 (de la Flor *et al.* 2021,
    /// §2 (ii)).
    #[test]
    fn test_layer_standard_hall_numbers_origin_choice() {
        for &(lg, spglib_hall, standard_hall) in &[(52, 85, 86), (62, 96, 97), (64, 99, 100)] {
            assert_eq!(
                LayerSetting::Spglib.hall_number(lg).unwrap(),
                spglib_hall,
                "LG {} Spglib Hall mismatch",
                lg
            );
            assert_eq!(
                LayerSetting::Standard.hall_number(lg).unwrap(),
                standard_hall,
                "LG {} Standard Hall mismatch",
                lg
            );
            assert_eq!(
                layer_hall_symbol_entry(spglib_hall).unwrap().setting,
                "1",
                "LG {} Hall {} should be origin choice 1",
                lg,
                spglib_hall
            );
            assert_eq!(
                layer_hall_symbol_entry(standard_hall).unwrap().setting,
                "2",
                "LG {} Hall {} should be origin choice 2",
                lg,
                standard_hall
            );
        }
    }

    /// Outside the centrosymmetric origin-choice LGs (52, 62, 64), Standard
    /// and Spglib agree. Spot-check the conventions:
    ///
    /// - LGs 5 and 7 take cell choice 1 (`setting = "c1"`) per
    ///   de la Flor *et al.* 2021 §2 (i).
    /// - LGs 8-18 (monoclinic-rectangular) take the `:a` axis labelling
    ///   per Fu *et al.* 2024 Table 5 / ITE (which only shows the `:a`
    ///   diagram for these LGs).
    /// - LGs 19-48 (orthorhombic) take the `abc` (no-swap) row, the ITE
    ///   default labelling.
    #[test]
    fn test_layer_standard_hall_numbers_match_conventions() {
        for lg in 1..=80 {
            if matches!(lg, 52 | 62 | 64) {
                continue;
            }
            assert_eq!(
                LayerSetting::Standard.hall_number(lg),
                LayerSetting::Spglib.hall_number(lg),
                "Standard and Spglib disagree for non-centrosymmetric LG {}",
                lg
            );
        }
        // Cell choice 1 for the two monoclinic-oblique LGs with multiple
        // cell choices in ITE (BCS paper §2 (i)).
        for lg in [5, 7] {
            let hall = LayerSetting::Standard.hall_number(lg).unwrap();
            assert_eq!(
                layer_hall_symbol_entry(hall).unwrap().setting,
                "c1",
                "LG {} expected cell choice 1 (c1)",
                lg
            );
        }
        // Remaining monoclinic-oblique LGs (3, 4, 6) have a single Hall in
        // the database; their setting fields are "" or "c".
        for lg in [3, 4, 6] {
            let hall = LayerSetting::Standard.hall_number(lg).unwrap();
            let s = layer_hall_symbol_entry(hall).unwrap().setting;
            assert!(
                s.is_empty() || s == "c",
                "LG {} (monoclinic-oblique) setting {:?}",
                lg,
                s
            );
        }
        // Monoclinic-rectangular: `:a` axis labelling, per Fu et al. 2024
        // Table 5 / ITE (which only depicts the :a diagram for these LGs).
        for lg in 8..=18 {
            let hall = LayerSetting::Standard.hall_number(lg).unwrap();
            let s = layer_hall_symbol_entry(hall).unwrap().setting;
            assert_eq!(
                s, "a",
                "LG {} (monoclinic-rectangular) expected :a labelling",
                lg
            );
        }
        // Orthorhombic: setting "" (abc, no b-ac swap).
        for lg in 19..=48 {
            let hall = LayerSetting::Standard.hall_number(lg).unwrap();
            let s = layer_hall_symbol_entry(hall).unwrap().setting;
            assert_eq!(s, "", "LG {} (orthorhombic) expected abc setting", lg);
        }
    }

    #[test]
    fn test_hall_numbers_lengths() {
        assert_eq!(LayerSetting::Standard.hall_numbers().len(), 80);
        assert_eq!(LayerSetting::Spglib.hall_numbers().len(), 80);
        assert_eq!(LayerSetting::HallNumber(7).hall_numbers(), vec![7]);
    }

    #[test]
    fn test_hall_number_lookup() {
        assert_eq!(LayerSetting::Standard.hall_number(1), Some(1));
        assert_eq!(LayerSetting::Standard.hall_number(5), Some(5));
        assert_eq!(LayerSetting::Standard.hall_number(52), Some(86));
        assert_eq!(LayerSetting::Standard.hall_number(80), Some(116));
        assert_eq!(LayerSetting::Standard.hall_number(0), None);
        assert_eq!(LayerSetting::Standard.hall_number(81), None);
        assert_eq!(LayerSetting::Spglib.hall_number(52), Some(85));
        assert_eq!(LayerSetting::HallNumber(1).hall_number(1), None);
    }

    #[test]
    fn test_default_is_standard() {
        assert_eq!(LayerSetting::default(), LayerSetting::Standard);
    }
}