diff --git a/irrad_control/analysis/constants.py b/irrad_control/analysis/constants.py index d8897785..cd4daec8 100644 --- a/irrad_control/analysis/constants.py +++ b/irrad_control/analysis/constants.py @@ -8,3 +8,12 @@ # nano prefix nano = 1e-9 + +# Conversion factor for MeV/g to Mrad, 1 eV = 1.602e-19 J, 1 rad = 0.01 J/kg +# -> MeV / g = 1e6 * 1.602e-19 J / 1e-3 kg +# -> MeV / g = 1e9 * 1.602e-19 J / kg +# -> MeV / g = 1e9 * 1.602e-19 * 1e2 rad +# -> MeV / g = 1e11 * 1.602e-19 rad +# -> Mev / g = 1e5 * 1.602e-19 Mrad +# -> Mev / g = 1e5 * elementary_charge * Mrad +MEV_PER_GRAM_TO_MRAD = 1e5 * elementary_charge diff --git a/irrad_control/analysis/damage.py b/irrad_control/analysis/damage.py index ab1b8635..e46cf989 100644 --- a/irrad_control/analysis/damage.py +++ b/irrad_control/analysis/damage.py @@ -13,7 +13,7 @@ def analyse_radiation_damage(data, config=None): bins = (100, 100) # Dict that holds results and error maps; bin centers - results = {r: None for r in ('proton', 'neq', 'tid')} + results = {r: None for r in ('primary', 'neq', 'tid')} errors = {e: None for e in results} bin_centers = {'x': None, 'y': None} @@ -21,6 +21,7 @@ def analyse_radiation_damage(data, config=None): if config is None: server = None # Only allow files with exactly one server for multipart to avoid adding unrelated fluence maps + ion_name = None # Loop over generator and get partial data files for nfile, data_part, config_part, session_basename in data: @@ -35,6 +36,7 @@ def analyse_radiation_damage(data, config=None): # Only allow one fixed server for multipart if server is None: server = server_config['name'] + ion_name = server_config['daq']['ion'] if server not in data_part: raise KeyError(f"Server '{server}' not present in file {session_basename}!") @@ -42,7 +44,7 @@ def analyse_radiation_damage(data, config=None): # Initialize damage and error maps if nfile == 0: - results['proton'], errors['proton'], bin_centers['x'], bin_centers['y'] = fluence.generate_fluence_map(beam_data=data_part[server]['Beam'], + results['primary'], errors['primary'], bin_centers['x'], bin_centers['y'] = fluence.generate_fluence_map(beam_data=data_part[server]['Beam'], scan_data=data_part[server]['Scan'], beam_sigma=beam_sigma, bins=bins) @@ -50,13 +52,13 @@ def analyse_radiation_damage(data, config=None): if server_config['daq']['kappa'] is None: del results['neq'] else: - results['neq'] = results['proton'] * server_config['daq']['kappa']['nominal'] + results['neq'] = results['primary'] * server_config['daq']['kappa']['nominal'] print(server_config['daq']['stopping_power'], type(server_config['daq']['stopping_power'])) if server_config['daq']['stopping_power'] is None: del results['tid'] else: - results['tid'] = formulas.tid_scan(proton_fluence=results['proton'], stopping_power=server_config['daq']['stopping_power']) + results['tid'] = formulas.tid_per_scan(primary_fluence=results['primary'], stopping_power=server_config['daq']['stopping_power']) continue @@ -65,23 +67,24 @@ def analyse_radiation_damage(data, config=None): beam_sigma=beam_sigma, bins=bins) # Add to overall map - results['proton'] += fluence_map_part - errors['proton'] = (errors['proton']**2 + fluence_map_part_error**2)**.5 + results['primary'] += fluence_map_part + errors['primary'] = (errors['primary']**2 + fluence_map_part_error**2)**.5 # Add to eqivalent fluence map if 'neq' in results: - results['neq'] += results['proton'] * server_config['daq']['kappa']['nominal'] - errors['neq'] = ((server_config['daq']['kappa']['nominal'] * errors['proton'])**2 + (results['proton'] * server_config['daq']['kappa']['sigma'])**2)**0.5 + results['neq'] += results['primary'] * server_config['daq']['kappa']['nominal'] + errors['neq'] = ((server_config['daq']['kappa']['nominal'] * errors['primary'])**2 + (results['primary'] * server_config['daq']['kappa']['sigma'])**2)**0.5 if 'tid' in results: - results['tid'] += formulas.tid_scan(proton_fluence=results['proton'], stopping_power=server_config['daq']['stopping_power']) - errors['tid'] = formulas.tid_scan(proton_fluence=errors['proton'], stopping_power=server_config['daq']['stopping_power']) + results['tid'] += formulas.tid_per_scan(primary_fluence=results['primary'], stopping_power=server_config['daq']['stopping_power']) + errors['tid'] = formulas.tid_per_scan(primary_fluence=errors['primary'], stopping_power=server_config['daq']['stopping_power']) else: server = config['name'] + ion_name = config['daq']['ion'] - results['proton'], errors['proton'], bin_centers['x'], bin_centers['y'] = fluence.generate_fluence_map(beam_data=data[server]['Beam'], + results['primary'], errors['primary'], bin_centers['x'], bin_centers['y'] = fluence.generate_fluence_map(beam_data=data[server]['Beam'], scan_data=data[server]['Scan'], beam_sigma=beam_sigma, bins=bins) @@ -89,14 +92,14 @@ def analyse_radiation_damage(data, config=None): if config['daq']['kappa'] is None: del results['neq'] else: - results['neq'] = results['proton'] * config['daq']['kappa']['nominal'] - errors['neq'] = ((config['daq']['kappa']['nominal'] * errors['proton'])**2 + (results['proton'] * config['daq']['kappa']['sigma'])**2)**.5 + results['neq'] = results['primary'] * config['daq']['kappa']['nominal'] + errors['neq'] = ((config['daq']['kappa']['nominal'] * errors['primary'])**2 + (results['primary'] * config['daq']['kappa']['sigma'])**2)**.5 if config['daq']['stopping_power'] is None: del results['tid'] else: - results['tid'] = formulas.tid_scan(proton_fluence=results['proton'], stopping_power=config['daq']['stopping_power']) - errors['tid'] = formulas.tid_scan(proton_fluence=errors['proton'], stopping_power=config['daq']['stopping_power']) + results['tid'] = formulas.tid_per_scan(primary_fluence=results['primary'], stopping_power=config['daq']['stopping_power']) + errors['tid'] = formulas.tid_per_scan(primary_fluence=errors['primary'], stopping_power=config['daq']['stopping_power']) if any(a is None for a in (list(bin_centers.values()) + list(results.values()))): raise ValueError('Uninitialized values! Something went wrong - maybe files not found?') @@ -123,16 +126,16 @@ def analyse_radiation_damage(data, config=None): is_dut = damage_map.shape == dut_map.shape - fig, _ = plotting.plot_damage_map_3d(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, contour=not is_dut, damage=damage, server=server, dut=is_dut) + fig, _ = plotting.plot_damage_map_3d(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, contour=not is_dut, damage=damage, ion_name=ion_name, server=server, dut=is_dut) figs.append(fig) - fig, _ = plotting.plot_damage_error_3d(damage_map=damage_map, error_map=errors[damage] if not is_dut else dut_error_map, map_centers_x=centers_x, map_centers_y=centers_y, contour=not is_dut, damage=damage, server=server, dut=is_dut) + fig, _ = plotting.plot_damage_error_3d(damage_map=damage_map, error_map=errors[damage] if not is_dut else dut_error_map, map_centers_x=centers_x, map_centers_y=centers_y, contour=not is_dut, damage=damage, ion_name=ion_name, server=server, dut=is_dut) figs.append(fig) - fig, _ = plotting.plot_damage_map_2d(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, damage=damage, server=server, dut=is_dut) + fig, _ = plotting.plot_damage_map_2d(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, damage=damage, ion_name=ion_name, server=server, dut=is_dut) figs.append(fig) - fig, _ = plotting.plot_damage_map_contourf(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, damage=damage, server=server, dut=is_dut) + fig, _ = plotting.plot_damage_map_contourf(damage_map=damage_map, map_centers_x=centers_x, map_centers_y=centers_y, damage=damage, ion_name=ion_name, server=server, dut=is_dut) figs.append(fig) logging.info("Finished plotting.") diff --git a/irrad_control/analysis/dtype.py b/irrad_control/analysis/dtype.py index 0f0fcfb7..49fdf0f9 100644 --- a/irrad_control/analysis/dtype.py +++ b/irrad_control/analysis/dtype.py @@ -37,8 +37,8 @@ ('row_stop_y', ' 1.0: self.send_cmd(hostname=server, target='__scan__', cmd='handle_event', cmd_data={'kwargs': {'event': 'beam_ok'}}) self._beam_down[server] = False + + def check_finish(self, server, eta_n_scans): + + if eta_n_scans == 0 and self.tab_widgets[server]['scan'].auto_finish_scan: + self.send_cmd(hostname=server, target='__scan__', cmd='handle_event', cmd_data={'kwargs': {'event': 'finish'}}) + def scan_status(self, server, status='started'): read_only_state = status == 'started' diff --git a/irrad_control/gui/widgets/control_widgets.py b/irrad_control/gui/widgets/control_widgets.py index 173d53c4..d9800d02 100644 --- a/irrad_control/gui/widgets/control_widgets.py +++ b/irrad_control/gui/widgets/control_widgets.py @@ -394,22 +394,24 @@ class ScanControlWidget(ControlWidget): scanParamsUpdated = QtCore.pyqtSignal(dict) - def __init__(self, server, parent=None): + def __init__(self, server, daq_setup, parent=None): super(ScanControlWidget, self).__init__(name='Scan Control', parent=parent) # Store server hostname self.server = server + self.daq_setup = daq_setup self.scan_params = {'row_sep': 1.0, 'scan_speed': 70.0, 'min_current': 0.0, - 'aim_damage': 'niel', - 'aim_value': 2e15, + 'aim_damage': 'primary', + 'aim_value': 1e14, 'rel_start': [0.0, 0.0], 'rel_end': [0.0, 0.0]} self._after_scan_container = None self.n_rows = None + self.auto_finish_scan = True self._init_ui() @@ -424,6 +426,33 @@ def update_scan_params(self, **kwargs): self.scan_params.update(kwargs) self.scanParamsUpdated.emit(self.scan_params) + def _damage_toggled(self, damage_buttons, sv, se): + + # Get active button + active = damage_buttons.checkedButton() + if active is None: + damage = 'primary' + else: + damage = active.text().lower() + + if damage == 'primary': + se.setSuffix(f" {self.daq_setup['ion']} / cm^2") + se.setRange(3, 20) + sv.setValue(1) + se.setValue(14) + elif damage == 'neq': + se.setSuffix(f" neq / cm^2") + se.setRange(3, 20) + sv.setValue(1) + se.setValue(14) + else: + se.setRange(1, 6) + se.setSuffix(' Mrad') + sv.setValue(1) + se.setValue(2) + + self.update_scan_params(aim_damage=damage) + def _init_ui(self): # Step size @@ -461,21 +490,36 @@ def _init_ui(self): # Fluence label_aim_damage = QtWidgets.QLabel('Aim damage:') label_aim_damage.setToolTip('Select type and quantity of damage to be introduced to DUT') - rbtn_niel = QtWidgets.QRadioButton('NIEL') + rbtn_neq = QtWidgets.QRadioButton('NEQ') rbtn_tid = QtWidgets.QRadioButton('TID') + rbtn_primary = QtWidgets.QRadioButton('Primary') + damage_buttons = QtWidgets.QButtonGroup() + damage_buttons.addButton(rbtn_neq) + damage_buttons.addButton(rbtn_tid) + damage_buttons.addButton(rbtn_primary) + layout_aim_damage = QtWidgets.QHBoxLayout() + + # Add radio buttons for different types of damage + if self.daq_setup['kappa'] is None: + damage_buttons.removeButton(rbtn_neq) + if self.daq_setup['stopping_power'] is None: + damage_buttons.removeButton(rbtn_tid) + spx_damage_val = QtWidgets.QDoubleSpinBox() spx_damage_val.setRange(1e-3, 10) spx_damage_val.setDecimals(3) spx_damage_exp = QtWidgets.QSpinBox() spx_damage_exp.setPrefix('e ') - rbtn_niel.toggled.connect(lambda toggled, sv=spx_damage_val, se=spx_damage_exp: - (se.setRange(3, 20), se.setSuffix(' neq / cm^2'), sv.setValue(2), se.setValue(15))) - rbtn_tid.toggled.connect(lambda toggled, sv=spx_damage_val, se=spx_damage_exp: - (se.setRange(1, 6), se.setSuffix(' Mrad'), sv.setValue(1), se.setValue(3))) - rbtn_niel.toggled.connect(lambda toggled: self.update_scan_params(aim_damage='niel' if toggled else 'tid')) + + for btn in damage_buttons.buttons(): + btn.toggled.connect(lambda _, bg=damage_buttons, sv=spx_damage_val, se=spx_damage_exp: self._damage_toggled(bg, sv, se)) + layout_aim_damage.addWidget(btn) + spx_damage_val.valueChanged.connect(lambda v: self.update_scan_params(aim_value=float(f'{v}e{spx_damage_exp.value()}'))) spx_damage_exp.valueChanged.connect(lambda v: self.update_scan_params(aim_value=float(f'{spx_damage_val.value()}e{v}'))) - rbtn_niel.toggle() + + # Toggle initially + rbtn_primary.toggle() # Start point label_start = QtWidgets.QLabel('Relative start point:') @@ -511,6 +555,12 @@ def _init_ui(self): spx_end_x.valueChanged.connect(lambda v: self.update_scan_params(rel_end=[v, spx_end_y.value()])) spx_end_y.valueChanged.connect(lambda v: self.update_scan_params(rel_start=[spx_end_x.value(), v])) + # Auto finish scan + checkbox_auto_finish = QtWidgets.QCheckBox('Auto finish scan') + checkbox_auto_finish.setToolTip("Automatically finish scan procedure when target damage is reached.") + checkbox_auto_finish.stateChanged.connect(lambda state: setattr(self, 'auto_finish_scan', bool(state))) + checkbox_auto_finish.setChecked(True) + # Scan btn_start = QtWidgets.QPushButton('START') btn_start.setToolTip("Start scan.") @@ -553,12 +603,13 @@ def _init_ui(self): layout_scan.addWidget(btn_pause) layout_scan.addWidget(btn_finish) layout_scan.addWidget(btn_stop) + layout_scan.addWidget(checkbox_auto_finish) # Add to layout self.add_widget(widget=[label_row_sep, spx_row_sep]) self.add_widget(widget=[label_scan_speed, spx_scan_speed]) self.add_widget(widget=[label_min_current, spx_min_current]) - self.add_widget(widget=[label_aim_damage, rbtn_niel, rbtn_tid]) + self.add_widget(widget=[label_aim_damage, layout_aim_damage]) self.add_widget(widget=[QtWidgets.QLabel(''), spx_damage_val, spx_damage_exp]) self.add_widget(widget=[label_start, spx_start_x, spx_start_y]) self.add_widget(widget=[label_end, spx_end_x, spx_end_y]) diff --git a/irrad_control/ions/__init__.py b/irrad_control/ions/__init__.py index 80c0bc48..c0ea80dc 100644 --- a/irrad_control/ions/__init__.py +++ b/irrad_control/ions/__init__.py @@ -4,6 +4,8 @@ import numpy as np from importlib import import_module +from irrad_control.analysis.constants import elementary_charge + class IrradIon(object): @@ -75,6 +77,24 @@ def _select_data(self, data_type, at_energy=None, at_index=None, as_dict=False, return _data + def rate(self, current): + """ + Returns the *rate* in particles / second, calculated from *current* in Ampere. + For IrradIons with n_charge = 1 current / elementary charge and rate are the same + + Parameters + ---------- + current : float + Ion beam current in Ampere + + Returns + ------- + ion rate + Number of ions per second + """ + # Ions per second + return current / (self.n_charge * elementary_charge) + def ekin_range(self): """ Return kinetic energy range as a tuple in MeV @@ -114,6 +134,14 @@ def hardness_factor(self, at_energy=None, at_index=None, as_dict=False, return_i # Generate all ions def get_ions(): + """ + Returns a dict with all available IrradIon.name, IrradIon key-value pairs + + Returns + ------- + dict + dict with IrradIon.names as keys and the respective IrradIon as value + """ ions = [] for ion in os.listdir(os.path.dirname(__file__)): try: diff --git a/irrad_control/ions/alpha/dut_energy.dat b/irrad_control/ions/alpha/dut_energy.dat new file mode 100644 index 00000000..9de353f3 --- /dev/null +++ b/irrad_control/ions/alpha/dut_energy.dat @@ -0,0 +1,18 @@ +### This file contains beam monitor calibration data for alphas ### +# Initial energy / MeV, energy at DUT mean / MeV, energy at DUT sigma / MeV +# [('initial_energy', '= 0: @@ -523,9 +523,8 @@ def handle_data(self, data): # FIXME: more precise result would be helpful pass - # Finish the scan programatically - if data['data']['eta_n_scans'] == 0: - self.send_cmd(server, 'stage', 'finish') + # Finish the scan programatically, if wanted + self.control_tab.check_finish(server=server, eta_n_scans=data['data']['eta_n_scans']) elif data['meta']['type'] == 'temp_arduino':