""" Classes for reading and writing STARDB files. """ import os, sys, gzip, bz2, lzma import re import textwrap import numpy as np import scipy.sparse import hashlib from collections.abc import Iterable from enum import IntEnum from copy import copy, deepcopy from pathlib import Path from isotope import ion as I from human import byte2human from logged import Logged from utils import prod, xz_file_size, CachedAttribute from loader import loader, _loader from abuset import AbuData, IonList from uuidtime import UUID1 def load(filename): return StarDB(filename) # used to replace '^' as anker point _db_path = '~/starfit_data/db' class StarDB(AbuData, Logged): """ Class for reading STARDB binary files. Compressed files with .gz will be automatically uncompressed. This may fail, however, if the file is bigger than 2GB or 4GB. Compressed files with .bz2 will be automatically uncompressed. This is currently rather inefficient because to determine the file size the entire stream need to be read first. FIELD_TYPES: 0 UNDEFINED Undefined 1 BYTE Byte 2 INT Integer 3 LONG Longword integer 4 FLOAT Floating point 5 DOUBLE Double-precision floating 6 COMPLEX Complex floating 7 STRING String 8 STRUCT Structure [NOT ALLOWED] 9 DCOMPLEX Double-precision complex 10 POINTER Pointer 11 OBJREF Object reference 12 UINT Unsigned Integer 13 ULONG Unsigned Longword Integer 14 LONG64 64-bit Integer 15 ULONG64 Unsigned 64-bit Integer """ current_version = 10100 _extension = 'stardb' sys_is_le = sys.byteorder == 'little' native_byteorder = '<' if sys_is_le else '>' defaultbyteorder = native_byteorder # this should actually become an information class abundance_type_names = ( 'isomer', 'isotope', 'element', 'mass number (isobar)', 'neutron number (isotone)', ) class AbundanceType(IntEnum): isomer = 0 isotope = 1 element = 2 isobar = 3 isotone = 4 abundance_class_names = ( 'all (raw)', 'rad (raw + decays)', 'dec (stable subset of radiso)', 'mix (mix of different types for special purpose)', ) class AbundanceClass(IntEnum): raw = 0 rad = 1 dec = 2 mix = 3 abundance_unit_names = ( 'mass (g)', 'mass (solar)', 'mol', 'mass fraction (X)', 'mol fraction (YPS)', 'log epsion', '[ ]', 'production factor (10^[])', 'log [Y/Si] + 6', ) class AbundanceUnit(IntEnum): g = 0 solar_mass = 1 mol = 2 mass_fraction = 3 mol_fraction = 4 log_eps = 5 bracket = 6 production_factor = 7 log_abu = 8 abundance_total_names = ( 'initial (total, not normalized, fallback is missing mass)', 'ejecta', ) class AbundanceTotal(IntEnum): initial = 0 ejecta = 1 abundance_data_names = ( 'all ejecta (SN ejecta + wind)', 'SN ejecta, no wind','wind only', 'ejecta including fallback and wind (outside piston)', ) class AbundanceData(IntEnum): all_ejecta = 0 SN_ejecta = 1 piston = 2 abundance_sum_names = ( 'mass fraction', 'number fraction', ) class AbundanceSum(IntEnum): mass_fraction = 0 number_fraction = 1 # add whether normalized where appropriate ??? flag_names = ( 'parameter', 'property', ) class Flags(IntEnum): parameter = 0 property = 1 type_names = ( 'UNDEFINED', 'BYTE', 'INT', 'LONG', 'FLOAT', 'DOUBLE', 'COMPLEX', 'STRING', 'STRUCT', 'DCOMPLEX', 'POINTER', 'OBJREF', 'UINT', 'ULONG', 'LONG64', 'ULONG64', ) class Type(IntEnum): undefined = 0 byte = 1 int = 2 long = 3 float = 4 double = 5 float64 = 5 complex = 6 string = 7 struct = 8 dcomplex = 9 pointer = 10 objref = 11 unit = 12 ulong = 13 long64 = 14 int64 = 14 ulong64 = 15 uint64 = 15 class SignatureError(Exception): """ Exception raised when signature could not be read. """ def __init__(self, filename): """ Store file name that causes error. """ self.filename = filename def __str__(self): """ Return error message. """ return "Error reading signature from file {:s}."\ .format(self.filename) class VersionError(Exception): """ Exception raised when version mismatch. """ def __init__(self): """ Just set up. """ def __str__(self): """ Return error message. """ return "Version Error.".format() class IntegrityError(Exception): """ Exception raised when file integrity seems broken. """ def __init__(self): """ Just set up. """ def __str__(self): """ Return error message. """ return "File Integrity Error.".format() class DataError(Exception): """ Exception raised when data seems faulty. """ def __init__(self): """ Just set up. """ def __str__(self): """ Return error message. """ return "Data seems faulty.".format() def __init__(self, filename = None, db = None, **kwargs): """ Initialize data fields and open file. Optionally the byte order can be specified. The default is big endian. """ kw = kwargs.copy() kw['filename'] = filename kw['db'] = db if filename is not None: self._from_file(**kw) elif db is not None: self._from_db(**kw) else: self._from_data(**kw) def abu(self): """ Overwrite inherited routine. There is more different kinsd of data than the orignial version can handle. """ return self.data def _set_dtypes(self): self.swapbyteorder = self.byteorder != self.native_byteorder try: if self.swapbyteorder: self.logger.info('Swapping endian.') else: self.logger.info('Not swapping endian.') except AttributeError: pass self.dtype_i8 = np.dtype(np.int64).newbyteorder(self.byteorder) self.dtype_u8 = np.dtype(np.uint64).newbyteorder(self.byteorder) self.dtype_f8 = np.dtype(np.float64).newbyteorder(self.byteorder) self.dtype_i8 = np.dtype(np.int64) self.dtype_u8 = np.dtype(np.uint64) self.dtype_f8 = np.dtype(np.float64) self.dtypes = np.zeros(len(self.type_names), dtype = np.object) self.dtypes[ [self.Type.float64, self.Type.int64, self.Type.uint64, ] ] = [ self.dtype_f8, self.dtype_i8, self.dtype_u8] def _from_db(self, db = None, **kwargs): """ Initialize from existing db. similar to copy but allow derived class initializtion """ self.byteorder = copy(db.byteorder) self._set_dtypes() self.version = copy(db.version) self.name = copy(db.name) self.comments = db.comments.copy() self.ions = db.ions.copy() self.data = db.data.copy() self.fielddata = db.fielddata.copy() self.nstar = db.nstar self.nabu = db.nabu self.nfield = db.nfield self.fieldnames = db.fieldnames.copy() self.fieldunits = db.fieldunits.copy() self.fieldtypes = db.fieldtypes.copy() self.fieldformats = db.fieldformats.copy() self.fieldflags = db.fieldflags.copy() self.abundance_type = copy(db.abundance_type) self.abundance_class = copy(db.abundance_class) self.abundance_unit = copy(db.abundance_unit) self.abundance_total = copy(db.abundance_total) self.abundance_norm = copy(db.abundance_norm) self.abundance_data = copy(db.abundance_data) self.abundance_sum = copy(db.abundance_sum) self.nvalues = db.nvalues.copy() self.values = db.values.copy() self.indices = db.indices.copy() self._from_db_other(db) def _from_db_other(self, db): """ Dummy routine to link in data copying by derived DB classes """ pass def _from_data(self, **kwargs): """ Create DB from provided data """ silent = kwargs.get('silent', False) self.setup_logger(silent = silent) self.byteorder = kwargs.get('byteorder', self.defaultbyteorder) self._set_dtypes() self.version = self.current_version self.name = kwargs.get('name', None) self.comments = kwargs.get('comments', tuple()) if isinstance (self.comments, str): comments = ('comments',) self.comments = np.array(self.comments, dtype = np.object) self.comments = np.append(self.comments, 'UUID: {}'.format(UUID1())) self.ions = kwargs.get('ions', None) self.data = kwargs.get('data', None) if isinstance(self.data, AbuData): if self.ions is None: self.ions = self.data.ions.copy() self.data = self.data.data.copy() self.fielddata = kwargs.get('fielddata', None) assert self.data.ndim == 2 self.nstar = self.data.shape[0] self.nabu = self.data.shape[1] self.nfield = len(self.fielddata.dtype) if not isinstance(self.ions, IonList): self.ions = IonList(self.ions) assert self.nstar == self.fielddata.shape[0] assert self.nabu == len(self.ions) self.fieldnames = kwargs.get('fieldnames' , None) self.fieldunits = kwargs.get('fieldunits' , None) self.fieldtypes = kwargs.get('fieldtypes' , None) self.fieldformats = kwargs.get('fieldformats', None) self.fieldflags = kwargs.get('fieldflags' , None) if self.fieldnames is None: self.fieldnames = np.array( self.fielddata.dtype.names, dtype = np.object) else: self.fieldnames = np.array( self.fieldnames, dtype = np.object) if self.fieldunits is None: self.fieldunits = np.array( [''] * self.nfield, dtype = np.object) else: self.fieldunits = np.array( self.fieldunits, dtype = np.object) if self.fieldflags is None: self.fieldflags = np.array( [0] * self.nfield, dtype = np.uint64) else: self.fieldflags = np.array( self.fieldflags, dtype = np.uint64) if self.fieldtypes is None: fieldtypes = [] dtypes = {np.uint64: self.Type.uint64, np.int64: self.Type.int64, np.float64: self.Type.float64, } for i in range(self.nfield): t = self.fielddata.dtype[i].type fieldtypes += [dtypes[t]] self.fieldtypes = np.array( fieldtypes, dtype = np.uint64) else: self.fieldtypes = np.array( self.fieldtypes, dtype = np.uint64) if self.fieldformats is None: self.fieldformats = np.array( ['8.2G'] * self.nfield, dtype = np.object) else: self.fieldformats = np.array( self.fieldformats, dtype = np.object) assert self.nfield == len(self.fieldnames ) assert self.nfield == len(self.fieldunits ) assert self.nfield == len(self.fieldtypes ) assert self.nfield == len(self.fieldformats) assert self.nfield == len(self.fieldflags ) self.abundance_type = kwargs.get('abundance_type' , self.AbundanceType.element) self.abundance_class = kwargs.get('abundance_class' , self.AbundanceClass.dec) self.abundance_unit = kwargs.get('abundance_unit' , self.AbundanceUnit.mol_fraction) self.abundance_total = kwargs.get('abundance_total' , self.AbundanceTotal.ejecta) self.abundance_norm = kwargs.get('abundance_norm' , None) self.abundance_data = kwargs.get('abundance_data' , self.AbundanceData.all_ejecta) self.abundance_sum = kwargs.get('abundance_sum' , self.AbundanceSum.number_fraction) if self.abundance_type is None: if self.ions[0].is_isomer: self.abundance_type = self.AbundanceType.isomer elif self.ions[0].is_isotope: self.abundance_type = self.AbundanceType.isotope elif self.ions[0].is_element: self.abundance_type = self.AbundanceType.element elif self.ions[0].is_isobar: self.abundance_type = self.AbundanceType.isobar elif self.ions[0].is_isotone: self.abundance_type = self.AbundanceType.isotone else: raise Exception('unknown isotope type') if self.name is None: self.name = '' if self.abundance_norm is None: self.abundance_norm = '' # compute and add hash sha1_comment = kwargs.get('sha1_comment', False) if sha1_comment: m = hashlib.sha1() m.update(self.data.tobytes()) m.update(self.fielddata.tobytes()) self.comments = self.comments.append('SHA1: {}'.format(m.hexdigest())) self.compute_fielddata() self.print_info() self.close_logger(timing = 'DB generated from data in') def write(self, filename = None, silent = False, byteorder = None, ): """ Write data to file. """ self.setup_logger(silent = silent) saved_byteorder = self.byteorder if byteorder is not None: self.byteorder = byteorder self.swapbyteorder = self.byteorder != self.native_byteorder self._open_file(filename, mode = "write") self._write() self._close() self.byteorder = saved_byteorder self.swapbyteorder = self.byteorder != self.native_byteorder self.close_logger(timing = f'File {filename!s} written in') def _from_file(self, filename = None, silent = False, **kwargs): """ Create DB by loading from file. """ self.setup_logger(silent = silent) if (s := str(filename)).startswith('^'): s = s[1:] if s.startswith('/'): s = s[1:] filename = Path(_db_path) / s self._open_file(filename) self.logger.info('Loading {:s}'.format( self.filename)) s = 'File size: {}'.format(byte2human(self.filesize)) if self.compressed: s += ' (compressed)' self.logger.info(s + '.') self.byteorder = self._check_signature() self._set_dtypes() self._load() self._close() self.close_logger(timing = 'Data loaded in') def _open_file(self, filename, mode = 'read'): """ Open the file. TODO - automatic compression mode if compression == None """ compressions = ['', '.gz', '.xz', '.bz2'] self.filename = os.path.expandvars(os.path.expanduser(filename)) if mode == 'read': for c in compressions: fn = self.filename + c if os.path.exists(fn): self.filename = fn break else: raise IOError("File not found.") access = 'rb' else: access = 'wb' if self.filename.endswith('.gz'): self.compressed = True self.compress_mode = 'gz' self.file = gzip.open(self.filename, access) if mode == 'read': pos = self.file.myfileobj.tell() self.file.myfileobj.seek(-4, os.SEEK_END) self.filesize = np.ndarray(1, dtype = " 0: self.logger.error('found zero data sets.') raise self.DataError() self._read_other() self._read_tail() self.compute_fielddata() self.print_info() def _read_other(self): """ Dummy routine to link in data writing by derived DB classes """ pass def compute_fielddata(self): """ compute field reference values """ nvalues = np.zeros(self.nfield, dtype=np.uint64) values = np.ndarray((self.nfield, self.nstar), dtype=np.float64) ivalues = np.ndarray((self.nfield, self.nstar), dtype=np.uint64) values[:] = np.nan for ifield in range(self.nfield): # values v = self.fielddata[self.fieldnames[ifield]] vv, vx = np.unique(v, return_inverse = True) nv = len(vv) values[ifield, 0:nv] = vv nvalues[ifield] = nv ivalues[ifield] = vx nvalues_max = max(nvalues) values = values[:,0:nvalues_max] self.nvalues = nvalues self.values = values self.indices = ivalues def print_info(self): """ Print out DB info """ self.logger.info('Data version: {:6d}'.format(int(self.version))) self.logger.info('data set name: {:s}'.format(self.name)) self.logger.info(''.ljust(58, "=")) for comment in self.comments: self.logger.info('COMMENT: {:s}'.format(comment)) self.logger.info(''.ljust(58, "=")) self.logger.info('data sets: {:6d}'.format(int(self.nstar))) self.logger.info('abundance sets: {:6d}'.format(int(self.nabu))) self.logger.info(''.ljust(58, "-")) self.logger.info('abundance type: {:1d} - {:s}'.format(int(self.abundance_type ),self.abundance_type_names[ self.abundance_type])) self.logger.info('abundance class: {:1d} - {:s}'.format(int(self.abundance_class),self.abundance_class_names[self.abundance_class])) self.logger.info('abundance unit: {:1d} - {:s}'.format(int(self.abundance_unit ),self.abundance_unit_names[ self.abundance_unit])) self.logger.info('abundance total: {:1d} - {:s}'.format(int(self.abundance_total),self.abundance_total_names[self.abundance_total])) s = self.abundance_norm if s == '': s = '(NONE)' self.logger.info('abundance norm: {:}'.format(s)) self.logger.info('abundance data: {:1d} - {:s}'.format(int(self.abundance_data ),self.abundance_data_names[self.abundance_data])) self.logger.info('abundance sum: {:1d} - {:s}'.format(int(self.abundance_sum ),self.abundance_sum_names[ self.abundance_sum])) self.logger.info(''.ljust(58, "-")) self.logger.info('{:d} data fields: '.format(self.nfield)) l1 = max(len(x) for x in self.fieldnames) l2 = 0 l3 = 0 l4 = max(len(x) for x in self.fieldunits) l5 = 0 re_len = re.compile('^([0-9]+)', flags = re.I) for ifield in range(self.nfield): # output formatting l5 = max(l5, len(self.type_names[self.fieldtypes[ifield]])) flen = int(re_len.findall(self.fieldformats[ifield])[0]) l2 = max(l2,flen) l3 = max(l3,len('{:d}'.format(self.nvalues[ifield]))) format = "{{:{:d}s}} [{{:>{:d}s}}] ({{:{:d}s}}) <{{:s}}>".format(l1, l4, l5) for ifield in range(self.nfield): self.logger.info(format.format( self.fieldnames[ifield], self.fieldunits[ifield], self.type_names[self.fieldtypes[ifield]], self.flag_names[self.fieldflags[ifield]])) if len(np.argwhere(self.type_names[self.fieldtypes[ifield]] == np.array(['DOUBLE','LONG64','ULONG64']))) == 0: self.logger.error('data type not yet supported') self.logger.error('only supporting 8-byte scalar data types') raise self.VersionError() self.logger.info(''.ljust(58, "-")) self.logger.info('ABUNDANCES:') s = ' '.join(str(i) for i in self.ions) s = textwrap.wrap(s, 50) for line in s: self.logger.info(line) # These two b;ock below should become their own function xpar = np.argwhere(self.fieldflags == 0) if len(xpar) > 0: self.logger.info(''.ljust(58, "-")) self.logger.info('PARAMETER RANGES:') for ip in xpar.flat: fmax = max(self.values[ip,0:self.nvalues[ip]]) fmin = min(self.values[ip,0:self.nvalues[ip]]) line = (self.fieldnames[ip]+': ', ("{:"+self.fieldformats[ip] + "}").format(fmin), ("{:"+self.fieldformats[ip] + "}").format(fmax), "{:d}".format(int(self.nvalues[ip]))) format = "{{:<{:d}s}} {{:>{:d}s}} ... {{:>{:d}s}} ({{:>{:d}s}} values)".format(l1+2,l2,l2,l3) self.logger.info(format.format(*line)) xprop = np.argwhere(self.fieldflags != 0) if len(xprop) > 0: self.logger.info(''.ljust(58,"-")) self.logger.info('PROPERTY RANGES:') for ip in xprop.flat: fmax = max(self.values[ip,0:self.nvalues[ip]]) fmin = min(self.values[ip,0:self.nvalues[ip]]) line=(self.fieldnames[ip]+': ', ("{:"+self.fieldformats[ip] + "}").format(fmin), ("{:"+self.fieldformats[ip] + "}").format(fmax), "{:d}".format(int(self.nvalues[ip]))) format="{{:<{:d}s}} {{:>{:d}s}} ... {{:>{:d}s}} ({{:>{:d}s}} values)".format(l1+2,l2,l2,l3) self.logger.info(format.format(*line)) if len(xpar) > 0: self.logger.info(''.ljust(58, "-")) self.logger.info('PARAMETER VALUES:') for ip in xpar.flat: self.logger.info(self.fieldnames[ip] + ':') flen = int(re_len.findall(self.fieldformats[ifield])[0]) s = '' f = " {:"+self.fieldformats[ip] + "}" for id in range(self.nvalues[ip]): if len(s) >= 50: self.logger.info(s) s = '' s += f.format(self.values[ip, id]) self.logger.info(s) maxpropvalues = 100 if len(xprop) > 0: self.logger.info(''.ljust(58, "-")) self.logger.info('PROPERTY VALUES:') for ip in xprop.flat: self.logger.info(self.fieldnames[ip] + ':') if self.nvalues[ip] > maxpropvalues: self.logger.info('(more than {:d} values)'.format(maxpropvalues)) else: flen = int(re_len.findall(self.fieldformats[ifield])[0]) s = '' f = " {:"+self.fieldformats[ip] + "}" for id in range(self.nvalues[ip]): if len(s) >= 50: self.logger.info(s) s = '' s += f.format(self.values[ip,id]) self.logger.info(s) self.logger.info(''.ljust(58, "-")) # compute hash m = hashlib.sha1() m.update(self.data.tobytes()) m.update(self.fielddata.tobytes()) self.logger.info('SHA1: {}'.format(m.hexdigest())) self.logger.info(''.ljust(58, "-")) def _write(self): """ Write data to file. """ self.version = self.current_version self.filesize = 0 self._write_signature() self._write_uin(self.version) self._write_str(self.name) self._write_str_arr(self.comments) self._write_uin(self.nstar ) self._write_uin(self.nfield) self._write_uin(self.nabu ) self._write_uin(self.abundance_type ) self._write_uin(self.abundance_class) self._write_uin(self.abundance_unit ) self._write_uin(self.abundance_total) self._write_str(self.abundance_norm ) self._write_uin(self.abundance_data ) self._write_uin(self.abundance_sum ) fieldformats = self.fieldformats.copy() for i in range(self.nfield): fieldformats[i] = self._pyformat2idlformat(self.fieldformats[i]) self._write_str(self.fieldnames ) self._write_str(self.fieldunits ) self._write_uin(self.fieldtypes ) self._write_str( fieldformats) self._write_uin(self.fieldflags ) # set A,Z,E from ions abu_Z = np.array([ion.Z for ion in self.ions], dtype=np.uint64) abu_E = np.array([ion.E for ion in self.ions], dtype=np.uint64) if self.abundance_type in (0, 1, 2, 3): abu_A = np.array([ion.A for ion in self.ions], dtype=np.uint64) elif self.abundance_type == 4: abu_A = np.array([ion.N for ion in self.ions], dtype=np.uint64) else: self.logger.error('anundance type not defined.') raise self.DataError() self._write_uin(abu_Z) self._write_uin(abu_A) self._write_uin(abu_E) self._write_stu(self.fielddata) self._write_dbl(self.data.transpose()) self._write_other() self._write_tail() def _write_other(self): """ Dummy routine to link in data reading by derived DB classes """ pass def _compute_index(self, array_limit = 2**24): nindex = np.ndarray((self.nfield, self.nstar), dtype=np.uint64) vindex = np.ndarray((self.nfield,), dtype=np.object) imax = np.ndarray((self.nfield,), dtype=np.uint64) for ifield in range(self.nfield): vindex[ifield] = scipy.sparse.lil_matrix((self.nstar, self.nstar), dtype=np.uint64) nv = self.nvalues[ifield] for iv in range(nv): ii, = np.where(self.indices[ifield] == iv) ni = len(ii) nindex[ifield, iv] = ni vindex[ifield][iv, :ni] = ii nimax = np.max(nindex[ifield,:nv]) imax[ifield] = nimax vindex[ifield] = vindex[ifield][0:nv,0:nimax] nvmax = np.max(self.nvalues) nimax = np.max(imax) if self.nfield * nvmax * nimax <= array_limit: v = np.ndarray((self.nfield, nvmax, nimax), dtype=np.uint64) for ifield in range(self.nfield): v[ifield, :self.nvalues[ifield], :imax[ifield]] = vindex[ifield].toarray() vindex = v else: print(self.nfield * nvmax * nimax) self._nvindex = nindex self._vindex = vindex @CachedAttribute def nvindex(self): """ return number of elements for each value in value index array """ try: self._nvindex except AttributeError: self._compute_index() return self._nvindex @CachedAttribute def vindex(self): """ return value index array array of star indices for given parameter value """ try: self._vindex except AttributeError: self._compute_index() return self._vindex @staticmethod def _idlformat2pyformat(format): """ Convert IDL format coding into python format coding. """ fmt_convert = {'I' : 'D'} fmt = format[1:] + fmt_convert.get(format[0], format[0]) return fmt @staticmethod def _pyformat2idlformat(format): """ Convert python format coding into IDL format coding. """ fmt_convert = {'D' : 'I'} fmt = fmt_convert.get(format[-1], format[-1]) + format[:-1] return fmt def get_star_slice(self, *args, **kwargs): """ return slice of star indices for given parameters """ if len(args) == 1 and isinstance(agrs[0], collections.Mapping): fields = args[0] else: fields = kwargs for key in fields: assert key in self.fieldnames vfilter = dict() for i, f in enumerate(self.fieldnames): val = fields.get(f, None) if val is not None: j = np.abs(self.values[i, :self.nvalues[i]] - fields[f]).argmin() vfilter[i] = int(j) mask = np.tile(True, self.nstar) for i,j in vfilter.items(): imask = self.fielddata[self.fieldnames[i]] == self.values[i,j] mask = np.logical_and(mask, imask) indices, = np.where(mask) return indices # ------------------------------------------------------ # data access methods # ------------------------------------------------------ def index_info(self, indices): """ Print field information for list of indices. """ if not isinstance(indices, Iterable): indices = (indices,) maxlenname = max(len(n) for n in self.fieldnames) maxlenvalue = max(int(f[:-1].split('.')[0]) for f in self.fieldformats) maxlenunit = max(len(n) for n in self.fieldunits) maxlen = maxlenname + maxlenvalue + maxlenunit + 3 print('-'*maxlen) for i in indices: print('{name:>{maxlenname}s}: {index:>{maxlenvalue}d}'.format( name = 'index', index = i, maxlenvalue = maxlenvalue, maxlenname = maxlenname, )) for f,n,u in zip(self.fieldformats, self.fieldnames, self.fieldunits): print('{name:>{maxlenname:d}s}: {value:>{maxlenvalue}s} {unit:s}'.format( name = n, unit = u, value = '{value:{format:s}}'.format( value = self.fielddata[i][n], format = f), maxlenname = maxlenname, maxlenvalue = maxlenvalue, )) print('-'*maxlen) def abundance_info(self, indices): """ Print out abundance info for givien index """ if not isinstance(indices, Iterable): indices = (indices,) print('=' * (9 + 13 * len(indices))) print(('{:>8s}' + ' '.join(['{:>12d}'] * len(indices))).format( 'Ion', *indices)) print('=' * (9 + 13 * len(indices))) for ion, abu in zip(self.ions, self.data[indices,:].transpose()): print(('{!s:>8}' + ' '.join(['{:12.5e}'] * len(indices))).format( ion, *abu )) print('=' * (9 + 13 * len(indices))) load = loader(StarDB, __name__ + '.load') _load = _loader(StarDB, __name__ + '.load') loadstardb = load def test(): import filecmp f0 = os.path.expanduser('~/kepler/znuc/test.stardb') f1 = os.path.expanduser('~/test.dat') d = StarDB(f0) print('='*40) d.write(f1) print('='*40) x = StarDB(f1) print(filecmp.cmp(f0, f1)) print('='*40) d = StarDB(db = x) d.write(f1) x = StarDB(f1) print(filecmp.cmp(f0, f1))