feat(filters): add fil_comb for explicit filter combinations
fil_trans selects by transmission, which is the wrong tool when a particular foil is wanted: it optimises against the thickness table and which combination wins is not something to rely on. The alternative was four hand-copied stage coordinates, where the out positions differ per unit (25.0 / 25.5 / 25.8 / 25.0) and getting one wrong leaves the stage off any tabulated slot -- _fil_trans_report matches within 0.1, so the combination then reports as unidentified. fil_comb takes one slot number per unit, 1..6 with 1 = out, as the SPEC fil_comb did, and looks the coordinates up. It reuses _position_transmission, _print_combination and _execute_combination unchanged, so it reports and moves exactly as fil_trans does, including the dry run and the default-yes prompt. Every slot is validated before the safety prompt is reached, so a typo raises rather than putting a question about an unexecutable combination in front of someone. Disabled slots -- Fe5 and the redundant opens -- raise too: _all_combinations skips them, so fil_comb must refuse them rather than move somewhere fil_trans would never choose. _attenuation_allowed is added here but not yet wired into fil_trans; that is the next commit, so this one cannot change existing behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
This commit is contained in:
committed by
x12sa
co-authored by
Claude Opus 5
parent
732c77222f
commit
ffb49b664b
@@ -297,6 +297,131 @@ class cSAXSFilterTransmission:
|
||||
self._execute_combination(best, energy_kev)
|
||||
return None
|
||||
|
||||
def fil_comb(
|
||||
self,
|
||||
*positions,
|
||||
energy_kev: Optional[float] = None,
|
||||
print_only: bool = True,
|
||||
) -> Optional[None]:
|
||||
"""
|
||||
Set the exposure-box filters to an explicit combination.
|
||||
|
||||
Takes one slot number per unit, 1..6, with 1 = out -- the same numbering
|
||||
as the SPEC fil_comb. Use this when a particular filter is wanted; use
|
||||
fil_trans when a particular transmission is wanted.
|
||||
|
||||
Examples:
|
||||
csaxs.fil_comb(1, 4, 1, 1) # Ti 400 only
|
||||
csaxs.fil_comb(1, 2, 3, 1) # Si 200 (unit 2) + Si 1600 (unit 3)
|
||||
csaxs.fil_comb(1, 1, 1, 1) # all out
|
||||
|
||||
Safety: identical to fil_trans. Any combination with transmission < 1
|
||||
requires DMM through and the CCM active, or an explicit confirmation.
|
||||
|
||||
Args:
|
||||
*positions: four slot numbers, one per unit, 1..6 (1 = out). A single
|
||||
list or tuple of four is also accepted.
|
||||
energy_kev: photon energy. Read from the CCM energy PV if omitted.
|
||||
print_only: dry run first and prompt before moving. Defaults to True.
|
||||
"""
|
||||
if not positions:
|
||||
print("\nUsage example:")
|
||||
print(" csaxs.fil_comb(1, 4, 1, 1) # one slot number per unit, 1 = out")
|
||||
print(" Use csaxs.fil_trans(T) to select by transmission instead.")
|
||||
print("\nCurrent filter transmission:")
|
||||
self._fil_trans_report(energy_kev=energy_kev)
|
||||
return None
|
||||
|
||||
# Accept fil_comb([1, 4, 1, 1]) as well as fil_comb(1, 4, 1, 1)
|
||||
if len(positions) == 1 and isinstance(positions[0], (list, tuple)):
|
||||
positions = tuple(positions[0])
|
||||
|
||||
# --- Energy handling (EPICS only) ---
|
||||
if energy_kev is None:
|
||||
try:
|
||||
energy_kev = float(epics_get("X12SA-OP-CCM1:ENERGY-GET"))
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"Energy not specified and could not read EPICS PV "
|
||||
"'X12SA-OP-CCM1:ENERGY-GET'."
|
||||
) from exc
|
||||
else:
|
||||
energy_kev = float(energy_kev)
|
||||
|
||||
# Validate everything before the safety prompt, so a typo never reaches it.
|
||||
comb = self._combination_from_positions(positions, energy_kev)
|
||||
|
||||
if comb["transmission"] < 1.0 and not self._attenuation_allowed():
|
||||
print("Aborted. Filters unchanged.")
|
||||
return None
|
||||
|
||||
print("\nExposure-box filter combination request")
|
||||
print("-" * 60)
|
||||
print(f"Requested combination : {comb['code']}")
|
||||
print(f"Photon energy : {energy_kev:.3f} keV")
|
||||
print(f"Mode : {'PRINT ONLY' if print_only else 'EXECUTE'}")
|
||||
print("-" * 60)
|
||||
|
||||
self._print_combination(comb, energy_kev, header="Requested combination")
|
||||
|
||||
if print_only:
|
||||
print("\n[DRY RUN] No motion executed yet.")
|
||||
if hasattr(self, "OMNYTools") and hasattr(self.OMNYTools, "yesno"):
|
||||
if self.OMNYTools.yesno(
|
||||
"Execute motion to the requested filter combination now?", "y"
|
||||
):
|
||||
self._execute_combination(comb, energy_kev)
|
||||
else:
|
||||
print("Execution skipped.")
|
||||
else:
|
||||
print("No interactive prompt available. Execution skipped (print_only=True).")
|
||||
return None
|
||||
|
||||
self._execute_combination(comb, energy_kev)
|
||||
return None
|
||||
|
||||
# -----------------------------
|
||||
# Safety
|
||||
# -----------------------------
|
||||
def _attenuation_allowed(self) -> bool:
|
||||
"""
|
||||
Check the beamline is in a state where inserting attenuators is sensible.
|
||||
|
||||
Requires DMM translation and rotation in THROUGH and the CCM active.
|
||||
Returns True to proceed. When the conditions are not met the user is
|
||||
prompted, defaulting to NO.
|
||||
"""
|
||||
try:
|
||||
dmm_trans = float(epics_get("X12SA-OP-DMM-EMLS-3010:THRU"))
|
||||
except Exception:
|
||||
dmm_trans = -1
|
||||
try:
|
||||
dmm_rot = float(epics_get("X12SA-OP-DMM-EMLS-3030:THRU"))
|
||||
except Exception:
|
||||
dmm_rot = -1
|
||||
try:
|
||||
ccm_energy = float(epics_get("X12SA-OP-CCM1:ENERGY-GET"))
|
||||
except Exception:
|
||||
ccm_energy = -1
|
||||
|
||||
if (dmm_trans == 1) and (dmm_rot == 1) and (ccm_energy > 1):
|
||||
return True
|
||||
|
||||
print("\n\u26a0\ufe0f SAFETY WARNING: Reducing transmission (< 1) typically requires:")
|
||||
print(" - DMM translation in THROUGH (THRU == 1)")
|
||||
print(" - DMM rotation in THROUGH (THRU == 1)")
|
||||
print(" - CCM energy > 1 keV")
|
||||
print("\nCurrent state:")
|
||||
print(f" DMM translation THRU : {dmm_trans}")
|
||||
print(f" DMM rotation THRU : {dmm_rot}")
|
||||
print(f" CCM energy (keV) : {ccm_energy}")
|
||||
|
||||
if hasattr(self, "OMNYTools") and hasattr(self.OMNYTools, "yesno"):
|
||||
return bool(
|
||||
self.OMNYTools.yesno("Conditions not satisfied. Proceed anyway?", default="n")
|
||||
)
|
||||
return False
|
||||
|
||||
# -----------------------------
|
||||
# Physics helpers
|
||||
# -----------------------------
|
||||
@@ -477,6 +602,57 @@ class cSAXSFilterTransmission:
|
||||
combos.sort(key=lambda c: c["transmission"]) # ascending
|
||||
return combos
|
||||
|
||||
def _combination_from_positions(self, positions, energy_kev: float) -> dict:
|
||||
"""
|
||||
Build a combination dict, same shape as the entries from _all_combinations,
|
||||
from explicit per-unit slot numbers 1..6.
|
||||
|
||||
Every slot is validated before anything moves, so a typo costs nothing.
|
||||
"""
|
||||
if len(positions) != self._UNITS:
|
||||
raise ValueError(
|
||||
f"fil_comb expects {self._UNITS} positions, got {len(positions)}."
|
||||
)
|
||||
|
||||
indices = []
|
||||
for unit_idx, pos in enumerate(positions):
|
||||
try:
|
||||
pos_int = int(pos)
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
f"Unit {unit_idx + 1}: position must be an integer 1..{self._PER_UNIT}."
|
||||
)
|
||||
if not 1 <= pos_int <= self._PER_UNIT:
|
||||
raise ValueError(
|
||||
f"Unit {unit_idx + 1}: position {pos_int} out of range 1..{self._PER_UNIT}."
|
||||
)
|
||||
indices.append(pos_int - 1)
|
||||
|
||||
mats = []
|
||||
transmission = 1.0
|
||||
for unit_idx, idx in enumerate(indices):
|
||||
entry = self._FILTERS[unit_idx * self._PER_UNIT + idx]
|
||||
(mat1, th1), (mat2, th2), enabled = entry
|
||||
if not enabled:
|
||||
raise ValueError(
|
||||
f"Unit {unit_idx + 1} position {idx + 1} is disabled in the filter table."
|
||||
)
|
||||
if self._POSITIONS_USER[unit_idx][idx] is None:
|
||||
raise ValueError(
|
||||
f"Unit {unit_idx + 1} position {idx + 1} has no defined coordinate."
|
||||
)
|
||||
transmission *= self._position_transmission(entry, energy_kev)
|
||||
mats.append(
|
||||
((mat1, th1), (mat2, th2) if (mat2 != "none" and th2 > 0.0) else None)
|
||||
)
|
||||
|
||||
return {
|
||||
"code": "".join(str(i + 1) for i in indices),
|
||||
"indices": indices,
|
||||
"materials": mats,
|
||||
"transmission": transmission,
|
||||
}
|
||||
|
||||
def _find_best_combination(self, target_T: float, energy_kev: float) -> dict:
|
||||
"""Pick combination with transmission closest to target."""
|
||||
combos = self._all_combinations(energy_kev)
|
||||
|
||||
Reference in New Issue
Block a user