"""tipsy
=====
Implements classes and functions for handling tipsy files. You rarely
need to access this module directly as it will be invoked
automatically via pynbody.load.
**Input**:
*filename*: file name string
**Optional Keywords**:
*paramfile*: string specifying the parameter file to load. If not
specified, the loader will look for a file `*.param` in the current and
parent directories.
"""
# for py2.5
from .. import array, util
from .. import family
from .. import units
from .. import config, config_parser
from .. import chunk
from . import nchilada
from . import SimSnap
import struct
import os
import numpy as np
import gzip
import sys
import warnings
import copy
import types
import math
import logging
logger = logging.getLogger('pynbody.snapshot.tipsy')
class TipsySnap(SimSnap):
_basic_loadable_keys = {family.dm: set(['phi', 'pos', 'eps', 'mass', 'vel']),
family.gas: set(['phi', 'temp', 'pos', 'metals', 'eps',
'mass', 'rho', 'vel']),
family.star: set(['phi', 'tform', 'pos', 'metals',
'eps', 'mass', 'vel']),
None: set(['phi', 'pos', 'eps', 'mass', 'vel'])}
def __init__(self, filename, **kwargs):
global config
super(TipsySnap, self).__init__()
only_header = kwargs.get('only_header', False)
if only_header:
warnings.warn(
"only_header kwarg is deprecated: all loading in TipsySnap is now lazy by default", RuntimeWarning)
must_have_paramfile = kwargs.get('must_have_paramfile', False)
take = kwargs.get('take', None)
verbose = kwargs.get('verbose', config['verbose'])
self.partial_load = take is not None
self._filename = util.cutgz(filename)
f = util.open_(filename, 'rb')
if not only_header:
logger.info("Loading %s", filename)
t, n, ndim, ng, nd, ns = struct.unpack("diiiii", f.read(28))
if (ndim > 3 or ndim < 1):
self._byteswap = True
f.seek(0)
t, n, ndim, ng, nd, ns = struct.unpack(">diiiii", f.read(28))
else:
self._byteswap = False
assert ndim == 3
self._header_t = t
f.read(4)
disk_family_slice = dict({family.gas: slice(0, ng),
family.dm: slice(ng, nd + ng),
family.star: slice(nd + ng, ng + nd + ns)})
self._load_control = chunk.LoadControl(disk_family_slice, 10240, take)
self._family_slice = self._load_control.mem_family_slice
self._num_particles = self._load_control.mem_num_particles
self._paramfilename = kwargs.get('paramfile', None)
self._decorate()
# describe the file structure as list of (num_parts, [list_of_properties])
# by default all fields are floats -- we look at the param file to determine
# whether we should expect some doubles
if self._paramfile.get('bDoublePos', 0):
ptype = 'd'
else:
ptype = 'f'
if self._paramfile.get('bDoubleVel', 0):
vtype = 'd'
else:
vtype = 'f'
self._g_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "rho", "temp", "eps", "metals", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f', 'f', 'f', 'f')})
self._d_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "eps", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f')})
self._s_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "metals", "tform", "eps", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f', 'f', 'f')})
if 'dKpcUnit' not in self._paramfile:
if must_have_paramfile:
raise RuntimeError("Could not find .param file for this run. Place it in the run's directory or parent directory.")
else:
warnings.warn(
"No readable param file in the run directory or parent directory: using defaults.", RuntimeWarning)
self._file_units_system = [
units.Unit(x) for x in ('G', '1 kpc', '1e10 Msol')]
time_unit = None
try:
time_unit = self.infer_original_units('yr')
except units.UnitsException:
pass
if time_unit is not None:
self.properties['time'] *= time_unit
del f
def _load_main_file(self):
logger.info("Loading data from main file %s", self._filename)
f = util.open_(self._filename, 'rb')
f.seek(32)
write = []
for w, ndim in ("pos", 3), ("vel", 3), ("mass", 1), ("eps", 1), ("phi", 1):
if w not in list(self.keys()):
self._create_array(w, ndim, zeros=False)
write.append(w)
for w in "rho", "temp":
if w not in list(self.gas.keys()):
self.gas._create_array(w, zeros=False)
write.append(w)
if ("metals" not in list(self.gas.keys())) and ("metals" not in list(self.star.keys())):
self.gas._create_array("metals", zeros=False)
self.star._create_array("metals", zeros=False)
write.append("metals")
if "tform" not in list(self.star.keys()):
self.star._create_array("tform", zeros=False)
write.append("tform")
if "temp" in write:
self.gas["temp"].units = "K"
for k in "pos", "vel", "mass", "eps", "phi":
if k in write:
self[k].set_default_units(quiet=True)
# only do this for cosmo runs
if "phi" in write and 'h' in self.properties:
self['phi'].units = self['phi'].units * units.a ** -3 # messy :-(
for k in "rho", "temp", "metals":
if k in write:
self.gas[k].set_default_units(quiet=True)
for k in "metals", "tform":
if k in write:
self.star[k].set_default_units(quiet=True)
if "pos" in write:
write += ['x', 'y', 'z']
if "vel" in write:
write += ['vx', 'vy', 'vz']
max_item_size = max(
[q.itemsize for q in (self._g_dtype, self._d_dtype, self._s_dtype)])
tbuf = bytearray(max_item_size * 10240)
for fam, dtype in ((family.gas, self._g_dtype), (family.dm, self._d_dtype), (family.star, self._s_dtype)):
self_fam = self[fam]
st_len = dtype.itemsize
for readlen, buf_index, mem_index in self._load_control.iterate([fam], [fam], multiskip=True):
# Read in the block
if mem_index is None:
f.seek(st_len * readlen, 1)
continue
buf = np.fromstring(f.read(st_len * readlen), dtype=dtype)
if self._byteswap:
buf = buf.byteswap()
if mem_index is not None:
# Copy into the correct arrays
for name in dtype.names:
if name in write:
self_fam[name][mem_index] = buf[name][buf_index]
def _update_loadable_keys(self):
def is_readable_array(x):
try:
f = util.open_(x, 'r')
return int(f.readline()) == len(self)
except ValueError:
# could be a binary file
f = util.open_(x, 'rb')
header = f.read(4)
if len(header)!=4:
return False
if self._byteswap:
buflen = struct.unpack(">i", header)[0]
else:
buflen = struct.unpack("i", header)[0]
ourlen_1 = (self._load_control.disk_num_particles)& 0xffffffff
ourlen_3 = (self._load_control.disk_num_particles*3)& 0xffffffff
if buflen == ourlen_1: # it's a vector
return True
elif buflen == ourlen_3: # it's an array
return True
else:
return False
except IOError:
return False
import glob
fs = list(map(util.cutgz, glob.glob(self._filename + ".*")))
res = [q[len(self._filename) + 1:] for q in list(filter(is_readable_array, fs))]
# Create an empty dictionary of sets to store the loadable
# arrays for each family
rdict = dict([(x, set()) for x in self.families()])
rdict.update(dict([a, copy.copy(b)]
for a, b in self._basic_loadable_keys.items() if a is not None))
# Now work out which families can load which arrays
# according to the stored metadata
for r in res:
fams = self._get_loadable_array_metadata(r)[1]
for x in fams or list(rdict.keys()):
rdict[x].add(r)
self._loadable_keys_registry = rdict
def loadable_keys(self, fam=None):
"""Produce and return a list of loadable arrays for this TIPSY file."""
if len(self._loadable_keys_registry) == 0:
self._update_loadable_keys()
if fam is not None:
# Return what is loadable for this family
return list(self._loadable_keys_registry[fam])
else:
# Return what is loadable to all families
return list(set.intersection(*list(self._loadable_keys_registry.values())))
def _update_snapshot(self, arrays, filename=None, fam_out=[family.gas, family.dm, family.star]):
"""
Write a TIPSY file, but only updating the requested information and leaving the
rest intact on disk.
"""
if self.partial_load:
raise RuntimeError("Writing back to partially loaded files not yet supported")
global config
# make arrays be a list
if isinstance(arrays, str):
arrays = [arrays]
# check if arrays includes a 3D array
if 'pos' in arrays:
arrays.remove('pos')
for arr in ['x', 'y', 'z']:
arrays.append(arr)
if 'vel' in arrays:
arrays.remove('vel')
for arr in ['vx', 'vy', 'vz']:
arrays.append(arr)
with self.lazy_off:
fin = util.open_(self.filename, "rb")
fout = util.open_(self.filename + ".tmp", "wb")
if self._byteswap:
t, n, ndim, ng, nd, ns = struct.unpack(">diiiii", fin.read(28))
fout.write(struct.pack(">diiiiii", t, n, ndim, ng, nd, ns, 0))
else:
t, n, ndim, ng, nd, ns = struct.unpack("diiiii", fin.read(28))
fout.write(struct.pack("diiiiii", t, n, ndim, ng, nd, ns, 0))
fin.read(4)
if family.gas in fam_out:
assert(ng == len(self[family.gas]))
if family.dm in fam_out:
assert(nd == len(self[family.dm]))
if family.star in fam_out:
assert(ns == len(self[family.star]))
max_block_size = 1024 ** 2 # particles
# describe the file structure as list of (num_parts,
# [list_of_properties])
file_structure = ((ng, family.gas, ["mass", "x", "y", "z", "vx", "vy", "vz", "rho", "temp", "eps", "metals", "phi"]),
(nd, family.dm, [
"mass", "x", "y", "z", "vx", "vy", "vz", "eps", "phi"]),
(ns, family.star, ["mass", "x", "y", "z", "vx", "vy", "vz", "metals", "tform", "eps", "phi"]))
# do the read/write -- at each block, replace the relevant array
for n_left, fam, st in file_structure:
n_done = 0
self_fam = self[fam]
while n_left > 0:
n_block = min(n_left, max_block_size)
# Read in the block
if(self._byteswap):
g = np.fromstring(
fin.read(len(st) * n_block * 4), 'f').byteswap().reshape((n_block, len(st)))
else:
g = np.fromstring(
fin.read(len(st) * n_block * 4), 'f').reshape((n_block, len(st)))
if fam in fam_out:
self_sub = self[fam][n_done:n_done + n_block]
# write over the relevant data
with self_sub.immediate_mode:
for i, name in enumerate(st):
if name in arrays:
ar = self_sub[name]
try:
if ar.units != 1 and ar.units != units.NoUnit():
g[:, i] = ar.in_original_units().view(
np.ndarray)
else:
g[:, i] = ar.view(np.ndarray)
except KeyError:
pass
# Write out the block
if self._byteswap:
g.byteswap().tofile(fout)
else:
g.tofile(fout)
# Increment total ptcls read in, decrement ptcls left of
# this type
n_left -= n_block
n_done += n_block
fin.close()
fout.close()
os.system("mv " + self.filename + ".tmp " + self.filename)
@staticmethod
def _write(self, filename=None, double_pos=None, double_vel=None, binary_aux_arrays=None, cosmological=True):
"""
Write a TIPSY (standard) formatted file.
Additionally, you can specify whether you want position and/or
velocity arrays written out in double precision. If you are
writing out a snapshot that was originally in tipsy format and
the bDoublePos/bDoubleVel flags are set in the parameter file,
then the write routine will follow those choices. If you are
writing a snapshot other than a tipsy snapshot, then you have
to specify these by hand.
**Optional Keywords**
*filename* (None): name of the file to be written out. If
None, the original file is overwritten.
*double_pos* (False): set to 'True' if you want to write out positions as doubles
*double_vel* (False): set to 'True' if you want to write out velocities as doubles
*binary_aux_arrays* (None): set to 'True' to write auxiliary
arrays in binary format; if left 'None', the preference is
taken from the param file
*cosmological* (True): if True (default), the header will store the self.properties['a'];
otherwise it will store self.properties['time']
"""
global config
if filename is None:
filename = self._filename
logger.info("Writing main file %s", filename)
f = util.open_(filename, 'wb')
t = 0.0
try:
if cosmological:
t = self.properties['a']
else:
t = self.properties['time']
except KeyError:
warnings.warn(
"Time is unknown: writing zero in header", RuntimeWarning)
n = len(self)
ndim = 3
ng = len(self.gas)
nd = len(self.dark)
ns = len(self.star)
byteswap = getattr(self, "_byteswap", sys.byteorder == "little")
if byteswap:
f.write(struct.pack(">diiiiii", t, n, ndim, ng, nd, ns, 0))
else:
f.write(struct.pack("diiiiii", t, n, ndim, ng, nd, ns, 0))
# needs to be done in blocks like reading
# describe the file structure as list of (num_parts,
# [list_of_properties])
if type(self) is not TipsySnap:
if double_pos is None:
double_pos = False
if double_vel is None:
double_vel = False
ptype = 'd' if double_pos else 'f'
vtype = 'd' if double_vel else 'f'
else:
dpos_param = self._paramfile.get('bDoublePos', False)
dvel_param = self._paramfile.get('bDoubleVel', False)
if double_pos:
ptype = 'd'
elif not double_pos:
ptype = 'f'
else:
ptype = 'd' if dpos_param else 'f'
if double_vel:
vtype = 'd'
elif not double_vel:
vtype = 'f'
else:
vtype = 'd' if dvel_param else 'f'
g_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "rho", "temp", "eps", "metals", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f', 'f', 'f', 'f')})
d_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "eps", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f')})
s_dtype = np.dtype({'names': ("mass", "x", "y", "z", "vx", "vy", "vz", "metals", "tform", "eps", "phi"),
'formats': ('f', ptype, ptype, ptype, vtype, vtype, vtype, 'f', 'f', 'f', 'f')})
file_structure = ((ng, family.gas, g_dtype),
(nd, family.dm, d_dtype),
(ns, family.star, s_dtype))
max_block_size = 1024 ** 2 # particles
with self.lazy_derive_off:
for n_left, fam, dtype in file_structure:
n_done = 0
self_type = self[fam]
while n_left > 0:
n_block = min(n_left, max_block_size)
#g = np.zeros((n_block,len(st)),dtype=np.float32)
g = np.empty(n_block, dtype=dtype)
self_type_block = self_type[n_done:n_done + n_block]
with self_type_block.immediate_mode:
# Copy from the correct arrays
for i, name in enumerate(dtype.names):
try:
g[name] = self_type_block[name]
except KeyError:
pass
# Write out the block
if byteswap:
g.byteswap().tofile(f)
else:
g.tofile(f)
# Increment total ptcls written, decrement ptcls left of
# this type
n_left -= n_block
n_done += n_block
f.close()
logger.info("Writing auxiliary arrays")
with self.lazy_off: # prevent any lazy reading or evaluation
for x in set(self.keys()).union(self.family_keys()):
if not self.is_derived_array(x) and x not in ["mass", "pos", "x", "y", "z", "vel", "vx", "vy", "vz", "rho", "temp",
"eps", "metals", "phi", "tform"]:
TipsySnap._write_array(
self, x, filename=filename + "." + x, binary=binary_aux_arrays)
@staticmethod
def __get_write_dtype(stored_dtype):
if issubclass(stored_dtype.type, np.integer):
return np.int32
else:
return np.float32
@staticmethod
def __write_block(f, ar, binary, byteswap):
ar = np.asarray(ar, dtype=TipsySnap.__get_write_dtype(ar.dtype))
if binary:
if byteswap:
ar.byteswap().tofile(f)
else:
ar.tofile(f)
else:
if issubclass(ar.dtype.type, np.integer):
fmt = "%d"
else:
fmt = "%e"
np.savetxt(f, ar, fmt=fmt)
def _get_loadable_array_metadata(self, array_name):
"""Given an array name, returns the metadata consisting of
the tuple units, families.
Returns:
*units*: the units of this data on disk
*families*: a list of family objects for which data is on disk
for this array, or None if this cannot be determined"""
try:
f = open(self.filename + "." + array_name + ".pynbody-meta", 'r')
except IOError:
return self._default_units_for(array_name), None, None
res = {}
for l in f:
X = l.split(":")
if len(X) == 2:
res[X[0].strip()] = X[1].strip()
try:
u = units.Unit(res['units'])
except:
u = None
try:
fams = [family.get_family(x.strip())
for x in res['families'].split(" ")]
except:
fams = None
try:
dtype = np.dtype(res['dtype'])
except:
dtype = None
return u, fams, dtype
@staticmethod
def _write_array_metafile(self, filename, units, families, dtype):
f = open(filename + ".pynbody-meta", "w")
print("# This file automatically created by pynbody", file=f)
if not hasattr(units, "_no_unit"):
print("units:", units, file=f)
print("families:", end=' ', file=f)
for x in families:
print(x.name, end=' ', file=f)
print(file=f)
if dtype is not None:
print("dtype:", TipsySnap.__get_write_dtype(dtype), file=f)
f.close()
if isinstance(self, TipsySnap):
# update the loadable keys if this operation is likely to have
# changed them
self._update_loadable_keys()
@staticmethod
def _families_in_main_file(array_name, fam=None):
fam_for_default = [fX for fX, ars in TipsySnap._basic_loadable_keys.items(
) if array_name in ars and fX in fam]
return fam_for_default
def _update_array(self, array_name, fam=None,
filename=None, binary=None, byteswap=None):
assert fam is not None
if self.partial_load:
raise RuntimeError("Writing back to partially loaded files not yet supported")
fam_in_main = self._families_in_main_file(array_name, fam)
if len(fam_in_main) > 0:
self._update_snapshot(array_name, fam_out=fam_in_main)
fam = list(set(fam).difference(fam_in_main))
if len(fam) == 0:
return
# If we have disk units for this array, check we can convert into them
aux_u, aux_f, aux_type = self._get_loadable_array_metadata(array_name)
if aux_f is None:
aux_f = self.families()
if aux_u is not None:
for f in fam:
if array_name in self[f]:
# check convertible
try:
self[f][array_name].units.in_units(aux_u)
except units.UnitsException:
raise IOError(
"Units must match the existing auxiliary array on disk.")
try:
data = self.__read_array_from_disk(array_name, filename=filename)
except IOError:
# doesn't really exist, probably because the other data on disk was
# in the main snapshot
self._write_array(self, array_name, fam, filename=filename, binary=binary,
byteswap=byteswap)
return
for f in fam:
if aux_u is not None:
data[self._get_family_slice(f)] = self[f][
array_name].in_units(aux_u)
else:
data[self._get_family_slice(f)] = self[f][array_name]
fam = list(set(aux_f).union(fam))
self._write_array(self, array_name, fam=fam, contents=data, binary=binary,
byteswap=byteswap, filename=filename)
@staticmethod
def _write_array(self, array_name, fam=None, contents=None,
filename=None, binary=None, byteswap=None):
"""Write the array to file."""
fam_in_main = TipsySnap._families_in_main_file(array_name, fam)
if len(fam_in_main) > 0:
if isinstance(self, TipsySnap):
self._update_snapshot(array_name, fam_out=fam_in_main)
fam = list(set(fam).difference(fam_in_main))
if len(fam) == 0:
return
else:
raise RuntimeError("Cannot call static _write_array to write into main tipsy file.")
units_out = None
dtype = None
if binary is None:
binary = getattr(self.ancestor, "_tipsy_arrays_binary", False)
if binary and (byteswap is None):
byteswap = getattr(self.ancestor, "_byteswap", False)
with self.lazy_off: # prevent any lazy reading or evaluation
if filename is None:
if self._filename[-3:] == '.gz':
filename = self._filename[:-3] + "." + array_name + ".gz"
else:
filename = self._filename + "." + array_name
if binary:
fhand = util.open_(filename, 'wb')
if byteswap:
fhand.write(struct.pack(">i", len(self)))
else:
fhand.write(struct.pack("i", len(self)))
else:
fhand = util.open_(filename, 'wb')
fhand.write((str(len(self)) + '\n').encode('utf-8'))
if contents is None:
if array_name in self.family_keys():
for f in [family.gas, family.dm, family.star]:
try:
dtype = self[f][array_name].dtype
ar = self[f][array_name]
units_out = ar.units
except KeyError:
ar = np.zeros(len(self[f]), dtype=int)
TipsySnap.__write_block(fhand, ar, binary, byteswap)
else:
ar = self[array_name]
dtype = self[array_name].dtype
units_out = ar.units
TipsySnap.__write_block(fhand, ar, binary, byteswap)
else:
TipsySnap.__write_block(fhand, contents, binary, byteswap)
units_out = contents.units
fhand.close()
if fam is None:
fam = [family.gas, family.dm, family.star]
TipsySnap._write_array_metafile(self, filename, units_out, fam, dtype)
def _load_array(self, array_name, fam=None, filename=None,
packed_vector=None):
if array_name in self._basic_loadable_keys[fam]:
self._load_main_file()
return
fams = self._get_loadable_array_metadata(
array_name)[1] or self.families()
if (fam is None and fams is not None and len(fams) != len(self.families())) or \
(fam is not None and fam not in fams):
# Top line says 'you requested all families but at least one isn't available'
# Bottom line says 'you requested one family, but that one's not
# available'
raise IOError("This array is marked as available only for families %s" % fams)
data = self.__read_array_from_disk(array_name, fam=fam,
filename=filename,
packed_vector=packed_vector)
if fam is None:
self[array_name] = data
else:
self[fam][array_name] = data
def __read_array_from_disk(self, array_name, fam=None, filename=None,
packed_vector=None):
"""Read a TIPSY-ASCII or TIPSY-BINARY auxiliary file with the
specified name. If fam is not None, read only the particles of
the specified family."""
if filename is None and array_name in ['massform', 'rhoform', 'tempform', 'phiform', 'nsmooth',
'xform', 'yform', 'zform', 'vxform', 'vyform', 'vzform',
'posform', 'velform','h2form']:
try:
self.read_starlog()
if fam is not None:
return self[fam][array_name]
else:
return self[array_name]
except IOError:
pass
import sys
import os
# N.B. this code is a bit inefficient for loading
# family-specific arrays, because it reads in the whole array
# then slices it. But of course the memory is only wasted
# while still inside this routine, so it's only really the
# wasted time that's a problem.
# determine whether the array exists in a file
if filename is None:
if self._filename[-3:] == '.gz':
filename = self._filename[:-3] + "." + array_name
else:
filename = self._filename + "." + array_name
f = util.open_(filename, 'r')
logger.info("Attempting to load auxiliary array %s", filename)
# if we get here, we've got the file - try loading it
units, _, dtype = self._get_loadable_array_metadata(array_name)
if dtype is None:
dtype = self._get_preferred_dtype(array_name)
try:
l = int(f.readline())
binary = False
if l != self._load_control.disk_num_particles:
raise IOError("Incorrect file format")
if not dtype:
# Inspect the first line to see whether it's float or int
l = "0\n"
while l == "0\n":
l = f.readline()
if "." in l or "e" in l or l[:-1] == "inf":
dtype = float
else:
dtype = int
# Restart at head of file
f.seek(0)
f.readline()
loadblock = lambda count: np.fromfile(
f, dtype=dtype, sep="\n", count=count)
# data = np.fromfile(f, dtype=tp, sep="\n")
except ValueError:
# this is probably a binary file
binary = True
f = util.open_(filename, 'rb')
# Read header and check endianness
if self._byteswap:
l = struct.unpack(">i", f.read(4))[0]
else:
l = struct.unpack("i", f.read(4))[0]
if l != self._load_control.disk_num_particles:
raise IOError("Incorrect file format")
if dtype is None:
# Set data format to be read (float or int) based on config
int_arrays = list(map(
str.strip, config_parser.get('tipsy', 'binary-int-arrays').split(",")))
if array_name in int_arrays:
dtype = 'i'
else:
dtype = 'f'
# Read longest data array possible.
# Assume byteswap since most will be.
if self._byteswap:
loadblock = lambda count: np.fromstring(
f.read(count * 4), dtype=dtype, count=count).byteswap()
# data = np.fromstring(f.read(3*len(self)*4),dtype).byteswap()
else:
loadblock = lambda count: np.fromstring(
f.read(count * 4), dtype=dtype, count=count)
# data = np.fromstring(f.read(3*len(self)*4),dtype)
ndim = 1
self.ancestor._tipsy_arrays_binary = binary
all_fam = [family.dm, family.gas, family.star]
if fam is None:
fam = all_fam
r = np.empty(len(self), dtype=dtype).view(array.SimArray)
else:
r = np.empty(len(self[fam]), dtype=dtype).view(array.SimArray)
for readlen, buf_index, mem_index in self._load_control.iterate(all_fam, fam):
buf = loadblock(readlen)
if mem_index is not None:
r[mem_index] = buf[buf_index]
if units is not None:
r.units = units
return r
def read_starlog(self, fam=None):
"""Read a TIPSY-starlog file."""
import sys
import os
import glob
import pynbody.bridge
x = os.path.abspath(self._filename)
done = False
filename = None
x = os.path.dirname(x)
# Attempt the loading of information
l = glob.glob(os.path.join(x, "*.starlog"))
if (len(l)):
for filename in l:
sl = StarLog(filename)
else:
l = glob.glob(os.path.join(x, "../*.starlog"))
if (len(l) == 0):
raise IOError("Couldn't find starlog file")
for filename in l:
sl = StarLog(filename)
logger.info("Bridging starlog into SimSnap")
b = pynbody.bridge.OrderBridge(self, sl)
b(b(b(sl))).star['iorderGas'] = b(b(sl)).star['iorderGas']
b(b(b(sl))).star['massform'] = b(b(sl)).star['massform']
b(b(b(sl))).star['rhoform'] = b(b(sl)).star['rhoform']
b(b(b(sl))).star['tempform'] = b(b(sl)).star['tempform']
b(b(b(sl)))['posform'] = b(b(sl))['pos']
b(b(b(sl)))['velform'] = b(b(sl))['vel']
#b(sl).star['iorderGas'] = sl.star['iorderGas'][:len(self.star)]
#b(sl).star['massform'] = sl.star['massform'][:len(self.star)]
#b(sl).star['rhoform'] = sl.star['rhoform'][:len(self.star)]
#b(sl).star['tempform'] = sl.star['tempform'][:len(self.star)]
#b(sl)['posform'] = sl['pos'][:len(self.star), :]
#b(sl)['velform'] = sl['vel'][:len(self.star), :]
if 'h2form' in list(sl.star.keys()):
b(b(b(sl))).star['h2form'] = b(b(sl)).star['h2form']
else: print("No H2 data found in StarLog file")
for i, x in enumerate(['x', 'y', 'z']):
self._arrays[x + 'form'] = self['posform'][:, i]
for i, x in enumerate(['vx', 'vy', 'vz']):
self._arrays[x + 'form'] = self['velform'][:, i]
@staticmethod
def _can_load(f):
try:
check = TipsySnap(f, verbose=False)
del check
except:
return False
return True
# caculate the number fraction YH, YHe as a function of metalicity. Cosmic
# production rate of helium relative to metals (in mass)
# delta Y/delta Z = 2.1 and primordial He Yp = 0.236 (Jimenez et al. 2003,
# Science 299, 5612.
# piecewise linear
# Y = Yp + dY/dZ*ZMetal up to ZMetal = 0.1, then linear decrease to 0 at Z=1)
# SUM_Metal = sum(ni/nH *mi),it is a fixed number for cloudy abundance.
# Massfraction fmetal = Z*SUM_metal/(1 + 4*nHe/nH + Z*SUM_metal) (1)
# 4*nHe/nH = mHe/mH = fHe/fH
# also fH + fHe + fMetal = 1 (2)
# if fHe specified, combining the 2 eq above will solve for
# fH and fMetal
def _abundance_estimator(metal):
Y_He = ((0.236 + 2.1 * metal) / 4.0) * (metal <= 0.1)
Y_He += ((-0.446 * (metal - 0.1) / 0.9 + 0.446) / 4.0) * (metal > 0.1)
Y_H = 1.0 - Y_He * 4. - metal
return Y_H, Y_He
[docs]@TipsySnap.derived_quantity
def HII(sim):
"""Number of HII ions per proton mass"""
Y_H, Y_He = _abundance_estimator(sim["metals"])
try:
return Y_H - sim["HI"] - 2 * sim["H2"]
except KeyError:
return Y_H - sim["HI"]
[docs]@TipsySnap.derived_quantity
def HeIII(sim):
"""Number of HeIII ions per proton mass"""
Y_H, Y_He = _abundance_estimator(sim["metals"])
return Y_He - sim["HeII"] - sim["HeI"]
[docs]@TipsySnap.derived_quantity
def ne(sim):
"""Number of electrons per proton mass"""
return sim["HII"] + sim["HeII"] + 2 * sim["HeIII"]
[docs]@TipsySnap.derived_quantity
def hetot(self):
"""Helium mass fraction including correction based on metallicity"""
return 0.236 + (2.1 * self['metals'])
[docs]@TipsySnap.derived_quantity
def hydrogen(self):
"""Hydrogen mass fraction including correction based on metallicity"""
return 1.0 - self['metals'] - self['hetot']
#from .tipsy import TipsySnap
# Asplund et al (2009) ARA&A solar abundances (Table 1)
# m_frac = 10.0^([X/H] - 12)*M_X/M_H*0.74
# OR
# http://en.wikipedia.org/wiki/Abundance_of_the_chemical_elements
# puts stuff more straighforwardly cites Arnett (1996)
# A+G from http://www.t4.lanl.gov/opacity/grevand1.html
# Anders + Grev (1989) Asplund
XSOLFe = 0.125E-2 # 1.31e-3
# Looks very wrong ([O/Fe] ~ 0.2-0.3 higher than solar),
# probably because SN ejecta are calculated with
# Woosley + Weaver (1995) based on Anders + Grevesse (1989)
# XSOLO=0.59E-2 # 5.8e-2
XSOLO = 0.84E-2
XSOLH = 0.706 # 0.74
XSOLC = 3.03e-3 # 2.39e-3
XSOLN = 9.2e-4 # 7e-4
XSOLNe = 1.66e-3 # 1.26e-3
XSOLMg = 6.44e-4 # 7e-4
XSOLSi = 7e-4 # 6.7e-4
[docs]@TipsySnap.derived_quantity
def feh(self):
"""Iron abundance [Fe/H] derived from tipsy array FeMassFrac, with solar
values from Asplund et al 09"""
minfe = np.amin(self['FeMassFrac'][np.where(self['FeMassFrac'] > 0)])
self['FeMassFrac'][np.where(self['FeMassFrac'] == 0)] = minfe
return np.log10(self['FeMassFrac'] / self['hydrogen']) - np.log10(XSOLFe / XSOLH)
[docs]@TipsySnap.derived_quantity
def oxh(self):
"""Oxygen abundance [O/H] derived from tipsy array FeMassFrac, with solar
values from Asplund et al 09"""
minox = np.amin(self['OxMassFrac'][np.where(self['OxMassFrac'] > 0)])
self['OxMassFrac'][np.where(self['OxMassFrac'] == 0)] = minox
return np.log10(self['OxMassFrac'] / self['hydrogen']) - np.log10(XSOLO / XSOLH)
[docs]@TipsySnap.derived_quantity
def ofe(self):
"""Oxygen-to-iron ratio [O/Fe] derived from tipsy arrays OxMassFrac and FeMassFrac
with solar values from Asplund et al 09"""
minox = np.amin(self['OxMassFrac'][np.where(self['OxMassFrac'] > 0)])
self['OxMassFrac'][np.where(self['OxMassFrac'] == 0)] = minox
minfe = np.amin(self['FeMassFrac'][np.where(self['FeMassFrac'] > 0)])
self['FeMassFrac'][np.where(self['FeMassFrac'] == 0)] = minfe
return np.log10(self['OxMassFrac'] / self['FeMassFrac']) - np.log10(XSOLO / XSOLFe)
[docs]@TipsySnap.derived_quantity
def mgfe(sim):
"""Magnesium-to-iron ratio [Mg/Fe] derived from tipsy arrays MgMassFrac and FeMassFrac
with solar values from Asplund et al 09"""
minmg = np.amin(sim['MgMassFrac'][np.where(sim['MgMassFrac'] > 0)])
sim['MgMassFrac'][np.where(sim['MgMassFrac'] == 0)] = minmg
minfe = np.amin(sim['FeMassFrac'][np.where(sim['FeMassFrac'] > 0)])
sim['FeMassFrac'][np.where(sim['FeMassFrac'] == 0)] = minfe
return np.log10(sim['MgMassFrac'] / sim['FeMassFrac']) - np.log10(XSOLMg / XSOLFe)
[docs]@TipsySnap.derived_quantity
def nefe(sim):
"""Neon-to-iron ratio [Ne/Fe] derived from tipsy arrays MgMassFrac and FeMassFrac
with solar values from Asplund et al 09"""
minne = np.amin(sim['NeMassFrac'][np.where(sim['NeMassFrac'] > 0)])
sim['NeMassFrac'][np.where(sim['NeMassFrac'] == 0)] = minne
minfe = np.amin(sim['FeMassFrac'][np.where(sim['FeMassFrac'] > 0)])
sim['FeMassFrac'][np.where(sim['FeMassFrac'] == 0)] = minfe
return np.log10(sim['NeMassFrac'] / sim['FeMassFrac']) - np.log10(XSOLNe / XSOLFe)
[docs]@TipsySnap.derived_quantity
def sife(sim):
"""Silicon-to-iron ratio [Si/Fe] derived from tipsy arrays MgMassFrac and FeMassFrac
with solar values from Asplund et al 09"""
minsi = np.amin(sim['SiMassFrac'][np.where(sim['SiMassFrac'] > 0)])
sim['SiMassFrac'][np.where(sim['SiMassFrac'] == 0)] = minsi
minfe = np.amin(sim['FeMassFrac'][np.where(sim['FeMassFrac'] > 0)])
sim['FeMassFrac'][np.where(sim['FeMassFrac'] == 0)] = minfe
return np.log10(sim['SiMassFrac'] / sim['FeMassFrac']) - np.log10(XSOLSi / XSOLFe)
[docs]@TipsySnap.derived_quantity
def c_s(self):
"""Ideal gas sound speed based on pressure and density"""
#x = np.sqrt(5./3.*units.k*self['temp']*self['mu'])
x = np.sqrt(5. / 3. * self['p'] / self['rho'])
x.convert_units('km s^-1')
return x
[docs]@TipsySnap.derived_quantity
def c_s_turb(self):
"""Turbulent sound speed (from Mac Low & Klessen 2004)"""
x = np.sqrt(self['c_s'] ** 2 + 1. / 3 * self['v_disp'] ** 2)
x.convert_units('km s^-1')
return x
[docs]@TipsySnap.derived_quantity
def mjeans(self):
"""Classical Jeans mass"""
x = np.pi ** (5. / 2.) * self['c_s'] ** 3 / \
(6. * units.G ** (3, 2) * self['rho'] ** (1, 2))
x.convert_units('Msol')
return x
[docs]@TipsySnap.derived_quantity
def mjeans_turb(self):
"""Turbulent Jeans mass"""
x = np.pi ** (5. / 2.) * self['c_s_turb'] ** 3 / \
(6. * units.G ** (3, 2) * self['rho'] ** (1, 2))
x.convert_units('Msol')
return x
[docs]@TipsySnap.derived_quantity
def ljeans(self):
"""Jeans length"""
x = self['c_s'] * np.sqrt(np.pi / (units.G * self['rho']))
x.convert_units('kpc')
return x
[docs]@TipsySnap.derived_quantity
def ljeans_turb(self):
"""Turbulent Jeans length"""
x = self['c_s_turb'] * np.sqrt(np.pi / (units.G * self['rho']))
x.convert_units('kpc')
return x
class StarLog(SimSnap):
def __init__(self, filename, sort=True, paramfile=None):
import os
super(StarLog, self).__init__()
self._filename = filename
self._paramfilename = paramfile
f = util.open_(filename, "rb")
self.properties = {}
bigstarlog = False
molecH = False
bigIOrds = False
file_structure = np.dtype({'names': ("iord", "iorderGas", "tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform"),
'formats': ('i4', 'i4', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8')})
size = struct.unpack("i", f.read(4))
if (size[0] > 1000 or size[0] < 10):
self._byteswap = True
f.seek(0)
size = struct.unpack(">i", f.read(4))
iSize = size[0]
if (iSize > file_structure.itemsize):
file_structure = np.dtype({'names': ("iord", "iorderGas", "tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform","h2form"),
'formats': ('i4', 'i4', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8','f8')})
molecH = True
# Unfortunately molecularH with small iOrders has the same as
# no moleculuarH with big iOrders. Attempt to distinguish here
if(iSize == file_structure.itemsize):
if(self._byteswap):
testread = np.fromstring(
f.read(iSize), dtype=file_structure).byteswap()
else:
testread = np.fromstring(f.read(iSize), dtype=file_structure)
# All star iorders are greater than any gas iorder
# so this indicates a bad format. (N.B. there is the
# possibility of a false negative)
if(testread['iord'][0] < testread['iorderGas'][0]):
file_structure = np.dtype({'names': ("iord", "iorderGas",
"tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform"),
'formats': ('i8', 'i8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8')})
f.seek(4)
logger.info("Using 64 bit iOrders")
molecH = False
bigIOrds = True
if (iSize != file_structure.itemsize):
file_structure = np.dtype({'names': ("iord", "iorderGas", "tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform",
"phiform", "nsmooth"),
'formats': ('i4', 'i4', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'i4')})
molecH = False
if (iSize != file_structure.itemsize and iSize != 104):
raise IOError("Unknown starlog structure iSize:" + \
str(iSize) + ", file_structure itemsize:" + \
str(file_structure.itemsize))
else:
bigstarlog = True
if molecH == True: print("h2 information found in StarLog!")
datasize = os.path.getsize(filename) - f.tell()
# check whether datasize is a multiple of iSize. If it is not,
# the starlog is likely corrupted, but try to read it anyway
if (datasize % iSize > 0) and (iSize != 104):
warnings.warn(
"The size of the starlog file does not make sense -- it is likely corrupted. Pynbody will read it anyway, but use with caution.")
datasize -= datasize % iSize
logger.info("Reading starlog file %s", filename)
if(self._byteswap):
g = np.fromstring(
f.read(datasize), dtype=file_structure).byteswap()
else:
g = np.fromstring(f.read(datasize), dtype=file_structure)
# hoping to provide backward compatibility for np.unique by
# copying relavent part of current numpy source:
# numpy/lib/arraysetops.py:192 (on 22nd March 2011)
if sort:
tmp = g['iord'].flatten()
# mergesort for stability
perm = tmp.argsort(kind='mergesort')
aux = tmp[perm]
flag = np.concatenate((aux[1:] != aux[:-1], [True]))
iord = aux[flag]
indices = perm[flag]
self._num_particles = len(indices)
else:
self._num_particles = len(g)
self._family_slice[family.star] = slice(0, self._num_particles)
self._create_arrays(["pos", "vel"], 3)
if(bigIOrds):
self._create_arrays(["iord"], dtype='int64')
else:
self._create_arrays(["iord"], dtype='int32')
self._create_arrays(["iord"], dtype='int32')
self._create_arrays(
["iorderGas", "massform", "rhoform", "tempform", "metals", "tform"])
if molecH==True:
self._create_arrays(["h2form"])
if bigstarlog:
self._create_arrays(["phiform", "nsmooth"])
self._decorate()
if sort:
for name in list(file_structure.fields.keys()):
self.star[name][:] = g[name][indices]
else:
for name in list(file_structure.fields.keys()):
self.star[name] = g[name]
@staticmethod
def _write(self, filename=None):
"""Write the starlog file. """
global config
with self.lazy_off:
if filename is None:
filename = self._filename
logger.info("Writing starlog file as %s", filename)
f = util.open_(filename, 'wb')
if 'phiform' in list(self.keys()): # long starlog format
file_structure = np.dtype({'names': ("iord", "iorderGas", "tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform",
"phiform", "nsmooth"),
'formats': ('i4', 'i4', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'i4')})
else: # short (old) starlog format
file_structure = np.dtype({'names': ("iord", "iorderGas", "tform",
"x", "y", "z",
"vx", "vy", "vz",
"massform", "rhoform", "tempform"),
'formats': ('i4', 'i4', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8',
'f8', 'f8', 'f8')})
if self._byteswap:
f.write(struct.pack(">i", file_structure.itemsize))
else:
f.write(struct.pack("i", file_structure.itemsize))
max_block_size = 1024 # particles
n_left = len(self)
n_done = 0
while n_left > 0:
n_block = min(n_left, max_block_size)
g = np.zeros(n_block, dtype=file_structure)
for arr in file_structure.names:
g[arr] = self[arr][n_done:n_done + n_block]
if self._byteswap:
g.byteswap().tofile(f)
else:
g.tofile(f)
n_left -= n_block
n_done += n_block
f.close()
@TipsySnap.decorator
@StarLog.decorator
@nchilada.NchiladaSnap.decorator
def load_paramfile(sim):
import sys
import os
import glob
x = os.path.abspath(sim._filename)
done = False
sim._paramfile = {}
f = None
if sim._paramfilename is None:
for i in range(2):
x = os.path.dirname(x)
l = [x for x in glob.glob(
os.path.join(x, "*.param")) if "mpeg" not in x]
for filename in l:
# Attempt the loading of information
try:
f = open(filename)
done = True
break # the file is there, break out of the loop
except IOError:
l = glob.glob(os.path.join(x, "../*.param"))
if l == []:
continue
try:
for filename in l:
f = open(filename)
break # the file is there, break out of the loop
except IOError:
continue
if done:
break
else:
filename = sim._paramfilename
try:
f = open(filename)
except IOError:
raise IOError("The parameter filename you supplied is invalid")
if f is None:
return
for line in f:
try:
if line[0] != "#":
s = line.split("#")[0].split()
sim._paramfile[s[0]] = " ".join(s[2:])
except IndexError as ValueError:
pass
if len(sim._paramfile) > 1:
sim._paramfile["filename"] = filename
@TipsySnap.decorator
@StarLog.decorator
@nchilada.NchiladaSnap.decorator
def param2units(sim):
with sim.lazy_off:
import sys
import math
import os
import glob
munit = dunit = hub = None
try:
hub = float(sim._paramfile["dHubble0"])
sim.properties['omegaM0'] = float(sim._paramfile["dOmega0"])
sim.properties['omegaL0'] = float(sim._paramfile["dLambda"])
except KeyError:
pass
try:
munit_st = sim._paramfile["dMsolUnit"] + " Msol"
munit = float(sim._paramfile["dMsolUnit"])
dunit_st = sim._paramfile["dKpcUnit"] + " kpc"
dunit = float(sim._paramfile["dKpcUnit"])
except KeyError:
pass
if munit is None or dunit is None:
if hub != None:
sim.properties['h'] = hub
return
denunit = munit / dunit ** 3
denunit_st = str(denunit) + " Msol kpc^-3"
#
# the obvious way:
#
#denunit_cgs = denunit * 6.7696e-32
#kpc_in_km = 3.0857e16
#secunit = 1./math.sqrt(denunit_cgs*6.67e-8)
#velunit = dunit*kpc_in_km/secunit
# the sensible way:
# avoid numerical accuracy problems by concatinating factors:
velunit = 8.0285 * math.sqrt(6.6743e-8 * denunit) * dunit
velunit_st = ("%.5g" % velunit) + " km s^-1"
# You have: kpc s / km
# You want: Gyr
#* 0.97781311
timeunit = dunit / velunit * 0.97781311
timeunit_st = ("%.5g" % timeunit) + " Gyr"
# Assuming G=1 in code units, phi is actually vel^2/a^3.
# See also gasoline's master.c:5511.
# Or should we be calculating phi as GM/R units (which
# is the same for G=1 runs)?
potunit_st = "%.5g km^2 s^-2" % (velunit ** 2)
if 'bComove' in sim._paramfile and int(sim._paramfile['bComove']) != 0:
hubunit = 10. * velunit / dunit
hubunit_st = ("%.3f" % (hubunit * hub))
sim.properties['h'] = hub * hubunit
if isinstance(sim, StarLog):
a = "aform"
else:
a = "a"
# append dependence on 'a' for cosmological runs
dunit_st += " " + a
denunit_st += " " + a + "^-3"
velunit_st += " " + a
potunit_st += " " + a + "^-1"
# Assume the box size is equal to the length unit
sim.properties['boxsize'] = units.Unit(dunit_st)
try:
sim["vel"].units = velunit_st
except KeyError:
pass
try:
sim["phi"].units = potunit_st
sim["eps"].units = dunit_st
except KeyError:
pass
try:
sim["pos"].units = dunit_st
except KeyError:
pass
try:
sim.gas["rho"].units = denunit_st
except KeyError:
pass
try:
sim["mass"].units = munit_st
except KeyError:
pass
try:
sim.star["tform"].units = timeunit_st
except KeyError:
pass
try:
sim.gas["metals"].units = ""
except KeyError:
pass
try:
sim.star["metals"].units = ""
except KeyError:
pass
try:
sim._file_units_system = [
sim["vel"].units, sim.star["mass"].units, sim["pos"].units, units.K]
except KeyError:
try:
sim._file_units_system = [
sim["vel"].units, sim.star["massform"].units, sim["pos"].units, units.K]
except KeyError:
try:
sim._file_units_system = [units.Unit(velunit_st),
units.Unit(munit_st),
units.Unit(dunit_st), units.K]
except:
pass
sim._override_units_system()
@TipsySnap.decorator
def settime(sim):
if 'bComove' in sim._paramfile and int(sim._paramfile['bComove']) != 0:
t = sim._header_t
sim.properties['a'] = t
try:
sim.properties['z'] = 1.0 / t - 1.0
except ZeroDivisionError:
# no sensible redshift
pass
if (sim.properties['z'] is not None and
'dMsolUnit' in sim._paramfile and
'dKpcUnit' in sim._paramfile):
from ..analysis import cosmology
sim.properties['time'] = cosmology.age(
sim, unit=sim.infer_original_units('yr'))
else:
# something has gone wrong with the cosmological side of
# things
warnings.warn(
"Paramfile suggests time is cosmological, but header values are not sensible in this context.", RuntimeWarning)
sim.properties['time'] = t
sim.properties['a'] = t
else:
# Assume a non-cosmological run
sim.properties['time'] = sim._header_t
@nchilada.NchiladaSnap.decorator
def settimeN(sim):
if 'bComove' in sim._paramfile and int(sim._paramfile['bComove']) != 0:
#t = sim._header_t
#sim.properties['a'] = t
try:
sim.properties['z'] = 1.0 / sim.properties['a'] - 1.0
except ZeroDivisionError:
# no sensible redshift
pass
if (sim.properties['z'] is not None and
'dMsolUnit' in sim._paramfile and
'dKpcUnit' in sim._paramfile):
from ..analysis import cosmology
sim.properties['time'] = cosmology.age(
sim, unit=sim.infer_original_units('yr'))
else:
# something has gone wrong with the cosmological side of
# things
warnings.warn(
"Paramfile suggests time is cosmological, but header values are not sensible in this context.", RuntimeWarning)
sim.properties['time'] = sim.properties['a']
#sim.properties['a'] = t
else:
# Assume a non-cosmological run
sim.properties['time'] = sim.properties['a']
time_unit = None
try:
time_unit = sim.infer_original_units('yr')
except units.UnitsException:
pass
if time_unit is not None:
sim.properties['time'] *= time_unit
@StarLog.decorator
def slparam2units(sim):
with sim.lazy_off:
import sys
import math
import os
import glob
munit = dunit = hub = None
try:
munit_st = sim._paramfile["dMsolUnit"] + " Msol"
munit = float(sim._paramfile["dMsolUnit"])
dunit_st = sim._paramfile["dKpcUnit"] + " kpc"
dunit = float(sim._paramfile["dKpcUnit"])
hub = float(sim._paramfile["dHubble0"])
except KeyError:
pass
if munit is None or dunit is None:
return
denunit = munit / dunit ** 3
denunit_st = str(denunit) + " Msol kpc^-3"
velunit = 8.0285 * math.sqrt(6.6743e-8 * denunit) * dunit
velunit_st = ("%.5g" % velunit) + " km s^-1"
# You have: kpc s / km
# You want: Gyr
#* 0.97781311
timeunit = dunit / velunit * 0.97781311
timeunit_st = ("%.5g" % timeunit) + " Gyr"
if hub != None:
# append dependence on 'a' for cosmological runs
dunit_st += " aform"
# denunit_st += " a^-3"
# N.B. density comoving -> physical conversion is done by Gasoline
# itself
sim.star["rhoform"].units = denunit_st
sim.star["massform"].units = munit_st