From cc4ef8449b86837b08ee0f85b8d7568e851a445c Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 20:44:47 -0500 Subject: [PATCH 1/9] Change natural abundance of free neutrons to 0% --- periodictable/mass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/periodictable/mass.py b/periodictable/mass.py index 1ad7b73..ad7fb41 100644 --- a/periodictable/mass.py +++ b/periodictable/mass.py @@ -125,7 +125,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None: el._mass, el._mass_unc = neutron_mass, neutron_mass_unc iso = el.add_isotope(1) iso._mass, iso._mass_unc = neutron_mass, neutron_mass_unc - iso._abundance, iso._abundance_unc = 100, 0 + iso._abundance, iso._abundance_unc = 0, 0 # Parse element mass table where each line looks like: # z El element mass(unc)|[low,high]|- note note ... From ad72a50397c2ddea949520f80623c7f6982c2c26 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 20:47:25 -0500 Subject: [PATCH 2/9] Create an html table generator with up-to-date neutron scattering lengths --- periodictable/nsf.py | 176 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 8 deletions(-) diff --git a/periodictable/nsf.py b/periodictable/nsf.py index fd8ff0b..99ced17 100644 --- a/periodictable/nsf.py +++ b/periodictable/nsf.py @@ -188,6 +188,7 @@ # Wiley InterScience. pp 126-146. doi:10.1107/97809553602060000584 # +from pathlib import Path from typing import TYPE_CHECKING, cast import numpy as np @@ -429,24 +430,34 @@ class Neutron: .. Note:: 1 barn = 100 |fm^2| """ b_c: float|None = None + b_c_unc: float|None = None b_c_units: str = "fm" b_c_i: float|None = None + b_c_i_unc: float|None = None b_c_i_units: str = "fm" b_c_complex: complex|None = None b_c_complex_units: str = "fm" bp: float|None = None # Not all isotopes provide b+/b- values + bp_unc: float|None = None bp_i: float|None = None + bp_i_unc: float|None = None bp_units: str = "fm" bm: float|None = None # Not all isotopes provide b+/b- values + bm_unc: float|None = None bm_i: float|None = None + bm_i_unc: float|None = None bm_units: str = "fm" coherent: float|None = None + coherent_unc: float|None = None coherent_units: str = "barn" incoherent: float|None = None + incoherent_unc: float|None = None incoherent_units: str = "barn" total: float|None = None + total_unc: float|None = None total_units: str = "barn" absorption: float|None = None + absorption_unc: float|None = None absorption_units: str = "barn" abundance: float = 0. abundance_units: str = "%" @@ -598,10 +609,14 @@ def init(table: PeriodicTable, reload: bool=False) -> None: nsf = Neutron() p = columns[1] spin = columns[2] - nsf.b_c, nsf.bp, nsf.bm = [fix_number(a) for a in columns[3:6]] + nsf.b_c, nsf.b_c_unc = _fix_number(columns[3]) + nsf.bp, nsf.bp_unc = _fix_number(columns[4]) + nsf.bm, nsf.bm_unc = _fix_number(columns[5]) nsf.is_energy_dependent = (columns[6] == 'E') - nsf.coherent, nsf.incoherent, nsf.total, nsf.absorption \ - = [fix_number(a) for a in columns[7:]] + nsf.coherent, nsf.coherent_unc = _fix_number(columns[7]) + nsf.incoherent, nsf.incoherent_unc = _fix_number(columns[8]) + nsf.total, nsf.total_unc = _fix_number(columns[9]) + nsf.absorption, nsf.absorption_unc = _fix_number(columns[10]) # 1 fm = (1 barn)(100 fm^2/barn)/(1 A) (1e-5 A/fm) # Note: Sears (1992) uses b = b' - i b'', so negate sigma_a for b''. # Warning: -b_c.imag may be -0, which can mess with your calculations. @@ -617,10 +632,13 @@ def init(table: PeriodicTable, reload: bool=False) -> None: # zero is well within the uncertainty measured in bulk Ir. if nsf.coherent is None: nsf.coherent = 4*pi/100*abs(nsf.b_c_complex)**2 + nsf.coherent_unc = np.nan # derived value if nsf.incoherent is None: nsf.incoherent = 0 + nsf.incoherent_unc = np.nan # derived value if nsf.total is None: nsf.total = nsf.coherent + nsf.incoherent + nsf.total_unc = np.nan # derived value parts = columns[0].split('-') Z = int(parts[0]) @@ -645,7 +663,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None: isotope.neutron = nsf isotope.nuclear_spin = spin # p column contains either abundance(uncertainty) or "half-life Y" - isotope.neutron.abundance = fix_number(p) if ' ' not in p else 0 + isotope.neutron.abundance = _fix_number(p)[0] if ' ' not in p else 0 # If the element is not yet initialized, copy info into the atom. # This serves to set the element info for elements with only @@ -668,8 +686,9 @@ def init(table: PeriodicTable, reload: bool=False) -> None: nsf = element[isotope_number].neutron # Read imaginary values - nsf.b_c_i, nsf.bp_i, nsf.bm_i = [ - fix_number(a) for a in columns[1:]] + nsf.b_c_i, nsf.b_c_i_unc = _fix_number(columns[1]) + nsf.bp_i, nsf.bp_i_unc = _fix_number(columns[2]) + nsf.bm_i, nsf.bm_i_unc = _fix_number(columns[2]) # Add energy-dependent tables energy_dependent_init(table) @@ -1753,14 +1772,147 @@ def sld_plot(table=None): # 63-Eu-151,-2.46,, # 64-Gd-157,-47,-75, -def fix_number(str): +def _fix_number(str): """ Converts strings of the form e.g., 35.24(2)* into numbers without uncertainty. Also accepts a limited range, e.g., <1e-6, which is converted as 1e-6. Missing values are set to 0. """ from .util import parse_uncertainty - return parse_uncertainty(str.replace('<','').replace('*',''))[0] + return parse_uncertainty(str.replace('<','').replace('*','')) + + +# TODO: add a citation column to the html scattering table +def scattering_table_html(path: Path|str|None=None, table: PeriodicTable|None=None) -> str: + """ + Generate an html table, returning it as a string. If path is given, write the + html to that path. + + Note: requires the uncertainties package, which is not otherwise required by periodictable. + """ + from uncertainties import ufloat as U + + head = """\ + + + Neutron Cross Sections + +""" + table = default_table(table) + + def format_num(re, re_unc, im=None, im_unc=None): + # Note: using NaN for uncertainty for derived values that should be + # left blank in the table (currently 191,193Ir cross sections) + re_str = "" if re is None or np.isnan(re_unc) else f"{U(re, re_unc):fS}" if re_unc else str(re) if re else "0" + if im is not None: + im_value = f"{U(abs(im), im_unc):fS}" if im_unc else str(abs(im)) + im_str = f"
{'+' if im >= 0 else '–'} {im_value}j" + else: + im_str = "" + return f"{re_str}{im_str}" + + rows = [] + rows.append(f"""\ + + + Z + A + I(π) + abundance % + bc {Neutron.b_c_units} + b+ {Neutron.bp_units} + b {Neutron.bm_units} + σc {Neutron.coherent_units} + σi {Neutron.incoherent_units} + σs {Neutron.total_units} + σa {Neutron.absorption_units} + """) + + # Generate table rows + for el in [table.n, *table]: + A = el.number + isotopes = [iso for iso in el if iso.neutron.b_c is not None] + singleton = len(isotopes) <= 1 + # print(f"{el=} {A=} {singleton=} {el.neutron.has_sld()=} {[*el]}") + if el.neutron.b_c is not None and not singleton: + # Multiple isotopes: put element summary above + n = el.neutron + rows.append(f"""\ + + {el} + {A} + + + + {format_num(n.b_c, n.b_c_unc, n.b_c_i, n.b_c_i_unc)} + {format_num(n.bp, n.bp_unc, n.bp_i, n.bp_i_unc)} + {format_num(n.bm, n.bm_unc, n.bm_i, n.bm_i_unc)} + {format_num(n.coherent, n.coherent_unc)} + {format_num(n.incoherent, n.incoherent_unc)} + {format_num(n.total, n.total_unc)} + {format_num(n.absorption, n.absorption_unc)} + """) + + for iso in isotopes: + Z = iso.isotope + spin = iso.nuclear_spin + n = iso.neutron + abundance = ( + f"{U(iso.abundance, iso._abundance_unc):fS}" if iso._abundance_unc + else "100" if iso.abundance == 100.0 + else "" if iso.abundance == 0.0 + else f"{iso.abundance}" + ) + rows.append(f"""\ + + {el if singleton else ''} + {A if singleton else ''} + {Z} + {spin} + {abundance} + {format_num(n.b_c, n.b_c_unc, n.b_c_i, n.b_c_i_unc)} + {format_num(n.bp, n.bp_unc, n.bp_i, n.bp_i_unc)} + {format_num(n.bm, n.bm_unc, n.bm_i, n.bm_i_unc)} + {format_num(n.coherent, n.coherent_unc)} + {format_num(n.incoherent, n.incoherent_unc)} + {format_num(n.total, n.total_unc)} + {format_num(n.absorption, n.absorption_unc)} + """) + + html = f""" + +{head} + +

Scattering lengths and cross sections for various isotopes evaluated at 2200 m s–1 +

+ +{'\n'.join(rows)} +
+

Natural abundance is from IUPAC Commission on Isotopic Abundances and Atomic Weights (CIAAW) + + +""" + + if path: + # Save the HTML to a file + with open(path, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"HTML table saved to {str(path)}") + + return html def sld_table(wavelength=1, table=None, isotopes=True): r""" @@ -2238,6 +2390,10 @@ def main(): sld: 3.37503 + 0.000582313 j (0.402605 incoherent) 1e-6/Ang^2 sigma_c: 3.37503 sigma_i: 0.000582313 sigma_a: 0.402605 1/cm 1/e penetration: 2.23871 cm + + To generate an updated cross section table use:: + + python -m periodictable.nsf --html path/filename.html """ import sys @@ -2245,6 +2401,10 @@ def main(): if not compounds: print("usage: python -m periodictable.nsf [-Lwavelength] atom ...") sys.exit(1) + if compounds[0] == '--html': + scattering_table_html(path=compounds[1]) + return + if compounds[0].startswith('-L'): wavelength = float(compounds[0][2:]) compounds = compounds[1:] From 0a747a19dd7402db51e301da2739c54ff8284730 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 22:02:09 -0500 Subject: [PATCH 3/9] use correct column for b- imaginary data --- periodictable/nsf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/periodictable/nsf.py b/periodictable/nsf.py index 99ced17..73bb81d 100644 --- a/periodictable/nsf.py +++ b/periodictable/nsf.py @@ -688,7 +688,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None: # Read imaginary values nsf.b_c_i, nsf.b_c_i_unc = _fix_number(columns[1]) nsf.bp_i, nsf.bp_i_unc = _fix_number(columns[2]) - nsf.bm_i, nsf.bm_i_unc = _fix_number(columns[2]) + nsf.bm_i, nsf.bm_i_unc = _fix_number(columns[3]) # Add energy-dependent tables energy_dependent_init(table) From bb0a6fc995e60745847c8714a049ff7519557ff8 Mon Sep 17 00:00:00 2001 From: bbm Date: Wed, 18 Feb 2026 12:04:54 -0500 Subject: [PATCH 4/9] add sticky header styles and anchor tags to each element --- periodictable/nsf.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/periodictable/nsf.py b/periodictable/nsf.py index a7719ef..3fb0296 100644 --- a/periodictable/nsf.py +++ b/periodictable/nsf.py @@ -1798,11 +1798,34 @@ def scattering_table_html(path: Path|str|None=None, table: PeriodicTable|None=No Neutron Cross Sections