From 4c6e12359ef9fa62d75441a439964ccb1cc87e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Garbacz?= Date: Mon, 8 Sep 2025 18:30:27 +0200 Subject: [PATCH 1/4] Element name translations (currently added Polish and German ones) --- Nuclide/src/element.rs | 19 ++++- Nuclide/src/nuclidedata.rs | 2 +- .../src/nuclidedata/element_translation.rs | 69 +++++++++++++++++++ Nuclide/tests/element_translation.rs | 15 ++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 Nuclide/src/nuclidedata/element_translation.rs create mode 100644 Nuclide/tests/element_translation.rs diff --git a/Nuclide/src/element.rs b/Nuclide/src/element.rs index 1550ebc..a95e167 100644 --- a/Nuclide/src/element.rs +++ b/Nuclide/src/element.rs @@ -1,7 +1,8 @@ use crate::Nuclide; +use crate::nuclidedata::element_translation::{NAME_PL, NAME_DE}; use crate::traits::ChemElement; -use crate::nuclidedata::index::SYMBOL; +use crate::nuclidedata::index::{NAME, SYMBOL}; use crate::nuclidedata::elemental::*; use crate::nuclidedata::ionization::IONIZATION_ENERGIES; @@ -538,6 +539,22 @@ impl Element { SYMBOL[*self as usize - 1] } + /// Returns the element name. + pub fn element_name(&self) -> String { + NAME[self.atomic_num() as usize - 1].to_string() + } + + /// Returns the element name in a given language. Be default, or if the language code is not + /// recognized (no data exists for such a translation), the original English name is returned. + pub fn element_name_by_lang(&self, lang: &str) -> String { + let el_idx = self.atomic_num() as usize - 1; + match lang { + "pl" => NAME_PL[el_idx].to_string(), + "de" => NAME_DE[el_idx].to_string(), + _ => NAME[el_idx].to_string() + } + } + /// Fraction as measured from samples on Earth. For non-abundant elements, /// returns only the most stable isotope. pub fn abundant_nuclides(&self) -> NuclideFraction { diff --git a/Nuclide/src/nuclidedata.rs b/Nuclide/src/nuclidedata.rs index fdb70e0..34d8ca5 100644 --- a/Nuclide/src/nuclidedata.rs +++ b/Nuclide/src/nuclidedata.rs @@ -5,4 +5,4 @@ pub(crate) mod half_life; pub(crate) mod index; pub(crate) mod ionization; pub(crate) mod spinparity; - +pub(crate) mod element_translation; diff --git a/Nuclide/src/nuclidedata/element_translation.rs b/Nuclide/src/nuclidedata/element_translation.rs new file mode 100644 index 0000000..1c037e8 --- /dev/null +++ b/Nuclide/src/nuclidedata/element_translation.rs @@ -0,0 +1,69 @@ +#[rustfmt::skip] +pub(crate) const NAME_PL: [&str; 118] = [ + + "Wodór" , "Hel" , "Lit" , "Beryl" , + "Bor" , "Węgiel" , "Azot" , "Tlen" , + "Fluor" , "Neon" , "Sód" , "Magnez" , + "Glin" , "Krzem" , "Fosfor" , "Siarka" , + "Chlor" , "Argon" , "Potas" , "Wapń" , + "Skand" , "Tytan" , "Wanad" , "Chrom" , + "Mangan" , "Żelazo" , "Kobalt" , "Nikiel" , + "Miedź" , "Cynk" , "Gal" , "German" , + "Arsen" , "Selenium" , "Bromine" , "Krypton" , + "Rubid" , "Stront" , "Itr" , "Cyrkon" , + "Niob" , "Molibden" , "Technet " , "Ruten" , + "Rod" , "Pallad" , "Srebro" , "Kadm" , + "Ind" , "Cyna" , "Antymon" , "Tellur" , + "Jod" , "Ksenon" , "Cez" , "Bar" , + "Lantan" , "Cer" , "Prazeodym" , "Neodym" , + "Promet" , "Samar" , "Europ" , "Gadolin" , + "Terb" , "Dysproz" , "Holm" , "Erb" , + "Thul" , "Iterb" , "Lutet" , "Hafn" , + "Tantal" , "Wolfram" , "Ren" , "Osm " , + "Iryd" , "Platyna" , "Złoto" , "Rtęć" , + "Tal" , "Ołów" , "Bizmut" , "Polon" , + "Astat" , "Radon" , "Frans" , "Rad" , + "Aktyn" , "Tor" , "Protaktyn" , "Uran" , + "Neptun" , "Pluton" , "Ameryk" , "Kiur" , + "Berkel" , "Kaliforn" , "Einstein" , "Ferm" , + "Mendelew" , "Nobel" , "Lorens" , "Rutherford" , + "Dubn" , "Seaborg" , "Bohr" , "Has" , + "Meitner" , "Darmsztadt" , "Roentgen" , "Kopernik" , + "Nihon" , "Flerow" , "Moscow" , "Liwermor" , + "Tenes" , "Oganeson" , +]; + +#[rustfmt::skip] +pub(crate) const NAME_DE: [&str; 118] = [ + + "Hydrogen" , "Helium" , "Lithium" , "Beryllium" , + "Bor" , "Kohlenstoff" , "Stickstoff" , "Sauerstoff" , + "Fluor" , "Neon" , "Natrium" , "Magnesium" , + "Aluminium" , "Silicium" , "Phosphor" , "Schwefel" , + "Chlor" , "Argon" , "Kalium" , "Calcium" , + "Scandium" , "Titan" , "Vanadium" , "Chrom" , + "Mangan" , "Eisen" , "Cobalt" , "Nickel" , + "Kupfer" , "Zink" , "Gallium" , "Germanium" , + "Arsen" , "Selen" , "Brom" , "Krypton" , + "Rubidium" , "Strontium" , "Yttrium" , "Zirkonium" , + "Niob" , "Molybdän" , "Technetium" , "Ruthenium" , + "Rhodium" , "Palladium" , "Silber" , "Cadmium" , + "Indium" , "Zinn" , "Antimon" , "Tellur" , + "Iod" , "Xenon" , "Cesium" , "Barium" , + "Lanthan" , "Cer" , "Praseodym" , "Neodym" , + "Promethium" , "Samarium" , "Europium" , "Gadolinium" , + "Terbium" , "Dysprosium" , "Holmium" , "Erbium" , + "Thulium" , "Ytterbium" , "Lutetium" , "Hafnium" , + "Tantal" , "Wolfram" , "Rhenium" , "Osmium" , + "Iridium" , "Platin" , "Gold" , "Quecksilber" , + "Thallium" , "Blei" , "Bismut" , "Polonium" , + "Astat" , "Radon" , "Francium" , "Radium" , + "Actinium" , "Thorium" , "Protactinium" , "Uran" , + "Neptunium" , "Plutonium" , "Americium" , "Curium" , + "Berkelium" , "Californium" , "Einsteinium" , "Fermium" , + "Mendelevium" , "Nobelium" , "Lawrencium" , "Rutherfordium" , + "Dubnium" , "Seaborgium" , "Bohrium" , "Hassium" , + "Meitnerium" , "Darmstadtium" , "Roentgenium" , "Copernicium" , + "Nihonium" , "Flerovium" , "Moscovium" , "Livermorium" , + "Tennessine" , "Oganesson" +]; diff --git a/Nuclide/tests/element_translation.rs b/Nuclide/tests/element_translation.rs new file mode 100644 index 0000000..7f540d8 --- /dev/null +++ b/Nuclide/tests/element_translation.rs @@ -0,0 +1,15 @@ +#[test] +fn elements_have_valid_name_translations_in_polish() { + //a few individual checks + let sulfur = Nuclide::Element::from_protons(16); + + assert_eq!("Siarka", sulfur.element_name_by_lang("pl")); + assert_eq!("Schwefel", sulfur.element_name_by_lang("de")); + assert_eq!("Sulfur", sulfur.element_name_by_lang("??")); + + //make sure no call for element's name with "pl" or 'de' lang panics + for element in Nuclide::Element::iter() { + element.element_name_by_lang("pl"); + element.element_name_by_lang("de"); + } +} From c51053e85cd6273a244d983530cb956ff091452c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Garbacz?= Date: Tue, 9 Sep 2025 20:25:18 +0200 Subject: [PATCH 2/4] Basic periodicity (periodic table related) features for elements: group, period, electron configuration. --- Nuclide/src/lib.rs | 2 + Nuclide/src/periodicity.rs | 250 +++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 Nuclide/src/periodicity.rs diff --git a/Nuclide/src/lib.rs b/Nuclide/src/lib.rs index 894cfc5..a5786c6 100644 --- a/Nuclide/src/lib.rs +++ b/Nuclide/src/lib.rs @@ -24,6 +24,7 @@ #![allow(non_snake_case)] mod traits; +mod periodicity; pub(crate) mod nuclidedata; pub(crate) mod nstruct; pub mod decay; @@ -34,6 +35,7 @@ pub(crate) mod constant; pub(crate) mod element; pub use crate::traits::{ChemElement,Isotope}; +pub use crate::periodicity::{Periodicity, ElectronConfig, Group, Period}; pub use crate::nstruct::Nuclide; pub use crate::particle::Particle; pub use crate::element::{Element,NuclideFraction}; diff --git a/Nuclide/src/periodicity.rs b/Nuclide/src/periodicity.rs new file mode 100644 index 0000000..a36d345 --- /dev/null +++ b/Nuclide/src/periodicity.rs @@ -0,0 +1,250 @@ +use crate::element::Element; +use crate::traits::ChemElement; + +/// Enum for periodic table groups. Naming (numbering) in accordance with IUPAC convention, +/// plus assignment of f-block (with "domestic" auxiliary ordinal) for lanthanides and actinides. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Group { + GNo(usize), // group number + FB(usize), // f-block with an auxiliary ordinal (for lanthanides and actinides) +} + +use Group::*; + +#[rustfmt::skip] +const GROUP : [Group;118] = [ + + GNo(1), GNo(18), + GNo(1), GNo(2), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), + GNo(1), GNo(2), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), + GNo(1), GNo(2), GNo(3), GNo(4), GNo(5), GNo(6), GNo(7), GNo(8), GNo(9), GNo(10), GNo(11), GNo(12), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), + GNo(1), GNo(2), GNo(3), GNo(4), GNo(5), GNo(6), GNo(7), GNo(8), GNo(9), GNo(10), GNo(11), GNo(12), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), + GNo(1), GNo(2), + FB(1), FB(2), FB(3), FB(4), FB(5), FB(6), FB(7), FB(8), FB(9), FB(10), FB(11), FB(12), FB(13), FB(14), FB(15), + GNo(4), GNo(5), GNo(6), GNo(7), GNo(8), GNo(9), GNo(10), GNo(11), GNo(12), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), + GNo(1), GNo(2), + FB(1), FB(2), FB(3), FB(4), FB(5), FB(6), FB(7), FB(8), FB(9), FB(10), FB(11), FB(12), FB(13), FB(14), FB(15), + GNo(4), GNo(5), GNo(6), GNo(7), GNo(8), GNo(9), GNo(10), GNo(11), GNo(12), GNo(13), GNo(14), GNo(15), GNo(16), GNo(17), GNo(18), +]; + +/// Enum for periodic table periods with separate value for lanthanides and actinides. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Period { + PNo(usize), // period number + Lanth, // lanthanides + Actin, // actinides +} + +use Period::*; + +#[rustfmt::skip] +const PERIOD : [Period;118] = [ + + PNo(1), PNo(1), + PNo(2), PNo(2), PNo(2), PNo(2), PNo(2), PNo(2), PNo(2), PNo(2), + PNo(3), PNo(3), PNo(3), PNo(3), PNo(3), PNo(3), PNo(3), PNo(3), + PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), PNo(4), + PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), PNo(5), + PNo(6), PNo(6), + Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, Lanth, + PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), PNo(6), + PNo(7), PNo(7), + Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, Actin, + PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), PNo(7), +]; + +#[rustfmt::skip] +const SUBSHELLS : [(&str, u8);19] = [ + ("1s", 2), + ("2s", 2), ("2p", 6), + ("3s", 2), ("3p", 6), + ("4s", 2), ("3d", 10), ("4p", 6), + ("5s", 2), ("4d", 10), ("5p", 6), + ("6s", 2), ("4f", 14), ("5d", 10), ("6p", 6), + ("7s", 2), ("5f", 14), ("6d", 10), ("7p", 6), +]; + +//TODO: maybe just some kind of "diff" instead of the whole (mostly redundant) configuration, e.g.: +//(24, &[("*4s",1), ("*3d",5)]), //Cr +//... +//(57, &[("*4f",0), ("+5d",1)]), //La +//where '*' means "modified subshell", and '+' means "added subshell"? +#[rustfmt::skip] +const NON_AUFBAU_CONFIGS : [(u8, &[(&str, u8)]);19] = [ // (an, [(subshell, no_electrons)]) + (24, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",1), ("3d",5)]), //Cr + (29, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",1), ("3d",10)]), //Cu + (41, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",4)]), //Nb + (42, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",5)]), //Mo + (44, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",7)]), //Ru + (45, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",8)]), //Rh + (46, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",0), ("4d",10)]), //Pd + (47, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",10)]), //Ag + (57, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",0), ("5d",1)]), //La + (58, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",1), ("5d",1)]), //Ce + (64, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",7), ("5d",1)]), //Gd + (78, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",1), ("4f",14), ("5d",9)]), //Pt + (79, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",1), ("4f",14), ("5d",10)]), //Au + (89, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 0), ("6d",1)]), //Ac + (90, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 0), ("6d",2)]), //Th + (91, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 2), ("6d",1)]), //Pa + (92, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 3), ("6d",1)]), //U + (93, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 4), ("6d",1)]), //Np + (96, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 7), ("6d",1)]), //Cm +]; + +//TODO: noble gas notation +#[allow(dead_code)] +const NOBLE_GASES : [u8; 7] = [2, 10, 18, 36, 54, 86, 118]; + +/// Struct representing electron configuration of a nuclide. +/// Currently, it can be constructed only for Element objects. +/// Examples: +/// ``` +/// let elem = ElectronConfig::new(Element::He); +/// println!("Helium electron configuration is: {}", elem.as_el_conf_spdf_notation()) +/// ``` +/// +pub struct ElectronConfig<'a> { + subshells: Vec<(&'a str, u8)>, +} + +impl ElectronConfig<'_> { + pub fn new<'a>(element: Element) -> ElectronConfig<'a> { + // REMARK: for elements above 103 there's little or no experimental verification of the electron configurations + let mut subshells: Vec<(&str, u8)> = Vec::new(); + + // Check whether we've got "non-standard" (aufbau principle-wise) electron configuration + for exception in NON_AUFBAU_CONFIGS { + if element.protons() == exception.0 { + return ElectronConfig { subshells: exception.1.iter().map(|ss| *ss).collect() } + } + } + // no, so it's aufbau principle-conformant electron configuration, let's "calculate" + let mut no_el: i8 = element.protons() as i8; + for subshell in SUBSHELLS { + no_el = no_el - subshell.1 as i8; + if no_el <= 0 { + subshells.push((subshell.0, (subshell.1 as i8 + no_el) as u8)); + break; + } else { + subshells.push(subshell); + } + } + ElectronConfig { subshells } + } + + /// Returns electron configuration as a simple text, e.g. "1s2 2s2 2p2 " for Carbon + pub fn as_el_conf_simple_text(&self) -> String { + self.subshells.iter().map(|x| format!("{}{} ", x.0, x.1)).collect() + } + + /// Returns electron configuration in spdf notation, e.g. "1s²2s²2p²" for Carbon + pub fn as_el_conf_spdf_notation(&self) -> String { + self.subshells.iter() + .map(|x| format!("{}{}", x.0, Self::num_to_unicode_superscripts(x.1))) + .collect() + } + + /// Returns electron configuration in noble gas notation, e.g. "[He]2s²2p²" for Carbon + pub fn as_el_conf_noble_notation(&self) -> String { + todo!() + } + + fn num_to_unicode_superscripts(number: u8) -> String { + #[rustfmt::skip] + const UNICODE_SUPERSCRIPT_CHARS : [&[u8]; 10] = [ + &[0xe2, 0x81, 0xb0], //U+2070 superscript 0 + &[0xc2, 0xb9], //U+00b9 superscript 1 + &[0xc2, 0xb2], //U+00b2 superscript 2 + &[0xc2, 0xb3], //U+00b3 superscript 3 + &[0xe2, 0x81, 0xb4], //U+2074 superscript 4 + &[0xe2, 0x81, 0xb5], //U+2075 ... + &[0xe2, 0x81, 0xb6], //U+2076 ... + &[0xe2, 0x81, 0xb7], //U+2077 ... + &[0xe2, 0x81, 0xb8], //U+2078 ... + &[0xe2, 0x81, 0xb9], //U+2079 superscript 9 + ]; + number.to_string().chars().into_iter() + .map(|c| c as usize - 48) + .map(|c| str::from_utf8(UNICODE_SUPERSCRIPT_CHARS[c]).unwrap()) + .collect() + } +} + +/// Periodic table related properties and qualities +pub trait Periodicity: Clone { + /// Group (family) in the periodic table (in accordance with IUPAC numbering) + fn group_iupac(&self) -> Group; + + /// Period in the periodic table + fn period(&self) -> Period; + + /// Electron configuration in ground state + fn orbitals_gs<'a>(&self) -> ElectronConfig; +} + +impl Periodicity for Element { + fn group_iupac(&self) -> Group { + GROUP[self.atomic_num() as usize - 1] + } + + fn period(&self) -> Period { + PERIOD[self.atomic_num() as usize - 1] + } + + fn orbitals_gs<'a>(&self) -> ElectronConfig { + ElectronConfig::new(*self) + } +} + +#[cfg(test)] +mod tests { + use Element; + use super::*; + + #[test] + fn helium_periodicity() { + let elem = Element::He; + assert_eq!(GNo(18), elem.group_iupac()); + assert_eq!(PNo(1), elem.period()); + let el_conf = ElectronConfig::new(elem); + assert_eq!(el_conf.subshells, vec![("1s", 2)]); + assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 "); + assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²"); + } + + #[test] + fn argon_electron_config() { + let elem = Element::Ar; + assert_eq!(GNo(18), elem.group_iupac()); + assert_eq!(PNo(3), elem.period()); + let el_conf = ElectronConfig::new(elem); + assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6)]); + assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 "); + assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶"); + } + + #[test] + fn copper_electron_config() { + let elem = Element::Cu; + assert_eq!(GNo(11), elem.group_iupac()); + assert_eq!(PNo(4), elem.period()); + let el_conf = ElectronConfig::new(elem); + assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6), ("4s", 1), ("3d", 10)]); + assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 4s1 3d10 "); + assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶4s¹3d¹⁰"); + } + + #[test] + fn gadolinium_electron_config() { + let elem = Element::Gd; + assert_eq!(FB(8), elem.group_iupac()); + assert_eq!(Lanth, elem.period()); + let el_conf = ElectronConfig::new(elem); + assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6), ("4s", 2), ("3d", 10), + ("4p", 6), ("5s", 2), ("4d",10), ("5p", 6), ("6s", 2), ("4f", 7), ("5d", 1)]); + assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f7 5d1 "); + assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶4s²3d¹⁰4p⁶5s²4d¹⁰5p⁶6s²4f⁷5d¹"); + } + +} From c8fb21877a6dee3dd0de6d7e08e01aba1fa11b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Garbacz?= Date: Tue, 14 Oct 2025 22:37:33 +0200 Subject: [PATCH 3/4] Enum for language choice --- Nuclide/Cargo.toml | 2 ++ Nuclide/src/element.rs | 36 ++++++++++++++++++++++------ Nuclide/src/lib.rs | 2 +- Nuclide/tests/element_translation.rs | 20 +++++++++++----- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/Nuclide/Cargo.toml b/Nuclide/Cargo.toml index 30af43a..b493a24 100644 --- a/Nuclide/Cargo.toml +++ b/Nuclide/Cargo.toml @@ -14,3 +14,5 @@ readme = "README.md" [dependencies] Pion = "0.0.2" +strum = { version = "0.27.2" } +strum_macros = "0.27.2" diff --git a/Nuclide/src/element.rs b/Nuclide/src/element.rs index a95e167..a818332 100644 --- a/Nuclide/src/element.rs +++ b/Nuclide/src/element.rs @@ -1,11 +1,11 @@ - +use std::str::FromStr; use crate::Nuclide; use crate::nuclidedata::element_translation::{NAME_PL, NAME_DE}; use crate::traits::ChemElement; use crate::nuclidedata::index::{NAME, SYMBOL}; use crate::nuclidedata::elemental::*; use crate::nuclidedata::ionization::IONIZATION_ENERGIES; - +use strum_macros::EnumString; // TODO // - calculate element mass const for each element @@ -131,6 +131,20 @@ impl ChemElement for NuclideFraction { } } +/// Enum for language of choice, e.g. for elements' names. +#[derive(EnumString)] +pub enum Lang { + #[strum(serialize = "Eng", serialize = "eng", serialize = "ENG", serialize = "en", serialize = "EN")] + /// English - default. In `from_str` can be used any of the following: `Eng|eng|ENG|en|EN` + Eng, + #[strum(serialize = "De", serialize = "de", serialize = "DE")] + /// German. In `from_str` can be used any of the following: `De|de|DE` + De, + #[strum(serialize = "Pol", serialize = "pol", serialize = "POL", serialize = "pl", serialize = "PL")] + /// Polish. In `from_str` can be used any of the following: `Pol|pol|POL|pl|PL` + Pol, +} + /// Enum for general chemical element properties /// /// ``` @@ -544,17 +558,25 @@ impl Element { NAME[self.atomic_num() as usize - 1].to_string() } - /// Returns the element name in a given language. Be default, or if the language code is not - /// recognized (no data exists for such a translation), the original English name is returned. - pub fn element_name_by_lang(&self, lang: &str) -> String { + /// Returns the element name in a given language. If the language code is not recognized + /// (no data exists for such a translation), the original English name is returned. + pub fn element_name_by_lang(&self, lang: Lang) -> String { let el_idx = self.atomic_num() as usize - 1; match lang { - "pl" => NAME_PL[el_idx].to_string(), - "de" => NAME_DE[el_idx].to_string(), + Lang::De => NAME_DE[el_idx].to_string(), + Lang::Pol => NAME_PL[el_idx].to_string(), _ => NAME[el_idx].to_string() } } + /// Uses [`element_name_by_lang`](#method.element_name_by_lang) with prior conversion of the provided + /// string representation of the language into appropriate enum value. In case of unrecognized + /// (erroneous or not supported) language representation, [`Lang::En`](Lang) (English) is used. + /// Should prove useful for Nuclide's "clients" using only string representations of languages. + pub fn element_name_by_lang_str(&self, lang: &str) -> String { + self.element_name_by_lang(Lang::from_str(lang).unwrap_or(Lang::Eng)) + } + /// Fraction as measured from samples on Earth. For non-abundant elements, /// returns only the most stable isotope. pub fn abundant_nuclides(&self) -> NuclideFraction { diff --git a/Nuclide/src/lib.rs b/Nuclide/src/lib.rs index a5786c6..9e291ae 100644 --- a/Nuclide/src/lib.rs +++ b/Nuclide/src/lib.rs @@ -38,4 +38,4 @@ pub use crate::traits::{ChemElement,Isotope}; pub use crate::periodicity::{Periodicity, ElectronConfig, Group, Period}; pub use crate::nstruct::Nuclide; pub use crate::particle::Particle; -pub use crate::element::{Element,NuclideFraction}; +pub use crate::element::{Element,NuclideFraction,Lang}; diff --git a/Nuclide/tests/element_translation.rs b/Nuclide/tests/element_translation.rs index 7f540d8..2be6f77 100644 --- a/Nuclide/tests/element_translation.rs +++ b/Nuclide/tests/element_translation.rs @@ -1,15 +1,23 @@ +use Nuclide::Lang; + #[test] fn elements_have_valid_name_translations_in_polish() { //a few individual checks let sulfur = Nuclide::Element::from_protons(16); - assert_eq!("Siarka", sulfur.element_name_by_lang("pl")); - assert_eq!("Schwefel", sulfur.element_name_by_lang("de")); - assert_eq!("Sulfur", sulfur.element_name_by_lang("??")); + //using enum + assert_eq!("Siarka", sulfur.element_name_by_lang(Lang::Pol)); + assert_eq!("Schwefel", sulfur.element_name_by_lang(Lang::De)); + assert_eq!("Sulfur", sulfur.element_name_by_lang(Lang::Eng)); + + //using string representation + assert_eq!("Siarka", sulfur.element_name_by_lang_str("pl")); + assert_eq!("Schwefel", sulfur.element_name_by_lang_str("de")); + assert_eq!("Sulfur", sulfur.element_name_by_lang_str("??")); - //make sure no call for element's name with "pl" or 'de' lang panics + //make sure no call for element's name with Lang::Pol or Lang::De panics for element in Nuclide::Element::iter() { - element.element_name_by_lang("pl"); - element.element_name_by_lang("de"); + element.element_name_by_lang(Lang::Pol); + element.element_name_by_lang(Lang::De); } } From 1ea6a9ad0450b1fda71cbb389b9e748929d6199d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Garbacz?= Date: Sun, 12 Oct 2025 22:42:55 +0200 Subject: [PATCH 4/4] Electron configuration based on basic quantum properties --- Nuclide/src/lib.rs | 3 +- Nuclide/src/periodicity.rs | 332 ++++++++++++++++++++++---------- Nuclide/src/quantum_electron.rs | 87 +++++++++ 3 files changed, 318 insertions(+), 104 deletions(-) create mode 100644 Nuclide/src/quantum_electron.rs diff --git a/Nuclide/src/lib.rs b/Nuclide/src/lib.rs index 9e291ae..84a5047 100644 --- a/Nuclide/src/lib.rs +++ b/Nuclide/src/lib.rs @@ -33,8 +33,9 @@ mod rng; mod mmodel; pub(crate) mod constant; pub(crate) mod element; +pub mod quantum_electron; -pub use crate::traits::{ChemElement,Isotope}; +pub use crate::traits::{ChemElement, Isotope}; pub use crate::periodicity::{Periodicity, ElectronConfig, Group, Period}; pub use crate::nstruct::Nuclide; pub use crate::particle::Particle; diff --git a/Nuclide/src/periodicity.rs b/Nuclide/src/periodicity.rs index a36d345..9dca610 100644 --- a/Nuclide/src/periodicity.rs +++ b/Nuclide/src/periodicity.rs @@ -36,6 +36,7 @@ pub enum Period { } use Period::*; +use crate::quantum_electron::{ElectronSpin, EnergyState, Location, QuantumElectron}; #[rustfmt::skip] const PERIOD : [Period;118] = [ @@ -54,149 +55,264 @@ const PERIOD : [Period;118] = [ ]; #[rustfmt::skip] -const SUBSHELLS : [(&str, u8);19] = [ - ("1s", 2), - ("2s", 2), ("2p", 6), - ("3s", 2), ("3p", 6), - ("4s", 2), ("3d", 10), ("4p", 6), - ("5s", 2), ("4d", 10), ("5p", 6), - ("6s", 2), ("4f", 14), ("5d", 10), ("6p", 6), - ("7s", 2), ("5f", 14), ("6d", 10), ("7p", 6), -]; + const NON_AUFBAU_CONFIGS : [(u8, u8, &[(u8, u8, i8, i8)]);19] = [ // (element Z, noble-gas Z, [(n, l, mu, me)]) + (24, 18, &[(4,0,0,-1), (3,2,-2,-1), (3,2,-1,-1), (3,2,0,-1), (3,2,1,-1), (3,2,2,-1)]), //Cr -> [Ar] 4s1 3d5 + (29, 18, &[(4,0,0,-1), (3,2,-2,-1), (3,2,-1,-1), (3,2,0,-1), (3,2,1,-1), (3,2,2,-1), (3,2,-2,1), (3,2,-1,1), (3,2,0,1), (3,2,1,1), (3,2,2,1)]), //Cu -> [Ar] 4s1 3d10 + (41, 36, &[(5,0,0,-1), (4,2,-2,-1), (4,2,-1,-1), (4,2,0,-1), (4,2,1,-1)]), //Nb -> [Kr] 5s1 4d4 + (42, 36, &[(5,0,0,-1), (4,2,-2,-1), (4,2,-1,-1), (4,2,0,-1), (4,2,1,-1), (4,2,2,-1)]), //Mo -> [Kr] 5s1 4d5 + (44, 36, &[(5,0,0,-1), (4,2,-2,-1), (4,2,-1,-1), (4,2,0,-1), (4,2,1,-1), (4,2,2,-1), (4,2,-2,1), (4,2,-1,1)]), //Ru -> [Kr] 5s1 4d7 + (45, 36, &[(5,0,0,-1), (4,2,-2,-1), (4,2,-1,-1), (4,2,0,-1), (4,2,1,-1), (4,2,2,-1), (4,2,-2,1), (4,2,-1,1), (4,2,0,1)]), //Rh -> [Kr] 5s1 4d8 + (46, 36, &[(4,2,-2,-1),(4,2,-1,-1), (4,2,0,-1), (4,2,1,-1), (4,2,2,-1), (4,2,-2,1), (4,2,-1,1), (4,2,0,1), (4,2,1,1), (4,2,2,1)]), //Pd -> [Kr] 4d10 + (47, 36, &[(5,0,0,-1), (4,2,-2,-1), (4,2,-1,-1), (4,2,0,-1), (4,2,1,-1), (4,2,2,-1), (4,2,-2,1), (4,2,-1,1), (4,2,0,1), (4,2,1,1), (4,2,2,1)]), //Ag -> [Kr] 5s1 4d10 + (57, 54, &[(6,0,0,-1), (6,0,0,1), (5,2,-2,-1)]), //La -> [Xe] 6s2 5d1 + (58, 54, &[(6,0,0,-1), (6,0,0,1), (4,3,-3,-1), (5,2,-2,-1)]), //Ce -> [Xe] 6s2 4f1 5d1 + (64, 54, &[(6,0,0,-1), (6,0,0,1), (4,3,-3,-1), (4,3,-2,-1), (4,3,-1,-1), (4,3,0,-1), (4,3,1,-1), (4,3,2,-1), (4,3,3,-1), (5,2,-2,-1)]), //Gd -> [Xe] 6s2 4f7 5d1 + (78, 54, &[(6,0,0,-1), (4,3,-3,-1), (4,3,-2,-1), (4,3,-1,-1), (4,3,0,-1), (4,3,1,-1), (4,3,2,-1), (4,3,3,-1), (4,3,-3,1), (4,3,-2,1), (4,3,-1,1), (4,3,0,1), (4,3,1,1), (4,3,2,1), (4,3,3,1), (5,2,-2,-1), (5,2,-1,-1), (5,2,0,-1), (5,2,1,-1), (5,2,2,-1), (5,2,-2,1), (5,2,-1,1), (5,2,0,1), (5,2,1,1)]), //Pt -> [Xe] 6s1 4f14 5d9 + (79, 54, &[(6,0,0,-1), (4,3,-3,-1), (4,3,-2,-1), (4,3,-1,-1), (4,3,0,-1), (4,3,1,-1), (4,3,2,-1), (4,3,3,-1), (4,3,-3,1), (4,3,-2,1), (4,3,-1,1), (4,3,0,1), (4,3,1,1), (4,3,2,1), (4,3,3,1), (5,2,-2,-1), (5,2,-1,-1), (5,2,0,-1), (5,2,1,-1), (5,2,2,-1), (5,2,-2,1), (5,2,-1,1), (5,2,0,1), (5,2,1,1), (5,2,2,1)]), //Au -> [Xe] 6s1 4f14 5d10 + (89, 86, &[(7,0,0,-1), (7,0,0,1), (6,2,-2,-1)]), //Ac -> [Rn] 7s2 6d1 + (90, 86, &[(7,0,0,-1), (7,0,0,1), (6,2,-2,-1), (6,2,-1,-1)]), //Th -> [Rn] 7s2 6d2 + (91, 86, &[(7,0,0,-1), (7,0,0,1), (5,3,-3,-1), (5,3,-2,-1), (6,2,-2,-1)]), //Pa -> [Rn] 7s2 5f2 6d1 + (92, 86, &[(7,0,0,-1), (7,0,0,1), (5,3,-3,-1), (5,3,-2,-1), (5,3,-1,-1), (6,2,-2,-1)]), //U -> [Rn] 7s2 5f3 6d1 + (93, 86, &[(7,0,0,-1), (7,0,0,1), (5,3,-3,-1), (5,3,-2,-1), (5,3,-1,-1), (5,3,0,-1), (6,2,-2,-1)]), //Np -> [Rn] 7s2 5f4 6d1 + (96, 86, &[(7,0,0,-1), (7,0,0,1), (5,3,-3,-1), (5,3,-2,-1), (5,3,-1,-1), (5,3,0,-1), (5,3,1,-1), (5,3,2,-1), (5,3,3,-1), (6,2,-2,-1)]), //Cm -> [Rn] 7s2 5f7 6d1 + ]; -//TODO: maybe just some kind of "diff" instead of the whole (mostly redundant) configuration, e.g.: -//(24, &[("*4s",1), ("*3d",5)]), //Cr -//... -//(57, &[("*4f",0), ("+5d",1)]), //La -//where '*' means "modified subshell", and '+' means "added subshell"? -#[rustfmt::skip] -const NON_AUFBAU_CONFIGS : [(u8, &[(&str, u8)]);19] = [ // (an, [(subshell, no_electrons)]) - (24, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",1), ("3d",5)]), //Cr - (29, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",1), ("3d",10)]), //Cu - (41, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",4)]), //Nb - (42, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",5)]), //Mo - (44, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",7)]), //Ru - (45, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",8)]), //Rh - (46, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",0), ("4d",10)]), //Pd - (47, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",1), ("4d",10)]), //Ag - (57, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",0), ("5d",1)]), //La - (58, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",1), ("5d",1)]), //Ce - (64, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",7), ("5d",1)]), //Gd - (78, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",1), ("4f",14), ("5d",9)]), //Pt - (79, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",1), ("4f",14), ("5d",10)]), //Au - (89, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 0), ("6d",1)]), //Ac - (90, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 0), ("6d",2)]), //Th - (91, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 2), ("6d",1)]), //Pa - (92, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 3), ("6d",1)]), //U - (93, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 4), ("6d",1)]), //Np - (96, &[("1s",2), ("2s",2), ("2p",6), ("3s",2), ("3p",6), ("4s",2), ("3d",10), ("4p",6), ("5s",2), ("4d",10), ("5p",6), ("6s",2), ("4f",14), ("5d",10), ("6p",6), ("7s",2), ("5f", 7), ("6d",1)]), //Cm -]; - -//TODO: noble gas notation -#[allow(dead_code)] -const NOBLE_GASES : [u8; 7] = [2, 10, 18, 36, 54, 86, 118]; +const REQUIRED_SHELLS_FOR_TOTAL_ELECTRONS: [(u8, u8);7] = [(1, 2), (2, 10), (3, 18), (4, 36), (5, 54), (6, 86), (7, 118)]; //partitioned by noble gases /// Struct representing electron configuration of a nuclide. +/// /// Currently, it can be constructed only for Element objects. +/// In case of methods generating textual representation of the configuration, by following +/// the general convention the orbitals are not written in accordance with Madelung rule, but rather +/// in order of increasing *n* (Principal Quantum Number) and then in order of increasing *l* +/// (Orbital Angular Momentum Quantum Number, a.k.a. Azimuthal Quantum Number). +/// /// Examples: /// ``` -/// let elem = ElectronConfig::new(Element::He); +/// let elem = QuantumElectronConfig::from_element(Element::He); /// println!("Helium electron configuration is: {}", elem.as_el_conf_spdf_notation()) /// ``` /// -pub struct ElectronConfig<'a> { - subshells: Vec<(&'a str, u8)>, +pub struct ElectronConfig { + electrons: Vec, } -impl ElectronConfig<'_> { - pub fn new<'a>(element: Element) -> ElectronConfig<'a> { - // REMARK: for elements above 103 there's little or no experimental verification of the electron configurations - let mut subshells: Vec<(&str, u8)> = Vec::new(); +impl ElectronConfig { + //TODO: for isotope, ion, etc., not only for elements + /// Creates electron configuration for the given element. + /// + /// REMARK: for elements above 103 there's little or no experimental verification of the electron configurations + pub fn from_element(element: Element) -> ElectronConfig { + let mut electrons: Vec = Vec::new(); // Check whether we've got "non-standard" (aufbau principle-wise) electron configuration + // If so, re-generate configuration based on lookup table (construct for noble gas, and add outer electrons) for exception in NON_AUFBAU_CONFIGS { if element.protons() == exception.0 { - return ElectronConfig { subshells: exception.1.iter().map(|ss| *ss).collect() } + let mut electrons = ElectronConfig::from_element(Element::from_protons(exception.1)).electrons; + let mut outer_electrons: Vec = exception.2 + .iter() + .map(|x| QuantumElectron::new(x.0, x.1, x.2, + if x.3 == -1 {ElectronSpin::MinusOneHalf} else {ElectronSpin::PlusOneHalf}, + Location::Outer, EnergyState::GroundState)) + .collect(); + electrons.append(&mut outer_electrons); + + return ElectronConfig { electrons } } } // no, so it's aufbau principle-conformant electron configuration, let's "calculate" let mut no_el: i8 = element.protons() as i8; - for subshell in SUBSHELLS { - no_el = no_el - subshell.1 as i8; - if no_el <= 0 { - subshells.push((subshell.0, (subshell.1 as i8 + no_el) as u8)); - break; - } else { - subshells.push(subshell); + let shells = Self::no_of_shells(no_el as u8); + let mut subshells: Vec<(u8, u8)> = Vec::new(); + // Generate all "empty" subshells + for n in 1..=shells { + for l in 0..n { + subshells.push((n,l)); + } + } + // Sort subshells according to Madelung rule + subshells.sort_by(|a, b| (a.0 + a.1).cmp(&(b.0 + b.1)).then(a.0.cmp(&b.0))); + + 'main_loop: for ss in subshells { + let mut me = ElectronSpin::PlusOneHalf; + for _ in 0..=1 { + me.flip_flop(); + for ml in -(ss.1 as i8)..=(ss.1 as i8) { + //TODO: Location + electrons.push(QuantumElectron::new(ss.0, ss.1, ml, me.clone(), Location::Core, EnergyState::GroundState)); + no_el -= 1; + if no_el == 0 { + break 'main_loop; + } + } } } - ElectronConfig { subshells } + + ElectronConfig { electrons } + } + + /// Electron configuration as a list of consecutive electrons + pub fn as_electrons(&self) -> Vec { + self.electrons.clone() + } + + /// Electron configuration grouped into consecutive subshells (ordering with Madelung rule) + pub fn as_subshells(&self) -> Vec> { + let mut electrons = self.electrons.clone(); + electrons.sort(); + Self::get_subshells(electrons) + } + + /// Electron configuration grouped into consecutive shells + pub fn as_shells(&self) -> Vec> { + let mut electrons = self.electrons.clone(); + electrons.sort_by_key(|e| e.p_qn); + Self::get_shells(electrons) } /// Returns electron configuration as a simple text, e.g. "1s2 2s2 2p2 " for Carbon pub fn as_el_conf_simple_text(&self) -> String { - self.subshells.iter().map(|x| format!("{}{} ", x.0, x.1)).collect() + Self::grouped_electrons_to_simplified_subshells_sorted_for_printing(self.as_subshells()).iter() + .map(|x| format!("{}{} ", x.0.to_string() + x.2, x.3)) + .collect() } /// Returns electron configuration in spdf notation, e.g. "1s²2s²2p²" for Carbon pub fn as_el_conf_spdf_notation(&self) -> String { - self.subshells.iter() - .map(|x| format!("{}{}", x.0, Self::num_to_unicode_superscripts(x.1))) + Self::grouped_electrons_to_simplified_subshells_sorted_for_printing(self.as_subshells()).iter() + .map(|x| format!("{}{}", x.0.to_string() + x.2, num_to_unicode_superscripts(x.3 as u8))) .collect() } - /// Returns electron configuration in noble gas notation, e.g. "[He]2s²2p²" for Carbon - pub fn as_el_conf_noble_notation(&self) -> String { - todo!() + /// Returns electron configuration in noble gas notation, e.g. "\[He\]2s²2p²" for Carbon + pub fn as_el_conf_noble_gas_notation(&self) -> String { + let mut electrons = self.electrons.clone(); + electrons.sort(); + let partition = Self::get_config_partitioned_at_noble_gas(electrons); + let outer_spdf: String = Self::grouped_electrons_to_simplified_subshells_sorted_for_printing(Self::get_subshells(partition.1)).iter() + .map(|x| format!("{}{}", x.0.to_string() + x.2, num_to_unicode_superscripts(x.3 as u8))) + .collect(); + match partition.0 { + None => outer_spdf, + Some(x) => format!("[{}]{}", x.symbol(), outer_spdf) + } } - fn num_to_unicode_superscripts(number: u8) -> String { - #[rustfmt::skip] - const UNICODE_SUPERSCRIPT_CHARS : [&[u8]; 10] = [ - &[0xe2, 0x81, 0xb0], //U+2070 superscript 0 - &[0xc2, 0xb9], //U+00b9 superscript 1 - &[0xc2, 0xb2], //U+00b2 superscript 2 - &[0xc2, 0xb3], //U+00b3 superscript 3 - &[0xe2, 0x81, 0xb4], //U+2074 superscript 4 - &[0xe2, 0x81, 0xb5], //U+2075 ... - &[0xe2, 0x81, 0xb6], //U+2076 ... - &[0xe2, 0x81, 0xb7], //U+2077 ... - &[0xe2, 0x81, 0xb8], //U+2078 ... - &[0xe2, 0x81, 0xb9], //U+2079 superscript 9 - ]; - number.to_string().chars().into_iter() - .map(|c| c as usize - 48) - .map(|c| str::from_utf8(UNICODE_SUPERSCRIPT_CHARS[c]).unwrap()) - .collect() + fn no_of_shells(no_el: u8) -> u8 { + // Computing analytically from the number of electrons is harder than just using simple lookup table + REQUIRED_SHELLS_FOR_TOTAL_ELECTRONS.iter().filter(|n| n.1 >= no_el).min_by_key(|x| x.1).map(|n| n.0).unwrap() + } + + fn get_subshells(electrons: Vec) -> Vec> { + Self::get_partitioned_configuration( + electrons, + |e1, e2| e1.p_qn != e2.p_qn || e1.oam_qn != e2.oam_qn) + } + + fn get_shells(electrons: Vec) -> Vec> { + Self::get_partitioned_configuration( + electrons, + |e1, e2| e1.p_qn != e2.p_qn) + } + + fn get_partitioned_configuration bool>( + electrons: Vec, partition_condition: F) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut iter = electrons.iter().peekable(); + let mut subshell: Vec = Vec::new(); + while let Some(&el) = iter.next() { + subshell.push(el.clone()); + if let Some(&next_el) = iter.peek() { + if partition_condition(el, *next_el) { + result.push(subshell); + subshell = Vec::new(); + } + } + } + result.push(subshell); + result + } + + fn get_config_partitioned_at_noble_gas(mut electrons: Vec) -> (Option, Vec) { + match electrons.len() { + 1|2 => (None, electrons), + _ => { + let noble_gas_no_el = *REQUIRED_SHELLS_FOR_TOTAL_ELECTRONS.iter() + .filter(|n| n.1 < electrons.len() as u8) + .max() + .unwrap(); + electrons.sort(); + let outer_electrons = electrons.drain(noble_gas_no_el.1 as usize..).collect(); + (Some(Element::from_protons(noble_gas_no_el.1)), outer_electrons) + } + } } + + fn grouped_electrons_to_simplified_subshells_sorted_for_printing<'a>(grouped_electrons: Vec>) -> Vec<(u8, u8, &'a str, usize)> { + let mut subshells: Vec<(u8, u8, &str, usize)> = grouped_electrons.iter() + .map(|x| (x[0].p_qn, x[0].oam_qn, x[0].subshell_symbol(), x.len())) + .collect(); + subshells.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + subshells + } + } /// Periodic table related properties and qualities pub trait Periodicity: Clone { + /// Electron configuration + fn electron_config<'a>(&self) -> ElectronConfig; + /// Group (family) in the periodic table (in accordance with IUPAC numbering) fn group_iupac(&self) -> Group; /// Period in the periodic table fn period(&self) -> Period; - - /// Electron configuration in ground state - fn orbitals_gs<'a>(&self) -> ElectronConfig; } impl Periodicity for Element { + /// Electron configuration of the given element + fn electron_config<'a>(&self) -> ElectronConfig { + ElectronConfig::from_element(*self) + } + + /// Group can be easily established based on electron configuration. Still, e.g. for performance + /// reasons, it is much simpler and faster to use lookup table instead of first generating + /// the complete configuration. fn group_iupac(&self) -> Group { GROUP[self.atomic_num() as usize - 1] } + /// Period can be easily established based on electron configuration. Still, e.g. for performance + /// reasons, it is much simpler and faster to use lookup table instead of first generating + /// the complete configuration. fn period(&self) -> Period { PERIOD[self.atomic_num() as usize - 1] } +} - fn orbitals_gs<'a>(&self) -> ElectronConfig { - ElectronConfig::new(*self) - } +fn num_to_unicode_superscripts(number: u8) -> String { + #[rustfmt::skip] + const UNICODE_SUPERSCRIPT_CHARS_AS_UTF8 : [&[u8]; 10] = [ + &[0xe2, 0x81, 0xb0], //U+2070 superscript 0 + &[0xc2, 0xb9], //U+00b9 superscript 1 + &[0xc2, 0xb2], //U+00b2 superscript 2 + &[0xc2, 0xb3], //U+00b3 superscript 3 + &[0xe2, 0x81, 0xb4], //U+2074 superscript 4 + &[0xe2, 0x81, 0xb5], //U+2075 ... + &[0xe2, 0x81, 0xb6], //U+2076 ... + &[0xe2, 0x81, 0xb7], //U+2077 ... + &[0xe2, 0x81, 0xb8], //U+2078 ... + &[0xe2, 0x81, 0xb9], //U+2079 superscript 9 + ]; + number.to_string().chars().into_iter() + .map(|c| c as usize - 48) + .map(|c| str::from_utf8(UNICODE_SUPERSCRIPT_CHARS_AS_UTF8[c]).unwrap()) + .collect() } + #[cfg(test)] mod tests { use Element; @@ -207,10 +323,21 @@ mod tests { let elem = Element::He; assert_eq!(GNo(18), elem.group_iupac()); assert_eq!(PNo(1), elem.period()); - let el_conf = ElectronConfig::new(elem); - assert_eq!(el_conf.subshells, vec![("1s", 2)]); - assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 "); - assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²"); + let quant_el_conf = ElectronConfig::from_element(elem); + assert_eq!(quant_el_conf.as_el_conf_simple_text(), "1s2 "); + assert_eq!(quant_el_conf.as_el_conf_spdf_notation(), "1s²"); + assert_eq!(quant_el_conf.as_el_conf_noble_gas_notation(), "1s²"); + } + + #[test] + fn lithium_periodicity() { + let elem = Element::Li; + assert_eq!(GNo(1), elem.group_iupac()); + assert_eq!(PNo(2), elem.period()); + let quant_el_conf = ElectronConfig::from_element(elem); + assert_eq!(quant_el_conf.as_el_conf_simple_text(), "1s2 2s1 "); + assert_eq!(quant_el_conf.as_el_conf_spdf_notation(), "1s²2s¹"); + assert_eq!(quant_el_conf.as_el_conf_noble_gas_notation(), "[He]2s¹"); } #[test] @@ -218,10 +345,10 @@ mod tests { let elem = Element::Ar; assert_eq!(GNo(18), elem.group_iupac()); assert_eq!(PNo(3), elem.period()); - let el_conf = ElectronConfig::new(elem); - assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6)]); - assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 "); - assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶"); + let quant_el_conf = ElectronConfig::from_element(elem); + assert_eq!(quant_el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 "); + assert_eq!(quant_el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶"); + assert_eq!(quant_el_conf.as_el_conf_noble_gas_notation(), "[Ne]3s²3p⁶"); } #[test] @@ -229,10 +356,10 @@ mod tests { let elem = Element::Cu; assert_eq!(GNo(11), elem.group_iupac()); assert_eq!(PNo(4), elem.period()); - let el_conf = ElectronConfig::new(elem); - assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6), ("4s", 1), ("3d", 10)]); - assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 4s1 3d10 "); - assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶4s¹3d¹⁰"); + let quant_el_conf = ElectronConfig::from_element(elem); + assert_eq!(quant_el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 3d10 4s1 "); + assert_eq!(quant_el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶3d¹⁰4s¹"); + assert_eq!(quant_el_conf.as_el_conf_noble_gas_notation(), "[Ar]3d¹⁰4s¹"); } #[test] @@ -240,11 +367,10 @@ mod tests { let elem = Element::Gd; assert_eq!(FB(8), elem.group_iupac()); assert_eq!(Lanth, elem.period()); - let el_conf = ElectronConfig::new(elem); - assert_eq!(el_conf.subshells, vec![("1s", 2), ("2s", 2), ("2p", 6), ("3s", 2), ("3p", 6), ("4s", 2), ("3d", 10), - ("4p", 6), ("5s", 2), ("4d",10), ("5p", 6), ("6s", 2), ("4f", 7), ("5d", 1)]); - assert_eq!(el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f7 5d1 "); - assert_eq!(el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶4s²3d¹⁰4p⁶5s²4d¹⁰5p⁶6s²4f⁷5d¹"); + let quant_el_conf = ElectronConfig::from_element(elem); + assert_eq!(quant_el_conf.as_el_conf_simple_text(), "1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f7 5s2 5p6 5d1 6s2 "); + assert_eq!(quant_el_conf.as_el_conf_spdf_notation(), "1s²2s²2p⁶3s²3p⁶3d¹⁰4s²4p⁶4d¹⁰4f⁷5s²5p⁶5d¹6s²"); + assert_eq!(quant_el_conf.as_el_conf_noble_gas_notation(), "[Xe]4f⁷5d¹6s²"); } } diff --git a/Nuclide/src/quantum_electron.rs b/Nuclide/src/quantum_electron.rs new file mode 100644 index 0000000..8c4f1c7 --- /dev/null +++ b/Nuclide/src/quantum_electron.rs @@ -0,0 +1,87 @@ +use std::cmp::Ordering; + +const SUBSHELL_SYMBOLS : [&str;6] = ["s", "p", "d", "f", "g", "h"]; + +/// Enum for electron spin (spin quantum number) values +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum ElectronSpin { + MinusOneHalf = -1, + PlusOneHalf = 1, +} + +impl ElectronSpin { + pub fn flip_flop(&mut self) { + *self = match self { + ElectronSpin::MinusOneHalf => ElectronSpin::PlusOneHalf, + ElectronSpin::PlusOneHalf => ElectronSpin::MinusOneHalf, + } + } +} + +/// Enum for electron "location" characteristic +/// TODO: add and handle Valence +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Location { + Core, + Outer, +} + +/// Enum for electron energy state +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum EnergyState { + GroundState, + Excited(u8), +} + +/// Struct representing electron in terms of its basic quantum features (quantum numbers). +/// +/// Also, two additional properties are provided that should specify electron's location +/// (inner or outer), and its excitation state. +#[derive(Debug, Copy, Clone)] +pub struct QuantumElectron { + pub p_qn: u8, //Principal Quantum Number + pub oam_qn: u8, //Orbital Angular Momentum Quantum Number, + m_qn: i8, //Magnetic Quantum Number + spin: ElectronSpin, + loc : Location, + state: EnergyState, +} + +impl QuantumElectron { + pub fn new(p_qn: u8, oam_qn: u8, m_qn: i8, spin: ElectronSpin, loc: Location, state: EnergyState) -> QuantumElectron { + if p_qn == 0 || oam_qn >= p_qn || m_qn.abs() as u8 >= p_qn { + panic!("Invalid quantum electron"); + } + QuantumElectron { p_qn, oam_qn, m_qn, spin, loc, state } + } + + pub fn subshell_symbol(&self) -> &'static str { + SUBSHELL_SYMBOLS[self.oam_qn as usize] + } +} + +impl PartialOrd for QuantumElectron { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for QuantumElectron { + /// Default ordering in accordance with Madelung rule + fn cmp(&self, other: &Self) -> Ordering { + (self.p_qn + self.oam_qn).cmp(&(other.p_qn + other.oam_qn)) + .then(self.p_qn.cmp(&other.p_qn)) + .then(self.m_qn.cmp(&other.m_qn)) + .then(self.spin.cmp(&other.spin)) + + } +} + +impl PartialEq for QuantumElectron { + fn eq(&self, other: &Self) -> bool { + self.p_qn == other.p_qn && self.oam_qn == other.oam_qn && self.m_qn == other.m_qn + && self.spin == other.spin && self.loc == other.loc && self.state == other.state + } +} + +impl Eq for QuantumElectron {}