#! /usr/bin/env python
"""
.. module:: tools.modpyslha
:synopsis: Modified pyslha reader.
A simple but flexible handler of the SUSY Les Houches Accord (SLHA) data format.
pyslha is a parser/writer module for particle physics SUSY Les Houches Accord
(SLHA) supersymmetric spectrum/decay files, and a collection of scripts which
use the interface, e.g. for conversion to and from the legacy ISAWIG format, or
to plot the mass spectrum and decay chains.
The current release supports SLHA version 1, and as far as I'm aware is also
fully compatible with SLHA2: the block structures are read and accessed
generically. If you have any problems, please provide an example input file and
I'll happily investigate. SLHA3 is not yet supported (or standardised) but in
recent releases the new structures will not crash the parser. Support will be
added once the format is standardised (and in response to demand!)
The plotting script provides output in PDF, EPS and PNG via LaTeX and the TikZ
graphics package, and as LaTeX/TikZ source for direct embedding into documents or
user-tweaking of the generated output.
Users of version 1.x should note that the interface has changed a little in
version 2.0.0 and onward, in particular in the interface of the Block objects,
which are now more dict-like: entries can be added and accessed via the usual
square-brackets indexing operators, including for multiple indices as is common
for mixing matrices e.g. NMIX[1,2] as opposed to the old NMIX.entries[1][2]
way. This does break backward compatibility but is a big improvement both for
internal code sanity and usability. The Block interface also now supplies
dict-like has_key(), keys(), and items() methods, as well as more specialist
value(), set_value() and is_single_valued() methods for improved access to ALPHA
and any other unindexed blocks.
If you use PySLHA, for either model data handling or spectrum visualisation,
please cite the paper: http://arxiv.org/abs/1305.4194
TODOs:
For 3.0.x:
* Add handling of XSECTION if/when standardised.
* In set_value, if first item is non-int, treat as None-indexed.
* Refine value string heuristic for strings with ints in them?
* Use Doc to handle document-level header comments.
* Add block and decay summary comments to the OrderedDict (wrap OrderedDict as DataStore?)
For 3.1.x:
* Preserve inline comments from read -> write (needs full-line/inline comment
separation). Can use separate comment dicts in Block and Decay, and
attach a multiline .comment attr to the returned/written dicts.
Later/maybe:
* Add Sphinx docs.
* Identify HERWIG decay matrix element to use in ISAWIG.
* Handle RPV SUSY in ISAWIG.
"""
__author__ = "Andy Buckley <andy.buckley@cern.ch>"
__version__ = "3.0.0"
###############################################################################
## Private utility functions
def _mkdict():
"""Return an OrderedDict if possible, or a normal dict if not."""
try:
from collections import OrderedDict
return OrderedDict()
except:
try:
from ordereddict import OrderedDict
return OrderedDict()
except:
return dict()
_d = _mkdict()
class _dict(type(_d)):
"""A cosmetic wrapper on an OrderedDict if possible, or a normal dict if not."""
def __init__(self, name=None):
"""
Initializes the dict class with name=name.
"""
super(_dict, self).__init__()
self.name = name
def __repr__(self):
"""
Generates a string representation of self.
"""
s = ""
if self.name:
s += self.name + "\n"
s += "\n ".join(str(val) for val in self.values())
return s
def _autotype(var):
"""
Automatically convert strings to numerical types if possible.
"""
if type(var) is not str:
return var
if var.isdigit() or (var.startswith("-") and var[1:].isdigit()):
return int(var)
try:
f = float(var)
return f
except ValueError:
return var
def _autostr(var, precision=8):
"""Automatically format numerical types as the right sort of string."""
if type(var) is float:
return ("% ." + str(precision) + "e") % var
elif not hasattr(var, "__iter__"):
return str(var)
else:
return ",".join(_autostr(subval) for subval in var)
def _autotuple(a):
"""Automatically convert the supplied iterable to a scalar or tuple as appropriate."""
if len(a) == 1:
return a[0]
return tuple(a)
def _read(f):
"""Read a file's contents, autodetecting whether the arg is a file or filename,
and treating '-' as as indication to read from stdin."""
if type(f) is str:
if f == "-":
return sys.stdin.read()
else:
with open(f, "r") as ff:
return ff.read()
else:
return f.read()
def _write(f, txt):
"""Write to a file, autodetecting whether the arg is a file or filename,
and treating '-' as as indication to write to stdout."""
if type(f) is str:
if f == "-":
sys.stdout.write(txt)
else:
with open(f, "w") as ff:
f.write(txt)
else:
f.write(txt)
###############################################################################
## Exceptions
[docs]class AccessError(Exception):
"""
Exception object to be raised when a SLHA block is accessed in an invalid way
"""
def __init__(self, errmsg):
self.msg = errmsg
def __str__(self):
return self.msg
[docs]class ParseError(Exception):
"""
Exception object to be raised when a spectrum file/string is malformed
"""
def __init__(self, errmsg):
self.msg = errmsg
def __str__(self):
return self.msg
###############################################################################
## The Doc top-level container object
[docs]class Doc(object):
"""
Top level container for everything in an SLHA record
"""
def __init__(self, blocks, decays=None, xsections=None):
"""
Initializes the Doc object with the info given.
"""
self.blocks = blocks
self.decays = decays
self.xsections = xsections
self.comment = ""
[docs] def write(self, filename=None, ignorenobr=False, precision=8):
"""
Convenient method for converting an SLHA Doc object to SLHA format,
either returned as a string or written to a file depending on whether
the filename variable is None.
"""
if filename is None:
return writeSLHA(self, ignorenobr, precision)
else:
write(filename, self, ignorenobr, precision)
def __repr__(self):
s = "<PySLHA Doc: %d blocks, %d decays, %d xsections" % \
(len(self.blocks or []), len(self.decays or []), len(self.xsections or []))
if self.comment:
s += " %s" % self.comment
s += ">"
return s
###############################################################################
## The data block, decay and particle classes
[docs]class Block(object):
"""
Object representation of any BLOCK elements read from an SLHA file.
Blocks have a name, may have an associated Q value, and contain a collection
of data entries, each indexed by one or more keys. Entries in the dictionary
are stored as numeric types (int or float) when a cast from the string in
the file has been possible.
Block is closely related to a Python dict (and, in fact, is implemented via
an OrderedDict when possible). The preferred methods of entry access use the
dict-like [] operator for getting and setting, and the keys() and items()
methods for iteration. Purely iterating over the object behaves like keys(),
as for an ordinary dict.
Multiple (integer) indices are possible, especially for entries in mixing
matrix blocks. These are now implemented in the natural way, e.g. for access
to the (1,2) element of a mixing matrix block, use bmix[1,2] = 0.123 and
print bmix[1,2]. The value() and set_value() functions behave
similarly. Multi-element values are also permitted.
It is possible, although not usual, to store unindexed values in a
block. This is only supported when that entry is the only one in the block,
and it is stored in the normal fashion but with None as the lookup key. The
value() method may be used without a key argument to retrieve this value, if
the is_single_valued() method returns True, and similarly the set_value()
method may be used to set it if only one argument is supplied and the object
is compatible.
"""
def __init__(self, name, q=None, entries=None):
self.name = name
self.entries = _dict()
if entries is not None:
self.entries.update(entries)
self.q = _autotype(q)
# TODO: Rename? To what?
[docs] def add_entry(self, args):
"""Add an entry to the block from an iterable (i.e. list or tuple) of
strings, or from a whitespace-separated string.
This method is just for convenience: it splits the single string
argument if necessary and converts the list of strings into numeric
types when possible. For the treatment of the resulting iterable see the
set_value method.
"""
## If the argument is a single string, split it and proceed
if type(args) is str:
args = args.split()
## Check that the arg is an iterable
if not hasattr(args, "__iter__"):
raise AccessError("Block entries must be iterable")
## Auto-convert the types in the list
args = map(_autotype, args)
## Re-join consecutive strings into single entries
i = 0
while i < len(args)-1:
if type(args[i]) is str and type(args[i+1]) is str:
args[i] += " " + args[i+1]
del args[i+1]
continue
i += 1
## Add the entry to the map, with appropriate indices
self.set_value(*args)
[docs] def is_single_valued(self):
"""Return true if there is only one entry, and it has no index: the
'value()' attribute may be used in that case without an argument."""
return len(self.entries) == 1 and self.entries.keys()[0] is None
[docs] def value(self, key=None, default=1):
"""Get a value from the block with the supplied key.
If no key is given, then the block must contain only one non-indexed
value otherwise an AccessError exception will be raised.\
"""
if key is None and not self.is_single_valued():
raise AccessError("Tried to access unique value of multi-value block")
if not self.has_key(key):
return default
return self.entries[key]
"Alias"
get = value
[docs] def set_value(self, *args):
"""Set a value in the block via supplied key/value arguments.
Indexing is determined automatically: any leading integers will be
treated as a multi-dimensional index, with the remaining entries being a
(potentially multi-dimensional) value. If all N args are ints, then the
first N-1 are treated as the index and the Nth as the value.
If there is only one arg it will be treated as the value of a
single-valued block. In this case the block must already contain at most
one non-indexed value otherwise an AccessError exception will be
raised.\
"""
if len(args) == 0:
raise AccessError("set_value() called without arguments")
elif len(args) == 1:
if len(self.entries) > 0 and not self.is_single_valued():
raise AccessError("Tried to set a unique value on a multi-value block")
self.entries[None] = args[0]
else:
## Find the first non-integer -- all previous items are indices
i_first_nonint = -1
for i, x in enumerate(args):
if type(x) is not int:
i_first_nonint = i
break
if i_first_nonint == 0:
pass
else:
self.entries[_autotuple(args[:i_first_nonint])] = _autotuple(args[i_first_nonint:])
"Alias"
set = set_value
[docs] def has_key(self, key):
"""Does the block have the given key?"""
return self.entries.has_key(key)
[docs] def keys(self):
"""Access the block item keys."""
return self.entries.keys()
[docs] def values(self):
"""Access the block item values."""
return self.entries.values()
[docs] def items(self, key=None):
"""Access the block items as (key,value) tuples.
Note: The Python 3 dict attribute 'items()' is used rather than the
'old' Python 2 'iteritems()' name for forward-looking compatibility.\
"""
return self.entries.iteritems()
def __len__(self):
return len(self.entries)
def __iter__(self):
return self.entries.__iter__()
def __getitem__(self, key):
return self.entries[key]
def __setitem__(self, key, value):
if key is not None and type(key) is not int and not all(type(x) is int for x in key):
raise AccessError("Attempted to set a block entry with a non-integer(s) index")
self.entries[key] = value
def __cmp__(self, other):
return cmp(self.name, other.name) and cmp(self.entries, other.entries)
def __repr__(self):
s = self.name
if self.q is not None:
s += " (Q=%s)" % self.q
s += " { " + "; ".join(_autostr(k) + " : " + _autostr(v) for k, v in self.items()) + " }"
return s
[docs]class Decay(object):
"""
Object representing a decay entry on a particle decribed by the SLHA file.
'Decay' objects are not a direct representation of a DECAY block in an SLHA
file... that role, somewhat confusingly, is taken by the Particle class.
Decay objects have three properties: a branching ratio, br, an nda number
(number of daughters == len(ids)), and a tuple of PDG PIDs to which the
decay occurs. The PDG ID of the particle whose decay this represents may
also be stored, but this is normally known via the Particle in which the
decay is stored.
"""
def __init__(self, br, nda, ids, parentid=None):
self.parentid = parentid
self.br = br
self.nda = nda
self.ids = ids
assert(self.nda == len(self.ids))
def __cmp__(self, other):
return cmp(other.br, self.br)
def __repr__(self):
return "% .8e %s" % (self.br, self.ids)
[docs]class Particle(object):
"""
Representation of a single, specific particle, decay block from an SLHA
file. These objects are not themselves called 'Decay', since that concept
applies more naturally to the various decays found inside this
object. Particle classes store the PDG ID (pid) of the particle being
represented, and optionally the mass (mass) and total decay width
(totalwidth) of that particle in the SLHA scenario. Masses may also be found
via the MASS block, from which the Particle.mass property is filled, if at
all. They also store a list of Decay objects (decays) which are probably the
item of most interest.
"""
def __init__(self, pid, totalwidth=None, mass=None):
self.pid = pid
self.totalwidth = totalwidth
self.mass = mass
self.decays = []
[docs] def add_decay(self, br, nda, ids):
"""
Adds the decay entry to the Particle object.
"""
self.decays.append(Decay(br, nda, ids))
self.decays.sort()
def __cmp__(self, other):
if abs(self.pid) == abs(other.pid):
return cmp(self.pid, other.pid)
return cmp(abs(self.pid), abs(other.pid))
def __repr__(self):
s = str(self.pid)
if self.mass is not None:
s += " : mass = %.8e GeV" % self.mass
if self.totalwidth is not None:
s += " : total width = %.8e GeV" % self.totalwidth
for d in self.decays:
if d.br > 0.0:
s += "\n %s" % d
return s
###############################################################################
## SLHA parsing and writing functions
[docs]def readSLHA(spcstr, ignorenobr=False, ignorenomass=False, ignoreblocks=[]):
"""
Read an SLHA definition from a string, returning dictionaries of blocks and
decays.
If the ignorenobr parameter is True, do not store decay entries with a
branching ratio of zero.
If the ignorenomass parameter is True, parse file even if mass block is
absent in the file (default is to raise a ParseError).
"""
blocks = _dict("Blocks")
decays = _dict("Decays")
#
import re
currentblock = None
currentdecay = None
for line in spcstr.splitlines():
## Handle (ignore) comment lines
# TODO: Store block/entry comments
if line.startswith("#"):
continue
if "#" in line:
line = line[:line.index("#")]
## Ignore empty lines (after comment removal and whitespace trimming)
if not line.strip():
continue
## Section header lines start with a non-whitespace character, data lines have a whitespace indent
# TODO: Are tabs also allowed for indents? Check the SLHA standard.
if not line.startswith(" "):
# TODO: Should we now strip the line to remove any trailing whitespace?
## Handle BLOCK start lines
if line.upper().startswith("BLOCK"):
#print line
match = re.match(r"BLOCK\s+(\w+)(\s+Q\s*=\s*.+)?", line.upper())
if not match:
continue
blockname = match.group(1)
if blockname in ignoreblocks:
currentblock = None
currentdecay = None
else:
qstr = match.group(2)
if qstr is not None:
qstr = qstr[qstr.find("=")+1:].strip()
currentblock = blockname
currentdecay = None
blocks[blockname] = Block(blockname, q=qstr)
## Handle DECAY start lines
elif line.upper().startswith("DECAY"):
match = re.match(r"DECAY\s+(-?\d+)\s+([\d\.E+-]+|NAN).*", line.upper())
if not match:
continue
pdgid = int(match.group(1))
width = float(match.group(2)) if match.group(2) != "NAN" else None
currentblock = "DECAY"
currentdecay = pdgid
decays[pdgid] = Particle(pdgid, width)
## Handle unknown section type start lines (and continue ignoring until a non-header line is found)
elif type(_autotype(line.split()[0])) is str:
import sys
# if line.split()[0]!="XSECTION":
## xsection isnt unknown.
# sys.stderr.write("Ignoring unknown section type: %s\n" % line.split()[0])
currentblock = None
currentdecay = None
## This non-empty line starts with an indent, hence must be an in-section data line
else:
# TODO: Should we now strip the line to remove the indent (and any trailing whitespace)?
if currentblock is not None:
items = line.split()
if len(items) < 1:
continue
if currentblock != "DECAY":
try:
blocks[currentblock].add_entry(items)
except:
import sys
sys.stderr.write("Ignoring badly formatted block: %s\n" % currentblock)
del blocks[currentblock]
currentblock = None
# TODO: Add handling of XSECTION if/when standardised
else:
br = float(items[0]) if items[0].upper() != "NAN" else None
nda = int(items[1])
ids = map(int, items[2:])
if br > 0.0 or not ignorenobr: # br == None is < 0
decays[currentdecay].add_decay(br, nda, ids)
## Try to populate Particle masses from the MASS block
# print blocks.keys()
try:
for pid in blocks["MASS"].keys():
if decays.has_key(pid):
decays[pid].mass = blocks["MASS"][pid]
except:
if not ignorenomass:
raise ParseError("No MASS block found: cannot populate particle masses")
rtn = Doc(blocks, decays)
return rtn
[docs]def writeSLHABlocks(blocks, precision=8):
"""Return an SLHA definition as a string, from the supplied blocks dict."""
sep = 3 * " "
blockstrs = []
for bname, b in blocks.iteritems():
namestr = b.name
if b.q is not None:
namestr += " Q= " + _autostr(float(b.q), precision)
blockstr = "BLOCK %s\n" % namestr
entrystrs = []
for k, v in b.items():
entrystr = sep
if type(k) == tuple:
entrystr += sep.join(_autostr(i, precision) for i in k)
elif k is not None:
entrystr += _autostr(k, precision)
entrystr += sep + _autostr(v, precision)
entrystrs.append(entrystr)
blockstr += "\n".join(entrystrs)
blockstrs.append(blockstr)
return "\n\n".join(blockstrs)
[docs]def writeSLHADecays(decays, ignorenobr=False, precision=8):
"""Return an SLHA decay definition as a string, from the supplied decays dict."""
sep = 3 * " "
blockstrs = []
for pid, particle in decays.iteritems():
blockstr = ("DECAY %d " % particle.pid) + _autostr(particle.totalwidth or -1, precision) + "\n"
decaystrs = []
for d in particle.decays:
if d.br > 0.0 or not ignorenobr:
products_str = sep.join("% d" % i for i in d.ids)
decaystr = sep + _autostr(d.br, precision) + sep + str(len(d.ids)) + sep + products_str
decaystrs.append(decaystr)
blockstr += "\n".join(decaystrs)
blockstrs.append(blockstr)
return "\n\n".join(blockstrs)
[docs]def writeSLHA(doc, ignorenobr=False, precision=8):
"""Return an SLHA definition as a string, from the supplied blocks and decays dicts."""
ss = [x for x in (writeSLHABlocks(doc.blocks, precision), writeSLHADecays(doc.decays, ignorenobr, precision)) if x]
return "\n\n".join(ss)
###############################################################################
## PDG <-> HERWIG particle ID code translations for ISAWIG handling
## Static array of HERWIG IDHW codes mapped to PDG MC ID codes, based on
## http://www.hep.phy.cam.ac.uk/~richardn/HERWIG/ISAWIG/susycodes.html
## + the IDPDG array and section 4.13 of the HERWIG manual.
_HERWIGID2PDGID = {}
_HERWIGID2PDGID[7] = -1
_HERWIGID2PDGID[8] = -2
_HERWIGID2PDGID[9] = -3
_HERWIGID2PDGID[10] = -4
_HERWIGID2PDGID[11] = -5
_HERWIGID2PDGID[12] = -6
_HERWIGID2PDGID[13] = 21
_HERWIGID2PDGID[59] = 22
_HERWIGID2PDGID[121] = 11
_HERWIGID2PDGID[122] = 12
_HERWIGID2PDGID[123] = 13
_HERWIGID2PDGID[124] = 14
_HERWIGID2PDGID[125] = 15
_HERWIGID2PDGID[126] = 16
_HERWIGID2PDGID[127] = -11
_HERWIGID2PDGID[128] = -12
_HERWIGID2PDGID[129] = -13
_HERWIGID2PDGID[130] = -14
_HERWIGID2PDGID[131] = -15
_HERWIGID2PDGID[132] = -16
_HERWIGID2PDGID[198] = 24 # W+
_HERWIGID2PDGID[199] = -24 # W-
_HERWIGID2PDGID[200] = 23 # Z0
_HERWIGID2PDGID[201] = 25 ## SM HIGGS
_HERWIGID2PDGID[203] = 25 ## HIGGSL0 (== PDG standard in this direction)
_HERWIGID2PDGID[204] = 35 ## HIGGSH0
_HERWIGID2PDGID[205] = 36 ## HIGGSA0
_HERWIGID2PDGID[206] = 37 ## HIGGS+
_HERWIGID2PDGID[207] = -37 ## HIGGS-
_HERWIGID2PDGID[401] = 1000001 ## SSDLBR
_HERWIGID2PDGID[407] = -1000001 ## SSDLBR
_HERWIGID2PDGID[402] = 1000002 ## SSULBR
_HERWIGID2PDGID[408] = -1000002 ## SSUL
_HERWIGID2PDGID[403] = 1000003 ## SSSLBR
_HERWIGID2PDGID[409] = -1000003 ## SSSL
_HERWIGID2PDGID[404] = 1000004 ## SSCLBR
_HERWIGID2PDGID[410] = -1000004 ## SSCL
_HERWIGID2PDGID[405] = 1000005 ## SSB1BR
_HERWIGID2PDGID[411] = -1000005 ## SSB1
_HERWIGID2PDGID[406] = 1000006 ## SST1BR
_HERWIGID2PDGID[412] = -1000006 ## SST1
_HERWIGID2PDGID[413] = 2000001 ## SSDR
_HERWIGID2PDGID[419] = -2000001 ## SSDRBR
_HERWIGID2PDGID[414] = 2000002 ## SSUR
_HERWIGID2PDGID[420] = -2000002 ## SSURBR
_HERWIGID2PDGID[415] = 2000003 ## SSSR
_HERWIGID2PDGID[421] = -2000003 ## SSSRBR
_HERWIGID2PDGID[416] = 2000004 ## SSCR
_HERWIGID2PDGID[422] = -2000004 ## SSCRBR
_HERWIGID2PDGID[417] = 2000005 ## SSB2
_HERWIGID2PDGID[423] = -2000005 ## SSB2BR
_HERWIGID2PDGID[418] = 2000006 ## SST2
_HERWIGID2PDGID[424] = -2000006 ## SST2BR
_HERWIGID2PDGID[425] = 1000011 ## SSEL-
_HERWIGID2PDGID[431] = -1000011 ## SSEL+
_HERWIGID2PDGID[426] = 1000012 ## SSNUEL
_HERWIGID2PDGID[432] = -1000012 ## SSNUELBR
_HERWIGID2PDGID[427] = 1000013 ## SSMUL-
_HERWIGID2PDGID[433] = -1000013 ## SSMUL+
_HERWIGID2PDGID[428] = 1000014 ## SSNUMUL
_HERWIGID2PDGID[434] = -1000014 ## SSNUMLBR
_HERWIGID2PDGID[429] = 1000015 ## SSTAU1-
_HERWIGID2PDGID[435] = -1000015 ## SSTAU1+
_HERWIGID2PDGID[430] = 1000016 ## SSNUTL
_HERWIGID2PDGID[436] = -1000016 ## SSNUTLBR
_HERWIGID2PDGID[437] = 2000011 ## SSEL-
_HERWIGID2PDGID[443] = -2000011 ## SSEL+
_HERWIGID2PDGID[438] = 2000012 ## SSNUEL
_HERWIGID2PDGID[444] = -2000012 ## SSNUELBR
_HERWIGID2PDGID[439] = 2000013 ## SSMUL-
_HERWIGID2PDGID[445] = -2000013 ## SSMUL+
_HERWIGID2PDGID[440] = 2000014 ## SSNUMUL
_HERWIGID2PDGID[446] = -2000014 ## SSNUMLBR
_HERWIGID2PDGID[441] = 2000015 ## SSTAU1-
_HERWIGID2PDGID[447] = -2000015 ## SSTAU1+
_HERWIGID2PDGID[442] = 2000016 ## SSNUTL
_HERWIGID2PDGID[448] = -2000016 ## SSNUTLBR
_HERWIGID2PDGID[449] = 1000021 ## GLUINO
_HERWIGID2PDGID[450] = 1000022 ## NTLINO1
_HERWIGID2PDGID[451] = 1000023 ## NTLINO2
_HERWIGID2PDGID[452] = 1000025 ## NTLINO3
_HERWIGID2PDGID[453] = 1000035 ## NTLINO4
_HERWIGID2PDGID[454] = 1000024 ## CHGINO1+
_HERWIGID2PDGID[456] = -1000024 ## CHGINO1-
_HERWIGID2PDGID[455] = 1000037 ## CHGINO2+
_HERWIGID2PDGID[457] = -1000037 ## CHGINO2-
_HERWIGID2PDGID[458] = 1000039 ## GRAVTINO
[docs]def herwigid2pdgid(hwid):
"""
Convert a particle ID code in the HERWIG internal IDHW format (as used by
ISAWIG) into its equivalent in the standard PDG ID code definition.
"""
return _HERWIGID2PDGID.get(hwid, hwid)
## PDG MC ID codes mapped to HERWIG IDHW codes, based on
## http://www.hep.phy.cam.ac.uk/~richardn/HERWIG/ISAWIG/susycodes.html
## + the IDPDG array and section 4.13 of the HERWIG manual.
_PDGID2HERWIGID = {}
_PDGID2HERWIGID[ -1] = 7
_PDGID2HERWIGID[ -2] = 8
_PDGID2HERWIGID[ -3] = 9
_PDGID2HERWIGID[ -4] = 10
_PDGID2HERWIGID[ -5] = 11
_PDGID2HERWIGID[ -6] = 12
_PDGID2HERWIGID[ 21] = 13
_PDGID2HERWIGID[ 22] = 59
_PDGID2HERWIGID[ 11] = 121
_PDGID2HERWIGID[ 12] = 122
_PDGID2HERWIGID[ 13] = 123
_PDGID2HERWIGID[ 14] = 124
_PDGID2HERWIGID[ 15] = 125
_PDGID2HERWIGID[ 16] = 126
_PDGID2HERWIGID[ -11] = 127
_PDGID2HERWIGID[ -12] = 128
_PDGID2HERWIGID[ -13] = 129
_PDGID2HERWIGID[ -14] = 130
_PDGID2HERWIGID[ -15] = 131
_PDGID2HERWIGID[ -16] = 132
_PDGID2HERWIGID[ 24] = 198 ## W+
_PDGID2HERWIGID[ -24] = 199 ## W-
_PDGID2HERWIGID[ 23] = 200 ## Z
_PDGID2HERWIGID[ 25] = 203 ## HIGGSL0 (added for PDG standard -> HERWIG IDHW) # TODO: should be 201?
_PDGID2HERWIGID[ 26] = 203 ## HIGGSL0
_PDGID2HERWIGID[ 35] = 204 ## HIGGSH0
_PDGID2HERWIGID[ 36] = 205 ## HIGGSA0
_PDGID2HERWIGID[ 37] = 206 ## HIGGS+
_PDGID2HERWIGID[ -37] = 207 ## HIGGS-
_PDGID2HERWIGID[ 1000001] = 401 ## SSDLBR
_PDGID2HERWIGID[-1000001] = 407 ## SSDLBR
_PDGID2HERWIGID[ 1000002] = 402 ## SSULBR
_PDGID2HERWIGID[-1000002] = 408 ## SSUL
_PDGID2HERWIGID[ 1000003] = 403 ## SSSLBR
_PDGID2HERWIGID[-1000003] = 409 ## SSSL
_PDGID2HERWIGID[ 1000004] = 404 ## SSCLBR
_PDGID2HERWIGID[-1000004] = 410 ## SSCL
_PDGID2HERWIGID[ 1000005] = 405 ## SSB1BR
_PDGID2HERWIGID[-1000005] = 411 ## SSB1
_PDGID2HERWIGID[ 1000006] = 406 ## SST1BR
_PDGID2HERWIGID[-1000006] = 412 ## SST1
_PDGID2HERWIGID[ 2000001] = 413 ## SSDR
_PDGID2HERWIGID[-2000001] = 419 ## SSDRBR
_PDGID2HERWIGID[ 2000002] = 414 ## SSUR
_PDGID2HERWIGID[-2000002] = 420 ## SSURBR
_PDGID2HERWIGID[ 2000003] = 415 ## SSSR
_PDGID2HERWIGID[-2000003] = 421 ## SSSRBR
_PDGID2HERWIGID[ 2000004] = 416 ## SSCR
_PDGID2HERWIGID[-2000004] = 422 ## SSCRBR
_PDGID2HERWIGID[ 2000005] = 417 ## SSB2
_PDGID2HERWIGID[-2000005] = 423 ## SSB2BR
_PDGID2HERWIGID[ 2000006] = 418 ## SST2
_PDGID2HERWIGID[-2000006] = 424 ## SST2BR
_PDGID2HERWIGID[ 1000011] = 425 ## SSEL-
_PDGID2HERWIGID[-1000011] = 431 ## SSEL+
_PDGID2HERWIGID[ 1000012] = 426 ## SSNUEL
_PDGID2HERWIGID[-1000012] = 432 ## SSNUELBR
_PDGID2HERWIGID[ 1000013] = 427 ## SSMUL-
_PDGID2HERWIGID[-1000013] = 433 ## SSMUL+
_PDGID2HERWIGID[ 1000014] = 428 ## SSNUMUL
_PDGID2HERWIGID[-1000014] = 434 ## SSNUMLBR
_PDGID2HERWIGID[ 1000015] = 429 ## SSTAU1-
_PDGID2HERWIGID[-1000015] = 435 ## SSTAU1+
_PDGID2HERWIGID[ 1000016] = 430 ## SSNUTL
_PDGID2HERWIGID[-1000016] = 436 ## SSNUTLBR
_PDGID2HERWIGID[ 2000011] = 437 ## SSEL-
_PDGID2HERWIGID[-2000011] = 443 ## SSEL+
_PDGID2HERWIGID[ 2000012] = 438 ## SSNUEL
_PDGID2HERWIGID[-2000012] = 444 ## SSNUELBR
_PDGID2HERWIGID[ 2000013] = 439 ## SSMUL-
_PDGID2HERWIGID[-2000013] = 445 ## SSMUL+
_PDGID2HERWIGID[ 2000014] = 440 ## SSNUMUL
_PDGID2HERWIGID[-2000014] = 446 ## SSNUMLBR
_PDGID2HERWIGID[ 2000015] = 441 ## SSTAU1-
_PDGID2HERWIGID[-2000015] = 447 ## SSTAU1+
_PDGID2HERWIGID[ 2000016] = 442 ## SSNUTL
_PDGID2HERWIGID[-2000016] = 448 ## SSNUTLBR
_PDGID2HERWIGID[ 1000021] = 449 ## GLUINO
_PDGID2HERWIGID[ 1000022] = 450 ## NTLINO1
_PDGID2HERWIGID[ 1000023] = 451 ## NTLINO2
_PDGID2HERWIGID[ 1000025] = 452 ## NTLINO3
_PDGID2HERWIGID[ 1000035] = 453 ## NTLINO4
_PDGID2HERWIGID[ 1000024] = 454 ## CHGINO1+
_PDGID2HERWIGID[-1000024] = 456 ## CHGINO1-
_PDGID2HERWIGID[ 1000037] = 455 ## CHGINO2+
_PDGID2HERWIGID[-1000037] = 457 ## CHGINO2-
_PDGID2HERWIGID[ 1000039] = 458 ## GRAVTINO
[docs]def pdgid2herwigid(pdgid):
"""
Convert a particle ID code in the standard PDG ID code definition into
its equivalent in the HERWIG internal IDHW format (as used by ISAWIG).
"""
return _PDGID2HERWIGID.get(pdgid, pdgid)
###############################################################################
## ISAWIG format reading/writing
[docs]def readISAWIG(isastr, ignorenobr=False):
"""
Read a spectrum definition from a string in the ISAWIG format, returning
dictionaries of blocks and decays. While this is not an SLHA format, it is
informally supported as a useful mechanism for converting ISAWIG spectra to
SLHA.
ISAWIG parsing based on the HERWIG SUSY specification format, from
http://www.hep.phy.cam.ac.uk/~richardn/HERWIG/ISAWIG/file.html
If the ignorenobr parameter is True, do not store decay entries with a
branching ratio of zero.
"""
blocks = _dict("Blocks")
decays = _dict("Decays")
LINES = isastr.splitlines()
def getnextvalidline():
"""
Returns the next line which is not empty nor a comment.
"""
while LINES:
s = LINES.pop(0).strip()
# print "*", s, "*"
## Return None if EOF reached
if len(s) == 0:
continue
## Strip comments
if "#" in s:
s = s[:s.index("#")].strip()
## Return if non-empty
if len(s) > 0:
return s
def getnextvalidlineitems():
"""
Returns the map of next line which is not empty nor a comment.
"""
return map(_autotype, getnextvalidline().split())
## Populate MASS block and create decaying particle objects
masses = Block("MASS")
numentries = int(getnextvalidline())
for i in xrange(numentries):
hwid, mass, lifetime = getnextvalidlineitems()
width = 1.0/(lifetime * 1.51926778e24) ## width in GeV == hbar/lifetime in seconds
pdgid = herwigid2pdgid(hwid)
masses[pdgid] = mass
decays[pdgid] = Particle(pdgid, width, mass)
#print pdgid, mass, width
blocks["MASS"] = masses
## Populate decays
for n in xrange(numentries):
numdecays = int(getnextvalidline())
for d in xrange(numdecays):
#print n, numentries-1, d, numdecays-1
decayitems = getnextvalidlineitems()
hwid = decayitems[0]
pdgid = herwigid2pdgid(hwid)
br = decayitems[1]
nme = decayitems[2]
daughter_hwids = decayitems[3:]
daughter_pdgids = []
for hw in daughter_hwids:
if hw != 0:
daughter_pdgids.append(herwigid2pdgid(hw))
if not decays.has_key(pdgid):
#print "Decay for unlisted particle %d, %d" % (hwid, pdgid)
decays[pdgid] = Particle(pdgid)
decays[pdgid].add_decay(br, len(daughter_pdgids), daughter_pdgids)
## Now the SUSY parameters
TANB, ALPHAH = getnextvalidlineitems()
blocks["MINPAR"] = Block("MINPAR")
blocks["MINPAR"][3] = TANB
blocks["ALPHA"] = Block("ALPHA")
blocks["ALPHA"].set_value(ALPHAH)
#
## Neutralino mixing matrix
blocks["NMIX"] = Block("NMIX")
for i in xrange(1, 5):
nmix_i = getnextvalidlineitems()
for j, v in enumerate(nmix_i):
blocks["NMIX"][i, j+1] = v
#
## Chargino mixing matrices V and U
blocks["VMIX"] = Block("VMIX")
vmix = getnextvalidlineitems()
blocks["VMIX"][1, 1] = vmix[0]
blocks["VMIX"][1, 2] = vmix[1]
blocks["VMIX"][2, 1] = vmix[2]
blocks["VMIX"][2, 2] = vmix[3]
blocks["UMIX"] = Block("UMIX")
umix = getnextvalidlineitems()
blocks["UMIX"][1, 1] = umix[0]
blocks["UMIX"][1, 2] = umix[1]
blocks["UMIX"][2, 1] = umix[2]
blocks["UMIX"][2, 2] = umix[3]
#
THETAT, THETAB, THETAL = getnextvalidlineitems()
import math
blocks["STOPMIX"] = Block("STOPMIX")
blocks["STOPMIX"][1, 1] = math.cos(THETAT)
blocks["STOPMIX"][1, 2] = -math.sin(THETAT)
blocks["STOPMIX"][2, 1] = math.sin(THETAT)
blocks["STOPMIX"][2, 2] = math.cos(THETAT)
blocks["SBOTMIX"] = Block("SBOTMIX")
blocks["SBOTMIX"][1, 1] = math.cos(THETAB)
blocks["SBOTMIX"][1, 2] = -math.sin(THETAB)
blocks["SBOTMIX"][2, 1] = math.sin(THETAB)
blocks["SBOTMIX"][2, 2] = math.cos(THETAB)
blocks["STAUMIX"] = Block("STAUMIX")
blocks["STAUMIX"][1, 1] = math.cos(THETAL)
blocks["STAUMIX"][1, 2] = -math.sin(THETAL)
blocks["STAUMIX"][2, 1] = math.sin(THETAL)
blocks["STAUMIX"][2, 2] = math.cos(THETAL)
#
ATSS, ABSS, ALSS = getnextvalidlineitems()
blocks["AU"] = Block("AU")
blocks["AU"][3, 3] = ATSS
blocks["AD"] = Block("AD")
blocks["AD"][3, 3] = ABSS
blocks["AE"] = Block("AE")
blocks["AE"][3, 3] = ALSS
#
MUSS = getnextvalidlineitems()[0]
blocks["MINPAR"][4] = MUSS
#
# TODO: Parse RPV boolean and couplings into SLHA2 blocks
return Doc(blocks, decays)
[docs]def writeISAWIG(doc, ignorenobr=False, precision=8):
"""
Return a SUSY spectrum definition in the format produced by ISAWIG for inut to HERWIG
as a string, from the supplied SLHA blocks and decays dicts.
ISAWIG parsing based on the HERWIG SUSY specification format, from
http://www.hep.phy.cam.ac.uk/~richardn/HERWIG/ISAWIG/file.html
If the ignorenobr parameter is True, do not write decay entries with a
branching ratio of zero.
"""
blocks = doc.blocks
decays = doc.decays
masses = blocks["MASS"]
## Init output string
out = ""
## First write out masses section:
## Number of SUSY + top particles
## IDHW, RMASS(IDHW), RLTIM(IDHW)
## repeated for each particle
## IDHW is the HERWIG identity code.
## RMASS and RTLIM are the mass in GeV, and lifetime in seconds respectively.
massout = ""
for pid in masses.keys():
lifetime = -1
try:
width = decays[pid].totalwidth
if width and width > 0:
lifetime = 1.0/(width * 1.51926778e24) ## lifetime in seconds == hbar/width in GeV
except:
pass
massout += ("%d " % pdgid2herwigid(pid)) + _autostr(masses[pid], precision) + " " + _autostr(lifetime, precision) + "\n"
out += "%d\n" % massout.count("\n")
out += massout
assert(len(masses) == len(decays))
## Next each particles decay modes together with their branching ratios and matrix element codes
## Number of decay modes for a given particle (IDK)
## IDK(*), BRFRAC(*), NME(*) & IDKPRD(1-5,*)
## repeated for each mode.
## Repeated for each particle.
## IDK is the HERWIG code for the decaying particle, BRFRAC is the branching ratio of
## the decay mode. NME is a code for the matrix element to be used, either from the
## SUSY elements or the main HERWIG MEs. IDKPRD are the HERWIG identity codes of the decay products.
for i, pid in enumerate(decays.keys()):
# if not decays.has_key(pid):
# continue
hwid = pdgid2herwigid(pid)
decayout = ""
#decayout += "@@@@ %d %d %d\n" % (i, pid, hwid)
for i_d, d in enumerate(decays[pid].decays):
## Skip decay if it has no branching ratio
if ignorenobr and d.br == 0:
continue
## Identify decay matrix element to use
## From std HW docs, or from this pair:
## Two new matrix element codes have been added for these new decays:
## NME = 200 3 body top quark via charged Higgs
## 300 3 body R-parity violating gaugino and gluino decays
nme = 0
# TODO: Get correct condition for using ME 100... this guessed from some ISAWIG output
if abs(pid) in (6, 12):
nme = 100
## Extra SUSY MEs
if len(d.ids) == 3:
# TODO: How to determine the conditions for using 200 and 300 MEs? Enumeration of affected decays?
pass
decayout += ("%d " % hwid) + _autostr(d.br, precision) + (" %d " % nme)
def is_quark(pid):
"""
Checks if the input pid belongs to the quark pids
"""
return (abs(pid) in range(1, 7))
def is_lepton(pid):
"""
Checks if the input pid belongs to the lepton pids
"""
return (abs(pid) in range(11, 17))
def is_squark(pid):
"""
Checks if the input pid belongs to the squark pids
"""
if abs(pid) in range(1000001, 1000007):
return True
if abs(pid) in range(2000001, 2000007):
return True
return False
def is_slepton(pid):
"""
Checks if the input pid belongs to the slepton pids
"""
if abs(pid) in range(1000011, 1000017):
return True
if abs(pid) in range(2000011, 2000016, 2):
return True
return False
def is_gaugino(pid):
"""
Checks if the input pid belongs to the gaugino pids
"""
if abs(pid) in range(1000022, 1000026):
return True
if abs(pid) in (1000035, 1000037):
return True
return False
def is_susy(pid):
"""
Checks if the input pid belongs to the squark, slepton,
gaugino or gluino pids
"""
return (is_squark(pid) or is_slepton(pid) or is_gaugino(pid) or pid == 1000021)
absids = map(abs, d.ids)
## Order decay products as required by HERWIG
## Top
if abs(pid) == 6:
def cmp_bottomlast(a, b):
"""Comparison function which always puts b/bbar last"""
if abs(a) == 5:
return True
if abs(b) == 5:
return False
return cmp(a, b)
if len(absids) == 2:
## 2 body mode, to Higgs: Higgs; Bottom
if (25 in absids or 26 in absids) and 5 in absids:
d.ids = sorted(d.ids, cmp=cmp_bottomlast)
elif len(absids) == 3:
## 3 body mode, via charged Higgs/W: quarks or leptons from W/Higgs; Bottom
if 37 in absids or 23 in absids:
d.ids = sorted(d.ids, cmp=cmp_bottomlast)
## Gluino
elif abs(pid) == 1000021:
if len(absids) == 2:
## 2 body mode
## without gluon: any order
## with gluon: gluon; colour neutral
if 21 in absids:
def cmp_gluonfirst(a, b):
"""Comparison function which always puts gluon first"""
if a == 21:
return False
if b == 21:
return True
return cmp(a, b)
d.ids = sorted(d.ids, cmp=cmp_gluonfirst)
elif len(absids) == 3:
## 3-body modes, R-parity conserved: colour neutral; q or qbar
def cmp_quarkslast(a, b):
"""Comparison function which always puts quarks last"""
if is_quark(a):
return True
if is_quark(b):
return False
return cmp(a, b)
d.ids = sorted(d.ids, cmp=cmp_quarkslast)
## Squark/Slepton
elif is_squark(pid) or is_slepton(pid):
def cmp_susy_quark_lepton(a, b):
"""Compares if a and b is susy/quark/lepton."""
if is_susy(a):
return False
if is_susy(b):
return True
if is_quark(a):
return False
if is_quark(b):
return True
return cmp(a, b)
## 2 body modes: Gaugino/Gluino with Quark/Lepton Gaugino quark
## Gluino lepton
## 3 body modes: Weak sparticle particles from W decay
## Squark
## 2 body modes: Lepton Number Violated quark lepton
## Baryon number violated quark quark
## Slepton
## 2 body modes: Lepton Number Violated q or qbar
d.ids = sorted(d.ids, cmp=cmp_bottomlast)
## Higgs
elif pid in (25, 26):
# TODO: Includes SUSY Higgses?
## Higgs
## 2 body modes: (s)quark-(s)qbar (s)q or (s)qbar
## (s)lepton-(s)lepton (s)l or (s)lbar
## 3 body modes: colour neutral q or qbar
if len(absids) == 3:
def cmp_quarkslast(a, b):
"""Comparison function which always puts quarks last"""
if is_quark(a):
return True
if is_quark(b):
return False
return cmp(a, b)
d.ids = sorted(d.ids, cmp=cmp_quarkslast)
elif is_gaugino(pid):
# TODO: Is there actually anything to do here?
## Gaugino
## 2 body modes: Squark-quark q or sq
## Slepton-lepton l or sl
##
## 3 body modes: R-parity conserved colour neutral q or qbar
## l or lbar
if len(absids) == 3:
def cmp_quarkslast(a, b):
"""Comparison function which always puts quarks last"""
if is_quark(a):
return True
if is_quark(b):
return False
return cmp(a, b)
d.ids = sorted(d.ids, cmp=cmp_quarkslast)
# TODO: Gaugino/Gluino
## 3 body modes: R-parity violating: Particles in the same order as the R-parity violating superpotential
## Pad out IDs list with zeros
ids = [0,0,0,0,0]
for i, pid in enumerate(d.ids):
ids[i] = pid
ids = map(str, ids)
decayout += " ".join(ids) + "\n"
decayout = "%d\n" % decayout.count("\n") + decayout
out += decayout
## Now the SUSY parameters
## TANB, ALPHAH:
out += _autostr(blocks["MINPAR"][3], precision) + " " + _autostr(blocks["ALPHA"].value(), precision) + "\n"
## Neutralino mixing matrix
nmix = blocks["NMIX"]
for i in xrange(1, 5):
out += _autostr(nmix[i,1], precision) + " " + \
_autostr(nmix[i,2], precision) + " " + \
_autostr(nmix[i,3], precision) + " " + \
_autostr(nmix[i,4], precision) + "\n"
## Chargino mixing matrices V and U
vmix = blocks["VMIX"]
out += _autostr(vmix[1,1], precision) + " " + \
_autostr(vmix[1,2], precision) + " " + \
_autostr(vmix[2,1], precision) + " " + \
_autostr(vmix[2,2], precision) + "\n"
umix = blocks["UMIX"]
out += _autostr(umix[1,1], precision) + " " + \
_autostr(umix[1,2], precision) + " " + \
_autostr(umix[2,1], precision) + " " + \
_autostr(umix[2,2], precision) + "\n"
## THETAT,THETAB,THETAL
import math
out += _autostr(math.acos(blocks["STOPMIX"][1,1]), precision) + " " + \
_autostr(math.acos(blocks["SBOTMIX"][1,1]), precision) + " " + \
_autostr(math.acos(blocks["STAUMIX"][1,1]), precision) + "\n"
## ATSS,ABSS,ALSS
out += _autostr(blocks["AU"][3,3], precision) + " " + \
_autostr(blocks["AD"][3,3], precision) + " " + \
_autostr(blocks["AE"][3,3], precision) + "\n"
## MUSS == sign(mu)
out += "%f\n" % blocks["MINPAR"][4]
## RPV SUSY
isRPV = False
out += "%d\n" % isRPV
# TODO: Write RPV couplings if RPV is True (lambda1,2,3; 27 params in each, sci format.
# TODO: Get the index orderings right
# if isRPV: ...
return out
###############################################################################
## File-level read/write functions
[docs]def read(spcfile, **kwargs):
"""
Read an SLHA or ISAWIG file (or stdin).
spcfile may either be a string filename or a file object.
If a s string, the assumed file format is based from the
filename; if a file it is assumed to be SLHA format.
Other keyword parameters are passed to readSLHA/readISAWIG.
"""
txt = _read(spcfile)
if type(spcfile) is str and spcfile.endswith(".isa"):
return readISAWIG(txt, **kwargs)
else:
return readSLHA(txt, **kwargs)
[docs]def readSLHAFile(spcfile, **kwargs):
"""
Read an SLHA file, returning dictionaries of blocks and decays.
Other keyword parameters are passed to readSLHA.
"""
return readSLHA(_read(spcfile), **kwargs)
[docs]def readISAWIGFile(isafile, **kwargs):
"""
Read a spectrum definition from a file in the ISAWIG format, returning
dictionaries of blocks and decays. While this is not an SLHA format, it is
informally supported as a useful mechanism for converting ISAWIG spectra to
SLHA.
isafile may either be a string filename or a file object.
Other keyword parameters are passed to readISAWIG.
"""
return readISAWIG(_read(isafile), **kwargs)
[docs]def write(spcfile, doc, **kwargs):
"""
Write to an SLHA or ISAWIG file (or stdout).
spcfile may either be a string filename or a file object.
If a s string, the assumed file format is based from the
filename; if a file it is assumed to be SLHA format.
Other keyword parameters are passed to writeSLHA/writeISAWIG.
"""
if type(spcfile) is str and spcfile.endswith(".isa"):
writeISAWIG(spcfile, doc, **kwargs)
else:
writeSLHA(spcfile, doc, **kwargs)
[docs]def writeSLHAFile(spcfile, doc, **kwargs):
"""
Write an SLHA file from the supplied blocks and decays dicts.
Other keyword parameters are passed to writeSLHA.
"""
_write(spcfile, writeSLHA(doc, **kwargs))
[docs]def writeISAWIGFile(isafile, doc, **kwargs):
"""
Write an ISAWIG file from the supplied blocks and decays dicts.
isafile may either be a string filename or a file object.
Other keyword parameters are passed to writeISAWIG.
"""
_write(isafile, writeISAWIG(doc, **kwargs))
###############################################################################
## Main function for module testing
if __name__ == "__main__":
import sys
for a in sys.argv[1:]:
doc = read(a)
print doc
print
for bname, b in sorted(doc.blocks.iteritems()):
print b
print
print doc.blocks.keys()
print doc.blocks["MASS"].get(25)
print
for p in sorted(doc.decays.values()):
print p
print
print writeSLHA(doc, ignorenobr=True)