def contours( self, param_1, param_1_minimum, param_1_maximum, param_1_n_steps, param_2=None, param_2_minimum=None, param_2_maximum=None, param_2_n_steps=None, progress=True, **options ): """ Generate confidence contours for the given parameters by stepping for the given number of steps between the given boundaries. Call it specifying only source_1, param_1, param_1_minimum and param_1_maximum to generate the profile of the likelihood for parameter 1. Specify all parameters to obtain instead a 2d contour of param_1 vs param_2 :param param_1: name of the first parameter :param param_1_minimum: lower bound for the range for the first parameter :param param_1_maximum: upper bound for the range for the first parameter :param param_1_n_steps: number of steps for the first parameter :param param_2: name of the second parameter :param param_2_minimum: lower bound for the range for the second parameter :param param_2_maximum: upper bound for the range for the second parameter :param param_2_n_steps: number of steps for the second parameter :param progress: (True or False) whether to display progress or not :param log: by default the steps are taken linearly. With this optional parameter you can provide a tuple of booleans which specify whether the steps are to be taken logarithmically. For example, 'log=(True,False)' specify that the steps for the first parameter are to be taken logarithmically, while they are linear for the second parameter. If you are generating the profile for only one parameter, you can specify 'log=(True,)' or 'log=(False,)' (optional) :param: parallel: whether to use or not parallel computation (default:False) :return: a : an array corresponding to the steps for the first parameter b : an array corresponding to the steps for the second parameter (or None if stepping only in one direction) contour : a matrix of size param_1_steps x param_2_steps containing the value of the function at the corresponding points in the grid. If param_2_steps is None (only one parameter), then this reduces to an array of size param_1_steps. """ # Figure out if we are making a 1d or a 2d contour if param_2 is None: n_dimensions = 1 else: n_dimensions = 2 # Check the options p1log = False p2log = False parallel = False if "log" in options.keys(): assert len(options["log"]) == n_dimensions, ( "When specifying the 'log' option you have to provide a " + "boolean for each dimension you are stepping on." ) p1log = bool(options["log"][0]) if param_2 is not None: p2log = bool(options["log"][1]) if "parallel" in options.keys(): parallel = bool(options["parallel"]) # Generate the steps if p1log: param_1_steps = numpy.logspace(math.log10(param_1_minimum), math.log10(param_1_maximum), param_1_n_steps) else: param_1_steps = numpy.linspace(param_1_minimum, param_1_maximum, param_1_n_steps) if n_dimensions == 2: if p2log: param_2_steps = numpy.logspace( math.log10(param_2_minimum), math.log10(param_2_maximum), param_2_n_steps ) else: param_2_steps = numpy.linspace(param_2_minimum, param_2_maximum, param_2_n_steps) else: # Only one parameter to step through # Put param_2_steps as nan so that the worker can realize that it does not have # to step through it param_2_steps = numpy.array([numpy.nan]) # Generate the grid grid = cartesian([param_1_steps, param_2_steps]) # Define the worker which will compute the value of the function at a given point in the grid # Restore best fit self._restore_best_fit() # Duplicate the options used for the original minimizer new_args = dict(self.minuit.fitarg) # Get the minuit names for the parameters minuit_param_1 = self._parameter_name_to_minuit_name(param_1) if param_2 is None: minuit_param_2 = None else: minuit_param_2 = self._parameter_name_to_minuit_name(param_2) # Instance the worker contour_worker = ContourWorker( self._f, self.minuit.values, new_args, minuit_param_1, minuit_param_2, self.name_to_position ) # We are finally ready to do the computation # Serial and parallel computation are slightly different, so check whether we are in one case # or the other if not parallel: # Serial computation if progress: # Computation with progress bar progress_bar = ProgressBar(grid.shape[0]) # Define a wrapper which will increase the progress before as well as run the actual computation def wrap(args): results = contour_worker(args) progress_bar.increase() return results # Do the computation results = map(wrap, grid) else: # Computation without the progress bar results = map(contour_worker, grid) else: # Parallel computation # Connect to the engines client = ParallelClient(**options) # Get a balanced view of the engines load_balance_view = client.load_balanced_view() # Distribute the work among the engines and start it, but return immediately the control # to the main thread amr = load_balance_view.map_async(contour_worker, grid) # print progress n_points = grid.flatten().shape[0] progress = ProgressBar(n_points) # This loop will check from time to time the status of the computation, which is happening on # different threads, and update the progress bar while not amr.ready(): # Check and report the status of the computation every second time.sleep(1) # if (debug): # stdouts = amr.stdout # # # clear_output doesn't do much in terminal environments # for stdout, stderr in zip(amr.stdout, amr.stderr): # if stdout: # print "%s" % (stdout[-1000:]) # if stderr: # print "%s" % (stderr[-1000:]) # sys.stdout.flush() progress.animate(amr.progress - 1) # If there have been problems, here is where they will be raised results = amr.get() # Always display 100% at the end progress.animate(n_points) # Add a new line after the progress bar print("\n") # Return results return ( param_1_steps, param_2_steps, numpy.array(results).reshape((param_1_steps.shape[0], param_2_steps.shape[0])), )
def get_contours( self, param_1, param_1_minimum, param_1_maximum, param_1_n_steps, param_2=None, param_2_minimum=None, param_2_maximum=None, param_2_n_steps=None, progress=True, **options ): """ Generate confidence contours for the given parameters by stepping for the given number of steps between the given boundaries. Call it specifying only source_1, param_1, param_1_minimum and param_1_maximum to generate the profile of the likelihood for parameter 1. Specify all parameters to obtain instead a 2d contour of param_1 vs param_2. NOTE: if using parallel computation, param_1_n_steps must be an integer multiple of the number of running engines. If that is not the case, the code will reduce the number of steps to match that requirement, and issue a warning :param param_1: fully qualified name of the first parameter or parameter instance :param param_1_minimum: lower bound for the range for the first parameter :param param_1_maximum: upper bound for the range for the first parameter :param param_1_n_steps: number of steps for the first parameter :param param_2: fully qualified name of the second parameter or parameter instance :param param_2_minimum: lower bound for the range for the second parameter :param param_2_maximum: upper bound for the range for the second parameter :param param_2_n_steps: number of steps for the second parameter :param progress: (True or False) whether to display progress or not :param log: by default the steps are taken linearly. With this optional parameter you can provide a tuple of booleans which specify whether the steps are to be taken logarithmically. For example, 'log=(True,False)' specify that the steps for the first parameter are to be taken logarithmically, while they are linear for the second parameter. If you are generating the profile for only one parameter, you can specify 'log=(True,)' or 'log=(False,)' (optional) :return: a tuple containing an array corresponding to the steps for the first parameter, an array corresponding to the steps for the second parameter (or None if stepping only in one direction), a matrix of size param_1_steps x param_2_steps containing the value of the function at the corresponding points in the grid. If param_2_steps is None (only one parameter), then this reduces to an array of size param_1_steps. """ if hasattr(param_1, "value"): # Substitute with the name param_1 = param_1.path if hasattr(param_2, "value"): param_2 = param_2.path # Check that the parameters exist assert param_1 in self._likelihood_model.free_parameters, ( "Parameter %s is not a free parameters of the " "current model" % param_1 ) if param_2 is not None: assert param_2 in self._likelihood_model.free_parameters, ( "Parameter %s is not a free parameters of the " "current model" % param_2 ) # Check that we have a valid fit assert self._current_minimum is not None, "You have to run the .fit method before calling get_contours." # Then restore the best fit self._minimizer._restore_best_fit() # Check minimal assumptions about the procedure assert not (param_1 == param_2), "You have to specify two different parameters" assert param_1_minimum < param_1_maximum, "Minimum larger than maximum for parameter 1" if param_2 is not None: assert param_2_minimum < param_2_maximum, "Minimum larger than maximum for parameter 2" # Check whether we are parallelizing or not if not threeML_config["parallel"]["use-parallel"]: a, b, cc = self.minimizer.contours( param_1, param_1_minimum, param_1_maximum, param_1_n_steps, param_2, param_2_minimum, param_2_maximum, param_2_n_steps, progress, **options ) # Collapse the second dimension of the results if we are doing a 1d contour if param_2 is None: cc = cc[:, 0] else: # With parallel computation # In order to distribute fairly the computation, the strategy is to parallelize the computation # by assigning to the engines one "line" of the grid at the time # Connect to the engines client = ParallelClient(**options) # Get the number of engines n_engines = client.get_number_of_engines() # Check whether the number of threads is larger than the number of steps in the first direction if n_engines > param_1_n_steps: n_engines = int(param_1_n_steps) custom_warnings.warn( "The number of engines is larger than the number of steps. Using only %s engines." % n_engines, ReducingNumberOfThreads, ) # Check if the number of steps is divisible by the number # of threads, otherwise issue a warning and make it so if float(param_1_n_steps) % n_engines != 0: # Set the number of steps to an integer multiple of the engines # (note that // is the floor division, also called integer division) param_1_n_steps = (param_1_n_steps // n_engines) * n_engines custom_warnings.warn( "Number of steps is not a multiple of the number of threads. Reducing steps to %s" % param_1_n_steps, ReducingNumberOfSteps, ) # Compute the number of splits, i.e., how many lines in the grid for each engine. # (note that this is guaranteed to be an integer number after the previous checks) p1_split_steps = param_1_n_steps // n_engines # Prepare arrays for results if param_2 is None: # One array pcc = numpy.zeros(param_1_n_steps) pa = numpy.linspace(param_1_minimum, param_1_maximum, param_1_n_steps) pb = None else: pcc = numpy.zeros((param_1_n_steps, param_2_n_steps)) # Prepare the two axes of the parameter space pa = numpy.linspace(param_1_minimum, param_1_maximum, param_1_n_steps) pb = numpy.linspace(param_2_minimum, param_2_maximum, param_2_n_steps) # Define the parallel worker which will go through the computation # NOTE: I only divide # on the first parameter axis so that the different # threads are more or less well mixed for points close and # far from the best fit def worker(start_index): # Re-create the minimizer # backup_freeParameters = copy.deepcopy(self.freeParameters) this_minimizer = self.Minimizer(self.minus_log_like_profile, self._free_parameters) this_p1min = pa[start_index * p1_split_steps] this_p1max = pa[(start_index + 1) * p1_split_steps - 1] # print("From %s to %s" % (this_p1min, this_p1max)) aa, bb, ccc = this_minimizer.contours( param_1, this_p1min, this_p1max, p1_split_steps, param_2, param_2_minimum, param_2_maximum, param_2_n_steps, False, **options ) # self.freeParameters = backup_freeParameters return ccc # Get a balanced view of the engines lview = client.load_balanced_view() # lview.block = True # Distribute the work among the engines and start it, but return immediately the control # to the main thread amr = lview.map_async(worker, range(n_engines)) # print progress progress = ProgressBar(n_engines) # This loop will check from time to time the status of the computation, which is happening on # different threads, and update the progress bar while not amr.ready(): # Check and report the status of the computation every second time.sleep(1 + np.random.uniform(0, 1)) # if (debug): # stdouts = amr.stdout # # # clear_output doesn't do much in terminal environments # for stdout, stderr in zip(amr.stdout, amr.stderr): # if stdout: # print "%s" % (stdout[-1000:]) # if stderr: # print "%s" % (stderr[-1000:]) # sys.stdout.flush() progress.animate(amr.progress - 1) # Always display 100% at the end progress.animate(n_engines - 1) # Add a new line after the progress bar print("\n") # print("Serial time: %1.f (speed-up: %.1f)" %(amr.serial_time, float(amr.serial_time) / amr.wall_time)) # Get the results. This will raise exceptions if something wrong happened during the computation. # We don't catch it so that the user will be aware of that res = amr.get() # Now re-assemble the vector of results taking the different parts from the engines for i in range(n_engines): if param_2 is None: pcc[i * p1_split_steps : (i + 1) * p1_split_steps] = res[i][:, 0] else: pcc[i * p1_split_steps : (i + 1) * p1_split_steps, :] = res[i] # Give the results the names that the following code expect. These are kept separate for debugging # purposes cc = pcc a = pa b = pb # Here we have done the computation, in parallel computation or not. Let's make the plot # with the contour if param_2 is not None: # 2d contour fig = self._plot_contours("%s" % (param_1), a, "%s" % (param_2,), b, cc) else: # 1d contour (i.e., a profile) fig = self._plot_profile("%s" % (param_1), a, cc) # Check if we found a better minimum. This shouldn't happen, but in case of very difficult fit # it might. if self._current_minimum - cc.min() > 0.1: if param_2 is not None: idx = cc.argmin() aidx, bidx = numpy.unravel_index(idx, cc.shape) print( "\nFound a better minimum: %s with %s = %s and %s = %s. Run again your fit starting from here." % (cc.min(), param_1, a[aidx], param_2, b[bidx]) ) else: idx = cc.argmin() print( "Found a better minimum: %s with %s = %s. Run again your fit starting from here." % (cc.min(), param_1, a[idx]) ) return a, b, cc, fig