JASory / Iridium

Atomic Physics Library
https://rust-cas.org
MIT License
19 stars 5 forks source link

Interest in an `Element` class #8

Closed jan-grimo closed 9 months ago

jan-grimo commented 1 year ago

I'd like to expand the available functionality in this library by an Element class that averages over isotopes so that I can use this library for chemical molecules, where atoms are most commonly represented as 'averages' over their isotopes.

This library already stores a lot of chemically relevant properties, such as electron affinity, ionization energies, electronegativities, etc. which are dependent on the atomic number only. I am aware I could write my own abstraction layer over the available data, but I would prefer to expand the scope of this library a little, so that the data is centralized and accessible for others.

Are you open to receiving a PR adding an Element class implementing some or all of Atom?

Open questions

JASory commented 11 months ago

Implementing Atom for an Element type doesn't really make sense as much of the methods are only applicable to atoms with a discrete number of nucleons. An alternative would be to create a base Element trait that implements elemental methods, and then have Atom as a supertrait.

Based on this issue and the discussion of this library at Terrapower, I am considering a sort of ConstructedNuclide structure, that allows user defined values to override the standard look-up values or even create new nuclides (currently the create method predicts the mass and binding energy given certain nucleons, so this would be moved from the Nuclide struct to ConstructedNuclide)

An example below

struct CNuclide{
// Atomic Mass
 am: Option<f64>,
// Proton
 p: Option<u64>,
// Neutron
 n: Option<u64>,
// many others
}

impl CNuclide{
// initialise all with None (except probably proton count) which means it refers to Nuclide's lookup tables for each None value
  fn new() -> Self{
    Self{am: None, p: None,n: None}
 }
  // Switch am to a Some value
  fn set_amu(&mut self,x: f64);
  // Atom trait method
fn am(&self) -> f64{
   match self.am{
     Some(x) => x,
    None => // look-up am for the proton neutron combination
   }
}

I think this would alleviate any need for averaging and isotope abundancies, since one could define a typical hydrogen nuclide like this.

const HYDROGEN : CNuclide = CNuclide{am: Some(1.00794), p: Some(1),n: Some(0)};

One could possibly also use floating-point averages of nucleons although I'm not sure of the utility of that. (I haven't fully evaluated how to implement Atom completely and accurately for this Constructed Nuclide, nuclear decay seems like it would be fairly difficult to implement if you wanted to model it over average nuclides).

I would be willing to implement elemental constant nuclides like this since it is simply taking a logical extension of the library, which already supports some theoretical nuclides via create.

JASory commented 11 months ago

I would like some clarity on what specifically the Element class would provide. Is isotope abundancies simply to calculate the atomic mass averages, are averages of nuclear decay and nucleon count needed as well? Would the previously described Constructed Nuclide, address all the interests?

As for the others, I will have to review and decide on a more consistent api. (This was my first serious software project so it's full of poor decisions that I haven't fully ironed out).

jan-grimo commented 11 months ago

Is isotope abundancies simply to calculate the atomic mass averages, are averages of nuclear decay and nucleon count needed as well?

A) Generally, I would think an Element struct would ideally look-up its mass instead of calculating it from an abundance-weighted average of it's isotopes. The manner in which it's implemented is not important to me, but to avoid data duplication, it could certainly be implemented using const functions, using new abundance information and the already available isotope masses.

B) I would like to be able to represent atoms in chemical molecules either as a particular isotope or as an 'average' over the natural abundancies, an element, i.e. specifically as:

enum Atom {
    Element(nuclide::Element),
    Isotope(nuclide::Isotope)
}

Some interface that abstracts over shared properties of the two concepts in this library would therefore be very useful for me. While the decay of these 'average' atoms over time could in principle be interesting in some rare cases, the chemistry I'm familiar with is only concerned with nuclear decay in the particular cases where molecules are prepared with a specific isotope in a specific place. So I would say that although nuclear decay for elements could be considered well-defined since all you would have to do is separate the element into its isotope fractions by abundance and then consider nuclear decay separately for all of them, I think until someone else comes along clamoring for it, we could judge it out-of-scope, either not implementing a hypothetical Decay trait or by returning Nones from partial functions of a broader interface.

Would the previously described Constructed Nuclide, address all the interests?

Your proposed ConstructedNuclide struct strikes me as odd: A struct full of Options seems to me more of an indication that the wrong abstraction has been chosen to unify multiple slightly different things into the same interface. From what I can tell, the library currently uses tabular information of a particular kind such as electronegativity where some indexing identifier of an isotope or element can be used to look it up, instead of listing all isotopes and elements, and gathering the available data into its member Options. I would think keeping the current approach is preferable to a whole lot of brute leg work.

Generally, I would argue it is preferable to keep structs as close to the thing they're modeling as possible and then describe things these structs have in common in traits. Elements, isotopes and isomers are not the same 'things', but all of them have a fair few properties in common, and some of them can decay. I'm not particularly familiar with nuclear physics, but from what I understand I would think the 'is' relationships go: an isomer is-an excited-state isotope, i.e. it can be modeled as a product-type of an isotope and its state, and an element is-an average of isotopes, i.e. it can be modeled as an abundance weighted average of isotopes.

That would suggest to me that at the bare minimum, all three types could implement the non-decay-related property functions as a shared trait, and isomers and isotopes additionally share decay. Elements could have the additional functionality of breaking into isotope fractions by natural abundance, and isomers their excited-state related properties.

Meaning, possibly some variant of:

struct Isotope {
    protons: u8,
    neutrons: u16
}

struct Isomer {
    isotope: Isotope,
    level: u8
}

struct Element {
    protons: u8,
    neutron_abundancies: Vec<(u16, f64)>
}

with traits e.g. Nuclide (Isotope, Isomer, and Element), and Decay (Isotope and Isomer), while Isomers state-decay related functions are not encompassed in any shared interface.

I'm sure I've overlooked something. What do you think?

jan-grimo commented 11 months ago

Conversely, if all of these concepts are to fit into one big box, I suppose it would be

struct Nuclide {
    protons: u8,
    neutrons: u16,
    state: u8
}

where isotopes are Nuclides of energy state zero and isomers Nuclides of energy state one and up.

The functionality I want could then operate on fractional compositions of Nuclides (i.e. mass, electronegativities, etc.), whereby Elements are only the particular case where that fractional composition is the natural abundancies.

JASory commented 11 months ago

I would accept a pull request implementing the Element struct as described with the fractional compositions. The purpose of the library is to provide something more versatile than just referencing hardcoded values, by having fractional compositions you could calculate the average values for individual samples.

Note that Nuclide largely does follow the schematic you presented, the difference is that the index value from neutrons and protons is calculated at creation instead of storing them and then calculating the index each time you call datapoints. In the case of the proposed Element struct, you could still use the proposed fields, however you would have to initialise each isotope of the element that exists in the sample using Nuclide::from_nucleons(proton,neutron) and then calculate the values from that.

Nuclide (respository not the crates publication) has since been updated with a ChemElement trait that splits the methods into elemental data and isotopes.

jan-grimo commented 11 months ago

Amazing, that sets good bounds on what I will implement. Thank you for preparing the ground so well, I was expecting to do most of what you just added to the repository myself.

Unfortunately, I will be away traveling without a laptop for some time, so my contribution to this will be a month or so coming, just so you're aware.