diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py index f24d80d..42325de 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py @@ -1,23 +1,21 @@ -# import builtins -# import datetime -# import os -# import subprocess -# import time -# from pathlib import Path +import inspect -# import numpy as np from bec_lib import bec_logger -# from bec_lib.alarm_handler import AlarmBase -# from bec_lib.pdf_writer import PDFWriter from typeguard import typechecked - +from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics +from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission +from csaxs_bec.bec_ipython_client.plugins.cSAXS.intensity_map_predict_gap import ( + predict_gap as _predict_gap, +) +from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSInitSmaractStages from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSSmaract from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools -from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission -from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics -from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits + +logger = bec_logger.logger + + class cSAXSError(Exception): pass @@ -36,6 +34,80 @@ class cSAXS( self.diagnostics = cSAXSDiagnostics() super().__init__(client=client) + # ------------------------------------------------------------------ + # Undulator + # ------------------------------------------------------------------ + + def predict_gap(self, energy: float, n: int = 3) -> None: + """Print the predicted undulator gap for *energy* [keV] on harmonic *n*. + + Examples + -------- + csaxs.predict_gap(6.2) # h=3 (default) + csaxs.predict_gap(10.0, n=5) # explicit harmonic + """ + import math + + gap = float(_predict_gap(energy, n=n)) + if math.isnan(gap): + print(f"Energy {energy:.3f} keV is unreachable on harmonic {n}.") + else: + print(f"Predicted gap for {energy:.3f} keV (h={n}): {gap:.4f} mm") + + # ------------------------------------------------------------------ + # Help / discovery + # ------------------------------------------------------------------ + + def commands(self) -> None: + """Print a table of all available cSAXS commands and sub-namespaces.""" + from rich import box + from rich.console import Console + from rich.table import Table + + console = Console() + + entries: list[tuple[str, str]] = [] + seen: set[str] = set() + + for cls in type(self).__mro__: + if cls is object: + continue + module = getattr(cls, "__module__", "") or "" + if "csaxs_bec" not in module: + continue + for name, func in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith("_") or name in seen: + continue + seen.add(name) + doc = (inspect.getdoc(func) or "").split("\n")[0].strip() + entries.append((name, doc)) + + entries.sort(key=lambda x: x[0]) + + tbl = Table(title="cSAXS Commands", box=box.SQUARE, show_lines=False) + tbl.add_column("Command", style="cyan bold", no_wrap=True, min_width=46) + tbl.add_column("Description") + for name, doc in entries: + tbl.add_row(f"csaxs.{name}()", doc) + console.print(tbl) + console.print("") + + ns = Table(title="Sub-namespaces", box=box.SQUARE, show_lines=False) + ns.add_column("Access", style="cyan bold", no_wrap=True, min_width=46) + ns.add_column("Description") + for access, desc in [ + ("csaxs.diagnostics.show_all()", "All diagnostic device readbacks"), + ( + "csaxs.diagnostics.bpm_xbox1 / .bpm_xbox2", + "BPM diagnostics — .show_all(), .gain(val)", + ), + ("csaxs.diagnostics.bim", "BIM diagnostics — .show_all(), .gain(val)"), + ("csaxs.diagnostics.beamstop", "Beamstop diode — .show_all(), .gain(val)"), + ("csaxs.diagnostics.polarization", "Polarization diodes — .show_all(), .gain(val)"), + ("csaxs.OMNYTools.*", "OMNY instrument tools"), + ]: + ns.add_row(access, desc) + console.print(ns) # this is the csaxs master file that imports all routines from csaxs @@ -45,4 +117,6 @@ class cSAXS( # csaxs = cSAXS(bec) # # then all commands can be accessed by for example -# csaxs._cSAXS_smaract_stages_..... \ No newline at end of file +# csaxs.commands() +# csaxs.predict_gap(6.2) +# csaxs._cSAXS_smaract_stages_... \ No newline at end of file diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py new file mode 100755 index 0000000..e991ba7 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py @@ -0,0 +1,26 @@ +"""Undulator gap predictor emitted by plot_intensity_map.py. +Edit the fitted constants in the signature to retune.""" + +import numpy as np + + +def predict_gap(energy, n=3, gap_min=5.0, + E_inf=3.83167, c0=3.1133, c1=-0.644678, c2=0.0210398): + """Undulator gap [mm] to place `energy` [keV] on harmonic `n`. + Fitted constants are the defaults below; edit them to retune. + Returns NaN where the energy is unreachable on that harmonic.""" + energy = np.asarray(energy, float) + arg = E_inf * n / energy - 1.0 # required K^2/2; must be > 0 + with np.errstate(invalid="ignore", divide="ignore"): + y = np.log(arg) + if abs(c2) < 1e-12: + g = (y - c0) / c1 + else: + disc = c1 * c1 - 4.0 * c2 * (c0 - y) + sq = np.sqrt(np.where(disc >= 0, disc, np.nan)) + r1 = (-c1 + sq) / (2.0 * c2) + r2 = (-c1 - sq) / (2.0 * c2) + g = np.where(c1 + 2.0 * c2 * r1 < 0, r1, r2) + g = np.where(arg > 0, g, np.nan) # above harmonic cutoff + g = np.where(g >= gap_min, g, np.nan) # below mechanical minimum + return g