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

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

Support loading BSL/OTOKO files with multiple frames

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