source: sasview/src/sas/sasgui/perspectives/file_converter/converter_panel.py @ a3c538e1

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since a3c538e1 was a3c538e1, checked in by lewis, 8 years ago

Warn when qax input is wrong and fix loader for single-frame files

  • Property mode set to 100644
File size: 18.0 KB
RevLine 
[77d92cd]1"""
2This module provides a GUI for the file converter
3"""
4
5import wx
6import sys
[a58706d]7import numpy as np
[77d92cd]8from wx.lib.scrolledpanel import ScrolledPanel
9from sas.sasgui.guiframe.panel_base import PanelBase
10from sas.sasgui.perspectives.calculator import calculator_widgets as widget
[c9a519f]11from sas.sasgui.perspectives.file_converter.converter_widgets import VectorInput
[de0df2c]12from sas.sasgui.perspectives.file_converter.meta_panels import MetadataWindow
13from sas.sasgui.perspectives.file_converter.meta_panels import DetectorPanel
[2a7722b]14from sas.sasgui.perspectives.file_converter.meta_panels import SamplePanel
[55bc56bc]15from sas.sasgui.perspectives.file_converter.meta_panels import SourcePanel
[eb8da5f]16from sas.sasgui.perspectives.file_converter.frame_select_dialog import FrameSelectDialog
[a58706d]17from sas.sasgui.guiframe.events import StatusEvent
[11794f2]18from sas.sasgui.guiframe.documentation_window import DocumentationWindow
[a58706d]19from sas.sasgui.guiframe.dataFitting import Data1D
[fdbea3c]20from sas.sasgui.guiframe.utils import check_float
[8976865]21from sas.sasgui.perspectives.file_converter.cansas_writer import CansasWriter
[4b862c4]22from sas.sasgui.perspectives.file_converter.bsl_loader import BSLLoader
[f2b3f28]23from sas.sascalc.dataloader.data_info import Detector
[2a7722b]24from sas.sascalc.dataloader.data_info import Sample
[55bc56bc]25from sas.sascalc.dataloader.data_info import Source
[fdbea3c]26from sas.sascalc.dataloader.data_info import Vector
[77d92cd]27
28# Panel size
29if sys.platform.count("win32") > 0:
30    PANEL_TOP = 0
31    _STATICBOX_WIDTH = 410
32    _BOX_WIDTH = 200
[36f4debb]33    PANEL_SIZE = 480
[77d92cd]34    FONT_VARIANT = 0
35else:
36    PANEL_TOP = 60
37    _STATICBOX_WIDTH = 430
38    _BOX_WIDTH = 200
[36f4debb]39    PANEL_SIZE = 500
[77d92cd]40    FONT_VARIANT = 1
41
42class ConverterPanel(ScrolledPanel, PanelBase):
43
44    def __init__(self, parent, base=None, *args, **kwargs):
45        ScrolledPanel.__init__(self, parent, *args, **kwargs)
46        PanelBase.__init__(self)
[a58706d]47        self.SetupScrolling()
[77d92cd]48        self.SetWindowVariant(variant=FONT_VARIANT)
49
50        self.base = base
51        self.parent = parent
[de0df2c]52        self.meta_frames = []
[77d92cd]53
[a58706d]54        self.q_input = None
55        self.iq_input = None
56        self.output = None
[4b862c4]57        self.data_type = "ascii"
[a58706d]58
[503cc34]59        self.title = None
60        self.run = None
61        self.run_name = None
62        self.instrument = None
63        self.detector = Detector()
64        self.sample = Sample()
65        self.source = Source()
66        self.properties = ['title', 'run', 'run_name', 'instrument']
67
68        self.detector.name = ''
69        self.source.radiation = 'neutron'
[4b862c4]70
[a58706d]71        self._do_layout()
[77d92cd]72        self.SetAutoLayout(True)
73        self.Layout()
74
[94f4518]75    def convert_to_cansas(self, frame_data, filename, single_file):
[8976865]76        reader = CansasWriter()
[ea2a7348]77        entry_attrs = None
78        if self.run_name is not None:
79            entry_attrs = { 'name': self.run_name }
[94f4518]80        if single_file:
81            reader.write(filename, frame_data,
82                sasentry_attrs=entry_attrs)
83        else:
84            # strip extension from filename
85            ext = "." + filename.split('.')[-1]
86            name = filename.replace(ext, '')
87            for i in range(len(frame_data)):
88                f_name = "{}{}{}".format(name, i+1, ext)
89                reader.write(f_name, [frame_data[i]],
90                    sasentry_attrs=entry_attrs)
[a58706d]91
92    def extract_data(self, filename):
93        data = np.loadtxt(filename, dtype=str)
94
[ff790b3]95        if len(data.shape) != 1:
96            msg = "Error reading {}: Only one column of data is allowed"
97            raise Exception(msg.format(filename.split('\\')[-1]))
98
[a58706d]99        is_float = True
100        try:
101            float(data[0])
102        except:
103            is_float = False
104
105        if not is_float:
106            end_char = data[0][-1]
107            # If lines end with comma or semi-colon, trim the last character
108            if end_char == ',' or end_char == ';':
109                data = map(lambda s: s[0:-1], data)
110            else:
111                msg = ("Error reading {}: Lines must end with a digit, comma "
112                    "or semi-colon").format(filename.split('\\')[-1])
113                raise Exception(msg)
114
115        return np.array(data, dtype=np.float32)
116
[eb8da5f]117    def ask_frame_range(self, n_frames):
118        valid_input = False
119        dlg = FrameSelectDialog(n_frames)
120        frames = None
121        increment = None
[94f4518]122        single_file = True
[eb8da5f]123        while not valid_input:
124            if dlg.ShowModal() == wx.ID_OK:
125                msg = ""
126                try:
127                    first_frame = int(dlg.first_input.GetValue())
128                    last_frame = int(dlg.last_input.GetValue())
129                    increment = int(dlg.increment_input.GetValue())
[94f4518]130                    single_file = dlg.single_btn.GetValue()
[eb8da5f]131                    if last_frame < 0 or first_frame < 0:
132                        msg = "Frame values must be positive"
133                    elif increment < 1:
134                        msg = "Increment must be greater than or equal to 1"
135                    elif first_frame > last_frame:
136                        msg = "First frame must be less than last frame"
137                    elif last_frame > n_frames:
138                        msg = "Last frame must be less than {}".format(n_frames)
139                    else:
140                        valid_input = True
141                except:
142                    valid_input = False
143                    msg = "Please enter valid integer values"
144
145                if not valid_input:
146                    wx.PostEvent(self.parent.manager.parent,
147                        StatusEvent(status=msg))
148            else:
[94f4518]149                return { 'frames': [], 'inc': None, 'file': single_file }
[eb8da5f]150        frames = range(first_frame, last_frame + increment,
151            increment)
[94f4518]152        return { 'frames': frames, 'inc': increment, 'file': single_file }
[eb8da5f]153
[a58706d]154    def on_convert(self, event):
[fdbea3c]155        if not self.validate_inputs():
156            return
157
[503cc34]158        self.sample.ID = self.title
[3ea9371]159
[a58706d]160        try:
[4b862c4]161            if self.data_type == 'ascii':
162                qdata = self.extract_data(self.q_input.GetPath())
163                iqdata = self.extract_data(self.iq_input.GetPath())
164            else: # self.data_type == 'bsl'
165                loader = BSLLoader(self.q_input.GetPath(),
166                    self.iq_input.GetPath())
167                bsl_data = loader.load_bsl_data()
[a3c538e1]168                qdata = bsl_data.q_axis.data
[eb8da5f]169                iqdata = bsl_data.data_axis.data
[a3c538e1]170                if len(qdata) > 1:
171                    msg = ("Q-Axis file has multiple frames. Only 1 frame is "
172                        "allowed for the Q-Axis")
173                    wx.PostEvent(self.parent.manager.parent,
174                        StatusEvent(status=msg, info="error"))
175                    return
176                else:
177                    qdata = qdata[0]
[eb8da5f]178                frames = [iqdata.shape[0]]
179                increment = 1
[94f4518]180                single_file = True
[a3c538e1]181                # Standard file has 3 frames: SAS, calibration and WAS
[eb8da5f]182                if frames[0] > 3:
[a3c538e1]183                    # File has multiple frames
[94f4518]184                    params = self.ask_frame_range(frames[0])
185                    frames = params['frames']
186                    increment = params['inc']
187                    single_file = params['file']
[eb8da5f]188                    if frames == []: return
[a3c538e1]189                else: # Only interested in SAS data
190                    frames = [0]
[a58706d]191        except Exception as ex:
192            msg = str(ex)
193            wx.PostEvent(self.parent.manager.parent,
194                StatusEvent(status=msg, info='error'))
195            return
196
[f2b3f28]197        output_path = self.output.GetPath()
198
[d112c30]199        if self.run is None:
[503cc34]200            self.run = []
[d112c30]201        elif not isinstance(self.run, list):
202            self.run = [self.run]
[503cc34]203
[ea2a7348]204        if self.title is None:
205            self.title = ''
206
[503cc34]207        metadata = {
208            'title': self.title,
209            'run': self.run,
210            'intrument': self.instrument,
211            'detector': [self.detector],
212            'sample': self.sample,
213            'source': self.source
214        }
[f2b3f28]215
[eb8da5f]216        frame_data = []
217        for i in frames:
218            data = Data1D(x=qdata, y=iqdata[i])
219            frame_data.append(data)
[94f4518]220        if single_file:
221            # Only need to set metadata on first Data1D object
222            frame_data[0].filename = output_path.split('\\')[-1]
223            for key, value in metadata.iteritems():
224                setattr(frame_data[0], key, value)
225        else:
226            # Need to set metadata for all Data1D objects
227            for datainfo in frame_data:
228                datainfo.filename = output_path.split('\\')[-1]
229                for key, value in metadata.iteritems():
230                    setattr(datainfo, key, value)
231
232
233        self.convert_to_cansas(frame_data, output_path, single_file)
[a58706d]234        wx.PostEvent(self.parent.manager.parent,
235            StatusEvent(status="Conversion completed."))
236
[11794f2]237    def on_help(self, event):
238        tree_location = ("user/sasgui/perspectives/file_converter/"
239            "file_converter_help.html")
240        doc_viewer = DocumentationWindow(self, -1, tree_location,
241            "", "File Converter Help")
242
[fdbea3c]243    def validate_inputs(self):
244        msg = "You must select a"
245        if self.q_input.GetPath() == '':
246            msg += " Q Axis input file."
247        elif self.iq_input.GetPath() == '':
248            msg += "n Intensity input file."
249        elif self.output.GetPath() == '':
250            msg += "destination for the converted file."
251        if msg != "You must select a":
252            wx.PostEvent(self.parent.manager.parent,
253                StatusEvent(status=msg, info='error'))
254            return
255
256        return True
257
[af84162]258    def show_detector_window(self, event):
[de0df2c]259        if self.meta_frames != []:
260            for frame in self.meta_frames:
261                frame.panel.on_close()
262        detector_frame = MetadataWindow(DetectorPanel,
263            parent=self.parent.manager.parent, manager=self,
[503cc34]264            metadata=self.detector, title='Detector Metadata')
[de0df2c]265        self.meta_frames.append(detector_frame)
266        self.parent.manager.put_icon(detector_frame)
267        detector_frame.Show(True)
[fdbea3c]268
[2a7722b]269    def show_sample_window(self, event):
270        if self.meta_frames != []:
271            for frame in self.meta_frames:
272                frame.panel.on_close()
273        sample_frame = MetadataWindow(SamplePanel,
274            parent=self.parent.manager.parent, manager=self,
[503cc34]275            metadata=self.sample, title='Sample Metadata')
[2a7722b]276        self.meta_frames.append(sample_frame)
277        self.parent.manager.put_icon(sample_frame)
278        sample_frame.Show(True)
279
[55bc56bc]280    def show_source_window(self, event):
281        if self.meta_frames != []:
282            for frame in self.meta_frames:
283                frame.panel.on_close()
284        source_frame = MetadataWindow(SourcePanel,
285            parent=self.parent.manager.parent, manager=self,
[503cc34]286            metadata=self.source, title="Source Metadata")
[55bc56bc]287        self.meta_frames.append(source_frame)
288        self.parent.manager.put_icon(source_frame)
289        source_frame.Show(True)
290
[a027549]291    def on_collapsible_pane(self, event):
292        self.Freeze()
293        self.SetupScrolling()
294        self.parent.Layout()
295        self.Thaw()
296
[4b862c4]297    def datatype_changed(self, event):
298        event.Skip()
299        dtype = event.GetEventObject().GetName()
300        self.data_type = dtype
301
[55775e8]302    def radiationtype_changed(self, event):
303        event.Skip()
304        rtype = event.GetEventObject().GetValue().lower()
[503cc34]305        self.source.radiation = rtype
[55775e8]306
[f2b3f28]307    def metadata_changed(self, event):
308        event.Skip()
309        textbox = event.GetEventObject()
310        attr = textbox.GetName()
311        value = textbox.GetValue().strip()
[fdbea3c]312
[503cc34]313        if value == '': value = None
314
315        setattr(self, attr, value)
[f2b3f28]316
317
[77d92cd]318    def _do_layout(self):
[f2b3f28]319        vbox = wx.BoxSizer(wx.VERTICAL)
[a58706d]320
[13b9a63]321        instructions = ("Select either single column ASCII files or BSL/OTOKO"
322            " files containing the Q-Axis and Intensity-axis data, chose where"
323            " to save the converted file, then click Convert to convert them "
324            "to CanSAS XML format. If required, metadata can also be input "
325            "below.")
[5afe77f]326        instruction_label = wx.StaticText(self, -1, instructions,
327            size=(_STATICBOX_WIDTH+40, -1))
328        instruction_label.Wrap(_STATICBOX_WIDTH+40)
[f2b3f28]329        vbox.Add(instruction_label, flag=wx.TOP | wx.LEFT | wx.RIGHT, border=5)
[77d92cd]330
[a58706d]331        section = wx.StaticBox(self, -1)
332        section_sizer = wx.StaticBoxSizer(section, wx.VERTICAL)
333        section_sizer.SetMinSize((_STATICBOX_WIDTH, -1))
[77d92cd]334
335        input_grid = wx.GridBagSizer(5, 5)
336
[4b862c4]337        y = 0
338
[13b9a63]339        q_label = wx.StaticText(self, -1, "Q-Axis Data: ")
[4b862c4]340        input_grid.Add(q_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
[a58706d]341
342        self.q_input = wx.FilePickerCtrl(self, -1,
343            size=(_STATICBOX_WIDTH-80, -1),
[13b9a63]344            message="Chose the Q-Axis data file.")
[4b862c4]345        input_grid.Add(self.q_input, (y,1), (1,1), wx.ALL, 5)
346        y += 1
[a58706d]347
[13b9a63]348        iq_label = wx.StaticText(self, -1, "Intensity-Axis Data: ")
[4b862c4]349        input_grid.Add(iq_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
[a58706d]350
351        self.iq_input = wx.FilePickerCtrl(self, -1,
352            size=(_STATICBOX_WIDTH-80, -1),
[13b9a63]353            message="Chose the Intensity-Axis data file.")
[4b862c4]354        input_grid.Add(self.iq_input, (y,1), (1,1), wx.ALL, 5)
355        y += 1
356
357        data_type_label = wx.StaticText(self, -1, "Input Format: ")
358        input_grid.Add(data_type_label, (y,0), (1,1),
359            wx.ALIGN_CENTER_VERTICAL, 5)
360        radio_sizer = wx.BoxSizer(wx.HORIZONTAL)
361        ascii_btn = wx.RadioButton(self, -1, "ASCII", name="ascii",
362            style=wx.RB_GROUP)
363        ascii_btn.Bind(wx.EVT_RADIOBUTTON, self.datatype_changed)
364        radio_sizer.Add(ascii_btn)
[13b9a63]365        bsl_btn = wx.RadioButton(self, -1, "BSL/OTOKO", name="bsl")
[4b862c4]366        bsl_btn.Bind(wx.EVT_RADIOBUTTON, self.datatype_changed)
367        radio_sizer.Add(bsl_btn)
368        input_grid.Add(radio_sizer, (y,1), (1,1), wx.ALL, 5)
369        y += 1
[a58706d]370
[55775e8]371        radiation_label = wx.StaticText(self, -1, "Radiation Type: ")
372        input_grid.Add(radiation_label, (y,0), (1,1), wx.ALL, 5)
373        radiation_input = wx.ComboBox(self, -1,
374            choices=["Neutron", "X-Ray", "Muon", "Electron"],
375            name="radiation", style=wx.CB_READONLY, value="Neutron")
376        radiation_input.Bind(wx.EVT_COMBOBOX, self.radiationtype_changed)
377        input_grid.Add(radiation_input, (y,1), (1,1))
378        y += 1
379
[a58706d]380        output_label = wx.StaticText(self, -1, "Output File: ")
[4b862c4]381        input_grid.Add(output_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
[a58706d]382
383        self.output = wx.FilePickerCtrl(self, -1,
384            size=(_STATICBOX_WIDTH-80, -1),
[55775e8]385            message="Chose where to save the output file.",
[a58706d]386            style=wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT | wx.FLP_USE_TEXTCTRL,
387            wildcard="*.xml")
[4b862c4]388        input_grid.Add(self.output, (y,1), (1,1), wx.ALL, 5)
389        y += 1
[a58706d]390
[fdbea3c]391        convert_btn = wx.Button(self, wx.ID_OK, "Convert")
[4b862c4]392        input_grid.Add(convert_btn, (y,0), (1,1), wx.ALL, 5)
[a58706d]393        convert_btn.Bind(wx.EVT_BUTTON, self.on_convert)
[77d92cd]394
[11794f2]395        help_btn = wx.Button(self, -1, "HELP")
396        input_grid.Add(help_btn, (y,1), (1,1), wx.ALL, 5)
397        help_btn.Bind(wx.EVT_BUTTON, self.on_help)
398
[a58706d]399        section_sizer.Add(input_grid)
[77d92cd]400
[f2b3f28]401        vbox.Add(section_sizer, flag=wx.ALL, border=5)
402
403        metadata_section = wx.CollapsiblePane(self, -1, "Metadata",
[fdbea3c]404            size=(_STATICBOX_WIDTH+40, -1), style=wx.WS_EX_VALIDATE_RECURSIVELY)
405        metadata_pane = metadata_section.GetPane()
[f2b3f28]406        metadata_grid = wx.GridBagSizer(5, 5)
407
[a027549]408        metadata_section.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
409            self.on_collapsible_pane)
410
[f2b3f28]411        y = 0
[503cc34]412        for item in self.properties:
[7c8ddb83]413            # Capitalise each word
414            label_txt = " ".join(
415                [s.capitalize() for s in item.replace('_', ' ').split(' ')])
416            if item == 'run':
417                label_txt = "Run Number"
[fdbea3c]418            label = wx.StaticText(metadata_pane, -1, label_txt,
[f2b3f28]419                style=wx.ALIGN_CENTER_VERTICAL)
[fdbea3c]420            input_box = wx.TextCtrl(metadata_pane, name=item,
[f2b3f28]421                size=(_STATICBOX_WIDTH-80, -1))
422            input_box.Bind(wx.EVT_TEXT, self.metadata_changed)
423            metadata_grid.Add(label, (y,0), (1,1),
424                wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
[fdbea3c]425            metadata_grid.Add(input_box, (y,1), (1,2), wx.EXPAND)
[f2b3f28]426            y += 1
427
[fdbea3c]428        detector_label = wx.StaticText(metadata_pane, -1,
429            "Detector:")
430        metadata_grid.Add(detector_label, (y, 0), (1,1), wx.ALL | wx.EXPAND, 5)
[af84162]431        detector_btn = wx.Button(metadata_pane, -1, "Enter Detector Metadata")
432        metadata_grid.Add(detector_btn, (y, 1), (1,1), wx.ALL | wx.EXPAND, 5)
433        detector_btn.Bind(wx.EVT_BUTTON, self.show_detector_window)
[fdbea3c]434        y += 1
435
[2a7722b]436        sample_label = wx.StaticText(metadata_pane, -1, "Sample: ")
437        metadata_grid.Add(sample_label, (y,0), (1,1), wx.ALL | wx.EXPAND, 5)
438        sample_btn = wx.Button(metadata_pane, -1, "Enter Sample Metadata")
439        metadata_grid.Add(sample_btn, (y,1), (1,1), wx.ALL | wx.EXPAND, 5)
440        sample_btn.Bind(wx.EVT_BUTTON, self.show_sample_window)
441        y += 1
442
[55bc56bc]443        source_label = wx.StaticText(metadata_pane, -1, "Source: ")
444        metadata_grid.Add(source_label, (y,0), (1,1), wx.ALL | wx.EXPAND, 5)
445        source_btn = wx.Button(metadata_pane, -1, "Enter Source Metadata")
446        source_btn.Bind(wx.EVT_BUTTON, self.show_source_window)
447        metadata_grid.Add(source_btn, (y,1), (1,1), wx.ALL | wx.EXPAND, 5)
448        y += 1
449
[fdbea3c]450        metadata_pane.SetSizer(metadata_grid)
[f2b3f28]451
452        vbox.Add(metadata_section, proportion=0, flag=wx.ALL, border=5)
[77d92cd]453
454        vbox.Fit(self)
455        self.SetSizer(vbox)
456
457class ConverterWindow(widget.CHILD_FRAME):
458
459    def __init__(self, parent=None, title='File Converter', base=None,
[a027549]460        manager=None, size=(PANEL_SIZE * 1.05, PANEL_SIZE / 1.25),
[77d92cd]461        *args, **kwargs):
462        kwargs['title'] = title
463        kwargs['size'] = size
464        widget.CHILD_FRAME.__init__(self, parent, *args, **kwargs)
465
466        self.manager = manager
467        self.panel = ConverterPanel(self, base=None)
468        self.Bind(wx.EVT_CLOSE, self.on_close)
469        self.SetPosition((wx.LEFT, PANEL_TOP))
470        self.Show(True)
471
472    def on_close(self, event):
473        if self.manager is not None:
474            self.manager.converter_frame = None
475        self.Destroy()
Note: See TracBrowser for help on using the repository browser.