From 951bcec08a45ca8e6a44432f474488aa5ae70041 Mon Sep 17 00:00:00 2001 From: Kyle Beyer Date: Tue, 25 Aug 2026 22:22:52 -0400 Subject: [PATCH] Fixes needed for nucleon-nucleus corpora curation - reaction.py: drop stray literal "f" from reaction_string/__str__ for product-type reactions (produced e.g. "Ca-48(P,N)fSc-48") - distribution.py: add "average" statistical_err_treatment, for EXFOR asymmetric error pairs where both columns store positive magnitudes; the existing "difference" treatment yields negative errors there - distribution.py: fix duplicated "+DATA-ERR" test that meant (+DATA-ERR, -DATA-ERR) pairs were never detected - distribution.py: implement EnergyDistribution.plot, which previously inherited Distribution.plot and raised NotImplementedError - requirements.txt: add pandas, imported by distribution.py Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LSfNX9V6VEYiSsSmaJPyq5 --- requirements.txt | 1 + src/exfor_tools/distribution.py | 69 ++++++++++++++++++++++++++++++++- src/exfor_tools/reaction.py | 6 +-- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index baae625..af2f43c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy>=1.21.5 x4i3>=1.2.0 periodictable>1.0 matplotlib +pandas diff --git a/src/exfor_tools/distribution.py b/src/exfor_tools/distribution.py index 3012249..4644d13 100644 --- a/src/exfor_tools/distribution.py +++ b/src/exfor_tools/distribution.py @@ -251,6 +251,8 @@ def determine_error_categories( statistical_err = -np.diff(statistical_err, axis=0)[0, :] elif statistical_err_treatment == "sum": statistical_err = np.sum(statistical_err, axis=0) + elif statistical_err_treatment == "average": + statistical_err = np.mean(np.abs(statistical_err), axis=0) else: raise ValueError( f"Unknown statistical_err_treatment option: {statistical_err_treatment}" @@ -411,6 +413,71 @@ def parse_subentry( ) ] + @classmethod + def plot( + cls, + measurements, + ax, + offsets=None, + data_symbol="", + rxn_label="", + log=True, + draw_baseline=False, + baseline_offset=None, + xlim=None, + fontsize=10, + label_kwargs={}, + ): + r""" + Given a collection of energy-dependent measurements, plots them on the same + axis. Unlike `AngularDistribution.plot`, measurements are not offset from one + another; they share a common energy axis and are distinguished by color and by + an EXFOR subentry label in the legend. + + `offsets`, `draw_baseline` and `baseline_offset` are accepted for signature + compatibility with `AngularDistribution.plot` (so that `ExforEntry.plot` can + forward to either) and are ignored. `xlim` is applied only if provided. + """ + # allow either a flat list of measurements or a list of lists + flat = [] + for m in measurements: + if isinstance(m, (list, tuple)): + flat.extend(m) + else: + flat.append(m) + + for m in flat: + label = m.subentry + if label_kwargs.get("label_exfor", True) is False: + label = None + ax.errorbar( + m.x, + m.y, + yerr=m.statistical_err, + xerr=m.x_err if not np.allclose(m.x_err, 0) else None, + marker="s", + markersize=2, + alpha=0.75, + linestyle="none", + elinewidth=1, + label=label, + ) + + if log: + ax.set_yscale("log") + if xlim is not None: + ax.set_xlim(xlim) + + x_units = flat[0].x_units if flat else "MeV" + y_units = flat[0].y_units if flat else "" + ax.set_xlabel(r"$E_{lab}$" + f" [{x_units}]", fontsize=fontsize) + ax.set_ylabel(f"{data_symbol} [{y_units}]", fontsize=fontsize) + if rxn_label: + ax.set_title(rxn_label, fontsize=fontsize) + if any(m.subentry for m in flat): + ax.legend(fontsize=max(fontsize - 2, 4)) + return ax + def to_dataframe(self, citation: str = "") -> pd.DataFrame: """ Converts the EnergyDistribution object to a DataFrame with a specific format for @@ -885,7 +952,7 @@ def extract_staterr_labels( """ if "+ERR-T" in labels and "-ERR-T" in labels: return ["+ERR-T", "-ERR-T"], "difference" - if "+DATA-ERR" in labels and "+DATA-ERR" in labels: + if "+DATA-ERR" in labels and "-DATA-ERR" in labels: return ["+DATA-ERR", "-DATA-ERR"], "difference" allowed_stat_err_combos = set( [frozenset([l, "ERR-DIG"]) for l in allowed_stat_errs] diff --git a/src/exfor_tools/reaction.py b/src/exfor_tools/reaction.py index 57ce78f..fc924a8 100644 --- a/src/exfor_tools/reaction.py +++ b/src/exfor_tools/reaction.py @@ -44,7 +44,7 @@ def __init__(self, target, projectile, process=None, product=None, residual=None self.reaction_string = ( f"{get_exfor_particle_symbol(*self.target)}" f"({get_exfor_particle_symbol(*self.projectile)}," - f"{get_exfor_particle_symbol(*self.product)})f" + f"{get_exfor_particle_symbol(*self.product)})" ) else: self.reaction_latex = ( @@ -54,7 +54,7 @@ def __init__(self, target, projectile, process=None, product=None, residual=None self.reaction_string = ( f"{get_exfor_particle_symbol(*self.target)}" f"({get_exfor_particle_symbol(*self.projectile)}," - f"{get_exfor_particle_symbol(*self.product)})f" + f"{get_exfor_particle_symbol(*self.product)})" f"{get_exfor_particle_symbol(*self.residual)}" ) @@ -68,7 +68,7 @@ def __str__(self): elif self.product is not None: return ( f"{get_exfor_particle_symbol(*self.target)}" - f"({get_exfor_particle_symbol(*self.projectile)},{get_exfor_particle_symbol(*self.product)})f" + f"({get_exfor_particle_symbol(*self.projectile)},{get_exfor_particle_symbol(*self.product)})" f"{get_exfor_particle_symbol(*self.residual) if self.residual is not None else ''}" ) else: