Commit 386d947c authored by Carsten Richter's avatar Carsten Richter

Merge branch 'energy-scan' into 'master'

Add support for multiple energies scan

Closes #47

See merge request kmap/xsocs!94
parents f1722b64 0b029764
Pipeline #6710 canceled with stages
......@@ -44,6 +44,7 @@ img_base = '/path/to/img/dir/'
# the beam energy (note that this value can be changed later when calling the
# img_2_qpeak function)
# Set to None to read it from scan headers
beam_energy = 8000.
# merge_scan_data will take all scans from the spec file that have a matching
......
......@@ -361,7 +361,7 @@ class XsocsGui(Qt.QMainWindow):
def __openProject(self):
dialog = ProjectChooserDialog(self)
dialog.setFixedWidth(500)
dialog.setMinimumWidth(500)
rc = dialog.exec_()
if rc == Qt.QDialog.Accepted:
projectFile = dialog.projectFile
......
......@@ -910,8 +910,11 @@ class MergeWidget(Qt.QDialog):
try:
name = 'Beam Energy'
self.__merger.beam_energy = \
assert_non_none(self.__acqParamWid.beam_energy)
if self.__acqParamWid.isBeamEnergyEnabled():
self.__merger.beam_energy = \
assert_non_none(self.__acqParamWid.beam_energy)
else: # Read from scan
self.__merger.beam_energy = None
name = 'Direct beam'
dir_beam_v = assert_non_none(self.__acqParamWid.direct_beam_v)
......@@ -1180,15 +1183,28 @@ class MergeWidget(Qt.QDialog):
self.__acqParamsGbx.setEnabled(len(selected_ids) > 0)
self.__imageROIGbx.setEnabled(len(selected_ids) > 0)
self.__acqParamWid.clear()
# Set-up default values from first selected scan
if len(selected_ids) > 0:
energies = []
for scan_id in selected_ids:
calib = self.__merger.get_calibration(scan_id)
if 'mononrj' in calib:
energies.append(calib['mononrj'] * 1000.)
else: # Missing energy
energies = None
break
# Disable beam energy input if available in scans
self.__acqParamWid.setBeamEnergyEnabled(energies is None)
if energies is not None:
self.__acqParamWid.setBeamEnergyFromList(energies)
calib = self.__merger.get_calibration(selected_ids[0])
if calib:
_logger.info("Load calibration information from scan %s",
selected_ids[0])
if 'mononrj' in calib:
# Convert from keV to eV
self.__acqParamWid.beam_energy = calib['mononrj'] * 1000
if 'pixperdeg' in calib:
self.__acqParamWid.chperdeg_v = calib['pixperdeg']
self.__acqParamWid.chperdeg_h = calib['pixperdeg']
......@@ -1196,6 +1212,10 @@ class MergeWidget(Qt.QDialog):
self.__acqParamWid.direct_beam_h = calib['cen_pix_x']
if 'cen_pix_y' in calib:
self.__acqParamWid.direct_beam_v = calib['cen_pix_y']
if energies is None and 'mononrj' in calib:
# Not all scans have beam energy, use that of first scan
# Convert from keV to eV
self.__acqParamWid.beam_energy = calib['mononrj'] * 1000
self.__totalScansEdit.setText(str(len(matched_ids)))
self.__selectedScansEdit.setText(str(len(selected_ids)))
......
......@@ -61,7 +61,7 @@ class ConversionParamsWidget(Qt.QWidget):
def __init__(self,
medfiltDims=None,
normalizers=None,
beamEnergy=None,
beamEnergies=None,
directBeam=None,
channelsPerDegree=None,
**kwargs):
......@@ -82,7 +82,8 @@ class ConversionParamsWidget(Qt.QWidget):
self.__acqParamWid = AcqParamsWidget()
# Set default values with provided info
self.__acqParamWid.beam_energy = beamEnergy
self.__acqParamWid.setBeamEnergyFromList(beamEnergies)
self.__acqParamWid.setBeamEnergyEnabled(len(set(beamEnergies)) == 1)
self.__acqParamWid.direct_beam_v = directBeam[0]
self.__acqParamWid.direct_beam_h = directBeam[1]
self.__acqParamWid.chperdeg_v = channelsPerDegree[0]
......@@ -369,7 +370,10 @@ class ConversionParamsWidget(Qt.QWidget):
def getBeamEnergy(self):
"""Returns beam energy in eV or None if no input"""
return self.__acqParamWid.beam_energy
if self.__acqParamWid.isBeamEnergyEnabled():
return self.__acqParamWid.beam_energy
else:
return None
def getDirectBeam(self):
"""Returns direct beam calibration position None if no input
......@@ -555,8 +559,7 @@ class QSpaceWidget(Qt.QDialog):
self.__nAnglesLabel, line, 1, alignment=Qt.Qt.AlignLeft)
infoLayout.setColumnStretch(2, 1)
self.__scansTable = Qt.QTableWidget(0, 2)
self.__scansTable.verticalHeader().hide()
self.__scansTable = Qt.QTableWidget(0, 4)
grpLayout.addWidget(self.__scansTable, alignment=Qt.Qt.AlignLeft)
# ################
......@@ -568,18 +571,18 @@ class QSpaceWidget(Qt.QDialog):
topLayout.addWidget(convGbx, 1, 0, alignment=Qt.Qt.AlignTop)
if entries: # Get default config from first entry
beamEnergy = xsocsH5.beam_energy(entries[0])
directBeam = xsocsH5.direct_beam(entries[0])
channelsPerDegree = xsocsH5.chan_per_deg(entries[0])
else: # This should not happen
beamEnergy = ''
directBeam = '', ''
channelsPerDegree = '', ''
beamEnergies = [xsocsH5.beam_energy(entry) for entry in entries]
self.__paramsWid = ConversionParamsWidget(
medfiltDims=self.__converter.medfilt_dims,
normalizers=xsocsH5.normalizers(),
beamEnergy=beamEnergy,
beamEnergies=beamEnergies,
directBeam=directBeam,
channelsPerDegree=channelsPerDegree)
self.__paramsWid.setNormalizer(normalizer)
......@@ -622,7 +625,7 @@ class QSpaceWidget(Qt.QDialog):
cancelBn.clicked.connect(self.close)
self.__convertBn.clicked.connect(self.__slotConvertBnClicked)
self.__fillScansInfos()
self.__fillScansInfo()
# Set default qspace bin size
if entries:
......@@ -647,7 +650,7 @@ class QSpaceWidget(Qt.QDialog):
img_size=xsocsH5.image_size(entry),
center_chan=xsocsH5.direct_beam(entry),
chan_per_deg=xsocsH5.chan_per_deg(entry),
beam_energy=xsocsH5.beam_energy(entry),
beam_energy=beamEnergies,
phi=phi,
eta=eta,
nu=nu,
......@@ -750,7 +753,7 @@ class QSpaceWidget(Qt.QDialog):
procDialog.deleteLater()
def __slotConvertDone(self):
"""Method called when the conversion has been completed succesfully."""
"""Method called when the conversion has been completed successfully."""
converter = self.__converter
if not converter:
return
......@@ -769,7 +772,7 @@ class QSpaceWidget(Qt.QDialog):
self._setStatus(self.StatusUnknown)
qspaceH5 = property(lambda self: self.__qspaceH5)
"""Written file (set when the conversion was succesful, None otherwise."""
"""Written file (set when the conversion was successful, None otherwise."""
status = property(lambda self: self.__status)
"""Status of the widget."""
......@@ -784,38 +787,48 @@ class QSpaceWidget(Qt.QDialog):
''.format(status))
self.__status = status
def __fillScansInfos(self):
def __fillScansInfo(self):
"""Fills the QTableWidget with info found in the input file"""
converter = self.__converter
if converter is None:
if self.__converter is None:
return
scans = converter.scans
scans = self.__converter.scans
self.__scansTable.verticalHeader().hide()
self.__scansTable.setHorizontalHeaderLabels(
['scan', 'eta (°)', 'phi (°)', 'energy (eV)'])
self.__scansTable.setSelectionMode(self.__scansTable.NoSelection)
self.__scansTable.setRowCount(len(scans))
for row, scan in enumerate(scans):
params = converter.scan_params(scan)
item = Qt.QTableWidgetItem(scan)
item.setFlags(item.flags() ^ Qt.Qt.ItemIsEditable)
self.__scansTable.setItem(row, 0, item)
item = Qt.QTableWidgetItem(str(params['angle']))
item.setFlags(item.flags() ^ Qt.Qt.ItemIsEditable)
self.__scansTable.setItem(row, 1, item)
with XsocsH5(self.__converter.xsocsH5_f, mode='r') as h5f:
for row, scan in enumerate(scans):
eta = h5f.positioner(scan, 'eta')
phi = h5f.positioner(scan, 'phi')
beam_energy = h5f.beam_energy(entry=scan)
for column, value in enumerate(
(scan, '%g' % eta, '%g' % phi, '%g' % beam_energy)):
item = Qt.QTableWidgetItem(value)
self.__scansTable.setItem(row, column, item)
self.__scansTable.resizeColumnsToContents()
width = (sum([self.__scansTable.columnWidth(i)
for i in range(self.__scansTable.columnCount())]) +
self.__scansTable.verticalHeader().width() +
20)
# TODO : the size is wrong when the
# verticalScrollBar isnt displayed yet
# verticalScrollBar is not displayed yet
# scans_table.verticalScrollBar().width())
size = self.__scansTable.minimumSize()
size.setWidth(width)
self.__scansTable.setMinimumSize(size)
# TODO : warning if the ROI is empty (too small to contain images)
params = converter.scan_params(scans[0])
roi = converter.roi
params = self.__converter.scan_params(scans[0])
roi = self.__converter.roi
if roi is None:
xMin = xMax = yMin = yMax = 'ns'
else:
......@@ -826,7 +839,7 @@ class QSpaceWidget(Qt.QDialog):
self.__roiYMinEdit.setText(str(yMin))
self.__roiYMaxEdit.setText(str(yMax))
indices = converter.sample_indices
indices = self.__converter.sample_indices
nImgTxt = '{0} / {1}'.format(len(indices),
params['n_images'])
self.__nImgLabel.setText(nImgTxt)
......
......@@ -63,6 +63,10 @@ IntensityViewEvent = namedtuple('IntensityViewEvent', ['roi',
'shiftFile'])
_BEAM_ENERGY_NAME = 'beam energy'
"""Name used to select beam energy as a pseudo-positioner"""
class CurvesList(Qt.QTreeWidget):
"""Widget displaying the list of curves in a plot
......@@ -271,7 +275,10 @@ class EntriesTable(Qt.QTableWidget):
with self.__intensityGroup.xsocsH5 as xsocsH5:
for row in range(1, self.rowCount()):
value = xsocsH5.positioner(self.__getEntry(row), name)
if name == _BEAM_ENERGY_NAME:
value = xsocsH5.beam_energy(self.__getEntry(row))
else:
value = xsocsH5.positioner(self.__getEntry(row), name)
item = self.item(row, 0)
item.setText(str(value))
......@@ -389,7 +396,11 @@ class IntensityView(Qt.QMainWindow):
for positioner in ('eta', 'phi'): # Add mostly used positioners first
if positioner in positioners:
self.__sortByComboBox.addItem(positioner)
self.__sortByComboBox.insertSeparator(2)
# Add beam energy
self.__sortByComboBox.addItem(_BEAM_ENERGY_NAME)
self.__sortByComboBox.insertSeparator(3)
for name in positioners:
self.__sortByComboBox.addItem(name)
self.__sortByComboBox.currentTextChanged.connect(
......@@ -685,7 +696,10 @@ class IntensityView(Qt.QMainWindow):
item = self.__iGroup.getIntensityItem(entry)
intensities[entryIdx] = item.getPointValue(selectedIndex)
xValue = self.__iGroup.xsocsH5.positioner(entry, positionerName)
if positionerName == _BEAM_ENERGY_NAME:
xValue = self.__iGroup.xsocsH5.beam_energy(entry)
else:
xValue = self.__iGroup.xsocsH5.positioner(entry, positionerName)
xData[entryIdx] = xValue if xValue is not None else entryIdx
if normalizerName:
......
......@@ -69,19 +69,18 @@ class AcqParamsWidget(Qt.QWidget):
layout = Qt.QGridLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# ===========
# beam energy
# ===========
row = 0
h_layout = Qt.QHBoxLayout()
self.__beam_nrg_edit = _dblLineEditWidget(8, read_only)
self.__energyWidget = Qt.QWidget()
h_layout = Qt.QHBoxLayout(self.__energyWidget)
self.__beam_nrg_edit = _dblLineEditWidget(12, read_only)
h_layout.addWidget(self.__beam_nrg_edit)
h_layout.addWidget(Qt.QLabel('<b>eV</b>'))
layout.addWidget(Qt.QLabel('Beam energy :'), row, 0)
layout.addLayout(h_layout, row, 1,
layout.addWidget(self.__energyWidget, row, 1,
alignment=Qt.Qt.AlignLeft | Qt.Qt.AlignTop)
# ===
......@@ -154,6 +153,20 @@ class AcqParamsWidget(Qt.QWidget):
self.__chpdeg_h_edit.clear()
self.__chpdeg_v_edit.clear()
def isBeamEnergyEnabled(self):
"""Returns whether beam energy is enabled on not.
:rtype: bool
"""
return self.__energyWidget.isEnabled()
def setBeamEnergyEnabled(self, enabled):
"""Enable/disable beam energy widget
:param bool enabled:
"""
self.__energyWidget.setEnabled(enabled)
@property
def beam_energy(self):
text = self.__beam_nrg_edit.text()
......@@ -165,6 +178,20 @@ class AcqParamsWidget(Qt.QWidget):
def beam_energy(self, beam_energy):
self.__beam_nrg_edit.setText(str(beam_energy))
def setBeamEnergyFromList(self, energies):
"""Set the beam energy from a list of energies
:param List[float] energies:
"""
nbEnergies = len(set(energies))
if nbEnergies == 0:
text = ''
elif nbEnergies == 1: # All energies are the same
text = "%g" % energies[0]
else:
text = "[%g;%g]" % (min(energies), max(energies))
self.__beam_nrg_edit.setText(text)
@property
def direct_beam_h(self):
text = self.__dir_beam_h_edit.text()
......
......@@ -187,20 +187,6 @@ class ProjectSummaryWidget(Qt.QWidget):
inputItem.addChild(Qt.QTreeWidgetItem(['Full path', xsocsFile]))
view.addTopLevelItem(inputItem)
# getting scan angles
errMsg = 'Failed to read scan angles.'
# TODO : check that there are at least 2 angles
text = '{0} [{1} -> {2}]'.format(
str(len(entries)),
str(xsocsH5.scan_angle(entries[0])),
str(xsocsH5.scan_angle(entries[-1])))
entriesItem = Qt.QTreeWidgetItem(['Angles', text])
for entryIdx, entry in enumerate(entries):
text = 'eta = {0}'.format(str(xsocsH5.scan_angle(entry)))
entryItem = Qt.QTreeWidgetItem([str(entryIdx), text])
entriesItem.addChild(entryItem)
view.addTopLevelItem(entriesItem)
# getting acquisition params
errMsg = 'Failed to read Acquisition parameters.'
title = ' '.join(str(value) for value in params.values())
......@@ -211,7 +197,31 @@ class ProjectSummaryWidget(Qt.QWidget):
commandItem.addChild(Qt.QTreeWidgetItem([key, str(value)]))
view.addTopLevelItem(commandItem)
# getting scan angles and energies
errMsg = 'Failed to read scan angles and energies.'
text = '{nb_scan} (eta:[{eta1:g}°;{eta2:g}°] ' \
'phi:[{phi1:g}°;{phi2:g}°] ' \
'energy:[{energy1:g}eV;{energy2:g}eV])'.format(
nb_scan=len(entries),
eta1=xsocsH5.positioner(entries[0], 'eta'),
eta2=xsocsH5.positioner(entries[-1], 'eta'),
phi1=xsocsH5.positioner(entries[0], 'phi'),
phi2=xsocsH5.positioner(entries[-1], 'phi'),
energy1=xsocsH5.beam_energy(entries[0]),
energy2=xsocsH5.beam_energy(entries[-1]))
entriesItem = Qt.QTreeWidgetItem(['Scans', text])
for entry in entries:
text = 'eta:{eta:g}°\tphi:{phi:g}°\tenergy:{nrj:g}eV'.format(
eta=xsocsH5.positioner(entry, 'eta'),
phi=xsocsH5.positioner(entry, 'phi'),
nrj=xsocsH5.beam_energy(entry))
entryItem = Qt.QTreeWidgetItem([entry, text])
entriesItem.addChild(entryItem)
view.addTopLevelItem(entriesItem)
for key, value in xsocsH5.acquisition_params(entries[0]).items():
if key == 'beam_energy':
continue # Skip beam_energy
view.addTopLevelItem(Qt.QTreeWidgetItem([key, str(value)]))
except Exception as ex:
......
......@@ -147,6 +147,14 @@ class KmapMerger(object):
self.__thread = None
self.beam_energy = None
"""Beam energy in eV to set for all scans.
If None (the default) the energy is read from SPEC for each scan.
Union[float,None]
"""
self.prefix = None
self.__set_status(self.READY)
......@@ -266,13 +274,22 @@ class KmapMerger(object):
results = {}
self.__proc_indices = {}
for proc_idx, (scan_id, infos) in enumerate(scans.items()):
# Get beam energy
if self.beam_energy is not None: # Use forced beam energy
beam_energy = self.beam_energy
else: # Read beam energy from each scan header
nrj_kev = self.get_calibration(scan_id).get('mononrj', None)
if nrj_kev is None:
raise RuntimeError("Cannot read beam energy")
beam_energy = nrj_kev * 1000. # From keV to eV
args = (scan_id,
proc_idx,
self.__spec_h5,
self.__output_dir,
infos['output'],
infos['image'],
self.beam_energy,
beam_energy,
self.chan_per_deg,
self.center_chan,
self.compression,
......@@ -465,8 +482,6 @@ class KmapMerger(object):
def check_parameters(self):
errors = []
if self.beam_energy is None:
errors.append('invalid "Beam energy"')
if self.output_dir is None:
errors.append('invalid "Output directory"')
if self.center_chan is None:
......@@ -475,12 +490,6 @@ class KmapMerger(object):
errors.append('invalid "Channel per degree"')
return errors
beam_energy = property(lambda self: self.__params['beam_energy'])
@beam_energy.setter
def beam_energy(self, beam_energy):
self.__params['beam_energy'] = float(beam_energy)
chan_per_deg = property(lambda self: self.__params['chan_per_deg'])
@chan_per_deg.setter
......
......@@ -31,6 +31,7 @@ __date__ = "01/03/2016"
import logging
import numbers
import os
import time
import ctypes
......@@ -68,7 +69,8 @@ def qspace_conversion(img_size, center_chan, chan_per_deg,
Calibration center channel in image coordinates (dim0, dim1)
:param List[float] chan_per_deg:
Channel per degree calibration in image coordinates (dim0, dim1)
:param float beam_energy: Energy in eV
:param Union[float,numpy.ndarray] beam_energy:
Energy in eV. Either a float or a 1D array with energy for each image
:param numpy.ndarray phi: 1D array of phi angle for each image
:param numpy.ndarray eta: 1D array of eta angle for each image
:param numpy.ndarray nu: 1D array of nu angle for each image
......@@ -86,6 +88,12 @@ def qspace_conversion(img_size, center_chan, chan_per_deg,
nu = np.array(nu, copy=False, dtype=np.float64)
delta = np.array(delta, copy=False, dtype=np.float64)
n_images = len(eta)
if isinstance(beam_energy, numbers.Real): # Convert to array
beam_energy = [beam_energy] * n_images
beam_energy = np.array(beam_energy, copy=False, dtype=np.float64)
qconv = xu.experiment.QConversion(['y-', 'z-'],
['z-', 'y-'],
[1, 0, 0])
......@@ -97,7 +105,6 @@ def qspace_conversion(img_size, center_chan, chan_per_deg,
# (righthanded)
hxrd = xu.HXRD([1, 0, 0],
[0, 0, 1],
en=beam_energy,
qconv=qconv)
hxrd.Ang2Q.init_area('z-',
......@@ -109,8 +116,6 @@ def qspace_conversion(img_size, center_chan, chan_per_deg,
chpdeg1=chan_per_deg[0],
chpdeg2=chan_per_deg[1])
n_images = len(eta)
# shape of the array that will store the q vectors for all
# rocking angles
q_shape = n_images, img_size[0], img_size[1], 3
......@@ -120,7 +125,8 @@ def qspace_conversion(img_size, center_chan, chan_per_deg,
for index in range(n_images):
qx, qy, qz = hxrd.Ang2Q.area(
eta[index], phi[index], nu[index], delta[index])
eta[index], phi[index], nu[index], delta[index],
en=beam_energy[index])
if coordinates == QSpaceCoordinates.CARTESIAN:
q_ar[index, :, :, 0] = qx
......@@ -615,7 +621,9 @@ class QSpaceConverter(object):
check_values(params, 'n_positions', 'Number of X/Y positions')
check_values(params, 'img_size', 'Images size')
if beam_energy_check:
check_values(params, 'beam_energy', 'Beam energy')
# Check that beam_energy is defined in each scan
if None in [scan_p['beam_energy'] for scan_p in params.values()]:
errors.append('Beam energy not available for all scans.')
if channels_per_degree_check:
check_values(params, 'chan_per_deg', 'Chan. per deg.')
if direct_beam_check:
......@@ -674,11 +682,12 @@ class QSpaceConverter(object):
first_param = params[entries[0]]
if beam_energy is None: # Load it from first entry
beam_energy = first_param['beam_energy']
if beam_energy is None:
raise ValueError('Invalid/missing beam energy : {0}.'
''.format(beam_energy))
# Load it from entries
beam_energy = [scan_p['beam_energy'] for scan_p in params.values()]
if None in beam_energy:
raise ValueError(
'Missing beam energy: {0}'.format(beam_energy))
if chan_per_deg is None: # Load it from first entry
chan_per_deg = first_param['chan_per_deg']
......@@ -749,7 +758,9 @@ class QSpaceConverter(object):
n_xy = len(sample_indices)
_logger.info('Parameters :')
_logger.info('\t- beam energy : {0}'.format(beam_energy))
_logger.info('\t- beam energy : {0}'.format(
self.beam_energy if self.beam_energy is not None
else 'Loaded from scans'))
_logger.info('\t- center channel : {0}'.format(center_chan))
_logger.info('\t- image roi offset : {0}'.format(image_roi_offset))
_logger.info('\t- channel per degree : {0}'.format(chan_per_deg))
......@@ -931,7 +942,7 @@ class QSpaceConverter(object):
qspace_sum_chunks=qspace_sum_chunks,
overwrite=self.__overwrite,
shiftH5=shiftH5,
beam_energy=beam_energy,
beam_energy=self.beam_energy,
direct_beam=center_chan,
channels_per_degree=chan_per_deg,
normalizer=normalizer,
......@@ -1255,7 +1266,8 @@ def _create_result_file(h5_fn,
qspace_h5.set_qspace_dimension_values(q_dim0, q_dim1, q_dim2)
qspace_h5.set_medfilt_dims(medfilt_dims)
qspace_h5.set_sample_roi(sample_roi)
qspace_h5.set_beam_energy(beam_energy)
if beam_energy is not None:
qspace_h5.set_beam_energy(beam_energy)
qspace_h5.set_direct_beam(direct_beam)
qspace_h5.set_channels_per_degree(channels_per_degree)
qspace_h5.set_maxipix_correction(maxipix_correction)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment