1189 lines
39 KiB
Python
1189 lines
39 KiB
Python
|
# coding: utf-8
|
||
|
"""
|
||
|
Module iodata
|
||
|
=============
|
||
|
|
||
|
This module contains all classes useful to manipulate, store and display
|
||
|
data results.
|
||
|
|
||
|
The :py:class:`Data` and :py:class:`DataSet` are the two enduser classes
|
||
|
important to manipulate the data.
|
||
|
Here is an example of how to store values in a Data object:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from msspec.iodata import Data
|
||
|
import numpy as np
|
||
|
|
||
|
|
||
|
# Let's create first some dumy data
|
||
|
X = np.arange(0, 20)
|
||
|
Y = X**2
|
||
|
|
||
|
# Create a Data object. You need to give a title as an argument
|
||
|
data = Data('all my data')
|
||
|
# and append a new DataSet with its title
|
||
|
dset = data.add_dset('Dataset 0')
|
||
|
|
||
|
# To feed the DataSet with columns, use the add_columns method
|
||
|
# and provide as many keywords as you like. Each key being the
|
||
|
# column name and each value being an array holding the column
|
||
|
# data.
|
||
|
dset.add_columns(x=X, y=Y, z=X+2, w=Y**3)
|
||
|
# you can provide parameters with their values with keywords as well
|
||
|
dset.add_parameter(name='truc', group='main', value='3.14', unit='eV')
|
||
|
|
||
|
# To plot these data, you need to add a 'view' with its title
|
||
|
view = dset.add_view('my view')
|
||
|
# You then need to select which columns you which to plot and
|
||
|
# and under wich conditions (with the 'which' keyword)
|
||
|
view.select('x', 'y', where="z<10", legend=r"z = 0")
|
||
|
view.select('x', 'y', where="z>10", legend=r"z = 1")
|
||
|
|
||
|
# To pop up the graphical window
|
||
|
data.view()
|
||
|
|
||
|
"""
|
||
|
|
||
|
|
||
|
import os
|
||
|
import numpy as np
|
||
|
import h5py
|
||
|
from lxml import etree
|
||
|
import msspec
|
||
|
from msspec.misc import LOGGER
|
||
|
import ase.io
|
||
|
from io import StringIO
|
||
|
|
||
|
import wx
|
||
|
import wx.grid
|
||
|
|
||
|
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
|
||
|
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg
|
||
|
from matplotlib.figure import Figure
|
||
|
|
||
|
from terminaltables import AsciiTable
|
||
|
from distutils.version import StrictVersion, LooseVersion
|
||
|
|
||
|
import sys
|
||
|
#sys.path.append('../../MsSpecGui/msspecgui/msspec/gui')
|
||
|
from .msspecgui.msspec.gui.clusterviewer import ClusterViewer
|
||
|
|
||
|
def cols2matrix(x, y, z, nx=88*1+1, ny=360*1+1):
|
||
|
# mix the values of existing theta and new theta and return the
|
||
|
# unique values
|
||
|
newx = np.linspace(np.min(x), np.max(x), nx)
|
||
|
newy = np.linspace(np.min(y), np.max(y), ny)
|
||
|
ux = np.unique(np.append(x, newx))
|
||
|
uy = np.unique(np.append(y, newy))
|
||
|
|
||
|
# create an empty matrix to hold the results
|
||
|
zz = np.empty((len(ux), len(uy)))
|
||
|
zz[:] = np.nan
|
||
|
|
||
|
for p in zip(x, y, z):
|
||
|
i = np.argwhere(ux == p[0])
|
||
|
j = np.argwhere(uy == p[1])
|
||
|
zz[i, j] = p[2]
|
||
|
|
||
|
for i in range(len(ux)):
|
||
|
#ok, = np.where(-np.isnan(zz[i,:]))
|
||
|
ok, = np.where(~np.isnan(zz[i, :]))
|
||
|
if len(ok) > 0:
|
||
|
xp = uy[ok]
|
||
|
fp = zz[i, ok]
|
||
|
zz[i,:] = np.interp(uy, xp, fp)
|
||
|
|
||
|
for i in range(len(uy)):
|
||
|
#ok, = np.where(-np.isnan(zz[:,i]))
|
||
|
ok, = np.where(~np.isnan(zz[:, i]))
|
||
|
if len(ok) > 0:
|
||
|
xp = ux[ok]
|
||
|
fp = zz[ok, i]
|
||
|
zz[:,i] = np.interp(ux, xp, fp)
|
||
|
|
||
|
return ux, uy, zz
|
||
|
|
||
|
|
||
|
class _DataPoint(dict):
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
dict.__init__(self, *args, **kwargs)
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
if name in list(self.keys()):
|
||
|
return self[name]
|
||
|
else:
|
||
|
raise AttributeError("'{}' object has no attribute '{}'".format(
|
||
|
self.__class__.__name__, name))
|
||
|
|
||
|
class DataSet(object):
|
||
|
"""
|
||
|
This class can create an object to hold column-oriented data.
|
||
|
|
||
|
:param title: The text used to entitled the dataset
|
||
|
:type title: str
|
||
|
:param notes: Some comments to add to the data
|
||
|
:type notes: str
|
||
|
|
||
|
"""
|
||
|
def __init__(self, title, notes=""):
|
||
|
self.title = title
|
||
|
self.notes = notes
|
||
|
self._views = []
|
||
|
self._parameters = []
|
||
|
self.attributes = {}
|
||
|
|
||
|
|
||
|
self._col_names = []
|
||
|
self._col_arrays = []
|
||
|
self._defaults = {'bool': False, 'str': '', 'int': 0, 'float': 0.,
|
||
|
'complex': complex(0)}
|
||
|
self._formats = {bool: '{:s}', str: '{:s}', int: '{:<20d}',
|
||
|
float: '{:<20.10e}', complex: 's'}
|
||
|
|
||
|
def _empty_array(self, val):
|
||
|
if isinstance(val, str):
|
||
|
t = 'S256'
|
||
|
else:
|
||
|
t = np.dtype(type(val))
|
||
|
|
||
|
if isinstance(val, bool):
|
||
|
default = self._defaults['bool']
|
||
|
elif isinstance(val, str):
|
||
|
default = self._defaults['str']
|
||
|
elif isinstance(val, int):
|
||
|
default = self._defaults['int']
|
||
|
elif isinstance(val, float):
|
||
|
default = self._defaults['float']
|
||
|
elif isinstance(val, complex):
|
||
|
default = self._defaults['complex']
|
||
|
else:
|
||
|
raise TypeError('Not a supported type')
|
||
|
|
||
|
return np.array([default]*len(self), dtype=t)
|
||
|
|
||
|
def add_row(self, **kwargs):
|
||
|
"""Add a row of data into the dataset.
|
||
|
|
||
|
:param kwargs: Each keyword is a column name. The number of keywords (columns) must be coherent with the
|
||
|
number of existing columns. If no column are defined yet, they will be created.
|
||
|
|
||
|
"""
|
||
|
for k, v in list(kwargs.items()):
|
||
|
if k not in self._col_names:
|
||
|
self._col_names.append(k)
|
||
|
self._col_arrays.append(self._empty_array(v))
|
||
|
for k, v in list(kwargs.items()):
|
||
|
i = self._col_names.index(k)
|
||
|
arr = self._col_arrays[i]
|
||
|
arr = np.append(arr, v)
|
||
|
self._col_arrays[i] = arr
|
||
|
|
||
|
def add_columns(self, **kwargs):
|
||
|
"""
|
||
|
Add columns to the dataset.
|
||
|
|
||
|
You can provide as many columns as you want to this function. This
|
||
|
function can be called several times on the same dataset but each time
|
||
|
with different column names. Column names are given as keywords.
|
||
|
|
||
|
:Example:
|
||
|
|
||
|
>>> from iodata import DataSet
|
||
|
>>> dset = DataSet('My Dataset', notes="Just an example")
|
||
|
>>> xdata = range(10)
|
||
|
>>> ydata = [i**2 for i in xdata]
|
||
|
>>> dset.add_columns(x=xdata, y=ydata)
|
||
|
>>> print dset
|
||
|
>>> +-------+
|
||
|
>>> | x y |
|
||
|
>>> +-------+
|
||
|
>>> | 0 0 |
|
||
|
>>> | 1 1 |
|
||
|
>>> | 2 4 |
|
||
|
>>> | 3 9 |
|
||
|
>>> | 4 16 |
|
||
|
>>> | 5 25 |
|
||
|
>>> | 6 36 |
|
||
|
>>> | 7 49 |
|
||
|
>>> | 8 64 |
|
||
|
>>> | 9 81 |
|
||
|
>>> +-------+
|
||
|
|
||
|
"""
|
||
|
for k, vv in list(kwargs.items()):
|
||
|
assert k not in self._col_names, ("'{}' column already exists"
|
||
|
"".format(k))
|
||
|
#if len(self) > 0:
|
||
|
# assert len(vv) == len(self), (
|
||
|
# 'Too many values in the column (max = {})'.format(
|
||
|
# len(self)))
|
||
|
for k, vv in list(kwargs.items()):
|
||
|
arr = np.array(vv)
|
||
|
self._col_names.append(k)
|
||
|
self._col_arrays.append(arr)
|
||
|
|
||
|
def delete_rows(self, itemspec):
|
||
|
"""
|
||
|
Delete the rows specified with itemspec.
|
||
|
|
||
|
"""
|
||
|
for i in range(len(self._col_names)):
|
||
|
self._col_arrays[i] = np.delete(self._col_arrays[i], itemspec)
|
||
|
|
||
|
def delete_columns(self, *tags):
|
||
|
"""
|
||
|
Removes all columns name passed as arguments
|
||
|
|
||
|
:param tags: column names.
|
||
|
:type tags: str
|
||
|
|
||
|
"""
|
||
|
for tag in tags:
|
||
|
i = self._col_names.index(tag)
|
||
|
self._col_names.pop(i)
|
||
|
self._col_arrays.pop(i)
|
||
|
|
||
|
def columns(self):
|
||
|
"""
|
||
|
Get all the column names.
|
||
|
|
||
|
:return: List of column names.
|
||
|
:rtype: List of str
|
||
|
|
||
|
"""
|
||
|
return self._col_names
|
||
|
|
||
|
def add_view(self, name, **plotopts):
|
||
|
"""
|
||
|
Creates a new view named *name* with specied plot options.
|
||
|
|
||
|
:param name: name of the view.
|
||
|
:type name: str
|
||
|
:param plotopts: list of keywords for configuring the plots.
|
||
|
:return: a view.
|
||
|
:rtype: :py:class:`iodata._DataSetView`
|
||
|
"""
|
||
|
if isinstance(name, str):
|
||
|
v = _DataSetView(self, name, **plotopts)
|
||
|
else:
|
||
|
v = name
|
||
|
v.dataset = self
|
||
|
self._views.append(v)
|
||
|
return v
|
||
|
|
||
|
def views(self):
|
||
|
"""Returns all the defined views in the dataset.
|
||
|
|
||
|
:return: A list of view
|
||
|
:rtype: List of :py:class:`iodata._DataSetView`
|
||
|
"""
|
||
|
return self._views
|
||
|
|
||
|
def add_parameter(self, **kwargs):
|
||
|
"""Add a parameter to store with the dataset.
|
||
|
|
||
|
:param kwargs: list of keywords with str values.
|
||
|
|
||
|
These keywords are:
|
||
|
* name: the name of the parameter.
|
||
|
* group: the name of a group it belongs to.
|
||
|
* value: the value of the parameter.
|
||
|
* unit: the unit of the parameter.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from iodata import DataSet
|
||
|
|
||
|
mydset = DataSet("Experiment")
|
||
|
mydset.add_parameter(name='Spectrometer', group='misc', value='Omicron', unit='')
|
||
|
|
||
|
"""
|
||
|
self._parameters.append(kwargs)
|
||
|
|
||
|
def parameters(self):
|
||
|
"""
|
||
|
Returns the list of defined parameters.
|
||
|
|
||
|
:return: all parameters defined in the :py:class:`iodata.DataSet` object.
|
||
|
:rtype: List of dict
|
||
|
"""
|
||
|
return self._parameters
|
||
|
|
||
|
def get_parameter(self, group=None, name=None):
|
||
|
"""Retrieves all parameters for a given name and group.
|
||
|
|
||
|
* If *name* is given and *group* is None, returns all parameters with such a *name* in all groups.
|
||
|
* If *group* is given and *name* is None, returns all parameters in such a *group*
|
||
|
* If both *name* and *group* are None. Returns all parameters (equivalent to
|
||
|
:py:func:`iodata.DataSet.parameters`).
|
||
|
|
||
|
:param group: The group name or None.
|
||
|
:type group: str
|
||
|
:param name: The parameter's name or None.
|
||
|
:type name: str
|
||
|
:return: A list of parameters.
|
||
|
:rtype: List of dict
|
||
|
"""
|
||
|
p = []
|
||
|
for _ in self._parameters:
|
||
|
if _['group'] == group or group == None:
|
||
|
if _['name'] == name or name == None:
|
||
|
p.append(_)
|
||
|
return p[0] if len(p) == 1 else p
|
||
|
|
||
|
def get_cluster(self):
|
||
|
"""Get all the atoms in the cluster.
|
||
|
|
||
|
:return: The cluster
|
||
|
:rtype: :py:class:`ase.Atoms`
|
||
|
"""
|
||
|
s = StringIO()
|
||
|
s.write(self.get_parameter(group='Cluster', name='cluster')['value'])
|
||
|
return ase.io.read(s, format='xyz')
|
||
|
|
||
|
|
||
|
def select(self, *args, **kwargs):
|
||
|
condition = kwargs.get('where', 'True')
|
||
|
indices = []
|
||
|
|
||
|
|
||
|
def export(self, filename="", mode="w"):
|
||
|
"""Export the DataSet to the given *filename*.
|
||
|
|
||
|
:param filename: The name of the file.
|
||
|
:type filename: str
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
Not yet implemented
|
||
|
"""
|
||
|
colnames = self.columns()
|
||
|
with open(filename, mode) as fd:
|
||
|
fd.write("# " + ("{:<20s}" * len(colnames)).format(*colnames
|
||
|
) + "\n")
|
||
|
for i in range(len(self)):
|
||
|
row = self[i]
|
||
|
for key in row.columns():
|
||
|
value = row[key][0]
|
||
|
fmt = '{:s}'
|
||
|
#print value
|
||
|
for t, f in list(self._formats.items()):
|
||
|
if isinstance(value, t):
|
||
|
fmt = f
|
||
|
break
|
||
|
#fd.write(' ')
|
||
|
fd.write(fmt.format(value))
|
||
|
#fd.write(str(value) + ', ')
|
||
|
fd.write('\n')
|
||
|
|
||
|
def __getitem__(self, itemspec):
|
||
|
if isinstance(itemspec, str):
|
||
|
return getattr(self, itemspec)
|
||
|
title = 'untitled'
|
||
|
new = DataSet(title)
|
||
|
|
||
|
new._col_names = self.columns()
|
||
|
for arr in self._col_arrays:
|
||
|
new._col_arrays.append(np.array(arr[itemspec]).flatten())
|
||
|
|
||
|
return new
|
||
|
|
||
|
def __setstate__(self, state):
|
||
|
self.__dict__ = state
|
||
|
|
||
|
def __getstate__(self):
|
||
|
return self.__dict__
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
if name in self._col_names:
|
||
|
i = self._col_names.index(name)
|
||
|
return self._col_arrays[i]
|
||
|
else:
|
||
|
raise AttributeError("'{}' object has no attribute '{}'".format(
|
||
|
self.__class__.__name__, name))
|
||
|
|
||
|
def __iter__(self):
|
||
|
for i in range(len(self)):
|
||
|
_ = {k: arr[i] for k, arr in zip(self._col_names,
|
||
|
self._col_arrays)}
|
||
|
point = _DataPoint(_)
|
||
|
yield point
|
||
|
|
||
|
def __len__(self):
|
||
|
try:
|
||
|
length = len(self._col_arrays[0])
|
||
|
except IndexError:
|
||
|
length = 0
|
||
|
return length
|
||
|
|
||
|
def __str__(self):
|
||
|
max_len = 10
|
||
|
max_col = 10
|
||
|
ncols = min(max_col, len(self._col_arrays))
|
||
|
table_data = [self._col_names[:ncols]]
|
||
|
table_data[0].insert(0, "")
|
||
|
|
||
|
all_indices = np.arange(0, len(self))
|
||
|
indices = all_indices
|
||
|
if len(self) > max_len:
|
||
|
indices = list(range(max_len/2)) + list(range(-max_len/2, 0))
|
||
|
|
||
|
_i = 0
|
||
|
for i in indices:
|
||
|
if i < _i:
|
||
|
row = ['...' for _ in range(ncols + 1)]
|
||
|
table_data.append(row)
|
||
|
row = [str(all_indices[i]),]
|
||
|
for j in range(ncols):
|
||
|
arr = self._col_arrays[j]
|
||
|
row.append(str(arr[i]))
|
||
|
if len(self._col_names) > max_col:
|
||
|
row.append('...')
|
||
|
table_data.append(row)
|
||
|
_i = i
|
||
|
|
||
|
table = AsciiTable(table_data)
|
||
|
table.outer_border = True
|
||
|
table.title = self.title
|
||
|
table.inner_column_border = False
|
||
|
return table.table
|
||
|
|
||
|
def __repr__(self):
|
||
|
s = "<{}('{}')>".format(self.__class__.__name__, self.title)
|
||
|
return s
|
||
|
|
||
|
class Data(object):
|
||
|
"""Creates a new Data object to store DataSets.
|
||
|
|
||
|
:param title: The title of the Data object.
|
||
|
:type str:
|
||
|
|
||
|
"""
|
||
|
def __init__(self, title=''):
|
||
|
self.title = title
|
||
|
self._datasets = []
|
||
|
self._dirty = False
|
||
|
|
||
|
def add_dset(self, title):
|
||
|
"""Adds a new DataSet in the Data object.
|
||
|
|
||
|
:param title: The name of the DataSet.
|
||
|
:type title: str
|
||
|
:return: The newly created DataSet.
|
||
|
:rtype: :py:class:`iodata.DataSet`
|
||
|
"""
|
||
|
titles = [d.title for d in self._datasets]
|
||
|
if not title in titles:
|
||
|
dset = DataSet(title)
|
||
|
self._datasets.append(dset)
|
||
|
self._dirty = True
|
||
|
return dset
|
||
|
else:
|
||
|
raise NameError('A Dataset with that name already exists!')
|
||
|
|
||
|
def delete_dset(self, title):
|
||
|
"""Removes a DataSet from the Data object.
|
||
|
|
||
|
:param title: The DataSet name to be removed.
|
||
|
:type title: str
|
||
|
|
||
|
"""
|
||
|
titles = [d.title for d in self._datasets]
|
||
|
i = titles.index(title)
|
||
|
self._datasets.pop(i)
|
||
|
self._dirty = True
|
||
|
|
||
|
def get_last_dset(self):
|
||
|
"""Get the last DataSet of the Data object.
|
||
|
|
||
|
:return: The lastly created DataSet in the Data object
|
||
|
:rtype: :py:class:`iodata.DataSet`
|
||
|
"""
|
||
|
return self._datasets[-1]
|
||
|
|
||
|
def is_dirty(self):
|
||
|
"""Wether the Data object needs to be saved.
|
||
|
|
||
|
:return: A boolean value to indicate if Data has changed since last dump to hard drive.
|
||
|
:rtype: bool
|
||
|
"""
|
||
|
return self._dirty
|
||
|
|
||
|
|
||
|
def save(self, filename, append=False):
|
||
|
"""Saves the current Data to the hard drive.
|
||
|
|
||
|
The Data, all its content along with parameters, defined views... are saved to the hard drive in the HDF5
|
||
|
file format. Please see `hdfgroup <https://support.hdfgroup.org/HDF5/>`_ for more details about HDF5.
|
||
|
|
||
|
:param filename: The name of the file to create or to append to.
|
||
|
:type filename: str
|
||
|
:param append: Wether to create a neww file or to append to an existing one.
|
||
|
:type append: bool
|
||
|
|
||
|
"""
|
||
|
mode = 'a' if append else 'w'
|
||
|
titles = [d.title for d in self._datasets]
|
||
|
with h5py.File(filename, mode) as fd:
|
||
|
if append:
|
||
|
try:
|
||
|
data_grp = fd['DATA']
|
||
|
meta_grp = fd['MsSpec viewer metainfo']
|
||
|
except Exception as err:
|
||
|
fd.close()
|
||
|
self.save(filename, append=False)
|
||
|
return
|
||
|
else:
|
||
|
data_grp = fd.create_group('DATA')
|
||
|
meta_grp = fd.create_group('MsSpec viewer metainfo')
|
||
|
|
||
|
data_grp.attrs['title'] = self.title
|
||
|
for dset in self._datasets:
|
||
|
if dset.title in data_grp:
|
||
|
LOGGER.warning('dataset \"{}\" already exists in file \"{}\", not overwritting'.format(
|
||
|
dset.title, os.path.abspath(filename)))
|
||
|
continue
|
||
|
grp = data_grp.create_group(dset.title)
|
||
|
grp.attrs['notes'] = dset.notes
|
||
|
for col_name in dset.columns():
|
||
|
data = dset[col_name]
|
||
|
grp.create_dataset(col_name, data=data)
|
||
|
|
||
|
meta_grp.attrs['version'] = msspec.__version__
|
||
|
|
||
|
root = etree.Element('metainfo')
|
||
|
# xmlize views
|
||
|
for dset in self._datasets:
|
||
|
views_node = etree.SubElement(root, 'views', dataset=dset.title)
|
||
|
for view in dset.views():
|
||
|
view_el = etree.fromstring(view.to_xml())
|
||
|
views_node.append(view_el)
|
||
|
|
||
|
# xmlize parameters
|
||
|
for dset in self._datasets:
|
||
|
param_node = etree.SubElement(root, 'parameters', dataset=dset.title)
|
||
|
for p in dset.parameters():
|
||
|
child = etree.SubElement(param_node, 'parameter')
|
||
|
for k, v in list(p.items()):
|
||
|
child.attrib[k] = v
|
||
|
xml_str = etree.tostring(root, pretty_print=False)
|
||
|
try:
|
||
|
del meta_grp['info']
|
||
|
except:
|
||
|
meta_grp.create_dataset('info', data=np.array((xml_str,)).view('S1'))
|
||
|
self._dirty = False
|
||
|
LOGGER.info('Data saved in {}'.format(os.path.abspath(filename)))
|
||
|
|
||
|
@staticmethod
|
||
|
def load(filename):
|
||
|
"""Loads an HDF5 file from the disc.
|
||
|
|
||
|
:param filename: The path to the file to laod.
|
||
|
:type filename: str
|
||
|
:return: A Data object.
|
||
|
:rtype: :py:class:`iodata.Data`
|
||
|
"""
|
||
|
output = Data()
|
||
|
with h5py.File(filename, 'r') as fd:
|
||
|
parameters = {}
|
||
|
views = {}
|
||
|
|
||
|
output.title = fd['DATA'].attrs['title']
|
||
|
for dset_name in fd['DATA'] :
|
||
|
parameters[dset_name] = []
|
||
|
views[dset_name] = []
|
||
|
dset = output.add_dset(dset_name)
|
||
|
dset.notes = fd['DATA'][dset_name].attrs['notes']
|
||
|
for h5dset in fd['DATA'][dset_name]:
|
||
|
dset.add_columns(**{h5dset: fd['DATA'][dset_name][h5dset].value})
|
||
|
|
||
|
try:
|
||
|
vfile = LooseVersion(fd['MsSpec viewer metainfo'].attrs['version'])
|
||
|
if vfile > LooseVersion(msspec.__version__):
|
||
|
raise NameError('File was saved with a more recent format')
|
||
|
xml = fd['MsSpec viewer metainfo']['info'].value.tostring()
|
||
|
root = etree.fromstring(xml)
|
||
|
for elt0 in root.iter('parameters'):
|
||
|
dset_name = elt0.attrib['dataset']
|
||
|
for elt1 in elt0.iter('parameter'):
|
||
|
parameters[dset_name].append(elt1.attrib)
|
||
|
|
||
|
for elt0 in root.iter('views'):
|
||
|
dset_name = elt0.attrib['dataset']
|
||
|
for elt1 in elt0.iter('view'):
|
||
|
view = _DataSetView(None, "")
|
||
|
view.from_xml(etree.tostring(elt1))
|
||
|
views[dset_name].append(view)
|
||
|
|
||
|
except Exception as err:
|
||
|
print(err)
|
||
|
|
||
|
|
||
|
for dset in output:
|
||
|
for v in views[dset.title]:
|
||
|
dset.add_view(v)
|
||
|
for p in parameters[dset.title]:
|
||
|
dset.add_parameter(**p)
|
||
|
|
||
|
output._dirty = False
|
||
|
return output
|
||
|
|
||
|
def __iter__(self):
|
||
|
for dset in self._datasets:
|
||
|
yield dset
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
try:
|
||
|
titles = [d.title for d in self._datasets]
|
||
|
i = titles.index(key)
|
||
|
except ValueError:
|
||
|
i = key
|
||
|
return self._datasets[i]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._datasets)
|
||
|
|
||
|
def __str__(self):
|
||
|
s = str([dset.title for dset in self._datasets])
|
||
|
return s
|
||
|
|
||
|
def __repr__(self):
|
||
|
s = "<Data('{}')>".format(self.title)
|
||
|
return s
|
||
|
|
||
|
def view(self):
|
||
|
"""Pops up a grphical window to show all the defined views of the Data object.
|
||
|
|
||
|
"""
|
||
|
app = wx.App(False)
|
||
|
app.SetAppName('MsSpec Data Viewer')
|
||
|
frame = _DataWindow(self)
|
||
|
frame.Show(True)
|
||
|
app.MainLoop()
|
||
|
|
||
|
|
||
|
class _DataSetView(object):
|
||
|
def __init__(self, dset, name, **plotopts):
|
||
|
self.dataset = dset
|
||
|
self.title = name
|
||
|
self._plotopts = dict(
|
||
|
title='No title',
|
||
|
xlabel='', ylabel='', grid=True, legend=[], colorbar=False,
|
||
|
projection='rectilinear', xlim=[None, None], ylim=[None, None],
|
||
|
scale='linear',
|
||
|
marker=None, autoscale=False)
|
||
|
self._plotopts.update(plotopts)
|
||
|
self._selection_tags = []
|
||
|
self._selection_conditions = []
|
||
|
|
||
|
def set_plot_options(self, **kwargs):
|
||
|
self._plotopts.update(kwargs)
|
||
|
|
||
|
def select(self, *args, **kwargs):
|
||
|
condition = kwargs.get('where', 'True')
|
||
|
legend = kwargs.get('legend', '')
|
||
|
self._selection_conditions.append(condition)
|
||
|
self._selection_tags.append(args)
|
||
|
self._plotopts['legend'].append(legend)
|
||
|
|
||
|
def tags(self):
|
||
|
return self._selection_tags
|
||
|
|
||
|
def get_data(self):
|
||
|
data = []
|
||
|
for condition, tags in zip(self._selection_conditions,
|
||
|
self._selection_tags):
|
||
|
indices = []
|
||
|
# replace all occurence of tags
|
||
|
for tag in self.dataset.columns():
|
||
|
condition = condition.replace(tag, "p['{}']".format(tag))
|
||
|
|
||
|
for i, p in enumerate(self.dataset):
|
||
|
if eval(condition):
|
||
|
indices.append(i)
|
||
|
|
||
|
values = []
|
||
|
for tag in tags:
|
||
|
values.append(getattr(self.dataset[indices], tag))
|
||
|
|
||
|
data.append(values)
|
||
|
return data
|
||
|
|
||
|
def serialize(self):
|
||
|
data = {
|
||
|
'name': self.title,
|
||
|
'selection_conditions': self._selection_conditions,
|
||
|
'selection_tags': self._selection_tags,
|
||
|
'plotopts': self._plotopts
|
||
|
}
|
||
|
root = etree.Element('root')
|
||
|
|
||
|
return data
|
||
|
|
||
|
def to_xml(self):
|
||
|
plotopts = self._plotopts.copy()
|
||
|
legends = plotopts.pop('legend')
|
||
|
|
||
|
root = etree.Element('view', name=self.title)
|
||
|
for key, value in list(plotopts.items()):
|
||
|
root.attrib[key] = str(value)
|
||
|
#root.attrib['dataset_name'] = self.dataset.title
|
||
|
|
||
|
for tags, cond, legend in zip(self._selection_tags,
|
||
|
self._selection_conditions,
|
||
|
legends):
|
||
|
curve = etree.SubElement(root, 'curve')
|
||
|
curve.attrib['legend'] = legend
|
||
|
curve.attrib['condition'] = cond
|
||
|
axes = etree.SubElement(curve, 'axes')
|
||
|
for tag in tags:
|
||
|
variable = etree.SubElement(axes, 'axis', name=tag)
|
||
|
|
||
|
|
||
|
return etree.tostring(root, pretty_print=False)
|
||
|
|
||
|
def from_xml(self, xmlstr):
|
||
|
root = etree.fromstring(xmlstr)
|
||
|
self.title = root.attrib['name']
|
||
|
#self._plotopts['title'] = root.attrib['title']
|
||
|
#self._plotopts['xlabel'] = root.attrib['xlabel']
|
||
|
# self._plotopts['ylabel'] = root.attrib['ylabel']
|
||
|
# self._plotopts['grid'] = bool(root.attrib['grid'])
|
||
|
# self._plotopts['colorbar'] = bool(root.attrib['colorbar'])
|
||
|
# self._plotopts['projection'] = root.attrib['projection']
|
||
|
# self._plotopts['marker'] = root.attrib['marker']
|
||
|
for key in list(self._plotopts.keys()):
|
||
|
try:
|
||
|
self._plotopts[key] = eval(root.attrib.get(key))
|
||
|
except:
|
||
|
self._plotopts[key] = root.attrib.get(key)
|
||
|
|
||
|
|
||
|
|
||
|
legends = []
|
||
|
conditions = []
|
||
|
tags = []
|
||
|
for curve in root.iter("curve"):
|
||
|
legends.append(curve.attrib['legend'])
|
||
|
conditions.append(curve.attrib['condition'])
|
||
|
variables = []
|
||
|
for var in curve.iter('axis'):
|
||
|
variables.append(var.attrib['name'])
|
||
|
tags.append(tuple(variables))
|
||
|
|
||
|
self._selection_conditions = conditions
|
||
|
self._selection_tags = tags
|
||
|
self._plotopts['legend'] = legends
|
||
|
|
||
|
def __repr__(self):
|
||
|
s = "<{}('{}')>".format(self.__class__.__name__, self.title)
|
||
|
return s
|
||
|
|
||
|
def __str__(self):
|
||
|
try:
|
||
|
dset_title = self.dataset.title
|
||
|
except AttributeError:
|
||
|
dset_title = "unknown"
|
||
|
s = '{}:\n'.format(self.__class__.__name__)
|
||
|
s += '\tname : %s\n' % self.title
|
||
|
s += '\tdataset : %s\n' % dset_title
|
||
|
s += '\ttags : %s\n' % str(self._selection_tags)
|
||
|
s += '\tconditions : %s\n' % str(self._selection_conditions)
|
||
|
return s
|
||
|
|
||
|
class _GridWindow(wx.Frame):
|
||
|
def __init__(self, dset, parent=None):
|
||
|
title = 'Data: ' + dset.title
|
||
|
wx.Frame.__init__(self, parent, title=title, size=(640, 480))
|
||
|
self.create_grid(dset)
|
||
|
|
||
|
def create_grid(self, dset):
|
||
|
grid = wx.grid.Grid(self, -1)
|
||
|
grid.CreateGrid(len(dset), len(dset.columns()))
|
||
|
for ic, c in enumerate(dset.columns()):
|
||
|
grid.SetColLabelValue(ic, c)
|
||
|
for iv, v in enumerate(dset[c]):
|
||
|
grid.SetCellValue(iv, ic, str(v))
|
||
|
|
||
|
class _ParametersWindow(wx.Frame):
|
||
|
def __init__(self, dset, parent=None):
|
||
|
title = 'Parameters: ' + dset.title
|
||
|
wx.Frame.__init__(self, parent, title=title, size=(400, 480))
|
||
|
self.create_tree(dset)
|
||
|
|
||
|
def create_tree(self, dset):
|
||
|
datatree = {}
|
||
|
for p in dset.parameters():
|
||
|
is_hidden = p.get('hidden', "False")
|
||
|
if is_hidden == "True":
|
||
|
continue
|
||
|
group = datatree.get(p['group'], [])
|
||
|
#strval = str(p['value'] * p['unit'] if p['unit'] else p['value'])
|
||
|
#group.append("{:s} = {:s}".format(p['name'], strval))
|
||
|
group.append("{} = {} {}".format(p['name'], p['value'], p['unit']))
|
||
|
datatree[p['group']] = group
|
||
|
|
||
|
tree = wx.TreeCtrl(self, -1)
|
||
|
root = tree.AddRoot('Parameters')
|
||
|
|
||
|
for key in list(datatree.keys()):
|
||
|
item0 = tree.AppendItem(root, key)
|
||
|
for item in datatree[key]:
|
||
|
tree.AppendItem(item0, item)
|
||
|
tree.ExpandAll()
|
||
|
tree.SelectItem(root)
|
||
|
|
||
|
class _DataWindow(wx.Frame):
|
||
|
def __init__(self, data):
|
||
|
assert isinstance(data, (Data, DataSet))
|
||
|
|
||
|
if isinstance(data, DataSet):
|
||
|
dset = data
|
||
|
data = Data()
|
||
|
data.first = dset
|
||
|
self.data = data
|
||
|
self._filename = None
|
||
|
self._current_dset = None
|
||
|
|
||
|
wx.Frame.__init__(self, None, title="", size=(640, 480))
|
||
|
|
||
|
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||
|
|
||
|
# Populate the menu bar
|
||
|
self.create_menu()
|
||
|
|
||
|
# Create the status bar
|
||
|
statusbar = wx.StatusBar(self, -1)
|
||
|
statusbar.SetFieldsCount(3)
|
||
|
statusbar.SetStatusWidths([-2, -1, -1])
|
||
|
self.SetStatusBar(statusbar)
|
||
|
|
||
|
# Add the notebook to hold all graphs
|
||
|
self.notebooks = {}
|
||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||
|
#sizer.Add(self.notebook)
|
||
|
self.SetSizer(sizer)
|
||
|
|
||
|
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_page_changed)
|
||
|
|
||
|
self.create_notebooks()
|
||
|
|
||
|
self.update_title()
|
||
|
|
||
|
def create_notebooks(self):
|
||
|
for key in list(self.notebooks.keys()):
|
||
|
nb = self.notebooks.pop(key)
|
||
|
nb.Destroy()
|
||
|
|
||
|
for dset in self.data:
|
||
|
nb = wx.Notebook(self, -1)
|
||
|
self.notebooks[dset.title] = nb
|
||
|
self.GetSizer().Add(nb, 1, wx.ALL|wx.EXPAND)
|
||
|
for view in dset.views():
|
||
|
self.create_page(nb, view)
|
||
|
|
||
|
self.create_menu()
|
||
|
|
||
|
self.show_dataset(self.data[0].title)
|
||
|
|
||
|
|
||
|
def create_menu(self):
|
||
|
menubar = wx.MenuBar()
|
||
|
menu1 = wx.Menu()
|
||
|
menu1.Append(110, "Open\tCtrl+O")
|
||
|
menu1.Append(120, "Save\tCtrl+S")
|
||
|
menu1.Append(130, "Save as...")
|
||
|
menu1.Append(140, "Export\tCtrl+E")
|
||
|
menu1.AppendSeparator()
|
||
|
menu1.Append(199, "Close\tCtrl+Q")
|
||
|
|
||
|
menu2 = wx.Menu()
|
||
|
for i, dset in enumerate(self.data):
|
||
|
menu_id = 201 + i
|
||
|
menu2.AppendRadioItem(menu_id, dset.title)
|
||
|
self.Bind(wx.EVT_MENU, self.on_menu_dataset, id=menu_id)
|
||
|
|
||
|
self.Bind(wx.EVT_MENU, self.on_open, id=110)
|
||
|
self.Bind(wx.EVT_MENU, self.on_save, id=120)
|
||
|
self.Bind(wx.EVT_MENU, self.on_saveas, id=130)
|
||
|
self.Bind(wx.EVT_MENU, self.on_close, id=199)
|
||
|
|
||
|
|
||
|
menu3 = wx.Menu()
|
||
|
menu3.Append(301, "Data")
|
||
|
menu3.Append(302, "Cluster")
|
||
|
menu3.Append(303, "Parameters")
|
||
|
|
||
|
self.Bind(wx.EVT_MENU, self.on_viewdata, id=301)
|
||
|
self.Bind(wx.EVT_MENU, self.on_viewcluster, id=302)
|
||
|
self.Bind(wx.EVT_MENU, self.on_viewparameters, id=303)
|
||
|
|
||
|
menubar.Append(menu1, "&File")
|
||
|
menubar.Append(menu2, "&Datasets")
|
||
|
menubar.Append(menu3, "&View")
|
||
|
self.SetMenuBar(menubar)
|
||
|
|
||
|
def on_open(self, event):
|
||
|
if self.data.is_dirty():
|
||
|
mbx = wx.MessageDialog(self, ('Displayed data is unsaved. Do '
|
||
|
'you wish to save before opening'
|
||
|
'another file ?'),
|
||
|
'Warning: Unsaved data',
|
||
|
wx.YES_NO | wx.ICON_WARNING)
|
||
|
if mbx.ShowModal() == wx.ID_YES:
|
||
|
self.on_saveas(wx.Event())
|
||
|
mbx.Destroy()
|
||
|
|
||
|
wildcard = "HDF5 files (*.hdf5)|*.hdf5"
|
||
|
dlg = wx.FileDialog(
|
||
|
self, message="Open a file...", defaultDir=os.getcwd(),
|
||
|
defaultFile="", wildcard=wildcard, style=wx.OPEN
|
||
|
)
|
||
|
|
||
|
if dlg.ShowModal() == wx.ID_OK:
|
||
|
path = dlg.GetPath()
|
||
|
self._filename = path
|
||
|
self.data = Data.load(path)
|
||
|
self.create_notebooks()
|
||
|
dlg.Destroy()
|
||
|
self.update_title()
|
||
|
|
||
|
def on_save(self, event):
|
||
|
if self._filename:
|
||
|
if self.data.is_dirty():
|
||
|
self.data.save(self._filename)
|
||
|
else:
|
||
|
self.on_saveas(event)
|
||
|
|
||
|
def on_saveas(self, event):
|
||
|
overwrite = True
|
||
|
wildcard = "HDF5 files (*.hdf5)|*.hdf5|All files (*.*)|*.*"
|
||
|
dlg = wx.FileDialog(
|
||
|
self, message="Save file as ...", defaultDir=os.getcwd(),
|
||
|
defaultFile='{}.hdf5'.format(self.data.title.replace(' ','_')),
|
||
|
wildcard=wildcard, style=wx.SAVE)
|
||
|
dlg.SetFilterIndex(0)
|
||
|
|
||
|
if dlg.ShowModal() == wx.ID_OK:
|
||
|
path = dlg.GetPath()
|
||
|
if os.path.exists(path):
|
||
|
mbx = wx.MessageDialog(self, ('This file already exists. '
|
||
|
'Do you wish to overwrite it ?'),
|
||
|
'Warning: File exists',
|
||
|
wx.YES_NO | wx.ICON_WARNING)
|
||
|
if mbx.ShowModal() == wx.ID_NO:
|
||
|
overwrite = False
|
||
|
mbx.Destroy()
|
||
|
if overwrite:
|
||
|
self.data.save(path)
|
||
|
self._filename = path
|
||
|
dlg.Destroy()
|
||
|
self.update_title()
|
||
|
|
||
|
def on_viewdata(self, event):
|
||
|
dset = self.data[self._current_dset]
|
||
|
frame = _GridWindow(dset, parent=self)
|
||
|
frame.Show()
|
||
|
|
||
|
def on_viewcluster(self, event):
|
||
|
win = wx.Frame(None, size=wx.Size(480, 340))
|
||
|
cluster_viewer = ClusterViewer(win, size=wx.Size(480, 340))
|
||
|
|
||
|
dset = self.data[self._current_dset]
|
||
|
s = StringIO()
|
||
|
s.write(dset.get_parameter(group='Cluster', name='cluster')['value'])
|
||
|
atoms = ase.io.read(s, format='xyz')
|
||
|
cluster_viewer.set_atoms(atoms, rescale=True, center=True)
|
||
|
cluster_viewer.rotate_atoms(45., 45.)
|
||
|
cluster_viewer.show_emitter(True)
|
||
|
win.Show()
|
||
|
|
||
|
def on_viewparameters(self, event):
|
||
|
dset = self.data[self._current_dset]
|
||
|
frame = _ParametersWindow(dset, parent=self)
|
||
|
frame.Show()
|
||
|
|
||
|
def on_close(self, event):
|
||
|
if self.data.is_dirty():
|
||
|
mbx = wx.MessageDialog(self, ('Displayed data is unsaved. Do you '
|
||
|
'really want to quit ?'),
|
||
|
'Warning: Unsaved data',
|
||
|
wx.YES_NO | wx.ICON_WARNING)
|
||
|
if mbx.ShowModal() == wx.ID_NO:
|
||
|
mbx.Destroy()
|
||
|
return
|
||
|
self.Destroy()
|
||
|
|
||
|
|
||
|
def on_menu_dataset(self, event):
|
||
|
menu_id = event.GetId()
|
||
|
dset_name = self.GetMenuBar().FindItemById(menu_id).GetText()
|
||
|
self.show_dataset(dset_name)
|
||
|
|
||
|
|
||
|
def show_dataset(self, name):
|
||
|
for nb in list(self.notebooks.values()):
|
||
|
nb.Hide()
|
||
|
self.notebooks[name].Show()
|
||
|
self.Layout()
|
||
|
self.update_statusbar()
|
||
|
self._current_dset = name
|
||
|
|
||
|
def create_page(self, nb, view):
|
||
|
opts = view._plotopts
|
||
|
p = wx.Panel(nb, -1)
|
||
|
|
||
|
figure = Figure()
|
||
|
|
||
|
axes = None
|
||
|
proj = opts['projection']
|
||
|
scale = opts['scale']
|
||
|
if proj == 'rectilinear':
|
||
|
axes = figure.add_subplot(111, projection='rectilinear')
|
||
|
elif proj in ('polar', 'ortho', 'stereo'):
|
||
|
axes = figure.add_subplot(111, projection='polar')
|
||
|
|
||
|
canvas = FigureCanvas(p, -1, figure)
|
||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||
|
|
||
|
toolbar = NavigationToolbar2WxAgg(canvas)
|
||
|
toolbar.Realize()
|
||
|
|
||
|
sizer.Add(toolbar, 0, wx.ALL|wx.EXPAND)
|
||
|
toolbar.update()
|
||
|
|
||
|
sizer.Add(canvas, 5, wx.ALL|wx.EXPAND)
|
||
|
|
||
|
p.SetSizer(sizer)
|
||
|
p.Fit()
|
||
|
p.Show()
|
||
|
|
||
|
|
||
|
for values, label in zip(view.get_data(), opts['legend']):
|
||
|
if proj in ('ortho', 'stereo'):
|
||
|
theta, phi, Xsec = cols2matrix(*values)
|
||
|
theta_ticks = np.arange(0, 91, 15)
|
||
|
if proj == 'ortho':
|
||
|
R = np.sin(np.radians(theta))
|
||
|
R_ticks = np.sin(np.radians(theta_ticks))
|
||
|
elif proj == 'stereo':
|
||
|
R = 2 * np.tan(np.radians(theta/2.))
|
||
|
R_ticks = 2 * np.tan(np.radians(theta_ticks/2.))
|
||
|
#R = np.tan(np.radians(theta/2.))
|
||
|
X, Y = np.meshgrid(np.radians(phi), R)
|
||
|
im = axes.pcolormesh(X, Y, Xsec)
|
||
|
axes.set_yticks(R_ticks)
|
||
|
axes.set_yticklabels(theta_ticks)
|
||
|
|
||
|
figure.colorbar(im)
|
||
|
|
||
|
elif proj == 'polar':
|
||
|
values[0] = np.radians(values[0])
|
||
|
axes.plot(*values, label=label, picker=5, marker=opts['marker'])
|
||
|
else:
|
||
|
if scale == 'semilogx':
|
||
|
pltcmd = axes.semilogx
|
||
|
elif scale == 'semilogy':
|
||
|
pltcmd = axes.semilogy
|
||
|
elif scale == 'log':
|
||
|
pltcmd = axes.loglog
|
||
|
else:
|
||
|
pltcmd = axes.plot
|
||
|
pltcmd(*values, label=label, picker=5, marker=opts['marker'])
|
||
|
axes.grid(opts['grid'])
|
||
|
axes.set_title(opts['title'])
|
||
|
axes.set_xlabel(opts['xlabel'])
|
||
|
axes.set_ylabel(opts['ylabel'])
|
||
|
axes.set_xlim(*opts['xlim'])
|
||
|
axes.set_ylim(*opts['ylim'])
|
||
|
if label:
|
||
|
axes.legend()
|
||
|
axes.autoscale(enable=opts['autoscale'])
|
||
|
|
||
|
|
||
|
# MPL events
|
||
|
figure.canvas.mpl_connect('motion_notify_event', self.on_mpl_motion)
|
||
|
figure.canvas.mpl_connect('pick_event', self.on_mpl_pick)
|
||
|
|
||
|
nb.AddPage(p, view.title)
|
||
|
|
||
|
|
||
|
def update_statusbar(self):
|
||
|
sb = self.GetStatusBar()
|
||
|
menu_id = self.GetMenuBar().FindMenu('Datasets')
|
||
|
menu = self.GetMenuBar().GetMenu(menu_id)
|
||
|
for item in menu.GetMenuItems():
|
||
|
if item.IsChecked():
|
||
|
sb.SetStatusText("%s" % item.GetText(), 1)
|
||
|
break
|
||
|
|
||
|
def update_title(self):
|
||
|
title = "MsSpec Data Viewer"
|
||
|
if self.data.title:
|
||
|
title += ": " + self.data.title
|
||
|
if self._filename:
|
||
|
title += " [" + os.path.basename(self._filename) + "]"
|
||
|
self.SetTitle(title)
|
||
|
|
||
|
def on_mpl_motion(self, event):
|
||
|
sb = self.GetStatusBar()
|
||
|
try:
|
||
|
txt = "[{:.3f}, {:.3f}]".format(event.xdata, event.ydata)
|
||
|
sb.SetStatusText(txt, 2)
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
def on_mpl_pick(self, event):
|
||
|
print(event.artist)
|
||
|
|
||
|
def on_page_changed(self, event):
|
||
|
self.update_statusbar()
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
if False:
|
||
|
data = Data('all my data')
|
||
|
dset = data.add_dset('Dataset 0')
|
||
|
X = np.arange(0, 20)
|
||
|
Y = X**2
|
||
|
|
||
|
dset.add_columns(x=X, y=Y, z=X+2, w=Y**3)
|
||
|
dset.add_parameter(name='truc', group='main', value='3.14', unit='eV')
|
||
|
dset.add_parameter(name='machin', group='main', value='abc', unit='')
|
||
|
|
||
|
# Z = [0,1]
|
||
|
#
|
||
|
# for z in Z:
|
||
|
# for x, y in zip(X, Y):
|
||
|
# dset.add_row(x=x, y=y, z=z, random=np.random.rand())
|
||
|
#
|
||
|
#
|
||
|
view = dset.add_view('my view', autoscale=True)
|
||
|
view.select('x', 'y', where="z<10", legend=r"z = 0")
|
||
|
view.select('x', 'y', where="z>10", legend=r"z = 1")
|
||
|
print(dset.get_parameter(group='main'))
|
||
|
constraint = lambda a, b: (a > 10 and a < 15) and b > 0
|
||
|
indices = list(map(constraint, dset.x, dset.w))
|
||
|
print(dset.y[indices])
|
||
|
|
||
|
#data.view()
|
||
|
import sys
|
||
|
data = Data.load(sys.argv[1])
|
||
|
data.view()
|
||
|
|
||
|
|
||
|
|
||
|
|