Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Once installed or cloned, the data is available as a single dataframe indexed on
>>> from nuclearmasses.mass_table import MassTable
>>> df = MassTable().full_data
```
You can then interrogate, or extract, what ever information you want.
You can then interrogate, or extract, whatever information you want.
For example, how has the mass excess and it's accuracy changed overtime for 190Re according to the AME
```python
>>> df[(df['A'] == 190) & (df['Symbol'] == 'Re')][['AMEMassExcess', 'AMEMassExcessError']]
Expand All @@ -61,7 +61,7 @@ TableYear
2016 -35635.830 70.852
2020 -35583.015 4.870
```
Or how do the mass excess of gold vary across the isotropic chain according to NUBASE in the most recent table for both experimentally measured and theoretical values
Or how does the mass excess of gold vary across the isotopic chain according to NUBASE in the most recent table for both experimentally measured and theoretical values
```python
>>> df.query("TableYear == 2020 and Symbol == 'Au'")[['A', 'NUBASEMassExcess', 'NUBASEMassExcessError', 'Experimental']]
A NUBASEMassExcess NUBASEMassExcessError Experimental
Expand Down Expand Up @@ -115,7 +115,15 @@ TableYear

If you have ideas for additional functionality or find bugs please create an [issue](https://github.com/php1ic/nuclearmasses/issues) or better yet a [pull request](https://github.com/php1ic/nuclearmasses/pulls).

We use a combination of [isort](https://pycqa.github.io/isort/), [ruff](https://docs.astral.sh/ruff/) and [mypy](https://www.mypy-lang.org/) to keep things tidy and hopefully catch errors and bugs before they happen.
The command below returns no errors or issues so should be run after any code changes.
We might add a CI pipeline in the future, but for the moment, it's a manual process.
```bash
isort . && ruff format && ruff check && mypy nuclearmasses
```

## Known issues

- [#5](https://github.com/php1ic/nuclearmasses/issues/5) The half life from the NUBASE data is stored as the individual elements, a column with the value in seconds would be useful
```python
>>> df[(df['A'] == 14) & (df['Symbol'] == 'C')][['HalfLifeValue', 'HalfLifeUnit', 'HalfLifeError']]
Expand Down
24 changes: 12 additions & 12 deletions nuclearmasses/ame_mass_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ def __init__(self, year: int):
self.END_MICRO_DU = 120

self.column_limits = [
(self.START_Z, self.END_Z),
(self.START_A, self.END_A),
(self.START_ME, self.END_ME),
(self.START_DME, self.END_DME),
(self.START_BE_PER_A, self.END_BE_PER_A),
(self.START_DBE_PER_A, self.END_DBE_PER_A),
(self.START_BETA_DECAY_ENERGY, self.END_BETA_DECAY_ENERGY),
(self.START_DBETA_DECAY_ENERGY, self.END_DBETA_DECAY_ENERGY),
(self.START_AM, self.END_AM),
(self.START_MICRO_U, self.END_MICRO_U),
(self.START_MICRO_DU, self.END_MICRO_DU),
]
(self.START_Z, self.END_Z),
(self.START_A, self.END_A),
(self.START_ME, self.END_ME),
(self.START_DME, self.END_DME),
(self.START_BE_PER_A, self.END_BE_PER_A),
(self.START_DBE_PER_A, self.END_DBE_PER_A),
(self.START_BETA_DECAY_ENERGY, self.END_BETA_DECAY_ENERGY),
(self.START_DBETA_DECAY_ENERGY, self.END_DBETA_DECAY_ENERGY),
(self.START_AM, self.END_AM),
(self.START_MICRO_U, self.END_MICRO_U),
(self.START_MICRO_DU, self.END_MICRO_DU),
]
105 changes: 55 additions & 50 deletions nuclearmasses/ame_mass_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,53 +26,53 @@ def _column_names(self) -> list[str]:
match self.year:
case _:
return [
"Z",
"A",
"AMEMassExcess",
"AMEMassExcessError",
"BindingEnergyPerA",
"BindingEnergyPerAError",
"BetaDecayEnergy",
"BetaDecayEnergyError",
"AtomicNumber",
"AtomicMass",
"AtomicMassError"
]
"Z",
"A",
"AMEMassExcess",
"AMEMassExcessError",
"BindingEnergyPerA",
"BindingEnergyPerAError",
"BetaDecayEnergy",
"BetaDecayEnergyError",
"AtomicNumber",
"AtomicMass",
"AtomicMassError",
]

def _data_types(self) -> dict:
"""Set the data type depending on the year"""
match self.year:
case _:
return {
"TableYear": "Int64",
"Symbol": "string",
"N": "Int64",
"Z": "Int64",
"A": "Int64",
"AMEMassExcess": "float64",
"AMEMassExcessError": "float64",
"BindingEnergyPerA": "float64",
"BindingEnergyPerAError": "float64",
"BetaDecayEnergy": "float64",
"BetaDecayEnergyError": "float64",
"AtomicMass": "float64",
"AtomicMassError": "float64",
}
"TableYear": "Int64",
"Symbol": "string",
"N": "Int64",
"Z": "Int64",
"A": "Int64",
"AMEMassExcess": "float64",
"AMEMassExcessError": "float64",
"BindingEnergyPerA": "float64",
"BindingEnergyPerAError": "float64",
"BetaDecayEnergy": "float64",
"BetaDecayEnergyError": "float64",
"AtomicMass": "float64",
"AtomicMassError": "float64",
}

def _na_values(self) -> dict:
"""Set the columns that have placeholder values"""
match self.year:
case 1983:
return {
"A": [''],
"BetaDecayEnergy": ['', '*'],
"BetaDecayEnergyError": ['', '*'],
}
"A": [""],
"BetaDecayEnergy": ["", "*"],
"BetaDecayEnergyError": ["", "*"],
}
case _:
return {
"BetaDecayEnergy": ['', '*'],
"BetaDecayEnergyError": ['', '*'],
}
"BetaDecayEnergy": ["", "*"],
"BetaDecayEnergyError": ["", "*"],
}

def read_file(self) -> pd.DataFrame:
"""Read the file using it's known format
Expand All @@ -83,43 +83,48 @@ def read_file(self) -> pd.DataFrame:
"""
try:
df = pd.read_fwf(
self.filename,
colspecs=self.column_limits,
names=self._column_names(),
na_values=self._na_values(),
keep_default_na=False,
on_bad_lines='warn',
skiprows=self.HEADER,
skipfooter=self.FOOTER
)
self.filename,
colspecs=self.column_limits,
names=self._column_names(),
na_values=self._na_values(),
keep_default_na=False,
on_bad_lines="warn",
skiprows=self.HEADER,
skipfooter=self.FOOTER,
)
# We use the NUBASE data to define whether or not an isotope is experimentally measured,
# so for this data we'll just drop any and all '#' characters
df.replace("#", "", regex=True, inplace=True)

if self.year == 1983:
# The column headers and units are repeated in the 1983 table
df = df[(df['A'] != 'A') & (~df["AMEMassExcess"].astype("string").str.contains('keV', na=False))]
df = df[(df["A"] != "A") & (~df["AMEMassExcess"].astype("string").str.contains("keV", na=False))]
# The A value is not in the column if it doesn't change so we need to fill down
df['A'] = df['A'].ffill()
df["A"] = df["A"].ffill()
# Isomeric states are sometimes included in this version of the file
# For each row in the dataframe, if the previous row has equal A and Z, drop the current row
df = df[~((df['A'] == df['A'].shift()) & (df['Z'] == df['Z'].shift()))]
df = df[~((df["A"] == df["A"].shift()) & (df["Z"] == df["Z"].shift()))]

if self.year == 1983 or self.year == 1993 or self.year == 1995:
df["BindingEnergyPerA"] = df["BindingEnergyPerA"].astype(float) / df['A'].astype(float)
df["BindingEnergyPerAError"] = df["BindingEnergyPerAError"].astype(float) / df['A'].astype(float)
df["BindingEnergyPerA"] = df["BindingEnergyPerA"].astype(float) / df["A"].astype(float)
df["BindingEnergyPerAError"] = df["BindingEnergyPerAError"].astype(float) / df["A"].astype(float)

df["TableYear"] = self.year
df["N"] = pd.to_numeric(df["A"]) - pd.to_numeric(df["Z"])
df["Symbol"] = pd.to_numeric(df["Z"]).map(self.z_to_symbol)

# Combine the two columns to create the atomic mass then drop the redundant column
df["AtomicMass"] = df["AtomicNumber"].astype("string") + "." + df["AtomicMass"].astype("string").str.replace(".", "", regex=False)
# Pandas is happy to use '+' for any type, but mypy doesn't like it, hence the use of str.cat()
df["AtomicMass"] = (
df["AtomicNumber"]
.astype("string")
.str.cat(df["AtomicMass"].astype("string").str.replace(".", "", regex=False), sep=".")
)
df = df.drop(columns=["AtomicNumber"])

# We need to rescale the error value because we combined the two columns above
df = df.assign(AtomicMassError=df["AtomicMassError"].astype(float) / 1.0e6)

return df.astype(self._data_types())
except ValueError as e:
print(f"Parsing error: {e}")

return df.astype(self._data_types())
30 changes: 15 additions & 15 deletions nuclearmasses/ame_reaction_1_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,18 @@ def __init__(self, year: int):
self.END_DQBN = 125

self.column_limits = [
(self.START_R1_A, self.END_R1_A),
(self.START_R1_Z, self.END_R1_Z),
(self.START_S2N, self.END_S2N),
(self.START_DS2N, self.END_DS2N),
(self.START_S2P, self.END_S2P),
(self.START_DS2P, self.END_DS2P),
(self.START_QA, self.END_QA),
(self.START_DQA, self.END_DQA),
(self.START_Q2B, self.END_Q2B),
(self.START_DQ2B, self.END_DQ2B),
(self.START_QEP, self.END_QEP),
(self.START_DQEP, self.END_DQEP),
(self.START_QBN, self.END_QBN),
(self.START_DQBN, self.END_DQBN),
]
(self.START_R1_A, self.END_R1_A),
(self.START_R1_Z, self.END_R1_Z),
(self.START_S2N, self.END_S2N),
(self.START_DS2N, self.END_DS2N),
(self.START_S2P, self.END_S2P),
(self.START_DS2P, self.END_DS2P),
(self.START_QA, self.END_QA),
(self.START_DQA, self.END_DQA),
(self.START_Q2B, self.END_Q2B),
(self.START_DQ2B, self.END_DQ2B),
(self.START_QEP, self.END_QEP),
(self.START_DQEP, self.END_DQEP),
(self.START_QBN, self.END_QBN),
(self.START_DQBN, self.END_DQBN),
]
Loading