after p22760 before cleanup

This commit is contained in:
2025-11-25 15:09:12 +01:00
parent 84948e2b7f
commit 6d277d4e71
14 changed files with 779 additions and 229 deletions

View File

@@ -3,7 +3,8 @@ from loguru import logger
from slic.devices.xoptics.aramis_attenuator import Attenuator
from slic.devices.xoptics.pulsepicker import PulsePicker
from slic.devices.xdiagnostics.intensitymonitor import IntensityMonitorPBPS
from .Cristallina_mono import CristallinaMono
#from .Cristallina_mono import CristallinaMono
from .cristallina_mono import CristallinaMono
from .alignment_laser import AlignmentLaser
from slic.devices.xoptics.kb import KBHor, KBVer

105
beamline/cristallina_mono.py Executable file
View File

@@ -0,0 +1,105 @@
from types import SimpleNamespace
from time import sleep
import numpy as np
from slic.core.adjustable import Adjustable, PVAdjustable, PVEnumAdjustable
from slic.core.adjustable.pvchangemon import PVChangeMonitor
from slic.core.device import Device
from slic.utils.hastyepics import get_pv as PV
from slic.devices.general.motor import Motor
from slic.devices.cameras import CameraCA
class CristallinaMono(Device):
"""Cristallina mono SAROP31-ODCC110"""
def __init__(self, ID, name="Cristallina double-channel-cut monochromator", **kwargs):
super().__init__(ID, name=name, **kwargs)
self.TX = Motor(ID + ":MOT_TX1")
self.bragg1 = Motor(ID + ":MOT_RX1")
self.bragg2 = Motor(ID + ":MOT_RX2")
self.energy = Motor(ID + ":MOT_ENY")
self.angle2_offset = Motor(ID + ":MOT_OFS")
self.screen = CristallinaMonoScreen("SAROP31-PSCR110", name='SAROP31-PSCR110')
class CristallinaMonoScreen(Device):
def __init__(self, ID, name="Profile Monitor", **kwargs):
super().__init__(ID, name=name, **kwargs)
self.cam = CameraCA(ID)
self.target = PVEnumAdjustable(ID + ":SCR_SP", name="target")
self.target_pos = Motor(ID + ":MOT_TY1", name="target position")
self.target_in_position = PVEnumAdjustable(ID + ":IN_POS",name="target in valid position")
def move_mono(self):
return self.target.set_target_value(1)
def move_pink(self):
return self.target.set_target_value(2)
def move_out(self):
return self.target.set_target_value(0)
# class DoubleCrystalMonoEnergy(Adjustable):
# def __init__(self, ID, name=None):
# self.wait_time = 0.1
# pvname_setvalue = "SAROP21-ARAMIS:ENERGY_SP"
# pvname_readback = "SAROP21-ARAMIS:ENERGY"
# pvname_moving = "SAROP21-ODCM098:MOVING"
# pvname_stop = "SAROP21-ODCM098:STOP.PROC"
# pv_setvalue = PV(pvname_setvalue)
# pv_readback = PV(pvname_readback)
# pv_moving = PV(pvname_moving)
# pv_stop = PV(pvname_stop)
# units = pv_readback.units
# super().__init__(ID, name=name, units=units)
# self.pvnames = SimpleNamespace(
# setvalue = pvname_setvalue,
# readback = pvname_readback,
# moving = pvname_moving,
# stop = pvname_stop
# )
# self.pvs = SimpleNamespace(
# setvalue = pv_setvalue,
# readback = pv_readback,
# moving = pv_moving,
# stop = pv_stop
# )
# def get_current_value(self):
# return self.pvs.readback.get()
# def set_current_value(self, value):
# self.pvs.setvalue.put(value)
# sleep(3)
# def set_target_value(self, value):
# self.set_current_value(value)
# # while abs(self.wait_for_valid_value() - value) > accuracy:
# while self.is_moving():
# sleep(self.wait_time)
# def wait_for_valid_value(self):
# val = np.nan
# while not np.isfinite(val):
# val = self.get_current_value()
# return val
# def is_moving(self):
# moving = self.pvs.moving.get()
# return bool(moving)
# def stop(self):
# self.pvs.stop.put(1)

View File

@@ -26,17 +26,16 @@ N_UNDS = list(range(3, 15 + 1))
### SETTINGS ####
PSSS_MOVE = True
DCCM_MOVE = True
PSSS_MOVE = False
DCCM_MOVE = False
TRAJECTORY_FEEDBACK_DISABLE_ENABLE = False
POINTING_FEEDFORWARD = False
energy_offset_undulators = 52 # eV
energy_offset_PSSS = 2 # eV
energy_offset_undulators = 43 # eV
energy_offset_PSSS = 0 # eV
energy_offset_DCCM = 0 # eV
DCCM_RX2_energy_offset = 1.6 # eV
# eV
DCCM_RX2_angle_offset = 0.0617 # deg
################
@@ -49,8 +48,8 @@ def print_configuration():
print(f"Undulator energy offset = {energy_offset_undulators}")
print(f"PSSS energy offset = {energy_offset_PSSS}")
print(f"DCCM energy offset = {energy_offset_DCCM}")
print(f" DCCM RX2 energy offset = {DCCM_RX2_energy_offset}")
# print(f" DCCM RX2 energy offset = {DCCM_RX2_energy_offset}")
print(f" DCCM RX2 angle offset = {DCCM_RX2_angle_offset}")
def pointing_slope_X(energy):
@@ -97,12 +96,19 @@ def set_DCCM_energy(energy: float):
DCCM_energy_PV_name = "SAROP31-ODCC110:MOT_ENY"
DCCM_energy_PV = PV(DCCM_energy_PV_name)
DCCM_energy_offset_PV_name = "SAROP31-ODCC110:MOT_OFS"
DCCM_energy_offset_PV = PV(DCCM_energy_offset_PV_name)
# DCCM_energy_offset_PV_name = "SAROP31-ODCC110:MOT_OFS"
# DCCM_energy_offset_PV = PV(DCCM_energy_offset_PV_name)
DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=True)
# DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=True)
# DCCM_energy_PV.put(energy, wait=False)
# DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=False)#
DCCM_angle_offset_PV_name = "SAROP31-ODCC110:MOT_OFS"
DCCM_angle_offset_PV = PV(DCCM_angle_offset_PV_name)
DCCM_angle_offset_PV.put(DCCM_RX2_angle_offset, wait=True)
DCCM_energy_PV.put(energy, wait=False)
DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=False)
DCCM_angle_offset_PV.put(DCCM_RX2_angle_offset, wait=False)
print(f"Finished adjusting DCCM.")

View File

@@ -6,13 +6,13 @@
from slic.core.acquisition.detcfg import DetectorConfig
# TODO: JF settings regarding raw conversion, compression, etc.
detectors = [
"JF16T03V02", # 1.5M from 2025
#detectors = [
# "JF16T03V02", # 1.5M from 2025
# "JF16T03V01", # 1.5M from 2022
# "JF17T16V01", # 8M
"JF20T01V01", # IO
# "JF20T01V01", # IO
# "JF05T01V01", # 0.5M stripsel borrowed from Bernina, now registered in esc network
]
#]
# ALLOWED_PARAMS = dict(
# adc_to_energy = bool,
@@ -28,7 +28,7 @@ detectors = [
# save_dap_results = bool
# )
detectors = DetectorConfig(detectors)
detectors = DetectorConfig("JF16T03V02","JF20T01V01")
# detectors_with_config["JF16T03V01"]['save_dap_results'] = True
# JF 1.5M default settings
@@ -38,6 +38,7 @@ detectors["JF16T03V02"]['adc_to_energy'] = True
detectors["JF16T03V02"]['compression'] = True
detectors["JF16T03V02"]['save_dap_results'] = False
detectors["JF16T03V02"]['geometry'] = True
#detectors["JF16T03V02"]['disabled_modules'] = [0, 2] # bottom module:0, middle module:1, top module:2
# I0 JF default settings
detectors["JF20T01V01"]['remove_raw_files'] = True
@@ -47,6 +48,16 @@ detectors["JF20T01V01"]['compression'] = True
detectors["JF20T01V01"]['save_dap_results'] = False
detectors["JF20T01V01"]['geometry'] = False
# JF 8M default settings
# detectors["JF17T16V01"]['remove_raw_files'] = True # We switched off quite a few modules, let's not keep all the raw data.
# detectors["JF17T16V01"]['factor'] = 0.1 # Some useful compromise to save space.
# detectors["JF17T16V01"]['adc_to_energy'] = True # We switched off quite a few modules, let's not keep all the raw data.
# detectors["JF17T16V01"]['compression'] = True
# detectors["JF17T16V01"]['save_dap_results'] = False
# detectors["JF17T16V01"]['geometry'] = True
#detectors["JF17T16V01"]['disabled_modules'] = [0, 1, 2, 3, 9, 10, 11, 12, 13, 14, 15]
detectors_I0_only = DetectorConfig(["JF20T01V01"])
# I0 JF settings with finer resolution
@@ -555,7 +566,7 @@ bs_channels = (
+ channels_PSCD153
+ channels_EVR
+ channels_digitizer
# + channels_Xeye
#+ channels_Xeye
+ diffractometer_1_bs
+ diffractometer_2_bs
# + camera_channels

View File

@@ -300,7 +300,6 @@ pvs_OOMV092_bernina = [
pvs_PPRM094_bernina = [
"SAROP21-PPRM113:MOTOR_PROBE",
#"SAROP21-PPRM113:FPICTURE",
]
@@ -863,6 +862,7 @@ pvs_diffractometer_extras = [
###############################
# DilSc
# LakeShore
ID_DilSc_LakeShore = "SARES31-DIL-LS1"
pvs_DilSc_Lakeshore = [
ID_DilSc_LakeShore + ":A_KELVIN",
@@ -873,14 +873,33 @@ pvs_DilSc_Lakeshore = [
ID_DilSc_LakeShore + ":7_RES",
ID_DilSc_LakeShore + ":8_RES",
ID_DilSc_LakeShore + ":1_RES",
ID_DilSc_LakeShore + ":2_RES",
ID_DilSc_LakeShore + ":3_RES",
ID_DilSc_LakeShore + ":A_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":7_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":8_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":1_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":2_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":3_EX_STRING.SVAL",
ID_DilSc_LakeShore + ":H0_SETP_GET",
ID_DilSc_LakeShore + ":H0_PWR_GET",
]
# Magnet Mercury iPS
ID_DilSc_MagnetPSU = "SARES31-MAG-IPS1"
pvs_DilSc_MagnetPSU = []
for axis in ['X','Y','Z']:
ID_with_axis = f'{ID_DilSc_MagnetPSU}'+'-'+f'{axis}'
pvs_DilSc_MagnetPSU.append( ID_with_axis + ':SIG_FLD' )
pvs_DilSc_MagnetPSU.append( ID_with_axis + ':SIG_FSET' )
pvs_DilSc_MagnetPSU.append( ID_with_axis + ':SIG_RFST' )
pvs_DilSc_MagnetPSU.append( ID_with_axis + ':ACTN' )
###############################
# PuMa Aerotech motor controller
@@ -970,11 +989,13 @@ pv_channels = (
+ pvs_standa
# + pvs_newport_300
# + pvs_smaract_xyz
# + pvs_diffractometer_1
+ pvs_diffractometer_2
+ pvs_diffractometer_extras
# + pvs_DilSc_Lakeshore
+ pvs_PuMa_Aerotech
+ pvs_diffractometer_1
# + pvs_diffractometer_2
# these do not work properly at the moment:
# + pvs_diffractometer_extras
+ pvs_DilSc_Lakeshore
+ pvs_DilSc_MagnetPSU
# + pvs_PuMa_Aerotech
+ pvs_huber_z
+ pvs_JJ_slits
# + pvs_attocube

View File

@@ -38,7 +38,8 @@ def setup_general_logging():
def setup_logging_pgroup(pgroup, level="INFO"):
try:
logger.add(
f"/sf/cristallina/data/{pgroup}/scratch/slic.log",
# f"/sf/cristallina/data/{pgroup}/scratch/slic.log",
f"/sf/cristallina/data/{pgroup}/res/slic.log",
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}",
level=level,
rotation="1 week",
@@ -135,11 +136,10 @@ from beamline import photon_energy
cr_photon_energy = photon_energy.PhotonEnergy()
# added limit to photon energy setpoint
cr_photon_energy.set_limits(5720-100, 5720+100)
cr_photon_energy.set_limits(5000, 13000)
logger.info(f"Photon energy offsets: PSSS {photon_energy.energy_offset_PSSS} eV , DCCM {photon_energy.energy_offset_DCCM} eV, undulator {photon_energy.energy_offset_undulators} eV.")
logger.info(f"Photon energy offsets: PSSS {photon_energy.energy_offset_PSSS} eV, DCCM {photon_energy.energy_offset_DCCM} eV, undulator {photon_energy.energy_offset_undulators} eV.")
@@ -167,19 +167,19 @@ dm1 = Diffractometer("SARES31-GPS")
dm2 = Diffractometer("SARES32-GPS")
# Set according to which diffractometer is being used
diffractometer = dm2
diffractometer = dm1
# Dilution fridge
#from crq_exp.dilsc import Dilution
from crq_exp.dilsc import Dilution
try:
dilution = Dilution()
dilution = Dilution('SARES31-DIL-LS1','SARES31-MAG-IPS1')
except Exception as e:
logger.warning(f"Error: Could not connect to dilution fridge. {e}")
dilution = None
# MX adajustables
# MX adjustables
# import mx.mx_adjustables
# Temporary quick hack thermometer addition for stand
@@ -196,8 +196,8 @@ downstream_transmission = PVAdjustable("SARFE10-OATT053:UsrRec.TC1")
from crq_exp.puma import Puma
puma = Puma()
# from crq_exp.puma import Puma
# puma = Puma()
################# Stand setup ##################
@@ -225,25 +225,25 @@ adjs_for_spreadsheet = {
"TRYBASE": diffractometer.try_base,
"THETA": diffractometer.theta,
"TWOTHETA": diffractometer.twotheta,
"Sample_X": puma.sample_x,
"Sample_Y": puma.sample_y,
"Sample_Z": puma.sample_z,
"Sample_R": puma.sample_r,
"Magnet_X": puma.magnet_x,
"Magnet_Y": puma.magnet_y,
"Magnet_Z": puma.magnet_z,
# PuMa positions if available:
# "Sample_X": puma.sample_x,
# "Sample_Y": puma.sample_y,
# " Sample_Z": puma.sample_z,
# "Sample_R": puma.sample_r,
# "Magnet_X": puma.magnet_x,
# "Magnet_Y": puma.magnet_y,
# "Magnet_Z": puma.magnet_z,
}
dilution = None
if dilution is not None:
adjs_dilsc = {
#"Magnet_X": dilution.x,
#"Magnet_Y": dilution.y,
#"Magnet_Z": dilution.z,
"DilSc_T_chip": T_chip,
"DilSc_T_plato": T_plato,
#"DilSc_T_chip": dilution.T_chip,
"DilSc_T_pucksensor": T_reg,
"Magnet_X": dilution.x,
"Magnet_Y": dilution.y,
"Magnet_Z": dilution.z,
"DilSc_T_reg": dilution.T_CHA,
"DilSc_T_CH7": dilution.T_CH7,
"DilSc_T_CH8": dilution.T_CH8,
}
adjs_for_spreadsheet.update(adjs_dilsc)
@@ -260,6 +260,7 @@ spreadsheet = Spreadsheet(
port=9090,
)
try:
from stand.client import Client
stand_host = "saresc-vcons-02.psi.ch"
@@ -332,7 +333,7 @@ DAQS = multiple_daqs.generate_DAQS(instrument, pgroup, bs_channels, pv_channels,
# required fraction defines amount of data recorded to save the step and move on to the next one
check_intensity_gas_monitor = PVCondition(
"SARFE10-PBPG050:PHOTON-ENERGY-PER-PULSE-US",
vmin=400,
vmin=500,
vmax=2000,
wait_time=0.5,
required_fraction=0.8,
@@ -363,20 +364,21 @@ temperature_setpoint=PVAdjustable('SARES31-DIL-LS1:H0_SETP_SET')
# d fixed to 3.0 mm, x_0 = 4.574 mm and theta_0 = -67.853 deg
import numpy as np
d = 3
theta_0 = -67.853
x_0 = 4.574
thetas = np.linspace(-2, 25)
TRX = d * np.sin(np.deg2rad(thetas + theta_0)) + x_0
linked_theta = LinkedInterpolated("ThetaLinked", diffractometer.theta, diffractometer.tr_x, thetas, TRX)
# d = 3
# theta_0 = -67.853
# x_0 = 4.574
# thetas = np.linspace(-2, 25)
# TRX = d * np.sin(np.deg2rad(thetas + theta_0)) + x_0
# linked_theta = LinkedInterpolated("ThetaLinked", diffractometer.theta, diffractometer.tr_x, thetas, TRX)
# Sample 3
d = 2.5000000
x_0 = 4.04466204
theta_0 = -64.1376413
thetas = np.linspace(-10, 15)
TRX = d * np.sin(np.deg2rad(thetas + theta_0)) + x_0
linked_theta_s3 = LinkedInterpolated("ThetaLinked", diffractometer.theta, diffractometer.tr_x, thetas, TRX)
# d = 2.5000000
# x_0 = 4.04466204
# theta_0 = -64.1376413
# thetas = np.linspace(-10, 15)
# TRX = d * np.sin(np.deg2rad(thetas + theta_0)) + x_0
# linked_theta_s3 = LinkedInterpolated("ThetaLinked", diffractometer.theta, diffractometer.tr_x, thetas, TRX)
# Beware: double_pixels_action interpolate does not seem to work, results in empty data
@@ -415,7 +417,7 @@ daq_1p5M_I0 = SFAcquisition(
spreadsheet=spreadsheet)
from tqdm.notebook import tqdm
def scan_with_sync(adjustable, start_pos, end_pos, step_size, n_pulses, filename, acquisitions = [daq], return_to_initial_values=False, condition=check_intensity_gas_monitor, spreadsheet=spreadsheet):
"""

182
crq_exp/dilsc-frappy.py Executable file
View File

@@ -0,0 +1,182 @@
""" DilSc prototype
"""
from slic.core.adjustable import Adjustable, PVAdjustable
from slic.core.device import Device, SimpleDevice
from frappy.client import SecopClient
from frappy import states
from frappy.datatypes import StatusType
class Dilution(Device):
def __init__(self, **kwargs):
self.name = 'DilSc'
ID = self.name
super().__init__(ID, **kwargs)
self.address = 'dilsc.psi.ch:5000'
self.dilsc = SecopClient(self.address)
self.dilsc.connect()
self.x = MagnetCoil("X", self.dilsc, 'x', limit_low=-0.6, limit_high=0.6)
self.y = MagnetCoil("Y", self.dilsc, 'y', limit_low=-0.6, limit_high=0.6)
self.z = MagnetCoil("Z", self.dilsc, 'z', limit_low=-5.2, limit_high=5.2)
self.T_plato = Thermometer('T_plato', self.dilsc, limit_low=0, limit_high=300)
self.T_chip = Thermometer('T_chip', self.dilsc, limit_low=0, limit_high=300)
self.T_reg = Thermometer('T_reg', self.dilsc, limit_low=0, limit_high=300)
class Thermometer(Adjustable):
def __init__(self, name, dilsc_connection, limit_low=-0.0001, limit_high=0.0001):
super().__init__(name, limit_low=limit_low, limit_high=limit_high)
self.dilsc = dilsc_connection
# Heater to regulate channel A on the Galgen LakeShore372. Give value in Ohm.
self.heater_resistance = 82
# Defines heater ranges and PID values for various control temperature regions.
self.heater_settings = [
{'range': 'off', 'pid': (800, 1, 0), 'temp_start': -1, 'temp_end': 0.00099},
{'range': '300uA', 'pid': (800, 1, 0), 'temp_start': 0.001, 'temp_end': 0.037},
{'range': '1mA', 'pid': (40, 14, 0), 'temp_start': 0.038, 'temp_end': 0.108},
{'range': '3mA', 'pid': (4, 15, 0), 'temp_start': 0.110, 'temp_end': 0.350},
{'range': '10mA', 'pid': (2, 12, 0), 'temp_start': 0.352, 'temp_end': 1.100},
{'range': '30mA', 'pid': (2, 60, 0), 'temp_start': 1.100, 'temp_end': 2.300}
]
def _check_connection(func):
def checker(self, *args, **kwargs):
if not self.dilsc.online:
raise ConnectionError(f'No connection to dilsc at {self.address}')
else:
return func(self, *args, **kwargs)
return checker
@_check_connection
def set_heater_range(self, range_value):
""" Sets the heater range for the control channel and sets the PID parameters for the associated control loop.
TODO: Consider redoing so that it only works for the T_reg channel.
TODO: the pid values are now sent with stringio and not saved as parameters in slic, this should be fixed.
-
"""
heater_range_map = {
'off': 0, '30uA': 1, '100uA': 2, '300uA': 3, '1mA': 4, '3mA': 5, '10mA': 6, '30mA': 7, '100mA': 8}
if range_value not in heater_range_map:
raise ValueError("Invalid range value. Allowed are: ['off','30uA','100uA','300uA','1mA','3mA','10mA','30mA','100mA']")
enum_value = heater_range_map[range_value]
self.dilsc.setParameter(self.name, 'htrrng', enum_value)
for setting in self.heater_settings:
if setting['range'] == range_value:
self.set_PID_parameters(*setting['pid'])
break
return
@_check_connection
def get_current_value(self):
cacheitem = self.dilsc.getParameter(f'{self.name}', 'value', trycache=False)
return cacheitem.value
@_check_connection
def set_target_value(self, value, adjust_heater_range=True):
if adjust_heater_range:
range_to_set = None
for setting in self.heater_settings:
if setting['temp_start'] <= value and setting['temp_end'] >= value:
range_to_set = setting['range']
if range_to_set != None:
self.set_heater_range(range_to_set)
else:
raise ValueError(f"Heater range for the target value {value} could not be set")
self.dilsc.setParameter(f'{self.name}', 'target', value)
@_check_connection
def get_target_value(self):
cacheitem = self.dilsc.getParameter(f'{self.name}', 'target', trycache=False)
return cacheitem.value
@_check_connection
def is_moving(self):
response = self.dilsc.getParameter(f'{self.name}','status', trycache=False)
return response[0][0] > StatusType.PREPARED
@_check_connection
def get_PID_parameters(self):
""" Returns the current PID parameters associated with the control loop for
this thermometer.
"""
# response = self.dilsc.getParameter(f'{self.name}','ctrlpars', trycache=False)
response = self.dilsc.execCommand('lscio', 'communicate', 'PID?')
return response
@_check_connection
def set_PID_parameters(self, p, i, d):
""" Sets the PID parameters for the associated control loop.
TODO:
- This still returns a timeout error but sets the correct values.
- The range is limited to less than the Lakeshore range allows, this needs
to be fixed in frappy.
"""
# self.dilsc.setParameter(f'{self.name}', 'ctrlpars', {'p': p, 'i': i, 'd': d})
command_string = f'PID {0},{p},{i},{d}; PID?'
self.dilsc.execCommand('lscio', 'communicate', command_string)
class MagnetCoil(Adjustable):
def __init__(self, name, dilsc_connection, direction, limit_low=-0.0001, limit_high=0.0001):
super().__init__(name, limit_low=-0.0001, limit_high=0.0001)
self.direction = direction.lower()
if self.direction not in ["x", "y", "z"]:
raise ValueError("Direction must be either x, y or z.")
self.dilsc = dilsc_connection
def _check_connection(func):
def checker(self, *args, **kwargs):
if not self.dilsc.online:
raise ConnectionError(f'No connection to dilsc at {self.address}')
else:
return func(self, *args, **kwargs)
return checker
@_check_connection
def get_current_value(self):
cacheitem = self.dilsc.getParameter(f'mf{self.direction}', 'value', trycache=False)
return cacheitem.value
@_check_connection
def set_target_value(self, value):
self.dilsc.setParameter(f'mf{self.direction}', 'target', value)
@_check_connection
def is_moving(self):
response = self.dilsc.getParameter(f'mf{self.direction}','status', trycache=False)
return response[0][0] > StatusType.PREPARED
@_check_connection
def set_ramp_speed(self, direction, value):
""" Sets ramp speed for a given direction (x,y or z) and value in T/min.
"""
if value > 0.5:
raise ValueError('Do not exceed 0.5 T/min unless you want a quench party')
if value <= 0:
raise ValueError('Only positive values are allowed')
if self.direction not in ["x", "y", "z"]:
raise ValueError("Direction must be either x, y or z.")
else:
self.dilsc.setParameter(direction, 'ramp', value)
return

View File

@@ -1,182 +1,75 @@
""" DilSc prototype
""" DilSc prototype with Edwin's EPICS panels
"""
from slic.core.adjustable import Adjustable, PVAdjustable
from slic.core.adjustable import Adjustable, PVAdjustable, Collection
from slic.core.device import Device, SimpleDevice
from frappy.client import SecopClient
from frappy import states
from frappy.datatypes import StatusType
import time
import numpy as np
class Dilution(Device):
def __init__(self, **kwargs):
def __init__(self, ID_DilSc_LakeShore, ID_DilSc_MagnetPSU, **kwargs):
super().__init__(ID_DilSc_LakeShore,ID_DilSc_MagnetPSU, **kwargs)
self.name = 'DilSc'
ID = self.name
super().__init__(ID, **kwargs)
self.address = 'dilsc.psi.ch:5000'
self.dilsc = SecopClient(self.address)
self.dilsc.connect()
self.ID_DilSc_LakeShore = ID_DilSc_LakeShore
self.x = MagnetCoil("X", self.dilsc, 'x', limit_low=-0.6, limit_high=0.6)
self.y = MagnetCoil("Y", self.dilsc, 'y', limit_low=-0.6, limit_high=0.6)
self.z = MagnetCoil("Z", self.dilsc, 'z', limit_low=-5.2, limit_high=5.2)
self.T_plato = Thermometer('T_plato', self.dilsc, limit_low=0, limit_high=300)
self.T_chip = Thermometer('T_chip', self.dilsc, limit_low=0, limit_high=300)
self.T_reg = Thermometer('T_reg', self.dilsc, limit_low=0, limit_high=300)
class Thermometer(Adjustable):
def __init__(self, name, dilsc_connection, limit_low=-0.0001, limit_high=0.0001):
self.x = MagnetCoil(ID_DilSc_MagnetPSU, 'X', limit_low=-0.6, limit_high=0.6)
self.y = MagnetCoil(ID_DilSc_MagnetPSU, 'Y', limit_low=-0.6, limit_high=0.6)
self.z = MagnetCoil(ID_DilSc_MagnetPSU, 'Z', limit_low=-5.2, limit_high=5.2)
super().__init__(name, limit_low=limit_low, limit_high=limit_high)
self.dilsc = dilsc_connection
self.T_CHA = PVAdjustable(ID_DilSc_LakeShore + ":A_KELVIN")
self.T_CH7 = PVAdjustable(ID_DilSc_LakeShore + ":7_KELVIN")
self.T_CH8 = PVAdjustable(ID_DilSc_LakeShore + ":8_KELVIN")
# Heater to regulate channel A on the Galgen LakeShore372. Give value in Ohm.
self.heater_resistance = 82
# Defines heater ranges and PID values for various control temperature regions.
self.heater_settings = [
{'range': 'off', 'pid': (800, 1, 0), 'temp_start': -1, 'temp_end': 0.00099},
{'range': '300uA', 'pid': (800, 1, 0), 'temp_start': 0.001, 'temp_end': 0.037},
{'range': '1mA', 'pid': (40, 14, 0), 'temp_start': 0.038, 'temp_end': 0.108},
{'range': '3mA', 'pid': (4, 15, 0), 'temp_start': 0.110, 'temp_end': 0.350},
{'range': '10mA', 'pid': (2, 12, 0), 'temp_start': 0.352, 'temp_end': 1.100},
{'range': '30mA', 'pid': (2, 60, 0), 'temp_start': 1.100, 'temp_end': 2.300}
]
def _check_connection(func):
def checker(self, *args, **kwargs):
if not self.dilsc.online:
raise ConnectionError(f'No connection to dilsc at {self.address}')
else:
return func(self, *args, **kwargs)
return checker
self.T_reg_setpoint = PVAdjustable(ID_DilSc_LakeShore + ':H0_SETP_SET')
@_check_connection
def set_heater_range(self, range_value):
""" Sets the heater range for the control channel and sets the PID parameters for the associated control loop.
TODO: Consider redoing so that it only works for the T_reg channel.
TODO: the pid values are now sent with stringio and not saved as parameters in slic, this should be fixed.
-
"""
heater_range_map = {
'off': 0, '30uA': 1, '100uA': 2, '300uA': 3, '1mA': 4, '3mA': 5, '10mA': 6, '30mA': 7, '100mA': 8}
if range_value not in heater_range_map:
raise ValueError("Invalid range value. Allowed are: ['off','30uA','100uA','300uA','1mA','3mA','10mA','30mA','100mA']")
enum_value = heater_range_map[range_value]
self.dilsc.setParameter(self.name, 'htrrng', enum_value)
for setting in self.heater_settings:
if setting['range'] == range_value:
self.set_PID_parameters(*setting['pid'])
break
return
self.heater = SimpleDevice('DilSc sample heater', p = PVAdjustable(ID_DilSc_LakeShore + ':H0_PID_P'),
i = PVAdjustable(ID_DilSc_LakeShore + ':H0_PID_I'),
d = PVAdjustable(ID_DilSc_LakeShore + ':H0_PID_D'),
)
@_check_connection
def get_current_value(self):
cacheitem = self.dilsc.getParameter(f'{self.name}', 'value', trycache=False)
return cacheitem.value
@_check_connection
def set_target_value(self, value, adjust_heater_range=True):
if adjust_heater_range:
range_to_set = None
for setting in self.heater_settings:
if setting['temp_start'] <= value and setting['temp_end'] >= value:
range_to_set = setting['range']
if range_to_set != None:
self.set_heater_range(range_to_set)
else:
raise ValueError(f"Heater range for the target value {value} could not be set")
self.dilsc.setParameter(f'{self.name}', 'target', value)
def set_pids(self,P,I,D):
self.heater.p.set_target_value(P)
self.heater.i.set_target_value(I)
self.heater.d.set_target_value(D)
@_check_connection
def get_target_value(self):
cacheitem = self.dilsc.getParameter(f'{self.name}', 'target', trycache=False)
return cacheitem.value
PVAdjustable(self.ID_DilSc_LakeShore +':H0_01_SEND.PROC').set_target_value(1)
@_check_connection
def is_moving(self):
response = self.dilsc.getParameter(f'{self.name}','status', trycache=False)
return response[0][0] > StatusType.PREPARED
@_check_connection
def get_PID_parameters(self):
""" Returns the current PID parameters associated with the control loop for
this thermometer.
"""
# response = self.dilsc.getParameter(f'{self.name}','ctrlpars', trycache=False)
response = self.dilsc.execCommand('lscio', 'communicate', 'PID?')
return response
@_check_connection
def set_PID_parameters(self, p, i, d):
""" Sets the PID parameters for the associated control loop.
TODO:
- This still returns a timeout error but sets the correct values.
- The range is limited to less than the Lakeshore range allows, this needs
to be fixed in frappy.
"""
# self.dilsc.setParameter(f'{self.name}', 'ctrlpars', {'p': p, 'i': i, 'd': d})
command_string = f'PID {0},{p},{i},{d}; PID?'
self.dilsc.execCommand('lscio', 'communicate', command_string)
class MagnetCoil(Adjustable):
def __init__(self, ID, direction, limit_low, limit_high):
super().__init__(ID, limit_low=limit_low, limit_high=limit_high)
self.direction = direction
self.ID = self.name + '-' + self.direction
def __init__(self, name, dilsc_connection, direction, limit_low=-0.0001, limit_high=0.0001):
if self.direction not in ["X", "Y", "Z"]:
raise ValueError("Direction must be either X, Y or Z.")
super().__init__(name, limit_low=-0.0001, limit_high=0.0001)
self.direction = direction.lower()
self.target = PVAdjustable(self.ID+':SET_FSET')
self.target_readback = PVAdjustable(self.ID+':SIG_FSET')
self.ramp_rate = PVAdjustable(self.ID+':SIG_RFST')
self.done = PVAdjustable(self.ID+':FSET_FLAG')
self.field_target_tolerance = 1e-3
if self.direction not in ["x", "y", "z"]:
raise ValueError("Direction must be either x, y or z.")
self.dilsc = dilsc_connection
def _check_connection(func):
def checker(self, *args, **kwargs):
if not self.dilsc.online:
raise ConnectionError(f'No connection to dilsc at {self.address}')
else:
return func(self, *args, **kwargs)
return checker
@_check_connection
def get_current_value(self):
cacheitem = self.dilsc.getParameter(f'mf{self.direction}', 'value', trycache=False)
return cacheitem.value
@_check_connection
def set_target_value(self, value):
self.dilsc.setParameter(f'mf{self.direction}', 'target', value)
@_check_connection
def is_moving(self):
response = self.dilsc.getParameter(f'mf{self.direction}','status', trycache=False)
return response[0][0] > StatusType.PREPARED
self.target.set_target_value(value)
PVAdjustable(self.ID+':ACTN_RTOS.PROC').set_target_value(1)
def get_current_value(self):
return PVAdjustable(self.ID+':SIG_FLD').get_current_value()
@_check_connection
def set_ramp_speed(self, direction, value):
""" Sets ramp speed for a given direction (x,y or z) and value in T/min.
"""
if value > 0.5:
raise ValueError('Do not exceed 0.5 T/min unless you want a quench party')
if value <= 0:
raise ValueError('Only positive values are allowed')
if self.direction not in ["x", "y", "z"]:
raise ValueError("Direction must be either x, y or z.")
else:
self.dilsc.setParameter(direction, 'ramp', value)
return
def hold(self):
return PVAdjustable(self.ID+':ACTN_HOLD.PROC').set_target_value(1)
def press_to_setpoint(self):
return PVAdjustable(self.ID+':ACTN_RTOS.PROC').set_target_value(1)
#TODO: Bodge. There must be a better way..
def is_moving(self):
time.sleep(0.1)
return abs( self.target.get_current_value() - self.get_current_value() ) > self.field_target_tolerance

View File

@@ -2,6 +2,13 @@ import requests
import numpy as np
from loguru import logger
from tqdm import tqdm
from slic.core.condition import ValueCondition
from slic.utils.npy import nice_arange
from slic.core.scanner.scaninfo import ScanInfo
def start_sequence(n: int = 100, pulse_phase: float = np.pi/8):
@@ -10,4 +17,89 @@ def start_sequence(n: int = 100, pulse_phase: float = np.pi/8):
r = requests.get(url, params=parameters)
d = r.json()
return d['pids']
return d['pids']
def happy_condition():
""" Enjoy the bright side of life.
"""
get_value = lambda: 1
check_time = 0.1
vmin = 0
vmax = 2
wait_time = 0.1
required_fraction = 0
condition = ValueCondition(get_value, check_time, vmin, vmax, wait_time, required_fraction)
return condition
def scan_with_sync(adjustable, start_pos, end_pos, step_size, n_pulses, filename, acquisitions = [None], return_to_initial_values=False, condition=None, spreadsheet=None, stand_client=None):
"""
Convenience function to make scans when pulse tube synchronisation is used for a given
start position, end position and step size.
"""
positions = nice_arange(start_pos, end_pos, step_size)
scan_with_sync_seq(adjustable, positions, n_pulses, filename, acquisitions=acquisitions, return_to_initial_values=return_to_initial_values, condition=condition, spreadsheet=spreadsheet, stand_client=stand_client)
def scan_with_sync_seq(adjustable, positions, n_pulses, filename, acquisitions = [None], return_to_initial_values=False, condition=None, spreadsheet=None, stand_client=None):
"""
Convenience function to make scans when pulse tube synchronisation is used for a given
list of positions to drive to.
"""
scaninfo = ScanInfo(filename, "scan_info", [adjustable], positions)
assert len(acquisitions) == 1, f"Only one acquisition can be given for one run. Now {acquisitions} were given."
for i, position in enumerate(tqdm(positions)):
# Go to the position and save readback
adjustable.set_target_value(position).wait()
readback = adjustable.get_current_value()
scaninfo.append([position], [readback], None, None)
if condition is None:
condition = happy_condition()
successful_acquisition = False
while not successful_acquisition:
condition.clear_and_start_counting()
try:
# Measure the step and save the pids
# It's done with try because every now and then the pids are not retrieved and the step should be repeated.
pids = start_sequence(n_pulses)
scan_info_d = scaninfo.to_sfdaq_dict()
successful_acquisition = condition.stop_counting_and_analyze()
except KeyboardInterrupt:
print('Aborting.')
break
except:
successful_acquisition = False
# Retrieve data
# First step needs to be made separately, because a run number can't be given to sfdaq until the folder is created.
# TODO: is this still true?
if i == 0:
for acquisition in acquisitions:
first_step = acquisition.retrieve(filename, pulseids=pids, scan_info=scan_info_d)
run_number= first_step['run_number']
else:
for acquisition in acquisitions:
acquisition.retrieve(filename, pulseids=pids, run_number=run_number, scan_info=scan_info_d)
if return_to_initial_values:
adjustable.set_target_value(positions[0]).wait()
# Write to stand table
if stand_client is not None:
if spreadsheet is not None:
stand_client.add_row(run=str(run_number), filename=filename, n_pulses=str(n_pulses), sync="True", **spreadsheet.get_adjs_values())
else:
stand_client.add_row(run=str(run_number), filename=filename, n_pulses=str(n_pulses), sync="True")
print(f"Scan {run_number} finished")

View File

@@ -0,0 +1,187 @@
Sensor Model: CX-1030-CD-0.3L
Serial Number: X165053
Data Format: 4 (Log Ohms/Kelvin)
SetPoint Limit: 325.0 (Kelvin)
Temperature coefficient: 1 (Negative)
Number of Breakpoints: 178
No. Units Temperature (K)
1 1.53734 330.052
2 1.54198 325.000
3 1.54763 319.000
4 1.55292 313.500
5 1.55832 308.000
6 1.56384 302.500
7 1.56948 297.000
8 1.57524 291.500
9 1.58112 286.000
10 1.58714 280.500
11 1.59329 275.000
12 1.59958 269.500
13 1.60602 264.000
14 1.61260 258.500
15 1.61934 253.000
16 1.62623 247.500
17 1.63329 242.000
18 1.64052 236.500
19 1.64724 231.500
20 1.65411 226.500
21 1.66114 221.500
22 1.66833 216.500
23 1.67568 211.500
24 1.68321 206.500
25 1.69091 201.500
26 1.69879 196.500
27 1.70687 191.500
28 1.71515 186.500
29 1.72363 181.500
30 1.73233 176.500
31 1.74125 171.500
32 1.75041 166.500
33 1.75886 162.000
34 1.76752 157.500
35 1.77640 153.000
36 1.78550 148.500
37 1.79485 144.000
38 1.80444 139.500
39 1.81429 135.000
40 1.82443 130.500
41 1.83485 126.000
42 1.84439 122.000
43 1.85418 118.000
44 1.86425 114.000
45 1.87462 110.000
46 1.88530 106.000
47 1.89631 102.000
48 1.90483 99.000
49 1.91207 96.500
50 1.91947 94.000
51 1.92704 91.500
52 1.93478 89.000
53 1.94271 86.500
54 1.95083 84.000
55 1.95915 81.500
56 1.96770 79.000
57 1.97647 76.500
58 1.98549 74.000
59 1.99477 71.500
60 2.00241 69.500
61 2.01023 67.500
62 2.01825 65.500
63 2.02648 63.500
64 2.03493 61.500
65 2.04406 59.400
66 2.05302 57.400
67 2.06225 55.400
68 2.07177 53.400
69 2.08160 51.400
70 2.09073 49.600
71 2.10015 47.800
72 2.10987 46.000
73 2.11991 44.200
74 2.13031 42.400
75 2.14109 40.600
76 2.15164 38.900
77 2.16193 37.300
78 2.17259 35.700
79 2.18366 34.100
80 2.19516 32.500
81 2.20638 31.000
82 2.21806 29.500
83 2.22941 28.100
84 2.24124 26.700
85 2.25360 25.300
86 2.26561 24.000
87 2.27721 22.800
88 2.28934 21.600
89 2.30210 20.400
90 2.31162 19.550
91 2.32031 18.800
92 2.32871 18.100
93 2.33679 17.450
94 2.34517 16.800
95 2.35388 16.150
96 2.36224 15.550
97 2.37092 14.950
98 2.37997 14.350
99 2.38863 13.800
100 2.39765 13.250
101 2.40709 12.700
102 2.41606 12.200
103 2.42545 11.700
104 2.43529 11.200
105 2.44564 10.700
106 2.45546 10.250
107 2.46577 9.800
108 2.47666 9.350
109 2.48819 8.900
110 2.49907 8.500
111 2.51056 8.100
112 2.52278 7.700
113 2.53582 7.300
114 2.54801 6.950
115 2.56097 6.600
116 2.57485 6.250
117 2.59065 5.880
118 2.60540 5.560
119 2.62128 5.240
120 2.63739 4.940
121 2.65480 4.640
122 2.67249 4.360
123 2.69172 4.080
124 2.70745 3.870
125 2.72182 3.690
126 2.73720 3.510
127 2.75279 3.340
128 2.76853 3.180
129 2.78651 3.010
130 2.80468 2.850
131 2.82307 2.700
132 2.84302 2.550
133 2.86320 2.410
134 2.88511 2.270
135 2.90906 2.130
136 2.93348 2.000
137 2.95817 1.880
138 2.98524 1.760
139 3.01521 1.640
140 3.04574 1.530
141 3.07965 1.420
142 3.11779 1.310
143 3.15700 1.210
144 3.16370 1.195
145 3.17876 1.160
146 3.19702 1.120
147 3.21628 1.080
148 3.23668 1.040
149 3.25832 1.000
150 3.28136 0.960
151 3.30594 0.920
152 3.32890 0.885
153 3.35329 0.850
154 3.37933 0.815
155 3.40721 0.780
156 3.43716 0.745
157 3.46945 0.710
158 3.49934 0.680
159 3.53133 0.650
160 3.56588 0.620
161 3.60325 0.590
162 3.64391 0.560
163 3.68840 0.530
164 3.72895 0.505
165 3.76208 0.486
166 3.79551 0.468
167 3.83118 0.450
168 3.86939 0.432
169 3.91040 0.414
170 3.94954 0.398
171 3.99133 0.382
172 4.03619 0.366
173 4.08440 0.350
174 4.13658 0.334
175 4.18610 0.320
176 4.23214 0.308
177 4.26529 0.300
178 4.45964 0.260

View File

@@ -1,7 +1,7 @@
# How to upload .340 curve to a LakeShore 372 in EPICS
If something unclear, contact the creator Edwin
active the enviromnet with pyepics in it. Then in this folder run 'python UploadTempCurve.py _340-file_ _LakeShoreBaseName:CVCurveNumber_'
active the enviromnet with pyepics in it (e.g. slic). Then in this folder run 'python UploadTempCurve.py _340-file_ _LakeShoreBaseName:CVCurveNumber_'
e.g. python UploadTempCurve.py U08910.340 SARES31-DIL-LS1:CV1
the code gives an error, but works.

43
p22760.py Normal file
View File

@@ -0,0 +1,43 @@
"""Module for the p22760 beamtime experiment at Cristallina. For slic purposes only.
"""
import scipy
import numpy as np
def get_theta_prediction(B_field):
"""Get predicted theta angle from B field using linear interpolation.
Args:
B_field (float): Magnetic field value in Tesla.
Returns:
tuple: Predicted theta angles (left, right) in degrees.
"""
B = np.array([0. , 0. , 0.05 , 0.1 , 0.15 , 0.2 , 0.25 , 0.3 ,
0.35 , 0.4 , 0.5 , 0.525 , 0.53 , 0.55 , 0.555 , 0.56 ,
0.565 , 0.5675, 0.57 , 0.5725, 0.575 , 0.6 ])
left = np.array([-16.72402197, -16.72592595, -16.72266943, -16.71780642,
-16.70710748, -16.68879129, -16.67129814, -16.64430405,
-16.61258093, -16.57162955, -16.43958512, -16.36767873,
-16.35235521, -16.20000072, -16.14585335, -16.14986136,
-16.14953497, -16.15033841, -16.14907281, -16.14883662,
-16.14936827, -16.14544489])
right = np.array([-15.82641889, -15.82810495, -15.82810142, -15.83362565,
-15.84191596, -15.85030174, -15.86875095, -15.88716295,
-15.91257856, -15.94358668, -16.0436225 , -16.07455302,
-16.08374928, -16.13199655, -16.14585335, -16.14986136,
-16.14953497, -16.15033841, -16.14907281, -16.14883662,
-16.14936827, -16.14544489])
interpolation_right = scipy.interpolate.interp1d(B, right, kind='linear')
interpolation_left = scipy.interpolate.interp1d(B, left, kind='linear')
theta_right = interpolation_right(B_field)
theta_left = interpolation_left(B_field)
return theta_left, theta_right

View File

@@ -1,7 +1,8 @@
pgroup_scratch = "p19150" # Scratch
# pgroup = "p19150" # Scratch
pgroup = "p19150" # Scratch
# pgroup = "p19152" # Scratch
# pgroup = "p19739" # commissioning March 2022 -- July 2022
@@ -54,4 +55,10 @@ pgroup_scratch = "p19150" # Scratch
# pgroup = "p22581" # Cr-Q commissioning Puma June 2025
pgroup = "p22761" # Cr-Q commissioning Puma June 2025
# pgroup = "p22761" # Cr-Q commissioning Puma Sep 2025
# pgroup = "p23016" # Cr-Q user experiment Vonka Nov 2025 on /sf/maloja/data
# pgroup = "p22760" # Cr-Q user experiment Vonka Nov 2025
# pgroup = "p22569" # Cr-Q user experiment Bianchi Dec 2025

View File

@@ -5,7 +5,7 @@ class Time(Adjustable):
""" Adjustable only for spreadsheet, no other functionality
"""
def __init__(self):
super().__init__(self, "")
super().__init__(self, "Time")
def get_current_value(self):
return datetime.datetime.now().replace(microsecond=0).isoformat()