Browse Source

Add element(…) function for retrieving atomic properties

David Peter 1 year ago
parent
commit
8359b9d85f

+ 10 - 0
Cargo.lock

@@ -785,6 +785,15 @@ version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
 
+[[package]]
+name = "mendeleev"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b27414e33dd1f1d774e1a134bca391b5bbd58c60810253e0a6bea7258064dcf"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "nibble_vec"
 version = "0.1.0"
@@ -880,6 +889,7 @@ dependencies = [
  "insta",
  "itertools 0.12.0",
  "libc",
+ "mendeleev",
  "num-format",
  "num-integer",
  "num-rational",

+ 8 - 1
book/src/list-functions.md

@@ -101,7 +101,7 @@ fn sphere_area<L>(radius: L) -> L^2
 fn sphere_volume<L>(radius: L) -> L^3
 ```
 
-### Random sampling 
+### Random sampling
 
 ```nbt
 fn random() -> Scalar
@@ -138,6 +138,13 @@ fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature
 fn fahrenheit(t_kelvin: Temperature) -> Scalar
 ```
 
+## Chemistry
+
+```nbt
+# Get properties of a chemical element by its symbol or name (case-insensitive).
+fn element(pattern: String) -> ChemicalElement
+```
+
 ## Strings
 
 ```nbt

+ 48 - 0
examples/chemistry_tests.nbt

@@ -0,0 +1,48 @@
+# Sources for the hard coded data below:
+# - https://en.wikipedia.org/wiki/Tungsten
+# - https://en.wikipedia.org/wiki/Chlorine
+# - https://en.wikipedia.org/wiki/Rubidium
+# - https://en.wikipedia.org/wiki/Electron_affinity_(data_page)
+# - https://en.wikipedia.org/wiki/Ionization_energies_of_the_elements_(data_page)
+# - https://en.wikipedia.org/wiki/Enthalpy_of_vaporization
+
+let w = element("tungsten")
+assert(w.symbol == "W")
+assert(w.name == "Tungsten")
+assert_eq(w.atomic_number, 74)
+assert_eq(w.group, 6)
+assert_eq(w.period, 6)
+assert_eq(w.melting_point, 3695 K, 20 K)
+# assert_eq(w.boiling_point, 6203 K, 20 K)     # <-- this seems to be wrong in the `mendeleev` crate
+assert_eq(w.density, 19.25 g/cm³, 0.1 g/cm³)
+assert_eq(w.electron_affinity, 0.816 eV, 0.01 eV)
+assert_eq(w.ionization_energy, 7.8640 eV, 0.2 eV)
+assert_eq(w.vaporization_heat, 807 kJ/mol, 20 kJ/mol)
+
+
+let chl = element("chlorine")
+assert(chl.symbol == "Cl")
+assert(chl.name == "Chlorine")
+assert_eq(chl.atomic_number, 17)
+assert_eq(chl.group, 17)
+assert_eq(chl.period, 3)
+assert_eq(chl.melting_point, 171.6 K, 1 K)
+assert_eq(chl.boiling_point, 239.11 K, 1 K)
+assert_eq(chl.density, 3.2 g/L, 0.1 g/L)
+assert_eq(chl.electron_affinity, 3.612 eV, 0.01 eV)
+assert_eq(chl.ionization_energy, 12.96764 eV, 0.01 eV)
+assert_eq(chl.vaporization_heat, 20.4 kJ/mol, 0.1 kJ/mol)
+
+
+let rb = element("rubidium")
+assert(rb.symbol == "Rb")
+assert(rb.name == "Rubidium")
+assert_eq(rb.atomic_number, 37)
+assert_eq(rb.group, 1)
+assert_eq(rb.period, 5)
+assert_eq(rb.melting_point, 312.45 K, 0.3 K)
+assert_eq(rb.boiling_point, 961 K, 0.3 K)
+assert_eq(rb.density, 1.534 g/cm³, 0.01 g/cm³)
+assert_eq(rb.electron_affinity, 0.485 eV, 0.02 eV)
+assert_eq(rb.ionization_energy, 4.1771 eV, 0.01 eV)
+assert_eq(rb.vaporization_heat, 75.8 kJ/mol, 0.1 kJ/mol)

+ 1 - 1
numbat-cli/tests/integration.rs

@@ -102,7 +102,7 @@ fn print_calls() {
         .assert()
         .success()
         .stdout(predicates::str::contains(
-            "1 \n2 m\nhello world\npi = 3.14159\n1 + 2 = 3\n",
+            "1\n2 m\nhello world\npi = 3.14159\n1 + 2 = 3\n",
         ));
 }
 

+ 30 - 0
numbat-wasm/Cargo.lock

@@ -337,6 +337,15 @@ version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
 
+[[package]]
+name = "mendeleev"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b27414e33dd1f1d774e1a134bca391b5bbd58c60810253e0a6bea7258064dcf"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.4"
@@ -402,6 +411,7 @@ dependencies = [
  "indexmap",
  "itertools",
  "libc",
+ "mendeleev",
  "num-format",
  "num-integer",
  "num-rational",
@@ -676,6 +686,26 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
 
+[[package]]
+name = "serde"
+version = "1.0.202"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.202"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "sha2"
 version = "0.10.8"

+ 1 - 0
numbat/Cargo.toml

@@ -37,6 +37,7 @@ html-escape = { version = "0.2.13", optional = true }
 rand = "0.8.5"
 strfmt = "0.2.4"
 indexmap = "2.2.6"
+mendeleev = "0.8.0"
 
 [features]
 default = ["fetch-exchangerates"]

+ 53 - 0
numbat/modules/chemistry/elements.nbt

@@ -0,0 +1,53 @@
+use units::si
+
+struct _ChemicalElementRaw {
+    symbol: String,
+    name: String,
+    atomic_number: Scalar,
+    group: Scalar,
+    group_name: String,
+    period: Scalar,
+    melting_point_kelvin: Scalar,
+    boiling_point_kelvin: Scalar,
+    density_gram_per_cm3: Scalar,
+    electron_affinity_electronvolt: Scalar,
+    ionization_energy_electronvolt: Scalar,
+    vaporization_heat_kilojoule_per_mole: Scalar,
+}
+
+fn _get_chemical_element_data_raw(pattern: String) -> _ChemicalElementRaw
+
+struct ChemicalElement {
+    symbol: String,
+    name: String,
+    atomic_number: Scalar,
+    group: Scalar,
+    group_name: String,
+    period: Scalar,
+    melting_point: Temperature,
+    boiling_point: Temperature,
+    density: MassDensity,
+    electron_affinity: Energy,
+    ionization_energy: Energy,
+    vaporization_heat: MolarEnthalpyOfVaporization,
+}
+
+fn _convert_from_raw(raw: _ChemicalElementRaw) -> ChemicalElement =
+    ChemicalElement {
+        symbol: raw.symbol,
+        name: raw.name,
+        atomic_number: raw.atomic_number,
+        group: raw.group,
+        group_name: raw.group_name,
+        period: raw.period,
+        melting_point: raw.melting_point_kelvin * K,
+        boiling_point: raw.boiling_point_kelvin * K,
+        density: raw.density_gram_per_cm3 * g/cm³,
+        electron_affinity: raw.electron_affinity_electronvolt * eV,
+        ionization_energy: raw.ionization_energy_electronvolt * eV,
+        vaporization_heat: raw.vaporization_heat_kilojoule_per_mole * kJ/mol,
+    }
+
+# Get properties of a chemical element by its symbol or name (case-insensitive).
+fn element(pattern: String) -> ChemicalElement =
+    _convert_from_raw(_get_chemical_element_data_raw(pattern))

+ 1 - 0
numbat/modules/core/dimensions.nbt

@@ -67,6 +67,7 @@ dimension CatalyticActivity = AmountOfSubstance / Time
 dimension Molarity = AmountOfSubstance / Volume
 dimension Molality = AmountOfSubstance / Mass
 dimension ChemicalPotential = Energy / AmountOfSubstance
+dimension MolarEnthalpyOfVaporization = Energy / AmountOfSubstance
 dimension MolarHeatCapacity = HeatCapacity / AmountOfSubstance
 
 dimension LuminousIntensity

+ 2 - 0
numbat/modules/prelude.nbt

@@ -30,5 +30,7 @@ use units::placeholder
 use physics::constants
 use physics::temperature_conversion
 
+use chemistry::elements
+
 use datetime::functions
 use datetime::human

+ 135 - 0
numbat/src/ffi.rs

@@ -436,6 +436,15 @@ pub(crate) fn functions() -> &'static HashMap<String, ForeignFunction> {
             },
         );
 
+        m.insert(
+            "_get_chemical_element_data_raw".to_string(),
+            ForeignFunction {
+                name: "_get_chemical_element_data_raw".into(),
+                arity: 1..=1,
+                callable: Callable::Function(Box::new(_get_chemical_element_data_raw)),
+            },
+        );
+
         m
     })
 }
@@ -950,3 +959,129 @@ fn random(args: &[Value]) -> Result<Value> {
 
     Ok(Value::Quantity(Quantity::from_scalar(output)))
 }
+
+fn _get_chemical_element_data_raw(args: &[Value]) -> Result<Value> {
+    use crate::span::{SourceCodePositition, Span};
+    use crate::typed_ast::StructInfo;
+    use crate::typed_ast::Type;
+    use crate::BaseRepresentation;
+    use indexmap::IndexMap;
+    use mendeleev::{Electronvolt, GramPerCubicCentimeter, Kelvin, KiloJoulePerMole};
+    use std::sync::Arc;
+
+    assert!(args.len() == 1);
+
+    let pattern = args[0].unsafe_as_string().to_lowercase();
+
+    if let Some(element) = mendeleev::Element::list()
+        .iter()
+        .find(|e| e.name().to_lowercase() == pattern || e.symbol().to_lowercase() == pattern)
+    {
+        let unknown_span = Span {
+            start: SourceCodePositition::start(),
+            end: SourceCodePositition::start(),
+            code_source_id: 0,
+        };
+
+        let type_scalar = Type::Dimension(BaseRepresentation::unity());
+
+        let mut fields: IndexMap<String, (Span, Type)> = IndexMap::new();
+        fields.insert("symbol".to_string(), (unknown_span, Type::String));
+        fields.insert("name".to_string(), (unknown_span, Type::String));
+        fields.insert(
+            "atomic_number".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert("group".to_string(), (unknown_span, type_scalar.clone()));
+        fields.insert("group_name".to_string(), (unknown_span, Type::String));
+        fields.insert("period".to_string(), (unknown_span, type_scalar.clone()));
+        fields.insert(
+            "melting_point_kelvin".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert(
+            "boiling_point_kelvin".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert(
+            "density_gram_per_cm3".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert(
+            "electron_affinity_electronvolt".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert(
+            "ionization_energy_electronvolt".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+        fields.insert(
+            "vaporization_heat_kilojoule_per_mole".to_string(),
+            (unknown_span, type_scalar.clone()),
+        );
+
+        let info = StructInfo {
+            name: "_ChemicalElementRaw".to_string(),
+            definition_span: unknown_span,
+            fields,
+        };
+        Ok(Value::StructInstance(
+            Arc::new(info),
+            vec![
+                Value::String(element.symbol().into()),
+                Value::String(element.name().into()),
+                Value::Quantity(Quantity::from_scalar(element.atomic_number() as f64)),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .group()
+                        .map_or(f64::NAN, |g| g.group_number() as f64),
+                )),
+                Value::String(
+                    element
+                        .group()
+                        .map(|g| g.group_name().unwrap_or("unknown").into())
+                        .unwrap_or("unknown".into()),
+                ),
+                Value::Quantity(Quantity::from_scalar(element.period() as f64)),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .melting_point()
+                        .map(|Kelvin(k)| k)
+                        .unwrap_or(f64::NAN),
+                )),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .boiling_point()
+                        .map(|Kelvin(k)| k)
+                        .unwrap_or(f64::NAN),
+                )),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .density()
+                        .map(|GramPerCubicCentimeter(d)| d)
+                        .unwrap_or(f64::NAN),
+                )),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .electron_affinity()
+                        .map(|Electronvolt(e)| e)
+                        .unwrap_or(f64::NAN),
+                )),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .ionization_energy()
+                        .map(|Electronvolt(e)| e)
+                        .unwrap_or(f64::NAN),
+                )),
+                Value::Quantity(Quantity::from_scalar(
+                    element
+                        .evaporation_heat()
+                        .map(|KiloJoulePerMole(e)| e)
+                        .unwrap_or(f64::NAN),
+                )),
+            ],
+        ))
+    } else {
+        Err(RuntimeError::ChemicalElementNotFound(pattern))
+    }
+}

+ 3 - 0
numbat/src/interpreter.rs

@@ -53,6 +53,9 @@ pub enum RuntimeError {
 
     #[error("Incorrect type for format specifiers: {0}")]
     InvalidTypeForFormatSpecifiers(String),
+
+    #[error("Chemical element not found: {0}")]
+    ChemicalElementNotFound(String),
 }
 
 #[derive(Debug, PartialEq, Eq)]

+ 1 - 1
numbat/src/quantity.rs

@@ -325,7 +325,7 @@ impl PrettyPrint for Quantity {
         let unit_str = format!("{}", self.unit());
 
         markup::value(formatted_number)
-            + if unit_str == "°" {
+            + if unit_str == "°" || unit_str.is_empty() {
                 markup::empty()
             } else {
                 markup::space()