diff --git a/binarycpython/utils/distribution_functions.py b/binarycpython/utils/distribution_functions.py index f5d6db84f0dae9943f62fbed43d3681cd788cafe..cdb941bb4858f25febccdf3720eb401ff18b5a26 100644 --- a/binarycpython/utils/distribution_functions.py +++ b/binarycpython/utils/distribution_functions.py @@ -314,17 +314,16 @@ def three_part_powerlaw( m0, m1, m2, m_max, p1, p2, p3 ) - # if m < m0: - prob = 0 # Below lower bound TODO: make this clear. - elif m0 < m <= m1: - prob = three_part_powerlaw_constants[0] * (m ** p1) # Between M0 and M1 - elif m1 < m <= m2: - prob = three_part_powerlaw_constants[1] * (m ** p2) # Between M1 and M2 - elif m2 < m <= m_max: - prob = three_part_powerlaw_constants[2] * (m ** p3) # Between M2 and M_MAX + prob = 0.0 # Below lower bound + elif m <= m1: + prob = three_part_powerlaw_constants[0] * (m ** p1) # Between m0 and m1 + elif m <= m2: + prob = three_part_powerlaw_constants[1] * (m ** p2) # Between m1 and m2 + elif m <= m_max: + prob = three_part_powerlaw_constants[2] * (m ** p3) # Between m2 and m_max else: - prob = 0 # Above M_MAX + prob = 0 # Above m_max return prob diff --git a/binarycpython/utils/grid.py b/binarycpython/utils/grid.py index 43339aff42c8bfc5494dda276d7ed5110e644f8f..b08016701c1b1e448460650a1be5b5db590ad886 100644 --- a/binarycpython/utils/grid.py +++ b/binarycpython/utils/grid.py @@ -50,6 +50,7 @@ import time import uuid _count = 0 +from diskcache import Cache from typing import Union, Any from collections import ( OrderedDict, @@ -549,7 +550,7 @@ class Population: valuerange: Union[list, str], samplerfunc: str, probdist: str, - dphasevol: Union[str, int], + dphasevol: Union[str, int] = -1, gridtype: str = "centred", branchpoint: int = 0, branchcode: Union[str, None] = None, @@ -644,6 +645,11 @@ class Population: Code added at the very bottom of the block. """ + # check parameters + if False and dphasevol != -1.0 and gridtype == 'discrete': + print("Error making grid: you have set the phasevol to be not -1 and gridtype to discrete, but a discrete grid has no phasevol calculation. You should only set the gridtype to discrete and not set the phasevol in this case.") + sys.exit() + # Add grid_variable grid_variable = { "name": name, @@ -666,7 +672,7 @@ class Population: } # Check for gridtype input - if not gridtype in [ + allowed_gridtypes = [ "edge", "right", "right edge", @@ -675,8 +681,10 @@ class Population: "centred", "centre", "center", - ]: - msg = "Unknown gridtype value. Please start another one" + "discrete" + ] + if not gridtype in allowed_gridtypes: + msg = "Unknown gridtype {gridtype}. Please choose one of: ".format(gridtype=gridtype) + ','.join(allowed_gridtypes) raise ValueError(msg) # Load it into the grid_options @@ -1367,7 +1375,7 @@ class Population: # Send closing signal to workers. When they receive this they will terminate if self.grid_options["verbosity"] >= _LOGGER_VERBOSITY_LEVEL: - stream_logger.debug(f"Signaling stop to processes") # DEBUG + stream_logger.debug(f"Signalling processes to stop") # DEBUG for _ in range(num_cores): job_queue.put("STOP") @@ -2464,7 +2472,10 @@ class Population: # Setting up the for loop # Add comment for for loop self._add_code( - "# for loop for {}".format(grid_variable["name"]) + "\n", + "# for loop for variable {name} gridtype {gridtype}".format( + name=grid_variable["name"], + gridtype=grid_variable["gridtype"], + ) + "\n", "sampled_values_{} = {}".format( grid_variable["name"], grid_variable["samplerfunc"] ) @@ -2472,136 +2483,160 @@ class Population: if vb: self._add_code( - "print('samples','{}',':',sampled_values_{})\n".format( - grid_variable["name"], grid_variable["name"] + "print('samples','{name}',':',sampled_values_{name})\n".format( + name=grid_variable["name"], ) ) - # # Some print statement - # self._add_code(( - # "print('phasevol_{}:', phasevol_{})".format(grid_variable["name"], - # grid_variable["name"]) - # + "\n" - # ) - - # TODO: make sure this works - # Adding for loop structure - # self._add_code(( - # "for {} in sampled_values_{}:".format( - # grid_variable["name"], grid_variable["name"] - # ) - # + "\n" - # ) - if vb: self._add_code( - "print('sample {} from',sampled_values_{})".format( - grid_variable["name"], grid_variable["name"] + "print('sample {name} from',sampled_values_{name})".format( + name=grid_variable["name"] ) + "\n" ) - - # if we're on a centred grid, we have actually - # one fewer grid point + # calculate number of values and starting location + # + # if we're sampling a continuous variable, we + # have one fewer grid point than the length of the + # sampled_values list if ( grid_variable["gridtype"] == "centred" or grid_variable["gridtype"] == "centre" or grid_variable["gridtype"] == "center" + or grid_variable["gridtype"] == "edge" + or grid_variable["gridtype"] == "left edge" + or grid_variable["gridtype"] == "left" + or grid_variable["gridtype"] == "right" + or grid_variable["gridtype"] == "right edge" ): offset = -1 - else: + elif grid_variable["gridtype"] == "discrete": + # discrete variables sample all the points offset = 0 - self._add_code( - "# for loop for {}".format(grid_variable["name"]) + "\n", - "nvalues_{} = len(sampled_values_{})+{}".format( - grid_variable["name"], - grid_variable["name"], - offset - ) - + "\n") + start = 0 - # if grid_variable["resolution"] == 0: - # sample from the sampled_values + # for loop over the variable if vb: self._add_code( - "print(\"var {} values \",sampled_values_{},\" len \",len(sampled_values_{})+{},\" gridtype {} offset {}\\n\")\n".format( - grid_variable["name"], - grid_variable["name"], - grid_variable["name"], - offset, - grid_variable['gridtype'], - offset + "print(\"var {name} values \",sampled_values_{name},\" len \",len(sampled_values_{name})+{offset},\" gridtype {gridtype} offset {offset}\\n\")\n".format( + name=grid_variable["name"], + offset=offset, + gridtype=grid_variable['gridtype'], ) ) - self._add_code( - "for {}_sample_number in range(max(0,len(sampled_values_{})+{})):".format( - grid_variable["name"], - grid_variable["name"], - offset, + "for {name}_sample_number in range({start},len(sampled_values_{name})+{offset}):".format( + name=grid_variable["name"], + offset=offset, + start=start ) + "\n" ) - # old code: use resolution passed in - # else: - # # use resolution passed in - # self._add_code( - # "for {}_sample_number in range({} if {} != 0 else max(0,len(sampled_values_{})-1)):".format( - # grid_variable["name"], - # grid_variable["resolution"], - # grid_variable["resolution"], - # grid_variable["name"], - # ) - # + "\n" - # ) - self._increment_indent_depth(+1) - # {}_this and {}_next are this grid point's index and the next - # grid point's index, used in many calculations below - self._add_code("if {}_sample_number == 0:\n".format(grid_variable["name"])) - self._add_code("{}_this = 0;\n".format(grid_variable["name"]), indent=1) - self._add_code("else:\n") - self._add_code( - "{}_this = {}_sample_number; ".format( - grid_variable["name"], grid_variable["name"] - ), - indent=1, - ) - self._add_code( - "\n", - "{}_next = {}_this + 1".format( - grid_variable["name"], grid_variable["name"] - ), - "\n", - ) + # {}_this_index is this grid point's index + # {}_prev_index and {}_next_index are the previous and next grid points, + # (which can be None if there is no previous or next, or if + # previous and next should not be used: this is deliberate) + # - # TODO: Make clear that the phasevol only works good - # TODO: add option to ignore this phasevol calculation and set it to 1 - # if you sample linearly in that thing. - # if phasevol is <= 0 then we SKIP that whole loop. Its not working then. - if ( - not grid_variable["dphasevol"] == -1 - ): # A method to turn off this calculation and allow a phasevol = 1 + if grid_variable["gridtype"] == "discrete": + # discrete grids only care about this, + # both prev and next should be None to + # force errors where they are used self._add_code( - "phasevol_{name} = (sampled_values_{name}[{name}_next]-sampled_values_{name}[{name}_this]) if {name}_next < nvalues_{name} else (sampled_values_{name}[{name}_next-1]-sampled_values_{name}[{name}_this-1])".format(name=grid_variable["name"]) + "{name}_this_index = {name}_sample_number ".format( + name=grid_variable["name"], + ), + ) + self._add_code( + "\n", + "{name}_prev_index = None if {name}_this_index == 0 else ({name}_this_index - 1) ".format( + name=grid_variable["name"], + ), + "\n", + ) + self._add_code( + "\n", + "{name}_next_index = None if {name}_this_index >= (len(sampled_values_{name})+{offset} - 1) else ({name}_this_index + 1)".format( + name=grid_variable["name"], + offset=offset + ), + "\n", + ) + + elif (grid_variable["gridtype"] == "centred" + or grid_variable["gridtype"] == "centre" + or grid_variable["gridtype"] == "center" + or grid_variable["gridtype"] == "edge" + or grid_variable["gridtype"] == "left" + or grid_variable["gridtype"] == "left edge"): + + # left and centred grids + self._add_code("if {}_sample_number == 0:\n".format(grid_variable["name"])) + self._add_code("{}_this_index = 0;\n".format(grid_variable["name"]), indent=1) + self._add_code("else:\n") + self._add_code("{name}_this_index = {name}_sample_number ".format(name=grid_variable["name"]),indent=1) + self._add_code("\n") + self._add_code("{name}_prev_index = ({name}_this_index - 1) if {name}_this_index > 0 else None ".format(name=grid_variable["name"])) + self._add_code("\n") + self._add_code("{name}_next_index = {name}_this_index + 1".format(name=grid_variable["name"])) + self._add_code("\n") + + elif(grid_variable["gridtype"] == "right" or + grid_variable["gridtype"] == "right edge"): + + # right edged grid + self._add_code("if {name}_sample_number == 0:\n".format(name=grid_variable["name"])) + self._add_code("{name}_this_index = 1;\n".format(name=grid_variable["name"]),indent=1) + self._add_code("else:\n") + self._add_code("{name}_this_index = {name}_sample_number + 1 ".format(name=grid_variable["name"],),indent=1) + self._add_code("\n") + self._add_code("{name}_prev_index = {name}_this_index - 1".format(name=grid_variable["name"])) + self._add_code("\n") + self._add_code("{name}_next_index = ({name}_this_index + 1) if {name}_this_index < len(sampled_values_{name}) else None".format(name=grid_variable["name"])) + self._add_code("\n") + + # calculate phase volume + if(grid_variable["dphasevol"] == -1): + # no phase volume required so set it to 1.0 + self._add_code("dphasevol_{name} = 1.0 # 666\n".format(name=grid_variable["name"])) + + elif(grid_variable["gridtype"] == "right" or + grid_variable["gridtype"] == "right edge"): + # right edges always have this and prev defined + self._add_code( + "dphasevol_{name} = (sampled_values_{name}[{name}_this_index] - sampled_values_{name}[{name}_prev_index])".format(name=grid_variable["name"]) + "\n" ) + elif grid_variable["gridtype"] == "discrete": + # discrete might have next defined, use it if we can, + # otherwise use prev + self._add_code( + "dphasevol_{name} = (sampled_values_{name}[{name}_next_index] - sampled_values_{name}[{name}_this_index]) if {name}_next_index else (sampled_values_{name}[{name}_this_index] - sampled_values_{name}[{name}_prev_index])".format(name=grid_variable["name"]) + + "\n" + ) else: - self._add_code("phasevol_{} = 1\n".format(grid_variable["name"])) + # left and centred always have this and next defined + self._add_code( + "dphasevol_{name} = (sampled_values_{name}[{name}_next_index] - sampled_values_{name}[{name}_this_index])".format(name=grid_variable["name"]) + + "\n" + ) + ############## # Add phasevol check: - self._add_code("if phasevol_{} <= 0:\n".format(grid_variable["name"])) + self._add_code("if dphasevol_{name} <= 0:\n".format(name=grid_variable["name"])) # TODO: We might actually want to add the starcount and probability to the totals regardless. # n that case we need another local variable which will prevent it from being run but will track those parameters # Add phasevol check action: self._add_code( - 'print("Grid generator: phasevol_{name} <= 0! (this=",{name}_this,"=",sampled_values_{name}[{name}_this],", next=",{name}_next,"=",sampled_values_{name}[{name}_next],") Skipping current sample.")'.format(name=grid_variable["name"]) + 'print("Grid generator: dphasevol_{name} <= 0! (this=",{name}_this_index,"=",sampled_values_{name}[{name}_this_index],", next=",{name}_next_index,"=",sampled_values_{name}[{name}_next_index],") Skipping current sample.")'.format(name=grid_variable["name"]) + "\n", "continue\n", indent=1, @@ -2609,34 +2644,22 @@ class Population: if vb: self._add_code( - "print('sample {name} from ',sampled_values_{name},' at this=',{name}_this,', next=',{name}_next)".format(name=grid_variable["name"]) + "print('sample {name} from ',sampled_values_{name},' at this=',{name}_this_index,', next=',{name}_next_index)".format(name=grid_variable["name"]) + "\n" ) - # select point location based on gridtype (left, centre or right) + # select sampled point location based on gridtype (left, centre or right) if ( grid_variable["gridtype"] == "edge" or grid_variable["gridtype"] == "left" or grid_variable["gridtype"] == "left edge" - ): - self._add_code( - "{name} = sampled_values_{name}[{name}_this]".format( - name=grid_variable["name"]) - + "\n" - ) - if vb: - self._add_code( - "print(\"sampled_values_{name}[{name}_this = \",{name}_this,\" {name}_next= \",{name}_next,\"] = \",{name},\"\\\n\")\n".format( - name=grid_variable["name"], - )) - elif ( - grid_variable["gridtype"] == "right" + or grid_variable["gridtype"] == "right" or grid_variable["gridtype"] == "right edge" + or grid_variable['gridtype'] == 'discrete' ): self._add_code( - + "{name} = sampled_values_{name}[{name}_next]".format( - name=grid_variable["name"], - ) + "{name} = sampled_values_{name}[{name}_this_index]".format( + name=grid_variable["name"]) + "\n" ) elif ( @@ -2645,17 +2668,17 @@ class Population: or grid_variable["gridtype"] == "center" ): self._add_code( - "{name} = 0.5 * (sampled_values_{name}[{name}_next] + sampled_values_{name}[{name}_this])".format(name=grid_variable["name"]) + "{name} = 0.5 * (sampled_values_{name}[{name}_next_index] + sampled_values_{name}[{name}_this_index])".format(name=grid_variable["name"]) + "\n" ) else: - msg = "Unknown gridtype value. PLease choose a different one" + msg = "Unknown gridtype value {type}.".format(type=grid_variable['gridtype']) raise ValueError(msg) if vb: self._add_code( - "print('hence {} = ',{})\n".format( - grid_variable["name"], grid_variable["name"] + "print('hence {name} = ',{name})\n".format( + name=grid_variable["name"] ) ) @@ -2666,18 +2689,18 @@ class Population: if grid_variable["condition"]: self._add_code( # Add comment - "# Condition for {}\n".format(grid_variable["name"]), + "# Condition for {name}\n".format(name=grid_variable["name"]), # Add condition check - "if not {}:\n".format(grid_variable["condition"]), + "if not {condition}:\n".format(condition=grid_variable["condition"]), indent=0, ) # Add condition failed action: if self.grid_options["verbosity"] >= 3: self._add_code( - 'print("Grid generator: Condition for {} not met!")'.format( - grid_variable["name"] + 'print("Grid generator: Condition for {name} not met!")'.format( + name=grid_variable["name"] ) + "\n", "continue" + "\n", @@ -2691,16 +2714,6 @@ class Population: # Add some whitespace self._add_code("\n") - # # Add debugging: - # if grid_variable['name']=='q': - # self._add_code(( - # indent=1, - # 'print("sampling:", sampled_values_{}, M_1)'.format( - # grid_variable["name"], grid_variable["name"] - # ) - # + "\n" - # ) - # Add some whitespace self._add_code("\n") @@ -2709,8 +2722,8 @@ class Population: # Add pre-code if grid_variable["precode"]: self._add_code( - "{}".format( - grid_variable["precode"].replace( + "{precode}".format( + precode=grid_variable["precode"].replace( "\n", "\n" + self._indent_block(0) ) ) @@ -2719,8 +2732,8 @@ class Population: # Set phasevol self._add_code( - "phasevol *= phasevol_{}\n".format( - grid_variable["name"], + "phasevol *= dphasevol_{name}\n".format( + name=grid_variable["name"], ) ) @@ -2730,29 +2743,29 @@ class Population: self._add_code( "\n", "# Setting probabilities\n", - "d{} = phasevol_{} * ({})".format( - grid_variable["name"], - grid_variable["name"], - grid_variable["probdist"], + "d{name} = dphasevol_{name} * ({probdist})".format( + name=grid_variable["name"], + probdist=grid_variable["probdist"], ) + "\n", # Save probability sum - "probabilities_sum[{}] += d{}".format( - grid_variable["grid_variable_number"], grid_variable["name"] + "probabilities_sum[{n}] += d{name}".format( + n=grid_variable["grid_variable_number"], + name=grid_variable["name"] ) + "\n", ) if grid_variable["grid_variable_number"] == 0: self._add_code( - "probabilities_list[0] = d{}".format(grid_variable["name"]) + "\n" + "probabilities_list[0] = d{name}".format(name=grid_variable["name"]) + "\n" ) else: self._add_code( - "probabilities_list[{}] = probabilities_list[{}] * d{}".format( - grid_variable["grid_variable_number"], - grid_variable["grid_variable_number"] - 1, - grid_variable["name"], + "probabilities_list[{this}] = probabilities_list[{prev}] * d{name}".format( + this=grid_variable["grid_variable_number"], + prev=grid_variable["grid_variable_number"] - 1, + name=grid_variable["name"], ) + "\n" ) @@ -2761,8 +2774,8 @@ class Population: # postcode if grid_variable["postcode"]: self._add_code( - "{}".format( - grid_variable["postcode"].replace( + "{postcode}".format( + postcode=grid_variable["postcode"].replace( "\n", "\n" + self._indent_block(0) ) ) @@ -2773,14 +2786,14 @@ class Population: # Increment starcount for this parameter self._add_code( "\n", - "# Increment starcount for {}\n".format(grid_variable["name"]), - "starcounts[{}] += 1".format( - grid_variable["grid_variable_number"], + "# Increment starcount for {name}\n".format(name=grid_variable["name"]), + "starcounts[{n}] += 1".format( + n=grid_variable["grid_variable_number"], ) + "\n", # Add value to dict - 'parameter_dict["{}"] = {}'.format( - grid_variable["parameter_name"], grid_variable["parameter_name"] + 'parameter_dict["{name}"] = {name}'.format( + name=grid_variable["parameter_name"] ) + "\n", "\n", @@ -2826,14 +2839,14 @@ class Population: self._increment_indent_depth(+1) self._add_code( "#" * 40 + "\n", - "# Code below is for finalising the handling of this iteration of the parameter {}\n".format( - grid_variable["name"] + "# Code below is for finalising the handling of this iteration of the parameter {name}\n".format( + name=grid_variable["name"] ), ) # Set phasevol # TODO: fix. this isn't supposed to be the value that we give it here. discuss - self._add_code("phasevol /= phasevol_{}\n\n".format(grid_variable["name"])) + self._add_code("phasevol /= dphasevol_{name}\n\n".format(name=grid_variable["name"])) self._increment_indent_depth(-2) @@ -2872,7 +2885,7 @@ class Population: self._increment_indent_depth(+1) self._add_code("\n", "#" * 40 + "\n", "if print_results:\n") self._add_code( - "print('Grid has handled {} stars with a total probability of {:g}'.format(_total_starcount,self.grid_options['_probtot']))\n", + "print('Grid has handled {starcount} stars with a total probability of {probtot:g}'.format(starcount=_total_starcount,probtot=self.grid_options['_probtot']))\n", indent=1, ) @@ -2895,14 +2908,15 @@ class Population: # Write to file gridcode_filename = self._gridcode_filename() + self.grid_options["gridcode_filename"] = gridcode_filename verbose_print( - "{}Writing grid code to {} [dry_run = {}]{}".format( - self.ANSI_colours["blue"], - gridcode_filename, - dry_run, - self.ANSI_colours["reset"], + "{blue}Writing grid code to {file} [dry_run = {dry}]{reset}".format( + blue=self.ANSI_colours["blue"], + file=gridcode_filename, + dry=dry_run, + reset=self.ANSI_colours["reset"], ), self.grid_options["verbosity"], 1, @@ -2927,8 +2941,10 @@ class Population: try: os.symlink(gridcode_filename, symlink) verbose_print( - "{}Symlinked grid code to {} {}".format( - self.ANSI_colours["blue"], symlink, self.ANSI_colours["reset"] + "{blue}Symlinked grid code to {symlink} {reset}".format( + blue=self.ANSI_colours["blue"], + symlink=symlink, + reset=self.ANSI_colours["reset"] ), self.grid_options["verbosity"], 1, @@ -2946,12 +2962,12 @@ class Population: self._add_code("#" * 40 + "\n") if branchcode: - self._add_code("# Branch code\nif {}:\n", branchcode) + self._add_code("# Branch code\nif {branchcode}:\n".format(branchcode=branchcode)) if branchpoint: self._add_code( - "# Code below will get evaluated for every system at this level of multiplicity (last one of that being {})\n".format( - grid_variable["name"] + "# Code below will get evaluated for every system at this level of multiplicity (last one of that being {name})\n".format( + name=grid_variable["name"] ) ) else: @@ -2963,8 +2979,8 @@ class Population: self._add_code( "\n", "# Weigh the probability by a custom weighting factor\n", - 'probability = self.grid_options["weight"] * probabilities_list[{}]'.format( - grid_variable["grid_variable_number"] + 'probability = self.grid_options["weight"] * probabilities_list[{n}]'.format( + n=grid_variable["grid_variable_number"] ) + "\n", # Take into account the multiplicity fraction: @@ -2984,8 +3000,8 @@ class Population: self._add_code( "_total_starcount += 1\n", # set probability and phasevol values into the system dict - 'parameter_dict["{}"] = {}'.format("probability", "probability") + "\n", - 'parameter_dict["{}"] = {}'.format("phasevol", "phasevol") + "\n", + 'parameter_dict["{p}"] = {p}'.format(p="probability") + "\n", + 'parameter_dict["{v}"] = {v}'.format(v="phasevol") + "\n", # Increment total probability "self._increment_probtot(probability)\n", indent=1, @@ -3015,8 +3031,8 @@ class Population: # Code to load the verbose_print( - message="Loading grid code function from {}".format( - self.grid_options["gridcode_filename"] + message="Loading grid code function from {file}".format( + file=self.grid_options["gridcode_filename"] ), verbosity=self.grid_options["verbosity"], minimal_verbosity=1, @@ -4165,8 +4181,7 @@ class Population: condition="({}[int(multiplicity)-1] > 0)".format( str(self.grid_options["Moe2017_options"]["multiplicity_modulator"]) ), - gridtype="edge", - dphasevol=-1, + gridtype="discrete", probdist=1, ) diff --git a/binarycpython/utils/spacing_functions.py b/binarycpython/utils/spacing_functions.py index 1e4c2ecf72297696b36d304ade77f05baaff531b..b76af0d8b32fe5599e7a86de711ca95b110b7a92 100644 --- a/binarycpython/utils/spacing_functions.py +++ b/binarycpython/utils/spacing_functions.py @@ -6,7 +6,10 @@ Tasks: """ from typing import Union +from diskcache import Cache +import datetime import functools +import json import math import numpy as np import py_rinterpolate @@ -158,25 +161,7 @@ def gaussian_zoom( @functools.lru_cache(maxsize=16) -def const_dt( - self, - dt=1000.0, - dlogt=0.1, - mmin=0.07, - mmax=100.0, - nres=1000, - logspacing=False, - tmin=3.0, # start at 3Myr - tmax=None, # use max_evolution_time by default - mindm=None, # tuple of tuples - maxdm=((0.07, 1.0, 0.1), (1.0, 300.0, 1.0)), # tuple of tuples - fsample=1.0, - factor=1.0, - logmasses=False, - log10masses=False, - showlist=False, - showtable=False, -): +def const_dt(self,cachedir=None,usecache=True,**kwargs): """ const_dt returns a list of masses spaced at a constant age difference @@ -197,6 +182,8 @@ def const_dt( showlist: if True, show the mass list once generated logmasses: if True, the masses are logged with math.log() log10masses: if True, the masses are logged with math.log10() + usecache: if True (the default) uses cached results if they are saved (in cachedir) + cachedir: if None, defaults to grid_options['tmp_dir']/const_dt_cache Returns: Array of masses. @@ -212,222 +199,271 @@ def const_dt( samplerfunc="const_dt(self,dlogt=0.1,nres=100,mmin=0.07,mmax=80.0,maxdm=((0.07,1.0,0.1),(1.0,10.0,1.0),(10.0,80.0,2.0)),showtable=True,logspacing=True,fsample=1.0/4.0)" """ - - # first, make a stellar lifetime table - # - # we should use the bse_options from self - # so our lifetime_population uses the same physics - lifetime_population = Population() - lifetime_population.bse_options = dict(self.bse_options) - - # we only want to evolve the star during nuclear burning, - # we don't want a dry run of the grid - # we want to use the right number of CPU cores - lifetime_population.set( - do_dry_run=False, - num_cores=self.grid_options["num_cores"], - max_stellar_type_1=10, - save_ensemble_chunks=False, - symlink_latest_gridcode=False, - modulo=1, - start_at=0, - slurm=0, - condor=0, - ) - - # make a grid in M1 - lifetime_population.add_grid_variable( - name="lnM_1", - parameter_name="M_1", - longname="log Primary mass", # == single-star mass - valuerange=[math.log(mmin), math.log(mmax)], - samplerfunc="const(math.log({mmin}),math.log({mmax}),{nres})".format( - mmin=mmin, mmax=mmax, nres=nres - ), - probdist="1", # dprob/dm1 : we don't care, so just set it to 1 - dphasevol="dlnM_1", - precode="M_1=math.exp(lnM_1)", - condition="", # Impose a condition on this grid variable. Mostly for a check for yourself - gridtype="edge", - ) - - # set up the parse function - def _parse_function(self, output): - if output: - for line in output.splitlines(): - data = line.split() - if data[0] == "SINGLE_STAR_LIFETIME": - # append (log10(mass), log10(lifetime)) tuples - logm = math.log10(float(data[1])) - logt = math.log10(float(data[2])) - # print(line) - # print("logM=",logm,"M=",10.0**logm," -> logt=",logt) - self.grid_results["interpolation table m->t"][logm] = logt - self.grid_results["interpolation table t->m"][logt] = logm - - lifetime_population.set( - parse_function=_parse_function, - ) - - # run to build the interpolation table - print("Running population to make lifetime interpolation table, please wait") - lifetime_population.evolve() - # print("Data table",lifetime_population.grid_results['interpolation table t->m']) - - if not "interpolation table t->m" in lifetime_population.grid_results or len(lifetime_population.grid_results["interpolation table t->m"].keys()) == 0: - print("\n\n\nError: The t->m lifetime table is empty. One usual cause for this is that the tmax or max_evolution_time option (currently passed in to const_dt as {tmax}) is too short for there to be any entries in the table before the first timestep. Try increasing tmax and max_evolution_time, shorten the timestep or, if using log times, set tstart to be closer to 0.\n".format(tmax=tmax)) - exit() - - - # convert to nested lists for the interpolator - # - # make time -> mass table - data_table_time_mass = [] - times = sorted(lifetime_population.grid_results["interpolation table t->m"].keys()) - for time in times: - mass = lifetime_population.grid_results["interpolation table t->m"][time] - # we have to make sure the time is monotonic (not guaranteed at high mass) - if len(data_table_time_mass) == 0: - data_table_time_mass.append([time, mass]) - elif mass < data_table_time_mass[-1][1]: - data_table_time_mass.append([time, mass]) - - # make mass -> time table - data_table_mass_time = [] - masses = sorted(lifetime_population.grid_results["interpolation table m->t"].keys()) - for mass in masses: - time = lifetime_population.grid_results["interpolation table m->t"][mass] - data_table_mass_time.append([mass, time]) - - # set up interpolators - interpolator_time_mass = py_rinterpolate.Rinterpolate( - table=data_table_time_mass, nparams=1, ndata=1, verbosity=0 # mass # lifetime - ) - interpolator_mass_time = py_rinterpolate.Rinterpolate( - table=data_table_mass_time, nparams=1, ndata=1, verbosity=0 # lifetime # mass - ) - - # function to get a mass given a time (Myr) - def _mass_from_time(linear_time): - return 10.0 ** interpolator_time_mass.interpolate([math.log10(linear_time)])[0] - - # function to get a time given a mass (Msun) - def _time_from_mass(mass): - return 10.0 ** interpolator_mass_time.interpolate([math.log10(mass)])[0] - - # return a unique list - def _uniq(_list): - return sorted(list(set(_list))) - - # format a whole list like %g - def _format(_list): - return [float("{x:g}".format(x=x)) for x in _list] - - # construct mass list, always include the min and max - mass_list = [mmin, mmax] - - # first, make sure the stars are separated by only - # maxdm - if maxdm: - for x in maxdm: - range_min = x[0] - range_max = x[1] - dm = x[2] - if dm < 0.0: - # use log scale - dlogm = -dm - logm = math.log(mmin) - logmmax = math.log(mmax) - logrange_min = math.log(range_min) - logrange_max = math.log(range_max) - while logm <= logmmax: - if logm >= logrange_min and logm <= logrange_max: - mass_list.append(math.exp(logm)) - logm += dlogm - else: - # use linear scale - m = mmin - while m <= mmax: - if m >= range_min and m <= range_max: - mass_list.append(m) - m += dm - - # start time loop at tmax or max_evolution_time - t = tmax if tmax else self.bse_options["max_evolution_time"] - - # set default mass list - if logspacing: - logt = math.log10(t) - logtmin = math.log10(tmin) - while logt > logtmin: - m = _mass_from_time(10.0 ** logt) - mass_list.append(m) - logt = max(logtmin, logt - dlogt * fsample) - else: - while t > tmin: - m = _mass_from_time(t) - mass_list.append(m) - t = max(tmin, t - dt * fsample) - - # make mass list unique - mass_list = _uniq(mass_list) - - if mindm: - for x in mindm: - range_min = x[0] - range_max = x[1] - mindm = x[2] - # impose a minimum dm: if two masses in the list - # are separated by < this, remove the second - for index, mass in enumerate(mass_list): - if index > 0 and mass >= range_min and mass <= range_max: - dm = mass_list[index] - mass_list[index - 1] - if dm < mindm: - mass_list[index - 1] = 0.0 - mass_list = _uniq(mass_list) - if mass_list[0] == 0.0: - mass_list.remove(0.0) - - # apply multiplication factor if given - if factor and factor != 1.0: - mass_list = [m * factor for m in mass_list] - - # reformat numbers - mass_list = _format(mass_list) - - # show the mass<>time table? - if showtable: - twas = 0.0 - logtwas = 0.0 - for i, m in enumerate(mass_list): - t = _time_from_mass(m) + if cachedir == None: + cachedir = self.grid_options['tmp_dir'] + '/const_dt_cache' + cache = Cache(cachedir) + @cache.memoize() + def _const_dt(cachedir, + num_cores, + bse_options, + dt=1000.0, + dlogt=0.1, + mmin=0.07, + mmax=100.0, + nres=1000, + logspacing=False, + tmin=3.0, # start at 3Myr + tmax=None, # use max_evolution_time by default + mindm=None, # tuple of tuples + maxdm=((0.07, 1.0, 0.1), (1.0, 300.0, 1.0)), # tuple of tuples + fsample=1.0, + factor=1.0, + logmasses=False, + log10masses=False, + showlist=False, + showtable=False, + usecache=True, + ): + + # first thing to do is make a stellar lifetime table + # + # we should use the bse_options passed in + # so our lifetime_population uses the same physics + # as the main grid + + # convert bse_options to dict + bse_options = json.loads(bse_options) + + # make new population object + lifetime_population = Population() + lifetime_population.bse_options = bse_options + + # we only want to evolve the star during nuclear burning, + # we don't want a dry run of the grid + # we want to use the right number of CPU cores + lifetime_population.set( + do_dry_run=False, + num_cores=num_cores, + max_stellar_type_1=10, + save_ensemble_chunks=False, + symlink_latest_gridcode=False, + modulo=1, + start_at=0, + slurm=0, + condor=0, + ) + + # make a grid in M1 + lifetime_population.add_grid_variable( + name="lnM_1", + parameter_name="M_1", + longname="log Primary mass", # == single-star mass + valuerange=[math.log(mmin), math.log(mmax)], + samplerfunc="const(math.log({mmin}),math.log({mmax}),{nres})".format( + mmin=mmin, mmax=mmax, nres=nres + ), + probdist="1", # dprob/dm1 : we don't care, so just set it to 1 + dphasevol="dlnM_1", + precode="M_1=math.exp(lnM_1)", + condition="", # Impose a condition on this grid variable. Mostly for a check for yourself + gridtype="edge", + ) + + # set up the parse function + def _parse_function(self, output): + if output: + for line in output.splitlines(): + data = line.split() + if data[0] == "SINGLE_STAR_LIFETIME": + # append (log10(mass), log10(lifetime)) tuples + logm = math.log10(float(data[1])) + logt = math.log10(float(data[2])) + # print(line) + # print("logM=",logm,"M=",10.0**logm," -> logt=",logt) + self.grid_results["interpolation table m->t"][logm] = logt + self.grid_results["interpolation table t->m"][logt] = logm + + lifetime_population.set( + parse_function=_parse_function, + ) + + # run to build the interpolation table + print("Running population to make lifetime interpolation table, please wait") + lifetime_population.evolve() + # print("Data table",lifetime_population.grid_results['interpolation table t->m']) + + if not "interpolation table t->m" in lifetime_population.grid_results or len(lifetime_population.grid_results["interpolation table t->m"].keys()) == 0: + print("\n\n\nError: The t->m lifetime table is empty. One usual cause for this is that the tmax or max_evolution_time option (currently passed in to const_dt as {tmax}) is too short for there to be any entries in the table before the first timestep. Try increasing tmax and max_evolution_time, shorten the timestep or, if using log times, set tstart to be closer to 0.\n".format(tmax=tmax)) + exit() + + + # convert to nested lists for the interpolator + # + # make time -> mass table + data_table_time_mass = [] + times = sorted(lifetime_population.grid_results["interpolation table t->m"].keys()) + for time in times: + mass = lifetime_population.grid_results["interpolation table t->m"][time] + # we have to make sure the time is monotonic (not guaranteed at high mass) + if len(data_table_time_mass) == 0: + data_table_time_mass.append([time, mass]) + elif mass < data_table_time_mass[-1][1]: + data_table_time_mass.append([time, mass]) + + # make mass -> time table + data_table_mass_time = [] + masses = sorted(lifetime_population.grid_results["interpolation table m->t"].keys()) + for mass in masses: + time = lifetime_population.grid_results["interpolation table m->t"][mass] + data_table_mass_time.append([mass, time]) + + # set up interpolators + interpolator_time_mass = py_rinterpolate.Rinterpolate( + table=data_table_time_mass, nparams=1, ndata=1, verbosity=0 # mass # lifetime + ) + interpolator_mass_time = py_rinterpolate.Rinterpolate( + table=data_table_mass_time, nparams=1, ndata=1, verbosity=0 # lifetime # mass + ) + + # function to get a mass given a time (Myr) + def _mass_from_time(linear_time): + return 10.0 ** interpolator_time_mass.interpolate([math.log10(linear_time)])[0] + + # function to get a time given a mass (Msun) + def _time_from_mass(mass): + return 10.0 ** interpolator_mass_time.interpolate([math.log10(mass)])[0] + + # return a unique list + def _uniq(_list): + return sorted(list(set(_list))) + + # format a whole list like %g + def _format(_list): + return [float("{x:g}".format(x=x)) for x in _list] + + # construct mass list, always include the min and max + mass_list = [mmin, mmax] + + # first, make sure the stars are separated by only + # maxdm + if maxdm: + for x in maxdm: + range_min = x[0] + range_max = x[1] + dm = x[2] + if dm < 0.0: + # use log scale + dlogm = -dm + logm = math.log(mmin) + logmmax = math.log(mmax) + logrange_min = math.log(range_min) + logrange_max = math.log(range_max) + while logm <= logmmax: + if logm >= logrange_min and logm <= logrange_max: + mass_list.append(math.exp(logm)) + logm += dlogm + else: + # use linear scale + m = mmin + while m <= mmax: + if m >= range_min and m <= range_max: + mass_list.append(m) + m += dm + + # start time loop at tmax or max_evolution_time + t = tmax if tmax else bse_options['max_evolution_time'] + + # set default mass list + if logspacing: logt = math.log10(t) - if twas > 0.0: - print( - "{i:4d} m={m:13g} t={t:13g} log10(t)={logt:13g} dt={dt:13g} dlog10(t)={dlogt:13g}".format( - i=i, m=m, t=t, logt=logt, dt=twas - t, dlogt=logtwas - logt + logtmin = math.log10(tmin) + while logt > logtmin: + m = _mass_from_time(10.0 ** logt) + mass_list.append(m) + logt = max(logtmin, logt - dlogt * fsample) + else: + while t > tmin: + m = _mass_from_time(t) + mass_list.append(m) + t = max(tmin, t - dt * fsample) + + # make mass list unique + mass_list = _uniq(mass_list) + + if mindm: + for x in mindm: + range_min = x[0] + range_max = x[1] + mindm = x[2] + # impose a minimum dm: if two masses in the list + # are separated by < this, remove the second + for index, mass in enumerate(mass_list): + if index > 0 and mass >= range_min and mass <= range_max: + dm = mass_list[index] - mass_list[index - 1] + if dm < mindm: + mass_list[index - 1] = 0.0 + mass_list = _uniq(mass_list) + if mass_list[0] == 0.0: + mass_list.remove(0.0) + + # apply multiplication factor if given + if factor and factor != 1.0: + mass_list = [m * factor for m in mass_list] + + # reformat numbers + mass_list = _format(mass_list) + + # show the mass<>time table? + if showtable: + twas = 0.0 + logtwas = 0.0 + for i, m in enumerate(mass_list): + t = _time_from_mass(m) + logt = math.log10(t) + if twas > 0.0: + print( + "{i:4d} m={m:13g} t={t:13g} log10(t)={logt:13g} dt={dt:13g} dlog10(t)={dlogt:13g}".format( + i=i, m=m, t=t, logt=logt, dt=twas - t, dlogt=logtwas - logt + ) ) - ) - else: - print( - "{i:4d} m={m:13g} t={t:13g} log10(t)={logt:13g}".format( - i=i, m=m, t=t, logt=logt + else: + print( + "{i:4d} m={m:13g} t={t:13g} log10(t)={logt:13g}".format( + i=i, m=m, t=t, logt=logt + ) ) - ) - twas = t - logtwas = logt - exit() + twas = t + logtwas = logt + exit() - # return the mass list as a numpy array - mass_list = np.unique(np.array(mass_list)) + # return the mass list as a numpy array + mass_list = np.unique(np.array(mass_list)) - # perhaps log the masses - if logmasses: - mass_list = np.log(mass_list) - if log10masses: - mass_list = np.log10(mass_list) + # perhaps log the masses + if logmasses: + mass_list = np.log(mass_list) + if log10masses: + mass_list = np.log10(mass_list) - if showlist: + + + return mass_list + + # call _const_dt and return the mass_list + # + # Note: because _const_dt is cached to disk, calling it may + # use the cached result. + # + # Note: we send a sorted JSON string instead of the + # bse_options dict to make sure the order is preserved + mass_list = _const_dt(cachedir, + self.grid_options['num_cores'], + json.dumps(self.bse_options,sort_keys=True), + **kwargs + ) + cache.close() + + if kwargs.get('showlist',True): print("const_dt mass list ({} masses)\n".format(len(mass_list)), mass_list) return mass_list diff --git a/setup.py b/setup.py index 721dd841d81ef5ec0906bac3b2075e4a4602cb02..077adef0dbf84e2eec2a6aade9ad2a8e7046a6fd 100644 --- a/setup.py +++ b/setup.py @@ -265,6 +265,7 @@ setup( "colorama", "compress_pickle", "datasize", + "diskcache", "h5py", "halo", "humanize",