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

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 d6bf064 was d6bf064, checked in by lewis, 8 years ago

Disable inputs that aren't used when BSL format selected

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