import datetime import getpass import gzip import json import socket import warnings from collections import defaultdict import lxml.etree as et import numpy as np from .. import version as pyerrorsversion from ..covobs import Covobs from ..obs import Obs, _merge_idx # Based on https://stackoverflow.com/a/10076823 def _etree_to_dict(t): """ Convert the content of an XML file to a python dict""" d = {t.tag: {} if t.attrib else None} children = list(t) if children: dd = defaultdict(list) for dc in map(_etree_to_dict, children): for k, v in dc.items(): dd[k].append(v) d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} if t.attrib: d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) if t.text: text = t.text.strip() if children or t.attrib: if text: d[t.tag]['#data'] = [text] else: d[t.tag] = text return d def _dict_to_xmlstring(d): if isinstance(d, dict): iters = '' for k in d: if k.startswith('#'): for la in d[k]: iters += la iters = '\n' + iters + '<{}array>\n'.format('/') return iters if isinstance(d[k], dict): iters += f'<{k}>\n' + _dict_to_xmlstring(d[k]) + '<{}{}>\n'.format('/', k) elif isinstance(d[k], str): if len(d[k]) > 100: iters += f'<{k}>\n ' + d[k] + ' \n<{}{}>\n'.format('/', k) else: iters += f'<{k}> ' + d[k] + ' <{}{}>\n'.format('/', k) elif isinstance(d[k], list): for i in range(len(d[k])): iters += _dict_to_xmlstring(d[k][i]) elif not d[k]: return '\n' else: raise Exception('Type', type(d[k]), 'not supported in export!') else: raise Exception('Type', type(d), 'not supported in export!') return iters def _dict_to_xmlstring_spaces(d, space=' '): s = _dict_to_xmlstring(d) o = '' c = 0 cm = False for li in s.split('\n'): if li.startswith('<{}'.format('/')): c -= 1 cm = True for _i in range(c): o += space o += li + '\n' if li.startswith('<') and not cm: if '<{}'.format('/') not in li: c += 1 cm = False return o def create_pobs_string(obsl, name, spec='', origin='', symbol=None, enstag=None): """Export a list of Obs or structures containing Obs to an xml string according to the Zeuthen pobs format. Tags are not written or recovered automatically. The separator | is removed from the replica names. Parameters ---------- obsl : list List of Obs that will be exported. The Obs inside a structure have to be defined on the same ensemble. name : str The name of the observable. spec : str Optional string that describes the contents of the file. origin : str Specify where the data has its origin. symbol : list A list of symbols that describe the observables to be written. May be empty. enstag : str Enstag that is written to pobs. If None, the ensemble name is used. Returns ------- xml_str : str XML formatted string of the input data """ if symbol is None: symbol = [] od = {} ename = obsl[0].e_names[0] names = list(obsl[0].deltas.keys()) nr = len(names) onames = [name.replace('|', '') for name in names] for o in obsl: if len(o.e_names) != 1: raise Exception('You try to export dobs to obs!') if o.e_names[0] != ename: raise Exception('You try to export dobs to obs!') if len(o.deltas.keys()) != nr: raise Exception('Incompatible obses in list') od['observables'] = {} od['observables']['schema'] = {'name': 'lattobs', 'version': '1.0'} od['observables']['origin'] = { 'who': getpass.getuser(), 'date': str(datetime.datetime.now())[:-7], 'host': socket.gethostname(), 'tool': {'name': 'pyerrors', 'version': pyerrorsversion.__version__}} od['observables']['pobs'] = {} pd = od['observables']['pobs'] pd['spec'] = spec pd['origin'] = origin pd['name'] = name if enstag: if not isinstance(enstag, str): raise Exception('enstag has to be a string!') pd['enstag'] = enstag else: pd['enstag'] = ename pd['nr'] = f'{nr}' pd['array'] = [] osymbol = 'cfg' if not isinstance(symbol, list): raise Exception('Symbol has to be a list!') if not (len(symbol) == 0 or len(symbol) == len(obsl)): raise Exception(f'Symbol has to be a list of lenght 0 or {len(obsl)}!') for s in symbol: osymbol += f' {s}' for r in range(nr): ad = {} ad['id'] = onames[r] Nconf = len(obsl[0].deltas[names[r]]) layout = f'{Nconf} i f{len(obsl)}' ad['layout'] = layout ad['symbol'] = osymbol data = '' for c in range(Nconf): data += f'{obsl[0].idl[names[r]][c]} ' for o in obsl: num = o.deltas[names[r]][c] + o.r_values[names[r]] if num == 0: data += '0 ' else: data += f'{num:1.16e} ' data += '\n' ad['#data'] = data pd['array'].append(ad) rs = '\n' + _dict_to_xmlstring_spaces(od) return rs def write_pobs(obsl, fname, name, spec='', origin='', symbol=None, enstag=None, gz=True): """Export a list of Obs or structures containing Obs to a .xml.gz file according to the Zeuthen pobs format. Tags are not written or recovered automatically. The separator | is removed from the replica names. Parameters ---------- obsl : list List of Obs that will be exported. The Obs inside a structure have to be defined on the same ensemble. fname : str Filename of the output file. name : str The name of the observable. spec : str Optional string that describes the contents of the file. origin : str Specify where the data has its origin. symbol : list A list of symbols that describe the observables to be written. May be empty. enstag : str Enstag that is written to pobs. If None, the ensemble name is used. gz : bool If True, the output is a gzipped xml. If False, the output is an xml file. Returns ------- None """ pobsstring = create_pobs_string(obsl, name, spec, origin, symbol, enstag) if not fname.endswith('.xml') and not fname.endswith('.gz'): fname += '.xml' if gz: if not fname.endswith('.gz'): fname += '.gz' fp = gzip.open(fname, 'wb') fp.write(pobsstring.encode('utf-8')) else: fp = open(fname, 'w', encoding='utf-8') fp.write(pobsstring) fp.close() def _import_data(string): return json.loads("[" + ",".join(string.replace(' +', ' ').split()) + "]") def _check(condition): if not condition: raise Exception("XML file format not supported") class _NoTagInDataError(Exception): """Raised when tag is not in data""" def __init__(self, tag): self.tag = tag super().__init__(f'Tag {self.tag} not in data!') def _find_tag(dat, tag): for i in range(len(dat)): if dat[i].tag == tag: return i raise _NoTagInDataError(tag) def _import_array(arr): name = arr[_find_tag(arr, 'id')].text.strip() index = _find_tag(arr, 'layout') try: sindex = _find_tag(arr, 'symbol') except _NoTagInDataError: sindex = 0 if sindex > index: tmp = _import_data(arr[sindex].tail) else: tmp = _import_data(arr[index].tail) li = arr[index].text.strip() m = li.split() if m[1] == "i" and m[2][0] == "f": nc = int(m[0]) na = int(m[2].lstrip('f')) _dat = [] mask = [] for a in range(na): mask += [a] _dat += [np.array(tmp[1 + a:: na + 1])] _check(len(tmp[0:: na + 1]) == nc) return [name, tmp[0:: na + 1], mask, _dat] elif m[1][0] == 'f' and len(m) < 3: sh = (int(m[0]), int(m[1].lstrip('f'))) return np.reshape(tmp, sh) elif any(['f' in s for s in m]): for si in range(len(m)): if m[si] == 'f': break sh = [int(m[i]) for i in range(si)] return np.reshape(tmp, sh) else: print(name, m) _check(False) def _import_rdata(rd): name, idx, _mask, deltas = _import_array(rd) return deltas, name, idx def _import_cdata(cd): _check(cd[0].tag == "id") _check(cd[1][0].text.strip() == "cov") cov = _import_array(cd[1]) grad = _import_array(cd[2]) return cd[0].text.strip(), cov, grad def read_pobs(fname, full_output=False, gz=True, separator_insertion=None): """Import a list of Obs from an xml.gz file in the Zeuthen pobs format. Tags are not written or recovered automatically. Parameters ---------- fname : str Filename of the input file. full_output : bool If True, a dict containing auxiliary information and the data is returned. If False, only the data is returned as list. separatior_insertion: str or int str: replace all occurences of "separator_insertion" within the replica names by "|%s" % (separator_insertion) when constructing the names of the replica. int: Insert the separator "|" at the position given by separator_insertion. None (default): Replica names remain unchanged. Returns ------- res : list[Obs] Imported data or res : dict Imported data and meta-data """ if not fname.endswith('.xml') and not fname.endswith('.gz'): fname += '.xml' if gz: if not fname.endswith('.gz'): fname += '.gz' with gzip.open(fname, 'r') as fin: content = fin.read() else: if fname.endswith('.gz'): warnings.warn(f"Trying to read from {fname} without unzipping!", UserWarning, stacklevel=2) with open(fname) as fin: content = fin.read() # parse xml file content root = et.fromstring(content) _check(root[2].tag == 'pobs') pobs = root[2] version = root[0][1].text.strip() _check(root[1].tag == 'origin') file_origin = _etree_to_dict(root[1])['origin'] deltas = [] names = [] idl = [] for i in range(5, len(pobs)): delta, name, idx = _import_rdata(pobs[i]) deltas.append(delta) if separator_insertion is None: pass elif isinstance(separator_insertion, int): name = name[:separator_insertion] + '|' + name[separator_insertion:] elif isinstance(separator_insertion, str): name = name.replace(separator_insertion, f"|{separator_insertion}") else: raise Exception("separator_insertion has to be string or int, is ", type(separator_insertion)) names.append(name) idl.append(idx) res = [Obs([d[i] for d in deltas], names, idl=idl) for i in range(len(deltas[0]))] descriptiond = {} for i in range(4): descriptiond[pobs[i].tag] = pobs[i].text.strip() _check(pobs[4].tag == "nr") _check(pobs[5].tag == 'array') if pobs[5][1].tag == 'symbol': symbol = pobs[5][1].text.strip() descriptiond['symbol'] = symbol if full_output: retd = {} tool = file_origin.get('tool', None) if tool: program = tool['name'] + ' ' + tool['version'] else: program = '' retd['program'] = program retd['version'] = version retd['who'] = file_origin['who'] retd['date'] = file_origin['date'] retd['host'] = file_origin['host'] retd['description'] = descriptiond retd['obsdata'] = res return retd else: return res # this is based on Mattia Bruno's implementation at https://github.com/mbruno46/pyobs/blob/master/pyobs/IO/xml.py def import_dobs_string(content, full_output=False, separator_insertion=True): """Import a list of Obs from a string in the Zeuthen dobs format. Tags are not written or recovered automatically. Parameters ---------- content : str XML string containing the data full_output : bool If True, a dict containing auxiliary information and the data is returned. If False, only the data is returned as list. separatior_insertion: str, int or bool str: replace all occurences of "separator_insertion" within the replica names by "|%s" % (separator_insertion) when constructing the names of the replica. int: Insert the separator "|" at the position given by separator_insertion. True (default): separator "|" is inserted after len(ensname), assuming that the ensemble name is a prefix to the replica name. None or False: No separator is inserted. Returns ------- res : list[Obs] Imported data or res : dict Imported data and meta-data """ root = et.fromstring(content) _check(root.tag == 'OBSERVABLES') _check(root[0].tag == 'SCHEMA') version = root[0][1].text.strip() _check(root[1].tag == 'origin') file_origin = _etree_to_dict(root[1])['origin'] _check(root[2].tag == 'dobs') dobs = root[2] descriptiond = {} for i in range(3): descriptiond[dobs[i].tag] = dobs[i].text.strip() _check(dobs[3].tag == 'array') symbol = [] if dobs[3][1].tag == 'symbol': symbol = dobs[3][1].text.strip() descriptiond['symbol'] = symbol mean = _import_array(dobs[3])[0] _check(dobs[4].tag == "ne") ne = int(dobs[4].text.strip()) _check(dobs[5].tag == "nc") idld = {} deltad = {} covd = {} gradd = {} names = [] e_names = [] enstags = {} for k in range(6, len(list(dobs))): if dobs[k].tag == "edata": _check(dobs[k][0].tag == "enstag") ename = dobs[k][0].text.strip() e_names.append(ename) _check(dobs[k][1].tag == "nr") R = int(dobs[k][1].text.strip()) for i in range(2, 2 + R): deltas, rname, idx = _import_rdata(dobs[k][i]) if separator_insertion is None or False: pass elif separator_insertion is True: if rname.startswith(ename): rname = rname[:len(ename)] + '|' + rname[len(ename):] elif isinstance(separator_insertion, int): rname = rname[:separator_insertion] + '|' + rname[separator_insertion:] elif isinstance(separator_insertion, str): rname = rname.replace(separator_insertion, f"|{separator_insertion}") else: raise Exception("separator_insertion has to be string or int, is ", type(separator_insertion)) if '|' in rname: new_ename = rname[:rname.index('|')] else: new_ename = ename enstags[new_ename] = ename idld[rname] = idx deltad[rname] = deltas names.append(rname) elif dobs[k].tag == "cdata": cname, cov, grad = _import_cdata(dobs[k]) covd[cname] = cov if grad.shape[1] == 1: gradd[cname] = [grad for i in range(len(mean))] else: gradd[cname] = grad.T else: _check(False) names = list(set(names)) for name in names: for i in range(len(deltad[name])): tmp = np.zeros_like(deltad[name][i]) for j in range(len(deltad[name][i])): if deltad[name][i][j] != 0.: tmp[j] = deltad[name][i][j] + mean[i] deltad[name][i] = tmp res = [] for i in range(len(mean)): deltas = [] idl = [] obs_names = [] for name in names: h = np.unique(deltad[name][i]) if len(h) == 1 and np.all(h == mean[i]): continue repdeltas = [] repidl = [] for j in range(len(deltad[name][i])): if deltad[name][i][j] != 0.: repdeltas.append(deltad[name][i][j]) repidl.append(idld[name][j]) if len(repdeltas) > 0: obs_names.append(name) deltas.append(repdeltas) idl.append(repidl) obsmeans = [np.average(deltas[j]) for j in range(len(deltas))] res.append(Obs([np.array(deltas[j]) - obsmeans[j] for j in range(len(obsmeans))], obs_names, idl=idl, means=obsmeans)) res[-1]._value = mean[i] _check(len(e_names) == ne) cnames = list(covd.keys()) for i in range(len(res)): new_covobs = {name: Covobs(0, covd[name], name, grad=gradd[name][i]) for name in cnames} for name in cnames: if np.all(new_covobs[name].grad == 0): del new_covobs[name] cnames_loc = list(new_covobs.keys()) for name in cnames_loc: res[i].names.append(name) res[i].shape[name] = 1 res[i].idl[name] = [] res[i]._covobs = new_covobs if symbol: for i in range(len(res)): res[i].tag = symbol[i] if res[i].tag == 'None': res[i].tag = None if full_output: retd = {} tool = file_origin.get('tool', None) if tool: program = tool['name'] + ' ' + tool['version'] else: program = '' retd['program'] = program retd['version'] = version retd['who'] = file_origin['who'] retd['date'] = file_origin['date'] retd['host'] = file_origin['host'] retd['description'] = descriptiond retd['enstags'] = enstags retd['obsdata'] = res return retd else: return res def read_dobs(fname, full_output=False, gz=True, separator_insertion=True): """Import a list of Obs from an xml.gz file in the Zeuthen dobs format. Tags are not written or recovered automatically. Parameters ---------- fname : str Filename of the input file. full_output : bool If True, a dict containing auxiliary information and the data is returned. If False, only the data is returned as list. gz : bool If True, assumes that data is gzipped. If False, assumes XML file. separatior_insertion: str, int or bool str: replace all occurences of "separator_insertion" within the replica names by "|%s" % (separator_insertion) when constructing the names of the replica. int: Insert the separator "|" at the position given by separator_insertion. True (default): separator "|" is inserted after len(ensname), assuming that the ensemble name is a prefix to the replica name. None or False: No separator is inserted. Returns ------- res : list[Obs] Imported data or res : dict Imported data and meta-data """ if not fname.endswith('.xml') and not fname.endswith('.gz'): fname += '.xml' if gz: if not fname.endswith('.gz'): fname += '.gz' with gzip.open(fname, 'r') as fin: content = fin.read() else: if fname.endswith('.gz'): warnings.warn(f"Trying to read from {fname} without unzipping!", UserWarning, stacklevel=2) with open(fname) as fin: content = fin.read() return import_dobs_string(content, full_output, separator_insertion=separator_insertion) def _dobsdict_to_xmlstring(d): if isinstance(d, dict): iters = '' for k in d: if k.startswith('#value'): for li in d[k]: iters += li return iters + '\n' elif k.startswith('#'): for li in d[k]: iters += li iters = '\n' + iters + '<{}array>\n'.format('/') return iters if isinstance(d[k], dict): iters += f'<{k}>\n' + _dobsdict_to_xmlstring(d[k]) + '<{}{}>\n'.format('/', k) elif isinstance(d[k], str): if len(d[k]) > 100: iters += f'<{k}>\n ' + d[k] + ' \n<{}{}>\n'.format('/', k) else: iters += f'<{k}> ' + d[k] + ' <{}{}>\n'.format('/', k) elif isinstance(d[k], list): tmps = '' if k in ['edata', 'cdata']: for i in range(len(d[k])): tmps += f'<{k}>\n' + _dobsdict_to_xmlstring(d[k][i]) + f'\n' else: for i in range(len(d[k])): tmps += _dobsdict_to_xmlstring(d[k][i]) iters += tmps elif isinstance(d[k], (int, float)): iters += f'<{k}> ' + str(d[k]) + ' <{}{}>\n'.format('/', k) elif not d[k]: return '\n' else: raise Exception('Type', type(d[k]), 'not supported in export!') else: raise Exception('Type', type(d), 'not supported in export!') return iters def _dobsdict_to_xmlstring_spaces(d, space=' '): s = _dobsdict_to_xmlstring(d) o = '' c = 0 cm = False for li in s.split('\n'): if li.startswith('<{}'.format('/')): c -= 1 cm = True for _i in range(c): o += space o += li + '\n' if li.startswith('<') and not cm: if '<{}'.format('/') not in li: c += 1 cm = False return o def create_dobs_string(obsl, name, spec='dobs v1.0', origin='', symbol=None, who=None, enstags=None): """Generate the string for the export of a list of Obs or structures containing Obs to a .xml.gz file according to the Zeuthen dobs format. Tags are not written or recovered automatically. The separator |is removed from the replica names. Parameters ---------- obsl : list List of Obs that will be exported. The Obs inside a structure do not have to be defined on the same set of configurations, but the storage requirement is increased, if this is not the case. name : str The name of the observable. spec : str Optional string that describes the contents of the file. origin : str Specify where the data has its origin. symbol : list A list of symbols that describe the observables to be written. May be empty. who : str Provide the name of the person that exports the data. enstags : dict Provide alternative enstag for ensembles in the form enstags = {ename: enstag} Otherwise, the ensemble name is used. Returns ------- xml_str : str XML string generated from the data """ if enstags is None: enstags = {} if symbol is None: symbol = [] od = {} r_names = [] for o in obsl: r_names += [name for name in o.names if name.split('|')[0] in o.mc_names] r_names = sorted(set(r_names)) mc_names = sorted(set([n.split('|')[0] for n in r_names])) for tmpname in mc_names: if tmpname not in enstags: enstags[tmpname] = tmpname ne = len(set(mc_names)) cov_names = [] for o in obsl: cov_names += list(o.cov_names) cov_names = sorted(set(cov_names)) nc = len(set(cov_names)) od['OBSERVABLES'] = {} od['OBSERVABLES']['SCHEMA'] = {'NAME': 'lattobs', 'VERSION': '1.0'} if who is None: who = getpass.getuser() od['OBSERVABLES']['origin'] = { 'who': who, 'date': str(datetime.datetime.now())[:-7], 'host': socket.gethostname(), 'tool': {'name': 'pyerrors', 'version': pyerrorsversion.__version__}} od['OBSERVABLES']['dobs'] = {} pd = od['OBSERVABLES']['dobs'] pd['spec'] = spec pd['origin'] = origin pd['name'] = name pd['array'] = {} pd['array']['id'] = 'val' pd['array']['layout'] = f'1 f{len(obsl)}' osymbol = '' if symbol: if not isinstance(symbol, list): raise Exception('Symbol has to be a list!') if not (len(symbol) == 0 or len(symbol) == len(obsl)): raise Exception(f'Symbol has to be a list of lenght 0 or {len(obsl)}!') osymbol = symbol[0] for s in symbol[1:]: osymbol += f' {s}' pd['array']['symbol'] = osymbol pd['array']['#values'] = [' '.join([f'{o.value:1.16e}' for o in obsl])] pd['ne'] = f'{ne}' pd['nc'] = f'{nc}' pd['edata'] = [] for name in mc_names: ed = {} ed['enstag'] = enstags[name] onames = sorted([n for n in r_names if (n.startswith(name + '|') or n == name)]) nr = len(onames) ed['nr'] = nr ed[''] = [] for r in range(nr): ad = {} repname = onames[r] ad['id'] = repname.replace('|', '') idx = _merge_idx([o.idl.get(repname, []) for o in obsl]) Nconf = len(idx) layout = f'{Nconf} i f{len(obsl)}' ad['layout'] = layout data = '' counters = [0 for o in obsl] offsets = [o.r_values[repname] - o.value if repname in o.r_values else 0 for o in obsl] for ci in idx: data += f'{ci} ' for oi in range(len(obsl)): o = obsl[oi] if repname in o.idl: if counters[oi] < 0: num = 0 if num == 0: data += '0 ' else: data += f'{num:1.16e} ' continue if o.idl[repname][counters[oi]] == ci: num = o.deltas[repname][counters[oi]] + offsets[oi] if num == 0: data += '0 ' else: data += f'{num:1.16e} ' counters[oi] += 1 if counters[oi] >= len(o.idl[repname]): counters[oi] = -1 else: num = 0 if num == 0: data += '0 ' else: data += f'{num:1.16e} ' else: data += '0 ' data += '\n' ad['#data'] = data ed[''].append(ad) pd['edata'].append(ed) allcov = {} for o in obsl: for cname in o.cov_names: if cname in allcov: if not np.array_equal(allcov[cname], o.covobs[cname].cov): raise Exception(f'Inconsistent covariance matrices for {cname}!') else: allcov[cname] = o.covobs[cname].cov pd['cdata'] = [] for cname in cov_names: cd = {} cd['id'] = cname covd = {'id': 'cov'} if allcov[cname].shape == (): ncov = 1 covd['layout'] = '1 1 f' covd['#data'] = f'{allcov[cname]:1.14e}' else: shape = allcov[cname].shape assert (shape[0] == shape[1]) ncov = shape[0] covd['layout'] = f'{ncov} {ncov} f' ds = '' for i in range(ncov): for j in range(ncov): val = allcov[cname][i][j] if val == 0: ds += '0 ' else: ds += f'{val:1.14e} ' ds += '\n' covd['#data'] = ds gradd = {'id': 'grad'} gradd['layout'] = f'{ncov} f{len(obsl)}' ds = '' for i in range(ncov): for o in obsl: if cname in o.covobs: val = o.covobs[cname].grad[i].item() if val != 0: ds += f'{val:1.14e} ' else: ds += '0 ' else: ds += '0 ' gradd['#data'] = ds cd['array'] = [covd, gradd] pd['cdata'].append(cd) rs = '\n' + _dobsdict_to_xmlstring_spaces(od) return rs def write_dobs(obsl, fname, name, spec='dobs v1.0', origin='', symbol=None, who=None, enstags=None, gz=True): """Export a list of Obs or structures containing Obs to a .xml.gz file according to the Zeuthen dobs format. Tags are not written or recovered automatically. The separator | is removed from the replica names. Parameters ---------- obsl : list List of Obs that will be exported. The Obs inside a structure do not have to be defined on the same set of configurations, but the storage requirement is increased, if this is not the case. fname : str Filename of the output file. name : str The name of the observable. spec : str Optional string that describes the contents of the file. origin : str Specify where the data has its origin. symbol : list A list of symbols that describe the observables to be written. May be empty. who : str Provide the name of the person that exports the data. enstags : dict Provide alternative enstag for ensembles in the form enstags = {ename: enstag} Otherwise, the ensemble name is used. gz : bool If True, the output is a gzipped XML. If False, the output is a XML file. Returns ------- None """ if enstags is None: enstags = {} dobsstring = create_dobs_string(obsl, name, spec, origin, symbol, who, enstags=enstags) if not fname.endswith('.xml') and not fname.endswith('.gz'): fname += '.xml' if gz: if not fname.endswith('.gz'): fname += '.gz' fp = gzip.open(fname, 'wb') fp.write(dobsstring.encode('utf-8')) else: fp = open(fname, 'w', encoding='utf-8') fp.write(dobsstring) fp.close()