From 0c9ef74c1c56a1908db1d052036e5e792ce2146a Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:50:18 +0100 Subject: [PATCH 01/35] Include pymc in the documentation. (#1305) * Include pymc in the documentation. * Moved Parameter description to correct place at init * Moved extra information of text upwards. * Added __all__ definition to pymc * Update pypesto/sample/pymc.py Co-authored-by: Daniel Weindl * Moved back docstring to attributes --------- Co-authored-by: Daniel Weindl --- doc/api.rst | 1 + pypesto/result/sample.py | 10 +++++----- pypesto/sample/pymc.py | 7 +++++++ setup.cfg | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index b99b9dfd3..cd716c538 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -24,6 +24,7 @@ API reference pypesto.profile.profile_next_guess pypesto.result pypesto.sample + pypesto.sample.pymc pypesto.select pypesto.select.postprocessors pypesto.startpoint diff --git a/pypesto/result/sample.py b/pypesto/result/sample.py index 13d277af7..6d18a2fad 100644 --- a/pypesto/result/sample.py +++ b/pypesto/result/sample.py @@ -9,9 +9,11 @@ class McmcPtResult(dict): """ The result of a sampler run using Markov-chain Monte Carlo. - Currently result object of all supported samplers. Can be used like a dict. + Here, `n_chain` denotes the number of chains, `n_iter` the number of + iterations (i.e., the chain length), and `n_par` the number of + parameters. - Parameters + Attributes ---------- trace_x: [n_chain, n_iter, n_par] Parameters. @@ -31,9 +33,6 @@ class McmcPtResult(dict): The estimated effective sample size. message: str Textual comment on the profile result. - - Here, `n_chain` denotes the number of chains, `n_iter` the number of - iterations (i.e., the chain length), and `n_par` the number of parameters. """ def __init__( @@ -48,6 +47,7 @@ def __init__( effective_sample_size: float = None, message: str = None, ): + """Initialize the McmcPtResult.""" super().__init__() self.trace_x = trace_x diff --git a/pypesto/sample/pymc.py b/pypesto/sample/pymc.py index 762329b67..89a2ac252 100644 --- a/pypesto/sample/pymc.py +++ b/pypesto/sample/pymc.py @@ -18,6 +18,13 @@ logger = logging.getLogger(__name__) +__all__ = [ + 'PymcSampler', + 'PymcObjectiveWithGradientOp', + 'PymcObjectiveOp', + 'PymcGradientOp', +] + # implementation based on: # https://www.pymc.io/projects/examples/en/latest/case_studies/blackbox_external_likelihood_numpy.html diff --git a/setup.cfg b/setup.cfg index 3f51e1851..714f1ebec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -150,6 +150,7 @@ doc = %(petab)s %(aesara)s %(jax)s + %(pymc)s example = %(julia)s %(pymc)s From 312f43a6c21d543ad31e0d104a8384eeebc1826b Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:25:43 +0100 Subject: [PATCH 02/35] fix knot initialization (#1313) --- .../hierarchical/semiquantitative/solver.py | 5 +-- pypesto/visualize/spline_approximation.py | 36 ++++++++----------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index 3527c4863..f3aa8b28b 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -1086,11 +1086,8 @@ def save_inner_parameters_to_inner_problem( group ) - lower_trian = np.tril(np.ones((len(s), len(s)))) - xi = np.dot(lower_trian, s) - for idx in range(len(inner_spline_parameters)): - inner_spline_parameters[idx].value = xi[idx] + inner_spline_parameters[idx].value = s[idx] sigma = group_dict[INNER_NOISE_PARS] diff --git a/pypesto/visualize/spline_approximation.py b/pypesto/visualize/spline_approximation.py index 7d73711d2..950cd6e77 100644 --- a/pypesto/visualize/spline_approximation.py +++ b/pypesto/visualize/spline_approximation.py @@ -185,11 +185,12 @@ def plot_splines_from_inner_result( group_idx = list(inner_problem.groups.keys()).index(group) # For each group get the inner parameters and simulation - xs = inner_problem.get_xs_for_group(group) - s = result[SCIPY_X] - inner_parameters = np.array([x.value for x in xs]) + # Utility matrix for the spline knot calculation + lower_trian = np.tril(np.ones((len(s), len(s)))) + spline_knots = np.dot(lower_trian, s) + measurements = inner_problem.groups[group][DATAPOINTS] simulation = inner_problem.groups[group][CURRENT_SIMULATION] @@ -201,22 +202,22 @@ def plot_splines_from_inner_result( ) = SemiquantInnerSolver._rescale_spline_bases( self=None, sim_all=simulation, - N=len(inner_parameters), + N=len(spline_knots), K=len(simulation), ) mapped_simulations = get_spline_mapped_simulations( - s, simulation, len(inner_parameters), delta_c, spline_bases, n + s, simulation, len(spline_knots), delta_c, spline_bases, n ) axs[group_idx].plot( simulation, measurements, 'bs', label='Measurements' ) axs[group_idx].plot( - spline_bases, inner_parameters, 'g.', label='Spline knots' + spline_bases, spline_knots, 'g.', label='Spline knots' ) axs[group_idx].plot( spline_bases, - inner_parameters, + spline_knots, linestyle='-', color='g', label='Spline function', @@ -224,7 +225,7 @@ def plot_splines_from_inner_result( if inner_solver.options[REGULARIZE_SPLINE]: alpha_opt, beta_opt = _calculate_optimal_regularization( s=s, - N=len(inner_parameters), + N=len(spline_knots), c=spline_bases, ) axs[group_idx].plot( @@ -392,10 +393,7 @@ def _add_spline_mapped_simulations_to_model_fit( ][0] # Get the inner parameters and simulation. - xs = inner_problem.get_xs_for_group(group) s = inner_result[SCIPY_X] - - inner_parameters = np.array([x.value for x in xs]) simulation = inner_problem.groups[group][CURRENT_SIMULATION] # For the simulation, get the spline bases @@ -406,12 +404,12 @@ def _add_spline_mapped_simulations_to_model_fit( ) = SemiquantInnerSolver._rescale_spline_bases( self=None, sim_all=simulation, - N=len(inner_parameters), + N=len(s), K=len(simulation), ) # and the spline-mapped simulations. mapped_simulations = get_spline_mapped_simulations( - s, simulation, len(inner_parameters), delta_c, spline_bases, n + s, simulation, len(s), delta_c, spline_bases, n ) # Plot the spline-mapped simulations to the ax with same color @@ -536,29 +534,25 @@ def _obtain_regularization_for_start( # for each result and group, plot the inner solution for result, group in zip(inner_results, inner_problem.groups): # For each group get the inner parameters and simulation - xs = inner_problem.get_xs_for_group(group) - s = result[SCIPY_X] - - inner_parameters = np.array([x.value for x in xs]) simulation = inner_problem.groups[group][CURRENT_SIMULATION] # For the simulation, get the spline bases ( - delta_c, + _, spline_bases, - n, + _, ) = SemiquantInnerSolver._rescale_spline_bases( self=None, sim_all=simulation, - N=len(inner_parameters), + N=len(s), K=len(simulation), ) if inner_solver.options[REGULARIZE_SPLINE]: reg_term = _calculate_regularization_for_group( s=s, - N=len(inner_parameters), + N=len(s), c=spline_bases, regularization_factor=inner_solver.options[ 'regularization_factor' From f9231f2c8015c38fefcc53b17bf00ff94fc12ad4 Mon Sep 17 00:00:00 2001 From: Simon Merkt <49190262+MerktSimon@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:11:24 +0100 Subject: [PATCH 03/35] Support ipopt with gradient approximation (#1310) * add approx grad ipopt option --------- Co-authored-by: Simon Merkt Co-authored-by: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> --- pypesto/optimize/optimizer.py | 196 +++++++++++++++------------- test/optimize/test_optimize.py | 232 +++++++++++++++++++-------------- 2 files changed, 241 insertions(+), 187 deletions(-) diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index 166020958..6d623f950 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -133,7 +133,7 @@ def wrapped_minimize( trace = "\n".join(traceback.format_exception(*sys.exc_info())) - logger.error(f'start {id} failed:\n{trace}') + logger.error(f"start {id} failed:\n{trace}") result = OptimizerResult( x0=x0, exitflag=-1, message=str(err), id=id ) @@ -320,7 +320,7 @@ class ScipyOptimizer(Optimizer): def __init__( self, - method: str = 'L-BFGS-B', + method: str = "L-BFGS-B", tol: float = None, options: Dict = None, ): @@ -372,18 +372,18 @@ def minimize( bounds = (lb, ub) fun = objective.get_res - jac = objective.get_sres if objective.has_sres else '2-point' + jac = objective.get_sres if objective.has_sres else "2-point" # TODO: pass jac computing methods in options if self.options is not None: ls_options = self.options.copy() - ls_options['verbose'] = ( + ls_options["verbose"] = ( 2 - if 'disp' in ls_options.keys() and ls_options['disp'] + if "disp" in ls_options.keys() and ls_options["disp"] else 0 ) - ls_options.pop('disp', None) - ls_options['max_nfev'] = ls_options.pop('maxfun', None) + ls_options.pop("disp", None) + ls_options["max_nfev"] = ls_options.pop("maxfun", None) else: ls_options = {} @@ -395,15 +395,15 @@ def minimize( jac=jac, bounds=bounds, tr_solver=ls_options.pop( - 'tr_solver', 'lsmr' if len(x0) > 1 else 'exact' + "tr_solver", "lsmr" if len(x0) > 1 else "exact" ), - loss='linear', + loss="linear", ftol=tol, **ls_options, ) # extract fval/grad from result, note that fval is not available # from least squares solvers - grad = getattr(res, 'grad', None) + grad = getattr(res, "grad", None) fval = None else: # is an fval based optimization method @@ -421,38 +421,38 @@ def minimize( # 'dogleg', 'trust-ncg'] # TODO: is it more efficient to have tuple as output of fun? method_supports_grad = self.method.lower() in [ - 'cg', - 'bfgs', - 'newton-cg', - 'l-bfgs-b', - 'tnc', - 'slsqp', - 'dogleg', - 'trust-ncg', - 'trust-krylov', - 'trust-exact', - 'trust-constr', + "cg", + "bfgs", + "newton-cg", + "l-bfgs-b", + "tnc", + "slsqp", + "dogleg", + "trust-ncg", + "trust-krylov", + "trust-exact", + "trust-constr", ] method_supports_hess = self.method.lower() in [ - 'newton-cg', - 'dogleg', - 'trust-ncg', - 'trust-krylov', - 'trust-exact', - 'trust-constr', + "newton-cg", + "dogleg", + "trust-ncg", + "trust-krylov", + "trust-exact", + "trust-constr", ] method_supports_hessp = self.method.lower() in [ - 'newton-cg', - 'trust-ncg', - 'trust-krylov', - 'trust-constr', + "newton-cg", + "trust-ncg", + "trust-krylov", + "trust-constr", ] # switch off passing over functions if not applicable (e.g. # NegLogParameterPrior) since grad/hess attributes do not exist if not isinstance(objective, Objective): - if not hasattr(objective, 'grad'): + if not hasattr(objective, "grad"): objective.grad = False - if not hasattr(objective, 'hess'): + if not hasattr(objective, "hess"): objective.hess = False # Todo Resolve warning by implementing saving of hess temporarily # in objective and pass to scipy seperately @@ -506,7 +506,7 @@ def fun(x): tol=self.tol, ) # extract fval/grad from result - grad = getattr(res, 'jac', None) + grad = getattr(res, "jac", None) fval = res.fun # fill in everything known, although some parts will be overwritten @@ -514,7 +514,7 @@ def fun(x): x=np.array(res.x), fval=fval, grad=grad, - hess=getattr(res, 'hess', None), + hess=getattr(res, "hess", None), exitflag=res.status, message=res.message, ) @@ -523,17 +523,17 @@ def fun(x): def is_least_squares(self): """Check whether optimizer is a least squares optimizer.""" - return re.match(r'(?i)^(ls_)', self.method) + return re.match(r"(?i)^(ls_)", self.method) def get_default_options(self): """Create default options specific for the optimizer.""" - options = {'disp': False} + options = {"disp": False} if self.is_least_squares(): - options['max_nfev'] = 1000 - elif self.method.lower() in ('l-bfgs-b', 'tnc'): - options['maxfun'] = 1000 - elif self.method.lower() in ('nelder-mead', 'powell'): - options['maxfev'] = 1000 + options["max_nfev"] = 1000 + elif self.method.lower() in ("l-bfgs-b", "tnc"): + options["maxfun"] = 1000 + elif self.method.lower() in ("nelder-mead", "powell"): + options["maxfev"] = 1000 return options @@ -547,9 +547,13 @@ def __init__(self, options: Dict = None): Parameters ---------- options: - Options are directly passed on to `cyipopt.minimize_ipopt`. + Options are directly passed on to `cyipopt.minimize_ipopt`, except + for the `approx_grad` option, which is handled separately. """ super().__init__() + self.approx_grad = False + if (options is not None) and "approx_grad" in options: + self.approx_grad = options.pop("approx_grad") self.options = options def __repr__(self) -> str: @@ -578,11 +582,21 @@ def minimize( bounds = np.array([problem.lb, problem.ub]).T + if self.approx_grad: + jac = None + elif objective.has_grad: + jac = objective.get_grad + else: + raise ValueError( + "For IPOPT, the objective must either be able to return " + "gradients or the `approx_grad` must be set to True." + ) + ret = cyipopt.minimize_ipopt( fun=objective.get_fval, x0=x0, method=None, # ipopt does not use this argument for anything - jac=objective.get_grad, + jac=jac, hess=None, # ipopt does not support Hessian yet hessp=None, # ipopt does not support Hessian vector product yet bounds=bounds, @@ -609,8 +623,8 @@ def __init__(self, options: Dict = None): self.options = options if self.options is None: self.options = DlibOptimizer.get_default_options(self) - elif 'maxiter' not in self.options: - raise KeyError('Dlib options are missing the key word ' 'maxiter.') + elif "maxiter" not in self.options: + raise KeyError("Dlib options are missing the key word " "maxiter.") def __repr__(self) -> str: rep = f"<{self.__class__.__name__}" @@ -653,7 +667,7 @@ def get_fval_vararg(*x): get_fval_vararg, list(lb), list(ub), - int(self.options['maxiter']), + int(self.options["maxiter"]), 0.002, ) @@ -667,7 +681,7 @@ def is_least_squares(self): def get_default_options(self): """Create default options specific for the optimizer.""" - return {'maxiter': 10000} + return {"maxiter": 10000} def check_x0_support(self, x_guesses: np.ndarray = None) -> bool: """Check whether optimizer supports x0.""" @@ -683,7 +697,7 @@ def __init__(self, options: Dict = None): super().__init__() if options is None: - options = {'maxiter': 200} + options = {"maxiter": 200} self.options = options def __repr__(self) -> str: @@ -756,7 +770,7 @@ def __init__(self, par_sigma0: float = 0.25, options: Dict = None): super().__init__() if options is None: - options = {'maxiter': 10000} + options = {"maxiter": 10000} self.options = options self.par_sigma0 = par_sigma0 @@ -783,7 +797,7 @@ def minimize( check_finite_bounds(lb, ub) sigma0 = self.par_sigma0 * np.median(ub - lb) - self.options['bounds'] = [lb, ub] + self.options["bounds"] = [lb, ub] try: import cma @@ -838,7 +852,7 @@ def __init__(self, options: Dict = None): super().__init__() if options is None: - options = {'maxiter': 100} + options = {"maxiter": 100} self.options = options def __repr__(self) -> str: @@ -908,7 +922,7 @@ class PyswarmsOptimizer(Optimizer): def __init__(self, par_popsize: float = 10, options: Dict = None): super().__init__() - all_options = {'maxiter': 1000, 'c1': 0.5, 'c2': 0.3, 'w': 0.9} + all_options = {"maxiter": 1000, "c1": 0.5, "c2": 0.3, "w": 0.9} if options is None: options = {} all_options.update(options) @@ -979,7 +993,7 @@ def successively_working_fval(swarm: np.ndarray) -> np.ndarray: cost, pos = optimizer.optimize( successively_working_fval, - iters=self.options['maxiter'], + iters=self.options["maxiter"], verbose=False, ) @@ -1036,8 +1050,8 @@ def __init__( if options is None: options = {} - elif 'maxiter' in options: - options['maxeval'] = options.pop('maxiter') + elif "maxiter" in options: + options["maxeval"] = options.pop("maxiter") if local_options is None: local_options = {} self.options = options @@ -1066,7 +1080,7 @@ def __init__( if local_method is not None and method not in needs_local_method: raise ValueError( f'Method "{method}" does not allow a local ' - f'method. Please set `local_method` to None.' + f"method. Please set `local_method` to None." ) self.local_methods = [ @@ -1115,7 +1129,7 @@ def __init__( if method not in methods: raise ValueError( f'"{method}" is not a valid method. Valid ' - f'methods are: {methods}' + f"methods are: {methods}" ) self.method = method @@ -1123,7 +1137,7 @@ def __init__( if local_method is not None and local_method not in self.local_methods: raise ValueError( f'"{local_method}" is not a valid method. Valid ' - f'methods are: {self.local_methods}' + f"methods are: {self.local_methods}" ) self.local_method = local_method @@ -1154,15 +1168,15 @@ def minimize( opt = nlopt.opt(self.method, problem.dim) valid_options = [ - 'ftol_abs', - 'ftol_rel', - 'xtol_abs', - 'xtol_rel', - 'stopval', - 'x_weights', - 'maxeval', - 'maxtime', - 'initial_step', + "ftol_abs", + "ftol_rel", + "xtol_abs", + "xtol_rel", + "stopval", + "x_weights", + "maxeval", + "maxtime", + "initial_step", ] def set_options(o, options): @@ -1170,9 +1184,9 @@ def set_options(o, options): if option not in valid_options: raise ValueError( f'"{option}" is not a valid option. Valid ' - f'options are: {valid_options}' + f"options are: {valid_options}" ) - getattr(o, f'set_{option}')(value) + getattr(o, f"set_{option}")(value) if self.local_method is not None: local_opt = nlopt.opt(self.local_method, problem.dim) @@ -1200,7 +1214,7 @@ def nlopt_objective(x, grad): set_options(opt, self.options) try: result = opt.optimize(x0) - msg = 'Finished Successfully.' + msg = "Finished Successfully." except ( nlopt.RoundoffLimited, nlopt.ForcedStop, @@ -1260,7 +1274,7 @@ def __init__( self, hessian_update: Optional[ fides.hessian_approximation.HessianApproximation - ] = 'default', + ] = "default", options: Optional[Dict] = None, verbose: Optional[int] = logging.INFO, ): @@ -1286,16 +1300,16 @@ def __init__( if ( (hessian_update is not None) - and (hessian_update != 'default') + and (hessian_update != "default") and not isinstance( hessian_update, fides.hessian_approximation.HessianApproximation, ) ): raise ValueError( - 'Incompatible type for hessian update. ' - 'Must be a HessianApproximation, ' - f'was {type(hessian_update)}.' + "Incompatible type for hessian update. " + "Must be a HessianApproximation, " + f"was {type(hessian_update)}." ) if options is None: @@ -1309,7 +1323,7 @@ def __repr__(self) -> str: rep = f"<{self.__class__.__name__} " # print everything that is customized if self.hessian_update is not None: - if self.hessian_update == 'default': + if self.hessian_update == "default": rep += f" hessian_update={self.hessian_update}" else: rep += ( @@ -1334,13 +1348,13 @@ def minimize( """Perform optimization. Parameters: see `Optimizer` documentation.""" import fides - if self.hessian_update == 'default': + if self.hessian_update == "default": if not problem.objective.has_hess: warnings.warn( - 'Fides is using BFGS as hessian approximation, ' - 'as the problem does not provide a Hessian. ' - 'Specify a Hessian to use a more efficient ' - 'hybrid approximation scheme.' + "Fides is using BFGS as hessian approximation, " + "as the problem does not provide a Hessian. " + "Specify a Hessian to use a more efficient " + "hybrid approximation scheme." ) _hessian_update = fides.BFGS() else: @@ -1354,13 +1368,13 @@ def minimize( else False ) - args = {'mode': MODE_RES if resfun else MODE_FUN} + args = {"mode": MODE_RES if resfun else MODE_FUN} if not problem.objective.has_grad: raise ValueError( - 'Fides cannot be applied to problems ' - 'with objectives that do not support ' - 'gradient evaluation.' + "Fides cannot be applied to problems " + "with objectives that do not support " + "gradient evaluation." ) if _hessian_update is None or ( @@ -1368,13 +1382,13 @@ def minimize( ): if not problem.objective.has_hess: raise ValueError( - 'Specified hessian update scheme cannot be ' - 'used with objectives that do not support ' - 'Hessian computation.' + "Specified hessian update scheme cannot be " + "used with objectives that do not support " + "Hessian computation." ) - args['sensi_orders'] = (0, 1, 2) + args["sensi_orders"] = (0, 1, 2) else: - args['sensi_orders'] = (0, 1) + args["sensi_orders"] = (0, 1) opt = fides.Optimizer( fun=problem.objective, diff --git a/test/optimize/test_optimize.py b/test/optimize/test_optimize.py index 00b160314..509192309 100644 --- a/test/optimize/test_optimize.py +++ b/test/optimize/test_optimize.py @@ -32,13 +32,13 @@ from ..util import CRProblem, rosen_for_sensi -@pytest.fixture(params=['cr', 'rosen-integrated', 'rosen-separated']) +@pytest.fixture(params=["cr", "rosen-integrated", "rosen-separated"]) def problem(request) -> pypesto.Problem: - if request.param == 'cr': + if request.param == "cr": return CRProblem().get_problem() - elif 'rosen' in request.param: - integrated = 'integrated' in request.param - obj = rosen_for_sensi(max_sensi_order=2, integrated=integrated)['obj'] + elif "rosen" in request.param: + integrated = "integrated" in request.param + obj = rosen_for_sensi(max_sensi_order=2, integrated=integrated)["obj"] lb = 0 * np.ones((1, 2)) ub = 1 * np.ones((1, 2)) return pypesto.Problem(objective=obj, lb=lb, ub=ub) @@ -48,35 +48,35 @@ def problem(request) -> pypesto.Problem: optimizers = [ *[ - ('scipy', method) + ("scipy", method) for method in [ - 'Nelder-Mead', - 'Powell', - 'CG', - 'BFGS', - 'dogleg', - 'Newton-CG', - 'L-BFGS-B', - 'TNC', - 'COBYLA', - 'SLSQP', - 'trust-constr', - 'trust-ncg', - 'trust-exact', - 'trust-krylov', - 'ls_trf', - 'ls_dogbox', + "Nelder-Mead", + "Powell", + "CG", + "BFGS", + "dogleg", + "Newton-CG", + "L-BFGS-B", + "TNC", + "COBYLA", + "SLSQP", + "trust-constr", + "trust-ncg", + "trust-exact", + "trust-krylov", + "ls_trf", + "ls_dogbox", ] ], # disabled: 'ls_lm' (ValueError when passing bounds) - ('ipopt', ''), - ('dlib', ''), - ('pyswarm', ''), - ('cmaes', ''), - ('scipydiffevolopt', ''), - ('pyswarms', ''), + ("ipopt", ""), + ("dlib", ""), + ("pyswarm", ""), + ("cmaes", ""), + ("scipydiffevolopt", ""), + ("pyswarms", ""), *[ - ('nlopt', method) + ("nlopt", method) for method in [ nlopt.LD_VAR1, nlopt.LD_VAR2, @@ -117,7 +117,7 @@ def problem(request) -> pypesto.Problem: ] ], *[ - ('fides', solver) + ("fides", solver) for solver in itt.product( [ None, @@ -172,7 +172,7 @@ def test_unbounded_minimize(optimizer): ub_init = 1.11 * np.ones((1, 2)) ub = np.inf * np.ones(ub_init.shape) problem = pypesto.Problem( - rosen_for_sensi(max_sensi_order=2)['obj'], + rosen_for_sensi(max_sensi_order=2)["obj"], lb, ub, lb_init=lb_init, @@ -183,17 +183,17 @@ def test_unbounded_minimize(optimizer): options = optimize.OptimizeOptions(allow_failed_starts=False) # check whether the optimizer is least squares - if isinstance(optimizer[1], str) and re.match(r'(?i)^(ls_)', optimizer[1]): + if isinstance(optimizer[1], str) and re.match(r"(?i)^(ls_)", optimizer[1]): return if optimizer in [ - ('dlib', ''), - ('pyswarm', ''), - ('cmaes', ''), - ('scipydiffevolopt', ''), - ('pyswarms', ''), + ("dlib", ""), + ("pyswarm", ""), + ("cmaes", ""), + ("scipydiffevolopt", ""), + ("pyswarms", ""), *[ - ('nlopt', method) + ("nlopt", method) for method in [ nlopt.GN_ESCH, nlopt.GN_ISRES, @@ -236,44 +236,44 @@ def test_unbounded_minimize(optimizer): ) # check that ub/lb were reverted - assert isinstance(result.optimize_result.list[0]['fval'], float) - if optimizer not in [('scipy', 'ls_trf'), ('scipy', 'ls_dogbox')]: - assert np.isfinite(result.optimize_result.list[0]['fval']) - assert result.optimize_result.list[0]['x'] is not None + assert isinstance(result.optimize_result.list[0]["fval"], float) + if optimizer not in [("scipy", "ls_trf"), ("scipy", "ls_dogbox")]: + assert np.isfinite(result.optimize_result.list[0]["fval"]) + assert result.optimize_result.list[0]["x"] is not None # check that result is not in bounds, optimum is at (1,1), so you would # hope that any reasonable optimizer manage to finish with x < ub, # but I guess some are pretty terrible - assert np.any(result.optimize_result.list[0]['x'] < lb_init) or np.any( - result.optimize_result.list[0]['x'] > ub_init + assert np.any(result.optimize_result.list[0]["x"] < lb_init) or np.any( + result.optimize_result.list[0]["x"] > ub_init ) def get_optimizer(library, solver): """Constructs Optimizer given and optimization library and optimization solver specification""" - options = {'maxiter': 100} + options = {"maxiter": 100} - if library == 'scipy': + if library == "scipy": if solver == "TNC" or solver.startswith("ls_"): - options['maxfun'] = options.pop('maxiter') + options["maxfun"] = options.pop("maxiter") optimizer = optimize.ScipyOptimizer(method=solver, options=options) - elif library == 'ipopt': + elif library == "ipopt": optimizer = optimize.IpoptOptimizer() - elif library == 'dlib': + elif library == "dlib": optimizer = optimize.DlibOptimizer(options=options) - elif library == 'pyswarm': + elif library == "pyswarm": optimizer = optimize.PyswarmOptimizer(options=options) - elif library == 'cmaes': + elif library == "cmaes": optimizer = optimize.CmaesOptimizer(options=options) - elif library == 'scipydiffevolopt': + elif library == "scipydiffevolopt": optimizer = optimize.ScipyDifferentialEvolutionOptimizer( options=options ) - elif library == 'pyswarms': + elif library == "pyswarms": optimizer = optimize.PyswarmsOptimizer(options=options) - elif library == 'nlopt': + elif library == "nlopt": optimizer = optimize.NLoptOptimizer(method=solver, options=options) - elif library == 'fides': + elif library == "fides": options[fides.Options.SUBSPACE_DIM] = solver[1] optimizer = optimize.FidesOptimizer( options=options, hessian_update=solver[0], verbose=40 @@ -301,12 +301,12 @@ def check_minimize(problem, library, solver, allow_failed_starts=False): progress_bar=False, ) - assert isinstance(result.optimize_result.list[0]['fval'], float) + assert isinstance(result.optimize_result.list[0]["fval"], float) if (library, solver) not in [ - ('nlopt', nlopt.GD_STOGO_RAND) # id 9, fails in 40% of cases + ("nlopt", nlopt.GD_STOGO_RAND) # id 9, fails in 40% of cases ]: - assert np.isfinite(result.optimize_result.list[0]['fval']) - assert result.optimize_result.list[0]['x'] is not None + assert np.isfinite(result.optimize_result.list[0]["fval"]) + assert result.optimize_result.list[0]["x"] is not None def test_trim_results(problem): @@ -318,7 +318,7 @@ def test_trim_results(problem): report_hess=False, report_sres=False ) prob = pypesto.Problem( - objective=rosen_for_sensi(max_sensi_order=2)['obj'], + objective=rosen_for_sensi(max_sensi_order=2)["obj"], lb=0 * np.ones((1, 2)), ub=1 * np.ones((1, 2)), ) @@ -336,7 +336,7 @@ def test_trim_results(problem): assert result.optimize_result.list[0].hess is None # sres - optimizer = optimize.ScipyOptimizer(method='ls_trf') + optimizer = optimize.ScipyOptimizer(method="ls_trf") result = optimize.minimize( problem=prob, optimizer=optimizer, @@ -358,19 +358,19 @@ def test_mpipoolengine(): # run the example file. subprocess.check_call( # noqa: S603,S607 [ - 'mpiexec', - '--oversubscribe', - '-np', - '2', - 'python', - '-m', - 'mpi4py.futures', - f'{path}/../../doc/example/example_MPIPool.py', + "mpiexec", + "--oversubscribe", + "-np", + "2", + "python", + "-m", + "mpi4py.futures", + f"{path}/../../doc/example/example_MPIPool.py", ] ) # read results - result1 = read_result('temp_result.h5', problem=True, optimize=True) + result1 = read_result("temp_result.h5", problem=True, optimize=True) # set optimizer optimizer = optimize.FidesOptimizer(verbose=40) # initialize problem with x_guesses and objective @@ -380,7 +380,7 @@ def test_mpipoolengine(): hess=sp.optimize.rosen_hess, ) x_guesses = np.array( - [result1.optimize_result.list[i]['x0'] for i in range(2)] + [result1.optimize_result.list[i]["x0"] for i in range(2)] ) problem = pypesto.Problem( objective=objective, @@ -398,16 +398,16 @@ def test_mpipoolengine(): for ix in range(2): assert_almost_equal( - result1.optimize_result.list[ix]['x'], - result2.optimize_result.list[ix]['x'], - err_msg='The final parameter values ' - 'do not agree for the engines.', + result1.optimize_result.list[ix]["x"], + result2.optimize_result.list[ix]["x"], + err_msg="The final parameter values " + "do not agree for the engines.", ) finally: - if os.path.exists('temp_result.h5'): + if os.path.exists("temp_result.h5"): # delete data - os.remove('temp_result.h5') + os.remove("temp_result.h5") def test_history_beats_optimizer(): @@ -437,20 +437,20 @@ def test_history_beats_optimizer(): for result in (result_hist, result_opt): # number of function evaluations - assert result.optimize_result.list[0]['n_fval'] <= max_fval + 1 + assert result.optimize_result.list[0]["n_fval"] <= max_fval + 1 # optimal value in bounds - assert np.all(problem.lb <= result.optimize_result.list[0]['x']) - assert np.all(problem.ub >= result.optimize_result.list[0]['x']) + assert np.all(problem.lb <= result.optimize_result.list[0]["x"]) + assert np.all(problem.ub >= result.optimize_result.list[0]["x"]) # entries filled - for key in ('fval', 'x', 'grad'): + for key in ("fval", "x", "grad"): val = result.optimize_result.list[0][key] assert val is not None and np.all(np.isfinite(val)) # TNC funnily reports the last value if not converged # (this may break if their implementation is changed at some point ...) assert ( - result_hist.optimize_result.list[0]['fval'] - < result_opt.optimize_result.list[0]['fval'] + result_hist.optimize_result.list[0]["fval"] + < result_opt.optimize_result.list[0]["fval"] ) @@ -476,15 +476,15 @@ def test_ess(problem, local_optimizer, ess_type, request): ) elif ess_type == "cess": if ( - 'cr' in request.node.callspec.id - or 'integrated' in request.node.callspec.id + "cr" in request.node.callspec.id + or "integrated" in request.node.callspec.id ): # Not pickleable - incompatible with CESS pytest.skip() # CESS with 4 processes ess_init_args = get_default_ess_options(num_workers=4, dim=problem.dim) for x in ess_init_args: - x['local_optimizer'] = local_optimizer + x["local_optimizer"] = local_optimizer ess = CESSOptimizer( ess_init_args=ess_init_args, max_iter=5, @@ -492,8 +492,8 @@ def test_ess(problem, local_optimizer, ess_type, request): ) elif ess_type == "sacess": if ( - 'cr' in request.node.callspec.id - or 'integrated' in request.node.callspec.id + "cr" in request.node.callspec.id + or "integrated" in request.node.callspec.id ): # Not pickleable - incompatible with CESS pytest.skip() @@ -504,7 +504,7 @@ def test_ess(problem, local_optimizer, ess_type, request): num_workers=12, dim=problem.dim ) for x in ess_init_args: - x['local_optimizer'] = local_optimizer + x["local_optimizer"] = local_optimizer ess = SacessOptimizer( max_walltime_s=1, sacess_loglevel=logging.DEBUG, @@ -521,11 +521,11 @@ def test_ess(problem, local_optimizer, ess_type, request): print("ESS result: ", res.summary()) # best values roughly: cr: 4.701; rosen 7.592e-10 - if 'rosen' in request.node.callspec.id: + if "rosen" in request.node.callspec.id: if local_optimizer: assert res.optimize_result[0].fval < 1e-4 assert res.optimize_result[0].fval < 1 - elif 'cr' in request.node.callspec.id: + elif "cr" in request.node.callspec.id: if local_optimizer: assert res.optimize_result[0].fval < 5 assert res.optimize_result[0].fval < 20 @@ -535,8 +535,8 @@ def test_ess(problem, local_optimizer, ess_type, request): def test_ess_multiprocess(problem, request): if ( - 'cr' in request.node.callspec.id - or 'integrated' in request.node.callspec.id + "cr" in request.node.callspec.id + or "integrated" in request.node.callspec.id ): # Not pickleable - incompatible with CESS pytest.skip() @@ -569,12 +569,12 @@ def test_ess_multiprocess(problem, request): def test_scipy_integrated_grad(): integrated = True - obj = rosen_for_sensi(max_sensi_order=2, integrated=integrated)['obj'] + obj = rosen_for_sensi(max_sensi_order=2, integrated=integrated)["obj"] lb = 0 * np.ones((1, 2)) ub = 1 * np.ones((1, 2)) x_guesses = [[0.5, 0.5]] problem = pypesto.Problem(objective=obj, lb=lb, ub=ub, x_guesses=x_guesses) - optimizer = optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) optimize_options = optimize.OptimizeOptions(allow_failed_starts=False) history_options = pypesto.HistoryOptions(trace_record=True) result = optimize.minimize( @@ -592,12 +592,52 @@ def test_scipy_integrated_grad(): ) +def test_ipopt_approx_grad(): + integrated = False + obj = rosen_for_sensi(max_sensi_order=0, integrated=integrated)["obj"] + lb = 0 * np.ones((1, 2)) + ub = 1 * np.ones((1, 2)) + x_guesses = [[0.5, 0.5]] + problem = pypesto.Problem(objective=obj, lb=lb, ub=ub, x_guesses=x_guesses) + optimizer = optimize.IpoptOptimizer( + options={"maxiter": 10, "approx_grad": True} + ) + optimize_options = optimize.OptimizeOptions(allow_failed_starts=False) + history_options = pypesto.HistoryOptions(trace_record=True) + result = optimize.minimize( + problem=problem, + optimizer=optimizer, + n_starts=1, + startpoint_method=pypesto.startpoint.uniform, + options=optimize_options, + history_options=history_options, + progress_bar=False, + ) + obj2 = rosen_for_sensi(max_sensi_order=1, integrated=integrated)["obj"] + problem2 = pypesto.Problem( + objective=obj2, lb=lb, ub=ub, x_guesses=x_guesses + ) + optimizer2 = optimize.IpoptOptimizer(options={"maxiter": 10}) + result2 = optimize.minimize( + problem=problem2, + optimizer=optimizer2, + n_starts=1, + startpoint_method=pypesto.startpoint.uniform, + options=optimize_options, + history_options=history_options, + progress_bar=False, + ) + np.testing.assert_array_almost_equal( + result.optimize_result[0].x, result2.optimize_result[0].x, decimal=4 + ) + + def test_correct_startpoint_usage(optimizer): """ Test that the startpoint is correctly used in all optimizers. """ # cmaes supports x0, but samples from this initial guess, therefore return - if optimizer == ('cmaes', ''): + if optimizer == ("cmaes", ""): return opt = get_optimizer(*optimizer) From 82b7d5da1273b9d442a634e5aabbda9685279d77 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:02:03 +0100 Subject: [PATCH 04/35] Ruff codechecks (#1307) * Removed old python versions from black * Replaced most precommit hooks for ruff. * Changed to ruff. Currently only checking for F rules * Solved F errors * Solved I errors * Solved D413 error * Solved D errors. (Ignoring for example python files) with correct line length * Included flake8-bandit * Included flake8-bugbear * Included flake8-comprehensions * Included juypternotebooks, added pycodestyyle. Removed flake8 nd nbqa from pre-commits * small formatting * correct tmpfile handling * Add D413 (blank line after last section) to ignore * Removed non existing file from excludes * Removed D107 from file specific, as it is in all files ignored * Removed black and isort from pyproject.toml * Revert D413 * removed flake8 and project from tox.removed .flake8 * fixed {} to dict() * Added two missing notebooks to test. * Removed depracated force_compile * corrected workflow_comparison * removed manual profiling from running and only as comment * removed outputs * removed workflow_comparison.ipynb --------- Co-authored-by: Daniel Weindl --- .flake8 | 41 - .pre-commit-config.yaml | 46 +- doc/api.rst | 1 - doc/conf.py | 118 +- doc/example.rst | 1 - doc/example/amici.ipynb | 16 +- .../benchmark_import.py | 31 +- doc/example/censored_data.ipynb | 6 +- doc/example/conversion_reaction/create.py | 12 +- doc/example/custom_objective_function.ipynb | 3 - doc/example/example_MPIPool.py | 15 +- doc/example/getting_started.ipynb | 112 +- doc/example/julia.ipynb | 2 +- doc/example/model_selection.ipynb | 30 +- doc/example/ordinal_data.ipynb | 68 +- doc/example/petab_import.ipynb | 8 +- doc/example/relative_data.ipynb | 60 +- doc/example/sampling_diagnostics.ipynb | 1 - doc/example/semiquantitative_data.ipynb | 28 +- doc/example/store.ipynb | 55 +- doc/example/synthetic_data.ipynb | 1 - doc/example/workflow_comparison.ipynb | 2275 ----------------- pypesto/C.py | 338 +-- pypesto/ensemble/covariance_analysis.py | 8 +- pypesto/ensemble/ensemble.py | 113 +- pypesto/ensemble/util.py | 40 +- pypesto/hierarchical/base_problem.py | 22 +- .../inner_calculator_collector.py | 37 +- pypesto/hierarchical/ordinal/calculator.py | 6 +- pypesto/hierarchical/ordinal/problem.py | 36 +- pypesto/hierarchical/ordinal/solver.py | 46 +- pypesto/hierarchical/petab.py | 25 +- pypesto/hierarchical/relative/calculator.py | 10 +- pypesto/hierarchical/relative/problem.py | 28 +- pypesto/hierarchical/relative/solver.py | 22 +- pypesto/hierarchical/relative/util.py | 7 +- .../semiquantitative/calculator.py | 4 +- .../hierarchical/semiquantitative/problem.py | 24 +- .../hierarchical/semiquantitative/solver.py | 26 +- pypesto/history/base.py | 2 +- pypesto/history/csv.py | 34 +- pypesto/history/hdf5.py | 18 +- pypesto/history/optimizer.py | 16 +- pypesto/history/options.py | 6 +- pypesto/history/util.py | 2 +- pypesto/logging.py | 6 +- pypesto/objective/aesara/base.py | 12 +- pypesto/objective/aggregated.py | 12 +- pypesto/objective/amici/amici.py | 128 +- pypesto/objective/amici/amici_calculator.py | 32 +- pypesto/objective/amici/amici_util.py | 11 +- pypesto/objective/base.py | 56 +- pypesto/objective/finite_difference.py | 2 +- pypesto/objective/function.py | 4 +- pypesto/objective/jax/base.py | 22 +- pypesto/objective/julia/base.py | 6 +- pypesto/objective/julia/petabJl.py | 18 +- pypesto/objective/julia/petab_jl_importer.py | 3 +- pypesto/objective/priors.py | 80 +- pypesto/optimize/ess/cess.py | 24 +- pypesto/optimize/ess/ess.py | 14 +- pypesto/optimize/ess/function_evaluator.py | 6 +- pypesto/optimize/ess/refset.py | 2 +- pypesto/optimize/ess/sacess.py | 193 +- pypesto/optimize/load.py | 6 +- pypesto/optimize/optimize.py | 4 +- pypesto/optimize/optimizer.py | 20 +- pypesto/optimize/options.py | 6 +- pypesto/optimize/task.py | 4 +- pypesto/optimize/util.py | 12 +- pypesto/petab/__init__.py | 6 +- pypesto/petab/importer.py | 20 +- pypesto/predict/amici_predictor.py | 12 +- pypesto/problem/base.py | 66 +- pypesto/profile/options.py | 6 +- pypesto/profile/profile.py | 12 +- pypesto/profile/profile_next_guess.py | 30 +- pypesto/profile/task.py | 4 +- pypesto/profile/walk_along_profile.py | 2 +- pypesto/result/optimize.py | 24 +- pypesto/result/predict.py | 51 +- pypesto/result/profile.py | 2 +- pypesto/result/sample.py | 12 +- pypesto/sample/adaptive_metropolis.py | 26 +- pypesto/sample/adaptive_parallel_tempering.py | 8 +- pypesto/sample/diagnostics.py | 7 +- pypesto/sample/dynesty.py | 2 +- pypesto/sample/emcee.py | 2 +- pypesto/sample/metropolis.py | 10 +- pypesto/sample/parallel_tempering.py | 16 +- pypesto/sample/pymc.py | 11 +- pypesto/sample/sample.py | 2 +- pypesto/select/__init__.py | 3 +- pypesto/select/method.py | 46 +- pypesto/select/postprocessors.py | 24 +- pypesto/select/problem.py | 20 +- pypesto/store/auto.py | 2 +- pypesto/store/hdf5.py | 6 +- pypesto/store/read_from_hdf5.py | 85 +- pypesto/store/save_to_hdf5.py | 20 +- pypesto/testing/examples.py | 16 +- pypesto/util.py | 4 +- pypesto/visualize/clust_color.py | 14 +- pypesto/visualize/dimension_reduction.py | 30 +- pypesto/visualize/ensemble.py | 70 +- pypesto/visualize/misc.py | 27 +- pypesto/visualize/model_fit.py | 24 +- pypesto/visualize/optimization_stats.py | 76 +- pypesto/visualize/optimizer_convergence.py | 8 +- pypesto/visualize/optimizer_history.py | 39 +- pypesto/visualize/ordinal_categories.py | 156 +- pypesto/visualize/parameters.py | 50 +- pypesto/visualize/profile_cis.py | 8 +- pypesto/visualize/profiles.py | 17 +- pypesto/visualize/reference_points.py | 20 +- pypesto/visualize/sampling.py | 181 +- pypesto/visualize/select.py | 25 +- pypesto/visualize/spline_approximation.py | 66 +- pypesto/visualize/waterfall.py | 36 +- pyproject.toml | 68 +- setup.cfg | 1 - test/base/test_aggregated.py | 56 +- test/base/test_engine.py | 6 +- test/base/test_ensemble.py | 18 +- test/base/test_history.py | 114 +- test/base/test_logging.py | 6 +- test/base/test_objective.py | 62 +- test/base/test_prior.py | 84 +- test/base/test_problem.py | 24 +- test/base/test_store.py | 42 +- test/base/test_workflow.py | 2 +- test/base/test_x_fixed.py | 2 +- test/hierarchical/test_censored.py | 36 +- test/hierarchical/test_hierarchical.py | 120 +- test/hierarchical/test_ordinal.py | 72 +- test/hierarchical/test_spline.py | 90 +- test/julia/test_pyjulia.py | 16 +- test/optimize/test_optimize.py | 7 +- test/petab/test_amici_objective.py | 26 +- test/petab/test_amici_predictor.py | 178 +- test/petab/test_petab_import.py | 24 +- test/petab/test_petab_suite.py | 12 +- test/petab/test_sbml_conversion.py | 48 +- test/profile/test_profile.py | 92 +- test/run_notebook.sh | 1 + test/sample/test_sample.py | 84 +- test/select/test_select.py | 124 +- test/util.py | 18 +- test/visualize/test_visualize.py | 94 +- tox.ini | 27 +- 150 files changed, 2588 insertions(+), 4853 deletions(-) delete mode 100644 .flake8 delete mode 100644 doc/example/workflow_comparison.ipynb diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b3232928e..000000000 --- a/.flake8 +++ /dev/null @@ -1,41 +0,0 @@ -######################## -# Flake8 Configuration # -######################## - -[flake8] - -extend-ignore = - # White space before : - E203 - # Don't be crazy if line too long - E501 - # Missing docstring in public module - D100 - # Missing docstring in public method - # D102 - # Missing docstring in magic method - D105 - # Missing docstring in __init__ - D107 - # Empty method in abstract base class - B027 - # Using f"{foo!r}" instead of f"'{foo}'" (only until April 2023) - B028 - # No blank lines allowed after function docstring. (Clashing with black) - D202 - -per-file-ignores = - # Imported but unused - */__init__.py:F401,D400,D205 - # Print and asserts - test/*:T201,S101,D - # Ignore errors in module docstring - pypesto/logging.py:D400,D205,D107 - pypesto/problem.py:D400,D205,D107 - pypesto/util.py:D400,D205,D107 - pypesto/C.py:D400,D205,D107 - # Module level import not at top of file - test/julia/test_pyjulia.py:E402 - -exclude = - amici_models diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0221998ae..872ca99ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,29 +10,6 @@ # `pre-commit run --all-files` as by default only changed files are checked repos: -- repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - description: The uncompromising code formatter -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) - - id: isort - name: isort (cython) - types: [cython] - - id: isort - name: isort (pyi) - types: [pyi] -- repo: https://github.com/nbQA-dev/nbQA - rev: 1.6.3 - hooks: - - id: nbqa-black - - id: nbqa-pyupgrade - args: [--py36-plus] - - id: nbqa-isort - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: @@ -54,12 +31,19 @@ repos: description: Replace or check mixed line endings - id: trailing-whitespace description: Trim trailing whitespaces -- repo: local +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.11 hooks: - - id: style - name: Check style - description: Check style - pass_filenames: false - entry: tox -e project,flake8 - language: system - types: [python] + # Run the linter. + - id: ruff + args: + - --fix + - --config + - pyproject.toml + + # Run the formatter. + - id: ruff-format + args: + - --config + - pyproject.toml diff --git a/doc/api.rst b/doc/api.rst index cd716c538..b99b9dfd3 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -24,7 +24,6 @@ API reference pypesto.profile.profile_next_guess pypesto.result pypesto.sample - pypesto.sample.pymc pypesto.select pypesto.select.postprocessors pypesto.startpoint diff --git a/doc/conf.py b/doc/conf.py index 7fc9a8585..bfb4ef812 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -20,7 +20,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) # Silence: # Debugger warning: It seems that frozen modules are being used, which may @@ -40,49 +40,49 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '3.0.4' +needs_sphinx = "3.0.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ # include documentation from docstrings - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", # generate autodoc summaries - 'sphinx.ext.autosummary', + "sphinx.ext.autosummary", # use mathjax for latex formulas - 'sphinx.ext.mathjax', + "sphinx.ext.mathjax", # link to code - 'sphinx.ext.viewcode', + "sphinx.ext.viewcode", # link to other projects' docs - 'sphinx.ext.intersphinx', + "sphinx.ext.intersphinx", # support numpy and google style docstrings - 'sphinx.ext.napoleon', + "sphinx.ext.napoleon", # support todo items - 'sphinx.ext.todo', + "sphinx.ext.todo", # source parser for jupyter notebook files - 'nbsphinx', + "nbsphinx", # code highlighting in jupyter cells - 'IPython.sphinxext.ipython_console_highlighting', + "IPython.sphinxext.ipython_console_highlighting", # support markdown-based docs - 'myst_parser', + "myst_parser", # bibtex references - 'sphinxcontrib.bibtex', + "sphinxcontrib.bibtex", # ensure that jQuery is installed - 'sphinxcontrib.jquery', + "sphinxcontrib.jquery", # type hint formatting - 'sphinx_autodoc_typehints', + "sphinx_autodoc_typehints", ] # default autodoc options # list for special-members seems not to be possible before 1.8 autodoc_default_options = { - 'members': True, - 'undoc-members': True, - 'special-members': '__init__, __call__', - 'imported-members': True, - 'show-inheritance': True, - 'autodoc_inherit_docstrings': True, + "members": True, + "undoc-members": True, + "special-members": "__init__, __call__", + "imported-members": True, + "show-inheritance": True, + "autodoc_inherit_docstrings": True, } autodoc_class_signature = "separated" @@ -91,18 +91,18 @@ # links for intersphinx intersphinx_mapping = { - 'amici': ('https://amici.readthedocs.io/en/latest/', None), - 'fides': ('https://fides-optimizer.readthedocs.io/en/latest/', None), - 'matplotlib': ('https://matplotlib.org/stable/', None), - 'numpy': ('https://numpy.org/devdocs/', None), - 'pandas': ('https://pandas.pydata.org/docs/', None), - 'petab': ( - 'https://petab.readthedocs.io/projects/libpetab-python/en/latest/', + "amici": ("https://amici.readthedocs.io/en/latest/", None), + "fides": ("https://fides-optimizer.readthedocs.io/en/latest/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "numpy": ("https://numpy.org/devdocs/", None), + "pandas": ("https://pandas.pydata.org/docs/", None), + "petab": ( + "https://petab.readthedocs.io/projects/libpetab-python/en/latest/", None, ), - 'petab_select': ('https://petab-select.readthedocs.io/en/develop/', None), - 'python': ('https://docs.python.org/3', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + "petab_select": ("https://petab-select.readthedocs.io/en/develop/", None), + "python": ("https://docs.python.org/3", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), } @@ -112,21 +112,21 @@ bibtex_bibfiles = ["using_pypesto.bib", "references.bib"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'pyPESTO' -copyright = '2018, The pyPESTO developers' -author = 'The pyPESTO developers' +project = "pyPESTO" +copyright = "2018, The pyPESTO developers" +author = "The pyPESTO developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -150,16 +150,16 @@ # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [ - '_build', - 'Thumbs.db', - '.DS_Store', - '**.ipynb_checkpoints', - 'example/tmp', - 'README.md', + "_build", + "Thumbs.db", + ".DS_Store", + "**.ipynb_checkpoints", + "example/tmp", + "README.md", ] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -183,15 +183,15 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { - 'collapse_navigation': False, - 'navigation_depth': -1, + "collapse_navigation": False, + "navigation_depth": -1, } # Title @@ -202,7 +202,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Favicon html_favicon = "logo/logo_favicon.png" @@ -210,7 +210,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'pyPESTOdoc' +htmlhelp_basename = "pyPESTOdoc" # -- Options for LaTeX output --------------------------------------------- @@ -236,10 +236,10 @@ latex_documents = [ ( master_doc, - 'pyPESTO.tex', - 'pyPESTO Documentation', - 'The pyPESTO developers', - 'manual', + "pyPESTO.tex", + "pyPESTO Documentation", + "The pyPESTO developers", + "manual", ), ] @@ -248,7 +248,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'pypesto', 'pyPESTO Documentation', [author], 1)] +man_pages = [(master_doc, "pypesto", "pyPESTO Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -259,11 +259,11 @@ texinfo_documents = [ ( master_doc, - 'pyPESTO', - 'pyPESTO Documentation', + "pyPESTO", + "pyPESTO Documentation", author, - 'pyPESTO', - 'One line description of project.', - 'Miscellaneous', + "pyPESTO", + "One line description of project.", + "Miscellaneous", ), ] diff --git a/doc/example.rst b/doc/example.rst index b1fbbae5e..a6f694110 100644 --- a/doc/example.rst +++ b/doc/example.rst @@ -27,7 +27,6 @@ Getting started example/getting_started.ipynb example/custom_objective_function.ipynb - example/workflow_comparison.ipynb PEtab and AMICI --------------- diff --git a/doc/example/amici.ipynb b/doc/example/amici.ipynb index 71df4adcb..477c63e52 100644 --- a/doc/example/amici.ipynb +++ b/doc/example/amici.ipynb @@ -54,8 +54,8 @@ "import pypesto.visualize as visualize\n", "import pypesto.visualize.model_fit as model_fit\n", "\n", - "mpl.rcParams['figure.dpi'] = 100\n", - "mpl.rcParams['font.size'] = 18\n", + "mpl.rcParams[\"figure.dpi\"] = 100\n", + "mpl.rcParams[\"font.size\"] = 18\n", "\n", "random.seed(1912)\n", "\n", @@ -795,7 +795,7 @@ }, "outputs": [], "source": [ - "optimizer_options = {'maxiter': 1e4, 'fatol': 1e-12, 'frtol': 1e-12}\n", + "optimizer_options = {\"maxiter\": 1e4, \"fatol\": 1e-12, \"frtol\": 1e-12}\n", "\n", "optimizer = optimize.FidesOptimizer(\n", " options=optimizer_options, verbose=logging.WARN\n", @@ -1373,14 +1373,13 @@ "outputs": [], "source": [ "# create temporary file\n", - "fn = tempfile.mktemp(\".h5\")\n", - "\n", + "fn = tempfile.NamedTemporaryFile(suffix=\".hdf5\", delete=False)\n", "# write result with write_result function.\n", "# Choose which parts of the result object to save with\n", "# corresponding booleans.\n", "store.write_result(\n", " result=result,\n", - " filename=fn,\n", + " filename=fn.name,\n", " problem=True,\n", " optimize=True,\n", " sample=True,\n", @@ -1412,7 +1411,10 @@ "outputs": [], "source": [ "# Read result\n", - "result2 = store.read_result(fn, problem=True)" + "result2 = store.read_result(fn, problem=True)\n", + "\n", + "# close file\n", + "fn.close()" ] }, { diff --git a/doc/example/boehm_JProteomeRes2014/benchmark_import.py b/doc/example/boehm_JProteomeRes2014/benchmark_import.py index 5f73ae8ee..a2089b814 100644 --- a/doc/example/boehm_JProteomeRes2014/benchmark_import.py +++ b/doc/example/boehm_JProteomeRes2014/benchmark_import.py @@ -1,7 +1,4 @@ import h5py -import numpy as np -import pandas as pd -import scipy as sp class DataProvider: @@ -12,40 +9,40 @@ def get_edata(self): pass def get_timepoints(self): - with h5py.File(self.h5_file, 'r') as f: - timepoints = f['/amiciOptions/ts'][:] + with h5py.File(self.h5_file, "r") as f: + timepoints = f["/amiciOptions/ts"][:] return timepoints def get_pscales(self): - with h5py.File(self.h5_file, 'r') as f: - pscale = f['/amiciOptions/pscale'][:] + with h5py.File(self.h5_file, "r") as f: + pscale = f["/amiciOptions/pscale"][:] return pscale def get_fixed_parameters(self): - with h5py.File(self.h5_file, 'r') as f: - fixed_parameters = f['/fixedParameters/k'][:] + with h5py.File(self.h5_file, "r") as f: + fixed_parameters = f["/fixedParameters/k"][:] fixed_parameters = fixed_parameters[0] return fixed_parameters def get_fixed_parameters_names(self): - with h5py.File(self.h5_file, 'r') as f: - fixed_parameters_names = f['/fixedParameters/parameterNames'][:] + with h5py.File(self.h5_file, "r") as f: + fixed_parameters_names = f["/fixedParameters/parameterNames"][:] return fixed_parameters_names def get_initial_states(self): pass def get_measurements(self): - with h5py.File(self.h5_file, 'r') as f: - measurements = f['/measurements/y'][:] + with h5py.File(self.h5_file, "r") as f: + measurements = f["/measurements/y"][:] return measurements def get_ysigma(self): - with h5py.File(self.h5_file, 'r') as f: - ysigma = f['/measurements/ysigma'][:] + with h5py.File(self.h5_file, "r") as f: + ysigma = f["/measurements/ysigma"][:] return ysigma def get_observableNames(self): - with h5py.File(self.h5_file, 'r') as f: - observable_names = f['/measurements/observableNames'] + with h5py.File(self.h5_file, "r") as f: + observable_names = f["/measurements/observableNames"] return observable_names diff --git a/doc/example/censored_data.ipynb b/doc/example/censored_data.ipynb index 0dbc45bb0..cfe783848 100644 --- a/doc/example/censored_data.ipynb +++ b/doc/example/censored_data.ipynb @@ -76,8 +76,8 @@ "metadata": {}, "outputs": [], "source": [ - "petab_folder = './example_censored/'\n", - "yaml_file = 'example_censored.yaml'\n", + "petab_folder = \"./example_censored/\"\n", + "yaml_file = \"example_censored.yaml\"\n", "\n", "petab_problem = petab.Problem.from_yaml(petab_folder + yaml_file)\n", "\n", @@ -106,7 +106,7 @@ "source": [ "from pandas import option_context\n", "\n", - "with option_context('display.max_colwidth', 400):\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(petab_problem.measurement_df)" ] }, diff --git a/doc/example/conversion_reaction/create.py b/doc/example/conversion_reaction/create.py index 06f33c7b2..1207ac2cd 100644 --- a/doc/example/conversion_reaction/create.py +++ b/doc/example/conversion_reaction/create.py @@ -19,7 +19,7 @@ def analytical_a(t, a0=a0, b0=b0, k1=k1, k2=k2): condition_df = pd.DataFrame( data={ - CONDITION_ID: ['c0'], + CONDITION_ID: ["c0"], } ).set_index([CONDITION_ID]) @@ -31,8 +31,8 @@ def analytical_a(t, a0=a0, b0=b0, k1=k1, k2=k2): measurement_df = pd.DataFrame( data={ - OBSERVABLE_ID: ['obs_a'] * nt, - SIMULATION_CONDITION_ID: ['c0'] * nt, + OBSERVABLE_ID: ["obs_a"] * nt, + SIMULATION_CONDITION_ID: ["c0"] * nt, TIME: times, MEASUREMENT: measurements, } @@ -40,15 +40,15 @@ def analytical_a(t, a0=a0, b0=b0, k1=k1, k2=k2): observable_df = pd.DataFrame( data={ - OBSERVABLE_ID: ['obs_a'], - OBSERVABLE_FORMULA: ['A'], + OBSERVABLE_ID: ["obs_a"], + OBSERVABLE_FORMULA: ["A"], NOISE_FORMULA: [sigma], } ).set_index([OBSERVABLE_ID]) parameter_df = pd.DataFrame( data={ - PARAMETER_ID: ['k1', 'k2'], + PARAMETER_ID: ["k1", "k2"], PARAMETER_SCALE: [LOG] * 2, LOWER_BOUND: [1e-5] * 2, UPPER_BOUND: [1e5] * 2, diff --git a/doc/example/custom_objective_function.ipynb b/doc/example/custom_objective_function.ipynb index 07d40819a..35c985884 100644 --- a/doc/example/custom_objective_function.ipynb +++ b/doc/example/custom_objective_function.ipynb @@ -69,9 +69,6 @@ }, "outputs": [], "source": [ - "import logging\n", - "\n", - "import matplotlib as mpl\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import scipy as sp\n", diff --git a/doc/example/example_MPIPool.py b/doc/example/example_MPIPool.py index 870115eeb..dd5b19c2a 100644 --- a/doc/example/example_MPIPool.py +++ b/doc/example/example_MPIPool.py @@ -1,6 +1,8 @@ -"""This file serves as an example how to use MPIPoolEngine +""" +This file serves as an example how to use MPIPoolEngine to optimize across nodes and also as a test for the -MPIPoolEngine.""" +MPIPoolEngine. +""" import numpy as np import scipy as sp @@ -13,8 +15,9 @@ def setup_rosen_problem(n_starts: int = 2): - """Set up the rosenbrock problem and return - a pypesto.Problem""" + """ + Set up the rosenbrock problem and return a pypesto.Problem. + """ objective = pypesto.Objective( fun=sp.optimize.rosen, grad=sp.optimize.rosen_der, @@ -38,7 +41,7 @@ def setup_rosen_problem(n_starts: int = 2): # set all your code into this if condition. # This way only one core performs the code # and distributes the work of the optimization. -if __name__ == '__main__': +if __name__ == "__main__": # set number of starts n_starts = 2 # create problem @@ -56,7 +59,7 @@ def setup_rosen_problem(n_starts: int = 2): ) # saving optimization results to hdf5 - file_name = 'temp_result.h5' + file_name = "temp_result.h5" opt_result_writer = OptimizationResultHDF5Writer(file_name) problem_writer = ProblemHDF5Writer(file_name) problem_writer.write(problem) diff --git a/doc/example/getting_started.ipynb b/doc/example/getting_started.ipynb index 4429ed7c0..8c02085e7 100644 --- a/doc/example/getting_started.ipynb +++ b/doc/example/getting_started.ipynb @@ -41,7 +41,7 @@ "np.random.seed(1)\n", "\n", "# increase image resolution\n", - "mpl.rcParams['figure.dpi'] = 300" + "mpl.rcParams[\"figure.dpi\"] = 300" ] }, { @@ -180,7 +180,7 @@ "outputs": [], "source": [ "# Objective function values of the different optimizer runs:\n", - "result_custom_problem.optimize_result.get_for_key('fval')" + "result_custom_problem.optimize_result.get_for_key(\"fval\")" ] }, { @@ -231,7 +231,7 @@ "source": [ "%%capture\n", "# directory of the PEtab problem\n", - "petab_yaml = './boehm_JProteomeRes2014/boehm_JProteomeRes2014.yaml'\n", + "petab_yaml = \"./boehm_JProteomeRes2014/boehm_JProteomeRes2014.yaml\"\n", "\n", "importer = pypesto.petab.PetabImporter.from_yaml(petab_yaml)\n", "problem = importer.create_problem(verbose=False)" @@ -265,9 +265,7 @@ "optimizer = optimize.ScipyOptimizer()\n", "\n", "# do the optimization\n", - "result = optimize.minimize(problem=problem, \n", - " optimizer=optimizer,\n", - " n_starts=5)" + "result = optimize.minimize(problem=problem, optimizer=optimizer, n_starts=5)" ] }, { @@ -306,7 +304,7 @@ "outputs": [], "source": [ "# Objective function values of the different optimizer runs:\n", - "result.optimize_result.get_for_key('fval')" + "result.optimize_result.get_for_key(\"fval\")" ] }, { @@ -349,8 +347,8 @@ }, "outputs": [], "source": [ - "optimizer_scipy_lbfgsb = optimize.ScipyOptimizer(method='L-BFGS-B')\n", - "optimizer_scipy_powell = optimize.ScipyOptimizer(method='Powell')\n", + "optimizer_scipy_lbfgsb = optimize.ScipyOptimizer(method=\"L-BFGS-B\")\n", + "optimizer_scipy_powell = optimize.ScipyOptimizer(method=\"Powell\")\n", "\n", "optimizer_fides = optimize.FidesOptimizer(verbose=logging.ERROR)\n", "optimizer_pyswarm = optimize.PyswarmOptimizer()" @@ -383,39 +381,47 @@ "%%capture --no-display\n", "n_starts = 10\n", "\n", - "# Due to run time we already use parallelization. \n", + "# Due to run time we already use parallelization.\n", "# This will be introduced in more detail later.\n", "engine = pypesto.engine.MultiProcessEngine()\n", "history_options = pypesto.HistoryOptions(trace_record=True)\n", "\n", "# Scipy: L-BFGS-B\n", - "result_lbfgsb = optimize.minimize(problem=problem, \n", - " optimizer=optimizer_scipy_lbfgsb,\n", - " engine=engine,\n", - " history_options=history_options,\n", - " n_starts=n_starts)\n", + "result_lbfgsb = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer_scipy_lbfgsb,\n", + " engine=engine,\n", + " history_options=history_options,\n", + " n_starts=n_starts,\n", + ")\n", "\n", "# Scipy: Powell\n", - "result_powell = optimize.minimize(problem=problem, \n", - " optimizer=optimizer_scipy_powell,\n", - " engine=engine,\n", - " history_options=history_options,\n", - " n_starts=n_starts)\n", + "result_powell = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer_scipy_powell,\n", + " engine=engine,\n", + " history_options=history_options,\n", + " n_starts=n_starts,\n", + ")\n", "\n", "# Fides\n", - "result_fides = optimize.minimize(problem=problem, \n", - " optimizer=optimizer_fides,\n", - " engine=engine,\n", - " history_options=history_options,\n", - " n_starts=n_starts)\n", + "result_fides = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer_fides,\n", + " engine=engine,\n", + " history_options=history_options,\n", + " n_starts=n_starts,\n", + ")\n", "\n", "\n", "# PySwarm\n", - "result_pyswarm = optimize.minimize(problem=problem, \n", - " optimizer=optimizer_pyswarm,\n", - " engine=engine,\n", - " history_options=history_options,\n", - " n_starts=n_starts) " + "result_pyswarm = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer_pyswarm,\n", + " engine=engine,\n", + " history_options=history_options,\n", + " n_starts=n_starts,\n", + ")" ] }, { @@ -450,7 +456,7 @@ " result_fides,\n", " result_pyswarm,\n", "]\n", - "optimizer_names = ['Scipy: L-BFGS-B', 'Scipy: Powell', 'Fides', 'pyswarm']\n", + "optimizer_names = [\"Scipy: L-BFGS-B\", \"Scipy: Powell\", \"Fides\", \"pyswarm\"]\n", "\n", "pypesto.visualize.waterfall(optimizer_results, legends=optimizer_names);" ] @@ -478,14 +484,14 @@ }, "outputs": [], "source": [ - "print('Average Run time per start:')\n", - "print('-------------------')\n", + "print(\"Average Run time per start:\")\n", + "print(\"-------------------\")\n", "\n", "for optimizer_name, optimizer_result in zip(\n", " optimizer_names, optimizer_results\n", "):\n", - " t = np.sum(optimizer_result.optimize_result.get_for_key('time')) / n_starts\n", - " print(f'{optimizer_name}: {t:f} s')" + " t = np.sum(optimizer_result.optimize_result.get_for_key(\"time\")) / n_starts\n", + " print(f\"{optimizer_name}: {t:f} s\")" ] }, { @@ -557,10 +563,12 @@ "engine = pypesto.engine.MultiProcessEngine()\n", "\n", "# Optimize\n", - "result = optimize.minimize(problem=problem, \n", - " optimizer=optimizer_scipy_lbfgsb,\n", - " engine=engine,\n", - " n_starts=25)" + "result = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer_scipy_lbfgsb,\n", + " engine=engine,\n", + " n_starts=25,\n", + ")" ] }, { @@ -600,10 +608,12 @@ "\n", "import pypesto.profile as profile\n", "\n", - "result = profile.parameter_profile(problem=problem, \n", - " result=result, \n", - " optimizer=optimizer_scipy_lbfgsb,\n", - " profile_index=[0, 1, 2])" + "result = profile.parameter_profile(\n", + " problem=problem,\n", + " result=result,\n", + " optimizer=optimizer_scipy_lbfgsb,\n", + " profile_index=[0, 1, 2],\n", + ")" ] }, { @@ -628,7 +638,7 @@ "outputs": [], "source": [ "# adapt x_labels..\n", - "x_labels = [f'Log10({name})' for name in problem.x_names]\n", + "x_labels = [f\"Log10({name})\" for name in problem.x_names]\n", "\n", "visualize.profiles(result, x_labels=x_labels, show_bounds=True);" ] @@ -660,7 +670,7 @@ " result, confidence_level=0.95, show_bounds=True\n", ")\n", "\n", - "ax.set_xlabel('Log10(Parameter value)');" + "ax.set_xlabel(\"Log10(Parameter value)\");" ] }, { @@ -718,7 +728,7 @@ }, "outputs": [], "source": [ - "result.sample_result['trace_x']" + "result.sample_result[\"trace_x\"]" ] }, { @@ -745,7 +755,7 @@ "outputs": [], "source": [ "sample.geweke_test(result=result)\n", - "result.sample_result['burn_in']" + "result.sample_result[\"burn_in\"]" ] }, { @@ -759,7 +769,7 @@ "outputs": [], "source": [ "sample.effective_sample_size(result=result)\n", - "result.sample_result['effective_sample_size']" + "result.sample_result[\"effective_sample_size\"]" ] }, { @@ -837,10 +847,12 @@ "import pypesto.store as store\n", "\n", "# create a temporary file, for demonstration purpose\n", - "result_file_name = tempfile.mktemp(\".hdf5\")\n", + "f_tmp = tempfile.NamedTemporaryFile(suffix=\".hdf5\", delete=False)\n", + "result_file_name = f_tmp.name\n", "\n", "# store the result\n", - "store.write_result(result, result_file_name)" + "store.write_result(result, result_file_name)\n", + "f_tmp.close()" ] }, { diff --git a/doc/example/julia.ipynb b/doc/example/julia.ipynb index 29ecc5afe..df22f2e24 100644 --- a/doc/example/julia.ipynb +++ b/doc/example/julia.ipynb @@ -394,7 +394,7 @@ "\n", "for i, label in zip(range(3), [\"S\", \"I\", \"R\"]):\n", " plt.plot(sol_true[i], color=f\"C{i}\", label=label)\n", - " plt.plot(data[i], 'x', color=f\"C{i}\")\n", + " plt.plot(data[i], \"x\", color=f\"C{i}\")\n", "plt.legend();" ] }, diff --git a/doc/example/model_selection.ipynb b/doc/example/model_selection.ipynb index 69b376461..72ae22a8d 100644 --- a/doc/example/model_selection.ipynb +++ b/doc/example/model_selection.ipynb @@ -305,11 +305,11 @@ "initial_model = Model(\n", " model_id=\"myModel\",\n", " petab_yaml=petab_yaml,\n", - " parameters=dict(\n", - " k1=0.1,\n", - " k2=ESTIMATE,\n", - " k3=ESTIMATE,\n", - " ),\n", + " parameters={\n", + " \"k1\": 0.1,\n", + " \"k2\": ESTIMATE,\n", + " \"k3\": ESTIMATE,\n", + " },\n", " criteria={petab_select_problem.criterion: np.inf},\n", ")\n", "\n", @@ -498,22 +498,22 @@ "initial_model_1 = Model(\n", " model_id=\"myModel1\",\n", " petab_yaml=petab_yaml,\n", - " parameters=dict(\n", - " k1=0,\n", - " k2=0,\n", - " k3=0,\n", - " ),\n", + " parameters={\n", + " \"k1\": 0,\n", + " \"k2\": 0,\n", + " \"k3\": 0,\n", + " },\n", " criteria={petab_select_problem.criterion: np.inf},\n", ")\n", "\n", "initial_model_2 = Model(\n", " model_id=\"myModel2\",\n", " petab_yaml=petab_yaml,\n", - " parameters=dict(\n", - " k1=ESTIMATE,\n", - " k2=ESTIMATE,\n", - " k3=0,\n", - " ),\n", + " parameters={\n", + " \"k1\": ESTIMATE,\n", + " \"k2\": ESTIMATE,\n", + " \"k3\": 0,\n", + " },\n", " criteria={petab_select_problem.criterion: np.inf},\n", ")\n", "\n", diff --git a/doc/example/ordinal_data.ipynb b/doc/example/ordinal_data.ipynb index e454ce0ec..2488cadf5 100644 --- a/doc/example/ordinal_data.ipynb +++ b/doc/example/ordinal_data.ipynb @@ -74,8 +74,8 @@ } ], "source": [ - "petab_folder = './example_ordinal/'\n", - "yaml_file = 'example_ordinal.yaml'\n", + "petab_folder = \"./example_ordinal/\"\n", + "yaml_file = \"example_ordinal.yaml\"\n", "\n", "petab_problem = petab.Problem.from_yaml(petab_folder + yaml_file)\n", "\n", @@ -475,7 +475,7 @@ "source": [ "from pandas import option_context\n", "\n", - "with option_context('display.max_colwidth', 400):\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(petab_problem.measurement_df)" ] }, @@ -561,7 +561,7 @@ "source": [ "objective = importer.create_objective(\n", " inner_options={\n", - " \"method\": 'reduced',\n", + " \"method\": \"reduced\",\n", " \"reparameterized\": True,\n", " \"interval_constraints\": \"max\",\n", " \"min_gap\": 0.1,\n", @@ -647,15 +647,15 @@ ], "source": [ "np.random.seed(n_starts)\n", - "problem.objective.calculator.inner_calculators[0].inner_solver = (\n", - " OrdinalInnerSolver(\n", - " options={\n", - " 'method': 'reduced',\n", - " 'reparameterized': True,\n", - " 'interval_constraints': 'max',\n", - " 'min_gap': 1e-10,\n", - " }\n", - " )\n", + "problem.objective.calculator.inner_calculators[\n", + " 0\n", + "].inner_solver = OrdinalInnerSolver(\n", + " options={\n", + " \"method\": \"reduced\",\n", + " \"reparameterized\": True,\n", + " \"interval_constraints\": \"max\",\n", + " \"min_gap\": 1e-10,\n", + " }\n", ")\n", "\n", "res_reduced_reparameterized = optimize.minimize(\n", @@ -686,15 +686,15 @@ ], "source": [ "np.random.seed(n_starts)\n", - "problem.objective.calculator.inner_calculators[0].inner_solver = (\n", - " OrdinalInnerSolver(\n", - " options={\n", - " 'method': 'reduced',\n", - " 'reparameterized': False,\n", - " 'interval_constraints': 'max',\n", - " 'min_gap': 1e-10,\n", - " }\n", - " )\n", + "problem.objective.calculator.inner_calculators[\n", + " 0\n", + "].inner_solver = OrdinalInnerSolver(\n", + " options={\n", + " \"method\": \"reduced\",\n", + " \"reparameterized\": False,\n", + " \"interval_constraints\": \"max\",\n", + " \"min_gap\": 1e-10,\n", + " }\n", ")\n", "\n", "res_reduced = optimize.minimize(\n", @@ -725,15 +725,15 @@ ], "source": [ "np.random.seed(n_starts)\n", - "problem.objective.calculator.inner_calculators[0].inner_solver = (\n", - " OrdinalInnerSolver(\n", - " options={\n", - " 'method': 'standard',\n", - " 'reparameterized': False,\n", - " 'interval_constraints': 'max',\n", - " 'min_gap': 1e-10,\n", - " }\n", - " )\n", + "problem.objective.calculator.inner_calculators[\n", + " 0\n", + "].inner_solver = OrdinalInnerSolver(\n", + " options={\n", + " \"method\": \"standard\",\n", + " \"reparameterized\": False,\n", + " \"interval_constraints\": \"max\",\n", + " \"min_gap\": 1e-10,\n", + " }\n", ")\n", "\n", "res_standard = optimize.minimize(\n", @@ -772,14 +772,14 @@ } ], "source": [ - "time_standard = res_standard.optimize_result.get_for_key('time')\n", + "time_standard = res_standard.optimize_result.get_for_key(\"time\")\n", "print(f\"Mean computation time for standard approach: {np.mean(time_standard)}\")\n", "\n", - "time_reduced = res_reduced.optimize_result.get_for_key('time')\n", + "time_reduced = res_reduced.optimize_result.get_for_key(\"time\")\n", "print(f\"Mean computation time for reduced approach: {np.mean(time_reduced)}\")\n", "\n", "time_reduced_reparameterized = (\n", - " res_reduced_reparameterized.optimize_result.get_for_key('time')\n", + " res_reduced_reparameterized.optimize_result.get_for_key(\"time\")\n", ")\n", "print(\n", " f\"Mean computation time for reduced reparameterized approach: {np.mean(time_reduced_reparameterized)}\"\n", diff --git a/doc/example/petab_import.ipynb b/doc/example/petab_import.ipynb index 359593cbe..4b3b5bbdc 100644 --- a/doc/example/petab_import.ipynb +++ b/doc/example/petab_import.ipynb @@ -155,9 +155,7 @@ "outputs": [], "source": [ "importer = pypesto.petab.PetabImporter.from_yaml(yaml_config)\n", - "problem = (\n", - " importer.create_problem()\n", - ") # creating the problem from the importer. The objective can be found at problem.objective" + "problem = importer.create_problem() # creating the problem from the importer. The objective can be found at problem.objective" ] }, { @@ -275,7 +273,7 @@ "def fd(x):\n", " grad = np.zeros_like(x)\n", " j = 0\n", - " for i, xi in enumerate(x):\n", + " for i, _xi in enumerate(x):\n", " mask = np.zeros_like(x)\n", " mask[i] += eps\n", " valinc, _ = objective(x + mask, sensi_orders=(0, 1))\n", @@ -344,7 +342,7 @@ "outputs": [], "source": [ "problem = importer.create_problem(\n", - " startpoint_kwargs=dict(check_fval=True, check_grad=True)\n", + " startpoint_kwargs={\"check_fval\": True, \"check_grad\": True}\n", ")" ] }, diff --git a/doc/example/relative_data.ipynb b/doc/example/relative_data.ipynb index 5b997dc92..ae42881bf 100644 --- a/doc/example/relative_data.ipynb +++ b/doc/example/relative_data.ipynb @@ -38,14 +38,12 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", "import time\n", "\n", "import amici\n", "import fides\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import petab\n", "from matplotlib.colors import to_rgba\n", "\n", "import pypesto\n", @@ -123,7 +121,7 @@ "source": [ "from pandas import option_context\n", "\n", - "with option_context('display.max_colwidth', 400):\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(petab_problem_hierarchical.observable_df)" ] }, @@ -143,7 +141,7 @@ "source": [ "from pandas import option_context\n", "\n", - "with option_context('display.max_colwidth', 400):\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(petab_problem_hierarchical.measurement_df)" ] }, @@ -196,19 +194,19 @@ "# Options for multi-start optimization\n", "minimize_kwargs = {\n", " # number of starts for multi-start optimization\n", - " 'n_starts': 3,\n", + " \"n_starts\": 3,\n", " # number of processes for parallel multi-start optimization\n", - " 'engine': pypesto.engine.MultiProcessEngine(n_procs=3),\n", + " \"engine\": pypesto.engine.MultiProcessEngine(n_procs=3),\n", " # raise in case of failures\n", - " 'options': OptimizeOptions(allow_failed_starts=False),\n", + " \"options\": OptimizeOptions(allow_failed_starts=False),\n", " # use the Fides optimizer\n", - " 'optimizer': pypesto.optimize.FidesOptimizer(\n", + " \"optimizer\": pypesto.optimize.FidesOptimizer(\n", " verbose=0, hessian_update=fides.BFGS()\n", " ),\n", "}\n", "# Set the same starting points for the hierarchical and non-hierarchical problem\n", "startpoints = pypesto.startpoint.latin_hypercube(\n", - " n_starts=minimize_kwargs['n_starts'],\n", + " n_starts=minimize_kwargs[\"n_starts\"],\n", " lb=problem2.lb_full,\n", " ub=problem2.ub_full,\n", ")\n", @@ -237,7 +235,7 @@ "# Run hierarchical optimization using NumericalInnerSolver\n", "start_time = time.time()\n", "problem.objective.calculator.inner_solver = NumericalInnerSolver(\n", - " minimize_kwargs={'n_starts': 1}\n", + " minimize_kwargs={\"n_starts\": 1}\n", ")\n", "result_num = pypesto.optimize.minimize(problem, **minimize_kwargs)\n", "print(f\"{result_num.optimize_result.get_for_key('fval')=}\")\n", @@ -269,10 +267,10 @@ "# Waterfall plot - analytical vs numerical inner solver\n", "pypesto.visualize.waterfall(\n", " [result_num, result_ana],\n", - " legends=['Numerical-Hierarchical', 'Analytical-Hierarchical'],\n", + " legends=[\"Numerical-Hierarchical\", \"Analytical-Hierarchical\"],\n", " size=(15, 6),\n", " order_by_id=True,\n", - " colors=np.array(list(map(to_rgba, ('green', 'purple')))),\n", + " colors=np.array(list(map(to_rgba, (\"green\", \"purple\")))),\n", ")" ] }, @@ -283,11 +281,11 @@ "outputs": [], "source": [ "# Time comparison - analytical vs numerical inner solver\n", - "ax = plt.bar(x=[0, 1], height=[time_ana, time_num], color=['purple', 'green'])\n", + "ax = plt.bar(x=[0, 1], height=[time_ana, time_num], color=[\"purple\", \"green\"])\n", "ax = plt.gca()\n", "ax.set_xticks([0, 1])\n", - "ax.set_xticklabels(['Analytical-Hierarchical', 'Numerical-Hierarchical'])\n", - "ax.set_ylabel('Computation time [s]')" + "ax.set_xticklabels([\"Analytical-Hierarchical\", \"Numerical-Hierarchical\"])\n", + "ax.set_ylabel(\"Computation time [s]\")" ] }, { @@ -321,9 +319,9 @@ "# Waterfall plot - hierarchical optimization with analytical inner solver vs standard optimization\n", "pypesto.visualize.waterfall(\n", " [result_ana, result_ord],\n", - " legends=['Analytical-Hierarchical', 'Non-Hierarchical'],\n", + " legends=[\"Analytical-Hierarchical\", \"Non-Hierarchical\"],\n", " order_by_id=True,\n", - " colors=np.array(list(map(to_rgba, ('purple', 'orange')))),\n", + " colors=np.array(list(map(to_rgba, (\"purple\", \"orange\")))),\n", " size=(15, 6),\n", ")" ] @@ -337,11 +335,11 @@ "# Time comparison - hierarchical optimization with analytical inner solver vs standard optimization\n", "import matplotlib.pyplot as plt\n", "\n", - "ax = plt.bar(x=[0, 1], height=[time_ana, time_ord], color=['purple', 'orange'])\n", + "ax = plt.bar(x=[0, 1], height=[time_ana, time_ord], color=[\"purple\", \"orange\"])\n", "ax = plt.gca()\n", "ax.set_xticks([0, 1])\n", - "ax.set_xticklabels(['Analytical-Hierarchical', 'Non-Hierarchical'])\n", - "ax.set_ylabel('Computation time [s]')" + "ax.set_xticklabels([\"Analytical-Hierarchical\", \"Non-Hierarchical\"])\n", + "ax.set_ylabel(\"Computation time [s]\")" ] }, { @@ -380,12 +378,12 @@ "pypesto.visualize.waterfall(\n", " [result_ana, result_ana_fw, result_num, result_ord],\n", " legends=[\n", - " 'Analytical-Hierarchical (adjoint)',\n", - " 'Analytical-Hierarchical (forward)',\n", - " 'Numerical-Hierarchical',\n", - " 'Non-Hierarchical',\n", + " \"Analytical-Hierarchical (adjoint)\",\n", + " \"Analytical-Hierarchical (forward)\",\n", + " \"Numerical-Hierarchical\",\n", + " \"Non-Hierarchical\",\n", " ],\n", - " colors=np.array(list(map(to_rgba, ('purple', 'blue', 'green', 'orange')))),\n", + " colors=np.array(list(map(to_rgba, (\"purple\", \"blue\", \"green\", \"orange\")))),\n", " order_by_id=True,\n", " size=(15, 6),\n", ")" @@ -403,20 +401,20 @@ "ax = plt.bar(\n", " x=[0, 1, 2, 3],\n", " height=[time_ana, time_ana_fw, time_num, time_ord],\n", - " color=['purple', 'blue', 'green', 'orange'],\n", + " color=[\"purple\", \"blue\", \"green\", \"orange\"],\n", ")\n", "ax = plt.gca()\n", "ax.set_xticks([0, 1, 2, 3])\n", "ax.set_xticklabels(\n", " [\n", - " 'Analytical-Hierarchical (adjoint)',\n", - " 'Analytical-Hierarchical (forward)',\n", - " 'Numerical-Hierarchical',\n", - " 'Non-Hierarchical',\n", + " \"Analytical-Hierarchical (adjoint)\",\n", + " \"Analytical-Hierarchical (forward)\",\n", + " \"Numerical-Hierarchical\",\n", + " \"Non-Hierarchical\",\n", " ]\n", ")\n", "plt.setp(ax.get_xticklabels(), fontsize=10, rotation=75)\n", - "ax.set_ylabel('Computation time [s]')" + "ax.set_ylabel(\"Computation time [s]\")" ] } ], diff --git a/doc/example/sampling_diagnostics.ipynb b/doc/example/sampling_diagnostics.ipynb index e9ad15197..21827fa74 100644 --- a/doc/example/sampling_diagnostics.ipynb +++ b/doc/example/sampling_diagnostics.ipynb @@ -57,7 +57,6 @@ "source": [ "import logging\n", "\n", - "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import petab\n", "\n", diff --git a/doc/example/semiquantitative_data.ipynb b/doc/example/semiquantitative_data.ipynb index e4e683032..3a9cd784b 100644 --- a/doc/example/semiquantitative_data.ipynb +++ b/doc/example/semiquantitative_data.ipynb @@ -86,8 +86,8 @@ "metadata": {}, "outputs": [], "source": [ - "petab_folder = './example_semiquantitative/'\n", - "yaml_file = 'example_semiquantitative.yaml'\n", + "petab_folder = \"./example_semiquantitative/\"\n", + "yaml_file = \"example_semiquantitative.yaml\"\n", "\n", "petab_problem = petab.Problem.from_yaml(petab_folder + yaml_file)\n", "\n", @@ -111,10 +111,10 @@ "import pandas as pd\n", "from pandas import option_context\n", "\n", - "noise_parameter_file = 'parameters_example_semiquantitative_noise.tsv'\n", + "noise_parameter_file = \"parameters_example_semiquantitative_noise.tsv\"\n", "# load the csv file\n", - "noise_parameter_df = pd.read_csv(petab_folder + noise_parameter_file, sep='\\t')\n", - "with option_context('display.max_colwidth', 400):\n", + "noise_parameter_df = pd.read_csv(petab_folder + noise_parameter_file, sep=\"\\t\")\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(noise_parameter_df)" ] }, @@ -132,7 +132,7 @@ "metadata": {}, "outputs": [], "source": [ - "with option_context('display.max_colwidth', 400):\n", + "with option_context(\"display.max_colwidth\", 400):\n", " display(petab_problem.measurement_df)" ] }, @@ -384,12 +384,12 @@ "n_spline_pars = int(np.ceil(spline_ratio * len(timepoints)))\n", "\n", "\n", - "par_type = 'spline'\n", + "par_type = \"spline\"\n", "mask = [np.full(len(simulation), True)]\n", "\n", "inner_parameters = [\n", " SplineInnerParameter(\n", - " inner_parameter_id=f'{par_type}_{1}_{par_index+1}',\n", + " inner_parameter_id=f\"{par_type}_{1}_{par_index+1}\",\n", " inner_parameter_type=InnerParameterType.SPLINE,\n", " scale=LIN,\n", " lb=-np.inf,\n", @@ -397,7 +397,7 @@ " ixs=mask,\n", " index=par_index + 1,\n", " group=1,\n", - " observable_id='observable_1',\n", + " observable_id=\"observable_1\",\n", " )\n", " for par_index in range(n_spline_pars)\n", "]\n", @@ -407,11 +407,11 @@ ")\n", "\n", "options = {\n", - " 'minimal_diff_on': {\n", - " 'min_diff_factor': 1 / 2,\n", + " \"minimal_diff_on\": {\n", + " \"min_diff_factor\": 1 / 2,\n", " },\n", - " 'minimal_diff_off': {\n", - " 'min_diff_factor': 0.0,\n", + " \"minimal_diff_off\": {\n", + " \"min_diff_factor\": 0.0,\n", " },\n", "}\n", "inner_solvers = {}\n", @@ -421,7 +421,7 @@ " inner_solvers[minimal_diff] = SemiquantInnerSolver(\n", " options=option,\n", " )\n", - " print(f'Using {minimal_diff} options: {option}')\n", + " print(f\"Using {minimal_diff} options: {option}\")\n", "\n", " # Solve the inner problem to obtain the optimal spline\n", " results[minimal_diff] = inner_solvers[minimal_diff].solve(\n", diff --git a/doc/example/store.ipynb b/doc/example/store.ipynb index fc83fc3ed..f96520f6c 100644 --- a/doc/example/store.ipynb +++ b/doc/example/store.ipynb @@ -73,8 +73,8 @@ "import pypesto.sample as sample\n", "import pypesto.visualize as visualize\n", "\n", - "mpl.rcParams['figure.dpi'] = 100\n", - "mpl.rcParams['font.size'] = 18\n", + "mpl.rcParams[\"figure.dpi\"] = 100\n", + "mpl.rcParams[\"font.size\"] = 18\n", "# set a random seed to get reproducible results\n", "random.seed(3142)\n", "\n", @@ -109,7 +109,7 @@ "source": [ "%%capture\n", "# directory of the PEtab problem\n", - "petab_yaml = './boehm_JProteomeRes2014/boehm_JProteomeRes2014.yaml'\n", + "petab_yaml = \"./boehm_JProteomeRes2014/boehm_JProteomeRes2014.yaml\"\n", "\n", "importer = pypesto.petab.PetabImporter.from_yaml(petab_yaml)\n", "problem = importer.create_problem(verbose=False)" @@ -279,14 +279,13 @@ "outputs": [], "source": [ "# create temporary file\n", - "fn = tempfile.mktemp(\".hdf5\")\n", - "\n", + "fn = tempfile.NamedTemporaryFile(suffix=\".hdf5\", delete=False)\n", "# write the result with the write_result function.\n", "# Choose which parts of the result object to save with\n", "# corresponding booleans.\n", "pypesto.store.write_result(\n", " result=result,\n", - " filename=fn,\n", + " filename=fn.name,\n", " problem=True,\n", " optimize=True,\n", " profile=True,\n", @@ -318,7 +317,7 @@ "outputs": [], "source": [ "# load result with read_result function\n", - "result_loaded = pypesto.store.read_result(fn)" + "result_loaded = pypesto.store.read_result(fn.name)" ] }, { @@ -669,20 +668,20 @@ "outputs": [], "source": [ "# create temporary file\n", - "fn_csv = tempfile.mktemp(\"_{id}.hdf5\")\n", - "# record the history and store to CSV\n", - "history_options = pypesto.HistoryOptions(\n", - " trace_record=True, storage_file=fn_csv\n", - ")\n", + "with tempfile.NamedTemporaryFile(suffix=\"_{id}.csv\") as fn_csv:\n", + " # record the history and store to CSV\n", + " history_options = pypesto.HistoryOptions(\n", + " trace_record=True, storage_file=fn_csv.name\n", + " )\n", "\n", - "# Run optimizations\n", - "result = optimize.minimize(\n", - " problem=problem,\n", - " optimizer=optimizer,\n", - " n_starts=n_starts,\n", - " history_options=history_options,\n", - " filename=None,\n", - ")" + " # Run optimizations\n", + " result = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer,\n", + " n_starts=n_starts,\n", + " history_options=history_options,\n", + " filename=None,\n", + " )" ] }, { @@ -756,7 +755,9 @@ "outputs": [], "source": [ "# create temporary file\n", - "fn_hdf5 = tempfile.mktemp(\".hdf5\")\n", + "f_hdf5 = tempfile.NamedTemporaryFile(suffix=\".hdf5\", delete=False)\n", + "fn_hdf5 = f_hdf5.name\n", + "\n", "# record the history and store to CSV\n", "history_options = pypesto.HistoryOptions(\n", " trace_record=True, storage_file=fn_hdf5\n", @@ -823,6 +824,18 @@ "visualize.optimizer_history(result_loaded_w_history, ax=ax[1])\n", "fig.set_size_inches((15, 5))" ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# close the temporary file\n", + "f_hdf5.close()" + ], + "metadata": { + "collapsed": false + } } ], "metadata": { diff --git a/doc/example/synthetic_data.ipynb b/doc/example/synthetic_data.ipynb index cf2f5df20..75041530c 100644 --- a/doc/example/synthetic_data.ipynb +++ b/doc/example/synthetic_data.ipynb @@ -38,7 +38,6 @@ "outputs": [], "source": [ "import amici.petab_simulate\n", - "import matplotlib.pyplot as plt\n", "import petab\n", "\n", "import pypesto.optimize\n", diff --git a/doc/example/workflow_comparison.ipynb b/doc/example/workflow_comparison.ipynb deleted file mode 100644 index 9461a59c6..000000000 --- a/doc/example/workflow_comparison.ipynb +++ /dev/null @@ -1,2275 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# pyPESTO vs no pyPESTO\n", - "\n", - "The objectives of this notebook are twofold:\n", - "\n", - "1. **General Workflow:** We walk step-by-step through a process to estimate parameters of dynamical models. By following this workflow, you will gain a clear understanding of the essential steps involved and how they contribute to the overall outcome.\n", - "\n", - "2. **Benefits of pyPESTO:** Throughout the notebook, we highlight the key advantages of using pyPESTO in each step of the workflow, compared to \"doing things manually\". By leveraging its capabilities, you can significantly increase efficiency and effectiveness when solving your parameter optimization tasks.\n", - "\n", - "This notebook is divided into several sections, each focusing on a specific aspect of the parameter estimation workflow. Here's an overview of what you find in each section:\n", - "\n", - "**Contents**\n", - "\n", - "1. **Objective Function:** We discuss the creation of an objective function that quantifies the goodness-of-fit between a model and observed data. We will demonstrate how pyPESTO simplifies this potentially cumbersome process and provides various options for objective function definition.\n", - "\n", - "2. **Optimization:** We show how to find optimal model parameters. We illustrate the general workflow and how pyPESTO allows to flexibly use different optimizers and to analyze and interpret results.\n", - "\n", - "3. **Profiling:** After a successful parameter optimization, we show how pyPESTO provides profile likelihood functionality to assess uncertainty and identifiability of selected parameters.\n", - "\n", - "4. **Sampling:** In addition to profiles, we use MCMC to sample from the Bayesian posterior distribution. We show how pyPESTO facilitates the use of different sampling methods.\n", - "\n", - "5. **Result Storage:** This section focuses on storing and organizing the results obtained from the parameter optimization workflow, which is necessary to keep results for later processing and sharing.\n", - "\n", - "By the end of this notebook, you'll have gained valuable insights into the parameter estimation workflow for dynamical models. Moreover, you'll have a clear understanding of the benefits pyPESTO brings to each step of this workflow. This tutorial will equip you with the knowledge and tools necessary to streamline your parameter estimation tasks and obtain accurate and reliable results." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# install dependencies\n", - "#!pip install pypesto[amici,petab]" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "ExecuteTime": { - "end_time": "2023-07-13T09:24:44.842827Z", - "start_time": "2023-07-13T09:24:44.811471Z" - }, - "jupyter": { - "outputs_hidden": false - }, - "tags": [] - }, - "outputs": [], - "source": [ - "# imports\n", - "import logging\n", - "import os\n", - "import random\n", - "from pprint import pprint\n", - "\n", - "import amici\n", - "import matplotlib as mpl\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import petab\n", - "import scipy.optimize\n", - "from IPython.display import Markdown, display\n", - "\n", - "import pypesto.optimize as optimize\n", - "import pypesto.petab\n", - "import pypesto.profile as profile\n", - "import pypesto.sample as sample\n", - "import pypesto.store as store\n", - "import pypesto.visualize as visualize\n", - "import pypesto.visualize.model_fit as model_fit\n", - "\n", - "mpl.rcParams['figure.dpi'] = 100\n", - "mpl.rcParams['font.size'] = 18\n", - "\n", - "# for reproducibility\n", - "random.seed(1912)\n", - "np.random.seed(1912)\n", - "\n", - "# name of the model\n", - "model_name = \"boehm_JProteomeRes2014\"\n", - "\n", - "# output directory\n", - "model_output_dir = \"tmp/\" + model_name" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Create an objective function\n", - "\n", - "As application problem, we consider the model by [Böhm et al., JProteomRes 2014](https://pubs.acs.org/doi/abs/10.1021/pr5006923), which describes, trained on quantitative mass spectronomy data, the process of dimerization of phosphorylated STAT5A and STAT5B, important transductors of activation signals of cytokine receptors to the nucleus. The model is available via the [PEtab benchmark collection](https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab). For simulation, we use [AMICI](https://github.com/AMICI-dev/AMICI), an efficient ODE simulation and sensitivity calculation routine. [PEtab](https://github.com/PEtab-dev/PEtab) is a data format specification that standardises parameter estimation problems in systems biology.." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Without pyPESTO\n", - "\n", - "To fit an (ODE) model to data, the model needs to be implemented in a simulation program as a function mapping parameters to simulated data. Simulations must then be mapped to experimentally observed data via formulation of a single-value cost function (e.g. squared or absolute differences, corresponding to a normal or Laplace measurement noise model).\n", - "Loading the model via PEtab and AMICI already simplifies these stepss substantially compared to encoding the model and the objective function manually:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "ExecuteTime": { - "end_time": "2023-07-13T09:24:47.602789Z", - "start_time": "2023-07-13T09:24:47.547768Z" - }, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-08-25 15:26:01.472 - amici.petab_import - INFO - Importing model ...\n", - "2023-08-25 15:26:01.473 - amici.petab_import - INFO - Validating PEtab problem ...\n", - "2023-08-25 15:26:01.558 - amici.petab_import - INFO - Model name is 'FullModel'.\n", - "Writing model code to '/Users/pauljonasjost/Documents/GitHub_Folders/pyPESTO/doc/example/amici_models/FullModel'.\n", - "2023-08-25 15:26:01.560 - amici.petab_import - INFO - Species: 8\n", - "2023-08-25 15:26:01.562 - amici.petab_import - INFO - Global parameters: 15\n", - "2023-08-25 15:26:01.563 - amici.petab_import - INFO - Reactions: 9\n", - "2023-08-25 15:26:01.618 - amici.petab_import - INFO - Observables: 3\n", - "2023-08-25 15:26:01.620 - amici.petab_import - INFO - Sigmas: 3\n", - "2023-08-25 15:26:01.632 - amici.petab_import - DEBUG - Adding output parameters to model: ['noiseParameter1_pSTAT5A_rel', 'noiseParameter1_pSTAT5B_rel', 'noiseParameter1_rSTAT5A_rel']\n", - "2023-08-25 15:26:01.634 - amici.petab_import - DEBUG - Adding initial assignments for dict_keys([])\n", - "2023-08-25 15:26:01.657 - amici.petab_import - DEBUG - Condition table: (1, 1)\n", - "2023-08-25 15:26:01.658 - amici.petab_import - DEBUG - Fixed parameters are ['ratio', 'specC17']\n", - "2023-08-25 15:26:01.660 - amici.petab_import - INFO - Overall fixed parameters: 2\n", - "2023-08-25 15:26:01.661 - amici.petab_import - INFO - Variable parameters: 16\n", - "2023-08-25 15:26:01.684 - amici.sbml_import - DEBUG - Finished processing SBML annotations ++ (1.34E-04s)\n", - "2023-08-25 15:26:01.717 - amici.sbml_import - DEBUG - Finished gathering local SBML symbols ++ (2.14E-02s)\n", - "2023-08-25 15:26:01.752 - amici.sbml_import - DEBUG - Finished processing SBML parameters ++ (2.24E-02s)\n", - "2023-08-25 15:26:01.765 - amici.sbml_import - DEBUG - Finished processing SBML compartments ++ (4.04E-04s)\n", - "2023-08-25 15:26:01.788 - amici.sbml_import - DEBUG - Finished processing SBML species initials +++ (6.57E-03s)\n", - "2023-08-25 15:26:01.799 - amici.sbml_import - DEBUG - Finished processing SBML rate rules +++ (7.80E-05s)\n", - "2023-08-25 15:26:01.801 - amici.sbml_import - DEBUG - Finished processing SBML species ++ (2.70E-02s)\n", - "2023-08-25 15:26:01.818 - amici.sbml_import - DEBUG - Finished processing SBML reactions ++ (5.84E-03s)\n", - "2023-08-25 15:26:01.838 - amici.sbml_import - DEBUG - Finished processing SBML rules ++ (9.97E-03s)\n", - "2023-08-25 15:26:01.849 - amici.sbml_import - DEBUG - Finished processing SBML events ++ (8.28E-05s)\n", - "2023-08-25 15:26:01.859 - amici.sbml_import - DEBUG - Finished processing SBML initial assignments++ (1.03E-04s)\n", - "2023-08-25 15:26:01.870 - amici.sbml_import - DEBUG - Finished processing SBML species references ++ (5.32E-04s)\n", - "2023-08-25 15:26:01.871 - amici.sbml_import - DEBUG - Finished importing SBML + (1.95E-01s)\n", - "2023-08-25 15:26:01.926 - amici.sbml_import - DEBUG - Finished processing SBML observables + (4.47E-02s)\n", - "2023-08-25 15:26:01.938 - amici.sbml_import - DEBUG - Finished processing SBML event observables + (2.83E-06s)\n", - "2023-08-25 15:26:02.017 - amici.de_export - DEBUG - Finished running smart_multiply ++ (2.33E-03s)\n", - "2023-08-25 15:26:02.122 - amici.de_export - DEBUG - Finished simplifying xdot +++ (8.01E-03s)\n", - "2023-08-25 15:26:02.123 - amici.de_export - DEBUG - Finished computing xdot ++ (1.85E-02s)\n", - "2023-08-25 15:26:02.147 - amici.de_export - DEBUG - Finished simplifying x0 +++ (1.72E-03s)\n", - "2023-08-25 15:26:02.149 - amici.de_export - DEBUG - Finished computing x0 ++ (1.35E-02s)\n", - "2023-08-25 15:26:02.152 - amici.de_export - DEBUG - Finished importing SbmlImporter + (1.45E-01s)\n", - "2023-08-25 15:26:02.334 - amici.de_export - DEBUG - Finished simplifying Jy ++++ (1.42E-01s)\n", - "2023-08-25 15:26:02.335 - amici.de_export - DEBUG - Finished computing Jy +++ (1.51E-01s)\n", - "2023-08-25 15:26:02.404 - amici.de_export - DEBUG - Finished simplifying y ++++ (4.67E-02s)\n", - "2023-08-25 15:26:02.405 - amici.de_export - DEBUG - Finished computing y +++ (5.86E-02s)\n", - "2023-08-25 15:26:02.425 - amici.de_export - DEBUG - Finished simplifying sigmay ++++ (1.38E-04s)\n", - "2023-08-25 15:26:02.426 - amici.de_export - DEBUG - Finished computing sigmay +++ (8.85E-03s)\n", - "2023-08-25 15:26:02.458 - amici.de_export - DEBUG - Finished writing Jy.cpp ++ (2.84E-01s)\n", - "2023-08-25 15:26:02.523 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (3.62E-02s)\n", - "2023-08-25 15:26:02.547 - amici.de_export - DEBUG - Finished simplifying dJydsigma ++++ (1.39E-02s)\n", - "2023-08-25 15:26:02.548 - amici.de_export - DEBUG - Finished computing dJydsigma +++ (7.16E-02s)\n", - "2023-08-25 15:26:02.558 - amici.de_export - DEBUG - Finished writing dJydsigma.cpp ++ (8.93E-02s)\n", - "2023-08-25 15:26:02.601 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.81E-02s)\n", - "2023-08-25 15:26:02.629 - amici.de_export - DEBUG - Finished simplifying dJydy ++++ (1.72E-02s)\n", - "2023-08-25 15:26:02.630 - amici.de_export - DEBUG - Finished computing dJydy +++ (5.57E-02s)\n", - "2023-08-25 15:26:02.641 - amici.de_export - DEBUG - Finished writing dJydy.cpp ++ (7.41E-02s)\n", - "2023-08-25 15:26:02.669 - amici.de_export - DEBUG - Finished simplifying Jz ++++ (9.60E-05s)\n", - "2023-08-25 15:26:02.670 - amici.de_export - DEBUG - Finished computing Jz +++ (8.37E-03s)\n", - "2023-08-25 15:26:02.682 - amici.de_export - DEBUG - Finished computing z +++ (1.74E-04s)\n", - "2023-08-25 15:26:02.701 - amici.de_export - DEBUG - Finished simplifying sigmaz ++++ (1.33E-04s)\n", - "2023-08-25 15:26:02.701 - amici.de_export - DEBUG - Finished computing sigmaz +++ (7.93E-03s)\n", - "2023-08-25 15:26:02.702 - amici.de_export - DEBUG - Finished writing Jz.cpp ++ (4.87E-02s)\n", - "2023-08-25 15:26:02.729 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (7.59E-05s)\n", - "2023-08-25 15:26:02.739 - amici.de_export - DEBUG - Finished simplifying dJzdsigma ++++ (2.09E-04s)\n", - "2023-08-25 15:26:02.740 - amici.de_export - DEBUG - Finished computing dJzdsigma +++ (1.98E-02s)\n", - "2023-08-25 15:26:02.742 - amici.de_export - DEBUG - Finished writing dJzdsigma.cpp ++ (2.80E-02s)\n", - "2023-08-25 15:26:02.769 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (6.99E-05s)\n", - "2023-08-25 15:26:02.778 - amici.de_export - DEBUG - Finished simplifying dJzdz ++++ (9.28E-05s)\n", - "2023-08-25 15:26:02.779 - amici.de_export - DEBUG - Finished computing dJzdz +++ (1.78E-02s)\n", - "2023-08-25 15:26:02.780 - amici.de_export - DEBUG - Finished writing dJzdz.cpp ++ (2.73E-02s)\n", - "2023-08-25 15:26:02.807 - amici.de_export - DEBUG - Finished simplifying Jrz ++++ (1.01E-04s)\n", - "2023-08-25 15:26:02.808 - amici.de_export - DEBUG - Finished computing Jrz +++ (7.65E-03s)\n", - "2023-08-25 15:26:02.818 - amici.de_export - DEBUG - Finished computing rz +++ (2.55E-04s)\n", - "2023-08-25 15:26:02.819 - amici.de_export - DEBUG - Finished writing Jrz.cpp ++ (2.59E-02s)\n", - "2023-08-25 15:26:02.845 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (9.20E-05s)\n", - "2023-08-25 15:26:02.856 - amici.de_export - DEBUG - Finished simplifying dJrzdsigma ++++ (9.63E-05s)\n", - "2023-08-25 15:26:02.857 - amici.de_export - DEBUG - Finished computing dJrzdsigma +++ (2.05E-02s)\n", - "2023-08-25 15:26:02.858 - amici.de_export - DEBUG - Finished writing dJrzdsigma.cpp ++ (2.91E-02s)\n", - "2023-08-25 15:26:02.886 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (6.81E-05s)\n", - "2023-08-25 15:26:02.899 - amici.de_export - DEBUG - Finished simplifying dJrzdz ++++ (1.23E-04s)\n", - "2023-08-25 15:26:02.900 - amici.de_export - DEBUG - Finished computing dJrzdz +++ (2.28E-02s)\n", - "2023-08-25 15:26:02.901 - amici.de_export - DEBUG - Finished writing dJrzdz.cpp ++ (3.12E-02s)\n", - "2023-08-25 15:26:02.928 - amici.de_export - DEBUG - Finished simplifying root ++++ (1.70E-04s)\n", - "2023-08-25 15:26:02.929 - amici.de_export - DEBUG - Finished computing root +++ (9.40E-03s)\n", - "2023-08-25 15:26:02.931 - amici.de_export - DEBUG - Finished writing root.cpp ++ (1.89E-02s)\n", - "2023-08-25 15:26:02.999 - amici.de_export - DEBUG - Finished simplifying w +++++ (3.15E-02s)\n", - "2023-08-25 15:26:03.000 - amici.de_export - DEBUG - Finished computing w ++++ (4.06E-02s)\n", - "2023-08-25 15:26:03.035 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (2.51E-02s)\n", - "2023-08-25 15:26:03.055 - amici.de_export - DEBUG - Finished simplifying dwdp ++++ (1.14E-02s)\n", - "2023-08-25 15:26:03.056 - amici.de_export - DEBUG - Finished computing dwdp +++ (1.05E-01s)\n", - "2023-08-25 15:26:03.078 - amici.de_export - DEBUG - Finished simplifying spl ++++ (1.36E-04s)\n", - "2023-08-25 15:26:03.079 - amici.de_export - DEBUG - Finished computing spl +++ (8.40E-03s)\n", - "2023-08-25 15:26:03.101 - amici.de_export - DEBUG - Finished simplifying sspl ++++ (1.03E-04s)\n", - "2023-08-25 15:26:03.103 - amici.de_export - DEBUG - Finished computing sspl +++ (1.16E-02s)\n", - "2023-08-25 15:26:03.110 - amici.de_export - DEBUG - Finished writing dwdp.cpp ++ (1.66E-01s)\n", - "2023-08-25 15:26:03.219 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (8.22E-02s)\n", - "2023-08-25 15:26:03.318 - amici.de_export - DEBUG - Finished simplifying dwdx ++++ (8.91E-02s)\n", - "2023-08-25 15:26:03.319 - amici.de_export - DEBUG - Finished computing dwdx +++ (1.91E-01s)\n", - "2023-08-25 15:26:03.359 - amici.de_export - DEBUG - Finished writing dwdx.cpp ++ (2.39E-01s)\n", - "2023-08-25 15:26:03.369 - amici.de_export - DEBUG - Finished writing create_splines.cpp ++ (3.98E-04s)\n", - "2023-08-25 15:26:03.404 - amici.de_export - DEBUG - Finished simplifying spline_values +++++ (1.11E-04s)\n", - "2023-08-25 15:26:03.405 - amici.de_export - DEBUG - Finished computing spline_values ++++ (9.25E-03s)\n", - "2023-08-25 15:26:03.417 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.12E-04s)\n", - "2023-08-25 15:26:03.427 - amici.de_export - DEBUG - Finished simplifying dspline_valuesdp ++++ (9.45E-05s)\n", - "2023-08-25 15:26:03.428 - amici.de_export - DEBUG - Finished computing dspline_valuesdp +++ (3.99E-02s)\n", - "2023-08-25 15:26:03.429 - amici.de_export - DEBUG - Finished writing dspline_valuesdp.cpp ++ (4.87E-02s)\n", - "2023-08-25 15:26:03.464 - amici.de_export - DEBUG - Finished simplifying spline_slopes +++++ (1.12E-04s)\n", - "2023-08-25 15:26:03.465 - amici.de_export - DEBUG - Finished computing spline_slopes ++++ (9.20E-03s)\n", - "2023-08-25 15:26:03.476 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (7.68E-05s)\n", - "2023-08-25 15:26:03.485 - amici.de_export - DEBUG - Finished simplifying dspline_slopesdp ++++ (9.28E-05s)\n", - "2023-08-25 15:26:03.486 - amici.de_export - DEBUG - Finished computing dspline_slopesdp +++ (3.93E-02s)\n", - "2023-08-25 15:26:03.487 - amici.de_export - DEBUG - Finished writing dspline_slopesdp.cpp ++ (4.71E-02s)\n", - "2023-08-25 15:26:03.523 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (7.05E-03s)\n", - "2023-08-25 15:26:03.538 - amici.de_export - DEBUG - Finished simplifying dwdw ++++ (3.78E-03s)\n", - "2023-08-25 15:26:03.539 - amici.de_export - DEBUG - Finished computing dwdw +++ (3.13E-02s)\n", - "2023-08-25 15:26:03.543 - amici.de_export - DEBUG - Finished writing dwdw.cpp ++ (4.30E-02s)\n", - "2023-08-25 15:26:03.588 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.64E-02s)\n", - "2023-08-25 15:26:03.599 - amici.de_export - DEBUG - Finished simplifying dxdotdw ++++ (4.12E-04s)\n", - "2023-08-25 15:26:03.600 - amici.de_export - DEBUG - Finished computing dxdotdw +++ (3.57E-02s)\n", - "2023-08-25 15:26:03.610 - amici.de_export - DEBUG - Finished writing dxdotdw.cpp ++ (5.61E-02s)\n", - "2023-08-25 15:26:03.642 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.69E-03s)\n", - "2023-08-25 15:26:03.656 - amici.de_export - DEBUG - Finished simplifying dxdotdx_explicit ++++ (1.36E-04s)\n", - "2023-08-25 15:26:03.657 - amici.de_export - DEBUG - Finished computing dxdotdx_explicit +++ (2.76E-02s)\n", - "2023-08-25 15:26:03.660 - amici.de_export - DEBUG - Finished writing dxdotdx_explicit.cpp ++ (3.91E-02s)\n", - "2023-08-25 15:26:03.691 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.63E-03s)\n", - "2023-08-25 15:26:03.702 - amici.de_export - DEBUG - Finished simplifying dxdotdp_explicit ++++ (2.59E-04s)\n", - "2023-08-25 15:26:03.703 - amici.de_export - DEBUG - Finished computing dxdotdp_explicit +++ (2.19E-02s)\n", - "2023-08-25 15:26:03.705 - amici.de_export - DEBUG - Finished writing dxdotdp_explicit.cpp ++ (3.14E-02s)\n", - "2023-08-25 15:26:03.747 - amici.de_export - DEBUG - Finished running smart_jacobian +++++ (2.99E-03s)\n", - "2023-08-25 15:26:03.834 - amici.de_export - DEBUG - Finished simplifying dydx +++++ (7.46E-02s)\n", - "2023-08-25 15:26:03.834 - amici.de_export - DEBUG - Finished computing dydx ++++ (9.83E-02s)\n", - "2023-08-25 15:26:03.853 - amici.de_export - DEBUG - Finished running smart_jacobian +++++ (2.80E-04s)\n", - "2023-08-25 15:26:03.865 - amici.de_export - DEBUG - Finished simplifying dydw +++++ (1.05E-04s)\n", - "2023-08-25 15:26:03.866 - amici.de_export - DEBUG - Finished computing dydw ++++ (2.15E-02s)\n", - "2023-08-25 15:26:03.951 - amici.de_export - DEBUG - Finished simplifying dydx ++++ (7.01E-02s)\n", - "2023-08-25 15:26:03.952 - amici.de_export - DEBUG - Finished computing dydx +++ (2.25E-01s)\n", - "2023-08-25 15:26:03.981 - amici.de_export - DEBUG - Finished writing dydx.cpp ++ (2.62E-01s)\n", - "2023-08-25 15:26:04.018 - amici.de_export - DEBUG - Finished running smart_jacobian +++++ (2.60E-04s)\n", - "2023-08-25 15:26:04.028 - amici.de_export - DEBUG - Finished simplifying dydp +++++ (9.56E-05s)\n", - "2023-08-25 15:26:04.028 - amici.de_export - DEBUG - Finished computing dydp ++++ (1.88E-02s)\n", - "2023-08-25 15:26:04.039 - amici.de_export - DEBUG - Finished simplifying dydp ++++ (9.50E-05s)\n", - "2023-08-25 15:26:04.040 - amici.de_export - DEBUG - Finished computing dydp +++ (4.04E-02s)\n", - "2023-08-25 15:26:04.042 - amici.de_export - DEBUG - Finished writing dydp.cpp ++ (5.07E-02s)\n", - "2023-08-25 15:26:04.061 - amici.de_export - DEBUG - Finished computing dzdx +++ (1.50E-04s)\n", - "2023-08-25 15:26:04.061 - amici.de_export - DEBUG - Finished writing dzdx.cpp ++ (8.54E-03s)\n", - "2023-08-25 15:26:04.079 - amici.de_export - DEBUG - Finished computing dzdp +++ (1.53E-04s)\n", - "2023-08-25 15:26:04.081 - amici.de_export - DEBUG - Finished writing dzdp.cpp ++ (9.44E-03s)\n", - "2023-08-25 15:26:04.098 - amici.de_export - DEBUG - Finished computing drzdx +++ (1.45E-04s)\n", - "2023-08-25 15:26:04.099 - amici.de_export - DEBUG - Finished writing drzdx.cpp ++ (9.13E-03s)\n", - "2023-08-25 15:26:04.117 - amici.de_export - DEBUG - Finished computing drzdp +++ (1.53E-04s)\n", - "2023-08-25 15:26:04.118 - amici.de_export - DEBUG - Finished writing drzdp.cpp ++ (8.88E-03s)\n", - "2023-08-25 15:26:04.144 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (3.40E-04s)\n", - "2023-08-25 15:26:04.154 - amici.de_export - DEBUG - Finished simplifying dsigmaydy ++++ (8.96E-05s)\n", - "2023-08-25 15:26:04.154 - amici.de_export - DEBUG - Finished computing dsigmaydy +++ (2.02E-02s)\n", - "2023-08-25 15:26:04.155 - amici.de_export - DEBUG - Finished writing dsigmaydy.cpp ++ (2.86E-02s)\n", - "2023-08-25 15:26:04.183 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (1.20E-03s)\n", - "2023-08-25 15:26:04.193 - amici.de_export - DEBUG - Finished simplifying dsigmaydp ++++ (1.58E-04s)\n", - "2023-08-25 15:26:04.194 - amici.de_export - DEBUG - Finished computing dsigmaydp +++ (1.88E-02s)\n", - "2023-08-25 15:26:04.197 - amici.de_export - DEBUG - Finished writing dsigmaydp.cpp ++ (2.99E-02s)\n", - "2023-08-25 15:26:04.208 - amici.de_export - DEBUG - Finished writing sigmay.cpp ++ (1.52E-03s)\n", - "2023-08-25 15:26:04.232 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (7.05E-05s)\n", - "2023-08-25 15:26:04.243 - amici.de_export - DEBUG - Finished simplifying dsigmazdp ++++ (1.14E-04s)\n", - "2023-08-25 15:26:04.244 - amici.de_export - DEBUG - Finished computing dsigmazdp +++ (1.93E-02s)\n", - "2023-08-25 15:26:04.244 - amici.de_export - DEBUG - Finished writing dsigmazdp.cpp ++ (2.79E-02s)\n", - "2023-08-25 15:26:04.256 - amici.de_export - DEBUG - Finished writing sigmaz.cpp ++ (5.24E-05s)\n", - "2023-08-25 15:26:04.272 - amici.de_export - DEBUG - Finished computing stau +++ (1.40E-04s)\n", - "2023-08-25 15:26:04.273 - amici.de_export - DEBUG - Finished writing stau.cpp ++ (8.13E-03s)\n", - "2023-08-25 15:26:04.292 - amici.de_export - DEBUG - Finished computing deltax +++ (1.35E-04s)\n", - "2023-08-25 15:26:04.293 - amici.de_export - DEBUG - Finished writing deltax.cpp ++ (8.38E-03s)\n", - "2023-08-25 15:26:04.313 - amici.de_export - DEBUG - Finished computing deltasx +++ (2.33E-04s)\n", - "2023-08-25 15:26:04.314 - amici.de_export - DEBUG - Finished writing deltasx.cpp ++ (1.03E-02s)\n", - "2023-08-25 15:26:04.332 - amici.de_export - DEBUG - Finished writing w.cpp ++ (9.05E-03s)\n", - "2023-08-25 15:26:04.343 - amici.de_export - DEBUG - Finished writing x0.cpp ++ (1.96E-03s)\n", - "2023-08-25 15:26:04.375 - amici.de_export - DEBUG - Finished simplifying x0_fixedParameters ++++ (2.13E-03s)\n", - "2023-08-25 15:26:04.376 - amici.de_export - DEBUG - Finished computing x0_fixedParameters +++ (1.20E-02s)\n", - "2023-08-25 15:26:04.380 - amici.de_export - DEBUG - Finished writing x0_fixedParameters.cpp ++ (2.49E-02s)\n", - "2023-08-25 15:26:04.409 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (2.16E-03s)\n", - "2023-08-25 15:26:04.420 - amici.de_export - DEBUG - Finished simplifying sx0 ++++ (9.81E-05s)\n", - "2023-08-25 15:26:04.421 - amici.de_export - DEBUG - Finished computing sx0 +++ (2.15E-02s)\n", - "2023-08-25 15:26:04.422 - amici.de_export - DEBUG - Finished writing sx0.cpp ++ (3.13E-02s)\n", - "2023-08-25 15:26:04.450 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (3.50E-04s)\n", - "2023-08-25 15:26:04.460 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (3.11E-04s)\n", - "2023-08-25 15:26:04.471 - amici.de_export - DEBUG - Finished simplifying sx0_fixedParameters ++++ (1.77E-04s)\n", - "2023-08-25 15:26:04.472 - amici.de_export - DEBUG - Finished computing sx0_fixedParameters +++ (2.92E-02s)\n", - "2023-08-25 15:26:04.475 - amici.de_export - DEBUG - Finished writing sx0_fixedParameters.cpp ++ (3.96E-02s)\n", - "2023-08-25 15:26:04.499 - amici.de_export - DEBUG - Finished writing xdot.cpp ++ (1.48E-02s)\n", - "2023-08-25 15:26:04.513 - amici.de_export - DEBUG - Finished writing y.cpp ++ (4.81E-03s)\n", - "2023-08-25 15:26:04.537 - amici.de_export - DEBUG - Finished simplifying x_rdata ++++ (1.97E-04s)\n", - "2023-08-25 15:26:04.538 - amici.de_export - DEBUG - Finished computing x_rdata +++ (8.23E-03s)\n", - "2023-08-25 15:26:04.540 - amici.de_export - DEBUG - Finished writing x_rdata.cpp ++ (1.77E-02s)\n", - "2023-08-25 15:26:04.566 - amici.de_export - DEBUG - Finished simplifying total_cl ++++ (1.07E-04s)\n", - "2023-08-25 15:26:04.566 - amici.de_export - DEBUG - Finished computing total_cl +++ (8.30E-03s)\n", - "2023-08-25 15:26:04.567 - amici.de_export - DEBUG - Finished writing total_cl.cpp ++ (1.67E-02s)\n", - "2023-08-25 15:26:04.592 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (8.56E-05s)\n", - "2023-08-25 15:26:04.601 - amici.de_export - DEBUG - Finished simplifying dtotal_cldp ++++ (9.78E-05s)\n", - "2023-08-25 15:26:04.602 - amici.de_export - DEBUG - Finished computing dtotal_cldp +++ (1.75E-02s)\n", - "2023-08-25 15:26:04.603 - amici.de_export - DEBUG - Finished writing dtotal_cldp.cpp ++ (2.57E-02s)\n", - "2023-08-25 15:26:04.630 - amici.de_export - DEBUG - Finished simplifying dtotal_cldx_rdata ++++ (1.07E-04s)\n", - "2023-08-25 15:26:04.631 - amici.de_export - DEBUG - Finished computing dtotal_cldx_rdata +++ (8.89E-03s)\n", - "2023-08-25 15:26:04.632 - amici.de_export - DEBUG - Finished writing dtotal_cldx_rdata.cpp ++ (1.72E-02s)\n", - "2023-08-25 15:26:04.662 - amici.de_export - DEBUG - Finished simplifying x_solver ++++ (1.60E-04s)\n", - "2023-08-25 15:26:04.663 - amici.de_export - DEBUG - Finished computing x_solver +++ (9.63E-03s)\n", - "2023-08-25 15:26:04.666 - amici.de_export - DEBUG - Finished writing x_solver.cpp ++ (2.06E-02s)\n", - "2023-08-25 15:26:04.692 - amici.de_export - DEBUG - Finished simplifying dx_rdatadx_solver ++++ (6.79E-04s)\n", - "2023-08-25 15:26:04.693 - amici.de_export - DEBUG - Finished computing dx_rdatadx_solver +++ (9.45E-03s)\n", - "2023-08-25 15:26:04.694 - amici.de_export - DEBUG - Finished writing dx_rdatadx_solver.cpp ++ (1.80E-02s)\n", - "2023-08-25 15:26:04.722 - amici.de_export - DEBUG - Finished simplifying dx_rdatadp ++++ (8.74E-04s)\n", - "2023-08-25 15:26:04.723 - amici.de_export - DEBUG - Finished computing dx_rdatadp +++ (1.08E-02s)\n", - "2023-08-25 15:26:04.725 - amici.de_export - DEBUG - Finished writing dx_rdatadp.cpp ++ (2.03E-02s)\n", - "2023-08-25 15:26:04.749 - amici.de_export - DEBUG - Finished running smart_jacobian ++++ (6.97E-05s)\n", - "2023-08-25 15:26:04.758 - amici.de_export - DEBUG - Finished simplifying dx_rdatadtcl ++++ (8.82E-05s)\n", - "2023-08-25 15:26:04.759 - amici.de_export - DEBUG - Finished computing dx_rdatadtcl +++ (1.66E-02s)\n", - "2023-08-25 15:26:04.760 - amici.de_export - DEBUG - Finished writing dx_rdatadtcl.cpp ++ (2.47E-02s)\n", - "2023-08-25 15:26:04.770 - amici.de_export - DEBUG - Finished writing z.cpp ++ (4.95E-05s)\n", - "2023-08-25 15:26:04.779 - amici.de_export - DEBUG - Finished writing rz.cpp ++ (5.57E-05s)\n", - "2023-08-25 15:26:04.812 - amici.de_export - DEBUG - Finished generating cpp code + (2.65E+00s)\n", - "2023-08-25 15:26:59.498 - amici.de_export - DEBUG - Finished compiling cpp code + (5.47E+01s)\n", - "2023-08-25 15:26:59.945 - amici.petab_import - INFO - Finished Importing PEtab model (5.85E+01s)\n", - "2023-08-25 15:26:59.954 - amici.petab_import - INFO - Successfully loaded model FullModel from pyPESTO/doc/example/amici_models/FullModel.\n" - ] - } - ], - "source": [ - "%%capture\n", - "# PEtab problem loading\n", - "petab_yaml = f\"./{model_name}/{model_name}.yaml\"\n", - "\n", - "petab_problem = petab.Problem.from_yaml(petab_yaml)\n", - "\n", - "# AMICI model complilation\n", - "amici_model = amici.petab_import.import_petab_problem(\n", - " petab_problem, force_compile=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "AMICI allows us to construct an objective function from the PEtab problem, already considering the noise distribution assumed for this model. We can also simulate the problem for a parameter with this simple setup." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "ExecuteTime": { - "end_time": "2023-07-13T09:24:50.218430Z", - "start_time": "2023-07-13T09:24:48.971684Z" - }, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "PEtab benchmark parameters\n", - "{'edatas': [::value_type' at 0x12c917d80\n", - " condition 'model1_data1' starting at t=0.0 with custom parameter scales, constants, parameters\n", - " 16x3 time-resolved datapoints\n", - " (48/48 measurements & 0/48 sigmas set)\n", - " 10x0 event-resolved datapoints\n", - " (0/0 measurements & 0/0 sigmas set)\n", - ">],\n", - " 'llh': -138.22199656856435,\n", - " 'rdatas': [::pointer' at 0x12c917c60> >)>],\n", - " 'sllh': None}\n", - "Individualized parameters\n", - "{'edatas': [::value_type' at 0x12c7dadf0\n", - " condition 'model1_data1' starting at t=0.0 with custom parameter scales, constants, parameters\n", - " 16x3 time-resolved datapoints\n", - " (48/48 measurements & 0/48 sigmas set)\n", - " 10x0 event-resolved datapoints\n", - " (0/0 measurements & 0/0 sigmas set)\n", - ">],\n", - " 'llh': -185.54291970899519,\n", - " 'rdatas': [::pointer' at 0x12c917060> >)>],\n", - " 'sllh': None}\n" - ] - } - ], - "source": [ - "# Simulation with PEtab nominal parameter values\n", - "print(\"PEtab benchmark parameters\")\n", - "pprint(amici.petab_objective.simulate_petab(petab_problem, amici_model))\n", - "\n", - "# Simulation with specified parameter values\n", - "parameters = np.array([-1.5, -5.0, -2.2, -1.7, 5.0, 4.2, 0.5, 0.8, 0.5])\n", - "ids = list(amici_model.getParameterIds())\n", - "ids[6:] = [\"sd_pSTAT5A_rel\", \"sd_pSTAT5B_rel\", \"sd_rSTAT5A_rel\"]\n", - "\n", - "print(\"Individualized parameters\")\n", - "pprint(\n", - " amici.petab_objective.simulate_petab(\n", - " petab_problem,\n", - " amici_model,\n", - " problem_parameters={x_id: x_i for x_id, x_i in zip(ids, parameters)},\n", - " scaled_parameters=True,\n", - " )\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We see that to call the objective function, we need to supply the parameters in a dictionary format. This is not really suitable for parameter estimation, as e.g. optimization packages usually work with (numpy) arrays. Therefore we need to create some kind of parameter mapping." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "185.54291970899519\n" - ] - } - ], - "source": [ - "class Objective:\n", - " \"\"\"\n", - " A very basic implementation to an objective function for AMICI, that can call the objective function just based on the parameters.\n", - " \"\"\"\n", - "\n", - " def __init__(self, petab_problem: petab.Problem, model: amici.Model):\n", - " \"\"\"Constructor for objective.\"\"\"\n", - " self.petab_problem = petab_problem\n", - " self.model = model\n", - " self.x_ids = list(self.model.getParameterIds())\n", - " # nned to change the names for the last ones\n", - " self.x_ids[6:] = [\"sd_pSTAT5A_rel\", \"sd_pSTAT5B_rel\", \"sd_rSTAT5A_rel\"]\n", - "\n", - " def x_dct(self, x: np.ndarray):\n", - " \"\"\"\n", - " Turn array of parameters to dictionary usable for objective call.\n", - " \"\"\"\n", - " return {x_id: x_i for x_id, x_i in zip(self.x_ids, x)}\n", - "\n", - " def __call__(self, x: np.ndarray):\n", - " \"\"\"Call the objective function\"\"\"\n", - " return -amici.petab_objective.simulate_petab(\n", - " petab_problem,\n", - " amici_model,\n", - " problem_parameters=self.x_dct(x),\n", - " scaled_parameters=True,\n", - " )[\"llh\"]\n", - "\n", - "\n", - "# Test it out\n", - "obj = Objective(petab_problem, amici_model)\n", - "pprint(obj(parameters))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Summary\n", - "\n", - "We have constructed a very basic functioning objective function for this specific problem. AMICI and PEtab already simplify the workflow compared to coding the objective from scratch, however there are still some shortcomings. Some things that we have not yet considered and that would require additional code are:\n", - "\n", - "* What if we have **multiple simulation conditions**? We would have to adjust the parameter mapping, to use the correct parameters and constants for each condition, and aggregate results.\n", - "* What if our system starts in a **steady state**? In this case we would have to preequilibrate, something that AMICI can do, but we would need to account for that additionally (and it would most likely also include an additional simulation condition).\n", - "* For later analysis we would like to be able to not only get the objective function but also the **residuals**, something that we can change in AMICI but we would have to account for this flexibility additionally.\n", - "* If we **fix a parameter** (i.e. keeping it constant while optimizing the remaining parameters), we would have to create a different parameter mapping (same for unfixing a parameter).\n", - "* What if we want to include **prior knowledge** of parameters?\n", - "* AMICI can also calculate **sensitivities** (`sllh` in the above output). During parameter estimation, the inference (optimization/sampling/...) algorithm typically calls the objective function many times both with and without sensitivities. Thus, we need to implement the ability to call e.g. function value, gradient and Hessian matrix (or an approximation thereof), as well as combinations of these for efficiency.\n", - "\n", - "This is most likely not the complete list but can already can get yield quite substantial coding effort. While each problem can be tackled, it is a lot of code lines, and we would need to rewrite it each time if we want to change something (or invest even more work and make the design of the objective function flexible).\n", - "\n", - "In short: **There is a need for a tool that can account for all these variables in the objective function formulation**." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### With pyPESTO\n", - "\n", - "All the above is easily addressed by using pyPESTO's objective function implementation. We support a multitude of objective functions (JAX, Aesara, AMICI, Julia, self-written). For PEtab models with AMICI, we take care of the parameter mapping, multiple simulation conditions (including preequilibration), changing between residuals and objective function, fixing parameters, and sensitivity calculation.\n", - "\n", - "While there is a lot of possibility for individualization, in its most basic form creating an objective from a petab file accounting for all of the above boils down to just a few lines in pyPESTO:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(185.5429188951038,\n", - " array([ 4.96886729e+02, 1.50715517e-01, 4.44258325e+01, 7.14778242e+02,\n", - " -5.19647592e-05, -1.66953531e+02, -1.50846999e+02, -6.86643591e+01,\n", - " -1.59022641e+01]))" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "petab_yaml = f\"./{model_name}/{model_name}.yaml\"\n", - "\n", - "petab_problem = petab.Problem.from_yaml(petab_yaml)\n", - "\n", - "# import the petab problem and generate a pypesto problem\n", - "importer = pypesto.petab.PetabImporter(petab_problem)\n", - "problem = importer.create_problem()\n", - "\n", - "# call the objective to get the objective function value and (additionally) the gradient\n", - "problem.objective(parameters, sensi_orders=(0, 1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Optimization\n", - "\n", - "After creating our objective function, we can now set up an optimization problem to find model parameters that best describe the data. For this we will need\n", - "* parameter bounds (to restrict the search area; these are usually based on domain knowledge)\n", - "* startpoints for the multistart local optimization (as dynamical system constrained optimization problems are in most cases non-convex)\n", - "* an optimizer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Without pyPESTO" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 483.97739572289964\n", - " x: [-3.352e+00 -2.216e+00 -1.774e+00 -1.776e+00 6.234e-01\n", - " 3.661e+00 1.261e+00 6.150e-01 2.480e-01]\n", - " nit: 18\n", - " jac: [ 2.192e+02 3.002e+02 7.278e+02 -2.483e+02 3.634e+02\n", - " -3.849e+01 -3.175e+01 -1.073e+03 -4.504e+02]\n", - " nfev: 520\n", - " njev: 52\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 242.1633515170673\n", - " x: [-1.690e+00 1.599e+00 4.108e+00 -2.908e+00 4.344e+00\n", - " 2.980e+00 2.152e+00 1.482e+00 1.401e+00]\n", - " nit: 19\n", - " jac: [-1.918e+01 -8.125e+00 8.223e+00 -3.257e+00 -1.422e+01\n", - " -1.719e+01 3.437e+01 -1.304e+01 3.139e+01]\n", - " nfev: 520\n", - " njev: 52\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 214.20136458346468\n", - " x: [-6.979e-02 -3.372e+00 -1.727e+00 4.216e+00 -4.263e+00\n", - " 4.777e+00 1.342e+00 1.486e+00 1.120e+00]\n", - " nit: 24\n", - " jac: [ 1.194e+01 -6.911e-01 -5.608e-01 -4.737e-01 -1.027e+00\n", - " -1.151e+01 -5.561e+00 1.893e+01 -1.621e+01]\n", - " nfev: 530\n", - " njev: 53\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.74599720689707\n", - " x: [ 1.320e+00 -2.696e+00 -5.719e-02 2.128e+00 -9.272e-01\n", - " -1.710e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 22\n", - " jac: [ 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00\n", - " -5.684e-06 5.684e-06 5.684e-06 0.000e+00]\n", - " nfev: 270\n", - " njev: 27\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 211.80059302429987\n", - " x: [-5.000e+00 4.982e+00 -4.710e+00 -4.963e+00 -4.994e+00\n", - " 4.055e+00 1.533e+00 1.645e+00 7.582e-01]\n", - " nit: 15\n", - " jac: [ 1.543e+01 -5.858e-01 -1.036e+00 6.517e+00 -7.668e+00\n", - " -2.808e+00 1.728e-01 4.503e+00 1.388e+00]\n", - " nfev: 520\n", - " njev: 52\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 223.69767170122753\n", - " x: [-2.092e+00 -1.184e+00 4.852e+00 -3.410e+00 -4.006e-01\n", - " 3.576e+00 1.544e+00 1.486e+00 1.247e+00]\n", - " nit: 35\n", - " jac: [-2.892e+00 1.587e+01 5.931e+00 -7.305e+00 -5.950e+00\n", - " 4.433e+00 1.067e+01 -2.547e+01 2.397e+01]\n", - " nfev: 530\n", - " njev: 53\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 237.2482338229555\n", - " x: [-3.714e-01 4.988e+00 -5.000e+00 4.999e+00 -4.999e+00\n", - " 5.000e+00 1.853e+00 1.778e+00 1.323e+00]\n", - " nit: 24\n", - " jac: [-4.974e-04 1.434e+00 1.573e-01 -1.395e-01 -2.983e-02\n", - " 3.767e+00 3.086e+01 2.687e+01 3.920e+00]\n", - " nfev: 510\n", - " njev: 51\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.74599744332093\n", - " x: [ 2.623e+00 -3.868e+00 4.241e+00 6.765e-01 4.940e+00\n", - " -4.530e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 26\n", - " jac: [ 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00\n", - " 0.000e+00 5.684e-06 -2.842e-06 -1.137e-05]\n", - " nfev: 450\n", - " njev: 45\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.74599618803322\n", - " x: [-5.000e+00 -1.669e+00 4.782e+00 3.631e+00 -4.844e+00\n", - " -4.694e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 20\n", - " jac: [ 5.684e-06 0.000e+00 0.000e+00 5.684e-06 -2.842e-06\n", - " 0.000e+00 -2.842e-06 2.842e-06 5.684e-06]\n", - " nfev: 430\n", - " njev: 43\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.74599529669632\n", - " x: [-5.000e+00 -5.000e+00 5.000e+00 -5.000e+00 -5.000e+00\n", - " -5.000e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 15\n", - " jac: [ 0.000e+00 0.000e+00 -0.000e+00 0.000e+00 0.000e+00\n", - " 0.000e+00 -2.842e-06 5.684e-06 2.842e-06]\n", - " nfev: 390\n", - " njev: 39\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 213.3930555660625\n", - " x: [-4.976e+00 4.990e+00 4.959e+00 -4.994e+00 -5.000e+00\n", - " 4.930e+00 1.550e+00 1.674e+00 7.217e-01]\n", - " nit: 14\n", - " jac: [ 1.565e+00 7.910e-02 1.781e+00 1.024e+00 1.101e+00\n", - " 2.874e+00 3.555e-01 4.905e-01 -4.516e-01]\n", - " nfev: 510\n", - " njev: 51\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.7459952943729\n", - " x: [-5.000e+00 -5.000e+00 -5.000e+00 -5.000e+00 -5.000e+00\n", - " -5.000e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 15\n", - " jac: [ 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00\n", - " -2.842e-06 -2.842e-06 -2.842e-06 -2.842e-06]\n", - " nfev: 360\n", - " njev: 36\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 249.74612304678104\n", - " x: [ 5.000e+00 -5.000e+00 -5.000e+00 5.000e+00 5.000e+00\n", - " 4.999e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 24\n", - " jac: [-2.842e-04 8.527e-06 -2.842e-06 0.000e+00 -8.527e-06\n", - " 3.411e-04 -3.351e-03 4.059e-03 -4.007e-04]\n", - " nfev: 510\n", - " njev: 51\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.74599744228198\n", - " x: [ 2.788e+00 -3.974e+00 3.981e+00 4.062e+00 -4.665e+00\n", - " -3.281e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 14\n", - " jac: [ 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00\n", - " 0.000e+00 0.000e+00 0.000e+00 -5.684e-06]\n", - " nfev: 480\n", - " njev: 48\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 249.5523160565121\n", - " x: [-2.706e+00 -3.267e+00 1.257e+00 2.064e+00 1.969e-01\n", - " 1.412e+00 1.874e+00 1.758e+00 1.297e+00]\n", - " nit: 15\n", - " jac: [-2.522e-01 -1.616e-01 6.545e-01 1.178e+00 4.326e-01\n", - " -2.745e-01 2.402e-01 -5.134e-02 4.700e-01]\n", - " nfev: 530\n", - " njev: 53\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 249.74599444226348\n", - " x: [-4.686e+00 2.111e+00 2.352e+00 3.564e+00 3.211e+00\n", - " 3.715e-01 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 36\n", - " jac: [ 5.684e-06 5.684e-06 -5.684e-06 5.684e-06 5.684e-06\n", - " -8.527e-06 -9.237e-04 2.177e-03 -2.240e-03]\n", - " nfev: 520\n", - " njev: 52\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 235.88920140413381\n", - " x: [-3.579e+00 3.028e+00 4.795e+00 4.039e+00 -1.795e+00\n", - " 3.737e+00 1.784e+00 1.808e+00 1.216e+00]\n", - " nit: 20\n", - " jac: [ 3.365e+00 3.241e+00 1.683e+00 2.220e+00 9.867e-01\n", - " -3.602e-01 2.902e+01 3.113e+01 -1.724e+01]\n", - " nfev: 610\n", - " njev: 61\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 220.02415658728512\n", - " x: [-4.987e+00 -4.995e+00 4.993e+00 -4.992e+00 4.825e+00\n", - " 3.875e+00 1.449e+00 1.627e+00 1.031e+00]\n", - " nit: 15\n", - " jac: [ 2.576e+00 -6.263e-01 1.389e+00 2.389e-01 7.329e-01\n", - " 9.629e-01 -4.125e-01 -6.170e-01 -1.864e+00]\n", - " nfev: 510\n", - " njev: 51\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 213.09054793943582\n", - " x: [-4.864e+00 -4.913e+00 3.790e+00 -4.928e+00 -4.981e+00\n", - " 4.780e+00 1.559e+00 1.668e+00 7.252e-01]\n", - " nit: 21\n", - " jac: [ 3.923e+00 1.645e+00 1.015e+01 6.265e+00 4.520e+00\n", - " 1.086e+01 2.107e+00 4.310e-01 3.321e-01]\n", - " nfev: 550\n", - " njev: 55\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 233.42558246297477\n", - " x: [-4.957e+00 4.967e+00 4.933e+00 4.973e+00 -3.787e+00\n", - " 4.875e+00 1.551e+00 1.644e+00 1.318e+00]\n", - " nit: 27\n", - " jac: [-2.601e-01 -1.059e+00 6.951e-01 -9.444e-01 3.608e-01\n", - " 3.734e+00 2.608e+00 -1.406e+00 3.095e+00]\n", - " nfev: 530\n", - " njev: 53\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.7459534303374\n", - " x: [-6.783e-01 -5.000e+00 5.000e+00 -5.000e+00 -6.724e-01\n", - " -2.462e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 13\n", - " jac: [-3.411e-05 2.785e-04 -1.421e-05 -1.222e-04 -3.979e-05\n", - " -8.527e-05 1.705e-05 2.558e-05 -7.390e-05]\n", - " nfev: 290\n", - " njev: 29\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\n", - " success: True\n", - " status: 0\n", - " fun: 249.7459974433216\n", - " x: [ 2.690e+00 -1.853e+00 2.859e+00 4.703e+00 4.025e+00\n", - " -4.232e+00 1.873e+00 1.759e+00 1.299e+00]\n", - " nit: 20\n", - " jac: [ 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00\n", - " 0.000e+00 0.000e+00 5.684e-06 5.684e-06]\n", - " nfev: 250\n", - " njev: 25\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 249.4079576472867\n", - " x: [-1.928e+00 -6.320e-01 2.840e+00 2.231e+00 -2.678e+00\n", - " 1.019e+00 1.870e+00 1.756e+00 1.299e+00]\n", - " nit: 10\n", - " jac: [ 1.235e+00 9.780e-01 6.752e-01 1.105e-01 2.556e+00\n", - " 1.004e-01 4.811e-01 5.480e-02 -4.183e-02]\n", - " nfev: 580\n", - " njev: 58\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 200.88846803388742\n", - " x: [-8.127e-01 3.724e+00 1.937e+00 -2.592e+00 4.676e+00\n", - " 3.893e+00 1.392e+00 1.065e+00 1.110e+00]\n", - " nit: 20\n", - " jac: [ 6.924e+01 4.523e+00 5.096e+00 1.620e+01 7.100e+00\n", - " -9.445e+01 9.871e+00 -4.982e+01 3.366e+01]\n", - " nfev: 690\n", - " njev: 69\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>,\n", - " message: STOP: TOTAL NO. of f AND g EVALUATIONS EXCEEDS LIMIT\n", - " success: False\n", - " status: 1\n", - " fun: 216.78983787278423\n", - " x: [ 9.620e-02 1.241e+00 3.601e+00 1.566e+00 -4.929e+00\n", - " 5.000e+00 1.412e+00 1.423e+00 1.287e+00]\n", - " nit: 28\n", - " jac: [ 8.744e-01 -2.230e-02 -1.398e-01 -4.002e-03 -2.683e-01\n", - " -6.436e-01 4.931e+00 1.177e+01 -2.101e+00]\n", - " nfev: 510\n", - " njev: 51\n", - " hess_inv: <9x9 LbfgsInvHessProduct with dtype=float64>]\n" - ] - } - ], - "source": [ - "# bounds\n", - "ub = 5 * np.ones(len(parameters))\n", - "lb = -5 * np.ones(len(parameters))\n", - "\n", - "# number of starts\n", - "n_starts = 25\n", - "\n", - "# draw uniformly distributed parameters within these bounds\n", - "x_guesses = np.random.random((n_starts, len(lb))) * (ub - lb) + lb\n", - "\n", - "# optimize\n", - "results = []\n", - "for x0 in x_guesses:\n", - " results.append(\n", - " scipy.optimize.minimize(\n", - " obj,\n", - " x0,\n", - " bounds=zip(lb, ub),\n", - " tol=1e-12,\n", - " options={\"maxfun\": 500},\n", - " method=\"L-BFGS-B\",\n", - " )\n", - " )\n", - "pprint(results)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We might want to change the optimizer, like e.g. [NLopt](https://nlopt.readthedocs.io/en/latest/)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[array([-4.72537584, -3.8267521 , 0.17032373, 0.5309411 , 1.82691101,\n", - " -0.13738387, -2.19222726, 2.40671846, -2.17865902]),\n", - " array([-4.85782185, 0.88722054, 3.46323867, -0.42272524, 2.97501061,\n", - " -0.44685985, -1.9617645 , 4.19526924, -0.68191772]),\n", - " array([ 2.75539723, -3.45625301, -1.74304845, 4.19486719, -4.36152045,\n", - " 3.20102079, 4.1666204 , 4.5071745 , 1.64830203]),\n", - " array([-0.13272644, -1.78655792, -2.05292081, 4.94102789, 0.68000657,\n", - " -0.41145952, 4.43118647, 2.86292729, -2.27641822]),\n", - " array([ 1.5071923 , 3.23408298, 4.40175342, -4.93504248, -3.68651524,\n", - " 4.89047865, -3.50203955, -3.98810331, -0.60343463]),\n", - " array([ 3.59031468, -0.64508741, 4.57795683, -4.55808472, 1.45169025,\n", - " 0.16191615, -0.9214029 , 1.8818166 , -2.04635126]),\n", - " array([-3.69691906, -1.11149925, -2.07266599, -1.6551983 , -1.05891694,\n", - " 0.25590375, 0.87136513, -1.83339326, -2.29220816]),\n", - " array([ 2.62294851, -3.86768587, 4.24057642, 0.67648706, 4.94028248,\n", - " -4.53014752, -4.41436998, 0.48069498, 2.08662195]),\n", - " array([ 1.11811741, -1.66877199, 4.78163474, 3.63123695, -4.84414353,\n", - " -4.69389636, -4.22521978, 1.05436896, 1.66464083]),\n", - " array([ 2.10705352, -4.17386158, -4.95145244, -0.4940422 , 2.44773506,\n", - " -0.72754709, -3.38821849, 4.3015123 , -4.03270095]),\n", - " array([-1.21935294, 4.99254589, -3.5227032 , -4.57026229, -4.27577682,\n", - " 1.65134668, -2.57941689, 3.3876373 , -3.08581727]),\n", - " array([-2.69338089, 1.3336723 , 4.00935726, 4.23436455, -4.97880599,\n", - " 0.66011236, -0.92734049, -0.72506365, -1.95148656]),\n", - " array([ 4.59360518, -1.45672536, 2.53472283, 1.59953602, 4.74752881,\n", - " 2.97708352, -1.75879731, 1.52861569, -4.47452224]),\n", - " array([ 2.78765335, -3.97396464, 3.98103304, 4.06162031, -4.66533684,\n", - " -3.28137522, 3.15208208, -2.66502967, -4.85197795]),\n", - " array([-2.01253459, -2.8480651 , 3.12268386, 1.19351138, -0.60901754,\n", - " 0.29935873, 3.43245553, 4.09645236, -0.05881582]),\n", - " array([-4.51736894, 2.39365053, -0.85230688, 2.93845516, 3.92387757,\n", - " 3.35653866, 4.52675063, 1.98365382, 3.80369101]),\n", - " array([-1.17029265, 3.16644483, 4.85261058, 4.35300099, 1.26174191,\n", - " 0.82811007, 4.66370112, 3.96059639, 3.24314499]),\n", - " array([-2.59120601, 0.69656874, -3.88289712, -1.74846428, -1.58175173,\n", - " 3.39830011, 0.0917892 , -0.85030875, -3.77417568]),\n", - " array([ 2.45781935, 1.53870162, -0.24553228, 1.49870916, -3.42788561,\n", - " 4.98603203, 3.19947195, -4.22036418, 0.83316028]),\n", - " array([ 2.20258998, 4.3092804 , -2.2015135 , -1.86005028, 4.82608847,\n", - " 2.24886943, -1.09100022, -1.53563431, 0.22579574]),\n", - " array([-0.53682615, -4.81637178, 4.95364701, -4.57752447, -0.60577667,\n", - " -2.88604054, 0.85270085, 0.07054205, -1.27549967]),\n", - " array([ 2.69036786, -1.85327759, 2.85858288, 4.70288006, 4.02501682,\n", - " -4.23172439, -0.72188414, 3.55067703, 4.87046828]),\n", - " array([-1.88273814, -0.60977486, 2.87775688, 2.25140401, -2.65489216,\n", - " 1.01047951, 3.69127283, 4.44893301, 2.65440911]),\n", - " array([0.66867122, 3.97870907, 2.13991535, 0.2612146 , 4.9597103 ,\n", - " 3.23568699, 4.22366861, 0.40266923, 3.15201217]),\n", - " array([ 4.03440768, 0.83760837, 3.55179682, 1.57898613, -4.87401594,\n", - " 4.33049155, 3.79628273, 1.90990052, 1.02667127])]\n" - ] - } - ], - "source": [ - "import nlopt\n", - "\n", - "opt = nlopt.opt(\n", - " nlopt.LD_LBFGS, len(parameters)\n", - ") # only one of many possible options\n", - "\n", - "opt.set_lower_bounds(lb)\n", - "opt.set_upper_bounds(ub)\n", - "\n", - "\n", - "def nlopt_objective(x, grad):\n", - " \"\"\"We need a wrapper function of the kind f(x,grad) for nlopt.\"\"\"\n", - " r = obj(x)\n", - " return r\n", - "\n", - "\n", - "opt.set_min_objective(nlopt_objective)\n", - "\n", - "\n", - "results = []\n", - "for x0 in x_guesses:\n", - " try:\n", - " result = opt.optimize(x0)\n", - " except (\n", - " nlopt.RoundoffLimited,\n", - " nlopt.ForcedStop,\n", - " ValueError,\n", - " RuntimeError,\n", - " MemoryError,\n", - " ) as e:\n", - " result = None\n", - " results.append(result)\n", - "\n", - "pprint(results)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can already see that the NLopt library takes different arguments and has a different result output than scipy. In order to be able to compare them, we need to modify the code again. We would at the very least like the end objective function value, our starting value and some kind of exit message." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[{'exitflag': 1,\n", - " 'fun': 1150653011.8393009,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-4.72537584, -3.8267521 , 0.17032373, 0.5309411 , 1.82691101,\n", - " -0.13738387, -2.19222726, 2.40671846, -2.17865902]),\n", - " 'x0': array([-4.72537584, -3.8267521 , 0.17032373, 0.5309411 , 1.82691101,\n", - " -0.13738387, -2.19222726, 2.40671846, -2.17865902])},\n", - " {'exitflag': 1,\n", - " 'fun': 373210706.087737,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-4.85782185, 0.88722054, 3.46323867, -0.42272524, 2.97501061,\n", - " -0.44685985, -1.9617645 , 4.19526924, -0.68191772]),\n", - " 'x0': array([-4.85782185, 0.88722054, 3.46323867, -0.42272524, 2.97501061,\n", - " -0.44685985, -1.9617645 , 4.19526924, -0.68191772])},\n", - " {'exitflag': 1,\n", - " 'fun': 425.9896608503201,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.75539723, -3.45625301, -1.74304845, 4.19486719, -4.36152045,\n", - " 3.20102079, 4.1666204 , 4.5071745 , 1.64830203]),\n", - " 'x0': array([ 2.75539723, -3.45625301, -1.74304845, 4.19486719, -4.36152045,\n", - " 3.20102079, 4.1666204 , 4.5071745 , 1.64830203])},\n", - " {'exitflag': 1,\n", - " 'fun': 113150729.31030881,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-0.13272644, -1.78655792, -2.05292081, 4.94102789, 0.68000657,\n", - " -0.41145952, 4.43118647, 2.86292729, -2.27641822]),\n", - " 'x0': array([-0.13272644, -1.78655792, -2.05292081, 4.94102789, 0.68000657,\n", - " -0.41145952, 4.43118647, 2.86292729, -2.27641822])},\n", - " {'exitflag': 1,\n", - " 'fun': 2170773400755.13,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 1.5071923 , 3.23408298, 4.40175342, -4.93504248, -3.68651524,\n", - " 4.89047865, -3.50203955, -3.98810331, -0.60343463]),\n", - " 'x0': array([ 1.5071923 , 3.23408298, 4.40175342, -4.93504248, -3.68651524,\n", - " 4.89047865, -3.50203955, -3.98810331, -0.60343463])},\n", - " {'exitflag': 1,\n", - " 'fun': 42320078.18806582,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 3.59031468, -0.64508741, 4.57795683, -4.55808472, 1.45169025,\n", - " 0.16191615, -0.9214029 , 1.8818166 , -2.04635126]),\n", - " 'x0': array([ 3.59031468, -0.64508741, 4.57795683, -4.55808472, 1.45169025,\n", - " 0.16191615, -0.9214029 , 1.8818166 , -2.04635126])},\n", - " {'exitflag': 1,\n", - " 'fun': 243163763.80728397,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-3.69691906, -1.11149925, -2.07266599, -1.6551983 , -1.05891694,\n", - " 0.25590375, 0.87136513, -1.83339326, -2.29220816]),\n", - " 'x0': array([-3.69691906, -1.11149925, -2.07266599, -1.6551983 , -1.05891694,\n", - " 0.25590375, 0.87136513, -1.83339326, -2.29220816])},\n", - " {'exitflag': 1,\n", - " 'fun': 30002126964787.39,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.62294851, -3.86768587, 4.24057642, 0.67648706, 4.94028248,\n", - " -4.53014752, -4.41436998, 0.48069498, 2.08662195]),\n", - " 'x0': array([ 2.62294851, -3.86768587, 4.24057642, 0.67648706, 4.94028248,\n", - " -4.53014752, -4.41436998, 0.48069498, 2.08662195])},\n", - " {'exitflag': 1,\n", - " 'fun': 12556009991670.39,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 1.11811741, -1.66877199, 4.78163474, 3.63123695, -4.84414353,\n", - " -4.69389636, -4.22521978, 1.05436896, 1.66464083]),\n", - " 'x0': array([ 1.11811741, -1.66877199, 4.78163474, 3.63123695, -4.84414353,\n", - " -4.69389636, -4.22521978, 1.05436896, 1.66464083])},\n", - " {'exitflag': 1,\n", - " 'fun': 634294830118.3749,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.10705352, -4.17386158, -4.95145244, -0.4940422 , 2.44773506,\n", - " -0.72754709, -3.38821849, 4.3015123 , -4.03270095]),\n", - " 'x0': array([ 2.10705352, -4.17386158, -4.95145244, -0.4940422 , 2.44773506,\n", - " -0.72754709, -3.38821849, 4.3015123 , -4.03270095])},\n", - " {'exitflag': 1,\n", - " 'fun': 9986849980.33512,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-1.21935294, 4.99254589, -3.5227032 , -4.57026229, -4.27577682,\n", - " 1.65134668, -2.57941689, 3.3876373 , -3.08581727]),\n", - " 'x0': array([-1.21935294, 4.99254589, -3.5227032 , -4.57026229, -4.27577682,\n", - " 1.65134668, -2.57941689, 3.3876373 , -3.08581727])},\n", - " {'exitflag': 1,\n", - " 'fun': 29198157.935426034,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-2.69338089, 1.3336723 , 4.00935726, 4.23436455, -4.97880599,\n", - " 0.66011236, -0.92734049, -0.72506365, -1.95148656]),\n", - " 'x0': array([-2.69338089, 1.3336723 , 4.00935726, 4.23436455, -4.97880599,\n", - " 0.66011236, -0.92734049, -0.72506365, -1.95148656])},\n", - " {'exitflag': 1,\n", - " 'fun': 2817631865279.2437,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 4.59360518, -1.45672536, 2.53472283, 1.59953602, 4.74752881,\n", - " 2.97708352, -1.75879731, 1.52861569, -4.47452224]),\n", - " 'x0': array([ 4.59360518, -1.45672536, 2.53472283, 1.59953602, 4.74752881,\n", - " 2.97708352, -1.75879731, 1.52861569, -4.47452224])},\n", - " {'exitflag': 1,\n", - " 'fun': 16029712077044.059,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.78765335, -3.97396464, 3.98103304, 4.06162031, -4.66533684,\n", - " -3.28137522, 3.15208208, -2.66502967, -4.85197795]),\n", - " 'x0': array([ 2.78765335, -3.97396464, 3.98103304, 4.06162031, -4.66533684,\n", - " -3.28137522, 3.15208208, -2.66502967, -4.85197795])},\n", - " {'exitflag': 1,\n", - " 'fun': 4468.484751390617,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-2.01253459, -2.8480651 , 3.12268386, 1.19351138, -0.60901754,\n", - " 0.29935873, 3.43245553, 4.09645236, -0.05881582]),\n", - " 'x0': array([-2.01253459, -2.8480651 , 3.12268386, 1.19351138, -0.60901754,\n", - " 0.29935873, 3.43245553, 4.09645236, -0.05881582])},\n", - " {'exitflag': 1,\n", - " 'fun': 426.9335166325137,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-4.51736894, 2.39365053, -0.85230688, 2.93845516, 3.92387757,\n", - " 3.35653866, 4.52675063, 1.98365382, 3.80369101]),\n", - " 'x0': array([-4.51736894, 2.39365053, -0.85230688, 2.93845516, 3.92387757,\n", - " 3.35653866, 4.52675063, 1.98365382, 3.80369101])},\n", - " {'exitflag': 1,\n", - " 'fun': 481.3231591295339,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-1.17029265, 3.16644483, 4.85261058, 4.35300099, 1.26174191,\n", - " 0.82811007, 4.66370112, 3.96059639, 3.24314499]),\n", - " 'x0': array([-1.17029265, 3.16644483, 4.85261058, 4.35300099, 1.26174191,\n", - " 0.82811007, 4.66370112, 3.96059639, 3.24314499])},\n", - " {'exitflag': 1,\n", - " 'fun': 36260050961.21613,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-2.59120601, 0.69656874, -3.88289712, -1.74846428, -1.58175173,\n", - " 3.39830011, 0.0917892 , -0.85030875, -3.77417568]),\n", - " 'x0': array([-2.59120601, 0.69656874, -3.88289712, -1.74846428, -1.58175173,\n", - " 3.39830011, 0.0917892 , -0.85030875, -3.77417568])},\n", - " {'exitflag': 1,\n", - " 'fun': 7147839056555.6,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.45781935, 1.53870162, -0.24553228, 1.49870916, -3.42788561,\n", - " 4.98603203, 3.19947195, -4.22036418, 0.83316028]),\n", - " 'x0': array([ 2.45781935, 1.53870162, -0.24553228, 1.49870916, -3.42788561,\n", - " 4.98603203, 3.19947195, -4.22036418, 0.83316028])},\n", - " {'exitflag': 1,\n", - " 'fun': 37797579.29678398,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.20258998, 4.3092804 , -2.2015135 , -1.86005028, 4.82608847,\n", - " 2.24886943, -1.09100022, -1.53563431, 0.22579574]),\n", - " 'x0': array([ 2.20258998, 4.3092804 , -2.2015135 , -1.86005028, 4.82608847,\n", - " 2.24886943, -1.09100022, -1.53563431, 0.22579574])},\n", - " {'exitflag': 1,\n", - " 'fun': 1146659.4928225973,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-0.53682615, -4.81637178, 4.95364701, -4.57752447, -0.60577667,\n", - " -2.88604054, 0.85270085, 0.07054205, -1.27549967]),\n", - " 'x0': array([-0.53682615, -4.81637178, 4.95364701, -4.57752447, -0.60577667,\n", - " -2.88604054, 0.85270085, 0.07054205, -1.27549967])},\n", - " {'exitflag': 1,\n", - " 'fun': 1236788.617249787,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 2.69036786, -1.85327759, 2.85858288, 4.70288006, 4.02501682,\n", - " -4.23172439, -0.72188414, 3.55067703, 4.87046828]),\n", - " 'x0': array([ 2.69036786, -1.85327759, 2.85858288, 4.70288006, 4.02501682,\n", - " -4.23172439, -0.72188414, 3.55067703, 4.87046828])},\n", - " {'exitflag': 1,\n", - " 'fun': 441.8147467190984,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([-1.88273814, -0.60977486, 2.87775688, 2.25140401, -2.65489216,\n", - " 1.01047951, 3.69127283, 4.44893301, 2.65440911]),\n", - " 'x0': array([-1.88273814, -0.60977486, 2.87775688, 2.25140401, -2.65489216,\n", - " 1.01047951, 3.69127283, 4.44893301, 2.65440911])},\n", - " {'exitflag': 1,\n", - " 'fun': 4453.420140298416,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([0.66867122, 3.97870907, 2.13991535, 0.2612146 , 4.9597103 ,\n", - " 3.23568699, 4.22366861, 0.40266923, 3.15201217]),\n", - " 'x0': array([0.66867122, 3.97870907, 2.13991535, 0.2612146 , 4.9597103 ,\n", - " 3.23568699, 4.22366861, 0.40266923, 3.15201217])},\n", - " {'exitflag': 1,\n", - " 'fun': 324.16551565091083,\n", - " 'message': 'Finished Successfully.',\n", - " 'x': array([ 4.03440768, 0.83760837, 3.55179682, 1.57898613, -4.87401594,\n", - " 4.33049155, 3.79628273, 1.90990052, 1.02667127]),\n", - " 'x0': array([ 4.03440768, 0.83760837, 3.55179682, 1.57898613, -4.87401594,\n", - " 4.33049155, 3.79628273, 1.90990052, 1.02667127])}]\n" - ] - } - ], - "source": [ - "results = []\n", - "for x0 in x_guesses:\n", - " try:\n", - " result = opt.optimize(x0)\n", - " msg = 'Finished Successfully.'\n", - " except (\n", - " nlopt.RoundoffLimited,\n", - " nlopt.ForcedStop,\n", - " ValueError,\n", - " RuntimeError,\n", - " MemoryError,\n", - " ) as e:\n", - " result = None\n", - " msg = str(e)\n", - " res_complete = {\n", - " \"x\": result,\n", - " \"x0\": x0,\n", - " \"fun\": opt.last_optimum_value(),\n", - " \"message\": msg,\n", - " \"exitflag\": opt.last_optimize_result(),\n", - " }\n", - " results.append(res_complete)\n", - "\n", - "pprint(results)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is smoothly running and does not take too much code. But again, we did not consider quite a few things regarding this problem:\n", - "\n", - "* The Optimizer internally uses **finite differences**, which is firstly inefficient and secondly inaccurate, especially for very stiff models. Constructing sensitivities and integrating them into the optimization can be quite tedious.\n", - "* There is no **tracking of the history**, we only get the end points. If we want to analyze this in more detail we need to implement this into the objective function.\n", - "* Many times, especcially for larger models, we might want to **change the optimizer** depending on the performance. For example, for some systems other optimizers might perform better, e.g. a global vs a local one. A detailed analysis on this would require some setup, and each optimizer takes arguments in a different form.\n", - "* For bigger models and more starts, **parallelization** becomes a key component to ensure efficency.\n", - "* Especially when considering multiple optimizers, the lack of a **quasi standardised result format** becomes apparent und thus one would either have to write a proper result class or individualize all downstream analysis for each optimizer." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### With pyPESTO\n", - "\n", - "Using pyPESTO, all the above is easily possible. A `pypesto.engine.MultiProcessEngine` allows to use parallelization, and `optimize.ScipyOptimizer` specifies to use a scipy based optimizer. Alternatively, e.g. try `optimize.FidesOptimizer` or `optimize.NLoptOptimizer`, all with consistent calls and output formats. The results of the single optimizer runs are filled into a unified pyPESTO result object." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Engine will use up to 8 processes (= CPU count).\n", - " 0%| | 0/25 [00:00\n", - "* message: ABNORMAL_TERMINATION_IN_LNSRCH \n", - "* number of evaluations: 141\n", - "* time taken to optimize: 6.583s\n", - "* startpoint: [-4.32816355 -4.74870318 -1.74009129 -1.26420992 3.53977881 0.54755365\n", - " 2.64804722 1.62058431 1.57747828]\n", - "* endpoint: [-1.56907929 -5. -2.2098163 -1.78589671 3.55917603 4.19771074\n", - " 0.58569077 0.81885971 0.49858833]\n", - "* final objective value: 138.2224842858494\n", - "* final gradient value: [-0.00783036 0.05534759 0.00129469 -0.00675505 -0.00121895 0.00394696\n", - " -0.00021472 0.00294705 0.00089969]\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "results_pypesto = optimize.minimize(\n", - " problem=problem,\n", - " optimizer=optimize.ScipyOptimizer(),\n", - " n_starts=n_starts,\n", - " engine=pypesto.engine.MultiProcessEngine(),\n", - ")\n", - "# a summary of the results\n", - "display(Markdown(results_pypesto.summary()))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Beyond the optimization itself, pyPESTO naturally provides various analysis and visualization routines:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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1puamtrgsq6f1NTUGsvPmTNH+Pn51btcmzZtxLp16+o8ZrGxsQIQsbGx4tChQ6Jbt2411jFq1CgxZ84cj9o5Z84cdd1Tp071aJl+/frVeexdx9k175QpU2qdZ9SoUeo8J6P68UhLSxNdunSp99hu2rSpznVNmjSp3vfP5fnnnxdarbbeYzR69Gi3c+/E9jbmfZEkSZIkyV31a+bJOHbsmBgwYIBH1+aFCxeqy1W/Z6jvZ9KkSW7ba+j+p/r925w5c8R7770ndDpdres2Go1i8eLFQgghSkpKxEUXXVRnOzp37ixyc3PrPA5xcXEe7c/TTz9d6/LV96u+n9ruCw8cOCB69+5d73IajUY899xz9b6XOTk5ok+fPnWuIzAwUPz5558e3+81pDnvQZvrHvzE82fatGm13rOuWLFCXea2227zaNsXXnihKC4urnPbU6ZMcVv/k08+Wee6goKCxNatW4UQQmRlZYn+/fvXOe+IESNERUVFve/F77//LsLDw+ttv5+fn/j5559rLNvS527147Z8+XJx//3317oe17lqt9vFnXfe6VGbH3rooXqPW0uQNbukc0b1rKu6MrVcrw8ePBi9Xs/QoUPRarXY7XZWrVrFDTfc4Db/7t271cj9ifW6qqqqEELQrVs3xowZQ5cuXQgJCaGqqoojR47w008/sWXLFo4dO8all15KcnKy+m0NQHh4OAsXLiQ3N5e7774bcNYRe/DBB2u0Ozw83O3/77zzjjrqnre3N9deey1Dhw4lJCSEvLw8Fi9ezC+//EJOTg7nnXcemzZtomvXrnUeO7PZzNVXX82uXbsYPnw411xzDW3btuXYsWPk5OQwduxYNcPorrvu4tixY4SFhfHxxx+7radv377q75WVleh0OoYMGcKwYcNISEjA39+fgoICUlNT+eqrr8jIyGDLli1ceeWVrF27Fr1eX2cbTyer1cp1113Hnj17GDRoEDfccAMRERGkp6fz+eefs3v3bnJycrjgggtITk5Wu9A21tNPP81rr70GgFar5frrr2fs2LF4eXmxY8cOPvvsM3Jycli5ciVjxoxh48aNmEwmdfmPP/6YioqKet8TcH9fJEmSJEk6NSZPnsymTZsASEhI4IYbbqBTp054eXlRUlLC3r17Wb16NRs3bnRb7sEHH+TKK6/k3XffZcWKFQB89NFHNe7/mnq/AfB///d//Pjjj4SFhXHnnXfSvXt3Kisr+fHHH/ntt98wm81MnDiR1NRUJk2axKJFixg2bBjXXXcdkZGRpKen8/7775Oenk5KSgqPPPIIX331Va3bqqysJDg4mPPPP58+ffoQFRWFXq8nIyODrVu3Mn/+fKxWK9OmTSM8PLzGSNIvv/wyeXl5PPvss+zatQuofeCAzp07u/3/4MGDDB48mLy8PACGDx/OpZdeSmxsLHa7nS1btjB37lwKCwt56aWX0Gg0tWbYWCwWLrzwQpKSkgDnfbjrmJWXl7N48WIWLFjAddddR+/evRv5TtSvOe5BT8U9+HfffcfixYsJCAhg0qRJ9OvXD61Wy7Zt29wGa6isrMRoNDJq1CgGDhxIhw4d8PHx4dixY+zbt48vv/ySgoICFi9ezK233spPP/3U4DF5//33+eGHH4iPj+f222+nU6dOFBUV8eWXX7J27VoKCwu59tpr2bFjB5dccglbt27l4osv5tJLLyUkJISUlBTeffdd8vPzWbNmDa+88govv/xyrdtasGABEydOxG63o9frufzyyxk9ejRt2rShpKSEFStWMH/+fEpLS7nqqqv4888/GTt2rLp8S5+71f3vf/9j0aJFREREcNttt9G9e3dsNht///23Wo7mvffe49NPPwUgMDCQm2++mT59+hAQEEBFRQVpaWls3LhR/VxqdVo62iZJp1N0dLQafT7xGyeHwyFCQkIEIF544QX1dVf0/6677qqxvvfff7/OaHZaWprYvn17ve355ptvhEajEYC47bbbap2neobXid/Y1WbTpk3qt3K9e/cW6enptc7366+/Cr1eLwAxaNCgWuc5MTNo+vTpDW6/+jdP9fn7779FVlZWndPNZrN46KGH1G3PnTu31vlaIrPL9fPkk08Kh8PhNo/FYhE33nijOs+ll15a67oa+qZv3bp1amagj4+PWLVqVY158vPz3b6devzxx+ttd0PviSRJkiRJNVW/9jdVTk6Oel3v37+/KCsrq3PetLQ0kZaWVuP1xmYJNSazCxADBw4UBQUFNeabPHmyW7YPIKZNm1brPkZGRgpAaLVakZmZWet2Fy1aJKxWa53tTktLE507d1YzZEpKSpq0f9XZ7XbRt29fAc4stfnz59c6X3Z2tpo9o9FoxM6dO2vM89JLL6nb7d27t8jLy6sxzw8//FAjy6k5Mrua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UD777LNalysuLua8887jjjvuYMWKFRQVFeHt7U1WVhY//PADY8aM4Yknnmi2dm7dupUePXrw8ccfc/ToUbRaLcXFxcydO5d+/frx999/17t8eno6ffv25f7772flypUUFBTg7e1NTk4Ov/zyCxdffDH33ntvrcvm5uYycOBAXnzxRVJSUrDb7VitVhYtWsT555/Phx9+6NE+rFu3jt69e/PJJ5+Qm5uLXq93m/7OO+9w55138v3337Nnzx719cOHDzNv3jyGDh3Ku+++W+f6P/vsM4YNG8bPP/9Mfn4+RqORtLQ0Xn31VQYMGFBvsGf+/PnceOONfPnll2zfvh2r1YpOpyMrK4uff/6Ziy++mMcff7zGcm3atMHf3x8AjUZDmzZt3H58fX09OjYAb7/9Nv369ePLL7/k8OHDmEwmysvLWb9+PY888ggDBw5s8G/7m2++YciQIXz//fdq5kdqaipvv/02I0aMaDDo15y2bdum/p6YmFhj+sm+36fKmjVrAGcgs3379qd9+1lZWbzwwgvN+jleWlpKamoqxcXF6PV6fH190ev1FBcXs2/fPvbt21frtNTU1HqDKPWtt6Flz3ZrftzIi9e+SVWZmTvFdiawr8YDriIcDFzyf9y8bAGD4wIJ8dWzaFsRH3y3mqKSYoxaPYEGH4xaPalluby3/Vc27tkmj3cLE1VLEEUPguOE8gmOHETRg2dkcOhs3KfGOBWfu/VpcrArLS2Njz76iBtvvJH4+Hj1W7HIyEgMBgPBwcEMGzaM//73vyxZskQGwCRJkqQzihDCrbhybXbt2oUQ4jS16Mz2wgsvkJuby/z58ykvL6e4uJjdu3czatQoHA4Hd999N1u3bq2x3B133MHKlSsxGAy8++67lJSUUFhYSGZmJv/6178AePPNNz0OBNWntLSUq666isLCQtq1a8cff/xBeXk5paWlrFu3jpiYGO6+++46ly8vL+fCCy9k165djB49mpUrV1JZWUlRURFFRUVMnz4dX19fPvzwQ955550ay0+aNIndu3fj5eXF7NmzKS0tpbCwkMOHDzNhwgQeeughjh071uB+3H333XTr1o1NmzZRXl5OWVkZf/zxhzq9bdu2TJkyhc2bN1NRUUFBQQGVlZUcOnSIhx56CIBHH32UpKSkGuveunUrd999Nw6Hg9GjR7Nnzx6KioooKytj3rx5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sgUhBP379+euu+7iuuuuO+J6Nm7cyNNPP828efMoLS1FlmUKCwu56KKLuPfee/F4POl5m+5nS7z66quH5Hgd67kA+Ne//sXNN99Mly5d2LNnD/PmzeOpp55i2bJlVFVVcf311/Ovf/0rPX9xcTGPP/44s2fPpqSkBIPBQPfu3bn00ku555572nQPPN57aOyeezA+8wwVqsoLwO1AriwTv/xy5C1b0G/alO6sKPR64tOmof/HPwjr9VRUVBAMBkkmk4TDYYxGI926dSM39+hOoNYgorMQ/j+lxSkA5Fwk5wNI5nMPzKfWIYKvQuR1EKGWV6jrAabhSIah3LR6F1sCVS3O+uOiSfy4x5T22I12JRAIpI97KBRCURTsdjs9e/Y8pISxpeOH41dIaj0i9BKoqVJBJDeS7aaU0CW37VkshODWfveyf2tps+mejlnc+eTNjLt8xDGvU1VV9u3bRyAQIBqNotPpUFUVnU6H3W4nNze32X5XR/08tP4dSsP1xJQEkiShCBW9JNPJ5uG2okmc6el+hC0efgz1Pj//+GwJHy3ZigA8DjMT+3Vi7sb91AUPZER1cNu5/8qJnNk9N32eFEVpcbxNUVSVmSu28uLnSympSeVIeVw2bj1nGE6jYPH6HYSjcQD80STBpMytF45hRI+8Y96WRgp/bYB1CzazZs4G1s7beMi1qzPoKBrShZ4jCul+Zle6DuxILBJn+4piti/fzab526nb7z1kvSabkV6jiug3phfDpgyi34jezdyuHzz/KW889CE11TWUsIuOFOLJ9nDtHy5n2h0XH/t+aJldpyfaS46GhsbpQGVlJZWVlWRmZtKxY8dTPZwfFNpzQON04khi12OPPcZvfvMbZFnm6aef5q677jqubb3xxhvceuut6ZwYk8mELMtEGsK+HQ4H77//Puecc056mUgkwvDhw9m0aROjRo3im2++afYL9MaNGxk+fDjRaJSHHnqIRx55JP1ZUxHo7bff5sc//jGBQAC73Y6iKOntZmZm8vXXXzN06NDj2j84cDwffPBBNmzYwCeffIJer8dqteL3+9PzHTzWpvzlL3/hN7/5DWpD6ZzVak23qIfU79v//e9/GTJkCJBqNjJ8+PC0GGAwGMjMbO5Qeeqpp7jqqqvS/2/LuYDmYte9997LvffeixACl8tFOBzmmmuuSYtd7777LjfccEN6Gw6HIx2wDNCpUydmzZpFnz59jukYH889NHbPPRifegqANcAwYBUwpOHzRpFLNZuJ33YbpscfRzKZCAQC7N69m3g8jtlsJhaLEYlEUFUVt9tNt27djvslW0RnpTomcvCrmZSaZrkaRCyVtaXsOfLK9EMh4wVkXSr36cZvZ7AlUNri7B2MDj6f9JvjGf4JoelxNxgMBINBhBAYjUbMZnOz497y8TsIORvJdgtYrkaSjy9na+GHy/jD9McP/aDhQnrovV+2SfCqr69n06ZNJBIJrFYrLpeLZDJJNBpNC6xNr7fPS1bx500fowgVs85ItslBQiiEkjHMOgO/6z/tmAWvRlZt38/Dr82irC5wxPnuPLsfgzunyg71ev0Rx3swCUXh8yWb+efMZVQ02Y5ZL9Mpw0y+y4Tc4NqSJThrQBc6uO1t2pZGc2rK6lg7dyNr5m5gzZwNVO+vbfa5yWqk98ge9Bndg6SS5P3HPj/qjxiA2WGicEhn+ozuQTwW57O/zwbAL+pZzhzOZDJOKeWIveO5649Z8Grtc6Bde8ReeeWV3HDDDe25Sg0NDQ2NE4CW16WhodESQgjuvvtufvOb32AymXjnnXeOW+j6+uuvueGGG1AUhV/96lfs3r2bSCRCKBRi69atXHHFFQQCAa644gr27duXXs5isfD2229jsVhYsmQJDz/8cPqzSCTC1VdfTTQaZcyYMTz00EMtbv/222+nW7duLFu2jEAgQCgUYtasWXTu3Jm6ujouu+wyAoEjv8wdCzNmzGD+/Pn861//wu/34/P52L9/PxddlMpCevTRR9mxY8chy7388sv8+te/xmq18qc//Yny8nJCoRDhcJiVK1cyadIkysvLufjiiwkGU+3kO3XqREVFBb/85S8BGD16NBUVFc3+NBW62noumlJZWcl9993HjTfeyL59+/B6vUQiEX73u98BsHr1aq677jpisRhjxoxh/fr1+P1+wuEwn376KXl5eenj0bgfJxoRi2F85hkAokCjty5KSptokJSIPvggUjCI+ZlnkEwmhBBUVFQQj8ex2+3IskwsFkOWZTIyMojH41RUVHA8/gEhlJQj6bBvkQ3TIm9D9KOjC10AajmSnMqz8iXCRxS6AKriAXyJ8DGN+URz8HGPx+NIkoTFYknnWzUe9yMfv0ZkcPwOKXsuku3W4xa6FEXhuXtebWHwqb/+ce+rx2wCEUJQU1ODwWDAaDSmhXmDwZA+Dk2vN0WofFq6CpNOj06SSYgk3kQYk2wgw2gjqiR4uXguilDbtJ/DenbirQeuw3KUcrM3F20nFosTj8dRFKXF8R4Og07HZWMH8PHvb+L+Kyamha1oUmVHdZjFu73sqYuQVAUd3Waq6+rT25Ek6Zi2pdEcT34mU64bz/2v3MUbe/7Bv7Y/zT3P/w8TrxqNO9tJLBxn3dxNvP3ox7z/5yMLXSabkcIzOmEw64kGYmz+ZgcfPPZFWuhShMImlgOgcuB6fOPhj4jHE4dd5/HSrmLXU089xauvtvBDr6GhoaFx2qCJXRoaGocjHo9z9dVX8/TTT+Nyufjyyy+ZPn36ca1TVVXuuusuVFVlxowZ/N///R9du3ZFkiQkSaJXr168++67XHzxxfj9fp544olmy/fv3z897bHHHmPevHkA3HvvvWzatAm3282bb755xLwQvV7P119/zZlnngmksmPOOeccvvzyS4xGI/v27eP5558/rv1sSn19PR999BE33nhjOmS6Y8eOvPfee+Tn56OqKu+++26zZQKBQFqwev/99/ntb3+bLo/T6XQMGzaMWbNmMWzYMEpKSnjppZeOeVzHey4aiUajXHLJJbz66qt06tQpPcaioiIAHnjgARKJBN27d+err75iwIABAMiyzEUXXcR///tf9Ho9xcXFRz3usVgMv9/f7E9bSDz1FBWqyhpgNbClYXrj/1cDFYDscCA1uZYikQjBYBCz2YwkSWn3G6Q6iiUSCWpqaqioqCAcDretwiW+snnpXUuYLwb7fUefTy1PrRP4w/r3WzWE6xc9w2ZvSavmPRk0Pe6qqpJo6ITZ1MUTDAZT56NVx09F0vdEkkztMr6NC7dSU1Lb8gwCqvfXsnHh1mNab+N+22y2tEMpEolQX19PNBrFZDId2G9gm7+M/aEabHozJl1KkAorMaJqShy06U3sC9WwzV/Wth0FtpVUEzmKGOGNJNhR6ScajeLz+dJClNlsbjbeI2E06OnR0YN6kFiVUAS7ayNsqgiiCPBHEgTCsfQXAcAxb0vjUCRJoqB7Hhf8z9k88Na9vFP+T15c9zg/eeIm+ozqedTlY6E4Nz9yDS/v/Du/fv8uzr97Evl9coiJCH5RT4B6QqS+VApQj1+k/tRUVfPNp9+ekH1qV7FLQ0NDQ+P0R1GUdBmJJnZpaGg04vf7Oe+883j33XfJy8vjm2++YeLEice93m+++YYdO3bg8Xi47bbbWpyvsTpg1qxZh3x2xx13cPnll6OqKtdddx0vvvgiL7zwAgD//Oc/D5svdfDyHTp0OGR6nz590mLe22+/3ep9OhpjxozhrLPOOmS6yWTi3HNT2Uvr169v9tkHH3yA1+tlyJAh6XkORq/X86Mf/Qg4/HE6Gu1xLhr5zW8OX/bm9XrTy91///1YrdZD5hkyZAiXX345AG+99dYRx/znP/8Zl8uV/tMorh0roriYF0iVLo5tMv2nDdOGAS80zNeUZDKJoijp8lmj0ZgWVoUQqKpKNBqlurqa/fv3s3PnToqLiykpKaGqqgqfz0ckEjmyCKZWt2ofJNNEJF3B0Wdsss6ySH2rZq+I+bhp6XOc+eVvGf/Vw/xo0VP8ZdMnrKrddUoiapoed1mW0810FEUhHA4TCAQIBALU1tYSj7Xu+LX2OLeG2vLWHdfWztdI0/02mUxYrVYkSUJVVcLhMH6/n2AwiM/nI6kkWVW3i0AySm0sQExNCVIGSY9BSl2vBllPUqjUx4+Q7XYUKo5SwtiIP5IqtTYYDMgNQfKNmWPJZLJV61i1o2XBVS9LxJMq++oiJETK/dUogrZlWxpHRpZlbG4b8VjiyMJuE8LeKLl5OXTu0gmn2UmgOkQJu1jOHFYyPz3fNtaynDksZw4l7KKmtO6E7IPWjVFDQ0PjB0aj0KXX60+rVukaGhqnjvLyciZMmMDatWvp2bMns2bNOmwYfFv49tvUN7Y+n++IQfCNmVR79+497OcvvfQSK1euZN++fdx+++0A3Hbbba1ynk2aNOmIn7355pusX7+eRCKBwXD83aFGjGg5p6fxGNTVNf/lvvE4bdmy5YiB542uhZaO05For3NhsVhazDhbvXp1uoxoypSWA8/PPvts3n333aMe99/85jf84he/SP/f7/e3SfCSioq4HbgYiHBA8HoWGNXw7zxALSlJdyuG1LNSp9ORTCbTpWVNhZfGEHG7PVU2mEgk0h30QqHmAoPBYMBkMmEymTAajZhMJgwGA5Kc3bqdaO18TebNt2RQHKxs/XJAVE1QHKykOFjJ+/uXIQFWvYl8cwZ9XR2ZmNOXkVk92tRxsLUcfNwdDgeqqhKPp0rlotEoQgh8Ph/hgBWTMgq7uQK7pRKj/vAlmSL8NhgGIOm7HPf4svJa14GztfM1cvB+WywWzGZzOu8uGo2SVBRW79vGju3fsCtZh1XVEySORW/Erjdjkg3p3LCEmkQvyWQYj71sUwjBgvW7eObjRa2a3+Oy43a7m10XiqIgy/IRf99MKirz1u7k9Tmr2bC7vMX54kmVpCow6GSsVgugNBO2WrMtjaMTqA+y8P2lzHlzIesXbD6mZRd/vJxXH3iTij0HhOWOFJJNPipKWvDqxWBcZAFgwoynoPWdUI+FdrkSVFVl1apV7N27l3A4rOV2aWhoaJzGaCWMGhoaB/Piiy8CqfvC7Nmz2+yeORxlZanymUQiQWXl0V+6WypBycjIYMaMGencq8LCQp5qCBs/GgUFLbthGj9LJpPU1dWRk5PTqnUeiSMFJDe+iDV1JMCB4xSNRtP36SPRWL5zLLTXucjKyko7Nw6mqupAx78jHffG5ihHO+6N4tDxYrj7bnJ/8xtyVZWmR24wMJRUFI0E8MUXJDp2JP7aa1gnT8ZisWC32/H5fOj1+rQIBinng6qqZGVl0a1bt7QDJxaLpcWJxr+TySSJRIJEItEspyyVOZSNMTYBo64Ekz6A0RDEoIsgSY3lXFKqq6DxjIYN54JaSYsBOrInPe9DA6dz9pxHj3p8njvjJuaUb2ZRzTZqYn6UJusWQCgZY0ewgh3BCj4pXYmEhFVnpIPZRW9nPhNz+jImuydGnfGo22oNhzvusixjNpvT14PZbMblchGJ6IlG84nGndT4e2IyBHBYKrCbKzEamgiOieWImqkI61VItruQdJ4Wtn50+o/rjadjFjWltYc/DRJkd8yi/7jex73fkiRhMplIyoLqcC2lCS81iQhGdBRIDrIkMz4RJS5DvRwn2XDdCCEIJWMU2jvQy3lsHWe3l1Tz+HsLWLl9PwCyJB1SXtiUDJuJwYV56HQH7gtCCKLRKC6XK13O3ZRgJMbHizfx1tw1lNelypP1Ohm9TiYaP9Sd5YskqQnG6eqxkZvpxOf1IoQgmUyi0+mOuC2NIxOPxln6+SrmvrmQ5V+sIdHk+A+c0Jezrh7D6398P+VUPEJu14L3lgBgtBjoObqQnqO7Muu5hYRqwyjiwDoduNMB9Y5sO+MvHnNC9uu4xa5nnnmGRx99lJqamvS0pmJXfX0948aNI5lMsmDBgnb5BUJDQ0NDo+1oYpeGhsbBXHjhhSxcuBCfz8fNN9/Mp59+etjys7bQWAI1YsQIli5delzr+uc//5n+d2lpKTt37mTgwIHHtc7ThcbjdNVVV7VrSeXhtnG85+JEOnpOFJLJROxnP8P41FOHvKs1/l9xu5G9XgxlZejPPpvYiBGI994jNze3WYaUTqdLu7pMJhO5ublpEUyWZSwWyyEv3I0RAgcLYYqiEI8niCuXIUIz0/PLkoLREMSoD2IyhDBl3ospqaYEEOcDDZ0HJQ775imioOwGfXdcBit9HAVHDKnv4yjgDE9PzvD05NekRIodgQrmVGxgWc1OdgUriKrNxQeBIKTE2B2qYneoipnla5GQsOgMeExOejnzGJfdmwm5/bC0QQCTJOmIx91sNtO1a1ccDgfJZJJg3QQC1f8mHMsklnAQSzio8ffAZAhit1Rgz7oSk7QYYgsg/AYi8hHCeguS7ZZ0mP+xoNPpuPPJm/nDFY8f/jQI+Mnfbz7mn5XD7bdXibDbX4k/HCQqJdlq8mGxWpnq6UtfYw67akpZULGZZEwlO2EmpFOokkKUqUHMegO3Fk1CJ7UuvajWH+K5zxbz8bcbEQKMeh2XjexFtsPIszPXHjrehl2/dmxPwuHQUX8+AMrr/Lw9by0fLdpAMJpykbrtFq4YP5ArJwxibXEZv3rx88bD2IwtlUHOHVpEOBRKi8uNLr/DbUujZRRFYf2Czcx9YyELP1xGyHfga4BuAzoz+drxnPWjMXTolBKFXdmuw3cfbcBoMTDknP70GltI56H5mCxGbDYb7kw3r9z7TovLXfvIZRiP0gChrRyX2HXXXXfx/PPPI4TA6XSm28E2JSMjg6FDh/LGG2/w3nvv8dOf/vS4BqyhoaGhcXxoYpeGhsbBDBs2jIcffphzzjmHOXPmcMEFF/D5559jsx1fxzIgXZLXlrK7pjz77LN8+umn6HQ6evXqxebNm7n66qtZuXLlUYW50tJSevXq1eJnkHJcZWaemFKK1tBex+lUb6NpNlpJSUk6tP5gSkpS2Twn87ibnnySGMDTT0PDO0sugCwT/9nPMD35JMqbbyLdcQdyIIB52TJE166EbryRgv/7P+q8XoLBYLobo8vlIjc394hOvkZ0Oh1Wq7XZtdroSkmJX9nEghIx/xfE4wqq0BGNu4jGXWAYhqR0g+pd6HQ6jMY+GKUnMSZexaTbi9EQRCcnQM4BDKCWIOpugMz/IOmLeG3MXdz47YzDCl59HAW8NqZ5t1VJkujpzKOnM4+f9DwHIQQ7AxXMr9zEyrpd7PRVEFZjqIhmYoRAEFbi7AvXsC9cw9cVG5A2vIdJNuAxOejuyGVMdi8m5/THbjy6+8bhcNCtWzcqKiqOeNz1ej3uDufhckok6/+PYEQQjOQQjnmIJXOJx6+grroIk+ksbKYbsEsvYFSXI4WeRUTeBNtdYL0KSTo2UW7c5SN46L1f8tw9rx6SaZSR62bURcOOaX1N97tT186sKN7E/sp9RBIxFEngk+OYs5z8pOtoBmd0RW4QsDrm5mN025m7ew2JaByjCp2x09eUzaiCvgy0d2xWmns4Yokkb81dw8tfLifUIECN7dORK0d2J9uVumatJiOvzt1Itf+AKNIhw84vr5jImd1zj3qeNu6p4PXZq5izZgeKmrpyuuZmct3koZx/Zh/MxpQ0MXlID/7yPxfy13fnU+UNtrit2tradJfO7OzsVv8s/pARQlC8bg9z31jIvLe/bZaVld0pi0k/Gsvka8fRbcCBUt9kIsmauRtZMXMNVqeFsL+549dkNXL2zeOZeMtIYokYiqJgNpvJysoiLy+PAQMGIMkSb/zhQ2jwRxkx48i2c+0jlzHtjotP2P62Wez68ssv+cc//oHD4eDf//43l1xyCXl5ec2sy41cc801vP7668yePVsTuzQ0NDROIY2WctDELg0NjeacccYZzJkzhylTpjB//nymTp3KF198kc4iaitjxqTKEyoqKli5ciVnnHHGMa9jw4YN3H///QA89NBD3HjjjQwePJgtW7Zw7733psPqW2LevHkt5nY1dnccOHBgu+R1tZUxY8bw2muvsWrVKsrLy8nLyzum5RvLCg/+4vngbcDxnYujMXTo0HR535w5c1oUu2bPTrWjHzRo0Ek97qYnnyTx0EN4OncGRSHzgQfg/vvTpXG6a66Bq69Gue8+5GeeQVIU7K++iuXdd5Efe4ychgoWvV6PxWI5LhdJqoTRgMFgSAnLmVMR4hxEbAWJeA2x4FJioVXEVT1x4wQSiQSKohCJRIhQCPwBomUgwuj1NozWIkzGJIbYHzFJGzHW3IzO8xqSvhuvjbkLXyLMH9a/T1mknnxLBg8NnI7LcHQHpyRJ9HDm0cOZx48BVajsClYxv3Izq+qK2RWswh8PIxrEr+YCWCoDrCRSR0mkjvlVm/l/mz7CKBvINNkotOcwMqsH5+UPwnWYXCmHw4HdbicSiZBMJo943CXzuehzp+COr8StVqMID8F4L0KhMKFQqMFZ14E6fodRLsYmvY/DtA6j8kek8GtgvxfMU5Fa6YKClOA1+pIz2LhwK7Xl9dhcVv5607PUV3j56rUFnH/b5FavC8AbDzO7Yj1fla/HFw9j0+uxGgwM8XTj0s5D6GI/NLdNp9Mxpkt/Rnbuy+bafdR66zHGBJl6K5IqsXfvXkwmEy6XC4fD0SzTSgjBnDU7eOrDhZTWpkoJi3LdXD++N73yM5EkCYfDgcvlomfPnlxx9ijW7CylxhfC47IxpHsBuoZ7z+HOkyoEc9fu5PXZq1hbfKAj5Jm9OnHdlGGM7tsVWT70XE4e0oOJg4qOuC232015eTkWi4Xu3btrjq4jULm3mrlvLmLOG9+wd/OBJgB2t43x00cy6dpxDBjXJ/0ciccSrP56PQs/XMqST1YQqD9QDuzMctB7RHe69O1I4Rmd6Tggl2gsSjgSxmg0pkVOp9OZPidnXTmWohGd+Wby5wgB//v8zzn3yiknzNHVSJvFrueffx5JkvjDH/7AJZdccsR5R41KxT5u2LChrZv7zjNjxgxmzJhxSrqZaGhoaDTSGJoLmtiloaFxKEOGDGHu3LlMmTKFhQsXct555zFz5szj+rb8rLPOonv37uzcuZN7772XOXPmpAO+D0ddXV0zp08kEuHqq68mGo0yduxYHnjgAXQ6HS+++CJXXnklL774Iueccw7Tpk1rcZ3PP/88d999Nx5P84yebdu28f777wOp8sFTyRVXXMGvfvUrvF4vv/jFL3jzzTdbfHlTVRW/34/b7U5PczqdQKobYksc77loDW63m3PPPZeZM2fy17/+leuuu+4Q5926dev44IMPANLdJU8m9sxMqpvkZh2CLKP7+9/hd79DmT4ded48dKEQ7p/9jMTf/kbwtdewjRlzQl6uJUmHZB6JyQxG+xjs1ZNAFCO5z0cYJ5NIJJqVQsZiXVOh+EAyHCEcBsTdiMjHSKIGfdVTmDNuxmQpwGg08v/6X4XRaDyuscuSTHdHLt0dudzGpLT4tbBqCytriykOVhFKRkk0lD4eTgCLqQnKI17KI16+rd7GE1s/xyDryTDa6GrN5kxPEVPzhuCxpF6YW1tWLUk6MKUaROgBtxXc7gwURSEUChEIBAiFQsTVIuLcT71vMwZ1AXZzMfbIQ5gsLyM7f4lkGt3q46HT6Rg0sV/6/9f8dhr/+MW/eP0P7zHlunEYzUd3jO0OVjGzbA3fVm8nKVLvi5kmO+fmDWJSbn+chqM74XSSzABPV/B0RQhBOBzG5/Olhb6qqiqqq6ux2Wy4XC721Qb52/vfsGZnyvGXYTPxo7G9GdM7H5PRiNvtxul0NhPHdJLEGT0Pn+nY9DyFo3Hemb+WN+euoaTGB6TyuKYO7821k4fSs+PRmy3oZPmI23K73dTV1SGEOKpz7YeIvzbAgveWMPfNhWxctDU93WAyMPLCoUy+djzDpw7BaEoJTrFIjJWz1rHwg6Us+WxlMwdXRo6LMZeeybjpoxgwrjf+gJ+6ujoSiQT+gB9ZlnE6nbjdbjp06HDIFxjhcBiny0lpeelxf4F2LLRZ7Fq2bBkAt9xyy1HndblcOJ1OKioq2rq57zx33XUXd911F36/H5fLdaqHo6Gh8QOl0dVlMplaDBfW0ND4YTNo0CDmzp3L5MmT+fbbbzn33HP58ssv02LKsaLX63n++ec577zzWLRoEePHj+dPf/oT48ePT/9CvGvXLr766itefvllLrnkEh588MH08vfeey+bN2/G7XbzxhtvpDNwrrjiCm699VZefvllfvzjH3PmmWe2GKyfSCQ4++yzefHFFxk+fHjKzTBnDrfddhuxWIxOnTpxxx13tGn/2gu3282TTz7JTTfdxNtvv43P5+Phhx9m+PDhaafUtm3b+OKLL3jppZd44IEHuO6669LL9+/fH4BNmzaxePFiRo8+9GX9eM9Fa3n00UeZPXs2O3fu5Nxzz+W5555jwIABqKrKl19+yY9//GOSySRFRUXpzpqnJZmZ6ObOhRUrUK+4AnnvXox79pAxYQLhiROJ//vfuDt2PGEv2ZKcgbBeD6HnEcFnkLImHzawP5X7FW+SBWYjqptGMvg+iUQdieq3CVouAzn1DiJJUrqrZNPukAaDoU370lT8urnoLBShsidYxcLqbays3UlxoJJQMoaCiqI2/eI/ta3Gcsi4mqQy6qMy6mNZ3U6e3T4LvaTDbbTS2erhjKwipuYPJt967GWvOp0Op9OJ0+lsJnyFwwNJKL2oj62lLrgao86Hvf5P2J1dMGfehWzsd/SVH8SFd5zN+3//jOr9tXz+/Ndcfs8Fh51PESora4v5omwtW/0HSkx7OHKZmj+EEVnd0ctty8eTJAmbzYbNZkNRFAKBAD6fj2g0yr6KGt7+YBGLtpQhAKNe5qJhhVw0vIgstwu3243Vam3TtVBZH+Dt+Wv5cOEGApFU92+Xzcz0cQO5cuIgsl3tJ3Q0dhVPJpPEYjEtmJ6UYLX0s1XMeXMhK2auIZlI/bxJksSgs/ox+ZpxjJs2Apsr5aKMBCMs+HQlCz9YwrL/riYaiqXXlZWfwbjLRzJu+kj6jemFLMv4/X727d9HMpkkEokQj8exWq2YzWY6dOiAw+E45LppvDdJknTSz1Gbxa66urq0FbI1ND6kNTQ0NDROHVoJo4aGRmsYMGAA8+fPZ9KkSSxZsoSzzz6bWbNmNXMSHQuTJ0/mvffe44YbbmDZsmVMmTIFg8GQznyNxQ78gn3ppZem//3hhx+mSxT/+c9/0rlz52brffrpp/n222/ZunUr1157LfPmzTtsIPQLL7yQFsTsdjuqqqa7Gbrdbj788MM2i3ntyY033kgkEuHuu+9m5syZzJw5E5PJhN1ux+/3N+vgePALxcSJE+nVqxfbtm1jzJgxZGRkpPfp8ccfZ/r06UDbz8WxMHToUP7zn/9www03sGjRIgYOHIjT6SQej6efQ506deKzzz47qd/yt5nhw5H37EE8/zzcdx9SOIxt/nyshYV477gD3R//iMPlOjFOL9stiPB/ILkVYl+D+dxD5tHpdIcJxe9IItaBWNVPicfqiSmVxE13k0jamoXlBwKB9BKyLB8igJlMJnQ63THtm06SKXLkUuTI5abCCWnxa3H1dpbX7mRnsJJwMpZ2MKEqyMhIEkiSnMoyEykJLCEUqmMBqmMBVtXv5oWds9FLOpwGC52sWQzN7MbU/MF0tXc48qAOOl4HC1/BYCbB4ADi0RXUBTZQF1AxVP4du7MbzuwfYbZ1a/UxMJqNXP/QFTzx4+d5688fct6tk7A6DpybUDLG3IqNzCpfR3UsVTqoQ2akpwdTCwbTw3FsJcyt2V+3243JYuOlz7/lzQXriTWIIMO7ZXHJsC50yfWQk9OBjIyMNjWg2LKvktfnrObrldtJNrzzd+7g5tpJQ7lwZF8spvYvV5MkCbPZTDAYJBqN/mDFLkVRWDdvE7Pf+IZvP1xOOHDAkVU0uCuTrxnHWT8ag6cgC4CQP8zcNxfyzQdLWTFzDfHogedKh84exk0bybhpI+kzsgeynPp5DIVC1NTUpDvLRqPR9HPD4XCQk5PTzAHYlFAoVQJpsVhOenOTNotdTqeT+vp6EonEUevs6+rq8Pl85OcfW8tTDQ0NDY32RRO7NDQ0Wkvfvn3Tgtfy5cuZMmUKX3/9NRkZGW1a36WXXsrOnTt57rnnmDlzJjt27MDr9WKz2ejduzfDhw/nggsu4Pzzzwdg//793HbbbQDceuutabGmKVarlbfeeouRI0eycOFCHn30UR5++OFD5hsxYgQrV67kT3/6E3PmzKG6upqCggLOP/98HnroITp27NimfToR3HHHHZx33nnMmDGDr7/+mt27d+P1enE6nRQVFTFq1CguvvjiQzLI9Ho9c+bM4fe//z1z5syhtLSU+vp6AIIHlewd67loC1dddRXDhg3j8ccfZ/bs2ZSUlGAwGBg8eDCXXXYZ99xzzykTGCORCFOnTgVg5syZrX5Jlu64A267DXH77fCvfyElk2Q8+yzJ11+n6m9/w3H11e3WxTS9TdmNsN4IoecQwWfBdHarM6UMphz0+TOw1l0PyiLQ7YGM/6CIDod0hYzH4+nOdo2/KzSi0+maiV+N/27ti2tT8ev6wvEoQmVXsJLlNTtZVrOTncEKwskYCaGkPF4SGNChk2SMckpoi6tJomoCVQiSQqEuHqQuHmSddy+v7pqPXpJx6C0UWDMZlNGFc3IH0sd99J/rpsKXquYSChUS8J1JsH4+ifgO6usqqK97CoOlL46sKTic+ZjN5qMKX+fcOJF3/vIJpTvK+eipL7j2wWmUhuv4snwtCyq3EFNTAoNDb+bs3IFMyRtAlunEhKsnEgk+WbSOF79cSU1DaVr3XDc3T+pPz/xMFEVBCEFtbS319fXpMsejubtUVbBwwy5en7OaVTsO5ECd0bMj104eyrj+hYfN42pPTCZTWuz6ISGEYMfqXamg+XcWU1den/4sp0s2ZzUEzXftl3I7B+qDfPXafBZ+sJRVX60jET/QXTW/KCctcPU8o6jZOY9Go1RXVxMOhxFCpL8Msdls6PX6Ft1cTWkUu2RZZuLEicCx3XePB0kcKcXyCEyaNIkFCxawYMECxo4dC5AOqD84l2rGjBn87Gc/45JLLuGjjz46/lF/h2ksY/T5fKfFN4gaGho/LHbs2EEkEqFLly5aSfUpQnsOaGicHPbs2UO3bt0A2L17N127dj21A9JoF9rjHhoKhdKOsmAw2LauoyUliOnTYdkyJBpyqPr0oe6f/yTrjDMOKTU8HoTqRVRPAhFEcj+DdBh31xGXVyoQddeDshd0nZAyX0fSNXcPCSHSeWBNhbBEItFi0wODwZAWwJr+fawxCUlVYVewkpW1u1heu5MdgZT4FRcHXshlZEw6PTadCaveRExJ4E9ECCsxkuLw1UM6ScauN5NnyWCAuxNTcgYwOLNrqxxaqqoS9K0jUP0BoVAtqqoHyYBkGIbeMgyHMxOHw3FE4Wv+O9/ypx89idlhZvjMC9mklqc/62z1MDV/MGOye2HStb/rqTGza/nmXbwwcxXbG8SQLIeZ/znvDC4eMzB9jSaTyXSZY1NnZ6Nzx+l0Nsv2i8QSfL50M2/MXc2+Ki8Aelnm7DN6cu2kofTtktPu+9MSwWCQ0tJSTCbTD+IeX76rMh00v3/bgcB/R6adCVeMYvK14+g7OlVy6Kvx8+3HK1j4wRLWzNmIkjyg0XTqlc/46aMYO20ERYMO/ZlIJBLU1NTg96ech4qioKoqOp0OWZZxOBx06NChRTdXI6qqUlxcjKqqZGdnk5WVcpe1+b7bQGufA212dk2fPp358+fz+9//nq+++qrFm9q6det48MEHkSTplARQamhoaGik0DoxamhoaGhotBMdOyItXQpz5iCuuw6pogLzli3kjRtH4PzzqX/uObLy8tql0+QBd9cMRPCZY3J3AUi6XMj8D6LuWlD2I+pugMz/pKY3ztMkx6tpTI2qqs3ywBqFsEQikf7T6NxoXE9TEaxRADtSKL5e1tHTmU9PZz7XdBtLUlUoDlayum43y2p2pMQvJUZUSRBR4hAPoEPGpDNQYM2kizUbVagN5Y5+/IkISaGgCBVfIowvEWarv5T39i1FRsKmN5NrcdHX2ZEpuQM4I7PwEJeaLMs4M4bgzBiCEllEqOYlAsEgoWiEhH899dER1Nf3Qa83pjtGNu0SGVUSxMfaMfdwEt3hZ8VzS7Dd0ZVhmYVMzR9CP9eJyXpTFAWfz8eukgr+PW8Di7amBBGTQce1Zw3ilqmjsB4UmK/X68nIyMDtdhOLxfD5fAQCARKJBLW1tdTW1mKxWEhIBr5YuZMPv92IL5T6fdJhMTFt3ACumjiYnIwT40w7Eo2/z8bjcRRFOellcicDX42fBe8uYc4b37B5yfb0dKPZwKiLz2DSNeMYft5gDEYDdRX1/PeFr/nmg6WsX7AZVTkgBHcb0DmdwdWl7+GvP0VRqKuro76+vlnwf+PPdVM3V2sIh8Ooqpq+J5xs2uzsSiQSDBkyhC1btjBx4kTuvfdebrnlFmpra9m6dSt79uzhs88+4+WXXyYSiTBq1CgWLVr0g++SoH2jr6GhcaqIxWJs27YNWZbp16/fD/5+fKrQngMaGicHzdn1/eS0cXYdzGOPIR5+GCkeB0A1mai99174xS/IzMw87pdwofoa3F0BJPdTSOapx74OpQxRdx0oJaDrhpT5HyRd67OumtIYit+8M2Ssxc7zjWLawS6w1oTiJ9QkOwIVrK/fy7KanewIlBNW4sTVZEO0fcrFZZINOA1m+rk609WeTXXEx65QNSXhGrzxMEmhcLgXXxkJq95ItslFX1cBZ+X0Z7SnB3rdAV+IECpEv0AJPEk4HCEYySUY64UwjAV9EZASjVSTzIrQPmbXbiakxogvqSP46y3ozHqe3Phnehd2bdPxPhKNX2Z6vV5q6r18unwnn63aRTyZEjrOH96Luy8fT7a79fl4qqoSCoXw+Xxs2VvB56t2sXhbGYqaOoL5WU6umzyUi0f1O0Q8O9ns2rWLRCJBp06d2r2M+FQRDcdY/MkK5r65kJWz1qVdWbIsMXhSfyZfO54xl52JzWmluqSWRR8uY+EHS9m4aGszJ2aPod0Ye/lIxk0bQadeBS1uT1VVvF4vdXV16Z9hg8FAMplMr8/pdNKhQ4djupdVVVVRX1+P2+3Gbre32323tc+BNotdAHv37uW8885j27ZtLd6khBAMGDCAWbNmkZube9h5fkhoLzkaGhqnCp/Px969e7FarXTv3v1UD+cHi/Yc0NA4OWhi1/eT01bsAohG4frrER98gNTwipXo0IHKv/8d29SpuFyu4+qErAaehtCzoO+BlPXZMbm7GhFKKaL2OlBLQVfYIHhlt3lMB5NMJg9xgcVisRYblcmyfIgAdrRQ/LiaZGeggg31+1hWu4PtgQoiyThxNYF6kPhl15vo7SpgaEY38ixudvgrWF23m92haryJEAk1eVgBTELCojOQbXLSy5nP+A59GJ/TB5MMhN9GBGegqj7CsSyC8RHsiZ7NjoBCWbgeFUFSUpHMBobl9+DL6z9m65IdXPSTc/n5jNva6UiT7rLo9XqJRKN8u7WMt7/dRl0w5boaXJTPL6+YQN8ux/4OrqqCxZv38PrsVSzftj89vVd+BhcM7cawwhxMJiNOpxOXy9UuDsa2UlZWRiAQIDs7m8zMY+/UebqgJBVWz9nA3DcX8u1Hy4kED+SQ9RjajcnXjmfi1WPIysugYk8VCz9YxqIPlzZzewH0HtGDcZePYNy0keQVHrmkVAhBIBCgpqYm3QClMY8vEknlu+n1enJyctrUTGT37t3E43Hy8/ORZfm7JXZBypr2t7/9jVdeeYW9e/c2+6ygoIAf//jH3Hfffe33EPmOo73kaGhonCoqKyuprKwkMzPztApj/qGhPQc0vqtcfvnlLF68+JiW+fDDDxk9evQJGtGRaavY9V3bzx8ap7XY1ciOHYjLL4eNG9N5XpGhQ6l67jky+/Q5aqBzSwjVj6g+K+Xucv0dyXJBm4YnkvtTDi+1HPTdkTL+g6TLatO6WrU9IUgmk4eE4sdisRbzwI4lFD+mJNgRqGCjdx8raovZHignosSJKUlUUiJbo/hl0Rnp7cxnUEZX+rgKyDG7WVi1hWU1O9gVrKQuHiKuJloUwMw6A1lGBz0cHkbYS7Er85nny2F3xImdLFwil57GfHrZc8m1uJGQ2LVqH09c9yI6vY5XtjxJftHxGUCi0Sg+nw+/34+qqmwvq+ffCzZTXOkDUq6rey4fx+QhPY75OovGk/x32WbemLuGPRV1qWMnS0wZ2pNrJg2he64bv99PIBBo5uKzWq24XC7sdvtxCbptoba2lpqaGhwOx3euIZ4Qgu0ri5n9+jcseHcx9Q3nECC3WwcmXzOOSdeOo3PvAkp3lrPwg5SDa/vK4vR8kiTRd3RPxk8bxdjLz6RD59aJ1+FwmOrq6nTEiV6vx263EwqF0sJXW9xcjcTjcXbv3o0kSRQVFRGNRr97YldTysrKKCsrQ1EUcnNz6dKlS3ut+nuD9pKjoaFxqtizZw9+v5/8/Hw8Hs+pHs4PFu05oPFdZeLEiSxYsOCYlpk3b166+9J3hR/Kfn5X+U6IXY188AHixz9GauiKKWQZ35VX4nv0UTy5uW3argg+iwg+nRKpsj5DktpWHimS+1IZXmol6HsiZf4bST65rhghRLM8sGMNxW+aB9ZUYIkqCbb7y9nk28+q2l1sC5QRVRLE1ARKQ5i9XtJhkvWYdUa623MYlNmVPs4Cejnz0SExu3Ij31ZvozhQQU0sQKwFAQxAAnSSQp4hxDnu/VxZ0BuD5XaCYSvBYBBFUXj2tn+x9dudjLh0CD997hYcDsdRux02RVXVdIh8o+Om2h/hncXb+XZrKQBWk4Fbp57JNZOGYjIcWzR3rT/EuwvW8d436/EGU+u3m41cNnYAV581mLzM5j9rqqoSDAbx+XyEw+H0dJ1Oh91ux+VytaprZXsQCoUoKSnBaDSmv+A43SndWc7cNxYx582FlO440LjAmeVgwpWjmXLdOPqM7Mm+raUs+mAZ33ywhF3rDhiLZFliwPi+jJs2kjGXnYknv/U/u7FYjJqamnSXXp1Oh9vtJplM4vf7EUJgMBjo0KFDm9xcjdTX11NVVYXVaqVTp07tet89JWKXxtHRXnI0NDROFVu3biUej1NYWHhcDy+N40N7DmhoaGi0nfYSuzp0SGVVVVVVndgKFFWFBx5APP44UjLVXVC1Wql88EGU66/H4/EcU9MYoQYa3F1+JNcTSJYL2zw0kdzT4PCqAn2vBsEro83ray+OFIp/OBrDsw92gjXmgUWVONv85Wzy7md1/W62+8uJKnFiapKkSLmTGsUvk85AF1s2gzK60MdZQG9XAR6Tg4gS5729S/i8ZBWlkfoW8r9SUyTAIKlkGHR0tXdllKsXHXZK/N9FzyBJEr/95Kfk9chJC0N2ux2bzXZYYSgej+P1evH7/WknVSyhMHPdfj5auo14UkGS4NLR/fnJRaPxuI7tWt5ZWsMbc1fzxfKtJBpyofIynVwzaQiXjO6H3XL0rqKJRAK/34/P52t2joxGIy6XC6fTedSufceDoijs3LkTgO7du5+2IfX1VT4WvLOYOW8uZOuyHenpJouR0ZcOZ/I14xh69kBKtpXxzftLWfjBUvZuLknPJ+tkBk/qz/hpIxl96ZlkdDi2rurJZDLdYbExeN7lcmG1Wqmurk6fO5fLRXZ29nEfx5KSEkKhULq8tD3vu5rYdZqiveRoaGicChRFYdOmTQD07dv3hP7SoXFktOeAhoaGRtv5zt5DvV64+mrErFk0ShrxTp0offJJzKNG4fF4Wp17JIIzEMGnUplbnv+22d0FIJK7EHXXg1oN+j5Ima8hye42r+9E0jQUv6kT7Eih+IfLA0ugsi1QxmZfKWvqd7PDX05UTRBTEiSail86AyZJj8NgQSAIJCMYZQN6SaanI4/JuQNR1CTfVG9lm7+MqqiPsBJLh+Y3GQkSYH20HP3iIFnj87j0/y6ghy0XuyEldOp0Omw2Gw6HA4vFQjgcxuv1NndN6fUsK67hX3PWUutPTT+jZ0fumz6BXp1a32hACMGSLXt5ffZqlm454BYa0C2P66cMZeKg7uh1bciDE4JIJILP5yMYDKZz2iRJSpc52my2E1Lm2BhS37Fjx9MqPikSjLD4k5XMeeMbVn29Pt0dUZYlhp49kEnXjGP0JcMp21mRFriaOr30Bh1Dzx7IuGmjGH3xGTizjr3jpaIo1NfXU19fnz4nDoeDjIwM/H4/Xq8XSLkmc3Jy2uX4qapKcXExqqrStWtXTKaji6bHgiZ2naZ8Zx/QGhoa32nC4TA7d+5Er9fTt2/fUz2cHzTac0BDQ0Oj7Xzn76GrV8OVV0JxKnNHAKExY6h48kmcnTuTlZV1VEeFUIMN7i4fkutvSJaLjmtIIrmzQfCqBX0/pMx/IcnH5ho5VQghUBTlkK6Q8Xi8xVB8nU7XTABTdLA7VsPWYDnr6/eyI1BBRIkTTsaIqwoCgQTIDblfBdYMhmd1Tzu/OlkzkRuaBSiKwtLa7cwrm8sm304qYzrCqh4VCXlfHPtP9iEJCP69E/aeLvKwUSA78egd5JpddDS6Mas69Hp9ukTTbrezuzbMc58vZ+v+KgA6Zru49/LxTBxU1OpSwVgiycwVW3lj9mqKy2sBkCWJSUO6c+3koQwqbL+8K0VR0mWOjWWXjcfe4XDgcrkwmUztVuZ4OoXUJxNJVn29nrlvLmTxxyuIhmPpz3oNL2LyteOZcOUoKvfWsPD9JSz8YCkVe6rT8xhMBoafN5ixl49g1EVnYHe3TXwSQuDz+aitrSXZ4Cq1WCxkZ2ejqiqVlZVpN5fb7cbj8bSbK66xtNRgMNCtW7d2L2c9YWJXbW0tL7zwAitWrEBRFAYMGMBNN91Ejx49jrjcmWeeSW1tLcXFxUec7/vOd/4BraGh8Z2krq6OkpIS7HY7hYWFp3o4P2i054CGhoZG2/ne3ENffRV+/nNoyM0ROh11N99M/f33k+nx4Ha7j+iAEcF/IIJ/B103JM8Xx+XuAhCJHYj660GtA8MApIx/IcnH7iI5XRBCkEgkDskDi8fjLeaB6fV6vESZ79/BIt8OwkqMmJokIRQMkh4JSIiU+GWQdBh1BsyygQyjjf7uTvRxFtDHVUBXewd0kowQKkQ/Jel9knXBGHO8nVj0OzPRryIkh1oJPVrQMFhwYSJbWPBgxSx0GGU9Dp2ZDJ2Tsn1JFm8upy6UwGoy8uMLRnLVhEEYW5nLVR8I894363l3wTrqAilHmNVk4JLR/blm0hAKPCdW2IzH4/j9fvx+f7MyR5PJhMvlwuFwHHfFQV1dHdXV1acspF4IwZZlO5j7xkIWvLsYb7U//Vl+91wmXzOOiVePxl8T4Jv3l7Low2VUl9Sm5zFZjJx5wVDGXT6SERcMxeqwHNdYgsEgNTU1xONxIFVS6vF4sFgs1NTU4POlgvANBgO5ublYrdY2b+9wVFVVUV9fj8vlIjf3+BoyHI4TInYtW7aMCy+8kLq6umbT9Xo9v/rVr3jkkUdavCnn5eVRVVXVos30h8L35gGtoaHxnaKsrIyamho8Hs93rlPN9w3tOaChoaHRdtrjHhqNRpk2bRoAH3zwwTFlZrUryST8/OeIF15AanAhKU4n5X/8I/ELLyQrKwun03lYV0TK3TUJhBfJ9VckyyXHPRyR2JZyeAkvGAYjZbyCJH+/Mj4bQ/EPdoJtDpaxKLCTbdHK9Lw5Bidj7IX0sxVQSYhd8Rq2hyvZE6khrirE1QQxNZkWv0w6AybZgNNgoY+roEH86kih3Y0x+i4i+A8q9oa5dVwvkgmZm14/jx2FZraGK6hOBgmrcRQhcGLEk7ThqXZi8plJV0W6FQwdIb9DNhM7D2BgZtcj7uuu8tpUHteyLcQSqXfwnAw7PzprCJeN6Y/DenKveyEE4XAYn89HKBRqVuZos9nSZY5tcQGFw2H279+PwWA4qV/qlmwvY84bC5n75kLKig9cO+5sJxOvGsOEq0aTiCVY+MEyvv14OXXl9el5LHYzIy8axrjLR3LGeYOx2I7/fEQiEaqrq9NuOr1eT1ZWFi6Xi1AoRFVVVVpwzMjIwOPxnJCy0t27dxOPx8nPz8fhSInm7XnfbXexy+v10qdPHyorUyexb9++mM1mNm7cSDweR5IkJk+ezMcff3xYZVATu1JoLzkaGhqngl27dhEMBunYseMpt3f/0NGeAxoaGhpt5zvVjbG1VFTAFVcgFi1CIqVtxHr0oPSZZ9D16oXH4zmsCCCCzyOCTzS4u/6LJB1/HqdIbEHU3dggeA1Fynjpeyd4NRJVEiys2sLMsrWUhutSji8h6GcvYKyjiK66jHT5V1NCSpw98Vr2JOsojteyP1ZHQijE1SRxNYmKwCDpMen0mGQDNr2JXs58ejuy6CpWM/uBFSx4R6XbQAP3/msYButYnO5uuN1u9oRreWb2fJZ9W0EyCk6zng4FEjmFEmbTAVFCkVTqiRHSK5isZvq6OzEltz9D3F1ZtbOMN+asZtHG3en5+3bJ4frJw5g0tDuG0yDAXVGUdHfJaDSanq7X65uVOR7L+oqLixFCUFRUdEKzaesq6pn/dipofvvKA1VrZpuJMZeeyYQrRyPrZBZ/vJzFn6xo5vKyuayMuvgMxk0byRnnDMJoNrbLmOLxODU1NQQCAQBkWSYjI4OMjFTDiaqqKvz+1DiMRiM5OTnt7uZqOpbdu3cjSRJFRUXp0sjTuhvjn//8Zx544AE8Hg+ffvopI0eOBFJljQ8++CAvvPACkiQxduxYvvjii0MGr4ldKbSXHA0NjZONEIItW7aQTCbp3r37CXu4abQO7TmgoaGh0Xa+l2JXI998A9dcA6WlAAhJInD22ZQ/9hhWjyddhtRIc3fXX5Asl7bLMERiU4Pg5QfDsAbB6zQ5Ru1ATdTPrPL1zKnYQEhJ5SlZdEYmdujLefmDybW40/MenAd2uFD8oBJjd6yG4lgNu+K1lCd8JFBS7i+RRBUCvaTDKMkY0GGoU4nctAERE1z5bIIrLtqF03khK0qv5IkPN7CjtAaALh0y+MX08XTp6uDL8rVsqtlLJBzBGpcxiObCV52IUlWZpGa7jqTvgJg1sHcHfnreOIb17NTuuUntRSwWS5c5NhUXzWYzTqcTp9PZqiypRjdRQUFBu3cdDwcifPvRcua8uZA1s9ejqikJRdbJnHHuICZcMQqz3cKy/65iyScrCNSH0ss6Mu2MuWQ446aPYsjk/hiMrWtE0RqSySR1dXV4vd50h0Wn00lWVhYGg4FgMEhlZSXJZBJJktLZXCfCzdVIfX09VVVVWK1WOnXqlJ5+WotdY8aMYenSpbz00kvcfPPNh3z+zjvvcMsttxCNRhk9ejRffvllsx3QxK4U2kuOhobGySaRSLBlyxYA+vfvf0IfcBpHR3sOaGhoaLSd77XY1cgTT8ADD0CD40U1Gqn56U+p/5//weFw4PF4MBpTjhARfAER/BvouiB5ZraLuwtAJDYg6m4CEQDDcKSMfyLJ390vy4QQbA+U80XpGlbUFqOQKqHrYHIxNX8wE3P6YtW3zkl0tFD8gBJlV7SGndEqdkaqqVQCJIRCApUECioC8ys16N6rRSoyYv6rh+jmDGr2p7KzHFYjt18wiismDDqsC0sIQYmvioX7NrCvshJfZZx4nQRJCVUIaiMJahwB6grqUe0KEhIWnZFss5PejnzG5/RmQoc+GHXt4ypqL4QQhEIh/H4/wWAwna0mSRJ2ux2Xy4XVam1RtCsvL8fv9+PxeMjKyjru8SQTSVbOWsecN75hyacriUXi6c/6jOzB+OmjcHocrJ69niWfriTsPxDE7852MuayEYyfPpKBE/qib2W+WmtRVTXdYbFRX7HZbGRnZ2MymVAU5RA3V25ubjOx/ERRWlpKMBg8pFnAaS12ZWZm4vP5CAQCLboCFi5cyIUXXkgwGGT06NHMnDkzvUM/dLFrxowZzJgxA0VR2L59u/aSo6GhcdIIBALs3r0bk8lEr169TvVwfvBoYpeGhoZG2/lBiF2QErpuuQXefhsaXteSHg9lf/kL0TFjcLvdZGZmopNjDe6ueiTXY0iWy9ttCCK+DlF/M4ggGEciZbyAJJ34l+X2JKkqLKnZzhdla9gVrEpP7+/qxNT8wQzJ7IZOap8vAYUQxGKxtAgRDodRFAVfIkwJAUoJsDdZT1UySNwXI3njZqSwSmBKD+JF2SAJbEUBsvvW0ynTQ5+MM+jj6kwfVwHZpubZbXsr63lz7mo+W7IZvSzoYDfSNdtO5442FFccvxImrMaoFmGqpTA1RFCkA6/9EhJmnQGPyUEPRx5js3sxOW8AltNEAEsmk+kyx1jsQDdDg8GQdns1Cr6NNDqK7HY7BQUFbdquEIJNi7elgubfW4K/NpD+rGPPPMZNH0VmrotN325j6eeriIYOjC0zL4Nxl49g3PSR9B/bu906Gx48Pr/fT21tbTp7y2w2k52dndZoAoEAVVVVaTdXRkYGWVlZJ+XLblVVKS4uRlVVunbt2qwU9bQWu4xGIw6Hg9ra2iPOt2TJEqZOnUogEGD06NHMmjULq9X6gxe7GtFecjQ0NE421dXVlJeX43K56NKly6kezg8e7TmgoaGh0XZ+MGJXI7t3w7RpsGYNkMrzig4cSNnTTyMKCsjIyMBt+BAp/FfQdUbyfNlu7i4AEV/bIHiFwDgaKeN5JOkUBfofA754mK8r1vN1+Xq8iVQHQoOkY1yH3pyXP4QuNk+7bi8Wi+H1egkEAun3XVmW044kWZaJx+PE43HK/LW8uXQ9i59dimnpPhSXmfDPB2IZEEZxxEgKgUESmCSBSWfBqLOTbXbR11WAyWdn7coqlm/e36iB0qtjNtdNGcbZQ3sg1FQWViAQSG1PSbIrWMm+SC17EnXsTdZTLoIkJPWQfZAAk2wgy2SnyJ7LaE8PzskbhN146gTORgGx0XTTVEuwWCy4XC7sdjs6nY5IJMK+ffvQ6/UUFhYeU9nm3i0lzH1jIXPfWkTF7gOiaEaOi7GXjSCrIJOdq3ex4su1zRxe2Z2yGD9tJGOnjaTvqJ4nTFBqDPevrq5Oi38GgwGPx4PD4UCSJJLJJFVVVencLpPJRE5OzklxczUSCoUoKSnBYDDQrVu3ZufgtBa7PB5POkDuaCrlkiVLOO+88wgGg4wZM4aZM2fSvXt3TexCe8nR0NA4+ezfv5/6+npycnLIyck51cP5waM9BzQ0NDTazg9O7Grk88/hppugwXggJAnfZZdR+Yc/oDfpyNT9L07zFmTX/0OyTmvXTYv4KkT9rSDCYByHlPEcktT68PCTyZ5gFV+UreXb6m0kReq9M9No4+zcgUzOHYDL2H6lmKqqEgwG8Xq96e53kDKJuN3uw2ZNLd68hyfeX8Cu8jqIK3jeWYMIx5n86NnI52axPVRBdbyWhBohLgQJJBKKBGUWEsUWFO+B9eV1sXDO6CIuHDyAro4OzRxqjV0nA4EAwWCwmTsqKVR2J2pZGy5hfbSMyrifqJJAcKgsIAFGWU+G0U6hvQMjPN05N3cQmWZHux3H1qKqKqFQCJ/PRzgcTpc5NoqKDoeDsrIyhBAUFhZiMBw5G6umrI55b33L3DcXsnPNgTB/i93MiAuGkt3Jw74tpaz+eh2J+IEssbzCHMZNG8m4aSPoNbz7Cc9Ci0aj1NTUEAqlcsB0Oh2ZmZm43W5kWUYIkXZzKYqCJElkZmaSmZl50qNLqqqqqK+vx+VykZub2+yz01rsaszsWrx4MSNGjDjq/IsXL2bq1KlpwWvTpk14vV5N7NJecjQ0NE4yO3bsIBKJ0KVLF1wu16kezg8e7TmgoaGh0XZ+sGIXgKrCH/4A/+//QUMJk2o2U/W//4tvWi8M6iw87gD2jm8iy+1bjibiKxD1t4GIgGkCknsGknR6lLwpQmVV7S5mlq1ls78kPb27PYfzC4YyIqs7ern9Ssri8Tg+nw+fz5d+tz1artTuijr+/sE36Q6JbpuZOy4ajbRqHy/+8t9kd8ziX9ufxmg2UhGqZ13tDlaVfsPitX4qdzhRIg3j1wl0nWKYi+KYnRJGSYdB0mPVm+huy6G3M59+7o70dBdgNprS44jFYgSDQQKBQDPhS5IkrFYrJquZtZESvqnZxjZ/GdVRP2El3qIAZpD1uA1Wuto6cEZWEVPzB5HTJNj/RJNMJvH7/fh8PuLxA06rQCCATqeja9eu6U6ETQn5Qiz8cDlz31zI2rkb04KZTq9j8KT+5HTJpmJPFevnbyKZOKBbdOyZx/jpoxg3bSRFg7uelLD/RCKR7rDYGD7vdrvJyspKi6jJZJLKykqCwSCQcnPl5uZiNp8a92Vjk4D8/HwcjuaC6Gktdt1///088cQT3H///Tz22GOtGkRTwavxBGlil/aSo6GhcfIQQrBxY+ph3qtXr2Nq46xxYtCeAxoaGhptR7uHAsEg/OhHKbdXA4mCfEr+PIlYXzdW1ySyC85r9/IlEVuGqP8xEAXTWUjuZ06p4BVKxphXuYlZZWupiqWCuHXIjPT04Lz8wfR05rXbtoQQBINBfD5f2mEDqVIyl8uFy+VCrz+0fNQbjPDif5fy3jfrUFSBXpa5+qzB3DZ1BE6bmXg0zk09f051SS0/eeImLr/nAvZXe3lr7ho+WbKJSCwlajqsUbr3qUTtksCvcxHHRlwVxNUkCaGgR8Yg6RrELx1GWU8XUxY9bDn0cRbQy12A3WxNh5c3Or6iDU0Q4IDwZbfbsdvtSJLE4prtzK/czGZ/CZVRH+FkDPUwAhikykSdBitdbB6GZXbj3PzBdG7nctGDEUIQjUbx+/3NMr4sFgsejweXy4XJaGbVrHXMeXMhSz9bSTyaSC/fa3gR+UW51JTWsWnxNlTlQHln1/6dGHf5SMZNH0nXfievm6WiKOkOi6qaGk9jh8V0Y4oW3FxZWVmnrOtmPB5n9+7dSJJEUVHRCcksa6Tdxa65c+cyZcoUcnJy2LNnT6tfmBoFr0AgoIldaA9oDQ2Nk0s0GmX79u3Isky/fv1O27bTPyS054CGhoZG29HuoU3YuBGmT4dt24BUnld4eAGlf7kCkfc/OByuZp0b2wMRW4yovx2IgWkykvupky54lYXr+bJ8LfMrNxNTU8KFXW9mSu4AzskbSJap/UrsEolE2sWVTKZK2RpFIbfbjc1mO+zvVglF4f0F63nhv0vwh1NOqgkDC7nn8vF0yWnuOPripTn8/X+ex5Zpp+vvLmLhtr3pPK7uBR6unzyUc4a4MMRmIMIfUZUwsSnsYXN8JJsjBdTFYsTVBDElSUxNkFAVdMgYJRmDrMeAjE7W0dHgppspix7WHHo68nBZ7M2yxA52fFksFhwOB3a7PS3kKYrCmvo9zK7cwEbvfioiXkJKDEUcmgEGoJdknAYrHa2ZDM7ownn5Q+juyD3svMeLqqqUl5c3lDJC9Y46Vny+jjVfbiTsO1Bmmt89l0698vFW+9m+opimckjR4K4NJYoj6dy7bSH3xzN+n89HbW1tWjOxWq1kZ2c3c2olEgmqqqrSbi6z2Uxubu4p/0Lb6/VSWVmJ1WqlU6dOJ3Rb7S52qarK7bffTjKZ5M4772T48OGtHszSpUv57W9/ixCCefPmtXq57yPaA1pDQ+Nk4vV62bdvH1arle7du5/q4WigPQc0NDQ0jgftHnoY3ngD7rwT/Cl3k9BJ1F17ATX/+xcknQ6Xy0VWVtZhnUdtQcS+bRC84mA6B8n9dyTpyPlIx71NIdjg3ccXZWtYU78nPb2TNYup+YMZm90bk659xtAYBu71egmFQmkxRK/X43K5DtsJsOmyizbu5u8ffMOeynogJVjdN30CI3p3PmT+pKLy1YqtPH3RX1Fqg4SHdyIytBNj+nXluslDObN352ZimkhsRwSfgNjc1P+xUK67ga2J8Wz217DJtx9vPExCJIkpKQEsoSaRkTA0uL4MyMiSTJ7BRTdTVsMfDw69GVVVSSaTKIqCXq9Hp9Olha/GXKzDXUfr6/fxdfk61nv3URapJ5iMtiiA6SQZh95MviWDARldOC9vEP3c7SOObFu1g09e/JKVn6+jvtyXnm7PtJFblE00EKNka3mzZXoNL2LctFGMmzaC/KITI8QdiUaXVk1NTbrDoslkwuPxNBNTGzsxVldXp91cWVlZZGZmnhZfZpeWlhIMBsnOziYzM/OEbqvdxS6N9kF7QGtoaJxMKioqqKqqIjMzk44dO57q4WigPQc0NDQ0jof2uIdGo1Guv/56AP7zn/+csnybdkVV4d57ETOeRWooxVIdDioefpjAhRciyzIZGRntFlotYt8g6n8CJMA8Fcn1t3btAtlITEnwTdUWvixbS0mkDkhlRg3J6MbU/CEMcLdfeVljDpTX602LDpBy17hcrnTXu5bYWVrDEx8sYOmWfQBk2C3cefFoLh3TH91BxzwQifHxtxt5a94aKuoCGHfW4JizHZ3FwF9XPMaAvocKY00R8RWIwOOQSHXpRMpAst+JsFxNeTTEZl8Jm3wlbPLtxx+PkBQKcTUlgMUbxK/GkkcdMhKQrXfQzZRFoclDF30GZkUmkUigqio6nQ69Xo9er8dms6WPx5FC4Lf7y/mybC3r6vdSEqklkIiQPIIAZtOZyLO46e/uzJTcAQzJ6Nqqa7W6pJZ5by1izpsL2bVub3q6yWakc6+OhANhSndUNFumcEhnzrxoCBOvGEO3Pl1OepB7I40dFhvLSfV6PR6PB6fT2exaSyQSVFZWpktoTxc3VyOqqlJcXIyqqnTp0uWw99T2vO9qYtdpivaSo6GhcTLZs2cPfr+f/Px8PJ4Tm5ug0Tq054CGhoZG2/lBB9S3AlFdgri0H9JiPxKp0sZEURGlTz1FvHt39Ho9WVlZuFyu4xaJRGw+ov4uUoLXBUiuv7ab4FUTCzCrbB1zKzcSTKaEALNsYGJOP87LH0Se5dDw8bYghCASieDz+dJB4JDqeOd0OlOZT0cRFOoDYZ7/fAkfLNyAKgQGvY5rJg3hlvPOxGFpvmxZrY+35q3l4283EoqmgtUz7BauGDeQFQ+8z75N+7nqV5dw22PXtWrsxL5GBP4GSkM3QV1HJPs9YL4QSUp16iuN1LHJmxK/tvhK8CcjKKqaKn1UkyRUJd110SDp0JMSfjJkK12NmXQzZNJJcmJRdM1KOXU6HRaLBafTmS7pbHSCtcSeYDWzytayqm43+8M1+BMREuLwMUcyEla9iRyzi36ujpyV05+RWd3R6XQEvSEWfrCUOW8sZP2CzU2C5mWyu3iIR+PUlXrT65Ikif7jenPmBUPoM6E7ZtcBZ55Op8PhcKTP9clwScViMWpqatKliLIsk5mZSUZGRjPhTQiBz+ejpqYGRVGQZZmsrCwyMjJOCzdXI6FQiJKSEvR6PYWFhYcd22kdUK/RPmgvORoaGieTrVu3Eo/HKSwsTD9gNE4t2nNAQ0NDo+1oYtfREaHXEPMeRLqjGqk0lcMkgPDkyZT85S9gs2E0GvF4POkg8jZvKzoX4f0ZKcHrYiTX/yFJbQumFkKwI1DOf0vXsKK2GIWUC6iDycV5+YOYmNMPm759nCyKoqS7+TXNqjKbzbjdbhwOx1HdPomkwtvz1/LPL5YRjKTWMXlId+6+bBwds93N5l2/q5w35qxizpqdqA2v34V5mVw3eRhTz+yNyaBn6eer+N3Fj2GyGHlt57Nk5bVO0BMiCZEPEMFnQK1KTdT3QXLcD8Yxzc6vKlRKwnUNzq/9bPaVEkxGUYVKTEkSV5MkhQIiJX4Zdfq0+OWUzRQZsuiKixxhwyL0NL1y9Ho9ZrMZp9OJxWLBZDJhMpkwGo1HDCsvj3iZVbaG5bXF7AvVUB8PkxTJQ2PwEyrG5WFM80PIKwIQPzBHh84elIRCbXl9epqskxl8Vj/GTRvFmEuHk5Fz4JzE43H8fj9+v7+Zi89kMqVda+1V9tuUZDJJbW0tPp8v3cCvpTLjeDxOZWUl4XAYAIvFQm5ubrtm8LUXVVVV1NfX43K5yM09fCnoaS12LVy4kAEDBuB2u9s8KA3tJUdDQ+PkoSgKmzZtAqBv374n5KGtcexozwENDQ2NtqOJXUdHiCiiegqoVUhvjkF68B2IpAK6hcFA/U9+QvWddwKku9ZZrda2by/6NcJ7N5AE86VIrj8fk+CVVBWW1Ozgi7LV7ApWpaf3c3Vkav4QhmZ2Qye1T5lZNBrF6/USCATSne5kWU67uFpTWiWEYP66Yp78cCH7q70A9O7UgfumT2BYzwOREYqqMm/tTl6fs5r1uw7kRI3s05nrpgxjVJ8uzfO4hOCesQ+yecl2Lr7zXH727G3HtG9CRCD0L0TonyBSjiGMo5Ac9yMZ+h92GVWo7A/Xscm7n02+/WzxlRJSYgghiKlJ4koiXXpokHWYdAYM6EACp2Sij74DnYQDtzBhw4DUIH/p9XqMRmNa6DIYDBiNxrT41fh3S4JibTTArPK1LKnawa4lxYS/qkK30I8UOlAGqTpTy8r+A9MkvUSX0V0ZefZARl04jL6D+hzlmKXy2Rq7bDZeE5Ikpcs1W2pCcCyoqkpdXR319fXpbdjtdrKzsw8Rr4QQeL1eampqUFUVWZbxeDy43e7Tys3VlN27dxOPx8nPz8fhOHyDiNNa7JJlGbPZzOWXX86tt97KWWed1ebB/ZDRXnI0NDROFuFwmJ07d6LX6+nbt++pHo5GA9pzQENDQ6PtaGJX6xChfyMCj4Kch+T6L9LtP4V//5vGNn9qVhblf/4zwfHjgdSLt8fjaXMGkIjOQnjvARSwTENy/gnpKAKVLx5mdsUGvi5fT30ilUVkkHSM7dCbqflD6GJrn/gFVVUJBAJ4vd50NhIccPE4nc4jOo+asr2kmsffW8DK7fsB8Dit3HXJWC4c2SedyxWKxvlk8UbemruG0tpU0wCDXsfU4b25dvJQehS0vF/r5m/il5N+j96g45WtT5HXLeeY91eodYjg8xB+A2hwLZnPR7Lfi6TvcsRlFaGyL1STdn5t9ZWlxa+4miSmJlGFihCgl+WU+NUQep8trHTXZ5Er2XHLFhySCdEg1jQVvhqRJKmZCNYogBmNRnZv2MecNxYy761FVJfUppcx2AwokkANJg/sr0EiOcxKYoydxJk2bHYzZ6i5IEvstATp7sxldHZPzskdiM3QspipKAqBQACfz9fsOtHr9c3KHI/pXDSUIdbW1qZLQC0WC9nZ2VgslkPmP9jNZbVaycnJOS3dXI0kEgl27dqFJEkUFRW1+LN02otdQFpNLCws5NZbb+Wmm25q0aqmcSjaS46GhsbJoq6ujpKSEux2O4WFhad6OBoNaM8BDQ0NjbajiV2tQ4gYonpyyt3l/D2S9RrYtw+mTYOVK1PzAMkBA9j/1FMk8vORJAmn04nH42mTG1xEvkD47iMleF2B5PzjYQWvvcFqvihbw7fV29J5TRkGG2fnDWRK7gBcxra7zJoSi8Xw+Xz4/X4UJbUdSZJwOBy43W7MZnOrnTK1/hDPfbqYjxdvRAgw6nVcP2UYN507HJs5JUSU1/l5e95aPlq0gWBDHpfbZuaKCYO4YvwgPK7WXWe/PvePrP56PWffOIFfvfrTNux5CpEsQQSfguinpM62HqxXIdl+iqTLatU6FKGyJ1jNZl8Jm30lbPGXElHiIARxVSGmJlCFQAJkScak02NGj0s1kaWa6WRwk210kKG34tRZMOkN6HS6wzq66sq8rPzvOlZ+tp6yHZXp6XqjDr1BTzR0oNzUZDEyfOoQRlw6jOBQIytie9gZqKAmFiCaTDBWFCALieVyORGpIWcMMMoGMo02Cu05jPR059z8wbiNh56XWCyWLnNsFKmAdJnm0QRSIQShUIjq6mri8dS1cKTS4e+im6sRr9dLZWUlVquVTp1a7qp52otdJpMJu91ObW1KXW0Mpjv//PO59dZbueCCC05ZJ4PvCtpLjoaGxsmirKyMmpoaPB4P+fn5p3o4Gg1ozwENDQ2NtqOJXa1HhP6DCPwR5Fyk7NlIUoM7ZNYsuOEGqEqVDApJInzppZQ88gg0lJZlZGSQkZHRasdTepuRzxG+XwIqWK5Gcj6CJEkoQmVV3S5mlq5ls78kPX+RPYfz84cw0tMDvdy2rK+mqKpKMBjE5/Ol3TGQEhoaXVzHIuTFEknenLuaV75ckQ6UP2dYT35+2Tjys1LX3+a9Fbw+ezVfr96OoqZerbvmZHDN5KFcMKIPFmPLHQsPx7YVO/npiN8gyxIvrv8bXfq2LCC0BpHYkurcGF+YmiDZkGy3gPVmJPnY8lxT4lcVGxvEr62+UqJqAgQkhEJMSSAa0rYkJGyygUxhwa2acKoGMgw2ss1OPCYHOfYMpCisnbWJRR+sYNvSnentSLKETq8jGT8gNJmsRgZO6svIi4cy/LwhODMcmEymQ0LxY0qcuZtWsLe+ktVqGduTtUSUeHpcTZEAg6wnw2Clq70Dw7OKOC9vMB0srtSxaxCt/H4/wWAwHYQvSRJ2ux2n03lImWMkEqGmpiZ9/el0OrKysloUruLxOBUVFUQaSo2/C26uppSWlhIMBvF4PGRltSyinvZiV25uLnv37uXDDz/klVdeYc6cOaiqmj5pubm53HTTTdxyyy0UFRW1efDfZ7SXHA0NjZNFcXExoVCIjh07kpmZeaqHo9GA9hzQ0NDQaDua2NV6Uu6uKaBWIjkeQrId1OHv0Ufhj3+EBueJMJup//Wvqf7Rj4Cjv6S3uN3IJwjfrwBByHQN34Qu5svydVTFfKn1InOmpzvn5w+mhyOvXZwr8Xg87eJq2jHQbrfjcrmwWq3Htg9CMGfNDp78cCFlDaWI/brkcN8VExhcVICiqnyzfhevz1nNmp2l6eWG9+rEdZOHMqZfN2S57fv1+2l/5duPljNu2ggeeu+XbV5PU0RsCSLwV0huTE2Qs5DsPwXLlUjSsQlyjSRVhV3BqnTZ4zZ/GTE1CQKSIuX8QqTOhaxCB8mGO2ogvqSOqjml1K6oRiQPZG7pDTqSiQPdGS0OM4Mm92Xg2X3oPbo7RvOh49TpdM2ywEwmEz6fD5/PR0ZGBh06dCCpJPm2ZjvzKzezxV9KVcRLWImjtiCA6SUdLqOVLlYPw7IKmZo3hByTk0AggN/vP6TM0el0YrVa0109gaOKxkII6uvrqa2tRVVVdDodHo+nXTqlnixUVaW4uBhVVenSpcsRM+++E2JXWVlZetq+fft4+eWXee2119i3b19qhQ0nZsKECdx2221MmzatzbXf30e0lxwNDY2TgRCCzZs3oygK3bt3P67gWY32RXsOaGhoaLSd9riHNoZSA8csgnzXEOE3EP5HQM5pcHcd9F4WDsN118FHH6UnqQUFVDz5JIEBAwAwGAx4PB4cDkerj1VZ/VvM3PsW3/jyiZIFcjZ2g5nJuf05J28QHtPhQ6yPad8aXDder5dwOJx23RgMhnTgvMFw7CLO5r2V/O39BWkRq4Pbzs8uHcvU4b2JJZJ8smQTb85dTUl1SrzT62TOO6MX104eSq9OHY57vwD2bNrP/wy8DyEEzy5/jF5ntI+RRAgBsS8RgSdA2ZuaqOuCZL8XzOcdNWftaKTEr0o2+UrY5Cthe4P4JRRBfI2X6FeVKAu9EDogaCEDB/QujE4jvab0YspV45h04WjMFlMqMyweJx6PE4vF0n8nEgkOJ2fEYjHC4TAWi4X8/PxmeWCNlWiKorCyfhdzKjay2bef8oiPUDJ6WAEMUgKY02ChkzWLYY4ujLYVYk3KJBIJIpEI0Wg03ZHS4/GQnZ3d4vUXi8WoqKhIi2Y2m42cnJw2Xa+nklAoRElJCXq9nsLCwiPeH9rzvntSxK5GhBB89dVXvPzyy3z66afE4/H04N1uN9deey233norgwYNauPufH/QXnI0NDROBolEgi1btgDQv39/rcT8NEJ7DmhoaGi0He0eemwIEW9wd1UgOX6HZLv+8DNu2QLTp8PmzanlAGXUKPb//e/E3W4glVeUnZ3d4hdoQgg2ePcxs2wta+p3I1Q/qJUUGENMzS1kXOf/xaw//tKsZDKZdu4kEon0dJvNhtvtbnP3vGpvkGc//ZbPl25GCDAb9NxwzhncePYZBCIx3pm/lg8WrscfTmVHOa0mpo0byFUTB9PBfWzlgK3h/258htn/+YYzzh3En2c+2K7rFiIBkfcQwWdBrUlN1PdPdW40jWqnbQi2rS7mo9e+Yul7KwhXBg98KEFTTUl2G3CNzqZgbEcyB3mQ9SknlN6gJ9Plpqsnnz6ZnSiwZjY7t6qqNhPBGoWwSCSCz+dDkiQyMjLSyzSG4h/sBDMYDEiShBCC9d49fFW+gQ3e/ZRH6ggmYyiiiRrXOGYh0RkHA+hAB9lOps5KltVFjisTo9GYdhVaLJb09oUQ1NXVUVtbixACnU5HdnY2TqfzOym6V1VVUV9fj8vlOqk57idV7GpKTU0N//73v3nllVfY3HCzbDxxw4YNY/ny5a3dh+8l2gNaQ0PjZBAIBNi9ezcmk4levXqd6uFoNEF7DmhoaGi0He0eeuyI8JsI/+9B7oCUPedQd1dT3n0Xbr8dvN7UsrJM9KabKPnVr9LmG5vNRnZ2drp6J6YkWFi9lS/L1rI/fKBz3tCMbpyXWUt/HkOSANttSPb72/RS3+gK8fl8zbKTdDodLpcLl8vV5oyjaDzJ67NX8epXK4jEUuLZ+Wf25meXjqU+GOH12av4auV2kmrqCHTKdnPt5KFcNLIvFtOJc+KU76rk5t53oyQVHp/3ewZN6Nfu2xBqCMKvIkIvg0h1xMQ4DsnxSyRDnzats2JPFXPfXMScN75h35bSFuczeMzox2ein5CJfoAToQNFSZIhLGSpJiyKDj0ysiQBEnFZIWGUyHF76JGZT19XRzrZPOgO40ZLJpPs2LGDeDxOVlYWQghisVi6UcHBSJLUTABr/Fuv1yNJElt8pXxVvo519XspCdVhTUp0Uh2YRSr/LSjF2Y8fk6QnX9hxyWYcegsdzC6K3Ln0yemKxWKhtrY27eay2+106NDhO+fmasqePXuIxWLk5+fjcBy/W7O1nDKxqylLly7lpZde4t133yUYDKbCCVu4wH4oaA9oDQ2Nk0F1dTXl5eW4XC66dDlym2mNk4v2HNDQ0NBoO+1xD43FYtx+++0AvPDCC9/7yJWUu+tsUMuRHA8i2W448gKqCr/+NTz5JDTkXwmHA9+f/kTVuecihECSJCSLgRXxEuZUbyKYTL3Am2QDZ+X047z8QeRZMlLLht9C+B9Ordv2P0j2+1oteCmKknZxNXa1g1QZlMvlwm63t9m9LoRg1sptPP3RIirqUzlLAwvz+MX0CXgbRK6V2w+E6Q/tUcB1k4cxbkA3dCfJMf/0XS/x2T9m0Xd0L55c+McT5v4RSi0i9ByE3wYSgATmi5Ds9yDpOx51eX9tgAXvLmbOmwvZ9O22FufL7pjFuGkjGTdtBH1H9yIhFHYGKtJljzsC5WkXlaSCJSmTJcxkCBOKqiIhIUsycVnBJ8eJGyS6uHPo7S6gj7OAQnuHdKOD/fv3Ew6Hyc3NxeVyIYRAUZRmZZCN/1bVQ51bkBJTmwpgiqLg9/tJJBJ44yG2hytZlyhjU6wSfzKS6i4qwIGRPGEjGyt6IadzwFQdWM0WRnfuz8ROA9rU9fR0IZFIsGvXLiRJoqio6KjNLNrzvntaiF2NhEIh3n77bV555RW+/fbbY17++4T2kqOhoXEy2L9/P/X19eTk5JCTk3Oqh6PRBO05oKGhodF2tID6tiHCbyP8D4Gc3eDuajlIOk1dHVx1FcyenZ6kFhWx5am/sMptpDRcjyKp1OiiCJuBcwoGMzGnHzb9oS+xIvQ6IvCH1H9sdyLZ725RuBFCEI1G8Xq9BIPBtBCh0+lwOBy43e7jFig37C7nb+8vYP2ucgByMx385MLRROMJ3py7hr1V9altyhLnDOvFtZOH0LfLySvTaqS2vJ4bu/+UWCTOo5/9LyMuGHZCtyeS+xDBv0P0vw1TDGC9Fsl+B5LcvNlRNBxj6WcrmfPmQpZ/sQZVObxglNutA+MuH8G46aPoNbzoiOJkTEmwPVCeCrz3lrAzUIGCiizArhhwJPVkCDMGSSahpkw0SVng08Xx6eIoOokezjz6uAroojixxSSyMjKP+LuwEIJEInFIHlg8Hk87CJPJJOFwOF022+godLvdmM3mtBhWGfMxs2wtq+p2sSdUTSAWwSWM5AobbkxIInXNJyWVGiL49HGsZiu93QVMyOnLWE9PDLrvhtPL6/VSWVmJxWKhc+fOR53/OxdQr3HsaC85GhoaJ4MdO3YQiUTo0qULLpfrVA9Hownac0BDQ0Oj7WhiV9tIubvOBbUUyfFbJNtNrV945UrElVci7d6dWhewfkQfPvztTyhwd6K7M4eOdg+ezFTnxpbEDBF6DRH4EwCS/WdI9p81+1xRFAKBAF6vl1gslp5uNptxu904HI7jziCtqAvwzCeLmLl8KwAWk4ErJ6RypT/+diO+UEOJmcXE5WP7c/XEIeRmnrzyrMPxz1+/zrt//YTCQV34x6q/nJQcVpHYmOrcGF+SmiDZkWw/RjFdz7r5xcx5YyELP1hKNBQ77PIFPfIYN20k46ePpPuQbm12pEWVBNv9ZWzylbDZV0JxoBIhVByqAZdixJbUY5R06OWU+BVDwa9P4tPFMaoyXZMObBYrnoIc+jgL6OnMP6wYe9hj0FA6W1lZme7yqSgKRqMRs9l82PPQmAdmMBiIRCKEQiF8SpRlkT0UJ+qIRSJYEzIG9cCyYSlBpRSigjAJScWiM+IxOejlzGd8h95MyOmLWXf8WXftTWlpKcFgEI/HQ1ZW1lHn18Su7zEzZsxgxowZKIrC9u3btZccDQ2NE4YQgo0bNyKEoFevXt/78ozvGprYpaGhodF2NLGr7YjwOwj/747J3eWLh5ldsYGvy9cz+MPZ3PT0x5ijqXJCodcTv/tuyu+8k1iD4+VonRtF6BVE4DGAVImc/U6i0Sg+nw+/3592ccmyjMPhSAd8Hy+RWIJ/fbWC/3y9imgiiSTB+AFFGA065q8rJpFMuYQKspxcM2koF4/uh818eggM/toA1xfdRdgf4YG37mHiVWNO2rZF7FtU/1/ZuWY3cz7IYN5HmXhrDl+u1rVfJ8ZePoLx00fStX/nE1JyGVXibPOXs9G7n82+/ewJVGNVdbgVI07FCEJgkHXoJB1xNYFdMRDVKawx1SJkkJHoYsumjytV9tjLmY/LeGjDBUVRqK+vp76+Pn1NOhwOPB4Per3+EBdYLBYj2VDym0wmCQaD6fgmk8mE2+3GYrGkO0L642GWlW2lpK4KbyxERImjoFBHlAopTA1hGkxgSIBZZyTLaKe7M48xnl5Mye2PzdAKd+YJQlVViouLUVWVLl26YDYffSyntdj1yCOPYLfbue+++9o8KA3tJUdDQ+PEE41G2b59O7Is069fv+9kd5fvM9pzQENDQ6PtaGJX2xEijqg5D5QSJMdvkGw3tzjv3lANM8vWsKhqayqHCHAbrJyT3Y8L/9/LmF59LZXtBYjMTMLPPEPlyJHpMi+z2YzH4znssRWhl1B8jxOM5uJL3kpUnJn+zGQy4XK5cDqdR80Aag2qKvhixRae+WgR1b5UAHthXhZWk4GNeyrS8w0qzOO6ycOYOLjopOVxHQuv//F9Xnv4HQp65PHypr+j0x//sTka5bsqmfPGQr7693zKiysPO0/hoC6MnzaKsdNG0KXP0bO92ptwMsZWfxlbfCVs9O6nzu/FoRhwqUZkVcKjmDCgo94Yp1IXoUoKEZHVlILUQEdLZkr8cnWklyMPXUShtrY2LVZZrVY8Hs9RRddEIkFlZSV1dXUkk0mEEJjN5hZzuWRZRq/XpztKRuIx9kdrKYnUU5Xws1fxsl/4CRBvNl5I/dcoG8g02Smy5zAyqwfn5Q/CeRjh7kQQCoUoKSlBr9dTWFjYqneN01rs0mgftJccDQ2NE43X62Xfvn1YrVa6d+9+qoejcRDac0Dju8rEiRNZsGABDz/8ML///e9P9XC+82jHs21oYtfxIcLvIvwPguxpcHcdeIFXhMrqut18UbqGzf4DweyF9g6cnz+UUZ4e6fBvyspg+nRYsuTAugcMwPvKK9S63WmhwGaz4fF40s6PeDyO1+vFV/01SnQpALJ5NPaMCWn3S3t9Sbe2uJTH31vA5r0pocZtM2M06KnyBlPblSQmD+3BtZOGMrAwr122eaIIByLcUHQXvpoAv/jnHUy9dfIJ2Y632seCd5fw5Stz2Llmz2Hn6TEwxrgLaxl7gY+Cnn2QHL9CMg4/IeM5VkLJGNv8pWys38+u2nJs3iRuxUhAThCRFYQQyHqZuAFqpAgVajAlbgpwqQbyklYyZCvZJgfZdjc987vSOTP3qKWjkUiEioqKdBMFp9NJhw4dkGU5HYp/cB7YwaH4jfM1CsZ6vR5VgjLVz8ZEJetjZVTEfUSUBIJDJRwJMMh6Mow2utk6MDyriPPzh5Blbv8y3Orqaurq6nC5XOTmti7L7lSIXd/d+H8NDQ0NjcPS2NK4NZZiDQ0NDQ0NjR8Qlssg9DwoJRB+E2y3EkrGmF+5iS/L1lEV8wGgQ+ZMT3fOzx9MD0feoQJUfj4sXgzz58M110B5OdKGDWSceSau6dOpefxxvNEooVCIUCiEwWBAluUDWVz6MzBaFZz6N3Ba52FwRZGsLTvNjoWyWh9PfbSIr1dtB8Cgk9HrdHgb8rhsZiOXjenP1WcNJj/ru5FranVY+NFvLuf5+17jP394j8nXjsPYTmWWkVCUJZ+s4L//nM2Gb7ZwOC9MrzO7M/HK0Yy9fAQ5na2I0MsQfgUS6xB11yJMZ6W6bBp6tsuY2opNb2JoZiFDMwuhCPZXlrGzbC+VSpC98Tpi4QiSIjAokIcZj2RA0umwYwAVomocn4iwLV5HnW87+JeTYbDRx1VAb2cBfVwFdLRmIksp8UtVVWpqavB6vQgh0Ov15OTkpEUdSIlWer2+mbhzcCh+owDW6PRKJBJp4csjjEykE5NMXbBl2rDax9bZcgABAABJREFU7WxRq1jq28WOUAVVUT8RJY6KIK4mqYz6qIz6WFq7g2e3f4le1uM2WOls8zA8s5Cp+UPIs2Yc13EOhVIuSav15DjJ2oomdmloaGh8z9DELg0NDQ0NDY3DIUkGsN2J8P+Wsrr/8FV5J+ZX7SCqptwkNp2JybkDODd/EB5TKxwhEyemXF5/+Qs89BDEYsjvvUeHTz/F/bvfsXv69Ga5RxaLBY/HQ0ZGBjbb9YigF0LPIgJ/BnRIthvavG+haJxXZ63g9dmriDdkcMmSREJRSSgquZkOrjlrCJeM6Y/D8t3LM73oJ+fwwd8/p3p/LZ+/8DWX331Bm9elJBVWz17Pp/+Yxcov15JMKM1nkKDPiB5M+tE4xlx2JtkdmweQS467EdYfIYLPQeQdiM1DxOYjLJch2X+OpMtv89jaE7fVQb41k0JzPld36UIgHmFD1W721JRR561HiiqYEim3oiKphHQCr0XBYrLiEUZqY0HqEyEW12xncU1KPLXrzfR25lNkzsYTN5KNFVmScblcZGdnt6r8VpKkdH5XU2FMCNHM/RWJRPD7/YRCIZLJJIFAgEAggEeWudzYC1PmQCwWC3q9ns3xSpb4i9keqqAq5ieUjKEiSKhJqmN+qmN+VtXt4vmds9FLOlwGC52sWQzNLGRq/mC62LNbdUwbhThJkk57V2ybxC5FUairqyM7+9ADEovFmDlzJjt37sThcDBhwgR69+593APV0NDQ0GgdmtiloaGhoaHRMlarlaqqqvS/f0gIIdgQG8oXpeNYGzAj5G9BzqDAksnU/MGM79AHk85w7Cv+1a/g5z9H3HgjvPceUiyG8cEHKXz6aUoee4y6gQORZRmz2Zx+kbdarUj2nyFQIPQPROBRQEayXXdMm1ZUlc+WbmbGx4uoDUSafaYKQf+uuVw3ZSiTBvdArzv98rhai9Fs5LrfTefvt7/AW//vQ6beOgmLvfXh/UIIti7fwcfPzGTxJysO6aQoSdB7RE/Ovn48Yy47k8zcI7t/JF0HJNfvEbYbEYG/Q+xLiHyIiHyOsF6PZL8dSXa3ZVfbjcbfhWOxGKqq4jBaODO3J0WGLLwWL8FYhNqQD18yTFUyiC4p4wnLKBEVn07FLukxWEzYDea0aBRMRFlatZ3FyhYATLKBPu6ODLR2pU9QociRg0Fum6dIkiRMJtMhzaVUVSUQCFBfX4/P5yMejxOPx4k2uCdNJhNdjHa62YaALbUeg8HAzkQNSwK72BauoDLmJ6TEUIRKUijUxoPUxoOs9e7llV3z0EsyDoOFjpZMBmV05dy8gfRyFRwyxkZXl9lsPqZcvVNx3z2msxCJRLjvvvv497//TSQSSQfW/+53v0OSJJYtW8ZVV13F/v37my137bXX8tJLL2E0nh4dLTQ0NDS+ryiKks4L0MQuDQ0NDQ2NQ5Ek6bBf2n9XUYTKNn8Z9fEQGUYbvZz56KTmok5MSbCoeiszy9ayP1wLahegksGW3UztPISBDh2SLgKHySYSQhCJREgmk+j1+sPmaiWTSXyhEL4//xlx660U/PznmLZtw1BVRddbbqHryJGE//MfagwGotEo1dXVeKuqyPvoI0wlJdCpN1yzGRH4AwIdUekSEok4erEZnVzD+0sUSuqddMp2c8WEQRgbQr9XbS/hr+/NZ3tJdbPxSBKcNag7108ZxsDCw5Rhfkc556aJvPPXTyjbWcEfrnwCo9mAxW5myvUTGDKp/2HFh/3by/jgic9Z8N5igvWhZp9JskTvM7tz3s2TGH3pcNzZx17WKem7IWU8jYivQwT+ConlEH4ZEXkPbLeD7fpmnT+FUCC+EtRqkLPBeAaS1D6B+wdfq42CTOM1l0wmCYVC6VLN3Kxs+vfqg9FoJBwOU+mtZU9NGVVhL5URH4FEFDWh4tdFCOriJElil4xYZAtJScVPFEWGzaEyNofKQIBbmCi0dqCbqwO9sjrRy5WPWWds1c8ptPDzLKecYy6XCyEEoVAIv99PIBAgkUigKArRaBSdToder0eWZeLxOJ1x0tk+GBrMY5IksSday4ZwKdujVexK1BFUow0CmEp9PER9PMQG335e37MQnSRj15vJt2Qw0NWZszJ64wil3F3/n72zjq+rvv//88h1i0uTStpCoYK1xZ3izmAw2JDhMH7IgOGd4GNs8IUhQ4cMH+6lQIeVAoVSgUrSpk3S2HU7+vvj5p4mjTRJU4PzfDxum5zzsXOu5J7Xeb9f7+Li4l7Pe0+fERlD5aXIXFam2qmOFHHq6H3wSBtWHxqQ2HXsscfy3nvvWS+OeDzOn/70JxRF4dJLL+WYY45h9erulRqeeuopdF3nqaeeGppV29jY2Nj0SD6qK+8PYGNjY7OxePzxxznrrLPQNI1rrrmGm266ab3Gq6ur4x//+Afvvfcey5cvxzAMRowYwcEHH8zvf/97RowY0aX9+eefz/33308oFGLu3LmMGjWq25j33XcfF1xwAZIk8cEHH7D33ntbc9XU1ABQW1uLqqrcdNNNvP/++7S0tFBeXs5hhx3GDTfcwLBhQ5+eY5omDz30EA899BALF+Y8cyZOnMiFF17Ir3/dd5TLSy+9xCOPPMKXX35JOBymsLCQqVOncuaZZ3Lsscf22Of000/n8ccf57TTTuOxxx7jscce44EHHmDBggWIosjkyZO54YYbrPOjaRr33Xcfjz32GIsXL0YQBPbYYw9uvPFGdtppp17XFo1Gueuuu3j55ZdZsmQJqqpSXV3NtGnTuOKKKxg9evTgT9p60tjYyAMPPMC5555LZeXmbU7eF7Nbl/DQ0g+oT7aimQayIDLcV8JZY/Zn55KxtGXjvNv4He83zSOh5b4juEQH+1bswUGumxjmWAnm5xAjZ3ktVkDwWgT3wUDueq+pqYlEIoGu60iShN/vp6KiAr/fTzqdJhKJkEgkrGtEafRo4rNmIX38MY5zzkFob4fPP8c3bhzeM84gfvvt6FddRcHDDyN0Muk2rxVQzhzG8vM+JqFmMbRWfmxyM7vWx4LVSVoTdQD8/cVZHL37BFqiSf73fW2X8+FxOjhmjwmctN+ODC8t2NCnf6MjO2SGjxtGw5Im5rw919o+48lZePxurnjsd+x13C60N4V57o5Xmfmf/9HeGOkyhiAKjJsyhsPOmcaex+5CoNDPUCA4t4eiJ0D5GDN+B2g/YCb+Cql/g/9i8BwD2RmYsZvAWFMJc+3X3GDp6bXqcDhIJBKkUimam5uRJAmn00lRURHDhw/vEmHk8/kY7fNREihg5cqVRIkSz6aI6mniqESUNFE9TVTMEhHTpBwGoiRR4Q4SdHiRsyauhIZHFZFSCo1t9fxQt5RVzjS4ZZoyERJqBgRwCFKX92medb2fISdY+f1+/H4/uq4Ti8WIxWLWNQBgRVE6nU6r2mM+BdKlKOxkFDNFLMXpz6VRtoppPk/VsSizmkYlSkLLoJkGumkQVVM4FJOVYXirtoVKfIBAS+M3yMV+dioZQ2XKSTqd7vYZEQjkUqFvmvcSr636ikx7nJY3v6H0sB15dOmHHFk1mWsnHbdez3tf9Lsa40svvcTxxx+PIAicdNJJ7LrrrsyaNYsXXngBt9vNlVdeyZ///GcuuugiLr30Uqqqqli2bBk333wzTzzxBIIgMGvWLHbfffcNdjBbAnYVLhsbmw1JW1sbq1atwu/3b9KLCJvesf8O2Gyp9FU98NZbb+Xqq69GFEXuvvtuLrzwwvWa66mnnuLMM8+0zKxdLheiKJJO51KUAoEAL7zwAgcddJDVJ51OM3XqVObPn89uu+3Gxx9/3EX0//7775k6dSqZTIYbbriBP/3pT9a+zmLXM888w9lnn008HrcuJvLzFhUV8d577/Up8PSX/Pm87rrrmDdvHq+88gqyLOP1eonFYla7tdeaR1EUTj31VJ599lkgd3GT/2zJ+yP96le/4vHHH8fh6JqW1lnsgpxQmb8bH4/HgdxNk//+978ceOCBHHXUUbz77rs4nU4cDkcXc+KPP/6YyZMnd1vf/PnzOeSQQ1i5MlfVz+1243A4rPFdLhdPPfUUv/jFLwZ03obiMzSbzfKb3/yG559/ns8++4xdd911UONsama3LuEv379IRlfxyS4cooxqaCS1LLIgsn3hKFYkW9HJvR7KXEEOHrYD+5VPwKt9iBm5qIdRc9EYQsHdJNTdqa2tRVEU3G43siyjaVqX92Hn6A2Px0NBQQF+v39N9TrDgOuvz3l6aVpumyxjdvzcOfYjf1Ha9Ks9abtkF75aXsYPjZWE3BJJ1eDzugitCbXHc1Ea8nHy/jtx3J4TCXh/upHtD175BM/f8WqfbUKlQaItsS7bBFFgqx1rOPL8g9jr+N3wBTdsGplp6pB5FTN+FxgNuY1iRVeRa83qcv8W3D1owSsej3d7raZSKdrb21FV1RK+nE4nkiTh9XqpqamxxJjexpEkiWQySTQazYm5DomMwyBGlmYlziojRkRSEEwYp4TwmDI4JGRJRjDBVDUiepqPWUEracSO94ssSJiAV3LyhwlHs3/FxD7fz27JwfUTf9FFGFubTCZjCV/5aqiA5enV0tJCNpu1/h7kvcEEQaCgoKBbFt5qJcbnqTpWptqpzLiQDYGMqVJILnU2hUpKUJktNtEmpHELDookHyOdhWwnlbO9dzhbjx3L3XXv8cqqORiKxrI7XiM8axHb3nkq/m1zKZJHV00ZsODV378D/U5afvLJJxEEgYsuuoinnnqKiy66iOeee47TTz+dTCbDzTffzMknn8xdd93FqFGjcDgcjBs3jscff5xjjjkGgCeeeGJAB2FjY2NjMzBsvy4bG5uNiWmaXHzxxVx99dW4XC6effbZ9Ra63nvvPU499VR0XefKK6+ktraWdDpNMplk0aJFnHDCCcTjcU444QRWrFhh9fN4PDzzzDN4PB4+++wzpk+fbu1Lp9OcdNJJZDIZ9thjD2644YZe5z/33HOpqanhiy++IB6Pk0wmeeeddxgxYgTt7e0ce+yxlmAzFNx77718+OGHPPbYY8RiMaLRKPX19Rx55JEA3HjjjSxevLhbv2uuuYZnn30WQRC4/vrraWtro729ndbWVq655hoA/vOf/3D99df3Ovcrr7zCc889xwMPPGBdJC1atIjJkyejaRoXXXQRl19+OXPmzOG5554jkUgQj8eZM2cOY8aMIZVKcfHFF3cbNx6Pc+SRR7Jy5Uqqqqp44403rLSbuXPnsuuuu5LNZjnllFP49ttvh+hM9h9N03j++ecBulwUbknopsFDSz8go6sUOn24JAdix3bV0AgrST5t+QHN1BkfrOb32xzBP6aczhFVO+GV5Fx0TY/kJCcjejNNTY0oioLf78fhcKDrulUhLpFIEA6HrQvlkSNHMmLECILB4BqhC3JpkTfdBG1tcOihuW2ahkBXoYtOv1c8+wlmVmHeyjKymkFzQsXnkNi2zN+tz6jyQm4641Bev+lMTjtoyk9a6FIUlRfufG2d7fJClyAKjNlhFJc+eC6vRB7n3i9v45DfHrDBhS4AQZAQPMcilL6DELgKCPUidEH+NWfGbs6JZAPENE2ampq6vFY1TSMajVrvb8MwcDgcFBcXU1BQgKIoNDU1dak8ufY4ecGss4BWVljM1DET2bViGw6t3IFTKnblBP/2HGiMoszwkiYnbrUocZrVOK1mGtkQ2MYowiM6cYgygiCgY6CbOlE1xR+/e56Lv3yUG79/kbiaJih7cu9nQcAlOSh0+sjoKg8v/QDdNHo7DbjdbsrKyhg9ejTDhg3D7/cjCAKpVIrly5cTDocRRRGHw4HX66WgoICysjJLDKuoqKCoqMg6h+XOIEeFJvEr93bs6h7J7gVj2cVXQ5kcQJZkEqKGDyfjjEJME9KmyiotwqepWu6Pf84Fq5/nsE9u55VVc3Ln1zAIz1pk/ZzntVVfkdaVAT/v/aHfYtdXX30FwCWXXNJle/4Lja7rXH311T32veqqqzBNk88++2yQy7SxsbGx6Q95scvj6b9hqY2Njc1gUBSFk046ibvvvptQKMTbb7/N8ccfv15jGobBhRdeiGEY3Hvvvdx2222MGjUKQRAQBIFx48bx3HPPcdRRRxGLxbjzzju79J84caK17dZbb2XmzJkAXHrppcyfP5+CggKefvrpPk11ZVnmvffeY+eddwZyKSMHHXQQb7/9Nk6nkxUrVnD//fev13F2JhwO89///pfTTjvN+uyurq7m+eefZ9iwYRiGwXPPPdelz6pVq7jrrruA3PfsP//5zxQUFABQWFjITTfdxGWXXQbAnXfeSWNjY49zRyIR/vWvf3HOOedYc48bN86KFqurq+Oee+7hlVde4YQTTsDhcCAIApMnT+bBBx8E4JNPPrGit/L885//pLa2FofDwdtvv81hhx1mCSDbb7897777LqNGjSKbzXLttdf2eX6y2awlxOUfg6WxsZGvv/6auXPnWtvmzp3L119/zddff93redoc+SHWQH2yFZ/sQhAEVEOjMROhXUmgmjqiIGAC54w9gOnbHc/OJWPX+AMpc/oQHgBM0pkMiehy3G43hmEQjUaJRqNWtKXb7UYURSorKykvL1/3TbZgEN58Ey6/vM9mAiAYJuZjqwm41kQkRjMapQEnIU9Xi4bj9pzEoTtvg2MARtlbKq/98x1Mo19JWRx78WG8Fn+C+7/+K4edNW1ARvZDiSC4EHy/hYLb1tHSBKMx99ocIOl0mkQigdvttioWhsNhVFVFFEWcTmeXCERBEHC73SQSCStKsfM4LpeLTCZDJBKxXu8ej4fi4mJ0XScQCFBTU8OIESMYVlLOCF8JZZKfGn8ZU3wj2ME5jDGuYnyyCw2dsJmh2PTgMUV008AlOih0+Ag4PDhFGdXUWZxooiUbRzE0mrIRGtNhMppirdcnu1iRbOWHWMM6z4coigQCAaqqqhg9ejTBYNDy0lIUhVgsRjgcRtd161xkMhkcDgelpaVWv7Fjx1JWVoYoipYHl2wKFMlexvsqOTAwju3kCg6QRvNL50S2lUopEj04OvzXTCCLjtKeILmkidTSNXZXqWWrSS5pIrmkiUx7nH8v+2jAz3t/6LfY1dzcjNPp7OZ/sNVWWwEgSRLbbrttj30nT56MLMtd7r7Z2NjY9BfdNPiqbRnvNHzLV23L+ryrsTn03VRzzmlbyodti1iUXY3DZRcEsbGx2XDEYjEOOeQQnnvuOSorK/n444/Zd99913vcjz/+mMWLF1NSUsJZZ53Va7tTTz0VgHfeeafbvvPOO4/jjjsOwzD49a9/zYMPPsgDDzwAwL/+9a9uXl899S8rK+u2fdttt7XEvGeeeabfx7Qu9thjD/bbb79u210uFwcfnEvp+e6777rse/HFF9E0DbfbzVVXXdXjuNdddx0ulwtVVXnhhRd6bDNixAhOPvnkbtvHjBnD2LG5dJm99tqLPffcs1ubffbZx6oYtvb68mLZ8ccfz8SJE7v1DQQCXHnllQC89dZbRKPRHtcHcMstt1jGzKFQiOHDh/fadl088MADTJ48ucvx/O53v2Py5MlMnjzZep1sCYSVJJppWFXfFEPr9p1BNTQeWfohty94heeWf8bstiU0Z6KYevM6x9d0N7qeS3GKxWJommZViguFQhQUFCDLMv10xFlDJ1+hvnA3tBJ0rxG2FN1AFgWcctfL11Wtvb92fmo0LO3ujd0b2+y8FS6Pa90NNxKCmepfQ6Nl3W3WIl+VMJFIEIvFUBQF0zQxTRNBEKzXaOfXqiRJGIaB1pFOmxd082OkUikMw0CSJILBID6fD1mWrT6CIFhpuz6fz5rLIcoUOLyM8ZRx6LAd2KmoBkMwcQkybhyYmGQNlZiWxie7KfcU4JNd7FK8FQ5BwinKCAjopkFcW/NecYiyZSDfX0zTRFEUNE2z1med5k7Hvva5yPfLi4bpdBpVVdE0DUmSrEf+HDsEiZ0d1Zzr24Ub/AdwX+UJPDDiJM4o3IUCwU3Lm9+w4KJHWXT5k9b8K/75HgsuepQFFz1Ky5vfsDLVPuDnvT/02704b+a2NvkcyZKSkq4hq2v1zedU2tjY2AyEmU3f87eFr9OcXXMnt8wV5PfbHsF+Fd2/QG/qvpvLnP/+4qt+zWljY2MzUBobG9lnn32YO3cuW2+9Ne+8806PZvCD4ZNPPgFyxuZ9GcHnq84uX768x/0PPfQQc+bMYcWKFZx77rkAnHXWWf2KPNt///373Pf000/z3XffoapqNy+swbDLLrv0ui9/Dtrbu14IzJmTi36YOnVqr34lhYWFTJkyhU8++cRqvzZTpkzptUpdeXk5S5YsYerUqT3ulySJkpISVq1aRTgctrYrimKJX9OmTev12A488EAgd9H19ddf9yj4AVx99dVWlBrkhNbBCl7nnnsuRx11FOl02hK87rnnHnbbbTeALcqovtDpQxZEVEPDJTnwSW5kl4RiaCiGRlZXMTFI6Vm+aq/lq/Y1Ru5eUWWkNIVR7jij3DFGueIMcyWRhTVigCxlQHASiUQQBMG66M9f7+WjZgZcDGfMmH41y1SVIItrXptOSUQzTBStq6BXPYjqgVsqw8aU97ttcWXhBlzJwDHF4nU3glx1xn6SzWaJRCK0tbWhKAqSJFlRwEAXoSv/+s3v03UdURTRNI3Vq1cTj8fJZDIYhoEgCDidTlwuV5eosHwklGEYtLa2kkgkrLTe/FwOhwOXy2XdCChw+vAIuYjYArcfBAdt2TiaqdOSiRJ05CKhJhfX8Hnrj7ik3N+UlmwMrVNKp2poyIJIodPX+znuEKlSqZT1MAwDVVUtYUqWZRwOh+Vflj8ugFQqZRWbyGazVoV3RVEQRRFJkqw0yPx5zj9cLpflcZb/u7iXbwzNSozmw3akYNetMLKqJXiNuOBA/NtWA+Ao8lPtLer38z4Q+v3pVFpayooVKwb9hz2bzRIK/Xw+jGxsbNafmU3f84e5T3fb3pyN8Ye5T3PbDif3KuZsir5b2pw2NjY2gyGfvuZ2u3n//ffXK9JmbRoacikaqqr2WOF7bTqnoHSmsLCQe++91/K9Gj16tJX2ty6qqqrWuU/TNNrb2ykv7//FZ2+sbZDcmbyQoKpdTbmbm5u7rKc3qquru7QfzNwDXV97e7t18dTX+vJr62t9QJcLx/WlsrKSyspKy2AfYIcddhiSggMbm3HBYQz3lVCbaM5Fg3T4+7gkB6ZpElaSjPKVcsk2h1GfaqM20czyZAsrUm2kdJmFSikL00Xk/ZIcgsFwV4JRrjgj3XEqZT8p3YuuKHg8ni5Cl2maZDIZQqHQwG0TLrggl8rYi1dariKkQOyEqQhNIIkCumEScss0RLNE05rVVhQETthn+0GcvS2TIy84mPsve3yd7Uqqi5m41zYbYUX9wzTaIfHQOloJOQN755S+xzJNEokEkUiEVCoXLSYIArIsk81mrc8KSZKsyMN8FFY+1TZfwVCWZZqbmy0xKx+plc1muxRfME3T8gBzOBxd/jYJgkAoFLLeE2sXbSh2+imVfNQZERKCikOUKHOHaMvGyegKbdkENf4yDqrcnldXfkVtopmQI/ee0k3DEuuSWpbR/jLGBbveBNI0zRK2ksmkFZ2VR5ZlAoEApmmSTqetmyN5371sNksqlUIURStls3PffBqzpml4PB4cDocV2SWKovVZv7ZXX/58HF+6E28lF+Es8qNn1vhyeUeX4xtbAYCIwKmj9+nzeR8s/Ra7qqurWbFiBfX19d0qfP3nP//p84MuHxI4lF+GbGxsftropsHfFr7eZ5s7FrzGREfFGg+KTn3/uqDvSjVD3XdDz/nX+a+xrViKJIjW3RndNLh9ft/97lz0BnuXj+82p42Njc1gOeKII5g1axbRaJQzzjiDV199tUv59vUh/8V5l1124fPPP1+vsf71r39ZP69atYolS5aw3XbbrdeYNjabC5IgctaY/fnL9y8SVpI9Vm87e+wBTCgYzoSCNddgmqGzMtVGXfg9atufZXkmQF02QNqQWJYJsiwTxIxIqLoXk1kU4KEyE2S4UESVs5By0Y+smrhcLioqKnqNDOwVpxMuuwz++tduu6xqjCcdgOCSKAsmaEs6ccsiSUVnYXOCzkmTv562E86BRpZtoRiGwSW79+1vl+eCf5zRpy/hxsRUvsSMXAbGanLSg0bOma3zM9kRiRW8BkHoed2qqlq+cXlBJx9pZZomgUAAXddRVRWPx2M9wuEwpmlaPlzpdJpMJoMkSQQCAev/YDCIx+MhkUhQW1tLPB63ijJkMhkURbEq1oqiiNfrxe/34/f7kSSJgoICamtrLe8wSZKsvmMKhvG/xCrC6pr3aUD2oBgaJgZJLcvbDXM5c/R+3Dj/JaJqCkwwMUnpClldxS05OHPM/ggmJFNJkskkqVSqm0CV99fyer2WPqOqqnX+Vq1alXteOq4lDMOworVkWcblcuHz+fD5fLjdbpxOJ6qqsmLFChRFsapZ6rpOKpWyboYkk8lux+1yuRhRNYK9U9vwYfPCXl8jR1ZNxiNtGPuVfn867LTTTnz66ad8+umn3cSuE088sc++eWP6CRMmDGKJNjY2P0fmttd1ScvriRYlzsxl37CNq+ud9UXZ1bQqiY3ad0PP2arG+Xj5d93mbFP77rc6E2Vuex2Ti0f32c7Gxsamv0yePJnp06dz0EEHMWPGDA4//HBef/11fL7e0yv6S0VF7k5vb+mJ/eWee+7h1VdfRZIkxo0bx4IFCzjppJOYM2fOOoW5VatWMW7cuF73Qe6Od1HRhkm76A95T7G1jeHXJr+/Jw+yDUVRUZF1wdPX+jrv25jrW5v8a25LZOeSsVw/8Rc8tPQD6pOtJLUssiAy2l/GmWP2Z+eSsd36yKLEKH8Zo/ynsE9pCWbsJgy9iRbVQ102wJLUKBZGhrHKMIibUZIOiUVaKwtizeQECoECh5exciVbtcUYlS1jlK+UMncQsb831m6/HQDzzjsROkd4iaBccAKJ/3cTZuMPlAZaMcwUny9zsGB1ktZELoJQFAR+PW0nLjlu7/U8g1sGhmFw3o5XUDsv538tOyQ0Te+qFwGegJsrHv0dex3Xe2r0xsI0DUg+gJm4G9BBqkEouAv05blKoJ0LJIgVOaHLffBaY+SikfKpddbN3g6RSxAEK5rI4/FQVFRkiUD5NNuSkhKy2SyJRMJKXXS5XJSUlFBWVmaJVfn5RFEkGAyyevVqotFol8iwsrIySkpK8Pl83cTEvGl9U1OTFTEliiKhUIhxFeMIZiu6vU8nhKoZ7i1hXnQFT9X9j4Mrt+faCcfyyLIPWRBdiW4YpFWFCb5KflmxM5VpF0uWLOnmk+d0Oi2xShRFFEUhHA7T0NCApmmW2JXvl7+pJEkSfr+fsrIyqypjPhV0bSRJ6vHY8p+fve3z+LwYgFd0EmdNZJej0IeIwJFVk7l20nEDf4H1k36LXbvssguPPPIIX3/9Nb/+9a8HNMlTTz0FMCTGpTY2Nj8PWrP9K+uuuIRufiVKbN3Gq0Pdd2PMqblFCgtyHgyCIKBH2vrVr7/n0sbGxqa/TJkyhRkzZjBt2jQ+/PBDDj30UN588038fv96jbvHHnsAuS/Oc+bMYcqUvlNaemLevHlcccUVANxwww2cdtpp7LDDDixcuJBLL710nSbkM2fO7NW3K1/dcbvtthsSv67BMmXKFJ544gnmzJlDNBrt0SokEol08fbaWDidTrbbbju++eYbZsyYwZlnntlju/fffx/IRSJs7DRCj8dDbW3Ow2pdBQs2d3YuGcvk4tH8EGsgrCQpdPoYFxzWr4huwX0wuKYhKXOoMFooUIooaS5hZ2cUt/4E/uBH1OujqRMvZGk8RV2yhdXZGBnB4PvEKr5PrLLG8khORvpKGOUrY5S/lFG+UoZ7i5HFniN1zJsvwLx0GebtHyD8I5yL7Zn1Fq7dD2GsaeJ0OolGI2w1NsIx06Ls/ocIAEftOp5rTjngZxXRddbEy6hflDvXvgIv/15yD26/m1fueYvv/7cIj9/NtN/sw477T9wsIrpMvQ0zegUo/8ttcB+NEPwjgugDxzbgmtZREbQl59HlnNIloiufZti5+mdeoMn7TkHuu7Df7ycUCuH1eq0or3g8TiQSIZ1OW1FdXq/XErJKS0utdEfDMEgkEtYjP09BQQG6ruN2uwkEApaI3xeBQAC/3086nbaqH+arGO4cCPT6Pn1j1dc8Ufsx7zR+S3s2wV+3O5n75r9FY6SVqf5RTCmsQVAFkkrSEvryaYSCIKAoiuWxlRe28qb0ea8tt9ttRW05nU7cbjdut9ta37ro69iAXve9sOJz6lNtlHiCFDm8yP/+fwiCwNHb780l44/YYBFdefr9KXHyySf3WK1lXei6zjbbbMP06dM56qijBtzfxsbm50mJq3ePkM5sWzWaUcWjumxrazOg4YON2ndjzDluWA3Di9ekImztUaDvm/pA/8+ljY2NzUDYcccd+eCDD5g2bRqzZs3ikEMO4a233urT42ld7LfffowdO5YlS5Zw6aWXMmPGjB4LJOVpb2/vEmGVTqc56aSTyGQy7Lnnnlx77bVIksSDDz7IL3/5Sx588EEOOuggfvGLX/Q65v3338/FF19MSUlJl+0//PCDVdVwXVkNG5pf/OIX/P73vyeTyXDbbbdx8803d2tz8803k81mcTgcfR7vhuCkk07im2++4YUXXuCaa67pVpExkUhwe0d0z2GHHbbRfX1FURyyogqbA5IgMj5Uve6GPSAIErh2IZ1Os6plFYah4/GVM6zicsTo9xTq89lOuhWh+gkEqZKMrrIi2UJdsoW6RAu1yWbqk22kdYVFsQYWxRq6rKvaW5QTwHyljPKXMsJbjE95ETN+OwgZuKoGHkxBKovw+H9h90MsESOTySC7x+EOViBLd6HpBtuMKP/ZCF2apvHbbS+hsaMKY6DIzxNL78EXykXRnnDZUZxw2eZ1fW0qszvSFpsBN0JwOniO6yKo5F9za5M3nI/FYhiGYXll5aO48mKTw+GwqrPmhS/DMIjH41Ylxc7G9J3TFPMG8/F4nHg8TjKZxDDWFD2QZdlKT8wLaANBEIReo4d7e58eUrE9Qd3Bq8u+IN7Yyn9a3qXGEUAzkuhZlWQiiSiKVrph5yIR+UfeWD9fVNDj8eB0Oq3j8Hq9lrH8YOnr2HratyLZykv1swEY5S2lPt3GiFEjEQWRE2v22OBCFwxA7BoskiRx7bX9yy+2sbGxybND0SjKXME+UxnL3SF2KBq1WfTd0ua0sbGxGQq23357PvjgAw444AA++eQTDj74YN5+++1eKwSuC1mWuf/++znkkEP43//+x957781NN93E3nvvbUVSLVu2jHfffZeHH36Yo48+muuuu87qf+mll7JgwQIKCgp46qmnrIujE044gTPPPJOHH36Ys88+m5133rlXL1lVVTnwwAN58MEHmTp1KqZpMmPGDM466yyy2SzDhw/nvPPOG9TxDRVVVVVcfPHF/O1vf+PWW2/F4XBw6aWXUlBQQCQS4W9/+xt/7fBEuuyyyzZ6lcHzzz+f+++/n9raWg499FAefPBBDj74YERRZN68eZx33nnU1tbicrm48cYbN+rabLqTSqVYtWoVhmHg9XqpqqpCFEXMoicw238D+grM9l9D0RO4pWFsHcw98uimwapUO8uTLdQmWjr+byapZ1mebGV5spWPAEwNjNWUyW2Mco1jpL+UmuLfst3E83HMngMzZlhj5t/v+eIHTllC0w1WR34e0eqapnHa2N/RvCIXxV9QGuTxJffgDQywIMBGwjR1SN6Pmfg/wABpDELB3QiOrdbRLxeJFY1GLcN5XdctAUqWZUvs8nq9FBQU4PP5rCiudDpt+YPrnVJivV4vwWCQQCBgGannBa58lcI8DofDErj6G+m0PuTXnUgkLHHOp2kc6BrLV7HlrFYjrMi20WamSQomu/i2tkS6fBqnrutWlJvL5bJSOvN+XV6v19q+KdBNgwcXv49uGkwurGFhhwjuk92kdWUdvYeOn4csbmNjs8UhCSKXjjuMq797ptc2l21zeI9h+pIg8vttj+ixSuGG6rulzWljY2MzVEyaNIkPP/yQ/fffn88++4wDDzyQd955h4KCgkGNd8ABB/D8889z6qmn8sUXXzBt2jQcDgfBYNDyBMlzzDHHWD+/9NJLVoriv/71r27paXfffTeffPIJixYt4pRTTmHmzJk9pqU88MADliDm9/sxDMO6CCsoKOCll14atJg3lNx8883U19fz3HPP8ec//5kbb7yRUChENBq1LuR+9atf8Ze//GWjry0QCPDqq69yyCGHsHLlSg477DDL7DgWy92gcblcPPnkk2y//cavpqcoinUz/qabbuozevCnTmehy+fzMWzYMCtyRJCqoOjJDsGr3hK8BKlrlU1JEBnhK2GEr4S9yrYFchf0rdk4dR3C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CygOLsHjakc0G3JeW4NBmdM1dbEnjNWDH9/GphNftC5mSbwJzdSJqCkAS7DKiVkulidaeHzpR/wYayRrqDRmIqimhohIgdNrtVuRbOWHWO9FxzqjGhrvNMxlcawRxdBoyETIGioI4JVd+Bxuil1+BODy8Udy95Qz+P02R3Dc8J0Z6yunORtDaU+QXNJEaulqa9zUstUklzSRXNJEpj3Ov5dtmO+xtthlY7OFoRsGf33uwx7vI+W33fH8h+gdd2JN07Qemq732jfPwzO+xVh/Kz8bG5uNwNVXX01paSnnn38+jY2Nm3o5NlswsViMQw45hOeee47Kyko+/vhj9t133402/5577smee+7Zbfs+++QMvAGOP/74HlN285FE6XSaxYt7v9C97rrruqXZAZxxxhlUV+dMdp955plBrb8nzjnnHMrKyrptnzRpEjU1uRSR7777rsu+hx9+GIBTTjmlSyRWZ6qrq9lvv/0AeOeddwa8rsceewxd1znkkEMsL7S1CQQCHHPMMX3OUVhYyLnnntvjvvfee4/29naAHv3gAC644AKruM3TTz/d63pvueUWQqGQ9ejtvKwLTdN44YUXOPHEE/n1r39tbb/zzjv57W9/y9lnn80rr7yCYRjouk40GqW5uZmmpiaam5tpb28nkUhY0V6ynHODcblciIIGgKq7yaoBEukKljfvTWP79kST1aiaJzdZ+HyMyMWYqRcw9XUIJT8zNE2zzqsoitb7Pv8d1tfUv/N1DvO4xfyY3cxVPUZ5XXXiGI7eaiK/P3Y0M14sINtyLWbiXkx1PgOysjZahradjU0ndNNgYXQVT9f9jyu+fpJb5r9MSsuS1VVMTCRE/LIbzdBJqBniaoaolub5FZ+R0RU0I5d67RAkil1+xI6IUocoo5kGYSXZ47yqobEouooXV3zBX+a9yJmf3899i98jrSuohoaJidgxt0OQuoz5TXstD/z4HrcteIV7fnib91fPI2OotLz5DQsuepRFlz9pzbPin++x4KJHWXDRo7S8+Q0rU+0b5Dzanl02NlsY3yxZ1S0qqzMmsDqc4IV3ZrFtVVfvkYWrwn32paPvN0tWMWXrwX2ZtLGx2XgsXLiQm266iUsvvZTx48fzm9/8hj322IOysrI+TZ/33nvvjbhKm82dxsZG9tlnH+bOncvWW2/NO++8083IfEOz884797hd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jL767V4jKNA0iDQ62Z97gL+yzp70F+emUJ53S53s6tevH0opnnrqqUKtnkpy8uRJJk+ejKZp9OvXr1xBVlR2drb+PDw8vNhyBdcV3MbPZDLx2GOPsWfPHhwOB3v37uWpp57CbDYXu8/nn3+emJgY/XHmbEClVsoPgenECZRSeksuc2nHSZAvdyGEEKJaio2NBeDEiRP6rMol+eOPP9i2bRsATzzxhD4jVkF33HFHpU2u8/DDD5c4zurSpUu5++67MZvNeDwe1qxZwxtvvMGYMWO48MILadKkCU8//XSFkhpFycnJYcGCBUBg6yiAnj170qBBAzIyMgJmWwyG7t2788EHH3D48GEcDgdpaWmkp6fzwQcfULt2bbKyshgxYgRr164tdh/+JJ1/UHq/gi3UKqNVWps2bejfv3+R6xYvXkxGRgaXXnop1157bZFlTCYTN910E0C5Z40M2rU08M4779ChQwc6dOjAHXfcAfj+L/iX+SesEuK8s2dPqYrVIxdLmBmjxYTL4cae69DXKaXIc3pweryYDBoWUwmphNzT67xomDUvNoMHDXArA55TaQiz5sWjPKQ7i+4WKc59oTzvljrZNW7cOOrXr8/hw4fp1asXu3efvW/trl276NWrF4cOHaJevXqMGzeuIrGekx577DG9G2dmZiYHDx4s347q1i1dscsuo2XLlrRq1YpWrVpR//LLg7p/IYQQQlStXr16YbPZ2LRpE127duX9999n7969xZZfv3494Es0dO3atcgyBoNBHzQ+2Lp06VLi+oiICKZPn86hQ4d49913GTNmDK1atdITZPv372fKlClccskl7CnlDVppzJ8/n9zcXJo0acI//vGPgHUGg6HSZjecMmUK48aNo169evp4VzExMYwbN47Vq1cTGxuLy+Vi0qRJRW6/e/dufVb0M5NdcLor47x588jLywtq7CW9l/7xcLdv306dOnWKfTzzzDOA730tj6BdSwPjx49nw4YNbNiwQe+ZMXPmTH1ZeWadF+Kc0Lx5qYodIQJnvguP043ZasISfrp7vKZphFtMWIwG3F6F011C18OI0+sMKFzKgKYpEs351LPkYtR8613KgFEzEmeJKN9xiWovlOfdUie7rFYrs2bNwmg08scff9CuXTtuv/12vv76a1JSUnA6nTidTlJSUvjqq6+47bbbaN++PX/88Qcmk4n3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ExERof8fGjRoEEuXLi1222Aca1XGW5wXXngh4LMghBBCBFNN/56pjO/uc8W5ci9YVWryZ6Gsaux5Q4lzwr59+1STJk0UoAAVHh6urFar/vell16q0tLSQh2mUkqpe+65R48LUDabTUVFRQUsS0xMVKtXrw51qAF++OEHlZSUpMdosVhUbGxsQNybNm0KdZhKKaUeeeSRgLhiY2OV2WzW/x48eLByuVxVEsvKlSsDYin4mDx5crHbHThwQGmaFlA+PDxchYWFBSy77bbblNvtDnm8Sik1efJkBSiz2aySkpKKfezduzfksdrtdtW/f/+AbSIjI5XFYtH/NhgM6rXXXgtKrEopZTKZCv3fj4iICFh2/fXXq9zc3KAeayjiLcqOHTuUzWYL2F4IIYQIFvmeCf5397niXLoXrCo19bNQVjX5vCEtu84Bbreb/v37s2/fPurWrct3331Hbm4ueXl5zJs3j6ioKDZt2sTo0aNDHSoAnTp14uWXX2bNmjWkp6eTn59PVlYW2dnZzJkzh1q1apGamsqgQYPIzMwMdbgA/PLLL9xwww0cO3aMIUOGsG7dOux2O+np6eTm5vLbb7/x+OOPExMTE+pQeeedd3jxxRcBGDlyJAcPHiQ9PZ3s7Gxmz55NREQES5cuZdKkSVUWU1xcHL169eLhhx/m008/pU6dOmfdxuPxoJSiV69ezJkzh8OHD5Obm0tOTg5//vknAwcOBGDWrFlBb9lTnngL6ty5M0ePHi320aRJk5DH+txzz/HFF18AcPfdd3Po0CGys7PJz89n/fr1XH311Xi9Xh588EE2bNgQlFjdbjedOnXirbfeYs+ePeTn55OTk8PevXv55z//CcCyZcsYP358UI81VPEW5PV6ue2227Db7Vx11VWVGrcQQoiaR75nfIL53X2uONfuBatKTfwslFWNP2+EOtsmzu69997Ts7BFtYb65JNP9PXff/99CCIsm+XLl+vxfvzxx6EOR+Xm5qpmzZopQN17772hDqdELpdLb3122WWXKY/HU6jMjBkzFKBMJpPas2dPpcdUVKurxo0bn7VFTkZGhtqwYUOx671er7ruuuv0Fkn5+fnBCLfc8Sp1umVXt27dghLL2VQkVv+vf8XFmpGRoSIjIxWgHn300SBE62sdWZLx48fr//cPHDgQsK4ix1peFYn3TG+88YYC1M0336x/TuQrVgghRLDI94xPML+7zxXn271gsNTEz0JZ1fTzhrTsOgfMmTMHgB49ehSZkR05ciRNmzYF4MMPP6zS2Mrjyiuv1J8fOnQohJH4fPTRR/z999/UqVOHl156KdThlGjDhg0cO3YMgAcffBCDofB/4TvuuIPY2Fjcbjcff/xxpcdkNBrLtV1MTAyXXXZZses1TeO2224DICcnh+3bt5ernjOVN95QqEisKSkpAFx++eVFro+JieHCCy8EfK9vMPTo0aPE9f5f2QDWr18fsC4U70tF4i1o7969PP744yQkJPD6668HLT4hhBAC5HumoGB9d59Lzrd7wWCpiZ+FspDzhgxQX+3l5eXxyy+/AHD99dcXWUbTNK677joAvv322yqLrbxWrVqlP2/evHkII/HxfykMGzYMm80W4mhKtn//fv1569atiyxjNBr1JMa58HkoScH3w+PxhDCSc0+zZs0Aiu2imJmZyc6dO4HiE2LBdq69n6WN94477iA3N5fXXnuNWrVqVUVoQgghahD5nim9c+1a42zOx3vBqnK+fRbKSs4bkuyq9rZv347X6wXg4osvLracf93Ro0er5UyHDoeDffv28d///pcxY8YAcMEFF9C/f/+Qx+XP9Hfo0IEDBw5w55130rBhQywWC0lJSfTv35+vvvoqpHEWpaSTtn/d1q1bqyqcSpGcnAyAxWLRE3jVwZ9//snFF19MeHg4kZGR+uyGmzZtCnVougkTJgC+1/Cee+7h8OHDACil2LhxI/369SMnJ4errrqqysZ48L+fAG3btq2SOiuiNPHOnDmTFStW0Lt3b2655ZYqikwIIURNId8zZXOuXWuczflyLxgK59tnoSzkvOEjya5q7siRI/rz+vXrF1uu4LqC24SazWZD0zRsNhtNmzbl3nvvJT09nS5durBixQqsVmtI49u3b58+Ve/ff//NxRdfzMyZMzl+/DgREREcP36cL7/8kn79+nHHHXeglAppvAUHPi8ukeV0Otm1axfga72Tm5tbFaEF3d69e3n77bcBGDFiBNHR0SGO6LTU1FS2b99OWFgYDoeDnTt38t5779GhQweeeOKJUIcHwD333MOkSZMwGAy89dZbNGjQgKioKGw2Gx06dGD37t08+uijrFixokq6EGZkZPD8888D0LVrVy666KJKr7MiShPv4cOHefjhhwkLC+Odd96p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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "_, axes = plt.subplots(ncols=2, figsize=(12, 6), constrained_layout=True)\n", - "visualize.waterfall(results_pypesto, ax=axes[0])\n", - "visualize.parameters(results_pypesto, ax=axes[1]);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Profiling\n", - "\n", - "Profile likelihood analysis allows to systematically asess parameter uncertainty and identifiability, tracing a maximum profile in the likelihood landscape starting from the optimal parameter value.\n", - "\n", - "### Without pyPESTO\n", - "\n", - "When it comes to profiling, we have the main apparatus already prepared with a working optimizer and our objective function. We still need a wrapper around the objective function as well as the geneal setup for the profiling, which includes selecting startpoints and cutoffs. For the sake of computation time, we will limit the maximum number of steps the scipy optimizer takes to 50." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'x': array([ 4.03440768, 0.83760837, 3.55179682, 1.57898613, -4.87401594,\n", - " 4.33049155, 3.79628273, 1.90990052, 1.02667127]),\n", - " 'x0': array([ 4.03440768, 0.83760837, 3.55179682, 1.57898613, -4.87401594,\n", - " 4.33049155, 3.79628273, 1.90990052, 1.02667127]),\n", - " 'fun': 324.16551565091083,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.75539723, -3.45625301, -1.74304845, 4.19486719, -4.36152045,\n", - " 3.20102079, 4.1666204 , 4.5071745 , 1.64830203]),\n", - " 'x0': array([ 2.75539723, -3.45625301, -1.74304845, 4.19486719, -4.36152045,\n", - " 3.20102079, 4.1666204 , 4.5071745 , 1.64830203]),\n", - " 'fun': 425.9896608503201,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-4.51736894, 2.39365053, -0.85230688, 2.93845516, 3.92387757,\n", - " 3.35653866, 4.52675063, 1.98365382, 3.80369101]),\n", - " 'x0': array([-4.51736894, 2.39365053, -0.85230688, 2.93845516, 3.92387757,\n", - " 3.35653866, 4.52675063, 1.98365382, 3.80369101]),\n", - " 'fun': 426.9335166325137,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-1.88273814, -0.60977486, 2.87775688, 2.25140401, -2.65489216,\n", - " 1.01047951, 3.69127283, 4.44893301, 2.65440911]),\n", - " 'x0': array([-1.88273814, -0.60977486, 2.87775688, 2.25140401, -2.65489216,\n", - " 1.01047951, 3.69127283, 4.44893301, 2.65440911]),\n", - " 'fun': 441.8147467190984,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-1.17029265, 3.16644483, 4.85261058, 4.35300099, 1.26174191,\n", - " 0.82811007, 4.66370112, 3.96059639, 3.24314499]),\n", - " 'x0': array([-1.17029265, 3.16644483, 4.85261058, 4.35300099, 1.26174191,\n", - " 0.82811007, 4.66370112, 3.96059639, 3.24314499]),\n", - " 'fun': 481.3231591295339,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([0.66867122, 3.97870907, 2.13991535, 0.2612146 , 4.9597103 ,\n", - " 3.23568699, 4.22366861, 0.40266923, 3.15201217]),\n", - " 'x0': array([0.66867122, 3.97870907, 2.13991535, 0.2612146 , 4.9597103 ,\n", - " 3.23568699, 4.22366861, 0.40266923, 3.15201217]),\n", - " 'fun': 4453.420140298416,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-2.01253459, -2.8480651 , 3.12268386, 1.19351138, -0.60901754,\n", - " 0.29935873, 3.43245553, 4.09645236, -0.05881582]),\n", - " 'x0': array([-2.01253459, -2.8480651 , 3.12268386, 1.19351138, -0.60901754,\n", - " 0.29935873, 3.43245553, 4.09645236, -0.05881582]),\n", - " 'fun': 4468.484751390617,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-0.53682615, -4.81637178, 4.95364701, -4.57752447, -0.60577667,\n", - " -2.88604054, 0.85270085, 0.07054205, -1.27549967]),\n", - " 'x0': array([-0.53682615, -4.81637178, 4.95364701, -4.57752447, -0.60577667,\n", - " -2.88604054, 0.85270085, 0.07054205, -1.27549967]),\n", - " 'fun': 1146659.4928225973,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.69036786, -1.85327759, 2.85858288, 4.70288006, 4.02501682,\n", - " -4.23172439, -0.72188414, 3.55067703, 4.87046828]),\n", - " 'x0': array([ 2.69036786, -1.85327759, 2.85858288, 4.70288006, 4.02501682,\n", - " -4.23172439, -0.72188414, 3.55067703, 4.87046828]),\n", - " 'fun': 1236788.617249787,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-2.69338089, 1.3336723 , 4.00935726, 4.23436455, -4.97880599,\n", - " 0.66011236, -0.92734049, -0.72506365, -1.95148656]),\n", - " 'x0': array([-2.69338089, 1.3336723 , 4.00935726, 4.23436455, -4.97880599,\n", - " 0.66011236, -0.92734049, -0.72506365, -1.95148656]),\n", - " 'fun': 29198157.935426034,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.20258998, 4.3092804 , -2.2015135 , -1.86005028, 4.82608847,\n", - " 2.24886943, -1.09100022, -1.53563431, 0.22579574]),\n", - " 'x0': array([ 2.20258998, 4.3092804 , -2.2015135 , -1.86005028, 4.82608847,\n", - " 2.24886943, -1.09100022, -1.53563431, 0.22579574]),\n", - " 'fun': 37797579.29678398,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 3.59031468, -0.64508741, 4.57795683, -4.55808472, 1.45169025,\n", - " 0.16191615, -0.9214029 , 1.8818166 , -2.04635126]),\n", - " 'x0': array([ 3.59031468, -0.64508741, 4.57795683, -4.55808472, 1.45169025,\n", - " 0.16191615, -0.9214029 , 1.8818166 , -2.04635126]),\n", - " 'fun': 42320078.18806582,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-0.13272644, -1.78655792, -2.05292081, 4.94102789, 0.68000657,\n", - " -0.41145952, 4.43118647, 2.86292729, -2.27641822]),\n", - " 'x0': array([-0.13272644, -1.78655792, -2.05292081, 4.94102789, 0.68000657,\n", - " -0.41145952, 4.43118647, 2.86292729, -2.27641822]),\n", - " 'fun': 113150729.31030881,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-3.69691906, -1.11149925, -2.07266599, -1.6551983 , -1.05891694,\n", - " 0.25590375, 0.87136513, -1.83339326, -2.29220816]),\n", - " 'x0': array([-3.69691906, -1.11149925, -2.07266599, -1.6551983 , -1.05891694,\n", - " 0.25590375, 0.87136513, -1.83339326, -2.29220816]),\n", - " 'fun': 243163763.80728397,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-4.85782185, 0.88722054, 3.46323867, -0.42272524, 2.97501061,\n", - " -0.44685985, -1.9617645 , 4.19526924, -0.68191772]),\n", - " 'x0': array([-4.85782185, 0.88722054, 3.46323867, -0.42272524, 2.97501061,\n", - " -0.44685985, -1.9617645 , 4.19526924, -0.68191772]),\n", - " 'fun': 373210706.087737,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-4.72537584, -3.8267521 , 0.17032373, 0.5309411 , 1.82691101,\n", - " -0.13738387, -2.19222726, 2.40671846, -2.17865902]),\n", - " 'x0': array([-4.72537584, -3.8267521 , 0.17032373, 0.5309411 , 1.82691101,\n", - " -0.13738387, -2.19222726, 2.40671846, -2.17865902]),\n", - " 'fun': 1150653011.8393009,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-1.21935294, 4.99254589, -3.5227032 , -4.57026229, -4.27577682,\n", - " 1.65134668, -2.57941689, 3.3876373 , -3.08581727]),\n", - " 'x0': array([-1.21935294, 4.99254589, -3.5227032 , -4.57026229, -4.27577682,\n", - " 1.65134668, -2.57941689, 3.3876373 , -3.08581727]),\n", - " 'fun': 9986849980.33512,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([-2.59120601, 0.69656874, -3.88289712, -1.74846428, -1.58175173,\n", - " 3.39830011, 0.0917892 , -0.85030875, -3.77417568]),\n", - " 'x0': array([-2.59120601, 0.69656874, -3.88289712, -1.74846428, -1.58175173,\n", - " 3.39830011, 0.0917892 , -0.85030875, -3.77417568]),\n", - " 'fun': 36260050961.21613,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.10705352, -4.17386158, -4.95145244, -0.4940422 , 2.44773506,\n", - " -0.72754709, -3.38821849, 4.3015123 , -4.03270095]),\n", - " 'x0': array([ 2.10705352, -4.17386158, -4.95145244, -0.4940422 , 2.44773506,\n", - " -0.72754709, -3.38821849, 4.3015123 , -4.03270095]),\n", - " 'fun': 634294830118.3749,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 1.5071923 , 3.23408298, 4.40175342, -4.93504248, -3.68651524,\n", - " 4.89047865, -3.50203955, -3.98810331, -0.60343463]),\n", - " 'x0': array([ 1.5071923 , 3.23408298, 4.40175342, -4.93504248, -3.68651524,\n", - " 4.89047865, -3.50203955, -3.98810331, -0.60343463]),\n", - " 'fun': 2170773400755.13,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 4.59360518, -1.45672536, 2.53472283, 1.59953602, 4.74752881,\n", - " 2.97708352, -1.75879731, 1.52861569, -4.47452224]),\n", - " 'x0': array([ 4.59360518, -1.45672536, 2.53472283, 1.59953602, 4.74752881,\n", - " 2.97708352, -1.75879731, 1.52861569, -4.47452224]),\n", - " 'fun': 2817631865279.2437,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.45781935, 1.53870162, -0.24553228, 1.49870916, -3.42788561,\n", - " 4.98603203, 3.19947195, -4.22036418, 0.83316028]),\n", - " 'x0': array([ 2.45781935, 1.53870162, -0.24553228, 1.49870916, -3.42788561,\n", - " 4.98603203, 3.19947195, -4.22036418, 0.83316028]),\n", - " 'fun': 7147839056555.6,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 1.11811741, -1.66877199, 4.78163474, 3.63123695, -4.84414353,\n", - " -4.69389636, -4.22521978, 1.05436896, 1.66464083]),\n", - " 'x0': array([ 1.11811741, -1.66877199, 4.78163474, 3.63123695, -4.84414353,\n", - " -4.69389636, -4.22521978, 1.05436896, 1.66464083]),\n", - " 'fun': 12556009991670.39,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.78765335, -3.97396464, 3.98103304, 4.06162031, -4.66533684,\n", - " -3.28137522, 3.15208208, -2.66502967, -4.85197795]),\n", - " 'x0': array([ 2.78765335, -3.97396464, 3.98103304, 4.06162031, -4.66533684,\n", - " -3.28137522, 3.15208208, -2.66502967, -4.85197795]),\n", - " 'fun': 16029712077044.059,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1},\n", - " {'x': array([ 2.62294851, -3.86768587, 4.24057642, 0.67648706, 4.94028248,\n", - " -4.53014752, -4.41436998, 0.48069498, 2.08662195]),\n", - " 'x0': array([ 2.62294851, -3.86768587, 4.24057642, 0.67648706, 4.94028248,\n", - " -4.53014752, -4.41436998, 0.48069498, 2.08662195]),\n", - " 'fun': 30002126964787.39,\n", - " 'message': 'Finished Successfully.',\n", - " 'exitflag': 1}]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# sort the results\n", - "results_sorted = sorted(results, key=lambda a: a[\"fun\"])\n", - "results_sorted" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([-5. , -2.2098163 , -1.78589671, 3.55917603, 4.19771074,\n", - " 0.58569077, 0.81885971, 0.49858833])" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "results_pypesto.optimize_result[0][\"x\"][problem.x_free_indices][1:]" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "direction: -1\n", - "direction: 1\n" - ] - } - ], - "source": [ - "# we optimimize the first parameter\n", - "# x_start = results_sorted[0][\"x\"][1:]\n", - "# x_fixed = results_sorted[0][\"x\"][0]\n", - "\n", - "x_start = results_pypesto.optimize_result[0][\"x\"][problem.x_free_indices][1:]\n", - "x_fixed = results_pypesto.optimize_result[0][\"x\"][problem.x_free_indices][0]\n", - "fval_min = results_pypesto.optimize_result[0][\"fval\"]\n", - "\n", - "# determine stepsize, ratios\n", - "stepsize = 0.05\n", - "ratio_min = 0.145\n", - "x_profile = [results_pypesto.optimize_result[0][\"x\"][problem.x_free_indices]]\n", - "fval_profile = [results_pypesto.optimize_result[0][\"fval\"]]\n", - "\n", - "# set up for nlopt optimizer\n", - "opt = nlopt.opt(\n", - " nlopt.LD_LBFGS, len(parameters) - 1\n", - ") # only one of many possible options\n", - "\n", - "opt.set_lower_bounds(lb[:-1])\n", - "opt.set_upper_bounds(ub[:-1])\n", - "\n", - "\n", - "for direction, bound in zip([-1, 1], (-5, 3)): # profile in both directions\n", - " print(f\"direction: {direction}\")\n", - " x0_curr = x_fixed\n", - " x_rest = x_start\n", - " run = True\n", - " while direction * (x0_curr - bound) < 0 and run:\n", - " x0_curr += stepsize * direction\n", - "\n", - " # define objective for fixed parameter\n", - " def fix_obj(x: np.ndarray):\n", - " x = np.insert(x, 0, x0_curr)\n", - " return obj(x)\n", - "\n", - " # define nlopt objective\n", - " def nlopt_objective(x, grad):\n", - " \"\"\"We need a wrapper function of the kind f(x,grad) for nlopt.\"\"\"\n", - " r = fix_obj(x)\n", - " return r\n", - "\n", - " opt.set_min_objective(nlopt_objective)\n", - " result = opt.optimize(x_rest)\n", - "\n", - " # update profiles\n", - " if direction == 1:\n", - " x_profile.append(np.insert(result, 0, x0_curr))\n", - " fval_profile.append(opt.last_optimum_value())\n", - " if np.exp(fval_min - fval_profile[-1]) <= ratio_min:\n", - " run = False\n", - " if direction == -1:\n", - " x_profile.insert(0, np.insert(result, 0, x0_curr))\n", - " fval_profile.insert(0, opt.last_optimum_value())\n", - " if np.exp(fval_min - fval_profile[0]) <= ratio_min:\n", - " run = False\n", - " x_rest = result" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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NWvIz8/HZrORmprKddddh2EYTJ8+nWXLll30touKihg7diyFhYW0aNGC2bNnN8RvwS3Udz/n5eVVPQ8MDKzxfaovq76Op+natStz585l165dFBcXc+LECQoKCli1ahV9+/bFMAxefPFFFixYcM71w8PDWbZsGZMmTaoaXdpstqpTuPPz88nJyfHo8S7Ubz9X/p3Iyclh3rx5XH/99aSlpWGz2cjPz2f16tW0b9+e/Px8xo0bx8GDBxv7t+cy6vvnOT09nalTpxIYGMgbb7zRyOndR333c012797NH//4RwCuvvrqul+TqU5fpzbBv/71r6ozR+ry+Pzzzy/4Hs76RvzChQur3tdisZzzTK+8vLyqb7XHx8df1HbLysqM2267rerMj1WrVtUr57l40n5+8cUXq9YvKyur8X3eeuutqtdlZWXVK3N17rSvL6SoqMi47LLLqs5YPH369Fmv2bp1qxEdHW1YrVbjgQceMLZv324UFBQY+/btM/76178aYWFhBmAMHz7csNvtTsvmSft54sSJVbmjoqKMgoKCs7bxww8/GBaLxQCMP//5z07L5kn72TAMY8iQIQZgzJ8/v8acDXF2mKft53M5cuSI0aVLl6o/5xkZGXXOoCNBDSAkJKTq+eDBg895pldwcDAPPPAAUHFFzKNHj553m3a7nbvvvpuPPvoIb29v/vvf/zJ06FDnBncz9d3P1dcvLCys8X2qL6u+jvyPv78/M2fOBCqO6Kxbt+6M5Xl5eQwfPpyMjAyefvppXn311arvB3Tq1Im//OUvLF26FIvFwueff86//vUvM34bLu9C+7n6n88//elP5zzCedlll3H99dcD+rJ/TS60n99++23Wrl3LpZde6jbXEHNFF9rP53Ls2DEGDx7M3r17ad26NevWraNdu3Z1zuA2V4z+1a9+xYgRI+q8flhYmBPTnF/175f07Nmzxtf16tWr6vnBgwdrvMCW3W5n/PjxLF68GC8vL/7zn/8wZswY5wWuxpP2c/VTLzMzM2v8blVmZiYAoaGhBAcH1ytzde60ry9G//79q57v37//jGX/+c9/qgropEmTzrn+4MGD6dOnD1u3bmXp0qX87ne/c0ouT9rPtfk7sW7dOqeOwzxlP9tsNiZPnozVauWvf/3rOS/safx8tf/y8vKq7xEGBgZitdb/uIOn7OdzOXbsWNWIt1WrVnzxxRf06NGjXu/vNiXIz88PPz8/s2NclF69euHl5XXB7zZU/kUBajy9u/II0AcffFBVgO644w6n5q3Ok/Zz9TPCUlJSavzQqDyLrHqZcgZ32tf1VXlmXcuWLc/7Rf6uXbuydetWDhw44LT39qT9HB8ff1Gvq/w74czLd3jKfj516hQ2mw3ggrd3WbhwYdX1xbZt28Yll1xS7/f3lP38S8eOHWPQoEFnFCBn/EzWOKwB+Pv7V/3lON+FDCs/GCwWyzkvGW6327nrrrvOKEB33nlng2R2R/Xdz926daN9+/YANZ4pU1BQwDfffAPg8ePHC6m8Pg1w1oUnK/8FnJOTc97RY+XRIo0da3a+/TxgwACCgoKAi/s7ca4LhEqF8+1ncZ6L2c9Hjx49qwD17t3bKe+vEtRAfvvb3wKwbt26qlP4qsvPz+f1118H4IorrqBly5ZnLK88ArR48WK8vb1ZuHChCtA51Gc/WywW7rnnHgAWLVp0zmunVF7PxsvLy6NPga1+NO1cSkpKeOqppwAICgpi8ODBZyyv/L6WYRhVl3f4pZSUlKrr3FQ/RO5J6rufAwICqo4Uv/766+csnD/++CPr168HYOTIkc6I7Xbqs59jYmIwDOO8jw4dOgBw7733Vv2aM44CuZv6/nmGM0dgrVu3Zv369U4rQJUhPdbJkyeN48ePVz2io6MNwJgyZcoZv56Xl3fWupX3QwGMAwcOnLXcbrcbl19+uQEYMTExxtq1a6vOeElLSzMGDRpkAIbVajXWrVt3xrrl5eXGnXfeaQCGt7e3sXjx4gb5/TcWV93PhlFxn7bKs8d69eplbN682TAMwygpKTFef/11w9fX1wCM+++/37k7pYE01L7+8ssvjcGDBxvvvffeGWdilJaWGmvXrq06wwMw5syZc9a28/PzjTZt2hiA4efnZ7zwwgtGTk5O1bL333+/armvr6+xe/du5+4YJ3PV/WwYhnH48OGqM+0GDx5spKWlGYZR8XdlzZo1Rvv27Q3AaNWqlXHs2DHn7ZQG4Mr7+Xzc6d5hhuG6+/nYsWNG7969DcCIjIys+rPsTB5dgir/oF7oca4/yBf6cDaMitP4evXqVfW6wMDAqh9OUHGa+1tvvXXWel999dUZr2nduvV5H4sWLXLynnEuV93PlTZv3mw0b9686vUhISGGj49P1X8PHTrUKC4udtLeaFgNta/Xr19/xvoBAQFGixYtzthPVqvVePLJJ2vMtmnTJqNly5ZnbCckJKTqdO3K/3eJiYlO3ivO58r72TAM4+uvvzaaNWtWtU5YWJgREBBQ9d+tWrUyNm3a5MQ90jBcfT9fKLe7lCBX3c/PPvts1euCgoIu+Fl46NChWv/e3eaL0e4oMjKSrVu38uqrr/LBBx+we/duioqKiImJ4frrr+eRRx455+0aKi8eBxW3FLjQ6fPnOjvBk9R1P1fq27cvqampzJkzh08++YSMjAyCgoKIjY3l3nvvZcKECU45q8OdxcXFMX/+fL777ju2b99OTk4Op0+fJjAwkF69ejFw4EDuu+++8166/vLLL2fHjh288cYbfPrpp+zatYvc3FyCgoLo1KkTgwcP5sEHH/ToW2Y4Yz8DDBw4kLS0NObPn8+nn35KRkYGFouFuLg4Ro4cycMPP3zWCN6TOGs/y/nVdz9X/ywsKCigoKDgvO9XlwutWgzjAkM7ERERkSbIs/95KyIiIh5LJUhEREQ8kkqQiIiIeCSVIBEREfFIKkEiIiLikVSCRERExCOpBImIiIhHUgkSERERj6QSJCIiIh5JJUhEREQ8kkqQiIiIeCSVIBEREfFIKkEiIiLikVSCRERExCP9f4R5GGUsoHEvAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(\n", - " [x[0] for x in x_profile], np.exp(np.min(fval_profile) - fval_profile)\n", - ");" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is a very basic implementation that still lacks a few things:\n", - "* If we want to profile all parameters, we will want to **parallelize** this to save time.\n", - "* We chose a very unflexible stepsize, in general we would want to be able to automatically **adjust the stepsize** during each profile calculation.\n", - "* As this approach requires (multiple) optimizations under the hood, the things discussed in the last step also apply here mostly." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### With pyPESTO\n", - "\n", - "pyPESTO takes care of those things and integrates the profiling directly into the Result object" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Engine will use up to 8 processes (= CPU count).\n", - "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:13<00:00, 13.82s/it]\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABxgAAAJoCAYAAABV8TSTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADPAElEQVR4nOzdeZyO5eLH8c8zY4x1Btm1EEelVEiSUJFQUlpPSaXlVKrTcirafq1H6zmnTftCy1GiQrKkZI0KJbSTLC0qMxiGmXl+f1wnkyIzj+Ge5fN+vZ7Xee7lue/vdM6Jeb73dV2xeDweR5IkSZIkSZIkSZIKICnqAJIkSZIkSZIkSZJKDgtGSZIkSZIkSZIkSQVmwShJkiRJkiRJkiSpwCwYJUmSJEmSJEmSJBWYBaMkSZIkSZIkSZKkArNglCRJkiRJkiRJklRgFoySJEmSJEmSJEmSCsyCUZIkSZIkSZIkSVKBlYs6QGmXl5fH8uXLqVq1KrFYLOo4kiRJkiRJkiRJ0h/E43FWr15N/fr1SUr68zGKFow72PLly9ltt92ijiFJkiRJkiRJkiRt07fffsuuu+76p+dYMO5gVatWBcJ/GWlpaRGnkSRJkiRJkiRJkv4oMzOT3XbbbVO39WcsGHewX6dFTUtLs2CUJEmSJEmSJElSsVaQJf/+fAJVSZIkSZIkSZIkSfoNC0ZJkiRJkiRJkiRJBWbBKEmSJEmSJEmSJKnALBglSZIkSZIkSZIkFZgFoyRJkiRJkiRJkqQCs2CUJEmSJEmSJEmSVGAWjJIkSZIkSZIkSZIKzIJRkiRJkiRJkiRJUoFZMEqSJEmSJEmSJEkqMAtGSZIkSZIkSZIkSQVmwShJkiRJkiRJkiSpwCwYJUmSJEmSJEmSJBWYBaMkSZIkSZIkSZKkAisxBWNWVhZvvvkmt99+O7169WKPPfYgFosRi8W4+eabi+Qe33//PVdddRV77bUXFStWpEaNGrRv354nn3ySeDxeJPeQJEmSJEmSJEmSSrJyUQcoqFmzZtG9e/cddv0PP/yQo48+mp9++gmAKlWqsHr1aqZOncrUqVN55ZVXGDlyJOXLl99hGSRJkiRJkiRJkqTirsSMYASoXr06nTp14uqrr+a///0vdevWLZLrZmRkcOyxx/LTTz+x99578/7777N69WrWrl3LQw89REpKCuPGjePyyy8vkvtJkiRJkiRJkiRJJVWJGcHYvn17fv7558329e/fv0iufe+99/Ldd99RsWJFxowZQ6NGjQAoX748/fr1IzMzk+uuu47HH3+cyy+/nKZNmxbJfSVJkiRJkiRJkqSSpsSMYExOTt5h1x4yZAgAp5122qZy8bcuvfRSqlSpQm5uLi+88MIOyyFJkiRJkiRJkiQVdyWmYNxRPvvsM5YsWQJAt27dtnhOlSpVaN++PQDjx4/fadkkSZIkSZIkSZKk4qbMF4yffPLJpvf77bffVs/79diCBQt2eCZJkiRJkiRJkiSpuCoxazDuKMuXL9/0vkGDBls979djmZmZrFmzhipVqmzxvOzsbLKzszdtZ2ZmFlFSSZIkSSpBNm6ElSvhxx/zXz/8sPn2r6/69aFHD+jZE7awbIUkSZIkqXgp8wXj6tWrN72vVKnSVs/77bHVq1dvtWAcOHAgt9xyS9EFlCRJkqTiYMOG/MLw90XhlorDX34p+LU//RTefhuuuAKaNw9FY8+e0KoVxGI77meSJEmSJCWkzBeMRW3AgAFceeWVm7YzMzPZbbfdIkwkSZIkSVuQnf3HUnBLReGv+zIyCn+PpCSoWRNq1cp/1a69+fYuu8C8efD66zB5cng/bx7cfjs0aADHHQfHHw+HHw7lyxf1PwVJkiRJUgLKfMFYtWrVTe+zsrJIS0vb4nlZWVlb/MzvpaamkpqaWnQBJUmSJKkw1qyB6dPh++//vDxMZDmH5OTNC8Pfl4W/365RI5SM23LkkfD3v8PPP8OYMfDaazB2LCxbBo88El5padCtWxjZ2K0bVKtW+PySJEmSpCJR5gvG+vXrb3q/bNmyrRaMy5YtAyAtLW2r06NKkiRJUmQ++ggefRSefz6UjAVRrlwoDH9fDG6tPKxWrWCFYaJq1IDevcNr/fowberrr8PIkfDdd/DSS+FVrlwY0dizZxjhuPvuOy6TJEmSJOkPynzBuN9++216/8knn7DPPvts8bxPPvkEgGbNmu2UXJIkSZK0TVlZ8PLL8Nhj8N57+fsbNoQmTbY90rBateK7xmGFCtC9e3g98gi8/34oG19/HRYsgLfeCq9LL4UWLfLXbTzggOL7M0mSJElSKVHmC8amTZuy++67s2TJEsaOHcvJJ5/8h3PWrl3LlClTAOjSpcvOjihJkiRJm1u4MJSKgwfDqlVhX0oK9OoFF14IHTuWrpItKQnatAmvf/4Tvvwyv2ycNg3mzAmvm2+GPfYIoxp79oQOHcI/F0mSJElSkdqBc9uUDLFYjD59+gAwdOhQFi9e/IdzHn74YdasWUNycjJnnHHGTk4oSZIkSUB2Nvz3v6E8bNYM7r8/lIsNG8LAgfDttzB0aJg6tDSVi1vSpAlcdRVMnhymTn3mmVAoVqwI33wDDz4InTuHkZq9e8OwYbB6ddSpJUmSJKnUKFEF4y+//MLKlSs3vfLy8gDIysrabP+a3603cvPNNxOLxYjFYlssEP/xj39Qt25dsrKyOOaYY/jwww8B2LBhA4888gg33ngjABdccAFNmzbdsT+kJEmSJP3WV1/BtdfCrrvC6aeHUi0pCY4/HsaODcf794c6daJOGo1ateDss+G112DlyjCqsW/fsH/VKnjhBTjllLDWZLduYZ3K5csjDi1JkiRJJVssHo/How5RUA0bNuSbb77Z5nlnnXUWzz777Kbtm2++mVtuuQWARYsW0bBhwz985sMPP+Too4/mp59+AqBq1aqsX7+ejRs3AmFq1JEjR5KamlqozJmZmaSnp5ORkUFaWlqhPitJkiSpjNq4EUaNCmXYhAn5+xs0gPPPh3PPDYWjti43N6xL+etUqp9/vvnx1q3z123cd9/SP+pTkiRJkrahMJ1WiRrBuCO1atWK+fPnc8UVV/CXv/yFjRs3UrlyZQ477DCeeOIJ3nzzzUKXi5IkSZJUKEuWwE03hXUETzwxlIuxWBh59/rrsHgx/N//WS4WRHIytGsHd98Nn30W1q28805o2zb8M33/fbjhBmjePEy5euWV8O67kJMTdXJJkiRJKvZK1AjGksgRjJIkSZL+VG5umOr00UdhzBj431IQ1K4dRiqefz40ahRtxtLmu+9g9Ogwrepbb4X1LX+1yy5wzDFhZOPRR0PlypHFlCRJkqSdqTCdlgXjDmbBKEmSJGmLVqyAp5+Gxx8PIxd/deSRcOGFoeAqXz66fGXFmjUwfnwYITp6NPz8c/6x1FQ46qjw30WPHmV3nUtJkiRJZYIFYzFiwShJkiRpk7w8ePvtMFrx9dfzp+OsUQPOPhsuuAD22ivSiGVaTg5Mm5a/buPXX+cfi8XgkEPy123ce+/ockqSJEnSDmDBWIxYMEqSJEnixx/h2WfDaMUvv8zf365dGK140klQoUJk8bQF8TjMn59fNr7//ubHmzbNLxsPOSSs+ShJkiRJJZgFYzFiwShJkiSVUfE4TJ0aRiu+8gps2BD2p6XBmWfC3/4GzZtHm1EFt2wZjBoV1m18+23YuDH/WO3acOyxoWw86iioWDGymJIkSZKUKAvGYsSCUZIkSSpjVq2CIUNCsbhwYf7+gw4KoxVPOw0qV44snopAZiaMHRtGNr7xBmRk5B+rWBGOPjqUjcceCzVrRpdTkiRJkgrBgrEYsWCUJEmSyoB4HGbNgsceg6FDYd26sL9SJTjjjDBasVWraDNqx9i4ESZPzp9KdcmS/GNJSWEa3F+nUm3SJLqckiRJkrQNFozFiAWjJEmSVIqtXg0vvhhGK86dm7+/efMwWvGMMyA9PbJ42snicfjoo/yycc6czY83a5ZfNrZuHQpISZIkSSomLBiLEQtGSZIkqRSaOzeMVnz+eVizJuxLTYVTTw2jFdu2hVgs0ogqBr75BkaODGXju+9CTk7+sXr1oEePUDYeeSRUqBBdTkmSJEnCgrFYsWCUJEmSSomsLHj55TBacebM/P1Nm4bRimedBTVqRJdPxduqVTBmTCgb33wzjH79VZUq0LVrKBu7d/d/R5IkSZIiYcFYjFgwSpIkSSXcwoVhtOLgwaEkAkhJgV69QrHYsaOjFVU42dkwaVIoG0eOhGXL8o8lJ0OHDvlTqTZsGFVKSZIkSWWMBWMxYsEoSZIklUDZ2TBiRBitOHly/v6GDcMUqOecA3XqRBZPpUg8Dh9+mL9u47x5mx/ff//8srFlS8tsSZIkSTuMBWMxYsEoSZIklSBffgmPPw7PPAMrV4Z9SUlw3HFhtOJRR4VtaUf5+uv8snHKFMjLyz+2667hf4s9e8Lhh0P58pHFlCRJklT6WDAWIxaMkiRJUjG3cSOMGhVGK06YkL+/QQM4/3w499xQ7Eg7208/wRtvhLJx3DhYuzb/WFoadOsGxx8f/jM9PbKYkiRJkkoHC8ZixIJRkiRJKqaWLIEnnoCnnoIVK8K+WAy6dg2jFbt3h3Llos0o/Wr9epg4MX/dxu+/zz+WkhJGNPbsGUY47rZbZDElSZIklVwWjMWIBaMkSZJUjOTmwtixYbTimDH500/Wrh1GKp5/PjRqFG1GaVvy8mDWrPypVBcu3Px4y5b56zbuv7/rNkqSJEkqEAvGYsSCUZIkSSoGVqwIIxWfeCKMXPzVkUeG0Yo9e7qenUquzz/PLxunT4ff/prfsGH+uo3t24fRjpIkSZK0BRaMxYgFoyRJkhSRvDx4++0wWvH11yEnJ+yvUQPOPhsuuAD22ivSiFKR++EHGD06/G9+wgRYty7/WPXqYerfnj3DVMBVq0aXU5IkSVKxY8FYjFgwSpIkSTvZjz/Cs8/CY4/BV1/l72/XLoxWPOkkqFAhsnjSTpOVFUrG11+HUaNg5cr8Y+XLhxG8xx8PPXpA/fqRxZQkSZJUPFgwFiMWjJIkSdJOEI/DlClhtOLw4bBhQ9iflgZnngl/+xs0bx5tRilKubkwY0YoG197Db78cvPjBx+cv25js2au2yhJkiSVQRaMxYgFoyRJkrQD/fILPPdcKBYXLszff9BBYbTiaadB5crR5ZOKo3g8/P/l13UbZ87c/HjjxvllY7t2kJwcTU5JkiRJO5UFYzFiwShJkiQVsXgcZs0KpeJLL+WvMVepEpxxRhit2KpVtBmlkmTFijCF6uuvw8SJkJ2df2yXXeDYY0PZ2KWLhb0kSZJUilkwFiMWjJIkSVIRWb0aXnwxFItz5+bvb948jFY84wxIT48snlQqrFkD48aFsnH06DBK+FcVKkDnzmHdxmOPhTp1IospSZIkqehZMBYjFoySJEnSdpo7N5SKL7wQyg+A1FQ49dQwWrFtW9eLk3aEnByYOjV/3cbFi/OPxWLh/3u/TqW6115RpZQkSZJURCwYixELRkmSJCkBWVnw8suhWPzt+nBNm4bRimedBTVqRJdPKmvicZg3L3/dxg8/3Pz4Xnvll41t2rhuoyRJklQCWTAWIxaMkiRJUiEsWACPPQZDhsCqVWFfSgr06hWKxY4dHa0oFQdLl8LIkaFsfOcd2Lgx/1jt2tCjRygbO3eGihWjyylJkiSpwCwYixELRkmSJGkbsrNhxIgwWnHy5Pz9DRuGKVDPOce13qTiLCMDxo4NZeOYMWH7V5UqQZcuYd3GY46BmjUjiylJkiTpz1kwFiMWjJIkSdJWfPklPP44PPMMrFwZ9iUnh5FPF14IRx0FSUnRZpRUOBs2wLvvhrJx5Ej49tv8Y0lJcNhh+VOpNm4cXU5JkiRJf2DBWIxYMEqSJEm/sXFjKB0eewwmTMjf36ABnH8+nHsu7LprdPkkFZ14HObMyV+38aOPNj++7775ZeNBB/lAgSRJkhQxC8ZixIJRkiRJApYsgSeegCefhO++C/tiMejaNYxW7N4dypWLNqOkHWvx4vx1G999F3Jz84/VqwfHHRfKxiOPhNTUyGJKkiRJZZUFYzFiwShJkqQyKzcX3nwzjFYcMwby8sL+2rXhvPPCiMWGDSONKCkiv/wS/r3w+uvh3xNr1uQfq1IlPHzQs2dYt7F69ehySpIkSWWIBWMxYsEoSZKkMmfFCnjqqTBiccmS/P1HHhlGK/bsCeXLR5dPUvGSnQ1vv52/buOKFfnHkpOhQwc4/vjw74499ogspiRJklTaWTAWIxaMkiRJKhPy8mDiRHj00VAS/Dr1YY0acM45cMEF0LRptBklFX95efDBB/nrNs6fv/nxAw7IX7exRYsw1bIkSZKkImHBWIxYMEqSJKlU+/FHePbZMA3qV1/l72/XLoxWPOkkqFAhsniSSrivvsovG6dOzZ9qGWC33fLXbezY0ZHRkiRJ0nayYCxGLBglSZJU6sTjMGVKGK04fDhs2BD2p6XBmWfC3/4GzZtHm1FS6bNyJbzxRigbx42DrKz8Y+np0K1bKBu7dQvbkiRJkgrFgrEYsWCUJElSqfHLL/Dcc6FYXLgwf/9BB4XRiqedBpUrR5dPUtmxbh289VYoG0eNgh9+yD+WkgKHHx7KxuOOCyMdJUmSJG2TBWMxYsEoSZKkEi0eh1mzQqk4dCisXx/2V6oEZ5wRRiu2ahVtRkllW24uzJyZP5XqZ59tfrxVq/x1G5s3d91GSZIkaSssGIsRC0ZJkiSVSKtXwwsvhLUV587N39+8eRiteMYZTkEoqXj67LP8snHGjPCgxK8aNswvG9u3h3LlIospSZIkFTcWjMWIBaMkSZJKlLlzw2jFF16ANWvCvtRUOPXUMFqxbVtH/0gqOb7/HkaPDmXjhAn5o7ABqleHY44JZePRR0PVqtHllCRJkooBC8ZixIJRkiRJxV5WFrz0UhitOHNm/v6mTcNoxbPOgho1ossnSUVh7VoYPz6UjaNHw08/5R8rXx46dcpft7FevehySpIkSRGxYCxGLBglSZJUbC1YEErFwYMhIyPsS0mBXr1Csdixo6MVJZVOOTkwfXr+VKpffbX58TZt8qdS3Wcf/10oSZKkMsGCsRixYJQkSVKxkp0Nw4eHYnHy5Pz9DRuGKVDPOQfq1IksniTtdPF4eODi17Jx1qzNjzdpkl82HnooJCdHk1OSJEnawSwYixELRkmSJBULX34Jjz8OzzwDK1eGfcnJ0KNHGK141FGQlBRtRkkqDpYvh1GjQtk4cSJs2JB/rGZNOPZY+OtfoUuX6DJKkiRJO4AFYzFiwShJkqTIbNwII0fCo4/CW2/l72/QAM4/H849F3bdNbp8klTcrV4NY8eGsvGNN2DVqvxjp58ODz8M1apFlU6SJEkqUhaMxYgFoyRJkna6b76BJ58Mr+++C/tiMejaNYxW7N4dypWLNqMklTQbN8KUKTBsGDzxBOTmwm67hXVsjzgi6nSSJEnSdrNgLEYsGCVJkrRT5ObCm2+G0YpjxoQ1xQBq14bzzgsjFhs2jDSiJJUa770HvXvDV1+FBziuugpuvx1SU6NOJkmSJCWsMJ2Wi6xIkiRJJdny5XDbbdCoUVhP8Y03Qrl45JHw8svw7bdwxx2Wi5JUlA45BObODQ9vxONw771w8MEwb17UySRJkqSdwoJRkiRJKok+/hhOPBF23x1uuikUiTVqhFE0n30GEyfCySdD+fJRJ5Wk0qlKFXj88bA+Y61a4d/LBx0E//oX5OVFnU6SJEnaoSwYJUmSpJIkHodBg8JImREjwtSo7drBc8/BsmVhFE3TplGnlKSy47jjwsjFY46BDRvCgx5HHQVLl0adTJIkSdphLBglSZKkkmLVKjjlFOjXD7Kzw5fZ8+bB1KlhLbAKFaJOKEllU506MGpUWAe3UiV4+21o3hyGDo06mSRJkrRDWDBKkiRJJcH770PLlvDKK1CuHNx3X/gye7/9ok4mSQKIxeBvf4M5c6B16/BQyF//Gh4AWbUq6nSSJElSkbJglCRJkoqzeBz+/e8wDeqiRdCwIUybBldeGb7MliQVL02bhn9P33QTJCfDCy/A/vvDpElRJ5MkSZKKjAWjJEmSVFz99FNY2+vKK2HjRjjxxDAy5uCDo04mSfozKSlwyy1hCuvGjeHbb+HII+Hqq8MU15IkSVIJZ8EoSZIkFUfTpsGBB8Lo0VC+PDz8MAwbBtWqRZ1MklRQhxwCc+fCeeeFEen33hseEvnkk6iTSZIkSdvFglGSJEkqTvLyYOBA6NgRli6Fv/wFZs6Eiy92SlRJKomqVIEnnoDXX4eaNeHjj+Ggg8L013l5UaeTJEmSEmLBKEmSJBUX338P3brBdddBbi6cfjp8+GEYyShJKtmOOy6MXDzmmDBN6pVXQpcu4WESSZIkqYSxYJQkSZKKg7ffDkXi+PFQsSI89RQ8/zxUrRp1MklSUalTB0aNgkcfhUqVYOJEaN4cXnop6mSSJElSoVgwSpIkSVHKzYWbb4bOneG776BZM3j/fejb1ylRJak0isXgb3+DOXOgdWtYtQpOOw169w7vJUmSpBLAglGSJEmKyvLloVi85RaIx+Hcc0O5uO++USeTJO1oTZvCtGlw002QlAQvvAD77w+TJkWdTJIkSdomC0ZJkiQpCmPHwgEHhC+SK1cO06E++WSYMk+SVDakpISHTKZOhcaN4dtv4cgj4ZprwjqNkiRJUjFlwShJkiTtTBs3Qv/+0K0brFwZSsbZs+GMM6JOJkmKStu2MHcunHdeGNF+zz3Qpg188knUySRJkqQtsmCUJEmSdpYlS6BjR7jrrrB98cXw3nthmjxJUtlWpQo88QS89hrUrAkffQQHHQT/+Q/k5UWdTpIkSdqMBaMkSZK0M7z+Ohx4IMyYAWlpMGwYPPwwVKgQdTJJUnHSsyfMmwfdu4dpUq+4Arp0gaVLo04mSZIkbWLBKEmSJO1I2dlw+eVw/PHwyy/QujXMmQMnnRR1MklScVW3LoweDY88AhUrwsSJsP/+8PLLUSeTJEmSAAtGSZIkacf56ito1w7uvz9sX3klTJ0Ke+4ZbS5JUvEXi8GFF4aHUg46KDykcuqpcOaZkJERdTpJkiSVcRaMkiRJ0o7w8svQogV8+CHUqAEjR8J990H58lEnkySVJHvtBdOnw403QlISPP98GM347rtRJ5MkSVIZZsEoSZIkFaV16+Cii8Iok9WrwwjGuXOhR4+ok0mSSqqUFLj11jAKvnFjWLIEjjgCrr02TMUtSZIk7WQWjJIkSVJR+fRTOOQQePTRMLXdddfBpEmw225RJ5MklQZt24aHVs47D+JxuPtuaNMG5s+POpkkSZLKGAtGSZIkqSg891xYI+vjj6FWLRg7Fu64A8qVizqZJKk0qVIFnngCXn0VataEjz6CVq3Cer95eVGnkyRJUhlhwShJkiRtj7Vr4ZxzoE+f8P6II8KXvV26RJ1MklSaHX88zJsH3buHaVIvvxyOPhqWLYs6mSRJksoAC0ZJkiQpUfPmQevW8OyzkJQEt9wCEyZAvXpRJ5MklQV168Lo0TBoEFSsCG+9Bc2bw8svR51MkiRJpZwFoyRJklRY8XiYnu7gg2HhwlAoTpwIN90EyclRp5MklSWxGFx0EcyZE6bq/uUXOPVUOPNMyMiIOp0kSZJKKQtGSZIkqTAyM+H00+GCC2D9eujaNUyJevjhUSeTJJVle+0F06fDjTeGUfXPPw/77w+TJ0edTJIkSaWQBaMkSZJUULNnQ6tWMHRoGKl4113wxhtQq1bUySRJgpQUuPVWmDIF9twTliwJD8Bce21Yp1GSJEkqIhaMkiRJ0rbE4/DQQ9C2LXz5Jey+exgRcs01YZSIJEnFyaGHwty5cO654c+wu++GNm1g/vyok0mSJKmU8NsQSZIk6c/88guceCJceils2AA9e4Z1rg49NOpkkiRtXdWq8OST8OqrULNmmM67VSu4/37Iy4s6nSRJkko4C0ZJkiRpa957D1q0CF/OpqSEL2VffRVq1Ig6mSRJBXP88TBvHnTrFqZJvfzysH7wsmVRJ5MkSVIJZsEoSZIk/V5eHtx7L7RvD998E9axmj4dLrsMYrGo00mSVDh164Y1gwcNgooVYcIEaN4chg2LOpkkSZJKKAtGSZIk6bdWroQePeDqqyEnB045BWbPhoMOijqZJEmJi8XgoovCNN8HHRSmAD/lFOjTBzIyok4nSZKkEsaCUZIkSfrV5Mlw4IEwZgykpsKjj8LQoZCeHnUySZKKxl57hVH5N9wASUnw3HNwwAHhz0BJkiSpgCwYJUmSpNxcuP12OOKIsCbVXnvBrFnwt785JaokqfRJSYHbboMpU8I04N98A4cfDv37w4YNUaeTJElSCWDBKEmSpLLtu+/g6KPhxhvD2ot9+sAHH8D++0edTJKkHevQQ2HuXOjbF+JxuOsuaNMG5s+POpkkSZKKOQtGSZIklV1vvRWmRJ04ESpVgmefhcGDoUqVqJNJkrRzVK0KTz0Fr74Ku+wSCsdWreCBB8KDN5IkSdIWWDBKkiSp7MnJCSMWu3SB77+H/fYLoxbPOivqZJIkReP442HePOjWDbKz4e9/h65dw9ThkiRJ0u9YMEqSJKlsWboUjjwyrLkYj8MFF4T1FvfZJ+pkkiRFq149eOMNePhhqFgRJkyA5s1h2LCok0mSJKmYsWCUJElS2fHGG2FK1ClTwpRw//0vPPZY+BJVkiRBLAYXXwyzZ4epUn/5BU45JYzyz8iIOp0kSZKKCQtGSZIklX4bN8LVV8Oxx8JPP0HLluGL09NOizqZJEnF0957w4wZcMMNkJQEQ4bAAQeEh3QkSZJU5lkwSpIkqXRbvBjat4d77w3bl14K06dDkyaRxpIkqdhLSYHbbgulYqNG8M030LEjDBgAGzZEnU6SJEkRsmCUJElS6TViBLRoATNnQrVqYfuBByA1NepkkiSVHIceCh99BH37hvWL77wTDjkEFiyIOpkkSZIiYsEoSZKk0mf9+jBS8cQTYdUqaNMG5syBE06IOpkkSSVT1arw1FPhYZ1ddgl/rrZqFR7cycuLOp0kSZJ2shJXMK5evZqbb76Z5s2bU6VKFdLT02ndujX33XcfG7Zzeo5XXnmFHj16UL9+fcqXL0/lypXZa6+9OP/885k7d27R/ACSJEnasb78Moy0eOihsH311WFqt4YNI40lSVKpcMIJMG8edO0aHuj5+9/D++XLo04mSZKknSgWj8fjUYcoqG+++YbDDz+cxYsXA1CpUiVyc3PJzs4GoEWLFkycOJHq1asX6rrZ2dmcfPLJjBo1atO+KlWqsGHDhk2lZVJSEvfeey9XXHFFoa6dmZlJeno6GRkZpKWlFeqzkiRJKqShQ+GCC2D16jC6YsgQ6N496lSSJJU+8Tg88gj84x+wbh3UqAGPPQYnnRR1MkmSJCWoMJ1WiRnBmJOTQ48ePVi8eDH16tVjwoQJrF27lqysLIYOHUrVqlWZM2cOvXv3LvS1//nPf24qFy+++GKWLl3K6tWrWbduHR988AGHHXYYeXl5XHXVVXz44YdF/aNJkiRpe2VlhWLxr38N5WL79jB3ruWiJEk7SiwGF18Ms2eHqVJ//hlOPhnOOgsyM6NOJ0mSpB2sxBSMgwcPZt68eQAMHz6czp07A2Fk4amnnspjjz0GwJgxY5g4cWKhrj1kyBAAOnbsyMMPP0yDBg02XbtVq1aMHj2aKlWqEI/HeeWVV4rqR5IkSVJRWLAgrLH4xBPhy84bb4S334Zdd406mSRJpd/ee8P06XD99ZCUFGYPOOCAMD25JEmSSq0SVTACHHHEEbRt2/YPx0877TQaNWoE5BeGBbVixQoADjrooC0eT09Pp2nTpgCsWbOmUNeWJEnSDvTss9C6NXzyCdSpAxMmwK23QrlyUSeTJKnsKF8ebr8dJk+GRo1g8WLo2BEGDID/LT0jSZKk0qVEFIxZWVlMmzYNgG7dum3xnFgsRteuXQEYP358oa6/5557Amx1+tOMjAw+//xzYOslpCRJknaiNWugTx8455wwPWrnzvDRR9CpU9TJJEkqu9q1C1OUn3NOWKPxzjvhkEPCbAOSJEkqVUpEwbhw4ULy8vIA2G+//bZ63q/HvvvuO37++ecCX/+iiy4CYNKkSfTr149ly5YBEI/HmT17Nsceeyxr1qyhbdu2Ca3xKEmSpCL00Udw0EHw3HNhKrbbb4dx48IIRkmSFK20NHj6aRg+HHbZBebMCWs0Pvgg/O+7HUmSJJV8JaJgXL58+ab3v66PuCW/Pfbbz2xLv379uOaaa0hKSmLQoEHsuuuuVK1alQoVKtCqVSu+/PJL+vfvz8SJE0lOTv7Ta2VnZ5OZmbnZS5IkSUUgHodHHw3rLX72GTRoAJMm5a/5JEmSio9evWDePOjaFdavh8sug27doBDf10iSJKn4KhHfxKxevXrT+0qVKm31vN8e++1ntiUpKYmBAwfy9NNPU6VKFSCstbjhf+sErF+/noyMDNauXbvNaw0cOJD09PRNr912263AOSRJkrQVGRlw2mlw0UWQnQ3HHBOmYGvfPupkkiRpa+rVgzFj4KGHoEIFGD8emjeHV16JOpkkSZK2U4koGHe0lStX0qlTJ84++2zatm3L1KlTWbVqFStWrGDEiBHUqlWLRx55hDZt2myaPnVrBgwYQEZGxqbXt99+u5N+CkmSpFLqgw+gZUt4+WUoVw7uvRdGjoSaNaNOJkmStiUWg379wlSpLVvCzz/DySfD2WeDsz5JkiSVWCWiYKxateqm91lZWVs977fHfvuZbTnrrLOYNGkSHTt2ZNy4cbRr14709HTq1q3LCSecwNSpU6lZsyZff/01/fv3/9NrpaamkpaWttlLkiRJCYjH4f774dBD4euvoWFDmDoVrrrKKVElSSpp9t4bZsyA664Lf44PHgwHHABTpkSdTJIkSQkoEd/M1K9ff9P7PxtB+Ntjv/3Mn1m4cCFjxowB4KqrriIWi/3hnNq1a9OnTx8ARowYQTweL9C1JUmSlKCff4bjj4fLL4eNG8M6TnPmhPUXJUlSyVS+PNxxB0yeDI0aweLF0LEjDBgA/1umRpIkSSVDiSgY99lnH5L+95T6J598stXzfj1Wt25datSoUaBrL1iwYNP7xo0bb/W8v/zlL0AYJfnDDz8U6NqSJElKwPTpcOCBYRrU8uXDuk2vvALVqkWdTJIkFYV27cJaymefHWYsuPNOOOQQWLgw6mSSJEkqoBJRMFaqVIl27doBMHbs2C2eE4/HGTduHABdunQp8LWTfjO91jfffLPV877//vtN76tUqVLg60uSJKmA8vLgrrugQwf49lto0gTeey+s27SFWSYkSVIJlpYGzzwDw4fDLrvkr9H44IOhdJQkSVKxViIKRgjrJAK88847zJw58w/Hhw0bxtdffw2waTrTgmjZsuWm94888sgWz1m7di1DhgwBYP/996dy5coFvr4kSZIK4IcfoHt36N8fcnPhr3+F2bOhRYuok0mSpB2pVy+YNw+OPhrWr4fLLoNu3WD58qiTSZIk6U+UqIKxefPmxONxTjzxRCZOnAhAXl4ew4YN4/zzzwegW7dudOrUabPP3nzzzcRiMWKxGIsXL97s2B577EGPHj0AGDVqFGeeeSZfffUV8XicjRs3Mn36dA4//PBN5eVVV121g39SSZKkMmbSpDAl6rhxUKECPPkkvPACVK0adTJJkrQz1KsHb74ZRi9WqBD+TtC8eRjdKEmSpGKpxBSM5cqVY+TIkTRs2JBly5bRuXNnKleuTOXKlTnllFPIzMykRYsWvPDCC4W+9tNPP02rVq0AeP7552nSpAlVqlTZNDXrBx98AMDVV19dqNGRkiRJ+hO5uXDLLdCpE6xYAfvsA++/D+ee65SokiSVNbEYXHJJmMGgZUv4+Wc46aSwTmNmZtTpJEmS9DslpmAEaNiwIR9//DE33XQT++23H7FYjJSUFFq1asW9997Le++9R/Xq1Qt93Zo1a/Lee+/x5JNPcvTRR1OnTh02btxIuXLl2HPPPenduzdTpkzh7rvv3gE/lSRJUhm0YgUcdRTcfHNYe/Gcc0K5uN9+USeTJElR2mcfmDEDBgyApCQYPBgOOACmTo06mSRJkn4jFo+7cvaOlJmZSXp6OhkZGaSlpUUdR5IkKXrjx0Pv3vDjj1C5Mjz6aNiWJEn6ralT4cwzYfHiUDZee214OKl8+aiTSZIklUqF6bRK1AhGSZIklWA5OXDddXD00aFcPOAA+PBDy0VJkrRlhx0GH30UpknNy4OBA6FtW1i4MOpkkiRJZZ4FoyRJkna8b7+Fww8PXwwCXHQRvPce7LVXpLEkSVIxl5YGzzwDr7wCNWrkr9H40EPgpFySJEmRsWCUJEnSjjVqFBx4IEybFr4kfPllGDQIKlSIOpkkSSopTjwR5s0LMyGsXw+XXgrdusHy5VEnkyRJKpMsGCVJkrRjbNgAV14Jxx0HP/8MBx0Ec+bAySdHnUySJJVE9evDm2/Cgw+GB5XGjYPmzWHEiKiTSZIklTkWjJIkSSp6X38d1k3697/D9hVXhBGMe+4ZbS5JklSyxWJwySVhqtQWLcJDTCeeCH37QlZW1OkkSZLKDAtGSZIkFa1XXglf+L3/PlSvDq+/Dv/6F5QvH3UySZJUWuyzT1jPecCAUDo+8wx07AgrVkSdTJIkqUywYJQkSVLRWL8eLr44TIGamQmHHgpz54YpUiVJkopa+fLwz3/CO+/ALrvABx/AwQeHKdklSZK0Q1kwSpIkaft9/jkccgg88kjY7t8fJk2C3XePNJYkSSoDOnaEmTPDqMalS8M07a+9FnUqSZKkUs2CUZIkSdvnhRegZUv46COoVQvGjoWBAyElJepkkiSprGjcGGbMgC5dwlqMvXrB3XdDPB51MkmSpFKpXFFdaPXq1cyePZsffvgBgNq1a9OyZUuqVq1aVLeQJElScbJ2LVx2GTz9dNg+4gh4/nmoXz/aXJIkqWxKT4c33oDLL4eHH4Zrr4VPP4VHH3UtaEmSpCK23QXjvHnzuP7663nzzTfJy8vb7FhSUhLHHHMMt912G82bN9/eW0mSJKm4+OqrsLbiggUQi8H//R/ccAMkJ0edTJIklWXlysFDD8Hee8Pf/w7PPBP+3jJ8ONSsGXU6SZKkUiMWjyc+V8SIESPo3bs32dnZbO0ysViM1NRUXnjhBU444YSEg5ZUmZmZpKenk5GRQVpaWtRxJEmStt8HH0D37vDjj1CvXpgi9Ygjok4lSZK0ubFj4dRTITMzTKE6enQoHiVJkrRFhem0El6DcdGiRZxxxhmsX7+ePfbYg0GDBvHFF1+wbt061q1bxxdffMGgQYNo2LAh69ev54wzzmDRokWJ3k6SJEnFwZtvwuGHh3LxwAPhww8tFyVJUvHUtWtYl7FRozCK8ZBD4K23ok4lSZJUKiRcMN5zzz1kZ2fTtm1bPv74Yy688EIaN25MamoqqampNG7cmAsvvJCPP/6Ytm3bkp2dzX333VeU2SVJkrQzPfss9OgR1l486ih4990wglGSJKm4atYMZs6Edu0gIyOUjo8+GnUqSZKkEi/hgvGtt94iFovx6KOPUqVKla2eV7lyZR599FHi8Tjjx49P9HaSJEmKSjwOd9wB55wDubnQu3eYYszp3yVJUklQqxZMnAh9+oS/y1x0UVifMScn6mSSJEklVsIF49KlS6latSrNmzff5rnNmzcnLS2NpUuXJno7SZIkRSE3F/r1gxtuCNv9+8OQIVC+fLS5JEmSCiM1NczG8M9/hu0HHoDjjgvrM0qSJKnQEi4YU1JS2LhxY4HOjcfjbNiwgZSUlERvJ0mSpJ1t3To46SR45BGIxeDBB2HgwPBekiSppInFYMAAeOUVqFgxrC196KGwaFHUySRJkkqchAvGJk2asH79esaNG7fNc8eNG8f69etp0qRJoreTJEnSzvTTT9C5M7z2Wnjif9gwuOSSqFNJkiRtvxNPhClToH59mD8f2rSBadOiTiVJklSiJFww9uzZk3g8zvnnn8/ChQu3et6CBQu44IILiMViHH/88YneTpIkSTvL4sXQrh1Mnw7VqsGECeGLOEmSpNKiVSuYNQtatoQff4Qjj4Tnn486lSRJUokRi8fj8UQ+mJmZyb777suyZcsoX748J598Mp06daJBgwZAWKNx4sSJvPLKK2zYsIFdd92V+fPnU7Vq1SL9AYq7zMxM0tPTycjIIC0tLeo4kiRJf27uXOjWDb77DnbdFcaOhX33jTqVJEnSjrF2LfTpAyNGhO3rr4dbb4WkhJ/JlyRJKrEK02klXDACzJ8/nx49erB48WJiW1mLJx6P06hRI0aOHMm+ZfDLKQtGSZJUYkycCCecAKtXQ/PmMGZMKBklSZJKs7w8uOGGsNY0hDWoBw+GSpWizSVJkrSTFabT2q7Hsfbdd18+/vhjBg4cyIEHHkhSUhLxeJx4PE5SUhIHHnggd911Fx999FGZLBclSZJKjBdeCCMXV6+Gww8P6xJZLkqSpLIgKQn++c9QKqakwCuvQMeOsHx51MkkSZKKre0awfh7Gzdu5OeffwagRo0apKSkFNWlSyxHMEqSpGItHod774Vrrgnbp54avlxLTY02lyRJUhSmTg0zOqxcCQ0awKhR0KJF1KkkSZJ2ip02gvH3UlJSqFOnDnXq1LFclCRJKu7y8uCKK/LLxSuugBdftFyUJEll12GHwcyZsM8+sGxZ2H7ttahTSZIkFTuuWC1JklQWrV8Pp50G998ftu+7D/71rzBFmCRJUlm2554wYwZ06QJZWdCrF9x1V5j5QZIkSQCUK8hJS5YsAcIIxXr16m22r7B23333hD4nSZKkIvLLL3D88TB5clhnaMiQUDZKkiQpSE+HN94IMzw89BD07w+ffgqPPQbly0edTpIkKXIFWoMxOTkZgL333pv58+dvtq9QN4vFyMnJKfTnSjLXYJQkScXKt99Ct24wfz6kpcGrr8KRR0adSpIkqfh6+GH4+98hNxc6dIDhw6FmzahTSZIkFbkiX4MxHo9vem1pX0FfeXl52/eTSZIkKXGffAJt24ZysX59mDLFclGSJGlb+vULoxnT0sIMEG3awMKFUaeSJEmKVIGmSF20aBEQpkj9/T5JkiSVAO++Cz17QkYG7LMPjB0LTl0vSZJUMEcfHdZlPPZY+Prr8NDWsGFw1FFRJ5MkSYpEgaZIVeKcIlWSJEVu2DDo3Rs2bIB27WDkSKhRI+pUkiRJJc+PP0KvXjB1KiQnw4MPwkUXRZ1KkiSpSBT5FKmSJEkqoR54AE49NZSLvXrBhAmWi5IkSYmqVQveegv69AlrMl58MVx2GeTkRJ1MkiRpp0q4YNxzzz055JBDCnx++/btady4caK3kyRJUmHk5cE118Df/w7xeFg76OWXoWLFqJNJkiSVbKmp8OyzMHBg2H7wQejRI0xFL0mSVEYkXDAuXryYJUuWFPj8pUuXsnjx4kRvJ0mSpILasAHOPBPuuSdsDxwYvvhKTo42lyRJUmkRi0H//jB8eHiAa+xYOPRQWLQo6mSSJEk7xU6bIjUnJ4ekJGdklSRJ2qEyM6F7d3jxRShXDgYPDl9+xWJRJ5MkSSp9evWCKVOgfn1YsAAOPjiszyhJklTK7ZTGb926dfzwww9UrVp1Z9xOkiSpbFq+HDp0gIkToUoVeOONsD6QJEmSdpxWrWDWLGjZElauhE6d4Lnnok4lSZK0Q5Ur6IlLliz5wxSnGzZsYMqUKcTj8S1+Jh6Ps2rVKl544QU2btxI8+bNtyusJEmStuLTT6FrV/jmG6hTB8aMCV9ySZIkacdr0AAmTw4Pd40YEf7z00/httvAGb0kSVIpVOCC8ZlnnuHWW2/dbN8vv/zC4Ycfvs3PxuNxYrEYf/vb3wodUJIkSdswfTr06AE//wx/+UtYA2jPPaNOJUmSVLZUrgzDhsGNN8I//xlen30GQ4ZApUpRp5MkSSpShXqEKh6Pb3rFYrHNtrf0AkhLS6Ndu3YMGTKE008/fYf8EJIkSWXWa6+Fabh+/hnatAllo+WiJElSNJKS4I47QqlYvjwMHx6msF++POpkkiRJRSoW39r8ptuQlJRE3bp1We5fkP5UZmYm6enpZGRkkJaWFnUcSZJUmjzyCFxyCeTlwbHHwksv+XS8JElScTF1KpxwQliXsUEDGDnSKewlSVKxVphOK+FJ4Pv06cMpp5yS6MclSZKUqHgcrr8eLr44lIvnnw+vvmq5KEmSVJwcdhjMnAnNmsGyZdC+ffg7myRJUimQ8AhGFYwjGCVJUpHauDEUioMHh+1bbgnr/MRi0eaSJEnSlmVkwKmnwrhxYfvOO+Gaa/z7myRJKnZ2yghGSZIk7WRr1kCPHqFcTE6GJ5+Em27yyylJkqTiLD0dRo+GSy8N2/37wznnQHZ2tLkkSZK2Q7miuMjSpUuZPn06S5cuZe3atfzZoMibbrqpKG4pSZJUtnz/PRxzDHz4YZgK9eWXw7YkSZKKv3Ll4IEHYO+94bLLwgNjX38NI0ZAzZpRp5MkSSq07ZoideXKlVx44YW89tprf1oqAsTjcWKxGLm5uYnerkRyilRJkrTdvvgCunYNX0LVrAlvvAEHHxx1KkmSJCVi/Hg4+WTIzIQ99wyjG/fZJ+pUkiRJO2eK1LVr13L44Yfz6quvkpKSQuvWrYnH46SkpNCuXTsaN25MPB4nHo9TvXp1OnbsSIcOHRK9nSRJUtk0axYcemgoF/fcE6ZPt1yUJEkqybp0gRkzwt/tvv4a2rYNpaMkSVIJknDB+PDDD7NgwQL22msvvv76a9577z0AatSoweTJk/n8889ZtGgRp5xyCqtWraJr16688847RRZckiSp1HvjDTjiCFi5Elq1CuXiX/4SdSpJkiRtr2bNYOZMaN8eMjKge3cYNCjqVJIkSQWWcMH46quvEovFGDhwIPXq1dviOXvssQdDhw7llFNO4brrrmPixIkJB5UkSSpTnnoKevaErCw4+miYNAnq1Ik6lSRJkopKzZowYQKcdRbk5kK/fmF9xpycqJNJkiRtU8IF46effgpA165dN9u/cePGP5x7xx13EI/HefDBBxO9nSRJUtkQj8Ott8J554Uvms46C0aNgipVok4mSZKkopaaCs88A3feGbYffBCOPTaMapQkSSrGEi4Y169fT/Xq1UlNTd20r0KFCqxZs+YP5zZq1Ij09HRmzZqV6O0kSZJKv5wc+Nvf4P/+L2xff334wiklJdpckiRJ2nFiMbj2WhgxAipVgnHj8tfgliRJKqYSLhjr1KlDdnb2Zvtq1arFhg0bWLp06Wb7c3NzWbt2LT/99FOit5MkSSrdsrKgVy944glISgpr8Nx+e/jCSZIkSaXfCSfAlClQvz4sWABt2sDUqVGnkiRJ2qKEC8bdd9+drKwsfvjhh037DjzwQCCsz/hbI0eOJCcnh9q1ayd6O0mSpNJr5Uo48sgwFWqFCjB8OFx0UdSpJEmStLO1bAmzZkGrVuHviJ06wZAhUaeSJEn6g4QLxrZt2wIwZcqUTftOPfVU4vE4AwYM4J577mHChAnce++9nHPOOcRiMbp167b9iSVJkkqTRYvCFFgzZ0L16vDWW3D88VGnkiRJUlQaNIDJk+HEE2HDhrAm93XXQV5e1MkkSZI2icXj8XgiH5w5cyZt27bluOOO47XXXgMgHo/TqVMnJk2aROw303nF43Hq1q3LBx98QP369YskeEmRmZlJeno6GRkZpKWlRR1HkiQVJ7NnQ/fu8P33sPvuMHYs7LNP1KkkSZJUHOTlwY03wj//GbZ79QqjGStXjjaXJEkqtQrTaSU8grFNmzbk5eVtKhcBYrEYb7zxBgMGDKBRo0aUK1eOXXbZhd69e/Pee++VuXJRkiRpq8aPh44dQ7l4wAEwY4bloiRJkvIlJcEdd4RSsXx5GDECOnSAZcuiTiZJkpT4CEYVjCMYJUnSHwwZAueeCzk5Ye3FESMgPT3qVJIkSSqupk0L0+ivXAn164e1u1u2jDqVJEkqZXbKCMYhQ4YwZMgQvv/++0QvIUmSVLbE43DnnWEdnZwc+Otf4c03LRclSZL059q1g1mzoFkzWL4c2rcPD6lJkiRFJOERjElJSZQrV45Vq1ZRqVKlos5VajiCUZIkAZCbC3//Ozz8cNi++upQNiYl/LyXJEmSypqMDDjttLB2N8DAgXDttRCLRZtLkiSVCjtlBGONGjVIS0uzXJQkSdqWdevglFNCuRiLwX/+A3ffbbkoSZKkwklPD9OjXnZZ2B4wAM4+G7KzI40lSZLKnoS/1dp7773JyMhgzZo1RZlHkiSpdPn5Z+jSJUxhVb48DB0aRjJKkiRJiShXDu6/Pzy8lpwc1vfu3DmszyhJkrSTJFwwnn322eTm5vLkk08WZR5JkqTSY8kSOOwwmDo1PG0+fnwYyShJkiRtr4svzl/Pe+pUOPhgWLAg6lSSJKmMSLhgPO+88zjxxBO59tprGTRoEDk5OUWZS5IkqWT7+GNo2xYWLoQGDcKXPh07Rp1KkiRJpclRR8GMGbDnnrBoUfj757hxUaeSJEllQCwej8cT+WDfvn2Jx+MMHz6ctWvXUr16dVq3bk3t2rVJTk7e8s1iMZ566qntClzSFGZBTEmSVEq8/TaccAJkZsK++4Yny3fbLepUkiRJKq1WroRevWDKlDBt6v33Q79+UaeSJEklTGE6rYQLxqSkJGKxGAX5+K/nxWIxcnNzE7ldiWXBKElSGTN0KPTpAxs3QocO8NprUL161KkkSZJU2mVnw4UXwrPPhu1+/eA//wlrNkqSJBVAYTqthP+G0adPH2KxWKIflyRJKn3+9S+46qrw/qST4LnnoEKFaDNJkiSpbEhNhaefhn32gf794eGH4Ysv4OWXwzqNkiRJRSjhEYwqGEcwSpJUBuTlwT/+Af/+d9i+7LLwPinh5a4lSZKkxL36KvTuDVlZoXAcPTqs0yhJkvQnCtNp+a2XJEnS9sjOhtNPzy8X7747TEVluShJkqSonHACTJ0KDRrAwoVw8MFhfUZJkqQi4jdfkiRJicrIgK5d4aWXICUFnn8err4anEZekiRJUWvRAmbNglat4KefoFMnGDw46lSSJKmUsGCUJElKxLJl0L49TJoEVavCmDFwxhlRp5IkSZLy1a8PkyeH9cE3boSzz4YBA8IU/5IkSdvBglGSJKmw5s+Htm1h3jyoWzd8adO5c9SpJEmSpD+qVCnMuHH99WH7zjvh5JNh7dpoc0mSpBLNglGSJKkwpkyBww6Db7+FvfaCGTPgwAOjTiVJkiRtXVIS3H47PPcclC8PI0ZAhw5hVg5JkqQEWDBKkiQV1PDhcNRRsGpVGME4bRo0bBh1KkmSJKlgeveGt9+GWrVg9mw4+GD48MOoU0mSpBLIglGSJKkgHnooTCWVnQ09e8LEibDLLlGnkiRJkgqnXTuYNQv23ReWLw/rio8YEXUqSZJUwlgwSpIk/Zl4HAYMgEsvDe8vvDCMZKxYMepkkiRJUmIaNoTp06FbN1i3Dk48EQYODH/flSRJKgALRkmSpK3ZsAHOOgvuvDNs3347DBoEycnR5pIkSZK2V1oajBwJl10Wtq+7Ds4+O8zYIUmStA0JF4xJSUmUK1eOL7/8sijzSJIkFQ+rV8Oxx8Jzz4VC8Zln4PrrIRaLOpkkSZJUNMqVg/vvz3+IbsgQ6NwZfvwx6mSSJKmYS7hgrFixIlWqVKFJkyZFmUeSJCl6330HHTvChAlQuTKMGhWe5pYkSZJKo4sugjffhPR0mDoV2rSBBQuiTiVJkoqxhAvGXXfdlY0bNxZlFkmSpOh99hm0bQtz5kCtWvDOO2FtGkmSJKk0O+ooeO89aNwYFi0KfyceNy7qVJIkqZhKuGA85phjWL9+Pe+++25R5pEkSYrOe+9Bu3aweDE0aQIzZkDr1lGnkiRJknaOvfeGmTOhQwfIzITu3eGhh6JOJUmSiqGEC8YBAwZQq1YtLrroIlasWFGUmSRJkna+kSPhyCPhp59CqThtWnh6W5IkSSpLdtklLBVwzjmQlweXXgr9+kFOTtTJJElSMRKLx+PxRD44efJkvvjiC6644gqSk5M588wzadeuHbVr1yY5OXmrn+vQoUPCYUuizMxM0tPTycjIIC0tLeo4kiRpSx5/PKw7k5cXntJ++eWw9qIkSZJUVsXjcM890L9/eN+lC7z0ElSrFnUySZK0gxSm00q4YExKSiIWixXqM7FYjJwy9rSTBaMkScVYPA433wy33hq2+/aFxx6DcuUijSVJkiQVG6+9BmecAVlZYQrV0aOd6UOSpFKqMJ1WwlOkAsTj8UK98vLytud2kiRJRWfjRjjvvPxy8aab4MknLRclSZKk3zr+eJg6FRo0gE8/hTZtYMqUqFNJkqSIJVww5uXlJfSSJEmK3Nq14YuSp5+GpKQwavGWW6CQszNIkiRJZUKLFjBrFhx0UFizvFMnePbZqFNJkqQIbdcIRkmSpBLnhx/giCNgzBioWBFefRUuuCDqVJIkSVLxVr8+vPsunHRSmA3knHNgwICwjrkkSSpzLBglSVLZ8dVX0K4dvP8+7LILTJwIxx0XdSpJkiSpZKhUCV56CW64IWzfeWcoHNeujTaXJEna6YpskaHVq1cze/ZsfvjhBwBq165Ny5YtqVq1alHdQpIkKXEffADdu8OPP0LDhjB2LOy1V9SpJEmSpJIlKQluuy38Xfrcc8OMIO3bw6hRYZ1GSZJUJmx3wThv3jyuv/563nzzzT+ssZiUlMQxxxzDbbfdRvPmzbf3VpIkSYl58004+eTwZHWLFmF61Lp1o04lSZIklVy9e8Oee4a1zefMgdatYeTIsE6jJEkq9bZritQRI0bQpk0b3njjDXJzc4nH45u9cnNzGTVqFG3atOHVV18tqsySJEkF9+yz0KNHKBePOiqsG2O5KEmSJG2/Qw+FWbNg331hxQro0AGGD486lSRJ2gkSLhgXLVrEGWecwfr169ljjz0YNGgQX3zxBevWrWPdunV88cUXDBo0iIYNG7J+/XrOOOMMFi1aVJTZJUmSti4ehzvugHPOgdzc8IT16NHg9O2SJElS0WnYEKZPh27dYN26sCbjP/8Z/j4uSZJKrYQLxnvuuYfs7Gzatm3Lxx9/zIUXXkjjxo1JTU0lNTWVxo0bc+GFF/Lxxx/Ttm1bsrOzue+++7Y78OrVq7n55ptp3rw5VapUIT09ndatW3PfffexYcOG7b7+d999x4033kirVq2oUaMGFStWZI899qBr167ceeedbNy4cbvvIUmSdrDcXLj4YrjhhrDdvz8MGQLly0ebS5IkSSqN0tLC9Kh//3vYvv56OOssyM6ONpckSdphYvF4Yo8TNW3alK+++oq5c+duc33FefPmccABB9CkSRM+//zzhIICfPPNNxx++OEsXrwYgEqVKpGbm0v2//6y0qJFCyZOnEj16tUTuv5LL73EBRdcQGZmJgAVKlSgfPnym7YBfvnlF6pVq1bga2ZmZpKenk5GRgZpaWkJ5ZIkSYWQlQWnnw6vvw6xGDzwAFxySdSpJEmSpLLh0UfD379zc6FdO3j1VahVK+pUkiSpAArTaSU8gnHp0qVUrVp1m+UiQPPmzUlLS2Pp0qWJ3o6cnBx69OjB4sWLqVevHhMmTGDt2rVkZWUxdOhQqlatypw5c+jdu3dC1x82bBinn346mZmZXHDBBcyfP59169aRkZFBZmYmkydP5oorriAlJSXhn0GSJO1gP/0EnTuHcjE1FYYNs1yUJEmSdqYLL4SxYyE9HaZNgzZtYP78qFNJkqQilvAIxvT0dHJycli7du02z43H41SuXJmUlBQyMjISuR1PPfUU5513HgDTp0+nbdu2mx3/73//y+mnnw7AW2+9RadOnQp87RUrVrDvvvvyyy+/cN9993HllVcmlHFLHMEoSdJOsngxdO0Kn30G1arBqFFw2GFRp5IkSZLKpk8/hWOPha++Cuugv/xy+Pu6JEkqtnbKCMYmTZqwfv16xo0bt81zx40bx/r162nSpEmit2Pw4MEAHHHEEX8oFwFOO+00GjVqBMCQIUMKde0HHniAX375hRYtWnDFFVcknFGSJEVk7lxo2zaUi7vtFp6UtlyUJEmSorP33jBzJnTsCKtXwzHHwEMPRZ1KkiQVkYQLxp49exKPxzn//PNZuHDhVs9bsGABF1xwAbFYjOOPPz6he2VlZTFt2jQAunXrtsVzYrEYXf/3FNT48eMLdf1fC8nevXsTi8USyihJkiIycSJ06ADffQfNm8OMGdCsWdSpJEmSJO2yC4wfD337Ql4eXHop9OsHOTlRJ5MkSdsp4YLx8ssvp0GDBixdupQWLVpw5pln8uyzzzJhwgQmTJjAM888Q+/evWnZsiVLly6lQYMGXH755Qnda+HCheTl5QGw3377bfW8X4999913/PzzzwW69qJFi1i+fDkArVq1Yt68eZx++unUq1eP1NRUdt11V0499dRNBackSSpGXngBunULT0QffjhMmQINGkSdSpIkSdKvypeHJ5+Eu++GWAwGDQqjGVetijqZJEnaDuUS/WBaWhpjx46lR48eLF68mBdffJEXX3zxD+fF43EaNWrEyJEjqVq1akL3+rUABGjwJ18a/vbY8uXLqVGjxjav/fnnn296P23aNG655RY2bNhAxYoVqVChAsuWLePll19m2LBh3HLLLdx4441/er3s7Gyys7M3bWdmZm4zgyRJKqR4HO65B669NmyfeioMHgypqdHmkiRJkvRHsRhcfTU0bQqnnx5GNbZtC6NHQ+PGUaeTJEkJSHgEI8C+++7Lxx9/zMCBAznwwANJSkoiHo8Tj8dJSkriwAMP5K677uKjjz5i3333Tfg+q1ev3vS+UqVKWz3vt8d++5k/88svv2x6f+ONN1K/fn0mTJjAmjVryMjIYP78+Rx++OHE43FuuukmRowY8afXGzhwIOnp6Zteu+22W4FySJKkAsrNhcsvzy8Xr7gCXnzRclGSJEkq7nr2hKlTYddd4dNPoU2bMAuJJEkqcbarYASoUqUK1157LR9++CFZWVmsWLGCFStWkJWVxYcffsjVV19NlSpViiLrDvHr1KsQRlsOHz6czp07k5QU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PB7n/vvv55133inQ/QYPHsy8efMAGD58OJ07dw4/QFISp5566qapTMeMGcPEiRMT/bEAuP766/nqq6+45ppr2HfffbfrWpKkYmDlSjjuuPDLzMaNcMIJMHcutG0bdTJJkiRJkkqnNm1gzpywRAmEh34POgg++ijSWJKkopFwwfj222/Tr18/evXqxVtvvcURRxyx2fqH5cuX54gjjuCtt97ihBNO4MILL2Tq1Kk0adKEp59+mrPOOot4PM4TTzxRoPsNHjwYgCOOOIK2W/hC+LTTTqNRo0ZA4deH/K333nuPBx54gKZNm3LDDTckfB1JUjExeTIceCCMHg2pqfDwwzB8uOs/SJIkSZK0o1WuDI8+CqNGQe3aMH8+tG4Nd98dpk+VJJVYCReM9913H/F4nHvuuWebazHefffd5Obmcvfdd2/a179/fwCmT5++zXtlZWUxbdo0ALp167bFc2KxGF27dgVg/PjxBfoZfi87O5u+ffsSj8d5/PHHqVChQkLXkSQVA7m5cOutcMQRYTqWpk3hvffg4oshwTWEJUmSJElSAo49FubNC7MLbdwI114LRx4J33wTdTJJUoISLhg/+OADqlWrRoMGDbZ57q677kq1atWYOXPmpn177bUXlSpV4ocfftjm5xcuXEheXh4A++2331bP+/XYd999x88//7zN6/7erbfeysKFCzn33HPp2LFjoT8vSSomli8P6zv83/9BXh6cdRZ8+GEYyShJkiRJkna+2rXhtdfgiSfCyMbJk2H//eG55yAejzqdJKmQEi4YV69ezdq1a9m4ceM2z92wYQNr165l9erVm+1PSUmhXLly2/z88uXLN73/s0Lzt8d++5mCmDNnDnfffTd16tThnnvuKdRnJUnFyJtvwgEHwKRJ4ReWIUPg2WehSpWok0mSJEmSVLbFYnDeeTB3LhxyCGRmQp8+cOqpkMCAEUlSdBIuGBs2bMjGjRt58cUXt3nuf//7XzZu3EjDhg037VuzZg0ZGRnUrl17m5//bTFZqVKlrZ7322O/LzP/TE5ODn379iUnJ4cHHniAatWqFfizv5ednU1mZuZmL0nSTrBhA/zjH9C9O6xcGUrGDz+EM8+MOpkkSZIkSfqtJk1gypSwtElyMgwbBs2bw1tvRZ1MklRACReMJ598MvF4nH79+vHf//53q+cNHTqUfv36EYvFOOWU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TjuN888/f9cErl2mvPVnlV2lqS8DdOrUidtvv52GDRuSkJAAQGJiIieccALjxo3jsMMOA6Bnz56kp6dv03bL70J2djZ16tRh2LBhNG7cGAi/4D7++OMZMmQIkUiENWvW0KdPn134zlRY5akv52bx4sWceuqprFq1ijp16jBo0KDo33qVHqWtLxf2nNfrv7KrvPVlr//KtvLWn1WyeDYnqUBeffVVIBxyntukurlZt24dp556KhMmTKBy5cp8+umnHHjggcURphRVpUqV6PMOHTpwyCGH5NimcuXK9OjRA4CpU6eyZMmSbdZ36dKFMWPG0LZtW0aMGEHr1q1JTU2ldu3anHXWWYwbN46aNWvyxx9/cNdddxXvG1K5Vdi+PGPGDB5++GEqV65M3759iz9gaQcK25+3bp+RkZHrcbZet3UbaVepWLEi//nPf4DwRrtRo0Zts37rfnnttddu947sww47jGOPPRaAzz//vBijlXK3s768PUuXLqVDhw7Mnj2bWrVqMWrUKOrWrVvcoaqcK4pzXq//VBIUti97/aeSpLD90eu/kqdCrAOQyqOLLrqI0047rcDtU1NTizCa/FuyZAkff/wxAJdddlm+5hrYklwcO3YslSpV4pNPPuHoo48urlBVzEpTX966NvuWEQHb06RJk+jzefPmUatWLQBmzpzJp59+CsCtt95KJJJzvtA99tiDSy+9lD59+jB06FAGDhy43e1U8pSnvtyjRw82bdrEgw8+SPXq1XPMR7B582YgHE22ZV1SUlJ0BINKvvLUn+vUqRNdvmDBAqpWrbrd9ltGFVStWtWKCaVIaerLedGqVavo8z/++GObdfn5XRg1apQlUkuZ8tSX/2np0qXRkr977LEHX375JQcccEBxh6hiUpr6cmHPeb3+K9vKU1/2+q/sK0/92eu/kscEoxQDSUlJJCUlxTqMAhs4cCCbN28mEolwxRVX5LndluTiV199RUpKCp988gnHHHNMMUaq4laa+nKTJk2Ij48nKytrh9sFQRB9vvXF4YwZM6LP991331zb77///kB4t9TSpUujX4KrZCtPfXnOnDkA3H333dx99925tp8/f370Tr+nnnqKm266qRBRa1cqT/25WbNm0ec///xzromZn3/+OXo8lR6lqS8XVosWLfK03ZbfBb/ALl3KU1/e2tKlS2nfvv02yUX/Dpdu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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "result_pypesto = profile.parameter_profile(\n", - " problem=problem,\n", - " result=results_pypesto,\n", - " optimizer=optimize.ScipyOptimizer(),\n", - " engine=pypesto.engine.MultiProcessEngine(),\n", - " profile_index=[0],\n", - ")\n", - "\n", - "visualize.profiles(result_pypesto);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Sampling\n", - "\n", - "pyPESTO also supports Bayesian sampling methods. These are used to retrieve posterior distributions and measure uncertainty globally.\n", - "\n", - "### Without pyPESTO\n", - "\n", - "While there are many available sampling methods, setting them up for a more complex objective function can be time intensive, and comparing different ones even more so." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "State([[-1.24027462 1.78932702 -3.93716867 -1.83911125 -1.99677746 4.14486841\n", - " 1.47913514 2.0021859 1.35932731]\n", - " [-1.47022978 1.7611698 4.98482712 0.85855303 -4.1989386 -4.3008923\n", - " 1.8615283 1.95714177 1.35676185]\n", - " [-0.41107448 -0.14952555 -1.77644606 4.16117419 -4.81759753 4.76263479\n", - " 1.60195281 1.74819962 1.4403907 ]\n", - " [-1.43602926 2.50778511 -2.30100058 -2.81499186 -4.86002175 3.36625301\n", - " 1.68878632 1.88056349 1.14940453]\n", - " [-1.22391434 1.81872122 -4.926788 -1.83840725 -1.59900807 4.96243857\n", - " 1.52924675 2.06366179 1.22987441]\n", - " [ 3.17131352 2.57330524 -1.1846534 -1.70751361 -2.87054037 0.42192922\n", - " 1.73901292 1.86776607 1.410648 ]\n", - " [-1.48461283 2.07931539 -3.32203888 -2.5926305 -2.01782331 4.26739815\n", - " 2.0839329 1.8225061 1.03565239]\n", - " [-1.93277149 -0.53931518 -1.76408703 4.33144966 4.79263617 -0.9410984\n", - " 1.80687188 1.61994219 1.35782666]\n", - " [ 0.10231878 3.61432235 -3.84512502 -4.9434848 -1.90631217 4.65431699\n", - " 2.07729333 1.65028872 1.19654252]\n", - " [ 4.50592151 -2.55538837 -1.16047637 4.24362302 4.53497182 1.87264848\n", - " 1.88624933 1.70845149 1.22235004]\n", - " [ 4.72615409 3.13268638 -1.56100893 -4.8662477 2.02282208 -3.87082935\n", - " 1.71348793 1.81644395 1.27623322]\n", - " [ 2.78834613 0.85239735 -0.21509618 2.03024593 -3.91778162 4.8823026\n", - " 1.7798872 1.89429546 1.29492976]\n", - " [-0.32634656 3.31840234 -1.24790645 -4.29790084 -4.71308262 3.9882119\n", - " 1.67219851 1.8025746 1.33922103]\n", - " [-0.08441014 1.99504729 -4.3086613 -2.44371181 -1.08546383 4.95857931\n", - " 1.58357273 2.03714516 1.29240578]\n", - " [-0.10478905 2.40772042 -4.44534855 -3.06426882 -0.89430395 4.15788078\n", - " 1.71021755 2.11709698 1.23181781]\n", - " [ 0.61026717 3.16617924 -3.2045833 -3.67833471 -2.67609702 4.98107667\n", - " 1.64134768 2.04945557 1.06515929]\n", - " [ 4.80721281 -0.14817726 -3.47387807 0.65699343 2.30248275 2.93320564\n", - " 1.94145041 1.85902189 1.20024436]\n", - " [-0.30164889 0.26109268 -1.84307512 3.18671824 -3.29807383 4.68070785\n", - " 1.74777087 1.80071269 1.29463877]], log_prob=[-225.64207758 -252.53559047 -229.04464792 -225.0066885 -226.23100939\n", - " -253.38487017 -229.64580756 -252.46891095 -229.74162106 -250.5537262\n", - " -252.83686794 -251.71454896 -226.72542441 -228.79079296 -237.22532707\n", - " -227.92871341 -251.80959409 -232.78825374], blobs=None, random_state=('MT19937', array([2206932849, 687533236, 392309260, 3170464034, 53645069,\n", - " 3010884295, 1924462243, 1739011224, 1215225621, 290578729,\n", - " 3346691071, 1848570829, 23027121, 456591643, 3025351839,\n", - " 44139322, 3859461820, 3384285855, 1545011441, 2880274270,\n", - " 1612523433, 348209045, 2395282107, 139706992, 2541325984,\n", - " 361020130, 1683022293, 3472867620, 989676495, 1333052438,\n", - " 261248819, 846013908, 363225567, 1078525269, 3382521778,\n", - " 1987817078, 1431689355, 919377321, 640858636, 1080089014,\n", - " 3234408472, 2099893506, 3873028967, 1835169171, 806641627,\n", - " 3825290061, 2135782189, 2804364627, 1288904372, 532697971,\n", - " 1285750807, 3181725207, 1937910098, 3735350617, 877929555,\n", - " 794118818, 531193134, 2968996371, 2235534554, 1078546710,\n", - " 1699481864, 16632259, 2038009533, 4124018018, 1654549904,\n", - " 1839175806, 281104275, 3001893995, 3549514596, 572512883,\n", - " 775895305, 2476554611, 1078562900, 477044261, 3332147477,\n", - " 1790764712, 1220166955, 1835496428, 2754893033, 1269592747,\n", - " 1030059335, 2361857228, 3976443209, 3069245420, 2891322212,\n", - " 777908704, 1732733343, 3104821860, 846811797, 2485970223,\n", - " 717890732, 3822556252, 4038352219, 1021866056, 782933989,\n", - " 3607286638, 2876106162, 1844124260, 1289090079, 771261560,\n", - " 1552270256, 1354994831, 3061800544, 2727263367, 3030113580,\n", - " 2186079388, 539503901, 877058179, 3425099351, 2714112648,\n", - " 584347502, 448943255, 481046113, 2494146037, 1959281397,\n", - " 2997223436, 580854431, 901139350, 4073689258, 2403752855,\n", - " 1273639913, 17097930, 1189258404, 1129946182, 3861197036,\n", - " 1187616964, 3950619282, 2894123197, 3052892285, 1794601679,\n", - " 3107229605, 1154736540, 1445112066, 1281647315, 3823808737,\n", - " 2464923304, 3066806796, 911645021, 3321406851, 2506397230,\n", - " 3224207588, 34403862, 4121992940, 125096971, 3733411609,\n", - " 2433840407, 1211748718, 692955217, 3920121066, 3170374543,\n", - " 963071047, 2240583049, 2557131029, 2215007747, 1682863338,\n", - " 1829007553, 188935160, 4233449025, 1142368962, 4126532027,\n", - " 1540531607, 3427751919, 1553010111, 2479983119, 3408252102,\n", - " 2263816213, 331359825, 3633921403, 3759892034, 292106085,\n", - " 1864810289, 1140673266, 2800793353, 2838103537, 396634619,\n", - " 2380262092, 558090601, 3954852938, 2356468210, 854842063,\n", - " 3987873003, 1413040425, 1717097406, 2845933124, 200449670,\n", - " 697004378, 2330358332, 913572043, 727824675, 2521505152,\n", - " 3756628260, 1304545993, 237809106, 2921467337, 3517022909,\n", - " 2809328755, 1400146847, 2513699124, 366244197, 2865045532,\n", - " 185705230, 2728436123, 1264754284, 377298617, 2139695975,\n", - " 2167647175, 223358529, 3465282111, 1175303169, 3186216422,\n", - " 3649327174, 41779725, 1271572271, 1509599366, 3834341205,\n", - " 776192713, 2664384316, 2403609316, 3263681045, 3055346811,\n", - " 119641578, 1236369036, 1658776216, 2518401352, 4226029546,\n", - " 3148558757, 2569699277, 2866355296, 2156478906, 1404501902,\n", - " 2259574338, 2099399259, 1361291934, 3002098967, 1676689722,\n", - " 802343793, 2988447027, 4257587183, 1160559483, 4259810484,\n", - " 26038768, 3634335801, 3081765329, 2625613137, 3151957490,\n", - " 925383249, 525896746, 2564842755, 2264351719, 1664592786,\n", - " 4270323838, 3033360425, 754685161, 2610981497, 4055010380,\n", - " 939595199, 551357476, 3155657354, 1972748719, 197478011,\n", - " 2898800626, 1689855652, 953799410, 585253348, 375694973,\n", - " 1377335697, 2538595639, 2825497566, 1340999129, 831526576,\n", - " 3017026296, 1486493792, 3366584623, 57393291, 2269395590,\n", - " 851853425, 1288518763, 249497874, 326769358, 1621412413,\n", - " 478423386, 4228785772, 3199093009, 2834245505, 3430966499,\n", - " 3276897556, 17435474, 3402869961, 2647167094, 1896074115,\n", - " 3830180145, 1079813803, 1492462393, 1934793483, 2199874291,\n", - " 3105650711, 2135627634, 2313133474, 1975487203, 1890372153,\n", - " 4112771771, 1009532521, 4071594554, 3150015758, 4198705016,\n", - " 3926942927, 1307590463, 2199556149, 1191234777, 3507715113,\n", - " 2175050552, 3877421719, 1129190928, 2107289827, 3479211066,\n", - " 2448609618, 804432187, 1598435854, 3338802337, 1787761744,\n", - " 1428721688, 3471720360, 2655347578, 3314264648, 3027267759,\n", - " 2007712732, 3733317522, 4012993888, 3517787824, 551121758,\n", - " 2049597321, 3456036022, 3415694232, 3759659216, 2509150560,\n", - " 2767078802, 171594234, 3992175113, 283686696, 4132055111,\n", - " 1994172934, 3077263724, 2389273218, 1682293509, 1448618303,\n", - " 3795182571, 3684132545, 1622325522, 3459644093, 2428584405,\n", - " 415654718, 421558721, 1903663875, 3716389580, 3419812698,\n", - " 3617346627, 1591072231, 2762520964, 116836745, 3639259734,\n", - " 1005442451, 1461831630, 867361387, 1942784541, 1142795005,\n", - " 1525588494, 1321625262, 162610824, 4008904733, 1776666739,\n", - " 873008342, 3840442180, 2973938450, 4265481404, 4283339674,\n", - " 2273252972, 71877482, 1390256942, 3544503825, 425620956,\n", - " 3851338020, 2957518941, 445243979, 1074579722, 2688962277,\n", - " 4273255105, 1546547539, 4024051829, 3945648095, 229231550,\n", - " 595803490, 3758182796, 2169358100, 3500261562, 4192015134,\n", - " 2183314072, 1545238201, 3103643224, 3841556466, 3855483966,\n", - " 1662567278, 3143839091, 808076356, 480190800, 2688847279,\n", - " 3994938844, 925302366, 2500422343, 610881158, 1984695872,\n", - " 3101566415, 3452810700, 4264390600, 1896509376, 2705432340,\n", - " 737630594, 843491200, 3532758010, 1025149261, 1657901107,\n", - " 3198420133, 3883637990, 2870068863, 2458990462, 3855620477,\n", - " 4085561001, 2402086898, 3598591303, 3550267891, 3130649350,\n", - " 811095721, 3994393403, 4237031623, 4083059107, 3051463399,\n", - " 3574114492, 3489500082, 1078191029, 1011531782, 3665502319,\n", - " 2506534754, 3377378812, 4091943684, 3385579500, 873609207,\n", - " 2952279524, 1124109539, 2561046657, 1209401355, 652418891,\n", - " 146960807, 2284822124, 70957741, 218064618, 353348997,\n", - " 193324864, 346234800, 2222422197, 907424622, 3028157175,\n", - " 3359071299, 326033693, 1308837373, 3853624073, 941872757,\n", - " 1348026446, 401040482, 1878332630, 2032502345, 3465082472,\n", - " 620100896, 3561419166, 494354990, 238926942, 3590224542,\n", - " 3575718072, 2671530629, 2301328592, 3229986077, 292475316,\n", - " 1970818708, 3723688063, 3273180879, 1219909701, 3669876766,\n", - " 3726886119, 4035180072, 3342544030, 4229704504, 2954320999,\n", - " 3660720816, 3963744058, 4088207964, 787636590, 1028989741,\n", - " 3551773942, 3067705925, 1879440107, 2690101453, 1476966661,\n", - " 1164988387, 567866675, 4223115538, 2801780003, 784163621,\n", - " 3001146061, 47857172, 3826349248, 591270366, 1038637042,\n", - " 2849851035, 2179802647, 2327748806, 803249147, 1437242643,\n", - " 2668896084, 887003105, 131613121, 1216052268, 1414385990,\n", - " 2639415044, 2951259651, 744354232, 2078830196, 2862706838,\n", - " 3251688536, 3902545329, 3578883028, 843511480, 2008248639,\n", - " 3610132004, 622281062, 3765494681, 593697613, 1024899973,\n", - " 2150321665, 3572264842, 3718275156, 3339033624, 789397804,\n", - " 455982697, 195867210, 832452258, 1590638004, 2841209280,\n", - " 1250620031, 4231398546, 2538639652, 1651308686, 4233459872,\n", - " 3251288337, 1530737085, 2508960905, 819142661, 2454195021,\n", - " 1499019860, 316344890, 1411618432, 1346866985, 2082162230,\n", - " 1861144179, 3200584504, 1713787377, 180706102, 1331333666,\n", - " 1253441295, 685235807, 1697835523, 3989857807, 2558228675,\n", - " 828902009, 1580370495, 2751730402, 2538134001, 1555804373,\n", - " 231859026, 818685043, 1092546692, 3623429586, 3779756715,\n", - " 4050788987, 796440633, 1710608815, 2296686361, 3037349092,\n", - " 1169055388, 3595308497, 268610246, 3144126922, 305091101,\n", - " 3004394692, 4235572670, 141994113, 1728717716, 1992324897,\n", - " 3387776119, 519323380, 4203830862, 2836686724, 1390785037,\n", - " 4054831231, 3030165607, 916606003, 3053193754, 4131727760,\n", - " 1575646449, 878167720, 38027722, 1743581095, 2239841900,\n", - " 3572764997, 55813195, 3787178673, 3949825982, 2088303512,\n", - " 3672572846, 2002937565, 1152259001, 2024262702, 3512380730,\n", - " 1978640799, 689801872, 1484426853, 2228701662], dtype=uint32), 379, 0, 0.0))" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import emcee\n", - "\n", - "n_samples = 500\n", - "\n", - "\n", - "# set up the sampler\n", - "# rewrite nll to llh\n", - "def log_prob(x):\n", - " \"\"\"Log-probability density function.\"\"\"\n", - " # check if parameter lies within bounds\n", - " if any(x < lb) or any(x > ub):\n", - " return -np.inf\n", - " # invert sign\n", - " return -1.0 * obj(x)\n", - "\n", - "\n", - "# def a function to get multiple startpoints for walkers\n", - "def get_epsilon_ball_initial_state(\n", - " center: np.ndarray,\n", - " lb: np.ndarray,\n", - " ub: np.ndarray,\n", - " nwalkers: int = 20,\n", - " epsilon: float = 1e-3,\n", - "):\n", - " \"\"\"Get walker initial positions as samples from an epsilon ball.\n", - "\n", - " The ball is scaled in each direction according to the magnitude of the\n", - " center in that direction.\n", - "\n", - " It is assumed that, because vectors are generated near a good point,\n", - " all generated vectors are evaluable, so evaluability is not checked.\n", - "\n", - " Points that are generated outside the problem bounds will get shifted\n", - " to lie on the edge of the problem bounds.\n", - "\n", - " Parameters\n", - " ----------\n", - " center:\n", - " The center of the epsilon ball. The dimension should match the full\n", - " dimension of the pyPESTO problem. This will be returned as the\n", - " first position.\n", - " lb, ub:\n", - " Upper and lower bounds of the objective.\n", - " nwalkers:\n", - " Number of emcee walkers.\n", - " epsilon:\n", - " The relative radius of the ball. e.g., if `epsilon=0.5`\n", - " and the center of the first dimension is at 100, then the upper\n", - " and lower bounds of the epsilon ball in the first dimension will\n", - " be 150 and 50, respectively.\n", - " \"\"\"\n", - " # Epsilon ball\n", - " lb = center * (1 - epsilon)\n", - " ub = center * (1 + epsilon)\n", - "\n", - " # Sample initial positions\n", - " dim = lb.size\n", - " lb = lb.reshape((1, -1))\n", - " ub = ub.reshape((1, -1))\n", - "\n", - " # create uniform points in [0, 1]\n", - " xs = np.random.random((nwalkers - 1, dim))\n", - "\n", - " # re-scale\n", - " xs = xs * (ub - lb) + lb\n", - "\n", - " initial_state_after_first = xs\n", - "\n", - " # Include `center` in initial positions\n", - " initial_state = np.row_stack(\n", - " (\n", - " center,\n", - " initial_state_after_first,\n", - " )\n", - " )\n", - "\n", - " return initial_state\n", - "\n", - "\n", - "sampler = emcee.EnsembleSampler(\n", - " nwalkers=18, ndim=len(ub), log_prob_fn=log_prob\n", - ")\n", - "sampler.run_mcmc(\n", - " initial_state=get_epsilon_ball_initial_state(\n", - " results_sorted[0][\"x\"], lb, ub, 18\n", - " ),\n", - " nsteps=n_samples,\n", - " skip_initial_state_check=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "trace_x = np.array([sampler.get_chain(flat=True)])\n", - "trace_neglogpost = np.array([-sampler.get_log_prob(flat=True)])" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj0AAAGwCAYAAABCV9SaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eZBlZ3ng+X/f5Sx3yaWqVFKhDQQYCVlgA2PNuEG/NtHRqBmgzdhjN9gsbXCzTDdjbJWYwTYO2023BhAmYgZM44DBxvaYMLixCbVZDAEdYHfQINPtZmkwSKKElipVbnc957zb749z8yqzKktUac3Mej7hDE7mOecuynLdp973WVRKKSGEEEIIsc/px/oFCCGEEEI8GiToEUIIIcR5QYIeIYQQQpwXJOgRQgghxHlBgh4hhBBCnBck6BFCCCHEeUGCHiGEEEKcFyToEUIIIcR5QYIeIYQQQpwXJOgRQgghxHnhQQU9f/u3f8tv/dZv8U//6T/lqquu4tChQ2RZxqFDh3j2s5/Nv/k3/4bV1dUHfIzjx49zww03cOWVV9LpdDh48CDXXXcd73//+zmbyRjf/e53ee1rX8sVV1xBWZYcPnyY66+/nj/7sz97MG9JCCGEEPucejCzt/7Vv/pXvOc975l/X5YlWZYxHA7nP7vgggv4+Mc/zo//+I+fdv+tt97K9ddfz8rKCgD9fp+qqvDeA3D99dfz8Y9/nDzPd3z+v/zLv+RnfuZnmEwmACwuLjIajYgxAvALv/ALfOADH0Apda5vTQghhBD71INa6bn22mt5xzvewX/6T/+JtbU1ptMpg8GA4XDIH/zBH3D48GFOnjzJi1/8YjY2Nrbdu7GxwQtf+EJWVla46qqr+PKXv8xwOGQ8HvPud7+bLMv41Kc+xRvf+MYdn/v222/nZ3/2Z5lMJjz72c/mW9/6FhsbG2xsbPAbv/EbAHzwgx/kHe94x4N5a0IIIYTYr9Ij4FOf+lQCEpD+6I/+aNu5X//1X09A6nQ66bbbbjvt3n/7b/9tApIxJn3rW9867fzLXvayBKQjR46ktbW1086/5jWvSUBaXFxMq6urD9t7EkIIIcTe9ogkMv9P/9P/ND/+/ve/v+3chz70IQBe8pKXcMUVV5x27xve8Ab6/T4hBP74j/9427nxeDzP2Xn961/P8vLyafe/+c1vBmAwGPDnf/7nD+VtCCGEEGIfeUSCni984Qvz4yc96Unz429961scO3YMgOc///k73tvv97nuuusA+PSnP73t3Be/+EWm0+kD3v+EJzyBpz71qTveL4QQQojz18MW9NR1zR133MG73/1uXv7ylwPw5Cc/mRe96EXza772ta/Nj6+55pozPtbmuW984xvbfn6u93/9618/h3cghBBCiP3MPtQHKMuSuq5P+/mzn/1s/r//7/+jKIr5z+6+++758SWXXHLGx9w8NxgMGI1G9Pv9bfcfOHCATqfzA+/f+nw7qet622uPMbK6usqhQ4ek8ksIIYTYI1JKDIdDLr74YrQ+83rOQw56jhw5QlVVjEYjxuMxAM997nN5+9vfzuWXX77t2q0l7d1u94yPufXccDicBz2b9z/QvVvPb32+ndx000381m/91gNeI4QQQoi94c477+TSSy894/mHHPTccccd8+MTJ07wh3/4h/ybf/NvuPbaa/n1X/91fvu3f/uhPsUj5s1vfjO/8iu/Mv9+Y2ODyy+/nDvvvJPFxcXH8JU9fHyIVC7QhEhKoBTkRlNmBmukIbcQQoi9bzAYcNlll7GwsPCA1z3koGerCy+8kBtuuIHrrruOH//xH+df/+t/zbXXXssLX/hCgG0vZjKZnDGw2Gw6eOo9m8dbzz/Q/T/ozRdFsW37bdPi4uK+CHp8iAxrT54lekajFcQELkSUVnQLK4GPEEKIfeMHpaY8Ip941157Lc95znMA+L3f+735zy+++OL58V133XXG+zfPLS4uzre2tt6/2RDxB92/9fnOR5ULhJgoM4PRCqUURivKzBBionLhsX6JQgghxKPmEftn/mYy8Xe+8535z7ZWXG2txDrV5rmrr75628/P9f4f/uEfPodXvL+EmKhDJDvDSk5mNHWIhHjOU0iEEEKIPekRC3puu+02YPsW01Oe8pR5cvMnP/nJHe8bj8fzPj/Pe97ztp17znOeM6/aOtP93/ve9/jmN7+54/3nk5gSKYE+w0qfVpBSe50QQghxPjjnoCeE8AOnoH/2s5/lP//n/wzAT/zET8x/rpTiFa94BQAf/vCHtyVBb3rPe97DaDTCGMPP//zPbzvX6/X46Z/+aQDe+973njbXC+Btb3sb0AZbL37xi8/2be07WinULIdnJ3GW1KylNF8IIcR54pyDnjvvvJNnPOMZvO997+O2227bFgDdeeed/F//1//FT/7kT5JS4uDBg/zyL//ytvuPHj3KkSNHmEwmvOAFL+DWW28FoGka3vve9/KWt7wFgNe85jU85SlPOe35f/u3f5ter8c999zDi170Iv7+7/8eaFeIfvu3f5t/9+/+HQC//uu/zoEDB8717e0bRisKo3Eh7njehUhhNOZMS0FCCCHEPqPSD1q2OcUdd9yxbWZWnucsLi4ynU7nfXoArrjiCv7sz/6MZzzjGac9xq233sr111/PysoK0K7KVFWFcw5ot6U+/vGP71hZBfCXf/mX/MzP/My8SmtpaYnRaEQIbWLuL/zCL/CBD3zgnBsMDgYDlpaW2NjY2DPVWyEmYkroWZLyVpvVW42PGK2ws2RmF9rvF6R6SwghxD5wtp/f5xz0NE3Dxz/+cT7/+c/zpS99ibvvvpuTJ09ijOHw4cP8yI/8CD/5kz/Jz/3czz1g1+Tjx4/ztre9jVtuuYU777yTsiy55ppreOUrX8mrXvWqB+yoCPDd736Xt73tbfzVX/0V99xzDwsLCzzjGc/gta997XwL7FztpaBns/9OvaX/TnFK/x0fIqPKMag9tYugEqUxLJSWfplJwCOEEGJfeMSCnv1srwQ9mys4ISayU/rvbK7gANuuSSnhYyLERG61rPIIIYTYN8728/thbU4oHh1b++9sMgqMNlQuzPvvbL9GYc3991cu0JegRwghxHlEgp495mz670xcQAG5NWe8pg6RTkySyCyEEOK8IUHPHnM2/XfirE596zVbE5639ugxSNAjhBDi/CBBzx6ztf+O2SFeiQm0VqjZcYptwrML26u8jJEePUIIIc4vEvTsMZv9dyofMfr07SsXIt1ZHs+o8fiQ7k94Tu2Kz8bEsVDaWY8lCXyEEEKcHySTdQ/aHCC6mdCcUpoPEN0cKFpmBh8Sk9qjFEwbz8bUsTquCSlR+7acXQghhDhfyErPHmRNW3J+ap+e0t7fpyfERG4UMdMMpg0+JjLd3pfZdsr6yqShk1uKbOeEZyGEEGI/kaBnj7JG0zeazhk6MseU0FqTGejlkFm97ZqUEoOpY9J4CXqEEEKcFyTo2eOMVjtWYGmliLNtrGK2HbZVTG0gtNmwUErXhRBC7HcS9OwTIab5cNFsNkg004rGB8rs9NQtFyIdq1Gz4EhK14UQQux3EvTscafO1/IxkilFv7R083bUxKQJlJk5bVxFZg0oKV0XQghxfpCgZw/zIbI2bRhMHCnNAqDaMao8McEF/Zx+YVAofFTbEp6LzOBjopytCgkhhBD7nQQ9e1jlApM6gALnI4OpQ2k40MupfWBUOVJKLJQZRimK3GC1Qik1X+0pJYlZCCHEeUKCnj0qxMTEhXacRIRJ47FWUczmbSmlaHQg0Y6cMLNOzj4mlErbytuFEEKI84EEPXtUTIkYEyFGGh+ICfItAYxWba9lrRRJgTGKhSJDzXJ4ZEtLCCHE+UaCnj1KK4XWap7Lo0jbBozGBAmwRmGVIsaEUpxxOrsQQgix38kn4B5ltKKbGbRWhAQJxWy4OgA+RjSQt5NF0VpJlZYQQojzmgQ9e8RmH56wJbIpM8NCx2KNovaeynl8iExmg0a7uW2DHaCzQ4NCIYQQ4nwi21u7nA/xtBlbhbk/CflAJ0eldujoyWHNRAf6uWGhsBjdxrS9wkqVlhBCiPOeBD27mA+RYe0JMZEZPW8uWPmIi4mFom0+eMFCh4UyZ22xZlSFNn/HQGkMC6WlX2ZSpSWEEOK8J0HPLla5QIhp2yqNUWC0oXKBygX6s2CmyAxHlrqEhe3jKKDtwOxjmI+nEEIIIc5HEvTsUiEm6hDPWG2VGU0dIp1ThoUarTDatKtE02Y+niKR6FhZ+RFCCHH+kqBnl4opkRLbytAb387WslqTmbZcfadhoVvHU2il6ObtSlHtIyvjBh8Ty91cAh8hhBDnFQl6dik966AcEzjv2Zg4Jk0gxIjRbSJzt9RolZ927+Z4CjtLeN5kjaZygXHtKTMz3xoTQgghzgfyqbdLGa0ojGZYN5wYVAxrR2Y0/cKSGc3quGJj1OB82Hbf1vEUO22NZUYTgeksX0gIIYQ4X0jQs4uVmWFcBYaVo5dn7ZYWikTiQLcAFOsTt+2ezfEUnNKhedPmeIoYEzFJ0COEEOL8Idtbu1hMoFJiqcwJMeJpA5bCaIrMEBOMGk/jI7lt49fN8RTMOjQbdfpjJpAOzUIIIc47EvTsYj5GUIqljp2NmUjbhoXGGJmmhI+RfLZotzmeYtoEXIgY3R5XzmO1RmuFUdKhWQghxPlHgp5dbDNI8RFyq06v0ortyo7V23cpy8zQLQz3bkz4zomawdRRNYEYod81PPnwAhctlo/mWxFCCCEec5LTs4vlVtPPLdPG73h+2nj6ucVotW0ulzWaTMH6sOH4YIrziV5uWCwzVFScGNScHFb4WRNDIYQQ4nwgKz273HI3Y+oDG5OGTm6xul3hmTYerRVWw/q0OW0u18rI4Uk8+cJFjFYowOi2I/PJYcW9g4rlXiFl60IIIc4bEvTscmVuedxiyfrEMWo801leTzc3WK1A63YbbMtcrrWJ4+61CYr23GaS86Z+kTGuPYOppzNbKRJCCCH2Owl69oAytxzJ7baOzI0PVD5uaz6YYqRynmMrI24/OaJXGLwP9ApLZzacFCAzkEjUwe/Y0VkIIYTYjyTo2UNyq8nRO87l8iGyPm0YTh1WG4pCY7SmiQlXeUJK85lbLoBCURgrZetCCCHOG5LQsQdtzuVKKc2nrU+a2egJrblgoeBAN8elRGY0ViumLtD4NnF5VDt6hWGxI1tbQgghzh+y0rMHxZgYThomLuBj26enaSIoWO7mxASH+x1IFRuTdnyF0YmN1LAxceSZ5shiuW1rTAghhNjvJOjZY3yIDGrHsPH4EOkXGT4ohpWjdpHMQJFZljoZi72M4xsV9w0rNqaeTp64ZLnL4w91uWChlCnrQgghzisS9OwxmxPUe4UlhESICa013czS+Ib1seOChXZMhTWa/uGMixY6DKc1i52cSw/2TqvmEkIIIc4HEvTsIVsnqHcyS8oStYs4H7FGoZQiKdCz403WKA70Cg71Cwl4hBBCnLck6NlDTp2grpTGFpqQJcpckxvN6rimagKND2RGU/tISol+0f6qKxdmOT6SwCyEEOL8IkHPHnKmCepGK4y2KKUIQIqJxkVcjFilMBomjWdQeRKJjjUslHZewi6EEEKcDyTo2UN2mqC+VUqwXGYsd3OKzOBDZNR4xpWfdXFuA5zaR1bGDT4mlru5BD5CCCHOC/Jpt8dsTlD3ITKp2wouHyLj2XGvsPQKS5kZQkxsTBwhpnliszWa3qw787j2VC481m9JCCGEeFQ8qKBnZWWFD37wg7zsZS/j6quvptfrURQFl156KS9+8Yv52Mc+9gMf4wtf+AL/7J/9My699FKKouDCCy/kH//jf8yf/MmfnNVrOH78ODfccANXXnklnU6HgwcPct111/H+97+flNKDeVt7gjWaA52cQ70cY1TblNB5Mq041MtZ7uYArAwrbjs54p71KfcNK45vTNgY1/PJ6pnRRGDqwnw6uxBCCLGfqfQgIoQsy/Dez78vyxJjDOPxeP6z5z//+Xz0ox+l2+2edv//+X/+n7ztbW+bf7+8vMx4PMY5B8D/8r/8L/zpn/4p1u68+3brrbdy/fXXs7KyAkC/36eqqvlruv766/n4xz9Onufn9L4GgwFLS0tsbGywuLh4Tvc+FkJMuC1BjNEKHyJr04bVYc3GpCEChdW42HZxXiwti50coxWVC3Qyw8F+sW2khRBCCLGXnO3n94P6pPPec+211/K7v/u7fPe732U6nTIajbj99tt59atfDcAnPvEJXvva15527/ve9755wPOSl7yEO++8k7W1NYbDIb//+79Pr9fjYx/7GG9605t2fO6NjQ1e+MIXsrKywlVXXcWXv/xlhsMh4/GYd7/73WRZxqc+9Sne+MY3Ppi3tqcYrSgzQ5mZeTXWZh+fzBr6ZY7Ruu3jk1usUUwaT+0CMUECtFYyf0sIIcR54UGt9Hzuc5/juc997hnPv+51r+N973sfAMeOHeOyyy4D2mDp0ksv5fjx4zzzmc/ky1/+Mlpvj7v+3b/7d7z+9a/HWsu3vvUtnvjEJ247/5a3vIW3vvWtdDodvv71r3PFFVdsO3/TTTfxq7/6qxhj+MY3vsFTnvKUs35fe22l51QhJlbGNcOpR6k2ABrVDqMVhW1zfJoQ6OWWMrMYBYf6Bf0ye6xfuhBCCPGgPaIrPQ8U8ADz1R6Ar3zlK/PjW2+9lePHjwNwww03nBbwAPyLf/EvWF5exnvPH/3RH512/kMf+hDQrhKdGvAAvOENb6Df7xNC4I//+I/P7g3tE40PrA0rTgwm3LsxZXVcM5461kY1g6kjxkgIidHU4XyYJzwLIYQQ54NHJJGjLMv5cQj3Vwd973vfmx9fffXVO95rjJmvznz605/edu5b3/oWx44dA9qcoZ30+32uu+66He/fz3yIbFSOjcoTYpvH0y8sZWFBKaaNZ1R7KhfJM8MF/ULK1YUQQpxXHpFPvM9//vPz46c97Wk7XrM1GDrTua997Wvbfr71+2uuueaM92+e+8Y3vvGAr7OuawaDwbavvapygdpF+mVGnml8jCilWCgzFjsZpVFY4FA34/KDPQ7JwFEhhBDnmYf9U299fZ2bbroJgOuuu44rr7xyfu4JT3jC/PjUgGZT0zT8/d//PdAmLW+tCLv77rvnx5dccskZX8PmucFgwGg0OuN1N910E0tLS/OvzdyjvSbExKj2jGpH7QKjqee+Yc3da1PuWh1z99qYb9834jv3Dblv1LAyrFjfUr4uhBBCnA8e1qAnxsjLX/5y7rnnHsqy5N3vfve288985jO56KKLAHjb2962rex90//z//w/21Zcth4Ph8P58U6l8Dud23rPqd785jezsbEx/7rzzjsf4N3tPiEmRpXj+6sjvnXvgP92bI2/+/4a96xNWB3WHFsZ8a17h9yzXmEUHF4oWSwNYxc4MahYnzQS+AghhDhvPKxBzy/90i9xyy23APCe97yHpz/96dvOW2v5jd/4DQC++c1v8sIXvpC//du/pWka7r33Xt7xjnfw5je/mSy7v5pop2Tnh0tRFCwuLm772gt8iKyPa247OeTWO1b50u2rfO17q9y9NsWHSEiJaeOpGkdMAUNiqVdwwWLJUq+kyAwhJenILIQQ4rzysEUUR48ena/svOtd7+JVr3rVjtf9b//b/8bRo0cB+NSnPsWznvUsiqLgcY97HG9605t4whOesK1Hz4EDB+bHCwsL8+PJZHLG17L13NZ79oPN5oMnhjUnBxW1D3SMRhlNSJHB1NHEQNKgjSLTGpcUG5MGrdrePla33Zh9TNKRWQghxHnjYQl63vSmN/HOd74TgJtvvvkHNgZ8xzvewRe/+EX++T//5/zwD/8wl112Gddeey1vfetb+epXv4oxbRn14x//+G1dlS+++OL58V133XXGx988t7i4SL/ff7Bva1fabD5Y+0iMbf8dZRT9wtLJDM5HpnXA+YQLidpHNAkCuNluolagAFIixkTcx2M7hBBCiE0Pecr6jTfeyM033wzA29/+dm644Yazuu/Zz342z372s3c8t9nb5x/8g3+w7edbK7a+9rWv8dSnPnXH+zeTpM9UFr9XhZiYuIAPiZQSSidijChAaejkFmUVIYDzgZTAGEWn0PQ7GWn2GNB2Y0Yp6cgshBDivPGQVnqOHj26LeC58cYbH/ILOn78OJ/5zGcAeMUrXrHt3FOe8hQuv/xyAD75yU/ueP94POYLX/gCAM973vMe8uvZTeJsZSaliFJgtUbTztwyShEV9HPLoW7OoX7BUjfnQLcgM5ZebrEaUkr4GNGA1YrcaGJKssUlhBBi33vQQc/Ro0e3bWk9HAFPCIHXve51NE3Dtddey/XXX7/tvFJqHgh9+MMf5o477jjtMd7znvcwGo0wxvDzP//zD/k17SZ6tjKjlMZoTZEZtGnzdIrMkFK7nYVSdKyhl1lCiNTOE3y7JbY5e4tZjFP7wMbUsT5tGFVOqrmEEELsWw8q6Nmaw/M7v/M7Z72lBXDbbbfxa7/2a/zt3/4tVVUBban7X//1X/O85z2PP//zP2d5eZnf//3fR+2w7XL06FGOHDnCZDLhBS94AbfeeivQ9vd573vfy1ve8hYAXvOa15zT3K29wGhFNzNY0/53MUrRzSxaKaw1aBS1C0zqNoCZ1B4fI94n7h5U3L02YVJ5SqPp5IYiMxSZpbAaqzWVjwxrL4GPEEKIfemcB44eO3aMxz/+8UBbTn748OEHvP7o0aPzai2A//Jf/gvPeMYz5t8fOHCA0WiEcw6Ayy+/nI997GM885nPPONj3nrrrVx//fWsrKwAbYVWVVXzx3je857Hxz/+cYqiOJe3ticGjm5Wb62NGqaNJwHTJrA6qlgZO6aNR6VEbgy90rLczSmsJpKwWtEvMrqFpVdmLHXz0x6/coHSahlCKoQQYs8428/vc05kjjFuO94cIHomp3ZEfsITnsBv/MZv8PnPf57vfOc7nDx5ksXFRa666ip+6qd+ite97nUP2HgQ4FnPehZf//rXedvb3sYtt9zCnXfeSa/X45prruGVr3wlr3rVqx7R/j6PJWs0Bzo5mVLcN4aTw5qJ8/QKy6FeQW41dQg0IWF1u6KTm3Y7bOI84zowqBwXxDYIKjKzbRxFZjR1iHRiwmhJcBZCCLF/nPNKz362F1Z6NvkQWZ86KhcwSmGNIkRYGVVMmoAG8kyTG4PWinHtaEKkCYEYEsvdnH6Zk88Gk24GPpt5QUudjExmcwkhhNgDHrGVHrE7bHZSXu7m+BCpXGDaBIZVw8mRQ6XEBQsFzkZ8iCSgm9t59VZKkFtNiInahXnQExMohZSxCyGE2Hck6NmDQkzUIZIZjQ+RUe3bknMFRIUGRo0nn2jKzDDxgU5myExEa4Wd5fhopdBGUYdIOdvOciFSWi1bW0IIIfYdCXr2oJgSKbWdlcdNO0aizAzj2qONQikY1J6T44pCa7TW9EvLuM441MvpFZbcaFyIWK2IMeFCxIW2QqzMzGP9FoUQQoiHnQQ9e5BWbWDjQjtqIjPtNlU1C4A2Ksdw2kBUOO3QKAa1o5s15Ea3jQt7OT4kKhdxMZJSopMZylMSm4UQQoj9QoKePchoRWE0o9oTUyJTihATU++pfLvVdahfYLRmNG2IMVHFCApy067klJmFDJTyLBnLYieXLS0hhBD7mgQ9e1SZmbaZYOXRSpFSoqoj6+MGqxQXLXZBKQ50M3JjqFxgVDu00m0Vl4/ElMitZqGwEvAIIYTY9yTo2aOs0Sx3MryPbEwbFNBETwyJXmGxVuFCopdndAqDrRVWtddMGt82KMytbGcJIYQ4b0jQs8eVmWZYK6omYLVGaWhcYDLbArNG0fiINZqlXo5RmsWyHUSaWwl2hBBCnD8k6NmjfGjnZIUEB/sFjYskpTg5qZnWkSwksrwNanKjSUCKCWNoq7ck4BFCCHGekaBnj6rc/aXqAIU15JkmkbhrbUKmFcaotjQ9RqZNIKbEBTbHx8iocrK1JYQQ4rwiQc8etLU54VaFNVyy3MEqxfHhlBgTo+BxPmK15shyyZGFkjyzVD7iYmJhywgKIYQQYj+ToGcP2tqc8FRlZrl4uUMnt1itmNYelyIHuwXdLQGO0W1FV+UCfQl6hBBCnAck6NmDNpsTxgRmh8BHa81SN6ObWQZZQ2bMjjk8MlFdCCHE+UT+ib8HbTYndCHueN6FOK/cMlqT7RQZ0a4UpdSuHAkhhBD7nQQ9e1SZGYxW84TmlFI7isKF+fysrStCO5GJ6kIIIc4nsr21R1nTdlKuXKAOkTQLYEqrt1VlFUZT+YjRpw8RlYnqQgghzicS9Oxh1mj6RtOJiZgSWqnTApgyM7jZClBmNHq28uNClInqQgghzisS9OwDRisMO6/WnO2KkBBCCLHfSdBzHjibFSEhhBBiv5Og5zzyQCtCQgghxH4nextCCCGEOC9I0COEEEKI84Jsb+0xQfJyhBBCiAdFgp49wod4WgVWYaQCSwghhDhbEvTsAT5EhrUnxLSt1865TkqXVSIhhBDnMwl69oDNURNbGwkadfaT0mWVSAghhJCgZ9cLMVGHSHaG4GRzUnru43yO1tZVnIdrlUgIIYTY6yTo2eViSqTUTkTf8XyMjCqPDxGj9WmrOA91lUgIIYTYLyTo2eW2Tko3pwQ+PkQGlafxgYUyIzNqvopT+UimFZMmUJwyXyvEhAuRxkcaH8itIbcS+AghhNjfJOjZ5YxWZ5yUXrlA7QJLnWwetKQYqZxnZdTgfcADS4Vlocwo8nZ1Z2PiGVQN3kciMJo6Llws6ZeZbHUJIYTYtyTo2QN2mpTuQmJYeYrMzFdyfIisTxuGU4cCrNVkKOqQqEY1pESIiSYktG4ft/aBYe1hUOFjYrmbS+AjhBBiX5KgZw/YaVK6j5HcKBbL+xORKxeY1AGrNbnVNCGilSLGxMR51sY1hdV0i4zCtgHPcp5jjCakxLj2lJmRHB8hhBD7kgQ9e8Spk9JTgqFxaN0GKCEmKhfn18YECujkhnHlmdaBSRWYJE8KiqkJZFbTyzVGa5oQ8DExdYFObqWPjxBCiH1Hgp49Zuuk9MLfn+sTUyLECCS0giZECqMprGFqPGvThttWRkwrP1vtsRzqlVy4VLLYsRijISXiLKiSaexCCCH2Gwl69rCtuT4ptas90yYybQK51fRyS+U9q4OaQdWQQiIp0EbRhMh94yk+RaYup19aCluitUIrCXiEEELsPxL07GHWaDpWszpuWJ80nBxV3DusyZTmUC/Hh8i08YzqgELRRI9Gkc9WgCofGVUOMwt0QjfRyYxsbQkhhNiXJOjZw6rGc3JUM6o9o7rt19PLDSlC5QNxFDk+rvAuMqwdubGUpQEUdYzElBhUHgV0rcFqdcbOz0IIIcReJ0HPHuVD5OS4YdIEjG7L2Be7BQD1rNNyFRONg0njMVpxyXIXbRSj2lPVgUgkzKrA+l1LmRu0rPIIIYTYpyTo2aPGtWfatJVWw6kjoSisbhOdtaKXW0JM6AQnxokYItZoytzQyTNSN1KFSOMCB7sZvTwjM0byeYQQQuxbspexB4WYmPqA0QpFIqSImlVtAVitSSrRKyzL/WwWCGkSCR8TRoPNNAroFQZjDdpAN5d8HiGEEPuXrPTsQTElFAqrFQmFUZpEnM/n0gpChEhiqVvQLyoGU48LkGKgaQIhRRLQsRkpJZY7Ob1C/jgIIYTYv+RTbg/SSmGNIkSNC5EyN0ydx8fNnj3talBhDHlmuOxgj0ntGdSOtYnD+4hViuVeTpFpLloquWS5K+MnhBBC7GsP6lNuZWWFD37wg7zsZS/j6quvptfrURQFl156KS9+8Yv52Mc+9gMf46/+6q/42Z/9WR7/+MdTliWdTocnPvGJ/PzP/zz/8T/+xx94/3A45Dd/8zd52tOeRr/fZ2lpiR/7sR/jne98J03TPJi3tWdsDiHVuj1OpLZnj4sMK8dw0pApSLQJz0eWSp5yZJGrL17mWZcf5JmPP8jTLz/AZQd7POnCRZ50wQJlLvGvEEKI/U2llNK53pRlGd77+fdlWWKMYTwez3/2/Oc/n49+9KN0u91t96aUeP3rX8/73ve++c86nQ4A0+l0/rNf/uVf5nd+53d2fP7vfe97/MRP/AR33HEHAN1ulxACdV0D8IxnPIPPfvazHDhw4Jze12AwYGlpiY2NDRYXF8/p3kebD5Fh7al9IEaonGdYe9bHDdYoDnQyFsqchdLSLzOA+ewuHxKJRMcaeoWVFR4hhBB72tl+fj+oTzvvPddeey2/+7u/y3e/+12m0ymj0Yjbb7+dV7/61QB84hOf4LWvfe1p9/7+7//+POD5X//X/5Vvf/vbTCYTJpMJ//2//3d+8id/EoB3vetdO64Yee950YtexB133MHjHvc4/uqv/orxeMxkMuHDH/4wCwsLfPWrX+VlL3vZg3lre8bmENJ+bikyTa/IuGix5GmXLPMjlx7giRcucvGBLsu9Ams0Sims0WRa080NB7sFSzJRXQghxHnkQa30fO5zn+O5z33uGc+/7nWvmwc2x44d47LLLpufe+5zn8vnP/95nvzkJ/PNb34Ta7dvqzjnuOqqq7jtttt4yUtewp/8yZ9sO/+BD3yAX/zFXwTgb/7mb/jxH//xbef/5E/+hJ/7uZ8D4DOf+Qz/6B/9o7N+X3tppWerMJuXpZU6rfrKh7br8uq0YThtV4ZKo1koMw70cvplJoGPEEKIPe0RXel5oIAHmK/2AHzlK1/Zdu6ee+4B4Ed+5EdOC3ig3Tr70R/9UQBGo9Fp5//gD/5g/hpODXgAXvKSl3DFFVcA8KEPfegBX+d+YWadlHcKeNamDSeGNcOJQytYKDKUVmxUjhODivVJgw/xMXrlQgghxKPnEfknflmW8+MQwrZzT3ziEwH4r//1v27LC9rknOO//Jf/AsD/8D/8D9vOTSYT/vqv/xpoc4Z2opTin/yTfwLApz/96Qf3BvaJygUmdcDHRG4NvSKjyAxlZml85ORwyl2rE9YnDSGe84KfEEIIsac8IkHP5z//+fnx0572tG3nXv/61wPwne98h5e+9KV85zvfmZ/71re+xc/+7M9y22238aQnPYlf/uVf3nbvN7/5TWJsVyWuueaaMz7/5rl7772X1dXVh/Re9qoQE6NZ1+YQE9ZoQkycGFR8+faT/Ke/P85//PZ9fO7b9/K5b93Lt+5eZ31cy6qPEEKIfethr1NeX1/npptuAuC6667jyiuv3Hb+RS96Ee9617v4P/6P/4OPfvSjfPSjH91WvbW8vMzrX/963vrWt562L3f33XfPjy+55JIzvoat5+6++24OHjy443V1Xc8rvqDdE9wPfGhL11fHNePK4WOicZp71iu+ec8G9w2n+AQQWXSWmMCiCCnyuOUey90cpRRuFgDttHUmhBBC7DUP60pPjJGXv/zl3HPPPZRlybvf/e4dr3vjG9/Iv//3/54LL7wQaIOdzXL1pmkYjUZsbGycdt9wOJwfn1oKv9XWc1vvOdVNN93E0tLS/GtrwvVetVnK3oREYS1FZgHNsdUJ/+37K9w3rCiNpp9b+nkOyhBCYmVSc3Lk2Jg67t2Yctf6hDtXJxxbHXP32kRWgYQQQux5D2vQ80u/9EvccsstALznPe/h6U9/+mnXTCYT/tk/+2e88IUv5PLLL+fTn/409913H/fddx+f/vSnufrqq/nDP/xDrr32Wv7u7/7u4Xx5p3nzm9/MxsbG/OvOO+98RJ/v0VC5djurV1h6hcEaTeM99w4n3Ddq8DEwDTD1ARcjWitUUgQPK6OaExsV96xPaVykmxt6ucXFxMq4kaRnIYQQe9rDtr119OjR+crOu971Ll71qlfteN2NN97In/7pn3LllVfyhS98YVvS8z/+x/+Y5zznOfzoj/4o3/72t/mX//Jf8oUvfGF+fmFhYX48mUzO+Fq2ntt6z6mKoqAoih/85vaIEBN1iGSzEvQyMxSZZlIHTqzXjCuPtVBmGotCoQgBgo00IbA+qemXhsMLJWVuUEqR0qzbc4iMa0+ZGfpS4i6EEGIPelg+vd70pjfxzne+E4Cbb76ZN77xjTteNxwO+b3f+z0A/uW//JfbAp5NnU6Hf/Wv/hUAX/ziFzlx4sT83MUXXzw/vuuuu874erae23rPfhdTIiXun7ZuNItlRlkoMq0wVpGSAqXo5JblbsFCaUkRpi7QhIRWGqM1G1PH8cGUlVHNcOpofMTFxHS2kiSEEELsNQ856Lnxxht5xzveAcDb3/52brjhhjNe++1vf3tepv6kJz3pjNf90A/90Pz49ttvnx8/9alPRev2JX/ta1874/2b544cOXLGJOb9SCuFUrA9JlEUJuPig10uXepyoJtzeCGn38noFBpjdBvMNIGONagEJ4YV31+dsDKoOTmqWRvXjOqaSe1ofCCeez9LIYQQ4jH3kIKeo0ePcvPNNwNtwHPjjTc+8JPp+5/ue9/73hmvO378+Px46/ZUt9vl2c9+NgCf/OQnd7w3pcSnPvUpAJ73vOf9gHewv2wOInUhzjsxjyqHTxE7Gz/RyS3OQ9V4BuOalfGU1eEIdKTMNSdGUzbGjsxouoWhsIaJC2xMAuPG40Lb+VkIIYTYax500HP06NFtW1o/KOABuOqqq+bl6e9///t3bE4YQphvgR04cOC0kvdXvvKVQDsK40tf+tJp93/kIx/htttuA+AVr3jFObyj/aHMDInEyqhm2gSMVvQLy0KZ0S0tpdHE6FgdTvnuyRHfOzFkUAdUgu+vTvj2vRvcsz7m7tUR9w1r6sbTyQxT52maiJGARwghxB71oIKerTk8v/M7v/OAW1pbdTqd+dysv/3bv+VFL3oR/+2//TdijMQY+bu/+zv+5//5f+Zv/uZvgLa03Riz7TFe+cpX8rSnPY2UEj/90z/NZz/7WaAtl//IRz7Cv/gX/wJoOzafy9yt/cIaTaYUViuMUSSgk1sO9jMOL3RZ6GQU1tLtWC7slVx+sMuTL+hzqFvgfEKjmFSe1UnN+rhiZVSxMqxQKGb/J9tbQggh9qRzHjh67NgxHv/4xwPtdtXhw4cf8PqjR49y9OjR+ffT6ZSf+qmf2rY9tVlBtbVR4Etf+lL+8A//8LSgB+COO+7guc99LnfccQfQbnvFGKmqCoBnPOMZfPazn+XAgQPn8tb27MDRrUJMrE8b7GwrMaZEEwJ33Neu3ISU2BjXxATjqadJAYMCFGvTBgNkmSVTkOWGhdyw2M05slBgM8sly12OLHWkWaEQQohd42w/v8+5ZH1zDMTm8db8m52cOjS00+nwl3/5l/zZn/0Zf/RHf8Stt97KiRMnUEpx2WWXce211/ILv/ALvOAFLzjjYz7hCU/g7/7u77j55pv59//+33P77beTZRk//MM/zEtf+lLe8IY3kOf5ub61fWFrBZdSCoNiUnsyrblwoeS+YUUCSJBZRQyGTMOk8RijiTGiSSx0c4xWlNayWOQobUgp0cmMBDxCCCH2pHNe6dnP9ttKj9GKEBP3DSo2pg2ZUaxPHYOpowmBqk7UPnB8fUTtE1ZD7RO50Sz2cg71C6yCXplhlObxF3R5ypElrPTpEUIIsYs8Yis9YnfbrOCqfCQlxX3DijvuGzOsHFYppiFQ6jbXp/GeewZtB+aQEnlmaHwg05YmRTq5pmsz1saOxx/qcslyVwIeIYQQe5YEPftQmRnWpzW3nRhz19qE+4YVlfPECJ1co5UiJIVrPMNJTdUElILaJUKMWB1oQqBrNQsXWg73S37ocQss9fZP92ohhBDnHwl69iEfIieGNXcPplRNJDeahCXNknmmdeC+wRQXI2MXmLqAtQqlNEWmsVphTcIFRaY1lx/scrB7evdsIYQQYi+RoGcfWhk1rI8aepkhU5qUMiIQYmR9XDOYepJWTKaB3BryjsWlSGENuVYkregVGf1Ohtaabp5RZKdX0QkhhBB7iQQ9e1CICTebdp4Zva2aqvGR9WkDQIxtcnJmTZvDEyCrDErBwU5OphRFblnoZGilqEPbfDAziq7VaKPplRlKtQnRUrUlhBBiL5OgZw/ZHC0xqD21iyQSHWtYKC39MsMajY+RGBNtvnE7JV2pdi5XgaGTaxY6FhKYTOECZFZTGk0WNS5GysxitaJfZCwWFqMUMaVZPx8hhBBib5KgZ4/wIbI2bRhMHFopunm73VT7yMq4wcfEcjfHak2eGTJjAYePiTyBUpAAhaYwmpQg0zlNiBg0ZWbpasWkaXv69EvLQi+nV2ZkVsu8LSGEEHue1B/vEZULTOqANZpuYbFGY42mNzse157KBXKrWSozikzTKy0+JkZ1IIRECImNaU1K0CstndySG4OPgY2qYVx7mhAockVhDQtG08uMNCQUQgixL8hKzx4QYmLiAilBbk+PUzOjqX1bhdXJLcvdjFGTU7tIvxMZTBpWxg7XBFyMLBUZlyz36OSatYnj+6sTBpOGWkeWuxmZMRijYNbPR8IdIYQQ+4EEPXtATIkYE5DYacFFbw4CjYmYEmVuuXS5Q2k15ciQG8O4dnhruTi3LPcy+qWlV2Yc6EUWyoxjq2OqxrPczTnYKeh3DGVmqUPk+LDmYIjzvCEhhBBiL5KgZw/QSqF1OxQ0JjCnBD4xtfk6iXZVSKtZ4HPQcuFih9oHfEiMa09mNdYopo1nY+LwMXJBv6RXWKYusFhmhBjpFxm9MkMrmDSBjakjKcXCbDtNCCGE2Gsk6NkDjFZ0M8Oo8kwbTye323Jsps5Tu4BVilHtUQoKoykzQ241udW4EAkpUViNUgrnI93cktk2qVlrKDJDZjSjOrI5ki2mdhVp6gJuWDOpHMu9glLyfIQQQuwxEvTsAT7E9iu2+TkbVUM/sxSZoQmJUdXQyy3dwpJbTUxQ+YiLab4yo5VCqTaIISVcSBSzwMWHSIgJHwPTJhBT4sSoZm3aUDWBUeW4b9yQUiIzmiOLHR63VHLRYslyr5CVHyGEEHuCBD27nA+RYe0JCQ71CzqZYVB51iuHbQK5USyVGQcXynnwYRQYbahcoHKB/qyB4eYgUqPbvjvZrAxdKUXlA94FolJkWjFsPIOYGE49g0mND20ARYpMG8/xjYqp81waExdseW4hhBBit5KgZ5erXCDERDkbA1FYw2Inx4XIpPbEmDjQL3bcatJKsVG5Ng/olK7NKbX5P0qBCxGt2kotUAwqx9QFMmNAJRofWejkHFgoqZxvR1dkGhcSq+OGfpnRl6BHCCHELidBzy4WYqIOkeyUgMJohdFtELQ6rmf5N/cHNT5ERrVjfew4PpiSSFhUW63VzenkhpQi4zpR5gZFIqXEpImsTSpWxxWl0WitGU4bIBFSYDhtyI2iiZHMazq5ZuoC4/r0PCMhhBBit5GgZxeLKbVJxmeIJaxWKBQ+JuxsHqgP7eyt9XHD2qhhUDlKY/EqUo1qGh840MtZ6uaUmSICo8ozqhy5bRsaDqaak+OG0dQxadoO0D4lysyyNAuaDnQyLlgoMLpddZIxFUIIIXY7CXp2sa3Jx6eWqUObi1NkmhDT/GebnZsbHxnUjtxolvs5WrXnah8ZVp7SGpZ6OaPKszZuSAmKXNPNC5yLOO8ZNlCFSIhQGIUCfEhMG4/RCqU0WmkUSsZUCCGE2PUk6NnFticfm9POuxBZLCxJKSoX0EoxbdqAZ33SQEosdwo2d8dya3AKpk3g3kHF2tThXGTkPJp2lSjLLUWu6JY5yw7qxjGpIgtlwUI3w/mIUhozm7ye0uZ2mwQ9QgghdjcJena5MjO4mKhcIDMaPVv5caGtwuoX7a+wcoFR0/bxqZ1r52sVGdbeH4xo1XZtnjrPoIoc6BWUVtPXlpgSw8qzNmlQKDKlUBqMMmidcDEyqhpiBK0Mzmv6BfQLS1KJEJMEPkIIIXY1CXp2OWs0C4Vtt6ZCW3WlFO2IiczMS8X7RpNbMxtXofABwiwnaHPnKSaofbvFlWlDJ7MkwPtAkWk6hWF15FFJUWSGXmGYdrPZwNF2vpfWqh1q2rU8brnDgV6ORklOjxBCiF1Pgp49wBpN32g6s9laWu28nZRbzWKZUbmIrtquyz4m8tm1zgecj6QIDYFh1TBuAusTN1s1yohJkWno2PaPRiezZLqt/AohUcdIx7YBUTe3MBuRITk9QgghdjsJevYQo9UDrqZsbjF1C0OZGaa1pwqexmkgUbvItPYMa8dCmeNioptZVBc2po7BpCYSyUz7LEorFkuL0RpjNEolnAtMvacJgamLuJDo5ea0snkhhBBit5GgZx/wITKuPZUPKNqKrwPdjBQja7Oy88ZHfEisTx0+BAoTiT7R6Rk6haHMDavjhtE0olVbJ7+cGRIw8ZHBqGFcOZRKdIt2Qnsv0+RGMa4965OG5W4unZmFEELsWhL07HFV4zk5qpm60K7IaMi0pswsFx+0XOgja+OG4bQhKcWhbs7atKZ2ibELmNrRLzIWygyNop9ZDvZyDvQLtFYMpo44raktGANlnnGoV7DYyVjqtJPYfWwnuJeZkc7MQgghdi0JevYwHyInxw2TJtAvs+2VXbTNC63W9MqMbmHJjGZ1XOES+FATYmRceQwKY9tgZbljWe62Ac1SN4cD7diKtXHDpPFkRpNZTTab5wWgVKL2gakL0plZCCHEriVBzx42rj3TWcCzGWhsHTaaYmLiAlZrOnn7q86NZaGMJKBynspHqD2HbM5CN8MYhbWGzXaHm8HNpNEk2lwhdUrSslZtNk+cJVpLFZcQQojdSIKePSrExNQHjFY7jqnIjGbaOLwP6KztraOUoswNpdNEDN3M4EOksJpDi2X7uCFS+0BuNKPaoxRYpeaJyjt1h44JEkgVlxBCiF1NEjD2qJgSCoXVbSByqnarS6GUQis9v6bINN3cYtDUIeBjQmtF7QKT2lM1AasU3cJSWI3VGhcTIUGIERfiac/lQkTDrJ+PBD1CCCF2J1np2aO0UlijCFFTu7ZpYIipzeOZJxMnlrttaXrbwdlgtWaxm2GNJg4S48rNev+AtZpuZji4UM4fY3O7zMeID20e0SQm8lkOUO0jKSWWOhlldvqoDCGEEGK3kKBnj2p79sB42nBy3LAxdSQShWnzdzKjuGCh4GAvZ9h4BhO3LVgxWnHhQknvUI9+2QZBk8aT251XazqZnW91jZvApAmgEqUxLJTZ/DGEEEKI3UqCnj3Kh8g0BDYqx7Tx5EZDgiZExqOKpU6OVe2qz4FOTqYUg9pvC1YOdot5sFK5MBsgunOTQa3AKM1iJ2O5q+bbXFuruIQQQojdTIKePapygcYl8tywpBUahY8REkSgmxtCaq/rlxnLvYKFTn5asOJDZFQ5Ji4wrDyj2lPadrWoyAyNbxObSWCMmo/A2GnquxBCCLGbSdCzB4VZKboPCas0nbL9NaaU5uXkzSxJeWvvnFODFR8iw9rPx1coYFg5RkBMNSlGKh8JMVH7xMFuhk6J5V4hW1lCCCH2HAl69qCYEjEmUopAm4SslKLxCed8e5ECZtedqXfO5paW1aotT9dtBVbVBO5enzCtA4udjH5hWOy0yczH1if4mLhgS7KzEEIIsRdI0LMH6dlkc6XaQaKVi2xUDRvjhmkTqEPE6MTFS12ekJsde+eEmKhDnPXz8TgfsVoTNaxXUzYmnty2Je/WGA7M5moNpg2r44Z+mcnICSGEEHuKBD17kNGKbmaYNoFhFTm+MWXiAlUTmDSOtVHFqIkcOzFiVNdYpU5bmYkpkRI4Hzg5qhhVgUHlWB1XfH9tQvCJfqfdFityTUxto59OZpk4z7j2MnJCCCHEniJBzx5VZoZuYbhrzbEybnAhMK4848ajjeZxCxkRuHe94dvZAGBb4KOVIqTI+rjh5LBm0ng2xo57BxUnNia4AN2JovGRUdWQKbhgoUN7e9v3R0ZOCCGE2Esk6NmjrNH0MotRisLA+sgz9o4ysyyVGb3CYm3be2dQtZPYt25JGa1IAUaVp/GJQe1ZGztijHTynFg3OBQbY4cLkTIzFJmhX+SAIjNaRk4IIYTYUyTo2aN8iIxqRxPbXJxuoekVJQudnCK3GN0OAN3cfpo0YduWVOMjlQtUTeTkeMo9axVNCGjVlrHHCN3ZWAmNZlgFjg9qfB8WCkuvkK0tIYQQe4sEPXvQZqm5j5BrgzaazFhiarec2BwOGkGjyI1ic0uq8YHaBU6MKu44OeHYypBjJ4asTBpQisUio8gMKrOMakcnM+RG4b3n+MaUrtU8/mBXRk4IIYTYc6T8Zg/aLDVf7GQc6OVoEuh2yyukNG9AWM+msGfWECNMa889G1Pu2ajYGDvqxtOEiLGGfpFhtaYJkZhAGUgxMmkiK5OaE8MaReLAYsEB6dMjhBBiD5KVnj1ma6k5wMF+zgWLJavVkEkd6eWacUztEFKjKIxhfVyxYTSro4o6JqxSdHKDUonSWg4ulCjToKqGlBQ+JaKPLHVLOoWmYw3dwnJhv0Op76/kEkIIIfaSB/XP9ZWVFT74wQ/yspe9jKuvvpper0dRFFx66aW8+MUv5mMf+9gZ71VKnfXXc5/73DM+zvHjx7nhhhu48sor6XQ6HDx4kOuuu473v//9s/lR+9NmqflmOk2ZWR5/QY8nHeqTGcXKuGFtVAOJXCnWJg2DqUen9r99DImTw4o7VkaMp55OZuhYQzELZoaVYzBpcDGhdTtvq1dYjix1ONDPqUOk8fEx/W8ghBBCPBgPaqXnyJEjeO/n35dlSZZl3HXXXdx11138xV/8Bc9//vP56Ec/Srfb3XbvRRdd9ICP7ZxjdXUVgB/7sR/b8Zpbb72V66+/npWVFQD6/T7D4ZAvfvGLfPGLX+SjH/0oH//4x8nz/MG8vV1NK4VSEBOYWeDTL3KuvmSZC5c6HFsZsTZt6BcZdd1OTT/QL+h1MyoXSEqRUNw3mrIWHId7BeVCQUowrh1jHLVLdPJ2gGmeKQ72Sw4vlJSZwcy2wMKWJGkhhBBiL3hQKz3ee6699lp+93d/l+9+97tMp1NGoxG33347r371qwH4xCc+wWtf+9rT7r333nsf8OtXf/VX59duPtZWGxsbvPCFL2RlZYWrrrqKL3/5ywyHQ8bjMe9+97vJsoxPfepTvPGNb3wwb23XM1pRGI2bBR6b/2uN5shShx++5ADPedKFPOOygzz+ggUuP9hjuV/QLzIKY9AKrIZ+nuFCpAqJTpZx+aE+T7l4iSdfeIDLDva5oF9yaKHgiYcWuPxgl15hZ2Xrej7aQgghhNhLVHoQe0Gf+9znHnDr6XWvex3ve9/7ADh27BiXXXbZWT/21VdfzTe/+U2e85zn8IUvfOG08295y1t461vfSqfT4etf/zpXXHHFtvM33XQTv/qrv4oxhm984xs85SlPOevnHgwGLC0tsbGxweLi4lnf92irmrbvztQFjNYYDZnWaK0oMsNCYWlC5LvHh20FVm6xpu3ZszFpmLrAuA6sjSu6RcZFSyUxgCbhfGDqA1rBgV7J4cWS0lqsUWRWE2KiX1gO9gpZ6RFCCLErnO3n94Na6XmggAe2r9B85StfOevH/Zu/+Ru++c1vAvCLv/iLO17zoQ99CICXvOQlpwU8AG94wxvo9/uEEPjjP/7js37uvcKHyNRHrNX0igyrFTHBuGmnquda4WOidm3uzbAO1L6t9iqsoZNZtFYQQSdFDJGqCoQQ2g7LRtPJMi452OeJhxc41C9Z6GT0y6zNJQJy0+b/hCirPUIIIfaOR6R6qyzL+XEI4azv+8AHPgDA0tISP/MzP3Pa+W9961scO3YMgOc///k7Pka/3+e6667jE5/4BJ/+9Kf5rd/6rXN56bveZrl6v8iAtporpvZrfdJwx8mGOkTWJjUnNibcN6g51Cu4YLHDYmnp5AZjCmoX6EZLkRs6uaGTGXqFpV/MgiJmSedASolxHfAhUlhN7QMuJpSCwmjKzEgJuxBCiF3vEQl6Pv/5z8+Pn/a0p53VPaPRiD/90z8F4KUvfelpCdAAX/va1+bH11xzzRkf65prruETn/gE3/jGNx7wOeu6pq7r+feDweCsXutj5dRyddgcJ9FWXW2MG9anDUa1qz29vGBoAydHdXtvN6NX5litOLxQ8sRDhgsXOyz3ivljZUaTUmJUOQa1Z9IEUG2Ze27a7bMis+hZMnXlIy4mFgorgY8QQohd7WEPetbX17npppsAuO6667jyyivP6r4Pf/jDjEYj4MxbW3fffff8+JJLLjnjY22eGwwGjEYj+v3+jtfddNNNe2ol6NRy9U2VC0zqdnsqAopEL7cc6Go6heH4xpjaR9anniYmLlzocNFCycGFggOd/LRgpfGJPDMcziyb47Vq167ubO3EbBQYbdpxFi7M53oJIYQQu9HDGvTEGHn5y1/OPffcQ1mWvPvd7z7re9///vcD8CM/8iM861nP2vGa4XA4P95pJWinc8Ph8IxBz5vf/GZ+5Vd+Zf79YDA4p6TrR9tO5eohJirXVnA1PpFCQlmDnUVG/TLDmj7eR0iJssi4oF9wZKlDmRl8TPgYyIzG+cD6xDFqPDEmtFb0c8tCafEpbVth2iozmjpEOlLGLoQQYhd7WIOeX/qlX+KWW24B4D3veQ9Pf/rTz+q+r3/963zpS18CzrzK80goioKiKB6153uoNsvVKx8xul1xaROKIylFfApYozCa+QpNW6Ku6HYyCqvbZoSFoQmBjcpRu0gioYFJ7bDGsNDJsRp8hEHlGNaObmZY7O7c90irdtxXTAmDBD1CCCF2p4dtP+Lo0aPzlZ13vetdvOpVrzrrezdXecqy5GUve9kZr1tYWJgfTyaTM1639dzWe/aDcjb5fDOheZZyTO0TBkNuLKDYbEQQEyQApTBGo7Rm2gTWxo4QEt3c0MstK5OG+0YNSrVBjNaa3GqWujk+JIaV59RirRAT49qzNm4YVY7GR6noEkIIsWs9LCs9b3rTm3jnO98JwM0333xOjQGbpuGP/uiPAPjpn/5plpeXz3jtxRdfPD++6667zliLf9dddwGwuLh4xq2tvcoazUJhqVygDrHN8dGQaUgWmhCpmoALmk5uCTGiaRsSWq3wMeJ8RGuFCxEXNEZriNArLJPGU9jt1Vj9MuPkqGJ1VNMrLD5GBlPPyVHF+rhho3IUxvC4pYILF9rePu22muT4CCGE2D0ectBz4403cvPNNwPw9re/nRtuuOGc7v+Lv/gLTp48Cfzgra2tFVtf+9rXeOpTn7rjdZtVXldfffU5vZa9whpN32gyFxhUDY2PrI4dx9bGTCuHD7M+PplhqZuz2MnpZIbGRdYmDfduTNioHc63A0eXOxajNY9bLokoKh8oo8XMgqRJ7bhvWHFiOGUw9YwmDcPKkRIUs9WgjlUMZs/tYmyrwrqnJ0kLIYQQj5WH9Il09OjRbQHPjTfeeM6Psbm19eQnP5l/+A//4QNe+5SnPIXLL78cgE9+8pM7XjMej+ednJ/3vOed8+vZK3yIDGrH2tgxrhxawUKZ0SkyMqMJPjJpPKPakYDSGqbO8f21CSsTR4bmUCfHqsR9k4q71kYcX5/gmzAfM+FjZDBxHN+omDYBndpS+I3KMW48LgZQYIwmzw2dPCMkmDSBce2p3Nn3aBJCCCEeaQ866Dl69Oi2La0HE/AcO3aMz3zmMwC86lWvQqkHToJVSvGKV7wCaEvc77jjjtOuec973sNoNMIYw8///M+f82vaKzbL1H1MJMBYxUWLJVc+bomnPG6JKy9e5prLDnDV45a59ECXPDP4CElFSqtZ6BV0OhmLvYJelpNQ3DusGdQBrRVaKWrXBk7DymFVm6hcaMVymbPUzSkzS6doy9or1wZLSmuqWTfo6SzvSAghhNgNHlTQszWH53d+53fOeUtr0//7//6/xBix1vLP//k/P6t7jh49ypEjR5hMJrzgBS/g1ltvBdrcoPe+97285S1vAeA1r3nNOc3d2ktCTExcwIc0GzqaICmsbiu3OkVGkRsya8kzQ0hw78aUjYnDuUQIicG4bvNxJg2Ni5BgUkfu3ZgSQiLGyOq45q7VCSdHFfcNKu5YGfH99SknJzU+JCKKGNsqLx/BxYhOCYjEGGUwqRBCiF3lnAeOHjt2jMc//vFAW+Fz+PDhB7z+6NGjHD169LSfxxi54oorOHbsGP/0n/5T/uIv/uKsX8Ott97K9ddfz8rKCtBWaFVVhXMOaLe1Pv7xj59zOfpeGTjqQmRlVDOqHJPGM6k9LiZyq4lJ4X2b5NzLDP3SkmLie2tTahdofCSmxOrYUXuHNZpcwcRHTEpcsNzlhy9eIreG769NGU7qduQEihAjo8axOnKgIdOKA92MfpFTZobcGgrbjqW4aLFkuZfLYFIhhBCPuLP9/D7nROYY47bj48ePP+D1m12WT/WZz3xmPkfrXHvzPOtZz+LrX/86b3vb27jlllu488476fV6XHPNNbzyla/kVa96FVrv3wRarRRaK5TSKKVBaUIMDKZtHk3tA94nJqUlqba/j1UKnVkiHucjxoLFtLk/KRGVYqGTsVRmLHdyiswyaQJGQeXalZs6KBaNpnaRceVwsS2V75cKlKKNnxNl1pa7d2bl9UIIIcRucM4rPfvZXlnpARhVjpVxw7j2jCrHfYOKkfMUxgKRECNWaTKj6ZWGfpmxNm1YHTWsjxqK3KCVYlR5To6nKNrZWkudnEsO9jjcK/AxMWocJKhDZOraLatJHbhnfYwPkV5hOdjL6eaGMmuPDy+WUr0lhBDiUfOIrfSI3aEdCZGY1o61Uc3KpCGGhNORSMIqUJkCIpM6cWSpg9GatXHDoPZ0fCAmWJvUDCtPv7Ac6pcs93OGE0cKbSwcYyLPNAlQEbyLQGAxN/jYruK4EEFlHOoVPG65I316hBBC7EoS9OwxPkTWJzX3DmruWZ9y98aE4+tT6saTWU2mNb3ScuFih4O9gjI3jJt2WvqFSyWkRcaVZ2VcMZi0JeXLCxmXLy9wZLmEpLhnNGFtWpFSO15ikZwiM5ApQtJ4Z1ha0Cx3cg71ci5Z7rLUK+gWlsxo2dISQgixK0nQs4f4ELlvXHHXyoTB1OFiZCHPcN3IJFNYFL0y44J+ycF+wYF+QWY0dlIzbQIpwhUXLoBS3LM+oXGBiQsURnOgX5CAk6MaPFibtTk6KVH7QG4MS2XOZQezdqxFSiyUGRctdVg6w0wuIYQQYjeRoGcPqVxgbeRoYiTLNCElyAwLXYsbRyZ1II+JqKAOCR8iRrdjJjIDg6mjW2Qsdywnx5oQE12tyLUiJhg3jmkTWCwtWWYYjBvWq4baBU7oml5mOLxQcMFCyYFezsF+Qa+QP0JCCCH2BvnE2iNCTAxrz7QJEBUxJHxMbeCjNFYblGobBI7rgMKRWUXmAiEk+oXBxcT6uCYzin5uCT6iI4SUqJyn8YFerikzy8h5mggGTWFSm/TceNIo0S0tR7IO/cJK3o4QQog9Q4KePSKmRAiRGANKJSKRJgRIin5hiTExqhuaGNApUjWO4+uBg72CCxYKFsqMCLNhooorDvVZ6dSsTWomVSACPZ1DSjQhMK09znuCShS5IaEotaJXWDrWoBJ46bYshBBiD5GgZ4/QSmGMRmtDCu1qjw+JYpY43CsyemVO1XjqCComigwOLxQsz8rPu1bTKzMqF7CZ4vBiiUJhVbuFNWraYGdUOUaNp3GRMjeUtl3RqZ0nJcXG1NPJG7pFRidv/wjF1K4GSRKzEEKI3UqCnj3CaMVCYckzzaCqqVMiBIi6XW1RJJZLi+lkdKwGBQd7Bf1Ojo8Jo1VbgUVb7u5jZLHIyJYURa5ZHzsmfoJP7ZZZbtpKsH6Z0S0tzkc6haWwhkRi3DgGVUORaSJtlZdSUJi2I7NsewkhhNhtJOjZI3yIVM7jfGBt7NiYOurak1uFtQajoZsZFjs5mQUfFHnWdknuZIZiSyCiVRukaK1Y7hUsdHIuXoo8yfU5Maz49j0D1qcNwUeMVlQukBL0i3bUROMj3sPGxNEvc3qFRSuICSofcTGxIPk+QgghdhkJevYAHyJr04bB1LNY5jzlIsOJ4ZTvrUxYH9V0Crigl3Oo1+HQQs5CmTGs24aDyzvMvoqzVRk9m2pvtMJoM5ufpRlOGybes1oHgvcs5Bmd3NDJLU0IxJRofEDr9uebj28UGG2oXKBygb4EPUIIIXYRCXr2gMoFJnXAzraOFjoZB/sFlx/sc2JjSkxw0VI74NNo3ebXGD2bun56jo0LkdLu3EQwt4YjB7o0ATI1ISbaVSKlWK9qpk2ElEiFRWtFjAnM9sfIjKYOkc5sW00IIYTYDSTo2eVCTExm20u5vX/lxOh2QGib4+NmDQMhkSitpp8bpj5SuUBm9Hz7yYV2y6rMzLbn2ExEBjAoOoXh8GJJ1UQmjWetdqCgV1h6uSEzmsJopi5gtEIpNX+Mze2zmBIGCXqEEELsDhL07HIxpXY1hcROiya50fRzS27a8ROFvX+7yZo26KlDnCcaZ7PVn9pHxnU7nsKFiFIwaQLrkxrnE+PG0/iI1Yoy0/SLEmva4KaTaazR9Muc2gcaF8iswYU4e61gDHQyIxVdQgghdg0JenY5rRRaK6DtmmxOiR9iggRkVm8LeIA2MDGaTmxzcCaNZ2XasD52HB9MWR+187UKa/ExUvtEmWkO9gqWS8vUR9bHNQMXWOrkGGXpdywXLpagFJUPON+uBBXWMmkca+OG+0Y1pTVcdqDi8Kx7swwgFUII8ViToGeXM1rRzQzT2YqM0dsTaFyIaNpVlTOtqKSUGDWetVHDqHIMpo7VSUPlEh2j2ZjWDCqHNYZ+2SGRcDGx3M3JrebYySExQaewKK2oXKRXGnxIjGpH1URGU8fYBaZNYCFvq7mGU4fRChcifvZ4EvgIIYR4rEjQsweUmaFbGAYTxySmeW5P7SMpJZY62bYcnU2buTqT2jOpAz4mElB5j0HxuOUOg8oxnXVkzjI1X7XpFopR7dkY10RlUErRLy1aaYa1Y+w8uVFopRk3TZvHExMLRUa/tKAU08YTUzvmYlx7ysxIRZcQQojHjAQ9e4A1mgOdnEwpBrM8HFSiNIaFMjtt66jt6dNOUK+awMa0IYQ24KldpAltE0EfIqOJY3XiaOpA3UQmeWDqAtAhkmh84mBpibSzvgoLvSJjdVQxcBEzyykChTORMtNorUipXaXyqb3Px8TUBTq5lRwfIYQQjwkJevYIa/S8kaALEWhLw08NIO7v6eOoXGRaO9YmjtpHjIHCzEqrVGJQe8YuoFLCp8jGpOLEKOF85Hu9nH6Rs9TN6OdtgvTqyJFnnsYFNiaOk+OKpaKgWxqsVlijmRWAtflHGqyaPd8sIVsquoQQQjxWJOjZYzYbCZ7JqHKsjRpqH1CqnclVu8S0rlgdVZTWkGe2XXlp2lL4JkZWxjWTJpBSwvvEcNKgtSG30C8LLj/YITcKpRU+KBSRzCqKQqEUjCpPZlU7viJThBgxSoOmLRubJWRvlsULIYQQjzYJevaRENvVmyZErG7LyseNZ1g3rE5q1ieOJlT0y5wMAMW4rhmOm3b7ygeakOhYTSSyUTk6xtCd5egMa4dG0+9YFjslpbf0MoOLCWMjUxexlaevLGYzyIF2FUirB0y2FkIIIR5pEvTsIy5EJnVAAUopBtOG9UlDHRLdLCMUiZVxZDJxNCHSeM+08W1CtG8TpLsGMJpxEzBJk1tLJ8txs4BqMW/DpUnjONgt6JV23gvIV4HBtMHqNvk6JYVVGqMUvcLumGwthBBCPFok6NlnYmorupoQmDhPTIluZlgoLGXWbjcFn9iYNqxNIpWLhBSJKZFrQ1lonIdeocl7YJJqe/xMPU0TsH0FGg50cxZKg7GaxW7GQplxr520JfRGgWrL6Je7ufTpEUIIsStI0LOPZEbTzSz3jeq2XD3MGhfqdqspzw2X2C5lblgZVaAUq8MKnyxj5RhWgfG4obDttPZOZmhCRKvUbn8lmPjAobxo53zZNiU5N4a8a8isprSaxU4+yz1SOyZbCyGEEI8FCXr2EaMVy92M1UnNRuXaWVypraTyvh1F0c0MZaaxWpMp0EZhEpRW42xko4JJ8HStQeUZ/VxjtGa5l7FUFigSHWvJjCHEdhXJaNXO+NKK5W67qiOEEELsNhL07DP9MuOixZLx1LFeO2KEzLaDQAvdDhL1AaYhUBYZR4xiUgeONxG0Js8M3geGtafIPd1eTmbMbEipwkXYmDqUUhgFHasZ1/4BmyQKIYQQu4EEPfuMNZoL+iWVC7iVMZPao4HF0tLNDbk1HB9MaWrPQmHoLXYY1QG0ophUrI4SoxDaQaLek+mCAx1D0poQE73CckE/p7Bt1dZG5TnQyTgoeTtCCCF2OQl69hEfYjtbq/a4AGVmCSGilKLINEVmqX2g9pGQFEtFxsF+Tpl7Kt92fNZKU1hNZjSPW+pyeLGDMRprFBctlBxZLLhwqYs1GhcitQv0C8tSN3+s374QQgjxgCTo2SfmnZgnDq0U3dxgVIECNiYN921MKWxDmVkWc8uoNMTUzu/yKdHLc3KtWepmDKaOlBSXH+pw8UKHSUwslpaLlrssdbau5rSrP+2wUk0pfXiEEELsYhL07BOVC0zqAAp8TKyMalYnDRvThpODmsp5OlbTKSzWaNZGFScHNSmldhJ6iqxNGiY+4mqHNoo77itY6uUcWi65+qIlDvULIGtXlGrHxsSzMW07ObdbXIWUpwshhNi1JOjZB0JMTFyYbzdtTByj2jGqPcOxY1o7xt7TeEXjIwnwIdCkxOqwookBH8ATcE0EFF2jCSniYiDUkZODity0KzvaaEaVY9oEQNEvLMZoNiqHCxEfE8vdXAIfIYQQu4oEPftAnA3zrH1g2ngCEaXAxUjS0O/mhEkikUC1g0BRmkPdnOADq5OIR2FTIi8tvSJjsVvQMe0oiyI31CExqgP3Dqb08nYsRZ61QU1hNN0io/aBkBLj2lNmhr4EPUIIIXYRCXr2Aa0UCaiaSOPbpoTORxoX0CTq2CYu1y7iTDuINKbIpPEko1nqFTgfcd4QU0JphVagtCLEhEKTUmJUO2KIxE6iyDOMgdxoctuWqVutaUJoh5m6QCe3kuMjhBBi15CgZx8wWlFaTUoRnwKRRABibDsyNy7iXUCnhEGhUsK7QHSRRCKmiDXt/SG151NIJB3JrUHrhDXtNPVpCFivMVbRL3KKzM63sXQ7fQJmK09x9nxCCCHEbiBBzz7RzS29MmNUtQNECW075tpHXIgY246DUGq2HeYNeaFwPhLb5s2gFCklEoo6REprMEZhlCY3hoU8wxhFv8jolRnlKSs5Md3/OFortJKARwghxO4hQc8+UWSGxy2WeB84MaqZukjQChL0Mo1P7TBSgBBBE9sBpJlmkNpVGZUA1QY9SoO1bU6PMQpr2m7Ny92MbmEBhQ8Ro+/vwOxjRANWKzrnUL4eZqtCWqk9ux22H96DEELsdxL07CP9MuPIgS7WGDq2oZtpViYNJzdqYgjMUpnxcfbhrBQa1Y6eiAlFIHlwwVM1irFRFDZDJY1VsFAajiyVWKMZTD3TxuNjxGqNj3E+i6tX2LMaR+FDpHKBOrRzwZRqk6LLzJxW+RViwoVI49uJ8Nku6Qt0Lu9BCCHEY0uCnn3EGs2BTttZucg15diAguA9Lhg0Cu8jykBMCmvbbahlH1Eo6sYzaRyDxkFqS9EXypzlbsEF/ZwL+yXL3aINOKxhdaoYTj2jxlEYzVKZnXWfntoF1iYNPia6uSW3ipig8hEXEwuzfkKbXabvG9cc36hYG9cooJdnXLhYcNFiyXKveEwCDB8iw9oTYhuEacWO70EIIcTuIEHPPmONZrlXsNDJuXgpEuIio8qxPmmYukBVB5glOusE2ioOdgsWOhkuJKrGzx5HMZh6TgynDCvPvaOatann4MaUCxdKDi+WXLLUJS21qzBGKzKj5ysvmyszIaZtWz9aQe0DJ4Y1w8qRW03HGvpFm5NUZobKBSoXKIG1WXPFExtTxj5QWotW7Vba8Y2KqfNcGhMXLJSPeoBRuUCIaduqllFg9P3vQcr2hRBi95CgZ58yWs3zbVKM3Dtoh4gudnO0SkybyNqkAg9LRUaKbYXWci+j8YG71qd8+94RJwdTEoncKnrWMqkctQu4GLlwscNyN9/2ob+5MrM6bVgZNhwfTJjUgRgThTXkmSLXmjI3HOgV5NbQhMjqbNVnqZuTGU09C5gmdWDceFxKLJYZuW2DiMZHSAkXEqvjhn6ZPaoBRoiJOkSyMzzn5nvozALCne53Ic6vfay36YQQ4nwgQc95YFQHMqPpZJbB1LE2aRjWjkkdqBrHiUHDkaWCTmYxGjamjnvWK0ZNg9WaygWGLjJWDZn1TJwnzzS9ItvWhNCHyMlxzX2DivWxY3VSM5x6Gu+xWhFiZH0SKXPDQsjpzKa+d/N2EOqkaZsadguLD4k6BmoXqZs2b8huCQyM1rgQMCimLjCu/aPaFyimREptmf5OtIKUOK1sf+tQ2HrWMqBjDQullfEdQgjxCJOgZx/ZuqW0qfGRk6MaEgzqhmHlcTGikqIwmqgNa5OawmrqLOBDZOwDg7rB+0TlE4GEUpBot6pWRg5zfMxyt6BbWDq5JaXE3esTvrc6YjD23D2YMqoaFFBklogmhkBpLForah8YTj0KRa/M2nwj2qaGmTVtynWCGAOB0PYA2hJgbPYEatsytmX5j2ZfIK02y//bLa1TxVlS89ay/a1DYTcDphBhddywNq65YKHkwsVHf5tOCCHOFxL07ANbt5TWx461SY1r2gBm2gROjCqsgpQUdjZ0tMgMRW7QBupxwGhwqQ061sY13kcmtWfsAkvdvO3hExPGaCCxNm04MZxyeKFkUjtODCvuuG/EoPKsTxs2Rg1h1t25sG2zwsHU4/KEtTmTpl0xWqgzFoqcwmqM1WSzgKibGWoV0dpgMLh0f3UU3N8TKKEAPUskfvS2iIxug8bKby/b3+RCpLTbt60qFxhMHJPGMXWRlVHDelUTfcJozdL6hCcdXuDyQz3K/KH9v6ZsnwkhxOke1D8pV1ZW+OAHP8jLXvYyrr76anq9HkVRcOmll/LiF7+Yj33sY2f1OIPBgLe97W38g3/wDzh8+PD8MZ773Ofym7/5m6yvr5/x3uFwyG/+5m/ytKc9jX6/z9LSEj/2Yz/GO9/5TpqmeTBva0/aXD04MaxZHzWMK0ftI4MqcHyj5p7BlLWJY7VyrNeOk6OaY2tj7l6b0DhP4xKFtTQBTgwrjo9qVqaOe4cVJ8cNE+dpfML7NiCa1I7g29lew2mg9pHViWNt3GCsprAK7yGpNhnaKAgpYbXCGsWkCayNGurGkXwiBbAKGh9YG9Wsjms00Css3cxQZJoib0vi/ZYVrBAjxETjPRoek/L1zefcTGhOqV0Jq1xou2RvyXUKMTGsXBsQTjwrw5rBtEFFRW41msRw6rjj5JhjK+N5Qvm58iGyPq65a33CnasTjq22v+v1cY2fBUFCCHG+Uiml9IMv2y7LMry//y/lsiwxxjAej+c/e/7zn89HP/pRut3ujo/xuc99jpe+9KUcP34cgDzP6Xa72wKdr371q/zoj/7oafd+73vf4yd+4ie44447AOh2u4QQqOsagGc84xl89rOf5cCBA+f0vgaDAUtLS2xsbLC4uHhO9z5WRpVjZdwwrtu+ORPnqZrA1AXuG9Q475k2gWHl6Oa23RNK4GLiUK+dhN7JLHUIjCpHoTUuBu7dmHL3Ro0xcHihS6bb3JnGR4zRXLhQcslylx+5fHmWXxMZTh33DSvWxhWj2lOFSIqKIldkSlO7wPq4Jik41C85stSlXxoWyhwXIi4EDvUKnnzhAsu9Yh7Qba3eylQ7B2zcOGoXKTLNkaUORxY7Z10u/3DYXEmpZ1VaMbVNGbVRO/bpcSFy7OSIe4cVIUTWpw3OQ69sV3QaH2hcYLGbcbCbc+mBPkeWO2d87p0aIW7dPtNKzZO+ax9JKbHUyVju5rJ9JoTYd8728/tB/e3nvefaa6/ld3/3d/nud7/LdDplNBpx++238+pXvxqAT3ziE7z2ta/d8f6//uu/5gUveAHHjx/np37qp/jyl79MVVWsra0xHo/5z//5P/Nrv/ZrLC0t7fjcL3rRi7jjjjt43OMex1/91V8xHo+ZTCZ8+MMfZmFhga9+9au87GUvezBvbU8JMTFxAR/S7EO4HTYaYsLPmvglBbk1ZFnGJERcggAoFPeNG2oXmTjPoHJYo3EpoYxth4gmRdUEjm9MuHcwYVDV6BTJdSIziSYFRlVDjBGjocw1aDBGk1kzG4QacE1kXHtSShiribQBgkoRqxQa2uBlVg3mZmXum32HHrdUcvGhLotlxsQ5VsY10zpyoFvw5MN9Ll7ukpRiZdywPmke0RWNzZWU762O+MbdG3z1zlX+/t4B318ZsT6uYVbCfmpgkRLUIeFcYlIFKpco7P3XtGs9CpJCK82gcm2F2inPPdpcLZq2/zuq3Pz9Vi4wqQPWaLqzHkHWaHqz43HtqVx4xP7bCCHEbvegVno+97nP8dznPveM51/3utfxvve9D4Bjx45x2WWXzc9NJhOe9rSncdttt/GGN7yB//v//r/P6bk/8IEP8Iu/+IsA/M3f/A0//uM/vu38n/zJn/BzP/dzAHzmM5/hH/2jf3TWj73XVnpciKyMakaVa3Nkas/UB6YuMqk8q5Ma7wNlZnEhMawdzoV2PhaJ3Gq6Zc5g4ugWbcPBGGDq2w/P+wZTRnVD7SNKKUpjWOxlXHagR7+0HOiVHCgzeh1LnlliSty9NuG+4ZSmiYxqz7B2NN5TGMNSv2BaeTJjOLycs9wtyYxhqbQcXmiTon1MdDLDwX6xrRx8c2VlbdxwclSRac1iNz8tZ4aUONQv6JfZw/7fe3MlZW3UBhuVC6BUu3iW2iBmocw42C9OW1GZNp6/Pz7g2OqYcRWoGs9CJ6PILMaodqXHRy7oFRzsZeSZ4fJD/XZ1jjM3QnQhYrSimxk2Ksdw2gaXahbsbq72hJiofaBfWA72CsnxEULsK2f7+f2gsiUfKOABePWrXz0Per7yla9sC3r+8A//kNtuu40jR47w9re//Zyf+w/+4A/mr+HUgAfgJS95Cb/2a7/G7bffzoc+9KFzCnr2Gj0b7KmURikNql0v0MT2QzGCB5RS9EpLmVvqxuNiQqVE4wPDcYPzEZVpCq3JC0PWaGofuWixQG1EfHB0Sks/sxijOTGacHKiGVWOu01b5m405FZTucjqqE2ERism3qFRFIVBaVjsWo4sdefJumpWBdXv5ACkGHYcVtp+SLdzwBbKnNyenpybGU3t2629R6J8fXMlxcd2nEeRGwrb5u3Uvg24QkqMa39aKf+4CRhtWCxyps2k3XKaNGgcJtNYrekWhjzTRNrVHqv1tud+oEaIg2nDycGUuwc1PrQrfB1jWO5aDvQKiqyta4uzrbFHq8ptr5DZaY89+R2IR8MjUr1VluX8OITty+kf+tCHAPiZn/mZbdedjclkwl//9V8Dbc7QTpRS/JN/8k9473vfy6c//elzevy9ZvNf+NMmzDoiK1wEE9oqLRRUdaA0msxq+oVlsWNwITGqHHVMdLqaXoB+x9IrcrqZpVe2TfeqxhGTRpmKxU6G1orRtGFl7OhkhsXCokIgJXBR4UOgl1u61tAohY+RC3sl5WweVzfL6HYMuTbEBIVtE4GbWbl5iAkNZxxWGlNbBQZpx/44m2Xsj8QH+6lbiTFBvmUlx2pNE9qAyMe0LfDa3FI61M+pvWdlYtiYjlibthnfnVxzsJ9zkS7boa9sb8T4gxohKgX3DmvuXa+oQ2ShyLBGUfnEiVFN7SOHF0qUVqcFlOf7B43MTnvsye9APJoekT9Rn//85+fHT3va0+bHdV3zla98BYBnPetZHDt2jNe85jVcdtll5HnORRddxIte9CL+w3/4Dzs+7je/+U1ibPMXrrnmmjM+/+a5e++9l9XV1Yf6dna1tpmfaXNkAGI7++nkoGYwrjm5PuW79464a3XMynBCConlMufKixa45uJl/n9PupBrn3yYKw4v8qQLFzi0kGOMJqTIiUlNEwP9TsZCaSmMgQSZNRzuFeQ2o/KQdJs3ks/ydXqdjLIwFFbTyUz7F5nWHFnucMUFC/TKjJVJw4lBxbTxhNiW1vsQH3BY6ebKFrRzuk61Wca+00rRQ7UZcKUUSSmiTgm8NgMuUoLZtZuBXB0iChg3jns3Ku5drRhMA8F7ct2uzDUusDKquWd9Sq4Vy91s23M/UCPExsW2EkyrdqUogbWaftk2fhzXfl4VtxlQ/qD8oPPB5pZh5dvml4VtV9cq3/78fPpv8ViR34F4tD3sKz3r6+vcdNNNAFx33XVceeWV83N33HHHvJx8M6dnOByS5zm9Xo8TJ05wyy23cMstt/CLv/iL/N7v/R5qy4fX3XffPT++5JJLzvgatp67++67OXjw4I7X1XU9r/iCdk9wr9k6ZNRaRUjgJjUpJbp5xqUHNNXsQ7eJiSYE+h3LBd2cQRM40CtAw30bbcVV3QQ2pjXTJhKjopMpQoQTgynDqu0vo4BVrZj4SCe3aKWwBorZsdGKI4vdNr+o8RgFRWYIqV1ZONQvyDPNcOpYHSeWuxml1T+wK/HWla02l2V7cORCfMCVoofi1K3ERNrWmHAz4EK1XRQ3A6+Y2qTyifOcHNZoBf2u5SJdUlcZo6YhJk2eaRY6Gb2OoVtm2/r0PFAjxBATJ0cVxwcVpTFUPjJxDd2x4WDPYoyhDomNiePi5S5lZmRQ6ozMTnvsye9APNoe1qAnxsjLX/5y7rnnHsqy5N3vfve282tra/Pjt771rSwvL/ORj3yEn/zJnyTLMo4dO8bRo0f5yEc+wvvf/36e+tSn8iu/8ivze4bD4fz4TKXwp57bes+pbrrpJn7rt37rnN7jbrR1yOhiUdMvLNMDnqTaLaQY07yk3WpNL7f0ypwsa0utu9ZyeKnk3o2K1VFNjO2KzYULBbnW3LkyYXXsaEIkM21Q0cTIZFhxoFfQzw1aa4qUSKqdsdXNDIOqxrtIpzAEF9iYNBgFvY6ll1uUgq41XHqwR7/MzipQ2VzZGkwck5h2LMs+00rRQ3HqVuLm0NPNwMvHNuCyWmG1uj/wim1X7I2px3soTPv6l8o+WsO49u024GLZbgHmlsYnGh/n7+2BGiFOG8f316Y0LnC4V7CoFaPKszZpuHvgOdDNyY1isWPpl20wM6rco/ZBs1ubJD7U2WnioZPfgXgsPKxBzy/90i9xyy23APCe97yHpz/96dvOb25NbR5/4AMf4MUvfvH8Z5dffjkf/vCH+fa3v81//a//lX/7b/8t//v//r9j7SPTOPrNb37ztqBqMBhsS7reiyJt1U5i+4dMv8xofKT2nk5myTNNmWvWJ20Vklaafm65YKFksfBYkyi0ZuQCxiiWezm189Q+YoyltJqg25UBH9u+yFVsl6iTUty1UXHv+pQE5JXGGFioA4HEeuVZLg0LvXb+VoJ5Q78f9MG4dWVrUHsmTQCVKI1hoczOqk/Pg81j2Qy4ahfa99u0s8G2Vm8ZpU7bovMp4VxA6YSnzVuwGWitKZMB2oToTLeJ6DFFfIzkW3afy8zgZo0Pt67O3DdsMCSWeyVKazKrONDPWepmrI8dHatZ7OX0ckuZ2Uftg2a3zxh7sLPTxMNHfgfisfCwRRNHjx6dr+y8613v4lWvetVp1ywsLMyPf+iHfmhbwLNJa83Ro0d5+ctfzsrKCrfeeiv/4//4P552/2QyOeNr2Xpu6z2nKoqCoijO/Kb2mK15Jzsl+2ZG4YIixYjzkQO9nAhM6kDtAtPaAYmFXka3axmMm3Zul6IdRUFO7RzOt4FV1JpJE5jUHq3aPkG5sUxr3w4rtYoQEpWPFGiUTuS6DWqOj2oGU8fJ3JKtTMi0ojubvL7ZZFApNZ8ltjUgskaz0Mkpc3vauQfiQ2QwdUxdaJv3ZfqcEiZP3UpcHzuGdYNB0csty938tAaJMSUy03ajbjyo2L5G70HpiFFtQ0MfEt4mMvRplVubz71Q2G0Jny60k+YvPthjbVxT+0A2+weC1orFTsawcXR9YHGxQ27bJpKP9AfNqU0Su/lmhVtkZdzgY3rMmyQ+mNlp4uElvwPxWHhYgp43velNvPOd7wTg5ptv5o1vfOOO123NtbnqqqvO+HhXX331/Ph73/vePOi5+OKL5z+/6667TltJ2npu09Z79ruteSecknMSYpr38smMQs2SjxdyS8cahrWn9hm4wFKZkWeaXGnuXq/p5gYfEk2M5NZSOce4cXQyS5FFxrWjdgFSpJcHQmyrkkqjGXlHN89Y7LaBQBMiGTCcOIbARYu6DXBoc0pWxzWVC+RZM+u8HKh9W4G2UGYsdCwKhU8JhcLMOiDbWYLzmYymDXevT9mo2xJ6axQda+kWhl6ZnXUey9atxIuX7h/uulPgFTabLOo2SFPKUYd2lWg6676caUWgHWFhVdsue2vl1qnP3Teazmylalx7jJkFFang3o0pGxNHNzMoBdMmMK48lyyV88ToR+ODZmuTxK0rXtZoKhdOK+l/LDyY2Wni4SW/A/FYeMhBz4033sjNN98MwNvf/nZuuOGGM1578OBBLrnkkm1ByU629kvcmsj81Kc+Fa01MUa+9rWvnbFs/Wtf+xoAR44cOWMS8360Ne+k9u2/uI02hJgY146JC1gN3dzQLywuJqKPLMwmpZdWszZx87lRy304slgSQmDkHH7aNr27oFeAaiewV5VhoVAcWe4yrNscoZhoc3u0YqnbTg1PKTFpAirUs47Q0CksWdYGX2Y2aDSEyNqkofGBwpq2cqzI8DFy36jm+6tjitxwoNsmQ2dBUxGYzIK1Yod8nqrx3LE2YVw5ljsFRrfTzSfO49Is30Src/oQNlrN/6Le3C7bdGoJ7tQFRtMGqzUX9EtSgrVpQ9UEvIaUIibLiCmxUOTbKrd2klJqV+ZcYDzrG9SxhiNLHTamDeMmtFnVCi5aKrn8YH+eGP1If9BslvanxI6B2yPdS+lcnGnLcLPh4yORGya2k9+BeLQ9pKDn6NGj8xWet7/97dx4440/8J7nPe95fPCDH+Sb3/zmGa/5xje+MT++4oor5sfdbpdnP/vZfOELX+CTn/zkjs+XUuJTn/rU/LnON1vzTqZNmyTb+MikCVjdzmPqFdl8TME8cbXMWCgzfErbkoQvWi5Zrxs6wXLFQcugatpgKcLJ0RSjDFddssxlyz1ODGs2xg0H+zmVD4ymnpgS6+OG48MpjYto0/5F9vhDHXq5Jdd6FuQqrNaMakfdzAKRDMqs/WC0UXNyVFM1YVYl1gbE61M3y8+Bugkc6OWnbVetjBomledgr5x/yGoNmc0ZTBsmdaBTnHseiw/teI3Kh/mqk4F2MOpsRWXaeIaV58SwZjD1WJUwGuJsCwgSFy6ULHVzLj7Q4VC/eMAJ61srr7q55UA3Z1A1eKPJreayg7358NNJE7igX7DYzU/7M/JIfdA8lr2UztVOW4ZKQWmlR8yjRX4H4tH2oP9EbQ14br755rMKeAB+4Rd+AYDvfOc7/Pmf//lp52OM85WjSy65hGc+85nbzr/yla8E2lEYX/rSl067/yMf+Qi33XYbAK94xSvO7s3sI5t5JxcuFCx0M1xIrE/bqqnF0nJ49gG7+ZfJZuLq1llXh3o5ZjYRPbOaC7sFxigGlcf5RKYUiYjVhkuWu/SzdmXiQCcjs20zvq61VN5xz0bNyHkO9nIuWipZyC2ZVoymkY1pRUxxvpqXUsK5SAjtV/Tt1tmk8ayMKqZNmCdpD2vP2qimcp7aBTRtwvC4Cdv6ezQ+MqwdZWZ2/BDu/P/Z+5Mey7I0rxv9rXbvfRpr3D0iMrKhqt6Xe690GTFFqgFixoQBTBgxgWENkIoxTJi9QuILICGmwDdASHyBVyq44l7RVFZ20XhjZqfZzWrv4FnbzNzDPbqMzMqK9CVZhqc15+yzm7We9X/+jbNMKRFjeQ2t+aoxh8SndxOfH2fOS2aKIvd/eRbfG2ip6qO06fa9YwyB//H8yH//5MgnNxMbq/nJ5cAfPd3y//r4gh892X5pwQOvS3yNVlxtRa22oichJqzRpFLZ9vatqNG60PRW0uuXJMTp3upfW67+l+ml9G2GNdJevRq8BLIOv73Q2vdDxvtr8H78Nse3Qnoec3j+1b/6V/zTf/pPv/bf/vEf/zH/4B/8A/79v//3/ON//I/JOfP3/t7fw1rLz372M/7ZP/tn/Nmf/RkA//Jf/kv0G4TOf/SP/hH/+l//a/7rf/2v/P2///f5t//23/J3/s7foZTCf/gP/4F/8k/+CSCOzd/nCIovG495Jx/sEi+OHmcUppGIU6koVe+l14+JqytJ2FkjC3nU1AIvp4Vf3J1Z5sx+49h4KaD+8NmGguI8RwD2nWMplXNMxKwoVdRinbdoKkNnyRXmmMgneLrN7GPGW00uldDk9eJro1DmTMoNldCavjPsZiNoCgpnDQpF7zUf7Tv2nZCbV9l1KrJ7dEa9lcditCAzuam53jUeS68V8OIcGIMgZPc+N63l5I3mMEbmVIR/lDMvTzNKaZ5uO4xSEskxOPaDZ2pcps1XFBxvU171zvLhRc/dGDnOiZs5conionfif/SOIupNftC3dWR+Uwn3l+ml9OsMo9V7hdBf8nh/Dd6P38b4xoGjP/vZz/iDP/gDQJRWH3zwwZf+/p/+6Z/yp3/6p69973w+83f/7t/lv/yX/wKIimqz2bzm4/PP//k/51/8i3/x1tf86U9/yt/+23+bn/70p4C0vUopzPMMwN/8m3+T//Sf/hPX19ff5KP9lQsc/aqRcuFuDPzFzZnzJLwciyxKV1vP1UYUUCi4Gjy11nuZ8bhkXp1nDlMgJmlZxFwYU2ReMkYpnNbsBi+FSkiULOGmWmvupoWcCt4Jd0hRGZpj8xwyU8p4o/jx9Z5nFx3eSNHz+WHkbgqc5oI3YIxEVoScyTETS0Wj6L3hR9cbtr0jpMwcC5dbxx893YnSrH2mXCo/eyWFU631C5yfmAqHJfB/PtvzZPdFJd/bpNcxFpaYeHYx3L9eLpU5JD47zHRGEUqld+JFdDsuvDwtLFHMIQdnqCg2LaDUe3Gu/j8+2DeV3NtHzIW7KdJZ/RrXbR1LzJzmyK5/8CpaCdbr38N345fzZdEBwGvqrbd5Kf1lq7fej/fj/fh+jd9Y4OibXjufffbZl/7+6XT6wve22y3/+T//Z/7Nv/k3/Lt/9+/4b//tv3E8HvnRj37EH//xH/Mnf/In/K2/9bfe+Zp/+Id/yJ/92Z/xf/1f/xf/8T/+R/78z/8c5xx/42/8Df7hP/yH/Mmf/Anev3vx+H0Y94ngZ+GznOaEb6GTY8gsaeI0Ry4Gz8dXA7XW1xaqWivjkjgtGarkeimlmJbMz16euRkjISesNlw3ROG692gUcwx8fjfhnOap0ey9QRtDzZUlS4uqNFXTeV7wRqTpSxIDwxfHiTFkjNGSB9YZ5pA4zoElCtdoWxwf5oJWit47QlmwSkiyMWWsNZQqv7vzlpsxCFk6iqpoRWdup4XrwXMxuHs0JyRpdSkkcf48p3vpdS7CURpDop8DFUdIVY77tPD8NEkLUGuebBwbbwgFxlC4Pc/MKWO1JeeCt4rrrefprmdxmefHhd6Zt5Kx4cuVVxJqmng5Bl4cF45BCtyVx2VaQRJLxQLbzrLtHc5qukdp7F/33voqR+df10vp/Xg/3o/34zcxvjHS830e3yek5zRHXp4D5yVxmoUXo5QipMzdHIkhY6zmeuP4f350weXguZsTIEjAr25GXp3FoyenyovzzPO7mXPMjCFwew6UqphCBDQXO8uzbcf10JFr4TRntl7zg/3Ah1dbUi78+YsTxylwCom7cSGXynYwOKWZYuFuCsxzJJSKMZqNlxaN0UqQpFLZdY5d5+h7y1Vv+WC/wRlxQN4P7t7V+GJwPNl2EvgZEp8cZuaQKUgIbq5CfO47w0+uBozWPD8vfHY3c3NeUIBG4azmg73ng4uNcGWauuzYeDveaJZUOS3iWD2HzHEOUCudtTitqaryye3EcUlsnKHzmtsxAYVdZ/nBRc9HFxueXfR8dDlw/SUoyGmWttlr5oe5cDsFPrudiKmgtcSRTCFxOwbmRQjLm16jUbyaEucpsOksP7ga+Gjf8+FF/5Uk6i87hnXMMdNb4WjAr+/I/PseiPpXffyuOnK/H9+/8RtDet6P3/3xZiK4Uopt57g5i4IoV9BW2lxGaT65mzgvgsBsOkvKhSlltJbsKO8M55i4mWY659h6zzhLq8cqz3GJvLwLLEthHBLaGLyBV+dIqpWlimfM3ZSgVJYlcI6ZGDNjTFx4A0qxxETIYIzGIMXKp3OgojFW44xi31WcVbgK5zljzcwPLjZcb6WlRa2EmLFbfz/B9t7ywdbzWV64mQJLyCw5s/OOvbe8HCPHUQq5pVZ6a6FW7uZAGAu5FIzWPNn1KKVa1pji1TmKCsxoae9YQ2ctBaBCqYVQMscxcpgXalFEq3n+amSKma13zDEyx8pm8PzYm3tu0Lvk829TXo0h8/K4EGvBWOFuqSzFwhQSc0zcTZVwlxmMxXstZok5c3ta8Fr8k+aY+dH15ksLn7fxih4XJm86Oj+W9n+T8WXts8emlfB2j6T34zc/1oJmvQ7rMC38eAqJ20kc3xWw8fYrHbnfF7nvx296vC96vofjbYnguYrHzq53OA2hFAYrSegxZn41Tex6w9Pay8IWMiGvSqrE4RzJWXEuooxacqLEylgKOWXOQVAiazQ/2Dt2nWXOhed3My9vZzaDpLR/fgyMqbB1DuMc55Q5RqAmpiDtqqogxMScKyjN4GArGnUxMDwGNr14Cy2xUKq0o2TulRDUTXNrjrkI12VJOKu53njOOrDF0lnDnAspFW7mwDkmroaOoZNQzikXIJFK5XaMIp83mqUZ7B3mQC1w0TuUVhglXkOX3qINPD8v3J0jz48zBYi5cribiLmy8QZtFDllxqi4GQNTSFxvui+NgXhT4nteEj9/OXKcAqVUlFFYpSiNfH6OhTFElljorOFmWdhrzw8vN2QqOQt5/NIojkvkdoz84EuKnsfRAWthEvPDQmW1kLR/XUfnt7XPTiHx4ryQUuEwy/m3fNHJ+33r7Dc7Vp7bqylwe47cjAsxZGqtDN6y7eT+ibngnL7ntt1NodlofNGR+8uK3O/D9VzFFbFtGPrfMSL/79N4X/R8D8ebieCplEbChc4qKhJFcVoSL86Z4xJ5dVyIubLrLJ1VnObMUhI5wTgnPjlLqKWqVX4WE9ZALNIGirnSWZGfZ2DoHZtaKbnw4jQT58g4T5yWwuAMkUxUEFNmCZk5CUl5sKIWmxIsOeN0JWvFaakEYF8qF4Nih+Kjncd7y90Y0apxaHYdVxvHFNL9pPzzmzPnMaCNIaZMaa7HW+8IpdApRUKhUM1jhnvUYo6QkiSZx1x4tu9bsZUwyrCUyGkWZ2StNapWiqqMoXA4B+YlivdQyvSNlG2txlqDQeG8xWpNpzUpldf4SO8qGqzR9MDdvPC/Pj/y08/PHJZIahNqZzRKKyoVrxWnIqKDORdSFmn/EjPOabzRhJQJqTJ4zWGOPEndOzk+K68o5MIU8n1h4pS6R6kKlevBw7e0+3lb8nYtsig+buF5Y15z8o65/E5EXHyfxz1X8BQ4zZHzInl8U5BIFKXEH+xuTgze8vHVQGc0BUXMYiJqx/CaI/fX4Yj9Vb2eKRdux4VPDyJmCLnglWLfWz666F8TQ7wfv53xvuj5Ho43E8EVlTlLMKZSovIZl8Q4SyyF1eJIPM6Bm5Rb+yCxpEpOmdtx4ThGlJItWKgZRaVULe7GMRNLoVZHSfKQ985Iqre1bL04KvfeYU2mAimLed4YhF9krURJGCO5VMYoTFGElIi5sHEG7wzWarwx5Fw4hcwPOk/V0tKqynI5OFKpvDovjVw88+qwcA6JOUzkWtkPjl7rFgdReFUKnTFsO0MqhVyafFbBzXlmibKY7jtHyJUPdh5QdKYy2I5YCiVXrIaQYJozY4yCrG09u1jwpdBpBVrJ77a0de80VMWu82itOYfMhdFfKp9PufDpceK///LAi2PAGMXeG85RiRniFKhKcTk4vNakyn0BlWpGF1hyQRuNd822oBQ0+q1hp2/eW53RvDwHgC8ktWut0M3o8PFk/nXbFu8KRF2jLZRWLKWw92KbALCkDFXQzN+FiIvv81ivQyqVCtDmDtf8oEoRFKjUSu80U0iA3O+1VvGEyoWhIbVGK85L4rwk4ccVmR+gtc9Sppb6VmXl7/pIufD8PPPLlyPnJaG1xip4NQd+eTvys1dnfvJkyx883XK17f7KFnZ/1cb7oud7Oh47M2slbZQ5ymJyDonzIjEIAEpZlC5sh46QcuOwKKyCUEBrw+AKhzmjFBg0xgup9zAG7kbZmU3LyKvTwtA5jmPi6c5TauYwZqqq1CqmhmQoVJaUmUNqCdwOg7gsO6cgS1hpLRWlNAXDxjs+2PVcbSy1wmEOqAr7jadqzRQrL47Lo3ZIJKaKMYbdoO7zqjSKqkSNZo0iFmmnea1wxggvKFVejZEpSAtIa9h6w81p4Rc3ZzZO/IxqrRilmGtinCvnOXBzXoRv5DSDNxgKXisuNl1zmtVYI0XcEhJKwbYT88Q3+UhvjpQLn91N/PdPbvnZqxnB7QTJ2fcGqBynROfEj6kW4VMYK8VIxZKBXGRnbrUW92gtu/G3hZ2+OVyzFwBZmN50dB6cvW/R1XbOH7tWf1nb4m3J29IaEO5IyfJz86gotFoTsizEqdTfiYiL7+N4kysYc6W0xBOn5U5cUmYMFasVIVdikXbW9fZBIXhaInfnwM5bQir3lhp3SyDGzBILmdpCiB27wfJ/PN3x4UX/tYj2vytjjpmbUxQqQWcJsTDnwsZahp0hxszLU5DzotR7hPK3NP7q3EHvxzcabyaCL7lwvBsJIUuxU2Sx9lYzF9l9Oa0x3nI3BZYEY4zMKYt822isTa0v3Sa+VFkCrC4GS4BKBipnr7neGW6nyO20UCtsfMeus+AUIWZ0FFRFa03OFWs1RgkpWBwFFYNzKCN8nm1viCHwvBRUFUfoy6Hy8UXPxcZjteI0p9YeKeQK1iiUKqQoSJc2miUV5ljovEUphVEaTOV2jmQkQf52nLk9zex7iwLxB9Ki1Io543rHD/YdqSp+eTvyixcjuVZCrkwxYZXCUKml0DnHOWbupkgdLIM3aCWGjkopLnuLNXKNVj7S20bKhRfnhZ+/GhmXQm+a87GCeclUMgZFVYVxkmPovcEpRYqZfWcZS2JeEruWt1Zqpbcab7887PTx0Fqx9bJzD+X16ICucRWWVBiXyF1LtRdjTHBZE/W72xZvk+WXWslF+Gm5FXmPC5o12oJaofHZvg2n6E3Lgseo1Huy9Be5gtRCoRl2rtcA7gvxpZHtbafvY2OMVlhtCLny+XFhiZnbUySmTIiV27NYU3ijuBoc2SRO58rP9EjM5SuJ9r8rI5fKcUlMIaORTYLMCxrvNLlAQaG0Etf4Kf7GEcp3Ec/h9+v+/t2/e96Pbz0eOzN/uOv5+HLgf3564JfLTEiVl+NCzaLk2np7T0Tdews5sijhTTgLOxTbzvD53chpiowSG4XvNT/aG0JKpKowRjNYQ1WKV1MmpsISCkuuLGkmFCc7vJhZcoQSiQVuYuJy43FFkXK5J9hqY7i2ui2yhTEbfE33i2WuijEWTBCicWlRFHfTwkXnMNawpMLL03KfQxaaI/K2N6gK1MK4ZD69nSgUrIGbSbx5LlukxwfbDmsUndN8fDUQcqEqIUYvOfMXL46kLIGpRsHFxrPvLM7KcQ9zYsmZUsRPZ9+LrNsbxdA5FApv5PXe1eOfY+YwRlIRryFrNaVUOmuopTJHUbZddp6XaWJMmQ/2PRcby2mK3C0ZIdokVK2CbjmNVZaUK1fDV4edghQm0o7UDPCFAiGXSq6FV6OQyB+7VsdchOoT81tDXt8WiCqvLfy0WsCb180Z12gLWXnVN464WGLmMAdeNdL5i+Ms3KQCvddc9I4nG/+dk6X/qiiVHheDS5QNwxQKqUAtUizXdg2MVnQNwSlJingBE+XzxVzQWq7haYmAFLkosEYQycEbBm+wzlCVQVvhqX0dov3vyii1SpROETQ9pXKPiIEU6lLUS9GY6m8OoXwX8TytyGyz+Ph9EQP87t8978evPdYQyc5rMeYzharAG0syBaUgU9G1ElMh1UpCEBirRREEUGPFWcduyCgSaMX10NF7wxItY0jkAqoWQkOErFYoVVEUljkxzoEXSqGV7DZKARR4A7vOUKsiNlLtvhFyd72XhVIp9oMT/otRTDGRUuHzu4lSe66edoRUqLlymALTkuha0YNSVDTeQYyVz+4WYixc9RalNHPOdF4xpcrnxygRE17jt56NN1QFxznxwX7AGYHucxaHYYPmh1dbXh1nzEbM/ja9xRpZtMeQGBwyoTW10X7wdE4mFqUqWit6a8jNFfvN9s8SMy+OC4c5UkqT90cxj7TteFKpDE7x5KLjavBcbBxPdj0f7DtKrdycF54fF3IFZwXh2nnH9dZ/I5+ex4VJ74SQvapTchGS9JoCv/H2XrGyytfnlpX2LpXa22T5ElNScFZWyFQeiqJUJNrCainav27ExboYfH5aeHGYeX5eCKFAhTkUQspMQfhORikqfCdk6b8qSqW3LZaHc+AwBVASEOyswWkYvMMYOf8XneFEI6TnjFXuPgB3WhJbbwgpc5oTIPYDUxAVZk5inZGKtJgrmc6ZFU/6SqL9Nx1voh/fFeKhlWwAtTbyrJYHRAykUM8VtJKgYFFcfvchvO8inp9moRb0Xt8jcN/V/f27Pt4XPb8nY46ZJUhv+Q+e7hiXzO0YOIdCzpklJXItxFQoRSBqbzVZl0Y6LsRK28U5iocxFWKueBTWGi6sZlqyTF6lUDOYzjbvmoztHDXJYlK1lpgKb3i291zvep5te1JrnY2zkLBzLYwpcwiF3kj2Vq2Fcc7EAp9NE8cugao4o7ibI5/cTdycFkouDJ3hcujYec1xSYxzZkqJ05x5cRixBryTXDCFIhWatxGgNM5atDZoNGMM3JxmOmtZSmFrDTdq4dQiOE4h3XOClIL9oEVFVytDr+/NDdFwswTKWOmM5mpwbLcywdycAs/LwuANVw31qbVyWpK4QFuNGSyvZsXxLnOcI85q9r3FUFliRdXE023HT55seLLrcVZTgf3g+aMPL9h3hs7Jo7/6C33TRWQtTO5aSv1hkqDVwxSYc2ZZxJNp2zk2neVy8DzdC+rojJYWYdb3k/ybraXadsoxlXuC+9XGMS2aKUqLdmkty1orndUYpdh29mulxK+KoZsxMi6CwrVLTkjiTeWdeEOhFLFUMer8NcnSf1WUSu9aLGOGUpQQ3inMzarCaDG7vGgRKAaF0dA5KeSPc2KK0vqez4XnhyM3S2DQ+r6gTLnI5sO2oldl9p3BDB5qRVO/kmj/dUYu4l91MwbuRjFuHWP6zuwP1nvZaYV3wjVc2dlrkZtLoTZxR+8s6G+OUH6d8S7ied9pjHF4qzBWNpffxf39V2G8L3p+D8ZKQMwFFKIUcjqTaiWmwCmIh8wSMzHKrlbVikVk79OyMMWCd4qwBKYQOM2ZUuFYhEw7dKbtsCEGKFWUShtnmWJEVUWK4qWjtKGWRNWai85jtENXxWmRCIWpcY5iSuSiGEOkILv9KRduXYRa8Rq00yznyN208MndhDOGVAupZEKDcGMsGKPJtTItkaVAbyFkiLFwWmZ0FfdlZzQbbzFKcZojv7ybJIFei9liJrOEilHwwcXA9VaiNyqVbe8wWhNTYRqbTN1JdMXGW5wyXO48H18OOGN4eZr55e3M/35x4v/+xUvOITEtGQlSVTzbd/zkcsvlRoJJvdUsqXFYiihlUqmYJH49qUgRcNE5tt6yGxxPdl4sCqgM1rB9y6K6TtLftM2yxMhfvBj55HbmvMR7BVsuldtJjuewSADrC7fw4rzw4UXPk40nFuGAbJPlPMd7N+y1taRQXAwSZ/F01/Fk1/GDi4E55nvk4bgEDNKavdr4b7RQnebIzTlwN0eOY+TmFEDqGnLO1Cq2C6UWNMJRCqni7a9Hln6bHN8o7hGwLzOm/G2Ody2W1ztBEKcYiVmQ4SUWYimN8N8S0zeeUiuxFLTSHOeAqvIMjjkRUobVC6vKYjs3tPKi3Yed1WhjOM8JbxXWeHqlUSjmKG2jb4LKrMjVp8eZP39+4rO7mZwETbzaSML7nNS3tj94M6cvlUKKmRQyUzNe9UZIyylnNt5wtXFS7PDdh/C+i3ieWz/YW0NB2pQPX99/McD7ouf3YKwERK0qssEX2bYxEhqqzUw+JrJVXPSewVvZeSVBWUBjYmRaAktVzCGTisizVYU4LmyKY5MKuoLSms6b+4ldR8nuOi2BmGloiMZqc8+zOc6RLht01WhViFRChpSTPKxFfHuUnhkuBobeMieYx4hD4zoN58i2qwxektfnlIkhc1mlhybhl4Zea5Ho54qzCRWEyF1RYAzWGpRRHOfIXzw/8rMXd5RSSTlT0Gw6y5PBNzVY4HLjMWgutp5NZ5lC5jhFDnNijJnLXh4zrSo/uOh5uu+5OQVuzpHb88xPX515eZpYopyLlBIxg3ea/31x4MOLgR9ebdj0jo2zPNv3XA6e+aLg7MxhlkUk18p177jYdWhvCG1BuRgs3n5xQk3N3PAcEjlXjJEC4m2F0Zt/dzMFPj8GSi1cbS3OwO0YmWKCKhPnHBK5Cj9DaXh+mPnly7N8Di8+JZ/eSFE5hsTYfIRiknbocRJismrQewWuNp794Pnh5bdvSSwx82qMpFLRgNLiWr7kTEzCT8mloFr+mry77Mx/HbL0KsfXzVH6zSLzTTfrbzpCkoXWav1rtX++SqVltEZp0FWRirQyE9Ki23WGXWe53nq2neUUBFHunEUricC5mxOhCrJ1d5bNAUgYsLeGmAq7jUdrxRwT4yKCivMmUevAp3eCyK6F/Fe5PMPDPfvisPCLlydOi/gITUo+7zlmnDNcOfOtEI/19dfswo03gMFqTQbUrLkdIzdToDeGZ1vPs72/R1y/LkL5Tca7iOdreqbRUAv39/Tjr99Eq+13Zbwven4PxmOzws5aQsqkUth4y+AMTzaew0VPbzU/uNqw7z23zSH41bTwq5uZ07jw2XEmqQmrttJuGQOZSqqgcmJKon4anKVzHVYLymGdmPmBoncyadZaUbpitaFTomCYQ8YYg0UxZVDasveOSiDkhNGaUpQQprWlU4mbOVN85cJYYizcEYmNg9H3Dh0Km94L0lPgR1c9F51jSpmf303cLtLemEIkV9h6yxLEUTnkzDSHFpgpEn5jK7VrbScFd3MkV8XTrajHVJWwVkUl5sRxSpwni7eLpNpbsRGYc+HVOHN7XghBULhVUxFai22ZEilXXo2Zzw8LH+w7PrwcKBWcNVwMkk32+WGm1sq+9zzZdc1vSOTdN2NA6zVp/WECm0Pi08PEq7N8vtIWy84Znu47frB/tzx4jpljOzaFZg6Jm9PCi3Hh1TFwO81ilaCl4Hl+nLBG+jixeTv90ZONtJJyJaVGwO4t3im88+1IK6kIJ2rb2YdFqHffONriMWl4DOIqvu2EwG2UcGlqFXSxVCGd5rq2tIAkKfEmCBInbcdv9hyGlDlNsV0Gdc/l6RqXRytZc77pYjOHxO0Ypc1ahB+285arFgT8ZedkdQl+XICVWoV4+yUqrdL+NiThU207I62t5sT+8rRQEfTi1Iq8w5R4fpo5jwmLRiFKzSUJwmUWQXQ6IwHD3hl2TtDJkgshVF6cF5SCZ7seawxjyBxvJ3Zd5Mmueyc6sSJXhzkSK+JqHlZiPy3OJdEZg3X6GyMe6+vbxs1ax8UgBeiTjeevf6TbZqB5phlaCO9XF23fZrxpUovSaCq6Xcv7OacJAB5//SZabb8r433R83sw3jQr7Kws2DHJzV+V4umu42rT8dFFL4RKq7g5STGS8pnPjzOf3U2MSyJncNZyuTeNlCkLpjPyoG06I++BxllZMYI3lJCgIQq9M/eS6qEzzEFxTrIAa92IqkqRaiWkzJKb8RkBo8Gqim0xGloralWEWhgqHEKg15bSTAZLgU66bWitQImvC639tXr2qBZCGpO4KqdYMNqy25jWtlF03rD3likWdKw4ZwWatpZQCufjxGFOHEPgPCcGb9j29t4U8pc3I58cRrTW3J4XpphJVXhUVNm9hlTRSKxFLpBi5rwoNr3lPCfuXGhSeoUzlr6zOKN4sulxRjEtmUzlcnBiePjGjlXMDWc+u5ulgDIK69w96vbZ7QSl8uMn27e2wsaY73eQxzny8jhzt2TmpRBS4jwmCjD4whIrp6mQM3RecTF4vDIsDQkqpUp7JEPJhW3n6LyWYicmKFIYWS1TtXf6G8Hub5KGS62Mc2rSeLn/uqjpnGJJYKxlCQGlEA5KrcxLolZ4eZTFYDt4fnTRi9dMbxsp/YtF2GMEKuXCOWRCltaWM+o1Ls+uE/sEpfhGi80aphubUspqaVMc5siUMh+/xdvmsUvw54eZuykQUsEZxUXvuOzdPblVFkFZLCHfq7SWnJmTVH3eGYbO0XWGfe/F4LMhJXIOFJbKnCIhCBpVkfvuya7DWsXtGFClorTwvY5zZVc8zhh6FENvuNpYOi/qwpAKIVfuxsDL5pb+dNfzg4v+C23O9Z5dkVRr9P0xyHXS5NbGHGNikwyYr494rK9fK29F2JwR7pq3hh9sxWTxtxHC+ua875onmclSuYaUpW2oefT1zcQAbxu/KXL4dzXeFz2/J+OxWeEaJuobodQbxb4TN+NVQbJ6/BQEMXg1LYJ4FMnXMW1HIIiRZugsKcPlYPHGMMfEFJK0C5yjixnfYHGLqBa6TlpQ52MSDx0lBNZxjqK20pACVFVRVXYmqcDNOVArfHzV82TnRQaqNaq5R+eqmbUQCVEycd0qMRc8jY7zLP4ZSovzsypFWihFlGtLc5Q1RlyiZdej6TrJEfJW4Z0lxkzOkXOs/MWrE1RxOR6shSoT2pNtz8dXG85zZM4ZbVTL7hLIflx5R1kI3imvhaigEtoolFEsEcapcLSZy0HiJbQSV+nBaG6mwKtzYJplx6+15ifXAz95uhNp/KNi4bwk7saI0grfimCADsOSMjlX7ubI9SI2Ao9HzIVpkXZaTFUQPFVFgWbacTtDr8SIsFYxl/SOe7XPk33fwlZlYgy5sLOSJ6a1xjlNSJL71hmNM/VeYn8cNVeDtGC/arxJGq5VTA6n5j1VpkLfiS/SuDhOKpNTYlkyulamVJlTwhtpi6I0FpjmwP+YIyndUQo4p6U94TUW9QUZ8MXg5P2Q+IElFZTSr3F5lpgxRgtR/ZH0/6sWx9sxElN57Tp5Dd567sbwBYn3Y5fgwxSZc6EWWEJhIt8r1awRc8vOaYzWaAMqC+ejFMmr08g92reFs79voT6YRZ6DKAznjLS0raIGGJtp6r63OKfYeodCFt0xFK4Gx4+fbumtluJmboafynBOMC6TtCRTxhnTuHSZV2P4Ah9nbfOUIk7yvVGM5bGv0PolVgup1G+EeKyvD5W3reurj9RaQMm1/O1ETzye91eFfCyVeRESevYSIVONhmb6+W1bbW+q/X5Xs/HeFz2/J+NxIfNqUhynRMhiYLdtELIzAr8qVe89flKp/LUPdrwYF5yeJTk8QS2VMeV7z5RpjhyWyvG8CL+GwjhLtEGiMofKHMVfx/UWauJ4KhKHkYS/s/egjSIkITxn2WiDlt3CzkIolcFKMaDRXG97lJZ8rMN55uYsCio3aDDyGmGJHOeI0Yrnd5NEP7S8sPMUOaco5OkqipNM4zoVhTJgtXANNk7f785TjcRYWFLFTJnrrcNbQ6dhTCLNVVpxc15Ijbh6XCLnJVOpUjBVSaK+myLjEslZiJ8hF6zSaGOpBTSVVCKvTpnjEnh1mvnoupff0ZppkQLzone45p/jjOIwRT55NXK98XgNV4PHW8MpJIKQIr7gvmy1JpdMSiIx3vX1Hqk4zZGbKfLZnRCXx6bqEZsCUdbUIoVUaWo4bTRGyUTfGwNFkVNl0om5kT1jLEC5jysZgxRedb0HjMZYUZxVZJc/+C/e42+OlTRstcRzxGYceTdG2fGryjY5OmO42sp7T1HQCWsMuSa8FeuBzhkGZ7Fa8WoMzCFhWrDqNEZux4XOSmG5b0gJwDkk3J1Ba8WTnWdopo6P5fhaiRT7etvRO/MFQuy7uCshFU4hvbMAHLy9v9YrAvHYJdg5IRYnq7jaSUsxtAy8bW8xWeJdVvAilso0JwkhLmJ1oVvA7eDlGYHXzSJDLFgjC9+d0W2zRGt7y6ansxrlVbO8KFxsHNvOsB+EWzadFg5T5PPDLB5iWmJanmwd+97TNwl8Zw3O6i/wcdY2j9ZGig1VMLmgjYIiyjyloCLo24p2fF3EY3192t+bN3599ZH6y2gZvWlSi1LEWtCDYev1fYG+ks+/bWHyptpvjvl3NhvvfdHzezQemxXKYisqrNMc+fQ0f6Eqd0ZzN0YMime7TgqUJTbOg7Qc5hj5/JQpuYoD8WCJc+YURD1ljWKwhlQSDpmoz+MiDOgqi8vOVs6ztLDSLAVQaVEEtYI2sNsotp2nr7Kj6q0km8eU2W88OUtLpZbMUit3U0FNEu6pqsIaCQO9WyJDsRgNU8iMIRFCAgVOy+SnKhidMdax6RxOG3GNrqK60EpJMaYUmuZrlCvOVDoju6pMpdfqPgfsetuxbT48MRfmGCWLqCmVOmdZamZJ0gqqqmCszJZzhFoLS824LGhEpvCDfY/V8PJuJjey7/W24wfbjt3ghWcQMr0rzOkB2qe2Qgq+sDNdzzlaPIhKrdRcXyNpPtl6cs0cZsXS0J6coWZZQGqV9kbKFW8VuipkPRTkZMmGvbGtFQcxZ06z7NiLUsynhdh2z0bDRXVcJscOKShSFYLt18nwUgpODe1RCigSKQJQ20L14rwwxYSuig93A390vaX3hopcvyXme+fpV+Mi/C6rSEmUTcYYNLVxgBRDJxLkKWds0RQjmVNUcR33VrPrHFlVUiMGeWvYeikavkiIFT7Ry3N4bdFIpVCasvCtz7uGqdZ7ifdjl2CKZMCtSh5rxEdZaTnWXGDTWYbOCopRkcWyVkpWpCLFhPeGq61n3z8sZDHLPa6abP1q64W/dJJQ38qawSW8vmJXP6tC7zQbJQTbJRbu5shpEiRYKbHySym1lpMU03LsoDV09+rN1/k4G2eYXMZZRQrcR6XoCudFcgW1UlxvOpxR3wjxeNxGkiiW1/8m5vIbUWd93fF43n8sAHg8ft0W1Jtqv86bewT5dy0b733R83s4aq1MKXNepM2ypC9W5XPMaNUM5DrN011HKvDLVyNzmMkKqIW7MZGyZHL1nWEMidvTzBJBW9g4BxR23rLf9IwhcDcGeiOogzWKjEDqr46Jc4AEkg+F0AxdAb0UFIGrNSZBC1djLoVrRVsApF2xhMSUxH/GVk3f2m9zTKgls/WOXAoTBWcNBvmcBeENhQS2ikePNWIkmFLlOEf5rEZjMngr0Rghy+JstCLWSj84StCoFu8hCg2Fs4atk8yd85I5x8TVpmOImVMIhCwoUM2ZpUJOmUlqFGqFKGAUzoA7zHw+TGgDuRaM0kx9R1GazlmsNWgDpzlzNwWuR8evbkZ6LwTOkCpzk+tKBIWMUmXXtnGCGmklKMljkqZwfzpOU2Z2Qi4vVHRUeKvovacWOM2Cdnmj6NsOvCZZTLedk6DWIm26lWtTa2IKBWdh6Cwba7BoTnPkapBC6euQfdcMr5jKvUT8vCRyhetNx0mLek6VKO64aAavudx6qHCYAkaJxLhWRU2KcUmEmFEUpphbISPePhqJdMm1tnMrxpm5Vo5TELJ5EVuDTe/48fXA//lsx7N9j3eWinA+1nOtlVgorIiRAs5LYmoF3OXGUxrh/TAlfOO2PV64Umnu2a3Ke+wSrFR9TcmzEpRXl2BpcxkRO3jLfnCvoRSnOXI7RVnklWwqcpF76jQnqIVkhGfktOLptpM2bitGj5NI3smgvSgiB19wGhLyHJ0XyaXrnAbdoZZAzhVnJKeuNhK0DoLI9Ot8kvgCH2dt82wXy7wkUoXBGoLNlFnu+YvB82zf8+RRK0a9Q2n35lhf/zBGxlLvkbUlyeZhpQ78JsaXxUvA6wXNb6Kt9qbar9QmzW/jdy0b733R83s4vk5VPkYxHRucZeM9+64QN2Ked2dEavrqbmJMCaNFUQOKkjPaaFQuWC3wt1KGagxOabbOcItmyYmht7jW459CkrYZDwUPSNFjNULyLeIOvd104q3RWX78ZMt+07Gkwm7o0EaTW5ukcw6rFFApuUBRXO86dr1MfNZozkskecMG1Vo00gJRaK63lq23QiZSBW/EOValjO4sg5Od8F4bnu0se+eZq/BQNIXDKPlfSjdVjqotbBQ+LD2nJQMFCnx2lIV0lMMlJZiyyErlushhrEOTuT1OOAdOy079MDXTvrRwe+4ayhFAwf/vk5fUKi0iq6V9ufGOZxc9P7wc2PUWpTXjksTduC2CbyNpWq253nlClnbQki0bA5dDz8cXg7QwjZBTj0skJSHwnkOi78R59jhHDJXeGuFlGc2ntyOnOeE7aSVe3EPuHZ0X+W/Mlc58NdlXKwmYXVprRxK7BekSSN9xMwWen2YxUPQW6xS7TqD9ORXGOTb0RoufSYWUK1MQ1ExXIR7XIo7mMWWK04QkaEqM0qo7h8gUUuOISTH7s5dnYqz8Ucp8dLnhqnFyjnPkPEcpdEMrLs5igHmcM0tI7DaOj3Y9ndW8OM2cQuZ68AzOcNn8Zra9u295rtdtJW+nDHOUAqS2e2olKK8uwaoF0dKKr80bi5R46ChuTqLyXFIjCjdUY+M1u96Ta+UwJy4Hx0ft3rjoLRe95fkxAIXrjdyrMUtG28ZZQTazJK3FJB5A0yKZepe9uJnPKXNaEhW4bK3btZX0Jh9nbfNo+RGvTgtThadDxw/3w70X1LZ3j/hfX981+3Eb6bCkpvisTZ3lfiNcli+LlwDeGjEhsSDqO40+eVMar97gNq1Go1mp3wk5/Pui53s83iRCghQ8L0+B85IIuby1Kp+i+LZsO0tVsqBuO8sYk7Q28oY5nPC9I425hUBWjNJ4Z7k0BmcTNVeMlok+xsSMwjnNzknrwChRGk1BWlO7Aa6MYo6VIDQPvBNY2DrNvhOfmq0zWK14su+52nRsOsMH+x6j4ec3mdsgeV4F4SxhFFQp4K4HT66Sf3UYA6clkWqlNxpjDNvOMTgLGrbN5C/VjCriNrvbV3bO8GwzSNr4ulcuiqwUOVbOSSIY5lIIsxAInVaEY+H53cwYA6cpyQLfIis0isvBYYzm1szUMVLF7Z9aJZdIa4ntKEUKqVQhB6gOvBcp+JISn93Cc7WIX04VCbNRMgE+3Xg2XmTF50Vccscl8Gw7yC65yi7VaI06zRynSEiNZNtiM1IRQ7reGp5uu+arA/vBEkrl5hiYQ+bp1nPRWe7GxJQTV4NHKziGzGEc2W88f/ik56893WK04qKTCXk3OCGCN6RpcPL9kMWBett99S7RaCXnPGV6J0VPoWKVuD/fjgvHc+B2DMxJCs7eCRJ3uenY944xRjJF2puqTd5FjC/fVOloNEsRo7eYYUoy0YeaqQquth1DJ0q/2lp/t/PC//xcnpFnu46QJJRWTC0thynwq9uJuzFSS0GpCrXw6m5mnCKdtYLMVEGBpF0lRPPOB57tuvsstYcFcuHTo1gVGCX3xbo4WiNk/s67pup5N6/lTY6g8LwkyHbfiVnktneSFzdFFqvZ9o6n247Oary1nMINMUEomb7xplJBVGC+4+c3471h5ZQETX6ycYwxMRdpTXoL+86x74X/s6R8H0ny+LhXFR9Kcb3t2A8OVRF1Zfd6yO63dc1+3Eb6TauzvixeQoKMK5umCDStzX43Bbad42Jw2JZn9l1En7wpja8I2kOt98XwnMSWQxst891fohz+fdHzPRxvcwYtuba2RObFOVJyaXlPmqutfxTuSKvGmxlbWaXT8u+umeOVUrnoLS+PlmPbEc8poRBSjFEGbQtLkOgHrSTkU00GbxS7QTyC5pjuHXCdFWJupyobpdoOQaB42yaezgmUfr31gqAogfE7b9injmdbQWP6UtGt1x9TobOai8GijWKcMjEKt8NZQaWWJBwBXypdb9l1lh/sevYbCQUFxaazXG88vZM8qZ+/OnE3FQ5T4Pl5oSbFdiu+QxWIUSI5lrTw4iQO2LpW5kW4NlOR3aDJwoPQKJ4OHRfe8sKPPD8sxCSfz6yk7Cr+HsYA9yhDA6OUZsmZnEA7JcgY0PeWzll6p1HK4JwBhEd1mMUD6flxYXCOi8Hz0UXP4KTYEOVMwmpRMBmjOM9tQbCaZ/tectCWpoYrFb1zTQKtGTzsNx5vNC9PMy+nQGc0w8bjvWmTpfjebAcnpFdneHEK3IwJq2FwkkVWVOWy81/beG/jLdZIjpozonSbY+FuCtyOC+dUMFpz2TtyhXOIvDpJS0lI6VqIxEquTaqF0yKhstaIx4xGkJBcIBbhqdRaUDUTi6jElNYMnWFrLVOuzEFI9ZSK03AOmcOcWmtJnrGQC3dzoNTarAoiSmmGwcAsnJZUC7vB0zspymKpzKmALmyM5bL59DxeIGMoXA+eVCqHc2SOiULhbpT23NXGierxayh51kV+0zlRLMK9q/m60PdIi+cwR8npMop974Xboy6ouZKKtEC1USxBvHQ+O86tBSact11v2ThN1+apuBQ6p+mMbQot4eaEVBisLObrcb9ZxHgDpRopTJT6Aq/t13XNXttI66ZTDFm/2+Lny+IlYjEoVRk6i7Fi9TCGRKqrH5UVPmYr4jbO3Mv3v81xvimN163tmLPweGoVxZwzitKcu6uQIr+z8/FNxvui53s23nQG9VaxTIXPDjNTzOy9Ze8Nx6UyLol5kZv2YqPuJ49S4Rwz55DprWXwuql9MiFGChXnLTZmrM4UlZmWwJgSVotKZy4ZciUmMZhD1RYcSTO0y82kUN5PFB2FMRa0MVx4DaoSivgAhQJaOzQi4dZUcobndyNLqXgtqePbzvFs07HUQiiFMGW00gx9R6dhXjKlyASvlGPTScTAHKSl1VktmWEKjNU82XZ8dDGIB5GWRf/5YW5ZTRqntGSJpcJpSRwWuN54LjeOH170OKcpSSbkWMSrB6swpbLNQkyIC2CaNNhKq/EDo0BZTqeFKSdpS6QqBU4jepd2nCnBXcncnVrx2FpeWsMwOPadyOxRijFGTreygDolXim9s2y9w1nF0xYzMKaKDYlt5+i94W5cGGYj2WttF5ujcGSebDyXTz0V2WFLq5N7p21ntfB7lBRt3VYLJyVXlpKlpaaaw2+sHHNu7ryCiIyxcIqZrdNsLuy9SuirRucMTzaOm3MgZlnIDnMgFymCB2swzjaEQTLkYi784uVJUBRnGJqq0erMEC37zjBWSQ3vrOxwJcIlYSrQ1GclS3GbixRWg/UoregVKAy9FffizkvBcpgCVmsuBsfL08LhHFmiOEZLlIMgXTpLm3EphSUJUqu1vJY3hlASGydtwcpDEOy6QAo/x7PfeJ53E58dF85jAKD3jqtNz9XGfyMlj1JS7HSPOEjrWNtZh1naV6VKsbjrLCF2VCV8m9U4slLpjgv/47MDY5Di7nIQjpogFyItj+2zZ4Qzc1wkmqZriJFSUhj0fLMiZiXAu3d85q/jmv111XffdnxZvETJ4i6u9KNoidY27FwziZwTSgsR/uYkz8OKbn/b43xTGn8YxX2/s/o+G88bzfWmk/bxX2Lcyvui53s23nQGXWFP71qarpEFf6dt469kjkui9w/w8LQk5pRwynK1kUVmSQVrDc92HVMUBCGmxKEzbJNmiRoW2dVFXZgXQSS8gc4JIqNRXAwOpQpFVW5PU3PHbURdGpmyZvleqSQFG6fZDo4P956uM4xjYu8tH194AlCnyN0csdrwZOv46HrgxWFmngpTTtRSKKfCrRYlhaoKpxtpN1amIAtCZ8Q7xRqZSMaYKUho6b53aCW78tpiLS63HYmMPVuudpq+y4yTHId3hr45yb4YIxRJkZ5yIYQiE1Obg88xYovIssdJZGuDUeydIXSGaRROTKsdKVlaW9BQH93iQGoLNGxcIKtB58pSKkOt5JSJSd5738vuOhZBALXhntdlmub4xWlmCrkFpcKvbkcyUridJ/EnGpzih1cDvdFsBlHprHljS8osKXMzLnxyM/HZcRZ5fSOHXvUOjZgAaq3ovSaEgkXcfUEm8lorx1FIs6cp8dzM4g/T2hePiZyPSZu1KUaKghDFjK7kSoxSPO68BFuOQawXppj55DhJEK4R8v5fe7LlcnB41/NHH+w5fHTJaYncTZG7c+AQEyEUNr2gVIVKiZlzzfK+teJKbcq5hoJqjXeGUmUzEVp+1U0MfLgX9dBcJGBWV7FjCFkKKGstTinJukLckgWZBd8pyIZaBGGNTtCMdYEE7jlN1xvPRe/4+FLcyHsn0RHXW0kw/zo7/tXleuW8vE2uDa0o6y37zr1mvng3i02D0eqe35ELOK257r148CjNOSVCaJEgFbE0KEXQwday7r0UOxJoq5ijuEKXKnNK58y9W/Xjz/ZmEbN+nnd99FXd+C5OytvjKN6uvvsm4zFVAXhnvETldcuA0jzArCkcpsLtuVLqCIjQYGprwx9cbXiy80wVwreQlj9udyqNROOU5nLvBHVcW54S+fLt41Z+3fG+6PkejTdJp7lU5pBFJVElwG8131JFWjA5w1QyL1lYQua0BM4hM1jLrtOEXFBaN76H4hzEmK5UhTeOC58oCB9hCrAEqFFQBqtk4TVavFlWRcbgLZTMeU6c5tXDQlCgzglaMBVp03QWdl3Pj643fLgfSLWw6CQKLBSxuUFf9k7aBFNk60RtsqRELtK7r7UQa2Ww4lq7xERIte2WZGJGiTvrfuMYjBXjNGOpzevFaE2KiSVJ5tYcC+cls+0sF4PlvBROm8A4J05T4sVh5tBSyNf2VIwFY8WZuPOWlFreVEwUL0qYXW+J1Yg/DQptpAVWaTu3KtJdg0xw1ooiSlNFSdMQCKsUuqVYh5DF9VkLIVMpRaqy+/LGEhIMXqG0+KVoFKFUvC2yi/OGn72c+fmrc9tlF8lGq4X//fzA/7zc8IfP9mx7y6d+JKbCq3PgZ6/O3IyBUqC3in0vRn3TEhljwmnFs23jM2lRUKVcGEPCN0XQq/PCr+4mXtyOlCqrmELTW1noKlWiPKogV0NnebIVwvr11nO16dj3ljkWbhSMqaCq7G4zgphMIfPqODPGyL63zRTRcJwT3gaudYfvLM/2HbvB8ZOnsltNudzzJs5LJMTCr25H/vzFGWsix1PmkGXXvx8cT3eefScKsVQKtRouB0EGl2NhTtI2vBo8xzHdB8kapdhsPIMxjIvEP6y7/NyQps9D4pQTXml6azkuSYqfVoCmUrFaBAfQvK+amGDldwzevBPlWMebLtdKyX0dVblH+R6PNXFc1GOCTqzhuNao1zyL5NpHtBU0qLcGFzWTEbVcCIk5F7ZOcb31PN17qhYCtjGakBMvjokXp4Xb88Jnx8ASE0+2ln0nhd7V1t8vwFIcPxQxQvZ+dwFX2ud9FyflbXEUuQjSsbRg4G8i2V6ibBpOs7igo8FreUZy5QvxEgpaWyk31E0JX62181K7ZmKbkRjcauWR0FpxtZXW3LeRlq/tzt5bNs3TSjW+z+NCs7bC8i+LzPy+6PkejTedQXMRh9GWKgcI7JhKwTtLzpXUUJUpJ16Nkiy+xELfafzZyKJoZTd0mKXHvoRCbLk7zhn6LOTHXV/Y2NRgbJnktFaUZjY4dIaLXnayUwDvEpsqktSrTQdacTzNjC2WgApdr/jwsuPprsdaxbKA7yw308Lnp0kKOgreGDptmKo82N4atp0Hldh0juuNKEl6o8m5crtE5nGhoFBWE0LiGMWdenCWJxvHk41HVeFJ1ApLiNwtgdspcJwC4xQ5Rtl9nZdIyLXJ5cVXZFoiN+dFIHwlbQzfOElio7/I7wfprbtF45y4y24HUQzFUpnnFu6qRa5uGqEZaP5CYChYqxsqI9JwZYBSOeXE4mQCcsbgTEO8tHCDjBLuVMkGVWEOQuoVU79MzPD8OPLJ7cTLMTJY8V4yphKzIuXK54cZVSvaGHSV4sYYQTiO59CIxa7xdywlJM7TwoTEfwB4b3m2dVwOTnggU+L5cebTmzO/vJt4eZ6pRRZKbaR9poC9t+x6UYDVAn1MzCFyte2kZdjIq5tOJNg3h8BxCYxOEJ2UCy9PE2OLcuick/yxi56K+Px01jJ0Yr0Q2667dwbtRTE0xfQar8RZxd531L3i1XkmxsSncyTGiLoS1ELus4GLFhcyeHnedJN4V0RllEvlFMTRPFF4cZ65Pc4U4DxHnJXMt1Rbe7Fbs7zELC+Wijfyfh/sep5ddOx6f9/Ofpva6V3jXSTfqKVwBDFFXL8/xSTFYSrcjJEXp4laZOF2VtC0/eCkEKpSxGmlcMpgdAal2XgJQi61cpgCSy5cdI6uydRrgc3GMobEzXFhakKCmIVDd5gDFTFKNc3PajXKE1n6QxFjtBSAc/qi3w7t7x67ZsMDChNS4eYcmGNm09l7wn9MhUJb6Kn4Rd8bSr4pNV+RylqFbP/L24kXx0XQ2RbmGgzMKVNywTSlZCxAVIyp8HKciBn2vggSjHymQpU5RIlpKAi/bxU65CrBv1ZrilbfWlrujKZvasu3/e1XFY6/6fG+6PkejTedQZVSmLYTCClxOy7cTovskK3Y7yslsQjeCFHXG80pRElVLpWqxD8llgq5iPvsFDkukdOYQVeUKpxCgVxx1lKqJuWEViJ7HXrN1mguNj2XG8e8RIyEGbPx4u8jiqKEsxqvCkZDbzWX2w1d5+icmAwuUVpvc0jN5TkzR6kI9r1l6BwfX1n6ztB5jZmkR/2Di57BO26nwKvjzOANl91e5MG1smhxZJ1zIcfMph/4+HJgO0jxY7TiNBdSlK9chCNTiiAEFQ1acZoXahGI9zwuHFPb5RbhPDijqGhiLiwhE7JkHnmh92CUOKYep4hSiqFTbKNCzdKScapB7O2aKwQlU0rUSlYJIdS2RbSUzKvTzPkkXkK9UTw3hk0rkLy3HMaFnbdCaq6Jj/YbBmc5LGJMiRIDy5QLg5XWAW2xlLBMuJsCz381sbGy4FWt8Nq0GA3DUjI6FvqYIGRenGfmObEdxGxwToneGkLo+fGTLZebjikKyrHkIs7AxmKNFD1jWIvvQimaXS/KQG8NTkuRPsXCYYqNaxLwRrPETKwifxMCsOK4REqCwWmeDI6Lbc+Trcfo9vuNC3QxOHQvkS1vIh0UaTmE9sxcbUQCfTdGBmt4Nc3cjZFXp4C1hqvOs+s1c6q8PC/0MXHRWYzRbZEUGfY0J15NkZord7O0TU6LqMrGkPjkNjVljBQ2FUhZ3H9X1Vtt7stb79h1jmf7jo8uBz7cdJTWCrnedly3+/xtY+UG3Y6S07XtrPD2aNLolhZOWREs8Y+aQ2JaRFr+alyg6iYMkOy1X90WPqbyZNcRq/j8LLnwaloaepRx1uA78UuKuUBDrjZO+G+hFKxW3IYsKeZeCvaooessz+jFaBNQrfezLvAAT3fda5+7d9L2fIxAlYaOGK1eI0g/loy/GhfGSQjpT3Ziq9F5Q+/svWrwHBK3pwC1MqVyLzUvjfh/MYiYACV2BaeQ2PVWPmcuHJbMRa/orWHMEtWigJyqzG0HOW/aKHLJlARKiarvotkaaK0pSRD8JWWuBg+POECnJeKCfOj9twj3/TaF429zvC96vkfjTWfQ3hl6bxoHYeHlObBxQs7tnG2LiqijjBGTur6T/CW0ptTCqXFYDIo5wOenmed3M1Mq4s5chDPhtOVy48VcTydMMs3zQ1Q0nXN0unJzmhlbFIPWir6hPEtO5KSx3vNUeQqZnfdcXXqe9h0prSGJM6/Oc/PZMXir2vsUzksllkrvFDUJ+RMFueU4aSrLkphjxepKJFE0KBQb57naGLSuPNt1/PUPL7hoqiOAm/Mirsa1MMbIXZv8P72ZuJsWcZmNmSVJYaI0zCLOwllFqbnFLqwTfuYcxHPGGwerfFiJ6mtuhmgAl5ueJ3vJQ7LW4IoYHVon/fewFHItWGPRVreFWBaVcWwtvARYqBpMbZJSo+msxCt0nSVm+MWLiXmunLYR3QqjUkRdlmvFG8Nxyahc0FoTl8hhSnx2e+Q4J8lqMg6jxcLgcmPZWoc2mlMKpMYryqVQFc3TBg6zIsXCz16d+fww8YPLDbEUlijqKzR0VdqPx3nh+XFhnCNFaQ5LJMbEh1cbngwdI5njHLg7t695YDcYsTtoGXNa5/uU9Jwrc8m4qGCrGJwsBodZ5OMhFg6thXi9lXDRndEMjzgtt0Xy4KQghq0zKK3p94brjWdapOX56rTgneFH1xue7BxLrnx2mNl5w+bJlm0v7cjTkth1hh8+3bA5BT47TJxCEI6ZMYyzmCvejAvnObZ7/MHfqlns0DsxB6yleUDQ2rhK2oC7oePpzvOTZzv+3x9f8f/4cM9Hl8N9SGnKhZfnmU/uZp7fznx6nFqbTBbeXS9WDvtBIjasUXSuQ2mIAZZGvp9bRtZuK6+7ZqvVWuVaLpmhE0ni4MQKQVR2kZsx3gf2vribSMDFxlOvxYNpP1hirtydAyGsZwBiqoSQqLlQteLVeUErRfJChB5D4rL37Hv3KBxV7vnHhW1qytfOiIeQbejgm7ELvTFEK5YBnx0memv54LKjs3IvqNY+v5vEtwmlWvuyUquCLO3/8xoTg6Bmzghyt3Iux5C46D27R1kswj+sXGwNGmkxVgT9d1azhILTms43h20Qk0qt79WQIWaO08LS0LZ975hi5sN9x9W2+8bE5seFY63CnYsp0/tvl+31XY33Rc/3bLzpDCqtENnximW9PHxTkDbWaRJ4XKO4K5EpFj67m5iTTAw354VYMwaYsxjpDRuPj9JTvj2NTAl2XWWK4ry5LCJhlxaXECkvusLsDCGKp483hm2nCaVwd1ooSrHtLX3WpCXyYoq8OAb6k2djTxQku+s8FQ4SAs5mk9kUKexkca6klDicNbFJ9VGK3knUgDeG23khhCThp9ZAqWy85elFz9O9Z+stV5uO613Hy9PCsiQKis/uJmJOhFjJRRESvDhKwXNeEudZuDZaQbZQmpmgBkZd6I3B6Mo4JfFySTCXRjbWmsutcI9CCyEtRbHxiv3Wc9UbjLVcdI7LluhttbSJxhiZlySqJDQUibA4z4GUK9oUPrjcyWLUGS6dqLSmUDBOzCJTrsyx0lspbP7X5wc+6Sx/9OGeD3Ydl1vPL1JmnCPGaPadEfJtSBJNkgtzyK3FYzEmNgVfIubC2WV6L6hGjkLK1loKKWsMV1vLh7uBi8FyN0d++mrkxTmKxDVXDtPCOCcSgoC9OiwcZ1GOmYZ8ra3EzmnJzQKuOsNy0aMNTLMkgX9wIeqkVWY+LanJyhVD5zBGSNhjyugqaFUxkFJhConbURAf38I1TVN8rW3lpAoxVbTKlObGq4BIwVrFk0tH5yzeC3F6zXuaQuZXdyOlVLaNjLvtHB/sB/7gCXxyO/GD48zzw8yvDiNjzjzNcty6Km7zLN5W7XykLP9OJdGi5kSGXjOtc8w5JqrSPNlJCO+fPz/KdU2Zv/Z0B8Avbkf+4vmJw5JZQmZZEksWYvXGG0rtBLG5mwhZCr59b/AtwkMrhTGiFLradPfzlNHiwu20YQwiUf+hGSQct1lSfJgGfv7qzJ9/fuD5KXBu1gkfXXRc9o5U4eV5YUmJF2Pg89vAFANmlHnoNCfupkVI31VxngMbb8QV20oBsxk8P3114sK710z8dr04KC9JXOtTqpxbdM2+l2XzbQav3mk+y+IDNnj5/c7m+9iQmDJaw9JUfWjYeou30vJOMTPGZjWSYegyV7WSssG3XL2Qs5x/bdl0orRaPbNca48C9yagALdjaMazUghZrdANOTVKM6bEcZLPOXixLrjcSNHzs9uRpYXarvy1ryK6r4XjaY58fpQW3XlJeC3FVEqZp7vuvrj+bY73Rc/3bHzBGXQReP566BiNWO5/fpwFOo6FU0yM8yILeZZohHOLDwBR1sQQSdJeZ+sdV52QbDeh8qJACHATM+dlFFVUaTJWIxP+MlU+HxdUMxrc9hZsJiS4GxfuxkQGulb8lyooidagloWadAuihEWeYbyDTgtBeV4Kk6Lt+nq8N/TeElNpRGLwXnTeY4hMS5MsOzEkDClxDpHbMfBk67g7LXx2MxHImCoZQM8PM2NKnEKVhbY5BIqUXIiUtUAsECL3klGrkcBUMrVK26u3iqGDjzvHxbbjuvfsNx0pi5LuvCRiENREo7kdC7UulF2h85ZdZ7jadnhv8HOz0s+K2ylIhhRCXN93sgvvrGmya9VywgpDr1FK2jFLjE2JYzBKeB1OS/7FORSueknbnkolhsD1tgdTKamSKsSG3khRLbt9pSVnKZXC7ZjJJ6hVQj9N27WPUcjrh7FjnDPXW0/vPV5LxlkpmvMSxNwuynscpzZ5I1L9pS30sRSmsIj3UlPhnZzjGOE8F/a95XrXieGegqf7nl3vSbnwbN/xy5sRhXiJHJdIZ929628uhWe7DmcNn95NIrcdVrdvURO9Oi/C31gqz48TGy+hmQoJyb09BWLKXA0d132Hc9I6q7UyhcQnp4WQs3Byho5nO89H+57rRsi2WhDDkCXg1WqpbqwSq4YVx1FV7sVcH1xQlsJ9q0VTcUo3s0rXXHo7eu/IRdqGz0+BTScZY58fhDu09aJ+Kii0hp0TvttpTq2FJqhXyhWVK50zgubVeq8GskZ4PMaIBCqmTK6ZlLLwwbS0ZXNDzCQLyzNfbhi8a5w2Tcnr81VYYuVmnNBKk1PGUDnnxKd3E8cp3Kv8YhQV0zlIQZg7Sy6KTKA3Gq/0vYnfaY5se0dFnOKN1veGmEsqfH5cKEWk4W/GLnRNWn8zRSE0ay3Zb5VWAIvhY2i2BiAIrqgdJWvs9hxRVfiSOsqGZIoRVcWnrFTIsZC6ds6M2GZAvTe+VErex1ktBqNN/r8dHKUqatVoG4lR3PePc2BcEttWRF3uJE9NKcWndyP/n8MdG28kVkZrrjdeSP1fIW2fcubuHNAKPr4csFoxp8InB3mOfnS9+a0XPu+Lnu/hWFn0g7c8Z+LVSbgkIcMyB5YoPJICTEvm09PCeQqc5kTIsmOttQrnpC1kRiE3Zw93UyQmseMvBcYIE0B4OAYPdOrB3r7zMFRQylBS4RAqS1xIuXKapViQdxZ0xHvYN/JtKE3h1Mi7Qy9SeK0NKWdSab41VJZQoIrlf2mxEmNM3I2ZnLMcd2s79U5eh6olq0qfUbUKUVYr+s4BlVw0KbfCT8EcIwoJQL3a9qI8mxPTUjnPsvj0Vs4BSoq3kCEruGw291artjA6EpWXx4kxCBk6pcKYCptq2fcSvUGunGPhdgw4o9lvKpdNZRSixpiK7+DzO4GRrTFsvGbTi3S+FrEYCKlyTkJE16pSlYS2TkkQCmsVWy8tkCkkXp4jS4hc73ouO8eLWDiOEtB5WhI3p8BhmliiXINcZlGMaVFXjSFKWGUudJZmvCeqvVKFhD2HhV/dBu5GLwXd4LnYDXRWsYTCYQzcTQtTSJwXKSrnLBltEZnEYpIC0zsoFLxW2Ib8LQnCOXGYZu7OgdOzTeO5VS4Gy+WmY06F2/MiShdryLlwlzKpZC57OS6qcEHmxvP52cszP391ZlwCx3PkHCJLEp8ebTQ775rqEVKS8N2UK1dbR+c9U8zcnAJjkL9VpRKr9KWuB2m5VEQRVCqcxsSUpKJJRRRrpzlyd5o5nGFK0t5alyCLoD7OgvWFyOqQXtFOWsO1wHmKdNag+8rtWbGxlv8ZD1htOEdpv8SUKaVgjGJcPZcaKuSNFv5eFUFD1wkSeXOKjEsi1koMwtf5YIx0XjOGcq92O84BUGKJIGxfKaQqLDlzjpmY4HIQov1xyRgF+60nhUiuojgqJXM7BZaYOS3SIt55y+DkefZWbCdyhdS07JvOsR0caFGm3k6BUArHKRGLEKafbrvX4j1qrRzGRdpCSt3HLuQiG4rOaHqrmKJk9c3R8cEe9p1lqZVzlBaVZN6JKnQK4lhcS8EqaQ3PIfLqmPj8IMTkwyL8Q2sUTsHl0HG16+iU4m4KHJd0H4aslVzbMSbulkgolY03PNt2ElnixVbDWjiGyOEcsEaz6zwf7rv7gucwBQ5z5PntzLYz7AYRhNycA7fTwo+vtzzb928tfOaYuTlFrNU86aUVl4vYXBgtm4vbMfKD90XP+/FdjJQLhyVynFd/j7aQNbOsEDJLztyNgWnJxFKESdv4JqugwDeljKoScXB7LBgjMuExRHKWHWVDzx/enwcvmQ0iX6dCToFzgjlBjILcJGDm4XU8gvoYrVobhXsvn9xs/lUPzkoIhFZrm6MwpiSQcUrkIl4ttYgfUc7NIFHLInk3SkFiTEHNhbY+o4wcby5LczqVz5+qLNSrmeiuF7m6qi3d2SacA5Wae7SRIikXOf5eSTRBZzUbL55D3sgC8WJZGFNlnMQhubJyczS9E+QlJVHrdMZw2y18uO/4yfWmeR8pXp0Dv7wdOYxRuFVUQqycWwvMGI2ncNaKJWVRe1EZY/PmUOAqlKQ5W3EuXlLhL17CT55sUVXcbD8/j7y4E07NGhmiqpzLGKSVY3RhUuLB1Chi4h+iM+MkarRSpRgsRQznlFpAgbcj+/6WzirmVJkWmIOcxwIscI9s3N9ryDfsAt0Cm75idWEJhRclkIvCGsMpnvFOcb3phUyeMxed5enWs/WGV2dBldCVnTFcb3usNa2FVTjPiVfHhdMSeXkOnOaMqZJYf5gjN7NUf1opbs4LTis2nWSeaYR0e3sOLFkK9FgSU8rMQcJJLwbHfuNw1tyTblfjzqWK8VtRwoeZorQUp8b/WM9Juz2lIKxIqGcCr+XmVTmxULDUe3Lt3RIZnJCM/+LmgK0K7y1hyWgjLSprdNvsJGKR+Jhpjtgmc996y663TMGLdP/uzBylUJpjYv5E1Ea7ZvjYG83GG46L2GB8fpQiwimFc6pF4mSmENHKUIttCihBVF4cZm7OC94qMVPsPVYZbpeZw5xIuXCeFu4myf1TSpRsCjjN+j4M2OqAakaOwkmvTEsCVclOVGEV4byMIeGMabYVIjaYc24WD0X4k6fI8+OZkATx6qzh80PXkMPEccl0RrGxotT0Vkw/e2uoStF5x3EObX6sEkNCJZTMXTPX9MYwBbkPjNYcZhGXGK3wRoQSN6MEtPadZd/if0LIfBZGnl50fOQHht6xGywb59h1hg8vhxaQDOeQOC6R85yYc2avHbsWwXFeEq/OEaNHeifI8+ORS23XtdA7ex9GG1vLtzSi+80YeLLrvrbL+ncx3hc939Ox+kVUhIAZi6iIrNW8PC3cHsfWp5ewwFIVVa2KLSmalBaFi9KKEKL40CA5ULnCvEjh0ugD91/rYlQAh/x8CRA0nMODgj5XKXbSo+M27fdThnkuzFnM+Kx52MHGKgGcq4y78yLLrEUCIRWKMTW5vpGIA9UKFoUsvndTIJa2kDaIKZb283ZcpS2yZx4Wk7aGoIFzqBginYOtl2O2GoqWP9BeisYl1Puw0BAzg/fse8Nu4zHI5FJqoabEnCMxSsFklcJQG5E403lR40l+k3xdDuKgnIvwLHbekrO4WC85c5hCQzAExVsJvrcN+i+lcJzTvS/JY/K51gpKJlXFJy/PVArnc2Ip7Zq1IslZuaalgrFyrUqGMTQjxcZtchoqlSnIObyP0Gjndy20N+0b0cvkGCOE0q5Juwf0o3vl8Wh8bWKEySY6qzHaYBVolVkC/O8XZ2KGj6979t7y4X7DHzzb8sG+58N94eV5aa7B7t6BGwRBzVTOMfHiHAixcDFIRlbfWZ5dbCjMzFG4J1DJaHad48m2o7cK6yxjLCx5bi0ITac19Fakxd4QQiX2hVRMU9RIsninpEBOiXvvnpILRYFx4NPDuaztv6Gd53mBIYDSBaXAOuFWbUuVbDsNMSeM0mJmaTVDFQIySZzdqVIsL7mQc2IMlVwVmywE+SkUTvOCUQtUGLMUCcfzzBzk/iu1uYB7h2+eWX6Nn+kcVy2XKSWISq6urhqlIatKKYrBSwtpnKUtK225wtaLV8/F4JlrZZpEVbrEjPea3jupzitMc+QcEs8PE6a5ku+bh8/GmWYwKhvFMWTGlPkkiIghh8rdvHBcZOZKBZwVZLWzgmLNQVLkpyUyeMvUEuOVUmhVKdZwdw4sIZNQPNl3XLbPbo0mJUGqjAJjhOg8TpnDUQot1Wue10DKhf1g0S3YdI7CM5qT5CdqKwXN9YXnqkWWTDHRW8uH+4GPn2zIpfKLVyNLy34zi/zt7Rh4eZx4fgpUJeh/TGKKaLRinjPPbxekaapeK1xiLhzHwBREZHC3JGqG3ukmdtAsSeajOSa8fSBl/6bH+6Lnezge25SDKDju2iJ3XiIvjxOfHhZiksl58JbQdpviZyFEN8mlEevinCphkcKgVCk8Al9cdNaxoj6NciHGcVqKoNiUJQrYFJjrw2Km2muOQW7OqoT34ay8Zm/BZDH6y0V8ZmwGoyvO0OIioORMiA/RA1MzTOys9NymhhzRCqj1/TPy/Va33BdkFTl2276fHx1vabEQtgWy2yrH2XuJjlAKtBUOilWgDdQ2+VktC5eQkSM5S96RNqLQQotdvqJy0Vl++Gzg6c7z0W6DAj49iNPxGBOqrI7TcDsuQjIOhXOUHddxWng1LpyXwPEshPCpCiIAzewQ2GpIKaHMQ5FIFRXLmKSwuXBgjGr8AGl1TI3LZBoaV+tD4QiC6igtfx/Cw/vGR/cLSKu0RInTsEgRZTJ07Xqs1+JeAFIaj6X9307LtahInlZVVeTbSsz39r3DGcXLYyB0WbKjvMFouAuBzw4zIRf23lIaGvLBviOWQmxcjPW8SMq8CAa0EXWMaaaQ0mKWNt+TjQelOC2RaU73MmtnjajZasEZg9eKc0yclsjGGakmasUbxQeXHYXMq3OQKA6j6LxlCJEUK9bJuc/1USHPQ7G+Gloq3e7bKsVTLKZxsIQEPljLtpf4GRPkLg+xkXCVxIXMRRDIXecwWjZJIRXmXEgp45zI76nCKQqp3l/jkCs5SPQIZeWeKGKM3JXCxhu8tUwhi9zeGEopnEbovAVE9fTqNHM3SlK7VprbccYag0YQEvHqESg6zhLVUKnMUbxxlCrcteBSrcF7I6HGjdey9ZbOWu6WwGGcOUdRg51H8QoSI9B2fkulKBGEeCPZfUIyB+OieOFoEWtsnOV2ks+/OorfHCfuRpGJ19biLFVUY+scGlK+t/ewzW/sxTlwCpnLTcemCR5OS+QwFlG5tUwzjeI4JV4cF8YYOS+Zn70882zfo3Tl+e3EZ4eJVOVaLilzCplpjsxZzGD//HLD1eAZOuH+SWEFF73j2X7DphMahUVRSuX5aeLnNzPnRRRhzoh68oOLnh9c9Vz0Ht25+3XqtzXeFz3fw7GaFNYGqVit6KwlZGHh++b4WoulcemknaI0Kho2nfSp7y0NK5RqKSpynoRTYeFeBfK4YFmh9fXG0kiB0zkpOpbQ2iDp9cXW8VBgxPZvTXN1NoKgrIWStzBHWVh7L+0qraUg6wQHYY6V0xIIqTIvUkTF2too7X0eLwYr4rB+hscPRn3j9307xtx+LxU4L8IRKqVFQBiRQo9R/r8zggTFCi+PE8uShNOzaTCJEtTGN4nx0iaeeSosakJpQy6JQuXVYebnfmrePJpnW8fVrqOgWWKUXWaCcUmclyiFZpE2VI6VFCuxfZjy6PPdF3gFwizSe+uk+FDG4lRmY1o7URuo4v+xLIU5CvKWWH8u53Ju17cAhwg6PrRe3lUwr+2q8/iOX2g/n7LcNy17Fd3eW2wK5OdjrdyeI72PfHS95QLFrvd8fLkVQn0zrTvOkeOcmJfM1htUEuXNGOJ9i3TTGazTqCitR9PMG3MpGGUwVByVpESdZnRlYzXOyaIulhIdL3Xl+V0kFc05io9NzGL2WTQM2nCYxOTu48sB73pQin1v8W7HcRYCnEHUPBhFVRN+KU2x9dAKLmU1qZTz5tag2ipWDoGCUYXDlNk4hzdVLAcUEv2gxJV6WSIhyxWbYiZXxcUgSI1WiqVIq6WsRp5LJLfcvN5aTK8oSPSJPGOKzonrtdGiINK6cfCCGBqWUplzpreVOYpnVBctMQlacTdmxvNCqKKceDUp9p2o40JMnIMQpJVS5Fo4FrFKoIrrcioSSWO0QhUJybw9LzilGXrPs4uOq8Hx+XHh7ixEdHFCFrsGqCxLJoQ2h1R5doYuEmon5GelMEniFlQV/pRSWlriXuI5qgaTFQSF0ZWK4rJ3GDR3SyG24qIA+96L4WsR5V7RldMSJV7ICFG/V4qzA10EKZ5j4pO7TAgZZaDT0va/WwKnpRnO5sLdtHCcUqMCyD15XooISiq8OM6Sk7fOxUrifNbN5epmbdpm4NPbmc9PMyHDxht2voh3UJFN3vXW8zd+eEkqDxEyv43xvuj5Ho7VpFApeTBFFivL++AsdQMoRWp+KUushCJyQu11y6gxlJQ4J4E1jVbsug4IWCvtmk0V9CLmByi9OYHct6IUMtF629onwDkJL+PLxvpatsAyP+xQa20tpFXd1VbsGGDJcJ5Cs1mXnezCw6K+LrJrYWbaV23H+7iAW4/9TZ5S5mERX1+rIsWZM6C8TOCmkRu3HS3hW0iOc4B5hlIipWQOs8h3V6lpDYkJRUiRF3eJm3aiHBlnApv+TGdkQctIcXUxDPzwquPH11u0Ukw5Qy3EUDmGyHlKLLWic8IY1bw5MtVDCQ/tu3XE9kUFHcAFOJ0jriUY+MaD6q0WtZST9hMNMautB5hp/CwrHK7CQ6H7XY31WAekANZKUMDUeEYrWdq3HLiYCyEmYk5YbVHVcDMFfvFqkp1yC/3cFMvLc2CZCrVUzkHUZYPWzLO0iEBkwU5rjIFUFcqK4kYUjFquUZHj6Z3hauPY9A5TDbfzRMoCf9ZSmVuI6JzF1ZkiZPKQE3/9gwue7Tr2neMPnyWUBk2lqMrgNE83HTlnpqW1n1o+nNMSPzPGLDYSqbYCS+5so4QETJEU+Vg0z48Tv8yZGKTFTUmMU2YuFW/kXu07S+80u96JR1XN5JiYQuAcEodzEFRPGyqqqTkVVlt53ox4UuVcsRWKrlilMboVY1Va8yVLmytnkXlPYebFSdR5aEW/saiYmWIlhcKiK6VGSTa3hporBSnIUhZBxNpal2e3vuZyLi3zzJwmSi18djvKvdQQv4xc185a+f1aUbaIuWiVQN1UEL+gDN6JAegSM6FFgWy8oXfCuzmFem/06awW/mEqfH6YUFVxXBZCTKSqmuLOytxdHik/FBzOgZf9Qu8ElXt1XghZpPQPhHZRtXZWEuh3nRDtpibJ18pSSiQkRUGLL5eTwtoayxwKSwhUqvD0lKw1Q5e4GSPaSKu+JnFiFw8rsT6ZZGfdznElpdpCcVcO6W8vkuJ90fM9HI9NCpckcKVEQxh2nSh3BiMSZDDUKr46RiuRC1OFmKc1BulFO++opdC7nlJFXhmLGHwtbSF3yA7gvqpoO37TFhzVeDNfd+FbWxmaxqNpnJvVhK0UKDPsOqhZyK5vLuCOh1bVWow9bqc85iE9LnBWSPnNsf6OQR4ehSy2u17UbZ1RAq/nzHESaHoJmark889JUK4lwRwLtYAzRXhEq/qmTaBzfr3ImjNM5wdUQwHOQ64T8xJ5cVr40dUGVGWJEjUyeLHvj1NkKpUXdws3Jzm3KyH4y8ZKHF4AFR9Qrr6TNpVXmo0zVKJwuBrStUjGqhSoDVFa0cHvaqwIj0XaiavlgaLtRLUUXM5A5xxKabzW5Kq4GRPOpuZaWzl3BqM9vde8bIoqWtEUS0WlxM0UuNp4tCmSUTYl8UxSijllFBWLLF7bTuO0TP4bZ0T50ksbzVvNy0GzKx1zzIQc7hffKVVigUEVOiutn5tj5BfmzK5zjXjq+ck1PN16Pj4NHBfhrdyeFg5TuDe2E1RUEsdzLux6y1xFHaiQcNQpiK/WkhJTbG2NOTYyvMJq8VJSSlK6rwfHxdbjrOODXcePrzdoo/nfz08cTkEUdand70qevFJVu6eNEJuVwlZFMXKHLVWhlsQcBQ2ZgnDbxEhR0Wn5+ylllhClldx8r1LLHhsDlIYweCtu7t47QsmEJRHXQrg91BLF0hbhlde33jcFxgLTq4WQ5ER2biXiS5RMyNK/XVo7udC4iojg4ZgSBSFAd/oR71HBUSPSb2vFLbyIeaLWmhRE6h5qQ0TbcdfS+HI6CIrazm+n1xZleybanLtuMnbO0DWnfRTYpAgpUYpjO1hMhXMUhWStFa01G5+ZovholVo4zYnbc5C579HEqNp7dU7xUrU4jntEUbcAXihVYbUh28YXW+kMCqYgbcj6ePL9DY/3Rc/3dKwmhUvMHJdIyhWDQM/eGYk+sIUxNFM7a6nNZXfQShZv5am5UBB/ith4HSGJ8mucEsfmnbOSe22ViYHSdhdGFp7UipLIA7ryrvt8RWHWouIeNWpwTK0PLZBDgcP07vNQEC6Ite34kAlhXo93hfp5aLusf/eusbbeUjvOlGAKoGoBq8hZEJw5vE7SXltoa8vQLA1Nig8E1DWu8ZH6nzbvvvV8bQMkB3PJpFNFgmUNxui24Cu0NpyXSRbFUQqY9BWf8W2j8qhQimB1QdsiRPWC7HbtA1dK8WAzsHKhBpq9wXcwNI3bowRtGpxqnh+yo6+qSudwJWxPgVLELPPFeeanz490XlDRn7464a3IuEMWyfDTradzkk/Vac20JC46jzUWVQNLc/b23jCHzGFaiLmy9Ya9t1SULApG4xopnSpKv6012K1im614HlVNHCe80+y9wTtL1zl6byUSZBLvmR+0Xf6azD5YyxQCh0WMEz+/GwkpY6ymt5pN33E5CEl30zmGWglBEtqXIItrqZoxiZfQFAuhJHKtOKvIaFTVDK4VbYOj946tF48qmlndRWd4svPsvOW86YXAPC2c59QcxkFRHhGXQaFIScwS+7Uf2vhjpbWKlBIOYKowLXBY1mc139+Prw3hnKMo9Cz3C5xqXLq1MK65Ibut4EmPNhxrAdQAy3vhgBWwgnGugHBywhs2AY9FHbTjW+eydd7QSN6dM/Ee/ZbNj/De0qMdWOVh82eSoKzWNsqALUxZNlG5Cuobq/AfvYIxwTFnChmcRRVxSe6MoXawzJEpZWlbzVGQqizJ7bGILQJVuFgpSwG2ovorP6xzcF7qa3YJBvBG2mK6bXiNkTavUYptL6q9mEVEsV6f39Z4X/R8T8djk0Kl4bRkrDN0VdQavZf4gZgzU8jcjhJsKHk20me+PQeOs1imD15g8TGIR0aInqEroLLIudtNHxFERtNkyit/Z0U5eHgw0juOvT762eP2Uy4PE9G7/vbNkWkTTwLbNWWSkuLMVNgOYvo2z5XDIpPr19l0rAWDRx5874XYa7SieedhbZuQeL199pg0vaI462SYeXsx8q5jyogirjMVWzI3asFakQOnnLCT5vacuBsXbscH2fevu7EqBabYJmhkh2kbbym0alCnVtwqmfhS4/x4Xi/qvu1YC1RdkSBaU7nyhs46yYprRUdI4jNjFYQlscSAWhS913SLEWWdqqQsu2DvHR9fdkwhsOkdzsg0qTSc54A2GqcVW6+YUub5WXgRnRM+3MZbemelYOksW28YvJBjrza+tTakJXA3JS77jk4HNn5Lbw3eqeZnYpmWzIvjQoiZ/++nd2x983lSkLX4teQiasRCYdvCRtFaFkhVZCEH7uYoieypMC6BV+fAYQ6cmvS8ACoXQrsJJQoFsspQlOTl6YLWld52TEvif316wFlZsazWHFNElSJFbhEEak4PwoNsQalCTo37htwbrinIhEQrpHDrWnYgoLKElhru65ovbYZU3iiuK6i2U1rb2isn73GRsv63PPpKNINHHoQW95uXrzEycr+vGzhHU66mhyIpr7/Iw1ywbnRe2zgV8FE2kqsitNAQTfvw2bVS9KWyZDhOEprrdaL3q8or8mmpHIMU72uQby5VkKX25qrK9zemMs5SlK1zcOZhw/vmWDKoDDbK5x2XwM05sPGai1PPR/uEUYpn200LqP3tVT3vi57v8VhNCveD53rwPN11nBeJn5hyxilNZztOc+Rq8DzdOX78ZMe2t8xRPBRux4UXx5lPbxc+OZyZl5mYZFdd6dh18OMrqKq0AiqyZABNCYVjgHOUiepNbs27xjoxrBPOY/7MtxkZOALjIpPdpu36nIf90PFkY5m2hf1p4tUspOTz13hdRVOEeeiMbdbuGlMq3mXy8qCIettntjy0aAyyCNQiE+RXcZ4ej1qkXZYNUDNdV1BFXFplN1oYz3DM3xzdedu45/wkWUgG4HIL295SaoFJcqCiaQqu1suvSorfdx3Du9Csx8PzsCB0SswIaUTx3hqsNWy9JuZ1565wpnBuCd4oQRHykhiDYbBi9laLRE1kpdAp8eIoaJFq7TqtQBlB0C4HMbTbdoaddpQqzrtb71C6EJKENnqn2TnLBzuJNthvPIMXz5Leey4QOf1hCo2AnNgPovo5jFEiN86RWBMhiru6NUEKTK3Y9I59yxLbDxtKkbYOKF6cJK7iPCeen8/MMRKjeGx1XuNUFX+gJbNMgZCrtJ9zu5fg3hBUAZ2tnMKE07DtNIdtj6ri8ZNKwVoxbwwlM86Z+d6ssqGgyG4+RFHuWQubHjYbz1UnPBNttBRKKaOqYtOCMGOQ3LreRTILZXwoOtbNxFdtgtYldb131sL/sZiiQ1rxqkiRs6K56/26Fi7fdh56XCx82Xi8MXp8/CuaJC0luedzayH3TjK0cgvaLVRRfiqZSytCaNdKiM1LTEyLbFxKkYJJqSpFTUONjBLVqaKK6lQL0hTKw2btXec9P/pvAfqyqkDFa+zorfj45ISz/FbDR98XPb8Hw2gx76oKbk5i+OVD4jgFPjksGBQfXHh+fL3lcuOZGxv/yVYCN63SzLFwXByfMRFSYs4VgwT/uTV5m4zuLCVEUmrQ+SO+3dvGyrd5/P97HgqF76oVAg8TTqjgksjlb9RCzQGlFaaz7HXGqIqZ5b3v0YTH55MHxZnIpkXuq7QWN1VnqRTGUO/5OSs69fizPv73aq73+H2+zpgRrg/IZ5oT9KmSXUZbaQnkJDuv9Vyv08t30Ua/XzgqqCqttMuN4jRllJLrn8oDD2vdWRtkEVz5PitcbloLYW4Ht06uK1rmEG5Db6HzokrUSmwZrnaey97QWyGOrkqTqkSSfXtaWEIm5sq4VLQu1F6I50ts5pQaXmTu3bdXjoTWgibFCtuNuOtuvON6Z9lvWhu43euSvQXmheJq8Hyw71qoq5M4jKGjFDmOobPsNw6FcFm0UtyNkfMSOc+B58eFcwicZzFA9EZk0M5qdnPildU8vey5au7ht1UxhsSL08RxyswpUXIhpIxCPGBMFWzDUrneeraDZVoKqWSmJcJSKC0bjrbYrW3cpcBpKaQ8sukkVDiUSlokjmKO+d5I0rTUd6cbF8Y+CAy2HfROCSelSjt9M1gGaxjnxGlOpJzRWZRKKWdCiuRWhK1Ch29ynyoeiubCg0oUHp5Pk163pHjb63zVsDygumuB9euMFflxyLnUZkVzDNXk+0IsVyhN4hrb3KtUK/ytahzMglVyr4r/V/tMrdhZ56pYmqN3kpZ1VA8cqPW8fZ1zUZFr5dqzYbS0SjsrBpNaKWL8LRJ6eF/0/N6Mx+2uV43smIpIcbOqvDwFbk+B0hxavdVi0e4t2ogXwwf7nnlJ9NawZJlEh05s2SsLIVWO88JhLqT0sLtwbcf3mMC8trgszUCt/btrT3Dku0El3jUiwgc6jfDZWDFUPEWM9YogLY+PtyI7wZU4q7XshgYnX05nNImKGEHmXPG28Vriww5znaAeQ+prIfC47fX4d7/ulLAStEOEu/jQQqw8IEq+vd/KH/imRaXjYef4mIdUgVALqiisUlwMhtOSGWfZRW56sStIyHnrG5S3ZJhmuFen14d7Qz/6ggdStWt+RwMVp2HTW64H4ax0RtQmyVSMVSyxNF5apXcOhSKPktGVErw6yiuv8QRhVaDxcM3WRXJdxJYFTE3QYk5OZzGBslryrRSQayXPYtB2Nwe2VtCMq51nvkgSuljhOC9NGJC5PQd++vmJ3GRnc8qcJuFdxCx+TwlFTJUBi9OZUArLyzM33tIbQ+c055CpaJwtLEEQLq0tnVXsesezbY93mnNrXS+xUGpCR6heMaWZJYGy4NvNut6LsUgheHOC56fM6keX04M/0Hq/2tVHSclinRtq1hlZuGOunJdMX1STt+cW35B5cVjIVUjPtVTmQHMO/+YFz/osGYT/ZZqStDSENPAw33wXeMP6fK8exW8i1t90rMdvNWx7Oa/OGpmHY6WUglJK/H0KYgNQK1FJAKwyEgsj908Vp3cjm9Z9Q4vW7lJqnKKS2zlbeTlaiOIxyz1hePt1eBNJW0ehoVLO8mzfs+sdF4PnopNQ09CsVH4b433R83s0rBGJ6TnI7k8phXeGEAu3Z8mJEWdfCWg0WkL4egyd0fzoYuCis7w4LveGbDVXXoyBU4iEJTdr9BYbgRQHq/Pu47EuIo8LC4dMRrE+yMN/k2NduNfjUcjDPvHFQqPywCUwgG8IRkhitqaVtJKMEadnwzo5AZ579+cQH3rxK0F5ndC0kiJgLVS+zf7nTeL0274PIqPumrEjNEPAr/G+KydrLUzWQjBk0CnTa8hGM/QdJc8sprbWzYNfkTcSalp1EQQA6LMsaut1WCXAaxtjJYKuP+87uNr0XO88F4Pn6aYHJSTZq40lxMrdsvD57cSrs4RNSpp6W7xXVUxtjtHqwdBwJZw+3vGvvKvByHvnCkuqoBPnJEWTd45traLea9lYIWdupsKzred679lmyY5KWdAiKBzmiCqwNZo7JBE750IMgr5IgK2cs9zCJGsVN1u0JNEPLR5gsApnJRzzNCXOsTTPIOGrnedELjOXg8U7iy6QU8QCp5Q5L4F5kmuxnvfHraQVgV144KRpXt+k3N9DDV30CDcE9eDWHYugP6VkcsnYZFBzFRk+EIPwUVJu/z89ILVfh8+3qjZXYYA8m02FZRr61HKzSobj/OAztRZ462bsXXy7tT29zmWPNyvrefGt8EutWPumXLbHHKNc5L7zVRShS5DQ5lTBqExnxLVdI8T9nNZCJUPNOK3ZdBqrJEZIa401ch+Lr1t7JyXzejUVW017FiplzFQn1y0lQYEeX5N1E/tmO7/T8Gyn5Dnd9Xx0PdA7w9OdpfOihEul4L8xzv3txvui5/dsnGbJDMqq0nuBF0sFmzVLKVRV6byEVBqrZbI1kueypIIvGmU1IQZyVpzHwC9uz3x+O3I7hiaR5z7OIMXX4wO+bASkX/x1evTf9ah8fR7N2orp2iJZqqiHNq3frQHW1k2WSXboNKpWFidbYaXa7jgL16Ui/14njjd3St/lCO2YdX4dcfq6O9G3nSfdMq86Dc4VljCxcfBsZyiqElIl10qnxbVYNf7HlCqdV1wbw+Xg0QamOYm3R+OL5Crn2neaTlm2vWfwTv7bSXurKkXMCQ0cl8QSJIYlZXG1XV1mj1PiMMKpvL6zN61P92V2CgUpls6zFKkhFE6T7KbFrHBh8AtWCcKZi5hoqpo4zhO/fGX4X86w33px/fWWfW/ZDx6vdXO1nnl5mLg5BUIW4m5pB6lQEsjZbgylNZ2TMGCjhLxxlwAtJnrnUc4frDJ6i62K8xSYYmLnDSgxG7wbR14eEof5dZfu9f5YTSDLo6/1OX1cGDxGJ9f7Sbf/0fph4S48cL0AtMr33195OivB93Eh8nXv0ciD4en9sWl57iwKa0Rm2DuJm9h0geMsyGRsx7jyCtdzsBZ5K9p7f+/wUPSthZB+9PN1A7gGLbzpQP5VY0Voe0t7biA11HRVSoYKWbfIElUpbU7xjy5IBULS4MQkUmOFz0YhBHEJN9pQi6KQ8Sh2W8cSE3djkI2Zl3mrKkF/1oiZx0j2eg010tK83GqeXQ6CtLZg0qebjsuhw5s1f/C3U/DA+6Ln92rkUrkdhciskUW41Iqq4iBbkZZXbyVQz1ta9o/4fVgF51DwRpNi4bPjxBgy52lhjqnl6tDiEB44MV/nAV9v+XXCXNUVv4tjRX1yepjoSm05YvmLifEK8LbQO9npdk58R0wnAZ6pKqY5MUb5ZZVbG0pLMZWjuAvPj47h1239PS4q10XN8e3NAwtyvaciUnyzCDfL+4w3TeVRwNlCF8Vr5P7GUAbbGYbeSgSBMxzOEW01Tonfh1gwWKzWYgAXM3OSomY1qfRGccqJ56fMy2Pgbpy5PaV7KXJKQtwM9Yuo1tc9nzPCm3pMUO/a39uGXmgtRVRMYpoZEEdwyGid6Q6ygDglpODdrsNrya46jEIqzkUkxxL70N4MMT2sVVCL3hUU4mocG2SVcqFUyW2qLfW7ViG4HudASkLQLlWK7JDkmr3rOV3P0erV9FVjRQDX16pI69KmB6RuvfSvvV99ow3Fw/PveP15elxofdl1W4sz216/RkGSvak4U9h0Fq0NtcBmcDir6VxkXAS5pDbFYX294Hnc8lyLM9++7i9V+37KD8/a2vJ6szB43E5d0aIVoeqdFA6dB6MMuoodBo3M7HUjISPXOCaZYzaD59JblNPMi3gwaSqmBRxrVUklMS31/9/emwdbkpZ1/p93yeVsd6nq6r2hG4EGBEZciBDEgBlBMCRUEMQRZICBHsbhB0g3IaEhYBgi0IARssjmihOOgAwOuICtjOuIPS64tBjS1Y3T9FpVdzlLLu/y++PJPPfcqntr3yu/FTfq3JuZ5+TJ95x8n/d5vs/3K1nLIH5atsnC+qhRUeG9aOxYYxrJAY1qFPunhWdcwsxt706dn7uF0VCzlKUYpUkU9FLpcO1lErBbq0WJ/nw3HD1w4AC//du/zW233cZf//Vfc/fdd+OcY9++fXzzN38zL33pS/m+7/u+HY/95V/+ZV72spcd8zU+//nP8x3f8R27bv/KV77CO97xDj73uc9x7733MhqN+MZv/EZe9apX8fznP/9k3tZFj9oHprWT+roPyL/mhhLBoilCPU9RVs6TakU0ChVlRbBvkGGMonaBA9OSSVESlGZpkNFLPJOqZjPEOVP/8Btp2/bZ3txa8bo8aUoOfitV3NaNz0e0N6eWtjBrooX2JgjCT1LItU20ZCsuW+px5XLOqJ+ilCFRsjqbVKW4M28UbBZCAmq9xyoDugJ9WBfc6UI7obSr11O95nPCOJBWW55ZUrYQ/SKjZTVtDPRyj9E1h2LkkJYSTuEcmbWkPXHvVloxDZ7ZpMTFQKYNefTUSokfXPAkRlP4yHhaM6tKqjrOxSAjknFTTRBxNMmEE3mPsMWLqoBQSdmwbjJ5bTAxz3pE4QS1pY4HpgF9aCbfhaYNufHJnU+27WetLcdaxFw0IGWNQSpt8SLpoPEuUDefR6XEKsKHgKsDk7LRlmlKyKfz+7U48bWfJ9iZ06IW/m8/fwoJlHupXAuQsUuaoLYOW35roSlLLgYl7WvNeXfNY6UaH7fmO5hZzSBLWMoS8bgTti+li/TTGtf3FC5IB2DwW0rqNUS/Ve5rGxpQNGKM8vxiQSOBylz3JzYlPr2VKQk0mRrVUACa9zLqCWdNKehnCf1UggOtYX1a01NiZdEzhixJSBPRZIoaZjPXKC8Lh2dWe0onHlpRQS+JRCPZ/aqShhTXfMASq4lWFrjz19Wq4VbB+qzEuSA6VcjiTelI2ixoSifvwyYiGDvoWTJjMQZmRYmrNDGKxcnaNGG1l7La67PSb8Oks4OTCnquvPJKnNu6ZeR5TpIk3HPPPdxzzz18+tOf5jnPeQ6f+MQn6Pf7Oz6H1pp9+/bt+hpZlu267Xd+53d4wQtewHQq9MelpSUOHjzI5z73OT73uc/xspe9jI9+9KMintVhG3TjcYOS+rkmzG8MSkGMYlOQp1bIniEyqWUFGbVi7yDBasNjrzJcMcz5p/vWODitqF3g4KTATjQhFpgArty6qS6mQFttDkVTXzfQTxVFGZnELc5LRNqh2wmq4PzB4s29ndjam6BRcsPLEzE4NVoxSgwPu2zAFZct8YQrlrBJwv1rM6alo/aezalBq4KxC9LaHbzU1RHXe5GRF1K4yN+f3gBIcebI44vj1vIkTGhWhLHpLtOiypo1ei1aaULwHBoHJtYz6icUVcW0lDTaRAWGqWFpmLCUGtZmno2Z5ACkhTdBqYg2jkkRZcIKW5mINlNzKgTTnVCzVc5oV+9tGcjQTNbsHHClzYq5hKMq1Jpme/Ty/ZzVjn5i6eWW1Ch8sCgk+6oAHwOFd0wrOa7NNLSr8p34Ki1O5Nq0+7YByKI2Tft22kWCZivIdmx9h2ogVkJWbxV/58rawCBJyVMjti2I31NuDapJt5Y+oqN0uiVKE5HOvmHfkFhxZreNlcZyblkdZKz0UtCaA5tNWbEhVY+LinHhxMPOlGRpREcJWOZlHb3lNxW1ZilLRCcJGlqAkftthKoOlD40XogKH2BcVExmtVyfDNLESkddaujlCbkSgchEi4aUc9JtKB1ZYichrf7C8fExoEJgXMCs9ExmtXgONh/GqpbnQDUUBOTaag2JjmQ2Yox8h4wSUjlKzmsJ1ZTAZMUSVUKipMwaiY2WWgStuWI5Z6WfUVY1962XTEIt4xwU/TTh8mHG3uWMpUE6v15nCycV9DjnePKTn8x/+k//ie/8zu/kEY94BAB33XUXP/3TP81HP/pRfvd3f5ebbrqJX/u1X9vxOa677jruuuuuE37t/fv388IXvpDpdMpTn/pUfvEXf5FHP/rRjMdj3vnOd/JTP/VT/NIv/RKPecxjeOMb33gyb++iRWI0/czgQiAxskI2vnHhDVpSrs0EazVERCK8molXT+ECS7nBKo3VCqM1+xomfvCB1WHK+mbJfeOE8bQmnxRszrb4Pdow13qIRlYzUrbIGGSGSeGYlRWTylM2K1VxcJbOn7LlMrBVYz+X2Imc3bZvT0pIS0hUZJBEpiZQ+E2+dqjk7+98AIxhPBXxxxjAWjFi9EFWXnONlCaaam+szT0F42USONWVervGam8EbVByuoKpxfPL2AoO25W91k3wqCTQMShK5ymd+CUpJYJpiVEEpUktpEasFdLEkGhNUXk2ZzXTqmaYpJIB0WLeOC0aw9l4+n2/Dkcb3CzC7/J4JxzvWFbNc4VaOHOTmePAeMwoV2TGoExjhOnk81LW8rM4ti0vrc0enSkO3eEZnxaHZ3jaclab1dKxIVF7KJV8D7KkFQAFFzwqCjE3SbWoRCcGYxTD1JBo07xBxSA1ZImhlxuqKmCioooBozTDXkKaaqYzzyBJiH0onBfFeiPEd2M1o0EixrJoMBJQhBDRsWHBI47zly/3GPUSjNLUTkpIlZfAc3Mm7uzWKHrWomJkmCeUQxG2XM4tewY5/dQSlXTMLmUJqlHhfmCzwFrLgXFJHURd3/vAZFaxWTmmhZPMmIUsES8/m1hGqZSNvfNsFiVFo8BaNrIMPSPZ1BAjDkVmND5IOVTU/KW9PLFaOsSAYZ6QWoWxml6asJqnrAxSYoQDkxm1V6ioUdpy+YpieMVI7GqU4vrVITdevcQwT/AeitozPIuBj4rxxF0v/uiP/ohnPOMZu27/L//lv/DBD34QgK9+9atcd911821teevhD3/4SQU9L3nJS/jYxz7GlVdeyR133MHKysq27TfddBMf+tCHWFpa4q677mJ1dfW4n3tjY4Pl5WXW19dZWlo64XO7ELA2KXlgs2RaiRbGpKqZloGNWcVmUUln0rzXdAshio9QLxOvIZAb1qSUlYBW4jg9ntU8NJ0yKSNFJc7MRkUUIoEeQiAShbRrFYMswWiLVYFpJe6+67OKaekxVmGQL3xZS1kkBOnuqjj7ZOeTRbvazZBVldLQa9SaQ2h0MRr+RusZJcRYaa+FhjjrtsifodHVmHByAUpbmmu5U+0t50RJlieDduXfZl3m2T/bzB80hFMrE90gSdE6UvuAVpossajoKQonbvQVrJUNz+oMn/v5BNv8ZDTWA8lWdqT1anKusUPx8r1pSzOLge1OwVoblLQBy4l+19qxbf9vtWsqtjJLiyXCnY5XC/+nSHa4l0MvMdKdFKNIa6SWNDH0U01mEwa5wVjxHoRIqhs3d2PwMbDaTxilllQbAkHUs5OW1Cv3Ou9j41kYCA0Zflo5Dk4qquCJQYIdqzWzOpInmn1LOYMsIVGKqZOgqN+o3/fTBKMih4qatc0Kj2RMBolhdZixp58z6lmGWYLRinsOTbl3bSqyIsBDk5KDGzMOrBd8bXPKrHaSlY/S2Wd1pKiF05NaGPZT+onFGjE3DYgo4HpRo32TTW4WFXuGOXuGKZULHBxXwjdE6A3rpSOGyKif008Nzntc7bFJm40yXNl0ZI2yhM3CsVHW4krvPVZpRllCnltSa7hiSfza8tSwlKdYqxlmlj2D7JQFCo93/j6pTM/RAh6AV7ziFfOg5/bbb98W9JwKJpMJn/zkJwF49atffUTAA/CmN72JD33oQ2xsbPA//+f/PC7+0KWEYZ5Qx4iZicEdRJyvyVJDlvboWU3t5AsuvjqSCk400HR6+RioG/+BLEJZe4yB5UGKi5ElMjGtcxaNop9IDXjmHONSXK4r50FpVnoizOaixkVFFWGQZmTa4dpEvNcYFRmkon8yngVUvX2l2E4ANVsr5pSt9Pm5nAzb1fQUqXtbJHO1yPWYlzQic2PV6EXGXS08T/t+T7Us0752OwmeTbQkzsM5H36BIGy0BILTGRyI1VYGAI819dz3qODCCX5PNxYF4kLDB0mMlFaVEuXjWQEb7P75Vxx9/CNbJbATCYjbbCwwdzH3XpocFru/dmtYWDx+Hpw33V8xenqpQmmD1gatRK3PaIvSsFk4rPKMa7HISa0mz4DKkRjNuFBUThSq6yDea4PUctVSzlUrA/YNM5Z6Cb3MkjQLwLqpufsYmJSeg+OSzVklmQ8lAULlI9OiZrMUHTStNEmuWOln7Gk69q5qgijvw1yccpCLKKMLkY1ZzT1rEvDcc2hGUdfk1ojhau0poqPynqKKRCWjYdG4oAiItUqv4eMkWvR5XIwiPukh1QYXQSlN32iSRJMnFte0e4UYmMxqKh+ZVVvt/ZuzklmtSbXMAXVdYowldSnjuqJ4SPSwigAheuo6UHnpvOwlFZcNMx5x+YgrlnOGvZTKi5yD1SI+ecG7rOd5Pn/s/embbv70T/+U2Uxog895znN23Of666/nsY99LHfcccec39NhC61IYc8aprln70BcuFOjyRODUopDjefWpHTUXswNXQxoFNPaU1fyha59gCDlh1jDnr7mquWMldxilCZNFIm2BCIbs4oHxiW1n6KU3GTqEKiDl7b5EOhlQsTUVjoG6lok1VMTpH6dSSeC1QVMQNVbhOcsFVKs97BZbd1Q0+Zvi0THw7tQ2u4bQ9Ny3Xwrypq5uWnN6Qmc/GH/t8HH4uQT2GpvbdEGdnHh93Yl3JYnTvT82gDqbAc9isYEtslozTNczfa2tNcGdot6RiClvXYSvpQyOzuhzZYUyHXJvNg+tAHt8QQ1R/t7+/xttubwaWkxG2PbLJ2RsW0HzQC9VBZTqWdu8hljI2+x8FxtIG+QQMkayFOxveknEuRkiSGzRjKmmSUxBouUWhSRzVJKPaFRStRJUxdueDA+Rh5YL4lKs5xpvIMHSjF0vfPBTfYMUkZ5SpY1JRzExHepl3L5UsaVSzkPW+2zVkhG2mpNUXse2Jxxdy1UAKM1S3nCSj9n3zCjl1kxcW3sQ1Kj8SE2buuaaeOx9uC4YH1ao4HVvmFtU7wRfZTOPaMTVgaQ2YqDY1GodiGgjahbL/csWtvGMV0Romc2i1IOVHK3qZzH6IhJdGMcC5uzmhACBunganXYUi2ZJN/oTpEYelbKy8EbglJMy0DlC1JrUA0lIqJIjGJoFcYY+lnKILMQFTGEprMt4qyc5wXvvfWFL3xh/vgJT3jCjvs8+OCDfNM3fRNf/vKX8d5z1VVX8ZSnPIX//J//M09/+tN3POYf/uEf5o8f//jH7/r6j3/847njjjv4x3/8x5M6/4sd1miGRtNr6ri6cUqGpsOr0tReE6ImloFaS17cGsVAicVC9MLe96mm5yVtbIympy2jTJGmmr39hF6WMCkd968XZInhmuUeWkv313hacXBWUjXdXj1r6K0aprVoRhSVZ1w4vFKkWpEmGtcPKAuJqZk4T2xaeH17I9UwspJRyVPop3IDnVVi1+CD8AXar5jVzL2VxAOm/ZuGXNLFdZCsQ+skDFuTwYkGDIvEzvbYRY2LdnLJEuaqz6mWgG6YJVhjqbwXFVYgNI7b00q636pa3ktqpe1WqYYLoeR337Yrh60MSdsldzYzJoEFTaPmPdN06cAWz6Nku9Bce927gGc72kCyJf2313UxYGkzjieTIWxJz4tZx8XPLUg5shXja6o/2Eb5e1oFMguDZhFSNt/F9rtHFO4fyOe+/dxmxtDLLHlqGKSWUW7o5Sl5YsXoNMq3KQYo65rCyf0s02CMsJaqpiY8yq3IbkwdPoq9TlUDRpNbReE9B0vh4EzrAOsFg9yyd5iRGEVZO+5fF+L4tasDLh/mFJlno3RE4OrVPleOelTOUfsoXYUxEJvSf4yRzGqMEmVsUeKOzffXMXN142OmyLA4Inu0oayFflA4Rx40AY2PCSv9SFF7iIosS1nuJaz0RDxs5h2xGR1jI9FrVITSB5SScp/wKRPqGNAOSh9RKFKTEK2D3NAzijqIPpQ0aCRkWrq2QKO13Ps1WhoDGkuRECSYS7QW78ehlLfqIAtZaxUhRqxS9BJzYXtvra2t8ba3vQ2Apz3tadx444077jedTvnrv/5rVldXmUwm7N+/n/379/Prv/7rvOxlL+NDH/oQ1m4/va997WsArK6u0uv1dj2Ha665Ztv+u6EsS8pya92/sbFx7Dd4EcFodURKUSvVdCRolJLWCd3cLmMEpZTccFJNlmpya+glhkFmWeql8xW7C6EhJAcSq7lskNFLDcNcuht8EI5G6URErnSymtEgj32knxqU0lRevvQHJyV3Hxyzr99jbUlqxkUNVV2zWYmgnQuBoqzJvdT+jZGs0mUGZmXNuPTkRjPop/NyktKK3GiChuACVQRiJAZJwdZ1YFxtEbKh8SHixIxBW/7Kor4HbJGy511FuplUVOu1o0mswdqEpdzig8XFKO/XBRLrGTTJ1Va7zjnXKLWKCWpsVmq1E7XdNntiaMw6mwkq0U07baPP1LZQu7B91a+bcW4NRNufxaBuMShRbA/s2uuRGckMxIVgJ0saXR0vpb6IBH2w1QZ+JrrMdsK5yISdKOafG9V8bpoyl4lbAUWIW6KAbbC+GABptixKNMIHCkiA4mLzNyXjkJqtxYJSUqqtmte0iazwiaLwW7fyFUBuLYNMTFtr7/FBdIZSa7BWVvuZ0ZQ+khpFag2p1WRWsjuJVvQSK/YWSgRTA03GQAWMTuinTQpJ9ZC8scL5SB3EiqR0gaBiMzGDR1rWFUKQN0hXVVEFbKIx1pCnCdYajNWkiab2kYOTimGezA2d2/JXYjQxRsZFzcFZxdpEyl0GuQ+t9FNWBynDPKGoPbPNks1CtNPKWko8GoUX/QHyxKK1h5iiC9DRo5UsOvN+Bk3QMMxTRlmCauQJxqUsaI0Cl0VmVU1RB6y2uCgL335iiD7gGv5RbjWZae/2CSFE6hCpa0fwEWs1MQScBhM1w1TTTxQ+6HkkkQCJzeglmlRrBnnCILH0rKV0QWxhQkUvMawMUka9hLzVIThLOK1BTwiBl7zkJdx7773kec573/veI/a5+uqrefOb38zznvc8brzxRrIsw3vPX/7lX/LmN7+ZP/iDP+CXfumXGAwG/PzP//y2Yzc3NwF2bYNv0W5v998Nb3vb23jrW996Im/xoofRkkaeVR6j1bzLSzXtsTStialV5FbTTw39VFZDw3y73sJKE9iA3AyOFs37pq4rWRsJmNrjtIKiDvy/gxP6WUpwjr+/d52NWYXzUJQak1iKsqZynoGVEtkwy8it3ByjktR3nhoGovTFbCYmiXkiN1fnA95FfBCdmGkd8F58YTarCu8jdeUZN35FsxL0ghZLO2G0kz9slRjswu+L3JwWLc8Ftmc7pGwQMVFMF1Gy8rXISpIo11cpIVXGxkXZ+5yIEDBDEC4UvUjwgbJyjKuAUuLKnafiTp4aLVmUOjCrKooyNKvVrRKh0SI6luhW4kBOsiXPlk5W8e35L2bSIluZpgikiWpEyeSJgg/UMVI3F6ktM7ZZgNb9u1UGPh1ox0bTlGeaCV5c1eV9umZMnJMOwrYL6nxAmwGbf3aQoBmE2GzMVqAuixZ5bLUEtpKd0TLha401Gq31ApnXo7UWTk2Q7iltJDMcI6gYqWpH4UNDqtUS9CjpLKq8WHRoFKOeZCOyVCwziKCab4v30s7to8g0WKPIEgmIkuannxkyK+empHsCq1Tjlydt06r5VrkYUMh5Svlc7HUmU+nhC43noKxvIr4JfBTSbFF6ZJKP4hHWirSKnYq0ck9KRy+1Tafg4sSt5sHQ1ctB2rihuZ9u3QdFk0jzwLpjWsuiLUYpRXkivUSLpQRKutSSVDqpkj5l5RmXDh8iVouqdGhuPL3UorQjBCESl85ReU0SYXWQk5tmIUTkoU0pkVmr6TfZs9RojNJU3jOpA1lpKLyTW42CGDVZZrl2T588S/DOk2gzv6cZrehnQpWQjmAJqDSwMSvJU8ueYcZVyz1W+hdIy/pueO1rX8tnPvMZAN73vvfxxCc+8Yh9nvWsZ/GsZz1r29+MMTzlKU/h93//93ne857Hpz/9ad7//vfz//1//x+PetSjTucpbsOb3vQmfvRHf3T++8bGxmkjXV/IkLSnoazFwZcgK7ZZISaLg8wQm5uWUYpBZneM1o+8GeyO7VknIU8Xtaf0TWtmDCzljdBazFgvHHc0dgNZalnR8FDlCFrT72dcOUzYM8rIrKVnE5SJ9NOUYW7xEdanFetpSUTEykBuhK25Y64MKzFSOzGDXC4TprW4VptJyea0lpt6aJR4lXRdhSgr4kWH8bYU0PKMFj19jsjy0HQtGehn8t1IG3PBLLUM0kTKiKlpDCaFsNhObCFCHQJGySp35sOcKCjOyhEXAzEGsjQltxpjJA0do2TZZlXN+qxkY1Ix85Ho/XwyS5tZVDJIErQsJZY8E0PNGCNr04ppJSW4OkiaoQ6BovRMa4/SqnFatrgoPlQhSIBGlGxd5QMq0JDnI2Xl51mkuiE8lxxf8NFml9qAdDED1ZZ+NGAj2LQRx2sGJ2nSbjMfKEtQjQCbWnieNrCF7cHYYiACHLMc12YC26Du8Bb79v0vZgoXeTDErX2MliBOh61yEU3gKfIHijyBPEvppdJK3AZFws1ROBVlkjZSkvE01Bi2gqcYFGlqMN7MBfqil7Mz1pAmEW20tEX3ZMU/yKwEMMZgDExKx8asRvmIUYo6DUSQz7wVmY1eakmNdG3ZhlPimvZvuXhbS4jaBTJrt+9jVFNq01gMZe1IjEI1OjvtWDkUsU1jIrwWRZM9an6aHq+5n9tuBNxj3f+s0ezpZ0yGjnptRhXEcy23VlrulRIrEh+ISr5/iY6k1rCUpWRJzbisKRvDzto1nKEk4Yp+gjGSCXI+ctlQvleJNuRWyM0HpwVLecD0DaM8ISJlrJZfoyuYVp7EKgZZijGiD5Raw75RjyuWMuoYmFUiYVL7OA8y01TKWSFESufoZ/J7nmquWMq54bIR/ezcGEKctle9+eab55md97znPbz85S8/4efQWnPrrbfy6U9/mhAC/+t//a9tQcloNAKYixLuhnZ7u/9uyLLsqCKIlyoWHdltIw1ax4Buaie91EpqdyFVe7qj9TnvaCEDpJUSjyUX+Lorlqhj5Cv3bbBeVLgQWV1K2TvMeNS+ETfsHXLFygAfYnNjkJvUZuUopo7oI31rKF2gqkUwTClIlaYODh88/SwhUbLiopcyLmsOjWshYueprOBimAc3IXic83gfZJJs2mtagnXrQt16k3nfaO4gk22ihYvTsxptEzJrGKWWYc+ynKdcNkrJ04TSeVxTD08Ok2+vXSDXFqs02kBeS5DjQiQ2K04hDksqW6l42NgZYkjYN5J6WQiBKsRm9Sevp5CbsfeBxMjzTGvhLOSJYSlPRDunFNHKEKHwnjJzjHwifloBvHfEKKtb32T4hH4hBogocS7vG8Owp3BeOv+cd0wqGM+kI+5YWZ/Ilh2BXvh/HgxZySrlKWRaiXFoKr5zAemi00UhWcAISfOCrUdaypbQ4WJX3eHlvMVsX3temq1sTWrkPKzeyip4+Yjho2Sb2mPbwKZd3bemomnSmLpai1JRfPWayUo3AWSKjGMvM6TWkqcyjcfG/sIYSXn5iEzoTVZAN9mb2jcBlZLzyRODijAuHdYobBOBK6MYJLJQya1MrEZpKddqTWJlQo5BnMFzo0itJQTRBIvIZ9w01gjOBzGtbdKGPqpt311x4oi77mOUItOK9aomBAkeYohEpUhS8T4LESmrNzpkVsv5ztVblWq4MrrJQp8aFyVLDFeu9NBGY6xiY1aTWovzno3CUZQOFSE2ujy5NU3sFVntp/StZqOsmVQOm2lW85Slfsq+Uc5lw5xRbhnmliTRjAvP19am3Ls+Y31csaefs9zUkjMjfosQcS4y9Y7NiYy7ZP0lmzRMDXsGKXtHPUChnCNamvKgFpI0EKMEWFErrl4asjJMsVo1ZPD8nAU8cJqCnje+8Y28613vAuDWW2/lda973Uk/1yMf+Uguu+wyHnroIe68885t266++moADh06xGw225XXc88992zbv8OJwxq9Y4q2xeGp2jOFxQxQW3aT8lrKN1y7hyuXcu47OGOjqsmN5mGXjbh+b599S/m83LZYX69jQPcMg1Qm+tZV3s1dhuEKDZWTiVk1N5gIrOQJw7RkvTAUpQQemZUafoyRrFndeB+YFTUT5whRHLFrHyhr35TTRDjM+UgIiiRRJAoqF5lUntoHCR76lpVeyjBN6KWWfmpFiVUpqY97T1WLVYhCVuKZ1QzylKIOxOAZ5VYMN13EE4hB3luiFT2rjwj62okjNdKxQkSEAmNsBCkl/d1PhSQqZFHFaKDmflrtdTRaKupOFAAAR3pJREFUzbMCPkRKH5gVjvVZyQObMzZnDkfj/wZApCgDhY+k0RPq2EzmsQkGZKJyXhN1JfyBQoQHD+dWKRr9GiSQ6OUwyjWjNGHYT1nOU4a9hEFqpRvIKFJjqUMUETkXcDEynpWsF45+Zljtu6Z0KH51tQ9zrlPr2N7OjSEKN8qopiykYJQ3HkMaQi0dRps1+KasJqUB4dDEltQdIGjx6SKKvtNSpkgS06irKxKtCD6gtSFJm3FT4FygDgEVpSPHEEkTQ6+X0NeWpZ4QhGNUWKsxCqzSImmhJGgISFaoqEXtNza5NaOEJ2aMIk8tiVKMS8ekcqSJFmPZNljQce7flBndiNopZlVgWjqWeyl5Zhlmdq4BttP3MrFyzzmVfQZZSj9JuH+zpHZOMlhKURSOGllw9BNNIJIYCZ5s09zQ/kCc8xhPx/0vTwyjnqWsM+o6MHEOHRWJFtFS7xU2kw6oYS5mvT5KFq6fWPJEs9RLWOqnDPJkfl/e6R59/WVDppWjcuK1FWJkfVazMa1E58d7NivH2mbFKHEkRubY2olQY6ItvVQ64IqGnG1R1CFiVCQ1hqrhacYYuXzYY9Szcw7R0jng8ByOUw56brnlFm699VYA3vGOd/CGN7zhlE9qNyx2bP3DP/wD3/It37Ljfm2X19d//defsXO5VHAiJaozjbbstjENaKVY7ieMektcvTLAe88oT9i3lM/r7C2OFry1aDlFbSeb84FxWTMuPOvTigcnMyalZ9hLuTrIDUAr1QiaSRmsaojFEfGxSY1uVExlxd1qHLVKwyBcg7JylLWnaIjTKtDcVFN6ecIgM9tu5O35trwC58M87ZwnWnyVYqAMsjLPE0M0Eec1dQgM8oTVQUqm9UlNHEYrlnoJK72UlX7CqJdsI6gf3hF4+HWWDkHHwXHJvWtTHhqLCvekqNgoasazitKJ/1FZOmF9KMXqKGXfUkqoIxuFY72q2JjVjCeidIuKZNYy6qdctZxx7Z4hl496DLOE5cQw6GUs9ROGvZRBLgGkUmw7V+cDa7OSew+VHJqUjMuKjWkthrpNFiQ1wjVpJ43KBWalo/Qe17z/REnppCbiarmwSgkZlADrs5rNWUXhPc6J7cGhSUFZBkITPLZk80Q1vlFJO/HKhCgaK0IMzoyUGJKmY6bN1lS1jF2M0M8sq8OMvf2UQZZijRLVYC+dgLZR4tVNsNTPNINMmhPKOlC60HQGmua1tj4npsmM1N5TB8n4DbOEYWZZ7ScMeklj7eCZVY7CS0agn1hWjpItPtbn6WT2qb3nnoMz7rh3jf93YMpaURKjLAJ6qbTDJ1oyOrPakxQ1VoFXigrFUt+yZ5Cetsn78Mz6/esFa9MSazTXLvdZGSRcNsxYHmTbrpEPsfH7Msdt2JlaTWq3C2Ks9lPGg3S+MPRE7CjFexFKTK2ZlxEjUThOPuDzrfDBhzgvjSkgqkgvsYwavaNRlpyxqsCJ4pSCnptvvnme4XnHO97BLbfccson9JWvfIWHHnoIgBtuuGHbtm/7tm+j1+sxm834vd/7vR2Dnrvvvps77rgD4AjuUIcLG4s3h41SyMSoyFJuGeX5Ub9QJxO8LffT+STtw8pxT+hwdOL24vPEGDkwKTk0LlFahBy1lhbXEAIr/ZQ9w2zX91U6L5pKzWSyXlZUlW+6vOK2FWE/M+wdZuwZZuSp3fWcjndyOZVs314yLh/lDFJLL5ty/3oJBPLMsm+Qi/eYkoByKU+4Yphz7d4+Vyz3KWrPgc2Sr63POLBRUpQ1M+8IaAbWsDRIuHalz6ifUjXXx0fY9J56Kl5m/VQzGux8XZf7KVcu9dkspONFglbJdPWazNCxxmJc1KwVFXUtKaDFwHFtIq5Uq4MMqzXBBx4YzxjkCYfGFWUtfLGohVAqZUMRscszgwGGvYS9g4zUaIZ5ylKebCt1moY31W/8mYZ5wrCXkKfCv1sM7NenjgPjgoPTkqIO5D6SWCH4j7KEQZOFGfUk49jqquw07i3B2QUJxNvXWvwMyfcpnrVs8ZFI2DvMeeQVI+7bmHFgXDIpJYtX1fL9NUrNxywCVhvyxLJvKeOKpZyVXT47J4vFzPp1q4Om1Cbf3cOv4enGsbL6wBYhvPks74Y2ELNazzO8526cd8ZJ2VDA9oDn1ltvPa4MT4zxqCagMUae//zn86lPfQqtNf/0T/90RMt7a0Nx1VVXcccdd7C8vLxt+3/9r/+VD3zgA4xGI+6+++7OhuIixeEBBoiHS91kPY6nU+xs3FCOhXFRc2AiGtKHrxxFgyPu2Bl3OBYnk522neiK8Gyg9oED45L1acVGU1JKrEY1q0XnAlUM9IxhuZcw6qXsW8pRyHV7cFLytUMz7jk0YVo4tFLsGWRcsZSRJIb1ac3arCIxmj2DjDzRVD5SO88gM1y7OuCyUX7Uyet4gsGdjtltLGaV566HxtK6i5RFH9wsmJRuHqCtzWrq0JBSU4sHkfxPZOLtJYZUa/YME4a58OpWd5HxP54Jpz3fdqI9/L2eb5PW6cbRxqvdfr7cLzrsjjNqQ7HI4Xn3u9/N61//+uM67u677+aFL3whr3jFK3jmM5/JDTfcIG1tIfDFL36Rt7zlLfz+7/8+IB5aO2n8/NRP/RSf+tSnuPfee3nuc5/LRz/6UR71qEcxmUx417vexS/8wi8A8BM/8RMnFPB0uLDQZm6cDxyaFNy3UfLARsH6rJWAh8QoLNtXme3KLbOSfl8dJFyxlHPZUm/XFfyZgg+xIQCzLRgR01ERZyxqkQxMjxGw7JTJOpkJ+2yi1YSyRrRQUJ7Ubk2ulQ5UZSU8m9KhtGZQOgaZ3bYqnlYr8wxL5Tx1DDgfCNEzyizLg5RBJkFjDpROU9V+rrVyNLPDnbSsjoWjZRWVEqJsYqQVuvQeaw1DpZjVEvBVXoIhoxR5Q6rtWzPP0mRWNx01llEuf+s1zuMng/Z8zzXX4lzhfCrhdzjzOOFMz1e/+lUe/vCHA9JttW/fvqPuf/PNN3PzzTcD4sK+WLLKsozRaMTm5uY2kcDdxAlb/M7v/A4veMEL5l1ay8vLjMfjueXFy172Mj760Y8eNau0E7pMz4UF5wMPTgruOTBlY1ZTeGmLXp/WTKoKYiSzZq7TUTox2huk4hgs5FHFsGe5bs+Ah+8dnPa09dHQZjqK2pMnQg6cNr4+923M2Cxr6iqw3E95xL4B1672j+v8nA/b2v2VgqxZpZ7revrhaDNdk9Ixqxw0GSkfIocmJSFEhrmUV1b6GWmz0h5l9oj3spixODSpuH+jIMRIL91anfsQKWvPtHJYrbhmpc/eUS7qsWchm1G5wP6HxswqL2NUe2a1o6g8M+eZFo5DM8n8tSUF3chC9JqgRzhFmstHkgFc6iWnxbCxQ4cLGWcs0xNC2Pb4/vvvP+r+4/F4/viKK67g53/+5/mLv/gL/vZv/5YHH3yQQ4cOkec5N9xwA095ylN4+ctfzlOf+tSjPud3fdd38aUvfYm3v/3tfP7zn+fee+9ldXWVJz3pSdx00008//nPP9G31eECRFF7Do1rqhBIEo2PEWcV/Z4lqsh4JlmCPNW4EIRcl1pQitIHcmsYZhajFAfGlWRSlDprglltpoOmBXxSuLn/TlkHEqXRaSQEz4MbJS4Erg3xqCUZ5wObjWhZK+wYIhRONHN2ChbONConytstCbpFWzZZ1IQqGhXvWSX8ilFuSY1mkCX0m3MvagkYDs/QtCt2oxUbMxG+k8ChyapVogezNqtY2yypvGP/QzlXLvfY25cS0ZkmW6ZWs5wnzCqHIkoHYCKq4zpo6iDO3PNOOt/qwsRt3UO5lcDH6rMv49+hw4WMk+b0XIzoMj0XDnyIPLBZ8LVDM8qGy7O4Yp5Vno2ixodAZg2lE2PA1ugva8idiyvnPX1ZMS+2u59pHJ7pODAp2Ji5xrwPQDph2oltuZfysL2DXc9vXNQULuxYqihqT27Fc+14lbJPBUXleGhc8sBYiNrjosZFaYcfpZKhWB1kjHqy9tooHWsTEUacVZ6lNGF1mLLStOK2gYhvhAxXeumuhPL2s1H70Bg6OtaKilkhfm4bhZRB9wwSrlzqsdSToGqUJ+wZZmc08C0qx/9bm/Hg+oy1WS0t4U7I2YVrJAiUZlY5xnVNz4hGSi8Rkb5RbtkzzFltSO7nQtW2Q4fzDWeU09Ohw7mGyOQHQuMeHAhzpdo2jNeAa/aLASJic6CUEgHXwxRWafaf1f6ItvczhTwxZInm/vWaaeGYFl4sD5q29mEmNh++UX49XP5+Ea0WziK3Y5GkWTrP/eu1dFY0F6hnTSNgdnqzG+3EfmCzmAtDOg+bRQ0q4vIto8XhVAKNUZrQTyz7hhmTyglh15o5ubmFbrIgu6nhtlYq1sr1qptSUgiRoKSdVhHZMxShtDQR3gxK9E8mpZOOpzMUSOSp5dqVHrnV+IMTDoxLlFJcNsyxOqKNwnlYK2qWQjJXy26lAvYOMi4bnfmsVIcOFyO6oKfDBQmtlMiia0P0vnH5Ddsk8lt9F2M0LnoCor1hlZJJ9DCF1dSK5khr23CiBNaTgTVS7ljOLZOqZloHIoElmzTt3NIiHJw/pvz9lnI129qRD01L1icVB6Yls9pz+SDnquWc5X7CLDJvpT6dGYO1ac24qLFGU4WA8QptRIF2bVKxNp6IcSTiy4TRYlJoxMPHeU8vE7n9pTxhZbCV8VEL+jqHo+UzFd6Tas2scByYVWhEHmBcOiZljTFSxuwlFs+it1LEhTMf+Oap5do9lj2DjAOTEuejlO+0wjW8I6MVw8xKs8dpkgro0OFSRxf0dLgg0ZJZe6mh9E5k+b34QemgAY+OsfF7MRgFlfONOaZCH6awmltNnljQaq5DcraQWsPqKCegiUHhYmCQJiS2Jd+2JqVHl79vxfYqH6T1eVIxLhyTqmZceWaFRwytpBsqAisD4cGczuxG5QLrRQ0oCdC8/G1cuMYnKLJZO7JGLHFWiTJuPzOiU9SUe7JSY5Zkco/QKCA3pckd1HAX+Uy5texbEq2VjdJxcFw0eylGecooE7VrYzTB+23eSsR41gJf8STa7jNnjWKQpucl8bxDhwsdXdDT4YJFnhhWhwnTsmajFG0T5yLTmWNa1dK2rhXRi1S6R1M23liJgkxrKhSjnmWln0iwA2edGDp3ts8MS/2EtWnVyP23QU9oDBGPLn9vtCIzmgOTimnp8SESlfilGQ3D3GKtWA60pRzpYtIErU46u3G4zsm0ckIi94HCeQonrflF7cXDSUfxvtIRE7cIzWL+KIHrcpoSQQIBRAm28oFD05LLhvmunCVxnVZsFhXjwlP5wJ5+hopglWLYS8SEMQbJoMXGtHQh84c6u4Hv4T5zx6MzdbiuzKI8wdnqROvQ4UJEF/R0uGBhjWbfICdRaq7TUzvPIDeMer2jWjeEKEHFnr4YeWaJfBV2c4w/02gtNgalZTKr2Shq6d5SDXdFHZ/8fWLEOHFSOmoX8C5SB3BOuEz9xBJgoZwDlfdYfeJlPefDNl+zQ9NS+DOV44HNQlR5tcZoKOuIDwFjDXWI+KhwofHs0aYhJ0PdGEBmiZg9hghFHVBao7V04hmjjpCjaPlMSsHarGJzVjecKItusl8bZU0vRqwB5aWUBRKAbXkrqXPWEXUsTaCdrndROKaVw4VmfFPL3mHK5aN8TsjvskUdOmyhC3o6XNCwRnPZqMfqIOeGy4bHpcjc2gUUlXCAgpKJL29WyKULTCt/2j1/jqaYu5v/jkKcqo9X/l5rRT/RzAwNtyUQQ6CXSAChtSbGsK2U43zE2XhC2Q3nA4dmFYfGFeOiZlI6ShcYF55J4ZjVkmXpW6iBaWOkaq2YnxIDDkVdezRKAiIMLkpmrlYKnUSsFdJ5zyoGqWGQGDTqiOCs5TPVLjAtpVOvFZscZAl7h5J1mlaePGo8MCscxMggM0Qjuk2m0cQ534T6drre08rz0GbFpKgxBgaZITWKQ5MK58U/6/KlXtfd1aHDArqgp8NFAaNlsjoeDDIhkJa1F5Lv1LFe1Nx9aLJt5RyiEKHFWHFL3XnRR+lk9zGNj1Pbtt124pyq/45W4sA9yDMCGo2jtoYQA+PSUXlxLl8s5YQYserEshtFLU7KLjS+21pKiXmmqYNh2WRipOkCKooNRuk9D4xnhCAdZrNSghAfQdHoCmmFTUwjIGnJE8s1KwFHxBg9J3YfHpy1vkDTSgRKFyd5oxXDPOWKZXGG9iFiAZ1JbauXSvfaSj89bzuidrreMQbSRBOVBSK9LCFJDFli8FECzTPdidahw4WGLujpcEnCNeWOjZl080yr7StnpeKWQFyIGC1KwUQoXcA3BpQns49orUgX0uEk3XZVfrK2AHN+UCWWDj7VhDqggia3hrVpjSKS5QqiwjnoJ+KUfryv19pnOB8bfkkk+Car5aXMkqWGXpqQek9ViT1EVXt8CPK6XhG1RumAqz1GKVHMBlTlcS5hmEHfGpzzTIqa9abD64pRfkRwZrQi0YqyFlXnw2O3SGTfIIOh7LfUT7e19p/PHVE7XW9Xi8I4SBYRtb1kaYyiqEWd+mxKMHTocL6jC3o6XJI41sq5bDyvFJAaRd0oGqtG52XRB+tE90EpepnFWH1G9GFaflCrckyAwnmciyQafISyDhhtWOqJLs7oBLIboeluijE0pbKwTSepeUukTReSSzwgreRGaRwBfCTBkhqDihWV9/NApfaxKVMKx0YBKm797IZ+akmsYbOo0UqTGFGjdiFglCKxYvXRa0QIL5QgYKfr7Qi04vjzpNdCybKVNwghnFUJhg4dznd0QU+HSw7HWjmDiBQuOK4QIpS1QzWlqeCPnH2PZx9Q+BipnBBqz4Q+zOH8IJSingV0jAyzTHycEmnlH2QJ1mrGlaP24bjapFv7DKU0SmlQGk2c6ySFxu/LR6gaYcDaw1I/o58lbJY141kNwWMNLA0sZSX6NERIE0WeaPIsIU0NSWLo5wlLg5R+ngoROsQjrlGWGPYNU2rvKZ2jneYzI/weF+I56c47Vex0vS1aypRNt/1coOow7Smt9VmXYOjQ4XxGF/R0uORwrJUzNAvmhZbgVruFGDFo4k5zyHHsI1mQeASZ+HTrw1ij5/ygq5e3tzfHGCnclg3FifpzLZbQpCykqMOWTlKsIoRIUOJ2rohkC+7pq/2MtbykctJtlWjp4vJesm5WQ6/R0RmkrT2EbXzSOOo1GuYJ+2Lk0FhMO1suVOkCMUaWT6CMd75gx+udKDKrqepIaD5LWx1oIPYlIrh5oQV5HTqcSXRBT4dLDsdaOUOzYF6YKGKQzijVEEYOb5k+7n2iiAzK66ojfk73qrw14VzUdilqIQ8vEr+NAqPNrmaeh2OnElodIkUZqCpPJBCRVnuthEejlCJNNApF4S1oRxsZJhqS9nQiJNaQpwlZZkkTgzb6uK7RYpZro3SUdQAVyY1hlCfnJUn5eLDT9VZKU9ViX2IMWAUGKQGOcks/NedlJ1qHDucSXdDT4ZLDsVbOIEJ2LMwVdYxk1qIaiwdrjpx0j2sfJ/yS1KptK/MzpQ+zqO2yORNTU+cCg0w6x3qpxRzm1TWrHKERDdxN9G7HEloM6J5hkIqp66RRfc4TQz9tLqZSVCGQ1cKbEn+0gFWiv3M6rtFilutsGKueDex6vZdSZnlTulOKxBgxzR1lnU5Phw47oAt6OlySONbKeafurUQriOCjoqrDjp1Zx7MPMTIrHYlSZ1QfZlHbZVY5tFbk1nDvtOL+jRlViA3/qDmgOc/KR3IrejnHEr3brYTmQ+TQpGKzqKm8ON0XtWNtWlFWkUSJqeasrtBAP7Eo9Gm9Rm2W62LBsa53p8jcocOx0QU9HS5JHM/KuXVrF32dLXXnRQ2ek92n1ek5E/owbSlrfVqxNqupfSC1QlA+VJWUtWdaBSrn5ufmQyTGiItyrGn6f1Krjyl6t1twoRWgYFI6FLDUeF2pccnarCKGgIqQNJN0lhh6qSVLNYnWpEZfEBo6ZxsXWzDXocPZRBf0dLhkcbwr57OhyHw6VuWH2xQ8tFngXMBazd6BBByl92gtvKKoFSGIL5fWCuf8vONMabYCkZMUvVvMps0qhwsBq8TfbJRY9l21wt6RdJOVLlA6zzCz9FJLYhSpFU5Klpguc9GhQ4fTgi7o6XDJ43xfOe9kMLm4LUT5aZ3VizpgDfRSw2aITAqHChFtJPVilEapWkp7EYJWKA2zWqw3iI3QToAMddKid4vZtIMzxebMsTYrKJ3n8qWcy0cZgzyRbFDhoBQ9odRqjLFMKk+AbZ1fHTp06HAq6IKeDh3OU+xm6OmaAGjR5qJ0gTp4UqNZzlP2DDIgkhlNSCJ1jLgqYBs389RYrHZULhKcGG+2reDOeaxXeK3QKoqNRJAfEk0IivFMk1lNnSdHDRgXs2lF7XloU/7vZ3Yu3rgxq1mfVUwLRx0iqTWMehqlFOOi5qEYuXJZX/JlrQ4dOpw6uqCnQ4fzEEcz9NycVsxqN7e5iERmtdhe9JpSUFLqucKxVqKDQwTnPMYYlJKAyHtH6QOTKrA+LSkqx7R2xIbAnCZiX5FYQy+35Ea4Nlcu9bhiVjMuHKuDhOV+Rv8o3mdGC8E2SzSRSGIULgTWJjX7H9rkgXFBWXkSo5lUNVrB3qEQpielvP/lfnqWrn6HDh0uVnRBT4cO5yGOZui5WSqi27K58AGs1eRaERq1YhQYo6lcoCxFbbltNa+DZHgqJ6rTMYLzDt8cW5aeKkhAVfuISyKm9kwrR89a9o0yisrx0OaMaVGTp5bVfsrVKz32DjPydOfbSquPBIrKBzamNV89MOGhzRIbIc8TKudZn1TcY2YoYHWQYbSmcJ7hDirMHTqcLhwPR6/DhY8u6OnQ4RyiFQsUryk118Zp270npZNuq9oxKT3TsmZSVJQhopzf/lw+EoC1WU3jS0DlPLUTro5SkCaGlTwnsYrMG0KAae2pXMAS0SiM1SQ+NCLRopRstcK7iM0l81MH8B6SXJNbOecHxwVViFy1lB8R+LQTSmY0M+UZF45D0xIfRDPIh4hW0smVJ4YQI2vTmjyxpIlGozr/qA5nBM6LL1zZfOaVEuuS47Fk6XDhoQt6OnQ4B3A+sDYtuW+j5IGNgvVZReXEZFMrmM4cpRN15FntmRWOIgh5eFY5yhCwWmOUZFCUElE/3wQqvUSjtGa5n3HZMOPKYUo/S8msYblnCVFxYFoRNqekQbPcy5gZR9QOmyrK2uNcIETQTaDRywyjNEEbyRjlqcUDLigyLd1WReVZm9Zc2QQ9h08oPgac96xtlqzPaimhacXBoiZNDEu5pZdYqiBWGRuzisuTHtp0/lHHi8OJ7+ezg/y5hvOBzVKynCdjydLhwkMX9HTocJbhfODBScE9B6ZszGoKH4gBpoVnUlWEEHBBgphZLe3hrnaNho5nXAUq71FKYZWeu1i0AY/zkbIUJ/fc1kxLS5nDFcuWPcOsaR3XrAwGXDFKmTrPrA7MCseBSYn3gUnliJ7GTAI0TQt7KlmdSGBa1vhgxNVbQWYVS72MceXmAdyRE4qmTgLKiGeU82CsaBaJGah0gykfqJwnksxX3t2kfXQcTnzfKCosin5qWB1knc7RDihqjw9xm+DliVqydLiw0AU9HTqcZRS159C4pgqBJNH4GHFW0e9ZooqMZxVKiQs7LqBVJEkM+EhUGuMdpjGEV1pEBBWKfmbwtSdoUVpOrXif91LD8iDFaOmIMkYyMlma0M8VsXRoPIRIrzaUtSJju8s8iFFrUcv5g8KqiNWRog44L2KDqdZUIbI+qwhBnOvzxFAT5tmGUZ6y1MtxDtCKrPHj2phVVE5c70XFWYjPedr5Rx0LhxPfi9qTGikGFi5wcFJSN91+rajkpQ4fIqWXz+VOSIym9IFexyW7qNAFPR06nEX4ENksHbPKQ1AEL+Th4COhiTKCUjgvpaUyKHyUTqfae8oqELwjIK3qdS3qydooVBmbbiuL1orVnrSur/YTUmuISgjEPauJiUKpuM0lPbGaNDFMSr/NeyyESOk942nNuHLMqsCBVM0nVa11IyaoGSSGPEnYO0qpKtHc6WcJS3nCyiBlpZ8yyBNWBykbRYlvJpTMGlKbM60c46ImRsVyL2XfsMdK7+LLTuzE5WpxMuWow4nvWWrIrAxg6TzEiI/xuEQlLxWEKGrpu11m3diydFyyiwtd0NOhw1lEiBHvAyF4lIoEAk3SZm4JoZFyVaI0A60pbaQKEGOAVGFjQj82fBkfMFFEA41W5Jmlbw1Wa1YGCct9izVKjm1KX5WO9JKEpV7CuHDbXNKDg+AidRDfsBgj09qzXtTUzlG7SAietUlEq1peMzH0rcF7zUObFXv6rinRRUa5xWjNpMlItdmGfmZY6WWsTyvGs5rSBlKjiFHMXkeDhOv29LlslF1UAc/RuFx5ohmlMi4nUo7yQcbItQF0hHThGKs1lZeAyIV4TFHJSwUtFy5EKWkdjtCQmjsu2cWFLujp0OEsQiuFMRqtDdF7NBpNkF6r5t4aQAi+1hAV9JqvaTyi3ARV8MQIidZYLWRfaxRV7THaIKYPGqU0KAliiJHVfsplo4x+Um/3HhtYBqmeCx/OKg/UZImm8inTQuwk1ic1ZYz0UkPPaBKrcV6sJ5JE42IkSw15ZjFWBBFR4GNkWjkSo9kzStkzTHhws+LAuGRWBzJtuHwp54qlnJXBxRfw7MblmrmafqJRQwleFwPEY5WjQhRhyRgDMQYUcVv2orn0NO14cxHKSz17YbQiM5rChW0Cmy0RvKzFFuVSDw4vNnRBT4cOZxFGK0aZpZcaSu/QgPFSntJBAx4dRZU4Tw1GyWS506RXu8DQpKjD9kmMpp8Yps4zrmr22hRFoKodRlmWeymj3OJCJE8t12YJVy/HI2wuKhc4MC6ZlE66yErHg9OCovIcHJesl45p6ZvgC4yB1Fqs1tQhkqEoXSS1bLOycEEsM65c7rHcT7ls2JuXepKmVfhinGiOxuXKvCZGkRxYDBCPpxzV6h8pJcFtbEqfbfai0aWkZbxr3XXCtcgTQ92UGpWCaenYKByTQrKYe/opMcaOAH4RoQt6OnQ4y8gTw+owYVrWbJQ1dYg4F5nOHNNGjTjRiugjFuH0VLWUm1TDM3AhYhrBQiLb9nEEUqMZWIOPoeGPRPaNLFev9BhkhgPTirKW3qyeNYxye8SNPbViAgoRpSIxagY+oa6jmIJaw6aqcCGikQyVNaAioCKJllJd5TypVkSjCF5R1pHMpHNxRaMVg6OoOe+Exbbs02H86oMEGJXzGC3k70Vezam2fR+Ny0WMJNZIdsEFKhdJLMddjjJa0U8Ms8pjtEIrcGEre+FCQANWK6xW9C7SoPJkYI1mlFnGRc0D45KNaYU2ipVeMjfYPTCpOgL4RYQu6OnQ4SzDGs2+QU6i1JzbUTvPIDeMej0So4VI3GDRZ6vl/SRWbdvv8H2MViz1ElaarM5yP2Gpl1IGUULWSlqZQXy7drqxH55BQGlyY0jMlvKzSTR48AihepFLYowhTy1aS0BUR0iBNLGMGnL1iWKxLfvAZsX9G1Ompad2AaVUc10UliOvi3A0tu/jQ2R9VnNgs2CtqJjVolw9zC17+xl7Rzmrg5Q9/fSE274XA7Pah8Y+pAbYxuUCycqEhlzV8q9OpBy16GivgKLylLWUTWOMYtqqJLjsOuG2wxrxdbNaszrIjggwi0Y2oiOAXxzogp4OHc4BlFIM85SHWctVyz2AY2YSTiajsZihGBc105kXYcGFic8aveON/fAMQmIUSaIY9VKmLjAdzyBEjJJgJ1UabRS1k1Kb0aLdkyeGzEj7+SBLGOUJoyw54WzDYlv2+rTiwXHBpAjMSk9ZC9eozYBl1kBjxNp6lB2ZJdNMCsd9mwVl7UDJuIQYWZuVHBiXXFPVxDjAqGPzbObBTVGzPquZVZ6q9qwVFa4SG49Z7ckTS2aFY9VeAR/lR6st/tWJlKMWHe2tVaxNajbLCoNikFpW+mmn07MLWiK4Vop0hyxYYjSl8x0B/CJBF/R06HAWsZip2GxUl3OjGTVt3P00QSm1TVH3ZLEY8LQ39jYL1IqygZBcfRCCsfOB2i/6eokDe+08s8qzWTomM4crPdGBi4FESbanxBNqgEgksj6p8C4wyhNiaqXsYiT7ULnA2rQ6oQBuXNQcnFTMKsd6KZmpXqZBW4KKhDqSNkFXHUQwMaqt99K+13afqfNsVtI2nyUGFs7FBeHCuGiovHS27cazacf0vs2C/Q+O+bcDEyYzyaYNMstSL2GQJxij8ZVno6hJrSFNJPuCUtTOS0YmTUitwuoTL0ctOtpfvdwpMh8vWiI4hxHAW7RE8I4AfnGgC3o6dDhLWMxUzCqH1opRlkg3VCsol1TEGNlovKnqylPW/qglGthexlFK0U8te4cpl49y9i3lZImhrj2b04KZC6zPHBtlRVkJj8S5QFHXJNrQzxN6qd5WAhqXjvVpxWZZMa08LgQCEUKjBI2EOsSIQqN0E2BYTT9JGCSWQW6l3GWAwHG9l3YfpSLTsqZqgpngoJ823khNFsRFEVQMEcraoZAgJvgjg0cXImtFxbQMoKX0pmFBkFFhDcxKR1mJMm/lIqndzrOJMXJoVnH/WsFdD21yYFKjUOSpKPo+NC4pnOeG3DLIU6JCgt1aSnKpURQuzru3NAmxYR6fbDlKMnxdCet4sWiEu1P7eksE7wjgFwe6oKdDh7OERQG51BrJLgAZhmnlODStqJzHak1ESjPrU8ehcUkd/I4lmsPLOKnVDDJDahQHNkumpWN9VrHczzg0ldbwopIgqnSBoqw5OBMNnhiFG1I5zyE44vmdD2TGkvYMkS0dk6TR16mb4KJdGS/uY63C+8ChcblruWmnklSMkVnlODirGJeO1EqLvFIwyhPyLKHXGJLGSBMwNCv3GDFo4g7zVAwRanlPIkKnZLZr4iOlIhbxCqtjwIVADOEInk3ZjOnBaUnhglx7qyhrhW8uhkfIsNemwqvKE8u0qplVkpHqpYZRz5AnhlGWMMyTrhx1FrFYxq19OCJgrH1oLFI6AvjFgC7o6dDhLGBRQA7YkQ8yqR0xQB09idEkWqEtaKtI484lmsPLOFliSBNLGSLjuua+9Rn7HxSV52lZMS4qfBCeTWI1xEiMEdMEWkkimjvAEc+fZUdmD1r+jlKg9e6t9SiFP0a56fD34lzgUFGzMZGAQiYkhdOaECKq8kStIBqsNUKB0YoYZFWuEPdWtcPqXEUFicZ6OZd5JbHZNaJwQE9pEiUkV6X1Np5NjOJQPy09k0JsH9zcnZ7mdSNWa6allAdHuWWll3DFKMMoGYc9o3y791NXjjrraIngG9Oaadj6jJYuEGNkuZd0BPCLBF3Q06HDWcCigNzh3AEfJGMgVhQRFRQ6RmoXqGrJ8MzcTl5Y28s4BEiClE/qEAgBfAjUzlM4z7isG+VDaWNWlaJ0YoNgrWaQWWKEqgnMjqdMdKb2cXVgfVazPi2pPVhr6SdQhohvUki1E/5RYiLReRFoNFDHSGbtgn7RzkHPnl6KjYGNWSDRcl3amM2FiPPQG1gRWUxMw7XZ4tkoJTwP7x0+BqwG5+Vvkfa5JPCJRMrg6UVD1ghU9lPh+6z00y7AOcdYJIJvlI5p5UFFcmMY5UmXcbuI0AU9HTqcBSy2f3OYeFyMYnwo3JpIahVaazzCMWk9gOLhxObDyjioiHNSPpu/LhF5yUimDb3cio+XkxKXj1KmyaMiN02ZqD3+OMpEZ2qfsvbMygpibLI2UiczROrgCTFQV54QAmVlGKQG0gQ8u+oXHVFOM4aYZkzKI7u36hCk/BhqCAneiUJvP7Fzno1qxtQYCxFmtccFqHygqgOVF78rFxSZUWgUVp0cSbnDmcciEbz2ssLoMm4XH7qgp0OHs4BF3kDpJANhtJnrxPy/A2MeGpcQYZBZ+qmVoCjGOS9GHXbzPbyME5BMyCKkAykSlCbvyQ3coElSSxICtgpoonBalNr2GsdTJjoT+wQlxGKHogyRoqqp6tAEMRGjwDf756lkqLK8Rz+1jHoJvdTsql8E2zWOfIjsW+41Oj1ihQGwkjQBidI8NK6pg7z2ILVzk7R2TNcMuOhZm9Us5SmDLCGEmnFZUzlPER2ml6JjlODtFEjKHc48OiL4xY0u6OnQ4SxhUUBuVjmmlePAuOSeQ1MOTgq8jwxTSwyBaeXQSuFjpHJhm+t5i8PLOEpJa7VdCFx8jKTaYKw4nnsiMUiWKTEam4i/llEGo+I2ccHjKROdiX3K2qNVJHrRuSnrgA/iOm+0IioIKpInhitX+ly10uP6vUOuWuqxbylneRcNneNRZJ5WjmnpmFWOsg6UQfzRjJIgq3SBBzYKfITlXkJROw6MC6ZlZFZ6xrMxw8wQQ0Q1PmP91LDcE3f5jqTcocO5RRf0dOhwlrDIG3hwAl+5b5N/W5sSfeTKpT5ZYpiUntp5XKOm60KgdBHdlESOZkPRCucFLZmEAFgNWaoJXosYn/O4KA3mIQo/xgXQykOQdulj2VwcjxXGqexTo6hckA4pH/A+iHCfVlilxZzVKrLU0k9Srlzqc/kop5/bhksjBOFFrSPJcG1lfw4PgFoFa61EQyhNDGhFX1syKxm5jaJiUjl8jBQucHAsUWiiDY+8fMjBQcL+B8fcfXBM7aUVf2mQcflSwmOvWOKq1T7L/fSsC9ztZNlxMXucdehwNHRBT4cOZxEtbyBEeKhfYpRCGUUvEbuGaeU4MCnZGNfEGMjShIFW5FbPu4JgdxuKcSmZCgXkmUzYvUQzc4FZ5VlKNN6LLo8PgX6iGaaGfiZ2Fcdrc3Em96l9ILFiwFl56ehqLKokKAmQasVynrIyEM+wqBQuREoXuG+jAGBjVs+1jlwz6R9N7yiEyLR2hEgTFFj2DFLIaIIw1WgGKQrnWZtVLDVdPb3UMsgSFIrMGJx37B3m7BlmTCrHuBLpgCwxTUnrzAcbO1l2lLVkDZd6KZcvZRelm32HDkdDF/R06HCW4UOkcNKV1e9ZQohi1KmUaM9Yy2qvxgB7BhnL/ZTVYYZuSLZHUzEunefgpGJjWlHUgdp7JrWnqGvqOpBYWMnFTDE1hjzV7BlkXL3aJ7HmlI07T8c+tQ88sD5j/4EJ/3rfBoXzVHVgWlSMm8zP3l7KFSs9VgaZdF7FiPeBjcpRuYgxki0qXWBc+Mb3yh1VI0grRSBilZrrGCkFWe1IrSW1mmkZmFSeMniUMtRJIAbIEsu4qnEhsDrKcCFh1EvIEkuaGGlvr86eh9NOlh1FFdGopoTouH89Mqsd14bIZaO8C3w6XBLogp4OHc4yQpQsh9YKE6QMFeNcWBhrFIkxGAW9LGHYE7+q4ylFDDLLUp4wHqQcnFWsTWo8JXpgGaQa35Q2hlnCcj/h8qWcvcOMPD1/bgVl64NkRFm6Dp4k0SyZnJVG6LCfWvppgtFbXlXSASc+YUqLxXuiFXmm2SwV0R1dI2ipL9eg7V5TSgKwmfNkNlC5wP0bM6alo6g8Waq5ds+QXm4oKteQoBWaSKIVIUDpA4PGsX1WeyoXzoqH06IQZuHEfmS5n5BaTeVENiFNNLWPHJxUDPPkmIHYYpms0xLqcKHipO50Bw4c4Ld/+7e57bbb+Ou//mvuvvtunHPs27ePb/7mb+alL30p3/d933dCz/mzP/uzvOlNb5r/Htvc9y7Y3NzkXe96F5/85CfZv38/xhge/ehH86IXvYjXvOY1pGl6Mm+tQ4czDq0UidXkqcXHGmLANSUbaDquQiBNRVn5RNuad/NgauEbEcDMmm1BwPkA5wMbZc3UeRKruWIlI59ZZpWjqByJNQsaOOIenlppaa9dREfpTgsBgpf36pwoKge1pUG0iBDB+0CMop9TBxGkczHiS1HpjaFiWgUq51AakkRhlGZSVFSlITgprQ0SQ4jimO58YJhLhkiGLxJCOOMeTotCmJULFFVAaz0nuButqYO01xutmDVms7sFYsfyi+sI2R0uJJxU0HPllVfinJv/nuc5SZJwzz33cM899/DpT3+a5zznOXziE5+g3+8f8/m+/OUv89a3vvW4X//uu+/m6U9/OnfddRcA/X6fsiy5/fbbuf322/n1X/91brvtNlZXV0/4vXXocKbRtjrniaZ2GuekzOC9lF6mlcOHSG8gJZKTbWu+EFtv2wzFILMkWhODoqgLvDc47ZnMSlRUaKvReZx7VcUQQUUyo7BGuqzk35basoo0xpKHIcZGQzBg0FRRRCF9gMqVzGoPIRBpRAWNZTjsUXkhm2srBOtSOYoaPMIJGvYThpnFGi2q1Ij+0pn2cFoUwgzB46OIYbYv2dqE0LbQI2rXOwVix/KLO5rrfIcO5yNO6lPqnOPJT34y73//+/nKV77CbDZjPB6zf/9+XvGKVwDwu7/7u9x0003HfK4QAi9/+cspioJv/dZvPa7Xfu5zn8tdd93FVVddxec//3kmkwnT6ZTf+I3fYDQa8Td/8ze8+MUvPpm31uESgQ+Rolnh7vSzMatZm1ZszOptjuSnC3liWOol9FMx4hxkUsY5MKmoQmTfUsZ1q4NLajJZdILvJZalfsKVKz2uWemz1LONA7ml37NcvtTjslFvrpa7d5hzxajHUj/HWgtKI/+Yq1/HRuvo8B+JBpR0wIVIai3Lg4xRPyEqzfq05KtrMw7OHJulpwwiApkoxdQ5IWUTGSa2UYjWrPRSVgZbGZCi9qRGkVp9xgUJF4UwtTbSybdAgm8NNFGKKGEbidE7BmKH+8UNMuGDtf+3rvNF7U/7+1gsp3XocLpwUpmeP/zDP+QZz3jGEX+//vrr+chHPoK1lg9+8IN87GMf42d+5me47rrrdn2un//5n+fP//zP+aEf+iEe+chH8hd/8RdHfe1f+ZVf4e///u8B+OQnPzkPlLTW/MAP/AAhBP7jf/yP/M7v/A633XYb/+E//IeTeYsdLlIspurXJtu7e+DYbuWnK5V/uOx9Zg3D3JIozXI/YXWQzQ1JTwUXEg+jzVC0Nh1KaZZ6Ij541UqPaelYm5T0EsNVewYMMrl9te9rVjkOTCp8GUmMog5gvFhsWK3RkV01gqzRhCiWHKO8EQ2Momit45BUF/QywyBPJJPkI9o5UiA1UDjHME0Z9TRZokitoqg9xgUJCBSi1XMWBAkXhTBTq8lTzcasxgVFqhW+saIPylNUnkQLFaANXFqSeYzSAbdZSOCfp2b+OQKwWlN5v811/nR8tpyXa1Y2PmZKQda02F8qC4AOZw4nFfTsFPAs4hWveAUf/OAHAbj99tt3DXr279/Pj//4j7N3717e85738L73ve+Yr/0rv/Ir83PYKTP0ohe9iB//8R9n//79/Oqv/moX9HSYYzFVPy5qJqWbd/dMihrn/Y5u5YcmFc4H6hC4fKl32rIvZ1L2/kLkYbQZClDUPqKUdHdFIpuF4/6NKRuzmn4mLer7RpmQsJsgYlH8UQEEEWssygA+oo6iEWQUzAqHBrw1RKOJIWKUYnloSZM+dfBSnlLgGp2jXmLoWVGBvmyYsTLMSIzi0KxmY1YRAvQTy95BclqD5mNh8Vrk1rCpatanNTFEau+pmo44VKSfp/y/tQmqsdBoW/pDhNI5YhTRSukkzFjqWfqZEOvbMtnp4ik5H9gspbwr2Sc5j6Ihm4+acmGHDieLM9Kykef5/LH3u6c9X/nKVzKZTHj/+9/Pvn37jvm80+mUP/uzPwPgOc95zo77KKV49rOfzQc+8AE+97nPneCZd7iYsZiqj7Ctu6cOhmLmtrmV97KEJDFNGv/MtRyfbu7NhcrDMFoyEQ9Vco6pNdQucHBc8tC0xLlIz2gGVlP5wN0Hxhwcl1y92p93nxmlyBKN86I5VMeA7hkGaX5UjSAVacZWCOb9RNShtdGMa0diPGuTihgCm7V0cq3NKiCSGc1lo4zNss+1IXD1Sp9rl3skewcYrUmtPusZtsVMorVSwrs3TDgwrqkr6Y7LU0ueaHyEA+tHtvT7EBlXHmtgmCZkiQOlRPMoRgZZMi+TnS6eUltK3uY6r8BoQ1F7itqf8Xb/Dhc3zkjQ84UvfGH++AlPeMKO+3z4wx/mtttu4zu+4zv44R/+4eN63jvuuIPQWE0//vGP33W/dtt9993HwYMH2bNnz477lWVJWZbz39fX1wHY2Ng4rvPpcOHAh8jBSclmIRYD09JTOs+sdpSVZ1w51sYFoDBaEzJDrC2VNVSJITWautAYV+DLlHqQnfAkdrZKTeOi5uC0mosUpomharbVzrM+i1SFpRpkDPPktL/+ycJ5CSSKiQRrEVib1nxtfUrwkT2DFK0SHlovqGvPZuWYzGqyxDJMNFUIRMRIdJAlrPQTrhimLK1s16DZSSPIh8jmrG4CUNFD2ixqAo5pVVHPKjbWZkycoygdlXNMvSf4SJoYMnosW8chVeOKKaPcstpoLGH0/PqfbWhg1UaGS5pVm/FQDtPSUYfQGM86CueZFSXTssYaMbptkQGuCEzrkoQE5RJ8ZahLwzgx9FNDTC09Mibx1N6l+NBVGK2pdrEKGYdA3etc6TsciXbePlbn92kPetbW1njb294GwNOe9jRuvPHGI/a55557uOWWW+j1evMy2PHga1/72vzxNddcs+t+i9u+9rWv7Rr0vO1tb9uxa+xoHKQOHTp06NChw/mJzc1NlpeXd91+WoOeEAIveclLuPfee8nznPe+97077nfTTTexvr7O29/+dh7xiEcc9/Nvbm7OHx+tFX5x2+Ixh+NNb3oTP/qjP7rt/A8ePMjevXt3dII+WWxsbHDdddfxb//2bywtLZ225+1wcujG4/xDNybnF7rxOL/QjcexEWNkc3OTq6+++qj7ndag57WvfS2f+cxnAHjf+97HE5/4xCP2+djHPsZnP/tZvuEbvmFbwHEukGUZWZZt+9vKysoZe72lpaXuA3seoRuP8w/dmJxf6Mbj/EI3HkfH0TI8LU4bI+zmm2+eZ3be85738PKXv/yIfe6//35e97rXYYzhwx/+sOhpnABGo9H88XQ63XW/xW2Lx3To0KFDhw4dLl2clqDnjW98I+9617sAuPXWW3nd6163434/9mM/xoEDB3jVq17FYx7zGMbj8bafqtoiwu30t8W01T333LPr+SxuO1aqq0OHDh06dOhwaeCUg55bbrmFd77znQC84x3v4A1veMOu++7fvx+AD3zgA4xGoyN+WgI0MP/bG9/4xvnfHvvYx6K1nPI//MM/7Po67bYrr7xyVxLz2USWZbz5zW8+opTW4dygG4/zD92YnF/oxuP8Qjcepw8qHqu/6yi4+eab5xmed7zjHdxyyy1H3f/pT386//t//+8Teo3Xvva1/NzP/dz892//9m/nT/7kT/j3//7fc9tttx2xf4yRRz7ykdx555388A//8FzMsEOHDh06dOhwaeOkMz2LAc+tt956zIAHRL8nxrjrz5vf/Ob5vu3fFgMegJe+9KUA/NEf/RF/+Zd/ecRrfPzjH+fOO+8EOG79nw4dOnTo0KHDxY+TCnoWOTzvfve7j1rSOt146UtfyhOe8ARijDz/+c+fZ3tCCHz84x/nla98JSCKzZ0FRYcOHTp06NChxQmXt7761a/y8Ic/HBCTz2PZR9x8883cfPPNx/Xcb3nLW+ZigUc7rbvuuotnPOMZ3HXXXYDo8oQQKIoCgCc96UncdtttrK6uHtfrdujQoUOHDh0ufpywTk9rA9E+vv/++4+6/3g8PvGzOgauv/56vvSlL3HrrbfyW7/1W+zfv58kSfj6r/96fvAHf5DXvOY1pGl62l+3Q4cOHTp06HDh4pSIzB06dOjQoUOHDhcKOrvaM4jNzU3e8pa38IQnPIHhcMjy8jLf8i3fwrve9a5t+kOXOg4cOMAv/dIv8eIXv5jHPe5xDAYDsizj2muv5Xu/93v51Kc+dcznONVrff/99/OGN7yBG2+8kV6vx549e3ja057GRz7ykWMa2AF85Stf4aabbuKGG24gz3P27dvHd37nd/LJT37yuK7BhYCf/dmfRSk1/zkauvE4c9jY2ODtb387T3nKU9i3b9/8u/KMZzyDt7zlLaytre14XDcmpx+f//zneeELX8jDH/5w8jyn1+vxiEc8gh/6oR86ZqdyNx7nCLHDGcFdd90Vr7/++ghEIPb7/Zhl2fz3Jz3pSfHgwYPn+jTPC1hr59cFiHmex8FgsO1vz3nOc+JkMtnx+FO91rfffnvcu3fvfP/hcLjtnL7zO78zlmW56/Gf/exnY7/fn++/tLQUtdbz31/2spfFEMIpX6dziX/+53+OeZ5vG5Pd0I3HmcMf/uEfxiuuuGL+XtI0jSsrK9vG5W/+5m+OOK4bk9OLEEK86aabtl33Xq8Xe73etr+9/vWv3/H4bjzOHbqg5wygruv4hCc8IQLxqquuip///OdjjDF67+Nv/MZvxNFoFIH4Xd/1Xef4TM8PAPHJT35yfP/73x+/8pWvzP++f//++IpXvGL+RXzxi198xLGneq3X1tbilVdeGYH4mMc8Jv7VX/1VjDHGsizje9/73pgkSQTiq1/96h2Pv/POO+cB2lOf+tT45S9/OcYY4+bmZvzJn/zJ+bm//e1vP6VrdC7hvY9PecpTIhC/9Vu/9ahBTzceZw5/+qd/Op9Un/e858W/+qu/mk9Mk8kkfvGLX4w//uM/Hu+8885tx3Vjcvrxi7/4i/Pz/v7v//74L//yL/Nt//zP/xy/53u+Z779t37rt7Yd243HuUUX9JwBfOQjH5l/cP78z//8iO3//b//9/n2P/iDPzgHZ3h+4Q//8A+Pun1xRfXVr35127ZTvdY/8RM/MV+lHT5ZxBjjz/zMz0QgGmPmN4dFvPjFL45AvPLKK+OhQ4eO2P6qV71qvpK6UDN7P/dzPxeB+EM/9EPxzW9+81GDnm48zgwmk0l8xCMeEYH4mte85oSO7cbk9OPpT396BOIjH/nIWNf1EdurqpqP14te9KJt27rxOLfogp4zgKc97WkRiM94xjN23B5CiDfccEME4g//8A+f5bO78PDFL35x11XTqV7rhz3sYfN07k7Y3NyMw+EwAvEnf/Int20bj8fzlfdb3/rWHY/fv3///Nx/8Rd/8Xje7nmFdlW4d+/e+MADDxwz6OnG48zgF37hF+YT1Ww2O6FjuzE5/bjxxhsjEJ///Ofvus/znve8CMTv/u7v3vb3bjzOLToi82nGdDrlz/7szwARSNwJSime/exnA/C5z33urJ3bhYo8z+ePvffzx6d6rb/85S/z1a9+9ajHD4dDnva0p+14/J/+6Z8ym82Oevz111/PYx/72B2PvxDwyle+kslkwrvf/e5janJ143Hm8Ku/+qsAvOAFL9j2fTgWujE5M3jEIx4BwN/93d/hnDtie13X/O3f/i0A3/zN3zz/ezce5x5d0HOacccdd8y1jB7/+Mfvul+77b777uPgwYNn5dwuVHzhC1+YP37CE54wf3yq13rRtPZ4jv+nf/qnbX8/0eP/8R//cdd9zkd8+MMf5rbbbuM7vuM7jsvSpRuPM4OyLLn99tsB+KZv+ia++tWv8qpXvYrrrruONE254ooreO5zn8tnP/vZI47txuTM4NWvfjUA//qv/8oP/uAP8q//+q/zbV/+8pd54QtfyJ133snXfd3X8frXv36+rRuPc48u6DnN+NrXvjZ/fM011+y63+K2xWM6bMfa2hpve9vbAHja057GjTfeON92qtf6RI/f2NjYJrbZHr+6ukqv1zvm8RfSON9zzz3ccsst9Ho9PvjBDx7XMd14nBncdddd8xbmO++8k8c//vF8+MMf5oEHHmAwGPDAAw/wmc98hu/+7u/mla985bZ25W5Mzgye+9zn8p73vIc0TfnEJz7Box71KPr9Pv1+n8c85jF84Qtf4NWvfjVf/OIXWVpamh/Xjce5Rxf0nGZsbm7OH/f7/V33W9y2eEyHLYQQeMlLXsK9995Lnue8973v3bb9VK/16Tr+aMcubr+Qxvmmm25ifX2dt7zlLfNU/rHQjceZwaFDh+aPf/qnf5okSfj4xz/OeDzm0KFD3H333bzgBS8A4CMf+Qjvec975vt3Y3Lm8LrXvY7f+q3f4vLLLwdgNpvNS0dVVTEej1lfX992TDce5x5d0NPhvMVrX/taPvOZzwDwvve9jyc+8Ynn+IwuDXzsYx/js5/9LN/wDd/Aj/7oj57r07nkcbj1z0c/+lG+//u/nyRJAHjYwx7Gb/zGb/Dv/t2/A+BnfuZnduSZdDh9mE6n/MAP/ADf/d3fzcMe9jA+97nP8eCDD/Lggw/yuc99jsc97nH82q/9Gk9+8pP50pe+dK5Pt8MCuqDnNGM0Gs0fT6fTXfdb3LZ4TAfBzTffPM/svOc97+HlL3/5Efuc6rU+Xccf7djF7RfCON9///287nWvwxjDhz/8Yaw9fnu+bjzODBbP81GPehTf+73fe8Q+Wuu5sfOBAwf4v//3/x5xbDcmpw+33HILv/mbv8mNN97In/zJn/DMZz6Tyy67jMsuu4xnPvOZ/PEf/zGPfvSjeeihh/iRH/mR+XHdeJx7dEHPacbVV189f3zPPffsut/itsVjOsAb3/hG3vWudwFw66238rrXvW7H/U71Wp/o8UtLSwyHwyOOP3To0DytfbTjL4Rx/rEf+zEOHDjAq171Kh7zmMcwHo+3/SzK4x/+t248zgwWuRuPecxjdt3vcY973Pzx3XffDXRjciawubnJhz70IQB+5Ed+ZMduul6vx3/7b/8NkI6pBx54AOjG43xAF/ScZjz2sY9Fa7msi0z5w9Fuu/LKK9mzZ89ZObcLAbfccgvvfOc7AXjHO97BG97whl33PdVrvdi9cDzHL04qJ3P813/91++6z/mC/fv3A/CBD3yA0Wh0xE9LKgfmf3vjG98IdONxprBnz56jklZbLBKYW2+0bkxOP/7lX/5lXj78uq/7ul33e9SjHjV/3H6vuvE49+iCntOMfr/PU5/6VAB+7/d+b8d9Yoz8/u//PgDPetazztq5ne+4+eabufXWWwEJeG655Zaj7n+q1/rRj340D3vYw456/GQy4U/+5E92PP7bvu3b5h0Qux1/9913c8cdd+x4/MWGbjzOHNpzbc99Jyy2J99www1ANyZnAm3QAlsZtZ1w//33zx+3ZaJuPM4DnENhxIsWrcy4Uir+n//zf47Y/j/+x//obCgOwxve8Ib5Nbn11luP+7hTvdatpHu/34/79+8/Yvvb3/7245J0v+qqq+La2toR21/96ldHII5Go4tC0v14bSi68Ti9+OM//uP5dfvUpz51xHbvfXziE58YgXjNNddE7/18WzcmpxfT6XSuavyN3/iNO9pQOOfmfnWrq6vROTff1o3HuUUX9JwBLBrKXXPNNfMPrvc+/uZv/mZcWlqKIM7hHWK85ZZb5l/yd7/73Sd07Kle60Xzvsc97nHx9ttvjzGKed/73//+mKZphOMz73va0542Nx4cj8fxrW99a1RKRbh4zPuOFfR043Hm8P3f//0RiHv37o2f+MQn5pPt3XffHV/4whfOx+WXf/mXtx3Xjcnpx2te85r59X72s58dv/SlL0XvffTex7/7u7+Lz3rWs+bbD7d76Mbj3KILes4Q9u/fH6+//vr5B7/f78c8z+e/P+lJT7ooo+gTxd133z2/JlrreMUVVxz1553vfOcRz3Gq1/r222+Pe/fune8/Go3mTsVAfNaznhWLotj1+M9+9rOx3+/P919eXo7GmPnvL3vZy+Zu2Bc6jhX0xNiNx5nCeDyO3/7t3z5/H1mWxdXV1fnvQHzzm9+847HdmJxeTKfT+OxnP3vbtc+yLGZZtu1vP/iDP7gty9OiG49zhy7oOYPY2NiIP/mTPxkf//jHx8FgEEejUfymb/qmeOutt8ayLM/16Z0XWDS3O56f3W7qp3qt77vvvvj6178+PupRj4p5nseVlZX4bd/2bfHDH/7wtlLBbvjXf/3X+MpXvjJef/31McuyeNlll8VnPvOZ8ROf+MSJXpLzGscT9MTYjceZgvc+fvjDH47f/u3fHvfs2ROTJInXXHNNfNGLXhT/7M/+7KjHdmNyehFCiB//+Mfj93zP98Rrr702pmkasyyL1113XXz+858fP/OZzxz1+G48zg1UjAuU/w4dOnTo0KFDh4sUXfdWhw4dOnTo0OGSQBf0dOjQoUOHDh0uCXRBT4cOHTp06NDhkkAX9HTo0KFDhw4dLgl0QU+HDh06dOjQ4ZJAF/R06NChQ4cOHS4JdEFPhw4dOnTo0OGSQBf0dOjQoUOHDh0uCXRBT4cOHTp06NDhkkAX9HTo0KFDhw4dLgl0QU+HDh06dOjQ4ZJAF/R06NChQ4cOHS4JdEFPhw4dOnTo0OGSwP8Pxz0rtUNRdDUAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(\n", - " trace_neglogpost.reshape(\n", - " 9000,\n", - " ),\n", - " \"o\",\n", - " alpha=0.05,\n", - ")\n", - "plt.ylim([240, 300]);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### With pyPESTO\n", - "\n", - "pyPESTO supports a number of samplers and unifies their usage, making a change of sampler comparatively easy. Instead of the below `sample.AdaptiveMetropolisSampler`, try e.g. also a `sample.EmceeSampler` (like above) or a `sample.AdaptiveParallelTemperingSampler`. It also unifies the result object to a certain extent to allow visualizations across samplers." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1000/1000 [00:03<00:00, 311.04it/s]\n", - "Elapsed time: 3.9380469999998695\n" - ] - } - ], - "source": [ - "# Sampling\n", - "sampler = sample.AdaptiveMetropolisSampler()\n", - "result_pypesto = sample.sample(\n", - " problem=problem,\n", - " sampler=sampler,\n", - " n_samples=1000,\n", - " result=result_pypesto,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmgAAAHECAYAAACN2dZBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACAHUlEQVR4nO3dd3hUVfoH8O+d9EJCIAklhEAohl4CigZpsrq6WHFRxBIRLKyIBTEUQRBFkVVXRbAgUbFhF3/rrgqCGiwhNBECgZBCM71Nkslk5vz+yM51JplJps+dme/nefIoM/fOPXOnvfec97xHEkIIEBEREZFiqDzdACIiIiIyxQCNiIiISGEYoBEREREpDAM0IiIiIoVhgEZERESkMAzQiIiIiBSGARoRERGRwjBAIyIiIlIYBmhERERECuNXAZper8f48eMhSRKam5vb3fajjz7CJZdcgs6dOyMsLAwDBw7Egw8+iDNnzrS737///W9cdtll6NKlC4KDg9G7d2/Mnj0beXl5drVZCIGamhpwwQciIiL/IfnTUk8ZGRl4+umnAQBarRaBgYFmt5szZw42bdoEAOjevTu6d++OvLw8qNVqdO7cGV9++SXS0tLa7LdixQqsWrUKABAbG4uEhATk5eWhvr4eERER+OyzzzB16lSb2lxTU4Po6GhUV1cjKirKpn2JiIjIO/lFD5pOp8PDDz8sB2ft2bRpEzZt2oTAwEBs2bIFZ8+exb59+3DmzBnccMMNqKqqwnXXXYf6+nqT/X744Qc5OFuzZg3++OMP7N+/H6dOncK0adOgVqsxc+ZMVFdXu+Q5EhERke/w+QAtLy8Pl1xyCdatW2fV9s888wwA4OGHH8asWbPk26OiorB582bExMSgpKQEn376qcl+b775JgDgL3/5CzIyMqBStZzamJgYvPPOO+jUqRPKysrw+eefO+NpERERkQ/z6QBt/fr1GDJkCHbt2oXExESsXbu23e2bmppw1VVX4fLLLzcJzgzCwsIwYMAAAEBhYaHJfcXFxQCAESNGtNkvKioKAwcOBAAUFRXZ9VyIiIjIf5hPwvIR2dnZAID58+dj9erV2Lt3b7vbBwcHtxvE1dbW4ujRowAgB2oGvXv3BgDs27evzX719fU4ceIEAKBPnz5Wt5+IiIj8k0/3oE2fPh1Hjx7FCy+84HCC/YEDB3DVVVehuroaQ4YMwTXXXGNy/1133YWAgABs374d69atg16vBwDU1dVh9uzZqKqqQlJSEqZPn+5QO4iIiMj3+dUszp07d2Ly5MkA2p/Faez222/Hjh075KHJK664Aq+//jp69OjRZtvPP/8c8+bNw5kzZxAbG4tevXrh+PHjqKurw+TJk/HGG2902IOm0Wig0Wjkf9fU1CAxMZGzOImIiPyI4nvQ0tPTIUmS1X/79+936vH//e9/m+SNnThxAl9//bXZbWNjY+UctLKyMuzfvx91dXVQqVTo2bNnh7XXgJYZoNHR0fJfYmKic54IEREReQ3FB2ietnv3bjQ2NuL333/HPffcg2PHjiE9PR3/+te/TLbbsmULJk6ciG+++QYrV67EqVOnoNFosHfvXkybNg3vvPMOLrroIvz+++/tHm/x4sWorq6W/wyTD4iIiMh/KH6IU6PRQKvVWr19eHi4XOKiNXuGOFtbtWoVVqxYgaioKBQVFSE6OhoVFRXo27cvampqsHbtWjz88MMm+wghcM011+CLL77AxIkTsXPnTquPx0K1RERE/kfxAZozOSNAU6vViImJgVarxXfffYdJkybh/fffx8yZMxEZGYmqqioEBAS02W///v0YNWoUAOD06dPo2bOnVcdjgEZEROR/OMRpRK/Xo6ioCD///LPFbSIiIhAbGwsAOHfuHIA/a6INGDDAbHAGAIMHD5b//+TJk85qMhEREfkgBmhGsrKykJSUhIsuugh//PGH2W3UajXKysoAAAkJCQAg92ydO3fO4qLmJSUl8v+zJ4yIiIjawwDNyNixY9G5c2cIIbBx40az26xfvx5arRaxsbEYN24cAMjDpmfPnsWXX35pdr833ngDANCtWzcMGTLEBa0nIiIiX8EAzUhoaCgWLVoEAHjyySexadMmuUdMp9PhpZdewtKlSwEATz/9NIKCggAAKSkpmDlzJoCWumnbtm2TH1Ov12PDhg14/PHHAQArV660OImBiIiICOAkgTbb6PV63HHHHcjMzATQUtusT58+OHnyJMrLyyFJElauXIlHH33UZD+1Wo2rrroKO3bsAAD07NkTCQkJOHnypDwkumjRIjz99NM2tdkVkwSEEDhQXImyWg0ML36gCtALQK8HGnUCxRX1GNgtElNS4hlQEhERuRkDNAs+/fRTbNy4EdnZ2airq0N8fDwmTJiABQsW4IIL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PDwckyZNwp133omZM2d6uolERERej2txksdxLU4iIiL/wxw0IiIiIoVhgEZERESkMAzQiIiIiBSGARoRERGRwjBAIyIiIlIYBmhERERECsMAjYiIiEhhGKARERERKQwDNCIiIiKFYYBGREREpDAM0IiIiIgUhgEaERERkcIEeroB1D7DWvY1NTUebgkRERHZqlOnTpAkyeb9GKApXG1tLQAgMTHRwy0hIiIiW1VXVyMqKsrm/SRh6KIhRdLr9Thz5ozdEbglNTU1SExMRHFxsV1vHLIez7V78Dy7B8+ze/A8u4c7zjN70HyUSqVCr169XPb4UVFR/PC7Cc+1e/A8uwfPs3vwPLuHEs8zJwkQERERKQwDNCIiIiKFYYDmp0JCQrBixQqEhIR4uik+j+faPXie3YPn2T14nt1DyeeZkwSIiIiIFIY9aEREREQKwwCNiIiISGEYoBEREREpDAM0P1NfX4/HHnsMKSkpCAkJQWxsLC677DJ89dVXnm6aYp06dQoPPPAABg0ahPDwcISHh2PIkCF45JFHUFJSYnYfe89zeXk5HnjgASQnJyM4OBjdunXDddddh59//tkVT02xmpubcf7550OSJGRmZprdhufYPjt27MB1112HHj16IDg4GD179sTNN9+MI0eOWNyH59o25eXlWLRoEc477zyEhoYiKioKF110EV599VXo9fp297PnfBUVFWHOnDno1asXgoODkZCQgFtvvbXd19SbvfLKK5AkCa+//rrFbdz9nnXJayDIb9TV1YkLLrhAABBBQUFi1KhRonfv3gKAACAee+wxTzdRcb7//nvRuXNnAUAEBASI8847T/Tv318EBAQIAKJ79+7iwIEDJvvYe57PnTsn+vXrJwCI8PBwkZqaKrp16yYfe9OmTe54yoqwatUq+Xxt3ry5zf08x/Z55JFH5HPUo0cPMXz4cBESEiIAiNDQUPH111+32Yfn2jYFBQXy+QkMDBSDBw82OV/Tpk0TTU1Nbfaz93zl5uaKrl27CgAiOjpajBkzRnTp0kV+Tf/zn/+4+im71a+//io6deokAIjXXnvN7Dbufs+66jVggOZHbrvtNgFAjBw5UhQVFcm3v/XWWyIwMFAAEN98840HW6gslZWVIi4uTgAQf/3rX8WZM2fk+06cOCHS0tIEANG3b1/R0NAg32fveZ44caIAIP7yl7+I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625ve89zaWmpuP/++0VycrIIDg4WnTt3Fn/961998ku2I+0FaELwHNtDr9eLLVu2iMmTJ4vOnTuLsLAwkZKSIpYuXdruFT7PtW3y8vLE3XffLfr16ydCQkJERESEGDlypFi5cqWorKy0uJ+956ugoEDccccdolevXiIoKEjExsaK6dOni3379jn3iSlERwGaEO5/z7riNZCE+N9qz0RERESkCJzFSURERKQwDNCIiIiIFIYBGhEREZHCMEAjIiIiUhgGaEREREQKwwCNiIiISGEYoBEREREpDAM0IiIiIoVhgEZERESkMAzQiIiIiBSGARoROU1mZiYkSUKvXr3M3n/kyBF4y+pyp06dQk1Njclt6enpkCQJN998s4da1b6dO3dCkiRIkoTm5maXH++xxx6DJEkYP368y49F5G8YoBGRy9XU1ODee+/F8OHDodPpPN2cdjU1NWH58uUYOHAgSkpKPN0cIvJTgZ5uABH5jmuvvRbjxo1DUFCQye179+7F+vXrPdQq25w5cwaPP/642fvWrFmDjIwMREdHu7lV1jn//PNx5MgRAEBgIL/eibwZP8FE5DTR0dGKDV6coUePHujRo4enm2FReHg4UlJSPN0MInICDnESERERKQwDNCJyGnOTBPr06YPJkyfL/w4KCoIkSSgoKDDZ9/vvv8f111+PHj16IDg4GN26dcM111yDHTt2mD1Wnz59IEkSDh48iAULFiAmJgaRkZFITU1FRUUFAEAIgS+++ALXX389kpKSEBYWhrCwMPTp0we33HIL9uzZY/KYkyZNQt++feV/DxgwAJIkYefOnQDanySg0+mwadMmTJ48GTExMQgJCUHv3r1xyy23YO/evW22LygogCRJ6N69O4QQ2LRpEy644AJ06tQJnTp1woUXXojNmzfbNKnC0iQBQ7s3btyIgoICzJ49G7169UJISAh69eqFOXPmtHk9DPR6PTZv3oyLLroInTt3RufOnfG3v/0N2dnZHbbHltd09uzZkCQJ4eHhyMvLa3P/ypUrIUkSIiMjcezYMavPCZHXEkRETrJ582YBQCQkJMi3XX/99WLo0KECgAAg0tLSRFpamjh79qy8zSOPPCLfHxMTI1JTU0X37t3l2x555JE2x0pKSpIfD4AYPHiwSEpKEhdeeKEQQgi9Xi9mzpwpP0ZCQoIYM2aM6Nu3r1CpVAKA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zWxbFi1aJO8fFRUlUlNTxcCBA+XaYpMnTxaBgYECgNyW1o8dGRkpxowZI7766ishhOVCteXl5W1WEhg7dqxcRywgIEA89dRTJvu0V2+tdTs2b97c/on/n47qoFkq9Gp43ZKSktrcd++998qP2bt3bzFmzBgRGhoqJEmSVxloXajWntf0m2++kV8b48LDQrQU/B00aJAAICZOnCh0Op1V54PIm7EHjYhcLjIyEh999BFGjBgBtVqN/Px8nDx5EgAgSRJeffVV/Pe//8W1116LwMBA7Nu3D7W1tRg3bhxeeOEFbN++Xa5bZq2nn34aH374IcaPH4+QkBAcOHAAVVVVuOSSS7BlyxZs375drltmqGVm8NFHH+HCCy+ETqfDsWPHcPz48XaP1aVLF+zcuROvvvoqJkyYgKqqKhw8eBCxsbG44447kJ2djUceecSm9ivFiy++iE8//RRTpkxBXV0djhw5grFjx+K///0vpk+fbnYfW1/TyspKpKenQwiB6667rk19tZCQELzxxhtQqVTYtWsX1q1b5/LnTeRpkhBOmr9NRERERE7BHjQiIiIihWGARkRERKQwDNCIiIiIFIYBGhEREZHCMEAjIiIiUhgGaEREREQKwwCNiIiISGEYoBEREREpDAM0IiIiIoVhgEZERESkMAzQiIiIiBSGARoRERGRwjBAIyIiIlKY/wc0taq9YKgDOQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# plot objective function trace\n", - "visualize.sampling_fval_traces(result_pypesto);" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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7RCp0B569OjB559/TkBAgGGbXK4/LZeVlV3xfpIksWDBAiZNmsSQIUPMHueVGBP/zJkzefbZZ+ttk8vlqNVqKioqzBfsZYyJPSEhgSFDhqBU/rMK6bBhw0hJSaGgoMC8ATfCKtZCNbVp06YZfd+ff/6ZgoICnnjiCdMFZEKtPbYLFy4QHh7O8uXLWbZsGWVlZQwaNIjnnnuu3ge7o2jt8Z07dw6tVstff/3FokWLUKlUxMXF8cwzz+Dv72+mKG1bUlISXl5eTJkyhdzcXKKionjyySfp169fo/vn5OQQGBhYb1vda5+dnd1pCpKMjAwWLVrEJ598gouLS7P7BwYG8sILL/D222+zfPlydDodUVFRnfbSsy3y8/Pjqquuqrft+++/p6amhpEjR17xft98843h0t9nn31m7jCvyJj4e/fuXe/farWab775hj59+uDt7W22WC9nTOw5OTlERUXV23bpd5mvr695gr0CmyvgMjIyuPrqq694+969e43+kOh0Or799ltuv/12/Pz8jA3RaOY4toqKCk6ePElxcbGhD8Lbb7/NPffcw9q1a3FwcGhTzK1hjuM7e/YsAE5OTnzwwQcUFhby7rvvcs899/Dbb7/h6OjYpphtTXPvwfbt2ykvL6eqqooXXngBhULBDz/8wPTp0/n111+JjIxscJ+amhrc3d3rbav7XKlUKtMegIU097rt3r2bZ555hjvvvJPBgweTkZHR7GPW1taSmJjItddey7Rp0yguLuZ///sfTzzxBF999RUKhcKUhyCYQWu/0zZt2sQ777zDfffdd8WBVmfOnGHx4sUsW7YMe3t7k8d8KXPEfymNRsP8+fNJSkpi2bJlJom5jjlir6mpafCaW/K7zOYKuICAADZs2HDF2y+9jNNahw4dIi0tjbvuusvox2gLcxybUqlEpVLx8ccfG+6/ePFiRo8ezdatW7nhhhuMjre1zHF8t956K2PGjKmXqD169GDMmDFs3bqVG2+80ahYbVVz74G/vz8HDhzAyckJOzs7APr27cupU6f4/vvvG+2I7OjoSG1tbb1tdV92zs7OJozecpp73VasWEF1dTVz5sxp8WN+8803xMfHs2HDBkOx1rVrV6699lq2bdvGhAkT2hy3YF6t+U778ccfefXVV5k4cSLz589vdH+VSsXTTz/NI488Qs+ePU0e7+VMHf+lKioqeOKJJ9i/fz+LFy++Ygu+scwRe0f7LrO5As7Ozq5V/XNaY9OmTfTu3dtsj98ccxxbYGAgAQEB9T7Mvr6+eHp6tqiVwJTM9d5d3mrn7++Pp6cnOTk5Jn8ua9eS9+Dy1jS5XE5ERAS5ubmN7h8YGGhoCa2Tl5cH0CEv0xujudft119/JS8vz9BvVpIkAB588EFuvfVWXnnllQb3OXjwIL17967X0hYeHo6XlxcpKSmmPQDBLFr6nfbWW2+xdOlS7r//fp599llkMlmj+x09epSkpCQWL15suJSuVqvRaDTExsbyxRdfMHjw4A4bf528vDwefPBBMjMz+fLLL4mLizNVyAbmiD0wMNDw3VXHkt9lNlfAmdOBAwcYPny4pcMwqbi4OFavXk1eXp7hWn5eXh7FxcWEh4dbOLq2e++99/jzzz/5888/DYmZkZFBcXFxo5f7hKbt2LGDxx9/nLVr1xIWFgboL4OcOXOGa6+9ttH7xMXF8dtvv1FRUYGrqyugH4zi4uLSLq0IHcH333+PRqMx/Ds3N5cZM2bw2muvXbG/TUBAAIcOHUKSJMNnNzc3l5KSErp27doeYQvtoK6AePbZZ5k5c2aT+/br14+NGzfW2/b999+zceNGvv/+e4sUEa2JH6C0tJR7772XiooKli1bZtE5OVsbe1xcHCtWrECr1Rp+WO3bt49u3brh4+Nj7nAb6JSjUJtTW1tLfn5+vaZSrVbL2bNnrf6Ec/mx3XDDDXTt2pXHH3+cEydOcOrUKebNm0e3bt0YO3asZYM1wuXHd80115CZmclLL73EhQsXOHDgAHPmzGHgwIGMHj3awtFan4EDB+Ll5cWzzz7LiRMnSExM5Nlnn6WkpIT77rsPaPgeTJgwAT8/P5544gnOnDnD5s2beffdd5k5c6bZ+/B0FCEhIYSHhxv+goODAX2RVvfFf/nrNm3aNFJTU3nxxRc5f/48R44cYe7cufTs2bNB52vBOsXHx7N06VJmzJjBLbfcQn5+vuGvsrIS0Pe7ys/PR6vV4ujoWO9zFB4ejoeHB0qlkvDw8Hbv09va+AHeeOMN0tPTeeutt/D29q53n/ac3NuY2KdOnUpFRQXPP/88586d49dff+Wbb75h9uzZ7Rb3pUQB14jDhw8zatQoDh8+bNhWUlKCWq3G09PTcoGZwOXHZm9vzzfffENwcDD33nsv06dPx8vLi2+++cYqT66XH1+fPn344osvSExMZMqUKTz22GP06tWLTz/9tNlmfqEhV1dXvvnmG3x9fXnggQe48847KSkp4YcffjCMwLr8PXBwcGDp0qXodDruuOMOXn75Ze6++27+/e9/W/JQOpzLX7fo6Gi+//570tLSuPPOO5kzZw7du3fnq6++MvQ/FKzb77//Duhb0UaNGlXv76uvvgJgw4YNjBo1iuzsbEuG2qjWxq/VatmwYQNqtZp77723wX3a8xiNee19fHxYunQpFy5cYPLkySxevJj58+czefLkdov7UjKprjOGIAiCIAiCYBVEC5wgCIIgCIKVEQWcIAiCIAiClREFnCAIgiAIgpURBZwgCIIgCIKVEQWcIAiCIAiClREFnCAIgiAIgpURBZxgNDEDjSCYl8gxQRCuxKoKuAULFhAdHX3FvystSWMJH330kUWXCGmNGTNmMGPGjFbdJykpibvuuqvetujoaD766CNThtZiv/76a4PPQ79+/bjhhhv49NNPjZrhe/z48U1+3oqKigD9Ysbvvvsu48aNo3///tx5553s3LnT1Idok8aPH8+CBQssHYbJZGRkEB0dza+//trmx2osxwShMSKPOierWwvVz8+PxYsXN3qbmJ28/fz555/1VqoAWLlyJYGBgRaKSG/x4sX4+fkhSRLV1dUcOnSIDz/8kJqaGp544olWP95VV111xRUD6hZ1f/7559m2bZthCbLVq1cze/ZsvvvuO5MuLC10Lo3lmCAIQh2rK+Ds7e0ZMGCApcMQGtER3pdevXoRGhpq+PeIESNIT09nxYoVRhVw3t7eTR5XRkYG69atY+HChUybNg2AYcOGcejQIZYvXy4KOEEQBMEsrOoSakvNmDGDBQsW8OmnnzJixAgGDRrEv//9bzIzM+vtd/z4cR544AGGDh3KwIEDefjhh0lKSmr186lUKt544w1GjhxJbGwszz33HCqVqsF+CQkJTJ8+nf79+zNkyBCeffZZw2W4OocPH2batGkMGDCAsWPH8u2333LfffcZmsfrmpa//vprrr/+evr378+qVasA2Lx5M3fffTexsbH06dOH66+/nmXLltV7/KysLB577DEGDRrEyJEj+frrrxvEWVNTwzvvvMO1115Lnz59GDhwIPfffz+nT58G9JeH61pBL71sevkl1Ly8PJ577jmuuuoq+vXrx2233caWLVvqPVd0dDTLli3j+eefZ8iQIcTGxvL4449TUFDQqvegKe7u7g3WPT1w4AAPPPAAcXFx9OnTh/Hjx/PRRx+h0+la9dj+/v788ssvTJw40bBNLpejVCob/QwITfvll1/o2bMnH3/8cYvvU1JSwsKFCxkxYgR9+/bljjvuYO/evYbbv/vuuwaXY/bt21fveRYsWMCMGTP45ZdfGDduHLGxsdx7772cOXPGqOPIz89n7ty5xMbGMmTIEF588UXDAtl1fv75Z2666Sb69OnD2LFj+eijjwyX+q+UYzqdjs8//5xrrrmGPn36cN111/H999/Xe9wZM2bw9NNPM3fuXAYMGMD9998PQHl5OW+88QYTJkygb9++3Hzzzfzyyy9GHZ/QsXWWPNJqtSxbtoxbbrmFfv36MXbsWN5+++16370LFizggQceYOXKlUyYMIF+/frxr3/9iwsXLrBt2zZuueUW+vfvz+233244x9XZvXs3d999N4MGDWLo0KE89dRTHWtNWsmKPPvss9K4ceMktVrd6J9Op5MkSZKmT58uDR48WLrmmmuk9evXS+vWrZPGjh0rjRs3TqqqqpIkSZL27t0rxcTESDNnzpQ2b94srV+/Xpo4caI0cOBA6dy5c62Ka86cOdKAAQOkb7/9Vtq+fbv0yCOPSDExMVJUVJRhn/3790sxMTHSAw88IG3dulVavXq1NHbsWOmmm26SqqurJUmSpHPnzkn9+vWT7r77bmnLli3SL7/8Io0YMULq06eP9Oyzz0qSJEnp6elSVFSUFBsbK/3yyy/Sn3/+KWVnZ0vbtm2ToqKipNdee03as2ePtHXrVmnWrFlSVFSUdOTIEUmSJKmyslIaN26c4XX5448/pBtuuEGKiYmRpk+fXu94hg8fLv38889SfHy89NNPP0kjR46UbrjhBkmn00nZ2dnSf/7zHykqKko6fPiwlJ2dLUmSJEVFRUkffvihJEmSlJ+fL40ePVqaMGGCtHr1amn79u3S3LlzpejoaGnNmjWG54qKipIGDRokLViwQNq5c6e0fPlyqW/fvtKTTz7Zqvdg1apVUlRUlJSammr4PJSXl0t///23FBcXJ3300UeGfU+fPi317t1bmjdvnrRz505px44d0jPPPCNFRUVJv//+u2G/cePGSfPnz2/0s9YYrVYrZWVlSa+99prUs2dPaceO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pLy8vB0itH4teY0XLFhAdHR0o3+LFy9up0iF1igsLOSZZ55h2LBhxMbG8tBDD3H+/PkW3Xft2rVER0eTkZFh5ijbprXHqFareeeddxg9ejQDBgxg+vTpnD59uh0jFszpwoULxMbG8uuvvza7r06nY9asWXz00UftEJnxWnJMSUlJPPTQQwwdOpThw4czd+5csrKy2jHK1mnJMZ08eZJ7772X2NhYhg0b1i7ndFHAmYhOp+Pdd99l5cqVze6rVqt58MEHUSqVrFy5kvfff5/4+HheeOGFdojUerXmNX7++efZtWtXvb+bbroJPz8/br/99naIVmitRx99lNTUVD7//HN++eUXHB0due+++6iurm7yfpmZmbzyyivtFGXbtPYYX3rpJX799Vdef/11Vq1ahbe3Nw8++KD4sWcD1Go1Tz/9NFVVVc3uW1tby3/+8x927tzZDpEZryXHVFxczP3334+joyPff/89X3zxBUVFRcyaNQuVStWO0bZMS46poKCA+++/n5CQEH799Vc++eQTDh482KLGnLYQBZwJnD9/nrvvvpuff/6Z4ODgZvc/d+4cKSkpzJkzh4iICAYPHsy0adM6fHJaUmtfYzc3N/z8/Ax/x48fZ8OGDbzzzjsEBAS0Q8RCa5SWlhISEsJrr71Gv379iIiI4N///jd5eXkkJSVd8X46nY5nnnmGmJiYdozWOK09xvT0dFatWsWiRYsYPXo0ERERvPbaa9jb23PixAkLHIFgSh999BGurq7N7nfo0CGmTJlCQkIC7u7u7RCZ8VpyTJs3b6aqqor//e9/REVF0adPH9566y3Onz/PoUOH2inSlmvJMWVmZjJq1CheeeUVunXrxsCBA7njjjvYvXu3WWMTBZwJ7Nu3j4iICH7//XdCQ0Ob3d/Lywu5XM5PP/1EbW0tRUVF/Pnnn/Tv378dorVOrX2NL6VSqVi0aBFTp05l6NChZopQaAsPDw/eeecdoqKiACgqKuKbb74hMDCQyMjIK97v008/Ra1WM3v27PYK1WitPcbdu3fj5ubGmDFjDNvc3d3ZunUrw4cPb7e4BdM7cOAAK1eu5M0332x237///pvRo0fz22+/4ebm1g7RGaelxzR8+HA++f/27jusqfPtA/g3k703ooBgQBAEZYgbd1vrrLVWW221dbX+XNVaq63W7qp1jzr6Olq14m5r3VtR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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "visualize.sampling_1d_marginals(result_pypesto);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Storage\n", - "\n", - "As the analysis itself is time consuming, it is neccesary to save the results for later usage. In this case it becomes even more apparent why one needs a **unified result object**, as otherwise saving will have to be adjusted each time one changes optimizer/sampler/profile startopint or other commonly changed things, or use an unsafe format such as pickling.\n", - "\n", - "pyPESTO offers a unified result object and a very easy to use saving/loading-scheme:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "import tempfile\n", - "\n", - "# create temporary file\n", - "fn = tempfile.mktemp(\".h5\")\n", - "\n", - "# write result with write_result function.\n", - "# Choose which parts of the result object to save with\n", - "# corresponding booleans.\n", - "store.write_result(\n", - " result=result_pypesto,\n", - " filename=fn,\n", - " problem=True,\n", - " optimize=True,\n", - " sample=True,\n", - " profile=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: You are loading a problem.\n", - "This problem is not to be used without a separately created objective.\n" - ] - } - ], - "source": [ - "# Read result\n", - "result2 = store.read_result(fn, problem=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnAAAAHQCAYAAAAh51fQAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADsYElEQVR4nOzdd3hTZfvA8W9G996bAi0tUFaBskFA3AoCjlcBB6Loq6CiIr4qrwv197oVxYFbEFREQFDZsgtlz1Io3XvvNOP8/giNlJaONGma9PlcVy/l5CS5T5I7585zniGTJElCEARBEARBsBpySwcgCIIgCIIgtI4o4ARBEARBEKyMKOAEQRAEQRCsjCjgBEEQBEEQrIwo4ARBEARBEKyMKOAEQRAEQRCsjCjgBEEQBEEQrIwo4ARBEARBEKxMpyvgpk+fzvTp0y0dhiBYHZE7gmAckTuCOSgtHUB7y87OtnQIgmCVRO4IgnFE7gjm0Ola4ARBEARBEKydKOAEQRAEQRCsjCjgBEEQBEEQrIwo4ARBEARBEKyMKOAEQRAEQRCsjCjgBEEQBEEQrIwo4ARBEARBEKxMp5sHrjNIOJ1DcZkKAC93Bwb3CrRwRIJgGnuOZVFaof9se7g6MKJfsIUjEgTbs+9Edr08G9YnyMIRCY0RBZwNKi5TkV9SbekwBMHkSitUFJerLB2GINi00goVhaU1lg5DaIa4hCoIgiAIgmBlRAEnCIIgCIJgZUQBJwiCIAiCYGVEAScIgiAIgmBlRAEnCIIgCIJgZUQBJwiCIAiCYGVEAScIgiAIgmBljCrgcnNzTR2HIHQaIn8EwTxEbgmdiVEF3Lhx45g1axYbNmygtrbW1DEJgk0T+SMI5iFyS+hMjCrg3njjDXQ6HU8//TSjRo3i5Zdf5vjx46aOTRBsksgfQTAPkVtCZ2LUUlqTJk1i0qRJ5Obmsnr1atasWcOPP/5IZGQkU6ZMYeLEifj6+po6VkGwCSJ/BME8RG4JnYlMkiTJFA908uRJ3nzzTRISElAoFIam7P79+5vi4U3m6quvBmDLli0WjsQ0Ll24HqBWrWHj/nRqVBp6hHnSv4cf1wwNt2CEQktYQ/50hNz5Y88Fw1qoXm4O3DCim8ViEaxDR8itjpA7rfHXvhTDWqg+Ho5cN6yrZQMSGtXmxewTEhJYs2YNmzZtoqysjJEjRzJ27Fi2b9/OXXfdxfz587nvvvtMEKrQmEsXrr+QVUr8yRzqSvKsgkrslHJRwHVgIn8EwTxEblnWvhPZlFbof2x5uDowrE+QhSOyPUYVcKmpqaxZs4a1a9eSmZlJSEgIM2bMYMqUKQQF6d+k6dOn8/TTT7NkyRKRJO2gtELF/pO5SBJEdfFErdFxIauMbYcyuP3qKIL9XC0donCRyB9BMA+RWx1HaYXK0IonmIdRBdx1112Hg4MDEyZM4NVXX2X48OGN7te9e3dSUlLaEp/QApIkceB0LjpJIjLUk+uGhpNfUk21SkNOYRXfbjjFc/cOsXSYwkXmyp/PPvuMXbt28f333xu2nT59mkWLFnHixAm8vb257777uOeee9p6CILQIYlzk9CZGFXAvfjii0ycOBE3N7cm9/v3v//Nv//9b6MCE1ouNaec/OJqFHIZN43siiSBTCYjNsqfP/amsOdYNlkFFQT7ila4jsAc+bNs2TLef/99Bg8ebNhWXFzM/fffz/jx43n55Zc5cuQIL7/8Mi4uLkydOrVNxyAIHZE4N5mOJEmk55ZzJrUIezsFYwaEoFDoJ6649PIoNH+JVCYze7idklHTiPz111/k5eU1etuZM2e45ZZb2hSU0HI6ncTx8wUAxHT3wcPVwXCbp5sD4YH6L7INu1MsEZ7QCFPmT25uLg8//DBvv/02Xbt2rXfbTz/9hJ2dHa+88goRERFMnTqV++67j88//7wt4QtCh2Xq3IqOjm7w9+uvv5oq3A5LJ0nsOZbN7mPZHE7M593lh3j6o51k5JUD/1werfurK+ZKK1Ss3JzIk+9t5/PfjrNh9wXOphXj5mzHvhPZ/LUvhb/2pbDvRLYlD89mtLgFLiEhgboBq/v37+fAgQMUFRU12G/btm2kp6ebLkKhSYlpxVRUqXGwUxDVxavB7f17+JGaU87m/alMv74njg5tHrciGMFc+XPy5Ens7OxYu3YtH3/8MZmZmfWec8iQISiV/7znw4YN47PPPqOgoKBdp1PYcyyrXofmEf2C2+25Bdtmrtw6c+YMDg4ObN68GdklTUjNte7ZgqNn80nLLUcug6guXqTnVXAuvYSnPtjBU9MGNdi/WqXh2/Wn+H1XMjW1WsP2WnUtB8/koarVcNXAUIoumTFBaLsWn81//vln1qxZg0wmQyaT8fLLLzfYpy6Jbr75ZtNF2IlcOiWIl7sDg3sFNrm/Vqtj/6kcAHp29cJO2bBBNTzQjUAfZ3IKq9h3Ipuxg8JMH7jQLHPlz/jx4xk/fnyjt+Xk5BAVFVVvm7+/PwDZ2dntWsCVVqgM038YS63RkV1QiUYn4epkZ6LIBGtnrtw6e/YsXbt2NeRMZ1FeVcv+U/olyQb1CmBoTCCDewXw1g8HOZlcyGtfxdOrqzfBvi5odBJZ+RUkZ5ah0eoAiAj14KYR3SgoreZMSjGHz+ZxIrkID1cHugS6W/LQbE6LC7gXXniBqVOnIkkS9957LwsXLiQyMrLePnK5HHd3d3r06GHyQDuDS6cEaYltB9MprajFwU5Bj7CGrW+g7wt31cBQVm46y9+HM0UBZyGWyJ+amhrs7e3rbXNw0F9iV6kaL6bq5qtqTHZ2tmEkX1sY0x+msLSahZ/vJS2nHBkwINqP4WJaAgHz5VZiYiIRERGmDrfD27DnAmqNDk9XByJCPADw8XDi1dkj+GLNcf7Yk8KpC0WculC/lTMyzJN/TYhiSEwgMpmMv/alEB3uhUIh48CpXOJP5uDr6YSzo/jxZSotLuDc3NwYMkQ/kvG7774jJiYGFxcXswUmNE2t0fHjxkQAenX1brT1rc5VsfoC7nBiHmWVtbi72F9xX8E8LJE/jo6ODdaDrCvcnJ2dzfrcTXFztjdcUm3J5VSNVsdrX+8nLUff/0YCDifm01X8mhcwX26dPXsWLy8vpk2bxoULFwgPD+eRRx5hzJgxje7fHj9+zE2SJLbs119m7tnVq96lYzulnH9P7c+4gWEs+fUoeUVVyOUyfDyc6Bvpw4OT+tbbv05EiAc5hZWk51ZwJqWYgT07V4umObW4gPvtt9+46qqr8PLyIisri6ysrCb3v/XWW9sam9CEjfGp5BVX4+yopEcXzyb3DQtwo1uwOxeyythzLIvrh3dtlxiFf1gifwIDAxt06K77d0BAQKP3aWqm+KZOUK3Vmkuqa3ec51x6Ca5Odtx6VQS7jmaRkl1G/MkcZk7sY7KYBOtkjtzSaDQkJycTGRnJggULcHV1Zf369Tz00EN8/fXXV5yexNqdTikiu7ASO4WcMH99X7/La7Je3by5aWS3enO8+Xg4Nlq86e8vY2S/YFZsOsv5zFL6RvqYLf7OpsUF3IIFC/jpp5/w8vJiwYIFTe4rk8lEAWdGNbUaftqsb32L6xWAUtH8YOIxsaFcyDrFjsOZooCzAEvkT1xcHCtWrECr1aJQKADYt28f3bp1w8fHcl+iOp1EZn4FBSXVKBUeTe6rH9V2FoAHJsag1ujo38OXtJwysgoqSc4spXtI048h2DZz5JZSqSQ+Ph6FQoGjoyMAffr0ISkpiS+//LLRAq69fvyY09YEfetbRKgHyotXddxd7OtNGxJixKTw3UPc8XRzoKRcRXJmGYE+4uqdKbS4gNuyZQt+fn6G/zcHMRFpy/y8JYmiMhX+Xk7EdPdpsiVDfvFH0ZgBIXy7/hQnkgsoLK3Gx8OpnaIVoH3y53JTp05l6dKlPP/888yaNYtjx47xzTffNNrJu71UVNXy7YbT5BZVAbDnWDYV1Rruvak3CnnDX/ArNiVSVaOhe7AH4wd34a99KTg72hHi70Z6bjmbD6TxUEjf9j4MoQMxV241dhm2R48e7Nq1y2TP0ZFodRJ7j+un94gOr9+n+tJVFTxcW98FRyaT0S/Slx2HM0nOKmVEv45/OdkatLiACwkJafT/62g0GioqKvD09DQqEDERacuk5pTx67YkAGZN6kNFlbrJ/d1dHQyjW4N8XMgurGT5X2eYc0dse4QrXGTu/GmMj48PS5cuZdGiRUyePBk/Pz/mz5/P5MmTTfYcrVGr1rLtYAYV1Wrs7eS4OtlRVKZi9fZz5BVV8dS0QfX6cqZml7FhTwoAM2+JQX5Jgdc1yJ303HL2Hs/mwUl9rnj5RrB95sitpKQk7rzzTpYsWcLQoUMN20+cONFggIStSEorpqyyFhcnO4L9XClpxajxlqRfjzBPdh7JpKS87SPSBT2jJgXTaDR8+umnhIeHc8sttxAfH8/cuXMpKytjyJAhfPjhh3h4tOyyRm5uLv/973+Jj49vciJSpVJJREQEqampfP75552ygKtRafjf9wlotBJDegcyrE8Qm/enNXu/utGtwX76Au7w2fx2iFa4ElPmz6XefPPNBtv69evHypUrTRF2m0iSxP6TOVRUq/FwtWfSmAjUGh2FpdVsTUhn97Es1BodC+4djJ1SgU4n8enqY+h0EsP7BtE/yq/e4wX6OKNUyCgoqSY5s5SIUE/LHJjQoZgqtyIiIujevTuvvPIKL7/8Ml5eXvz0008cOXKEVatWtcORtL+6KakGRfs32hrelJZcZnVyUBLgrZ/S6nxGSZvjFYxcieHDDz9kyZIllJWVAfDaa6/h6enJc889R1paGu+8806LH+vSiUj79+9f77YrTUSakpJCQUGBMaFbLY1Wx7s/HiItpxwvNwcevb1/q1sdwgPdkMsgv7ialOwyM0UqNMeU+WMt0nMrSM+rQC6DW8dEGOZxGxjtz9RxPVDIZew/lcMzH+6kuLyGb9af4sT5QuyVch5oZKCCUiGnW7D+RPzDn6f5Y88F9hxruvO6YPtMlVtyuZxPP/2Ufv368cQTTzB58mSOHj3K119/3WBuRVtx4OLcb3G9Gx/g1JxLV2eoqK5tdJ+6eeCS0kuMeg6hPqNa4NavX8+8efOYNm0a58+fJykpiTfffJNbb70VT09P/ve///HKK6+06LHMMRGpLQznvlRVjZp3lx8i/mQOcrmM8YPDOHg6l7CA1nUmdbBXEuznSkZeBVsOpDV6YhTMz5T5Yw0kSSL+pP7XfY8uXgT5ulBW+c8XvI+HI2NiQ9hxOJPzmaXc89JfhtsemdqPAO/GpzzpEeZJUnoJ5zNKrzgPotC5mDK3fH19eeONN8wccceQV1xFSnYZchkM7BnA3uPm+TEU6u/KgZNQWFpDXnEV/l6Wm87IFhjVApeXl2doLdu+fTtyudwwN05gYCDl5eUmCc6YiUhtzYWsUua9/zfxJ3NQKuTcNKIr9nYK8kuqKW+m/1tjul9stdh+MMMwczboV4HYFJ9q+Es4nWOyYxDqa6/86Sj2n8whv6QapUJG727eje4T6OPC+MFh+HjoR/y5u9jz+J0DmDAk/IqPWzf6tLhcheqS5XuEzquz5ZapHDytb32LDvc26zyhDnYKfDz1OX7oTONr1gotZ1QLnL+/PxkZGQwePJitW7fSq1cvvL31X8yHDx8mMLDpJaBaytiJSG1hOLckSazflcyX606i1ujw9XBk/ow4MvLKW7Vaw+WCfF1wdlRSUqEi4XQuwy7OZt/aVSAE47VX/nQUdQMReoR54Wh/5a8cX08n7pwQxejYUJzsFSguTo9TN+lv8GX9alyd7PByc6C4XEVecRWBPuLXfGfX2XLLVPa38fJpawT5ulJQUsPBM7liSqs2MqoF7uabb+aNN97ggQce4ODBg4YBBYsWLeKjjz7illtuMUlwxkxEagu0Oh1/7E3l09XHUWt0DO4VwAdPjaPXFVovWkMulxF9cdH7P/amtPnxhNZrr/zpCLILKjmUqM/ZyNCWDcxwdbIzFG/wz6S/FVUN+9XUdZaum5ZE6Nw6U26ZSk2thmNJ+oFtcb3NX+AG++qnZzmalI9ao2tmb6EpRhVwTzzxBDNnzkQmk/HUU09x9913A3D8+HFmzpzJI488YpLg4uLiOHjwIFrtP5dHOsJEpOak1UnsOJzJuYwSlAoZD07qw8IHhpq0WbtvpC9ymb4J+0JWqckeV2iZ9sqfjuCvfSkAdAlww9XZ9JdmQvxFASf8ozPllqkcP1dArUaHn5cT4YFuZn8+LzcHnByUVKu0nE4pNPvz2TKjLqHKZDJmz57N7Nmz621fsWKFSYKq0xEnIjUnSZI4eDqXnMIq7JRyFj4wlAFRpl83ztPVgVH9Q9hxJJOftyQxf8bg5u8kmEx75Y+lSZLEjiOZAPTqap5BBnW/5ssqa6mqaX2fUMG2dJbcMqW60aeDewW0y3yKMpmMLoFuJKYWc/B0Hv0i/Zq/k9Aoowo4gPLycvbt20dVVRWSJDW43RRLAXW0iUjNLTGtmPOZ+haxG4Z3NUvxVmfq+B7sOJLJziOZ3HpVhNmeR2hce+SPpSWmFpNfXI2Tg4LwIHejBt00x8lBaViiJzO/0uSPL1ifzpBbpiJJEgcuDmAY0g6XT+uE1xVwZ3K5/5aYdnteW2NUAbdz507mzp1LdXXjnd6NXcuxI09Eam6lFSp2HNa3VvSN8KFrkLtZn697iAfjB4exNSGdT389xvXDrjzaTzAtc+VPR7PzYuvb0JigFq3Xa6wAL+eLBVyF2Z5DsA6dJbdMJSW77OKaxHJyiyr5a1+KUWudtlZYgBsyGaTmlIulHdvAqALunXfeoXv37jz33HMEBAQgl5vvy7mz+G7DaWpqtXi42tO7W/v077vvpt7sPZ5NUnoJfp5OhJu5aBT0OkP+SJJkmFh39IAQCkvNN8I5wNuZxLRiUcAJnSK3TCnhYutbiL8LpRX6QULGrHXaWk4OSiJD9fM4Hk7MZ8KQLmZ/TltkVAF3/vx5Pvnkk3rrlgrGy8qvYPMB/ZJYcb0C6q35aE5e7o48dGsfPlh5hL0nsnF2VOInJlY0u86QP2t3JFNQWoNCLsPc51A/LydkQGlFLQUl1fh6il/znVVnyC1T2n3xR1a3oNYv3ddWA6P9LxZweaKAM5JRX63BwcFUVIhfu6ay/K9EdDqJrkHu7V5AXR3XhTEDQpAk2HEki/JGpmoQTKsz5E/d6DI/LyezT7Jrb6fAy10/Oeixc2Kd386sM+SWqWQVVHA+oxS5XEZEC6f4MRWZDGKj9X28D5/NR6tr2FdRaJ5RBdzs2bP5+OOPycjIMHU8nU5BSTU7j+hfx2F9/ulE2k6NcMhkMubcMQB/Lydq1Vr+PpQhZrU3M1vMnz3HsvhjzwX+2HOBo0n5pOXqZ7wP8nFpl+cPuDiJ7+GzooDrzGwxt8xl1xF961u/SF+cHIwez2gUdxd7SipqsFfKKa+qZc3f59r1+W2FUe/aunXryM3N5ZprrsHb2xtHR8d6t8tkMjZv3mySAG3dn/tS0EnQN8IXfy9nw2oI7q4OJJzOobjsnyXDWrv2aUs5Oii5ZVR3ftyUSHmVmp1HMrn96h5meS7BNvOnbrJdAGdHJVkXR4QG+bZPARfk48LpC0UcOZuPTie1WzcEoWOxxdwyB0mSDIOMRvUPAdq/BayiSo2/tzMZeRWculDElHHtHoLVM6qACwwMFEuSmIBao2PjvlQAbhzZleoaTb3bL1/eytPNwWyxuDjZcVVsKJsPpJFfUs22g+lcP7xru8wL1NnYev6k55aj1Uk4OSjNuq7ipXw9HVEq5JSUq0jNKaNbcPv36REsz9Zzy1SS0ktIyS7DTilnRL8gw4Cj9hbk40JGXoWhxV5oHaMKuDfeeMPUcXRK+05kU1yuwsvNgWF9gtiWkG7ReDzdHBjZP5i/D2ZwOqWY33dd4JbR3S0aky2y9fy5kFUGQKCPc7v9AFDI5YT4uZCaU86hM3migOukbD23TOXPi8sojuofjJsZVkhpqbr1i3MKK6msVuPiZGexWKxRm8aHnT9/nu+++463336b3NxcEhISRAfSVvjj4iLf1w4LN+s8Wa0R5OPCgCj9zNhL157gxPkCC0dku2w1f+oKuPbq/1YnLEC/DNDhs3nN7CnYOlvNLVOorFYbVki5blhXi8bi6myPm7MdkqRfG1VoHaNa4HQ6HQsXLmTVqlVIkoRMJuOGG27gk08+IS0tjR9++EE0Yzfjjz3JHD9fgEwGzg5KzrTjmnDNdQ+KDveiqkZDYlox7yw7yIdPj7PorzRbY8v5U63SGC771/26bo22NNjV9RE9mVxETa0GR/v27ZgtWJ4t55ap/LE3BVWtlvBAN3p387Z0OAT7uZKYWsy+E9mM6Bds6XCsilHNPp988gnr1q3jtddeY/fu3YblSp555hl0Oh3vvfeeSYO0RbuOZgMQ4udKTa3WLMsMXUndAIlN8alsik9tUDzKZDLGDQol2NeFgtIaPv75aKNL0gjGseX8yS7QD17w83LCwYgCys3ZvsGI1pbydHXA38sJjVbHsXOi5bgzsuXcMoVatZa1O84DMGVcZIfo4xzmr//htf9ULhqtzsLRWBejCrhVq1Yxd+5cpk6diqenp2F7r169mDt3Lrt37zZVfDapWqXhdEoRAJFhnhaJoW6ARH5JdaPFo72dgqemDUIhl7H7WBbbDoph+aZiy/mTU6gv4LpcvJxpjLoRrcXlKipaMS+hTCZjSIy+dWX3Uct0yhYsy5ZzyxQ27U+juFyFr6cTY2JDLR0OAD6eTjg5KKmsVnNc/PBqFaMKuIKCAnr16tXobQEBAZSVlbUpKFu3/VAGao0ON2c7Ar077soHUV28uOu6aACWrjlBaYWqmXsILWGr+SNJEjmFVcA/v6rbm35KBIg/kY1aI+Yz7GxsNbdMoUalYeWmRABuGxfZYfpdy2Uyuofol3HcJX54tYpR72B4eDh///13o7ft37+f8HCxMPqVSJLEht0XAH3rW0dowm5MXT+5qeN60DXInfKqWpauPWHZoGyEreZPcbkKlVqLvVJOQDsPYKjTq6s33u6OVNZoxKS+nZCt5lZb7TuRzTvLD+pnPXB3wNPNgb/2pfDXvpQOMVCtR5gXADuPZFKj0jSzt1DHqF6+9957LwsXLkStVjNu3DhkMhmpqanEx8fz1VdfsWDBAlPHaTPOpBSTkl2GUiGjewee6uDSiYSH9A4gJbuM7QczGDcwjIE9/S0dnlWz1fyp6//WJdAdhYUm0pXLZYzqH8zancls3p/GkN6du8N6Z2OrudVWuUWVHDyjH509ZkAIFdVqCktrgPZZvL45IX4uBPm4kF1Yya6jWWJt1BYyqoC7/fbbKSoqYsmSJSxfvhyAefPmYWdnx6xZs7jrrrtMGqQt2bBH3/oWFeaFvZ3CwtE0ra6fnEIhZ0APX44kFfDJqqN8Mn98h4+9I7PV/Knr/9Y92N2icVw7NJy1O5OJP5lDfnE1fl5icfvOwlZzq60OnclDrdHh6epATHdvyirbb9BcS8hkMq4Z2oXvNpzmz30pXB0X1mGvTnUkRo+zf/DBB7nlllvYv38/SqUSNzc3+vfvX6/jqFBfaYXKcI2/b6SvhaNpnWF9gkjPqyC3qIq1O5O5bbxYaqstbC1/atVaCi5OH9LNwgVceJA7fSN8OX6+gD/2XuCeG3tbNB6hfdlabrVVYWk1R5P0l0n79fDtsIXR1XFd+HFjIompxRxLKqC6VmPod+3hqp/sXqiv1QXc77//zooVKzh69Cgajf5ataOjIwMHDuSuu+5iwoQJJg/SVmyMT0Wj1dEjzJMAb+d6y2R1dPZ2Cu65sTfv/XiInzaf5eq4MLzcHJu/o1CPreZPVn4lOglcnezwcnekrLLlo0fN4eZR3Th+voANuy8waUwEHq7mW4ZO6BhsNbfaasWms2h1Er6eTgS309rExvB2d+T64V1ZtzOZH/48zdVxYRSViYFzTWlxAafVannqqaf4888/CQgI4KabbsLX11c/8iwnh/379zNnzhwmTZrEm2++ac6YrZJWq2PDxZUXbhzRzermVZPLYOzAUH7flUxSegnL/jzDY7cPsHRYVsPW8yc9T7+WYaCFBi9cblifILoHe5CcVcqKjYnMntLP0iEJZmLrudUWWfkVbIzXr7c9oAO3vtW5fXwP/tqXypnUYoL9XAnowLM0dAQtLuCWL1/Oxo0bef7555k+fXqDD4JWq2XFihW8/vrrDB48mNtuu83kwVqzfSdzKCipxt3FnjGxIfx9yLrmVXN3deBQYi79e/iSlF7CX/tS6Rrkxs2jIiwdmlWw9fypW4w6yLdjfOHK5TKG9gkkOauU9Xsu4OnmwJ3XRFs6LMEMbD232uL7P06j00mEB7nh59UxcrMpXu6O3HVtNN+uP8XOI5lcNyxcrALUhBZPI/Lbb7/xr3/9ixkzZjRaxSsUCqZNm8Ydd9zB6tWrTRqkLfh9VzIA1w/varUDAIrLVNgpFXQJ1E/SumF3itW1JFqKLedPTmElpRW1yGR0qF/MXm4OhAe5IUnw6/Zz5BVVWTokwQxsObfa4mRyIbuOZiGTwXAr6j82eWwkvbt5o9bo2HkkE7VGrM5wJS0u4C5cuMCYMWOa3W/06NGcPXu2TUHZmgtZpZw4X4hcLuPGEV0tHU6b9e/hh1wuIz2vgoTTuZYOxyrYcv7Uzbfm6+GEnbJj/TiJ6xWIh4s9VTUanv5wR4uX5rp0Oa89x8Tkoh2ZLeeWsXQ6iaVrjgP6Udm+ntYzElshl/HM9ME4Oyoprahl7/EsdKKhoFEtLuCqq6vx8Gh+3jIvLy8qKyvbFJSt+XXbOQBG9A3Cx8N6EulKXJ3siO6in3jxq3Unxfp1LWDL+VN+ccBCqIVWX6jTWPceO6WcsYNC8XZ3pLhcxQuf7mH+Rzv5dVsSpy8UUatufLWGS5fzEiuQdGy2nFvG2pqQxrmMUpwdlUy/vvGVKToyX08nbhrZDblcRmZ+JfuOZ1s6pA6pxX3gJElCoWj+17VcLheX1S6RmV/BjsP6/m62NPVG727eXMguJSOvgr/2pXLTyG6WDqlDs+X8mTw2kpLyGpwd7Swah5uzPXuOZVFaoSLY759i0tnRjiljI8gurOLPvSmcTikyrEWsVMiICPEkuqsX/SJ8GdwrAEUHWWJIaBlbzq0r2Xci+4pTbBSV1fDVupMA3DkhGk+3jj8Cu7EfXwHezgyNCWTv8WwOJeazNSGd8YPD2j+4DszoeeCEllm5KRGdBENjAokI9bR0OCZjb6dgaO9A/j6cyfK/zjB2YCguTpY9gQuWYaeUExbgRnG55Vuq6lrO3F3qd3y2t1Pw8JR+3Da+B3uOZ3EsqYDE1GJKKlQkphWTmFbM2h3JBHg7c9/NvW3mRC/YptIKlWElhUvpdBIfrjxMeZUaP08nHB0UHWKprOa4u9jXK0pDLv4A6xrkTmmFilMXivjopyME+7nQM9zbkqF2KK0q4F566SVcXZu+TFJRUdGmgGxJVn6FYbTpv2xwBFyfCF/OZZSSmV/BjxsTmTWpj6VD6tBE/lhO3S98X08nJo6OYOLoCCRJIreoijMXW+R2Hskit6iK//sugW7B7gzq6d/h+vQJjRO5pffdhlMcPJOHXC4jrncAJeUqvN07fgsc1C9KL13eq1+kLzUqDclZZfzfdwksmT8eRwfR9gSt6AMXFxeHi4sLkiQ1+efi4sLgwYPNGbPV+GrdSXQSxPUOIDLM09LhmJxCLjMUbet2nicxtcjCEXVcIn8sq+7yat3AhKNJ+ew9ns3hxDyqVRpG9Atm9uQ+xPUKQCGXcSGrjL/2pVJWaflWRaFpIrf0l5F/+OM0qy72t756cJjNTF4tk8mYMLQL/l5OFJRU88vWJEuH1GG0uIz9/vvvzRmHzUk4nUv8yRwUchn33xxj6XDMQi6Dwb0CGDsolO0HM/hg5WE+mDdWtFo0QuSP5dVdXgX9JZuyytp6/66q0RAZ5kmvbl6s2nae8io1m/enM2lMd0uGLTSjs+dWeVUtLy/dZ1is/p4be+HuYt/oJVZrZa9UMGtSH17/5gCrtp3j6rguBHXgVSXai+itawblVbUs/vkIALeM7k5YgBsJp3PYFJ/KpvhUzqQUWjZAE3F3dSDhdA5RoZ44OypJz61g6ZoTlg5LENok2NeV28f3wMvNAZVay5odySSlF1s6LEGoR5IkktJLWP5XIgfP5KFUyHns9gHcfnWUpUMzOZlMv7rKgCg/NFqdOM9cJAq4Vrq0ENsUn0rC6Zx6t+t0Eq9+uY/C0ho8XR0I9HbmTEohxWUq8kuqyS+pprxKbaHoTa+4TEV5tZrBvQIA2LAnhd/+Pm/hqAShbZwdlYwfHIaPhyMqtZYXPt3DmRTRRUDoGKpq1Gw/lEHC6VzUGh09w7348KmxXDcs3NKhmYW7iz3xJ3OI6eaNXAb7T+VwODHP0mFZnCjgWunSQiy/pJriSxbblSSJb9af4nRKMTIZDIkJoLSy1qYKtisJ8XNlRF/9UPYv157gm99PotY0PsdWc0WwIHQE9nYKxg0KI9jXhaoaDQs/32MVI/oE23b6QhErNp0lp7AKhVzGNUPCmDw2klMXCvlrX4rNfkZLK1ToJIgM089BunTtCbSdfA5SUcCZiEarY8mqY6zeru9EOjQm0CYm7W2NQT39ueta/WjbVdvO8dhb21iz43yDJYyaKoIFoSOxU8q5aWQ3+vfwpVql5aWl+zgkfvkLFnLwTC4vfLaHqhoNHi72XDcsnCExgZRX1VJYWkNhaQ0V1bWWDtOs+kb44GCvIC2nnD/3pVo6HIsSBVwbabQ6dh/NYu472/ljbwoyGVwVG0K34OZnBrc1MpmMu6/ryfwZg/F0cyCroJKla07wwKJNPPJ/W/jit+McPJMr1rYTrIq9nZwXHxjGwJ7+qGq1vPzFXn77+5yYK05oVzuPZPLaV/HUqrWEB7px7dBwmxlp2hr2dgqGxgQCsOzPM5RX2XbB2hQxmYoRJEmioKSaC9llZORWoLq4HI+Hqz1zbh9AWWUt+SXVFo7SckYPCGFQT3+2JaTz9+FMElOLyMirICOvgrU7k1HIZXQLdqdPhC9OYj4foYNzc7bn4Olc4nr6U1Wt5kxqMV+uPcnhs/nMmtiHsAA3S4co2LjN+1P58KcjSBKMGRBCTIQ3JeWdt3Dp092HlOwy0nLK+fiXozw7YzCyxpZzsHHi7NkKtWotx84VcCgxj7LKf5LH18ORcYP1/RDcnO3ZFN+5m3VBv3zRTaO6c9Oo7lRU1XI0Sf+6HUrMo6CkmnMZpaTmlDO4VwC+Ho6WDlcQmlRaoaKsSs2AKD/8PJ3YeyKHQ2fy+PeZrXQNcifIxxk3F3v8vZyJ6x2Io4MCR3slbs72HDiVU2/ZoxH9gi18NII1Wb8rmU9X6xemv25YOI9M7c/m/Z37HCOXy3j8zljmf7ST3Uez2BCR0imXcxQFXAvtP5XDF78dJ6dQ359LqZARFuDGgCg/7rspBrm881X/l7vSS3AmtYiqGjU9w72YEBdK/Mlcth3MoKishr3HsykoqWZE/xBcxVJcQgcnk8noG+nLtBt68fW6k8SfzCElu4yU7DLDPj/8ecbw//ZKOf7ezoQFuImWOqFVdDqJlZsSWb4xEYCJY7oza2KfTtnS1JioLl5Mv6EX364/xWerj6FUyLh2aHinen2sooDT6XQsXryYn3/+mfLycuLi4li4cCFhYeZf2DaroIIvfjtBwulcAFwclUSHe9Mt2B17OwV+nk6ieLuobl64SwclhAW4GgYtAHi6ORDi58o1Q7pwKqWIE+cLSEov4fF3t/PMtEH07CrWuTMlS+aOLQvxc+WFmUPJKqjg299PkZFXQZVKjVYrodboUKm1qDU6ajU6Q/eBE+cLGD0gBEmSOtVJxlpZMne2H0pn9bZzJGfpfxjcMSGK6df3FJ+by0wdF0l+cRUb9qSw+Oej7DuRww3DuxIR6sHZtGLDlTIPVweG9Qlq8rEuXYu1Jft3BFZRwH3yyScsX76cN998k8DAQN566y1mzZrFunXrsLe3b/4BjFBeVctPm8/y+64LaLQ6lAoZk8ZE4OfpROkll09F7VbfpcUa6Au2xsjlMvp09yHIx5l9J3LIK6ri2Y93ceeEKKaMi8TR3io+mh2eJXLH1l16Dg32dWVAlB/hQe4AhAe6GVZ4kCQJVyc7jp4r4Ni5Asqr1GzYk0JuURUPTe5LsG/Ta3cKlmWJ3JEkiV1Hs1iy6hhVNRrkMhljB4Uw44ZeZnk+a1WXgzKZjNmT++Ht7siPGxNJOJ1raGyRoR/w4GCvwNXJjm0H03F3ccDDxR53V3s8XBxwd7HHw9UBD1d7ikqrKamwrn6FHf4sWVtby1dffcXTTz/N2LFjAXjvvfcYPXo0Gzdu5OabbzbZc2m1Os6mlbAlIY0dhzOoVukHJ8RG+fHgrX0JC3Br0L/t8lansADxpdwaPh5O3HVtNKdTithxOJMfNyayKT6VqeN7MHpASKccZWUq7Zk7nUnduqqlFSqC/a6c7zKZDF9PJ+J6BRAe6MbJ5CIS04o5eCaPR/+3jSnjIrltfA8xkKcDau/cqaxWs/d4Nmt2nDdcjndztmNYnyB6dvUy6XPZAncX+3otZuFB7nz09Dj+2JvCoTO5ZBdUopNApdaiUmspq6wlq6Cy2ce1U8rx8XCkW7A73YI9iAj1RNGBW2k6/DfHmTNnqKysZPjw4YZt7u7u9O7dmwMHDpgskd5fcZAtBzLqbQv1dyUy1JP7bu5db063X7YkotaCnQKeu2+oodWpuKyGFZsSKSqt5tKZMoJ8nCiv0qCQQ2mlbU3qO7xPIPtP5aDVgUIGjg5K4noHEOTryt7j+oIMoF+kN3lF1azclIibkx2lFyc37hfpjUKuoEugG/+e0o+ftyWRX1zNZ6uPs3TNCUL9XSmrrGVk/2C6BnkYkkmrk1BrtNSqdag1WtJzy9l/Kod+Eb54uDlSXl1LRm45Pu5OaLRa0nLLiQz1wsvdAaVCjp1CjlIpp6CkmmPnCnh62iBio/0t9jqaQ3vlzqP/t5m0vOa/HDsyBVA37bS/pwNyuYycon/WknR3VqLRgoO9nOLyWsIDXPD1cuZwYj7dgtxIzi4n0NuJ7MJqgnycGNonmNyiSlZsTEQC/L0cCQtwQ5IkQv1cyMivvNjCn8zVcV0YGhNIdLiXUS3PSeklLF1znFkT+9CjS/ud7IvKavhzbwrXD++Kt7ttDURqr9ypqdWw6Kt4jiT9M/muo72CvpG+aDRaNu9PY8fhNApKqvHxcOJsWhGb4tORAEd7GTW1+qlsgrydyC7SX/nw93KkuLyWwb38OXAyB2uatUkO1IXr5WZPcSMjbR3tZegkGWMHhlBYWsOhxHyuGhDMU9PjeOjWviRnduHz1ceICPPgwMkcsgurCfV3Jqa7H0VlNdSoNBSUVJNTVIWXmwOShGFNZLVGR05hFTmFVew9noOrkx39o/yIjfKnZ1cvgn1dWr3Wt04nUVqporhMxZ7jWeQXV1FVo6GguIqM/Erm3DGAMbGhRr1eMqmDT2a0ceNG5syZw9GjR3F0/OdL4vHHH6empobPPvuswX2uvvrqKz5eRkYGCoWCoKD617fzS6rR6fQvhb1SjoO9ArlMRmllLd4ejtgp9FPmqdRaSsr/6ePl7e6ITpLQ6fR9X6pVmjYdr7VRKmRotPU/Qg4Xm63Lq2qp+3Qp5DK0uoYftUu3e3s4olTIqa7RUFWjbnR/c3FxsmswiCIoKIgffvih3WIwtfbKndzLJmruDGSA4uJnXyaDy79FXRztqNVo6815WJcrDvYKnOyVlFfVNviMy+Uyw4+US/s7SZJ08Tn0/9X/STSWITKZ/r6yi48hk2H4f1rTmCChf/xLnuef49THodVJeLo54GDX8KRmzfnTXrmj1emnpAL9++Zgr8TZQYlOJ1FRrUZzcaUBR3sF9nYKqlWaFs+jaaeU2/Scm/Z2CnQ6CY1Wh51SjpuzPTpJolato6pGjbOjkmqVBkkCuUyGu4v+doCaWi21ai1ODkrcXewpq6ylWqUx5LJcJkNCapDXoM9R+cW8kjfSJ1FCn5s6nf7+umZKrLoYLtXS3OnwLXDV1foP9+V9DhwcHCgtLW3148lkMpTK+oednZ0N0CC5QN+iVO957RQEeDs3+thODjR4IzqCpo7PnIy9NOTsqMTZsWX3tdSxWYP2yB0AnUr/WOI9uFzTo6od7J3E57eDaq/cUchlDc4nTX0mrOFyu6U/0w52Ctyc9bnn5tz4+fjy19HdxR53F3tD7AFWko8d/tNQ9+untra23i8hlUqFk1PjS1Vt2bKlVc9R98uptfezFrZ8fLZ8bG3VHrkD4j1oC/HadUztlTuNsfbPhDXHb22xd/iltOqq+Ly8+usP5uXlERAQYImQBMEqiNwRBOOI3BGsQYcv4Hr27Imrqyvx8fGGbWVlZZw6dYq4uDgLRiYIHZvIHUEwjsgdwRp0+Euo9vb2TJ8+nbfffhtvb29CQkJ46623CAwM5Nprr7V0eILQYYncEQTjiNwRrEGHL+AA5s6di0aj4YUXXqCmpoa4uDi+/PJL7OzE0kuC0BSRO4JgHJE7QkdnFQWcQqHgmWee4ZlnnrF0KIJgVUTuCIJxRO4IHV2H7wMnCIIgCIIg1NfhJ/IVBEEQBEEQ6hMtcIIgCIIgCFZGFHCCIAiCIAhWRhRwgiAIgiAIVkYUcIIgCIIgCFZGFHCCIAiCIAhWptMXcAsXLmTBggVN7rNgwQKio6Mb/Vu8eHE7Rdp6LTk2gIqKCv773/8ybNgwBg0axMMPP0x6eno7RNg2LT2+JUuWNPreCcY5ePBgo6/npcsOXS4jI4PZs2czcOBARo0axfvvv49Wq23HqDuWhIQEevXq1eRrBpCWlsbDDz/M4MGDGTVqFAsXLqS8vLydohTaQ3Z2NvPmzWPkyJHExcXxwAMPkJSU1OL7v/DCC4wfP96METbNmPi3bt3K1KlTiY2NZfz48fzf//0fNTU17RTxP4yJ/fTp00yfPp0BAwYwfvx4vvvuu3aKtqFOW8DpdDreffddVq5c2ey+zz//PLt27ar3d9NNN+Hn58ftt9/eDtG2TmuODWDOnDnEx8fz8ccfs2zZMsrLy3nkkUfQ6XRmjtQ4rT2+xMREJk2a1OA9FIyTmJhIly5dGryesbGxje6vVqt54IEHAFixYgUvvfQSP/74Ix9//HF7ht1hlJeXM3/+/GbzS61W8+CDD6JUKlm5ciXvv/8+8fHxvPDCC+0UqWButbW1PPTQQ+Tn5/Ppp5+yfPlyXFxcuPfeeykqKmr2/ps3b+bnn39uh0gbZ0z8CQkJPPbYY1xzzTWsXr2a//73v2zYsIGXX365w8deXFzM/fffT5cuXVi1ahWPPvoob7/9NqtWrWrX2A2kTujcuXPSnXfeKQ0bNkwaO3as9Oyzz7bq/lu2bJGio6Olffv2mSlC47X22Pbt2ydFR0dLZ86cMWxLSkqSxo4dK50/f97c4baaMe/dDTfcIH399dfmD66T+O9//ys9/PDDLd5/3bp1Up8+faSSkhLDthUrVkgDBw6UVCqVOULs0ObNmyfdc889UlRUVJPfIadOnZKioqLq5ea3334rxcbGtkeYQjvYvXu3FBUVJeXk5Bi21dTUSP3795d+/vnnJu+bm5srDRs2TJo+fbo0btw4c4faKGPif+qpp6T77ruv3rbVq1dLMTEx7fp9YEzsn376qTRq1ChJrVYbtr3zzjvStddea/Z4G9MpW+D27dtHREQEv//+O6Ghoa26r0qlYtGiRUydOpWhQ4eaKULjtfbYdu3aRVRUVL1LipGRkWzbto3u3bubM1SjtPb4amtrSUlJ6ZDHYq0SExOJiIho8f4JCQnExMTg4eFh2DZs2DAqKio4ffq0OULssNasWcPhw4f5z3/+0+y+Xl5eyOVyfvrpJ2praykqKuLPP/+kf//+7RCp0B569OjB559/TkBAgGGbXK4/LZeVlV3xfpIksWDBAiZNmsSQIUPMHueVGBP/zJkzefbZZ+ttk8vlqNVqKioqzBfsZYyJPSEhgSFDhqBU/rMK6bBhw0hJSaGgoMC8ATfCKtZCNbVp06YZfd+ff/6ZgoICnnjiCdMFZEKtPbYLFy4QHh7O8uXLWbZsGWVlZQwaNIjnnnuu3ge7o2jt8Z07dw6tVstff/3FokWLUKlUxMXF8cwzz+Dv72+mKG1bUlISXl5eTJkyhdzcXKKionjyySfp169fo/vn5OQQGBhYb1vda5+dnd1pCpKMjAwWLVrEJ598gouLS7P7BwYG8sILL/D222+zfPlydDodUVFRnfbSsy3y8/Pjqquuqrft+++/p6amhpEjR17xft98843h0t9nn31m7jCvyJj4e/fuXe/farWab775hj59+uDt7W22WC9nTOw5OTlERUXV23bpd5mvr695gr0CmyvgMjIyuPrqq694+969e43+kOh0Or799ltuv/12/Pz8jA3RaOY4toqKCk6ePElxcbGhD8Lbb7/NPffcw9q1a3FwcGhTzK1hjuM7e/YsAE5OTnzwwQcUFhby7rvvcs899/Dbb7/h6OjYpphtTXPvwfbt2ykvL6eqqooXXngBhULBDz/8wPTp0/n111+JjIxscJ+amhrc3d3rbav7XKlUKtMegIU097rt3r2bZ555hjvvvJPBgweTkZHR7GPW1taSmJjItddey7Rp0yguLuZ///sfTzzxBF999RUKhcKUhyCYQWu/0zZt2sQ777zDfffdd8WBVmfOnGHx4sUsW7YMe3t7k8d8KXPEfymNRsP8+fNJSkpi2bJlJom5jjlir6mpafCaW/K7zOYKuICAADZs2HDF2y+9jNNahw4dIi0tjbvuusvox2gLcxybUqlEpVLx8ccfG+6/ePFiRo8ezdatW7nhhhuMjre1zHF8t956K2PGjKmXqD169GDMmDFs3bqVG2+80ahYbVVz74G/vz8HDhzAyckJOzs7APr27cupU6f4/vvvG+2I7OjoSG1tbb1tdV92zs7OJozecpp73VasWEF1dTVz5sxp8WN+8803xMfHs2HDBkOx1rVrV6699lq2bdvGhAkT2hy3YF6t+U778ccfefXVV5k4cSLz589vdH+VSsXTTz/NI488Qs+ePU0e7+VMHf+lKioqeOKJJ9i/fz+LFy++Ygu+scwRe0f7LrO5As7Ozq5V/XNaY9OmTfTu3dtsj98ccxxbYGAgAQEB9T7Mvr6+eHp6tqiVwJTM9d5d3mrn7++Pp6cnOTk5Jn8ua9eS9+Dy1jS5XE5ERAS5ubmN7h8YGGhoCa2Tl5cH0CEv0xujudft119/JS8vz9BvVpIkAB588EFuvfVWXnnllQb3OXjwIL17967X0hYeHo6XlxcpKSmmPQDBLFr6nfbWW2+xdOlS7r//fp599llkMlmj+x09epSkpCQWL15suJSuVqvRaDTExsbyxRdfMHjw4A4bf528vDwefPBBMjMz+fLLL4mLizNVyAbmiD0wMNDw3VXHkt9lNlfAmdOBAwcYPny4pcMwqbi4OFavXk1eXp7hWn5eXh7FxcWEh4dbOLq2e++99/jzzz/5888/DYmZkZFBcXFxo5f7hKbt2LGDxx9/nLVr1xIWFgboL4OcOXOGa6+9ttH7xMXF8dtvv1FRUYGrqyugH4zi4uLSLq0IHcH333+PRqMx/Ds3N5cZM2bw2muvXbG/TUBAAIcOHUKSJMNnNzc3l5KSErp27doeYQvtoK6AePbZZ5k5c2aT+/br14+NGzfW2/b999+zceNGvv/+e4sUEa2JH6C0tJR7772XiooKli1bZtE5OVsbe1xcHCtWrECr1Rp+WO3bt49u3brh4+Nj7nAb6JSjUJtTW1tLfn5+vaZSrVbL2bNnrf6Ec/mx3XDDDXTt2pXHH3+cEydOcOrUKebNm0e3bt0YO3asZYM1wuXHd80115CZmclLL73EhQsXOHDgAHPmzGHgwIGMHj3awtFan4EDB+Ll5cWzzz7LiRMnSExM5Nlnn6WkpIT77rsPaPgeTJgwAT8/P5544gnOnDnD5s2beffdd5k5c6bZ+/B0FCEhIYSHhxv+goODAX2RVvfFf/nrNm3aNFJTU3nxxRc5f/48R44cYe7cufTs2bNB52vBOsXHx7N06VJmzJjBLbfcQn5+vuGvsrIS0Pe7ys/PR6vV4ujoWO9zFB4ejoeHB0qlkvDw8Hbv09va+AHeeOMN0tPTeeutt/D29q53n/ac3NuY2KdOnUpFRQXPP/88586d49dff+Wbb75h9uzZ7Rb3pUQB14jDhw8zatQoDh8+bNhWUlKCWq3G09PTcoGZwOXHZm9vzzfffENwcDD33nsv06dPx8vLi2+++cYqT66XH1+fPn344osvSExMZMqUKTz22GP06tWLTz/9tNlmfqEhV1dXvvnmG3x9fXnggQe48847KSkp4YcffjCMwLr8PXBwcGDp0qXodDruuOMOXn75Ze6++27+/e9/W/JQOpzLX7fo6Gi+//570tLSuPPOO5kzZw7du3fnq6++MvQ/FKzb77//Duhb0UaNGlXv76uvvgJgw4YNjBo1iuzsbEuG2qjWxq/VatmwYQNqtZp77723wX3a8xiNee19fHxYunQpFy5cYPLkySxevJj58+czefLkdov7UjKprjOGIAiCIAiCYBVEC5wgCIIgCIKVEQWcIAiCIAiClREFnCAIgiAIgpURBZwgCIIgCIKVEQWcIAiCIAiClREFnCAIgiAIgpURBZxgNDEDjSCYl8gxQRCuxKoKuAULFhAdHX3FvystSWMJH330kUWXCGmNGTNmMGPGjFbdJykpibvuuqvetujoaD766CNThtZiv/76a4PPQ79+/bjhhhv49NNPjZrhe/z48U1+3oqKigD9Ysbvvvsu48aNo3///tx5553s3LnT1Idok8aPH8+CBQssHYbJZGRkEB0dza+//trmx2osxwShMSKPOierWwvVz8+PxYsXN3qbmJ28/fz555/1VqoAWLlyJYGBgRaKSG/x4sX4+fkhSRLV1dUcOnSIDz/8kJqaGp544olWP95VV111xRUD6hZ1f/7559m2bZthCbLVq1cze/ZsvvvuO5MuLC10Lo3lmCAIQh2rK+Ds7e0ZMGCApcMQGtER3pdevXoRGhpq+PeIESNIT09nxYoVRhVw3t7eTR5XRkYG69atY+HChUybNg2AYcOGcejQIZYvXy4KOEEQBMEsrOoSakvNmDGDBQsW8OmnnzJixAgGDRrEv//9bzIzM+vtd/z4cR544AGGDh3KwIEDefjhh0lKSmr186lUKt544w1GjhxJbGwszz33HCqVqsF+CQkJTJ8+nf79+zNkyBCeffZZw2W4OocPH2batGkMGDCAsWPH8u2333LfffcZmsfrmpa//vprrr/+evr378+qVasA2Lx5M3fffTexsbH06dOH66+/nmXLltV7/KysLB577DEGDRrEyJEj+frrrxvEWVNTwzvvvMO1115Lnz59GDhwIPfffz+nT58G9JeH61pBL71sevkl1Ly8PJ577jmuuuoq+vXrx2233caWLVvqPVd0dDTLli3j+eefZ8iQIcTGxvL4449TUFDQqvegKe7u7g3WPT1w4AAPPPAAcXFx9OnTh/Hjx/PRRx+h0+la9dj+/v788ssvTJw40bBNLpejVCob/QwITfvll1/o2bMnH3/8cYvvU1JSwsKFCxkxYgR9+/bljjvuYO/evYbbv/vuuwaXY/bt21fveRYsWMCMGTP45ZdfGDduHLGxsdx7772cOXPGqOPIz89n7ty5xMbGMmTIEF588UXDAtl1fv75Z2666Sb69OnD2LFj+eijjwyX+q+UYzqdjs8//5xrrrmGPn36cN111/H999/Xe9wZM2bw9NNPM3fuXAYMGMD9998PQHl5OW+88QYTJkygb9++3Hzzzfzyyy9GHZ/QsXWWPNJqtSxbtoxbbrmFfv36MXbsWN5+++16370LFizggQceYOXKlUyYMIF+/frxr3/9iwsXLrBt2zZuueUW+vfvz+233244x9XZvXs3d999N4MGDWLo0KE89dRTHWtNWsmKPPvss9K4ceMktVrd6J9Op5MkSZKmT58uDR48WLrmmmuk9evXS+vWrZPGjh0rjRs3TqqqqpIkSZL27t0rxcTESDNnzpQ2b94srV+/Xpo4caI0cOBA6dy5c62Ka86cOdKAAQOkb7/9Vtq+fbv0yCOPSDExMVJUVJRhn/3790sxMTHSAw88IG3dulVavXq1NHbsWOmmm26SqqurJUmSpHPnzkn9+vWT7r77bmnLli3SL7/8Io0YMULq06eP9Oyzz0qSJEnp6elSVFSUFBsbK/3yyy/Sn3/+KWVnZ0vbtm2ToqKipNdee03as2ePtHXrVmnWrFlSVFSUdOTIEUmSJKmyslIaN26c4XX5448/pBtuuEGKiYmRpk+fXu94hg8fLv38889SfHy89NNPP0kjR46UbrjhBkmn00nZ2dnSf/7zHykqKko6fPiwlJ2dLUmSJEVFRUkffvihJEmSlJ+fL40ePVqaMGGCtHr1amn79u3S3LlzpejoaGnNmjWG54qKipIGDRokLViwQNq5c6e0fPlyqW/fvtKTTz7Zqvdg1apVUlRUlJSammr4PJSXl0t///23FBcXJ3300UeGfU+fPi317t1bmjdvnrRz505px44d0jPPPCNFRUVJv//+u2G/cePGSfPnz2/0s9YYrVYrZWVlSa+99prUs2dPaceO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pLy8vB0itH4teY0XLFhAdHR0o3+LFy9up0iF1igsLOSZZ55h2LBhxMbG8tBDD3H+/PkW3Xft2rVER0eTkZFh5ijbprXHqFareeeddxg9ejQDBgxg+vTpnD59uh0jFszpwoULxMbG8uuvvza7r06nY9asWXz00UftEJnxWnJMSUlJPPTQQwwdOpThw4czd+5csrKy2jHK1mnJMZ08eZJ7772X2NhYhg0b1i7ndFHAmYhOp+Pdd99l5cqVze6rVqt58MEHUSqVrFy5kvfff5/4+HheeOGFdojUerXmNX7++efZtWtXvb+bbroJPz8/br/99naIVmitRx99lNTUVD7//HN++eUXHB0due+++6iurm7yfpmZmbzyyivtFGXbtPYYX3rpJX799Vdef/11Vq1ahbe3Nw8++KD4sWcD1Go1Tz/9NFVVVc3uW1tby3/+8x927tzZDpEZryXHVFxczP3334+joyPff/89X3zxBUVFRcyaNQuVStWO0bZMS46poKCA+++/n5CQEH799Vc++eQTDh482KLGnLYQBZwJnD9/nrvvvpuff/6Z4ODgZvc/d+4cKSkpzJkzh4iICAYPHsy0adM6fHJaUmtfYzc3N/z8/Ax/x48fZ8OGDbzzzjsEBAS0Q8RCa5SWlhISEsJrr71Gv379iIiI4N///jd5eXkkJSVd8X46nY5nnnmGmJiYdozWOK09xvT0dFatWsWiRYsYPXo0ERERvPbaa9jb23PixAkLHIFgSh999BGurq7N7nfo0CGmTJlCQkIC7u7u7RCZ8VpyTJs3b6aqqor//e9/REVF0adPH9566y3Onz/PoUOH2inSlmvJMWVmZjJq1CheeeUVunXrxsCBA7njjjvYvXu3WWMTBZwJ7Nu3j4iICH7//XdCQ0Ob3d/Lywu5XM5PP/1EbW0tRUVF/Pnnn/Tv378dorVOrX2NL6VSqVi0aBFTp05l6NChZopQaAsPDw/eeecdoqKiACgqKuKbb74hMDCQyMjIK97v008/Ra1WM3v27PYK1WitPcbdu3fj5ubGmDFjDNvc3d3ZunUrw4cPb7e4BdM7cOAAK1eu5M0332x237///pvRo0fz22+/4ebm1g7RGaelxzR8+HA++f/27jusqfPtA/g3k703ooBgQBAEZYgbd1vrrLVWW221dbX+XNVaq63W7qp1jzr6Olq14m5r3VtR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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# plot profiles\n", - "visualize.sampling_1d_marginals(result2);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This concludes our brief rundown of a typical pyPESTO workflow and manual alternatives. In addition to what was shown here, pyPESTO provides a lot more functionality, including but not limited to visualization routines, diagnostics, model selection and hierarchical optimization. For further information, see the other example notebooks and the API documentation." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/pypesto/C.py b/pypesto/C.py index 25f1468cb..8cae31cea 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -10,38 +10,38 @@ ############################################################################### # ENSEMBLE -PREDICTOR = 'predictor' -PREDICTION_ID = 'prediction_id' -PREDICTION_RESULTS = 'prediction_results' -PREDICTION_ARRAYS = 'prediction_arrays' -PREDICTION_SUMMARY = 'prediction_summary' - -HISTORY = 'history' -OPTIMIZE = 'optimize' -SAMPLE = 'sample' - -MEAN = 'mean' -MEDIAN = 'median' -STANDARD_DEVIATION = 'std' -PERCENTILE = 'percentile' -SUMMARY = 'summary' -WEIGHTED_SIGMA = 'weighted_sigma' - -X_NAMES = 'x_names' -NX = 'n_x' -X_VECTOR = 'x_vectors' -NVECTORS = 'n_vectors' -VECTOR_TAGS = 'vector_tags' -ENSEMBLE_TYPE = 'ensemble_type' -PREDICTIONS = 'predictions' - -SIMULTANEOUS = 'simultaneous' -POINTWISE = 'pointwise' - -LOWER_BOUND = 'lower_bound' -UPPER_BOUND = 'upper_bound' -PREEQUILIBRATION_CONDITION_ID = 'preequilibrationConditionId' -SIMULATION_CONDITION_ID = 'simulationConditionId' +PREDICTOR = "predictor" +PREDICTION_ID = "prediction_id" +PREDICTION_RESULTS = "prediction_results" +PREDICTION_ARRAYS = "prediction_arrays" +PREDICTION_SUMMARY = "prediction_summary" + +HISTORY = "history" +OPTIMIZE = "optimize" +SAMPLE = "sample" + +MEAN = "mean" +MEDIAN = "median" +STANDARD_DEVIATION = "std" +PERCENTILE = "percentile" +SUMMARY = "summary" +WEIGHTED_SIGMA = "weighted_sigma" + +X_NAMES = "x_names" +NX = "n_x" +X_VECTOR = "x_vectors" +NVECTORS = "n_vectors" +VECTOR_TAGS = "vector_tags" +ENSEMBLE_TYPE = "ensemble_type" +PREDICTIONS = "predictions" + +SIMULTANEOUS = "simultaneous" +POINTWISE = "pointwise" + +LOWER_BOUND = "lower_bound" +UPPER_BOUND = "upper_bound" +PREEQUILIBRATION_CONDITION_ID = "preequilibrationConditionId" +SIMULATION_CONDITION_ID = "simulationConditionId" COLOR_HIT_BOTH_BOUNDS = [0.6, 0.0, 0.0, 0.9] COLOR_HIT_ONE_BOUND = [0.95, 0.6, 0.0, 0.9] @@ -59,48 +59,48 @@ class EnsembleType(Enum): ############################################################################### # OBJECTIVE -MODE_FUN = 'mode_fun' # mode for function values -MODE_RES = 'mode_res' # mode for residuals -ModeType = Literal['mode_fun', 'mode_res'] # type for `mode` argument -FVAL = 'fval' # function value -FVAL0 = 'fval0' # function value at start -GRAD = 'grad' # gradient -HESS = 'hess' # Hessian -HESSP = 'hessp' # Hessian vector product -RES = 'res' # residual -SRES = 'sres' # residual sensitivities -RDATAS = 'rdatas' # returned simulated data sets - -TIME = 'time' # time -N_FVAL = 'n_fval' # number of function evaluations -N_GRAD = 'n_grad' # number of gradient evaluations -N_HESS = 'n_hess' # number of Hessian evaluations -N_RES = 'n_res' # number of residual evaluations -N_SRES = 'n_sres' # number of residual sensitivity evaluations -START_TIME = 'start_time' # start time -X = 'x' -X0 = 'x0' -ID = 'id' +MODE_FUN = "mode_fun" # mode for function values +MODE_RES = "mode_res" # mode for residuals +ModeType = Literal["mode_fun", "mode_res"] # type for `mode` argument +FVAL = "fval" # function value +FVAL0 = "fval0" # function value at start +GRAD = "grad" # gradient +HESS = "hess" # Hessian +HESSP = "hessp" # Hessian vector product +RES = "res" # residual +SRES = "sres" # residual sensitivities +RDATAS = "rdatas" # returned simulated data sets + +TIME = "time" # time +N_FVAL = "n_fval" # number of function evaluations +N_GRAD = "n_grad" # number of gradient evaluations +N_HESS = "n_hess" # number of Hessian evaluations +N_RES = "n_res" # number of residual evaluations +N_SRES = "n_sres" # number of residual sensitivity evaluations +START_TIME = "start_time" # start time +X = "x" +X0 = "x0" +ID = "id" ############################################################################### # HIERARCHICAL SCALING + OFFSET -INNER_PARAMETERS = 'inner_parameters' -INNER_RDATAS = 'inner_rdatas' -PARAMETER_TYPE = 'parameterType' -X_INNER_OPT = 'x_inner_opt' -RELATIVE = 'relative' +INNER_PARAMETERS = "inner_parameters" +INNER_RDATAS = "inner_rdatas" +PARAMETER_TYPE = "parameterType" +X_INNER_OPT = "x_inner_opt" +RELATIVE = "relative" class InnerParameterType(str, Enum): """Specifies different inner parameter types.""" - OFFSET = 'offset' - SCALING = 'scaling' - SIGMA = 'sigma' - ORDINAL = 'ordinal' - SPLINE = 'spline' + OFFSET = "offset" + SCALING = "scaling" + SIGMA = "sigma" + ORDINAL = "ordinal" + SPLINE = "spline" DUMMY_INNER_VALUE = { @@ -113,24 +113,24 @@ class InnerParameterType(str, Enum): INNER_PARAMETER_BOUNDS = { InnerParameterType.OFFSET: { - LOWER_BOUND: -float('inf'), - UPPER_BOUND: float('inf'), + LOWER_BOUND: -float("inf"), + UPPER_BOUND: float("inf"), }, InnerParameterType.SCALING: { - LOWER_BOUND: -float('inf'), - UPPER_BOUND: float('inf'), + LOWER_BOUND: -float("inf"), + UPPER_BOUND: float("inf"), }, InnerParameterType.SIGMA: { LOWER_BOUND: 0, - UPPER_BOUND: float('inf'), + UPPER_BOUND: float("inf"), }, InnerParameterType.ORDINAL: { - LOWER_BOUND: -float('inf'), - UPPER_BOUND: float('inf'), + LOWER_BOUND: -float("inf"), + UPPER_BOUND: float("inf"), }, InnerParameterType.SPLINE: { - LOWER_BOUND: -float('inf'), - UPPER_BOUND: float('inf'), + LOWER_BOUND: -float("inf"), + UPPER_BOUND: float("inf"), }, } @@ -138,26 +138,26 @@ class InnerParameterType(str, Enum): # OPTIMAL SCALING # Should go to PEtab constants at some point -MEASUREMENT_CATEGORY = 'measurementCategory' -MEASUREMENT_TYPE = 'measurementType' -CENSORING_BOUNDS = 'censoringBounds' - -ORDINAL = 'ordinal' -CENSORED = 'censored' -LEFT_CENSORED = 'left-censored' -RIGHT_CENSORED = 'right-censored' -INTERVAL_CENSORED = 'interval-censored' +MEASUREMENT_CATEGORY = "measurementCategory" +MEASUREMENT_TYPE = "measurementType" +CENSORING_BOUNDS = "censoringBounds" + +ORDINAL = "ordinal" +CENSORED = "censored" +LEFT_CENSORED = "left-censored" +RIGHT_CENSORED = "right-censored" +INTERVAL_CENSORED = "interval-censored" CENSORING_TYPES = [LEFT_CENSORED, RIGHT_CENSORED, INTERVAL_CENSORED] -REDUCED = 'reduced' -STANDARD = 'standard' -MAXMIN = 'max-min' -MAX = 'max' +REDUCED = "reduced" +STANDARD = "standard" +MAXMIN = "max-min" +MAX = "max" -METHOD = 'method' -REPARAMETERIZED = 'reparameterized' -INTERVAL_CONSTRAINTS = 'interval_constraints' -MIN_GAP = 'min_gap' +METHOD = "method" +REPARAMETERIZED = "reparameterized" +INTERVAL_CONSTRAINTS = "interval_constraints" +MIN_GAP = "min_gap" ORDINAL_OPTIONS = [ METHOD, REPARAMETERIZED, @@ -165,38 +165,38 @@ class InnerParameterType(str, Enum): MIN_GAP, ] -CAT_LB = 'cat_lb' -CAT_UB = 'cat_ub' +CAT_LB = "cat_lb" +CAT_UB = "cat_ub" -NUM_CATEGORIES = 'num_categories' -NUM_DATAPOINTS = 'num_datapoints' -SURROGATE_DATA = 'surrogate_data' -NUM_INNER_PARAMS = 'num_inner_params' -LB_INDICES = 'lb_indices' -UB_INDICES = 'ub_indices' +NUM_CATEGORIES = "num_categories" +NUM_DATAPOINTS = "num_datapoints" +SURROGATE_DATA = "surrogate_data" +NUM_INNER_PARAMS = "num_inner_params" +LB_INDICES = "lb_indices" +UB_INDICES = "ub_indices" -QUANTITATIVE_IXS = 'quantitative_ixs' -QUANTITATIVE_DATA = 'quantitative_data' -NUM_CONSTR_FULL = 'num_constr_full' -C_MATRIX = 'C_matrix' -W_MATRIX = 'W_matrix' -W_DOT_MATRIX = 'W_dot_matrix' +QUANTITATIVE_IXS = "quantitative_ixs" +QUANTITATIVE_DATA = "quantitative_data" +NUM_CONSTR_FULL = "num_constr_full" +C_MATRIX = "C_matrix" +W_MATRIX = "W_matrix" +W_DOT_MATRIX = "W_dot_matrix" -SCIPY_SUCCESS = 'success' -SCIPY_FUN = 'fun' -SCIPY_X = 'x' +SCIPY_SUCCESS = "success" +SCIPY_FUN = "fun" +SCIPY_X = "x" ############################################################################### # SPLINE APPROXIMATION FOR SEMIQUANTITATIVE DATA -MEASUREMENT_TYPE = 'measurementType' +MEASUREMENT_TYPE = "measurementType" -SEMIQUANTITATIVE = 'semiquantitative' +SEMIQUANTITATIVE = "semiquantitative" -SPLINE_RATIO = 'spline_ratio' -MIN_DIFF_FACTOR = 'min_diff_factor' -REGULARIZE_SPLINE = 'regularize_spline' -REGULARIZATION_FACTOR = 'regularization_factor' +SPLINE_RATIO = "spline_ratio" +MIN_DIFF_FACTOR = "min_diff_factor" +REGULARIZE_SPLINE = "regularize_spline" +REGULARIZATION_FACTOR = "regularization_factor" SPLINE_APPROXIMATION_OPTIONS = [ SPLINE_RATIO, MIN_DIFF_FACTOR, @@ -206,15 +206,15 @@ class InnerParameterType(str, Enum): MIN_SIM_RANGE = 1e-16 -SPLINE_PAR_TYPE = 'spline' -N_SPLINE_PARS = 'n_spline_pars' -DATAPOINTS = 'datapoints' -MIN_DATAPOINT = 'min_datapoint' -MAX_DATAPOINT = 'max_datapoint' -EXPDATA_MASK = 'expdata_mask' -CURRENT_SIMULATION = 'current_simulation' -INNER_NOISE_PARS = 'inner_noise_pars' -OPTIMIZE_NOISE = 'optimize_noise' +SPLINE_PAR_TYPE = "spline" +N_SPLINE_PARS = "n_spline_pars" +DATAPOINTS = "datapoints" +MIN_DATAPOINT = "min_datapoint" +MAX_DATAPOINT = "max_datapoint" +EXPDATA_MASK = "expdata_mask" +CURRENT_SIMULATION = "current_simulation" +INNER_NOISE_PARS = "inner_noise_pars" +OPTIMIZE_NOISE = "optimize_noise" ############################################################################### @@ -232,62 +232,62 @@ class InnerParameterType(str, Enum): SUFFIXES_HDF5 = ["hdf5", "h5"] SUFFIXES = SUFFIXES_CSV + SUFFIXES_HDF5 -CPU_TIME_TOTAL = 'cpu_time_total' -PREEQ_CPU_TIME = 'preeq_cpu_time' -PREEQ_CPU_TIME_BACKWARD = 'preeq_cpu_timeB' -POSTEQ_CPU_TIME = 'posteq_cpu_time' -POSTEQ_CPU_TIME_BACKWARD = 'posteq_cpu_timeB' +CPU_TIME_TOTAL = "cpu_time_total" +PREEQ_CPU_TIME = "preeq_cpu_time" +PREEQ_CPU_TIME_BACKWARD = "preeq_cpu_timeB" +POSTEQ_CPU_TIME = "posteq_cpu_time" +POSTEQ_CPU_TIME_BACKWARD = "posteq_cpu_timeB" ############################################################################### # PRIOR -LIN = 'lin' # linear -LOG = 'log' # logarithmic to basis e -LOG10 = 'log10' # logarithmic to basis 10 +LIN = "lin" # linear +LOG = "log" # logarithmic to basis e +LOG10 = "log10" # logarithmic to basis 10 -UNIFORM = 'uniform' -PARAMETER_SCALE_UNIFORM = 'parameterScaleUniform' -NORMAL = 'normal' -PARAMETER_SCALE_NORMAL = 'parameterScaleNormal' -LAPLACE = 'laplace' -PARAMETER_SCALE_LAPLACE = 'parameterScaleLaplace' -LOG_UNIFORM = 'logUniform' -LOG_NORMAL = 'logNormal' -LOG_LAPLACE = 'logLaplace' +UNIFORM = "uniform" +PARAMETER_SCALE_UNIFORM = "parameterScaleUniform" +NORMAL = "normal" +PARAMETER_SCALE_NORMAL = "parameterScaleNormal" +LAPLACE = "laplace" +PARAMETER_SCALE_LAPLACE = "parameterScaleLaplace" +LOG_UNIFORM = "logUniform" +LOG_NORMAL = "logNormal" +LOG_LAPLACE = "logLaplace" ############################################################################### # PREDICT -OUTPUT_IDS = 'output_ids' # data member in PredictionConditionResult -PARAMETER_IDS = 'x_names' # data member in PredictionConditionResult -TIMEPOINTS = 'timepoints' # data member in PredictionConditionResult -OUTPUT = 'output' # field in the return dict of AmiciPredictor -OUTPUT_SENSI = 'output_sensi' # field in the return dict of AmiciPredictor -OUTPUT_WEIGHT = 'output_weight' # field in the return dict of AmiciPredictor -OUTPUT_SIGMAY = 'output_sigmay' # field in the return dict of AmiciPredictor +OUTPUT_IDS = "output_ids" # data member in PredictionConditionResult +PARAMETER_IDS = "x_names" # data member in PredictionConditionResult +TIMEPOINTS = "timepoints" # data member in PredictionConditionResult +OUTPUT = "output" # field in the return dict of AmiciPredictor +OUTPUT_SENSI = "output_sensi" # field in the return dict of AmiciPredictor +OUTPUT_WEIGHT = "output_weight" # field in the return dict of AmiciPredictor +OUTPUT_SIGMAY = "output_sigmay" # field in the return dict of AmiciPredictor # separator in the conditions_ids between preequilibration and simulation # condition -CONDITION_SEP = '::' +CONDITION_SEP = "::" -AMICI_T = 't' # return field in amici simulation result -AMICI_X = 'x' # return field in amici simulation result -AMICI_SX = 'sx' # return field in amici simulation result -AMICI_Y = 'y' # return field in amici simulation result -AMICI_SY = 'sy' # return field in amici simulation result -AMICI_LLH = 'llh' # return field in amici simulation result -AMICI_STATUS = 'status' # return field in amici simulation result -AMICI_SIGMAY = 'sigmay' # return field in amici simulation result -AMICI_SSIGMAY = 'ssigmay' # return field in amici simulation result -AMICI_SSIGMAZ = 'ssigmaz' # return field in amici simulation result +AMICI_T = "t" # return field in amici simulation result +AMICI_X = "x" # return field in amici simulation result +AMICI_SX = "sx" # return field in amici simulation result +AMICI_Y = "y" # return field in amici simulation result +AMICI_SY = "sy" # return field in amici simulation result +AMICI_LLH = "llh" # return field in amici simulation result +AMICI_STATUS = "status" # return field in amici simulation result +AMICI_SIGMAY = "sigmay" # return field in amici simulation result +AMICI_SSIGMAY = "ssigmay" # return field in amici simulation result +AMICI_SSIGMAZ = "ssigmaz" # return field in amici simulation result -CONDITION = 'condition' -CONDITION_IDS = 'condition_ids' +CONDITION = "condition" +CONDITION_IDS = "condition_ids" -CSV = 'csv' # return file format -H5 = 'h5' # return file format +CSV = "csv" # return file format +H5 = "h5" # return file format ############################################################################### # VISUALIZE @@ -304,23 +304,23 @@ class InnerParameterType(str, Enum): RGBA_BLACK = (RGBA_MIN, RGBA_MIN, RGBA_MIN, RGBA_MAX) # black as an RGBA color # optimizer history -TRACE_X_TIME = 'time' -TRACE_X_STEPS = 'steps' +TRACE_X_TIME = "time" +TRACE_X_STEPS = "steps" # supported values to plot on x-axis TRACE_X = (TRACE_X_TIME, TRACE_X_STEPS) -TRACE_Y_FVAL = 'fval' -TRACE_Y_GRADNORM = 'gradnorm' +TRACE_Y_FVAL = "fval" +TRACE_Y_GRADNORM = "gradnorm" # supported values to plot on y-axis TRACE_Y = (TRACE_Y_FVAL, TRACE_Y_GRADNORM) # parameter indices -FREE_ONLY = 'free_only' # only estimated parameters -ALL = 'all' # all parameters, also for start indices +FREE_ONLY = "free_only" # only estimated parameters +ALL = "all" # all parameters, also for start indices # start indices -ALL_CLUSTERED = 'all_clustered' # best + all that are in a cluster of size > 1 -FIRST_CLUSTER = 'first_cluster' # all starts that belong to the first cluster +ALL_CLUSTERED = "all_clustered" # best + all that are in a cluster of size > 1 +FIRST_CLUSTER = "first_cluster" # all starts that belong to the first cluster # waterfall max value WATERFALL_MAX_VALUE = 1e100 diff --git a/pypesto/ensemble/covariance_analysis.py b/pypesto/ensemble/covariance_analysis.py index d9b2dfe70..0ea68c3d2 100644 --- a/pypesto/ensemble/covariance_analysis.py +++ b/pypesto/ensemble/covariance_analysis.py @@ -266,8 +266,8 @@ def get_spectral_decomposition_lowlevel( above_cutoff = rel_eigenvalues > cutoff_relative_separable else: raise Exception( - 'Need a lower cutoff (absolute or relative, ' - 'e.g., 1e-16, to compute separable directions.' + "Need a lower cutoff (absolute or relative, " + "e.g., 1e-16, to compute separable directions." ) # restrict to those above cutoff @@ -297,8 +297,8 @@ def get_spectral_decomposition_lowlevel( below_cutoff = 1 / rel_eigenvalues > cutoff_relative_identifiable else: raise Exception( - 'Need an inverse upper cutoff (absolute or relative, ' - 'e.g., 1e-16, to compute identifiable directions.' + "Need an inverse upper cutoff (absolute or relative, " + "e.g., 1e-16, to compute identifiable directions." ) # restrict to those below cutoff diff --git a/pypesto/ensemble/ensemble.py b/pypesto/ensemble/ensemble.py index b190a7b56..2b3f85c2e 100644 --- a/pypesto/ensemble/ensemble.py +++ b/pypesto/ensemble/ensemble.py @@ -125,10 +125,13 @@ def __iter__(self): yield PREDICTION_ID, self.prediction_id yield PREDICTION_RESULTS, self.prediction_results yield PREDICTION_ARRAYS, self.prediction_arrays - yield PREDICTION_SUMMARY, { - i_key: dict(self.prediction_summary[i_key]) - for i_key in self.prediction_summary.keys() - } + yield ( + PREDICTION_SUMMARY, + { + i_key: dict(self.prediction_summary[i_key]) + for i_key in self.prediction_summary.keys() + }, + ) yield LOWER_BOUND, self.lower_bound yield UPPER_BOUND, self.upper_bound @@ -219,14 +222,14 @@ def compute_summary( # check if prediction results are available if not self.prediction_results: raise ArithmeticError( - 'Cannot compute summary statistics from ' - 'empty prediction results.' + "Cannot compute summary statistics from " + "empty prediction results." ) # if weightings shall be used, check whether weights are there if weighting: if not self.prediction_results[0].conditions[0].output_weight: raise ValueError( - 'There are no weights in the prediction results.' + "There are no weights in the prediction results." ) n_conditions = len(self.prediction_results[0].conditions) @@ -432,7 +435,7 @@ def compute_chi2(self, amici_objective: AmiciObjective) -> float: weighting=True, compute_weighted_sigma=True ) except TypeError: - raise ValueError('Computing a summary failed.') + raise ValueError("Computing a summary failed.") from None n_conditions = len(self.prediction_results[0].conditions) chi_2 = [] for i_cond in range(n_conditions): @@ -452,8 +455,8 @@ def compute_chi2(self, amici_objective: AmiciObjective) -> float: ) if y_meas.shape != mean_traj.shape: raise ValueError( - 'Shape of trajectory and shape ' - 'of measurements does not match.' + "Shape of trajectory and shape " + "of measurements does not match." ) chi_2.append( np.nansum(((y_meas - mean_traj) / weighted_sigmas) ** 2) @@ -540,7 +543,7 @@ def __init__( if x_names is not None: self.x_names = x_names else: - self.x_names = [f'x_{ix}' for ix in range(self.n_x)] + self.x_names = [f"x_{ix}" for ix in range(self.n_x)] # Do we have predictions for this ensemble? self.predictions = [] @@ -642,8 +645,8 @@ def from_optimization_endpoints( abs_cutoff = result.optimize_result[0].fval + rel_cutoff if percentile is not None: logger.warning( - 'percentile is going to be ignored as ' - 'rel_cutoff is not `None`.' + "percentile is going to be ignored as " + "rel_cutoff is not `None`." ) else: abs_cutoff = calculate_cutoff(result=result, percentile=percentile) @@ -658,32 +661,32 @@ def from_optimization_endpoints( # did not reach maximum size and the next value is still # lower than the cutoff value if ( - start['fval'] <= abs_cutoff + start["fval"] <= abs_cutoff and len(x_vectors) < max_size # 'x' can be None if optimization failed at the startpoint - and start['x'] is not None + and start["x"] is not None ): - x_vectors.append(start['x'][result.problem.x_free_indices]) + x_vectors.append(start["x"][result.problem.x_free_indices]) # the vector tag will be a -1 to indicate it is the last step - vector_tags.append((int(start['id']), -1)) + vector_tags.append((int(start["id"]), -1)) else: break # print a warning if there are no vectors within the ensemble if len(x_vectors) == 0: raise ValueError( - 'The ensemble does not contain any vectors. ' - 'Either the cutoff value was too small\n or the ' - 'result.optimize_result object might be empty.' + "The ensemble does not contain any vectors. " + "Either the cutoff value was too small\n or the " + "result.optimize_result object might be empty." ) elif len(x_vectors) < max_size: logger.info( - f'The ensemble contains {len(x_vectors)} parameter ' - 'vectors, which is less than the maximum size.\nIf ' - 'you want to include more \nvectors, you can consider ' - 'raising the cutoff value or including parameters ' - 'from \nthe history with the `from_history` function.' + f"The ensemble contains {len(x_vectors)} parameter " + "vectors, which is less than the maximum size.\nIf " + "you want to include more \nvectors, you can consider " + "raising the cutoff value or including parameters " + "from \nthe history with the `from_history` function." ) x_vectors = np.stack(x_vectors, axis=1) @@ -742,11 +745,11 @@ def from_optimization_history( abs_cutoff = result.optimize_result[0].fval + rel_cutoff else: abs_cutoff = calculate_cutoff(result=result, percentile=percentile) - if not result.optimize_result.list[0].history.options['trace_record']: + if not result.optimize_result.list[0].history.options["trace_record"]: logger.warning( - 'The optimize result has no trace. The Ensemble ' - 'will automatically be created through ' - 'from_optimization_endpoints().' + "The optimize result has no trace. The Ensemble " + "will automatically be created through " + "from_optimization_endpoints()." ) return Ensemble.from_optimization_endpoints( result=result, @@ -764,7 +767,7 @@ def from_optimization_history( # calculate the number of starts whose final nllh is below cutoff n_starts = sum( - start['fval'] <= abs_cutoff + start["fval"] <= abs_cutoff for start in result.optimize_result.list ) @@ -797,7 +800,7 @@ def from_optimization_history( x_vectors.extend([x_trace[start][ind] for ind in indices]) vector_tags.extend( [ - (int(result.optimize_result.list[start]['id']), ind) + (int(result.optimize_result.list[start]["id"]), ind) for ind in indices ] ) @@ -805,10 +808,10 @@ def from_optimization_history( # raise a `ValueError` if there are no vectors within the ensemble if len(x_vectors) == 0: raise ValueError( - 'The ensemble does not contain any vectors. ' - 'Either the `cutoff` value was too \nsmall ' - 'or the `result.optimize_result` object might ' - 'be empty.' + "The ensemble does not contain any vectors. " + "Either the `cutoff` value was too \nsmall " + "or the `result.optimize_result` object might " + "be empty." ) x_vectors = np.stack(x_vectors, axis=1) @@ -1034,33 +1037,33 @@ def check_identifiability(self) -> pd.DataFrame: perc_list = [ int(i_key[11:]) for i_key in self.summary.keys() - if i_key[0:4] == 'perc' + if i_key[0:4] == "perc" ] perc_lower = [perc for perc in perc_list if perc < 50] perc_upper = [perc for perc in perc_list if perc > 50] # create dict of identifiability tmp_identifiability = { - 'parameterId': x_name, - 'lowerBound': lb, - 'upperBound': ub, - 'ensemble_mean': mean, - 'ensemble_std': std, - 'ensemble_median': median, - 'within lb: 1 std': lb < mean - std, - 'within ub: 1 std': ub > mean + std, - 'within lb: 2 std': lb < mean - 2 * std, - 'within ub: 2 std': ub > mean + 2 * std, - 'within lb: 3 std': lb < mean - 3 * std, - 'within ub: 3 std': ub > mean + 3 * std, + "parameterId": x_name, + "lowerBound": lb, + "upperBound": ub, + "ensemble_mean": mean, + "ensemble_std": std, + "ensemble_median": median, + "within lb: 1 std": lb < mean - std, + "within ub: 1 std": ub > mean + std, + "within lb: 2 std": lb < mean - 2 * std, + "within ub: 2 std": ub > mean + 2 * std, + "within lb: 3 std": lb < mean - 3 * std, + "within ub: 3 std": ub > mean + 3 * std, } # handle percentiles for perc in perc_lower: - tmp_identifiability[f'within lb: perc {perc}'] = ( + tmp_identifiability[f"within lb: perc {perc}"] = ( lb < self.summary[get_percentile_label(perc)][ix] ) for perc in perc_upper: - tmp_identifiability[f'within ub: perc {perc}'] = ( + tmp_identifiability[f"within ub: perc {perc}"] = ( ub > self.summary[get_percentile_label(perc)][ix] ) @@ -1069,14 +1072,14 @@ def check_identifiability(self) -> pd.DataFrame: # create DataFrame parameter_identifiability = pd.DataFrame(parameter_identifiability) parameter_identifiability.index = parameter_identifiability[ - 'parameterId' + "parameterId" ] return parameter_identifiability def entries_per_start( - fval_traces: list['np.ndarray'], + fval_traces: list["np.ndarray"], cutoff: float, max_size: int, max_per_start: int, @@ -1191,12 +1194,12 @@ def get_percentile_label(percentile: Union[float, int, str]) -> str: if percentile == round(percentile): percentile = round(percentile) if isinstance(percentile, float): - percentile_str = f'{percentile:.2f}' + percentile_str = f"{percentile:.2f}" # Add `...` to the label if the percentile value changed after rounding if float(percentile_str) != percentile: - percentile_str += '...' + percentile_str += "..." percentile = percentile_str - return f'{PERCENTILE} {percentile}' + return f"{PERCENTILE} {percentile}" def calculate_cutoff( diff --git a/pypesto/ensemble/util.py b/pypesto/ensemble/util.py index 3a1352e35..3d3a95d0a 100644 --- a/pypesto/ensemble/util.py +++ b/pypesto/ensemble/util.py @@ -31,7 +31,7 @@ def read_from_csv( path: str, - sep: str = '\t', + sep: str = "\t", index_col: int = 0, headline_parser: Callable = None, ensemble_type: EnsembleType = None, @@ -82,7 +82,7 @@ def read_from_csv( def read_ensemble_from_hdf5( filename: str, - input_type: Literal['optimize', 'sample'] = OPTIMIZE, + input_type: Literal["optimize", "sample"] = OPTIMIZE, remove_burn_in: bool = True, chain_slice: slice = None, cutoff: float = np.inf, @@ -120,10 +120,10 @@ def read_ensemble_from_hdf5( ) else: raise ValueError( - 'The type you provided was neither ' + "The type you provided was neither " f'"{SAMPLE}" nor "{OPTIMIZE}". Those are ' - 'currently the only supported types. ' - 'Please choose one of them.' + "currently the only supported types. " + "Please choose one of them." ) @@ -192,12 +192,12 @@ def write_ensemble_prediction_to_h5( An optional filepath where the file should be saved to. """ # parse base path - base = Path('') + base = Path("") if base_path is not None: base = Path(base_path) # open file - with h5py.File(output_file, 'a') as f: + with h5py.File(output_file, "a") as f: # write prediction ID if available if ensemble_prediction.prediction_id is not None: f.create_dataset( @@ -244,7 +244,7 @@ def write_ensemble_prediction_to_h5( ) in ensemble_prediction.prediction_summary.items(): if summary is None: continue - tmp_base_path = os.path.join(base, f'{SUMMARY}_{summary_id}') + tmp_base_path = os.path.join(base, f"{SUMMARY}_{summary_id}") f.create_group(tmp_base_path) summary.write_to_h5(output_file, base_path=tmp_base_path) @@ -253,7 +253,7 @@ def write_ensemble_prediction_to_h5( ensemble_prediction.prediction_results ): tmp_base_path = os.path.join( - base, f'{PREDICTION_RESULTS}_{i_result}' + base, f"{PREDICTION_RESULTS}_{i_result}" ) result.write_to_h5(output_file, base_path=tmp_base_path) @@ -288,8 +288,8 @@ def get_prediction_dataset( dataset = ens.prediction_arrays[OUTPUT].transpose() else: raise Exception( - 'Need either an Ensemble object with predictions or ' - 'an EnsemblePrediction object as input. Stopping.' + "Need either an Ensemble object with predictions or " + "an EnsemblePrediction object as input. Stopping." ) return dataset @@ -301,7 +301,7 @@ def read_ensemble_prediction_from_h5( ): """Read an ensemble prediction from an HDF5 File.""" # open file - with h5py.File(input_file, 'r') as f: + with h5py.File(input_file, "r") as f: pred_res_list = [] bounds = {} for key in f.keys(): @@ -315,25 +315,25 @@ def read_ensemble_prediction_from_h5( bounds[key] = f[key][:] continue bounds[key] = [ - f[f'{key}/{cond}'][()] for cond in f[key].keys() + f[f"{key}/{cond}"][()] for cond in f[key].keys() ] bounds[key] = np.array(bounds[key]) continue - x_names = list(decode_array(f[f'{key}/{X_NAMES}'][()])) - condition_ids = list(decode_array(f[f'{key}/condition_ids'][()])) + x_names = list(decode_array(f[f"{key}/{X_NAMES}"][()])) + condition_ids = list(decode_array(f[f"{key}/condition_ids"][()])) pred_cond_res_list = [] for id, _ in enumerate(condition_ids): - output = f[f'{key}/{id}/{OUTPUT}'][:] + output = f[f"{key}/{id}/{OUTPUT}"][:] output_ids = tuple( - decode_array(f[f'{key}/{id}' f'/{OUTPUT_IDS}'][:]) + decode_array(f[f"{key}/{id}" f"/{OUTPUT_IDS}"][:]) ) - timepoints = f[f'{key}/{id}/{TIMEPOINTS}'][:] + timepoints = f[f"{key}/{id}/{TIMEPOINTS}"][:] try: - output_weight = f[f'{key}/{id}/{OUTPUT_WEIGHT}'][()] + output_weight = f[f"{key}/{id}/{OUTPUT_WEIGHT}"][()] except KeyError: output_weight = None try: - output_sigmay = f[f'{key}/{id}/{OUTPUT_SIGMAY}'][:] + output_sigmay = f[f"{key}/{id}/{OUTPUT_SIGMAY}"][:] except KeyError: output_sigmay = None pred_cond_res_list.append( diff --git a/pypesto/hierarchical/base_problem.py b/pypesto/hierarchical/base_problem.py index be6c916a7..3a9fc8238 100644 --- a/pypesto/hierarchical/base_problem.py +++ b/pypesto/hierarchical/base_problem.py @@ -54,16 +54,16 @@ def __init__(self, xs: list[InnerParameter], data: list[np.ndarray]): if self.is_empty(): raise ValueError( - 'There are no parameters in the inner problem of hierarchical ' - 'optimization.' + "There are no parameters in the inner problem of hierarchical " + "optimization." ) @staticmethod def from_petab_amici( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], - ) -> 'InnerProblem': + petab_problem: "petab.Problem", + amici_model: "amici.Model", + edatas: list["amici.ExpData"], + ) -> "InnerProblem": """Create an InnerProblem from a PEtab problem and AMICI objects.""" def get_x_ids(self) -> list[str]: @@ -118,7 +118,7 @@ def get_for_id(self, inner_parameter_id: str) -> InnerParameter: try: return self.xs[inner_parameter_id] except KeyError: - raise KeyError(f"Cannot find parameter with id {id}.") + raise KeyError(f"Cannot find parameter with id {id}.") from None def is_empty(self) -> bool: """Check for emptiness. @@ -190,7 +190,7 @@ def check_edatas(self, edatas: list[amici.ExpData]) -> bool: # TODO replace but edata1==edata2 once this makes it into amici # https://github.com/AMICI-dev/AMICI/issues/1880 data = [ - amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas + amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas ] if len(self.data) != len(data): @@ -218,11 +218,11 @@ def scale_value( val: Union[float, np.array], scale: str ) -> Union[float, np.array]: """Scale a single value.""" - if scale == 'lin': + if scale == "lin": return val - if scale == 'log': + if scale == "log": return np.log(val) - if scale == 'log10': + if scale == "log10": return np.log10(val) raise ValueError(f"Scale {scale} not recognized.") diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 72d21af2d..02d54fa01 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -59,8 +59,8 @@ SemiquantProblem, ) -AmiciModel = Union['amici.Model', 'amici.ModelPtr'] -AmiciSolver = Union['amici.Solver', 'amici.SolverPtr'] +AmiciModel = Union["amici.Model", "amici.ModelPtr"] +AmiciSolver = Union["amici.Solver", "amici.SolverPtr"] class InnerCalculatorCollector(AmiciCalculator): @@ -88,9 +88,9 @@ class InnerCalculatorCollector(AmiciCalculator): def __init__( self, data_types: set[str], - petab_problem: 'petab.Problem', + petab_problem: "petab.Problem", model: AmiciModel, - edatas: list['amici.ExpData'], + edatas: list["amici.ExpData"], inner_options: dict, ): super().__init__() @@ -99,9 +99,7 @@ def __init__( self.data_types = data_types self.inner_calculators: list[ AmiciCalculator - ] = ( - [] - ) # TODO make into a dictionary (future PR, together with .hierarchical of Problem) + ] = [] # TODO make into a dictionary (future PR, together with .hierarchical of Problem) self.construct_inner_calculators( petab_problem, model, edatas, inner_options ) @@ -118,9 +116,9 @@ def initialize(self): def construct_inner_calculators( self, - petab_problem: 'petab.Problem', + petab_problem: "petab.Problem", model: AmiciModel, - edatas: list['amici.ExpData'], + edatas: list["amici.ExpData"], inner_options: dict, ): """Construct inner calculators for each data type.""" @@ -213,11 +211,11 @@ def validate_options(self, inner_options: dict): def _get_quantitative_data_mask( self, - edatas: list['amici.ExpData'], + edatas: list["amici.ExpData"], ) -> list[np.ndarray]: # transform experimental data edatas = [ - amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas + amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas ] quantitative_data_mask = [ @@ -449,10 +447,10 @@ def __call__( for r in rdatas ): raise RuntimeError( - 'Cannot use least squares solver with' - 'parameter dependent sigma! Support can be ' - 'enabled via ' - 'amici_model.setAddSigmaResiduals().' + "Cannot use least squares solver with" + "parameter dependent sigma! Support can be " + "enabled via " + "amici_model.setAddSigmaResiduals()." ) self._known_least_squares_safe = True # don't check this again @@ -537,7 +535,7 @@ def calculate_quantitative_result( # transform experimental data edatas = [ - amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas + amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas ] # calculate the function value @@ -547,8 +545,7 @@ def calculate_quantitative_result( sigma_i = rdata[AMICI_SIGMAY][mask] nllh += 0.5 * np.nansum( - np.log(2 * np.pi * sigma_i**2) - + (data_i - sim_i) ** 2 / sigma_i**2 + np.log(2 * np.pi * sigma_i**2) + (data_i - sim_i) ** 2 / sigma_i**2 ) # calculate the gradient if requested @@ -601,9 +598,7 @@ def calculate_quantitative_result( ), axis=1, ) + np.nansum( - np.multiply( - sensitivities_i, ((sim_i - data_i) / sigma_i**2) - ), + np.multiply(sensitivities_i, ((sim_i - data_i) / sigma_i**2)), axis=1, ) add_sim_grad_to_opt_grad( diff --git a/pypesto/hierarchical/ordinal/calculator.py b/pypesto/hierarchical/ordinal/calculator.py index 757f04559..6b13baacb 100644 --- a/pypesto/hierarchical/ordinal/calculator.py +++ b/pypesto/hierarchical/ordinal/calculator.py @@ -68,7 +68,7 @@ def __init__( self.inner_solver = inner_solver if ( self.inner_problem.method - is not self.inner_solver.options['method'] + is not self.inner_solver.options["method"] ): raise ValueError( f"The inner problem method {self.inner_problem.method} and the inner solver method {self.inner_solver.options['method']} have to coincide." @@ -86,12 +86,12 @@ def __call__( mode: str, amici_model: AmiciModel, amici_solver: AmiciSolver, - edatas: list['amici.ExpData'], + edatas: list["amici.ExpData"], n_threads: int, x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list['amici.ReturnData'] = None, + rdatas: list["amici.ReturnData"] = None, ): """Perform the actual AMICI call. diff --git a/pypesto/hierarchical/ordinal/problem.py b/pypesto/hierarchical/ordinal/problem.py index 11c58512a..8c7e0b73f 100644 --- a/pypesto/hierarchical/ordinal/problem.py +++ b/pypesto/hierarchical/ordinal/problem.py @@ -153,8 +153,8 @@ def _initialize_groups(self) -> None: self.groups[group][W_DOT_MATRIX] = self.initialize_w(group) else: raise ValueError( - 'Censoring types of optimal scaling parameters of a group ' - 'have to either be all None, or all not None.' + "Censoring types of optimal scaling parameters of a group " + "have to either be all None, or all not None." ) def initialize(self) -> None: @@ -172,10 +172,10 @@ def initialize(self) -> None: @staticmethod def from_petab_amici( petab_problem: petab.Problem, - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], + amici_model: "amici.Model", + edatas: list["amici.ExpData"], method: str = None, - ) -> 'OrdinalProblem': + ) -> "OrdinalProblem": """Construct the inner problem from the `petab_problem`.""" if not method: method = REDUCED @@ -388,10 +388,10 @@ def get_d( d = np.zeros(self.groups[group][NUM_CONSTR_FULL]) d[ - 2 * self.groups[group][NUM_DATAPOINTS] - + 1 : 2 * self.groups[group][NUM_DATAPOINTS] + 2 * self.groups[group][NUM_DATAPOINTS] + 1 : 2 + * self.groups[group][NUM_DATAPOINTS] + self.groups[group][NUM_CATEGORIES] - ] = (interval_gap + eps) + ] = interval_gap + eps d[ 2 * self.groups[group][NUM_DATAPOINTS] @@ -415,8 +415,8 @@ def get_dd_dtheta( dinterval_gap_dtheta = max_sy / (4 * (len(xs) - 1) + 1) dd_dtheta[ - 2 * self.groups[group][NUM_DATAPOINTS] - + 1 : 2 * self.groups[group][NUM_DATAPOINTS] + 2 * self.groups[group][NUM_DATAPOINTS] + 1 : 2 + * self.groups[group][NUM_DATAPOINTS] + self.groups[group][NUM_CATEGORIES] ] = dinterval_gap_dtheta @@ -475,8 +475,8 @@ def get_inner_parameter_dictionary(self) -> dict: def optimal_scaling_inner_problem_from_petab_problem( petab_problem: petab.Problem, - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], + amici_model: "amici.Model", + edatas: list["amici.ExpData"], method: str, ): """Construct the inner problem from the `petab_problem`.""" @@ -491,7 +491,7 @@ def optimal_scaling_inner_problem_from_petab_problem( ) # transform experimental data - data = [amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas] + data = [amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas] # matrixify ix_matrices = ix_matrices_from_arrays(ixs, data) @@ -511,7 +511,7 @@ def optimal_scaling_inner_problem_from_petab_problem( def optimal_scaling_inner_parameters_from_measurement_df( df: pd.DataFrame, method: str, - amici_model: 'amici.Model', + amici_model: "amici.Model", ) -> list[OrdinalParameter]: """Create list of inner free parameters from PEtab measurement table dependent on the method provided.""" df = df.reset_index() @@ -533,7 +533,7 @@ def optimal_scaling_inner_parameters_from_measurement_df( # Add optimal scaling parameters for ordinal measurements. for par_type, par_estimate in zip(par_types, estimate): for _, row in observable_df.iterrows(): - par_id = f'{par_type}_{observable_id}_{row[MEASUREMENT_TYPE]}_{int(row[MEASUREMENT_CATEGORY])}' + par_id = f"{par_type}_{observable_id}_{row[MEASUREMENT_TYPE]}_{int(row[MEASUREMENT_CATEGORY])}" # Create only one set of bound parameters per category of a group. if par_id not in [ @@ -570,7 +570,7 @@ def optimal_scaling_inner_parameters_from_measurement_df( unique_censoring_bounds.index(row[CENSORING_BOUNDS]) + 1 ) - par_id = f'{par_type}_{observable_id}_{row[MEASUREMENT_TYPE]}_{category}' + par_id = f"{par_type}_{observable_id}_{row[MEASUREMENT_TYPE]}_{category}" # Create only one set of bound parameters per category of a group. if par_id not in [ inner_par.inner_parameter_id @@ -611,8 +611,8 @@ def get_estimate_for_method(method: str) -> tuple[bool, bool]: def optimal_scaling_ixs_for_measurement_specific_parameters( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', + petab_problem: "petab.Problem", + amici_model: "amici.Model", inner_parameters: list[OrdinalParameter], ) -> dict[str, list[tuple[int, int, int]]]: """Create mapping of parameters to measurements. diff --git a/pypesto/hierarchical/ordinal/solver.py b/pypesto/hierarchical/ordinal/solver.py index 9925f1bdd..5c65b1722 100644 --- a/pypesto/hierarchical/ordinal/solver.py +++ b/pypesto/hierarchical/ordinal/solver.py @@ -75,7 +75,7 @@ def validate_options(self): raise ValueError( f"Inner solver method cannot be {self.options[METHOD]}. Please enter either {STANDARD} or {REDUCED}" ) - elif type(self.options[REPARAMETERIZED]) is not bool: + elif not isinstance(self.options[REPARAMETERIZED], bool): raise ValueError( f"Inner solver option 'reparameterized' has to be boolean, not {type(self.options[REPARAMETERIZED])}." ) @@ -83,7 +83,7 @@ def validate_options(self): raise ValueError( f"Inner solver method cannot be {self.options[INTERVAL_CONSTRAINTS]}. Please enter either {MAX} or {MAXMIN}" ) - elif type(self.options[MIN_GAP]) is not float: + elif not isinstance(self.options[MIN_GAP], float): raise ValueError( f"Inner solver option 'reparameterized' has to be a float, not {type(self.options[MIN_GAP])}." ) @@ -91,13 +91,14 @@ def validate_options(self): self.options[METHOD] == STANDARD and self.options[REPARAMETERIZED] ): raise NotImplementedError( - 'Combining standard approach with ' - 'reparameterization not implemented.' + "Combining standard approach with " + "reparameterization not implemented." ) elif self.options[METHOD] == STANDARD: warnings.warn( - 'Standard approach is not recommended, as it is less efficient.' - 'Please consider using the reduced approach instead.' + "Standard approach is not recommended, as it is less efficient." + "Please consider using the reduced approach instead.", + stacklevel=2, ) # Check for any other options for key in self.options: @@ -185,7 +186,7 @@ def calculate_obj_function(x_inner_opt: list): x_inner_opt[idx][SCIPY_SUCCESS] for idx in range(len(x_inner_opt)) ]: obj = np.inf - warnings.warn("Inner optimization failed.") + warnings.warn("Inner optimization failed.", stacklevel=2) else: obj = np.sum( [ @@ -463,7 +464,7 @@ def get_mu(group: int, problem: OrdinalProblem, residual: np.ndarray): mu = linalg.lstsq( problem.groups[group][C_MATRIX].transpose(), -2 * residual.dot(problem.groups[group][W_MATRIX]), - lapack_driver='gelsy', + lapack_driver="gelsy", ) return mu[0] @@ -565,9 +566,10 @@ def grad_surr(x): results = minimize(obj_surr, jac=grad_surr, **inner_options) except ValueError: warnings.warn( - "x0 violate bound constraints. Retrying with array of zeros." + "x0 violate bound constraints. Retrying with array of zeros.", + stacklevel=2, ) - inner_options['x0'] = np.zeros(len(inner_options['x0'])) + inner_options["x0"] = np.zeros(len(inner_options["x0"])) results = minimize(obj_surr, jac=grad_surr, **inner_options) return results @@ -612,7 +614,7 @@ def get_inner_optimization_options( np.asarray([x.value for x in category_lower_bounds]), np.asarray([x.value for x in category_upper_bounds]), ], - 'F', + "F", ) if options[METHOD] == REDUCED: @@ -651,10 +653,10 @@ def get_inner_optimization_options( * parameter_length, ) inner_options = { - 'x0': x0, - 'method': 'L-BFGS-B', - 'options': {'maxiter': 2000, 'ftol': 1e-10}, - 'bounds': bounds, + "x0": x0, + "method": "L-BFGS-B", + "options": {"maxiter": 2000, "ftol": 1e-10}, + "bounds": bounds, } else: constraints = get_constraints_for_optimization( @@ -662,10 +664,10 @@ def get_inner_optimization_options( ) inner_options = { - 'x0': x0, - 'method': 'SLSQP', - 'options': {'maxiter': 2000, 'ftol': 1e-10, 'disp': None}, - 'constraints': constraints, + "x0": x0, + "method": "SLSQP", + "options": {"maxiter": 2000, "ftol": 1e-10, "disp": None}, + "constraints": constraints, } return inner_options @@ -999,7 +1001,7 @@ def get_bounds_for_category( elif x_category > 1: x_lower = optimal_scaling_bounds[x_category - 2] + interval_gap else: - raise ValueError('Category value needs to be larger than 0.') + raise ValueError("Category value needs to be larger than 0.") elif options[METHOD] == STANDARD: x_lower = optimal_scaling_bounds[2 * x_category - 2] x_upper = optimal_scaling_bounds[2 * x_category - 1] @@ -1036,7 +1038,7 @@ def get_constraints_for_optimization( b[0] = 0 b[1::2] = interval_range b[2::2] = interval_gap - ineq_cons = {'type': 'ineq', 'fun': lambda x: a.dot(x) - b} + ineq_cons = {"type": "ineq", "fun": lambda x: a.dot(x) - b} return ineq_cons @@ -1142,7 +1144,7 @@ def calculate_censored_obj( return_dictionary = { SCIPY_SUCCESS: True, SCIPY_FUN: obj, - SCIPY_X: np.ravel([cat_lb_values, cat_ub_values], order='F'), + SCIPY_X: np.ravel([cat_lb_values, cat_ub_values], order="F"), } return return_dictionary diff --git a/pypesto/hierarchical/petab.py b/pypesto/hierarchical/petab.py index 8b5210a90..8dfd598ee 100644 --- a/pypesto/hierarchical/petab.py +++ b/pypesto/hierarchical/petab.py @@ -6,15 +6,16 @@ import petab import sympy as sp from more_itertools import one -from petab.C import ESTIMATE, LIN -from petab.C import LOWER_BOUND as PETAB_LOWER_BOUND from petab.C import ( + ESTIMATE, + LIN, NOISE_PARAMETERS, OBSERVABLE_ID, OBSERVABLE_PARAMETERS, OBSERVABLE_TRANSFORMATION, PARAMETER_SEPARATOR, ) +from petab.C import LOWER_BOUND as PETAB_LOWER_BOUND from petab.C import UPPER_BOUND as PETAB_UPPER_BOUND from petab.observables import get_formula_placeholders @@ -24,9 +25,6 @@ INNER_PARAMETER_BOUNDS, INTERVAL_CENSORED, LEFT_CENSORED, -) -from ..C import LOWER_BOUND as PYPESTO_LOWER_BOUND -from ..C import ( MEASUREMENT_CATEGORY, MEASUREMENT_TYPE, ORDINAL, @@ -34,9 +32,10 @@ RELATIVE, RIGHT_CENSORED, SEMIQUANTITATIVE, + InnerParameterType, ) +from ..C import LOWER_BOUND as PYPESTO_LOWER_BOUND from ..C import UPPER_BOUND as PYPESTO_UPPER_BOUND -from ..C import InnerParameterType def correct_parameter_df_bounds(parameter_df: pd.DataFrame) -> pd.DataFrame: @@ -304,7 +303,7 @@ def _validate_measurement_specific_observable_formula( """ formula, formula_inner_parameters = _get_symbolic_formula_from_measurement( measurement=measurement, - formula_type='observable', + formula_type="observable", petab_problem=petab_problem, inner_parameters=inner_parameters, ) @@ -389,7 +388,7 @@ def _validate_measurement_specific_noise_formula( """ formula, formula_inner_parameters = _get_symbolic_formula_from_measurement( measurement=measurement, - formula_type='noise', + formula_type="noise", petab_problem=petab_problem, inner_parameters=inner_parameters, ) @@ -420,7 +419,7 @@ def _validate_measurement_specific_noise_formula( def _get_symbolic_formula_from_measurement( measurement: pd.Series, - formula_type: Literal['observable', 'noise'], + formula_type: Literal["observable", "noise"], petab_problem: petab.Problem, inner_parameters: dict[str, InnerParameterType], ) -> tuple[sp.Expr, dict[sp.Symbol, InnerParameterType]]: @@ -447,7 +446,7 @@ def _get_symbolic_formula_from_measurement( observable_id = measurement[OBSERVABLE_ID] formula_string = petab_problem.observable_df.loc[ - observable_id, formula_type + 'Formula' + observable_id, formula_type + "Formula" ] symbolic_formula = sp.sympify(formula_string) @@ -457,7 +456,7 @@ def _get_symbolic_formula_from_measurement( override_type=formula_type, ) if formula_placeholders: - overrides = measurement[formula_type + 'Parameters'] + overrides = measurement[formula_type + "Parameters"] overrides = ( overrides.split(PARAMETER_SEPARATOR) if isinstance(overrides, str) @@ -472,10 +471,10 @@ def _get_symbolic_formula_from_measurement( if sp.Symbol(inner_parameter_id) in symbolic_formula.free_symbols } - if formula_type == 'noise': + if formula_type == "noise": max_parameters = 1 expected_inner_parameter_types = [InnerParameterType.SIGMA] - elif formula_type == 'observable': + elif formula_type == "observable": max_parameters = 2 expected_inner_parameter_types = [ InnerParameterType.OFFSET, diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index 6a2a41fe0..1c9529213 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -84,7 +84,7 @@ def __call__( x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list['amici.ReturnData'] = None, + rdatas: list["amici.ReturnData"] = None, ): """Perform the actual AMICI call, with hierarchical optimization. @@ -127,9 +127,9 @@ def __call__( """ if not self.inner_problem.check_edatas(edatas=edatas): raise ValueError( - 'The experimental data provided to this call differs from ' - 'the experimental data used to setup the hierarchical ' - 'optimizer.' + "The experimental data provided to this call differs from " + "the experimental data used to setup the hierarchical " + "optimizer." ) if ( @@ -266,7 +266,7 @@ def calculate_directly( x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list['amici.ReturnData'] = None, + rdatas: list["amici.ReturnData"] = None, ): """Calculate directly via solver calculate methods. diff --git a/pypesto/hierarchical/relative/problem.py b/pypesto/hierarchical/relative/problem.py index 05dc9f32e..4910619aa 100644 --- a/pypesto/hierarchical/relative/problem.py +++ b/pypesto/hierarchical/relative/problem.py @@ -57,10 +57,10 @@ def __init__(self, **kwargs): @staticmethod def from_petab_amici( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], - ) -> 'RelativeInnerProblem': + petab_problem: "petab.Problem", + amici_model: "amici.Model", + edatas: list["amici.ExpData"], + ) -> "RelativeInnerProblem": """Create an InnerProblem from a PEtab problem and AMICI objects.""" return inner_problem_from_petab_problem( petab_problem, amici_model, edatas @@ -85,7 +85,7 @@ def check_edatas(self, edatas: list[amici.ExpData]) -> bool: # TODO replace but edata1==edata2 once this makes it into amici # https://github.com/AMICI-dev/AMICI/issues/1880 data = [ - amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas + amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas ] if len(self.data) != len(data): @@ -99,9 +99,9 @@ def check_edatas(self, edatas: list[amici.ExpData]) -> bool: def inner_problem_from_petab_problem( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], + petab_problem: "petab.Problem", + amici_model: "amici.Model", + edatas: list["amici.ExpData"], ) -> AmiciInnerProblem: """ Create inner problem from PEtab problem. @@ -123,7 +123,7 @@ def inner_problem_from_petab_problem( ) # transform experimental data - data = [amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas] + data = [amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas] # matrixify ix_matrices = ix_matrices_from_arrays(ixs, data) @@ -133,14 +133,14 @@ def inner_problem_from_petab_problem( par.ixs = ix_matrices[par.inner_parameter_id] par_group_types = { - tuple(obs_pars.split(';')): ( + tuple(obs_pars.split(";")): ( petab_problem.parameter_df.loc[obs_par, PARAMETER_TYPE] - for obs_par in obs_pars.split(';') + for obs_par in obs_pars.split(";") ) for (obs_id, obs_pars), _ in petab_problem.measurement_df.groupby( [petab.OBSERVABLE_ID, petab.OBSERVABLE_PARAMETERS], dropna=True ) - if ';' in obs_pars # prefilter for at least 2 observable parameters + if ";" in obs_pars # prefilter for at least 2 observable parameters } coupled_pars = { @@ -232,8 +232,8 @@ def inner_parameters_from_parameter_df( def ixs_for_measurement_specific_parameters( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', + petab_problem: "petab.Problem", + amici_model: "amici.Model", x_ids: list[str], ) -> dict[str, list[tuple[int, int, int]]]: """ diff --git a/pypesto/hierarchical/relative/solver.py b/pypesto/hierarchical/relative/solver.py index 03b8a3a4c..a0a6ea3d7 100644 --- a/pypesto/hierarchical/relative/solver.py +++ b/pypesto/hierarchical/relative/solver.py @@ -226,8 +226,8 @@ def apply_inner_parameters_to_rdatas( inner_parameters: The inner parameters to apply to the rdatas. """ - sim = [rdata['y'] for rdata in rdatas] - sigma = [rdata['sigmay'] for rdata in rdatas] + sim = [rdata["y"] for rdata in rdatas] + sigma = [rdata["sigmay"] for rdata in rdatas] # apply offsets, scalings and sigmas for x in problem.get_xs_for_type(InnerParameterType.SCALING): @@ -422,11 +422,11 @@ def __init__( if self.problem_kwargs is None: self.problem_kwargs = {} - self.minimize_kwargs['n_starts'] = self.minimize_kwargs.get( - 'n_starts', 1 + self.minimize_kwargs["n_starts"] = self.minimize_kwargs.get( + "n_starts", 1 ) - self.minimize_kwargs['progress_bar'] = self.minimize_kwargs.get( - 'progress_bar', False + self.minimize_kwargs["progress_bar"] = self.minimize_kwargs.get( + "progress_bar", False ) self.x_guesses = None @@ -517,7 +517,7 @@ def fun(x): # perform the actual optimization result = minimize(pypesto_problem, **self.minimize_kwargs) - best_par = result.optimize_result.list[0]['x'] + best_par = result.optimize_result.list[0]["x"] # Check if the index of an optimized parameter on the dummy bound # is not in the list of specified bounds. If so, raise an error. @@ -545,7 +545,7 @@ def fun(x): # cache self.x_guesses = np.array( [ - entry['x'] + entry["x"] for entry in result.optimize_result.list[: self.n_cached] ] ) @@ -575,14 +575,14 @@ def sample_startpoints( ------- The sampled startpoints appended to the cached startpoints. """ - if self.minimize_kwargs['n_starts'] == 1 and self.x_guesses is None: + if self.minimize_kwargs["n_starts"] == 1 and self.x_guesses is None: return np.array( [list(problem.get_dummy_values(scaled=False).values())] ) elif self.x_guesses is not None: - n_samples = self.minimize_kwargs['n_starts'] - len(self.x_guesses) + n_samples = self.minimize_kwargs["n_starts"] - len(self.x_guesses) else: - n_samples = self.minimize_kwargs['n_starts'] - 1 + n_samples = self.minimize_kwargs["n_starts"] - 1 if n_samples <= 0: return self.x_guesses diff --git a/pypesto/hierarchical/relative/util.py b/pypesto/hierarchical/relative/util.py index b58f15523..4465b3b12 100644 --- a/pypesto/hierarchical/relative/util.py +++ b/pypesto/hierarchical/relative/util.py @@ -34,7 +34,7 @@ def get_finite_quotient( """ try: with warnings.catch_warnings(): - warnings.filterwarnings('error') + warnings.filterwarnings("error") quotient = float(numerator / denominator) if not np.isfinite(quotient): raise ValueError @@ -477,10 +477,7 @@ def compute_nllh_gradient_for_condition( return np.nansum( np.multiply( ssigma, - ( - (np.full(data.shape, 1) - (data - sim) ** 2 / sigma**2) - / sigma - ), + ((np.full(data.shape, 1) - (data - sim) ** 2 / sigma**2) / sigma), ), axis=(1, 2), ) + np.nansum( diff --git a/pypesto/hierarchical/semiquantitative/calculator.py b/pypesto/hierarchical/semiquantitative/calculator.py index f2ffcd039..c7349387f 100644 --- a/pypesto/hierarchical/semiquantitative/calculator.py +++ b/pypesto/hierarchical/semiquantitative/calculator.py @@ -78,12 +78,12 @@ def __call__( mode: str, amici_model: AmiciModel, amici_solver: AmiciSolver, - edatas: list['amici.ExpData'], + edatas: list["amici.ExpData"], n_threads: int, x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list['amici.ReturnData'] = None, + rdatas: list["amici.ReturnData"] = None, ): """Perform the actual AMICI call. diff --git a/pypesto/hierarchical/semiquantitative/problem.py b/pypesto/hierarchical/semiquantitative/problem.py index 65bd62bb7..e360989bf 100644 --- a/pypesto/hierarchical/semiquantitative/problem.py +++ b/pypesto/hierarchical/semiquantitative/problem.py @@ -124,10 +124,10 @@ def initialize(self) -> None: @staticmethod def from_petab_amici( petab_problem: petab.Problem, - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], + amici_model: "amici.Model", + edatas: list["amici.ExpData"], spline_ratio: float = None, - ) -> 'SemiquantProblem': + ) -> "SemiquantProblem": """Construct the inner problem from the `petab_problem`.""" if spline_ratio is None: spline_ratio = get_default_options() @@ -235,8 +235,8 @@ def get_default_options() -> dict: def spline_inner_problem_from_petab_problem( petab_problem: petab.Problem, - amici_model: 'amici.Model', - edatas: list['amici.ExpData'], + amici_model: "amici.Model", + edatas: list["amici.ExpData"], spline_ratio: float = None, ): """Construct the inner problem from the `petab_problem`.""" @@ -262,7 +262,7 @@ def spline_inner_problem_from_petab_problem( ) # transform experimental data - data = [amici.numpy.ExpDataView(edata)['observedData'] for edata in edatas] + data = [amici.numpy.ExpDataView(edata)["observedData"] for edata in edatas] # matrixify ix_matrices = ix_matrices_from_arrays(ixs, data) @@ -282,7 +282,7 @@ def spline_inner_problem_from_petab_problem( def spline_inner_parameters_from_measurement_df( df: pd.DataFrame, spline_ratio: float, - amici_model: 'amici.Model', + amici_model: "amici.Model", ) -> list[SplineInnerParameter]: """Create list of inner free spline parameters from PEtab measurement table.""" df = df.reset_index() @@ -307,7 +307,7 @@ def spline_inner_parameters_from_measurement_df( # Create n_spline_parameters number of spline inner parameters. for par_index in range(n_spline_parameters): - par_id = f'{par_type}_{observable_id}_{group}_{par_index+1}' + par_id = f"{par_type}_{observable_id}_{group}_{par_index+1}" inner_parameters.append( SplineInnerParameter( inner_parameter_id=par_id, @@ -328,8 +328,8 @@ def spline_inner_parameters_from_measurement_df( def noise_inner_parameters_from_parameter_df( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', + petab_problem: "petab.Problem", + amici_model: "amici.Model", ) -> list[SplineInnerParameter]: """Create list of inner free noise parameters from PEtab parameter table.""" # Select the semiquantitative measurements. @@ -378,8 +378,8 @@ def noise_inner_parameters_from_parameter_df( def spline_ixs_for_measurement_specific_parameters( - petab_problem: 'petab.Problem', - amici_model: 'amici.Model', + petab_problem: "petab.Problem", + amici_model: "amici.Model", inner_parameters: list[SplineInnerParameter], ) -> dict[str, list[tuple[int, int, int]]]: """Create mapping of parameters to measurements. diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index f3aa8b28b..a26463ad2 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -52,15 +52,15 @@ def __init__(self, options: dict = None): def validate_options(self): """Validate the current options dictionary.""" - if type(self.options[MIN_DIFF_FACTOR]) is not float: + if not isinstance(self.options[MIN_DIFF_FACTOR], float): raise TypeError(f"{MIN_DIFF_FACTOR} must be of type float.") elif self.options[MIN_DIFF_FACTOR] < 0: raise ValueError(f"{MIN_DIFF_FACTOR} must not be negative.") - elif type(self.options[REGULARIZE_SPLINE]) is not bool: + elif not isinstance(self.options[REGULARIZE_SPLINE], bool): raise TypeError(f"{REGULARIZE_SPLINE} must be of type bool.") if self.options[REGULARIZE_SPLINE]: - if type(self.options[REGULARIZATION_FACTOR]) is not float: + if not isinstance(self.options[REGULARIZATION_FACTOR], float): raise TypeError( f"{REGULARIZATION_FACTOR} must be of type float." ) @@ -142,7 +142,10 @@ def calculate_obj_function(x_inner_opt: list): x_inner_opt[idx][SCIPY_SUCCESS] for idx in range(len(x_inner_opt)) ): obj = np.inf - warnings.warn("Inner optimization failed.") + warnings.warn( + "Inner optimization failed.", + stacklevel=2, + ) else: obj = np.sum( [ @@ -331,8 +334,7 @@ def calculate_gradients( n=n, ) dJ_dsigma2 = ( - K / (2 * sigma**2) - - residual_squared / sigma**4 + K / (2 * sigma**2) - residual_squared / sigma**4 ) dsigma2_dtheta = ssigma_all[0] * sigma dsigma_grad_term = dJ_dsigma2 * dsigma2_dtheta @@ -492,7 +494,9 @@ def _rescale_spline_bases(self, sim_all: np.ndarray, N: int, K: int): if n[i] > N: n[i] = N warnings.warn( - "Interval for a simulation has been set to a larger value than the number of spline parameters." + "Interval for a simulation has been set to a larger " + "value than the number of spline parameters.", + stacklevel=2, ) # In case the simulations are sufficiently apart: else: @@ -757,8 +761,7 @@ def _calculate_nllh_gradient_for_group( # Combine all terms into the gradient of the negative log-likelihood nllh_gradient = ( - residuals_squared_gradient / (sigma**2) - + regularization_term_gradient + residuals_squared_gradient / (sigma**2) + regularization_term_gradient ) return nllh_gradient @@ -1086,8 +1089,11 @@ def save_inner_parameters_to_inner_problem( group ) + lower_trian = np.tril(np.ones((len(s), len(s)))) + xi = np.dot(lower_trian, s) + for idx in range(len(inner_spline_parameters)): - inner_spline_parameters[idx].value = s[idx] + inner_spline_parameters[idx].value = xi[idx] sigma = group_dict[INNER_NOISE_PARS] diff --git a/pypesto/history/base.py b/pypesto/history/base.py index 8870830ff..0e01df195 100644 --- a/pypesto/history/base.py +++ b/pypesto/history/base.py @@ -535,7 +535,7 @@ def reduce_result_via_options( # apply options to result for key in HistoryBase.RESULT_KEYS: if result.get(key) is None or not options.get( - f'trace_record_{key}', True + f"trace_record_{key}", True ): result[key] = np.nan diff --git a/pypesto/history/csv.py b/pypesto/history/csv.py index c672fe8fe..989bc7e95 100644 --- a/pypesto/history/csv.py +++ b/pypesto/history/csv.py @@ -65,7 +65,7 @@ def __init__( trace = pd.read_csv(self.file, header=[0, 1], index_col=0) # replace 'nan' in cols with np.NAN cols = pd.DataFrame(trace.columns.to_list()) - cols[cols == 'nan'] = np.NaN + cols[cols == "nan"] = np.NaN trace.columns = pd.MultiIndex.from_tuples( cols.to_records(index=False).tolist() ) @@ -138,7 +138,7 @@ def _update_trace( # create table row row = pd.Series( - name=len(self._trace), index=self._trace.columns, dtype='object' + name=len(self._trace), index=self._trace.columns, dtype="object" ) values = self._simulation_to_values(result, used_time) @@ -150,7 +150,7 @@ def _update_trace( X: x, GRAD: result[GRAD], }.items(): - if var == X or self.options[f'trace_record_{var}']: + if var == X or self.options[f"trace_record_{var}"]: row[var] = val else: row[(var, np.nan)] = np.nan @@ -186,31 +186,31 @@ def _trace_columns(self) -> list[tuple]: def _init_trace(self, x: np.ndarray): """Initialize the trace.""" if self.x_names is None: - self.x_names = [f'x{i}' for i, _ in enumerate(x)] + self.x_names = [f"x{i}" for i, _ in enumerate(x)] columns = self._trace_columns() for var in [X, GRAD]: - if var == X or self.options[f'trace_record_{var}']: + if var == X or self.options[f"trace_record_{var}"]: columns.extend([(var, x_name) for x_name in self.x_names]) else: columns.extend([(var,)]) # TODO: multi-index for res, sres, hess self._trace = pd.DataFrame( - columns=pd.MultiIndex.from_tuples(columns), dtype='float64' + columns=pd.MultiIndex.from_tuples(columns), dtype="float64" ) # only non-float64 trace_dtypes = { - RES: 'object', - SRES: 'object', - HESS: 'object', - N_FVAL: 'int64', - N_GRAD: 'int64', - N_HESS: 'int64', - N_RES: 'int64', - N_SRES: 'int64', + RES: "object", + SRES: "object", + HESS: "object", + N_FVAL: "int64", + N_GRAD: "int64", + N_HESS: "int64", + N_RES: "int64", + N_SRES: "int64", } for var, dtype in trace_dtypes.items(): @@ -331,9 +331,9 @@ def string2ndarray(x: Union[str, float]) -> Union[np.ndarray, float]: """ if not isinstance(x, str): return x - if x.startswith('[['): + if x.startswith("[["): return np.vstack( - [np.fromstring(xx, sep=' ') for xx in x[2:-2].split(']\n [')] + [np.fromstring(xx, sep=" ") for xx in x[2:-2].split("]\n [")] ) else: - return np.fromstring(x[1:-1], sep=' ') + return np.fromstring(x[1:-1], sep=" ") diff --git a/pypesto/history/hdf5.py b/pypesto/history/hdf5.py index 9b5425e45..e7f308452 100644 --- a/pypesto/history/hdf5.py +++ b/pypesto/history/hdf5.py @@ -142,7 +142,7 @@ def finalize(self, message: str = None, exitflag: str = None) -> None: super().finalize() # add message and exitflag to trace - grp = self._f.require_group(f'{HISTORY}/{self.id}/{MESSAGES}/') + grp = self._f.require_group(f"{HISTORY}/{self.id}/{MESSAGES}/") if message is not None: grp.attrs[MESSAGE] = message if exitflag is not None: @@ -153,7 +153,7 @@ def load( id: str, file: Union[str, Path], options: Union[HistoryOptions, dict] = None, - ) -> 'Hdf5History': + ) -> "Hdf5History": """Load the History object from memory.""" history = Hdf5History(id=id, file=file, options=options) if options is None: @@ -288,7 +288,7 @@ def start_time(self) -> float: def message(self) -> str: """Optimizer message in case of finished optimization.""" try: - return self._f[f'{HISTORY}/{self.id}/{MESSAGES}/'].attrs[MESSAGE] + return self._f[f"{HISTORY}/{self.id}/{MESSAGES}/"].attrs[MESSAGE] except KeyError: return None @@ -297,7 +297,7 @@ def message(self) -> str: def exitflag(self) -> str: """Optimizer exitflag in case of finished optimization.""" try: - return self._f[f'{HISTORY}/{self.id}/{MESSAGES}/'].attrs[EXITFLAG] + return self._f[f"{HISTORY}/{self.id}/{MESSAGES}/"].attrs[EXITFLAG] except KeyError: return None @@ -340,22 +340,22 @@ def _update_trace( for key in values.keys(): if values[key] is not None: - self._require_group()[f'{iteration}/{key}'] = values[key] + self._require_group()[f"{iteration}/{key}"] = values[key] self._require_group().attrs[N_ITERATIONS] += 1 @with_h5_file("r") def _get_group(self) -> h5py.Group: """Get the HDF5 group for the current history.""" - return self._f[f'{HISTORY}/{self.id}/{TRACE}/'] + return self._f[f"{HISTORY}/{self.id}/{TRACE}/"] @with_h5_file("a") def _require_group(self) -> h5py.Group: """Get, or if necessary create, the group in the hdf5 file.""" with contextlib.suppress(KeyError): - return self._f[f'{HISTORY}/{self.id}/{TRACE}/'] + return self._f[f"{HISTORY}/{self.id}/{TRACE}/"] - grp = self._f.create_group(f'{HISTORY}/{self.id}/{TRACE}/') + grp = self._f.create_group(f"{HISTORY}/{self.id}/{TRACE}/") grp.attrs[N_ITERATIONS] = 0 grp.attrs[N_FVAL] = 0 grp.attrs[N_GRAD] = 0 @@ -396,7 +396,7 @@ def _get_hdf5_entries( for iteration in ix: try: dataset = self._f[ - f'{HISTORY}/{self.id}/{TRACE}/{iteration}/{entry_id}' + f"{HISTORY}/{self.id}/{TRACE}/{iteration}/{entry_id}" ] if dataset.shape == (): entry = dataset[()] # scalar diff --git a/pypesto/history/optimizer.py b/pypesto/history/optimizer.py index 7dfd76248..2e81b876e 100644 --- a/pypesto/history/optimizer.py +++ b/pypesto/history/optimizer.py @@ -156,7 +156,7 @@ def finalize( ) # update everything for key in self.MIN_KEYS: - setattr(self, key + '_min', result[key]) + setattr(self, key + "_min", result[key]) # check if history has same point if ( @@ -169,7 +169,7 @@ def finalize( for key in self.MIN_KEYS: if result[key] is not None: # if getattr(self, f'{key}_min') is None: - setattr(self, f'{key}_min', result[key]) + setattr(self, f"{key}_min", result[key]) def _update_vals(self, x: np.ndarray, result: ResultDict) -> None: """Update initial and best function values.""" @@ -188,7 +188,7 @@ def _update_vals(self, x: np.ndarray, result: ResultDict) -> None: ): # need to update all values, as better fval found for key in HistoryBase.RESULT_KEYS: - setattr(self, f'{key}_min', result.get(key)) + setattr(self, f"{key}_min", result.get(key)) self.x_min = x return @@ -196,11 +196,11 @@ def _update_vals(self, x: np.ndarray, result: ResultDict) -> None: # identify this situation by checking that x hasn't changed. if self.x_min is not None and np.array_equal(self.x_min, x): for key in (GRAD, HESS, SRES): - val_min = getattr(self, f'{key}_min', None) + val_min = getattr(self, f"{key}_min", None) if is_none_or_nan_array(val_min) and not is_none_or_nan_array( val := result.get(key) ): - setattr(self, f'{key}_min', val) + setattr(self, f"{key}_min", val) def _maybe_compute_init_and_min_vals_from_trace(self) -> None: """Try to set initial and best function value from trace. @@ -225,7 +225,7 @@ def _maybe_compute_init_and_min_vals_from_trace(self) -> None: # assign values for key in OptimizerHistory.MIN_KEYS: - setattr(self, f'{key}_min', result[key]) + setattr(self, f"{key}_min", result[key]) def _admissible(self, x: np.ndarray) -> bool: """Check whether point `x` is admissible (i.e. within bounds). @@ -264,7 +264,7 @@ def _get_optimal_point_from_history(self) -> ResultDict: # fill in parameter and function value from that index for var in (X, FVAL, RES): - val = getattr(self.history, f'get_{var}_trace')(ix_min) + val = getattr(self.history, f"get_{var}_trace")(ix_min) if val is not None and not np.all(np.isnan(val)): result[var] = val # convert to float if var is FVAL to be sure @@ -279,7 +279,7 @@ def _get_optimal_point_from_history(self) -> ResultDict: if not allclose(result[X], self.history.get_x_trace(ix)): # different parameter continue - val = getattr(self.history, f'get_{var}_trace')(ix) + val = getattr(self.history, f"get_{var}_trace")(ix) if not is_none_or_nan_array(val): result[var] = val # successfuly found diff --git a/pypesto/history/options.py b/pypesto/history/options.py index 6f676f7b5..f1efa9082 100644 --- a/pypesto/history/options.py +++ b/pypesto/history/options.py @@ -68,7 +68,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @@ -91,8 +91,8 @@ def _sanity_check(self): @staticmethod def assert_instance( - maybe_options: Union['HistoryOptions', dict], - ) -> 'HistoryOptions': + maybe_options: Union["HistoryOptions", dict], + ) -> "HistoryOptions": """ Return a valid options object. diff --git a/pypesto/history/util.py b/pypesto/history/util.py index 8f658962a..35e6f0d49 100644 --- a/pypesto/history/util.py +++ b/pypesto/history/util.py @@ -9,7 +9,7 @@ from ..C import SUFFIXES ResultDict = dict[str, Union[float, np.ndarray]] -MaybeArray = Union[np.ndarray, 'np.nan'] +MaybeArray = Union[np.ndarray, "np.nan"] class HistoryTypeError(ValueError): diff --git a/pypesto/logging.py b/pypesto/logging.py index ab914a260..fbc284a64 100644 --- a/pypesto/logging.py +++ b/pypesto/logging.py @@ -9,10 +9,10 @@ def log( - name: str = 'pypesto', + name: str = "pypesto", level: int = logging.INFO, console: bool = True, - filename: str = '', + filename: str = "", ): """ Log messages from `name` with `level` to any combination of console/file. @@ -54,7 +54,7 @@ def log_to_console(level: int = logging.INFO): def log_to_file( - level: int = logging.INFO, filename: str = '.pypesto_logging.log' + level: int = logging.INFO, filename: str = ".pypesto_logging.log" ): """ Log to file. diff --git a/pypesto/objective/aesara/base.py b/pypesto/objective/aesara/base.py index 216af738f..8ba7d3c29 100644 --- a/pypesto/objective/aesara/base.py +++ b/pypesto/objective/aesara/base.py @@ -24,7 +24,7 @@ "Using an aeasara objective requires an installation of " "the python package aesara. Please install aesara via " "`pip install aesara`." - ) + ) from None class AesaraObjective(ObjectiveBase): @@ -57,10 +57,10 @@ def __init__( x_names: Sequence[str] = None, ): if not isinstance(objective, ObjectiveBase): - raise TypeError('objective must be an ObjectiveBase instance') + raise TypeError("objective must be an ObjectiveBase instance") if not objective.check_mode(MODE_FUN): raise NotImplementedError( - f'objective must support mode={MODE_FUN}' + f"objective must support mode={MODE_FUN}" ) super().__init__(x_names) self.base_objective = objective @@ -125,14 +125,14 @@ def call_unprocessed( # them accessible to aesara compiled functions set_return_dict, return_dict = ( - 'return_dict' in kwargs, - kwargs.pop('return_dict', False), + "return_dict" in kwargs, + kwargs.pop("return_dict", False), ) self.cached_base_ret = self.base_objective( self.infun(x), sensi_orders, mode, return_dict=True, **kwargs ) if set_return_dict: - kwargs['return_dict'] = return_dict + kwargs["return_dict"] = return_dict ret = {} if RDATAS in self.cached_base_ret: ret[RDATAS] = self.cached_base_ret[RDATAS] diff --git a/pypesto/objective/aggregated.py b/pypesto/objective/aggregated.py index c34d1ca76..ab74a638b 100644 --- a/pypesto/objective/aggregated.py +++ b/pypesto/objective/aggregated.py @@ -30,19 +30,19 @@ def __init__( # input typechecks if not isinstance(objectives, Sequence): raise TypeError( - f'Objectives must be a Sequence, ' f'was {type(objectives)}.' + f"Objectives must be a Sequence, " f"was {type(objectives)}." ) if not all( isinstance(objective, ObjectiveBase) for objective in objectives ): raise TypeError( - 'Objectives must only contain elements of type' - 'pypesto.Objective' + "Objectives must only contain elements of type" + "pypesto.Objective" ) if not objectives: - raise ValueError('Length of objectives must be at least one') + raise ValueError("Length of objectives must be at least one") self._objectives = objectives @@ -54,7 +54,7 @@ def __deepcopy__(self, memodict=None): objectives=[deepcopy(objective) for objective in self._objectives], x_names=deepcopy(self.x_names), ) - for key in set(self.__dict__.keys()) - {'_objectives', 'x_names'}: + for key in set(self.__dict__.keys()) - {"_objectives", "x_names"}: other.__dict__[key] = deepcopy(self.__dict__[key]) return other @@ -125,7 +125,7 @@ def get_config(self) -> dict: """Return basic information of the objective configuration.""" info = super().get_config() for n_obj, obj in enumerate(self._objectives): - info[f'objective_{n_obj}'] = obj.get_config() + info[f"objective_{n_obj}"] = obj.get_config() return info diff --git a/pypesto/objective/amici/amici.py b/pypesto/objective/amici/amici.py index 13e2d87e8..76b239b4e 100644 --- a/pypesto/objective/amici/amici.py +++ b/pypesto/objective/amici/amici.py @@ -40,8 +40,8 @@ except ImportError: pass -AmiciModel = Union['amici.Model', 'amici.ModelPtr'] -AmiciSolver = Union['amici.Solver', 'amici.SolverPtr'] +AmiciModel = Union["amici.Model", "amici.ModelPtr"] +AmiciSolver = Union["amici.Solver", "amici.SolverPtr"] class AmiciObjectBuilder(abc.ABC): @@ -61,7 +61,7 @@ def create_solver(self, model: AmiciModel) -> AmiciSolver: """Create an AMICI solver.""" @abc.abstractmethod - def create_edatas(self, model: AmiciModel) -> Sequence['amici.ExpData']: + def create_edatas(self, model: AmiciModel) -> Sequence["amici.ExpData"]: """Create AMICI experimental data.""" @@ -72,17 +72,17 @@ def __init__( self, amici_model: AmiciModel, amici_solver: AmiciSolver, - edatas: Union[Sequence['amici.ExpData'], 'amici.ExpData'], + edatas: Union[Sequence["amici.ExpData"], "amici.ExpData"], max_sensi_order: Optional[int] = None, x_ids: Optional[Sequence[str]] = None, x_names: Optional[Sequence[str]] = None, - parameter_mapping: Optional['ParameterMapping'] = None, + parameter_mapping: Optional["ParameterMapping"] = None, guess_steadystate: Optional[Optional[bool]] = None, n_threads: Optional[int] = 1, fim_for_hess: Optional[bool] = True, amici_object_builder: Optional[AmiciObjectBuilder] = None, calculator: Optional[AmiciCalculator] = None, - amici_reporting: Optional['amici.RDataReporting'] = None, + amici_reporting: Optional["amici.RDataReporting"] = None, ): """ Initialize objective. @@ -181,8 +181,8 @@ def __init__( ): if self.guess_steadystate: raise ValueError( - 'Steadystate prediction is not supported ' - 'for models with conservation laws!' + "Steadystate prediction is not supported " + "for models with conservation laws!" ) self.guess_steadystate = False @@ -193,9 +193,9 @@ def __init__( ): if self.guess_steadystate: raise ValueError( - 'Steadystate guesses cannot be enabled ' - 'when `integrationOnly` as ' - 'SteadyStateSensitivityMode!' + "Steadystate guesses cannot be enabled " + "when `integrationOnly` as " + "SteadyStateSensitivityMode!" ) self.guess_steadystate = False @@ -206,8 +206,8 @@ def __init__( # preallocate guesses, construct a dict for every edata for which # we need to do preequilibration self.steadystate_guesses = { - 'fval': np.inf, - 'data': { + "fval": np.inf, + "data": { iexp: {} for iexp, edata in enumerate(self.edatas) if len(edata.fixedParametersPreequilibration) @@ -238,10 +238,10 @@ def __init__( def get_config(self) -> dict: """Return basic information of the objective configuration.""" info = super().get_config() - info['x_names'] = self.x_names - info['model_name'] = self.amici_model.getName() - info['solver'] = str(type(self.amici_solver)) - info['sensi_order'] = self.max_sensi_order + info["x_names"] = self.x_names + info["model_name"] = self.amici_model.getName() + info["solver"] = str(type(self.amici_solver)) + info["sensi_order"] = self.max_sensi_order return info @@ -278,15 +278,15 @@ def initialize(self): self.reset_steadystate_guesses() self.calculator.initialize() - def __deepcopy__(self, memodict: Dict = None) -> 'AmiciObjective': + def __deepcopy__(self, memodict: Dict = None) -> "AmiciObjective": import amici other = self.__class__.__new__(self.__class__) for key in set(self.__dict__.keys()) - { - 'amici_model', - 'amici_solver', - 'edatas', + "amici_model", + "amici_solver", + "edatas", }: other.__dict__[key] = copy.deepcopy(self.__dict__[key]) @@ -308,9 +308,9 @@ def __getstate__(self) -> Dict: state = {} for key in set(self.__dict__.keys()) - { - 'amici_model', - 'amici_solver', - 'edatas', + "amici_model", + "amici_solver", + "edatas", }: state[key] = self.__dict__[key] @@ -326,14 +326,14 @@ def __getstate__(self) -> Dict: ) raise # read in byte stream - with open(_fd, 'rb', closefd=False) as f: - state['amici_solver_settings'] = f.read() + with open(_fd, "rb", closefd=False) as f: + state["amici_solver_settings"] = f.read() finally: # close file descriptor and remove temporary file os.close(_fd) os.remove(_file) - state['AMICI_model_settings'] = amici.get_model_settings( + state["AMICI_model_settings"] = amici.get_model_settings( self.amici_model ) @@ -342,7 +342,7 @@ def __getstate__(self) -> Dict: def __setstate__(self, state: Dict) -> None: import amici - if state['amici_object_builder'] is None: + if state["amici_object_builder"] is None: raise NotImplementedError( "AmiciObjective does not support __setstate__ without " "an `amici_object_builder`." @@ -357,14 +357,14 @@ def __setstate__(self, state: Dict) -> None: _fd, _file = tempfile.mkstemp() try: # write solver settings to temporary file - with open(_fd, 'wb', closefd=False) as f: - f.write(state['amici_solver_settings']) + with open(_fd, "wb", closefd=False) as f: + f.write(state["amici_solver_settings"]) # read in solver settings try: amici.readSolverSettingsFromHDF5(_file, solver) except AttributeError as err: if not err.args: - err.args = ('',) + err.args = ("",) err.args += ( "Unpickling an AmiciObjective requires an AMICI " "installation with HDF5 support.", @@ -382,7 +382,7 @@ def __setstate__(self, state: Dict) -> None: self.apply_custom_timepoints() amici.set_model_settings( self.amici_model, - state['AMICI_model_settings'], + state["AMICI_model_settings"], ) def check_sensi_orders( @@ -433,9 +433,9 @@ def __call__( if ( return_dict and self.amici_reporting is None - and 'amici_reporting' not in kwargs + and "amici_reporting" not in kwargs ): - kwargs['amici_reporting'] = amici.RDataReporting.full + kwargs["amici_reporting"] = amici.RDataReporting.full return super().__call__(x, sensi_orders, mode, return_dict, **kwargs) @@ -444,9 +444,9 @@ def call_unprocessed( x: np.ndarray, sensi_orders: Tuple[int, ...], mode: ModeType, - edatas: Sequence['amici.ExpData'] = None, - parameter_mapping: 'ParameterMapping' = None, - amici_reporting: Optional['amici.RDataReporting'] = None, + edatas: Sequence["amici.ExpData"] = None, + parameter_mapping: "ParameterMapping" = None, + amici_reporting: Optional["amici.RDataReporting"] = None, ): """ Call objective function without pre- or post-processing and formatting. @@ -477,7 +477,7 @@ def call_unprocessed( # update steady state if ( self.guess_steadystate - and self.steadystate_guesses['fval'] < np.inf + and self.steadystate_guesses["fval"] < np.inf ): for data_ix in range(len(self.edatas)): self.apply_steadystate_guess(data_ix, x_dct) @@ -507,10 +507,10 @@ def call_unprocessed( # check whether we should update data for preequilibration guesses if ( self.guess_steadystate - and nllh <= self.steadystate_guesses['fval'] + and nllh <= self.steadystate_guesses["fval"] and nllh < np.inf ): - self.steadystate_guesses['fval'] = nllh + self.steadystate_guesses["fval"] = nllh for data_ix, rdata in enumerate(rdatas): self.store_steadystate_guess(data_ix, x_dct, rdata) @@ -533,16 +533,16 @@ def apply_steadystate_guess(self, condition_ix: int, x_dct: Dict) -> None: mapping = self.parameter_mapping[condition_ix].map_sim_var x_sim = map_par_opt_to_par_sim(mapping, x_dct, self.amici_model) x_ss_guess = [] # resets initial state by default - if condition_ix in self.steadystate_guesses['data']: - guess_data = self.steadystate_guesses['data'][condition_ix] - if guess_data['x_ss'] is not None: - x_ss_guess = guess_data['x_ss'] - if guess_data['sx_ss'] is not None: + if condition_ix in self.steadystate_guesses["data"]: + guess_data = self.steadystate_guesses["data"][condition_ix] + if guess_data["x_ss"] is not None: + x_ss_guess = guess_data["x_ss"] + if guess_data["sx_ss"] is not None: linear_update = ( - guess_data['sx_ss'] + guess_data["sx_ss"] .transpose() .dot( - (x_sim - guess_data['x'])[ + (x_sim - guess_data["x"])[ np.asarray(self.edatas[condition_ix].plist) ] ) @@ -557,7 +557,7 @@ def store_steadystate_guess( self, condition_ix: int, x_dct: Dict, - rdata: 'amici.ReturnData', + rdata: "amici.ReturnData", ) -> None: """ Store condition parameter, steadystate and steadystate sensitivity. @@ -565,9 +565,9 @@ def store_steadystate_guess( Stored in steadystate_guesses if steadystate guesses are enabled for this condition. """ - if condition_ix not in self.steadystate_guesses['data']: + if condition_ix not in self.steadystate_guesses["data"]: return - preeq_guesses = self.steadystate_guesses['data'][condition_ix] + preeq_guesses = self.steadystate_guesses["data"][condition_ix] # update parameter condition_map_sim_var = self.parameter_mapping[ @@ -576,20 +576,20 @@ def store_steadystate_guess( x_sim = map_par_opt_to_par_sim( condition_map_sim_var, x_dct, self.amici_model ) - preeq_guesses['x'] = x_sim + preeq_guesses["x"] = x_sim # update steadystates - preeq_guesses['x_ss'] = rdata['x_ss'] - preeq_guesses['sx_ss'] = rdata['sx_ss'] + preeq_guesses["x_ss"] = rdata["x_ss"] + preeq_guesses["sx_ss"] = rdata["sx_ss"] def reset_steadystate_guesses(self) -> None: """Reset all steadystate guess data.""" if not self.guess_steadystate: return - self.steadystate_guesses['fval'] = np.inf - for condition in self.steadystate_guesses['data']: - self.steadystate_guesses['data'][condition] = {} + self.steadystate_guesses["fval"] = np.inf + for condition in self.steadystate_guesses["data"]: + self.steadystate_guesses["data"][condition] = {} def apply_custom_timepoints(self) -> None: """Apply custom timepoints, if applicable. @@ -604,7 +604,7 @@ def set_custom_timepoints( self, timepoints: Sequence[Sequence[Union[float, int]]] = None, timepoints_global: Sequence[Union[float, int]] = None, - ) -> 'AmiciObjective': + ) -> "AmiciObjective": """ Create a copy of this objective that is evaluated at custom timepoints. @@ -624,18 +624,18 @@ def set_custom_timepoints( The customized copy of this objective. """ if timepoints is None and timepoints_global is None: - raise KeyError('Timepoints were not specified.') + raise KeyError("Timepoints were not specified.") amici_objective = copy.deepcopy(self) if timepoints is not None: if len(timepoints) != len(amici_objective.edatas): raise ValueError( - 'The number of condition-specific timepoints `timepoints` ' - 'does not match the number of experimental conditions.\n' - f'Number of provided timepoints: {len(timepoints)}. ' - 'Number of experimental conditions: ' - f'{len(amici_objective.edatas)}.' + "The number of condition-specific timepoints `timepoints` " + "does not match the number of experimental conditions.\n" + f"Number of provided timepoints: {len(timepoints)}. " + "Number of experimental conditions: " + f"{len(amici_objective.edatas)}." ) custom_timepoints = timepoints else: @@ -662,7 +662,7 @@ def check_gradients_match_finite_differences( bool Indicates whether gradients match (True) FDs or not (False) """ - if x is None and 'petab_problem' in dir(self.amici_object_builder): + if x is None and "petab_problem" in dir(self.amici_object_builder): x = self.amici_object_builder.petab_problem.x_nominal_scaled x_free = self.amici_object_builder.petab_problem.x_free_indices return super().check_gradients_match_finite_differences( diff --git a/pypesto/objective/amici/amici_calculator.py b/pypesto/objective/amici/amici_calculator.py index a68b95062..ba888366b 100644 --- a/pypesto/objective/amici/amici_calculator.py +++ b/pypesto/objective/amici/amici_calculator.py @@ -32,8 +32,8 @@ except ImportError: ParameterMapping = None -AmiciModel = Union['amici.Model', 'amici.ModelPtr'] -AmiciSolver = Union['amici.Solver', 'amici.SolverPtr'] +AmiciModel = Union["amici.Model", "amici.ModelPtr"] +AmiciSolver = Union["amici.Solver", "amici.SolverPtr"] class AmiciCalculator: @@ -122,16 +122,16 @@ def __call__( ): if not amici_model.getAddSigmaResiduals() and any( ( - (r['ssigmay'] is not None and np.any(r['ssigmay'])) - or (r['ssigmaz'] is not None and np.any(r['ssigmaz'])) + (r["ssigmay"] is not None and np.any(r["ssigmay"])) + or (r["ssigmaz"] is not None and np.any(r["ssigmaz"])) ) for r in rdatas ): raise RuntimeError( - 'Cannot use least squares solver with' - 'parameter dependent sigma! Support can be ' - 'enabled via ' - 'amici_model.setAddSigmaResiduals().' + "Cannot use least squares solver with" + "parameter dependent sigma! Support can be " + "enabled via " + "amici_model.setAddSigmaResiduals()." ) self._known_least_squares_safe = True # don't check this again @@ -166,7 +166,7 @@ def calculate_function_values( dim = len(x_ids) # check if the simulation failed - if any(rdata['status'] < 0.0 for rdata in rdatas): + if any(rdata["status"] < 0.0 for rdata in rdatas): return get_error_output( amici_model, edatas, rdatas, sensi_orders, mode, dim ) @@ -185,7 +185,7 @@ def calculate_function_values( condition_map_sim_var = parameter_mapping[data_ix].map_sim_var # add objective value - nllh -= rdata['llh'] + nllh -= rdata["llh"] if mode == MODE_FUN: if not np.isfinite(nllh): @@ -199,7 +199,7 @@ def calculate_function_values( x_ids, par_sim_ids, condition_map_sim_var, - rdata['sllh'], + rdata["sllh"], snllh, coefficient=-1.0, ) @@ -221,7 +221,7 @@ def calculate_function_values( x_ids, par_sim_ids, condition_map_sim_var, - rdata['FIM'], + rdata["FIM"], s2nllh, coefficient=+1.0, ) @@ -232,18 +232,18 @@ def calculate_function_values( elif mode == MODE_RES: if 0 in sensi_orders: - chi2 += rdata['chi2'] + chi2 += rdata["chi2"] res = ( - np.hstack([res, rdata['res']]) + np.hstack([res, rdata["res"]]) if res.size - else rdata['res'] + else rdata["res"] ) if 1 in sensi_orders: opt_sres = sim_sres_to_opt_sres( x_ids, par_sim_ids, condition_map_sim_var, - rdata['sres'], + rdata["sres"], coefficient=1.0, ) sres = np.vstack([sres, opt_sres]) if sres.size else opt_sres diff --git a/pypesto/objective/amici/amici_util.py b/pypesto/objective/amici/amici_util.py index d14691c64..d0aa54cdd 100644 --- a/pypesto/objective/amici/amici_util.py +++ b/pypesto/objective/amici/amici_util.py @@ -30,8 +30,8 @@ except ImportError: ParameterMapping = ParameterMappingForCondition = None -AmiciModel = Union['amici.Model', 'amici.ModelPtr'] -AmiciSolver = Union['amici.Solver', 'amici.SolverPtr'] +AmiciModel = Union["amici.Model", "amici.ModelPtr"] +AmiciSolver = Union["amici.Solver", "amici.SolverPtr"] logger = logging.getLogger(__name__) @@ -101,6 +101,7 @@ def create_plist_from_par_opt_to_par_sim(mapping_par_opt_to_par_sim): warnings.warn( "This function will be removed in future releases. ", DeprecationWarning, + stacklevel=2, ) plist = [] @@ -350,7 +351,7 @@ def log_simulation(data_ix, rdata) -> None: logger.debug(f"status: {rdata['status']}") logger.debug(f"llh: {rdata['llh']}") - t_steadystate = 't_steadystate' + t_steadystate = "t_steadystate" if t_steadystate in rdata and rdata[t_steadystate] != np.nan: logger.debug(f"t_steadystate: {rdata[t_steadystate]}") @@ -360,8 +361,8 @@ def log_simulation(data_ix, rdata) -> None: def get_error_output( amici_model: AmiciModel, - edatas: Sequence['amici.ExpData'], - rdatas: Sequence['amici.ReturnData'], + edatas: Sequence["amici.ExpData"], + rdatas: Sequence["amici.ReturnData"], sensi_orders: Tuple[int, ...], mode: ModeType, dim: int, diff --git a/pypesto/objective/base.py b/pypesto/objective/base.py index a38114bd4..c3960bd14 100644 --- a/pypesto/objective/base.py +++ b/pypesto/objective/base.py @@ -55,7 +55,7 @@ def __init__( self.pre_post_processor = PrePostProcessor() self.history = NoHistory() - def __deepcopy__(self, memodict=None) -> 'ObjectiveBase': + def __deepcopy__(self, memodict=None) -> "ObjectiveBase": """Create deepcopy of objective object.""" other = type(self)() # maintain type for derived classes for attr, val in self.__dict__.items(): @@ -260,7 +260,7 @@ def get_config(self) -> dict: Return it as a dictionary. """ - info = {'type': self.__class__.__name__} + info = {"type": self.__class__.__name__} return info def check_sensi_orders( @@ -418,7 +418,7 @@ def check_grad_multi_eps( self, *args, multi_eps: Optional[Iterable] = None, - label: str = 'rel_err', + label: str = "rel_err", **kwargs, ): """ @@ -439,10 +439,10 @@ def check_grad_multi_eps( Valid options are the column labels of the dataframe returned by the `ObjectiveBase.check_grad` method. """ - if 'eps' in kwargs: + if "eps" in kwargs: raise KeyError( - 'Please use the `multi_eps` (not the `eps`) argument with ' - '`check_grad_multi_eps` to specify step sizes.' + "Please use the `multi_eps` (not the `eps`) argument with " + "`check_grad_multi_eps` to specify step sizes." ) if multi_eps is None: @@ -456,7 +456,7 @@ def check_grad_multi_eps( # the step size (`eps`) that produced the smallest error (`label`). combined_result = None for eps, result in results.items(): - result['eps'] = eps + result["eps"] = eps if combined_result is None: combined_result = result continue @@ -574,14 +574,14 @@ def check_grad( # log for dimension ix if verbosity > 1: logger.info( - f'index: {ix}\n' - f'grad: {grad_ix}\n' - f'fd_f: {fd_f_ix}\n' - f'fd_b: {fd_b_ix}\n' - f'fd_c: {fd_c_ix}\n' - f'fd_err: {fd_err_ix}\n' - f'abs_err: {abs_err_ix}\n' - f'rel_err: {rel_err_ix}\n' + f"index: {ix}\n" + f"grad: {grad_ix}\n" + f"fd_f: {fd_f_ix}\n" + f"fd_b: {fd_b_ix}\n" + f"fd_c: {fd_c_ix}\n" + f"fd_err: {fd_err_ix}\n" + f"abs_err: {abs_err_ix}\n" + f"rel_err: {rel_err_ix}\n" ) # append to lists @@ -600,24 +600,24 @@ def check_grad( # create data dictionary for dataframe data = { - 'grad': grad_list, - 'fd_f': fd_f_list, - 'fd_b': fd_b_list, - 'fd_c': fd_c_list, - 'fd_err': fd_err_list, - 'abs_err': abs_err_list, - 'rel_err': rel_err_list, + "grad": grad_list, + "fd_f": fd_f_list, + "fd_b": fd_b_list, + "fd_c": fd_c_list, + "fd_err": fd_err_list, + "abs_err": abs_err_list, + "rel_err": rel_err_list, } # update data dictionary if detailed output is requested if detailed: prefix_data = { - 'fval': [fval] * len(x_indices), - 'fval_p': fval_p_list, - 'fval_m': fval_m_list, + "fval": [fval] * len(x_indices), + "fval_p": fval_p_list, + "fval_m": fval_m_list, } - std_str = '(grad-fd_c)/std({fd_f,fd_b,fd_c})' - mean_str = '|grad-fd_c|/mean(|fd_f-fd_b|,|fd_f-fd_c|,|fd_b-fd_c|)' + std_str = "(grad-fd_c)/std({fd_f,fd_b,fd_c})" + mean_str = "|grad-fd_c|/mean(|fd_f-fd_b|,|fd_f-fd_c|,|fd_b-fd_c|)" postfix_data = { std_str: std_check_list, mean_str: mean_check_list, @@ -628,7 +628,7 @@ def check_grad( result = pd.DataFrame( data=data, index=[ - self.x_names[ix] if self.x_names is not None else f'x_{ix}' + self.x_names[ix] if self.x_names is not None else f"x_{ix}" for ix in x_indices ], ) diff --git a/pypesto/objective/finite_difference.py b/pypesto/objective/finite_difference.py index fde349d81..70883b1d6 100644 --- a/pypesto/objective/finite_difference.py +++ b/pypesto/objective/finite_difference.py @@ -336,7 +336,7 @@ def __init__( def __deepcopy__( self, memodict: Dict = None, - ) -> 'FD': + ) -> "FD": """Create deepcopy of Objective.""" other = self.__class__.__new__(self.__class__) for attr, val in self.__dict__.items(): diff --git a/pypesto/objective/function.py b/pypesto/objective/function.py index 30932b6c5..5c9a33e27 100644 --- a/pypesto/objective/function.py +++ b/pypesto/objective/function.py @@ -119,13 +119,13 @@ def has_sres(self) -> bool: def get_config(self) -> dict: """Return basic information of the objective configuration.""" info = super().get_config() - info['x_names'] = self.x_names + info["x_names"] = self.x_names sensi_order = 0 while self.check_sensi_orders( sensi_orders=(sensi_order,), mode=MODE_FUN ): sensi_order += 1 - info['sensi_order'] = sensi_order - 1 + info["sensi_order"] = sensi_order - 1 return info def call_unprocessed( diff --git a/pypesto/objective/jax/base.py b/pypesto/objective/jax/base.py index 2576656f2..c29876fc8 100644 --- a/pypesto/objective/jax/base.py +++ b/pypesto/objective/jax/base.py @@ -25,14 +25,14 @@ "Using a jax objective requires an installation of " "the python package jax. Please install jax via " "`pip install jax jaxlib`." - ) + ) from None # jax compatible (jittable) objective function using host callback, see # https://jax.readthedocs.io/en/latest/jax.experimental.host_callback.html @partial(custom_jvp, nondiff_argnums=(0,)) -def _device_fun(obj: 'JaxObjective', x: jnp.array): +def _device_fun(obj: "JaxObjective", x: jnp.array): """Jax compatible objective function execution using host callback. This function does not actually call the underlying objective function, @@ -60,7 +60,7 @@ def _device_fun(obj: 'JaxObjective', x: jnp.array): @partial(custom_jvp, nondiff_argnums=(0,)) -def _device_fun_grad(obj: 'JaxObjective', x: jnp.array): +def _device_fun_grad(obj: "JaxObjective", x: jnp.array): """Jax compatible objective gradient execution using host callback. This function does not actually call the underlying objective function, @@ -90,7 +90,7 @@ def _device_fun_grad(obj: 'JaxObjective', x: jnp.array): ) -def _device_fun_hess(obj: 'JaxObjective', x: jnp.array): +def _device_fun_hess(obj: "JaxObjective", x: jnp.array): """Jax compatible objective Hessian execution using host callback. This function does not actually call the underlying objective function, @@ -126,7 +126,7 @@ def _device_fun_hess(obj: 'JaxObjective', x: jnp.array): @_device_fun.defjvp def _device_fun_jvp( - obj: 'JaxObjective', primals: jnp.array, tangents: jnp.array + obj: "JaxObjective", primals: jnp.array, tangents: jnp.array ): """JVP implementation for device_fun.""" (x,) = primals @@ -136,7 +136,7 @@ def _device_fun_jvp( @_device_fun_grad.defjvp def _device_fun_grad_jvp( - obj: 'JaxObjective', primals: jnp.array, tangents: jnp.array + obj: "JaxObjective", primals: jnp.array, tangents: jnp.array ): """JVP implementation for device_fun_grad.""" (x,) = primals @@ -164,10 +164,10 @@ def __init__( x_names: Sequence[str] = None, ): if not isinstance(objective, ObjectiveBase): - raise TypeError('objective must be an ObjectiveBase instance') + raise TypeError("objective must be an ObjectiveBase instance") if not objective.check_mode(MODE_FUN): raise NotImplementedError( - f'objective must support mode={MODE_FUN}' + f"objective must support mode={MODE_FUN}" ) super().__init__(x_names) self.base_objective = objective @@ -241,14 +241,14 @@ def call_unprocessed( # this computes all the results from the inner objective, rendering # them accessible as cached values for device_fun, etc. set_return_dict, return_dict = ( - 'return_dict' in kwargs, - kwargs.pop('return_dict', False), + "return_dict" in kwargs, + kwargs.pop("return_dict", False), ) self.cached_base_ret = self.base_objective( self.infun(x), sensi_orders, mode, return_dict=True, **kwargs ) if set_return_dict: - kwargs['return_dict'] = return_dict + kwargs["return_dict"] = return_dict ret = {} if RDATAS in self.cached_base_ret: ret[RDATAS] = self.cached_base_ret[RDATAS] diff --git a/pypesto/objective/julia/base.py b/pypesto/objective/julia/base.py index 1157cd1ad..e45f54404 100644 --- a/pypesto/objective/julia/base.py +++ b/pypesto/objective/julia/base.py @@ -108,7 +108,7 @@ def __init__( raise ImportError( "Install PyJulia, e.g. via `pip install pypesto[julia]`, " "and see the class documentation", - ) + ) from None # store module name and source file self.module: str = module @@ -170,7 +170,7 @@ def __setstate__(self, d): fun, grad, hess, res, sres = self._get_callables() super().__init__(fun=fun, grad=grad, hess=hess, res=res, sres=sres) - def __deepcopy__(self, memodict=None) -> 'JuliaObjective': + def __deepcopy__(self, memodict=None) -> "JuliaObjective": return JuliaObjective( module=self.module, source_file=self.source_file, @@ -195,7 +195,7 @@ def display_source_ipython(source_file: str): formatter = HtmlFormatter() return display.HTML( '{}'.format( - formatter.get_style_defs('.highlight'), + formatter.get_style_defs(".highlight"), highlight(code, JuliaLexer(), formatter), ) ) diff --git a/pypesto/objective/julia/petabJl.py b/pypesto/objective/julia/petabJl.py index bf58592ec..03da3bf50 100644 --- a/pypesto/objective/julia/petabJl.py +++ b/pypesto/objective/julia/petabJl.py @@ -44,7 +44,7 @@ def __init__( raise ImportError( "Install PyJulia, e.g. via `pip install pypesto[julia]`, " "and see the class documentation", - ) + ) from None self.module = module self.source_file = source_file @@ -76,9 +76,9 @@ def __getstate__(self): """Get state for pickling.""" # if not dumped, dump it via JLD2 return { - 'module': self.module, - 'source_file': self.source_file, - '_petab_problem_name': self._petab_problem_name, + "module": self.module, + "source_file": self.source_file, + "_petab_problem_name": self._petab_problem_name, } def __setstate__(self, state): @@ -87,15 +87,17 @@ def __setstate__(self, state): setattr(self, key, value) # lazy imports try: - from julia import Main # noqa: F401 - from julia import Pkg + from julia import ( + Main, # noqa: F401 + Pkg, + ) Pkg.activate(".") except ImportError: raise ImportError( "Install PyJulia, e.g. via `pip install pypesto[julia]`, " "and see the class documentation", - ) + ) from None # Include module if not already included _read_source(self.module, self.source_file) @@ -141,7 +143,7 @@ def precompile_model(self, force_compile: bool = False): raise ImportError( "Install PyJulia, e.g. via `pip install pypesto[julia]`, " "and see the class documentation", - ) + ) from None # setting up a local project, where the precompilation will be done in from julia import Pkg diff --git a/pypesto/objective/julia/petab_jl_importer.py b/pypesto/objective/julia/petab_jl_importer.py index 7b8618f22..466f3c7a9 100644 --- a/pypesto/objective/julia/petab_jl_importer.py +++ b/pypesto/objective/julia/petab_jl_importer.py @@ -113,7 +113,6 @@ def create_objective( precompile: Whether to precompile the julia module for speed up in multistart optimization. - """ # lazy imports try: @@ -122,7 +121,7 @@ def create_objective( raise ImportError( "Install PyJulia, e.g. via `pip install pypesto[julia]`, " "and see the class documentation", - ) + ) from None if self.source_file is None: self.source_file = f"{self.module}.jl" diff --git a/pypesto/objective/priors.py b/pypesto/objective/priors.py index 78ce47262..95bb0ffa6 100644 --- a/pypesto/objective/priors.py +++ b/pypesto/objective/priors.py @@ -100,7 +100,7 @@ def call_unprocessed( elif order == 2: res[C.HESS] = self.hessian_neg_log_density(x) else: - raise ValueError(f'Invalid sensi order {order}.') + raise ValueError(f"Invalid sensi order {order}.") if mode == C.MODE_RES: for order in sensi_orders: @@ -109,7 +109,7 @@ def call_unprocessed( elif order == 1: res[C.SRES] = self.residual_jacobian(x) else: - raise ValueError(f'Invalid sensi order {order}.') + raise ValueError(f"Invalid sensi order {order}.") return res @@ -127,20 +127,20 @@ def check_sensi_orders( for order in sensi_orders: if order == 0: return all( - prior.get('residual', None) is not None + prior.get("residual", None) is not None for prior in self.prior_list ) elif order == 1: return all( - prior.get('residual_dx', None) is not None + prior.get("residual_dx", None) is not None for prior in self.prior_list ) else: return False else: raise ValueError( - f'Invalid input: Expected mode {C.MODE_FUN} or ' - f'{C.MODE_RES}, received {mode} instead.' + f"Invalid input: Expected mode {C.MODE_FUN} or " + f"{C.MODE_RES}, received {mode} instead." ) return True @@ -151,20 +151,20 @@ def check_mode(self, mode: C.ModeType) -> bool: return True elif mode == C.MODE_RES: return all( - prior.get('residual', None) is not None + prior.get("residual", None) is not None for prior in self.prior_list ) else: raise ValueError( - f'Invalid input: Expected mode {C.MODE_FUN} or ' - f'{C.MODE_RES}, received {mode} instead.' + f"Invalid input: Expected mode {C.MODE_FUN} or " + f"{C.MODE_RES}, received {mode} instead." ) def neg_log_density(self, x): """Evaluate the negative log-density at x.""" density_val = 0 for prior in self.prior_list: - density_val -= prior['density_fun'](x[prior['index']]) + density_val -= prior["density_fun"](x[prior["index"]]) return density_val @@ -173,7 +173,7 @@ def gradient_neg_log_density(self, x): grad = np.zeros_like(x) for prior in self.prior_list: - grad[prior['index']] -= prior['density_dx'](x[prior['index']]) + grad[prior["index"]] -= prior["density_dx"](x[prior["index"]]) return grad @@ -182,8 +182,8 @@ def hessian_neg_log_density(self, x): hessian = np.zeros((len(x), len(x))) for prior in self.prior_list: - hessian[prior['index'], prior['index']] -= prior['density_ddx']( - x[prior['index']] + hessian[prior["index"], prior["index"]] -= prior["density_ddx"]( + x[prior["index"]] ) return hessian @@ -193,8 +193,8 @@ def hessian_vp_neg_log_density(self, x, p): h_dot_p = np.zeros_like(p) for prior in self.prior_list: - h_dot_p[prior['index']] -= ( - prior['density_ddx'](x[prior['index']]) * p[prior['index']] + h_dot_p[prior["index"]] -= ( + prior["density_ddx"](x[prior["index"]]) * p[prior["index"]] ) return h_dot_p @@ -202,7 +202,7 @@ def hessian_vp_neg_log_density(self, x, p): def residual(self, x): """Evaluate the residual representation of the prior at x.""" return np.asarray( - [prior['residual'](x[prior['index']]) for prior in self.prior_list] + [prior["residual"](x[prior["index"]]) for prior in self.prior_list] ) def residual_jacobian(self, x): @@ -214,8 +214,8 @@ def residual_jacobian(self, x): """ sres = np.zeros((len(self.prior_list), len(x))) for iprior, prior in enumerate(self.prior_list): - sres[iprior, prior['index']] = prior['residual_dx']( - x[prior['index']] + sres[iprior, prior["index"]] = prior["residual_dx"]( + x[prior["index"]] ) return sres @@ -249,14 +249,14 @@ def get_parameter_prior_dict( prior_type, prior_parameters ) - if parameter_scale == C.LIN or prior_type.startswith('parameterScale'): + if parameter_scale == C.LIN or prior_type.startswith("parameterScale"): return { - 'index': index, - 'density_fun': log_f, - 'density_dx': d_log_f_dx, - 'density_ddx': dd_log_f_ddx, - 'residual': res, - 'residual_dx': d_res_dx, + "index": index, + "density_fun": log_f, + "density_dx": d_log_f_dx, + "density_ddx": dd_log_f_ddx, + "residual": res, + "residual_dx": d_res_dx, } elif parameter_scale == C.LOG: @@ -295,12 +295,12 @@ def d_res_log(x_log): d_res_log = None return { - 'index': index, - 'density_fun': log_f_log, - 'density_dx': d_log_f_log, - 'density_ddx': dd_log_f_log, - 'residual': res_log, - 'residual_dx': d_res_log, + "index": index, + "density_fun": log_f_log, + "density_dx": d_log_f_log, + "density_ddx": dd_log_f_log, + "residual": res_log, + "residual_dx": d_res_log, } elif parameter_scale == C.LOG10: @@ -340,12 +340,12 @@ def d_res_log(x_log10): return d_res_dx(10**x_log10) * log10 * 10**x_log10 return { - 'index': index, - 'density_fun': log_f_log10, - 'density_dx': d_log_f_log10, - 'density_ddx': dd_log_f_log10, - 'residual': res_log, - 'residual_dx': d_res_log, + "index": index, + "density_fun": log_f_log10, + "density_dx": d_log_f_log10, + "density_ddx": dd_log_f_log10, + "residual": res_log, + "residual_dx": d_res_log, } else: @@ -521,9 +521,7 @@ def d_log_f_dx(x): return -1 / x - (np.log(x) - mean) / (sigma**2 * x) def dd_log_f_ddx(x): - return 1 / (x**2) - (1 - np.log(x) + mean) / ( - sigma**2 * x**2 - ) + return 1 / (x**2) - (1 - np.log(x) + mean) / (sigma**2 * x**2) return log_f, d_log_f_dx, dd_log_f_ddx, None, None @@ -532,7 +530,7 @@ def dd_log_f_ddx(x): raise NotImplementedError else: raise ValueError( - f'NegLogPriors of type {prior_type} are currently ' 'not supported' + f"NegLogPriors of type {prior_type} are currently " "not supported" ) diff --git a/pypesto/optimize/ess/cess.py b/pypesto/optimize/ess/cess.py index 06122247b..2d036ba75 100644 --- a/pypesto/optimize/ess/cess.py +++ b/pypesto/optimize/ess/cess.py @@ -123,8 +123,10 @@ def minimize( """ if startpoint_method is not None: warn( - "Passing `startpoint_method` directly is deprecated, use `problem.startpoint_method` instead.", + "Passing `startpoint_method` directly is deprecated, " + "use `problem.startpoint_method` instead.", DeprecationWarning, + stacklevel=1, ) self._initialize() @@ -135,12 +137,12 @@ def minimize( ) refsets = [ - RefSet(evaluator=evaluator, dim=ess_init_args['dim_refset']) + RefSet(evaluator=evaluator, dim=ess_init_args["dim_refset"]) for ess_init_args in self.ess_init_args ] for refset, ess_init_args in zip(refsets, self.ess_init_args): refset.initialize_random( - n_diverse=ess_init_args.get('n_diverse', 10 * problem.dim) + n_diverse=ess_init_args.get("n_diverse", 10 * problem.dim) ) while True: @@ -207,13 +209,13 @@ def _create_result( Currently, this returns the overall best value and the final RefSet. """ common_result_fields = { - 'exitflag': self.exit_flag, + "exitflag": self.exit_flag, # meaningful? this is the overall time, and identical for all # reported points - 'time': time.time() - self.starttime, + "time": time.time() - self.starttime, # TODO # 'n_fval': self.evaluator.n_eval, - 'optimizer': str(self), + "optimizer": str(self), } i_result = 0 result = pypesto.Result(problem=problem) @@ -262,17 +264,13 @@ def _run_scatter_searches( # 2.5 < tau < 3.5, default: 2.5 ess_init_args = [ dict( - { - 'max_eval': int( - 10**2.5 * refsets[0].evaluator.problem.dim - ) - }, + {"max_eval": int(10**2.5 * refsets[0].evaluator.problem.dim)}, **ess_kwargs, ) for ess_kwargs in self.ess_init_args ] - ctx = multiprocessing.get_context('spawn') + ctx = multiprocessing.get_context("spawn") with ctx.Pool(len(self.ess_init_args)) as pool: ess_optimizers = pool.starmap( self._run_single_ess, @@ -349,7 +347,7 @@ def _update_refsets( for i, ess_init_args in enumerate(self.ess_init_args): refsets[i] = RefSet( - dim=ess_init_args['dim_refset'], evaluator=evaluator + dim=ess_init_args["dim_refset"], evaluator=evaluator ) refsets[i].initialize_from_array(x_diverse=x, fx_diverse=fx) refsets[i].sort() diff --git a/pypesto/optimize/ess/ess.py b/pypesto/optimize/ess/ess.py index 347d5517b..7379160aa 100644 --- a/pypesto/optimize/ess/ess.py +++ b/pypesto/optimize/ess/ess.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -__all__ = ['ESSOptimizer', 'ESSExitFlag'] +__all__ = ["ESSOptimizer", "ESSExitFlag"] class ESSExitFlag(int, enum.Enum): @@ -192,8 +192,10 @@ def _initialize_minimize( """ if startpoint_method is not None: warn( - "Passing `startpoint_method` directly is deprecated, use `problem.startpoint_method` instead.", + "Passing `startpoint_method` directly is deprecated, " + "use `problem.startpoint_method` instead.", DeprecationWarning, + stacklevel=1, ) self._initialize() @@ -301,12 +303,12 @@ def _create_result(self) -> pypesto.Result: Currently, this returns the overall best value and the final RefSet. """ common_result_fields = { - 'exitflag': self.exit_flag, + "exitflag": self.exit_flag, # meaningful? this is the overall time, and identical for all # reported points - 'time': time.time() - self.starttime, - 'n_fval': self.evaluator.n_eval, - 'optimizer': str(self), + "time": time.time() - self.starttime, + "n_fval": self.evaluator.n_eval, + "optimizer": str(self), } i_result = 0 result = pypesto.Result(problem=self.evaluator.problem) diff --git a/pypesto/optimize/ess/function_evaluator.py b/pypesto/optimize/ess/function_evaluator.py index e53646f46..aa8b67f6b 100644 --- a/pypesto/optimize/ess/function_evaluator.py +++ b/pypesto/optimize/ess/function_evaluator.py @@ -46,8 +46,10 @@ def __init__( """ if startpoint_method is not None: warn( - "Passing `startpoint_method` directly is deprecated, use `problem.startpoint_method` instead.", + "Passing `startpoint_method` directly is deprecated, " + "use `problem.startpoint_method` instead.", DeprecationWarning, + stacklevel=1, ) self.problem: Problem = problem @@ -168,7 +170,7 @@ def __getstate__(self): return { k: v for k, v in vars(self).items() - if k not in {'_thread_local', '_executor'} + if k not in {"_thread_local", "_executor"} } def __setstate__(self, state): diff --git a/pypesto/optimize/ess/refset.py b/pypesto/optimize/ess/refset.py index 3eac611e2..b5fec2a1e 100644 --- a/pypesto/optimize/ess/refset.py +++ b/pypesto/optimize/ess/refset.py @@ -136,7 +136,7 @@ def prune_too_close(self): for j in range(i + 1, self.dim): # check proximity # zero-division may occur here - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): while ( np.max(np.abs((x[i] - x[j]) / x[j])) <= self.proximity_threshold diff --git a/pypesto/optimize/ess/sacess.py b/pypesto/optimize/ess/sacess.py index 1b006a3e0..c41dd4fa7 100644 --- a/pypesto/optimize/ess/sacess.py +++ b/pypesto/optimize/ess/sacess.py @@ -173,8 +173,10 @@ def minimize( """ if startpoint_method is not None: warn( - "Passing `startpoint_method` directly is deprecated, use `problem.startpoint_method` instead.", + "Passing `startpoint_method` directly is deprecated, " + "use `problem.startpoint_method` instead.", DeprecationWarning, + stacklevel=1, ) start_time = time.time() @@ -189,7 +191,7 @@ def minimize( logging_handler = logging.StreamHandler() logging_handler.setFormatter( logging.Formatter( - '%(asctime)s %(name)s %(levelname)-8s %(message)s' + "%(asctime)s %(name)s %(levelname)-8s %(message)s" ) ) logging_thread = logging.handlers.QueueListener( @@ -529,17 +531,17 @@ def run( evaluator = create_function_evaluator( problem, startpoint_method, - n_procs=self._ess_kwargs.get('n_procs'), - n_threads=self._ess_kwargs.get('n_threads'), + n_procs=self._ess_kwargs.get("n_procs"), + n_threads=self._ess_kwargs.get("n_threads"), ) # create initial refset self._refset = RefSet( - dim=self._ess_kwargs['dim_refset'], evaluator=evaluator + dim=self._ess_kwargs["dim_refset"], evaluator=evaluator ) self._refset.initialize_random( n_diverse=max( - self._ess_kwargs.get('n_diverse', 10 * problem.dim), + self._ess_kwargs.get("n_diverse", 10 * problem.dim), self._refset.dim, ) ) @@ -581,8 +583,8 @@ def _setup_ess(self, startpoint_method: StartpointMethod) -> ESSOptimizer: """Run ESS.""" ess_kwargs = self._ess_kwargs.copy() # account for sacess walltime limit - ess_kwargs['max_walltime_s'] = min( - ess_kwargs.get('max_walltime_s', np.inf), + ess_kwargs["max_walltime_s"] = min( + ess_kwargs.get("max_walltime_s", np.inf), self._max_walltime_s - (time.time() - self._start_time), ) @@ -635,7 +637,7 @@ def _maybe_adapt(self, problem: Problem): self._ess_kwargs = self._manager.reconfigure_worker( self._worker_idx ) - self._refset.resize(self._ess_kwargs['dim_refset']) + self._refset.resize(self._ess_kwargs["dim_refset"]) self._logger.debug( f"Updated settings on worker {self._worker_idx} to " f"{self._ess_kwargs}" @@ -789,161 +791,161 @@ def dim_refset(x): settings = [ # settings for first worker { - 'dim_refset': dim_refset(10), - 'balance': 0.5, - 'local_n2': 10, + "dim_refset": dim_refset(10), + "balance": 0.5, + "local_n2": 10, }, # for the remaining workers, cycle through these settings # 1 { - 'dim_refset': dim_refset(1), - 'balance': 0.0, - 'local_n1': 1, - 'local_n2': 1, + "dim_refset": dim_refset(1), + "balance": 0.0, + "local_n1": 1, + "local_n2": 1, }, # 2 { - 'dim_refset': dim_refset(3), - 'balance': 0.0, - 'local_n1': 1000, - 'local_n2': 1000, + "dim_refset": dim_refset(3), + "balance": 0.0, + "local_n1": 1000, + "local_n2": 1000, }, # 3 { - 'dim_refset': dim_refset(5), - 'balance': 0.25, - 'local_n1': 10, - 'local_n2': 10, + "dim_refset": dim_refset(5), + "balance": 0.25, + "local_n1": 10, + "local_n2": 10, }, # 4 { - 'dim_refset': dim_refset(10), - 'balance': 0.5, - 'local_n1': 20, - 'local_n2': 20, + "dim_refset": dim_refset(10), + "balance": 0.5, + "local_n1": 20, + "local_n2": 20, }, # 5 { - 'dim_refset': dim_refset(15), - 'balance': 0.25, - 'local_n1': 100, - 'local_n2': 100, + "dim_refset": dim_refset(15), + "balance": 0.25, + "local_n1": 100, + "local_n2": 100, }, # 6 { - 'dim_refset': dim_refset(12), - 'balance': 0.25, - 'local_n1': 1000, - 'local_n2': 1000, + "dim_refset": dim_refset(12), + "balance": 0.25, + "local_n1": 1000, + "local_n2": 1000, }, # 7 { - 'dim_refset': dim_refset(7.5), - 'balance': 0.25, - 'local_n1': 15, - 'local_n2': 15, + "dim_refset": dim_refset(7.5), + "balance": 0.25, + "local_n1": 15, + "local_n2": 15, }, # 8 { - 'dim_refset': dim_refset(5), - 'balance': 0.25, - 'local_n1': 7, - 'local_n2': 7, + "dim_refset": dim_refset(5), + "balance": 0.25, + "local_n1": 7, + "local_n2": 7, }, # 9 { - 'dim_refset': dim_refset(2), - 'balance': 0.0, - 'local_n1': 1000, - 'local_n2': 1000, + "dim_refset": dim_refset(2), + "balance": 0.0, + "local_n1": 1000, + "local_n2": 1000, }, # 10 { - 'dim_refset': dim_refset(0.5), - 'balance': 0.0, - 'local_n1': 1, - 'local_n2': 1, + "dim_refset": dim_refset(0.5), + "balance": 0.0, + "local_n1": 1, + "local_n2": 1, }, # 11 { - 'dim_refset': dim_refset(1.5), - 'balance': 1.0, - 'local_n1': 1, - 'local_n2': 1, + "dim_refset": dim_refset(1.5), + "balance": 1.0, + "local_n1": 1, + "local_n2": 1, }, # 12 { - 'dim_refset': dim_refset(3.5), - 'balance': 1.0, - 'local_n1': 4, - 'local_n2': 4, + "dim_refset": dim_refset(3.5), + "balance": 1.0, + "local_n1": 4, + "local_n2": 4, }, # 13 { - 'dim_refset': dim_refset(5.5), - 'balance': 0.1, - 'local_n1': 10, - 'local_n2': 10, + "dim_refset": dim_refset(5.5), + "balance": 0.1, + "local_n1": 10, + "local_n2": 10, }, # 14 { - 'dim_refset': dim_refset(10.5), - 'balance': 0.3, - 'local_n1': 20, - 'local_n2': 20, + "dim_refset": dim_refset(10.5), + "balance": 0.3, + "local_n1": 20, + "local_n2": 20, }, # 15 { - 'dim_refset': dim_refset(15.5), - 'balance': 0.2, - 'local_n1': 1000, - 'local_n2': 1000, + "dim_refset": dim_refset(15.5), + "balance": 0.2, + "local_n1": 1000, + "local_n2": 1000, }, # 16 { - 'dim_refset': dim_refset(12.5), - 'balance': 0.2, - 'local_n1': 10, - 'local_n2': 10, + "dim_refset": dim_refset(12.5), + "balance": 0.2, + "local_n1": 10, + "local_n2": 10, }, # 17 { - 'dim_refset': dim_refset(8), - 'balance': 0.75, - 'local_n1': 15, - 'local_n2': 15, + "dim_refset": dim_refset(8), + "balance": 0.75, + "local_n1": 15, + "local_n2": 15, }, # 18 { - 'dim_refset': dim_refset(5.5), - 'balance': 0.75, - 'local_n1': 1000, - 'local_n2': 1000, + "dim_refset": dim_refset(5.5), + "balance": 0.75, + "local_n1": 1000, + "local_n2": 1000, }, # 19 { - 'dim_refset': dim_refset(2.2), - 'balance': 1.0, - 'local_n1': 2, - 'local_n2': 2, + "dim_refset": dim_refset(2.2), + "balance": 1.0, + "local_n1": 2, + "local_n2": 2, }, # 20 { - 'dim_refset': dim_refset(1), - 'balance': 1.0, - 'local_n1': 1, - 'local_n2': 1, + "dim_refset": dim_refset(1), + "balance": 1.0, + "local_n1": 1, + "local_n2": 1, }, ] # Set local optimizer for cur_settings in settings: if local_optimizer is True: - cur_settings['local_optimizer'] = SacessFidesFactory( + cur_settings["local_optimizer"] = SacessFidesFactory( fides_kwargs={"verbose": logging.WARNING} ) elif local_optimizer is not False: - cur_settings['local_optimizer'] = local_optimizer + cur_settings["local_optimizer"] = local_optimizer return [ settings[0], @@ -967,7 +969,6 @@ class SacessFidesFactory: fides_kwargs: Keyword arguments for the :class:`FidesOptimizer`. See :meth:`FidesOptimizer.__init__`. Must not include ``options``. - """ def __init__( @@ -989,7 +990,7 @@ def __init__( except ImportError: from ..optimizer import OptimizerImportError - raise OptimizerImportError("fides") + raise OptimizerImportError("fides") from None def __call__( self, max_walltime_s: int, max_eval: int diff --git a/pypesto/optimize/load.py b/pypesto/optimize/load.py index e0dc618c3..7c42e35af 100644 --- a/pypesto/optimize/load.py +++ b/pypesto/optimize/load.py @@ -193,7 +193,7 @@ def read_result_from_file( ) result = OptimizerResult( id=identifier, - message='loaded from file', + message="loaded from file", exitflag=EXITFLAG_LOADED_FROM_FILE, time=( max(opt_hist.history.get_time_trace()) @@ -272,9 +272,9 @@ def optimization_result_from_history( history. But missing "Time, Message and Exitflag" keys. """ result = Result() - with h5py.File(filename, 'r') as f: + with h5py.File(filename, "r") as f: ids = list(f[HISTORY].keys()) - x0s = [f[f'{HISTORY}/{id}/{TRACE}/0/{X}'][()] for id in ids] + x0s = [f[f"{HISTORY}/{id}/{TRACE}/0/{X}"][()] for id in ids] for id, x0 in zip(ids, x0s): history = Hdf5History(id=id, file=filename) diff --git a/pypesto/optimize/optimize.py b/pypesto/optimize/optimize.py index 462d3549a..b3d64b717 100644 --- a/pypesto/optimize/optimize.py +++ b/pypesto/optimize/optimize.py @@ -95,8 +95,10 @@ def minimize( startpoint_method = problem.startpoint_method else: warn( - "Passing `startpoint_method` directly is deprecated, use `problem.startpoint_method` instead.", + "Passing `startpoint_method` directly is deprecated, " + "use `problem.startpoint_method` instead.", DeprecationWarning, + stacklevel=1, ) # convert startpoint method to class instance diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index 6d623f950..3dd38fcbe 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -463,7 +463,8 @@ def minimize( "for each evaluation of hess, fun will be " "evaluated again. This can lead to increased " "computation times. If possible, separate fun " - "and hess." + "and hess.", + stacklevel=1, ) if objective.grad is True: @@ -576,7 +577,7 @@ def minimize( try: import cyipopt except ImportError: - raise OptimizerImportError("ipopt") + raise OptimizerImportError("ipopt") from None objective = problem.objective @@ -651,7 +652,7 @@ def minimize( try: import dlib except ImportError: - raise OptimizerImportError("dlib") + raise OptimizerImportError("dlib") from None if not objective.has_fun: raise ValueError( @@ -723,7 +724,7 @@ def minimize( try: import pyswarm except ImportError: - raise OptimizerImportError("pyswarm") + raise OptimizerImportError("pyswarm") from None check_finite_bounds(lb, ub) @@ -802,7 +803,7 @@ def minimize( try: import cma except ImportError: - raise OptimizerImportError("cma") + raise OptimizerImportError("cma") from None result = ( cma.CMAEvolutionStrategy( @@ -952,7 +953,7 @@ def minimize( try: import pyswarms except ImportError: - raise OptimizerImportError("pyswarms") + raise OptimizerImportError("pyswarms") from None # check for finite values for the bounds if np.isfinite(lb).all() is np.False_: @@ -1060,7 +1061,7 @@ def __init__( try: import nlopt except ImportError: - raise OptimizerImportError("nlopt") + raise OptimizerImportError("nlopt") from None if method is None: method = nlopt.LD_LBFGS @@ -1296,7 +1297,7 @@ def __init__( try: import fides except ImportError: - raise OptimizerImportError("fides") + raise OptimizerImportError("fides") from None if ( (hessian_update is not None) @@ -1354,7 +1355,8 @@ def minimize( "Fides is using BFGS as hessian approximation, " "as the problem does not provide a Hessian. " "Specify a Hessian to use a more efficient " - "hybrid approximation scheme." + "hybrid approximation scheme.", + stacklevel=1, ) _hessian_update = fides.BFGS() else: diff --git a/pypesto/optimize/options.py b/pypesto/optimize/options.py index 69acd7bce..dfa148260 100644 --- a/pypesto/optimize/options.py +++ b/pypesto/optimize/options.py @@ -42,15 +42,15 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @staticmethod def assert_instance( - maybe_options: Union['OptimizeOptions', Dict], - ) -> 'OptimizeOptions': + maybe_options: Union["OptimizeOptions", Dict], + ) -> "OptimizeOptions": """ Return a valid options object. diff --git a/pypesto/optimize/task.py b/pypesto/optimize/task.py index f28c26412..10ae83dd8 100644 --- a/pypesto/optimize/task.py +++ b/pypesto/optimize/task.py @@ -17,12 +17,12 @@ class OptimizerTask(Task): def __init__( self, - optimizer: 'pypesto.optimize.Optimizer', + optimizer: "pypesto.optimize.Optimizer", problem: Problem, x0: np.ndarray, id: str, history_options: HistoryOptions, - optimize_options: 'pypesto.optimize.OptimizeOptions', + optimize_options: "pypesto.optimize.OptimizeOptions", ): """Create the task object. diff --git a/pypesto/optimize/util.py b/pypesto/optimize/util.py index 4e2e86383..65a219ec6 100644 --- a/pypesto/optimize/util.py +++ b/pypesto/optimize/util.py @@ -101,14 +101,14 @@ def postprocess_hdf5_history( # create hdf5 file that gathers the others within history group if "{id}" in storage_file: storage_file = storage_file.replace("{id}", "") - with h5py.File(storage_file, mode='w') as f: + with h5py.File(storage_file, mode="w") as f: # create file and group f.require_group("history") # append links to each single result file for result in ret: - id = result['id'] - f[f'history/{id}'] = h5py.ExternalLink( - result['history'].file, f'history/{id}' + id = result["id"] + f[f"history/{id}"] = h5py.ExternalLink( + result["history"].file, f"history/{id}" ) # reset storage file (undo preprocessing changes) @@ -185,6 +185,6 @@ def check_finite_bounds(lb, ub): """Raise if bounds are not finite.""" if not np.isfinite(lb).all() or not np.isfinite(ub).all(): raise ValueError( - 'Selected optimizer cannot work with unconstrained ' - 'optimization problems.' + "Selected optimizer cannot work with unconstrained " + "optimization problems." ) diff --git a/pypesto/petab/__init__.py b/pypesto/petab/__init__.py index 8fe8fb736..526997aa2 100644 --- a/pypesto/petab/__init__.py +++ b/pypesto/petab/__init__.py @@ -18,7 +18,8 @@ warnings.warn( "PEtab import requires an installation of petab " "(https://github.com/PEtab-dev/PEtab). " - "Install via `pip3 install petab`." + "Install via `pip3 install petab`.", + stacklevel=1, ) try: import amici @@ -26,5 +27,6 @@ warnings.warn( "PEtab import requires an installation of amici " "(https://github.com/AMICI-dev/AMICI). " - "Install via `pip3 install amici`." + "Install via `pip3 install amici`.", + stacklevel=1, ) diff --git a/pypesto/petab/importer.py b/pypesto/petab/importer.py index 4801d1214..7eeb954b5 100644 --- a/pypesto/petab/importer.py +++ b/pypesto/petab/importer.py @@ -502,7 +502,7 @@ def create_objective( self._non_quantitative_data_types is not None and self._hierarchical ): - inner_options = kwargs.pop('inner_options', None) + inner_options = kwargs.pop("inner_options", None) inner_options = ( inner_options if inner_options is not None @@ -518,15 +518,17 @@ def create_objective( amici_reporting = amici.RDataReporting.full # FIXME: currently not supported with hierarchical - if 'guess_steadystate' in kwargs and kwargs['guess_steadystate']: + if "guess_steadystate" in kwargs and kwargs["guess_steadystate"]: warnings.warn( - "`guess_steadystate` not supported with hierarchical optimization. Disabling `guess_steadystate`." + "`guess_steadystate` not supported with hierarchical " + "optimization. Disabling `guess_steadystate`.", + stacklevel=1, ) - kwargs['guess_steadystate'] = False + kwargs["guess_steadystate"] = False inner_parameter_ids = calculator.get_inner_par_ids() par_ids = [x for x in par_ids if x not in inner_parameter_ids] - max_sensi_order = kwargs.get('max_sensi_order', None) + max_sensi_order = kwargs.get("max_sensi_order", None) if ( self._non_quantitative_data_types is not None @@ -622,11 +624,9 @@ def create_predictor( # create a identifiers of preequilibration and simulation condition ids # which can then be stored in the prediction result - edata_conditions = ( - objective.amici_object_builder.petab_problem.get_simulation_conditions_from_measurement_df() - ) + edata_conditions = objective.amici_object_builder.petab_problem.get_simulation_conditions_from_measurement_df() if PREEQUILIBRATION_CONDITION_ID not in list(edata_conditions.columns): - preeq_dummy = [''] * edata_conditions.shape[0] + preeq_dummy = [""] * edata_conditions.shape[0] edata_conditions[PREEQUILIBRATION_CONDITION_ID] = preeq_dummy edata_conditions.drop_duplicates(inplace=True) @@ -674,7 +674,7 @@ def create_prior(self) -> Union[NegLogParameterPriors, None]: float(param) for param in self.petab_problem.parameter_df.loc[ x_id, petab.OBJECTIVE_PRIOR_PARAMETERS - ].split(';') + ].split(";") ] scale = self.petab_problem.parameter_df.loc[ diff --git a/pypesto/predict/amici_predictor.py b/pypesto/predict/amici_predictor.py index 7b17d36a0..56842f3a6 100644 --- a/pypesto/predict/amici_predictor.py +++ b/pypesto/predict/amici_predictor.py @@ -156,7 +156,7 @@ def __call__( x: np.ndarray, sensi_orders: tuple[int, ...] = (0,), mode: ModeType = MODE_FUN, - output_file: str = '', + output_file: str = "", output_format: str = CSV, include_llh_weights: bool = False, include_sigmay: bool = False, @@ -199,8 +199,8 @@ def __call__( # sanity check for output if 2 in sensi_orders: raise Exception( - 'Prediction simulation does currently not support ' - 'second order output.' + "Prediction simulation does currently not support " + "second order output." ) # add llh and sigmay to amici output fields if requested if include_llh_weights and AMICI_LLH not in self.amici_output_fields: @@ -257,8 +257,8 @@ def __call__( results.write_to_h5(output_file=output_file) else: raise ValueError( - f'Call to unknown format {output_format} for ' - f'output of pyPESTO prediction.' + f"Call to unknown format {output_format} for " + f"output of pyPESTO prediction." ) # return dependent on sensitivity order @@ -345,7 +345,7 @@ def _get_outputs( ) def _default_output( - amici_outputs: list[dict[str, np.array]] + amici_outputs: list[dict[str, np.array]], ) -> tuple[ list[np.array], list[np.array], diff --git a/pypesto/problem/base.py b/pypesto/problem/base.py index ab802eee8..20b95acce 100644 --- a/pypesto/problem/base.py +++ b/pypesto/problem/base.py @@ -123,9 +123,9 @@ def __init__( if x_fixed_indices is None: x_fixed_indices = [] - x_fixed_indices = _make_iterable_if_value(x_fixed_indices, 'int') + x_fixed_indices = _make_iterable_if_value(x_fixed_indices, "int") self.x_fixed_indices: List[int] = [ - _type_conversion_with_check(idx, ix, 'fixed indices', 'int') + _type_conversion_with_check(idx, ix, "fixed indices", "int") for idx, ix in enumerate(x_fixed_indices) ] @@ -133,9 +133,9 @@ def __init__( # or remove values during profile computation if x_fixed_vals is None: x_fixed_vals = [] - x_fixed_vals = _make_iterable_if_value(x_fixed_vals, 'float') + x_fixed_vals = _make_iterable_if_value(x_fixed_vals, "float") self.x_fixed_vals: List[float] = [ - _type_conversion_with_check(idx, x, 'fixed values', 'float') + _type_conversion_with_check(idx, x, "fixed values", "float") for idx, x in enumerate(x_fixed_vals) ] @@ -146,13 +146,13 @@ def __init__( if x_names is None and objective.x_names is not None: x_names = objective.x_names elif x_names is None: - x_names = [f'x{j}' for j in range(0, self.dim_full)] + x_names = [f"x{j}" for j in range(0, self.dim_full)] if len(set(x_names)) != len(x_names): raise ValueError("Parameter names x_names must be unique") self.x_names: List[str] = list(x_names) if x_scales is None: - x_scales = ['lin'] * self.dim_full + x_scales = ["lin"] * self.dim_full self.x_scales = x_scales self.x_priors = x_priors_defs @@ -208,7 +208,7 @@ def normalize(self) -> None: Reduce all vectors to dimension dim and have the objective accept vectors of dimension dim. """ - for attr in ['lb_full', 'lb_init_full', 'ub_full', 'ub_init_full']: + for attr in ["lb_full", "lb_init_full", "ub_full", "ub_init_full"]: value = self.__getattribute__(attr) if value.size == 1: self.__setattr__(attr, value * np.ones(self.dim_full)) @@ -248,11 +248,11 @@ def normalize(self) -> None: "x_fixed_indices and x_fixed_vals must have the same length." ) if np.isnan(self.lb).any(): - raise ValueError('lb must not contain nan values') + raise ValueError("lb must not contain nan values") if np.isnan(self.ub).any(): - raise ValueError('ub must not contain nan values') + raise ValueError("ub must not contain nan values") if np.any(self.lb >= self.ub): - raise ValueError('lb None: """Fix specified parameters to specified values.""" - parameter_indices = _make_iterable_if_value(parameter_indices, 'int') - parameter_vals = _make_iterable_if_value(parameter_vals, 'float') + parameter_indices = _make_iterable_if_value(parameter_indices, "int") + parameter_vals = _make_iterable_if_value(parameter_vals, "float") # first clean to-be-fixed indices to avoid redundancies for iter_index, (x_index, x_value) in enumerate( @@ -300,10 +300,10 @@ def fix_parameters( # check if parameter was already fixed, otherwise add it to the # fixed parameters index = _type_conversion_with_check( - iter_index, x_index, 'indices', 'int' + iter_index, x_index, "indices", "int" ) val = _type_conversion_with_check( - iter_index, x_value, 'values', 'float' + iter_index, x_value, "values", "float" ) if index in self.x_fixed_indices: self.x_fixed_vals[self.x_fixed_indices.index(index)] = val @@ -318,12 +318,12 @@ def unfix_parameters( ) -> None: """Free specified parameters.""" # check and adapt input - parameter_indices = _make_iterable_if_value(parameter_indices, 'int') + parameter_indices = _make_iterable_if_value(parameter_indices, "int") # first clean to-be-freed indices for iter_index, x_index in enumerate(parameter_indices): index = _type_conversion_with_check( - iter_index, x_index, 'indices', 'int' + iter_index, x_index, "indices", "int" ) if index in self.x_fixed_indices: fixed_x_index = self.x_fixed_indices.index(index) @@ -470,20 +470,20 @@ def print_parameter_summary(self) -> None: pd.DataFrame( index=self.x_names, data={ - 'free': [ + "free": [ idx in self.x_free_indices for idx in range(self.dim_full) ], - 'lb_full': self.lb_full, - 'ub_full': self.ub_full, + "lb_full": self.lb_full, + "ub_full": self.ub_full, }, ) ) _convtypes = { - 'float': {'attr': '__float__', 'conv': float}, - 'int': {'attr': '__int__', 'conv': int}, + "float": {"attr": "__float__", "conv": float}, + "int": {"attr": "__int__", "conv": int}, } @@ -499,24 +499,24 @@ def _type_conversion_with_check( Raises and appropriate error if not possible. """ if convtype not in _convtypes: - raise ValueError(f'Unsupported type {convtype}') + raise ValueError(f"Unsupported type {convtype}") - can_convert = hasattr(value, _convtypes[convtype]['attr']) + can_convert = hasattr(value, _convtypes[convtype]["attr"]) # this may fail for weird custom ypes that can be converted to int but # not float, but we probably don't want those as indiced anyways - lossless_conversion = not convtype == 'int' or ( - hasattr(value, _convtypes['float']['attr']) + lossless_conversion = not convtype == "int" or ( + hasattr(value, _convtypes["float"]["attr"]) and (float(value) - int(value) == 0.0) ) if not can_convert or not lossless_conversion: raise ValueError( - f'All {valuename} must support lossless conversion to {convtype}. ' - f'Found type {type(value)} at index {index}, which cannot ' - f'be converted to {convtype}.' + f"All {valuename} must support lossless conversion to {convtype}. " + f"Found type {type(value)} at index {index}, which cannot " + f"be converted to {convtype}." ) - return _convtypes[convtype]['conv'](value) + return _convtypes[convtype]["conv"](value) def _make_iterable_if_value( @@ -525,9 +525,9 @@ def _make_iterable_if_value( ) -> Union[Iterable[SupportsFloat], Iterable[SupportsInt]]: """Convert scalar values to iterables for scalar input, may update type.""" if convtype not in _convtypes: - raise ValueError(f'Unsupported type {convtype}') + raise ValueError(f"Unsupported type {convtype}") - if not hasattr(value, '__iter__'): - return [_type_conversion_with_check(0, value, 'values', convtype)] + if not hasattr(value, "__iter__"): + return [_type_conversion_with_check(0, value, "values", convtype)] else: return value diff --git a/pypesto/profile/options.py b/pypesto/profile/options.py index f3784db7d..3d3c69862 100644 --- a/pypesto/profile/options.py +++ b/pypesto/profile/options.py @@ -73,15 +73,15 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @staticmethod def create_instance( - maybe_options: Union['ProfileOptions', Dict] - ) -> 'ProfileOptions': + maybe_options: Union["ProfileOptions", Dict], + ) -> "ProfileOptions": """ Return a valid options object. diff --git a/pypesto/profile/profile.py b/pypesto/profile/profile.py index ad2af89e6..1518b7913 100644 --- a/pypesto/profile/profile.py +++ b/pypesto/profile/profile.py @@ -23,7 +23,7 @@ def parameter_profile( profile_index: Iterable[int] = None, profile_list: int = None, result_index: int = 0, - next_guess_method: Union[Callable, str] = 'adaptive_step_regression', + next_guess_method: Union[Callable, str] = "adaptive_step_regression", profile_options: ProfileOptions = None, progress_bar: bool = None, filename: Union[str, Callable, None] = None, @@ -117,11 +117,11 @@ def create_next_guess( elif callable(next_guess_method): raise NotImplementedError( - 'Passing function handles for computation of next ' - 'profiling point is not yet supported.' + "Passing function handles for computation of next " + "profiling point is not yet supported." ) else: - raise ValueError('Unsupported input for next_guess_method.') + raise ValueError("Unsupported input for next_guess_method.") # create the profile result object (retrieve global optimum) or append to # existing list of profiles @@ -166,8 +166,8 @@ def create_next_guess( # fill in the ProfilerResults at the right index for indexed_profile in indexed_profiles: result.profile_result.list[-1][ - indexed_profile['index'] - ] = indexed_profile['profile'] + indexed_profile["index"] + ] = indexed_profile["profile"] autosave( filename=filename, diff --git a/pypesto/profile/profile_next_guess.py b/pypesto/profile/profile_next_guess.py index 0af3df4fc..fd523e062 100644 --- a/pypesto/profile/profile_next_guess.py +++ b/pypesto/profile/profile_next_guess.py @@ -6,7 +6,7 @@ from ..result import ProfilerResult from .options import ProfileOptions -__all__ = ['next_guess', 'fixed_step', 'adaptive_step'] +__all__ = ["next_guess", "fixed_step", "adaptive_step"] def next_guess( @@ -15,10 +15,10 @@ def next_guess( par_direction: Literal[1, -1], profile_options: ProfileOptions, update_type: Literal[ - 'fixed_step', - 'adaptive_step_order_0', - 'adaptive_step_order_1', - 'adaptive_step_regression', + "fixed_step", + "adaptive_step_order_0", + "adaptive_step_order_1", + "adaptive_step_regression", ], current_profile: ProfilerResult, problem: Problem, @@ -58,20 +58,20 @@ def next_guess( ------- The next initial guess as base for the next profile point. """ - if update_type == 'fixed_step': + if update_type == "fixed_step": return fixed_step( x, par_index, par_direction, profile_options, problem ) - if update_type == 'adaptive_step_order_0': + if update_type == "adaptive_step_order_0": order = 0 - elif update_type == 'adaptive_step_order_1': + elif update_type == "adaptive_step_order_1": order = 1 - elif update_type == 'adaptive_step_regression': + elif update_type == "adaptive_step_regression": order = np.nan else: raise ValueError( - f'Unsupported `update_type` {update_type} for `next_guess`.' + f"Unsupported `update_type` {update_type} for `next_guess`." ) return adaptive_step( @@ -437,7 +437,7 @@ def do_line_search( """ # Was the initial step too big or too small? direction = "decrease" if next_obj_target < next_obj else "increase" - if direction == 'increase': + if direction == "increase": adapt_factor = options.step_size_factor else: adapt_factor = 1 / options.step_size_factor @@ -451,12 +451,12 @@ def do_line_search( # Check if we hit the bounds if ( - direction == 'decrease' + direction == "decrease" and step_size_guess == options.min_step_size ): return next_x if ( - direction == 'increase' + direction == "increase" and step_size_guess == options.max_step_size ): return next_x @@ -467,8 +467,8 @@ def do_line_search( next_obj = problem.objective(problem.get_reduced_vector(next_x)) # check for root crossing and compute correct step size in case - if (direction == 'decrease' and next_obj_target >= next_obj) or ( - direction == 'increase' and next_obj_target <= next_obj + if (direction == "decrease" and next_obj_target >= next_obj) or ( + direction == "increase" and next_obj_target <= next_obj ): return next_x_interpolate( next_obj, last_obj, next_x, last_x, next_obj_target diff --git a/pypesto/profile/task.py b/pypesto/profile/task.py index 0cdbfaddc..ac5e82d31 100644 --- a/pypesto/profile/task.py +++ b/pypesto/profile/task.py @@ -22,7 +22,7 @@ def __init__( options: ProfileOptions, i_par: int, global_opt: float, - optimizer: 'pypesto.optimize.Optimizer', + optimizer: "pypesto.optimize.Optimizer", create_next_guess: Callable, ): """ @@ -76,4 +76,4 @@ def execute(self) -> dict[str, Any]: ) # return the ProfilerResult and the index of the parameter profiled - return {'profile': self.current_profile, 'index': self.i_par} + return {"profile": self.current_profile, "index": self.i_par} diff --git a/pypesto/profile/walk_along_profile.py b/pypesto/profile/walk_along_profile.py index 202019ea7..c4f610001 100644 --- a/pypesto/profile/walk_along_profile.py +++ b/pypesto/profile/walk_along_profile.py @@ -127,7 +127,7 @@ def walk_along_profile( # if too many parameters are fixed, there is nothing to do ... fval = problem.objective(np.array([])) optimizer_result = OptimizerResult( - id='0', + id="0", x=np.array([]), fval=fval, n_fval=0, diff --git a/pypesto/result/optimize.py b/pypesto/result/optimize.py index 8d2742ad9..e6705c961 100644 --- a/pypesto/result/optimize.py +++ b/pypesto/result/optimize.py @@ -13,7 +13,7 @@ from ..problem import Problem from ..util import assign_clusters, delete_nan_inf -OptimizationResult = Union['OptimizerResult', 'OptimizeResult'] +OptimizationResult = Union["OptimizerResult", "OptimizeResult"] logger = logging.getLogger(__name__) @@ -122,7 +122,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @@ -212,7 +212,7 @@ def __getattr__(self, key): try: return [res[key] for res in self.list] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None def __getitem__(self, index): """Define `optimize_result[i]` to access the i-th result.""" @@ -222,7 +222,7 @@ def __getitem__(self, index): raise IndexError( f"{index} out of range for optimize result of " f"length {len(self.list)}." - ) + ) from None def __getstate__(self): # while we override __getattr__ as we do now, this is required to keep @@ -274,11 +274,11 @@ def summary( counter_message = " " + counter_message.replace("\n", "\n ") times_message = ( - f'\t* Mean execution time: {np.mean(self.time):0.3f}s\n' - f'\t* Maximum execution time: {np.max(self.time):0.3f}s,' - f'\tid={self[np.argmax(self.time)].id}\n' - f'\t* Minimum execution time: {np.min(self.time):0.3f}s,\t' - f'id={self[np.argmin(self.time)].id}' + f"\t* Mean execution time: {np.mean(self.time):0.3f}s\n" + f"\t* Maximum execution time: {np.max(self.time):0.3f}s," + f"\tid={self[np.argmax(self.time)].id}\n" + f"\t* Minimum execution time: {np.min(self.time):0.3f}s,\t" + f"id={self[np.argmin(self.time)].id}" ) # special handling in case there are only non-finite fvals @@ -315,7 +315,7 @@ def append( self, optimize_result: OptimizationResult, sort: bool = True, - prefix: str = '', + prefix: str = "", ): """ Append an OptimizerResult or an OptimizeResult to the result object. @@ -410,7 +410,9 @@ def get_for_key(self, key) -> list: warnings.warn( "get_for_key() is deprecated in favour of " "optimize_result.key and will be removed in future " - "releases." + "releases.", + DeprecationWarning, + stacklevel=1, ) return [res[key] for res in self.list] diff --git a/pypesto/result/predict.py b/pypesto/result/predict.py index 8c856657e..d77056792 100644 --- a/pypesto/result/predict.py +++ b/pypesto/result/predict.py @@ -72,18 +72,18 @@ def __init__( self.x_names = x_names if x_names is None and output_sensi is not None: self.x_names = [ - f'parameter_{i_par}' for i_par in range(output_sensi.shape[1]) + f"parameter_{i_par}" for i_par in range(output_sensi.shape[1]) ] def __iter__(self): """Allow usage like a dict.""" - yield 'timepoints', self.timepoints - yield 'output_ids', self.output_ids - yield 'x_names', self.x_names - yield 'output', self.output - yield 'output_sensi', self.output_sensi - yield 'output_weight', self.output_weight - yield 'output_sigmay', self.output_sigmay + yield "timepoints", self.timepoints + yield "output_ids", self.output_ids + yield "x_names", self.x_names + yield "output", self.output + yield "output_sensi", self.output_sensi + yield "output_weight", self.output_weight + yield "output_sigmay", self.output_sigmay def __eq__(self, other): """Check equality of two PredictionConditionResults.""" @@ -166,10 +166,10 @@ def __iter__(self): if self.conditions: parameter_ids = self.conditions[0].x_names - yield 'conditions', [dict(cond) for cond in self.conditions] - yield 'condition_ids', self.condition_ids - yield 'comment', self.comment - yield 'parameter_ids', parameter_ids + yield "conditions", [dict(cond) for cond in self.conditions] + yield "condition_ids", self.condition_ids + yield "comment", self.comment + yield "parameter_ids", parameter_ids def __eq__(self, other): """Check equality of two PredictionResults.""" @@ -206,8 +206,8 @@ def _prepare_csv_output(output_file): makes sense. Returns a pathlib.Path object of the output. """ # allow entering with names with and without file type endings - if '.' in output_file: - output_path, output_suffix = output_file.split('.') + if "." in output_file: + output_path, output_suffix = output_file.split(".") else: output_path = output_file output_suffix = CSV @@ -220,7 +220,7 @@ def _prepare_csv_output(output_file): output_path.mkdir(parents=True, exist_ok=False) # add the suffix output_dummy = Path(output_path.stem).with_suffix( - f'.{output_suffix}' + f".{output_suffix}" ) return output_path, output_dummy @@ -235,13 +235,13 @@ def _prepare_csv_output(output_file): if cond.output is not None: # create filename for this condition filename = output_path.joinpath( - output_dummy.stem + f'_{i_cond}' + output_dummy.suffix + output_dummy.stem + f"_{i_cond}" + output_dummy.suffix ) # create DataFrame and write to file result = pd.DataFrame( index=timepoints, columns=cond.output_ids, data=cond.output ) - result.to_csv(filename, sep='\t') + result.to_csv(filename, sep="\t") # handle output sensitivities, if computed if cond.output_sensi is not None: @@ -250,7 +250,7 @@ def _prepare_csv_output(output_file): # create filename for this condition and parameter filename = output_path.joinpath( output_dummy.stem - + f'_{i_cond}__s{i_par}' + + f"_{i_cond}__s{i_par}" + output_dummy.suffix ) # create DataFrame and write to file @@ -259,7 +259,7 @@ def _prepare_csv_output(output_file): columns=cond.output_ids, data=cond.output_sensi[:, i_par, :], ) - result.to_csv(filename, sep='\t') + result.to_csv(filename, sep="\t") def write_to_h5(self, output_file: str, base_path: str = None): """ @@ -276,11 +276,11 @@ def write_to_h5(self, output_file: str, base_path: str = None): """ # check if the file exists and append to it in case it does output_path = Path(output_file) - filemode = 'w' + filemode = "w" if os.path.exists(output_path): - filemode = 'r+' + filemode = "r+" - base = Path('.') + base = Path(".") if base_path is not None: base = Path(base_path) @@ -339,11 +339,12 @@ def _check_existence(output_path): output_path_out = output_path while output_path_out.exists(): output_path_out = output_path_out.with_name( - output_path_out.stem + f'_{round(time() * 1000)}' + output_path_out.stem + f"_{round(time() * 1000)}" ) warn( - 'Output name already existed! Changed the name of the output ' - 'by appending the unix timestampp to make it unique!' + "Output name already existed! Changed the name of the output " + "by appending the unix timestamp to make it unique!", + stacklevel=3, ) return output_path_out diff --git a/pypesto/result/profile.py b/pypesto/result/profile.py index a0beb20e4..af14e5f1a 100644 --- a/pypesto/result/profile.py +++ b/pypesto/result/profile.py @@ -110,7 +110,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ diff --git a/pypesto/result/sample.py b/pypesto/result/sample.py index 6d18a2fad..2cb144b1b 100644 --- a/pypesto/result/sample.py +++ b/pypesto/result/sample.py @@ -9,11 +9,9 @@ class McmcPtResult(dict): """ The result of a sampler run using Markov-chain Monte Carlo. - Here, `n_chain` denotes the number of chains, `n_iter` the number of - iterations (i.e., the chain length), and `n_par` the number of - parameters. + Currently result object of all supported samplers. Can be used like a dict. - Attributes + Parameters ---------- trace_x: [n_chain, n_iter, n_par] Parameters. @@ -33,6 +31,9 @@ class McmcPtResult(dict): The estimated effective sample size. message: str Textual comment on the profile result. + + Here, `n_chain` denotes the number of chains, `n_iter` the number of + iterations (i.e., the chain length), and `n_par` the number of parameters. """ def __init__( @@ -47,7 +48,6 @@ def __init__( effective_sample_size: float = None, message: str = None, ): - """Initialize the McmcPtResult.""" super().__init__() self.trace_x = trace_x @@ -105,7 +105,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ diff --git a/pypesto/sample/adaptive_metropolis.py b/pypesto/sample/adaptive_metropolis.py index 59cc08501..27c5f4c93 100644 --- a/pypesto/sample/adaptive_metropolis.py +++ b/pypesto/sample/adaptive_metropolis.py @@ -71,35 +71,35 @@ def default_options(cls): # controls adaptation degeneration velocity of the proposals # in [0, 1], with 0 -> no adaptation, i.e. classical # Metropolis-Hastings - 'decay_constant': 0.51, + "decay_constant": 0.51, # number of samples before adaptation decreases significantly. # a higher value reduces the impact of early adaptation - 'threshold_sample': 1, + "threshold_sample": 1, # regularization factor for ill-conditioned cov matrices of # the adapted proposal density. regularization might happen if the # eigenvalues of the cov matrix strongly differ in order # of magnitude. in this case, the algorithm adds a small # diag matrix to the cov matrix with elements of this factor - 'reg_factor': 1e-6, + "reg_factor": 1e-6, # initial covariance matrix. defaults to a unit matrix - 'cov0': None, + "cov0": None, # target acceptance rate - 'target_acceptance_rate': 0.234, + "target_acceptance_rate": 0.234, # show progress - 'show_progress': None, + "show_progress": None, } def initialize(self, problem: Problem, x0: np.ndarray): """Initialize the sampler.""" super().initialize(problem, x0) - if self.options['cov0'] is not None: - cov0 = self.options['cov0'] + if self.options["cov0"] is not None: + cov0 = self.options["cov0"] if isinstance(cov0, numbers.Real): cov0 = float(cov0) * np.eye(len(x0)) else: cov0 = np.eye(len(x0)) - self._cov = regularize_covariance(cov0, self.options['reg_factor']) + self._cov = regularize_covariance(cov0, self.options["reg_factor"]) self._mean_hist = self.trace_x[-1] self._cov_hist = self._cov self._cov_scale = 1.0 @@ -112,10 +112,10 @@ def _update_proposal( self, x: np.ndarray, lpost: float, log_p_acc: float, n_sample_cur: int ): # parse options - decay_constant = self.options['decay_constant'] - threshold_sample = self.options['threshold_sample'] - reg_factor = self.options['reg_factor'] - target_acceptance_rate = self.options['target_acceptance_rate'] + decay_constant = self.options["decay_constant"] + threshold_sample = self.options["threshold_sample"] + reg_factor = self.options["reg_factor"] + target_acceptance_rate = self.options["target_acceptance_rate"] # compute historical mean and covariance self._mean_hist, self._cov_hist = update_history_statistics( diff --git a/pypesto/sample/adaptive_parallel_tempering.py b/pypesto/sample/adaptive_parallel_tempering.py index 5697e187b..761d7df8a 100644 --- a/pypesto/sample/adaptive_parallel_tempering.py +++ b/pypesto/sample/adaptive_parallel_tempering.py @@ -32,10 +32,10 @@ def default_options(cls) -> Dict: """Get default options for sampler.""" options = super().default_options() # scaling factor for temperature adaptation - options['eta'] = 100 + options["eta"] = 100 # controls the adaptation degeneration velocity of the temperature # adaption. - options['nu'] = 1e3 + options["nu"] = 1e3 return options @@ -45,8 +45,8 @@ def adjust_betas(self, i_sample: int, swapped: Sequence[bool]): return # parameters - nu = self.options['nu'] - eta = self.options['eta'] + nu = self.options["nu"] + eta = self.options["eta"] betas = self.betas # booleans to integer array diff --git a/pypesto/sample/diagnostics.py b/pypesto/sample/diagnostics.py index 6fbce38ae..ac593c7de 100644 --- a/pypesto/sample/diagnostics.py +++ b/pypesto/sample/diagnostics.py @@ -27,7 +27,6 @@ def geweke_test(result: Result, zscore: float = 2.0) -> int: burn_in: Iteration where the first and the last fraction of the chain do not differ significantly regarding Geweke test -> Burn-In - """ # Get parameter samples as numpy arrays chain = np.asarray(result.sample_result.trace_x[0]) @@ -36,7 +35,7 @@ def geweke_test(result: Result, zscore: float = 2.0) -> int: burn_in = burn_in_by_sequential_geweke(chain=chain, zscore=zscore) # Log - logger.info(f'Geweke burn-in index: {burn_in}') + logger.info(f"Geweke burn-in index: {burn_in}") # Fill in burn-in value into result result.sample_result.burn_in = burn_in @@ -89,7 +88,7 @@ def auto_correlation(result: Result) -> float: _auto_correlation = max(auto_correlation_vector) # Log - logger.info(f'Estimated chain autocorrelation: {_auto_correlation}') + logger.info(f"Estimated chain autocorrelation: {_auto_correlation}") # Fill in autocorrelation value into result result.sample_result.auto_correlation = _auto_correlation @@ -136,7 +135,7 @@ def effective_sample_size(result: Result) -> float: ess = N / (1 + _auto_correlation) # Log - logger.info(f'Estimated effective sample size: {ess}') + logger.info(f"Estimated effective sample size: {ess}") # Fill in effective sample size value into result result.sample_result.effective_sample_size = ess diff --git a/pypesto/sample/dynesty.py b/pypesto/sample/dynesty.py index 379944506..5bb5cb0dc 100644 --- a/pypesto/sample/dynesty.py +++ b/pypesto/sample/dynesty.py @@ -312,4 +312,4 @@ def setup_dynesty() -> None: dynesty.utils.pickle_module = cloudpickle except ImportError: - raise SamplerImportError("dynesty") + raise SamplerImportError("dynesty") from None diff --git a/pypesto/sample/emcee.py b/pypesto/sample/emcee.py index d671365a6..224a94d99 100644 --- a/pypesto/sample/emcee.py +++ b/pypesto/sample/emcee.py @@ -46,7 +46,7 @@ def __init__( try: import emcee except ImportError: - raise SamplerImportError("emcee") + raise SamplerImportError("emcee") from None super().__init__() self.nwalkers: int = nwalkers diff --git a/pypesto/sample/metropolis.py b/pypesto/sample/metropolis.py index b4310c529..f69bfc026 100644 --- a/pypesto/sample/metropolis.py +++ b/pypesto/sample/metropolis.py @@ -50,8 +50,8 @@ def __init__(self, options: Dict = None): def default_options(cls): """Return the default options for the sampler.""" return { - 'std': 1.0, # the proposal standard deviation - 'show_progress': None, # whether to show the progress + "std": 1.0, # the proposal standard deviation + "show_progress": None, # whether to show the progress } def initialize(self, problem: Problem, x0: np.ndarray): @@ -73,7 +73,7 @@ def sample(self, n_samples: int, beta: float = 1.0): lpost = -self.trace_neglogpost[-1] lprior = -self.trace_neglogprior[-1] - show_progress = self.options.get('show_progress', None) + show_progress = self.options.get("show_progress", None) # loop over iterations for _ in tqdm(range(int(n_samples)), enable=show_progress): @@ -98,7 +98,7 @@ def make_internal(self, temper_lpost: bool): temper_lpost: Whether to temperate the posterior or only the likelihood. """ - self.options['show_progress'] = False + self.options["show_progress"] = False self.temper_lpost = temper_lpost def _perform_step( @@ -161,7 +161,7 @@ def _perform_step( def _propose_parameter(self, x: np.ndarray): """Propose a step.""" - x_new: np.ndarray = x + self.options['std'] * np.random.randn(len(x)) + x_new: np.ndarray = x + self.options["std"] * np.random.randn(len(x)) return x_new def _update_proposal( diff --git a/pypesto/sample/parallel_tempering.py b/pypesto/sample/parallel_tempering.py index 567fd935b..df1eea580 100644 --- a/pypesto/sample/parallel_tempering.py +++ b/pypesto/sample/parallel_tempering.py @@ -46,15 +46,15 @@ def __init__( if betas is None: betas = near_exponential_decay_betas( n_chains=n_chains, - exponent=self.options['exponent'], - max_temp=self.options['max_temp'], + exponent=self.options["exponent"], + max_temp=self.options["max_temp"], ) if betas[0] != 1.0: raise ValueError("The first chain must have beta=1.0") self.betas0 = np.array(betas) self.betas = None - self.temper_lpost = self.options['temper_log_posterior'] + self.temper_lpost = self.options["temper_log_posterior"] self.samplers = [ copy.deepcopy(internal_sampler) for _ in range(len(self.betas0)) @@ -67,10 +67,10 @@ def __init__( def default_options(cls) -> Dict: """Return the default options for the sampler.""" return { - 'max_temp': 5e4, - 'exponent': 4, - 'temper_log_posterior': False, - 'show_progress': None, + "max_temp": 5e4, + "exponent": 4, + "temper_log_posterior": False, + "show_progress": None, } def initialize( @@ -89,7 +89,7 @@ def initialize( def sample(self, n_samples: int, beta: float = 1.0): """Sample and swap in between samplers.""" - show_progress = self.options.get('show_progress', None) + show_progress = self.options.get("show_progress", None) # loop over iterations for i_sample in tqdm(range(int(n_samples)), enable=show_progress): # TODO test diff --git a/pypesto/sample/pymc.py b/pypesto/sample/pymc.py index 89a2ac252..c6b37e3fa 100644 --- a/pypesto/sample/pymc.py +++ b/pypesto/sample/pymc.py @@ -18,13 +18,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - 'PymcSampler', - 'PymcObjectiveWithGradientOp', - 'PymcObjectiveOp', - 'PymcGradientOp', -] - # implementation based on: # https://www.pymc.io/projects/examples/en/latest/case_studies/blackbox_external_likelihood_numpy.html @@ -135,7 +128,7 @@ def translate_options(cls, options): Options configuring the sampler. """ if not options: - options = {'chains': 1} + options = {"chains": 1} return options def initialize(self, problem: Problem, x0: np.ndarray): @@ -171,7 +164,7 @@ def sample(self, n_samples: int, beta: float = 1.0): try: import pymc except ImportError: - raise SamplerImportError("pymc") + raise SamplerImportError("pymc") from None problem = self.problem log_post = PymcObjectiveOp.create_instance(problem.objective, beta) diff --git a/pypesto/sample/sample.py b/pypesto/sample/sample.py index 05d9a0835..f1e3ad65b 100644 --- a/pypesto/sample/sample.py +++ b/pypesto/sample/sample.py @@ -72,7 +72,7 @@ def sample( result.optimize_result.sort() if len(result.optimize_result.list) > 0: x0 = problem.get_reduced_vector( - result.optimize_result.list[0]['x'] + result.optimize_result.list[0]["x"] ) # TODO multiple x0 for PT, #269 diff --git a/pypesto/select/__init__.py b/pypesto/select/__init__.py index 8ee6a03a9..eaff283d7 100644 --- a/pypesto/select/__init__.py +++ b/pypesto/select/__init__.py @@ -20,5 +20,6 @@ warnings.warn( "pyPESTO's model selection methods require an installation of PEtab " "Select (https://github.com/PEtab-dev/petab_select). Install via " - "`pip3 install petab-select` or `pip3 install pypesto[select]`." + "`pip3 install petab-select` or `pip3 install pypesto[select]`.", + stacklevel=1, ) diff --git a/pypesto/select/method.py b/pypesto/select/method.py index b576146f6..36a726be6 100644 --- a/pypesto/select/method.py +++ b/pypesto/select/method.py @@ -23,8 +23,8 @@ class MethodSignalProceed(str, Enum): """Indicators for how a model selection method should proceed.""" # TODO move to PEtab Select? - STOP = 'stop' - CONTINUE = 'continue' + STOP = "stop" + CONTINUE = "continue" @dataclass @@ -62,7 +62,7 @@ class MethodLogger: column_width: int = 12 column_sep: str = " | " - def __init__(self, level: str = 'info'): + def __init__(self, level: str = "info"): self.logger = logging.getLogger(__name__) self.level = level @@ -84,7 +84,7 @@ def log(self, message, level: str = None) -> None: def new_selection(self) -> None: """Start logging a new model selection.""" padding = 20 - self.log('-' * padding + 'New Selection' + '-' * padding) + self.log("-" * padding + "New Selection" + "-" * padding) columns = { "Predecessor model subspace:ID": "model0", "Model subspace:ID": "model", @@ -141,9 +141,9 @@ def get_model_id(model: Model) -> str: ------- The ID. """ - model_subspace_id = model.model_subspace_id or '' + model_subspace_id = model.model_subspace_id or "" original_model_id = model.model_id or model.get_hash() - model_id = model_subspace_id + ':' + original_model_id + model_id = model_subspace_id + ":" + original_model_id return model_id def float_to_str(value: float, precision: int = 3) -> str: @@ -261,22 +261,22 @@ def __init__( # TODO deprecated old_model_problem_options = {} for key, value in [ - ('postprocessor', model_postprocessor), + ("postprocessor", model_postprocessor), ( - 'model_to_pypesto_problem_method', + "model_to_pypesto_problem_method", model_to_pypesto_problem_method, ), - ('minimize_options', minimize_options), - ('objective_customizer', objective_customizer), + ("minimize_options", minimize_options), + ("objective_customizer", objective_customizer), ]: if value is not None: old_model_problem_options[key] = value self.logger.log( - f'Specifying `{key}` as an individual argument is ' - 'deprecated. Please instead specify it within some ' - '`model_problem_options` dictionary, e.g. ' + f"Specifying `{key}` as an individual argument is " + "deprecated. Please instead specify it within some " + "`model_problem_options` dictionary, e.g. " f'`model_problem_options={{"{key}": ...}}`.', - level='warning', + level="warning", ) self.model_problem_options = {} self.model_problem_options |= old_model_problem_options @@ -291,10 +291,10 @@ def __init__( if candidate_space is not None and method is not None: self.logger.log( ( - 'Both `candidate_space` and `method` were provided. ' - 'Please only provide one. The method will be ignored here.' + "Both `candidate_space` and `method` were provided. " + "Please only provide one. The method will be ignored here." ), - level='warning', + level="warning", ) # Get method. self.method = ( @@ -309,8 +309,8 @@ def __init__( # Require either a candidate space or a method. if candidate_space is None and self.method is None: raise ValueError( - 'Please provide one of either `candidate_space` or `method`, ' - 'or specify the `method` in the PEtab Select problem.' + "Please provide one of either `candidate_space` or `method`, " + "or specify the `method` in the PEtab Select problem." ) # Use candidate space if provided. if candidate_space is not None: @@ -529,10 +529,10 @@ def new_model_problem( ] if str(model.petab_yaml) != str(predecessor_model.petab_yaml): raise NotImplementedError( - 'The PEtab YAML files differ between the model and its ' - 'predecessor model. This may imply different (fixed union ' - 'estimated) parameter sets. Support for this is not yet ' - 'implemented.' + "The PEtab YAML files differ between the model and its " + "predecessor model. This may imply different (fixed union " + "estimated) parameter sets. Support for this is not yet " + "implemented." ) x_guess = { **predecessor_model.parameters, diff --git a/pypesto/select/postprocessors.py b/pypesto/select/postprocessors.py index c0ad7a5e2..ae8a9d587 100644 --- a/pypesto/select/postprocessors.py +++ b/pypesto/select/postprocessors.py @@ -10,11 +10,11 @@ from .model_problem import TYPE_POSTPROCESSOR, ModelProblem __all__ = [ - 'model_id_binary_postprocessor', - 'multi_postprocessor', - 'report_postprocessor', - 'save_postprocessor', - 'waterfall_plot_postprocessor', + "model_id_binary_postprocessor", + "multi_postprocessor", + "report_postprocessor", + "save_postprocessor", + "waterfall_plot_postprocessor", ] @@ -150,10 +150,10 @@ def report_postprocessor( header = [] row = [] - header.append('model_id') + header.append("model_id") row.append(problem.model.model_id) - header.append('total_time') + header.append("total_time") row.append(str(sum(start_optimization_times))) for criterion in criteria: @@ -161,7 +161,7 @@ def report_postprocessor( row.append(str(problem.model.get_criterion(criterion))) # Arbitrary convergence criterion - header.append('n_converged') + header.append("n_converged") row.append( str( ( @@ -174,10 +174,10 @@ def report_postprocessor( for start_index, start_optimization_time in enumerate( start_optimization_times ): - header.append(f'start_time_{start_index}') + header.append(f"start_time_{start_index}") row.append(str(start_optimization_time)) - with open(output_filepath, 'a+') as f: + with open(output_filepath, "a+") as f: if write_header: - f.write('\t'.join(header) + '\n') - f.write('\t'.join(row) + '\n') + f.write("\t".join(header) + "\n") + f.write("\t".join(row) + "\n") diff --git a/pypesto/select/problem.py b/pypesto/select/problem.py index 7bc2832d5..20308a391 100644 --- a/pypesto/select/problem.py +++ b/pypesto/select/problem.py @@ -50,11 +50,13 @@ def __init__( # TODO deprecated if model_postprocessor is not None: warnings.warn( - 'Specifying `model_postprocessor` directly is deprecated. ' - 'Please specify it with `model_problem_options`, e.g. ' - 'model_problem_options={"postprocessor": ...}`.' + "Specifying `model_postprocessor` directly is deprecated. " + "Please specify it with `model_problem_options`, e.g. " + 'model_problem_options={"postprocessor": ...}`.', + DeprecationWarning, + stacklevel=1, ) - self.model_problem_options['postprocessor'] = model_postprocessor + self.model_problem_options["postprocessor"] = model_postprocessor self.set_state( calibrated_models={}, @@ -75,7 +77,7 @@ def create_method_caller(self, **kwargs) -> MethodCaller: """ kwargs = kwargs.copy() model_problem_options = self.model_problem_options | kwargs.pop( - 'model_problem_options', {} + "model_problem_options", {} ) return MethodCaller( @@ -117,13 +119,13 @@ def handle_select_kwargs( """Check keyword arguments to select calls.""" if "newly_calibrated_models" in kwargs: raise ValueError( - 'Please supply `newly_calibrated_models` via ' - '`pypesto.select.Problem.set_state`.' + "Please supply `newly_calibrated_models` via " + "`pypesto.select.Problem.set_state`." ) if "calibrated_models" in kwargs: raise ValueError( - 'Please supply `calibrated_models` via ' - '`pypesto.select.Problem.set_state`.' + "Please supply `calibrated_models` via " + "`pypesto.select.Problem.set_state`." ) def select( diff --git a/pypesto/store/auto.py b/pypesto/store/auto.py index 58ad26b29..9ff8da626 100644 --- a/pypesto/store/auto.py +++ b/pypesto/store/auto.py @@ -51,7 +51,7 @@ def autosave( filename = default_filename elif isinstance(filename, str): if os.path.exists(filename) and not overwrite: - with h5py.File(filename, 'r') as f: + with h5py.File(filename, "r") as f: storage_used = store_type in f.keys() if storage_used: logger.warning( diff --git a/pypesto/store/hdf5.py b/pypesto/store/hdf5.py index 9d2b738a1..1ffefd407 100644 --- a/pypesto/store/hdf5.py +++ b/pypesto/store/hdf5.py @@ -47,11 +47,11 @@ def write_string_array(f: h5py.Group, path: str, strings: Collection) -> None: dset = f.create_dataset(path, (len(strings),), dtype=dt) if len(strings): - dset[:] = [s.encode('utf8') for s in strings] + dset[:] = [s.encode("utf8") for s in strings] def write_float_array( - f: h5py.Group, path: str, values: Collection[Number], dtype='f8' + f: h5py.Group, path: str, values: Collection[Number], dtype="f8" ) -> None: """ Write float array to hdf5. @@ -77,7 +77,7 @@ def write_float_array( def write_int_array( - f: h5py.Group, path: str, values: Collection[int], dtype=' 'ProfilerResult': +) -> "ProfilerResult": """Read HDF5 results per start. Parameters @@ -38,13 +38,13 @@ def read_hdf5_profile( result = ProfilerResult(np.empty((0, 0)), np.array([]), np.array([])) for profile_key in result.keys(): - if profile_key in f[f'/profiling/{profile_id}/{parameter_id}']: + if profile_key in f[f"/profiling/{profile_id}/{parameter_id}"]: result[profile_key] = f[ - f'/profiling/{profile_id}/{parameter_id}/{profile_key}' + f"/profiling/{profile_id}/{parameter_id}/{profile_key}" ][:] - elif profile_key in f[f'/profiling/{profile_id}/{parameter_id}'].attrs: + elif profile_key in f[f"/profiling/{profile_id}/{parameter_id}"].attrs: result[profile_key] = f[ - f'/profiling/{profile_id}/{parameter_id}' + f"/profiling/{profile_id}/{parameter_id}" ].attrs[profile_key] return result @@ -53,7 +53,7 @@ def read_hdf5_optimization( f: h5py.File, file_name: Union[Path, str], opt_id: str, -) -> 'OptimizerResult': +) -> "OptimizerResult": """Read HDF5 results per start. Parameters @@ -68,18 +68,18 @@ def read_hdf5_optimization( result = OptimizerResult() for optimization_key in result.keys(): - if optimization_key == 'history': + if optimization_key == "history": if optimization_key in f: - result['history'] = Hdf5History(id=opt_id, file=file_name) - result['history'].recover_options(file_name) + result["history"] = Hdf5History(id=opt_id, file=file_name) + result["history"].recover_options(file_name) continue - if optimization_key in f[f'/optimization/results/{opt_id}']: + if optimization_key in f[f"/optimization/results/{opt_id}"]: result[optimization_key] = f[ - f'/optimization/results/{opt_id}/{optimization_key}' + f"/optimization/results/{opt_id}/{optimization_key}" ][:] - elif optimization_key in f[f'/optimization/results/{opt_id}'].attrs: + elif optimization_key in f[f"/optimization/results/{opt_id}"].attrs: result[optimization_key] = f[ - f'/optimization/results/{opt_id}' + f"/optimization/results/{opt_id}" ].attrs[optimization_key] return result @@ -122,19 +122,20 @@ def read(self, objective: ObjectiveBase = None) -> Problem: objective = Objective() # raise warning that objective is not loaded. warnings.warn( - 'You are loading a problem. This problem is not to be used ' - 'without a separately created objective.' + "You are loading a problem. This problem is not to be used " + "without a separately created objective.", + stacklevel=2, ) problem = Problem(objective, [], []) - with h5py.File(self.storage_filename, 'r') as f: - for problem_key in f['/problem']: - if problem_key == 'config': + with h5py.File(self.storage_filename, "r") as f: + for problem_key in f["/problem"]: + if problem_key == "config": continue - setattr(problem, problem_key, f[f'/problem/{problem_key}'][:]) - for problem_attr in f['/problem'].attrs: + setattr(problem, problem_key, f[f"/problem/{problem_key}"][:]) + for problem_attr in f["/problem"].attrs: setattr( - problem, problem_attr, f['/problem'].attrs[problem_attr] + problem, problem_attr, f["/problem"].attrs[problem_attr] ) # h5 uses numpy for everything; convert to lists where necessary @@ -170,7 +171,7 @@ def __init__(self, storage_filename: Union[str, Path]): def read(self) -> Result: """Read HDF5 result file and return pyPESTO result object.""" with h5py.File(self.storage_filename, "r") as f: - for opt_id in f['/optimization/results']: + for opt_id in f["/optimization/results"]: result = read_hdf5_optimization( f, self.storage_filename, opt_id ) @@ -204,10 +205,10 @@ def read(self) -> Result: """Read HDF5 result file and return pyPESTO result object.""" sample_result = {} with h5py.File(self.storage_filename, "r") as f: - for key in f['/sampling/results']: - sample_result[key] = f[f'/sampling/results/{key}'][:] - for key in f['/sampling/results'].attrs: - sample_result[key] = f['/sampling/results'].attrs[key] + for key in f["/sampling/results"]: + sample_result[key] = f[f"/sampling/results/{key}"][:] + for key in f["/sampling/results"].attrs: + sample_result[key] = f["/sampling/results"].attrs[key] try: self.results.sample_result = McmcPtResult(**sample_result) except TypeError: @@ -244,13 +245,13 @@ def read(self) -> Result: """Read HDF5 result file and return pyPESTO result object.""" profiling_list = [] with h5py.File(self.storage_filename, "r") as f: - for profile_id in f['/profiling']: + for profile_id in f["/profiling"]: profiling_list.append( - [None for _ in f[f'/profiling/{profile_id}']] + [None for _ in f[f"/profiling/{profile_id}"]] ) - for parameter_id in f[f'/profiling/{profile_id}']: - if f[f'/profiling/{profile_id}/' f'{parameter_id}'].attrs[ - 'IsNone' + for parameter_id in f[f"/profiling/{profile_id}"]: + if f[f"/profiling/{profile_id}/" f"{parameter_id}"].attrs[ + "IsNone" ]: continue profiling_list[int(profile_id)][ @@ -309,9 +310,9 @@ def read_result( result.optimize_result = temp_result.optimize_result except KeyError: logger.warning( - 'Loading the optimization result failed. It is ' - 'highly likely that no optimization result exists ' - f'within {filename}.' + "Loading the optimization result failed. It is " + "highly likely that no optimization result exists " + f"within {filename}." ) if profile: @@ -321,9 +322,9 @@ def read_result( result.profile_result = temp_result.profile_result except KeyError: logger.warning( - 'Loading the profiling result failed. It is ' - 'highly likely that no profiling result exists ' - f'within {filename}.' + "Loading the profiling result failed. It is " + "highly likely that no profiling result exists " + f"within {filename}." ) if sample: @@ -333,9 +334,9 @@ def read_result( result.sample_result = temp_result.sample_result except KeyError: logger.warning( - 'Loading the sampling result failed. It is ' - 'highly likely that no sampling result exists ' - f'within {filename}.' + "Loading the sampling result failed. It is " + "highly likely that no sampling result exists " + f"within {filename}." ) return result @@ -354,7 +355,7 @@ def load_objective_config(filename: Union[str, Path]): A dictionary of the information, stored instead of the actual objective in problem.objective. """ - with h5py.File(filename, 'r') as f: - info_str = f['problem/config'][()].decode() + with h5py.File(filename, "r") as f: + info_str = f["problem/config"][()].decode() info = ast.literal_eval(info_str) return info diff --git a/pypesto/store/save_to_hdf5.py b/pypesto/store/save_to_hdf5.py index f50345542..a38f34a06 100644 --- a/pypesto/store/save_to_hdf5.py +++ b/pypesto/store/save_to_hdf5.py @@ -73,18 +73,18 @@ def write(self, problem, overwrite: bool = False): os.makedirs(basedir, exist_ok=True) with h5py.File(self.storage_filename, "a") as f: - check_overwrite(f, overwrite, 'problem') + check_overwrite(f, overwrite, "problem") attrs_to_save = [ a for a in dir(problem) - if not a.startswith('__') + if not a.startswith("__") and not callable(getattr(problem, a)) and not hasattr(type(problem), a) ] problem_grp = f.create_group("problem") # save the configuration - f['problem/config'] = str(problem.objective.get_config()) + f["problem/config"] = str(problem.objective.get_config()) for problem_attr in attrs_to_save: value = getattr(problem, problem_attr) @@ -126,17 +126,17 @@ def write(self, result: Result, overwrite=False): os.makedirs(basedir, exist_ok=True) with h5py.File(self.storage_filename, "a") as f: - check_overwrite(f, overwrite, 'optimization') + check_overwrite(f, overwrite, "optimization") optimization_grp = f.require_group("optimization") # settings = # optimization_grp.create_dataset("settings", settings, dtype=) results_grp = optimization_grp.require_group("results") for start in result.optimize_result.list: - start_id = start['id'] + start_id = start["id"] start_grp = results_grp.require_group(start_id) for key in start.keys(): - if key == 'history': + if key == "history": continue if isinstance(start[key], np.ndarray): write_array(start_grp, key, start[key]) @@ -185,7 +185,7 @@ def write(self, result: Result, overwrite: bool = False): os.makedirs(basedir, exist_ok=True) with h5py.File(self.storage_filename, "a") as f: - check_overwrite(f, overwrite, 'sampling') + check_overwrite(f, overwrite, "sampling") results_grp = f.require_group("sampling/results") for key in result.sample_result.keys(): @@ -228,7 +228,7 @@ def write(self, result: Result, overwrite: bool = False): os.makedirs(basedir, exist_ok=True) with h5py.File(self.storage_filename, "a") as f: - check_overwrite(f, overwrite, 'profiling') + check_overwrite(f, overwrite, "profiling") profiling_grp = f.require_group("profiling") for profile_id, profile in enumerate(result.profile_result.list): @@ -248,10 +248,10 @@ def _write_profiler_result( Writes a single profile for a single parameter to the provided HDF5 group. """ if parameter_profile is None: - result_grp.attrs['IsNone'] = True + result_grp.attrs["IsNone"] = True return - result_grp.attrs['IsNone'] = False + result_grp.attrs["IsNone"] = False for key, value in parameter_profile.items(): try: diff --git a/pypesto/testing/examples.py b/pypesto/testing/examples.py index 60a059d7e..32fa0b6d3 100644 --- a/pypesto/testing/examples.py +++ b/pypesto/testing/examples.py @@ -6,7 +6,7 @@ def get_Boehm_JProteomeRes2014_hierarchical_petab() -> ( - 'petab.Problem' # noqa: F821 + "petab.Problem" # noqa: F821 ): """ Get Boehm_JProteomeRes2014 problem with scaled/offset observables. @@ -49,9 +49,9 @@ def get_Boehm_JProteomeRes2014_hierarchical_petab() -> ( petab.ESTIMATE: 1, } for par_id in ( - 'offset_pSTAT5A_rel', - 'offset_pSTAT5B_rel', - 'offset_rSTAT5A_rel', + "offset_pSTAT5A_rel", + "offset_pSTAT5B_rel", + "offset_rSTAT5A_rel", ) ] @@ -66,9 +66,9 @@ def get_Boehm_JProteomeRes2014_hierarchical_petab() -> ( } for par_id, nominal_value in zip( ( - 'scaling_pSTAT5A_rel', - 'scaling_pSTAT5B_rel', - 'scaling_rSTAT5A_rel', + "scaling_pSTAT5A_rel", + "scaling_pSTAT5B_rel", + "scaling_rSTAT5A_rel", ), (3.85261197844677, 6.59147818673419, 3.15271275648527), ) @@ -108,7 +108,7 @@ def get_Boehm_JProteomeRes2014_hierarchical_petab() -> ( def get_Boehm_JProteomeRes2014_hierarchical_petab_corrected_bounds() -> ( - 'petab.Problem' # noqa: F821 + "petab.Problem" # noqa: F821 ): """ See `get_Boehm_JProteomeRes2014_hierarchical_petab`. diff --git a/pypesto/util.py b/pypesto/util.py index 767e57165..14b321e6b 100644 --- a/pypesto/util.py +++ b/pypesto/util.py @@ -191,7 +191,7 @@ def get_condition_label(condition_id: str) -> str: ------- The condition label. """ - return f'condition_{condition_id}' + return f"condition_{condition_id}" def assign_clusters(vals): @@ -226,7 +226,7 @@ def assign_clusters(vals): # get clustering based on distance clust = cluster.hierarchy.fcluster( - cluster.hierarchy.linkage(vals), t=0.1, criterion='distance' + cluster.hierarchy.linkage(vals), t=0.1, criterion="distance" ) # get unique clusters diff --git a/pypesto/visualize/clust_color.py b/pypesto/visualize/clust_color.py index 9c2af0efe..459998dfd 100644 --- a/pypesto/visualize/clust_color.py +++ b/pypesto/visualize/clust_color.py @@ -152,9 +152,9 @@ def assign_colors( # Shape of array did not match n_vals. Error due to size mismatch: raise ValueError( - 'Incorrect color input. Colors must be specified either as ' - 'list of `[r, g, b, alpha]` with length equal to that of `vals` ' - f'(here: {n_vals}), or as a single `[r, g, b, alpha]`.' + "Incorrect color input. Colors must be specified either as " + "list of `[r, g, b, alpha]` with length equal to that of `vals` " + f"(here: {n_vals}), or as a single `[r, g, b, alpha]`." ) @@ -198,10 +198,10 @@ def assign_colors_for_list( # if the user specified color lies does not match the number of results if len(colors) != num_entries: raise ( - 'Incorrect color input. Colors must be specified either as ' - 'list of [r, g, b, alpha] with length equal to function ' - 'values Number of function (here: ' + str(num_entries) + '), ' - 'or as one single [r, g, b, alpha] color.' + "Incorrect color input. Colors must be specified either as " + "list of [r, g, b, alpha] with length equal to function " + "values Number of function (here: " + str(num_entries) + "), " + "or as one single [r, g, b, alpha] color." ) return colors diff --git a/pypesto/visualize/dimension_reduction.py b/pypesto/visualize/dimension_reduction.py index c9e69e6b2..2db23d769 100644 --- a/pypesto/visualize/dimension_reduction.py +++ b/pypesto/visualize/dimension_reduction.py @@ -43,8 +43,8 @@ def projection_scatter_umap( n_components = len(components) if n_components == 2: # handle components - x_label = f'UMAP component {components[0] + 1}' - y_label = f'UMAP component {components[1] + 1}' + x_label = f"UMAP component {components[0] + 1}" + y_label = f"UMAP component {components[1] + 1}" dataset = umap_coordinates[:, components] # call lowlevel routine @@ -55,7 +55,7 @@ def projection_scatter_umap( # We got more than two components. Plot a cross-classification table # Create the labels first component_labels = [ - f'UMAP component {components[i_comp] + 1}' + f"UMAP component {components[i_comp] + 1}" for i_comp in range(n_components) ] # reduce pca components @@ -99,7 +99,7 @@ def projection_scatter_umap_original( umap_object.embedding_ = umap_object.embedding_[:, components] # use umap's original plotting routine to visualize - umap.plot.points(umap_object, values=color_by, theme='viridis', **kwargs) + umap.plot.points(umap_object, values=color_by, theme="viridis", **kwargs) def projection_scatter_pca( @@ -128,8 +128,8 @@ def projection_scatter_pca( n_components = len(components) if n_components == 2: # handle components - x_label = f'PCA component {components[0] + 1}' - y_label = f'PCA component {components[1] + 1}' + x_label = f"PCA component {components[0] + 1}" + y_label = f"PCA component {components[1] + 1}" dataset = pca_coordinates[:, components] @@ -141,7 +141,7 @@ def projection_scatter_pca( # We got more than two components. Plot a cross-classification table # Create the labels first component_labels = [ - f'PCA component {components[i_comp] + 1}' + f"PCA component {components[i_comp] + 1}" for i_comp in range(n_components) ] # reduce pca components @@ -179,14 +179,14 @@ def ensemble_crosstab_scatter_lowlevel( # wo don't even try to plot this into an existing axes object. # Overplotting a multi-axes figure is asking for trouble... - if 'ax' in kwargs.keys(): - del kwargs['ax'] + if "ax" in kwargs.keys(): + del kwargs["ax"] for x_comp in range(0, n_components - 1): for y_comp in range(x_comp + 1, n_components): # handle axis labels - x_label = '' - y_label = '' + x_label = "" + y_label = "" if x_comp == 0: y_label = component_labels[y_comp] if y_comp == n_components - 1: @@ -211,12 +211,12 @@ def ensemble_scatter_lowlevel( dataset, ax: Optional[plt.Axes] = None, size: Optional[Tuple[float]] = (12, 6), - x_label: str = 'component 1', - y_label: str = 'component 2', + x_label: str = "component 1", + y_label: str = "component 2", color_by: Sequence[float] = None, - color_map: str = 'viridis', + color_map: str = "viridis", background_color: RGBA = (0.0, 0.0, 0.0, 1.0), - marker_type: str = '.', + marker_type: str = ".", scatter_size: float = 0.5, invert_scatter_order: bool = False, ): diff --git a/pypesto/visualize/ensemble.py b/pypesto/visualize/ensemble.py index 4103583d5..e4dc44ed0 100644 --- a/pypesto/visualize/ensemble.py +++ b/pypesto/visualize/ensemble.py @@ -133,12 +133,12 @@ def ensemble_identifiability_lowlevel( # plot dashed lines indicating the number rof non-identifiable parameters vert = [-0.05, 1.05] - ax.plot([x_both, x_both], vert, 'k--', linewidth=1.5) - ax.plot([x_both + x_lb, x_both + x_lb], vert, 'k--', linewidth=1.5) + ax.plot([x_both, x_both], vert, "k--", linewidth=1.5) + ax.plot([x_both + x_lb, x_both + x_lb], vert, "k--", linewidth=1.5) ax.plot( [x_both + x_lb + x_ub, x_both + x_lb + x_ub], vert, - 'k--', + "k--", linewidth=1.5, ) @@ -147,68 +147,68 @@ def ensemble_identifiability_lowlevel( ax.text( x_both / 2, -0.05, - 'both bounds hit', + "both bounds hit", color=COLOR_HIT_BOTH_BOUNDS, rotation=-90, - va='top', - ha='center', + va="top", + ha="center", ) if patches_lb_hit: ax.text( x_both + x_lb / 2, -0.05, - 'lower bound hit', + "lower bound hit", color=COLOR_HIT_ONE_BOUND, rotation=-90, - va='top', - ha='center', + va="top", + ha="center", ) if patches_ub_hit: ax.text( x_both + x_lb + x_ub / 2, -0.05, - 'upper bound hit', + "upper bound hit", color=COLOR_HIT_ONE_BOUND, rotation=-90, - va='top', - ha='center', + va="top", + ha="center", ) if patches_none_hit: ax.text( 1 - x_none / 2, -0.05, - 'no bounds hit', + "no bounds hit", color=COLOR_HIT_NO_BOUNDS, rotation=-90, - va='top', - ha='center', + va="top", + ha="center", ) ax.text( 0, -0.7, - 'identifiable parameters: {:4.1f}%'.format(x_none * 100), - va='top', + "identifiable parameters: {:4.1f}%".format(x_none * 100), + va="top", ) # plot upper and lower bounds - ax.text(-0.03, 1.0, 'upper\nbound', ha='right', va='center') - ax.text(-0.03, 0.0, 'lower\nbound', ha='right', va='center') - ax.plot([-0.02, 1.03], [0, 0], 'k:', linewidth=1.5) - ax.plot([-0.02, 1.03], [1, 1], 'k:', linewidth=1.5) + ax.text(-0.03, 1.0, "upper\nbound", ha="right", va="center") + ax.text(-0.03, 0.0, "lower\nbound", ha="right", va="center") + ax.plot([-0.02, 1.03], [0, 0], "k:", linewidth=1.5) + ax.plot([-0.02, 1.03], [1, 1], "k:", linewidth=1.5) plt.xticks([]) plt.yticks([]) # plot frame - ax.plot([0, 0], vert, 'k-', linewidth=1.5) - ax.plot([1, 1], vert, 'k-', linewidth=1.5) + ax.plot([0, 0], vert, "k-", linewidth=1.5) + ax.plot([1, 1], vert, "k-", linewidth=1.5) # beautify axes plt.xlim((-0.15, 1.1)) plt.ylim((-0.78, 1.15)) - ax.spines['right'].set_visible(False) - ax.spines['left'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['top'].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["top"].set_visible(False) return ax @@ -255,10 +255,10 @@ def _prepare_identifiability_plot(id_df: pd.DataFrame): def _affine_transform(par_info): # rescale parameters to bounds - lb = par_info['lowerBound'] - ub = par_info['upperBound'] - val_l = par_info['ensemble_mean'] - par_info['ensemble_std'] - val_u = par_info['ensemble_mean'] + par_info['ensemble_std'] + lb = par_info["lowerBound"] + ub = par_info["upperBound"] + val_l = par_info["ensemble_mean"] - par_info["ensemble_std"] + val_u = par_info["ensemble_mean"] + par_info["ensemble_std"] # check if parameter confidence intervals/credible ranges hit bound if val_l <= lb: lower_val = 0.0 @@ -274,13 +274,13 @@ def _affine_transform(par_info): for par_id in list(id_df.index): # check which of the parameters seems to be identifiable and group them if ( - id_df.loc[par_id, 'within lb: 1 std'] - and id_df.loc[par_id, 'within ub: 1 std'] + id_df.loc[par_id, "within lb: 1 std"] + and id_df.loc[par_id, "within ub: 1 std"] ): none_hit.append(_affine_transform(id_df.loc[par_id, :])) - elif id_df.loc[par_id, 'within lb: 1 std']: + elif id_df.loc[par_id, "within lb: 1 std"]: ub_hit.append(_affine_transform(id_df.loc[par_id, :])) - elif id_df.loc[par_id, 'within ub: 1 std']: + elif id_df.loc[par_id, "within ub: 1 std"]: lb_hit.append(_affine_transform(id_df.loc[par_id, :])) else: both_hit.append(_affine_transform(id_df.loc[par_id, :])) diff --git a/pypesto/visualize/misc.py b/pypesto/visualize/misc.py index 739ae5e35..d190edc47 100644 --- a/pypesto/visualize/misc.py +++ b/pypesto/visualize/misc.py @@ -81,7 +81,7 @@ def process_result_list( # No legends were passed: create some custom legends legends = [] for i_leg in range(len(results)): - legends.append('Result ' + str(i_leg)) + legends.append("Result " + str(i_leg)) else: # legends were passed by user: check length try: @@ -89,8 +89,8 @@ def process_result_list( legends = [legends] if len(legends) != len(results): raise ValueError( - 'List of results passed and list of labels do ' - 'not have the same length.' + "List of results passed and list of labels do " + "not have the same length." ) except TypeError: legend_type_error = True @@ -126,18 +126,19 @@ def process_offset_y( """ # check whether the offset specified by the user is sufficient if offset_y is not None: - if (scale_y == 'log10') and (min_val + offset_y <= 0.0): + if (scale_y == "log10") and (min_val + offset_y <= 0.0): warnings.warn( "Offset specified by user is insufficient. " "Ignoring specified offset and using " + str(np.abs(min_val) + 1.0) - + " instead." + + " instead.", + stacklevel=2, ) else: return offset_y else: # check whether scaling is lin or log10 - if scale_y == 'lin': + if scale_y == "lin": # linear scaling doesn't need any offset return 0.0 @@ -175,19 +176,21 @@ def process_y_limits(ax, y_limits): y_limits = [y_limits[0], y_limits[1]] # check validity of bounds if plotting in log-scale - if ax.get_yscale() == 'log' and y_limits[0] <= 0.0: + if ax.get_yscale() == "log" and y_limits[0] <= 0.0: tmp_y_limits = ax.get_ylim() if y_limits[1] <= 0.0: y_limits = tmp_y_limits warnings.warn( "Invalid bounds for plotting in " - "log-scale. Using defaults bounds." + "log-scale. Using defaults bounds.", + stacklevel=2, ) else: y_limits = [tmp_y_limits[0], y_limits[1]] warnings.warn( "Invalid lower bound for plotting in " - "log-scale. Using only upper bound." + "log-scale. Using only upper bound.", + stacklevel=2, ) # set limits @@ -202,7 +205,7 @@ def process_y_limits(ax, y_limits): if ax_limits[0] > data_limits[0] or ax_limits[1] < data_limits[1]: # Get range of data data_range = data_limits[1] - data_limits[0] - if ax.get_yscale() == 'log': + if ax.get_yscale() == "log": data_range = np.log10(data_range) new_limits = ( np.power(10, np.log10(data_limits[0]) - 0.02 * data_range), @@ -243,7 +246,7 @@ def rgba2rgb(fg: RGB_RGBA, bg: RGB_RGBA = None) -> RGB: else: if len(bg) != LEN_RGB: raise IndexError( - 'A background color of unexpected length was provided: {bg}' + "A background color of unexpected length was provided: {bg}" ) bg = (*bg, RGBA_MAX) @@ -252,7 +255,7 @@ def rgba2rgb(fg: RGB_RGBA, bg: RGB_RGBA = None) -> RGB: return fg if len(fg) != LEN_RGBA: raise IndexError( - 'A foreground color of unexpected length was provided: {fg}' + "A foreground color of unexpected length was provided: {fg}" ) def apparent_composite_color_component( diff --git a/pypesto/visualize/model_fit.py b/pypesto/visualize/model_fit.py index 3175834d6..9dec63b33 100644 --- a/pypesto/visualize/model_fit.py +++ b/pypesto/visualize/model_fit.py @@ -24,7 +24,7 @@ from .ordinal_categories import plot_categories_from_pypesto_result from .spline_approximation import _add_spline_mapped_simulations_to_model_fit -AmiciModel = Union['amici.Model', 'amici.ModelPtr'] +AmiciModel = Union["amici.Model", "amici.ModelPtr"] __all__ = ["visualize_optimized_model_fit", "time_trajectory_model"] @@ -71,7 +71,7 @@ def visualize_optimized_model_fit( axes: `matplotlib.axes.Axes` object of the created plot. None: In case subplots are saved to file """ - x = result.optimize_result.list[start_index]['x'][ + x = result.optimize_result.list[start_index]["x"][ pypesto_problem.x_free_indices ] objective_result = pypesto_problem.objective(x, return_dict=True) @@ -122,9 +122,9 @@ def visualize_optimized_model_fit( if return_dict: return { - 'axes': axes, - 'objective_result': objective_result, - 'simulation_df': simulation_df, + "axes": axes, + "objective_result": objective_result, + "simulation_df": simulation_df, } return axes @@ -235,7 +235,7 @@ def _get_simulation_rdatas( simulation_timepoints = np.linspace(start=0, stop=end_time, num=1000) # get optimization result - parameters = result.optimize_result.list[start_index]['x'] + parameters = result.optimize_result.list[start_index]["x"] # reduce vector to only include free indices. Needed downstream. parameters = problem.get_reduced_vector(parameters) @@ -249,11 +249,11 @@ def _get_simulation_rdatas( # evaluate objective with return dict = True to get inner parameters ret = problem.objective( - parameters, mode='mode_fun', sensi_orders=(0,), return_dict=True + parameters, mode="mode_fun", sensi_orders=(0,), return_dict=True ) # update parameter dictionary with inner parameters - inner_parameters = ret['inner_parameters'] + inner_parameters = ret["inner_parameters"] x_dct.update(inner_parameters) parameter_mapping = problem.objective.parameter_mapping @@ -288,16 +288,16 @@ def _get_simulation_rdatas( # evaluate objective with return dict = True to get data ret = obj( - parameters, mode='mode_fun', sensi_orders=(0,), return_dict=True + parameters, mode="mode_fun", sensi_orders=(0,), return_dict=True ) - rdatas = ret['rdatas'] + rdatas = ret["rdatas"] return rdatas def _time_trajectory_model_with_states( model: AmiciModel, - rdatas: Union['amici.ReturnData', Sequence['amici.ReturnData']], + rdatas: Union["amici.ReturnData", Sequence["amici.ReturnData"]], state_ids: Sequence[str], state_names: Sequence[str], observable_ids: Union[str, Sequence[str]], @@ -372,7 +372,7 @@ def _time_trajectory_model_with_states( def _time_trajectory_model_without_states( model: AmiciModel, - rdatas: Union['amici.ReturnData', Sequence['amici.ReturnData']], + rdatas: Union["amici.ReturnData", Sequence["amici.ReturnData"]], observable_ids: Union[str, Sequence[str]], ): """ diff --git a/pypesto/visualize/optimization_stats.py b/pypesto/visualize/optimization_stats.py index 1fa60421f..dc957332b 100644 --- a/pypesto/visualize/optimization_stats.py +++ b/pypesto/visualize/optimization_stats.py @@ -19,7 +19,7 @@ def optimization_run_properties_one_plot( start_indices: Optional[Union[int, Iterable[int]]] = None, colors: Optional[Union[List[float], List[List[float]]]] = None, legends: Optional[Union[str, List[str]]] = None, - plot_type: str = 'line', + plot_type: str = "line", ) -> matplotlib.axes.Axes: """ Plot stats for allproperties specified in properties_to_plot on one plot. @@ -64,12 +64,12 @@ def optimization_run_properties_one_plot( """ if properties_to_plot is None: properties_to_plot = [ - 'time', - 'n_fval', - 'n_grad', - 'n_hess', - 'n_res', - 'n_sres', + "time", + "n_fval", + "n_grad", + "n_hess", + "n_res", + "n_sres", ] if colors is None: @@ -79,8 +79,8 @@ def optimization_run_properties_one_plot( if len(colors) != len(properties_to_plot): raise ValueError( - 'Number of RGBA colors should be the same as number ' - 'of optimization properties to plot' + "Number of RGBA colors should be the same as number " + "of optimization properties to plot" ) if legends is None: @@ -90,8 +90,8 @@ def optimization_run_properties_one_plot( if len(legends) != len(properties_to_plot): raise ValueError( - 'Number of legends should be the same as number of ' - 'optimization properties to plot' + "Number of legends should be the same as number of " + "optimization properties to plot" ) ax = plt.subplots()[1] @@ -122,7 +122,7 @@ def optimization_run_properties_per_multistart( start_indices: Optional[Union[int, Iterable[int]]] = None, colors: Optional[Union[List[float], List[List[float]]]] = None, legends: Optional[Union[str, List[str]]] = None, - plot_type: str = 'line', + plot_type: str = "line", ) -> Dict[str, plt.Subplot]: """ One plot per optimization property in properties_to_plot. @@ -176,12 +176,12 @@ def optimization_run_properties_per_multistart( """ if properties_to_plot is None: properties_to_plot = [ - 'time', - 'n_fval', - 'n_grad', - 'n_hess', - 'n_res', - 'n_sres', + "time", + "n_fval", + "n_grad", + "n_hess", + "n_res", + "n_sres", ] num_subplot = len(properties_to_plot) @@ -217,7 +217,7 @@ def optimization_run_property_per_multistart( start_indices: Optional[Union[int, Iterable[int]]] = None, colors: Optional[Union[List[float], List[List[float]]]] = None, legends: Optional[Union[str, List[str]]] = None, - plot_type: str = 'line', + plot_type: str = "line", ) -> matplotlib.axes.Axes: """ Plot stats for an optimization run property specified by opt_run_property. @@ -257,12 +257,12 @@ def optimization_run_property_per_multistart( The plot axes. """ supported_properties = { - 'time': 'Wall-clock time (seconds)', - 'n_fval': 'Number of function evaluations', - 'n_grad': 'Number of gradient evaluations', - 'n_hess': 'Number of Hessian evaluations', - 'n_res': 'Number of residuals evaluations', - 'n_sres': 'Number of residual sensitivity evaluations', + "time": "Wall-clock time (seconds)", + "n_fval": "Number of function evaluations", + "n_grad": "Number of gradient evaluations", + "n_hess": "Number of Hessian evaluations", + "n_res": "Number of residuals evaluations", + "n_sres": "Number of residual sensitivity evaluations", } if opt_run_property not in supported_properties: @@ -277,20 +277,20 @@ def optimization_run_property_per_multistart( # axes if axes is None: - ncols = 2 if plot_type == 'both' else 1 + ncols = 2 if plot_type == "both" else 1 fig, axes = plt.subplots(1, ncols) fig.set_size_inches(*size) fig.suptitle( - f'{supported_properties[opt_run_property]} per optimizer run' + f"{supported_properties[opt_run_property]} per optimizer run" ) else: axes.set_title( - f'{supported_properties[opt_run_property]} per optimizer run' + f"{supported_properties[opt_run_property]} per optimizer run" ) # loop over results for j, result in enumerate(results): - if plot_type == 'both': + if plot_type == "both": axes[0] = stats_lowlevel( result, opt_run_property, @@ -309,7 +309,7 @@ def optimization_run_property_per_multistart( start_indices, colors[j], legends[j], - plot_type='hist', + plot_type="hist", ) else: axes = stats_lowlevel( @@ -324,7 +324,7 @@ def optimization_run_property_per_multistart( ) if sum((legend is not None for legend in legends)) > 0: - if plot_type == 'both': + if plot_type == "both": for ax in axes: ax.legend() else: @@ -339,9 +339,9 @@ def stats_lowlevel( axis_label: str, ax: matplotlib.axes.Axes, start_indices: Optional[Union[int, Iterable[int]]] = None, - color: Union[str, List[float], List[List[float]]] = 'C0', + color: Union[str, List[float], List[List[float]]] = "C0", legend: Optional[str] = None, - plot_type: str = 'line', + plot_type: str = "line", ): """ Plot values of the optimization run property across different multistarts. @@ -391,7 +391,7 @@ def stats_lowlevel( sorted_indices = sorted(range(n_starts), key=lambda j: fvals[j]) values = values[sorted_indices] - if plot_type == 'line': + if plot_type == "line": # plot line ax.plot(range(n_starts), values, color=[0.7, 0.7, 0.7, 0.6]) @@ -401,12 +401,12 @@ def stats_lowlevel( tmp_legend = legend else: tmp_legend = None - ax.scatter(i, v, color=colors[i], marker='o', label=tmp_legend) - ax.set_xlabel('Ordered optimizer run') + ax.scatter(i, v, color=colors[i], marker="o", label=tmp_legend) + ax.set_xlabel("Ordered optimizer run") ax.set_ylabel(axis_label) else: - ax.hist(values, color=color, bins='auto', label=legend) + ax.hist(values, color=color, bins="auto", label=legend) ax.set_xlabel(axis_label) - ax.set_ylabel('Number of multistarts') + ax.set_ylabel("Number of multistarts") return ax diff --git a/pypesto/visualize/optimizer_convergence.py b/pypesto/visualize/optimizer_convergence.py index 05f5beb86..e892fd85f 100644 --- a/pypesto/visualize/optimizer_convergence.py +++ b/pypesto/visualize/optimizer_convergence.py @@ -10,8 +10,8 @@ def optimizer_convergence( result: Result, ax: Optional[plt.Axes] = None, - xscale: str = 'symlog', - yscale: str = 'log', + xscale: str = "symlog", + yscale: str = "log", size: Tuple[float] = (18.5, 10.5), ) -> plt.Axes: """ @@ -66,10 +66,10 @@ def optimizer_convergence( ] msgs = result.optimize_result.message conv_data = pd.DataFrame( - {'fval': fvals, 'gradient norm': grad_norms, 'exit message': msgs} + {"fval": fvals, "gradient norm": grad_norms, "exit message": msgs} ) sns.scatterplot( - x='fval', y='gradient norm', hue='exit message', data=conv_data, ax=ax + x="fval", y="gradient norm", hue="exit message", data=conv_data, ax=ax ) ax.set_yscale(yscale) ax.set_xscale(xscale) diff --git a/pypesto/visualize/optimizer_history.py b/pypesto/visualize/optimizer_history.py index 68817f69d..5e360ca74 100644 --- a/pypesto/visualize/optimizer_history.py +++ b/pypesto/visualize/optimizer_history.py @@ -27,7 +27,7 @@ def optimizer_history( size: Tuple = (18.5, 10.5), trace_x: str = TRACE_X_STEPS, trace_y: str = TRACE_Y_FVAL, - scale_y: str = 'log10', + scale_y: str = "log10", offset_y: Optional[float] = None, colors: Optional[Union[RGBA, List[RGBA]]] = None, y_limits: Optional[Union[float, List[float], np.ndarray]] = None, @@ -125,12 +125,12 @@ def optimizer_history( def optimizer_history_lowlevel( vals: List[np.ndarray], - scale_y: str = 'log10', + scale_y: str = "log10", colors: Optional[Union[RGBA, List[RGBA]]] = None, ax: Optional[plt.Axes] = None, size: Tuple = (18.5, 10.5), - x_label: str = 'Optimizer steps', - y_label: str = 'Objective value', + x_label: str = "Optimizer steps", + y_label: str = "Objective value", legend_text: Optional[str] = None, ) -> plt.Axes: """ @@ -181,9 +181,9 @@ def optimizer_history_lowlevel( vals = np.asarray(vals) if vals.shape[0] != 2 or vals.ndim != 2: raise ValueError( - 'If numpy array is passed directly to lowlevel ' - 'routine of optimizer_history, shape needs to ' - 'be 2 x n.' + "If numpy array is passed directly to lowlevel " + "routine of optimizer_history, shape needs to " + "be 2 x n." ) fvals = [vals[1, -1]] vals = [vals] @@ -209,7 +209,7 @@ def optimizer_history_lowlevel( tmp_legend = None # line plots - if scale_y == 'log10': + if scale_y == "log10": ax.semilogy(val[0, :], val[1, :], color=color, label=tmp_legend) else: ax.plot(val[0, :], val[1, :], color=color, label=tmp_legend) @@ -217,7 +217,7 @@ def optimizer_history_lowlevel( # set labels ax.set_xlabel(x_label) ax.set_ylabel(y_label) - ax.set_title('Optimizer history') + ax.set_title("Optimizer history") if legend_text is not None: ax.legend() @@ -389,23 +389,22 @@ def get_labels(trace_x: str, trace_y: str, offset_y: float) -> Tuple[str, str]: Returns ------- labels for x and y axes - """ - x_label = '' - y_label = '' + x_label = "" + y_label = "" if trace_x == TRACE_X_TIME: - x_label = 'Computation time [s]' + x_label = "Computation time [s]" else: - x_label = 'Optimizer steps' + x_label = "Optimizer steps" if trace_y == TRACE_Y_GRADNORM: - y_label = 'Gradient norm' + y_label = "Gradient norm" else: - y_label = 'Objective value' + y_label = "Objective value" if offset_y != 0: - y_label = 'Offsetted ' + y_label.lower() + y_label = "Offsetted " + y_label.lower() return x_label, y_label @@ -461,7 +460,7 @@ def handle_options( ax.plot( [0, max_len], [i_ref.fval + offset_y, i_ref.fval + offset_y], - '--', + "--", color=i_ref.color, label=i_ref.legend, ) @@ -471,8 +470,8 @@ def handle_options( ax.legend() else: logger.warning( - f'Reference point is currently only implemented for trace_y == ' - f'{TRACE_Y_FVAL} and will not be plotted for trace_y == {trace_y}.' + f"Reference point is currently only implemented for trace_y == " + f"{TRACE_Y_FVAL} and will not be plotted for trace_y == {trace_y}." ) return ax diff --git a/pypesto/visualize/ordinal_categories.py b/pypesto/visualize/ordinal_categories.py index ab8e6973e..1672465a9 100644 --- a/pypesto/visualize/ordinal_categories.py +++ b/pypesto/visualize/ordinal_categories.py @@ -68,7 +68,7 @@ def plot_categories_from_pypesto_result( x_dct = dict( zip( pypesto_result.problem.objective.x_ids, - pypesto_result.optimize_result.list[start_index]['x'], + pypesto_result.optimize_result.list[start_index]["x"], ) ) x_dct.update( @@ -105,7 +105,9 @@ def plot_categories_from_pypesto_result( # If any amici simulation failed, raise warning and return None. if any(rdata.status != amici.AMICI_SUCCESS for rdata in inner_rdatas): warnings.warn( - 'Warning: Some AMICI simulations failed. Cannot plot inner solutions.' + "Warning: Some AMICI simulations failed. Cannot plot inner " + "solutions.", + stacklevel=2, ) return None @@ -152,8 +154,8 @@ def plot_categories_from_pypesto_result( def plot_categories_from_inner_result( - inner_problem: 'pypesto.hierarchical.ordinal.problem.OrdinalProblem', - inner_solver: 'pypesto.hierarchical.ordinal.solver.OrdinalInnerSolver', + inner_problem: "pypesto.hierarchical.ordinal.problem.OrdinalProblem", + inner_solver: "pypesto.hierarchical.ordinal.solver.OrdinalInnerSolver", results: list[dict], simulation: list[np.ndarray], timepoints: list[np.ndarray], @@ -215,7 +217,7 @@ def plot_categories_from_inner_result( ) # Get the ax for the current observable. - ax = axes['plot' + str(meas_obs_idx + 1)] + ax = axes["plot" + str(meas_obs_idx + 1)] else: ax = axes[list(inner_problem.groups.keys()).index(group)] @@ -313,10 +315,10 @@ def plot_categories_from_inner_result( ax.legend() if not use_given_axes: - ax.set_title(f'Group {group}, {measurement_type} data') + ax.set_title(f"Group {group}, {measurement_type} data") - ax.set_xlabel('Timepoints') - ax.set_ylabel('Simulation/Surrogate data') + ax.set_xlabel("Timepoints") + ax.set_ylabel("Simulation/Surrogate data") if not use_given_axes: for ax in axes[len(results) :]: @@ -345,7 +347,7 @@ def _plot_category_rectangles_across_conditions( timepoints, [upper_bound] * len(timepoints), [lower_bound] * len(timepoints), - color='gray', + color="gray", alpha=0.5, ) @@ -354,9 +356,9 @@ def _plot_category_rectangles_across_conditions( [], [], [], - color='gray', + color="gray", alpha=0.5, - label='Categories', + label="Categories", ) @@ -383,23 +385,23 @@ def _plot_category_rectangles( # Draw a vertical short grey arrow at the middle point of the interval # at the upper_bounds[i] height ax.annotate( - '', + "", xy=(middle_timepoint, upper_bounds[i]), xytext=( middle_timepoint, upper_bounds[i] + 0.1 * max(surrogate_data), ), arrowprops={ - 'arrowstyle': '<-', - 'color': 'gray', - 'linewidth': 2, + "arrowstyle": "<-", + "color": "gray", + "linewidth": 2, }, ) ax.text( middle_timepoint, upper_bounds[i] + 0.1 * max(surrogate_data), - 'INF', - color='gray', + "INF", + color="gray", fontsize=12, ) # Extend the ax to contain the text @@ -414,7 +416,7 @@ def _plot_category_rectangles( timepoints[i - interval_length : i + 1], upper_bounds[i - interval_length : i + 1], lower_bounds[i - interval_length : i + 1], - color='gray', + color="gray", alpha=0.5, ) else: @@ -427,23 +429,23 @@ def _plot_category_rectangles( # Draw a vertical short grey arrow at the middle point of the interval # at the upper_bounds[i] height ax.annotate( - '', + "", xy=(middle_timepoint, upper_bounds[i]), xytext=( middle_timepoint, upper_bounds[i] + 0.1 * max(surrogate_data), ), arrowprops={ - 'arrowstyle': '<-', - 'color': 'gray', - 'linewidth': 2, + "arrowstyle": "<-", + "color": "gray", + "linewidth": 2, }, ) ax.text( middle_timepoint, upper_bounds[i] + 0.1 * max(surrogate_data), - 'INF', - color='gray', + "INF", + color="gray", fontsize=12, ) # Extend the ax to contain the text @@ -469,7 +471,7 @@ def _plot_category_rectangles( [lower_bounds[i]], ) ), - color='gray', + color="gray", alpha=0.5, ) interval_length = 0 @@ -481,9 +483,9 @@ def _plot_category_rectangles( [], [], [], - color='gray', + color="gray", alpha=0.5, - label='Categories', + label="Categories", ) elif measurement_type == CENSORED: # Add to legend meaning of rectangles @@ -491,14 +493,14 @@ def _plot_category_rectangles( [], [], [], - color='gray', + color="gray", alpha=0.5, - label='Censoring areas', + label="Censoring areas", ) def _get_data_for_plotting( - inner_parameters: list['OrdinalParameter'], + inner_parameters: list["OrdinalParameter"], optimal_scaling_bounds: list, sim: list[np.ndarray], timepoints: list[np.ndarray], @@ -712,16 +714,16 @@ def _plot_observable_fit_across_conditions( ax.plot( condition_ids_from_petab, whole_simulation, - linestyle='-', - marker='.', - color='b', - label='Simulation', + linestyle="-", + marker=".", + color="b", + label="Simulation", ) ax.plot( petab_censored_conditions, surrogate_all, - 'rx', - label='Surrogate data', + "rx", + label="Surrogate data", ) _plot_category_rectangles( ax, @@ -739,8 +741,8 @@ def _plot_observable_fit_across_conditions( ax.plot( petab_quantitative_conditions, quantitative_data, - 'gs', - label='Quantitative data', + "gs", + label="Quantitative data", ) elif measurement_type == ORDINAL: @@ -755,16 +757,16 @@ def _plot_observable_fit_across_conditions( ax.plot( condition_ids_from_petab, simulation_all, - linestyle='-', - marker='.', - color='b', - label='Simulation', + linestyle="-", + marker=".", + color="b", + label="Simulation", ) ax.plot( condition_ids_from_petab, surrogate_all, - 'rx', - label='Surrogate data', + "rx", + label="Surrogate data", ) _plot_category_rectangles( @@ -777,13 +779,13 @@ def _plot_observable_fit_across_conditions( ) # Set the condition xticks on an angle - ax.tick_params(axis='x', rotation=25) + ax.tick_params(axis="x", rotation=25) ax.legend() if not use_given_axes: - ax.set_title(f'Group {group}, {measurement_type} data') + ax.set_title(f"Group {group}, {measurement_type} data") - ax.set_xlabel('Conditions') - ax.set_ylabel('Simulation/Surrogate data') + ax.set_xlabel("Conditions") + ax.set_ylabel("Simulation/Surrogate data") def _plot_observable_fit_for_one_condition( @@ -807,10 +809,10 @@ def _plot_observable_fit_for_one_condition( ax.plot( timepoints_all[0], simulation_all[0], - linestyle='-', - marker='.', - color='b', - label='Simulation', + linestyle="-", + marker=".", + color="b", + label="Simulation", ) elif measurement_type == CENSORED: quantitative_data = inner_problem.groups[group][QUANTITATIVE_DATA] @@ -823,23 +825,23 @@ def _plot_observable_fit_for_one_condition( ax.plot( timepoints[0], simulation[0][:, observable_index], - linestyle='-', - marker='.', - color='b', - label='Simulation', + linestyle="-", + marker=".", + color="b", + label="Simulation", ) ax.plot( quantitative_timepoints, quantitative_data, - 'gs', - label='Quantitative data', + "gs", + label="Quantitative data", ) ax.plot( timepoints_all[0], surrogate_all[0], - 'rx', - label='Surrogate data', + "rx", + label="Surrogate data", ) # Plot the categorie rectangles @@ -874,7 +876,7 @@ def _plot_observable_fit_for_multiple_conditions( if use_given_axes: colors = [] for line in ax.lines: - if 'simulation' in line.get_label(): + if "simulation" in line.get_label(): colors.append(line.get_color()) # Get as many colors as there are conditions else: @@ -910,8 +912,8 @@ def _plot_observable_fit_for_multiple_conditions( ax.plot( timepoints_all[condition_index], simulation_all[condition_index], - linestyle='-', - marker='.', + linestyle="-", + marker=".", color=color, label=condition_id, ) @@ -920,22 +922,22 @@ def _plot_observable_fit_for_multiple_conditions( ax.plot( timepoints[condition_index], simulation[condition_index][:, observable_index], - linestyle='-', - marker='.', + linestyle="-", + marker=".", color=color, label=condition_id, ) ax.plot( quantitative_timepoints[condition_index], quantitative_data[condition_index], - marker='s', + marker="s", color=color, ) ax.plot( timepoints_all[condition_index], surrogate_all[condition_index], - 'x', + "x", color=color, ) @@ -975,24 +977,24 @@ def _plot_observable_fit_for_multiple_conditions( ax.plot( [], [], - 'x', - color='black', - label='Surrogate data', + "x", + color="black", + label="Surrogate data", ) if not use_given_axes: ax.plot( [], [], - linestyle='-', - marker='.', - color='black', - label='Simulation', + linestyle="-", + marker=".", + color="black", + label="Simulation", ) if measurement_type == CENSORED: ax.plot( [], [], - marker='s', - color='black', - label='Quantitative data', + marker="s", + color="black", + label="Quantitative data", ) diff --git a/pypesto/visualize/parameters.py b/pypesto/visualize/parameters.py index c8210b55c..6c28fa33f 100644 --- a/pypesto/visualize/parameters.py +++ b/pypesto/visualize/parameters.py @@ -26,7 +26,7 @@ def parameters( results: Union[Result, Sequence[Result]], ax: Optional[matplotlib.axes.Axes] = None, - parameter_indices: Union[str, Sequence[int]] = 'free_only', + parameter_indices: Union[str, Sequence[int]] = "free_only", lb: Optional[Union[np.ndarray, List[float]]] = None, ub: Optional[Union[np.ndarray, List[float]]] = None, size: Optional[Tuple[float, float]] = None, @@ -86,9 +86,9 @@ def parameters( (results, colors, legends) = process_result_list(results, colors, legends) if isinstance(parameter_indices, str): - if parameter_indices == 'all': + if parameter_indices == "all": parameter_indices = range(0, results[0].problem.dim_full) - elif parameter_indices == 'free_only': + elif parameter_indices == "free_only": parameter_indices = results[0].problem.x_free_indices else: raise ValueError( @@ -151,21 +151,21 @@ def scale_parameters(x): if len(parameter_indices) < results[0].problem.dim_full: x_ref = np.array( results[0].problem.get_reduced_vector( - i_ref['x'], parameter_indices + i_ref["x"], parameter_indices ) ) else: - x_ref = np.array(i_ref['x']) + x_ref = np.array(i_ref["x"]) x_ref = np.reshape(x_ref, (1, x_ref.size)) x_ref = scale_parameters(x_ref) # plot reference parameters using lowlevel routine ax = parameters_lowlevel( x_ref, - [i_ref['fval']], + [i_ref["fval"]], ax=ax, - colors=i_ref['color'], - linestyle='--', + colors=i_ref["color"], + linestyle="--", legend_text=i_ref.legend, balance_alpha=balance_alpha, ) @@ -176,8 +176,8 @@ def scale_parameters(x): def parameter_hist( result: Result, parameter_name: str, - bins: Union[int, str] = 'auto', - ax: Optional['matplotlib.Axes'] = None, + bins: Union[int, str] = "auto", + ax: Optional["matplotlib.Axes"] = None, size: Optional[Tuple[float]] = (18.5, 10.5), color: Optional[List[float]] = None, start_indices: Optional[Union[int, List[int]]] = None, @@ -242,7 +242,7 @@ def parameters_lowlevel( ax: Optional[matplotlib.axes.Axes] = None, size: Optional[Tuple[float, float]] = None, colors: Optional[Sequence[Union[np.ndarray, List[float]]]] = None, - linestyle: str = '-', + linestyle: str = "-", legend_text: Optional[str] = None, balance_alpha: bool = True, ) -> matplotlib.axes.Axes: @@ -309,7 +309,7 @@ def parameters_lowlevel( parameters_ind, linestyle, color=colors[j_x], - marker='o', + marker="o", label=tmp_legend, ) @@ -321,14 +321,14 @@ def parameters_lowlevel( parameters_ind = np.array(parameters_ind).flatten() if lb is not None: lb = np.array(lb, dtype="float64") - ax.plot(lb.flatten(), parameters_ind, 'k--', marker='+') + ax.plot(lb.flatten(), parameters_ind, "k--", marker="+") if ub is not None: ub = np.array(ub, dtype="float64") - ax.plot(ub.flatten(), parameters_ind, 'k--', marker='+') + ax.plot(ub.flatten(), parameters_ind, "k--", marker="+") - ax.set_xlabel('Parameter value') - ax.set_ylabel('Parameter') - ax.set_title('Estimated parameters') + ax.set_xlabel("Parameter value") + ax.set_ylabel("Parameter") + ax.set_title("Estimated parameters") if legend_text is not None: ax.legend() @@ -489,11 +489,11 @@ def _handle_inner_inputs( def parameters_correlation_matrix( result: Result, - parameter_indices: Union[str, Sequence[int]] = 'free_only', + parameter_indices: Union[str, Sequence[int]] = "free_only", start_indices: Optional[Union[int, Iterable[int]]] = None, - method: Union[str, Callable] = 'pearson', + method: Union[str, Callable] = "pearson", cluster: bool = True, - cmap: Union[Colormap, str] = 'bwr', + cmap: Union[Colormap, str] = "bwr", return_table: bool = False, ) -> matplotlib.axes.Axes: """ @@ -534,7 +534,7 @@ def parameters_correlation_matrix( ) # put all parameters into a dataframe, where columns are parameters parameters = [ - result.optimize_result[i_start]['x'][parameter_indices] + result.optimize_result[i_start]["x"][parameter_indices] for i_start in start_indices ] x_labels = [ @@ -558,7 +558,7 @@ def parameters_correlation_matrix( def optimization_scatter( result: Result, - parameter_indices: Union[str, Sequence[int]] = 'free_only', + parameter_indices: Union[str, Sequence[int]] = "free_only", start_indices: Optional[Union[int, Iterable[int]]] = None, diag_kind: str = "kde", suptitle: str = None, @@ -604,18 +604,18 @@ def optimization_scatter( # resulting in optimize_result[start]["x"] being None start_indices_finite = start_indices[ [ - result.optimize_result[i_start]['x'] is not None + result.optimize_result[i_start]["x"] is not None for i_start in start_indices ] ] # compare start_indices with start_indices_finite and log a warning if len(start_indices) != len(start_indices_finite): logger.warning( - 'Some start indices were removed due to inf values at the start.' + "Some start indices were removed due to inf values at the start." ) # put all parameters into a dataframe, where columns are parameters parameters = [ - result.optimize_result[i_start]['x'][parameter_indices] + result.optimize_result[i_start]["x"][parameter_indices] for i_start in start_indices_finite ] x_labels = [ diff --git a/pypesto/visualize/profile_cis.py b/pypesto/visualize/profile_cis.py index d77b151c7..9f159b814 100644 --- a/pypesto/visualize/profile_cis.py +++ b/pypesto/visualize/profile_cis.py @@ -13,7 +13,7 @@ def profile_cis( confidence_level: float = 0.95, profile_indices: Sequence[int] = None, profile_list: int = 0, - color: Union[str, tuple] = 'C0', + color: Union[str, tuple] = "C0", show_bounds: bool = False, ax: matplotlib.axes.Axes = None, ) -> matplotlib.axes.Axes: @@ -69,7 +69,7 @@ def profile_cis( x_names = [problem.x_names[ix] for ix in profile_indices] for ix, (lb, ub) in enumerate(intervals): - ax.plot([lb, ub], [ix + 1, ix + 1], marker='|', color=color) + ax.plot([lb, ub], [ix + 1, ix + 1], marker="|", color=color) parameters_ind = np.arange(1, len(intervals) + 1) ax.set_yticks(parameters_ind) @@ -79,8 +79,8 @@ def profile_cis( if show_bounds: lb = problem.lb_full[profile_indices] - ax.plot(lb, parameters_ind, 'k--', marker='+') + ax.plot(lb, parameters_ind, "k--", marker="+") ub = problem.ub_full[profile_indices] - ax.plot(ub, parameters_ind, 'k--', marker='+') + ax.plot(ub, parameters_ind, "k--", marker="+") return ax diff --git a/pypesto/visualize/profiles.py b/pypesto/visualize/profiles.py index 8069f38e9..bac901bb1 100644 --- a/pypesto/visualize/profiles.py +++ b/pypesto/visualize/profiles.py @@ -243,18 +243,18 @@ def profiles_lowlevel( # labels if x_labels is None: - ax[counter].set_xlabel(f'Parameter {i_plot}') + ax[counter].set_xlabel(f"Parameter {i_plot}") else: ax[counter].set_xlabel(x_labels[counter]) if counter % columns == 0: - ax[counter].set_ylabel('Log-posterior ratio') + ax[counter].set_ylabel("Log-posterior ratio") else: # fix pyPESTO/pyPESTO/pypesto/visualize/profiles.py:228: # UserWarning: FixedFormatter should only be used # together with FixedLocator. Fix from matplotlib #18848. ax[counter].set_yticks(ax[counter].get_yticks()) - ax[counter].set_yticklabels(['' for _ in ax[counter].get_yticks()]) + ax[counter].set_yticklabels(["" for _ in ax[counter].get_yticks()]) # increase counter and cleanup legend counter += 1 @@ -308,8 +308,8 @@ def profile_lowlevel( # axes if ax is None: ax = plt.subplots()[1] - ax.set_xlabel('Parameter value') - ax.set_ylabel('Log-posterior ratio') + ax.set_xlabel("Parameter value") + ax.set_ylabel("Log-posterior ratio") fig = plt.gcf() fig.set_size_inches(*size) @@ -345,7 +345,7 @@ def handle_reference_points(ref, ax, profile_indices): # loop over axes objects for i_par, i_ax in enumerate(ax): for i_ref in ref: - current_x = i_ref['x'][profile_indices[i_par]] + current_x = i_ref["x"][profile_indices[i_par]] i_ax.plot( [current_x, current_x], [0.0, 1.0], @@ -500,8 +500,9 @@ def process_profile_indices( if ind not in plottable_indices: profile_indices_ret.remove(ind) warn( - 'Requested to plot profile for parameter index %i, ' - 'but profile has not been computed.' % ind + f"Requested to plot profile for parameter index {ind}, " + "but profile has not been computed.", + stacklevel=2, ) return profile_indices_ret diff --git a/pypesto/visualize/reference_points.py b/pypesto/visualize/reference_points.py index c7aae3e5c..f471704e2 100644 --- a/pypesto/visualize/reference_points.py +++ b/pypesto/visualize/reference_points.py @@ -79,17 +79,17 @@ def __init__( self.x = np.array(x) else: raise ValueError( - 'Parameter vector x not passed, but is a ' - 'mandatory input when creating a reference ' - 'point. Stopping.' + "Parameter vector x not passed, but is a " + "mandatory input when creating a reference " + "point. Stopping." ) if fval is not None: self.fval = fval else: raise ValueError( - 'Objective value fval not passed, but is a ' - 'mandatory input when creating a reference ' - 'point. Stopping.' + "Objective value fval not passed, but is a " + "mandatory input when creating a reference " + "point. Stopping." ) if color is not None: self.color = color @@ -103,7 +103,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @@ -125,7 +125,7 @@ def assign_colors(ref: Sequence[ReferencePoint]) -> Sequence[ReferencePoint]: # loop over reference points auto_color_count = 0 for i_ref in ref: - if i_ref['auto_color']: + if i_ref["auto_color"]: auto_color_count += 1 auto_colors = [ @@ -136,8 +136,8 @@ def assign_colors(ref: Sequence[ReferencePoint]) -> Sequence[ReferencePoint]: # loop over reference points and assign auto_colors auto_color_count = 0 for i_num, i_ref in enumerate(ref): - if i_ref['auto_color']: - i_ref['color'] = auto_colors[i_num] + if i_ref["auto_color"]: + i_ref["color"] = auto_colors[i_num] auto_color_count += 1 return ref diff --git a/pypesto/visualize/sampling.py b/pypesto/visualize/sampling.py index 0934e9f2e..ddd8b9209 100644 --- a/pypesto/visualize/sampling.py +++ b/pypesto/visualize/sampling.py @@ -32,9 +32,9 @@ prediction_errorbar_settings = { - 'fmt': 'none', - 'color': 'k', - 'capsize': 10, + "fmt": "none", + "color": "k", + "capsize": 10, } @@ -87,14 +87,14 @@ def sampling_fval_traces( _, ax = plt.subplots(figsize=size) sns.set(style="ticks") - kwargs = {'edgecolor': "w", 'linewidth': 0.3, 's': 10} # for edge color + kwargs = {"edgecolor": "w", "linewidth": 0.3, "s": 10} # for edge color if full_trace: - kwargs['hue'] = "converged" - if len(params_fval[kwargs['hue']].unique()) == 1: - kwargs['palette'] = ["#477ccd"] - elif len(params_fval[kwargs['hue']].unique()) == 2: - kwargs['palette'] = ["#868686", "#477ccd"] - kwargs['legend'] = False + kwargs["hue"] = "converged" + if len(params_fval[kwargs["hue"]].unique()) == 1: + kwargs["palette"] = ["#477ccd"] + elif len(params_fval[kwargs["hue"]].unique()) == 2: + kwargs["palette"] = ["#868686", "#477ccd"] + kwargs["legend"] = False sns.scatterplot( x="iteration", y="logPosterior", data=params_fval, ax=ax, **kwargs @@ -106,10 +106,10 @@ def sampling_fval_traces( _burn_in = result.sample_result.burn_in if full_trace and _burn_in > 0: - ax.axvline(_burn_in, linestyle='--', linewidth=1.5, color='k') + ax.axvline(_burn_in, linestyle="--", linewidth=1.5, color="k") - ax.set_xlabel('iteration index') - ax.set_ylabel('log-posterior') + ax.set_xlabel("iteration index") + ax.set_ylabel("log-posterior") if title: ax.set_title(title) @@ -237,7 +237,7 @@ def _plot_trajectories_by_condition( # Each subplot has all data for a single condition. for condition_index, condition_id in enumerate(condition_ids): ax = axes.flat[condition_index] - ax.set_title(f'Condition: {labels[condition_id]}') + ax.set_title(f"Condition: {labels[condition_id]}") # Each subplot has all data for all condition-specific outputs. for output_index, output_id in enumerate(output_ids): facecolor0 = variable_colors[output_index] @@ -251,7 +251,7 @@ def _plot_trajectories_by_condition( ax.plot( t_average, y_average, - 'k-', + "k-", ) if add_sd: t_std, y_std = _get_statistic_data( @@ -262,8 +262,8 @@ def _plot_trajectories_by_condition( ) if (t_std != t_average).all(): raise ValueError( - 'Unknown error: timepoints for average and standard ' - 'deviation do not match.' + "Unknown error: timepoints for average and standard " + "deviation do not match." ) ax.errorbar( t_average, @@ -297,8 +297,8 @@ def _plot_trajectories_by_condition( # some incorrect time points. if not (np.array(t_lower) == np.array(t_upper)).all(): raise ValueError( - 'The timepoints of the data for the upper and lower ' - 'percentiles do not match.' + "The timepoints of the data for the upper and lower " + "percentiles do not match." ) # Plot a shaded region between the data that correspond to the # lower and upper percentiles. @@ -318,12 +318,12 @@ def _plot_trajectories_by_condition( ax.scatter( measurements[0], measurements[1], - marker='o', + marker="o", facecolor=facecolor0, edgecolor=( - 'white' + "white" if rgb_to_hls(*facecolor0)[1] < 0.5 - else 'black' + else "black" ), ) @@ -360,7 +360,7 @@ def _plot_trajectories_by_output( # next condition plot starts at the end of the previous condition plot. t0 = 0 ax = axes.flat[output_index] - ax.set_title(f'Trajectory: {labels[output_id]}') + ax.set_title(f"Trajectory: {labels[output_id]}") # Each subplot is divided by conditions, with vertical lines. for condition_index, condition_id in enumerate(condition_ids): facecolor0 = variable_colors[condition_index] @@ -368,7 +368,7 @@ def _plot_trajectories_by_output( ax.axvline( t0, linewidth=2, - color='k', + color="k", ) t_max = t0 @@ -384,7 +384,7 @@ def _plot_trajectories_by_output( ax.plot( t_average_shifted, y_average, - 'k-', + "k-", ) if add_sd: t_std, y_std = _get_statistic_data( @@ -395,8 +395,8 @@ def _plot_trajectories_by_output( ) if (t_std != t_average).all(): raise ValueError( - 'Unknown error: timepoints for average and standard ' - 'deviation do not match.' + "Unknown error: timepoints for average and standard " + "deviation do not match." ) ax.errorbar( t_average_shifted, @@ -433,8 +433,8 @@ def _plot_trajectories_by_output( # some incorrect time points. if not (np.array(t_lower) == np.array(t_upper)).all(): raise ValueError( - 'The timepoints of the data for the upper and lower ' - 'percentiles do not match.' + "The timepoints of the data for the upper and lower " + "percentiles do not match." ) # Plot a shaded region between the data that correspond to the # lower and upper percentiles. @@ -454,12 +454,12 @@ def _plot_trajectories_by_output( ax.scatter( [t0 + _t for _t in measurements[0]], measurements[1], - marker='o', + marker="o", facecolor=facecolor0, edgecolor=( - 'white' + "white" if rgb_to_hls(*facecolor0)[1] < 0.5 - else 'black' + else "black" ), ) # Set t0 to the last plotted timepoint of the current condition @@ -468,7 +468,7 @@ def _plot_trajectories_by_output( def _get_condition_and_output_ids( - summary: Dict[str, PredictionResult] + summary: Dict[str, PredictionResult], ) -> Tuple[Sequence[str], Sequence[str]]: """Get all condition and output IDs in a prediction summary. @@ -496,7 +496,7 @@ def _get_condition_and_output_ids( ] ).all() ): - raise KeyError('All predictions must have the same set of conditions.') + raise KeyError("All predictions must have the same set of conditions.") condition_ids = all_condition_ids[0] output_ids = sorted( @@ -591,7 +591,7 @@ def _handle_legends( for index, level in enumerate(levels): ci_lines.append( [ - f'{level}% CI', + f"{level}% CI", Line2D( *fake_data, color=rgba2rgb( @@ -608,16 +608,16 @@ def _handle_legends( if add_sd: capline = Line2D( *fake_data, - color=prediction_errorbar_settings['color'], + color=prediction_errorbar_settings["color"], # https://github.com/matplotlib/matplotlib/blob # /710fce3df95e22701bd68bf6af2c8adbc9d67a79/lib/matplotlib/ # axes/_axes.py#L3424= - markersize=2.0 * prediction_errorbar_settings['capsize'], + markersize=2.0 * prediction_errorbar_settings["capsize"], ) - average_title += ' + SD' + average_title += " + SD" barline = LineCollection( np.empty((2, 2, 2)), - color=prediction_errorbar_settings['color'], + color=prediction_errorbar_settings["color"], ) average_line_object = ErrorbarContainer( ( @@ -636,13 +636,13 @@ def _handle_legends( if grouped_measurements: data_line = [ [ - 'Data', + "Data", Line2D( *fake_data, linewidth=0, - marker='o', - markerfacecolor='grey', - markeredgecolor='white', + marker="o", + markerfacecolor="grey", + markeredgecolor="white", ), ] ] @@ -651,16 +651,16 @@ def _handle_legends( # CI level, and variable name, legends. legend_options_top_right = { - 'bbox_to_anchor': (1 + artist_padding, 1), - 'loc': 'upper left', + "bbox_to_anchor": (1 + artist_padding, 1), + "loc": "upper left", } legend_options_bottom_right = { - 'bbox_to_anchor': (1 + artist_padding, 0), - 'loc': 'lower left', + "bbox_to_anchor": (1 + artist_padding, 0), + "loc": "lower left", } legend_titles = { - OUTPUT: 'Conditions', - CONDITION: 'Trajectories', + OUTPUT: "Conditions", + CONDITION: "Trajectories", } legend_variables = axes.flat[n_col - 1].legend( variable_lines[:, 1], @@ -673,7 +673,7 @@ def _handle_legends( level_lines[:, 1], level_lines[:, 0], **legend_options_bottom_right, - title='Prediction', + title="Prediction", ) fig.add_artist(legend_variables) @@ -825,7 +825,7 @@ def sampling_prediction_trajectories( ) = condition_id.split(petab.PARAMETER_SEPARATOR) else: preequilibration_condition_id, simulation_condition_id = ( - '', + "", condition_id, ) condition = { @@ -858,7 +858,7 @@ def sampling_prediction_trajectories( variable_names = condition_ids n_subplots = len(output_ids) else: - raise ValueError(f'Unsupported groupby value: {groupby}') + raise ValueError(f"Unsupported groupby value: {groupby}") level_opacities, variable_colors = _handle_colors( levels=levels, @@ -878,8 +878,8 @@ def sampling_prediction_trajectories( axes = np.array([[axes]]) if len(axes.flat) < n_subplots: raise ValueError( - 'Provided `axes` contains insufficient subplots. At least ' - f'{n_subplots} are required.' + "Provided `axes` contains insufficient subplots. At least " + f"{n_subplots} are required." ) artist_padding = axis_label_padding / (fig.get_size_inches() * fig.dpi)[0] @@ -935,26 +935,26 @@ def sampling_prediction_trajectories( xmin = min(ax.get_position().xmin for ax in axes.flat) ymin = min(ax.get_position().ymin for ax in axes.flat) xlabel = ( - 'Cumulative time across all conditions' + "Cumulative time across all conditions" if groupby == OUTPUT - else 'Time' + else "Time" ) fig.text( 0.5, ymin - artist_padding, xlabel, - ha='center', - va='center', + ha="center", + va="center", transform=fig.transFigure, ) fig.text( xmin - artist_padding, 0.5, - 'Simulated values', - ha='center', - va='center', + "Simulated values", + ha="center", + va="center", transform=fig.transFigure, - rotation='vertical', + rotation="vertical", ) # plt.tight_layout() # Ruins layout for `groupby == OUTPUT`. @@ -1031,7 +1031,7 @@ def sampling_parameter_cis( np.append(x1, x1[::-1]), np.append(y1, y2[::-1]), color=colors[n], - label=str(level) + '% CI', + label=str(level) + "% CI", ) if show_median: @@ -1042,8 +1042,8 @@ def sampling_parameter_cis( ax.plot( [_median, _median], [npar - _step, npar + _step], - 'k-', - label='MCMC median', + "k-", + label="MCMC median", ) # increment height of boxes @@ -1053,8 +1053,8 @@ def sampling_parameter_cis( ax.set_yticklabels( result.problem.get_reduced_vector(result.problem.x_names) ) - ax.set_xlabel('Parameter value') - ax.set_ylabel('Parameter name') + ax.set_xlabel("Parameter value") + ax.set_ylabel("Parameter name") if title: ax.set_title(title) @@ -1134,15 +1134,15 @@ def sampling_parameter_traces( par_ax = dict(zip(param_names, ax.flat)) sns.set(style="ticks") - kwargs = {'edgecolor': "w", 'linewidth': 0.3, 's': 10} # for edge color + kwargs = {"edgecolor": "w", "linewidth": 0.3, "s": 10} # for edge color if full_trace: - kwargs['hue'] = "converged" - if len(params_fval[kwargs['hue']].unique()) == 1: - kwargs['palette'] = ["#477ccd"] - elif len(params_fval[kwargs['hue']].unique()) == 2: - kwargs['palette'] = ["#868686", "#477ccd"] - kwargs['legend'] = False + kwargs["hue"] = "converged" + if len(params_fval[kwargs["hue"]].unique()) == 1: + kwargs["palette"] = ["#477ccd"] + elif len(params_fval[kwargs["hue"]].unique()) == 2: + kwargs["palette"] = ["#868686", "#477ccd"] + kwargs["legend"] = False if result.sample_result.burn_in is None: _burn_in = 0 @@ -1163,12 +1163,12 @@ def sampling_parameter_traces( if full_trace and _burn_in > 0: _ax.axvline( _burn_in, - linestyle='--', + linestyle="--", linewidth=1.5, - color='k', + color="k", ) - _ax.set_xlabel('iteration index') + _ax.set_xlabel("iteration index") _ax.set_ylabel(param_names[idx]) if use_problem_bounds: _ax.set_ylim([theta_lb[idx], theta_ub[idx]]) @@ -1227,7 +1227,7 @@ def sampling_scatter( # TODO: Think this throws the axis errors in seaborn. ax = sns.pairplot( - params_fval.drop(['logPosterior', 'iteration'], axis=1), + params_fval.drop(["logPosterior", "iteration"], axis=1), diag_kind=diag_kind, ) @@ -1252,8 +1252,8 @@ def sampling_1d_marginals( i_chain: int = 0, par_indices: Sequence[int] = None, stepsize: int = 1, - plot_type: str = 'both', - bw_method: str = 'scott', + plot_type: str = "both", + bw_method: str = "scott", suptitle: str = None, size: Tuple[float, float] = None, ): @@ -1307,28 +1307,28 @@ def sampling_1d_marginals( # fig, ax = plt.subplots(nr_params, figsize=size)[1] for idx, par_id in enumerate(param_names): - if plot_type == 'kde': + if plot_type == "kde": # TODO: add bw_adjust as option? sns.kdeplot( params_fval[par_id], bw_method=bw_method, ax=par_ax[par_id] ) - elif plot_type == 'hist': + elif plot_type == "hist": # fixes usage of sns distplot which throws a future warning sns.histplot( - x=params_fval[par_id], ax=par_ax[par_id], stat='density' + x=params_fval[par_id], ax=par_ax[par_id], stat="density" ) sns.rugplot(x=params_fval[par_id], ax=par_ax[par_id]) - elif plot_type == 'both': + elif plot_type == "both": sns.histplot( x=params_fval[par_id], kde=True, ax=par_ax[par_id], - stat='density', + stat="density", ) sns.rugplot(x=params_fval[par_id], ax=par_ax[par_id]) par_ax[par_id].set_xlabel(param_names[idx]) - par_ax[par_id].set_ylabel('Density') + par_ax[par_id].set_ylabel("Density") sns.despine() @@ -1383,7 +1383,8 @@ def get_data_to_plot( warnings.warn( "Burn in index not found in the results, the full chain " "will be shown.\nYou may want to use, e.g., " - "`pypesto.sample.geweke_test`." + "`pypesto.sample.geweke_test`.", + stacklevel=2, ) _burn_in = 0 else: @@ -1412,14 +1413,14 @@ def get_data_to_plot( # transform ndarray to pandas for the use of seaborn pd_params = pd.DataFrame(arr_param, columns=param_names) - pd_fval = pd.DataFrame(data=arr_fval, columns=['logPosterior']) + pd_fval = pd.DataFrame(data=arr_fval, columns=["logPosterior"]) - pd_iter = pd.DataFrame(data=indices, columns=['iteration']) + pd_iter = pd.DataFrame(data=indices, columns=["iteration"]) if full_trace: converged = np.zeros((len(arr_fval))) converged[_burn_in:] = 1 - pd_conv = pd.DataFrame(data=converged, columns=['converged']) + pd_conv = pd.DataFrame(data=converged, columns=["converged"]) params_fval = pd.concat( [pd_params, pd_fval, pd_iter, pd_conv], axis=1, ignore_index=False diff --git a/pypesto/visualize/select.py b/pypesto/visualize/select.py index 7d47512d9..84d1ed0d9 100644 --- a/pypesto/visualize/select.py +++ b/pypesto/visualize/select.py @@ -75,8 +75,7 @@ def plot_selected_models( criterion_values = { labels.get( model.get_hash(), default_label_maker(model) - ): model.get_criterion(criterion) - - zero + ): model.get_criterion(criterion) - zero for model in selected_models } @@ -84,14 +83,14 @@ def plot_selected_models( criterion_values.keys(), criterion_values.values(), linewidth=linewidth, - color='lightgrey', + color="lightgrey", # edgecolor='k' ) ax.get_xticks() ax.set_xticks(list(range(len(criterion_values)))) ax.set_ylabel( - criterion + ('(relative)' if relative else '(absolute)'), fontsize=fz + criterion + ("(relative)" if relative else "(absolute)"), fontsize=fz ) # could change to compared_model_ids, if all models are plotted ax.set_xticklabels( @@ -103,8 +102,8 @@ def plot_selected_models( ytl = ax.get_yticks() ax.set_ylim([min(ytl), max(ytl)]) # removing top and right borders - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) return ax @@ -180,19 +179,19 @@ def plot_calibrated_models_digraph( ) else: raise NotImplementedError( - 'Plots for models with `None` as their predecessor model are ' - 'not yet implemented.' + "Plots for models with `None` as their predecessor model are " + "not yet implemented." ) - from_ = 'None' + from_ = "None" to = labels.get(model.get_hash(), default_label_maker(model)) edges.append((from_, to)) G.add_edges_from(edges) default_options = { - 'node_color': 'lightgrey', - 'arrowstyle': '-|>', - 'node_shape': 's', - 'node_size': 2500, + "node_color": "lightgrey", + "arrowstyle": "-|>", + "node_shape": "s", + "node_size": 2500, } if options is not None: default_options.update(options) diff --git a/pypesto/visualize/spline_approximation.py b/pypesto/visualize/spline_approximation.py index 950cd6e77..0a1d6d826 100644 --- a/pypesto/visualize/spline_approximation.py +++ b/pypesto/visualize/spline_approximation.py @@ -58,14 +58,14 @@ def plot_splines_from_pypesto_result( pypesto_result.problem.objective.calculator, InnerCalculatorCollector ): raise ValueError( - 'The calculator must be an instance of the InnerCalculatorCollector.' + "The calculator must be an instance of the InnerCalculatorCollector." ) # Get the parameters from the pypesto result for the start_index. x_dct = dict( zip( pypesto_result.problem.objective.x_ids, - pypesto_result.optimize_result.list[start_index]['x'], + pypesto_result.optimize_result.list[start_index]["x"], ) ) @@ -101,7 +101,9 @@ def plot_splines_from_pypesto_result( # If any amici simulation failed, raise warning and return None. if any(rdata.status != amici.AMICI_SUCCESS for rdata in inner_rdatas): warnings.warn( - 'Warning: Some AMICI simulations failed. Cannot plot inner solutions.' + "Warning: Some AMICI simulations failed. Cannot plot inner " + "solutions.", + stacklevel=2, ) return None @@ -129,8 +131,8 @@ def plot_splines_from_pypesto_result( def plot_splines_from_inner_result( - inner_problem: 'pypesto.hierarchical.spline_approximation.problem.SplineInnerProblem', - inner_solver: 'pypesto.hierarchical.spline_approximation.solver.SplineInnerSolver', + inner_problem: "pypesto.hierarchical.spline_approximation.problem.SplineInnerProblem", + inner_solver: "pypesto.hierarchical.spline_approximation.solver.SplineInnerSolver", results: list[dict], observable_ids=None, **kwargs, @@ -210,17 +212,17 @@ def plot_splines_from_inner_result( ) axs[group_idx].plot( - simulation, measurements, 'bs', label='Measurements' + simulation, measurements, "bs", label="Measurements" ) axs[group_idx].plot( - spline_bases, spline_knots, 'g.', label='Spline knots' + spline_bases, spline_knots, "g.", label="Spline knots" ) axs[group_idx].plot( spline_bases, spline_knots, - linestyle='-', - color='g', - label='Spline function', + linestyle="-", + color="g", + label="Spline function", ) if inner_solver.options[REGULARIZE_SPLINE]: alpha_opt, beta_opt = _calculate_optimal_regularization( @@ -231,22 +233,22 @@ def plot_splines_from_inner_result( axs[group_idx].plot( spline_bases, alpha_opt * spline_bases + beta_opt, - linestyle='--', - color='orange', - label='Regularization line', + linestyle="--", + color="orange", + label="Regularization line", ) axs[group_idx].plot( - simulation, mapped_simulations, 'r^', label='Mapped simulation' + simulation, mapped_simulations, "r^", label="Mapped simulation" ) axs[group_idx].legend() if observable_ids is not None: - axs[group_idx].set_title(f'{observable_ids[group-1]}') + axs[group_idx].set_title(f"{observable_ids[group-1]}") else: - axs[group_idx].set_title(f'Group {group}') + axs[group_idx].set_title(f"Group {group}") - axs[group_idx].set_xlabel('Model output') - axs[group_idx].set_ylabel('Measurements') + axs[group_idx].set_xlabel("Model output") + axs[group_idx].set_ylabel("Measurements") for ax in axs[len(results) :]: ax.remove() @@ -326,7 +328,7 @@ def _add_spline_mapped_simulations_to_model_fit( x_dct = dict( zip( pypesto_problem.objective.x_ids, - result.optimize_result.list[start_index]['x'], + result.optimize_result.list[start_index]["x"], ) ) x_dct.update( @@ -359,7 +361,9 @@ def _add_spline_mapped_simulations_to_model_fit( # If any amici simulation failed, raise warning and return None. if any(rdata.status != amici.AMICI_SUCCESS for rdata in inner_rdatas): warnings.warn( - 'Warning: Some AMICI simulations failed. Cannot plot inner solutions.' + "Warning: Some AMICI simulations failed. Cannot plot inner " + "solutions.", + stacklevel=2, ) return None @@ -416,7 +420,7 @@ def _add_spline_mapped_simulations_to_model_fit( # and timepoints as the lines which have 'simulation' in their label. plotted_index = 0 for line in ax.lines: - if 'simulation' in line.get_label(): + if "simulation" in line.get_label(): color = line.get_color() timepoints = line.get_xdata() condition_mapped_simulations = mapped_simulations[ @@ -428,18 +432,18 @@ def _add_spline_mapped_simulations_to_model_fit( timepoints, condition_mapped_simulations, color=color, - linestyle='dotted', - marker='^', + linestyle="dotted", + marker="^", ) # Add linestyle='dotted' and marker='^' to the legend as black spline mapped simulations. ax.plot( [], [], - color='black', - linestyle='dotted', - marker='^', - label='Spline mapped simulation', + color="black", + linestyle="dotted", + marker="^", + label="Spline mapped simulation", ) # Reset the legend. @@ -472,7 +476,7 @@ def _obtain_regularization_for_start( x_dct = dict( zip( pypesto_result.problem.objective.x_ids, - pypesto_result.optimize_result.list[start_index]['x'], + pypesto_result.optimize_result.list[start_index]["x"], ) ) @@ -507,7 +511,9 @@ def _obtain_regularization_for_start( # If any amici simulation failed, raise warning and return None. if any(rdata.status != amici.AMICI_SUCCESS for rdata in inner_rdatas): warnings.warn( - 'Warning: Some AMICI simulations failed. Cannot plot inner solutions.' + "Warning: Some AMICI simulations failed. Cannot plot inner " + "solutions.", + stacklevel=2, ) return None @@ -555,7 +561,7 @@ def _obtain_regularization_for_start( N=len(s), c=spline_bases, regularization_factor=inner_solver.options[ - 'regularization_factor' + "regularization_factor" ], ) reg_term_sum += reg_term diff --git a/pypesto/visualize/waterfall.py b/pypesto/visualize/waterfall.py index 8d09b1bc8..ffdde2c4a 100644 --- a/pypesto/visualize/waterfall.py +++ b/pypesto/visualize/waterfall.py @@ -24,7 +24,7 @@ def waterfall( ax: Optional[plt.Axes] = None, size: Optional[tuple[float, float]] = (18.5, 10.5), y_limits: Optional[Tuple[float]] = None, - scale_y: Optional[str] = 'log10', + scale_y: Optional[str] = "log10", offset_y: Optional[float] = None, start_indices: Optional[Union[Sequence[int], int]] = None, n_starts_to_zoom: int = 0, @@ -85,7 +85,7 @@ def waterfall( if n_starts_to_zoom: # create zoom in inset_axes = inset_locator.inset_axes( - ax, width="30%", height="30%", loc='center right' + ax, width="30%", height="30%", loc="center right" ) inset_locator.mark_inset(ax, inset_axes, loc1=2, loc2=4) else: @@ -175,12 +175,12 @@ def waterfall( if any(legends): ax.legend() # labels - ax.set_xlabel('Ordered optimizer run') + ax.set_xlabel("Ordered optimizer run") if offset_y == 0.0: - ax.set_ylabel('Function value') + ax.set_ylabel("Function value") else: - ax.set_ylabel(f'Objective value (offset={offset_y:0.3e})') - ax.set_title('Waterfall plot') + ax.set_ylabel(f"Objective value (offset={offset_y:0.3e})") + ax.set_title("Waterfall plot") return ax @@ -188,7 +188,7 @@ def waterfall_lowlevel( fvals, ax: Optional[plt.Axes] = None, size: Optional[Tuple[float]] = (18.5, 10.5), - scale_y: str = 'log10', + scale_y: str = "log10", offset_y: float = 0.0, colors: Optional[Union[RGBA, Sequence[RGBA]]] = None, legend_text: Optional[str] = None, @@ -239,7 +239,7 @@ def waterfall_lowlevel( # plot ax.xaxis.set_major_locator(MaxNLocator(integer=True)) # plot line - if scale_y == 'log10': + if scale_y == "log10": ax.semilogy(start_indices, fvals, color=[0.7, 0.7, 0.7, 0.6]) else: ax.plot(start_indices, fvals, color=[0.7, 0.7, 0.7, 0.6]) @@ -255,18 +255,18 @@ def waterfall_lowlevel( tmp_legend = None # line plot (linear or logarithmic) - if scale_y == 'log10': + if scale_y == "log10": ax.semilogy( - j, fval, color=color, marker='o', label=tmp_legend, alpha=1.0 + j, fval, color=color, marker="o", label=tmp_legend, alpha=1.0 ) else: ax.plot( - j, fval, color=color, marker='o', label=tmp_legend, alpha=1.0 + j, fval, color=color, marker="o", label=tmp_legend, alpha=1.0 ) # check if y-axis has a reasonable scale y_min, y_max = ax.get_ylim() - if scale_y == 'log10': + if scale_y == "log10": if np.log10(y_max) - np.log10(y_min) < 1.0: ax.set_ylim( ax.dataLim.y0 - 0.001 * abs(ax.dataLim.y0), @@ -278,12 +278,12 @@ def waterfall_lowlevel( ax.set_ylim(y_mean - 0.5, y_mean + 0.5) # labels - ax.set_xlabel('Ordered optimizer run') + ax.set_xlabel("Ordered optimizer run") if offset_y == 0.0: - ax.set_ylabel('Function value') + ax.set_ylabel("Function value") else: - ax.set_ylabel('Objective value (offset={offset_y:0.3e})') - ax.set_title('Waterfall plot') + ax.set_ylabel("Objective value (offset={offset_y:0.3e})") + ax.set_title("Waterfall plot") if legend_text is not None: ax.legend() @@ -337,7 +337,7 @@ def process_offset_for_list( # if there are references, also account for those if references: - min_val = min(min_val, np.nanmin([r['fval'] for r in references])) + min_val = min(min_val, np.nanmin([r["fval"] for r in references])) offset_y = process_offset_y(offset_y, scale_y, float(min_val)) @@ -430,7 +430,7 @@ def handle_options(ax, max_len_fvals, ref, y_limits, offset_y): ax.plot( [0, max_len_fvals - 1], [i_ref.fval + offset_y, i_ref.fval + offset_y], - '--', + "--", color=i_ref.color, label=i_ref.legend, ) diff --git a/pyproject.toml b/pyproject.toml index 06bda6a3f..858e52ba8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,66 @@ requires = [ ] build-backend = "setuptools.build_meta" -[tool.black] +[tool.ruff] line-length = 79 -target-version = ['py38', 'py39'] -skip-string-normalization = true +exclude = ["amici_models"] +extend-include = ["*.ipynb"] +lint.ignore = [ + "B027", + "D100", + "D105", + "D107", + "D202", + "E501", + "F403", + "F405", + "D413" +] +lint.select = [ + "F", # Pyflakes + "I", # isort + "D", # pydocstyle (PEP 257) + "S", # flake8-bandit + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "T20", # flake8-print + "W", # pycodestyle Warnings + "E", # pycodestyle Errors + # "ANN", # flakes-annotations TODO: currently produces ~1500 errors to manual fix +] +[tool.ruff.lint.pydocstyle] +convention = "pep257" -[tool.isort] -profile = "black" -line_length = 79 -multi_line_output = 3 +[tool.ruff.lint.per-file-ignores] +"test/julia/test_pyjulia.py" = ["E402"] +"pypesto/C.py" = [ + "D400", + "D205", +] +"*/__init__.py" = [ + "F401", + "D400", + "D205", +] +"pypesto/logging.py" = [ + "D400", + "D205", +] +"test/*" = [ + "T201", + "S101", + "D", +] +"pypesto/util.py" = [ + "D400", + "D205", +] +"doc/example/*.py" = [ # ignore docstyle in example pthon files. + "D", +] +"doc/conf.py" = [ + "E402", +] +"*.ipynb" = [ + "T20", "E402", "D" +] diff --git a/setup.cfg b/setup.cfg index 714f1ebec..3f51e1851 100644 --- a/setup.cfg +++ b/setup.cfg @@ -150,7 +150,6 @@ doc = %(petab)s %(aesara)s %(jax)s - %(pymc)s example = %(julia)s %(pymc)s diff --git a/test/base/test_aggregated.py b/test/base/test_aggregated.py index f212ef380..97de70119 100644 --- a/test/base/test_aggregated.py +++ b/test/base/test_aggregated.py @@ -18,25 +18,25 @@ def convreact_for_funmode(max_sensi_order, x=None): - obj = load_amici_objective('conversion_reaction')[0] + obj = load_amici_objective("conversion_reaction")[0] return { - 'obj': obj, - 'max_sensi_order': max_sensi_order, - 'x': x, - 'fval': obj.get_fval(x), - 'grad': obj.get_grad(x), - 'hess': obj.get_hess(x), + "obj": obj, + "max_sensi_order": max_sensi_order, + "x": x, + "fval": obj.get_fval(x), + "grad": obj.get_grad(x), + "hess": obj.get_hess(x), } def convreact_for_resmode(max_sensi_order, x=None): - obj = load_amici_objective('conversion_reaction')[0] + obj = load_amici_objective("conversion_reaction")[0] return { - 'obj': obj, - 'max_sensi_order': max_sensi_order, - 'x': x, - 'res': obj.get_res(x), - 'sres': obj.get_sres(x), + "obj": obj, + "max_sensi_order": max_sensi_order, + "x": x, + "res": obj.get_res(x), + "sres": obj.get_sres(x), } @@ -58,10 +58,10 @@ def test_evaluate(): def _test_evaluate_prior(struct): - x = struct['x'] - prior_list = [get_parameter_prior_dict(0, 'normal', [0, 1], 'lin')] + x = struct["x"] + prior_list = [get_parameter_prior_dict(0, "normal", [0, 1], "lin")] obj = pypesto.objective.AggregatedObjective( - [struct['obj'], NegLogParameterPriors(prior_list)] + [struct["obj"], NegLogParameterPriors(prior_list)] ) for mode, max_sensi_order in zip([MODE_RES, MODE_FUN], [1, 2]): sensi_orders = range(max_sensi_order + 1) @@ -71,12 +71,12 @@ def _test_evaluate_prior(struct): def _test_evaluate_funmode(struct): - obj = pypesto.objective.AggregatedObjective([struct['obj'], struct['obj']]) - x = struct['x'] - fval_true = 2 * struct['fval'] - grad_true = 2 * struct['grad'] - hess_true = 2 * struct['hess'] - max_sensi_order = struct['max_sensi_order'] + obj = pypesto.objective.AggregatedObjective([struct["obj"], struct["obj"]]) + x = struct["x"] + fval_true = 2 * struct["fval"] + grad_true = 2 * struct["grad"] + hess_true = 2 * struct["hess"] + max_sensi_order = struct["max_sensi_order"] # check function values if max_sensi_order >= 2: @@ -118,11 +118,11 @@ def _test_evaluate_funmode(struct): def _test_evaluate_resmode(struct): - obj = pypesto.objective.AggregatedObjective([struct['obj'], struct['obj']]) - x = struct['x'] - res_true = np.hstack([struct['res'], struct['res']]) - sres_true = np.vstack([struct['sres'], struct['sres']]) - max_sensi_order = struct['max_sensi_order'] + obj = pypesto.objective.AggregatedObjective([struct["obj"], struct["obj"]]) + x = struct["x"] + res_true = np.hstack([struct["res"], struct["res"]]) + sres_true = np.vstack([struct["sres"], struct["sres"]]) + max_sensi_order = struct["max_sensi_order"] # check function values if max_sensi_order >= 1: @@ -144,7 +144,7 @@ def _test_evaluate_resmode(struct): def test_exceptions(): with pytest.raises(TypeError): pypesto.objective.AggregatedObjective( - rosen_for_sensi(2, False, [0, 1])['obj'] + rosen_for_sensi(2, False, [0, 1])["obj"] ) with pytest.raises(TypeError): pypesto.objective.AggregatedObjective([0.5]) diff --git a/test/base/test_engine.py b/test/base/test_engine.py index d4d045c20..f86fc000f 100644 --- a/test/base/test_engine.py +++ b/test/base/test_engine.py @@ -29,11 +29,11 @@ def test_basic(): def _test_basic(engine): # set up problem - objective = rosen_for_sensi(max_sensi_order=2)['obj'] + objective = rosen_for_sensi(max_sensi_order=2)["obj"] lb = 0 * np.ones((1, 2)) ub = 1 * np.ones((1, 2)) problem = pypesto.Problem(objective, lb, ub) - optimizer = pypesto.optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = pypesto.optimize.ScipyOptimizer(options={"maxiter": 10}) result = pypesto.optimize.minimize( problem=problem, n_starts=2, @@ -66,7 +66,7 @@ def _test_petab(engine): ) objective = petab_importer.create_objective() problem = petab_importer.create_problem(objective) - optimizer = pypesto.optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = pypesto.optimize.ScipyOptimizer(options={"maxiter": 10}) result = pypesto.optimize.minimize( problem=problem, n_starts=3, diff --git a/test/base/test_ensemble.py b/test/base/test_ensemble.py index d45a8662e..c12e702b3 100644 --- a/test/base/test_ensemble.py +++ b/test/base/test_ensemble.py @@ -32,7 +32,7 @@ def test_ensemble_from_optimization(): problem = pypesto.Problem(objective=objective, lb=lb, ub=ub) - optimizer = optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) history_options = pypesto.HistoryOptions(trace_record=True) result = optimize.minimize( problem=problem, @@ -44,10 +44,10 @@ def test_ensemble_from_optimization(): # change fvals of each start for i_start, optimizer_result in enumerate(result.optimize_result.list): - optimizer_result['fval'] = i_start - for i_iter in range(len(optimizer_result['history']._trace['fval'])): - optimizer_result['history']._trace['fval'][i_iter] = ( - len(optimizer_result['history']._trace['fval']) + optimizer_result["fval"] = i_start + for i_iter in range(len(optimizer_result["history"]._trace["fval"])): + optimizer_result["history"]._trace["fval"][i_iter] = ( + len(optimizer_result["history"]._trace["fval"]) + i_start - i_iter ) @@ -63,13 +63,13 @@ def test_ensemble_from_optimization(): # compare vector_tags with the expected values: ep_tags = [ - (int(result.optimize_result.list[i]['id']), -1) for i in [0, 1, 2, 3] + (int(result.optimize_result.list[i]["id"]), -1) for i in [0, 1, 2, 3] ] hist_tags = [ ( - int(result.optimize_result.list[i]['id']), - len(result.optimize_result.list[i]['history']._trace['fval']) + int(result.optimize_result.list[i]["id"]), + len(result.optimize_result.list[i]["history"]._trace["fval"]) - 1 - j, ) @@ -111,7 +111,7 @@ def post_processor(amici_outputs, output_type, output_ids): ) ensemble_prediction = get_ensemble_prediction(max_size=10) - fn = 'test_file.hdf5' + fn = "test_file.hdf5" try: write_ensemble_prediction_to_h5(ensemble_prediction, fn) ensemble_prediction_r = read_ensemble_prediction_from_h5( diff --git a/test/base/test_history.py b/test/base/test_history.py index 070231526..d8e9ded27 100644 --- a/test/base/test_history.py +++ b/test/base/test_history.py @@ -41,16 +41,16 @@ class HistoryTest(unittest.TestCase): def check_history(self): kwargs = { - 'objective': self.obj, - 'ub': self.ub, - 'lb': self.lb, + "objective": self.obj, + "ub": self.ub, + "lb": self.lb, } if self.fix_pars: kwargs = { **kwargs, **{ - 'x_fixed_indices': self.x_fixed_indices, - 'x_fixed_vals': self.x_fixed_indices, + "x_fixed_indices": self.x_fixed_indices, + "x_fixed_vals": self.x_fixed_indices, }, } self.problem = pypesto.Problem(**kwargs) @@ -62,7 +62,7 @@ def check_history(self): self.history_options.trace_save_iter = 1 - for storage_type in ['.csv', '.hdf5', None]: + for storage_type in [".csv", ".hdf5", None]: with tempfile.TemporaryDirectory(dir=".") as tmpdir: if storage_type == ".csv": _, fn = tempfile.mkstemp( @@ -132,11 +132,11 @@ def check_load_from_file(self, start: pypesto.OptimizerResult, id: str): key for key in start.keys() if key - not in ['history', 'message', 'exitflag', 'time', 'optimizer'] + not in ["history", "message", "exitflag", "time", "optimizer"] ] for attr in result_attributes: # if we didn't record we can't recover the value - if not self.history_options.get(f'trace_record_{attr}', True): + if not self.history_options.get(f"trace_record_{attr}", True): continue # note that we can expect slight deviations in grad when using @@ -146,7 +146,7 @@ def check_load_from_file(self, start: pypesto.OptimizerResult, id: str): # increase atol if start[attr] is None: continue # reconstituted may carry more information - if attr in ['sres', 'grad', 'hess'] and rstart[attr] is None: + if attr in ["sres", "grad", "hess"] and rstart[attr] is None: continue # may not always recover those elif isinstance(start[attr], np.ndarray): assert np.allclose( @@ -191,17 +191,17 @@ def check_reconstruct_history( history_attributes = [ a for a in dir(start.history) - if not a.startswith('__') + if not a.startswith("__") and not callable(getattr(start.history, a)) and a not in [ - 'options', - '_abc_impl', - '_start_time', - 'start_time', - '_trace', - 'x_names', - 'editable', + "options", + "_abc_impl", + "_start_time", + "start_time", + "_trace", + "x_names", + "editable", ] # exitflag and message are not stored in CsvHistory and ( @@ -219,8 +219,8 @@ def check_reconstruct_history( history_entries = [X, FVAL, GRAD, HESS, RES, SRES] for entry in history_entries: - original_trace = getattr(start.history, f'get_{entry}_trace')() - reconst_trace = getattr(reconst_history, f'get_{entry}_trace')() + original_trace = getattr(start.history, f"get_{entry}_trace")() + reconst_trace = getattr(reconst_history, f"get_{entry}_trace")() for iteration in range(len(original_trace)): # comparing nan and None difficult if original_trace[iteration] is None: @@ -279,11 +279,11 @@ def xfull(x_trace): for var, fun in funs.items(): for it in range(len(start.history)): x_full = xfull(start.history.get_x_trace(it)) - val = getattr(start.history, f'get_{var}_trace')(it) + val = getattr(start.history, f"get_{var}_trace")(it) fun_val = fun(x_full) if not getattr( - self.history_options, f'trace_record_{var}', True + self.history_options, f"trace_record_{var}", True ): assert np.isnan(val) continue @@ -318,16 +318,16 @@ def xfull(x_trace): self.problem.get_reduced_matrix(fun_val), ), var else: - raise RuntimeError('missing test implementation') + raise RuntimeError("missing test implementation") class ResModeHistoryTest(HistoryTest): @classmethod def setUpClass(cls): cls.optimizer = pypesto.optimize.ScipyOptimizer( - method='ls_trf', options={'max_nfev': 100} + method="ls_trf", options={"max_nfev": 100} ) - cls.obj, _ = load_amici_objective('conversion_reaction') + cls.obj, _ = load_amici_objective("conversion_reaction") cls.lb = -2 * np.ones((1, 2)) cls.ub = 2 * np.ones((1, 2)) @@ -386,7 +386,7 @@ class CRResModeHistoryTest(HistoryTest): @classmethod def setUpClass(cls): cls.optimizer = pypesto.optimize.ScipyOptimizer( - method='ls_trf', options={'max_nfev': 100} + method="ls_trf", options={"max_nfev": 100} ) problem = CRProblem() cls.obj = problem.get_objective(fim_for_hess=True) @@ -413,8 +413,8 @@ class FunModeHistoryTest(HistoryTest): @classmethod def setUpClass(cls): cls.optimizer = pypesto.optimize.ScipyOptimizer( - method='trust-exact', - options={'maxiter': 100}, + method="trust-exact", + options={"maxiter": 100}, ) cls.lb = 0 * np.ones((1, 2)) @@ -426,7 +426,7 @@ def test_trace_grad(self): self.obj = rosen_for_sensi( max_sensi_order=2, integrated=False, - )['obj'] + )["obj"] self.history_options = HistoryOptions( trace_record=True, @@ -440,7 +440,7 @@ def test_trace_grad_integrated(self): self.obj = rosen_for_sensi( max_sensi_order=2, integrated=True, - )['obj'] + )["obj"] self.history_options = HistoryOptions( trace_record=True, @@ -454,7 +454,7 @@ def test_trace_all(self): self.obj = rosen_for_sensi( max_sensi_order=2, integrated=True, - )['obj'] + )["obj"] self.history_options = HistoryOptions( trace_record=True, @@ -467,7 +467,7 @@ def test_trace_all(self): self.check_history() def test_trace_all_aggregated(self): - self.obj = rosen_for_sensi(max_sensi_order=2, integrated=True)['obj'] + self.obj = rosen_for_sensi(max_sensi_order=2, integrated=True)["obj"] self.history_options = HistoryOptions( trace_record=True, @@ -491,7 +491,7 @@ class CRFunModeHistoryTest(HistoryTest): @classmethod def setUpClass(cls): cls.optimizer = pypesto.optimize.ScipyOptimizer( - method='trust-exact', options={'maxiter': 100} + method="trust-exact", options={"maxiter": 100} ) problem = CRProblem() cls.obj = problem.get_objective(fim_for_hess=True) @@ -518,14 +518,14 @@ def test_trace_all(self): def history(request) -> pypesto.HistoryBase: # initialize history with the requested backend if request.param == "memory": - history = pypesto.MemoryHistory(options={'trace_record': True}) + history = pypesto.MemoryHistory(options={"trace_record": True}) elif request.param == "csv": - file = tempfile.mkstemp(suffix='.csv')[1] - history = pypesto.CsvHistory(file, options={'trace_record': True}) + file = tempfile.mkstemp(suffix=".csv")[1] + history = pypesto.CsvHistory(file, options={"trace_record": True}) elif request.param == "hdf5": - file = tempfile.mkstemp(suffix='.hdf5')[1] + file = tempfile.mkstemp(suffix=".hdf5")[1] history = pypesto.Hdf5History( - id="id", file=file, options={'trace_record': True} + id="id", file=file, options={"trace_record": True} ) elif request.param == "": history = pypesto.CountHistory() @@ -535,7 +535,7 @@ def history(request) -> pypesto.HistoryBase: # add some entries to the history for _ in range(10): result = {FVAL: np.random.randn(), GRAD: np.random.randn(7)} - history.update(np.random.randn(7), (0, 1), 'mode_fun', result) + history.update(np.random.randn(7), (0, 1), "mode_fun", result) history.finalize(message="some message", exitflag="some flag") return history @@ -583,15 +583,15 @@ def test_trace_subset(history: pypesto.HistoryBase): arr = list(range(0, len(history), 2)) for var in [ - 'fval', - 'grad', - 'hess', - 'res', - 'sres', - 'x', - 'time', + "fval", + "grad", + "hess", + "res", + "sres", + "x", + "time", ]: - getter = getattr(history, f'get_{var}_trace') + getter = getattr(history, f"get_{var}_trace") full_trace = getter() partial_trace = getter(arr) @@ -606,7 +606,7 @@ def test_trace_subset(history: pypesto.HistoryBase): # check individual type val = getter(0) - if var in ['fval', 'time']: + if var in ["fval", "time"]: assert isinstance(val, float), var else: assert isinstance(val, np.ndarray) or np.isnan(val), var @@ -634,8 +634,8 @@ def test_hdf5_history_mp(): objective=objective2, lb=lb, ub=ub, x_guesses=startpoints ) - optimizer1 = pypesto.optimize.ScipyOptimizer(options={'maxiter': 10}) - optimizer2 = pypesto.optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer1 = pypesto.optimize.ScipyOptimizer(options={"maxiter": 10}) + optimizer2 = pypesto.optimize.ScipyOptimizer(options={"maxiter": 10}) with tempfile.TemporaryDirectory(dir=".") as tmpdirname: _, fn = tempfile.mkstemp(".hdf5", dir=f"{tmpdirname}") @@ -670,13 +670,13 @@ def test_hdf5_history_mp(): ) for mp_res in result_memory_mp.optimize_result.list: for mem_res in result_hdf5_mem.optimize_result.list: - if mp_res['id'] == mem_res['id']: + if mp_res["id"] == mem_res["id"]: for entry in history_entries: hdf5_entry_trace = getattr( - mp_res['history'], f'get_{entry}_trace' + mp_res["history"], f"get_{entry}_trace" )() mem_entry_trace = getattr( - mem_res['history'], f'get_{entry}_trace' + mem_res["history"], f"get_{entry}_trace" )() for iteration in range(len(hdf5_entry_trace)): # comparing nan and None difficult @@ -695,19 +695,19 @@ def test_hdf5_amici_history(): objective1 = pypesto.Objective( fun=so.rosen, grad=so.rosen_der, hess=so.rosen_hess ) - objective2 = load_amici_objective('conversion_reaction')[0] + objective2 = load_amici_objective("conversion_reaction")[0] lb = -2 * np.ones((1, 2)) ub = 2 * np.ones((1, 2)) problem1 = pypesto.Problem(objective=objective1, lb=lb, ub=ub) problem2 = pypesto.Problem(objective=objective2, lb=lb, ub=ub) - optimizer = pypesto.optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = pypesto.optimize.ScipyOptimizer(options={"maxiter": 10}) with tempfile.TemporaryDirectory(dir=".") as tmpdirname: for f_ext, amici_history_class in zip( [".csv", ".hdf5"], [CsvAmiciHistory, Hdf5AmiciHistory] ): - _, fn = tempfile.mkstemp(f_ext, '{id}', dir=f"{tmpdirname}") + _, fn = tempfile.mkstemp(f_ext, "{id}", dir=f"{tmpdirname}") history_options = pypesto.HistoryOptions( trace_record=True, storage_file=fn @@ -724,7 +724,7 @@ def test_hdf5_amici_history(): result1.optimize_result.list[0].history, amici_history_class ) os.remove(fn) - os.remove(fn.replace('{id}', '0')) + os.remove(fn.replace("{id}", "0")) # optimizing with amici history saved in hdf5 result2 = pypesto.optimize.minimize( @@ -787,7 +787,7 @@ def test_trim_history(): def test_hd5_history_from_other(history: pypesto.HistoryBase): """Check that we can copy different histories to HDF5 and that the re-loaded history matches the original one.""" - hdf5_file = tempfile.mkstemp(suffix='.h5')[1] + hdf5_file = tempfile.mkstemp(suffix=".h5")[1] pypesto.Hdf5History.from_history(history, hdf5_file, id_="0") # write a second time to test `overwrite` argument diff --git a/test/base/test_logging.py b/test/base/test_logging.py index 66285dc51..5a44439ba 100644 --- a/test/base/test_logging.py +++ b/test/base/test_logging.py @@ -11,7 +11,7 @@ def test_optimize(): # logging filename = ".test_logging.tmp" pypesto.logging.log_to_file(logging.DEBUG, filename) - logger = logging.getLogger('pypesto') + logger = logging.getLogger("pypesto") if os.path.exists(filename): os.remove(filename) fh = logging.FileHandler(filename) @@ -31,7 +31,7 @@ def fun(_): problem = pypesto.Problem(objective, -1, 1) optimizer = pypesto.optimize.ScipyOptimizer() - options = {'allow_failed_starts': True} + options = {"allow_failed_starts": True} # optimization pypesto.optimize.minimize( @@ -46,7 +46,7 @@ def fun(_): # assert logging worked assert os.path.exists(filename) - f = open(filename, 'rb') + f = open(filename, "rb") content = str(f.read()) f.close() diff --git a/test/base/test_objective.py b/test/base/test_objective.py index 773301b79..73615be32 100644 --- a/test/base/test_objective.py +++ b/test/base/test_objective.py @@ -34,12 +34,12 @@ def test_evaluate(integrated): def _test_evaluate(struct): - obj = struct['obj'] - x = struct['x'] - fval_true = struct['fval'] - grad_true = struct['grad'] - hess_true = struct['hess'] - max_sensi_order = struct['max_sensi_order'] + obj = struct["obj"] + x = struct["x"] + fval_true = struct["fval"] + grad_true = struct["grad"] + hess_true = struct["hess"] + max_sensi_order = struct["max_sensi_order"] # check function values if max_sensi_order >= 2: @@ -82,9 +82,9 @@ def test_return_type(integrated, max_sensi_order): def _test_return_type(struct): - obj = struct['obj'] - x = struct['x'] - max_sensi_order = struct['max_sensi_order'] + obj = struct["obj"] + x = struct["x"] + max_sensi_order = struct["max_sensi_order"] ret = obj(x, (0,)) assert isinstance(ret, numbers.Number) @@ -112,9 +112,9 @@ def test_sensis(integrated, max_sensi_order): def _test_sensis(struct): - obj = struct['obj'] - x = struct['x'] - max_sensi_order = struct['max_sensi_order'] + obj = struct["obj"] + x = struct["x"] + max_sensi_order = struct["max_sensi_order"] obj(x, (0,)) if max_sensi_order >= 1: @@ -134,7 +134,7 @@ def test_finite_difference_checks(): Test the finite difference gradient check methods by expected relative error. """ - x = sp.Symbol('x') + x = sp.Symbol("x") # Setup single-parameter objective function fun_expr = x**10 @@ -161,7 +161,7 @@ def rel_err(eps_): np.array([theta]), eps=eps, verbosity=False ) np.testing.assert_almost_equal( - result_single_eps['rel_err'].squeeze(), + result_single_eps["rel_err"].squeeze(), rel_err(eps), ) @@ -172,7 +172,7 @@ def rel_err(eps_): ) np.testing.assert_almost_equal( - result_multi_eps['rel_err'].squeeze(), + result_multi_eps["rel_err"].squeeze(), min(rel_err(_eps) for _eps in multi_eps), ) @@ -186,30 +186,30 @@ def test_aesara(max_sensi_order, integrated): prob = rosen_for_sensi(max_sensi_order, integrated, [0, 1]) # create aesara specific symbolic tensor variables - x = aet.specify_shape(aet.vector('x'), (2,)) + x = aet.specify_shape(aet.vector("x"), (2,)) # apply inverse transform such that we evaluate at prob['x'] - x_ref = np.arcsinh(prob['x']) + x_ref = np.arcsinh(prob["x"]) # compose rosenbrock function with sinh transformation - obj = AesaraObjective(prob['obj'], x, aet.sinh(x)) + obj = AesaraObjective(prob["obj"], x, aet.sinh(x)) # check function values and derivatives, also after copy for _obj in (obj, copy.deepcopy(obj)): # function value - assert _obj(x_ref) == prob['fval'] + assert _obj(x_ref) == prob["fval"] # gradient if max_sensi_order > 0: assert np.allclose( - _obj(x_ref, sensi_orders=(1,)), prob['grad'] * np.cosh(x_ref) + _obj(x_ref, sensi_orders=(1,)), prob["grad"] * np.cosh(x_ref) ) # hessian if max_sensi_order > 1: assert np.allclose( - prob['hess'] * (np.diag(np.power(np.cosh(x_ref), 2))) - + np.diag(prob['grad'] * np.sinh(x_ref)), + prob["hess"] * (np.diag(np.power(np.cosh(x_ref), 2))) + + np.diag(prob["grad"] * np.sinh(x_ref)), _obj(x_ref, sensi_orders=(2,)), ) @@ -224,30 +224,30 @@ def test_jax(max_sensi_order, integrated): prob = rosen_for_sensi(max_sensi_order, integrated, [0, 1]) # apply inverse transform such that we evaluate at prob['x'] - x_ref = np.arcsinh(prob['x']) + x_ref = np.arcsinh(prob["x"]) def jac_op(x: jnp.array) -> jnp.array: return jax.lax.sinh(x) # compose rosenbrock function with sinh transformation - obj = JaxObjective(prob['obj'], jac_op) + obj = JaxObjective(prob["obj"], jac_op) # check function values and derivatives, also after copy for _obj in (obj, copy.deepcopy(obj)): # function value - assert _obj(x_ref) == prob['fval'] + assert _obj(x_ref) == prob["fval"] # gradient if max_sensi_order > 0: assert np.allclose( - _obj(x_ref, sensi_orders=(1,)), prob['grad'] * np.cosh(x_ref) + _obj(x_ref, sensi_orders=(1,)), prob["grad"] * np.cosh(x_ref) ) # hessian if max_sensi_order > 1: assert np.allclose( - prob['hess'] * (np.diag(np.power(np.cosh(x_ref), 2))) - + np.diag(prob['grad'] * np.sinh(x_ref)), + prob["hess"] * (np.diag(np.power(np.cosh(x_ref), 2))) + + np.diag(prob["grad"] * np.sinh(x_ref)), _obj(x_ref, sensi_orders=(2,)), ) @@ -329,7 +329,7 @@ def test_fds(fd_method, fd_delta): p = problem.p_true # check that function values coincide (call delegated) - for attr in ['fval', 'res']: + for attr in ["fval", "res"]: val = getattr(obj, f"get_{attr}")(p) val_fd = getattr(obj_fd, f"get_{attr}")(p) val_fd_grad = getattr(obj_fd_grad, f"get_{attr}")(p) @@ -347,7 +347,7 @@ def test_fds(fd_method, fd_delta): atol = rtol = 1e-4 else: atol = rtol = 1e-2 - for attr in ['grad', 'hess', 'sres']: + for attr in ["grad", "hess", "sres"]: val = getattr(obj, f"get_{attr}")(p) val_fd = getattr(obj_fd, f"get_{attr}")(p) val_fd_grad = getattr(obj_fd_grad, f"get_{attr}")(p) @@ -361,7 +361,7 @@ def test_fds(fd_method, fd_delta): # cannot completely coincide assert (val != val_fd_grad).any(), attr - if attr == 'hess': + if attr == "hess": assert (val_fd != val_fd_grad).any(), attr # should use available actual functionality assert (val == val_fd_fake).all(), attr diff --git a/test/base/test_prior.py b/test/base/test_prior.py index abda1fe34..8b4ef34d9 100644 --- a/test/base/test_prior.py +++ b/test/base/test_prior.py @@ -11,7 +11,7 @@ from pypesto.objective import NegLogParameterPriors from pypesto.objective.priors import get_parameter_prior_dict -scales = ['lin', 'log', 'log10'] +scales = ["lin", "log", "log10"] @pytest.fixture(params=scales) @@ -20,23 +20,23 @@ def scale(request): prior_type_lists = [ - ['uniform'], - ['normal'], - ['laplace'], - ['logNormal'], - ['parameterScaleUniform'], - ['parameterScaleNormal'], - ['parameterScaleLaplace'], - ['laplace', 'parameterScaleNormal', 'parameterScaleLaplace'], - ['laplace', 'logNormal', 'parameterScaleNormal', 'parameterScaleLaplace'], + ["uniform"], + ["normal"], + ["laplace"], + ["logNormal"], + ["parameterScaleUniform"], + ["parameterScaleNormal"], + ["parameterScaleLaplace"], + ["laplace", "parameterScaleNormal", "parameterScaleLaplace"], + ["laplace", "logNormal", "parameterScaleNormal", "parameterScaleLaplace"], [ - 'uniform', - 'normal', - 'laplace', - 'logNormal', - 'parameterScaleUniform', - 'parameterScaleNormal', - 'parameterScaleLaplace', + "uniform", + "normal", + "laplace", + "logNormal", + "parameterScaleUniform", + "parameterScaleNormal", + "parameterScaleLaplace", ], ] @@ -53,9 +53,9 @@ def test_mode(scale, prior_type_list): """ problem_dict = { - 'lin': {'lb': 0, 'ub': 3, 'opt': 1}, - 'log': {'lb': -3, 'ub': 3, 'opt': 0}, - 'log10': {'lb': -3, 'ub': 2, 'opt': 0}, + "lin": {"lb": 0, "ub": 3, "opt": 1}, + "log": {"lb": -3, "ub": 3, "opt": 0}, + "log10": {"lb": -3, "ub": 2, "opt": 0}, } prior_list = [ @@ -64,15 +64,15 @@ def test_mode(scale, prior_type_list): prior_type, ( [1, 2] - if prior_type in ['uniform', 'parameterScaleUniform'] + if prior_type in ["uniform", "parameterScaleUniform"] else [1, 1] ), scale, ) for iprior, prior_type in enumerate(prior_type_list) ] - ubs = np.asarray([problem_dict[scale]['ub'] for _ in prior_type_list]) - lbs = np.asarray([problem_dict[scale]['lb'] for _ in prior_type_list]) + ubs = np.asarray([problem_dict[scale]["ub"] for _ in prior_type_list]) + lbs = np.asarray([problem_dict[scale]["lb"] for _ in prior_type_list]) test_prior = NegLogParameterPriors(prior_list) test_problem = pypesto.Problem( @@ -86,23 +86,23 @@ def test_mode(scale, prior_type_list): topt = [] # test uniform distribution: for prior_type, prior in zip(prior_type_list, prior_list): - if prior_type.startswith('parameterScale'): - scale = 'lin' - if prior_type in ['uniform', 'parameterScaleUniform']: + if prior_type.startswith("parameterScale"): + scale = "lin" + if prior_type in ["uniform", "parameterScaleUniform"]: # check inside and outside of interval - funprior = prior['density_fun'] + funprior = prior["density_fun"] assert np.isinf(funprior(lin_to_scaled(0.5, scale))) assert np.isclose(funprior(lin_to_scaled(1.5, scale)), math.log(1)) assert np.isinf(funprior(lin_to_scaled(2.5, scale))) - resprior = prior['residual'] + resprior = prior["residual"] assert np.isinf(resprior(lin_to_scaled(0.5, scale))) assert np.isclose(resprior(lin_to_scaled(1.5, scale)), 0) assert np.isinf(resprior(lin_to_scaled(2.5, scale))) topt.append(np.nan) else: - topt.append(problem_dict[scale]['opt']) + topt.append(problem_dict[scale]["opt"]) - if prior_type.endswith('logNormal'): + if prior_type.endswith("logNormal"): assert not test_prior.has_res assert not test_prior.has_sres @@ -110,8 +110,8 @@ def test_mode(scale, prior_type_list): # test log-density based and residual representation if any(~np.isnan(topt)): - for method in ['L-BFGS-B', 'ls_trf']: - if method == 'ls_trf' and not test_prior.has_res: + for method in ["L-BFGS-B", "ls_trf"]: + if method == "ls_trf" and not test_prior.has_res: continue optimizer = pypesto.optimize.ScipyOptimizer(method=method) startpoints = pypesto.startpoint.UniformStartpoints( @@ -128,7 +128,7 @@ def test_mode(scale, prior_type_list): # flat functions don't have local minima, so dont check this # for uniform priors - num_optim = result.optimize_result.list[0]['x'][~np.isnan(topt)] + num_optim = result.optimize_result.list[0]["x"][~np.isnan(topt)] assert np.isclose( num_optim, topt[~np.isnan(topt)], atol=1e-03 ).all() @@ -145,7 +145,7 @@ def test_derivatives(prior_type_list, scale): prior_type, ( [-1, 1] - if prior_type in ['uniform', 'parameterScaleUniform'] + if prior_type in ["uniform", "parameterScaleUniform"] else [1, 1] ), scale, @@ -177,25 +177,25 @@ def lin_to_scaled(x: float, scale: str): """ transforms x to linear scale """ - if scale == 'lin': + if scale == "lin": return x - elif scale == 'log': + elif scale == "log": return math.log(x) - elif scale == 'log10': + elif scale == "log10": return math.log10(x) else: - ValueError(f'Unknown scale {scale}') + ValueError(f"Unknown scale {scale}") def scaled_to_lin(x: float, scale: str): """ transforms x to scale """ - if scale == 'lin': + if scale == "lin": return x - elif scale == 'log': + elif scale == "log": return math.exp(x) - elif scale == 'log10': + elif scale == "log10": return 10**x else: - ValueError(f'Unknown scale {scale}') + ValueError(f"Unknown scale {scale}") diff --git a/test/base/test_problem.py b/test/base/test_problem.py index cc2bf417b..93724dbb9 100644 --- a/test/base/test_problem.py +++ b/test/base/test_problem.py @@ -67,7 +67,7 @@ def test_fix_parameters(problem): problem.fix_parameters(3.5, 2) with pytest.raises(ValueError): - problem.fix_parameters(1, '2') + problem.fix_parameters(1, "2") def test_full_index_to_free_index(problem): @@ -81,30 +81,30 @@ def test_full_index_to_free_index(problem): def test_x_names(): """Test that `x_names` are handled properly.""" kwargs = { - 'objective': pypesto.Objective(), - 'lb': [-5] * 3, - 'ub': [4] * 3, - 'x_fixed_indices': [1], - 'x_fixed_vals': [42.0], + "objective": pypesto.Objective(), + "lb": [-5] * 3, + "ub": [4] * 3, + "x_fixed_indices": [1], + "x_fixed_vals": [42.0], } # non-unique values with pytest.raises(ValueError): - pypesto.Problem(x_names=['x1', 'x2', 'x2'], **kwargs) + pypesto.Problem(x_names=["x1", "x2", "x2"], **kwargs) # too few or too many arguments with pytest.raises(AssertionError): - pypesto.Problem(x_names=['x1', 'x2'], **kwargs) + pypesto.Problem(x_names=["x1", "x2"], **kwargs) with pytest.raises(AssertionError): - pypesto.Problem(x_names=['x1', 'x2', 'x3', 'x4'], **kwargs) + pypesto.Problem(x_names=["x1", "x2", "x3", "x4"], **kwargs) # all fine - problem = pypesto.Problem(x_names=['a', 'b', 'c'], **kwargs) - assert problem.x_names == ['a', 'b', 'c'] + problem = pypesto.Problem(x_names=["a", "b", "c"], **kwargs) + assert problem.x_names == ["a", "b", "c"] # defaults problem = pypesto.Problem(**kwargs) - assert problem.x_names == ['x0', 'x1', 'x2'] + assert problem.x_names == ["x0", "x1", "x2"] def test_out_of_bounds_x_guesses(caplog): diff --git a/test/base/test_store.py b/test/base/test_store.py index 8ff1cd574..d90a8030d 100644 --- a/test/base/test_store.py +++ b/test/base/test_store.py @@ -98,7 +98,7 @@ def test_storage_problem(hdf5_file): problem_attrs = [ value for name, value in vars(ProblemHDF5Writer).items() - if not name.startswith('_') and not callable(value) + if not name.startswith("_") and not callable(value) ] for attr in problem_attrs: if isinstance(problem.__dict__[attr], np.ndarray): @@ -132,8 +132,8 @@ def test_storage_trace(hdf5_file): objective=objective2, lb=lb, ub=ub, x_guesses=startpoints ) - optimizer1 = optimize.ScipyOptimizer(options={'maxiter': 10}) - optimizer2 = optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer1 = optimize.ScipyOptimizer(options={"maxiter": 10}) + optimizer2 = optimize.ScipyOptimizer(options={"maxiter": 10}) history_options_hdf5 = pypesto.HistoryOptions( trace_record=True, storage_file=hdf5_file @@ -163,10 +163,10 @@ def test_storage_trace(hdf5_file): ) for mem_res in result_memory.optimize_result.list: for hdf_res in result_hdf5.optimize_result.list: - if mem_res['id'] == hdf_res['id']: + if mem_res["id"] == hdf_res["id"]: for entry in history_entries: hdf5_entry_trace = getattr( - hdf_res['history'], f'get_{entry}_trace' + hdf_res["history"], f"get_{entry}_trace" )() for iteration in range(len(hdf5_entry_trace)): # comparing nan and None difficult @@ -177,7 +177,7 @@ def test_storage_trace(hdf5_file): continue np.testing.assert_array_equal( getattr( - mem_res['history'], f'get_{entry}_trace' + mem_res["history"], f"get_{entry}_trace" )()[iteration], hdf5_entry_trace[iteration], ) @@ -220,7 +220,7 @@ def test_storage_profiling(): progress_bar=False, ) - fn = 'test_file.hdf5' + fn = "test_file.hdf5" try: pypesto_profile_writer = ProfileResultHDF5Writer(fn) pypesto_profile_writer.write(profile_original) @@ -230,7 +230,7 @@ def test_storage_profiling(): for key in profile_original.profile_result.list[0][0].keys(): if ( profile_original.profile_result.list[0][0].keys is None - or key == 'time_path' + or key == "time_path" ): continue elif isinstance( @@ -281,11 +281,11 @@ def test_storage_sampling(): n_starts=n_starts, progress_bar=False, ) - x_0 = result_optimization.optimize_result[0]['x'] + x_0 = result_optimization.optimize_result[0]["x"] sampler = sample.AdaptiveParallelTemperingSampler( internal_sampler=sample.AdaptiveMetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, n_chains=1, ) @@ -296,7 +296,7 @@ def test_storage_sampling(): x0=[x_0], ) - fn = 'test_file.hdf5' + fn = "test_file.hdf5" try: pypesto_sample_writer = SamplingResultHDF5Writer(fn) pypesto_sample_writer.write(sample_original) @@ -304,7 +304,7 @@ def test_storage_sampling(): sample_read = pypesto_sample_reader.read() for key in sample_original.sample_result.keys(): - if sample_original.sample_result[key] is None or key == 'time': + if sample_original.sample_result[key] is None or key == "time": continue elif isinstance(sample_original.sample_result[key], np.ndarray): np.testing.assert_array_equal( @@ -356,7 +356,7 @@ def test_storage_all(): # Sampling sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, } ) result = sample.sample( @@ -366,7 +366,7 @@ def test_storage_all(): result=result, ) # Read and write - filename = 'test_file.hdf5' + filename = "test_file.hdf5" try: write_result(result=result, filename=filename) result_read = read_result(filename=filename) @@ -374,7 +374,7 @@ def test_storage_all(): # test optimize for i, opt_res in enumerate(result.optimize_result.list): for key in opt_res: - if key == 'history': + if key == "history": continue if isinstance(opt_res[key], np.ndarray): np.testing.assert_array_equal( @@ -387,7 +387,7 @@ def test_storage_all(): for key in result.profile_result.list[0][0].keys(): if ( result.profile_result.list[0][0].keys is None - or key == 'time_path' + or key == "time_path" ): continue elif isinstance(result.profile_result.list[0][0][key], np.ndarray): @@ -403,7 +403,7 @@ def test_storage_all(): # test sample for key in result.sample_result.keys(): - if result.sample_result[key] is None or key == 'time': + if result.sample_result[key] is None or key == "time": continue elif isinstance(result.sample_result[key], np.ndarray): np.testing.assert_array_equal( @@ -426,11 +426,11 @@ def test_storage_objective_config(): of the objective function. """ # create a problem with a function objective - problem_fun = create_problem(2, x_names=['a', 'b']) + problem_fun = create_problem(2, x_names=["a", "b"]) # create a problem with amici Objective problem_amici = create_petab_problem() # put together into aggregated objective - problem_agg = create_problem(2, x_names=['a', 'b']) + problem_agg = create_problem(2, x_names=["a", "b"]) objective_agg = pypesto.objective.AggregatedObjective( objectives=[problem_fun.objective, problem_amici.objective] ) @@ -439,7 +439,7 @@ def test_storage_objective_config(): config_amici = problem_amici.objective.get_config() config_agg = objective_agg.get_config() - fn = 'test_file.hdf5' + fn = "test_file.hdf5" try: writer = ProblemHDF5Writer(fn) writer.write(problem=problem_fun, overwrite=True) @@ -455,7 +455,7 @@ def test_storage_objective_config(): for key in config_amici: assert config_amici[key] == config_amici_r[key] for key in config_agg_r: - if key == 'type': + if key == "type": assert config_agg[key] == config_agg_r[key] else: assert len(config_agg_r[key]) == len(config_fun_r) or len( diff --git a/test/base/test_workflow.py b/test/base/test_workflow.py index 5f3764734..0ae2e9285 100644 --- a/test/base/test_workflow.py +++ b/test/base/test_workflow.py @@ -22,7 +22,7 @@ def close_fig(fun): @wraps(fun) def wrapped_fun(*args, **kwargs): ret = fun(*args, **kwargs) - plt.close('all') + plt.close("all") return ret return wrapped_fun diff --git a/test/base/test_x_fixed.py b/test/base/test_x_fixed.py index 56502dcbb..fc7ea2e00 100644 --- a/test/base/test_x_fixed.py +++ b/test/base/test_x_fixed.py @@ -46,7 +46,7 @@ def test_optimize(): def create_problem(): - objective = rosen_for_sensi(2)['obj'] + objective = rosen_for_sensi(2)["obj"] lb = [-3, -3, -3, -3, -3] ub = [3, 3, 3, 3, 3] x_fixed_indices = [1, 3] diff --git a/test/hierarchical/test_censored.py b/test/hierarchical/test_censored.py index bd5f4cdcd..c19e2bb14 100644 --- a/test/hierarchical/test_censored.py +++ b/test/hierarchical/test_censored.py @@ -21,12 +21,12 @@ example_censored_yaml = ( Path(__file__).parent - / '..' - / '..' - / 'doc' - / 'example' - / 'example_censored' - / 'example_censored.yaml' + / ".." + / ".." + / "doc" + / "example" + / "example_censored" + / "example_censored.yaml" ) @@ -35,7 +35,7 @@ def test_optimization(): petab_problem = petab.Problem.from_yaml(example_censored_yaml) optimizer = pypesto.optimize.ScipyOptimizer( - method='L-BFGS-B', options={'maxiter': 10} + method="L-BFGS-B", options={"maxiter": 10} ) importer = pypesto.petab.PetabImporter(petab_problem, hierarchical=True) @@ -48,13 +48,13 @@ def test_optimization(): problem=problem, n_starts=1, optimizer=optimizer ) # Check that optimization finished without infinite or nan values. - assert np.isfinite(result.optimize_result.list[0]['fval']) - assert np.all(np.isfinite(result.optimize_result.list[0]['x'])) - assert np.all(np.isfinite(result.optimize_result.list[0]['grad'][2:])) + assert np.isfinite(result.optimize_result.list[0]["fval"]) + assert np.all(np.isfinite(result.optimize_result.list[0]["x"])) + assert np.all(np.isfinite(result.optimize_result.list[0]["grad"][2:])) # Check that optimization finished with a lower objective value. assert ( - result.optimize_result.list[0]['fval'] - < result.optimize_result.list[0]['fval0'] + result.optimize_result.list[0]["fval"] + < result.optimize_result.list[0]["fval0"] ) @@ -100,7 +100,7 @@ def calculate(problem, x_dct): # with finite differences. assert np.allclose( finite_differences_results[1], - calculator_result['grad'], + calculator_result["grad"], ) @@ -111,11 +111,11 @@ def _inner_problem_exp(): data = np.full(simulation.shape, np.nan) data[4:6] = [5, 6] - par_types = ['cat_lb', 'cat_ub'] + par_types = ["cat_lb", "cat_ub"] categories = [1, 2, 4, 5, 1, 2, 4, 5] inner_parameter_ids = [ - f'{par_type}_{category}' + f"{par_type}_{category}" for par_type in par_types for category in set(categories) ] @@ -190,6 +190,6 @@ def test_ordinal_solver(): sigma=[np.full(len(simulation), 1 / np.sqrt(np.pi * 2))], )[0] - assert result['success'] is True - assert np.allclose(np.asarray(result['x']), expected_values, rtol=rtol) - assert np.allclose(result['fun'], 0, rtol=rtol) + assert result["success"] is True + assert np.allclose(np.asarray(result["x"]), expected_values, rtol=rtol) + assert np.allclose(result["fun"], 0, rtol=rtol) diff --git a/test/hierarchical/test_hierarchical.py b/test/hierarchical/test_hierarchical.py index 1889b032b..e9bee2709 100644 --- a/test/hierarchical/test_hierarchical.py +++ b/test/hierarchical/test_hierarchical.py @@ -86,8 +86,8 @@ def test_hierarchical_optimization_pipeline(): problems[True].set_x_guesses(startpoints[:, outer_indices]) inner_solvers = { - 'analytical': AnalyticalInnerSolver(), - 'numerical': NumericalInnerSolver(), + "analytical": AnalyticalInnerSolver(), + "numerical": NumericalInnerSolver(), } history_options = pypesto.HistoryOptions(trace_record=True) @@ -112,29 +112,29 @@ def get_result(problem, inner_solver_id, inner_solvers=inner_solvers): best_fval = result.optimize_result.list[0].fval result = { - 'list': result.optimize_result.list, - 'time': wall_time, - 'best_x': best_x, - 'best_fval': best_fval, + "list": result.optimize_result.list, + "time": wall_time, + "best_x": best_x, + "best_fval": best_fval, } return result results = {} for problem, inner_solver_id in [ - (problems[True], 'analytical'), + (problems[True], "analytical"), (problems[False], False), - (problems[True], 'numerical'), + (problems[True], "numerical"), ]: results[inner_solver_id] = get_result(problem, inner_solver_id) trace_False = np.array( - results[False]['list'][0].history.get_fval_trace(trim=True) + results[False]["list"][0].history.get_fval_trace(trim=True) ) trace_numerical = np.array( - results['numerical']['list'][0].history.get_fval_trace(trim=True) + results["numerical"]["list"][0].history.get_fval_trace(trim=True) ) trace_analytical = np.array( - results['numerical']['list'][0].history.get_fval_trace(trim=True) + results["numerical"]["list"][0].history.get_fval_trace(trim=True) ) # The analytical inner solver is at least as good as (fval / speed) the @@ -185,7 +185,7 @@ def calculate(problem, x_dct): x_dct = dict(zip(petab_problem.x_ids, petab_problem.x_nominal_scaled)) # Nominal sigma values are close to optimal. # One is changed here to facilitate testing. - x_dct['sd_pSTAT5A_rel'] = 0.5 + x_dct["sd_pSTAT5A_rel"] = 0.5 calculator_results = { flag: calculate(problems[flag], x_dct=x_dct) for flag in flags @@ -194,18 +194,18 @@ def calculate(problem, x_dct): # Hierarchical optimization means that the results differ here, because # the `False` case has suboptimal sigma values. assert not np.isclose( - calculator_results[True]['fval'], - calculator_results[False]['fval'], + calculator_results[True]["fval"], + calculator_results[False]["fval"], ) assert not np.isclose( - calculator_results[True]['grad'], - calculator_results[False]['grad'], + calculator_results[True]["grad"], + calculator_results[False]["grad"], ).all() for inner_idx, inner_par in enumerate( problems[True].objective.calculator.get_inner_par_ids() ): - x_dct[inner_par] = calculator_results[True]['inner_parameters'][ + x_dct[inner_par] = calculator_results[True]["inner_parameters"][ inner_idx ] calculator_results[False] = calculate(problem=problems[False], x_dct=x_dct) @@ -213,12 +213,12 @@ def calculate(problem, x_dct): # The `False` case has copied the optimal sigma values from hierarchical # optimization, so can produce the same results now. assert np.isclose( - calculator_results[True]['fval'], - calculator_results[False]['fval'], + calculator_results[True]["fval"], + calculator_results[False]["fval"], ) assert np.isclose( - calculator_results[True]['grad'], - calculator_results[False]['grad'], + calculator_results[True]["grad"], + calculator_results[False]["grad"], ).all() parameters = [x_dct[x_id] for x_id in petab_problem.x_free_ids] @@ -325,22 +325,22 @@ def inner_problem_exp(add_scaling: bool = True, add_offset: bool = True): timepoints = np.linspace(0, 3, 101) expected_values = { - 'scaling_': 5, - 'offset_': 2, - 'sigma_': 3, + "scaling_": 5, + "offset_": 2, + "sigma_": 3, } simulation = function(timepoints) data = copy.deepcopy(simulation) if add_scaling: - data *= expected_values['scaling_'] + data *= expected_values["scaling_"] if add_offset: - data += expected_values['offset_'] + data += expected_values["offset_"] - data[0::2] -= expected_values['sigma_'] - data[1::2] += expected_values['sigma_'] + data[0::2] -= expected_values["sigma_"] + data[1::2] += expected_values["sigma_"] mask = np.full(data.shape, True) @@ -355,16 +355,16 @@ def inner_problem_exp(add_scaling: bool = True, add_offset: bool = True): ) for inner_parameter_id, inner_parameter_type in [ ( - ('offset_', InnerParameterType.OFFSET) + ("offset_", InnerParameterType.OFFSET) if add_offset else (None, None) ), ( - ('scaling_', InnerParameterType.SCALING) + ("scaling_", InnerParameterType.SCALING) if add_scaling else (None, None) ), - ('sigma_', InnerParameterType.SIGMA), + ("sigma_", InnerParameterType.SIGMA), ] if inner_parameter_id is not None ] @@ -395,11 +395,11 @@ def test_analytical_inner_solver(): scaled=False, ) - assert np.isclose(result['offset_'], expected_values['offset_'], rtol=rtol) + assert np.isclose(result["offset_"], expected_values["offset_"], rtol=rtol) assert np.isclose( - result['scaling_'], expected_values['scaling_'], rtol=rtol + result["scaling_"], expected_values["scaling_"], rtol=rtol ) - assert np.isclose(result['sigma_'], expected_values['sigma_'], rtol=rtol) + assert np.isclose(result["sigma_"], expected_values["sigma_"], rtol=rtol) def test_numerical_inner_solver(): @@ -410,7 +410,7 @@ def test_numerical_inner_solver(): rtol = 1e-3 - solver = NumericalInnerSolver(minimize_kwargs={'n_starts': 10}) + solver = NumericalInnerSolver(minimize_kwargs={"n_starts": 10}) result = solver.solve( problem=inner_problem, sim=[simulation], @@ -418,11 +418,11 @@ def test_numerical_inner_solver(): scaled=False, ) - assert np.isclose(result['offset_'], expected_values['offset_'], rtol=rtol) + assert np.isclose(result["offset_"], expected_values["offset_"], rtol=rtol) assert np.isclose( - result['scaling_'], expected_values['scaling_'], rtol=rtol + result["scaling_"], expected_values["scaling_"], rtol=rtol ) - assert np.isclose(result['sigma_'], expected_values['sigma_'], rtol=rtol) + assert np.isclose(result["sigma_"], expected_values["sigma_"], rtol=rtol) def test_non_coupled_analytical_inner_solver(): @@ -442,8 +442,8 @@ def test_non_coupled_analytical_inner_solver(): sigma=[dummy_sigma], scaled=False, ) - assert np.isclose(result['offset_'], expected_values['offset_'], rtol=rtol) - assert np.isclose(result['sigma_'], expected_values['sigma_'], rtol=rtol) + assert np.isclose(result["offset_"], expected_values["offset_"], rtol=rtol) + assert np.isclose(result["sigma_"], expected_values["sigma_"], rtol=rtol) # Test for only scaling inner_problem, expected_values, simulation = inner_problem_exp( @@ -462,9 +462,9 @@ def test_non_coupled_analytical_inner_solver(): ) assert np.isclose( - result['scaling_'], expected_values['scaling_'], rtol=rtol + result["scaling_"], expected_values["scaling_"], rtol=rtol ) - assert np.isclose(result['sigma_'], expected_values['sigma_'], rtol=rtol) + assert np.isclose(result["sigma_"], expected_values["sigma_"], rtol=rtol) def test_constrained_inner_solver(): @@ -477,11 +477,11 @@ def test_constrained_inner_solver(): all_ub = [(7, 4), (4, 1), (4, 3), (6, 4)] all_expected_values = [ - {'scaling_': 6, 'offset_': 3}, - {'scaling_': 4, 'offset_': 1}, + {"scaling_": 6, "offset_": 3}, + {"scaling_": 4, "offset_": 1}, { - 'scaling_': 4, # all_lb[2][0], - 'offset_': np.clip( + "scaling_": 4, # all_lb[2][0], + "offset_": np.clip( compute_optimal_offset( data=inner_problem.data, sim=[simulation], @@ -494,7 +494,7 @@ def test_constrained_inner_solver(): ), }, { - 'scaling_': np.clip( + "scaling_": np.clip( compute_optimal_scaling( data=inner_problem.data, sim=[simulation], @@ -505,17 +505,17 @@ def test_constrained_inner_solver(): 4, # all_lb[3][0], 6, # all_ub[3][0], ), - 'offset_': 3, # all_lb[3][1], + "offset_": 3, # all_lb[3][1], }, ] for lb, ub, expected_values in zip(all_lb, all_ub, all_expected_values): # Set seed for reproducibility np.random.seed(1) - inner_problem.get_for_id('scaling_').lb = lb[0] - inner_problem.get_for_id('scaling_').ub = ub[0] - inner_problem.get_for_id('offset_').lb = lb[1] - inner_problem.get_for_id('offset_').ub = ub[1] + inner_problem.get_for_id("scaling_").lb = lb[0] + inner_problem.get_for_id("scaling_").ub = ub[0] + inner_problem.get_for_id("offset_").lb = lb[1] + inner_problem.get_for_id("offset_").ub = ub[1] copied_sim = copy.deepcopy(simulation) rtol = 1e-3 @@ -529,7 +529,7 @@ def test_constrained_inner_solver(): ) copied_sim = copy.deepcopy(simulation) - solver = NumericalInnerSolver(minimize_kwargs={'n_starts': 10}) + solver = NumericalInnerSolver(minimize_kwargs={"n_starts": 10}) num_res = solver.solve( problem=inner_problem, sim=[copied_sim], @@ -537,21 +537,21 @@ def test_constrained_inner_solver(): scaled=False, ) - assert np.isclose(ana_res['offset_'], num_res['offset_'], rtol=rtol) - assert np.isclose(ana_res['scaling_'], num_res['scaling_'], rtol=rtol) + assert np.isclose(ana_res["offset_"], num_res["offset_"], rtol=rtol) + assert np.isclose(ana_res["scaling_"], num_res["scaling_"], rtol=rtol) assert np.isclose( - ana_res['offset_'], expected_values['offset_'], rtol=rtol + ana_res["offset_"], expected_values["offset_"], rtol=rtol ) assert np.isclose( - ana_res['scaling_'], expected_values['scaling_'], rtol=rtol + ana_res["scaling_"], expected_values["scaling_"], rtol=rtol ) def test_non_coupled_constrained_inner_solver(): """Test non-coupled box-constrained hierarchical inner parameters.""" for current_par, add_scaling, add_offset, lb, ub in zip( - ['scaling_', 'scaling_', 'offset_', 'offset_'], + ["scaling_", "scaling_", "offset_", "offset_"], [True, True, False, False], [False, False, True, True], [6, None, 3, None], @@ -583,7 +583,7 @@ def test_non_coupled_constrained_inner_solver(): ) copied_sim = copy.deepcopy(simulation) - solver = NumericalInnerSolver(minimize_kwargs={'n_starts': 10}) + solver = NumericalInnerSolver(minimize_kwargs={"n_starts": 10}) num_res = solver.solve( problem=inner_problem, sim=[copied_sim], @@ -650,7 +650,7 @@ def test_validate(): pd.DataFrame( { petab.PARAMETER_ID: ["s"], - "parameterType": ['scaling'], + "parameterType": ["scaling"], "estimate": [1], } ) diff --git a/test/hierarchical/test_ordinal.py b/test/hierarchical/test_ordinal.py index 8bf4f95ff..ed0b1a681 100644 --- a/test/hierarchical/test_ordinal.py +++ b/test/hierarchical/test_ordinal.py @@ -47,12 +47,12 @@ example_ordinal_yaml = ( Path(__file__).parent - / '..' - / '..' - / 'doc' - / 'example' - / 'example_ordinal' - / 'example_ordinal.yaml' + / ".." + / ".." + / "doc" + / "example" + / "example_ordinal" + / "example_ordinal.yaml" ) @@ -79,7 +79,7 @@ def test_optimization(inner_options: list[dict]): # Set seed for reproducibility. np.random.seed(0) optimizer = pypesto.optimize.ScipyOptimizer( - method='L-BFGS-B', options={'maxiter': 10} + method="L-BFGS-B", options={"maxiter": 10} ) for option in inner_options: problem = _create_problem(petab_problem, option) @@ -87,13 +87,13 @@ def test_optimization(inner_options: list[dict]): problem=problem, n_starts=1, optimizer=optimizer ) # Check that optimization finished without infinite or nan values. - assert np.isfinite(result.optimize_result.list[0]['fval']) - assert np.all(np.isfinite(result.optimize_result.list[0]['x'])) - assert np.all(np.isfinite(result.optimize_result.list[0]['grad'][2:])) + assert np.isfinite(result.optimize_result.list[0]["fval"]) + assert np.all(np.isfinite(result.optimize_result.list[0]["x"])) + assert np.all(np.isfinite(result.optimize_result.list[0]["grad"][2:])) # Check that optimization finished with a lower objective value. assert ( - result.optimize_result.list[0]['fval'] - < result.optimize_result.list[0]['fval0'] + result.optimize_result.list[0]["fval"] + < result.optimize_result.list[0]["fval0"] ) @@ -186,36 +186,36 @@ def inner_calculate(problem, x_dct): # Check the inner calculator and the inner calculator collector # give the same results. assert np.allclose( - inner_calculator_results[STANDARD]['fval'], - calculator_results[STANDARD]['fval'], + inner_calculator_results[STANDARD]["fval"], + calculator_results[STANDARD]["fval"], ) assert np.allclose( - inner_calculator_results[STANDARD]['grad'], - calculator_results[STANDARD]['grad'], + inner_calculator_results[STANDARD]["grad"], + calculator_results[STANDARD]["grad"], ) # The results of the objective gradient and function value # should not depend on the method given. assert np.isclose( - calculator_results[STANDARD]['fval'], - calculator_results[REDUCED]['fval'], + calculator_results[STANDARD]["fval"], + calculator_results[REDUCED]["fval"], ) assert np.allclose( - calculator_results[STANDARD]['grad'], - calculator_results[REDUCED]['grad'], + calculator_results[STANDARD]["grad"], + calculator_results[REDUCED]["grad"], ) # Check that the gradient is the same as the one obtained # with finite differences. assert np.allclose( finite_differences_results[1], - calculator_results[STANDARD]['grad'], + calculator_results[STANDARD]["grad"], ) # Since the nominal parameters are close to true ones, # the fval and grad should both be low. - assert np.all(calculator_results[STANDARD]['fval'] < 0.2) - assert np.all(calculator_results[STANDARD]['grad'] < 0.1) + assert np.all(calculator_results[STANDARD]["fval"] < 0.2) + assert np.all(calculator_results[STANDARD]["grad"] < 0.1) def _inner_problem_exp(): @@ -248,7 +248,7 @@ def _inner_problem_exp(): categories = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] inner_parameter_ids = [ - f'{par_type}_{category}' + f"{par_type}_{category}" for par_type in par_types for category in set(categories) ] @@ -306,10 +306,10 @@ def test_ordinal_solver(): )[0] assert np.allclose( - standard_result['x'], expected_values[STANDARD], rtol=rtol + standard_result["x"], expected_values[STANDARD], rtol=rtol ) - assert np.allclose(standard_result['fun'], 0, rtol=rtol) - assert np.allclose(standard_result['jac'], 0, rtol=rtol) + assert np.allclose(standard_result["fun"], 0, rtol=rtol) + assert np.allclose(standard_result["jac"], 0, rtol=rtol) solver = OrdinalInnerSolver( options={METHOD: REDUCED, REPARAMETERIZED: False} @@ -322,10 +322,10 @@ def test_ordinal_solver(): )[0] assert np.all( - np.isclose(reduced_result['x'], expected_values[REDUCED], rtol=rtol) + np.isclose(reduced_result["x"], expected_values[REDUCED], rtol=rtol) ) - assert np.allclose(reduced_result['fun'], 0, rtol=rtol) - assert np.allclose(reduced_result['jac'], 0, rtol=rtol) + assert np.allclose(reduced_result["fun"], 0, rtol=rtol) + assert np.allclose(reduced_result["jac"], 0, rtol=rtol) def test_surrogate_data_analytical_calculation(): @@ -342,12 +342,12 @@ def test_surrogate_data_analytical_calculation(): n_categories = len(optimal_inner_parameters) / 2 expected_values = {} - expected_values['interval_range'] = max(sim) / (2 * n_categories + 1) - expected_values['interval_gap'] = max(sim) / (4 * (n_categories - 1) + 1) + expected_values["interval_range"] = max(sim) / (2 * n_categories + 1) + expected_values["interval_gap"] = max(sim) / (4 * (n_categories - 1) + 1) # As we have optimized the inner parameters, the surrogate data # should be the same as the simulation. - expected_values['surrogate_data'] = sim + expected_values["surrogate_data"] = sim options = { METHOD: STANDARD, @@ -372,9 +372,9 @@ def test_surrogate_data_analytical_calculation(): ) assert np.isclose( - interval_range, expected_values['interval_range'], rtol=rtol + interval_range, expected_values["interval_range"], rtol=rtol ) - assert np.isclose(interval_gap, expected_values['interval_gap'], rtol=rtol) + assert np.isclose(interval_gap, expected_values["interval_gap"], rtol=rtol) assert np.allclose( - surrogate_data, expected_values['surrogate_data'], rtol=rtol + surrogate_data, expected_values["surrogate_data"], rtol=rtol ) diff --git a/test/hierarchical/test_spline.py b/test/hierarchical/test_spline.py index 70e211e0e..3283c1a1f 100644 --- a/test/hierarchical/test_spline.py +++ b/test/hierarchical/test_spline.py @@ -34,10 +34,10 @@ inner_options = [ { - 'spline_ratio': spline_ratio, - 'min_diff_factor': min_diff_factor, - 'regularize_spline': regularize_spline, - 'regularization_factor': regularization_factor, + "spline_ratio": spline_ratio, + "min_diff_factor": min_diff_factor, + "regularize_spline": regularize_spline, + "regularization_factor": regularization_factor, } for spline_ratio in [1.0, 1 / 4] for min_diff_factor in [1 / 2, 0.0] @@ -47,12 +47,12 @@ example_semiquantitative_yaml = ( Path(__file__).parent - / '..' - / '..' - / 'doc' - / 'example' - / 'example_semiquantitative' - / 'example_semiquantitative_linear.yaml' + / ".." + / ".." + / "doc" + / "example" + / "example_semiquantitative" + / "example_semiquantitative_linear.yaml" ) @@ -75,13 +75,13 @@ def test_optimization(inner_options: dict): problem=problem, n_starts=1, optimizer=optimizer ) # Check that optimization finished without infinite or nan values. - assert np.isfinite(result.optimize_result.list[0]['fval']) - assert np.all(np.isfinite(result.optimize_result.list[0]['x'])) - assert np.all(np.isfinite(result.optimize_result.list[0]['grad'][2:])) + assert np.isfinite(result.optimize_result.list[0]["fval"]) + assert np.all(np.isfinite(result.optimize_result.list[0]["x"])) + assert np.all(np.isfinite(result.optimize_result.list[0]["grad"][2:])) # Check that optimization finished with a lower value. assert ( - result.optimize_result.list[0]['fval'] - < result.optimize_result.list[0]['fval0'] + result.optimize_result.list[0]["fval"] + < result.optimize_result.list[0]["fval0"] ) @@ -108,13 +108,13 @@ def test_spline_calculator_and_objective(): problems = {} options = { - 'minimal_diff_on': { - 'spline_ratio': 1 / 2, - 'min_diff_factor': 1 / 2, + "minimal_diff_on": { + "spline_ratio": 1 / 2, + "min_diff_factor": 1 / 2, }, - 'minimal_diff_off': { - 'spline_ratio': 1 / 2, - 'min_diff_factor': 0.0, + "minimal_diff_off": { + "spline_ratio": 1 / 2, + "min_diff_factor": 0.0, }, } @@ -181,13 +181,13 @@ def inner_calculate(problem, x_dct): # Check the inner calculator and the inner calculator collector # give the same results. assert np.allclose( - inner_calculator_results['minimal_diff_on']['fval'], - calculator_results['minimal_diff_on']['fval'], + inner_calculator_results["minimal_diff_on"]["fval"], + calculator_results["minimal_diff_on"]["fval"], atol=atol, ) assert np.allclose( - inner_calculator_results['minimal_diff_on']['grad'], - calculator_results['minimal_diff_on']['grad'], + inner_calculator_results["minimal_diff_on"]["grad"], + calculator_results["minimal_diff_on"]["grad"], atol=atol, ) @@ -195,20 +195,20 @@ def inner_calculate(problem, x_dct): # will not depend on whether we constrain minimal difference. # In general, this is not the case. assert np.isclose( - calculator_results['minimal_diff_on']['fval'], - calculator_results['minimal_diff_off']['fval'], + calculator_results["minimal_diff_on"]["fval"], + calculator_results["minimal_diff_off"]["fval"], atol=atol, ) assert np.allclose( - calculator_results['minimal_diff_on']['grad'], - calculator_results['minimal_diff_off']['grad'], + calculator_results["minimal_diff_on"]["grad"], + calculator_results["minimal_diff_off"]["grad"], atol=atol, ) # The gradient should be close to the one calculated using # finite differences. assert np.allclose( - calculator_results['minimal_diff_on']['grad'], + calculator_results["minimal_diff_on"]["grad"], FD_results[1], atol=atol, ) @@ -217,9 +217,9 @@ def inner_calculate(problem, x_dct): # the fval and grad should both be low. expected_fval = np.log(2 * np.pi) * 18 / 2 assert np.isclose( - calculator_results['minimal_diff_on']['fval'], expected_fval, atol=atol + calculator_results["minimal_diff_on"]["fval"], expected_fval, atol=atol ) - assert np.all(calculator_results['minimal_diff_off']['grad'] < grad_atol) + assert np.all(calculator_results["minimal_diff_off"]["grad"] < grad_atol) def test_extract_expdata_using_mask(): @@ -260,22 +260,22 @@ def _inner_problem_exp(): n_spline_pars = int(np.ceil(spline_ratio * len(timepoints))) expected_values = { - 'fun': np.log(2 * np.pi) * n_timepoints / 2, - 'jac': np.zeros(n_spline_pars), - 'x': np.asarray([0.0, 2.0, 2.0, 2.0, 2.0, 2.0]), + "fun": np.log(2 * np.pi) * n_timepoints / 2, + "jac": np.zeros(n_spline_pars), + "x": np.asarray([0.0, 2.0, 2.0, 2.0, 2.0, 2.0]), } - par_type = 'spline' + par_type = "spline" mask = [np.full(len(simulation), True)] inner_parameters = [ SplineInnerParameter( - inner_parameter_id=f'{par_type}_{1}_{par_index+1}', + inner_parameter_id=f"{par_type}_{1}_{par_index+1}", inner_parameter_type=InnerParameterType.SPLINE, scale=LIN, lb=-np.inf, ub=np.inf, - observable_id='obs1', + observable_id="obs1", ixs=mask, index=par_index + 1, group=1, @@ -298,11 +298,11 @@ def test_spline_inner_solver(): inner_problem, expected_values, simulation, sigma = _inner_problem_exp() options = { - 'minimal_diff_on': { - 'min_diff_factor': 1 / 2, + "minimal_diff_on": { + "min_diff_factor": 1 / 2, }, - 'minimal_diff_off': { - 'min_diff_factor': 0.0, + "minimal_diff_off": { + "min_diff_factor": 0.0, }, } @@ -324,13 +324,13 @@ def test_spline_inner_solver(): for minimal_diff in options.keys(): assert np.isclose( - results[minimal_diff][0]['fun'], expected_values['fun'], rtol=rtol + results[minimal_diff][0]["fun"], expected_values["fun"], rtol=rtol ) assert np.allclose( - results[minimal_diff][0]['jac'], expected_values['jac'], rtol=rtol + results[minimal_diff][0]["jac"], expected_values["jac"], rtol=rtol ) assert np.allclose( - results[minimal_diff][0]['x'], expected_values['x'], rtol=rtol + results[minimal_diff][0]["x"], expected_values["x"], rtol=rtol ) diff --git a/test/julia/test_pyjulia.py b/test/julia/test_pyjulia.py index f72b4d601..dc0abcdef 100644 --- a/test/julia/test_pyjulia.py +++ b/test/julia/test_pyjulia.py @@ -73,15 +73,15 @@ def test_petabJL_interface(): problem = importer.create_problem(precompile=False) parameters = np.genfromtxt( - f'{examples_dir}/{model_name}/Boehm_validation/Parameter.csv', - delimiter=',', + f"{examples_dir}/{model_name}/Boehm_validation/Parameter.csv", + delimiter=",", skip_header=1, ) # check objective function obj_ref = np.genfromtxt( - f'{examples_dir}/{model_name}/Boehm_validation/ObjectiveValue.csv', - delimiter=',', + f"{examples_dir}/{model_name}/Boehm_validation/ObjectiveValue.csv", + delimiter=",", skip_header=1, ) obj = problem.objective(parameters, sensi_orders=(0,)) @@ -90,8 +90,8 @@ def test_petabJL_interface(): # check gradient value grad_ref = np.genfromtxt( - f'{examples_dir}/{model_name}/Boehm_validation/Gradient.csv', - delimiter=',', + f"{examples_dir}/{model_name}/Boehm_validation/Gradient.csv", + delimiter=",", skip_header=1, ) grad = problem.objective(parameters, sensi_orders=(1,)) @@ -100,8 +100,8 @@ def test_petabJL_interface(): # check hessian value hess_ref = np.genfromtxt( - f'{examples_dir}/{model_name}/Boehm_validation/Hessian.csv', - delimiter=',', + f"{examples_dir}/{model_name}/Boehm_validation/Hessian.csv", + delimiter=",", skip_header=1, ) hess = problem.objective(parameters, sensi_orders=(2,)) diff --git a/test/optimize/test_optimize.py b/test/optimize/test_optimize.py index 509192309..947909485 100644 --- a/test/optimize/test_optimize.py +++ b/test/optimize/test_optimize.py @@ -356,8 +356,8 @@ def test_mpipoolengine(): # get the path to this file: path = os.path.dirname(__file__) # run the example file. - subprocess.check_call( # noqa: S603,S607 - [ + subprocess.check_call( + [ # noqa: S603,S607 "mpiexec", "--oversubscribe", "-np", @@ -548,7 +548,8 @@ def test_ess_multiprocess(problem, request): ess = ESSOptimizer( max_iter=20, # also test passing a callable as local_optimizer - local_optimizer=lambda max_walltime_s, **kwargs: optimize.FidesOptimizer( + local_optimizer=lambda max_walltime_s, + **kwargs: optimize.FidesOptimizer( options={FidesOptions.MAXTIME: max_walltime_s} ), ) diff --git a/test/petab/test_amici_objective.py b/test/petab/test_amici_objective.py index 537d07b47..2b6a7cb2c 100644 --- a/test/petab/test_amici_objective.py +++ b/test/petab/test_amici_objective.py @@ -25,13 +25,13 @@ def test_add_sim_grad_to_opt_grad(): Test gradient mapping/summation works as expected. 17 = 1 + 2*5 + 2*3 """ - par_opt_ids = ['opt_par_1', 'opt_par_2', 'opt_par_3'] + par_opt_ids = ["opt_par_1", "opt_par_2", "opt_par_3"] mapping_par_opt_to_par_sim = { - 'sim_par_1': 'opt_par_1', - 'sim_par_2': 'opt_par_3', - 'sim_par_3': 'opt_par_3', + "sim_par_1": "opt_par_1", + "sim_par_2": "opt_par_3", + "sim_par_3": "opt_par_3", } - par_sim_ids = ['sim_par_1', 'sim_par_2', 'sim_par_3'] + par_sim_ids = ["sim_par_1", "sim_par_2", "sim_par_3"] sim_grad = np.asarray([1.0, 3.0, 5.0]) opt_grad = np.asarray([1.0, 1.0, 1.0]) @@ -59,11 +59,11 @@ def test_error_leastsquares_with_ssigma(): importer = pypesto.petab.PetabImporter(petab_problem) obj = importer.create_objective() problem = importer.create_problem( - obj, startpoint_kwargs={'check_fval': True, 'check_grad': True} + obj, startpoint_kwargs={"check_fval": True, "check_grad": True} ) optimizer = pypesto.optimize.ScipyOptimizer( - 'ls_trf', options={'max_nfev': 50} + "ls_trf", options={"max_nfev": 50} ) with pytest.raises(RuntimeError): optimize.minimize( @@ -84,7 +84,7 @@ def test_preeq_guesses(): """ model_name = "Brannmark_JBC2010" importer = pypesto.petab.PetabImporter.from_yaml( - os.path.join(models.MODELS_DIR, model_name, model_name + '.yaml') + os.path.join(models.MODELS_DIR, model_name, model_name + ".yaml") ) problem = importer.create_problem() obj = problem.objective @@ -96,7 +96,7 @@ def test_preeq_guesses(): obj.amici_solver.setRelativeTolerance(1e-12) # assert that initial guess is uninformative - assert obj.steadystate_guesses['fval'] == np.inf + assert obj.steadystate_guesses["fval"] == np.inf optimizer = optimize.ScipyOptimizer() startpoints = pypesto.startpoint.UniformStartpoints(check_fval=False) @@ -109,14 +109,14 @@ def test_preeq_guesses(): progress_bar=False, ) - assert obj.steadystate_guesses['fval'] < np.inf - assert len(obj.steadystate_guesses['data']) == len(obj.edatas) + assert obj.steadystate_guesses["fval"] < np.inf + assert len(obj.steadystate_guesses["data"]) == len(obj.edatas) # check that we have test a problem where plist is nontrivial assert any(len(e.plist) != len(e.parameters) for e in obj.edatas) df = obj.check_grad( problem.get_reduced_vector( - result.optimize_result.list[0]['x'], problem.x_free_indices + result.optimize_result.list[0]["x"], problem.x_free_indices ), eps=1e-3, verbosity=0, @@ -128,4 +128,4 @@ def test_preeq_guesses(): # assert that resetting works problem.objective.initialize() - assert obj.steadystate_guesses['fval'] == np.inf + assert obj.steadystate_guesses["fval"] == np.inf diff --git a/test/petab/test_amici_predictor.py b/test/petab/test_amici_predictor.py index bdb860255..f081a9e18 100644 --- a/test/petab/test_amici_predictor.py +++ b/test/petab/test_amici_predictor.py @@ -21,15 +21,15 @@ @pytest.fixture() def conversion_reaction_model(): # read in sbml file - model_name = 'conversion_reaction' + model_name = "conversion_reaction" example_dir = os.path.join( - os.path.dirname(__file__), '..', '..', 'doc', 'example' + os.path.dirname(__file__), "..", "..", "doc", "example" ) sbml_file = os.path.join( - example_dir, model_name, f'model_{model_name}.xml' + example_dir, model_name, f"model_{model_name}.xml" ) model_output_dir = os.path.join( - example_dir, 'tmp', f'{model_name}_enhanced' + example_dir, "tmp", f"{model_name}_enhanced" ) # try to import the exisiting model, if possible @@ -47,42 +47,42 @@ def conversion_reaction_model(): def create_observable(sbml_model, obs_id): # create a parameter, which will get a rule assignmed as observable parameter = sbml_model.createParameter() - parameter.setId(f'observable_{obs_id}') - parameter.setName(f'observable_{obs_id}') + parameter.setId(f"observable_{obs_id}") + parameter.setName(f"observable_{obs_id}") parameter.constant = True rule = sbml_importer.sbml.createAssignmentRule() - rule.setId(f'observable_{obs_id}') - rule.setName(f'observable_{obs_id}') - rule.setVariable(f'observable_{obs_id}') + rule.setId(f"observable_{obs_id}") + rule.setName(f"observable_{obs_id}") + rule.setVariable(f"observable_{obs_id}") rule.setFormula(obs_id) # add initial assignments to sbml model def create_intial_assignment(sbml_model, spec_id): # create a parameter, which will get a rule assignmed as observable parameter = sbml_model.createParameter() - parameter.setId(f'{spec_id}0') - parameter.setName(f'{spec_id}0') + parameter.setId(f"{spec_id}0") + parameter.setName(f"{spec_id}0") parameter.constant = True assignment = sbml_importer.sbml.createInitialAssignment() - assignment.setSymbol(f'{spec_id}') + assignment.setSymbol(f"{spec_id}") math = ( '' - f'{spec_id}0' + f"{spec_id}0" ) assignment.setMath(libsbml.readMathMLFromString(math)) - for spec in ('A', 'B'): + for spec in ("A", "B"): create_observable(sbml_importer.sbml, spec) create_intial_assignment(sbml_importer.sbml, spec) # add constant parameters and observables to AMICI model - constant_parameters = ['A0', 'B0'] + constant_parameters = ["A0", "B0"] observables = amici.assignmentRules2observables( sbml_importer.sbml, # the libsbml model object filter_function=lambda variable: variable.getId().startswith( - 'observable_' + "observable_" ), ) # generate the python module for the model. @@ -100,15 +100,15 @@ def create_intial_assignment(sbml_model, spec_id): model = model_module.getModel() except RuntimeError as err: print( - 'pyPESTO unit test ran into an error importing the conversion ' - 'reaction enhanced model. This may happen due to an old version ' - 'of this model being present in your python path (e.g., ' - 'incorrect AMICI version comparing to the installed one). ' - 'Delete the conversion_reaction_enhanced model from your python ' - 'path and retry. Your python path is currently:' + "pyPESTO unit test ran into an error importing the conversion " + "reaction enhanced model. This may happen due to an old version " + "of this model being present in your python path (e.g., " + "incorrect AMICI version comparing to the installed one). " + "Delete the conversion_reaction_enhanced model from your python " + "path and retry. Your python path is currently:" ) print(sys.path) - print('Original error message:') + print("Original error message:") raise err return model @@ -151,8 +151,8 @@ def check_outputs(predicted, out, n_cond, n_timepoints, n_obs, n_par): # check whether conversion to dict worked well preDict = dict(predicted) assert isinstance(preDict, dict) - assert len(preDict['conditions']) == n_cond - for cond in preDict['conditions']: + assert len(preDict["conditions"]) == n_cond + for cond in preDict["conditions"]: assert isinstance(cond, dict) # correct shape for outputs? @@ -188,25 +188,25 @@ def test_simple_prediction(edata_objects): # assert folder is there with all files # remove file is already existing - if os.path.exists('deleteme'): - shutil.rmtree('deleteme') + if os.path.exists("deleteme"): + shutil.rmtree("deleteme") p = default_predictor( - x, output_file='deleteme.csv', sensi_orders=(1,), output_format='csv' + x, output_file="deleteme.csv", sensi_orders=(1,), output_format="csv" ) check_outputs(p, out=(1,), n_cond=1, n_timepoints=10, n_obs=2, n_par=2) # check created files - assert os.path.exists('deleteme') - assert set(os.listdir('deleteme')) == { - 'deleteme_0__s0.csv', - 'deleteme_0__s1.csv', + assert os.path.exists("deleteme") + assert set(os.listdir("deleteme")) == { + "deleteme_0__s0.csv", + "deleteme_0__s1.csv", } - shutil.rmtree('deleteme') + shutil.rmtree("deleteme") # assert h5 file is there - p = default_predictor(x, output_file='deleteme.h5', output_format='h5') + p = default_predictor(x, output_file="deleteme.h5", output_format="h5") check_outputs(p, out=(0,), n_cond=1, n_timepoints=10, n_obs=2, n_par=2) - assert os.path.exists('deleteme.h5') - os.remove('deleteme.h5') + assert os.path.exists("deleteme.h5") + os.remove("deleteme.h5") def test_complex_prediction(edata_objects): @@ -216,7 +216,7 @@ def test_complex_prediction(edata_objects): def pp_out(raw_outputs): # compute ratios of simulations across conditions - amici_y = [raw_output['y'] for raw_output in raw_outputs] + amici_y = [raw_output["y"] for raw_output in raw_outputs] outs1 = np.array( [ amici_y[0][:, 1] / amici_y[0][:, 0], @@ -238,8 +238,8 @@ def pp_out(raw_outputs): return [outs1, outs2] def pps_out(raw_outputs): - amici_y = [raw_output['y'] for raw_output in raw_outputs] - amici_sy = [raw_output['sy'] for raw_output in raw_outputs] + amici_y = [raw_output["y"] for raw_output in raw_outputs] + amici_sy = [raw_output["sy"] for raw_output in raw_outputs] # compute ratios of simulations across conditions (yes, I know this is # symbolically wrong, but we only check the shape of the outputs...) s_outs1 = np.zeros((10, 2, 5)) @@ -288,7 +288,7 @@ def pps_out(raw_outputs): return [s_outs1, s_outs2] def ppt_out(raw_outputs): - amici_t = [raw_output['t'] for raw_output in raw_outputs] + amici_t = [raw_output["t"] for raw_output in raw_outputs] # compute ratios of simulations across conditions t_out1 = amici_t[0] t_out2 = amici_t[1] @@ -305,7 +305,7 @@ def ppt_out(raw_outputs): post_processor=pp_out, post_processor_sensi=pps_out, post_processor_time=ppt_out, - output_ids=[f'ratio_{i_obs}' for i_obs in range(5)], + output_ids=[f"ratio_{i_obs}" for i_obs in range(5)], ) # let's set the parameter vector x = np.array([3.0, 0.5]) @@ -316,53 +316,53 @@ def ppt_out(raw_outputs): # assert folder is there with all files # remove file is already existing - if os.path.exists('deleteme'): - shutil.rmtree('deleteme') + if os.path.exists("deleteme"): + shutil.rmtree("deleteme") p = complex_predictor( - x, output_file='deleteme.csv', sensi_orders=(0, 1), output_format='csv' + x, output_file="deleteme.csv", sensi_orders=(0, 1), output_format="csv" ) check_outputs(p, out=(0, 1), n_cond=2, n_timepoints=10, n_obs=5, n_par=2) # check created files - assert os.path.exists('deleteme') + assert os.path.exists("deleteme") expected_files = { - 'deleteme_0.csv', - 'deleteme_0__s0.csv', - 'deleteme_0__s1.csv', - 'deleteme_1.csv', - 'deleteme_1__s0.csv', - 'deleteme_1__s1.csv', + "deleteme_0.csv", + "deleteme_0__s0.csv", + "deleteme_0__s1.csv", + "deleteme_1.csv", + "deleteme_1__s0.csv", + "deleteme_1__s1.csv", } - assert set(os.listdir('deleteme')) == expected_files - shutil.rmtree('deleteme') + assert set(os.listdir("deleteme")) == expected_files + shutil.rmtree("deleteme") # assert h5 file is there p = complex_predictor( - x, output_file='deleteme.h5', sensi_orders=(0, 1), output_format='h5' + x, output_file="deleteme.h5", sensi_orders=(0, 1), output_format="h5" ) check_outputs(p, out=(0, 1), n_cond=2, n_timepoints=10, n_obs=5, n_par=2) - assert os.path.exists('deleteme.h5') - os.remove('deleteme.h5') + assert os.path.exists("deleteme.h5") + os.remove("deleteme.h5") def test_petab_prediction(): """ Test prediction via PEtab """ - model_name = 'conversion_reaction' + model_name = "conversion_reaction" # get the PEtab model yaml_file = os.path.join( os.path.dirname(__file__), - '..', - '..', - 'doc', - 'example', + "..", + "..", + "doc", + "example", model_name, - f'{model_name}.yaml', + f"{model_name}.yaml", ) petab_problem = petab.Problem.from_yaml(yaml_file) # import PEtab problem - petab_problem.model_name = f'{model_name}_petab' + petab_problem.model_name = f"{model_name}_petab" importer = pypesto.petab.PetabImporter(petab_problem) # create prediction via PEtab predictor = importer.create_predictor() @@ -380,12 +380,12 @@ def test_petab_prediction(): # read a set of ensemble vectors from the csv ensemble_file = os.path.join( os.path.dirname(__file__), - '..', - '..', - 'doc', - 'example', + "..", + "..", + "doc", + "example", model_name, - 'parameter_ensemble.tsv', + "parameter_ensemble.tsv", ) ensemble = pypesto.ensemble.read_from_csv( ensemble_file, @@ -398,13 +398,13 @@ def test_petab_prediction(): summary = ensemble.compute_summary(percentiles_list=[10, 25, 75, 90]) assert isinstance(summary, dict) assert set(summary.keys()) == { - 'mean', - 'std', - 'median', - 'percentile 10', - 'percentile 25', - 'percentile 75', - 'percentile 90', + "mean", + "std", + "median", + "percentile 10", + "percentile 25", + "percentile 75", + "percentile 90", } parameter_identifiability = ensemble.check_identifiability() @@ -416,7 +416,7 @@ def test_petab_prediction(): ) # check some of the basic functionality: compressing output to large arrays ensemble_prediction.condense_to_arrays() - for field in ('timepoints', 'output', 'output_sensi'): + for field in ("timepoints", "output", "output_sensi"): isinstance(ensemble_prediction.prediction_arrays[field], np.ndarray) # computing summaries @@ -426,13 +426,13 @@ def test_petab_prediction(): # define some short hands pred = ensemble_prediction.prediction_summary keyset = { - 'mean', - 'std', - 'median', - 'percentile 5', - 'percentile 20', - 'percentile 80', - 'percentile 95', + "mean", + "std", + "median", + "percentile 5", + "percentile 20", + "percentile 80", + "percentile 95", } # check some properties assert set(pred.keys()) == keyset @@ -440,13 +440,13 @@ def test_petab_prediction(): assert pred[key].comment == key # check some particular properties of this example - assert pred['mean'].conditions[0].output[0, 0] == 1.0 - assert pred['median'].conditions[0].output[0, 0] == 1.0 - assert pred['std'].conditions[0].output[0, 0] == 0.0 + assert pred["mean"].conditions[0].output[0, 0] == 1.0 + assert pred["median"].conditions[0].output[0, 0] == 1.0 + assert pred["std"].conditions[0].output[0, 0] == 0.0 # check writing to h5 pypesto.ensemble.write_ensemble_prediction_to_h5( - ensemble_prediction, 'deleteme_ensemble.h5' + ensemble_prediction, "deleteme_ensemble.h5" ) - assert os.path.exists('deleteme_ensemble.h5') - os.remove('deleteme_ensemble.h5') + assert os.path.exists("deleteme_ensemble.h5") + os.remove("deleteme_ensemble.h5") diff --git a/test/petab/test_petab_import.py b/test/petab/test_petab_import.py index 2c56abb64..de4f7f3da 100644 --- a/test/petab/test_petab_import.py +++ b/test/petab/test_petab_import.py @@ -22,11 +22,11 @@ # In CI, bionetgen is installed here BNGPATH = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', '..', 'BioNetGen-2.8.5') + os.path.join(os.path.dirname(__file__), "..", "..", "BioNetGen-2.8.5") ) -if 'BNGPATH' not in os.environ: +if "BNGPATH" not in os.environ: logging.warning(f"Env var BNGPATH was not set. Setting to {BNGPATH}") - os.environ['BNGPATH'] = BNGPATH + os.environ["BNGPATH"] = BNGPATH class PetabImportTest(unittest.TestCase): @@ -40,7 +40,7 @@ def test_0_import(self): for model_name in ["Zheng_PNAS2012", "Boehm_JProteomeRes2014"]: # test yaml import for one model: yaml_config = os.path.join( - models.MODELS_DIR, model_name, model_name + '.yaml' + models.MODELS_DIR, model_name, model_name + ".yaml" ) petab_problem = petab.Problem.from_yaml(yaml_config) self.petab_problems.append(petab_problem) @@ -98,7 +98,7 @@ def test_4_optimize(self): for obj_edatas, importer in zip(self.obj_edatas, self.petab_importers): obj = obj_edatas[0] optimizer = pypesto.optimize.ScipyOptimizer( - options={'maxiter': 10} + options={"maxiter": 10} ) problem = importer.create_problem(obj) startpoints = importer.create_startpoint_method() @@ -117,7 +117,7 @@ def test_check_gradients(self): # Check gradients of simple model (should always be a true positive) model_name = "Bachmann_MSB2011" petab_problem = pypesto.petab.PetabImporter.from_yaml( - os.path.join(models.MODELS_DIR, model_name, model_name + '.yaml') + os.path.join(models.MODELS_DIR, model_name, model_name + ".yaml") ) objective = petab_problem.create_objective() @@ -138,7 +138,7 @@ def test_plist_mapping(): edata.plist).""" model_name = "Boehm_JProteomeRes2014" petab_problem = pypesto.petab.PetabImporter.from_yaml( - os.path.join(models.MODELS_DIR, model_name, model_name + '.yaml') + os.path.join(models.MODELS_DIR, model_name, model_name + ".yaml") ) # define test parameter @@ -169,7 +169,7 @@ def test_max_sensi_order(): correctly.""" model_name = "Boehm_JProteomeRes2014" problem = pypesto.petab.PetabImporter.from_yaml( - os.path.join(models.MODELS_DIR, model_name, model_name + '.yaml') + os.path.join(models.MODELS_DIR, model_name, model_name + ".yaml") ) # define test parameter @@ -209,14 +209,14 @@ def test_max_sensi_order(): def test_petab_pysb_optimization(): - test_case = '0001' + test_case = "0001" test_case_dir = petabtests.get_case_dir( - test_case, version='v2.0.0', format_='pysb' + test_case, version="v2.0.0", format_="pysb" ) petab_yaml = test_case_dir / petabtests.problem_yaml_name(test_case) # expected results solution = petabtests.load_solution( - test_case, format='pysb', version='v2.0.0' + test_case, format="pysb", version="v2.0.0" ) petab_problem = petab.Problem.from_yaml(petab_yaml) @@ -241,7 +241,7 @@ def test_petab_pysb_optimization(): assert np.all(fvals <= -solution[petabtests.LLH]) -if __name__ == '__main__': +if __name__ == "__main__": suite = unittest.TestSuite() suite.addTest(PetabImportTest()) unittest.main() diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index 02dde9401..c7b47a244 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -65,10 +65,10 @@ def _execute_case(case, model_type, version): model_name = ( f'petab_test_case_{case}_{model_type}_{version.replace(".", "_" )}' ) - output_folder = f'amici_models/{model_name}' + output_folder = f"amici_models/{model_name}" # import and create objective function - if case.startswith('0006'): + if case.startswith("0006"): petab_problem = petab.Problem.from_yaml(yaml_file) petab.flatten_timepoint_specific_output_overrides(petab_problem) importer = pypesto.petab.PetabImporter( @@ -92,14 +92,14 @@ def _execute_case(case, model_type, version): ret = obj(problem_parameters, sensi_orders=(0,), return_dict=True) # extract results - rdatas = ret['rdatas'] - chi2 = sum(rdata['chi2'] for rdata in rdatas) - llh = -ret['fval'] + rdatas = ret["rdatas"] + chi2 = sum(rdata["chi2"] for rdata in rdatas) + llh = -ret["fval"] simulation_df = amici.petab_objective.rdatas_to_measurement_df( rdatas, model, importer.petab_problem.measurement_df ) - if case.startswith('0006'): + if case.startswith("0006"): simulation_df = petab.unflatten_simulation_df( simulation_df, petab_problem ) diff --git a/test/petab/test_sbml_conversion.py b/test/petab/test_sbml_conversion.py index ad86d280b..22653a0a6 100644 --- a/test/petab/test_sbml_conversion.py +++ b/test/petab/test_sbml_conversion.py @@ -14,24 +14,24 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) optimizers = { - 'scipy': [ - 'Nelder-Mead', - 'Powell', - 'CG', - 'BFGS', - 'Newton-CG', - 'L-BFGS-B', - 'TNC', - 'COBYLA', - 'SLSQP', - 'trust-ncg', - 'trust-exact', - 'trust-krylov', - 'ls_trf', - 'ls_dogbox', + "scipy": [ + "Nelder-Mead", + "Powell", + "CG", + "BFGS", + "Newton-CG", + "L-BFGS-B", + "TNC", + "COBYLA", + "SLSQP", + "trust-ncg", + "trust-exact", + "trust-krylov", + "ls_trf", + "ls_dogbox", ], # disabled: ,'trust-constr', 'ls_lm', 'dogleg' - 'pyswarm': [''], + "pyswarm": [""], } ATOL = 1e-2 @@ -40,7 +40,7 @@ class AmiciObjectiveTest(unittest.TestCase): def runTest(self): - for example in ['conversion_reaction']: + for example in ["conversion_reaction"]: objective, model = load_amici_objective(example) x0 = np.array(list(model.getParameters())) @@ -82,21 +82,21 @@ def runTest(self): def parameter_estimation(objective, library, solver, fixed_pars, n_starts): - if re.match(r'(?i)^(ls_)', solver): - options = {'max_nfev': 10} + if re.match(r"(?i)^(ls_)", solver): + options = {"max_nfev": 10} else: - options = {'maxiter': 10} + options = {"maxiter": 10} - if library == 'scipy': + if library == "scipy": optimizer = pypesto.optimize.ScipyOptimizer( method=solver, options=options ) - elif library == 'pyswarm': + elif library == "pyswarm": optimizer = pypesto.optimize.PyswarmOptimizer(options=options) else: raise ValueError("This code should not be reached") - optimizer.temp_file = os.path.join('test', 'tmp_{index}.csv') + optimizer.temp_file = os.path.join("test", "tmp_{index}.csv") dim = len(objective.x_ids) lb = -2 * np.ones((1, dim)) @@ -126,7 +126,7 @@ def parameter_estimation(objective, library, solver, fixed_pars, n_starts): ) -if __name__ == '__main__': +if __name__ == "__main__": suite = unittest.TestSuite() suite.addTest(AmiciObjectiveTest()) unittest.main() diff --git a/test/profile/test_profile.py b/test/profile/test_profile.py index a1371c948..328525914 100644 --- a/test/profile/test_profile.py +++ b/test/profile/test_profile.py @@ -25,7 +25,7 @@ class ProfilerTest(unittest.TestCase): def setUp(cls): cls.objective: ObjectiveBase = rosen_for_sensi( max_sensi_order=2, integrated=True - )['obj'] + )["obj"] with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -39,10 +39,10 @@ def setUp(cls): def test_default_profiling(self): # loop over methods for creating new initial guesses method_list = [ - 'fixed_step', - 'adaptive_step_order_0', - 'adaptive_step_order_1', - 'adaptive_step_regression', + "fixed_step", + "adaptive_step_order_0", + "adaptive_step_order_1", + "adaptive_step_regression", ] for i_run, method in enumerate(method_list): # run profiling @@ -65,39 +65,39 @@ def test_default_profiling(self): self.assertEqual(len(result.profile_result.list[i_run]), 2) # check whether profiling needed maybe too many steps - steps = result.profile_result.list[i_run][0]['ratio_path'].size - if method == 'adaptive_step_regression': + steps = result.profile_result.list[i_run][0]["ratio_path"].size + if method == "adaptive_step_regression": self.assertTrue( steps < 20, - 'Profiling with regression based ' - 'proposal needed too many steps.', + "Profiling with regression based " + "proposal needed too many steps.", ) self.assertTrue( steps > 1, - 'Profiling with regression based ' - 'proposal needed not enough steps.', + "Profiling with regression based " + "proposal needed not enough steps.", ) - elif method == 'adaptive_step_order_1': + elif method == "adaptive_step_order_1": self.assertTrue( steps < 25, - 'Profiling with 1st order based ' - 'proposal needed too many steps.', + "Profiling with 1st order based " + "proposal needed too many steps.", ) self.assertTrue( steps > 1, - 'Profiling with 1st order based ' - 'proposal needed not enough steps.', + "Profiling with 1st order based " + "proposal needed not enough steps.", ) - elif method == 'adaptive_step_order_0': + elif method == "adaptive_step_order_0": self.assertTrue( steps < 100, - 'Profiling with 0th order based ' - 'proposal needed too many steps.', + "Profiling with 0th order based " + "proposal needed too many steps.", ) self.assertTrue( steps > 1, - 'Profiling with 0th order based ' - 'proposal needed not enough steps.', + "Profiling with 0th order based " + "proposal needed not enough steps.", ) # standard plotting @@ -120,7 +120,7 @@ def test_engine_profiling(self): problem=self.problem, result=self.result, optimizer=self.optimizer, - next_guess_method='fixed_step', + next_guess_method="fixed_step", engine=engine, progress_bar=False, ) @@ -129,10 +129,10 @@ def test_engine_profiling(self): for count, _engine in enumerate(engines[1:]): for j in range(len(self.result.profile_result.list[0])): assert_almost_equal( - self.result.profile_result.list[0][j]['x_path'], - self.result.profile_result.list[count][j]['x_path'], - err_msg='The values of the profiles for' - ' the different engines do not match', + self.result.profile_result.list[0][j]["x_path"], + self.result.profile_result.list[count][j]["x_path"], + err_msg="The values of the profiles for" + " the different engines do not match", ) def test_selected_profiling(self): @@ -154,7 +154,7 @@ def test_selected_profiling(self): result=self.result, optimizer=self.optimizer, profile_index=np.array([1]), - next_guess_method='fixed_step', + next_guess_method="fixed_step", result_index=1, profile_options=options, progress_bar=False, @@ -187,7 +187,7 @@ def test_selected_profiling(self): problem=self.problem, result=result, optimizer=self.optimizer, - next_guess_method='fixed_step', + next_guess_method="fixed_step", profile_index=np.array([0]), profile_options=options, progress_bar=False, @@ -204,7 +204,7 @@ def test_extending_profiles(self): problem=self.problem, result=self.result, optimizer=self.optimizer, - next_guess_method='fixed_step', + next_guess_method="fixed_step", progress_bar=False, ) @@ -217,7 +217,7 @@ def test_extending_profiles(self): problem=self.problem, result=result, optimizer=self.optimizer, - next_guess_method='fixed_step', + next_guess_method="fixed_step", profile_index=np.array([1]), profile_list=0, progress_bar=False, @@ -264,7 +264,7 @@ def test_approximate_profiles(self): # dont make this a class method such that we dont optimize twice def test_profile_with_history(): - objective = rosen_for_sensi(max_sensi_order=2, integrated=False)['obj'] + objective = rosen_for_sensi(max_sensi_order=2, integrated=False)["obj"] with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -286,7 +286,7 @@ def test_profile_with_history(): result.optimize_result.list[0].x[3], ], ) - problem.objective.history = pypesto.MemoryHistory({'trace_record': True}) + problem.objective.history = pypesto.MemoryHistory({"trace_record": True}) profile.parameter_profile( problem=problem, result=result, @@ -301,7 +301,7 @@ def test_profile_with_history(): @close_fig def test_profile_with_fixed_parameters(): """Test using profiles with fixed parameters.""" - obj = rosen_for_sensi(max_sensi_order=1)['obj'] + obj = rosen_for_sensi(max_sensi_order=1)["obj"] lb = -2 * np.ones(5) ub = 2 * np.ones(5) @@ -313,7 +313,7 @@ def test_profile_with_fixed_parameters(): x_fixed_indices=[0, 3], ) - optimizer = optimize.ScipyOptimizer(options={'maxiter': 50}) + optimizer = optimize.ScipyOptimizer(options={"maxiter": 50}) result = optimize.minimize( problem=problem, optimizer=optimizer, @@ -323,10 +323,10 @@ def test_profile_with_fixed_parameters(): for i_method, next_guess_method in enumerate( [ - 'fixed_step', - 'adaptive_step_order_0', - 'adaptive_step_order_1', - 'adaptive_step_regression', + "fixed_step", + "adaptive_step_order_0", + "adaptive_step_order_1", + "adaptive_step_regression", ] ): print(next_guess_method) @@ -344,20 +344,20 @@ def test_profile_with_fixed_parameters(): visualize.profile_cis(result, profile_list=i_method) # test profiling with all parameters fixed but one - problem.fix_parameters([2, 3, 4], result.optimize_result.list[0]['x'][2:5]) + problem.fix_parameters([2, 3, 4], result.optimize_result.list[0]["x"][2:5]) profile.parameter_profile( problem=problem, result=result, optimizer=optimizer, - next_guess_method='adaptive_step_regression', + next_guess_method="adaptive_step_regression", progress_bar=False, ) def create_optimization_results(objective, dim_full=2): # create optimizer, pypesto problem and options - options = {'maxiter': 200} - optimizer = optimize.ScipyOptimizer(method='l-bfgs-b', options=options) + options = {"maxiter": 200} + optimizer = optimize.ScipyOptimizer(method="l-bfgs-b", options=options) lb = -2 * np.ones(dim_full) ub = 2 * np.ones(dim_full) @@ -446,7 +446,7 @@ def test_gh1165(lb, ub): Check profiles with non-symmetric bounds and whole_path=True span the full parameter domain. """ - obj = rosen_for_sensi(max_sensi_order=1)['obj'] + obj = rosen_for_sensi(max_sensi_order=1)["obj"] problem = pypesto.Problem( objective=obj, @@ -454,7 +454,7 @@ def test_gh1165(lb, ub): ub=ub, ) - optimizer = optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) result = optimize.minimize( problem=problem, optimizer=optimizer, @@ -467,7 +467,7 @@ def test_gh1165(lb, ub): problem=problem, result=result, optimizer=optimizer, - next_guess_method='fixed_step', + next_guess_method="fixed_step", profile_index=[par_idx], progress_bar=False, profile_options=profile.ProfileOptions( @@ -479,7 +479,7 @@ def test_gh1165(lb, ub): ), ) # parameter value of the profiled parameter - x_path = result.profile_result.list[0][par_idx]['x_path'][par_idx, :] + x_path = result.profile_result.list[0][par_idx]["x_path"][par_idx, :] # ensure we cover lb..ub assert x_path[0] == lb[par_idx], (x_path.min(), lb[par_idx]) assert x_path[-1] == ub[par_idx], (x_path.max(), ub[par_idx]) diff --git a/test/run_notebook.sh b/test/run_notebook.sh index 58f2c77fb..c885350c5 100755 --- a/test/run_notebook.sh +++ b/test/run_notebook.sh @@ -28,6 +28,7 @@ nbs_1=( 'ordinal_data.ipynb' 'censored_data.ipynb' 'semiquantitative_data.ipynb' + 'getting_started.ipynb' ) # Sampling notebooks diff --git a/test/sample/test_sample.py b/test/sample/test_sample.py index 6eb2b6330..1591d9213 100644 --- a/test/sample/test_sample.py +++ b/test/sample/test_sample.py @@ -43,7 +43,7 @@ def nllh(x): objective = pypesto.Objective(fun=nllh) problem = pypesto.Problem( - objective=objective, lb=[-10], ub=[10], x_names=['x'] + objective=objective, lb=[-10], ub=[10], x_names=["x"] ) return problem @@ -63,7 +63,7 @@ def nllh(x): objective = pypesto.Objective(fun=nllh) problem = pypesto.Problem( - objective=objective, lb=[-100], ub=[200], x_names=['x'] + objective=objective, lb=[-100], ub=[200], x_names=["x"] ) return problem @@ -96,7 +96,7 @@ def rosenbrock_problem(): def create_petab_problem(): current_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.abspath( - os.path.join(current_path, '..', '..', 'doc', 'example') + os.path.join(current_path, "..", "..", "doc", "example") ) # import to petab petab_problem = petab.Problem.from_yaml( @@ -116,7 +116,7 @@ def sample_petab_problem(): sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) result = sample.sample( @@ -146,66 +146,66 @@ def negative_log_prior(x): @pytest.fixture( params=[ - 'Metropolis', - 'AdaptiveMetropolis', - 'ParallelTempering', - 'AdaptiveParallelTempering', - 'Pymc', - 'Emcee', - 'Dynesty', + "Metropolis", + "AdaptiveMetropolis", + "ParallelTempering", + "AdaptiveParallelTempering", + "Pymc", + "Emcee", + "Dynesty", ] ) def sampler(request): - if request.param == 'Metropolis': + if request.param == "Metropolis": return sample.MetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) - elif request.param == 'AdaptiveMetropolis': + elif request.param == "AdaptiveMetropolis": return sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) - elif request.param == 'ParallelTempering': + elif request.param == "ParallelTempering": return sample.ParallelTemperingSampler( internal_sampler=sample.MetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, betas=[1, 1e-2, 1e-4], ) - elif request.param == 'AdaptiveParallelTempering': + elif request.param == "AdaptiveParallelTempering": return sample.AdaptiveParallelTemperingSampler( internal_sampler=sample.AdaptiveMetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, n_chains=5, ) - elif request.param == 'Pymc': + elif request.param == "Pymc": return PymcSampler(tune=5, progressbar=False) - elif request.param == 'Emcee': + elif request.param == "Emcee": return sample.EmceeSampler(nwalkers=10) - elif request.param == 'Dynesty': + elif request.param == "Dynesty": return sample.DynestySampler() -@pytest.fixture(params=['gaussian', 'gaussian_mixture', 'rosenbrock']) +@pytest.fixture(params=["gaussian", "gaussian_mixture", "rosenbrock"]) def problem(request): - if request.param == 'gaussian': + if request.param == "gaussian": return gaussian_problem() - if request.param == 'gaussian_mixture': + if request.param == "gaussian_mixture": return gaussian_mixture_problem() - elif request.param == 'rosenbrock': + elif request.param == "rosenbrock": return rosenbrock_problem() def test_pipeline(sampler, problem): """Check that a typical pipeline runs through.""" # optimization - optimizer = optimize.ScipyOptimizer(options={'maxiter': 10}) + optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) result = optimize.minimize( problem=problem, n_starts=3, @@ -233,7 +233,7 @@ def test_ground_truth(): sampler = sample.AdaptiveParallelTemperingSampler( internal_sampler=sample.AdaptiveMetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, n_chains=5, ) @@ -257,11 +257,11 @@ def test_ground_truth(): # test against different distributions - statistic, pval = kstest(samples, 'norm') + statistic, pval = kstest(samples, "norm") print(statistic, pval) assert statistic < 0.1 - statistic, pval = kstest(samples, 'uniform') + statistic, pval = kstest(samples, "uniform") print(statistic, pval) assert statistic > 0.1 @@ -275,7 +275,7 @@ def test_ground_truth_separated_modes(): sampler = sample.AdaptiveParallelTemperingSampler( internal_sampler=sample.AdaptiveMetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, n_chains=3, ) @@ -309,7 +309,7 @@ def test_ground_truth_separated_modes(): # initiated around the "first" mode of the distribution sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) result = sample.sample( @@ -339,7 +339,7 @@ def test_ground_truth_separated_modes(): # initiated around the "second" mode of the distribution sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) result = sample.sample( @@ -372,7 +372,7 @@ def test_multiple_startpoints(): sampler = sample.ParallelTemperingSampler( internal_sampler=sample.MetropolisSampler(), options={ - 'show_progress': False, + "show_progress": False, }, n_chains=2, ) @@ -426,7 +426,7 @@ def test_geweke_test_unconverged(): sampler = sample.MetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) @@ -455,7 +455,7 @@ def test_autocorrelation_pipeline(): sampler = sample.MetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) @@ -505,7 +505,7 @@ def test_autocorrelation_short_chain(): sampler = sample.MetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) @@ -591,12 +591,12 @@ def test_empty_prior(): # define pypesto problem without prior object test_problem = pypesto.Problem( - objective=posterior_fun, lb=-10, ub=10, x_names=['x'] + objective=posterior_fun, lb=-10, ub=10, x_names=["x"] ) sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) @@ -632,12 +632,12 @@ def test_prior(): x_priors_defs=prior_object, lb=-10, ub=10, - x_names=['x'], + x_names=["x"], ) sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) @@ -678,7 +678,7 @@ def test_samples_cis(): # set a sampler sampler = sample.MetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) diff --git a/test/select/test_select.py b/test/select/test_select.py index 06944d00e..38c2359ad 100644 --- a/test/select/test_select.py +++ b/test/select/test_select.py @@ -26,17 +26,17 @@ model_problem_options = { # Options sent to `pypesto.optimize.optimize.minimize`, to reduce run time. - 'minimize_options': { - 'engine': pypesto.engine.MultiProcessEngine(), - 'n_starts': 20, - 'filename': None, - 'progress_bar': False, + "minimize_options": { + "engine": pypesto.engine.MultiProcessEngine(), + "n_starts": 20, + "filename": None, + "progress_bar": False, } } # Tolerances for the differences between expected and test values. tolerances = { - 'rtol': 1e-2, - 'atol': 1e-2, + "rtol": 1e-2, + "atol": 1e-2, } @@ -45,10 +45,10 @@ def petab_problem_yaml() -> Path: """The location of the PEtab problem YAML file.""" return ( Path(__file__).parent.parent.parent - / 'doc' - / 'example' - / 'model_selection' - / 'example_modelSelection.yaml' + / "doc" + / "example" + / "model_selection" + / "example_modelSelection.yaml" ) @@ -57,10 +57,10 @@ def petab_select_problem_yaml() -> Path: """The location of the PEtab Select problem YAML file.""" return ( Path(__file__).parent.parent.parent - / 'doc' - / 'example' - / 'model_selection' - / 'petab_select_problem.yaml' + / "doc" + / "example" + / "model_selection" + / "petab_select_problem.yaml" ) @@ -80,22 +80,22 @@ def pypesto_select_problem(petab_select_problem) -> pypesto.select.Problem: def initial_models(petab_problem_yaml) -> List[Model]: """Models that can be used to initialize a search.""" initial_model_1 = Model( - model_id='myModel1', + model_id="myModel1", petab_yaml=petab_problem_yaml, parameters={ - 'k1': 0, - 'k2': 0, - 'k3': 0, + "k1": 0, + "k2": 0, + "k3": 0, }, criteria={Criterion.AIC: np.inf}, ) initial_model_2 = Model( - model_id='myModel2', + model_id="myModel2", petab_yaml=petab_problem_yaml, parameters={ - 'k1': ESTIMATE, - 'k2': ESTIMATE, - 'k3': 0, + "k1": ESTIMATE, + "k2": ESTIMATE, + "k3": 0, }, criteria={Criterion.AIC: np.inf}, ) @@ -107,24 +107,24 @@ def test_problem_select(pypesto_select_problem): """Test the `Problem.select` method.""" expected_results = [ { - 'candidates_model_subspace_ids': ['M1_0'], - 'best_model_subspace_id': 'M1_0', - 'best_model_aic': 36.97, + "candidates_model_subspace_ids": ["M1_0"], + "best_model_subspace_id": "M1_0", + "best_model_aic": 36.97, }, { - 'candidates_model_subspace_ids': ['M1_1', 'M1_2', 'M1_3'], - 'best_model_subspace_id': 'M1_3', - 'best_model_aic': -4.71, + "candidates_model_subspace_ids": ["M1_1", "M1_2", "M1_3"], + "best_model_subspace_id": "M1_3", + "best_model_aic": -4.71, }, { - 'candidates_model_subspace_ids': ['M1_5', 'M1_6'], - 'best_model_subspace_id': 'M1_6', - 'best_model_aic': -4.15, + "candidates_model_subspace_ids": ["M1_5", "M1_6"], + "best_model_subspace_id": "M1_6", + "best_model_aic": -4.15, }, { - 'candidates_model_subspace_ids': ['M1_7'], - 'best_model_subspace_id': 'M1_7', - 'best_model_aic': -4.06, + "candidates_model_subspace_ids": ["M1_7"], + "best_model_subspace_id": "M1_7", + "best_model_aic": -4.06, }, ] @@ -152,15 +152,15 @@ def test_problem_select(pypesto_select_problem): test_best_model_aic = best_model.get_criterion(Criterion.AIC) test_result = { - 'candidates_model_subspace_ids': test_candidates_model_subspace_ids, - 'best_model_subspace_id': test_best_model_subspace_id, - 'best_model_aic': test_best_model_aic, + "candidates_model_subspace_ids": test_candidates_model_subspace_ids, + "best_model_subspace_id": test_best_model_subspace_id, + "best_model_aic": test_best_model_aic, } # The expected "forward" models were found. assert ( - test_result['candidates_model_subspace_ids'] - == expected_result['candidates_model_subspace_ids'] + test_result["candidates_model_subspace_ids"] + == expected_result["candidates_model_subspace_ids"] ) # The best model is as expected. @@ -172,7 +172,7 @@ def test_problem_select(pypesto_select_problem): # The best model has its criterion value set and is the expected value. assert np.isclose( [test_result["best_model_aic"]], - [expected_result['best_model_aic']], + [expected_result["best_model_aic"]], **tolerances, ) @@ -193,11 +193,11 @@ def test_problem_select_to_completion(pypesto_select_problem): ) expected_calibrated_models_subspace_ids = { - 'M1_0', - 'M1_1', - 'M1_4', - 'M1_5', - 'M1_7', + "M1_0", + "M1_1", + "M1_4", + "M1_5", + "M1_7", } test_calibrated_models_subspace_ids = { model.model_subspace_id @@ -213,12 +213,12 @@ def test_problem_select_to_completion(pypesto_select_problem): # The first iteration is from a virtual model, which will appear # as the best model for that iteration. VIRTUAL_INITIAL_MODEL, - 'M1_0', - 'M1_1', + "M1_0", + "M1_1", # This iteration with models `{'M1_4', 'M1_5'}` didn't have a better # model than the previous iteration. - 'M1_1', - 'M1_7', + "M1_1", + "M1_7", ] test_best_model_subspace_ids = [ (model.model_subspace_id if model != VIRTUAL_INITIAL_MODEL else model) @@ -263,7 +263,7 @@ def test_problem_multistart_select(pypesto_select_problem, initial_models): model_problem_options=model_problem_options, ) - expected_best_model_subspace_id = 'M1_3' + expected_best_model_subspace_id = "M1_3" test_best_model_subspace_id = best_model.model_subspace_id # The best model is as expected. assert test_best_model_subspace_id == expected_best_model_subspace_id @@ -277,13 +277,13 @@ def test_problem_multistart_select(pypesto_select_problem, initial_models): ] expected_best_models_criterion_values = { - 'M1_3': -4.705, + "M1_3": -4.705, # 'M1_7': -4.056, # skipped -- reproducibility requires many starts } test_best_models_criterion_values = { model.model_subspace_id: model.get_criterion(Criterion.AIC) for model in best_models - if model.model_subspace_id != 'M1_7' # skipped, see above + if model.model_subspace_id != "M1_7" # skipped, see above } # The best models are as expected and have the expected criterion values. pd.testing.assert_series_equal( @@ -297,10 +297,10 @@ def test_problem_multistart_select(pypesto_select_problem, initial_models): for initial_model in initial_models } expected_predecessor_model_hashes = { - 'M1_1': initial_model_id_hash_map['myModel1'], - 'M1_2': initial_model_id_hash_map['myModel1'], - 'M1_3': initial_model_id_hash_map['myModel1'], - 'M1_7': initial_model_id_hash_map['myModel2'], + "M1_1": initial_model_id_hash_map["myModel1"], + "M1_2": initial_model_id_hash_map["myModel1"], + "M1_3": initial_model_id_hash_map["myModel1"], + "M1_7": initial_model_id_hash_map["myModel2"], } test_predecessor_model_hashes = { model.model_subspace_id: model.predecessor_model_hash @@ -312,7 +312,7 @@ def test_problem_multistart_select(pypesto_select_problem, initial_models): def test_postprocessors(petab_select_problem): """Test model calibration postprocessors.""" - output_path = Path('output') + output_path = Path("output") output_path.mkdir(exist_ok=True, parents=True) postprocessor_1 = partial( pypesto.select.postprocessors.save_postprocessor, @@ -327,7 +327,7 @@ def test_postprocessors(petab_select_problem): postprocessors=[postprocessor_1, postprocessor_2], ) model_problem_options = { - 'postprocessor': multi_postprocessor, + "postprocessor": multi_postprocessor, } pypesto_select_problem = pypesto.select.Problem( petab_select_problem=petab_select_problem, @@ -341,7 +341,7 @@ def test_postprocessors(petab_select_problem): model_problem_options=model_problem_options, ) - expected_newly_calibrated_models_subspace_ids = ['M1_0'] + expected_newly_calibrated_models_subspace_ids = ["M1_0"] test_newly_calibrated_models_subspace_ids = [ model.model_subspace_id for model in newly_calibrated_models_1.values() ] @@ -428,8 +428,8 @@ def test_vis(pypesto_select_problem): def test_custom_objective(petab_problem_yaml): parameters = { - 'k2': 0.333, - 'sigma_x2': 0.444, + "k2": 0.333, + "sigma_x2": 0.444, } parameters_x_guesses = [parameters] diff --git a/test/util.py b/test/util.py index 0230926ed..524aab704 100644 --- a/test/util.py +++ b/test/util.py @@ -89,12 +89,12 @@ def arg_fun(x): fun=arg_fun, grad=arg_grad, hess=arg_hess, res=arg_res, sres=arg_sres ) return { - 'obj': obj, - 'max_sensi_order': max_sensi_order, - 'x': x, - 'fval': fun(x), - 'grad': grad(x), - 'hess': hess(x), + "obj": obj, + "max_sensi_order": max_sensi_order, + "x": x, + "fval": fun(x), + "grad": grad(x), + "hess": hess(x), } @@ -287,15 +287,15 @@ def get_problem(self): def load_amici_objective(example_name): """Load an `AmiciObjective for test purposes.""" # name of the model that will also be the name of the python module - model_name = 'model_' + example_name + model_name = "model_" + example_name # sbml file sbml_file = os.path.join( - 'doc', 'example', example_name, model_name + '.xml' + "doc", "example", example_name, model_name + ".xml" ) # directory to which the generated model code is written - model_output_dir = os.path.join('doc', 'example', 'tmp', model_name) + model_output_dir = os.path.join("doc", "example", "tmp", model_name) if not os.path.exists(model_output_dir): os.makedirs(model_output_dir) diff --git a/test/visualize/test_visualize.py b/test/visualize/test_visualize.py index 50b93b35f..93c0031f1 100644 --- a/test/visualize/test_visualize.py +++ b/test/visualize/test_visualize.py @@ -33,7 +33,7 @@ def close_fig(fun): @wraps(fun) def wrapped_fun(*args, **kwargs): ret = fun(*args, **kwargs) - plt.close('all') + plt.close("all") return ret return wrapped_fun @@ -66,7 +66,7 @@ def create_problem(n_parameters: int = 2, x_names: Sequence[str] = None): def create_petab_problem(): current_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.abspath( - os.path.join(current_path, '..', '..', 'doc', 'example') + os.path.join(current_path, "..", "..", "doc", "example") ) # import to petab @@ -87,7 +87,7 @@ def sample_petab_problem(): sampler = sample.AdaptiveMetropolisSampler( options={ - 'show_progress': False, + "show_progress": False, }, ) result = sample.sample( @@ -135,17 +135,17 @@ def create_optimization_result_nan_inf(): # append nan and inf # depending on optimizer failed starts's x can be None or vector of np.nan optimizer_result = pypesto.OptimizerResult( - fval=float('nan'), x=np.array([float('nan'), float('nan')]), id='nan' + fval=float("nan"), x=np.array([float("nan"), float("nan")]), id="nan" ) result.optimize_result.append(optimize_result=optimizer_result) optimizer_result = pypesto.OptimizerResult( - fval=float('nan'), x=None, id='nan_none' + fval=float("nan"), x=None, id="nan_none" ) result.optimize_result.append(optimize_result=optimizer_result) optimizer_result = pypesto.OptimizerResult( - fval=-float('inf'), - x=np.array([-float('inf'), -float('inf')]), - id='inf', + fval=-float("inf"), + x=np.array([-float("inf"), -float("inf")]), + id="inf", ) result.optimize_result.append(optimize_result=optimizer_result) @@ -157,9 +157,9 @@ def create_optimization_history(): problem = create_problem() # create optimizer - optimizer_options = {'maxfun': 200} + optimizer_options = {"maxfun": 200} optimizer = optimize.ScipyOptimizer( - method='TNC', options=optimizer_options + method="TNC", options=optimizer_options ) history_options = pypesto.HistoryOptions( @@ -217,7 +217,7 @@ def create_plotting_options(): # create sets of reference points (from tuple, dict and from list) ref1 = ([1.0, 1.5], 0.2) ref2 = ([1.8, 1.9], 0.6) - ref3 = {'x': np.array([1.4, 1.7]), 'fval': 0.4} + ref3 = {"x": np.array([1.4, 1.7]), "fval": 0.4} ref4 = [ref1, ref2] ref_point = visualize.create_references(ref4) @@ -324,7 +324,7 @@ def test_waterfall_with_options(): # Test with linear scale visualize.waterfall( - result_1, reference=ref3, scale_y='lin', offset_y=0.2, y_limits=5.0 + result_1, reference=ref3, scale_y="lin", offset_y=0.2, y_limits=5.0 ) @@ -386,7 +386,7 @@ def test_parameters_with_options(scale_to_interval): # test calls with specific options visualize.parameters( result_1, - parameter_indices='all', + parameter_indices="all", reference=ref_point, size=alt_fig_size, colors=[1.0, 0.3, 0.3, 0.5], @@ -395,7 +395,7 @@ def test_parameters_with_options(scale_to_interval): visualize.parameters( [result_1, result_2], - parameter_indices='all', + parameter_indices="all", reference=ref_point, balance_alpha=False, start_indices=(0, 1, 4), @@ -404,7 +404,7 @@ def test_parameters_with_options(scale_to_interval): visualize.parameters( [result_1, result_2], - parameter_indices='free_only', + parameter_indices="free_only", start_indices=3, scale_to_interval=scale_to_interval, ) @@ -448,9 +448,9 @@ def test_parameters_hist(): problem = create_problem() # create optimizer - optimizer_options = {'maxfun': 200} + optimizer_options = {"maxfun": 200} optimizer = optimize.ScipyOptimizer( - method='TNC', options=optimizer_options + method="TNC", options=optimizer_options ) # run optimization @@ -462,8 +462,8 @@ def test_parameters_hist(): progress_bar=False, ) - visualize.parameter_hist(result_1, 'x1') - visualize.parameter_hist(result_1, 'x1', start_indices=list(range(10))) + visualize.parameter_hist(result_1, "x1") + visualize.parameter_hist(result_1, "x1", start_indices=list(range(10))) @pytest.mark.parametrize("scale_to_interval", [None, (0, 1)]) @@ -564,7 +564,7 @@ def _test_ensemble_dimension_reduction(): # test call via low-level routine 1 visualize.ensemble_crosstab_scatter_lowlevel( - umap_components, component_labels=('A', 'B', 'C') + umap_components, component_labels=("A", "B", "C") ) pca_components, pca_object = ensemble.get_pca_representation_parameters( @@ -667,7 +667,7 @@ def test_profiles_with_options(): reference=ref_point, size=alt_fig_size, profile_list_ids=[0, 1], - legends=['profile list 0', 'profile list 1'], + legends=["profile list 0", "profile list 1"], colors=[[1.0, 0.3, 0.3, 0.5], [0.5, 0.9, 0.4, 0.3]], ) @@ -748,8 +748,8 @@ def test_optimizer_history_with_options(): start_indices=[0, 1, 4, 11], reference=ref_point, size=alt_fig_size, - trace_x='steps', - trace_y='fval', + trace_x="steps", + trace_y="fval", offset_y=-10.0, colors=[1.0, 0.3, 0.3, 0.5], ) @@ -761,7 +761,7 @@ def test_optimizer_history_with_options(): start_indices=[0, 1, 4, 11], reference=ref_point, size=alt_fig_size, - scale_y='lin', + scale_y="lin", ) # Test with y-limits as float @@ -770,8 +770,8 @@ def test_optimizer_history_with_options(): y_limits=5.0, start_indices=3, reference=ref3, - trace_x='time', - trace_y='gradnorm', + trace_x="time", + trace_y="gradnorm", offset_y=10.0, ) @@ -804,9 +804,9 @@ def test_optimization_stats(): problem = create_problem() # create optimizer - optimizer_options = {'maxfun': 200} + optimizer_options = {"maxfun": 200} optimizer = optimize.ScipyOptimizer( - method='TNC', options=optimizer_options + method="TNC", options=optimizer_options ) # run optimization @@ -827,49 +827,49 @@ def test_optimization_stats(): ) visualize.optimization_run_property_per_multistart( - result_1, 'n_fval', legends='best result' + result_1, "n_fval", legends="best result" ) visualize.optimization_run_property_per_multistart( - result_1, 'n_fval', plot_type='hist', legends='best result' + result_1, "n_fval", plot_type="hist", legends="best result" ) - visualize.optimization_run_property_per_multistart(result_2, 'n_fval') + visualize.optimization_run_property_per_multistart(result_2, "n_fval") # test plotting of lists visualize.optimization_run_property_per_multistart( [result_1, result_2], - 'n_fval', - legends=['result1', 'result2'], - plot_type='line', + "n_fval", + legends=["result1", "result2"], + plot_type="line", ) visualize.optimization_run_property_per_multistart( - result_1, 'time', plot_type='hist', legends='best result' + result_1, "time", plot_type="hist", legends="best result" ) visualize.optimization_run_property_per_multistart( [result_1, result_2], - 'time', + "time", colors=[[0.5, 0.9, 0.9, 0.3], [0.9, 0.7, 0.8, 0.5]], - legends=['result1', 'result2'], - plot_type='hist', + legends=["result1", "result2"], + plot_type="hist", ) visualize.optimization_run_properties_per_multistart([result_1, result_2]) - visualize.optimization_run_properties_one_plot(result_1, ['time']) + visualize.optimization_run_properties_one_plot(result_1, ["time"]) visualize.optimization_run_properties_one_plot( - result_1, ['n_fval', 'n_grad', 'n_hess'] + result_1, ["n_fval", "n_grad", "n_hess"] ) visualize.optimization_run_property_per_multistart( [result_1, result_2], - 'time', + "time", colors=[[0.5, 0.9, 0.9, 0.3], [0.9, 0.7, 0.8, 0.5]], - legends=['result1', 'result2'], - plot_type='both', + legends=["result1", "result2"], + plot_type="both", ) @@ -1049,9 +1049,9 @@ def test_sampling_1d_marginals(): result, i_chain=1, stepsize=5, size=(10, 10) ) # call with other modes - visualize.sampling_1d_marginals(result, plot_type='hist') + visualize.sampling_1d_marginals(result, plot_type="hist") visualize.sampling_1d_marginals( - result, plot_type='kde', bw_method='silverman' + result, plot_type="kde", bw_method="silverman" ) @@ -1115,7 +1115,7 @@ def test_visualize_optimized_model_fit(): """Test pypesto.visualize.visualize_optimized_model_fit""" current_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.abspath( - os.path.join(current_path, '..', '..', 'doc', 'example') + os.path.join(current_path, "..", "..", "doc", "example") ) # import to petab @@ -1148,7 +1148,7 @@ def test_time_trajectory_model(): """Test pypesto.visualize.time_trajectory_model""" current_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.abspath( - os.path.join(current_path, '..', '..', 'doc', 'example') + os.path.join(current_path, "..", "..", "doc", "example") ) # import to petab diff --git a/tox.ini b/tox.ini index ba32fe490..4259240aa 100644 --- a/tox.ini +++ b/tox.ini @@ -137,32 +137,7 @@ commands = description = Run notebooks 2 -# Style, management, docs - -[testenv:project] -skip_install = true -deps = - pyroma -commands = - pyroma --min=10 . -description = - Check the package friendliness - -[testenv:flake8] -skip_install = true -deps = - flake8 >= 5.0.4 - flake8-bandit >= 4.1.1 - flake8-bugbear >= 22.8.23 - flake8-colors >= 0.1.9 - flake8-comprehensions >= 3.10.0 - flake8-print >= 5.0.0 - flake8-isort >= 4.0.2 - flake8-docstrings >= 1.6.0 -commands = - flake8 pypesto test -description = - Run flake8 with various plugins. +# Management, docs [testenv:doc] extras = From ee852cb216a65b12bf2d1e16edc8c0fba6c3e714 Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:35:28 +0100 Subject: [PATCH 05/35] Relative: Fix return of relative calculator if sim fails (#1315) * fix calc return if simulation failed * Daniel review change * Fix the extension --- .../hierarchical/inner_calculator_collector.py | 5 +++-- pypesto/hierarchical/relative/calculator.py | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 02d54fa01..91c02e423 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -474,8 +474,9 @@ def __call__( snllh += inner_result[GRAD] all_inner_pars.update(inner_result[X_INNER_OPT]) - if INNER_PARAMETERS in inner_result: - interpretable_inner_pars.extend(inner_result[INNER_PARAMETERS]) + inner_pars = inner_result.get(INNER_PARAMETERS) + if inner_pars is not None: + interpretable_inner_pars.extend(inner_pars) # add the quantitative data contribution if self.quantitative_data_mask is not None: diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index 1c9529213..c665941ab 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -165,10 +165,17 @@ def __call__( ) inner_result[X_INNER_OPT] = {} - inner_result[INNER_PARAMETERS] = np.array( - [inner_parameters[x_id] for x_id in self.inner_problem.get_x_ids()] + + inner_result[INNER_PARAMETERS] = ( + np.array( + [ + inner_parameters[x_id] + for x_id in self.inner_problem.get_x_ids() + ] + ) + if inner_parameters is not None + else None ) - # print("relative_inner_parameters: ", inner_parameters) return inner_result @@ -222,8 +229,7 @@ def call_amici_twice( inner_result[HESS] = np.full( shape=(dim, dim), fill_value=np.nan ) - inner_result[INNER_PARAMETERS] = None - return inner_result + return inner_result, None inner_parameters = self.inner_solver.solve( problem=self.inner_problem, @@ -325,7 +331,7 @@ def calculate_directly( inner_result[GRAD] = np.full( shape=len(x_ids), fill_value=np.nan ) - return filter_return_dict(inner_result) + return filter_return_dict(inner_result), None inner_parameters = self.inner_solver.solve( problem=self.inner_problem, From 7ecee813d75634dfa2187004b3f931b851117bba Mon Sep 17 00:00:00 2001 From: Maren Philipps <55318391+m-philipps@users.noreply.github.com> Date: Wed, 6 Mar 2024 23:25:02 +0100 Subject: [PATCH 06/35] Fix unwanted amici model recompilation (#1319) * PetabImporter checks for existing amici model in the location specified by output_folder before re-compiling. --- pypesto/petab/importer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pypesto/petab/importer.py b/pypesto/petab/importer.py index 7eeb954b5..819e3c8d3 100644 --- a/pypesto/petab/importer.py +++ b/pypesto/petab/importer.py @@ -2,7 +2,6 @@ from __future__ import annotations -import importlib import logging import os import shutil @@ -353,7 +352,7 @@ def _must_compile(self, force_compile: bool): # try to import (in particular checks version) try: # importing will already raise an exception if version wrong - importlib.import_module(self.model_name) + amici.import_model_module(self.model_name, self.output_folder) except ModuleNotFoundError: return True except amici.AmiciVersionError as e: From d32563b8225eddb06a8b17c87506d2d40c03e31d Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:45:58 +0100 Subject: [PATCH 07/35] Semiquant: Add spline knots to the optimization result (#1314) * Add spline knots to the optimization result Adds spline knots to the optimization result so they can be easily obtained after optimization. This then allows easy further use, if needed. * Documentation update & fix notebook * Fix types * Daniel review change Co-authored-by: Daniel Weindl * add test spline knot saving * Update pypesto/visualize/spline_approximation.py Co-authored-by: Maren Philipps <55318391+m-philipps@users.noreply.github.com> --------- Co-authored-by: Daniel Weindl Co-authored-by: Maren Philipps <55318391+m-philipps@users.noreply.github.com> --- doc/example/semiquantitative_data.ipynb | 7 ++- pypesto/C.py | 2 +- .../inner_calculator_collector.py | 19 +++++-- pypesto/hierarchical/ordinal/calculator.py | 8 +-- pypesto/hierarchical/relative/calculator.py | 5 +- .../semiquantitative/calculator.py | 10 ++-- .../hierarchical/semiquantitative/problem.py | 44 ++++++++++++++++ .../hierarchical/semiquantitative/solver.py | 3 +- pypesto/history/csv.py | 5 -- pypesto/objective/amici/amici.py | 7 ++- pypesto/optimize/optimizer.py | 8 ++- pypesto/problem/hierarchical.py | 9 ++++ pypesto/result/optimize.py | 1 + pypesto/visualize/spline_approximation.py | 52 +++++++++++++++---- test/hierarchical/test_spline.py | 49 +++++++++++++++++ 15 files changed, 187 insertions(+), 42 deletions(-) diff --git a/doc/example/semiquantitative_data.ipynb b/doc/example/semiquantitative_data.ipynb index 3a9cd784b..b9a35a0b4 100644 --- a/doc/example/semiquantitative_data.ipynb +++ b/doc/example/semiquantitative_data.ipynb @@ -431,7 +431,10 @@ " )\n", "\n", " plot_splines_from_inner_result(\n", - " inner_problem, inner_solvers[minimal_diff], results[minimal_diff]\n", + " inner_problem,\n", + " inner_solvers[minimal_diff],\n", + " results[minimal_diff],\n", + " sim=[simulation],\n", " )\n", " plt.show()" ] @@ -467,7 +470,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.10.10" }, "vscode": { "interpreter": { diff --git a/pypesto/C.py b/pypesto/C.py index 8cae31cea..a00140b9b 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -89,7 +89,6 @@ class EnsembleType(Enum): INNER_PARAMETERS = "inner_parameters" INNER_RDATAS = "inner_rdatas" PARAMETER_TYPE = "parameterType" -X_INNER_OPT = "x_inner_opt" RELATIVE = "relative" @@ -207,6 +206,7 @@ class InnerParameterType(str, Enum): MIN_SIM_RANGE = 1e-16 SPLINE_PAR_TYPE = "spline" +SPLINE_KNOTS = "spline_knots" N_SPLINE_PARS = "n_spline_pars" DATAPOINTS = "datapoints" MIN_DATAPOINT = "min_datapoint" diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 91c02e423..ea3b4bc82 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -31,9 +31,10 @@ RES, SEMIQUANTITATIVE, SPLINE_APPROXIMATION_OPTIONS, + SPLINE_KNOTS, SPLINE_RATIO, SRES, - X_INNER_OPT, + InnerParameterType, ModeType, ) from ..objective.amici.amici_calculator import AmiciCalculator @@ -107,6 +108,7 @@ def __init__( self.quantitative_data_mask = self._get_quantitative_data_mask(edatas) self._known_least_squares_safe = False + self.semiquant_observable_ids = None def initialize(self): """Initialize.""" @@ -177,6 +179,12 @@ def construct_inner_calculators( semiquant_problem.get_noise_dummy_values(scaled=True) ) self.inner_calculators.append(semiquant_calculator) + self.semiquant_observable_ids = [ + model.getObservableIds()[group - 1] + for group in semiquant_problem.get_groups_for_xs( + InnerParameterType.SPLINE + ) + ] if self.data_types - { RELATIVE, @@ -382,7 +390,7 @@ def __call__( nllh, snllh, s2nllh, chi2, res, sres = init_return_values( sensi_orders, mode, dim ) - all_inner_pars = {} + spline_knots = None interpretable_inner_pars = [] # set order in solver @@ -421,7 +429,7 @@ def __call__( RES: res, SRES: sres, RDATAS: rdatas, - X_INNER_OPT: all_inner_pars, + SPLINE_KNOTS: None, INNER_PARAMETERS: None, } ret[FVAL] = np.inf @@ -473,10 +481,11 @@ def __call__( if 1 in sensi_orders: snllh += inner_result[GRAD] - all_inner_pars.update(inner_result[X_INNER_OPT]) inner_pars = inner_result.get(INNER_PARAMETERS) if inner_pars is not None: interpretable_inner_pars.extend(inner_pars) + if SPLINE_KNOTS in inner_result: + spline_knots = inner_result[SPLINE_KNOTS] # add the quantitative data contribution if self.quantitative_data_mask is not None: @@ -507,7 +516,7 @@ def __call__( # Add inner parameters to return dict # only if the objective value improved. if ret[FVAL] < self.best_fval: - ret[X_INNER_OPT] = all_inner_pars + ret[SPLINE_KNOTS] = spline_knots ret[INNER_PARAMETERS] = ( interpretable_inner_pars if len(interpretable_inner_pars) > 0 diff --git a/pypesto/hierarchical/ordinal/calculator.py b/pypesto/hierarchical/ordinal/calculator.py index 6b13baacb..890b93df9 100644 --- a/pypesto/hierarchical/ordinal/calculator.py +++ b/pypesto/hierarchical/ordinal/calculator.py @@ -17,7 +17,6 @@ RDATAS, RES, SRES, - X_INNER_OPT, ) from ...objective.amici.amici_calculator import ( AmiciCalculator, @@ -126,7 +125,7 @@ def __call__( Returns ------- inner_result: - A dict containing the calculation results: FVAL, GRAD, RDATAS and X_INNER_OPT. + A dict containing the calculation results: FVAL, GRAD, RDATAS. """ if mode == MODE_RES: raise ValueError( @@ -178,7 +177,6 @@ def __call__( RES: res, SRES: sres, RDATAS: rdatas, - X_INNER_OPT: self.inner_problem.get_inner_parameter_dictionary(), } # if any amici simulation failed, it's unlikely we can compute @@ -201,13 +199,9 @@ def __call__( inner_result[FVAL] = self.inner_solver.calculate_obj_function( x_inner_opt ) - inner_result[ - X_INNER_OPT - ] = self.inner_problem.get_inner_parameter_dictionary() # calculate analytical gradients if requested if sensi_order > 0: - # print([opt['fun'] for opt in x_inner_opt]) sy = [rdata[AMICI_SY] for rdata in rdatas] ssigma = [rdata[AMICI_SSIGMAY] for rdata in rdatas] inner_result[GRAD] = self.inner_solver.calculate_gradients( diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index c665941ab..6768c5ad0 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -25,7 +25,6 @@ RDATAS, RES, SRES, - X_INNER_OPT, ModeType, ) from ...objective.amici.amici_calculator import ( @@ -123,7 +122,7 @@ def __call__( Returns ------- inner_result: - A dict containing the calculation results: FVAL, GRAD, RDATAS and X_INNER_OPT. + A dict containing the calculation results: FVAL, GRAD, RDATAS and INNER_PARAMETERS. """ if not self.inner_problem.check_edatas(edatas=edatas): raise ValueError( @@ -164,8 +163,6 @@ def __call__( rdatas=rdatas, ) - inner_result[X_INNER_OPT] = {} - inner_result[INNER_PARAMETERS] = ( np.array( [ diff --git a/pypesto/hierarchical/semiquantitative/calculator.py b/pypesto/hierarchical/semiquantitative/calculator.py index c7349387f..2dfdf87a3 100644 --- a/pypesto/hierarchical/semiquantitative/calculator.py +++ b/pypesto/hierarchical/semiquantitative/calculator.py @@ -15,8 +15,8 @@ MODE_RES, RDATAS, RES, + SPLINE_KNOTS, SRES, - X_INNER_OPT, ) from ...objective.amici.amici_calculator import ( AmiciCalculator, @@ -119,7 +119,8 @@ def __call__( Returns ------- inner_result: - A dict containing the calculation results: FVAL, GRAD, RDATAS and X_INNER_OPT. + A dict containing the calculation results: FVAL, GRAD, RDATAS, + INNER_PARAMETERS, and SPLINE_KNOTS. """ if mode == MODE_RES: raise ValueError( @@ -175,7 +176,6 @@ def __call__( RES: res, SRES: sres, RDATAS: rdatas, - X_INNER_OPT: self.inner_problem.get_inner_parameter_dictionary(), } # if any amici simulation failed, it's unlikely we can compute @@ -198,9 +198,7 @@ def __call__( inner_result[FVAL] = self.inner_solver.calculate_obj_function( x_inner_opt ) - inner_result[ - X_INNER_OPT - ] = self.inner_problem.get_inner_parameter_dictionary() + inner_result[SPLINE_KNOTS] = self.inner_problem.get_spline_knots() inner_result[ INNER_PARAMETERS diff --git a/pypesto/hierarchical/semiquantitative/problem.py b/pypesto/hierarchical/semiquantitative/problem.py index e360989bf..dad59e8fb 100644 --- a/pypesto/hierarchical/semiquantitative/problem.py +++ b/pypesto/hierarchical/semiquantitative/problem.py @@ -205,6 +205,50 @@ def get_inner_parameter_dictionary(self) -> dict: inner_par_dict[x_id] = x.value return inner_par_dict + def get_spline_knots( + self, + ) -> list[list[np.ndarray[float], np.ndarray[float]]]: + """Get spline knots of all semiquantitative observables. + + Returns + ------- + list[list[np.ndarray[float], np.ndarray[float]]] + A list of lists with two arrays. Each list in the first level corresponds + to a semiquantitative observable. Each of these lists contains two arrays: + the first array contains the spline bases, the second array contains the + spline knot values. The ordering of the observable lists is the same + as in `pypesto.problem.hierarchical.semiquant_observable_ids`. + """ + # We need the solver only for the rescaling function. + from .solver import SemiquantInnerSolver + + all_spline_knots = [] + + for group in self.get_groups_for_xs(InnerParameterType.SPLINE): + group_dict = self.groups[group] + n_spline_pars = group_dict[N_SPLINE_PARS] + n_data_points = group_dict[NUM_DATAPOINTS] + + inner_pars = np.array( + [x.value for x in self.get_xs_for_group(group)] + ) + + # Utility matrix for the spline knot calculation + lower_trian = np.tril(np.ones((n_spline_pars, n_spline_pars))) + knot_values = np.dot(lower_trian, inner_pars) + + _, knot_bases, _ = SemiquantInnerSolver._rescale_spline_bases( + sim_all=group_dict[CURRENT_SIMULATION], + N=n_spline_pars, + K=n_data_points, + ) + + spline_knots_for_observable = [knot_bases, knot_values] + + all_spline_knots.append(spline_knots_for_observable) + + return all_spline_knots + def get_measurements_for_group(self, gr) -> np.ndarray: """Get measurements for a group.""" # Taking the ixs of first inner parameter since diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index a26463ad2..92173899d 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -440,7 +440,8 @@ def inner_gradient_wrapper(x): return results - def _rescale_spline_bases(self, sim_all: np.ndarray, N: int, K: int): + @staticmethod + def _rescale_spline_bases(sim_all: np.ndarray, N: int, K: int): """Rescale the spline bases. Before the optimization of the spline parameters, we have to fix the diff --git a/pypesto/history/csv.py b/pypesto/history/csv.py index 989bc7e95..a777b4786 100644 --- a/pypesto/history/csv.py +++ b/pypesto/history/csv.py @@ -20,7 +20,6 @@ RES, SRES, TIME, - X_INNER_OPT, ModeType, X, ) @@ -155,10 +154,6 @@ def _update_trace( else: row[(var, np.nan)] = np.nan - if X_INNER_OPT in result: - for x_inner_id, x_inner_opt_value in result[X_INNER_OPT].items(): - row[(X_INNER_OPT, x_inner_id)] = x_inner_opt_value - self._trace = pd.concat( (self._trace, pd.DataFrame([row])), ) diff --git a/pypesto/objective/amici/amici.py b/pypesto/objective/amici/amici.py index 76b239b4e..90d7f2cac 100644 --- a/pypesto/objective/amici/amici.py +++ b/pypesto/objective/amici/amici.py @@ -14,6 +14,7 @@ MODE_FUN, MODE_RES, RDATAS, + SPLINE_KNOTS, SUFFIXES_CSV, SUFFIXES_HDF5, ModeType, @@ -232,8 +233,9 @@ def __init__( # `set_custom_timepoints` method for more information. self.custom_timepoints = None - # Initialize the dictionary for saving of inner parameters. + # Initialize the list for saving of inner parameter values. self.inner_parameters: list[float] = None + self.spline_knots: list[list[list[float]]] = None def get_config(self) -> dict: """Return basic information of the objective configuration.""" @@ -504,6 +506,9 @@ def call_unprocessed( if ret.get(INNER_PARAMETERS, None) is not None: self.inner_parameters = ret[INNER_PARAMETERS] + if ret.get(SPLINE_KNOTS, None) is not None: + self.spline_knots = ret[SPLINE_KNOTS] + # check whether we should update data for preequilibration guesses if ( self.guess_steadystate diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index 3dd38fcbe..b86dbc7f7 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -11,7 +11,7 @@ import numpy as np import scipy.optimize -from ..C import FVAL, GRAD, INNER_PARAMETERS, MODE_FUN, MODE_RES +from ..C import FVAL, GRAD, INNER_PARAMETERS, MODE_FUN, MODE_RES, SPLINE_KNOTS from ..history import HistoryOptions, NoHistory, OptimizerHistory from ..objective import Objective from ..problem import Problem @@ -68,6 +68,12 @@ def wrapped_minimize( ): result[INNER_PARAMETERS] = problem.objective.inner_parameters + if ( + hasattr(problem.objective, SPLINE_KNOTS) + and problem.objective.spline_knots is not None + ): + result[SPLINE_KNOTS] = problem.objective.spline_knots + return result return wrapped_minimize diff --git a/pypesto/problem/hierarchical.py b/pypesto/problem/hierarchical.py index 26061d691..bc8b2a2ff 100644 --- a/pypesto/problem/hierarchical.py +++ b/pypesto/problem/hierarchical.py @@ -34,6 +34,11 @@ class HierarchicalProblem(Problem): Only relevant if hierarchical is True. Contains the bounds of easily interpretable inner parameters only, e.g. noise parameters, scaling factors, offsets. + semiquant_observable_ids: + The ids of semiquantitative observables. Only relevant if hierarchical + is True. If not None, the optimization result's `spline_knots` will be + a list of lists of spline knots for each semiquantitative observable in + the order of these ids. """ def __init__( @@ -70,3 +75,7 @@ def __init__( self.inner_lb = np.array(inner_lb) self.inner_ub = np.array(inner_ub) + + self.semiquant_observable_ids = ( + self.objective.calculator.semiquant_observable_ids + ) diff --git a/pypesto/result/optimize.py b/pypesto/result/optimize.py index e6705c961..28434e20a 100644 --- a/pypesto/result/optimize.py +++ b/pypesto/result/optimize.py @@ -117,6 +117,7 @@ def __init__( self.optimizer = optimizer self.free_indices = None self.inner_parameters = None + self.spline_knots = None def __getattr__(self, key): try: diff --git a/pypesto/visualize/spline_approximation.py b/pypesto/visualize/spline_approximation.py index 0a1d6d826..51ca0e434 100644 --- a/pypesto/visualize/spline_approximation.py +++ b/pypesto/visualize/spline_approximation.py @@ -12,8 +12,10 @@ AMICI_Y, CURRENT_SIMULATION, DATAPOINTS, + EXPDATA_MASK, REGULARIZE_SPLINE, SCIPY_X, + SPLINE_KNOTS, ) from ..problem import Problem from ..result import Result @@ -26,6 +28,7 @@ from ..hierarchical.semiquantitative.solver import ( SemiquantInnerSolver, _calculate_regularization_for_group, + extract_expdata_using_mask, get_spline_mapped_simulations, ) except ImportError: @@ -61,6 +64,25 @@ def plot_splines_from_pypesto_result( "The calculator must be an instance of the InnerCalculatorCollector." ) + # Get the spline knot values from the pypesto result + spline_knot_values = [ + obs_spline_knots[1] + for obs_spline_knots in pypesto_result.optimize_result.list[ + start_index + ][SPLINE_KNOTS] + ] + + # Get inner parameters per observable as differences of spline knot values + inner_parameters = [ + np.concatenate([[obs_knot_values[0]], np.diff(obs_knot_values)]) + for obs_knot_values in spline_knot_values + ] + + inner_results = [ + {SCIPY_X: obs_inner_parameter} + for obs_inner_parameter in inner_parameters + ] + # Get the parameters from the pypesto result for the start_index. x_dct = dict( zip( @@ -107,9 +129,8 @@ def plot_splines_from_pypesto_result( ) return None - # Get simulation and sigma. + # Get simulation. sim = [rdata[AMICI_Y] for rdata in inner_rdatas] - sigma = [rdata[AMICI_SIGMAY] for rdata in inner_rdatas] spline_calculator = None for ( @@ -119,14 +140,23 @@ def plot_splines_from_pypesto_result( spline_calculator = calculator break + if spline_calculator is None: + raise ValueError( + "No SemiquantCalculator found in the inner_calculators of the objective. " + "Cannot plot splines." + ) + # Get the inner solver and problem. inner_solver = spline_calculator.inner_solver inner_problem = spline_calculator.inner_problem - inner_results = inner_solver.solve(inner_problem, sim, sigma) - return plot_splines_from_inner_result( - inner_problem, inner_solver, inner_results, observable_ids, **kwargs + inner_problem, + inner_solver, + inner_results, + sim, + observable_ids, + **kwargs, ) @@ -134,6 +164,7 @@ def plot_splines_from_inner_result( inner_problem: "pypesto.hierarchical.spline_approximation.problem.SplineInnerProblem", inner_solver: "pypesto.hierarchical.spline_approximation.solver.SplineInnerSolver", results: list[dict], + sim: list[np.ndarray], observable_ids=None, **kwargs, ): @@ -147,6 +178,10 @@ def plot_splines_from_inner_result( The inner solver. results: The results from the inner solver. + sim: + The simulated model output. + observable_ids: + The ids of the observables. kwargs: Additional arguments to pass to the plotting function. @@ -194,7 +229,9 @@ def plot_splines_from_inner_result( spline_knots = np.dot(lower_trian, s) measurements = inner_problem.groups[group][DATAPOINTS] - simulation = inner_problem.groups[group][CURRENT_SIMULATION] + simulation = extract_expdata_using_mask( + expdata=sim, mask=inner_problem.groups[group][EXPDATA_MASK] + ) # For the simulation, get the spline bases ( @@ -202,7 +239,6 @@ def plot_splines_from_inner_result( spline_bases, n, ) = SemiquantInnerSolver._rescale_spline_bases( - self=None, sim_all=simulation, N=len(spline_knots), K=len(simulation), @@ -406,7 +442,6 @@ def _add_spline_mapped_simulations_to_model_fit( spline_bases, n, ) = SemiquantInnerSolver._rescale_spline_bases( - self=None, sim_all=simulation, N=len(s), K=len(simulation), @@ -549,7 +584,6 @@ def _obtain_regularization_for_start( spline_bases, _, ) = SemiquantInnerSolver._rescale_spline_bases( - self=None, sim_all=simulation, N=len(s), K=len(simulation), diff --git a/test/hierarchical/test_spline.py b/test/hierarchical/test_spline.py index 3283c1a1f..1d99f05e0 100644 --- a/test/hierarchical/test_spline.py +++ b/test/hierarchical/test_spline.py @@ -8,11 +8,13 @@ import pypesto.logging import pypesto.optimize import pypesto.petab +import pypesto.store from pypesto.C import ( INNER_NOISE_PARS, LIN, MODE_FUN, OPTIMIZE_NOISE, + SPLINE_KNOTS, InnerParameterType, ) from pypesto.hierarchical.semiquantitative import ( @@ -462,3 +464,50 @@ def test_calculate_regularization_for_group(): regularization_gradient, expected_regularization_gradient, ) + + +def test_save_and_load_spline_knots(): + """Test the saving and loading of spline knots in an optimization result.""" + # Run optimization + petab_problem = petab.Problem.from_yaml(example_semiquantitative_yaml) + importer = pypesto.petab.PetabImporter( + petab_problem, + hierarchical=True, + ) + objective = importer.create_objective() + problem = importer.create_problem(objective) + + optimizer = pypesto.optimize.ScipyOptimizer( + method="L-BFGS-B", + options={"disp": None, "ftol": 2.220446049250313e-09, "gtol": 1e-5}, + ) + # Set seed for reproducibility. + np.random.seed(0) + result = pypesto.optimize.minimize( + problem=problem, n_starts=2, optimizer=optimizer + ) + + # Get spline knots + spline_knots_before = [ + result.optimize_result.list[i][SPLINE_KNOTS] for i in range(2) + ] + pypesto.store.write_result( + result=result, + filename="test_spline_knots.hdf5", + ) + # Load spline knots + result_loaded = pypesto.store.read_result("test_spline_knots.hdf5") + spline_knots_after = [ + result_loaded.optimize_result.list[i][SPLINE_KNOTS] for i in range(2) + ] + # Check that the loaded spline knots are the same as the original ones + assert np.all( + [ + np.allclose(knots_before, knots_after) + for knots_before, knots_after in zip( + spline_knots_before, spline_knots_after + ) + ] + ) + # Clean up + Path("test_spline_knots.hdf5").unlink() From dbb52c21ce680e251ab34b75a43e223e4e515f3d Mon Sep 17 00:00:00 2001 From: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:53:00 +0100 Subject: [PATCH 08/35] Deprecate `CmaesOptimizer` in favor of `CmaOptimizer` (#1311) --- doc/example/getting_started.ipynb | 2 +- pypesto/optimize/__init__.py | 2 +- pypesto/optimize/optimizer.py | 15 ++++++++++++++- setup.cfg | 4 ++-- test/optimize/test_optimize.py | 12 ++++++------ tox.ini | 4 ++-- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/doc/example/getting_started.ipynb b/doc/example/getting_started.ipynb index 8c02085e7..2f98e6d69 100644 --- a/doc/example/getting_started.ipynb +++ b/doc/example/getting_started.ipynb @@ -332,7 +332,7 @@ "* [Particle Swarm](https://github.com/tisimst/pyswarm) (`optimize.PyswarmOptimizer()`)\n", " * Particle swarm algorithm\n", " * Gradient-free\n", - "* [CMA-ES](https://pypi.org/project/cma-es/) (`optimize.CmaesOptimizer()`)\n", + "* [CMA-ES](https://pypi.org/project/cma-es/) (`optimize.CmaOptimizer()`)\n", " * Covariance Matrix Adaptation Evolution Strategy\n", " * Evolutionary Algorithm" ] diff --git a/pypesto/optimize/__init__.py b/pypesto/optimize/__init__.py index 4845aa9dd..72d6777cb 100644 --- a/pypesto/optimize/__init__.py +++ b/pypesto/optimize/__init__.py @@ -21,7 +21,7 @@ ) from .optimize import minimize from .optimizer import ( - CmaesOptimizer, + CmaOptimizer, DlibOptimizer, FidesOptimizer, IpoptOptimizer, diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index b86dbc7f7..671b6a1aa 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -753,7 +753,7 @@ def check_x0_support(self, x_guesses: np.ndarray = None) -> bool: return False -class CmaesOptimizer(Optimizer): +class CmaOptimizer(Optimizer): """ Global optimization using covariance matrix adaptation evolutionary search. @@ -832,6 +832,19 @@ def is_least_squares(self): return False +class CmaesOptimizer(CmaOptimizer): + """Deprecated, use CmaOptimizer instead.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + warnings.warn( + "`CmaesOptimizer` has been renamed to `CmaOptimizer`, " + "please update your code.", + DeprecationWarning, + stacklevel=1, + ) + + class ScipyDifferentialEvolutionOptimizer(Optimizer): """ Global optimization using scipy's differential evolution optimizer. diff --git a/setup.cfg b/setup.cfg index 3f51e1851..3b9f66343 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,7 +89,7 @@ all_optimizers = %(dlib)s %(nlopt)s %(pyswarm)s - %(cmaes)s + %(cma)s %(pyswarms)s %(fides)s amici = @@ -104,7 +104,7 @@ nlopt = nlopt >= 2.6.2 pyswarm = pyswarm >= 0.6 -cmaes = +cma = cma >= 3.0.3 pyswarms = pyswarms >= 1.3.0 diff --git a/test/optimize/test_optimize.py b/test/optimize/test_optimize.py index 947909485..f876cf75d 100644 --- a/test/optimize/test_optimize.py +++ b/test/optimize/test_optimize.py @@ -72,7 +72,7 @@ def problem(request) -> pypesto.Problem: ("ipopt", ""), ("dlib", ""), ("pyswarm", ""), - ("cmaes", ""), + ("cma", ""), ("scipydiffevolopt", ""), ("pyswarms", ""), *[ @@ -189,7 +189,7 @@ def test_unbounded_minimize(optimizer): if optimizer in [ ("dlib", ""), ("pyswarm", ""), - ("cmaes", ""), + ("cma", ""), ("scipydiffevolopt", ""), ("pyswarms", ""), *[ @@ -263,8 +263,8 @@ def get_optimizer(library, solver): optimizer = optimize.DlibOptimizer(options=options) elif library == "pyswarm": optimizer = optimize.PyswarmOptimizer(options=options) - elif library == "cmaes": - optimizer = optimize.CmaesOptimizer(options=options) + elif library == "cma": + optimizer = optimize.CmaOptimizer(options=options) elif library == "scipydiffevolopt": optimizer = optimize.ScipyDifferentialEvolutionOptimizer( options=options @@ -637,8 +637,8 @@ def test_correct_startpoint_usage(optimizer): """ Test that the startpoint is correctly used in all optimizers. """ - # cmaes supports x0, but samples from this initial guess, therefore return - if optimizer == ("cmaes", ""): + # cma supports x0, but samples from this initial guess, therefore return + if optimizer == ("cma", ""): return opt = get_optimizer(*optimizer) diff --git a/tox.ini b/tox.ini index 4259240aa..db88c95bd 100644 --- a/tox.ini +++ b/tox.ini @@ -95,7 +95,7 @@ description = Test Julia interface [testenv:optimize] -extras = test,dlib,ipopt,pyswarm,cmaes,nlopt,fides,mpi,pyswarms,petab +extras = test,dlib,ipopt,pyswarm,cma,nlopt,fides,mpi,pyswarms,petab commands = pytest --cov=pypesto --cov-report=xml --cov-append \ test/optimize @@ -123,7 +123,7 @@ description = [testenv:notebooks1] allowlist_externals = bash -extras = example,amici,petab,pyswarm,pymc3,cmaes,nlopt,fides +extras = example,amici,petab,pyswarm,pymc3,cma,nlopt,fides commands = bash test/run_notebook.sh 1 description = From 3c2d2f1b3dcac0ac9fdb8aec6eab21976d5d2d29 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:02:29 -0700 Subject: [PATCH 09/35] Removed amici DeprecationWarnings (#1316) * changed to new amici import format, removed some depracation warnings. Some are left, one needs to keep an eye out for probably. * corrected to amici.petab.conditions.fill_in_parameters * added minimal version to amici --- pypesto/optimize/optimize.py | 2 +- pypesto/petab/importer.py | 32 +++++++++++++++++------------- setup.cfg | 2 +- test/base/test_history.py | 2 +- test/base/test_prior.py | 4 ++-- test/petab/test_amici_objective.py | 5 +++-- test/petab/test_petab_import.py | 3 +-- test/petab/test_petab_suite.py | 4 ++-- 8 files changed, 29 insertions(+), 25 deletions(-) diff --git a/pypesto/optimize/optimize.py b/pypesto/optimize/optimize.py index b3d64b717..96b7f23c1 100644 --- a/pypesto/optimize/optimize.py +++ b/pypesto/optimize/optimize.py @@ -98,7 +98,7 @@ def minimize( "Passing `startpoint_method` directly is deprecated, " "use `problem.startpoint_method` instead.", DeprecationWarning, - stacklevel=1, + stacklevel=2, ) # convert startpoint method to class instance diff --git a/pypesto/petab/importer.py b/pypesto/petab/importer.py index 819e3c8d3..b19b12ebb 100644 --- a/pypesto/petab/importer.py +++ b/pypesto/petab/importer.py @@ -52,10 +52,12 @@ try: import amici - import amici.parameter_mapping - import amici.petab_import - import amici.petab_objective + import amici.petab + import amici.petab.conditions + import amici.petab.parameter_mapping + import amici.petab.simulations import petab + from amici.petab.import_helpers import check_model from petab.C import ( ESTIMATE, NOISE_PARAMETERS, @@ -330,7 +332,7 @@ def _create_model(self) -> amici.Model: module_name=self.model_name, module_path=self.output_folder ) model = module.getModel() - amici.petab_import.check_model( + check_model( amici_model=model, petab_problem=self.petab_problem, ) @@ -381,7 +383,7 @@ def compile_model(self, **kwargs): if os.path.exists(self.output_folder): shutil.rmtree(self.output_folder) - amici.petab_import.import_petab_problem( + amici.petab.import_petab_problem( petab_problem=self.petab_problem, model_name=self.model_name, model_output_dir=self.output_folder, @@ -412,7 +414,7 @@ def create_edatas( if model is None: model = self.create_model(verbose=verbose) - return amici.petab_objective.create_edatas( + return amici.petab.conditions.create_edatas( amici_model=model, petab_problem=self.petab_problem, simulation_conditions=simulation_conditions, @@ -471,12 +473,14 @@ def create_objective( model=model, simulation_conditions=simulation_conditions ) - parameter_mapping = amici.petab_objective.create_parameter_mapping( - petab_problem=self.petab_problem, - simulation_conditions=simulation_conditions, - scaled_parameters=True, - amici_model=model, - fill_fixed_parameters=False, + parameter_mapping = ( + amici.petab.parameter_mapping.create_parameter_mapping( + petab_problem=self.petab_problem, + simulation_conditions=simulation_conditions, + scaled_parameters=True, + amici_model=model, + fill_fixed_parameters=False, + ) ) par_ids = self.petab_problem.x_ids @@ -486,7 +490,7 @@ def create_objective( problem_parameters = dict( zip(self.petab_problem.x_ids, self.petab_problem.x_nominal_scaled) ) - amici.parameter_mapping.fill_in_parameters( + amici.petab.conditions.fill_in_parameters( edatas=edatas, problem_parameters=problem_parameters, scaled_parameters=True, @@ -835,7 +839,7 @@ def rdatas_to_measurement_df( measurement_df = self.petab_problem.measurement_df - return amici.petab_objective.rdatas_to_measurement_df( + return amici.petab.simulations.rdatas_to_measurement_df( rdatas, model, measurement_df ) diff --git a/setup.cfg b/setup.cfg index 3b9f66343..071142c2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -93,7 +93,7 @@ all_optimizers = %(pyswarms)s %(fides)s amici = - amici >= 0.18.0 + amici >= 0.21.0 petab = petab >= 0.2.0 ipopt = diff --git a/test/base/test_history.py b/test/base/test_history.py index d8e9ded27..0bd94de80 100644 --- a/test/base/test_history.py +++ b/test/base/test_history.py @@ -54,6 +54,7 @@ def check_history(self): }, } self.problem = pypesto.Problem(**kwargs) + self.problem.startpoint_method = pypesto.startpoint.uniform optimize_options = pypesto.optimize.OptimizeOptions( allow_failed_starts=False, @@ -80,7 +81,6 @@ def check_history(self): problem=self.problem, optimizer=self.optimizer, n_starts=n_starts, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, history_options=self.history_options, progress_bar=False, diff --git a/test/base/test_prior.py b/test/base/test_prior.py index 8b4ef34d9..7e3f6506d 100644 --- a/test/base/test_prior.py +++ b/test/base/test_prior.py @@ -10,6 +10,7 @@ from pypesto.C import MODE_FUN, MODE_RES from pypesto.objective import NegLogParameterPriors from pypesto.objective.priors import get_parameter_prior_dict +from pypesto.startpoint import UniformStartpoints scales = ["lin", "log", "log10"] @@ -114,14 +115,13 @@ def test_mode(scale, prior_type_list): if method == "ls_trf" and not test_prior.has_res: continue optimizer = pypesto.optimize.ScipyOptimizer(method=method) - startpoints = pypesto.startpoint.UniformStartpoints( + test_problem.startpoint_method = UniformStartpoints( check_fval=True, ) result = pypesto.optimize.minimize( problem=test_problem, optimizer=optimizer, n_starts=10, - startpoint_method=startpoints, progress_bar=False, ) diff --git a/test/petab/test_amici_objective.py b/test/petab/test_amici_objective.py index 2b6a7cb2c..c6636444b 100644 --- a/test/petab/test_amici_objective.py +++ b/test/petab/test_amici_objective.py @@ -99,13 +99,14 @@ def test_preeq_guesses(): assert obj.steadystate_guesses["fval"] == np.inf optimizer = optimize.ScipyOptimizer() - startpoints = pypesto.startpoint.UniformStartpoints(check_fval=False) + problem.startpoint_method = pypesto.startpoint.UniformStartpoints( + check_fval=False + ) result = optimize.minimize( problem=problem, optimizer=optimizer, n_starts=1, - startpoint_method=startpoints, progress_bar=False, ) diff --git a/test/petab/test_petab_import.py b/test/petab/test_petab_import.py index de4f7f3da..4c9cd2243 100644 --- a/test/petab/test_petab_import.py +++ b/test/petab/test_petab_import.py @@ -101,12 +101,11 @@ def test_4_optimize(self): options={"maxiter": 10} ) problem = importer.create_problem(obj) - startpoints = importer.create_startpoint_method() + problem.startpoint_method = importer.create_startpoint_method() result = pypesto.optimize.minimize( problem=problem, optimizer=optimizer, n_starts=2, - startpoint_method=startpoints, progress_bar=False, ) diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index c7b47a244..29877ea42 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -2,7 +2,7 @@ import logging -import amici.petab_objective +import amici.petab.simulations import petab import petabtests import pytest @@ -95,7 +95,7 @@ def _execute_case(case, model_type, version): rdatas = ret["rdatas"] chi2 = sum(rdata["chi2"] for rdata in rdatas) llh = -ret["fval"] - simulation_df = amici.petab_objective.rdatas_to_measurement_df( + simulation_df = amici.petab.simulations.rdatas_to_measurement_df( rdatas, model, importer.petab_problem.measurement_df ) From f2b64d1ecbe730ca2f4133ef3e799cafe1e3e151 Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:34:45 +0100 Subject: [PATCH 10/35] Repeat change of #1313 (#1323) Repeating the change made in #1313. Somehow it got overwritten to the old version. Co-authored-by: Jonas Arruda <69197639+arrjon@users.noreply.github.com> --- pypesto/hierarchical/semiquantitative/solver.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index 92173899d..00ee00dba 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -1090,11 +1090,8 @@ def save_inner_parameters_to_inner_problem( group ) - lower_trian = np.tril(np.ones((len(s), len(s)))) - xi = np.dot(lower_trian, s) - for idx in range(len(inner_spline_parameters)): - inner_spline_parameters[idx].value = xi[idx] + inner_spline_parameters[idx].value = s[idx] sigma = group_dict[INNER_NOISE_PARS] From b1298eaca0d54135f7b1f66676b51db3274546f3 Mon Sep 17 00:00:00 2001 From: Jonas Arruda <69197639+arrjon@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:37:54 +0100 Subject: [PATCH 11/35] Adding Thermodynamic Integration (#1326) * thermodynamic integration * compute_log_evidence * added constants in C.py * thermodynamic integration not supported for adaptive PT * adding logger for beta init --- pypesto/C.py | 7 ++ pypesto/sample/adaptive_parallel_tempering.py | 21 +++++ pypesto/sample/parallel_tempering.py | 81 ++++++++++++++++++- test/sample/test_sample.py | 43 ++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/pypesto/C.py b/pypesto/C.py index a00140b9b..639189cde 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -256,6 +256,13 @@ class InnerParameterType(str, Enum): LOG_NORMAL = "logNormal" LOG_LAPLACE = "logLaplace" +############################################################################### +# SAMPLING + +EXPONENTIAL_DECAY = ( + "exponential_decay" # temperature schedule for parallel tempering +) +BETA_DECAY = "beta_decay" # temperature schedule for parallel tempering ############################################################################### # PREDICT diff --git a/pypesto/sample/adaptive_parallel_tempering.py b/pypesto/sample/adaptive_parallel_tempering.py index 761d7df8a..adaaf2cb2 100644 --- a/pypesto/sample/adaptive_parallel_tempering.py +++ b/pypesto/sample/adaptive_parallel_tempering.py @@ -4,6 +4,8 @@ import numpy as np +from ..C import EXPONENTIAL_DECAY +from ..result import Result from .parallel_tempering import ParallelTemperingSampler @@ -36,6 +38,8 @@ def default_options(cls) -> Dict: # controls the adaptation degeneration velocity of the temperature # adaption. options["nu"] = 1e3 + # initial temperature schedule as in Vousden et. al. 2016. + options["beta_init"] = EXPONENTIAL_DECAY return options @@ -61,3 +65,20 @@ def adjust_betas(self, i_sample: int, swapped: Sequence[bool]): # fill in self.betas = betas + + def compute_log_evidence( + self, result: Result, method: str = "trapezoid" + ) -> float: + """Perform thermodynamic integration to estimate the log evidence. + + Parameters + ---------- + result: + Result object containing the samples. + method: + Integration method, either 'trapezoid' or 'simpson' (uses scipy for integration). + """ + raise NotImplementedError( + "Thermodynamic integration is not implemented for adaptive parallel tempering, " + "since the temperature schedule is adapted during the sampling process." + ) diff --git a/pypesto/sample/parallel_tempering.py b/pypesto/sample/parallel_tempering.py index df1eea580..47a6a99a1 100644 --- a/pypesto/sample/parallel_tempering.py +++ b/pypesto/sample/parallel_tempering.py @@ -1,13 +1,17 @@ import copy +import logging from typing import Dict, List, Sequence, Union import numpy as np +from ..C import BETA_DECAY, EXPONENTIAL_DECAY from ..problem import Problem -from ..result import McmcPtResult +from ..result import McmcPtResult, Result from ..util import tqdm from .sampler import InternalSampler, Sampler +logger = logging.getLogger(__name__) + class ParallelTemperingSampler(Sampler): """Simple parallel tempering sampler. @@ -43,12 +47,18 @@ def __init__( # set betas if (betas is None) == (n_chains is None): raise ValueError("Set either betas or n_chains.") - if betas is None: + if betas is None and self.options["beta_init"] == EXPONENTIAL_DECAY: + logger.info('Initializing betas with "near-exponential decay".') betas = near_exponential_decay_betas( n_chains=n_chains, exponent=self.options["exponent"], max_temp=self.options["max_temp"], ) + elif betas is None and self.options["beta_init"] == BETA_DECAY: + logger.info('Initializing betas with "beta decay".') + betas = beta_decay_betas( + n_chains=n_chains, alpha=self.options["alpha"] + ) if betas[0] != 1.0: raise ValueError("The first chain must have beta=1.0") self.betas0 = np.array(betas) @@ -71,6 +81,8 @@ def default_options(cls) -> Dict: "exponent": 4, "temper_log_posterior": False, "show_progress": None, + "beta_init": BETA_DECAY, # replaced in adaptive PT + "alpha": 0.3, } def initialize( @@ -164,6 +176,71 @@ def swap_samples(self) -> Sequence[bool]: def adjust_betas(self, i_sample: int, swapped: Sequence[bool]): """Adjust temperature values. Default: Do nothing.""" + def compute_log_evidence( + self, result: Result, method: str = "trapezoid" + ) -> float: + """Perform thermodynamic integration to estimate the log evidence. + + Parameters + ---------- + result: + Result object containing the samples. + method: + Integration method, either 'trapezoid' or 'simpson' (uses scipy for integration). + """ + from scipy.integrate import simpson, trapezoid + + if self.options["beta_init"] == EXPONENTIAL_DECAY: + logger.warning( + "The temperature schedule is not optimal for thermodynamic integration. " + f"Carefully check the results. Consider using beta_init='{BETA_DECAY}' for better results." + ) + + burn_in = result.sample_result.burn_in + trace_loglike = ( + result.sample_result.trace_neglogprior[::-1, burn_in:] + - result.sample_result.trace_neglogpost[::-1, burn_in:] + ) + mean_loglike_per_beta = np.mean(trace_loglike, axis=1) + if method == "trapezoid": + log_evidence = trapezoid( + y=mean_loglike_per_beta, x=self.betas[::-1] + ) + elif method == "simpson": + log_evidence = simpson( + y=mean_loglike_per_beta, x=self.betas[::-1], even="last" + ) + else: + raise ValueError( + f"Unknown method {method}. Choose 'trapezoid' or 'simpson'." + ) + + return log_evidence + + +def beta_decay_betas(n_chains: int, alpha: float) -> np.ndarray: + """Initialize betas to the (j-1)th quantile of a Beta(alpha, 1) distribution. + + Proposed by Xie et al. (2011) to be used for thermodynamic integration. + + Parameters + ---------- + n_chains: + Number of chains to use. + alpha: + Tuning parameter that modulates the skew of the distribution over the temperatures. + For alpha=1 we have a uniform distribution, and as alpha decreases towards zero, + temperatures become positively skewed. Xie et al. (2011) propose alpha=0.3 as a good start. + """ + if alpha <= 0 or alpha > 1: + raise ValueError("alpha must be in (0, 1]") + + # special case of one chain + if n_chains == 1: + return np.array([1.0]) + + return np.power(np.arange(n_chains) / (n_chains - 1), 1 / alpha)[::-1] + def near_exponential_decay_betas( n_chains: int, exponent: float, max_temp: float diff --git a/test/sample/test_sample.py b/test/sample/test_sample.py index 1591d9213..78bcb9104 100644 --- a/test/sample/test_sample.py +++ b/test/sample/test_sample.py @@ -6,6 +6,7 @@ import petab import pytest import scipy.optimize as so +from scipy.integrate import quad from scipy.stats import ks_2samp, kstest, multivariate_normal, norm, uniform import pypesto @@ -743,3 +744,45 @@ def test_dynesty_mcmc_samples(): assert (np.diff(original_sample_result.trace_neglogpost) <= 0).all() # MCMC samples are not assert not (np.diff(mcmc_sample_result.trace_neglogpost) <= 0).all() + + +def test_thermodynamic_integration(): + # test thermodynamic integration + problem = gaussian_problem() + + # approximation should be better for more chains + for n_chains, tol in zip([10, 20], [1, 1e-1]): + sampler = sample.ParallelTemperingSampler( + internal_sampler=sample.AdaptiveMetropolisSampler(), + options={"show_progress": False, "beta_init": "beta_decay"}, + n_chains=n_chains, + ) + + result = optimize.minimize( + problem, + progress_bar=False, + ) + + result = sample.sample( + problem, + n_samples=5000, + result=result, + sampler=sampler, + ) + + # compute the log evidence using trapezoid and simpson rule + log_evidence = sampler.compute_log_evidence(result, method="trapezoid") + log_evidence_simps = sampler.compute_log_evidence( + result, method="simpson" + ) + + # compute evidence + evidence = quad( + lambda x: 1 / (problem.ub - problem.lb) * np.exp(gaussian_llh(x)), + a=problem.lb, + b=problem.ub, + ) + + # compare to known value + assert np.isclose(log_evidence, np.log(evidence[0]), atol=tol) + assert np.isclose(log_evidence_simps, np.log(evidence[0]), atol=tol) From 2558763bf277771727d59bf734e43f063c74b6b2 Mon Sep 17 00:00:00 2001 From: Jonas Arruda <69197639+arrjon@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:41:23 +0100 Subject: [PATCH 12/35] Dynesty warnings added (#1324) * added warnings to dynesty and support for posterior * adapted objective in test to likelihood * added string constants * added objective type to dynesty in notebook --- doc/example/sampler_study.ipynb | 2 +- pypesto/C.py | 2 ++ pypesto/sample/dynesty.py | 44 +++++++++++++++++++++++++++--- test/sample/test_sample.py | 47 ++++++++++++++++++++++++++++++--- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/doc/example/sampler_study.ipynb b/doc/example/sampler_study.ipynb index d138010b9..c7bf161e8 100644 --- a/doc/example/sampler_study.ipynb +++ b/doc/example/sampler_study.ipynb @@ -590,7 +590,7 @@ "metadata": {}, "outputs": [], "source": [ - "sampler = sample.DynestySampler()\n", + "sampler = sample.DynestySampler(objective_type=\"negloglike\")\n", "result = sample.sample(\n", " problem=problem,\n", " n_samples=None,\n", diff --git a/pypesto/C.py b/pypesto/C.py index 639189cde..ac8f43463 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -70,6 +70,8 @@ class EnsembleType(Enum): RES = "res" # residual SRES = "sres" # residual sensitivities RDATAS = "rdatas" # returned simulated data sets +OBJECTIVE_NEGLOGPOST = "neglogpost" # objective is negative log-posterior +OBJECTIVE_NEGLOGLIKE = "negloglike" # objective is negative log-likelihood TIME = "time" # time N_FVAL = "n_fval" # number of function evaluations diff --git a/pypesto/sample/dynesty.py b/pypesto/sample/dynesty.py index 5bb5cb0dc..b4d797fb8 100644 --- a/pypesto/sample/dynesty.py +++ b/pypesto/sample/dynesty.py @@ -30,6 +30,7 @@ import numpy as np +from ..C import OBJECTIVE_NEGLOGLIKE, OBJECTIVE_NEGLOGPOST from ..problem import Problem from ..result import McmcPtResult from .sampler import Sampler, SamplerImportError @@ -62,6 +63,7 @@ def __init__( sampler_args: dict = None, run_args: dict = None, dynamic: bool = True, + objective_type: str = OBJECTIVE_NEGLOGPOST, ): """ Initialize sampler. @@ -76,6 +78,9 @@ def __init__( method of the dynesty sampler. dynamic: Whether to use dynamic or static nested sampling. + objective_type: + The objective to optimize (as defined in pypesto.problem). Either "neglogpost" or + "negloglike". If "neglogpost", x_priors have to be defined in the problem. """ # check dependencies import dynesty @@ -94,6 +99,13 @@ def __init__( run_args = {} self.run_args: dict = run_args + if objective_type not in [OBJECTIVE_NEGLOGPOST, OBJECTIVE_NEGLOGLIKE]: + raise ValueError( + f"Objective has to be either '{OBJECTIVE_NEGLOGPOST}' or '{OBJECTIVE_NEGLOGLIKE}' " + f"as defined in pypesto.problem." + ) + self.objective_type = objective_type + # set in initialize self.problem: Union[Problem, None] = None self.sampler: Union[ @@ -112,8 +124,6 @@ def prior_transform(self, prior_sample: np.ndarray) -> np.ndarray: ---------- prior_sample: The prior sample, provided by dynesty. - problem: - The pyPESTO problem. Returns ------- @@ -130,7 +140,13 @@ def loglikelihood(self, x): if any(x < self.problem.lb) or any(x > self.problem.ub): return -np.inf # invert sign - # TODO this is possibly the posterior if priors are defined + if self.objective_type == OBJECTIVE_NEGLOGPOST: + # problem.objective returns negative log-posterior + # compute log-likelihood by subtracting log-prior + return -1.0 * ( + self.problem.objective(x) - self.problem.x_priors(x) + ) + # problem.objective returns negative log-likelihood return -1.0 * self.problem.objective(x) def initialize( @@ -149,6 +165,28 @@ def initialize( if self.dynamic: sampler_class = dynesty.DynamicNestedSampler + # check if objective fits to the pyPESTO problem + if self.objective_type == OBJECTIVE_NEGLOGPOST: + if self.problem.x_priors is None: + # objective is the negative log-posterior, but no priors are defined + # sampler needs the likelihood + raise ValueError( + f"x_priors have to be defined in the problem if objective is '{OBJECTIVE_NEGLOGPOST}'." + ) + else: + # if objective is the negative log likelihood, we will ignore x_priors even if they are defined + if self.problem.x_priors is not None: + logger.warning( + f"Assuming '{OBJECTIVE_NEGLOGLIKE}' as objective. " + f"'x_priors' defined in the problem will be ignored." + ) + + # if priors are uniform, we can use the default prior transform (assuming that bounds are set correctly) + logger.warning( + "Assuming 'prior_transform' is correctly specified. If 'x_priors' is not uniform, 'prior_transform'" + " has to be adjusted accordingly." + ) + # initialize sampler self.sampler = sampler_class( loglikelihood=self.loglikelihood, diff --git a/test/sample/test_sample.py b/test/sample/test_sample.py index 78bcb9104..b3ca4fd1e 100644 --- a/test/sample/test_sample.py +++ b/test/sample/test_sample.py @@ -13,6 +13,7 @@ import pypesto.optimize as optimize import pypesto.petab import pypesto.sample as sample +from pypesto.C import OBJECTIVE_NEGLOGLIKE, OBJECTIVE_NEGLOGPOST from pypesto.sample.pymc import PymcSampler @@ -190,7 +191,7 @@ def sampler(request): elif request.param == "Emcee": return sample.EmceeSampler(nwalkers=10) elif request.param == "Dynesty": - return sample.DynestySampler() + return sample.DynestySampler(objective_type="negloglike") @pytest.fixture(params=["gaussian", "gaussian_mixture", "rosenbrock"]) @@ -722,13 +723,13 @@ def test_samples_cis(): assert (diff == 0).all() # check if lower bound is smaller than upper bound assert (lb < ub).all() - # check if dimmensions agree + # check if dimensions agree assert lb.shape == ub.shape def test_dynesty_mcmc_samples(): problem = gaussian_problem() - sampler = sample.DynestySampler() + sampler = sample.DynestySampler(objective_type=OBJECTIVE_NEGLOGLIKE) result = sample.sample( problem=problem, @@ -746,6 +747,46 @@ def test_dynesty_mcmc_samples(): assert not (np.diff(mcmc_sample_result.trace_neglogpost) <= 0).all() +def test_dynesty_posterior(): + # define negative log posterior + posterior_fun = pypesto.Objective(fun=negative_log_posterior) + + # define negative log prior + prior_fun = pypesto.Objective(fun=negative_log_prior) + + # define pypesto prior object + prior_object = pypesto.NegLogPriors(objectives=[prior_fun]) + + # define pypesto problem using prior object + test_problem = pypesto.Problem( + objective=posterior_fun, + x_priors_defs=prior_object, + lb=-10, + ub=10, + x_names=["x"], + ) + + # define sampler + sampler = sample.DynestySampler( + objective_type=OBJECTIVE_NEGLOGPOST + ) # default + + result = sample.sample( + problem=test_problem, + sampler=sampler, + n_samples=None, + filename=None, + ) + + original_sample_result = sampler.get_original_samples() + mcmc_sample_result = result.sample_result + + # Nested sampling function values are monotonically increasing + assert (np.diff(original_sample_result.trace_neglogpost) <= 0).all() + # MCMC samples are not + assert not (np.diff(mcmc_sample_result.trace_neglogpost) <= 0).all() + + def test_thermodynamic_integration(): # test thermodynamic integration problem = gaussian_problem() From 446e123e343a1ab92b0dc93fab378991becc68e6 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 18 Mar 2024 13:50:06 +0100 Subject: [PATCH 13/35] Hierarchical optimization: fix unnecessary simulation (#1327) Currently model simulation is run twice, even when no sensitivities are requested and the optimal inner parameters could be applied directly. --- pypesto/hierarchical/relative/calculator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index 6768c5ad0..1033e7246 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -132,10 +132,10 @@ def __call__( ) if ( - amici_solver.getSensitivityMethod() - == amici.SensitivityMethod_adjoint - or 2 in sensi_orders - ): + 1 in sensi_orders + and amici_solver.getSensitivityMethod() + == amici.SensitivityMethod.adjoint + ) or 2 in sensi_orders: inner_result, inner_parameters = self.call_amici_twice( x_dct=x_dct, sensi_orders=sensi_orders, From 263b30e9160910e07008d28957075e54c394195c Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:48:24 +0100 Subject: [PATCH 14/35] Push fix (#1330) --- pypesto/hierarchical/inner_calculator_collector.py | 4 ++++ pypesto/hierarchical/semiquantitative/solver.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index ea3b4bc82..0d2fe3f79 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -240,6 +240,10 @@ def _get_quantitative_data_mask( ): condition_mask[inner_par.ixs[cond_idx]] = False + # Put to False all entries that have a nan value in the edata + for condition_mask, edata in zip(quantitative_data_mask, edatas): + condition_mask[np.isnan(edata)] = False + # If there is no quantitative data, return None if not all(mask.any() for mask in quantitative_data_mask): return None diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index 00ee00dba..07254a913 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -580,7 +580,7 @@ def _get_inner_optimization_options( inner_options = { "x0": x0, "method": "L-BFGS-B", - "options": {"ftol": 1e-16, "disp": None}, + "options": {"disp": None}, "bounds": Bounds(lb=constraint_min_diff), } From 48ad07b40022d74b487bc67c4907dddeb789b7b6 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 20 Mar 2024 20:02:32 +0100 Subject: [PATCH 15/35] ESSOptimizer: Respect local_n2 in case of failed initial local search (#1328) Fixes an issue where ESSOptimizer would try to repeatedly perform a local search from the best known value, instead of trying other promising points, in case the initial local search yields a non-finite result. --- pypesto/optimize/ess/ess.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pypesto/optimize/ess/ess.py b/pypesto/optimize/ess/ess.py index 7379160aa..b0d62f718 100644 --- a/pypesto/optimize/ess/ess.py +++ b/pypesto/optimize/ess/ess.py @@ -167,11 +167,12 @@ def _initialize(self): self.x_best: Optional[np.array] = None # Overall best function value found so far self.fx_best: float = np.inf - # Results from local searches + # Results from local searches (only those with finite fval) self.local_solutions: list[OptimizerResult] = [] # Index of current iteration self.n_iter: int = 0 # ESS iteration at which the last local search took place + # (only local searches with a finite result are counted) self.last_local_search_niter: int = 0 # Whether self.x_best has changed in the current iteration self.x_best_has_changed: bool = False @@ -469,14 +470,14 @@ def _do_local_search( self.logger.debug("Local search only from best point.") local_search_x0_fx0_candidates = ((self.x_best, self.fx_best),) # first local search? - elif not self.local_solutions and self.n_iter >= self.local_n1: + elif self.n_iter == self.local_n1: self.logger.debug( "First local search from best point due to " f"local_n1={self.local_n1}." ) local_search_x0_fx0_candidates = ((self.x_best, self.fx_best),) elif ( - self.local_solutions + self.n_iter >= self.local_n1 and self.n_iter - self.last_local_search_niter >= self.local_n2 ): quality_order = np.argsort(fx_best_children) @@ -543,6 +544,11 @@ def _do_local_search( optimizer_result.fval, ) break + else: + self.logger.debug( + "Local search: No finite value found in any local search." + ) + return self.last_local_search_niter = self.n_iter self.evaluator.reset_round_counter() @@ -618,7 +624,7 @@ def _go_beyond(self, x_best_children, fx_best_children): def _report_iteration(self): """Log the current iteration.""" if self.n_iter == 0: - self.logger.info("iter | best | nf | refset |") + self.logger.info("iter | best | nf | refset | nlocal") with np.printoptions( edgeitems=5, From 81ff7ff43356db0a79a1459a62f86a7889f1f667 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 21 Mar 2024 10:29:02 +0100 Subject: [PATCH 16/35] Remove CESSOptimizer (#1320) `SacessOptimizer` is superior to `CESSOptimizer` and I wouldn't recommend using the latter. I think it's best to just remove it. No value, just extra maintenance effort. I don't think anybody was using it yet anyways, so this shouldn't actually be a breaking change for anybody. --- pypesto/optimize/__init__.py | 1 - pypesto/optimize/ess/__init__.py | 1 - pypesto/optimize/ess/cess.py | 353 ------------------------------- test/optimize/test_optimize.py | 19 +- 4 files changed, 1 insertion(+), 373 deletions(-) delete mode 100644 pypesto/optimize/ess/cess.py diff --git a/pypesto/optimize/__init__.py b/pypesto/optimize/__init__.py index 72d6777cb..968fe9e55 100644 --- a/pypesto/optimize/__init__.py +++ b/pypesto/optimize/__init__.py @@ -7,7 +7,6 @@ """ from .ess import ( - CESSOptimizer, ESSOptimizer, SacessFidesFactory, SacessOptimizer, diff --git a/pypesto/optimize/ess/__init__.py b/pypesto/optimize/ess/__init__.py index 51c854c19..fef613895 100644 --- a/pypesto/optimize/ess/__init__.py +++ b/pypesto/optimize/ess/__init__.py @@ -1,6 +1,5 @@ """Enhanced Scatter Search.""" -from .cess import CESSOptimizer from .ess import ESSOptimizer from .function_evaluator import ( FunctionEvaluator, diff --git a/pypesto/optimize/ess/cess.py b/pypesto/optimize/ess/cess.py deleted file mode 100644 index 2d036ba75..000000000 --- a/pypesto/optimize/ess/cess.py +++ /dev/null @@ -1,353 +0,0 @@ -"""Cooperative Enhanced Scatter Search.""" - -import logging -import multiprocessing -import os -import time -from typing import Dict, List, Optional -from warnings import warn - -import numpy as np - -import pypesto.optimize -from pypesto import Problem -from pypesto.startpoint import StartpointMethod - -from .ess import ESSExitFlag, ESSOptimizer -from .function_evaluator import FunctionEvaluator -from .refset import RefSet - -logger = logging.getLogger(__name__) - - -class CESSOptimizer: - r""" - Cooperative Enhanced Scatter Search Optimizer (CESS). - - A cooperative scatter search algorithm based on :footcite:t:`VillaverdeEge2012`. - In short, multiple scatter search instances with different hyperparameters - are running in different threads/processes, and exchange information. - Some instances focus on diversification while others focus on - intensification. Communication happens at fixed time intervals. - - Proposed hyperparameter values in :footcite:t:`VillaverdeEge2012`: - - * ``dim_refset``: ``[0.5 n_parameter, 20 n_parameters]`` - * ``local_n2``: ``[0, 100]`` - * ``balance``: ``[0, 0.5]`` - * ``n_diverse``: ``[5 n_par, 20 n_par]`` - * ``max_eval``: such that - :math:`\tau = log10(max\_eval / n\_par) \in [2.5, 3.5]` - with a recommended default value of :math:`\tau = 2.5`. - - Attributes - ---------- - ess_init_args: - List of argument dictionaries passed to - :func:`ESSOptimizer.__init__`. The length of this list is the - number of parallel ESS processes. - Resource limits such as ``max_eval`` apply to a single CESS - iteration, not to the full search. - max_iter: - Maximum number of CESS iterations. - max_walltime_s: - Maximum walltime in seconds. Will only be checked between local - optimizations and other simulations, and thus, may be exceeded by - the duration of a local search. Defaults to no limit. - fx_best: - The best objective value seen so far. - x_best: - Parameter vector corresponding to ``fx_best``. - starttime: - Starting time of the most recent optimization. - i_iter: - Current iteration number. - - References - ---------- - .. footbibliography:: - """ - - def __init__( - self, - ess_init_args: List[Dict], - max_iter: int, - max_walltime_s: float = np.inf, - ): - """Construct. - - Parameters - ---------- - ess_init_args: - List of argument dictionaries passed to - :func:`ESSOptimizer.__init__`. The length of this list is the - number of parallel ESS processes. - Resource limits such as ``max_eval`` apply to a single CESS - iteration, not to the full search. - max_iter: - Maximum number of CESS iterations. - max_walltime_s: - Maximum walltime in seconds. Will only be checked between local - optimizations and other simulations, and thus, may be exceeded by - the duration of a local search. Defaults to no limit. - """ - self.max_walltime_s = max_walltime_s - self.ess_init_args = ess_init_args - self.max_iter = max_iter - - self._initialize() - - def _initialize(self): - """(Re-)initialize.""" - self.starttime = time.time() - self.i_iter = 0 - # Overall best parameters found so far - self.x_best: Optional[np.array] = None - # Overall best function value found so far - self.fx_best: float = np.inf - - def minimize( - self, - problem: Problem, - startpoint_method: StartpointMethod = None, - ) -> pypesto.Result: - """Minimize the given objective using CESS. - - Parameters - ---------- - problem: - Problem to run ESS on. - startpoint_method: - Method for choosing starting points. - **Deprecated. Use ``problem.startpoint_method`` instead.** - """ - if startpoint_method is not None: - warn( - "Passing `startpoint_method` directly is deprecated, " - "use `problem.startpoint_method` instead.", - DeprecationWarning, - stacklevel=1, - ) - - self._initialize() - - evaluator = FunctionEvaluator( - problem=problem, - startpoint_method=startpoint_method, - ) - - refsets = [ - RefSet(evaluator=evaluator, dim=ess_init_args["dim_refset"]) - for ess_init_args in self.ess_init_args - ] - for refset, ess_init_args in zip(refsets, self.ess_init_args): - refset.initialize_random( - n_diverse=ess_init_args.get("n_diverse", 10 * problem.dim) - ) - - while True: - logger.info("-" * 50) - self._report_iteration() - self.i_iter += 1 - - # run scatter searches - ess_optimizers = self._run_scatter_searches( - refsets=refsets, - ) - # collect refsets from the different ESS runs - refsets = [result.refset for result in ess_optimizers] - - # update best values from ESS results - for result in ess_optimizers: - self._maybe_update_global_best(result.x_best, result.fx_best) - - if not self._keep_going(i_eval=evaluator.n_eval): - break - - # create refsets for the next iteration - self._update_refsets(refsets=refsets, evaluator=evaluator) - - # TODO merge results - self._report_final() - - # TODO what should the result look like? - return self._create_result(problem, refsets) - - def _report_iteration(self): - """Log the current iteration.""" - if self.max_iter == 0: - logger.info("iter | best |") - - with np.printoptions( - edgeitems=30, - linewidth=100000, - formatter={"float": lambda x: "%.3g" % x}, - ): - logger.info(f"{self.i_iter:4} | {self.fx_best:+.2E} | ") - - def _report_final(self): - """Log scatter search summary.""" - with np.printoptions( - edgeitems=30, - linewidth=100000, - formatter={"float": lambda x: "%.3g" % x}, - ): - logger.info( - f"CESS finished with {self.exit_flag!r} " - f"after {self.i_iter} iterations, " - f"{time.time() - self.starttime:.3g}s. " - # f"Num local solutions: {len(self.local_solutions)}." - ) - # logger.info(f"Final refset: {np.sort(self.refset.fx)} ") - logger.info(f"Best fval {self.fx_best}") - - def _create_result( - self, problem: pypesto.Problem, refsets: List[RefSet] - ) -> pypesto.Result: - """Create the result object. - - Currently, this returns the overall best value and the final RefSet. - """ - common_result_fields = { - "exitflag": self.exit_flag, - # meaningful? this is the overall time, and identical for all - # reported points - "time": time.time() - self.starttime, - # TODO - # 'n_fval': self.evaluator.n_eval, - "optimizer": str(self), - } - i_result = 0 - result = pypesto.Result(problem=problem) - - # save global best - optimizer_result = pypesto.OptimizerResult( - id=str(i_result), - x=self.x_best, - fval=self.fx_best, - message="Global best", - **common_result_fields, - ) - optimizer_result.update_to_full(problem) - # TODO DW: Create a single History with the global best? - result.optimize_result.append(optimizer_result) - - # save refsets - for i_refset, refset in enumerate(refsets): - for i in range(refset.dim): - i_result += 1 - result.optimize_result.append( - pypesto.OptimizerResult( - id=str(i_result), - x=refset.x[i], - fval=refset.fx[i], - message=f"RefSet[{i_refset}][{i}]", - **common_result_fields, - ) - ) - result.optimize_result[-1].update_to_full(result.problem) - - # TODO DW: also save local solutions? - # (need to track fvals or re-evaluate) - - return result - - def _run_scatter_searches( - self, - refsets: List[RefSet], - ) -> List[ESSOptimizer]: - """Start all scatter searches in different processes.""" - # set default value of max_eval if not present. - # only set it on a copy, as the original dicts may be re-used - # for different optimization problems. - # reasonable value proposed in [VillaverdeEge2012]: - # 2.5 < tau < 3.5, default: 2.5 - ess_init_args = [ - dict( - {"max_eval": int(10**2.5 * refsets[0].evaluator.problem.dim)}, - **ess_kwargs, - ) - for ess_kwargs in self.ess_init_args - ] - - ctx = multiprocessing.get_context("spawn") - with ctx.Pool(len(self.ess_init_args)) as pool: - ess_optimizers = pool.starmap( - self._run_single_ess, - ( - [ess_kwargs, refset] - for (ess_kwargs, refset) in zip(ess_init_args, refsets) - ), - chunksize=1, - ) - return list(ess_optimizers) - - def _run_single_ess( - self, - ess_kwargs, - refset: RefSet, - ) -> ESSOptimizer: - """ - Run ESS. - - Helper for `starmap`. - """ - # different random seeds per process? - np.random.seed((os.getpid() * int(time.time() * 1000)) % 2**32) - - ess = ESSOptimizer(**ess_kwargs) - ess.minimize(refset=refset) - return ess - - def _keep_going(self, i_eval) -> bool: - """Check exit criteria. - - Returns - ------- - ``True`` if none of the exit criteria is met, ``False`` otherwise. - """ - # TODO DW which further stopping criteria: gtol, fatol, frtol? - - # elapsed iterations - if self.i_iter >= self.max_iter: - self.exit_flag = ESSExitFlag.MAX_ITER - return False - - # elapsed time - if time.time() - self.starttime >= self.max_walltime_s: - self.exit_flag = ESSExitFlag.MAX_TIME - return False - - return True - - def _maybe_update_global_best(self, x, fx): - """Update the global best value if the provided value is better.""" - if fx < self.fx_best: - self.x_best = x[:] - self.fx_best = fx - - def _update_refsets( - self, refsets: List[RefSet], evaluator: FunctionEvaluator - ): - """ - Update refsets. - - Create new refsets based on the combined final refsets of the previous - CESS iteration. - - Updates ``refsets`` in place. - """ - # gather final refset entries - x = np.vstack([refset.x for refset in refsets]) - fx = np.concatenate([refset.fx for refset in refsets]) - - # reset function evaluation counter - evaluator.n_eval = 0 - evaluator.n_eval_round = 0 - - for i, ess_init_args in enumerate(self.ess_init_args): - refsets[i] = RefSet( - dim=ess_init_args["dim_refset"], evaluator=evaluator - ) - refsets[i].initialize_from_array(x_diverse=x, fx_diverse=fx) - refsets[i].sort() diff --git a/test/optimize/test_optimize.py b/test/optimize/test_optimize.py index f876cf75d..e89506afa 100644 --- a/test/optimize/test_optimize.py +++ b/test/optimize/test_optimize.py @@ -19,7 +19,6 @@ import pypesto import pypesto.optimize as optimize from pypesto.optimize.ess import ( - CESSOptimizer, ESSOptimizer, SacessFidesFactory, SacessOptimizer, @@ -457,7 +456,7 @@ def test_history_beats_optimizer(): @pytest.mark.filterwarnings( "ignore:Passing `startpoint_method` directly is deprecated.*:DeprecationWarning" ) -@pytest.mark.parametrize("ess_type", ["ess", "cess", "sacess"]) +@pytest.mark.parametrize("ess_type", ["ess", "sacess"]) @pytest.mark.parametrize( "local_optimizer", [None, optimize.FidesOptimizer(), SacessFidesFactory()], @@ -474,22 +473,6 @@ def test_ess(problem, local_optimizer, ess_type, request): n_threads=2, balance=0.5, ) - elif ess_type == "cess": - if ( - "cr" in request.node.callspec.id - or "integrated" in request.node.callspec.id - ): - # Not pickleable - incompatible with CESS - pytest.skip() - # CESS with 4 processes - ess_init_args = get_default_ess_options(num_workers=4, dim=problem.dim) - for x in ess_init_args: - x["local_optimizer"] = local_optimizer - ess = CESSOptimizer( - ess_init_args=ess_init_args, - max_iter=5, - max_walltime_s=10, - ) elif ess_type == "sacess": if ( "cr" in request.node.callspec.id From 9bde8562ef1f7c5e11d8a2eb99e64b33798febeb Mon Sep 17 00:00:00 2001 From: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:12:33 +0100 Subject: [PATCH 17/35] dynesty: method to save raw results; deprecate `DynestySampler.results` in favor of `DynestySampler.raw_results` (#1331) --- doc/example/sampler_study.ipynb | 4 +- pypesto/sample/dynesty.py | 181 ++++++++++++++++++++++---------- 2 files changed, 131 insertions(+), 54 deletions(-) diff --git a/doc/example/sampler_study.ipynb b/doc/example/sampler_study.ipynb index c7bf161e8..b466b028d 100644 --- a/doc/example/sampler_study.ipynb +++ b/doc/example/sampler_study.ipynb @@ -620,7 +620,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The internal `dynesty` sampler can be saved and restored, for post-sampling analysis. For example, pyPESTO stores resampled MCMC-like samples from the `dynesty` sampler by default. The following code shows how to save and load the internal dynesty sampler, to facilitate post-sampling analysis of both the resampled and original chains. First, we save the internal sampler." + "The internal `dynesty` sampler can be saved and restored, for post-sampling analysis. For example, pyPESTO stores resampled MCMC-like samples from the `dynesty` sampler by default. The following code shows how to save and load the internal dynesty sampler, to facilitate post-sampling analysis of both the resampled and original chains. N.B.: when working across different computers, you might prefer to work with the raw sample results via `pypesto.sample.dynesty.save_raw_results` and `load_raw_results`.", + "\n", + "First, we save the internal sampler." ] }, { diff --git a/pypesto/sample/dynesty.py b/pypesto/sample/dynesty.py index b4d797fb8..f6db40021 100644 --- a/pypesto/sample/dynesty.py +++ b/pypesto/sample/dynesty.py @@ -25,9 +25,12 @@ from __future__ import annotations +import importlib import logging +import warnings from typing import List, Union +import cloudpickle # noqa: S403 import numpy as np from ..C import OBJECTIVE_NEGLOGLIKE, OBJECTIVE_NEGLOGPOST @@ -35,6 +38,15 @@ from ..result import McmcPtResult from .sampler import Sampler, SamplerImportError +dynesty_pickle = cloudpickle +if importlib.util.find_spec("dynesty") is None: + dynesty = type("", (), {})() + dynesty.results = type("", (), {})() + dynesty.results.Results = None +else: + import dynesty + + dynesty.utils.pickle_module = dynesty_pickle logger = logging.getLogger(__name__) @@ -47,7 +59,7 @@ class DynestySampler(Sampler): To work with the original samples, modify the results object with `pypesto_result.sample_result = sampler.get_original_samples()`, where `sampler` is an instance of `pypesto.sample.DynestySampler`. The original - dynesty results object is available at `sampler.results`. + dynesty results object is available at `sampler.raw_results`. NB: the dynesty samplers can be customized significantly, by providing `sampler_args` and `run_args` to your `pypesto.sample.DynestySampler()` @@ -79,13 +91,14 @@ def __init__( dynamic: Whether to use dynamic or static nested sampling. objective_type: - The objective to optimize (as defined in pypesto.problem). Either "neglogpost" or - "negloglike". If "neglogpost", x_priors have to be defined in the problem. + The objective to optimize (as defined in `pypesto.problem`). Either + `pypesto.C.OBJECTIVE_NEGLOGLIKE` or + `pypesto.C.OBJECTIVE_NEGLOGPOST`. If + `pypesto.C.OBJECTIVE_NEGLOGPOST`, then `x_priors` have to + be defined in the problem. """ - # check dependencies - import dynesty - - setup_dynesty() + if importlib.util.find_spec("dynesty") is None: + raise SamplerImportError("dynesty") super().__init__() @@ -152,13 +165,9 @@ def loglikelihood(self, x): def initialize( self, problem: Problem, - x0: Union[np.ndarray, List[np.ndarray]], + x0: Union[np.ndarray, List[np.ndarray]] = None, ) -> None: """Initialize the sampler.""" - import dynesty - - setup_dynesty() - self.problem = problem sampler_class = dynesty.NestedSampler @@ -212,7 +221,22 @@ def sample(self, n_samples: int, beta: float = None) -> None: ) self.sampler.run_nested(**self.run_args) - self.results = self.sampler.results + + @property + def results(self): + """Deprecated in favor of `raw_results`.""" + warnings.warn( + "Accessing dynesty results via `sampler.results` is " + "deprecated. Please use `sampler.raw_results` instead.", + DeprecationWarning, + stacklevel=1, + ) + return self.raw_results + + @property + def raw_results(self): + """Get the raw dynesty results.""" + return self.sampler.results def save_internal_sampler(self, filename: str) -> None: """Save the state of the internal dynesty sampler. @@ -225,10 +249,6 @@ def save_internal_sampler(self, filename: str) -> None: filename: The internal sampler will be saved here. """ - import dynesty - - setup_dynesty() - dynesty.utils.save_sampler( sampler=self.sampler, fname=filename, @@ -242,11 +262,11 @@ def restore_internal_sampler(self, filename: str) -> None: filename: The internal sampler will be saved here. """ - import dynesty - - setup_dynesty() - - self.sampler = dynesty.utils.restore_sampler(fname=filename) + pool = self.sampler_args.get("pool", None) + self.sampler = dynesty.utils.restore_sampler( + fname=filename, + pool=pool, + ) def get_original_samples(self) -> McmcPtResult: """Get the samples into the fitting pypesto format. @@ -255,7 +275,7 @@ def get_original_samples(self) -> McmcPtResult: ------- The pyPESTO sample result. """ - return get_original_dynesty_samples(sampler=self.sampler) + return get_original_dynesty_samples(sampler=self) def get_samples(self) -> McmcPtResult: """Get MCMC-like samples into the fitting pypesto format. @@ -264,25 +284,87 @@ def get_samples(self) -> McmcPtResult: ------- The pyPESTO sample result. """ - return get_mcmc_like_dynesty_samples(sampler=self.sampler) + return get_mcmc_like_dynesty_samples(sampler=self) + + +def _get_raw_results( + sampler: DynestySampler, + raw_results: dynesty.result.Results, +) -> dynesty.results.Results: + if (sampler is None) == (raw_results is None): + raise ValueError( + "Please supply exactly one of `sampler` or `raw_results`." + ) + + if raw_results is not None: + return raw_results + + if not isinstance(sampler, DynestySampler): + raise ValueError( + "Please provide a pyPESTO `DynestySampler` if using " + "the `sampler` argument of this method." + ) + + return sampler.raw_results -def get_original_dynesty_samples(sampler) -> McmcPtResult: +def save_raw_results(sampler: DynestySampler, filename: str) -> None: + """Save dynesty sampler results to file. + + Restoring the dynesty sampler on a different computer than the one that + samples were generated is problematic (e.g. an AMICI model might get + compiled automatically). This method should avoid that, by only saving + the results. + + Parameters + ---------- + sampler: + The pyPESTO `DynestySampler` object used during sampling. + filename: + The file where the results will be saved. + """ + raw_results = _get_raw_results(sampler=sampler, raw_results=None) + with open(filename, "wb") as f: + dynesty_pickle.dump(raw_results, f) + + +def load_raw_results(filename: str) -> dynesty.results.Results: + """Load dynesty sample results from file. + + Parameters + ---------- + filename: + The file where the results will be loaded from. + """ + with open(filename, "rb") as f: + raw_results = dynesty_pickle.load(f) + return raw_results + + +def get_original_dynesty_samples( + sampler: DynestySampler = None, + raw_results: dynesty.results.Results = None, +) -> McmcPtResult: """Get original dynesty samples. + Only one of `sampler` or `raw_results` should be provided. + Parameters ---------- sampler: - The (internal!) dynesty sampler. See - `pypesto.sample.DynestySampler.__init__`, specifically the - `save_internal` argument, for more details. + The pyPESTO `DynestySampler` object with sampling results. + raw_results: + The raw results. See :func:`save_raw_results` and + :func:`load_raw_results`. Returns ------- The sample result. """ - trace_x = np.array([sampler.results.samples]) - trace_neglogpost = -np.array([sampler.results.logl]) + raw_results = _get_raw_results(sampler=sampler, raw_results=raw_results) + + trace_x = np.array([raw_results.samples]) + trace_neglogpost = -np.array([raw_results.logl]) # the sampler uses custom adaptive priors trace_neglogprior = np.full(trace_neglogpost.shape, np.nan) @@ -299,36 +381,40 @@ def get_original_dynesty_samples(sampler) -> McmcPtResult: return result -def get_mcmc_like_dynesty_samples(sampler) -> McmcPtResult: +def get_mcmc_like_dynesty_samples( + sampler: DynestySampler = None, + raw_results: dynesty.results.Results = None, +) -> McmcPtResult: """Get MCMC-like samples. + Only one of `sampler` or `raw_results` should be provided. + Parameters ---------- sampler: - The (internal!) dynesty sampler. See - `pypesto.sample.DynestySampler.__init__`, specifically the - `save_internal` argument, for more details. + The pyPESTO `DynestySampler` object with sampling results. + raw_results: + The raw results. See :func:`save_raw_results` and + :func:`load_raw_results`. Returns ------- The sample result. """ - import dynesty - - setup_dynesty() + raw_results = _get_raw_results(sampler=sampler, raw_results=raw_results) - if len(sampler.results.importance_weights().shape) != 1: + if len(raw_results.importance_weights().shape) != 1: raise ValueError( "Unknown error. The dynesty importance weights are not a 1D array." ) # resample according to importance weights indices = dynesty.utils.resample_equal( - np.arange(sampler.results.importance_weights().shape[0]), - sampler.results.importance_weights(), + np.arange(raw_results.importance_weights().shape[0]), + raw_results.importance_weights(), ) - trace_x = np.array([sampler.results.samples[indices]]) - trace_neglogpost = -np.array([sampler.results.logl[indices]]) + trace_x = np.array([raw_results.samples[indices]]) + trace_neglogpost = -np.array([raw_results.logl[indices]]) trace_neglogprior = np.array([np.full((len(indices),), np.nan)]) betas = np.array([1.0]) @@ -340,14 +426,3 @@ def get_mcmc_like_dynesty_samples(sampler) -> McmcPtResult: betas=betas, ) return result - - -def setup_dynesty() -> None: - """Import dynesty.""" - try: - import cloudpickle # noqa: S403 - import dynesty.utils - - dynesty.utils.pickle_module = cloudpickle - except ImportError: - raise SamplerImportError("dynesty") from None From 9cf4bf20ac6d54331d0a148c20ea7404c294e463 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:58:45 -0700 Subject: [PATCH 18/35] Added large code formatings to ignore in git blame (#1317) --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..4ad8396d2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# .git-blame-ignore-revs +# Adjusted to ruff code-checks. Timestamp: 05.03.2024 +82b7d5da1273b9d442a634e5aabbda9685279d77 +# Adjusted to black formatting. Timestamp: 11.01.2022 +ea0b0b47a2990806a8b8f1f9fa3b4257828e8df9 +165e5f9d36fc7ccc882c14f35ba345b3b6b41c6d From 80167263631e190a23471325f8ab715eaf9c0d2b Mon Sep 17 00:00:00 2001 From: Doresic <85789271+Doresic@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:21:10 +0100 Subject: [PATCH 19/35] Relative: Fix return of inner parameters on objective call (#1333) * Fix return of inner parameters on objective call * hierarchical_decorator outside of history_decorator --------- Co-authored-by: Daniel Weindl --- .../inner_calculator_collector.py | 21 +++++---------- pypesto/objective/amici/amici.py | 11 -------- pypesto/optimize/optimizer.py | 27 +++++++++---------- pypesto/visualize/model_fit.py | 11 ++++---- 4 files changed, 25 insertions(+), 45 deletions(-) diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 0d2fe3f79..07e220538 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -112,7 +112,6 @@ def __init__( def initialize(self): """Initialize.""" - self.best_fval = np.inf for calculator in self.inner_calculators: calculator.initialize() @@ -125,7 +124,6 @@ def construct_inner_calculators( ): """Construct inner calculators for each data type.""" self.necessary_par_dummy_values = {} - self.best_fval = np.inf if RELATIVE in self.data_types: relative_inner_problem = RelativeInnerProblem.from_petab_amici( @@ -382,9 +380,6 @@ def __call__( parameter_mapping=parameter_mapping, fim_for_hess=fim_for_hess, ) - # only return inner parameters if the objective value improved - if ret[FVAL] > self.best_fval: - ret[INNER_PARAMETERS] = None return filter_return_dict(ret) # get dimension of outer problem @@ -517,16 +512,12 @@ def __call__( RDATAS: rdatas, } - # Add inner parameters to return dict - # only if the objective value improved. - if ret[FVAL] < self.best_fval: - ret[SPLINE_KNOTS] = spline_knots - ret[INNER_PARAMETERS] = ( - interpretable_inner_pars - if len(interpretable_inner_pars) > 0 - else None - ) - self.best_fval = ret[FVAL] + ret[INNER_PARAMETERS] = ( + interpretable_inner_pars + if len(interpretable_inner_pars) > 0 + else None + ) + ret[SPLINE_KNOTS] = spline_knots return filter_return_dict(ret) diff --git a/pypesto/objective/amici/amici.py b/pypesto/objective/amici/amici.py index 90d7f2cac..b424f002d 100644 --- a/pypesto/objective/amici/amici.py +++ b/pypesto/objective/amici/amici.py @@ -10,11 +10,9 @@ from ...C import ( FVAL, - INNER_PARAMETERS, MODE_FUN, MODE_RES, RDATAS, - SPLINE_KNOTS, SUFFIXES_CSV, SUFFIXES_HDF5, ModeType, @@ -233,10 +231,6 @@ def __init__( # `set_custom_timepoints` method for more information. self.custom_timepoints = None - # Initialize the list for saving of inner parameter values. - self.inner_parameters: list[float] = None - self.spline_knots: list[list[list[float]]] = None - def get_config(self) -> dict: """Return basic information of the objective configuration.""" info = super().get_config() @@ -503,11 +497,6 @@ def call_unprocessed( nllh = ret[FVAL] rdatas = ret[RDATAS] - if ret.get(INNER_PARAMETERS, None) is not None: - self.inner_parameters = ret[INNER_PARAMETERS] - - if ret.get(SPLINE_KNOTS, None) is not None: - self.spline_knots = ret[SPLINE_KNOTS] # check whether we should update data for preequilibration guesses if ( diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index 671b6a1aa..5625e1dfe 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -14,7 +14,7 @@ from ..C import FVAL, GRAD, INNER_PARAMETERS, MODE_FUN, MODE_RES, SPLINE_KNOTS from ..history import HistoryOptions, NoHistory, OptimizerHistory from ..objective import Objective -from ..problem import Problem +from ..problem import HierarchicalProblem, Problem from ..result import OptimizerResult from .load import fill_result_from_history from .options import OptimizeOptions @@ -61,18 +61,17 @@ def wrapped_minimize( optimize_options=optimize_options, ) - # add inner parameters - if ( - hasattr(problem.objective, INNER_PARAMETERS) - and problem.objective.inner_parameters is not None - ): - result[INNER_PARAMETERS] = problem.objective.inner_parameters - - if ( - hasattr(problem.objective, SPLINE_KNOTS) - and problem.objective.spline_knots is not None - ): - result[SPLINE_KNOTS] = problem.objective.spline_knots + if isinstance(problem, HierarchicalProblem): + # Call the objective to obtain inner parameters of + # the optimal outer optimization parameters + return_dict = problem.objective( + result.x, + return_dict=True, + ) + if INNER_PARAMETERS in return_dict: + result[INNER_PARAMETERS] = return_dict[INNER_PARAMETERS] + if SPLINE_KNOTS in return_dict: + result[SPLINE_KNOTS] = return_dict[SPLINE_KNOTS] return result @@ -239,8 +238,8 @@ def minimize_decorator_collection(minimize): @wraps(minimize) @fix_decorator @time_decorator - @history_decorator @hierarchical_decorator + @history_decorator def wrapped_minimize( self, problem: Problem, diff --git a/pypesto/visualize/model_fit.py b/pypesto/visualize/model_fit.py index 9dec63b33..26d296d1d 100644 --- a/pypesto/visualize/model_fit.py +++ b/pypesto/visualize/model_fit.py @@ -17,9 +17,8 @@ from petab.visualize import plot_problem from ..C import CENSORED, ORDINAL, RDATAS, SEMIQUANTITATIVE -from ..hierarchical.relative.calculator import RelativeAmiciCalculator from ..petab.importer import get_petab_non_quantitative_data_types -from ..problem import Problem +from ..problem import HierarchicalProblem, Problem from ..result import Result from .ordinal_categories import plot_categories_from_pypesto_result from .spline_approximation import _add_spline_mapped_simulations_to_model_fit @@ -241,7 +240,7 @@ def _get_simulation_rdatas( parameters = problem.get_reduced_vector(parameters) # simulate with custom timepoints for hierarchical model - if isinstance(problem.objective.calculator, RelativeAmiciCalculator): + if isinstance(problem, HierarchicalProblem): # get parameter dictionary x_dct = dict( zip(problem.x_names, result.optimize_result.list[start_index].x) @@ -253,8 +252,10 @@ def _get_simulation_rdatas( ) # update parameter dictionary with inner parameters - inner_parameters = ret["inner_parameters"] - x_dct.update(inner_parameters) + inner_parameter_dict = dict( + zip(problem.inner_x_names, ret["inner_parameters"]) + ) + x_dct.update(inner_parameter_dict) parameter_mapping = problem.objective.parameter_mapping edatas = copy.deepcopy(problem.objective.edatas) From f418577595a6123cf927bc778bc480a4a7a8a085 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:39:11 -0700 Subject: [PATCH 20/35] Fixed amici depractions. (#1344) --- pypesto/objective/amici/amici.py | 5 +++-- pypesto/objective/amici/amici_calculator.py | 6 +++--- pypesto/visualize/model_fit.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pypesto/objective/amici/amici.py b/pypesto/objective/amici/amici.py index b424f002d..c740b3578 100644 --- a/pypesto/objective/amici/amici.py +++ b/pypesto/objective/amici/amici.py @@ -35,7 +35,7 @@ if TYPE_CHECKING: try: import amici - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass @@ -105,7 +105,8 @@ def __init__( Names of optimization parameters. parameter_mapping: Mapping of optimization parameters to model parameters. Format - as created by `amici.petab_objective.create_parameter_mapping`. + as created by + `amici.petab.parameter_mapping.create_parameter_mapping`. The default is just to assume that optimization and simulation parameters coincide. guess_steadystate: diff --git a/pypesto/objective/amici/amici_calculator.py b/pypesto/objective/amici/amici_calculator.py index ba888366b..781533da2 100644 --- a/pypesto/objective/amici/amici_calculator.py +++ b/pypesto/objective/amici/amici_calculator.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: try: import amici - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: ParameterMapping = None @@ -86,7 +86,7 @@ def __call__( Whether to use the FIM (if available) instead of the Hessian (if requested). """ - import amici.parameter_mapping + import amici.petab.conditions # set order in solver sensi_order = 0 @@ -100,7 +100,7 @@ def __call__( amici_solver.setSensitivityOrder(sensi_order) # fill in parameters - amici.parameter_mapping.fill_in_parameters( + amici.petab.conditions.fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/visualize/model_fit.py b/pypesto/visualize/model_fit.py index 26d296d1d..378f3c0fd 100644 --- a/pypesto/visualize/model_fit.py +++ b/pypesto/visualize/model_fit.py @@ -13,7 +13,7 @@ import matplotlib.pyplot as plt import numpy as np import petab -from amici.petab_objective import rdatas_to_simulation_df +from amici.petab.simulations import rdatas_to_simulation_df from petab.visualize import plot_problem from ..C import CENSORED, ORDINAL, RDATAS, SEMIQUANTITATIVE From 038b2858260b5c69c0c0d955d32571195fa05fa8 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Sun, 31 Mar 2024 14:40:12 -0600 Subject: [PATCH 21/35] Integrate RoadRunner as simulator (#1336) * First working example * temp fix * tmp_fix 2 * Added petab testsuite. Currently everything aside from Preequilibration and InitialAssignments overwrites works. * All except for one petab test working. I.e. functioning ss-computation * Sped up computation for MODE_FUN. Two tests only pass unreliably, one test expectedly fails. * Fixed error, where the conditions in edatas did not align with the parameter mappings * Removed pandas calculation to dataframe from calculator, made stand alone function. Expected speedup: ~5-10x * Added SolverOptions for roadrunner simulation and slightly more convenient way to interact with it. * Some cleanup * Added get_config. Skipping 0018 for now. * Support MODE_RES correctly * added petab testsuite to tox for roadrunner. * added roadrunner dependency * Case 0018 works now as well. Fixed a problem if not all observables were measured at all timepoints * Review suggestions and sanity check on filling parameters and edata * Removed "unscale_parameter" function. Considerably speed up model compilation. * removed timepoints from edatas. Added to documentation * Removed fill_replicates, as it is now possible with roadrunner. Also able to more conveniently setValues. Added newer version restriction to roadrunner. * Added roadrunner to tox.ini docs. * Added roadrunner to tox.ini base. * Changed to use API token for deployment * Added short docstring to each file * Added more descriptive docstring. * Reverted deploy changes, in favor of separate PR * Update pypesto/objective/roadrunner/roadrunner_calculator.py Co-authored-by: Daniel Weindl * Hopefully fixes doc build * Removed name for type for correct docstring. * Should fix docs now and also pandas warning * typo in tox * Integrated suggestions * Added roadrunner to doc installation --------- Co-authored-by: Daniel Weindl --- doc/api.rst | 1 + pypesto/C.py | 4 + pypesto/objective/roadrunner/__init__.py | 9 + .../roadrunner/petab_importer_roadrunner.py | 361 +++++++++++++++ pypesto/objective/roadrunner/road_runner.py | 143 ++++++ .../roadrunner/roadrunner_calculator.py | 416 +++++++++++++++++ pypesto/objective/roadrunner/utils.py | 430 ++++++++++++++++++ setup.cfg | 4 + test/base/test_roadrunner.py | 106 +++++ tox.ini | 7 +- 10 files changed, 1478 insertions(+), 3 deletions(-) create mode 100644 pypesto/objective/roadrunner/__init__.py create mode 100644 pypesto/objective/roadrunner/petab_importer_roadrunner.py create mode 100644 pypesto/objective/roadrunner/road_runner.py create mode 100644 pypesto/objective/roadrunner/roadrunner_calculator.py create mode 100644 pypesto/objective/roadrunner/utils.py create mode 100644 test/base/test_roadrunner.py diff --git a/doc/api.rst b/doc/api.rst index b99b9dfd3..f0ff709b6 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -16,6 +16,7 @@ API reference pypesto.objective.aesara pypesto.objective.jax pypesto.objective.julia + pypesto.objective.roadrunner pypesto.optimize pypesto.petab pypesto.predict diff --git a/pypesto/C.py b/pypesto/C.py index ac8f43463..79660c02b 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -292,6 +292,10 @@ class InnerParameterType(str, Enum): AMICI_SSIGMAY = "ssigmay" # return field in amici simulation result AMICI_SSIGMAZ = "ssigmaz" # return field in amici simulation result +ROADRUNNER_LLH = "llh" # return field in roadrunner objective +ROADRUNNER_INSTANCE = "roadrunner_instance" +ROADRUNNER_SIMULATION = "simulation_results" + CONDITION = "condition" CONDITION_IDS = "condition_ids" diff --git a/pypesto/objective/roadrunner/__init__.py b/pypesto/objective/roadrunner/__init__.py new file mode 100644 index 000000000..c2b3325da --- /dev/null +++ b/pypesto/objective/roadrunner/__init__.py @@ -0,0 +1,9 @@ +""" +RoadRunner objective +==================== +""" + +from .petab_importer_roadrunner import PetabImporterRR +from .road_runner import RoadRunnerObjective +from .roadrunner_calculator import RoadRunnerCalculator +from .utils import simulation_to_measurement_df diff --git a/pypesto/objective/roadrunner/petab_importer_roadrunner.py b/pypesto/objective/roadrunner/petab_importer_roadrunner.py new file mode 100644 index 000000000..b5238724b --- /dev/null +++ b/pypesto/objective/roadrunner/petab_importer_roadrunner.py @@ -0,0 +1,361 @@ +"""Importer for PEtab problems using RoadRunner. + +Creates from a PEtab problem a roadrunner model, a roadrunner objective or a +pypesto problem with a roadrunner objective. The actual form of the likelihood +depends on the noise model specified in the provided PEtab problem. +""" +from __future__ import annotations + +import numbers +from pathlib import Path +from typing import Any, Iterable, Optional, Union + +import libsbml +import petab +import roadrunner +from petab.C import ( + OBSERVABLE_FORMULA, + PREEQUILIBRATION_CONDITION_ID, + SIMULATION_CONDITION_ID, +) +from petab.models.sbml_model import SbmlModel +from petab.parameter_mapping import ParMappingDictQuadruple + +from ...petab.importer import PetabStartpoints +from ...problem import Problem +from ...startpoint import StartpointMethod +from ..aggregated import AggregatedObjective +from ..priors import NegLogParameterPriors, get_parameter_prior_dict +from .road_runner import RoadRunnerObjective +from .roadrunner_calculator import RoadRunnerCalculator +from .utils import ExpData + + +class PetabImporterRR: + """ + Importer for PEtab problems using RoadRunner. + + Create a :class:`roadrunner.RoadRunner` instance, + a :class:`pypesto.objective.RoadRunnerObjective` or a + :class:`pypesto.problem.Problem` from PEtab files. The actual + form of the likelihood depends on the noise model specified in the provided PEtab problem. + For more information, see the + `PEtab documentation `_. + """ # noqa + + def __init__( + self, petab_problem: petab.Problem, validate_petab: bool = True + ): + """Initialize importer. + + Parameters + ---------- + petab_problem: + Managing access to the model and data. + validate_petab: + Flag indicating if the PEtab problem shall be validated. + """ + self.petab_problem = petab_problem + if validate_petab: + if petab.lint_problem(petab_problem): + raise ValueError("Invalid PEtab problem.") + self.rr = roadrunner.RoadRunner() + + @staticmethod + def from_yaml(yaml_config: Union[Path, str]) -> PetabImporterRR: + """Simplified constructor using a petab yaml file.""" + petab_problem = petab.Problem.from_yaml(yaml_config) + + return PetabImporterRR(petab_problem=petab_problem) + + def _check_noise_formulae( + self, + edatas: Optional[list[ExpData]] = None, + parameter_mapping: Optional[list[ParMappingDictQuadruple]] = None, + ): + """Check if the noise formulae are valid. + + Currently, only static values or singular parameters are supported. + Complex formulae are not supported. + """ + # check that parameter mapping is available + if parameter_mapping is None: + parameter_mapping = self.create_parameter_mapping() + # check that edatas are available + if edatas is None: + edatas = self.create_edatas() + # check that noise formulae are valid + for edata, par_map in zip(edatas, parameter_mapping): + for noise_formula in edata.noise_formulae: + # constant values are allowed + if isinstance(noise_formula, numbers.Number): + continue + # single parameters are allowed + if noise_formula in par_map[1].keys(): + continue + raise NotImplementedError( + "Noise formulae must be either constants or single " + "parameters. For more complex noise models, please " + "use amici for now." + ) + + def _write_observables_to_model(self): + """Write observables of petab problem to the model.""" + # add all observables as species + for obs_id in self.petab_problem.observable_df.index: + self.rr.addParameter(obs_id, 0.0, False) + # extract all parameters from observable formulas + parameters = petab.get_output_parameters( + self.petab_problem.observable_df, + self.petab_problem.model, + noise=True, + observables=True, + ) + # add all parameters to the model + for param_id in parameters: + self.rr.addParameter(param_id, 0.0, False) + formulae = self.petab_problem.observable_df[ + OBSERVABLE_FORMULA + ].to_dict() + + # add all observable formulas as assignment rules + for obs_id, formula in formulae.items(): + self.rr.addAssignmentRule(obs_id, formula, forceRegenerate=False) + + # regenerate model to apply changes + self.rr.regenerateModel() + + def create_edatas(self) -> list[ExpData]: + """Create a List of :class:`ExpData` objects from the PEtab problem.""" + # Create Dataframes per condition + return ExpData.from_petab_problem(self.petab_problem) + + def fill_model(self): + """Fill the RoadRunner model inplace from the PEtab problem. + + Parameters + ---------- + return_model: + Flag indicating if the model should be returned. + """ + if not isinstance(self.petab_problem.model, SbmlModel): + raise ValueError( + "The model is not an SBML model. Using " + "RoadRunner as simulator requires an SBML model." + ) # TODO: add Pysb support + if self.petab_problem.model.sbml_document: + sbml_document = self.petab_problem.model.sbml_document + elif self.petab_problem.model.sbml_model: + sbml_document = ( + self.petab_problem.model.sbml_model.getSBMLDocument() + ) + else: + raise ValueError("No SBML model found.") + sbml_writer = libsbml.SBMLWriter() + sbml_string = sbml_writer.writeSBMLToString(sbml_document) + self.rr.load(sbml_string) + self._write_observables_to_model() + + def create_parameter_mapping(self): + """Create a parameter mapping from the PEtab problem.""" + simulation_conditions = ( + self.petab_problem.get_simulation_conditions_from_measurement_df() + ) + mapping = petab.get_optimization_to_simulation_parameter_mapping( + condition_df=self.petab_problem.condition_df, + measurement_df=self.petab_problem.measurement_df, + parameter_df=self.petab_problem.parameter_df, + observable_df=self.petab_problem.observable_df, + model=self.petab_problem.model, + ) + # check whether any species in the condition table are assigned + species = self.rr.model.getFloatingSpeciesIds() + # overrides in parameter table are handled already + overrides = [ + specie + for specie in species + if specie in self.petab_problem.condition_df.columns + ] + if not overrides: + return mapping + for (_, condition), mapping_per_condition in zip( + simulation_conditions.iterrows(), mapping + ): + for override in overrides: + preeq_id = condition.get(PREEQUILIBRATION_CONDITION_ID) + sim_id = condition.get(SIMULATION_CONDITION_ID) + if preeq_id: + mapping_per_condition[0][ + override + ] = self.petab_problem.condition_df.loc[preeq_id, override] + mapping_per_condition[2][override] = "lin" + if sim_id: + mapping_per_condition[1][ + override + ] = self.petab_problem.condition_df.loc[sim_id, override] + mapping_per_condition[3][override] = "lin" + return mapping + + def create_objective( + self, + rr: Optional[roadrunner.RoadRunner] = None, + edatas: Optional[ExpData] = None, + ) -> RoadRunnerObjective: + """Create a :class:`pypesto.objective.RoadRunnerObjective`. + + Parameters + ---------- + rr: + RoadRunner instance. + edatas: + ExpData object. + """ + roadrunner_instance = rr + if roadrunner_instance is None: + roadrunner_instance = self.rr + self.fill_model() + if edatas is None: + edatas = self.create_edatas() + + parameter_mapping = self.create_parameter_mapping() + + # get x_names + x_names = self.petab_problem.get_x_ids() + + calculator = RoadRunnerCalculator() + + # run the check for noise formulae + self._check_noise_formulae(edatas, parameter_mapping) + + return RoadRunnerObjective( + rr=roadrunner_instance, + edatas=edatas, + parameter_mapping=parameter_mapping, + petab_problem=self.petab_problem, + calculator=calculator, + x_names=x_names, + ) + + def create_prior(self) -> Union[NegLogParameterPriors, None]: + """ + Create a prior from the parameter table. + + Returns None, if no priors are defined. + """ + prior_list = [] + + if petab.OBJECTIVE_PRIOR_TYPE not in self.petab_problem.parameter_df: + return None + + for i, x_id in enumerate(self.petab_problem.x_ids): + prior_type_entry = self.petab_problem.parameter_df.loc[ + x_id, petab.OBJECTIVE_PRIOR_TYPE + ] + + if ( + isinstance(prior_type_entry, str) + and prior_type_entry != petab.PARAMETER_SCALE_UNIFORM + ): + prior_params = [ + float(param) + for param in self.petab_problem.parameter_df.loc[ + x_id, petab.OBJECTIVE_PRIOR_PARAMETERS + ].split(";") + ] + + scale = self.petab_problem.parameter_df.loc[ + x_id, petab.PARAMETER_SCALE + ] + + prior_list.append( + get_parameter_prior_dict( + i, prior_type_entry, prior_params, scale + ) + ) + return NegLogParameterPriors(prior_list) + + def create_startpoint_method(self, **kwargs) -> StartpointMethod: + """Create a startpoint method. + + Parameters + ---------- + **kwargs: + Additional keyword arguments passed on to + :meth:`pypesto.startpoint.FunctionStartpoints.__init__`. + """ + return PetabStartpoints(petab_problem=self.petab_problem, **kwargs) + + def create_problem( + self, + objective: Optional[RoadRunnerObjective] = None, + x_guesses: Optional[Iterable[float]] = None, + problem_kwargs: Optional[dict[str, Any]] = None, + startpoint_kwargs: Optional[dict[str, Any]] = None, + **kwargs, + ) -> Problem: + """Create a :class:`pypesto.problem.Problem`. + + Parameters + ---------- + objective: + Objective as created by :meth:`create_objective`. + x_guesses: + Guesses for the parameter values, shape (g, dim), where g denotes + the number of guesses. These are used as start points in the + optimization. + problem_kwargs: + Passed to :meth:`pypesto.problem.Problem.__init__`. + startpoint_kwargs: + Keyword arguments forwarded to + :meth:`PetabImporter.create_startpoint_method`. + **kwargs: + Additional key word arguments passed on to the objective, + if not provided. + + Returns + ------- + A :class:`pypesto.problem.Problem` instance. + """ + if objective is None: + objective = self.create_objective(**kwargs) + + x_fixed_indices = self.petab_problem.x_fixed_indices + x_fixed_vals = self.petab_problem.x_nominal_fixed_scaled + x_ids = self.petab_problem.x_ids + lb = self.petab_problem.lb_scaled + ub = self.petab_problem.ub_scaled + + x_scales = [ + self.petab_problem.parameter_df.loc[x_id, petab.PARAMETER_SCALE] + for x_id in x_ids + ] + + if problem_kwargs is None: + problem_kwargs = {} + + if startpoint_kwargs is None: + startpoint_kwargs = {} + + prior = self.create_prior() + + if prior is not None: + objective = AggregatedObjective([objective, prior]) + + problem = Problem( + objective=objective, + lb=lb, + ub=ub, + x_fixed_indices=x_fixed_indices, + x_fixed_vals=x_fixed_vals, + x_guesses=x_guesses, + x_names=x_ids, + x_scales=x_scales, + x_priors_defs=prior, + startpoint_method=self.create_startpoint_method( + **startpoint_kwargs + ), + copy_objective=False, + **problem_kwargs, + ) + + return problem diff --git a/pypesto/objective/roadrunner/road_runner.py b/pypesto/objective/roadrunner/road_runner.py new file mode 100644 index 000000000..c711944e4 --- /dev/null +++ b/pypesto/objective/roadrunner/road_runner.py @@ -0,0 +1,143 @@ +"""Objective function for RoadRunner models. + +Currently does not support sensitivities. +""" +from collections import OrderedDict +from typing import Optional, Sequence, Tuple, Union + +import numpy as np +import roadrunner +from petab import Problem as PetabProblem +from petab.parameter_mapping import ParMappingDictQuadruple + +from ...C import MODE_FUN, MODE_RES, ROADRUNNER_INSTANCE, X_NAMES, ModeType +from ..base import ObjectiveBase +from .roadrunner_calculator import RoadRunnerCalculator +from .utils import ExpData, SolverOptions + + +class RoadRunnerObjective(ObjectiveBase): + """Objective function for RoadRunner models. + + Currently does not support sensitivities. + """ + + def __init__( + self, + rr: roadrunner.RoadRunner, + edatas: Union[Sequence[ExpData], ExpData], + parameter_mapping: list[ParMappingDictQuadruple], + petab_problem: PetabProblem, + calculator: Optional[RoadRunnerCalculator] = None, + x_names: Optional[Sequence[str]] = None, + solver_options: Optional[SolverOptions] = None, + ): + """Initialize the RoadRunner objective function. + + Parameters + ---------- + rr: + RoadRunner instance for simulation. + edatas: + The experimental data. If a list is passed, its entries correspond + to multiple experimental conditions. + parameter_mapping: + Mapping of optimization parameters to model parameters. Format as + created by `petab.get_optimization_to_simulation_parameter_mapping`. + The default is just to assume that optimization and simulation + parameters coincide. + petab_problem: + The corresponding PEtab problem. Needed to calculate NLLH. + Might be removed later. + calculator: + The calculator to use. If None, a new instance is created. + x_names: + Names of optimization parameters. + """ + self.roadrunner_instance = rr + # make sure edatas are a list + if isinstance(edatas, ExpData): + edatas = [edatas] + self.edatas = edatas + self.parameter_mapping = parameter_mapping + self.petab_problem = petab_problem + if calculator is None: + calculator = RoadRunnerCalculator() + self.calculator = calculator + if solver_options is None: + solver_options = SolverOptions() + self.solver_options = solver_options + super().__init__(x_names=x_names) + + def get_config(self) -> dict: + """Return basic information of the objective configuration.""" + info = super().get_config() + info["solver_options"] = repr(self.solver_options) + info[X_NAMES] = self.x_names + info[ROADRUNNER_INSTANCE] = self.roadrunner_instance.getInfo() + return info + + # TODO: Check whether we need some sort of pickling + + def __call__( + self, + x: np.ndarray, + sensi_orders: Tuple[int, ...] = (0,), + mode: ModeType = MODE_FUN, + return_dict: bool = False, + **kwargs, + ) -> Union[float, np.ndarray, dict]: + """See :class:`ObjectiveBase` documentation.""" + return super().__call__(x, sensi_orders, mode, return_dict, **kwargs) + + def call_unprocessed( + self, + x: np.ndarray, + sensi_orders: Tuple[int, ...], + mode: ModeType, + edatas: Optional[Sequence[ExpData]] = None, + parameter_mapping: Optional[list[ParMappingDictQuadruple]] = None, + ) -> dict: + """ + Call objective function without pre- or post-processing and formatting. + + Returns + ------- + result: + A dict containing the results. + """ + # fill in values if not passed + if edatas is None: + edatas = self.edatas + if parameter_mapping is None: + parameter_mapping = self.parameter_mapping + # convert x to dictionary + x = OrderedDict(zip(self.x_names, x)) + ret = self.calculator( + x_dct=x, + mode=mode, + roadrunner_instance=self.roadrunner_instance, + edatas=edatas, + x_ids=self.x_names, + parameter_mapping=parameter_mapping, + petab_problem=self.petab_problem, + solver_options=self.solver_options, + ) + return ret + + def check_sensi_orders( + self, + sensi_orders: Tuple[int, ...], + mode: ModeType, + ) -> bool: + """See :class:`ObjectiveBase` documentation.""" + if not sensi_orders: + return True + sensi_order = max(sensi_orders) + max_sensi_order = 0 + + return sensi_order <= max_sensi_order + + def check_mode(self, mode: ModeType) -> bool: + """See `ObjectiveBase` documentation.""" + return mode in [MODE_FUN, MODE_RES] diff --git a/pypesto/objective/roadrunner/roadrunner_calculator.py b/pypesto/objective/roadrunner/roadrunner_calculator.py new file mode 100644 index 000000000..65f147d4e --- /dev/null +++ b/pypesto/objective/roadrunner/roadrunner_calculator.py @@ -0,0 +1,416 @@ +"""RoadRunner calculator for PEtab problems. + +Handles all RoadRunner.simulate calls, calculates likelihoods and residuals. +""" +import numbers +from typing import Optional, Sequence + +import numpy as np +import petab +import roadrunner +from petab.parameter_mapping import ParMappingDictQuadruple + +from ...C import ( + FVAL, + MODE_FUN, + MODE_RES, + RES, + ROADRUNNER_LLH, + ROADRUNNER_SIMULATION, + TIME, + ModeType, +) +from .utils import ( + ExpData, + SolverOptions, + simulation_to_measurement_df, + unscale_parameters, +) + +LLH_TYPES = { + "lin_normal": lambda measurement, simulation, sigma: -0.5 + * ( + np.log(2 * np.pi * (sigma**2)) + + ((measurement - simulation) / sigma) ** 2 + ), + "log_normal": lambda measurement, simulation, sigma: -0.5 + * ( + np.log(2 * np.pi * (sigma**2) * (measurement**2)) + + ((np.log(measurement) - np.log(simulation)) / sigma) ** 2 + ), + "log10_normal": lambda measurement, simulation, sigma: -0.5 + * ( + np.log(2 * np.pi * (sigma**2) * (measurement**2) * np.log(10) ** 2) + + ((np.log10(measurement) - np.log10(simulation)) / sigma) ** 2 + ), + "lin_laplace": lambda measurement, simulation, sigma: -np.log(2 * sigma) + - (np.abs(measurement - simulation) / sigma), + "log_laplace": lambda measurement, simulation, sigma: -np.log( + 2 * sigma * simulation + ) + - (np.abs(np.log(measurement) - np.log(simulation)) / sigma), + "log10_laplace": lambda measurement, simulation, sigma: -np.log( + 2 * sigma * simulation * np.log(10) + ) + - (np.abs(np.log10(measurement) - np.log10(simulation)) / sigma), +} + + +class RoadRunnerCalculator: + """Class to handle RoadRunner simulation and obtain objective value.""" + + def __call__( + self, + x_dct: dict, # TODO: sensi_order support + mode: ModeType, + roadrunner_instance: roadrunner.RoadRunner, + edatas: list[ExpData], + x_ids: Sequence[str], + parameter_mapping: list[ParMappingDictQuadruple], + petab_problem: petab.Problem, + solver_options: Optional[SolverOptions], + ): + """Perform the RoadRunner call and obtain objective function values. + + Parameters + ---------- + x_dct: + Parameter dictionary. + mode: + Mode of the call. + roadrunner_instance: + RoadRunner instance. + edatas: + List of ExpData. + x_ids: + Sequence of parameter IDs. + parameter_mapping: + Parameter parameter_mapping. + petab_problem: + PEtab problem. + solver_options: + Solver options of the roadrunner instance Integrator. These will + modify the roadrunner instance inplace. + + Returns + ------- + Tuple of objective function values. + """ + # sanity check that edatas and conditions are consistent + if len(edatas) != len(parameter_mapping): + raise ValueError( + "Number of edatas and conditions are not consistent." + ) + if solver_options is None: + solver_options = SolverOptions() + # apply solver options + solver_options.apply_to_roadrunner(roadrunner_instance) + simulation_results = {} + llh_tot = 0 + for edata, mapping_per_condition in zip(edatas, parameter_mapping): + sim_res, llh = self.simulate_per_condition( + x_dct, roadrunner_instance, edata, mapping_per_condition + ) + simulation_results[edata.condition_id] = sim_res + llh_tot += llh + + if mode == MODE_FUN: + return { + FVAL: -llh_tot, + ROADRUNNER_SIMULATION: simulation_results, + ROADRUNNER_LLH: llh_tot, + } + if mode == MODE_RES: # TODO: speed up by not using pandas + simulation_df = simulation_to_measurement_df( + simulation_results, petab_problem.measurement_df + ) + res_df = petab.calculate_residuals( + petab_problem.measurement_df, + simulation_df, + petab_problem.observable_df, + petab_problem.parameter_df, + ) + return { + RES: res_df, + ROADRUNNER_SIMULATION: simulation_results, + FVAL: -llh_tot, + } + + def simulate_per_condition( + self, + x_dct: dict, + roadrunner_instance: roadrunner.RoadRunner, + edata: ExpData, + parameter_mapping_per_condition: ParMappingDictQuadruple, + ) -> tuple[np.ndarray, float]: + """Simulate the model for a single condition. + + Parameters + ---------- + x_dct: + Parameter dictionary. + roadrunner_instance: + RoadRunner instance. + edata: + ExpData of a single condition. + parameter_mapping_per_condition: + Parameter parameter_mapping for a single condition. + + Returns + ------- + Tuple of simulation results in form of a numpy array and the + negative log-likelihood. + """ + # get timepoints and outputs to simulate + timepoints = list(edata.timepoints) + if timepoints[0] != 0.0: + timepoints = [0.0] + timepoints + if len(timepoints) == 1: + timepoints = [0.0] + timepoints + observables_ids = edata.get_observable_ids() + # steady state stuff + steady_state_calculations = False + state_variables = roadrunner_instance.model.getFloatingSpeciesIds() + # some states might be hidden as parameters with rate rules + rate_rule_ids = roadrunner_instance.getRateRuleIds() + state_variables += [ + rate_rule_id + for rate_rule_id in rate_rule_ids + if rate_rule_id not in state_variables + ] + # obs_ss = [] # TODO: add them to return values with info + state_ss = [] + + # if the first and third parameter mappings are not empty, we need + # to pre-equlibrate the model + if ( + parameter_mapping_per_condition[0] + and parameter_mapping_per_condition[2] + ): + steady_state_calculations = True + roadrunner_instance.conservedMoietyAnalysis = True + self.fill_in_parameters( + x_dct, + roadrunner_instance, + parameter_mapping_per_condition, + preeq=True, + ) + # allow simulation to reach steady state + roadrunner_instance.getSteadyStateSolver().setValue( + "allow_presimulation", True + ) + # steady state output = observables + state variables + steady_state_selections = observables_ids + state_variables + roadrunner_instance.steadyStateSelections = steady_state_selections + steady_state = roadrunner_instance.getSteadyStateValuesNamedArray() + # we split the steady state into observables and state variables + # obs_ss = steady_state[:, : len(observables_ids)].flatten() + state_ss = steady_state[:, len(observables_ids) :].flatten() + # turn off conserved moiety analysis + roadrunner_instance.conservedMoietyAnalysis = False + # reset the model + roadrunner_instance.reset() + # set parameters + par_map = self.fill_in_parameters( + x_dct, roadrunner_instance, parameter_mapping_per_condition + ) + # if steady state calculations are required, set state variables + if steady_state_calculations: + roadrunner_instance.setValues(state_variables, state_ss) + # fill in overriden species + self.fill_in_parameters( + x_dct, + roadrunner_instance, + parameter_mapping_per_condition, + filling_mode="only_species", + ) + + sim_res = roadrunner_instance.simulate( + times=timepoints, selections=[TIME] + observables_ids + ) + + llhs = calculate_llh(sim_res, edata, par_map) + + # reset the model + roadrunner_instance.reset() + + return sim_res, llhs + + def fill_in_parameters( + self, + problem_parameters: dict, + roadrunner_instance: Optional[roadrunner.RoadRunner] = None, + parameter_mapping: Optional[ParMappingDictQuadruple] = None, + preeq: bool = False, + filling_mode: Optional[str] = None, + ) -> dict: + """Fill in parameters into the roadrunner instance. + + Parameters + ---------- + roadrunner_instance: + RoadRunner instance to fill in parameters + problem_parameters: + Problem parameters as parameterId=>value dict. Only + parameters included here will be set. Remaining parameters will + be used as already set in `amici_model` and `edata`. + parameter_mapping: + Parameter mapping for current condition. Quadruple of dicts, + where the first dict contains the parameter mapping for pre- + equilibration, the second dict contains the parameter mapping + for the simulation, the third and fourth dict contain the scaling + factors for the pre-equilibration and simulation, respectively. + preeq: + Whether to fill in parameters for pre-equilibration. + filling_mode: + Which parameters to fill in. If None or "all", + all parameters are filled in. + Other options are "only_parameters" and "only_species". + + Returns + ------- + dict: + Mapping of parameter IDs to values. + """ + if filling_mode is None: + filling_mode = "all" + # check for valid filling modes + if filling_mode not in ["all", "only_parameters", "only_species"]: + raise ValueError( + "Invalid filling mode. Choose from 'all', " + "'only_parameters', 'only_species'." + ) + mapping = parameter_mapping[1] # default: simulation condition mapping + scaling = parameter_mapping[3] # default: simulation condition scaling + if preeq: + mapping = parameter_mapping[0] # pre-equilibration mapping + scaling = parameter_mapping[2] # pre-equilibration scaling + + # Parameter parameter_mapping may contain parameter_ids as values, + # these *must* be replaced + + def _get_par(model_par, val): + """Get parameter value from problem_parameters and mapping. + + Replace parameter IDs in parameter_mapping dicts by values from + problem_parameters where necessary + """ + if isinstance(val, str): + try: + # estimated parameter + return problem_parameters[val] + except KeyError: + # condition table overrides must have been handled already, + # e.g. by the PEtab parameter parameter_mapping, but + # parameters from InitialAssignments may still be present. + if mapping[val] == model_par: + # prevent infinite recursion + raise + return _get_par(val, mapping[val]) + if model_par in problem_parameters: + # user-provided + return problem_parameters[model_par] + # prevent nan-propagation in derivative + if np.isnan(val): + return 0.0 + # constant value + return val + + mapping_values = { + key: _get_par(key, val) for key, val in mapping.items() + } + # we assume the parameters to be given in the scale defined in the + # petab problem. Thus, they need to be unscaled. + mapping_values = unscale_parameters(mapping_values, scaling) + # seperate the parameters into ones that overwrite species and others + mapping_params = {} + mapping_species = {} + for key, value in mapping_values.items(): + if key in roadrunner_instance.model.getFloatingSpeciesIds(): + # values that originally were NaN are not set + if isinstance(mapping[key], str) or not np.isnan(mapping[key]): + mapping_species[key] = float(value) + else: + mapping_params[key] = value + + if filling_mode == "only_parameters" or filling_mode == "all": + # set parameters. + roadrunner_instance.setValues(mapping_params) + # reset is necessary to apply the changes to initial assignments + roadrunner_instance.reset() + if filling_mode == "only_species" or filling_mode == "all": + # set species + roadrunner_instance.setValues(mapping_species) + return mapping_values + + +def calculate_llh( + simulations: np.ndarray, + edata: ExpData, + parameter_mapping: dict, +) -> float: + """Calculate the negative log-likelihood for a single condition. + + Parameters + ---------- + simulations: + Simulations of condition. + edata: + ExpData of a single condition. + parameter_mapping: + Parameter mapping for the condition. + + Returns + ------- + float: + Negative log-likelihood. + """ + # if 0 is not in timepoints, remove the first row of the simulation + if 0.0 not in edata.timepoints: + simulations = simulations[1:, :] + + if not np.array_equal(simulations[:, 0], edata.timepoints): + raise ValueError( + "Simulation and Measurement have different timepoints." + ) + # check that simulation and condition have same dimensions and timepoints + if simulations.shape != edata.measurements.shape: + raise ValueError( + "Simulation and Measurement have different dimensions." + ) + # we can now drop the timepoints + simulations = simulations[:, 1:] + measurements = edata.measurements[:, 1:] + + def _fill_in_noise_formula(noise_formula): + """Fill in the noise formula.""" + if isinstance(noise_formula, numbers.Number): + return float(noise_formula) + # if it is not a number, it is assumed to be a string + if noise_formula in parameter_mapping.keys(): + return parameter_mapping[noise_formula] + + # replace noise formula with actual value from mapping + noise_formulae = np.array( + [_fill_in_noise_formula(formula) for formula in edata.noise_formulae] + ) + # check that the rows of noise are the columns of the simulation + if noise_formulae.shape[0] != simulations.shape[1]: + raise ValueError("Noise and Simulation have different dimensions.") + # per observable, decide on the llh function based on the noise dist + llhs = np.array( + [ + LLH_TYPES[noise_dist]( + measurements[:, i], simulations[:, i], noise_formulae[i] + ) + for i, noise_dist in enumerate(edata.noise_distributions) + ] + ).transpose() + # check whether all nan values in llhs coincide with nan measurements + if not np.all(np.isnan(llhs) == np.isnan(measurements)): + return np.nan + + # sum over all observables, ignoring nans + llhs = np.nansum(llhs) + + return llhs diff --git a/pypesto/objective/roadrunner/utils.py b/pypesto/objective/roadrunner/utils.py new file mode 100644 index 000000000..037c6ca2d --- /dev/null +++ b/pypesto/objective/roadrunner/utils.py @@ -0,0 +1,430 @@ +"""Utility functions for working with roadrunner and PEtab. + +Includes ExpData class for managing experimental data, SolverOptions class for +managing roadrunner solver options, and utility functions to convert between +PEtab measurement data and a roarunner simulation output back and forth. +""" +from __future__ import annotations + +import warnings +from typing import Optional, Sequence + +import numpy as np +import pandas as pd +import petab +import roadrunner +from petab.C import ( + LIN, + MEASUREMENT, + NOISE_DISTRIBUTION, + NOISE_FORMULA, + NORMAL, + OBSERVABLE_ID, + OBSERVABLE_TRANSFORMATION, + SIMULATION, + SIMULATION_CONDITION_ID, + TIME, +) + + +class ExpData: + """Class for managing experimental data for a single condition.""" + + def __init__( + self, + condition_id: str, + measurements: np.ndarray, + observable_ids: Sequence[str], + noise_distributions: np.ndarray, + noise_formulae: np.ndarray, + ): + """ + Initialize the ExpData object. + + Parameters + ---------- + condition_id: + Identifier of the condition. + measurements: + Numpy Array containing the measurement data. It is a 2D array of + dimension (n_timepoints, n_observables + 1). The first column is + the timepoints, the remaining columns are the observable values. + Observables not measured at a given timepoint should be NaN. + timepoints: + Timepoints of the measurement data. + observable_ids: + Observable ids of the measurement data. Order must match the + columns of the measurements array (-time). + noise_distributions: + Numpy Array describing noise distributions of the measurement + data. Dimension: (n_timepoints, n_observables). Each entry is a + string describing scale and type of noise distribution, the name + is "scale_type". E.g. "lin_normal", "log_normal", "log10_normal". + noise_formulae: + Numpy Array describing noise formulae of the measurement data. + Dimension: (n_timepoints, n_observables). Each entry is a string + describing the noise formula, either a parameter name or a constant. + """ + self.condition_id = condition_id + self.measurements = measurements + self.observable_ids = observable_ids + self.noise_distributions = noise_distributions + self.noise_formulae = noise_formulae + + self.sanity_check() + + # define timepoints as a property + @property + def timepoints(self): + """Timepoints of the measurement data.""" + return self.measurements[:, 0] + + def get_observable_ids(self): + """ + Get the observable ids of the measurement data. + + Returns + ------- + observable_ids: + Observable ids of the measurement data. + """ + return self.observable_ids + + def sanity_check(self): + """Perform a sanity check of the data.""" + if self.measurements.shape[1] != len(self.observable_ids) + 1: + raise ValueError( + "Number of columns in measurements does not match number of " + "observable ids + time." + ) + # check that the noise distributions and noise formulae have the + # same length as the number of observables + if len(self.noise_distributions) != len(self.observable_ids): + raise ValueError( + "Number of noise distributions does not match number of " + "observable ids." + ) + if len(self.noise_formulae) != len(self.observable_ids): + raise ValueError( + "Number of noise formulae does not match number of " + "observable ids." + ) + + @staticmethod + def from_petab_problem(petab_problem: petab.Problem) -> list[ExpData]: + """ + Create a list of ExpData object from a petab problem. + + Parameters + ---------- + petab_problem: + PEtab problem. + """ + # extract all condition ids from measurement data + condition_ids = list( + petab_problem.measurement_df["simulationConditionId"].unique() + ) + exp_datas = [ + ExpData.from_petab_single_condition( + condition_id=condition_id, petab_problem=petab_problem + ) + for condition_id in condition_ids + ] + return exp_datas + + @staticmethod + def from_petab_single_condition( + condition_id: str, petab_problem: petab.Problem + ) -> ExpData: + """ + Create an ExpData object from a single condition of a petab problem. + + Parameters + ---------- + condition_id: + Identifier of the condition. + petab_problem: + PEtab problem. + """ + # extract measurement data for a single condition + measurement_df = petab_problem.measurement_df[ + petab_problem.measurement_df[SIMULATION_CONDITION_ID] + == condition_id + ] + # turn measurement data into a numpy array + measurements, observale_ids = measurement_df_to_matrix(measurement_df) + # construct noise distributions and noise formulae + noise_distributions, noise_formulae = construct_noise_matrices( + petab_problem, observale_ids + ) + return ExpData( + condition_id=condition_id, + measurements=measurements, + observable_ids=observale_ids, + noise_distributions=noise_distributions, + noise_formulae=noise_formulae, + ) + + +class SolverOptions(dict): + """Class for managing solver options of roadrunner.""" + + def __init__( + self, + integrator: Optional[str] = None, + relative_tolerance: Optional[float] = None, + absolute_tolerance: Optional[float] = None, + maximum_num_steps: Optional[int] = None, + **kwargs, + ): + """ + Initialize the SolverOptions object. Can be used as a dictionary. + + Parameters + ---------- + integrator: + Integrator to use. + relative_tolerance: + Relative tolerance of the integrator. + absolute_tolerance: + Absolute tolerance of the integrator. + maximum_num_steps: + Maximum number of steps to take. + kwargs: + Additional solver options. + """ + super().__init__() + if integrator is None: + integrator = "cvode" + self.integrator = integrator + if relative_tolerance is None: + relative_tolerance = 1e-6 + self.relative_tolerance = relative_tolerance + if absolute_tolerance is None: + absolute_tolerance = 1e-12 + self.absolute_tolerance = absolute_tolerance + if maximum_num_steps is None: + maximum_num_steps = 20000 + self.maximum_num_steps = maximum_num_steps + self.update(kwargs) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) from None + + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + def __repr__(self): + """Return a dict representation of the SolverOptions object.""" + return f"{self.__class__.__name__}({super().__repr__()})" + + def apply_to_roadrunner(self, roadrunner_instance: roadrunner.RoadRunner): + """ + Apply the solver options to a roadrunner object inplace. + + Parameters + ---------- + roadrunner_instance: + Roadrunner object to apply the solver options to. + """ + # don't allow 'gillespie' integrator + if self.integrator == "gillespie": + raise ValueError("Gillespie integrator is not supported.") + # copy the options + options = self.copy() + # set integrator and remove integrator from options + roadrunner_instance.setIntegrator(options.pop("integrator")) + integrator = roadrunner_instance.getIntegrator() + # set the remaining options + for key, value in options.items(): + # try to set the options, if it fails, raise a warning + try: + integrator.setValue(key, value) + except RuntimeError as e: + warnings.warn( + f"Failed to set option {key} to {value}. Reason: {e}. " + f"Valid keys are: {integrator.getSettings()}.", + stacklevel=2, + ) + + +def unscale_parameters(value_dict: dict, petab_scale_dict: dict) -> dict: + """ + Unscale the scaled parameters from target scale to linear. + + Parameters + ---------- + value_dict: + Dictionary with values to scale. + petab_scale_dict: + Target Scales. + + Returns + ------- + unscaled_parameters: + Dict of unscaled parameters. + """ + if value_dict.keys() != petab_scale_dict.keys(): + raise AssertionError("Keys don't match.") + + for key, value in value_dict.items(): + value_dict[key] = petab.parameters.unscale( + value, petab_scale_dict[key] + ) + + return value_dict + + +def measurement_df_to_matrix( + measurement_df: pd.DataFrame, +) -> tuple[np.ndarray, list[str]]: + """ + Convert a PEtab measurement DataFrame to a matrix. + + Parameters + ---------- + measurement_df: + DataFrame containing the measurement data. + + Returns + ------- + measurement_matrix: + Numpy array containing the measurement data. It is a 2D array of + dimension (n_timepoints, n_observables + 1). The first column is + the timepoints, the remaining columns are the observable values. + Observables not measured at a given timepoint will be NaN. + observable_ids: + Observable ids of the measurement data. + """ + measurement_df = measurement_df.loc[ + :, ["observableId", "time", "measurement"] + ] + # get potential replicates via placeholder "count" + measurement_df["count"] = measurement_df.groupby( + ["observableId", "time"] + ).cumcount() + pivot_df = measurement_df.pivot( + index=["time", "count"], + columns="observableId", + values="measurement", + ).fillna(np.nan) + pivot_df.reset_index(inplace=True) + pivot_df.drop(columns="count", inplace=True) + + observable_ids = pivot_df.columns[1:] + measurement_matrix = pivot_df.to_numpy() + + return measurement_matrix, list(observable_ids) + + +def construct_noise_matrices( + petab_problem: petab.Problem, + observable_ids: Sequence[str], +) -> tuple[np.ndarray, np.ndarray]: + """ + Construct noise matrices from a PEtab problem. + + Parameters + ---------- + petab_problem: + PEtab problem. + observable_ids: + Observable ids of the measurement data. + + Returns + ------- + noise_distributions: + Numpy Array describing noise distributions of the measurement + data. Dimension: (1, n_observables). Each entry is a + string describing scale and type of noise distribution, the name + is "scale_type". E.g. "lin_normal", "log_normal", "log10_normal". + noise_formulae: + Numpy Array describing noise formulae of the measurement data. + Dimension: (1, n_observables). Each entry is a string + describing the noise formula, either a parameter name or a constant. + """ + + def _get_noise(observable_id: str) -> tuple[str, str]: + """ + Get noise distribution and noise formula for a single observable. + + Parameters + ---------- + observable_id: + Identifier of the observable. + + Returns + ------- + noise: + Tuple of noise distribution and noise formula. + """ + obs_df = petab_problem.observable_df + # check whether Index name is "observableId", if yes, get the row + if obs_df.index.name == OBSERVABLE_ID: + row = obs_df.loc[observable_id] + elif OBSERVABLE_ID in obs_df.columns: + row = obs_df[obs_df[OBSERVABLE_ID] == observable_id].iloc[0] + else: + raise ValueError("No observableId in observable_df.") + # noise distribution + noise_scale = LIN + noise_type = NORMAL + # check if "observableTransformation" and "noiseDistribution" exist + if OBSERVABLE_TRANSFORMATION in obs_df.columns: + if not pd.isna(row[OBSERVABLE_TRANSFORMATION]): + noise_scale = row[OBSERVABLE_TRANSFORMATION] + if NOISE_DISTRIBUTION in obs_df.columns: + if not pd.isna(row[NOISE_DISTRIBUTION]): + noise_type = row[NOISE_DISTRIBUTION] + noise_distribution = f"{noise_scale}_{noise_type}" + # TODO: check if noise_distribution is a allowed + # noise formula + noise_formula = row[NOISE_FORMULA] + return noise_distribution, noise_formula + + # extract noise distributions and noise formulae + noise = [_get_noise(observable_id) for observable_id in observable_ids] + + noise_distributions, noise_formulae = zip(*noise) + return np.array(noise_distributions), np.array(noise_formulae) + + +def simulation_to_measurement_df( + simulations: dict, + measurement_df: pd.DataFrame, +) -> pd.DataFrame: + """ + Convert simulation results to a measurement DataFrame. + + Parameters + ---------- + simulations: + Dictionary containing the simulation results of a roadrunner + simulator. The keys are the condition ids and the values are the + simulation results. + measurement_df: + DataFrame containing the measurement data of the PEtab problem. + """ + simulation_conditions = petab.get_simulation_conditions(measurement_df) + meas_dfs = [] + for _, condition_id in simulation_conditions.iterrows(): + meas_df_cond = measurement_df[ + measurement_df[SIMULATION_CONDITION_ID] + == condition_id[SIMULATION_CONDITION_ID] + ] + sim_res = simulations[condition_id[SIMULATION_CONDITION_ID]] + # in each row, replace the "measurement" with the simulation value + for index, row in meas_df_cond.iterrows(): + timepoint = row[TIME] + observable_id = row[OBSERVABLE_ID] + time_index = np.where(sim_res[TIME] == timepoint)[0][0] + sim_value = sim_res[observable_id][time_index] + meas_df_cond.at[index, MEASUREMENT] = sim_value + # rename measurement to simulation + meas_df_cond = meas_df_cond.rename(columns={MEASUREMENT: SIMULATION}) + meas_dfs.append(meas_df_cond) + sim_res_df = pd.concat(meas_dfs) + return sim_res_df diff --git a/setup.cfg b/setup.cfg index 071142c2b..0f60b84b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,7 @@ all = %(example)s %(select)s %(test)s + %(roadrunner)s all_optimizers = %(ipopt)s %(dlib)s @@ -94,6 +95,8 @@ all_optimizers = %(fides)s amici = amici >= 0.21.0 +roadrunner = + libroadrunner >= 2.6.0 petab = petab >= 0.2.0 ipopt = @@ -150,6 +153,7 @@ doc = %(petab)s %(aesara)s %(jax)s + %(roadrunner)s example = %(julia)s %(pymc)s diff --git a/test/base/test_roadrunner.py b/test/base/test_roadrunner.py new file mode 100644 index 000000000..43fd3cd16 --- /dev/null +++ b/test/base/test_roadrunner.py @@ -0,0 +1,106 @@ +"""Test the roadrunner interface.""" + +import logging + +import petab +import petabtests +import pytest + +import pypesto.objective.roadrunner as objective_rr + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@pytest.mark.parametrize( + "case, model_type, version", + [ + (case, "sbml", "v1.0.0") + for case in petabtests.get_cases(format_="sbml", version="v1.0.0") + ], +) +def test_petab_case(case, model_type, version): + """Wrapper for _execute_case for handling test outcomes""" + try: + _execute_case_rr(case, model_type, version) + except Exception as e: + if isinstance( + e, NotImplementedError + ) or "Timepoint-specific parameter overrides" in str(e): + logger.info( + f"Case {case} expectedly failed. Required functionality is " + f"not implemented: {e}" + ) + pytest.skip(str(e)) + else: + raise e + + +def _execute_case_rr(case, model_type, version): + """Run a single PEtab test suite case""" + case = petabtests.test_id_str(case) + logger.info(f"Case {case}") + + # case folder + case_dir = petabtests.get_case_dir(case, model_type, version) + + # load solution + solution = petabtests.load_solution( + case, format=model_type, version=version + ) + gt_llh = solution[petabtests.LLH] + gt_simulation_dfs = solution[petabtests.SIMULATION_DFS] + tol_llh = solution[petabtests.TOL_LLH] + tol_simulations = solution[petabtests.TOL_SIMULATIONS] + + # import petab problem + yaml_file = case_dir / petabtests.problem_yaml_name(case) + + importer = objective_rr.PetabImporterRR.from_yaml(yaml_file) + petab_problem = importer.petab_problem + obj = importer.create_objective() + + # the scaled parameters + problem_parameters = importer.petab_problem.x_nominal_scaled + + # simulate + ret = obj(problem_parameters, sensi_orders=(0,), return_dict=True) + + # extract results + llh = -ret["fval"] + simulation_df = objective_rr.simulation_to_measurement_df( + ret["simulation_results"], petab_problem.measurement_df + ) + + simulation_df = simulation_df.rename( + columns={petab.SIMULATION: petab.MEASUREMENT} + ) + petab.check_measurement_df(simulation_df, petab_problem.observable_df) + simulation_df = simulation_df.rename( + columns={petab.MEASUREMENT: petab.SIMULATION} + ) + simulation_df[petab.TIME] = simulation_df[petab.TIME].astype(int) + + # check if matches + llhs_match = petabtests.evaluate_llh(llh, gt_llh, tol_llh) + simulations_match = petabtests.evaluate_simulations( + [simulation_df], gt_simulation_dfs, tol_simulations + ) + + # log matches + logger.log( + logging.INFO if simulations_match else logging.ERROR, + f"LLH: simulated: {llh}, expected: {gt_llh}, match = {llhs_match}", + ) + logger.log( + logging.INFO if simulations_match else logging.ERROR, + f"Simulations: match = {simulations_match}", + ) + + if not all([llhs_match, simulations_match]): + logger.error(f"Case {version}/{model_type}/{case} failed.") + raise AssertionError( + f"Case {case}: Test results do not match expectations" + ) + + logger.info(f"Case {version}/{model_type}/{case} passed.") diff --git a/tox.ini b/tox.ini index db88c95bd..2aa6d02e8 100644 --- a/tox.ini +++ b/tox.ini @@ -49,9 +49,10 @@ description = # Unit tests [testenv:base] -extras = test,test_petab,amici,petab,emcee,dynesty,mltools,aesara,pymc,jax,fides +extras = test,test_petab,amici,petab,emcee,dynesty,mltools,aesara,pymc,jax,fides,roadrunner deps = git+https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab.git@master\#subdirectory=src/python + git+https://github.com/PEtab-dev/petab_test_suite@main commands = pytest --cov=pypesto --cov-report=xml --cov-append \ test/base --durations=0 \ @@ -72,7 +73,7 @@ description = Test basic functionality on Windows [testenv:petab] -extras = test,amici,petab,pyswarm +extras = test,amici,petab,pyswarm,roadrunner deps = git+https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab.git@master\#subdirectory=src/python git+https://github.com/PEtab-dev/petab_test_suite@main @@ -141,7 +142,7 @@ description = [testenv:doc] extras = - doc,amici,petab,aesara,jax,select + doc,amici,petab,aesara,jax,select,roadrunner commands = sphinx-build -W -b html doc/ doc/_build/html description = From ace9b8b6e45fd21e2dc6b139523b717117283071 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Tue, 2 Apr 2024 05:20:11 -0600 Subject: [PATCH 22/35] Changed to use API token for deployment (#1341) --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1b2f31cbc..0ee0cc564 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,8 +27,8 @@ jobs: - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* From b50bc8fc1853002d845db80f7e2332cb410f92a6 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 3 Apr 2024 10:04:40 +0200 Subject: [PATCH 23/35] Ensembles: don't expect `OptimizerResult.id` to be convertible to `int` (#1351) Fixes `Ensemble.from_optimization_{history,endpoints}`, which incorrectly assumed that `OptimizerResult.id` is always convertible to `int`. Closes #1349. --- pypesto/ensemble/ensemble.py | 10 +++++----- test/base/test_ensemble.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pypesto/ensemble/ensemble.py b/pypesto/ensemble/ensemble.py index 2b3f85c2e..8a17cd84b 100644 --- a/pypesto/ensemble/ensemble.py +++ b/pypesto/ensemble/ensemble.py @@ -2,7 +2,7 @@ import logging from functools import partial -from typing import TYPE_CHECKING, Callable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Union import numpy as np import pandas as pd @@ -480,7 +480,7 @@ def __init__( self, x_vectors: np.ndarray, x_names: Sequence[str] = None, - vector_tags: Sequence[tuple[int, int]] = None, + vector_tags: Sequence[Any] = None, ensemble_type: EnsembleType = None, predictions: Sequence[EnsemblePrediction] = None, lower_bound: np.ndarray = None, @@ -522,7 +522,7 @@ def __init__( self.x_vectors = x_vectors self.n_x = x_vectors.shape[0] self.n_vectors = x_vectors.shape[1] - self.vector_tags = vector_tags + self.vector_tags = list(vector_tags) if vector_tags is not None else [] self.summary = None # store bounds @@ -669,7 +669,7 @@ def from_optimization_endpoints( x_vectors.append(start["x"][result.problem.x_free_indices]) # the vector tag will be a -1 to indicate it is the last step - vector_tags.append((int(start["id"]), -1)) + vector_tags.append((start["id"], -1)) else: break @@ -800,7 +800,7 @@ def from_optimization_history( x_vectors.extend([x_trace[start][ind] for ind in indices]) vector_tags.extend( [ - (int(result.optimize_result.list[start]["id"]), ind) + (result.optimize_result.list[start]["id"], ind) for ind in indices ] ) diff --git a/test/base/test_ensemble.py b/test/base/test_ensemble.py index c12e702b3..12089af53 100644 --- a/test/base/test_ensemble.py +++ b/test/base/test_ensemble.py @@ -63,12 +63,12 @@ def test_ensemble_from_optimization(): # compare vector_tags with the expected values: ep_tags = [ - (int(result.optimize_result.list[i]["id"]), -1) for i in [0, 1, 2, 3] + (result.optimize_result.list[i]["id"], -1) for i in [0, 1, 2, 3] ] hist_tags = [ ( - int(result.optimize_result.list[i]["id"]), + result.optimize_result.list[i]["id"], len(result.optimize_result.list[i]["history"]._trace["fval"]) - 1 - j, From e0bec86dab8c81fd336db5a0665eac28f93d16d1 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 3 Apr 2024 10:19:55 +0200 Subject: [PATCH 24/35] SacessOptimizer: use 'spawn' start method (#1353) Use 'spawn' by default, but allow passing other start methods via `SacessOptimizer(..., mp_start_method=...)`. Closes #1343. --- pypesto/optimize/ess/sacess.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pypesto/optimize/ess/sacess.py b/pypesto/optimize/ess/sacess.py index c41dd4fa7..2991551d5 100644 --- a/pypesto/optimize/ess/sacess.py +++ b/pypesto/optimize/ess/sacess.py @@ -7,7 +7,7 @@ import os import time from math import ceil, sqrt -from multiprocessing import Manager, Process +from multiprocessing import get_context from multiprocessing.managers import SyncManager from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -67,6 +67,7 @@ def __init__( sacess_loglevel: int = logging.INFO, ess_loglevel: int = logging.WARNING, tmpdir: Union[Path, str] = None, + mp_start_method: str = "spawn", ): """Construct. @@ -105,6 +106,9 @@ def __init__( current working directory named ``SacessOptimizerTemp-{random suffix}``. When setting this option, make sure any optimizers running in parallel have a unique `tmpdir`. + mp_start_method: + The start method for the multiprocessing context. + See :mod:`multiprocessing` for details. """ if (num_workers is None and ess_init_args is None) or ( num_workers is not None and ess_init_args is not None @@ -135,6 +139,7 @@ def __init__( self.histories: Optional[ list["pypesto.history.memory.MemoryHistory"] ] = None + self.mp_ctx = get_context(mp_start_method) def minimize( self, @@ -195,12 +200,12 @@ def minimize( ) ) logging_thread = logging.handlers.QueueListener( - multiprocessing.Queue(-1), logging_handler + self.mp_ctx.Queue(-1), logging_handler ) # shared memory manager to handle shared state # (simulates the sacess manager process) - with Manager() as shmem_manager: + with self.mp_ctx.Manager() as shmem_manager: sacess_manager = SacessManager( shmem_manager=shmem_manager, ess_options=ess_init_args, @@ -223,7 +228,7 @@ def minimize( ] # launch worker processes worker_processes = [ - Process( + self.mp_ctx.Process( name=f"{self.__class__.__name__}-worker-{i:02d}", target=_run_worker, args=( From 5879755db975a2c9f3ff248da5204f19dbe290d5 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Fri, 5 Apr 2024 08:35:56 +0200 Subject: [PATCH 25/35] Require scipy<1.13.0 for pypesto[pymc] (#1360) See #1354. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 0f60b84b5..129cedb13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -117,6 +117,7 @@ mpi = mpi4py >= 3.0.3 pymc = arviz >= 0.12.1 + scipy < 1.13.0 # https://github.com/ICB-DCM/pyPESTO/issues/1354 aesara >= 2.8.6 pymc >= 4.2.1 aesara = From 321ee9dbd0223da0c08172fc08d61796e0d176aa Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Fri, 5 Apr 2024 09:45:20 +0200 Subject: [PATCH 26/35] pre-commit: pyupgrade (#1352) Stop using deprecated type annotations and imports. Include ruff pyupgrade rules in pre-commit. --- doc/conf.py | 1 - pypesto/C.py | 6 +- pypesto/ensemble/covariance_analysis.py | 8 +-- pypesto/ensemble/dimension_reduction.py | 14 ++--- pypesto/ensemble/ensemble.py | 13 ++-- pypesto/ensemble/task.py | 4 +- pypesto/ensemble/util.py | 3 +- .../inner_calculator_collector.py | 13 ++-- pypesto/hierarchical/ordinal/calculator.py | 2 +- pypesto/hierarchical/relative/calculator.py | 8 +-- .../semiquantitative/calculator.py | 2 +- pypesto/history/amici.py | 3 +- pypesto/history/base.py | 3 +- pypesto/history/csv.py | 3 +- pypesto/history/generate.py | 2 +- pypesto/history/hdf5.py | 3 +- pypesto/history/memory.py | 3 +- pypesto/history/util.py | 3 +- pypesto/objective/aesara/base.py | 9 +-- pypesto/objective/aggregated.py | 9 +-- pypesto/objective/amici/amici.py | 23 +++---- pypesto/objective/amici/amici_calculator.py | 13 ++-- pypesto/objective/amici/amici_util.py | 25 ++++---- pypesto/objective/base.py | 19 +++--- pypesto/objective/finite_difference.py | 20 +++--- pypesto/objective/function.py | 9 +-- pypesto/objective/jax/base.py | 5 +- pypesto/objective/julia/petabJl.py | 2 +- pypesto/objective/julia/petab_jl_importer.py | 36 +++++------ pypesto/objective/pre_post_process.py | 8 +-- pypesto/objective/priors.py | 9 +-- .../roadrunner/petab_importer_roadrunner.py | 23 +++---- pypesto/objective/roadrunner/road_runner.py | 9 +-- .../roadrunner/roadrunner_calculator.py | 3 +- pypesto/objective/roadrunner/utils.py | 10 +-- pypesto/optimize/ess/function_evaluator.py | 7 ++- pypesto/optimize/ess/refset.py | 4 +- pypesto/optimize/ess/sacess.py | 14 ++--- pypesto/optimize/optimize.py | 3 +- pypesto/optimize/optimizer.py | 29 +++++---- pypesto/optimize/options.py | 4 +- pypesto/optimize/util.py | 4 +- pypesto/petab/importer.py | 36 +++++------ pypesto/predict/amici_predictor.py | 17 ++--- pypesto/predict/task.py | 4 +- pypesto/problem/base.py | 21 +++---- pypesto/problem/hierarchical.py | 7 ++- pypesto/profile/approximate.py | 2 +- pypesto/profile/options.py | 4 +- pypesto/profile/profile.py | 3 +- pypesto/profile/util.py | 3 +- pypesto/result/optimize.py | 3 +- pypesto/result/predict.py | 5 +- pypesto/result/sample.py | 2 +- pypesto/sample/adaptive_metropolis.py | 5 +- pypesto/sample/adaptive_parallel_tempering.py | 4 +- pypesto/sample/auto_correlation.py | 2 +- pypesto/sample/dynesty.py | 13 ++-- pypesto/sample/emcee.py | 9 ++- pypesto/sample/geweke_test.py | 7 +-- pypesto/sample/metropolis.py | 5 +- pypesto/sample/parallel_tempering.py | 9 +-- pypesto/sample/pymc.py | 9 ++- pypesto/sample/sample.py | 4 +- pypesto/sample/sampler.py | 8 +-- pypesto/sample/util.py | 7 +-- pypesto/select/misc.py | 2 +- pypesto/select/problem.py | 3 +- pypesto/startpoint/base.py | 4 +- pypesto/store/hdf5.py | 2 +- pypesto/util.py | 5 +- pypesto/visualize/clust_color.py | 6 +- pypesto/visualize/dimension_reduction.py | 7 ++- pypesto/visualize/ensemble.py | 8 +-- pypesto/visualize/misc.py | 5 +- pypesto/visualize/model_fit.py | 3 +- pypesto/visualize/optimization_stats.py | 31 ++++----- pypesto/visualize/optimizer_convergence.py | 4 +- pypesto/visualize/optimizer_history.py | 37 +++++------ pypesto/visualize/parameters.py | 45 ++++++------- pypesto/visualize/profile_cis.py | 3 +- pypesto/visualize/profiles.py | 5 +- pypesto/visualize/reference_points.py | 3 +- pypesto/visualize/sampling.py | 63 ++++++++++--------- pypesto/visualize/select.py | 13 ++-- pypesto/visualize/spline_approximation.py | 3 +- pypesto/visualize/waterfall.py | 11 ++-- pyproject.toml | 1 + test/base/test_history.py | 2 +- test/select/test_select.py | 3 +- test/visualize/test_visualize.py | 2 +- 91 files changed, 439 insertions(+), 412 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index bfb4ef812..27cd4cb7c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # pyPESTO documentation build configuration file, created by # sphinx-quickstart on Mon Jul 30 08:30:38 2018. diff --git a/pypesto/C.py b/pypesto/C.py index 79660c02b..a3c36af31 100644 --- a/pypesto/C.py +++ b/pypesto/C.py @@ -5,7 +5,7 @@ """ from enum import Enum -from typing import Literal, Tuple, Union +from typing import Literal, Union ############################################################################### # ENSEMBLE @@ -307,8 +307,8 @@ class InnerParameterType(str, Enum): LEN_RGB = 3 # number of elements in an RGB color LEN_RGBA = 4 # number of elements in an RGBA color -RGB = Tuple[(float,) * LEN_RGB] # typing of an RGB color -RGBA = Tuple[(float,) * LEN_RGBA] # typing of an RGBA color +RGB = tuple[(float,) * LEN_RGB] # typing of an RGB color +RGBA = tuple[(float,) * LEN_RGBA] # typing of an RGBA color RGB_RGBA = Union[RGB, RGBA] # typing of an RGB or RGBA color RGBA_MIN = 0 # min value for an RGBA element RGBA_MAX = 1 # max value for an RGBA element diff --git a/pypesto/ensemble/covariance_analysis.py b/pypesto/ensemble/covariance_analysis.py index 0ea68c3d2..389a6217c 100644 --- a/pypesto/ensemble/covariance_analysis.py +++ b/pypesto/ensemble/covariance_analysis.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Union import numpy as np @@ -60,7 +60,7 @@ def get_spectral_decomposition_parameters( only_identifiable_directions: bool = False, cutoff_absolute_identifiable: float = 1e-16, cutoff_relative_identifiable: float = 1e-16, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """ Compute the spectral decomposition of ensemble parameters. @@ -128,7 +128,7 @@ def get_spectral_decomposition_predictions( only_identifiable_directions: bool = False, cutoff_absolute_identifiable: float = 1e-16, cutoff_relative_identifiable: float = 1e-16, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """ Compute the spectral decomposition of ensemble predictions. @@ -191,7 +191,7 @@ def get_spectral_decomposition_lowlevel( only_identifiable_directions: bool = False, cutoff_absolute_identifiable: float = 1e-16, cutoff_relative_identifiable: float = 1e-16, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """ Compute the spectral decomposition of ensemble parameters or predictions. diff --git a/pypesto/ensemble/dimension_reduction.py b/pypesto/ensemble/dimension_reduction.py index d1e8f1aa2..1cd1e777e 100644 --- a/pypesto/ensemble/dimension_reduction.py +++ b/pypesto/ensemble/dimension_reduction.py @@ -1,4 +1,4 @@ -from typing import Callable, Tuple, Union +from typing import Callable, Union import numpy as np @@ -11,7 +11,7 @@ def get_umap_representation_parameters( n_components: int = 2, normalize_data: bool = False, **kwargs, -) -> Tuple: +) -> tuple: """ UMAP of parameter ensemble. @@ -51,7 +51,7 @@ def get_umap_representation_predictions( n_components: int = 2, normalize_data: bool = False, **kwargs, -) -> Tuple: +) -> tuple: """ UMAP of ensemble prediction. @@ -97,7 +97,7 @@ def get_pca_representation_parameters( n_components: int = 2, rescale_data: bool = True, rescaler: Union[Callable, None] = None, -) -> Tuple: +) -> tuple: """ PCA of parameter ensemble. @@ -139,7 +139,7 @@ def get_pca_representation_predictions( n_components: int = 2, rescale_data: bool = True, rescaler: Union[Callable, None] = None, -) -> Tuple: +) -> tuple: """ PCA of ensemble prediction. @@ -188,7 +188,7 @@ def _get_umap_representation_lowlevel( n_components: int = 2, normalize_data: bool = False, **kwargs, -) -> Tuple: +) -> tuple: """ Low level UMAP of parameter ensemble. @@ -239,7 +239,7 @@ def _get_pca_representation_lowlevel( n_components: int = 2, rescale_data: bool = True, rescaler: Union[Callable, None] = None, -) -> Tuple: +) -> tuple: """ Low level PCA of parameter ensemble. diff --git a/pypesto/ensemble/ensemble.py b/pypesto/ensemble/ensemble.py index 8a17cd84b..ecd3d7b85 100644 --- a/pypesto/ensemble/ensemble.py +++ b/pypesto/ensemble/ensemble.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging +from collections.abc import Sequence from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable import numpy as np import pandas as pd @@ -67,7 +68,7 @@ class EnsemblePrediction: def __init__( self, - predictor: Optional[Callable[[Sequence], PredictionResult]] = None, + predictor: Callable[[Sequence], PredictionResult] | None = None, prediction_id: str = None, prediction_results: Sequence[PredictionResult] = None, lower_bound: Sequence[np.ndarray] = None, @@ -272,7 +273,7 @@ def _stack_outputs_sensi(ic: int) -> np.array: # stack into one numpy array return np.stack(output_sensi_list, axis=-1) - def _stack_weights(ic: int) -> Union[np.ndarray, None]: + def _stack_weights(ic: int) -> np.ndarray | None: """ Stack weights. @@ -845,7 +846,7 @@ def _map_parameters_by_objective( self, predictor: Callable, default_value: float = None, - ) -> list[Union[int, float]]: + ) -> list[int | float]: """ Create mapping for parameters from ensemble to predictor. @@ -1079,7 +1080,7 @@ def check_identifiability(self) -> pd.DataFrame: def entries_per_start( - fval_traces: list["np.ndarray"], + fval_traces: list[np.ndarray], cutoff: float, max_size: int, max_per_start: int, @@ -1168,7 +1169,7 @@ def get_vector_indices( return sorted(candidates, key=lambda i: trace_start[i])[:n_vectors] -def get_percentile_label(percentile: Union[float, int, str]) -> str: +def get_percentile_label(percentile: float | int | str) -> str: """Convert a percentile to a label. Labels for percentiles are used at different locations (e.g. ensemble diff --git a/pypesto/ensemble/task.py b/pypesto/ensemble/task.py index 5f95a6756..daabe32fc 100644 --- a/pypesto/ensemble/task.py +++ b/pypesto/ensemble/task.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Callable, List +from typing import Any, Callable import numpy as np @@ -32,7 +32,7 @@ def __init__( self.vectors = vectors self.id = id - def execute(self) -> List[Any]: + def execute(self) -> list[Any]: """Execute the task.""" logger.debug(f"Executing task {self.id}.") results = [] diff --git a/pypesto/ensemble/util.py b/pypesto/ensemble/util.py index 3d3a95d0a..4638e981b 100644 --- a/pypesto/ensemble/util.py +++ b/pypesto/ensemble/util.py @@ -1,8 +1,9 @@ """Ensemble utilities.""" import os +from collections.abc import Sequence from pathlib import Path -from typing import Callable, Literal, Sequence, Union +from typing import Callable, Literal, Union import h5py import numpy as np diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 07e220538..80e3151e5 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -7,7 +7,8 @@ from __future__ import annotations import copy -from typing import Sequence, Union +from collections.abc import Sequence +from typing import Union import numpy as np @@ -89,9 +90,9 @@ class InnerCalculatorCollector(AmiciCalculator): def __init__( self, data_types: set[str], - petab_problem: "petab.Problem", + petab_problem: petab.Problem, model: AmiciModel, - edatas: list["amici.ExpData"], + edatas: list[amici.ExpData], inner_options: dict, ): super().__init__() @@ -117,9 +118,9 @@ def initialize(self): def construct_inner_calculators( self, - petab_problem: "petab.Problem", + petab_problem: petab.Problem, model: AmiciModel, - edatas: list["amici.ExpData"], + edatas: list[amici.ExpData], inner_options: dict, ): """Construct inner calculators for each data type.""" @@ -217,7 +218,7 @@ def validate_options(self, inner_options: dict): def _get_quantitative_data_mask( self, - edatas: list["amici.ExpData"], + edatas: list[amici.ExpData], ) -> list[np.ndarray]: # transform experimental data edatas = [ diff --git a/pypesto/hierarchical/ordinal/calculator.py b/pypesto/hierarchical/ordinal/calculator.py index 890b93df9..8044d5d7d 100644 --- a/pypesto/hierarchical/ordinal/calculator.py +++ b/pypesto/hierarchical/ordinal/calculator.py @@ -1,7 +1,7 @@ """Definition of an optimal scaling calculator class.""" import copy -from typing import Sequence +from collections.abc import Sequence import numpy as np diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index 1033e7246..3ba59452f 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -1,7 +1,7 @@ from __future__ import annotations import copy -from typing import Optional, Sequence +from collections.abc import Sequence import numpy as np @@ -46,7 +46,7 @@ class RelativeAmiciCalculator(AmiciCalculator): def __init__( self, inner_problem: AmiciInnerProblem, - inner_solver: Optional[InnerSolver] = None, + inner_solver: InnerSolver | None = None, ): """Initialize the calculator from the given problem. @@ -83,7 +83,7 @@ def __call__( x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list["amici.ReturnData"] = None, + rdatas: list[amici.ReturnData] = None, ): """Perform the actual AMICI call, with hierarchical optimization. @@ -269,7 +269,7 @@ def calculate_directly( x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, - rdatas: list["amici.ReturnData"] = None, + rdatas: list[amici.ReturnData] = None, ): """Calculate directly via solver calculate methods. diff --git a/pypesto/hierarchical/semiquantitative/calculator.py b/pypesto/hierarchical/semiquantitative/calculator.py index 2dfdf87a3..3b2cd44a4 100644 --- a/pypesto/hierarchical/semiquantitative/calculator.py +++ b/pypesto/hierarchical/semiquantitative/calculator.py @@ -1,5 +1,5 @@ import copy -from typing import Sequence +from collections.abc import Sequence import numpy as np diff --git a/pypesto/history/amici.py b/pypesto/history/amici.py index cfd330cd5..6d0c8fb9b 100644 --- a/pypesto/history/amici.py +++ b/pypesto/history/amici.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from pathlib import Path -from typing import Sequence, Union +from typing import Union import numpy as np diff --git a/pypesto/history/base.py b/pypesto/history/base.py index 0e01df195..6c6786b8c 100644 --- a/pypesto/history/base.py +++ b/pypesto/history/base.py @@ -3,7 +3,8 @@ import numbers import time from abc import ABC, abstractmethod -from typing import Sequence, Union +from collections.abc import Sequence +from typing import Union import numpy as np diff --git a/pypesto/history/csv.py b/pypesto/history/csv.py index a777b4786..eeeacb389 100644 --- a/pypesto/history/csv.py +++ b/pypesto/history/csv.py @@ -3,7 +3,8 @@ import copy import os import time -from typing import Sequence, Union +from collections.abc import Sequence +from typing import Union import numpy as np import pandas as pd diff --git a/pypesto/history/generate.py b/pypesto/history/generate.py index 3e291c7c7..17da5dd97 100644 --- a/pypesto/history/generate.py +++ b/pypesto/history/generate.py @@ -1,7 +1,7 @@ """Generate a history from options and inputs.""" +from collections.abc import Sequence from pathlib import Path -from typing import Sequence from ..C import SUFFIXES_CSV, SUFFIXES_HDF5 from .base import CountHistory, HistoryBase diff --git a/pypesto/history/hdf5.py b/pypesto/history/hdf5.py index e7f308452..ccf625b91 100644 --- a/pypesto/history/hdf5.py +++ b/pypesto/history/hdf5.py @@ -2,9 +2,10 @@ import contextlib import time +from collections.abc import Sequence from functools import wraps from pathlib import Path -from typing import Sequence, Union +from typing import Union import h5py import numpy as np diff --git a/pypesto/history/memory.py b/pypesto/history/memory.py index 5e61ec825..ac1bf1991 100644 --- a/pypesto/history/memory.py +++ b/pypesto/history/memory.py @@ -1,7 +1,8 @@ """In-memory history.""" import time -from typing import Any, Sequence, Union +from collections.abc import Sequence +from typing import Any, Union import numpy as np diff --git a/pypesto/history/util.py b/pypesto/history/util.py index 35e6f0d49..297a2ebcf 100644 --- a/pypesto/history/util.py +++ b/pypesto/history/util.py @@ -1,8 +1,9 @@ """History utility functions.""" import numbers +from collections.abc import Sequence from functools import wraps -from typing import Sequence, Union +from typing import Union import numpy as np diff --git a/pypesto/objective/aesara/base.py b/pypesto/objective/aesara/base.py index 8ba7d3c29..dc2528f10 100644 --- a/pypesto/objective/aesara/base.py +++ b/pypesto/objective/aesara/base.py @@ -7,7 +7,8 @@ """ import copy -from typing import Optional, Sequence, Tuple +from collections.abc import Sequence +from typing import Optional import numpy as np @@ -107,7 +108,7 @@ def check_sensi_orders(self, sensi_orders, mode: ModeType) -> bool: def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs, ) -> ResultDict: @@ -196,7 +197,7 @@ def grad(self, inputs, g): parameter values. """ if self._log_prob_grad is None: - return super(AesaraObjectiveOp, self).grad(inputs, g) + return super().grad(inputs, g) (theta,) = inputs log_prob_grad = self._log_prob_grad(theta) return [g[0] * log_prob_grad] @@ -244,7 +245,7 @@ def grad(self, inputs, g): parameter values. """ if self._log_prob_hess is None: - return super(AesaraObjectiveGradOp, self).grad(inputs, g) + return super().grad(inputs, g) (theta,) = inputs log_prob_hess = self._log_prob_hess(theta) return [g[0].dot(log_prob_hess)] diff --git a/pypesto/objective/aggregated.py b/pypesto/objective/aggregated.py index ab74a638b..1246bca2d 100644 --- a/pypesto/objective/aggregated.py +++ b/pypesto/objective/aggregated.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from copy import deepcopy -from typing import Any, Dict, Sequence, Tuple +from typing import Any import numpy as np @@ -67,7 +68,7 @@ def check_mode(self, mode: ModeType) -> bool: def check_sensi_orders( self, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, ) -> bool: """See `ObjectiveBase` documentation.""" @@ -79,9 +80,9 @@ def check_sensi_orders( def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, - kwargs_list: Sequence[Dict[str, Any]] = None, + kwargs_list: Sequence[dict[str, Any]] = None, **kwargs, ) -> ResultDict: """ diff --git a/pypesto/objective/amici/amici.py b/pypesto/objective/amici/amici.py index c740b3578..9c7035329 100644 --- a/pypesto/objective/amici/amici.py +++ b/pypesto/objective/amici/amici.py @@ -3,8 +3,9 @@ import os import tempfile from collections import OrderedDict +from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Optional, Union import numpy as np @@ -275,7 +276,7 @@ def initialize(self): self.reset_steadystate_guesses() self.calculator.initialize() - def __deepcopy__(self, memodict: Dict = None) -> "AmiciObjective": + def __deepcopy__(self, memodict: dict = None) -> "AmiciObjective": import amici other = self.__class__.__new__(self.__class__) @@ -294,7 +295,7 @@ def __deepcopy__(self, memodict: Dict = None) -> "AmiciObjective": return other - def __getstate__(self) -> Dict: + def __getstate__(self) -> dict: import amici if self.amici_object_builder is None: @@ -336,7 +337,7 @@ def __getstate__(self) -> Dict: return state - def __setstate__(self, state: Dict) -> None: + def __setstate__(self, state: dict) -> None: import amici if state["amici_object_builder"] is None: @@ -384,7 +385,7 @@ def __setstate__(self, state: Dict) -> None: def check_sensi_orders( self, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, ) -> bool: """See `ObjectiveBase` documentation.""" @@ -417,11 +418,11 @@ def check_mode(self, mode: ModeType) -> bool: def __call__( self, x: np.ndarray, - sensi_orders: Tuple[int, ...] = (0,), + sensi_orders: tuple[int, ...] = (0,), mode: ModeType = MODE_FUN, return_dict: bool = False, **kwargs, - ) -> Union[float, np.ndarray, Tuple, ResultDict]: + ) -> Union[float, np.ndarray, tuple, ResultDict]: """See `ObjectiveBase` documentation.""" import amici @@ -439,7 +440,7 @@ def __call__( def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, edatas: Sequence["amici.ExpData"] = None, parameter_mapping: "ParameterMapping" = None, @@ -511,11 +512,11 @@ def call_unprocessed( return ret - def par_arr_to_dct(self, x: Sequence[float]) -> Dict[str, float]: + def par_arr_to_dct(self, x: Sequence[float]) -> dict[str, float]: """Create dict from parameter vector.""" return OrderedDict(zip(self.x_ids, x)) - def apply_steadystate_guess(self, condition_ix: int, x_dct: Dict) -> None: + def apply_steadystate_guess(self, condition_ix: int, x_dct: dict) -> None: """ Apply steady state guess to `edatas[condition_ix].x0`. @@ -551,7 +552,7 @@ def apply_steadystate_guess(self, condition_ix: int, x_dct: Dict) -> None: def store_steadystate_guess( self, condition_ix: int, - x_dct: Dict, + x_dct: dict, rdata: "amici.ReturnData", ) -> None: """ diff --git a/pypesto/objective/amici/amici_calculator.py b/pypesto/objective/amici/amici_calculator.py index 781533da2..de1d99030 100644 --- a/pypesto/objective/amici/amici_calculator.py +++ b/pypesto/objective/amici/amici_calculator.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union import numpy as np @@ -47,12 +48,12 @@ def initialize(self): def __call__( self, - x_dct: Dict, - sensi_orders: Tuple[int], + x_dct: dict, + sensi_orders: tuple[int], mode: ModeType, amici_model: AmiciModel, amici_solver: AmiciSolver, - edatas: List[amici.ExpData], + edatas: list[amici.ExpData], n_threads: int, x_ids: Sequence[str], parameter_mapping: ParameterMapping, @@ -150,11 +151,11 @@ def __call__( def calculate_function_values( rdatas, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, amici_model: AmiciModel, amici_solver: AmiciSolver, - edatas: List[amici.ExpData], + edatas: list[amici.ExpData], x_ids: Sequence[str], parameter_mapping: ParameterMapping, fim_for_hess: bool, diff --git a/pypesto/objective/amici/amici_util.py b/pypesto/objective/amici/amici_util.py index d0aa54cdd..ff5ca29fd 100644 --- a/pypesto/objective/amici/amici_util.py +++ b/pypesto/objective/amici/amici_util.py @@ -3,7 +3,8 @@ import logging import numbers import warnings -from typing import TYPE_CHECKING, Dict, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union import numpy as np @@ -37,8 +38,8 @@ def map_par_opt_to_par_sim( - condition_map_sim_var: Dict[str, Union[float, str]], - x_dct: Dict[str, float], + condition_map_sim_var: dict[str, float | str], + x_dct: dict[str, float], amici_model: AmiciModel, ) -> np.ndarray: """ @@ -149,8 +150,8 @@ def create_identity_parameter_mapping( def par_index_slices( par_opt_ids: Sequence[str], par_sim_ids: Sequence[str], - condition_map_sim_var: Dict[str, Union[float, str]], -) -> Tuple[np.ndarray, np.ndarray]: + condition_map_sim_var: dict[str, float | str], +) -> tuple[np.ndarray, np.ndarray]: """ Generate numpy arrays for indexing based on `mapping_par_opt_to_par_sim`. @@ -210,7 +211,7 @@ def par_index_slices( def add_sim_grad_to_opt_grad( par_opt_ids: Sequence[str], par_sim_ids: Sequence[str], - condition_map_sim_var: Dict[str, Union[float, str]], + condition_map_sim_var: dict[str, float | str], sim_grad: np.ndarray, opt_grad: np.ndarray, coefficient: float = 1.0, @@ -258,7 +259,7 @@ def add_sim_grad_to_opt_grad( def add_sim_hess_to_opt_hess( par_opt_ids: Sequence[str], par_sim_ids: Sequence[str], - condition_map_sim_var: Dict[str, Union[float, str]], + condition_map_sim_var: dict[str, float | str], sim_hess: np.ndarray, opt_hess: np.ndarray, coefficient: float = 1.0, @@ -309,7 +310,7 @@ def add_sim_hess_to_opt_hess( def sim_sres_to_opt_sres( par_opt_ids: Sequence[str], par_sim_ids: Sequence[str], - condition_map_sim_var: Dict[str, Union[float, str]], + condition_map_sim_var: dict[str, float | str], sim_sres: np.ndarray, coefficient: float = 1.0, ) -> np.ndarray: @@ -361,9 +362,9 @@ def log_simulation(data_ix, rdata) -> None: def get_error_output( amici_model: AmiciModel, - edatas: Sequence["amici.ExpData"], - rdatas: Sequence["amici.ReturnData"], - sensi_orders: Tuple[int, ...], + edatas: Sequence[amici.ExpData], + rdatas: Sequence[amici.ReturnData], + sensi_orders: tuple[int, ...], mode: ModeType, dim: int, ) -> dict: @@ -402,7 +403,7 @@ def get_error_output( def init_return_values( - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, dim: int, error: bool = False, diff --git a/pypesto/objective/base.py b/pypesto/objective/base.py index c3960bd14..0f9cc982a 100644 --- a/pypesto/objective/base.py +++ b/pypesto/objective/base.py @@ -1,7 +1,8 @@ import copy import logging from abc import ABC, abstractmethod -from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union +from collections.abc import Iterable, Sequence +from typing import Optional, Union import numpy as np import pandas as pd @@ -10,7 +11,7 @@ from ..history import NoHistory, create_history from .pre_post_process import FixedParametersProcessor, PrePostProcessor -ResultDict = Dict[str, Union[float, np.ndarray, Dict]] +ResultDict = dict[str, Union[float, np.ndarray, dict]] logger = logging.getLogger(__name__) @@ -96,7 +97,7 @@ def has_sres(self) -> bool: return self.check_sensi_orders((1,), MODE_RES) @property - def x_names(self) -> Union[List[str], None]: + def x_names(self) -> Union[list[str], None]: """Parameter names.""" if self._x_names is None: return self._x_names @@ -126,11 +127,11 @@ def create_history(self, id, x_names, options): def __call__( self, x: np.ndarray, - sensi_orders: Tuple[int, ...] = (0,), + sensi_orders: tuple[int, ...] = (0,), mode: ModeType = MODE_FUN, return_dict: bool = False, **kwargs, - ) -> Union[float, np.ndarray, Tuple, ResultDict]: + ) -> Union[float, np.ndarray, tuple, ResultDict]: """ Obtain arbitrary sensitivities. @@ -208,7 +209,7 @@ def __call__( def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs, ) -> ResultDict: @@ -265,7 +266,7 @@ def get_config(self) -> dict: def check_sensi_orders( self, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, ) -> bool: """ @@ -317,10 +318,10 @@ def check_sensi_orders( @staticmethod def output_to_tuple( - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs: Union[float, np.ndarray], - ) -> Tuple: + ) -> tuple: """ Return values as requested by the caller. diff --git a/pypesto/objective/finite_difference.py b/pypesto/objective/finite_difference.py index 70883b1d6..78d997a57 100644 --- a/pypesto/objective/finite_difference.py +++ b/pypesto/objective/finite_difference.py @@ -2,7 +2,7 @@ import copy import logging -from typing import Callable, Dict, List, Tuple, Union +from typing import Callable, Union import numpy as np @@ -315,7 +315,7 @@ def __init__( delta_grad: Union[FDDelta, np.ndarray, float, str] = 1e-6, delta_res: Union[FDDelta, float, np.ndarray, str] = 1e-6, method: str = CENTRAL, - x_names: List[str] = None, + x_names: list[str] = None, ): super().__init__(x_names=x_names) self.obj: ObjectiveBase = obj @@ -335,7 +335,7 @@ def __init__( def __deepcopy__( self, - memodict: Dict = None, + memodict: dict = None, ) -> "FD": """Create deepcopy of Objective.""" other = self.__class__.__new__(self.__class__) @@ -371,7 +371,7 @@ def has_sres(self) -> bool: def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs, ) -> ResultDict: @@ -397,7 +397,7 @@ def call_unprocessed( def _call_mode_fun( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], **kwargs, ) -> ResultDict: """Handle calls in function value mode. @@ -486,7 +486,7 @@ def f_grad(x): def _call_mode_res( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], **kwargs, ) -> ResultDict: """Handle calls in residual mode. @@ -531,9 +531,9 @@ def f_res(x): def _call_from_obj_fun( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], **kwargs, - ) -> Tuple[Tuple[int, ...], ResultDict]: + ) -> tuple[tuple[int, ...], ResultDict]: """ Call objective function for sensitivities. @@ -560,9 +560,9 @@ def _call_from_obj_fun( def _call_from_obj_res( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], **kwargs, - ) -> Tuple[Tuple[int, ...], ResultDict]: + ) -> tuple[tuple[int, ...], ResultDict]: """ Call objective function for sensitivities in residual mode. diff --git a/pypesto/objective/function.py b/pypesto/objective/function.py index 5c9a33e27..696da36a8 100644 --- a/pypesto/objective/function.py +++ b/pypesto/objective/function.py @@ -1,4 +1,5 @@ -from typing import Callable, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Callable, Union import numpy as np @@ -131,7 +132,7 @@ def get_config(self) -> dict: def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs, ) -> ResultDict: @@ -154,7 +155,7 @@ def call_unprocessed( def _call_mode_fun( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], ) -> ResultDict: if not sensi_orders: result = {} @@ -224,7 +225,7 @@ def _call_mode_fun( def _call_mode_res( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], ) -> ResultDict: if not sensi_orders: result = {} diff --git a/pypesto/objective/jax/base.py b/pypesto/objective/jax/base.py index c29876fc8..f5775275a 100644 --- a/pypesto/objective/jax/base.py +++ b/pypesto/objective/jax/base.py @@ -7,8 +7,9 @@ """ import copy +from collections.abc import Sequence from functools import partial -from typing import Callable, Sequence, Tuple +from typing import Callable import numpy as np @@ -219,7 +220,7 @@ def check_sensi_orders(self, sensi_orders, mode: ModeType) -> bool: def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, **kwargs, ) -> ResultDict: diff --git a/pypesto/objective/julia/petabJl.py b/pypesto/objective/julia/petabJl.py index 03da3bf50..84de76999 100644 --- a/pypesto/objective/julia/petabJl.py +++ b/pypesto/objective/julia/petabJl.py @@ -160,7 +160,7 @@ def precompile_model(self, force_compile: bool = False): ) # add a new line at the top of the original module to use the # precompiled module - with open(self.source_file, "r") as read_f: + with open(self.source_file) as read_f: if read_f.readline().endswith("_pre\n"): with open("dummy_temp_file.jl", "w+") as write_f: write_f.write(f"using {self.module}_pre\n\n") diff --git a/pypesto/objective/julia/petab_jl_importer.py b/pypesto/objective/julia/petab_jl_importer.py index 466f3c7a9..3d76900b5 100644 --- a/pypesto/objective/julia/petab_jl_importer.py +++ b/pypesto/objective/julia/petab_jl_importer.py @@ -4,7 +4,7 @@ import logging import os.path -from typing import Iterable, List, Optional, Tuple, Union +from collections.abc import Iterable import numpy as np @@ -51,12 +51,12 @@ def __init__( @staticmethod def from_yaml( yaml_file: str, - ode_solver_options: Optional[dict] = None, - gradient_method: Optional[str] = None, - hessian_method: Optional[str] = None, - sparse_jacobian: Optional[bool] = None, - verbose: Optional[bool] = None, - directory: Optional[str] = None, + ode_solver_options: dict | None = None, + gradient_method: str | None = None, + hessian_method: str | None = None, + sparse_jacobian: bool | None = None, + verbose: bool | None = None, + directory: str | None = None, ) -> PetabJlImporter: """ Create a `PetabJlImporter` from a yaml file. @@ -100,7 +100,7 @@ def from_yaml( ) def create_objective( - self, precompile: Optional[bool] = True + self, precompile: bool | None = True ) -> PEtabJlObjective: """ Create a `pypesto.objective.PEtabJlObjective` from the PEtab.jl problem. @@ -144,10 +144,10 @@ def create_objective( def create_problem( self, - x_guesses: Optional[Iterable[float]] = None, - lb_init: Union[np.ndarray, List[float], None] = None, - ub_init: Union[np.ndarray, List[float], None] = None, - precompile: Optional[bool] = True, + x_guesses: Iterable[float] | None = None, + lb_init: np.ndarray | list[float] | None = None, + ub_init: np.ndarray | list[float] | None = None, + precompile: bool | None = True, ) -> Problem: """ Create a `pypesto.Problem` from the PEtab.jl problem. @@ -181,11 +181,11 @@ def create_problem( def _get_default_options( - ode_solver_options: Union[dict, None] = None, - gradient_method: Union[str, None] = None, - hessian_method: Union[str, None] = None, - sparse_jacobian: Union[str, None] = None, - verbose: Union[str, None] = None, + ode_solver_options: dict | None = None, + gradient_method: str | None = None, + hessian_method: str | None = None, + sparse_jacobian: str | None = None, + verbose: str | None = None, ) -> dict: """ If values are not specified, get default values for the options. @@ -263,7 +263,7 @@ def _get_default_options( def _write_julia_file( yaml_file: str, options: dict, directory: str -) -> Tuple[str, str]: +) -> tuple[str, str]: """ Write the Julia file. diff --git a/pypesto/objective/pre_post_process.py b/pypesto/objective/pre_post_process.py index ae9ef7f13..9b6983142 100644 --- a/pypesto/objective/pre_post_process.py +++ b/pypesto/objective/pre_post_process.py @@ -1,4 +1,4 @@ -from typing import Dict, Sequence +from collections.abc import Sequence import numpy as np @@ -36,7 +36,7 @@ def preprocess(self, x: np.ndarray) -> np.ndarray: # pylint: disable=R0201 """ return x - def postprocess(self, result: Dict) -> Dict: # pylint: disable=R0201 + def postprocess(self, result: dict) -> dict: # pylint: disable=R0201 """ Convert all arrays into np.ndarrays if necessary, and return them. @@ -65,7 +65,7 @@ def reduce(self, x: np.ndarray) -> np.ndarray: # pylint: disable=R0201 return x @staticmethod - def as_ndarrays(result: Dict) -> Dict: + def as_ndarrays(result: dict) -> dict: """ Convert all array_like objects to np.ndarrays. @@ -128,7 +128,7 @@ def reduce(self, x: np.ndarray) -> np.ndarray: else: return x - def postprocess(self, result: Dict) -> Dict: + def postprocess(self, result: dict) -> dict: """Constrain results to optimization parameter dimensions.""" result = super().postprocess(result) diff --git a/pypesto/objective/priors.py b/pypesto/objective/priors.py index 95bb0ffa6..493cf9b41 100644 --- a/pypesto/objective/priors.py +++ b/pypesto/objective/priors.py @@ -1,7 +1,8 @@ import logging import math +from collections.abc import Sequence from copy import deepcopy -from typing import Callable, Dict, List, Sequence, Tuple, Union +from typing import Callable, Union import numpy as np @@ -50,7 +51,7 @@ class NegLogParameterPriors(ObjectiveBase): def __init__( self, - prior_list: List[Dict], + prior_list: list[dict], x_names: Sequence[str] = None, ): """ @@ -75,7 +76,7 @@ def __deepcopy__(self, memodict=None): def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: C.ModeType, **kwargs, ) -> ResultDict: @@ -115,7 +116,7 @@ def call_unprocessed( def check_sensi_orders( self, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: C.ModeType, ) -> bool: """See `ObjectiveBase` documentation.""" diff --git a/pypesto/objective/roadrunner/petab_importer_roadrunner.py b/pypesto/objective/roadrunner/petab_importer_roadrunner.py index b5238724b..f952d5b11 100644 --- a/pypesto/objective/roadrunner/petab_importer_roadrunner.py +++ b/pypesto/objective/roadrunner/petab_importer_roadrunner.py @@ -7,8 +7,9 @@ from __future__ import annotations import numbers +from collections.abc import Iterable from pathlib import Path -from typing import Any, Iterable, Optional, Union +from typing import Any import libsbml import petab @@ -62,7 +63,7 @@ def __init__( self.rr = roadrunner.RoadRunner() @staticmethod - def from_yaml(yaml_config: Union[Path, str]) -> PetabImporterRR: + def from_yaml(yaml_config: Path | str) -> PetabImporterRR: """Simplified constructor using a petab yaml file.""" petab_problem = petab.Problem.from_yaml(yaml_config) @@ -70,8 +71,8 @@ def from_yaml(yaml_config: Union[Path, str]) -> PetabImporterRR: def _check_noise_formulae( self, - edatas: Optional[list[ExpData]] = None, - parameter_mapping: Optional[list[ParMappingDictQuadruple]] = None, + edatas: list[ExpData] | None = None, + parameter_mapping: list[ParMappingDictQuadruple] | None = None, ): """Check if the noise formulae are valid. @@ -198,8 +199,8 @@ def create_parameter_mapping(self): def create_objective( self, - rr: Optional[roadrunner.RoadRunner] = None, - edatas: Optional[ExpData] = None, + rr: roadrunner.RoadRunner | None = None, + edatas: ExpData | None = None, ) -> RoadRunnerObjective: """Create a :class:`pypesto.objective.RoadRunnerObjective`. @@ -236,7 +237,7 @@ def create_objective( x_names=x_names, ) - def create_prior(self) -> Union[NegLogParameterPriors, None]: + def create_prior(self) -> NegLogParameterPriors | None: """ Create a prior from the parameter table. @@ -287,10 +288,10 @@ def create_startpoint_method(self, **kwargs) -> StartpointMethod: def create_problem( self, - objective: Optional[RoadRunnerObjective] = None, - x_guesses: Optional[Iterable[float]] = None, - problem_kwargs: Optional[dict[str, Any]] = None, - startpoint_kwargs: Optional[dict[str, Any]] = None, + objective: RoadRunnerObjective | None = None, + x_guesses: Iterable[float] | None = None, + problem_kwargs: dict[str, Any] | None = None, + startpoint_kwargs: dict[str, Any] | None = None, **kwargs, ) -> Problem: """Create a :class:`pypesto.problem.Problem`. diff --git a/pypesto/objective/roadrunner/road_runner.py b/pypesto/objective/roadrunner/road_runner.py index c711944e4..98a9a9e7e 100644 --- a/pypesto/objective/roadrunner/road_runner.py +++ b/pypesto/objective/roadrunner/road_runner.py @@ -3,7 +3,8 @@ Currently does not support sensitivities. """ from collections import OrderedDict -from typing import Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Optional, Union import numpy as np import roadrunner @@ -82,7 +83,7 @@ def get_config(self) -> dict: def __call__( self, x: np.ndarray, - sensi_orders: Tuple[int, ...] = (0,), + sensi_orders: tuple[int, ...] = (0,), mode: ModeType = MODE_FUN, return_dict: bool = False, **kwargs, @@ -93,7 +94,7 @@ def __call__( def call_unprocessed( self, x: np.ndarray, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, edatas: Optional[Sequence[ExpData]] = None, parameter_mapping: Optional[list[ParMappingDictQuadruple]] = None, @@ -127,7 +128,7 @@ def call_unprocessed( def check_sensi_orders( self, - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, ) -> bool: """See :class:`ObjectiveBase` documentation.""" diff --git a/pypesto/objective/roadrunner/roadrunner_calculator.py b/pypesto/objective/roadrunner/roadrunner_calculator.py index 65f147d4e..0f1958dc0 100644 --- a/pypesto/objective/roadrunner/roadrunner_calculator.py +++ b/pypesto/objective/roadrunner/roadrunner_calculator.py @@ -3,7 +3,8 @@ Handles all RoadRunner.simulate calls, calculates likelihoods and residuals. """ import numbers -from typing import Optional, Sequence +from collections.abc import Sequence +from typing import Optional import numpy as np import petab diff --git a/pypesto/objective/roadrunner/utils.py b/pypesto/objective/roadrunner/utils.py index 037c6ca2d..364ea8776 100644 --- a/pypesto/objective/roadrunner/utils.py +++ b/pypesto/objective/roadrunner/utils.py @@ -7,7 +7,7 @@ from __future__ import annotations import warnings -from typing import Optional, Sequence +from collections.abc import Sequence import numpy as np import pandas as pd @@ -171,10 +171,10 @@ class SolverOptions(dict): def __init__( self, - integrator: Optional[str] = None, - relative_tolerance: Optional[float] = None, - absolute_tolerance: Optional[float] = None, - maximum_num_steps: Optional[int] = None, + integrator: str | None = None, + relative_tolerance: float | None = None, + absolute_tolerance: float | None = None, + maximum_num_steps: int | None = None, **kwargs, ): """ diff --git a/pypesto/optimize/ess/function_evaluator.py b/pypesto/optimize/ess/function_evaluator.py index aa8b67f6b..a17d42961 100644 --- a/pypesto/optimize/ess/function_evaluator.py +++ b/pypesto/optimize/ess/function_evaluator.py @@ -2,9 +2,10 @@ import multiprocessing import threading +from collections.abc import Sequence from concurrent.futures import ThreadPoolExecutor from copy import deepcopy -from typing import Optional, Sequence, Tuple +from typing import Optional from warnings import warn import numpy as np @@ -87,7 +88,7 @@ def multiple(self, xs: Sequence[np.ndarray]) -> np.array: self.n_eval_round += len(xs) return res - def single_random(self) -> Tuple[np.array, float]: + def single_random(self) -> tuple[np.array, float]: """Evaluate objective at a random point. The point is generated by the given``startpoint_method``. A new point @@ -104,7 +105,7 @@ def single_random(self) -> Tuple[np.array, float]: fx = self.single(x) return x, fx - def multiple_random(self, n: int) -> Tuple[np.array, np.array]: + def multiple_random(self, n: int) -> tuple[np.array, np.array]: """Evaluate objective at ``n`` random points. The points are generated by the given``startpoint_method``. New points diff --git a/pypesto/optimize/ess/refset.py b/pypesto/optimize/ess/refset.py index b5fec2a1e..5c75d54a2 100644 --- a/pypesto/optimize/ess/refset.py +++ b/pypesto/optimize/ess/refset.py @@ -1,6 +1,6 @@ """ReferenceSet functionality for scatter search.""" -from typing import Any, Dict, Optional +from typing import Any, Optional import numpy as np @@ -65,7 +65,7 @@ def __init__( self.fx = fx self.n_stuck = np.zeros(shape=[dim]) - self.attributes: Dict[Any, np.array] = {} + self.attributes: dict[Any, np.array] = {} def sort(self): """Sort RefSet by quality.""" diff --git a/pypesto/optimize/ess/sacess.py b/pypesto/optimize/ess/sacess.py index 2991551d5..c750f79c3 100644 --- a/pypesto/optimize/ess/sacess.py +++ b/pypesto/optimize/ess/sacess.py @@ -10,7 +10,7 @@ from multiprocessing import get_context from multiprocessing.managers import SyncManager from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union from uuid import uuid1 from warnings import warn @@ -62,7 +62,7 @@ class SacessOptimizer: def __init__( self, num_workers: Optional[int] = None, - ess_init_args: Optional[List[Dict[str, Any]]] = None, + ess_init_args: Optional[list[dict[str, Any]]] = None, max_walltime_s: float = np.inf, sacess_loglevel: int = logging.INFO, ess_loglevel: int = logging.WARNING, @@ -345,7 +345,7 @@ class SacessManager: def __init__( self, shmem_manager: SyncManager, - ess_options: List[Dict[str, Any]], + ess_options: list[dict[str, Any]], dim: int, ): self._num_workers = len(ess_options) @@ -365,12 +365,12 @@ def __init__( self._logger = logging.getLogger() self._result_queue = shmem_manager.Queue() - def get_best_solution(self) -> Tuple[np.array, float]: + def get_best_solution(self) -> tuple[np.array, float]: """Get the best objective value and corresponding parameters.""" with self._lock: return np.array(self._best_known_x), self._best_known_fx.value - def reconfigure_worker(self, worker_idx: int) -> Dict: + def reconfigure_worker(self, worker_idx: int) -> dict: """Reconfigure the given worker. Updates the ESS options for the given worker to those of the worker at @@ -495,7 +495,7 @@ class SacessWorker: def __init__( self, manager: SacessManager, - ess_kwargs: Dict[str, Any], + ess_kwargs: dict[str, Any], worker_idx: int, max_walltime_s: float = np.inf, loglevel: int = logging.INFO, @@ -763,7 +763,7 @@ def get_default_ess_options( "pypesto.optimize.Optimizer", Callable[..., "pypesto.optimize.Optimizer"], ] = True, -) -> List[Dict]: +) -> list[dict]: """Get default ESS settings for (SA)CESS. Returns settings for ``num_workers`` parallel scatter searches, combining diff --git a/pypesto/optimize/optimize.py b/pypesto/optimize/optimize.py index 96b7f23c1..539a1fca3 100644 --- a/pypesto/optimize/optimize.py +++ b/pypesto/optimize/optimize.py @@ -1,5 +1,6 @@ import logging -from typing import Callable, Iterable, Union +from collections.abc import Iterable +from typing import Callable, Union from warnings import warn from ..engine import Engine, SingleCoreEngine diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index 5625e1dfe..ddf6b2d2b 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -6,7 +6,7 @@ import time import warnings from functools import wraps -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING import numpy as np import scipy.optimize @@ -327,7 +327,7 @@ def __init__( self, method: str = "L-BFGS-B", tol: float = None, - options: Dict = None, + options: dict = None, ): super().__init__() @@ -546,7 +546,7 @@ def get_default_options(self): class IpoptOptimizer(Optimizer): """Use IpOpt (https://pypi.org/project/ipopt/) for optimization.""" - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): """ Initialize. @@ -623,7 +623,7 @@ def is_least_squares(self): class DlibOptimizer(Optimizer): """Use the Dlib toolbox for optimization.""" - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): super().__init__() self.options = options @@ -699,7 +699,7 @@ def check_x0_support(self, x_guesses: np.ndarray = None) -> bool: class PyswarmOptimizer(Optimizer): """Global optimization using pyswarm.""" - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): super().__init__() if options is None: @@ -760,7 +760,7 @@ class CmaOptimizer(Optimizer): (https://github.com/CMA-ES/pycma). """ - def __init__(self, par_sigma0: float = 0.25, options: Dict = None): + def __init__(self, par_sigma0: float = 0.25, options: dict = None): """ Initialize. @@ -867,7 +867,7 @@ class ScipyDifferentialEvolutionOptimizer(Optimizer): population size, default value 15 """ - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): super().__init__() if options is None: @@ -938,7 +938,7 @@ class PyswarmsOptimizer(Optimizer): Default: 1000 """ - def __init__(self, par_popsize: float = 10, options: Dict = None): + def __init__(self, par_popsize: float = 10, options: dict = None): super().__init__() all_options = {"maxiter": 1000, "c1": 0.5, "c2": 0.3, "w": 0.9} @@ -1045,8 +1045,8 @@ def __init__( self, method=None, local_method=None, - options: Dict = None, - local_options: Dict = None, + options: dict = None, + local_options: dict = None, ): """ Initialize. @@ -1291,11 +1291,10 @@ class FidesOptimizer(Optimizer): def __init__( self, - hessian_update: Optional[ - fides.hessian_approximation.HessianApproximation - ] = "default", - options: Optional[Dict] = None, - verbose: Optional[int] = logging.INFO, + hessian_update: None + | (fides.hessian_approximation.HessianApproximation) = "default", + options: dict | None = None, + verbose: int | None = logging.INFO, ): """ Initialize. diff --git a/pypesto/optimize/options.py b/pypesto/optimize/options.py index dfa148260..9983a7ccf 100644 --- a/pypesto/optimize/options.py +++ b/pypesto/optimize/options.py @@ -1,4 +1,4 @@ -from typing import Dict, Union +from typing import Union class OptimizeOptions(dict): @@ -49,7 +49,7 @@ def __getattr__(self, key): @staticmethod def assert_instance( - maybe_options: Union["OptimizeOptions", Dict], + maybe_options: Union["OptimizeOptions", dict], ) -> "OptimizeOptions": """ Return a valid options object. diff --git a/pypesto/optimize/util.py b/pypesto/optimize/util.py index 65a219ec6..2ba565a18 100644 --- a/pypesto/optimize/util.py +++ b/pypesto/optimize/util.py @@ -2,8 +2,8 @@ import logging import os +from collections.abc import Iterable from pathlib import Path -from typing import Iterable, List import h5py import numpy as np @@ -79,7 +79,7 @@ def preprocess_hdf5_history( def postprocess_hdf5_history( - ret: List[OptimizerResult], + ret: list[OptimizerResult], storage_file: str, history_options: HistoryOptions, ) -> None: diff --git a/pypesto/petab/importer.py b/pypesto/petab/importer.py index b19b12ebb..ea00937bf 100644 --- a/pypesto/petab/importer.py +++ b/pypesto/petab/importer.py @@ -8,19 +8,13 @@ import sys import tempfile import warnings +from collections.abc import Iterable, Sequence from dataclasses import dataclass from functools import partial from importlib.metadata import version from typing import ( Any, Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - Union, ) import numpy as np @@ -94,7 +88,7 @@ def __init__( validate_petab: bool = True, validate_petab_hierarchical: bool = True, hierarchical: bool = False, - inner_options: Dict = None, + inner_options: dict = None, ): """Initialize importer. @@ -179,7 +173,7 @@ def __init__( @staticmethod def from_yaml( - yaml_config: Union[dict, str], + yaml_config: dict | str, output_folder: str = None, model_name: str = None, ) -> PetabImporter: @@ -203,7 +197,7 @@ def check_gradients( *args, rtol: float = 1e-2, atol: float = 1e-3, - mode: Union[str, List[str]] = None, + mode: str | list[str] = None, multi_eps=None, **kwargs, ) -> bool: @@ -408,7 +402,7 @@ def create_edatas( model: amici.Model = None, simulation_conditions=None, verbose: bool = True, - ) -> List[amici.ExpData]: + ) -> list[amici.ExpData]: """Create list of :class:`amici.amici.ExpData` objects.""" # create model if model is None: @@ -568,10 +562,10 @@ def create_predictor( self, objective: AmiciObjective = None, amici_output_fields: Sequence[str] = None, - post_processor: Union[Callable, None] = None, - post_processor_sensi: Union[Callable, None] = None, - post_processor_time: Union[Callable, None] = None, - max_chunk_size: Union[int, None] = None, + post_processor: Callable | None = None, + post_processor_sensi: Callable | None = None, + post_processor_time: Callable | None = None, + max_chunk_size: int | None = None, output_ids: Sequence[str] = None, condition_ids: Sequence[str] = None, ) -> AmiciPredictor: @@ -655,7 +649,7 @@ def create_predictor( return predictor - def create_prior(self) -> Union[NegLogParameterPriors, None]: + def create_prior(self) -> NegLogParameterPriors | None: """ Create a prior from the parameter table. @@ -709,9 +703,9 @@ def create_startpoint_method(self, **kwargs) -> StartpointMethod: def create_problem( self, objective: AmiciObjective = None, - x_guesses: Optional[Iterable[float]] = None, - problem_kwargs: Dict[str, Any] = None, - startpoint_kwargs: Dict[str, Any] = None, + x_guesses: Iterable[float] | None = None, + problem_kwargs: dict[str, Any] = None, + startpoint_kwargs: dict[str, Any] = None, **kwargs, ) -> Problem: """Create a :class:`pypesto.problem.Problem`. @@ -1043,8 +1037,8 @@ class PetabStartpoints(CheckedStartpoints): def __init__(self, petab_problem: petab.Problem, **kwargs): super().__init__(**kwargs) self._parameter_df = petab_problem.parameter_df.copy() - self._priors: Optional[List[Tuple]] = None - self._free_ids: Optional[List[str]] = None + self._priors: list[tuple] | None = None + self._free_ids: list[str] | None = None def _setup( self, diff --git a/pypesto/predict/amici_predictor.py b/pypesto/predict/amici_predictor.py index 56842f3a6..60d4935e8 100644 --- a/pypesto/predict/amici_predictor.py +++ b/pypesto/predict/amici_predictor.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Sequence from copy import deepcopy -from typing import TYPE_CHECKING, Callable, Sequence, Union +from typing import TYPE_CHECKING, Callable import numpy as np @@ -56,13 +57,13 @@ class AmiciPredictor: def __init__( self, amici_objective: AmiciObjective, - amici_output_fields: Union[Sequence[str], None] = None, - post_processor: Union[PostProcessor, None] = None, - post_processor_sensi: Union[PostProcessor, None] = None, - post_processor_time: Union[PostProcessor, None] = None, - max_chunk_size: Union[int, None] = None, - output_ids: Union[Sequence[str], None] = None, - condition_ids: Union[Sequence[str], None] = None, + amici_output_fields: Sequence[str] | None = None, + post_processor: PostProcessor | None = None, + post_processor_sensi: PostProcessor | None = None, + post_processor_time: PostProcessor | None = None, + max_chunk_size: int | None = None, + output_ids: Sequence[str] | None = None, + condition_ids: Sequence[str] | None = None, ): """ Initialize predictor. diff --git a/pypesto/predict/task.py b/pypesto/predict/task.py index 8cefc360b..2312e7a0b 100644 --- a/pypesto/predict/task.py +++ b/pypesto/predict/task.py @@ -1,5 +1,5 @@ import logging -from typing import Sequence, Tuple +from collections.abc import Sequence from ..C import ModeType from ..engine import Task @@ -30,7 +30,7 @@ def __init__( self, predictor, #: 'pypesto.predict.Predictor', # noqa: F821 x: Sequence[float], - sensi_orders: Tuple[int, ...], + sensi_orders: tuple[int, ...], mode: ModeType, id: str, ): diff --git a/pypesto/problem/base.py b/pypesto/problem/base.py index 20b95acce..8b11c5dd4 100644 --- a/pypesto/problem/base.py +++ b/pypesto/problem/base.py @@ -1,9 +1,8 @@ import copy import logging +from collections.abc import Iterable from typing import ( Callable, - Iterable, - List, Optional, SupportsFloat, SupportsInt, @@ -90,8 +89,8 @@ class Problem: def __init__( self, objective: ObjectiveBase, - lb: Union[np.ndarray, List[float]], - ub: Union[np.ndarray, List[float]], + lb: Union[np.ndarray, list[float]], + ub: Union[np.ndarray, list[float]], dim_full: Optional[int] = None, x_fixed_indices: Optional[SupportsIntIterableOrValue] = None, x_fixed_vals: Optional[SupportsFloatIterableOrValue] = None, @@ -99,8 +98,8 @@ def __init__( x_names: Optional[Iterable[str]] = None, x_scales: Optional[Iterable[str]] = None, x_priors_defs: Optional[NegLogParameterPriors] = None, - lb_init: Union[np.ndarray, List[float], None] = None, - ub_init: Union[np.ndarray, List[float], None] = None, + lb_init: Union[np.ndarray, list[float], None] = None, + ub_init: Union[np.ndarray, list[float], None] = None, copy_objective: bool = True, startpoint_method: Union[StartpointMethod, Callable, bool] = None, ): @@ -124,7 +123,7 @@ def __init__( if x_fixed_indices is None: x_fixed_indices = [] x_fixed_indices = _make_iterable_if_value(x_fixed_indices, "int") - self.x_fixed_indices: List[int] = [ + self.x_fixed_indices: list[int] = [ _type_conversion_with_check(idx, ix, "fixed indices", "int") for idx, ix in enumerate(x_fixed_indices) ] @@ -134,7 +133,7 @@ def __init__( if x_fixed_vals is None: x_fixed_vals = [] x_fixed_vals = _make_iterable_if_value(x_fixed_vals, "float") - self.x_fixed_vals: List[float] = [ + self.x_fixed_vals: list[float] = [ _type_conversion_with_check(idx, x, "fixed values", "float") for idx, x in enumerate(x_fixed_vals) ] @@ -149,7 +148,7 @@ def __init__( x_names = [f"x{j}" for j in range(0, self.dim_full)] if len(set(x_names)) != len(x_names): raise ValueError("Parameter names x_names must be unique") - self.x_names: List[str] = list(x_names) + self.x_names: list[str] = list(x_names) if x_scales is None: x_scales = ["lin"] * self.dim_full @@ -197,7 +196,7 @@ def dim(self) -> int: return self.dim_full - len(self.x_fixed_indices) @property - def x_free_indices(self) -> List[int]: + def x_free_indices(self) -> list[int]: """Return non fixed parameters.""" return sorted(set(range(0, self.dim_full)) - set(self.x_fixed_indices)) @@ -394,7 +393,7 @@ def get_full_matrix( def get_reduced_vector( self, x_full: Union[np.ndarray, None], - x_indices: Optional[List[int]] = None, + x_indices: Optional[list[int]] = None, ) -> Union[np.ndarray, None]: """ Keep only those elements, which indices are specified in x_indices. diff --git a/pypesto/problem/hierarchical.py b/pypesto/problem/hierarchical.py index bc8b2a2ff..b54fdd3d0 100644 --- a/pypesto/problem/hierarchical.py +++ b/pypesto/problem/hierarchical.py @@ -1,5 +1,6 @@ import logging -from typing import Iterable, List, Optional, SupportsFloat, SupportsInt, Union +from collections.abc import Iterable +from typing import Optional, SupportsFloat, SupportsInt, Union import numpy as np @@ -44,8 +45,8 @@ class HierarchicalProblem(Problem): def __init__( self, inner_x_names: Optional[Iterable[str]] = None, - inner_lb: Optional[Union[np.ndarray, List[float]]] = None, - inner_ub: Optional[Union[np.ndarray, List[float]]] = None, + inner_lb: Optional[Union[np.ndarray, list[float]]] = None, + inner_ub: Optional[Union[np.ndarray, list[float]]] = None, **problem_kwargs: dict, ): super().__init__(**problem_kwargs) diff --git a/pypesto/profile/approximate.py b/pypesto/profile/approximate.py index 34363691d..ea8db4c10 100644 --- a/pypesto/profile/approximate.py +++ b/pypesto/profile/approximate.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable +from collections.abc import Iterable import numpy as np from scipy.stats import multivariate_normal diff --git a/pypesto/profile/options.py b/pypesto/profile/options.py index 3d3c69862..f2c9dc42a 100644 --- a/pypesto/profile/options.py +++ b/pypesto/profile/options.py @@ -1,4 +1,4 @@ -from typing import Dict, Union +from typing import Union class ProfileOptions(dict): @@ -80,7 +80,7 @@ def __getattr__(self, key): @staticmethod def create_instance( - maybe_options: Union["ProfileOptions", Dict], + maybe_options: Union["ProfileOptions", dict], ) -> "ProfileOptions": """ Return a valid options object. diff --git a/pypesto/profile/profile.py b/pypesto/profile/profile.py index 1518b7913..e4e124964 100644 --- a/pypesto/profile/profile.py +++ b/pypesto/profile/profile.py @@ -1,6 +1,7 @@ import copy import logging -from typing import Callable, Iterable, Union +from collections.abc import Iterable +from typing import Callable, Union from ..engine import Engine, SingleCoreEngine from ..optimize import Optimizer diff --git a/pypesto/profile/util.py b/pypesto/profile/util.py index 89ae5f919..6a87403f8 100644 --- a/pypesto/profile/util.py +++ b/pypesto/profile/util.py @@ -1,6 +1,7 @@ """Utility function for profile module.""" -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any import numpy as np import scipy.stats diff --git a/pypesto/result/optimize.py b/pypesto/result/optimize.py index 28434e20a..4fb2883fa 100644 --- a/pypesto/result/optimize.py +++ b/pypesto/result/optimize.py @@ -3,8 +3,9 @@ import logging import warnings from collections import Counter +from collections.abc import Sequence from copy import deepcopy -from typing import Sequence, Union +from typing import Union import numpy as np import pandas as pd diff --git a/pypesto/result/predict.py b/pypesto/result/predict.py index d77056792..a13c2785c 100644 --- a/pypesto/result/predict.py +++ b/pypesto/result/predict.py @@ -1,9 +1,10 @@ """PredictionResult and PredictionConditionResult.""" import os +from collections.abc import Sequence from pathlib import Path from time import time -from typing import Dict, Sequence, Union +from typing import Union from warnings import warn import h5py @@ -122,7 +123,7 @@ class PredictionResult: def __init__( self, - conditions: Sequence[Union[PredictionConditionResult, Dict]], + conditions: Sequence[Union[PredictionConditionResult, dict]], condition_ids: Sequence[str] = None, comment: str = None, ): diff --git a/pypesto/result/sample.py b/pypesto/result/sample.py index 2cb144b1b..70df221b1 100644 --- a/pypesto/result/sample.py +++ b/pypesto/result/sample.py @@ -1,6 +1,6 @@ """Sampling result.""" -from typing import Iterable +from collections.abc import Iterable import numpy as np diff --git a/pypesto/sample/adaptive_metropolis.py b/pypesto/sample/adaptive_metropolis.py index 27c5f4c93..c0b9e13dd 100644 --- a/pypesto/sample/adaptive_metropolis.py +++ b/pypesto/sample/adaptive_metropolis.py @@ -1,5 +1,4 @@ import numbers -from typing import Dict, Tuple import numpy as np @@ -57,7 +56,7 @@ class AdaptiveMetropolisSampler(MetropolisSampler): * https://github.com/ICB-DCM/PESTO/blob/master/private/updateStatistics.m """ - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): super().__init__(options) self._cov = None self._mean_hist = None @@ -146,7 +145,7 @@ def update_history_statistics( x_new: np.ndarray, n_cur_sample: int, decay_constant: float, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Update sampling mean and covariance matrix via weighted average. Update sampling mean and covariance matrix based on the previous diff --git a/pypesto/sample/adaptive_parallel_tempering.py b/pypesto/sample/adaptive_parallel_tempering.py index adaaf2cb2..1738c6ce7 100644 --- a/pypesto/sample/adaptive_parallel_tempering.py +++ b/pypesto/sample/adaptive_parallel_tempering.py @@ -1,6 +1,6 @@ """AdaptiveParallelTemperingSampler class.""" -from typing import Dict, Sequence +from collections.abc import Sequence import numpy as np @@ -30,7 +30,7 @@ class AdaptiveParallelTemperingSampler(ParallelTemperingSampler): """ @classmethod - def default_options(cls) -> Dict: + def default_options(cls) -> dict: """Get default options for sampler.""" options = super().default_options() # scaling factor for temperature adaptation diff --git a/pypesto/sample/auto_correlation.py b/pypesto/sample/auto_correlation.py index 0a27a155c..dbf33a7eb 100644 --- a/pypesto/sample/auto_correlation.py +++ b/pypesto/sample/auto_correlation.py @@ -26,7 +26,7 @@ def autocorrelation_sokal(chain: np.ndarray) -> np.ndarray: the MCMC chain. """ nsamples, npar = chain.shape - tau_est = np.zeros((npar)) + tau_est = np.zeros(npar) # Calculate the fast Fourier transform x = np.fft.fft(chain, axis=0) diff --git a/pypesto/sample/dynesty.py b/pypesto/sample/dynesty.py index f6db40021..89a14ba24 100644 --- a/pypesto/sample/dynesty.py +++ b/pypesto/sample/dynesty.py @@ -28,7 +28,6 @@ import importlib import logging import warnings -from typing import List, Union import cloudpickle # noqa: S403 import numpy as np @@ -120,12 +119,10 @@ def __init__( self.objective_type = objective_type # set in initialize - self.problem: Union[Problem, None] = None - self.sampler: Union[ - dynesty.DynamicNestedSampler, - dynesty.NestedSampler, - None, - ] = None + self.problem: Problem | None = None + self.sampler: ( + dynesty.DynamicNestedSampler | dynesty.NestedSampler | None + ) = None def prior_transform(self, prior_sample: np.ndarray) -> np.ndarray: """Transform prior sample from unit cube to pyPESTO prior. @@ -165,7 +162,7 @@ def loglikelihood(self, x): def initialize( self, problem: Problem, - x0: Union[np.ndarray, List[np.ndarray]] = None, + x0: np.ndarray | list[np.ndarray] = None, ) -> None: """Initialize the sampler.""" self.problem = problem diff --git a/pypesto/sample/emcee.py b/pypesto/sample/emcee.py index 224a94d99..1afe8930f 100644 --- a/pypesto/sample/emcee.py +++ b/pypesto/sample/emcee.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from typing import List, Union import numpy as np @@ -60,9 +59,9 @@ def __init__( self.run_args: dict = run_args # set in initialize - self.problem: Union[Problem, None] = None - self.sampler: Union[emcee.EnsembleSampler, None] = None - self.state: Union[emcee.State, None] = None + self.problem: Problem | None = None + self.sampler: emcee.EnsembleSampler | None = None + self.state: emcee.State | None = None def get_epsilon_ball_initial_state( self, @@ -123,7 +122,7 @@ def get_epsilon_ball_initial_state( def initialize( self, problem: Problem, - x0: Union[np.ndarray, List[np.ndarray]], + x0: np.ndarray | list[np.ndarray], ) -> None: """Initialize the sampler. diff --git a/pypesto/sample/geweke_test.py b/pypesto/sample/geweke_test.py index 48fe319dc..186c9890d 100644 --- a/pypesto/sample/geweke_test.py +++ b/pypesto/sample/geweke_test.py @@ -2,7 +2,6 @@ import logging import warnings -from typing import Tuple import numpy as np from scipy.stats import norm @@ -51,7 +50,7 @@ def spectrum(x: np.ndarray, nfft: int = None, nw: int = None) -> np.ndarray: index = np.arange(nw) # Normalizing scale factor kmu = k * np.linalg.norm(w) ** 2 - spectral_density = np.zeros((nfft)) + spectral_density = np.zeros(nfft) for _ in range(k): xw = w * x[index] @@ -95,7 +94,7 @@ def spectrum0(x: np.ndarray) -> np.ndarray: def calculate_zscore( chain: np.ndarray, a: float = 0.1, b: float = 0.5 -) -> Tuple[float, float]: +) -> tuple[float, float]: """ Perform a Geweke test on a chain. @@ -197,7 +196,7 @@ def burn_in_by_sequential_geweke( # to sorting p-values max_z = np.max(np.absolute(z), axis=1) idxs = max_z.argsort()[::-1] # sort descend - alpha2 = zscore * np.ones((len(idxs))) + alpha2 = zscore * np.ones(len(idxs)) for i in range(len(max_z)): alpha2[idxs[i]] /= len(fragments) - np.argwhere(idxs == i).item(0) + 1 diff --git a/pypesto/sample/metropolis.py b/pypesto/sample/metropolis.py index f69bfc026..34c5d8d85 100644 --- a/pypesto/sample/metropolis.py +++ b/pypesto/sample/metropolis.py @@ -1,4 +1,5 @@ -from typing import Dict, Sequence, Union +from collections.abc import Sequence +from typing import Union import numpy as np @@ -36,7 +37,7 @@ class MetropolisSampler(InternalSampler): * https://github.com/ICB-DCM/PESTO/blob/master/private/performPT.m """ - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): super().__init__(options) self.problem: Union[Problem, None] = None self.neglogpost: Union[ObjectiveBase, None] = None diff --git a/pypesto/sample/parallel_tempering.py b/pypesto/sample/parallel_tempering.py index 47a6a99a1..8f2c88f9f 100644 --- a/pypesto/sample/parallel_tempering.py +++ b/pypesto/sample/parallel_tempering.py @@ -1,6 +1,7 @@ import copy import logging -from typing import Dict, List, Sequence, Union +from collections.abc import Sequence +from typing import Union import numpy as np @@ -40,7 +41,7 @@ def __init__( internal_sampler: InternalSampler, betas: Sequence[float] = None, n_chains: int = None, - options: Dict = None, + options: dict = None, ): super().__init__(options) @@ -74,7 +75,7 @@ def __init__( sampler.make_internal(temper_lpost=self.temper_lpost) @classmethod - def default_options(cls) -> Dict: + def default_options(cls) -> dict: """Return the default options for the sampler.""" return { "max_temp": 5e4, @@ -86,7 +87,7 @@ def default_options(cls) -> Dict: } def initialize( - self, problem: Problem, x0: Union[np.ndarray, List[np.ndarray]] + self, problem: Problem, x0: Union[np.ndarray, list[np.ndarray]] ): """Initialize all samplers.""" n_chains = len(self.samplers) diff --git a/pypesto/sample/pymc.py b/pypesto/sample/pymc.py index c6b37e3fa..1ad43373e 100644 --- a/pypesto/sample/pymc.py +++ b/pypesto/sample/pymc.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from typing import Union import arviz as az import numpy as np @@ -112,10 +111,10 @@ def __init__( ): super().__init__(kwargs) self.step_function = step_function - self.problem: Union[Problem, None] = None - self.x0: Union[np.ndarray, None] = None - self.trace: Union[pymc.backends.Text, None] = None - self.data: Union[az.InferenceData, None] = None + self.problem: Problem | None = None + self.x0: np.ndarray | None = None + self.trace: pymc.backends.Text | None = None + self.data: az.InferenceData | None = None @classmethod def translate_options(cls, options): diff --git a/pypesto/sample/sample.py b/pypesto/sample/sample.py index f1e3ad65b..5130b1b3d 100644 --- a/pypesto/sample/sample.py +++ b/pypesto/sample/sample.py @@ -1,6 +1,6 @@ import logging from time import process_time -from typing import Callable, List, Optional, Union +from typing import Callable, Optional, Union import numpy as np @@ -18,7 +18,7 @@ def sample( problem: Problem, n_samples: Optional[int], sampler: Sampler = None, - x0: Union[np.ndarray, List[np.ndarray]] = None, + x0: Union[np.ndarray, list[np.ndarray]] = None, result: Result = None, filename: Union[str, Callable, None] = None, overwrite: bool = False, diff --git a/pypesto/sample/sampler.py b/pypesto/sample/sampler.py index c94f8b7a3..b2e2f6fc9 100644 --- a/pypesto/sample/sampler.py +++ b/pypesto/sample/sampler.py @@ -1,7 +1,7 @@ """Various Sampler classes.""" import abc -from typing import Dict, List, Union +from typing import Union import numpy as np @@ -26,12 +26,12 @@ class Sampler(abc.ABC): `initialize`, and updated in `sample`. """ - def __init__(self, options: Dict = None): + def __init__(self, options: dict = None): self.options = self.__class__.translate_options(options) @abc.abstractmethod def initialize( - self, problem: Problem, x0: Union[np.ndarray, List[np.ndarray]] + self, problem: Problem, x0: Union[np.ndarray, list[np.ndarray]] ): """Initialize the sampler. @@ -64,7 +64,7 @@ def get_samples(self) -> McmcPtResult: """Get the generated samples.""" @classmethod - def default_options(cls) -> Dict: + def default_options(cls) -> dict: """ Set/Get default options. diff --git a/pypesto/sample/util.py b/pypesto/sample/util.py index 4b1faf32b..30e322659 100644 --- a/pypesto/sample/util.py +++ b/pypesto/sample/util.py @@ -2,7 +2,6 @@ import logging import os -from typing import Tuple import numpy as np @@ -17,7 +16,7 @@ def calculate_ci_mcmc_sample( result: Result, ci_level: float = 0.95, exclude_burn_in: bool = True, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Calculate parameter credibility intervals based on MCMC samples. Parameters @@ -51,7 +50,7 @@ def calculate_ci_mcmc_sample( def calculate_ci_mcmc_sample_prediction( simulated_values: np.ndarray, ci_level: float = 0.95, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Calculate prediction credibility intervals based on MCMC samples. Parameters @@ -74,7 +73,7 @@ def calculate_ci( values: np.ndarray, ci_level: float, **kwargs, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Calculate confidence/credibility levels using percentiles. Parameters diff --git a/pypesto/select/misc.py b/pypesto/select/misc.py index 25459a318..62cfad31c 100644 --- a/pypesto/select/misc.py +++ b/pypesto/select/misc.py @@ -1,7 +1,7 @@ """Miscellaneous methods.""" import logging -from typing import Iterable +from collections.abc import Iterable import pandas as pd import petab diff --git a/pypesto/select/problem.py b/pypesto/select/problem.py index 20308a391..9692890ef 100644 --- a/pypesto/select/problem.py +++ b/pypesto/select/problem.py @@ -1,7 +1,8 @@ """Manage all components of a pyPESTO model selection problem.""" import warnings -from typing import Any, Iterable, Optional +from collections.abc import Iterable +from typing import Any, Optional import petab_select from petab_select import Model diff --git a/pypesto/startpoint/base.py b/pypesto/startpoint/base.py index b0fc8799a..f6c57ce5a 100644 --- a/pypesto/startpoint/base.py +++ b/pypesto/startpoint/base.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Callable, Union +from typing import TYPE_CHECKING, Callable import numpy as np @@ -243,7 +243,7 @@ def sample( def to_startpoint_method( - maybe_startpoint_method: Union[StartpointMethod, Callable, bool], + maybe_startpoint_method: StartpointMethod | Callable | bool, ) -> StartpointMethod: """Create StartpointMethod instance if possible, otherwise raise. diff --git a/pypesto/store/hdf5.py b/pypesto/store/hdf5.py index 1ffefd407..e6e4db4cd 100644 --- a/pypesto/store/hdf5.py +++ b/pypesto/store/hdf5.py @@ -1,7 +1,7 @@ """Convenience functions for working with HDF5 files.""" +from collections.abc import Collection from numbers import Integral, Number, Real -from typing import Collection import h5py import numpy as np diff --git a/pypesto/util.py b/pypesto/util.py index 14b321e6b..b0734a7d7 100644 --- a/pypesto/util.py +++ b/pypesto/util.py @@ -6,9 +6,10 @@ """ +from collections.abc import Sequence from numbers import Number from operator import itemgetter -from typing import Any, Callable, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Optional, Union import numpy as np from scipy import cluster @@ -247,7 +248,7 @@ def delete_nan_inf( x: Optional[Sequence[Union[np.ndarray, list[float]]]] = None, xdim: Optional[int] = 1, magnitude_bound: Optional[float] = np.inf, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """ Delete nan and inf values in fvals. diff --git a/pypesto/visualize/clust_color.py b/pypesto/visualize/clust_color.py index 459998dfd..bf0cf02c9 100644 --- a/pypesto/visualize/clust_color.py +++ b/pypesto/visualize/clust_color.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import Optional, Union import matplotlib.cm as cm import numpy as np @@ -160,8 +160,8 @@ def assign_colors( def assign_colors_for_list( num_entries: int, - colors: Optional[Union[RGBA, List[RGBA], np.ndarray]] = None, -) -> Union[List[List[float]], np.ndarray]: + colors: Optional[Union[RGBA, list[RGBA], np.ndarray]] = None, +) -> Union[list[list[float]], np.ndarray]: """ Create a list of colors for a list of items. diff --git a/pypesto/visualize/dimension_reduction.py b/pypesto/visualize/dimension_reduction.py index 2db23d769..f0ce1750f 100644 --- a/pypesto/visualize/dimension_reduction.py +++ b/pypesto/visualize/dimension_reduction.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Sequence, Tuple +from collections.abc import Sequence +from typing import TYPE_CHECKING import matplotlib.pyplot as plt import numpy as np @@ -209,8 +210,8 @@ def ensemble_crosstab_scatter_lowlevel( def ensemble_scatter_lowlevel( dataset, - ax: Optional[plt.Axes] = None, - size: Optional[Tuple[float]] = (12, 6), + ax: plt.Axes | None = None, + size: tuple[float] | None = (12, 6), x_label: str = "component 1", y_label: str = "component 2", color_by: Sequence[float] = None, diff --git a/pypesto/visualize/ensemble.py b/pypesto/visualize/ensemble.py index e4dc44ed0..413341223 100644 --- a/pypesto/visualize/ensemble.py +++ b/pypesto/visualize/ensemble.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Optional import matplotlib.pyplot as plt import numpy as np @@ -13,7 +13,7 @@ def ensemble_identifiability( ensemble: Ensemble, ax: Optional[plt.Axes] = None, - size: Optional[Tuple[float]] = (12, 6), + size: Optional[tuple[float]] = (12, 6), ): """ Visualize identifiablity of parameter ensemble. @@ -58,7 +58,7 @@ def ensemble_identifiability_lowlevel( ub_hit: np.ndarray, both_hit: np.ndarray, ax: Optional[plt.Axes] = None, - size: Optional[Tuple[float]] = (16, 10), + size: Optional[tuple[float]] = (16, 10), ): """ Low-level identifiablity routine. @@ -186,7 +186,7 @@ def ensemble_identifiability_lowlevel( ax.text( 0, -0.7, - "identifiable parameters: {:4.1f}%".format(x_none * 100), + f"identifiable parameters: {x_none * 100:4.1f}%", va="top", ) diff --git a/pypesto/visualize/misc.py b/pypesto/visualize/misc.py index d190edc47..d23f710b5 100644 --- a/pypesto/visualize/misc.py +++ b/pypesto/visualize/misc.py @@ -1,6 +1,7 @@ import warnings +from collections.abc import Iterable from numbers import Number -from typing import Iterable, List, Optional, Union +from typing import Optional, Union import numpy as np @@ -24,7 +25,7 @@ def process_result_list( - results: Union[Result, List[Result]], colors=None, legends=None + results: Union[Result, list[Result]], colors=None, legends=None ): """ Assign colors and legends to a list of results, check user provided lists. diff --git a/pypesto/visualize/model_fit.py b/pypesto/visualize/model_fit.py index 378f3c0fd..dd2fd0d4a 100644 --- a/pypesto/visualize/model_fit.py +++ b/pypesto/visualize/model_fit.py @@ -5,7 +5,8 @@ """ import copy -from typing import Sequence, Union +from collections.abc import Sequence +from typing import Union import amici import amici.plotting diff --git a/pypesto/visualize/optimization_stats.py b/pypesto/visualize/optimization_stats.py index dc957332b..74a5323e7 100644 --- a/pypesto/visualize/optimization_stats.py +++ b/pypesto/visualize/optimization_stats.py @@ -1,5 +1,6 @@ +from collections.abc import Iterable, Sequence from numbers import Real -from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union +from typing import Optional, Union import matplotlib.axes import matplotlib.pyplot as plt @@ -14,11 +15,11 @@ def optimization_run_properties_one_plot( results: Result, - properties_to_plot: Optional[List[str]] = None, - size: Tuple[float, float] = (18.5, 10.5), + properties_to_plot: Optional[list[str]] = None, + size: tuple[float, float] = (18.5, 10.5), start_indices: Optional[Union[int, Iterable[int]]] = None, - colors: Optional[Union[List[float], List[List[float]]]] = None, - legends: Optional[Union[str, List[str]]] = None, + colors: Optional[Union[list[float], list[list[float]]]] = None, + legends: Optional[Union[str, list[str]]] = None, plot_type: str = "line", ) -> matplotlib.axes.Axes: """ @@ -117,13 +118,13 @@ def optimization_run_properties_one_plot( def optimization_run_properties_per_multistart( results: Union[Result, Sequence[Result]], - properties_to_plot: Optional[List[str]] = None, - size: Tuple[float, float] = (18.5, 10.5), + properties_to_plot: Optional[list[str]] = None, + size: tuple[float, float] = (18.5, 10.5), start_indices: Optional[Union[int, Iterable[int]]] = None, - colors: Optional[Union[List[float], List[List[float]]]] = None, - legends: Optional[Union[str, List[str]]] = None, + colors: Optional[Union[list[float], list[list[float]]]] = None, + legends: Optional[Union[str, list[str]]] = None, plot_type: str = "line", -) -> Dict[str, plt.Subplot]: +) -> dict[str, plt.Subplot]: """ One plot per optimization property in properties_to_plot. @@ -213,10 +214,10 @@ def optimization_run_property_per_multistart( results: Union[Result, Sequence[Result]], opt_run_property: str, axes: Optional[matplotlib.axes.Axes] = None, - size: Tuple[float, float] = (18.5, 10.5), + size: tuple[float, float] = (18.5, 10.5), start_indices: Optional[Union[int, Iterable[int]]] = None, - colors: Optional[Union[List[float], List[List[float]]]] = None, - legends: Optional[Union[str, List[str]]] = None, + colors: Optional[Union[list[float], list[list[float]]]] = None, + legends: Optional[Union[str, list[str]]] = None, plot_type: str = "line", ) -> matplotlib.axes.Axes: """ @@ -323,7 +324,7 @@ def optimization_run_property_per_multistart( plot_type, ) - if sum((legend is not None for legend in legends)) > 0: + if sum(legend is not None for legend in legends) > 0: if plot_type == "both": for ax in axes: ax.legend() @@ -339,7 +340,7 @@ def stats_lowlevel( axis_label: str, ax: matplotlib.axes.Axes, start_indices: Optional[Union[int, Iterable[int]]] = None, - color: Union[str, List[float], List[List[float]]] = "C0", + color: Union[str, list[float], list[list[float]]] = "C0", legend: Optional[str] = None, plot_type: str = "line", ): diff --git a/pypesto/visualize/optimizer_convergence.py b/pypesto/visualize/optimizer_convergence.py index e892fd85f..fe7c4f635 100644 --- a/pypesto/visualize/optimizer_convergence.py +++ b/pypesto/visualize/optimizer_convergence.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Optional import matplotlib.pyplot as plt import numpy as np @@ -12,7 +12,7 @@ def optimizer_convergence( ax: Optional[plt.Axes] = None, xscale: str = "symlog", yscale: str = "log", - size: Tuple[float] = (18.5, 10.5), + size: tuple[float] = (18.5, 10.5), ) -> plt.Axes: """ Visualize to help spotting convergence issues. diff --git a/pypesto/visualize/optimizer_history.py b/pypesto/visualize/optimizer_history.py index 5e360ca74..474c030ff 100644 --- a/pypesto/visualize/optimizer_history.py +++ b/pypesto/visualize/optimizer_history.py @@ -1,5 +1,6 @@ import logging -from typing import Iterable, List, Optional, Tuple, Union +from collections.abc import Iterable +from typing import Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -22,20 +23,20 @@ def optimizer_history( - results: Union[Result, List[Result]], + results: Union[Result, list[Result]], ax: Optional[plt.Axes] = None, - size: Tuple = (18.5, 10.5), + size: tuple = (18.5, 10.5), trace_x: str = TRACE_X_STEPS, trace_y: str = TRACE_Y_FVAL, scale_y: str = "log10", offset_y: Optional[float] = None, - colors: Optional[Union[RGBA, List[RGBA]]] = None, - y_limits: Optional[Union[float, List[float], np.ndarray]] = None, - start_indices: Optional[Union[int, List[int]]] = None, + colors: Optional[Union[RGBA, list[RGBA]]] = None, + y_limits: Optional[Union[float, list[float], np.ndarray]] = None, + start_indices: Optional[Union[int, list[int]]] = None, reference: Optional[ - Union[ReferencePoint, dict, List[ReferencePoint], List[dict]] + Union[ReferencePoint, dict, list[ReferencePoint], list[dict]] ] = None, - legends: Optional[Union[str, List[str]]] = None, + legends: Optional[Union[str, list[str]]] = None, ) -> plt.Axes: """ Plot history of optimizer. @@ -124,11 +125,11 @@ def optimizer_history( def optimizer_history_lowlevel( - vals: List[np.ndarray], + vals: list[np.ndarray], scale_y: str = "log10", - colors: Optional[Union[RGBA, List[RGBA]]] = None, + colors: Optional[Union[RGBA, list[RGBA]]] = None, ax: Optional[plt.Axes] = None, - size: Tuple = (18.5, 10.5), + size: tuple = (18.5, 10.5), x_label: str = "Optimizer steps", y_label: str = "Objective value", legend_text: Optional[str] = None, @@ -226,7 +227,7 @@ def optimizer_history_lowlevel( def get_trace( result: Result, trace_x: Optional[str], trace_y: Optional[str] -) -> List[np.ndarray]: +) -> list[np.ndarray]: """ Get the values of the optimizer trace from the pypesto.Result object. @@ -253,7 +254,7 @@ def get_trace( label for y-axis to be plotted later. """ # get data frames - histories: List[HistoryBase] = result.optimize_result.history + histories: list[HistoryBase] = result.optimize_result.history vals = [] @@ -306,12 +307,12 @@ def get_trace( def get_vals( - vals: List[np.ndarray], + vals: list[np.ndarray], scale_y: Optional[str], offset_y: float, trace_y: str, start_indices: Iterable[int], -) -> Tuple[List[np.ndarray], float]: +) -> tuple[list[np.ndarray], float]: """ Postprocess the values of the optimization history. @@ -373,7 +374,7 @@ def get_vals( return vals, offset_y -def get_labels(trace_x: str, trace_y: str, offset_y: float) -> Tuple[str, str]: +def get_labels(trace_x: str, trace_y: str, offset_y: float) -> tuple[str, str]: """ Generate labels for x and y axes of the history plot. @@ -411,9 +412,9 @@ def get_labels(trace_x: str, trace_y: str, offset_y: float) -> Tuple[str, str]: def handle_options( ax: plt.Axes, - vals: List[np.ndarray], + vals: list[np.ndarray], trace_y: str, - ref: List[ReferencePoint], + ref: list[ReferencePoint], y_limits: Union[float, np.ndarray, None], offset_y: float, ) -> plt.Axes: diff --git a/pypesto/visualize/parameters.py b/pypesto/visualize/parameters.py index 6c28fa33f..a482a94ae 100644 --- a/pypesto/visualize/parameters.py +++ b/pypesto/visualize/parameters.py @@ -1,5 +1,6 @@ import logging -from typing import Callable, Iterable, List, Optional, Sequence, Tuple, Union +from collections.abc import Iterable, Sequence +from typing import Callable, Optional, Union import matplotlib.axes import matplotlib.pyplot as plt @@ -27,15 +28,15 @@ def parameters( results: Union[Result, Sequence[Result]], ax: Optional[matplotlib.axes.Axes] = None, parameter_indices: Union[str, Sequence[int]] = "free_only", - lb: Optional[Union[np.ndarray, List[float]]] = None, - ub: Optional[Union[np.ndarray, List[float]]] = None, - size: Optional[Tuple[float, float]] = None, - reference: Optional[List[ReferencePoint]] = None, - colors: Optional[Union[RGBA, List[RGBA]]] = None, - legends: Optional[Union[str, List[str]]] = None, + lb: Optional[Union[np.ndarray, list[float]]] = None, + ub: Optional[Union[np.ndarray, list[float]]] = None, + size: Optional[tuple[float, float]] = None, + reference: Optional[list[ReferencePoint]] = None, + colors: Optional[Union[RGBA, list[RGBA]]] = None, + legends: Optional[Union[str, list[str]]] = None, balance_alpha: bool = True, start_indices: Optional[Union[int, Iterable[int]]] = None, - scale_to_interval: Optional[Tuple[float, float]] = None, + scale_to_interval: Optional[tuple[float, float]] = None, plot_inner_parameters: bool = True, ) -> matplotlib.axes.Axes: """ @@ -178,9 +179,9 @@ def parameter_hist( parameter_name: str, bins: Union[int, str] = "auto", ax: Optional["matplotlib.Axes"] = None, - size: Optional[Tuple[float]] = (18.5, 10.5), - color: Optional[List[float]] = None, - start_indices: Optional[Union[int, List[int]]] = None, + size: Optional[tuple[float]] = (18.5, 10.5), + color: Optional[list[float]] = None, + start_indices: Optional[Union[int, list[int]]] = None, ): """ Plot parameter values as a histogram. @@ -236,12 +237,12 @@ def parameter_hist( def parameters_lowlevel( xs: np.ndarray, fvals: np.ndarray, - lb: Optional[Union[np.ndarray, List[float]]] = None, - ub: Optional[Union[np.ndarray, List[float]]] = None, + lb: Optional[Union[np.ndarray, list[float]]] = None, + ub: Optional[Union[np.ndarray, list[float]]] = None, x_labels: Optional[Iterable[str]] = None, ax: Optional[matplotlib.axes.Axes] = None, - size: Optional[Tuple[float, float]] = None, - colors: Optional[Sequence[Union[np.ndarray, List[float]]]] = None, + size: Optional[tuple[float, float]] = None, + colors: Optional[Sequence[Union[np.ndarray, list[float]]]] = None, linestyle: str = "-", legend_text: Optional[str] = None, balance_alpha: bool = True, @@ -337,12 +338,12 @@ def parameters_lowlevel( def handle_inputs( result: Result, - parameter_indices: List[int], - lb: Optional[Union[np.ndarray, List[float]]] = None, - ub: Optional[Union[np.ndarray, List[float]]] = None, + parameter_indices: list[int], + lb: Optional[Union[np.ndarray, list[float]]] = None, + ub: Optional[Union[np.ndarray, list[float]]] = None, start_indices: Optional[Union[int, Iterable[int]]] = None, plot_inner_parameters: bool = False, -) -> Tuple[np.ndarray, np.ndarray, List[str], np.ndarray, List[np.ndarray]]: +) -> tuple[np.ndarray, np.ndarray, list[str], np.ndarray, list[np.ndarray]]: """ Compute the correct bounds for the parameter indices to be plotted. @@ -434,8 +435,8 @@ def handle_inputs( def _handle_inner_inputs( result: Result, ) -> Union[ - Tuple[None, None, None, None], - Tuple[list[np.ndarray], list[str], np.ndarray, np.ndarray], + tuple[None, None, None, None], + tuple[list[np.ndarray], list[str], np.ndarray, np.ndarray], ]: """Handle inner parameters from hierarchical optimization, if available. @@ -562,7 +563,7 @@ def optimization_scatter( start_indices: Optional[Union[int, Iterable[int]]] = None, diag_kind: str = "kde", suptitle: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, show_bounds: bool = False, ): """ diff --git a/pypesto/visualize/profile_cis.py b/pypesto/visualize/profile_cis.py index 9f159b814..bbe1dbfa5 100644 --- a/pypesto/visualize/profile_cis.py +++ b/pypesto/visualize/profile_cis.py @@ -1,4 +1,5 @@ -from typing import Sequence, Union +from collections.abc import Sequence +from typing import Union import matplotlib.axes import matplotlib.pyplot as plt diff --git a/pypesto/visualize/profiles.py b/pypesto/visualize/profiles.py index bac901bb1..f4ecb6443 100644 --- a/pypesto/visualize/profiles.py +++ b/pypesto/visualize/profiles.py @@ -1,4 +1,5 @@ -from typing import Optional, Sequence, Union +from collections.abc import Sequence +from typing import Optional, Union from warnings import warn import matplotlib.pyplot as plt @@ -180,7 +181,7 @@ def profiles_lowlevel( fvals = [fvals] # number of non-trivial profiles - n_profiles = sum((fval is not None for fval in fvals)) + n_profiles = sum(fval is not None for fval in fvals) # if axes already exists, we have to match profiles to axes if not create_new_ax: diff --git a/pypesto/visualize/reference_points.py b/pypesto/visualize/reference_points.py index f471704e2..9edf1afb0 100644 --- a/pypesto/visualize/reference_points.py +++ b/pypesto/visualize/reference_points.py @@ -1,4 +1,5 @@ -from typing import Optional, Sequence, Union +from collections.abc import Sequence +from typing import Optional, Union import numpy as np diff --git a/pypesto/visualize/sampling.py b/pypesto/visualize/sampling.py index ddd8b9209..46c6b4d34 100644 --- a/pypesto/visualize/sampling.py +++ b/pypesto/visualize/sampling.py @@ -1,7 +1,8 @@ import logging import warnings +from collections.abc import Sequence from colorsys import rgb_to_hls -from typing import Dict, Optional, Sequence, Tuple, Union +from typing import Optional, Union import matplotlib.axes import matplotlib.pyplot as plt @@ -44,7 +45,7 @@ def sampling_fval_traces( full_trace: bool = False, stepsize: int = 1, title: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, ax: matplotlib.axes.Axes = None, ): """ @@ -119,7 +120,7 @@ def sampling_fval_traces( return ax -def _get_level_percentiles(level: float) -> Tuple[float, float]: +def _get_level_percentiles(level: float) -> tuple[float, float]: """Convert a credibility level to percentiles. Similar to the highest-density region of a symmetric, unimodal distribution @@ -147,11 +148,11 @@ def _get_level_percentiles(level: float) -> Tuple[float, float]: def _get_statistic_data( - summary: Dict[str, PredictionResult], + summary: dict[str, PredictionResult], statistic: str, condition_id: str, output_id: str, -) -> Tuple[Sequence[float], Sequence[float]]: +) -> tuple[Sequence[float], Sequence[float]]: """Get statistic-, condition-, and output-specific data. Parameters @@ -182,18 +183,18 @@ def _get_statistic_data( def _plot_trajectories_by_condition( - summary: Dict[str, PredictionResult], + summary: dict[str, PredictionResult], condition_ids: Sequence[str], output_ids: Sequence[str], axes: matplotlib.axes.Axes, levels: Sequence[float], - level_opacities: Dict[int, float], - labels: Dict[str, str], + level_opacities: dict[int, float], + labels: dict[str, str], variable_colors: Sequence[RGB], average: str = MEDIAN, add_sd: bool = False, - grouped_measurements: Dict[ - Tuple[str, str], Sequence[Sequence[float]] + grouped_measurements: dict[ + tuple[str, str], Sequence[Sequence[float]] ] = None, ) -> None: """Plot predicted trajectories, with subplots grouped by condition. @@ -276,10 +277,10 @@ def _plot_trajectories_by_condition( for level_index, level in enumerate(levels): # Get the percentiles that correspond to the credibility level, # as their labels in the `summary`. - lower_label, upper_label = [ + lower_label, upper_label = ( get_percentile_label(percentile) for percentile in _get_level_percentiles(level) - ] + ) # Get the data for each percentile. t_lower, lower_data = _get_statistic_data( summary, @@ -329,18 +330,18 @@ def _plot_trajectories_by_condition( def _plot_trajectories_by_output( - summary: Dict[str, PredictionResult], + summary: dict[str, PredictionResult], condition_ids: Sequence[str], output_ids: Sequence[str], axes: matplotlib.axes.Axes, levels: Sequence[float], - level_opacities: Dict[int, float], - labels: Dict[str, str], + level_opacities: dict[int, float], + labels: dict[str, str], variable_colors: Sequence[RGB], average: str = MEDIAN, add_sd: bool = False, - grouped_measurements: Dict[ - Tuple[str, str], Sequence[Sequence[float]] + grouped_measurements: dict[ + tuple[str, str], Sequence[Sequence[float]] ] = None, ) -> None: """Plot predicted trajectories, with subplots grouped by output. @@ -408,10 +409,10 @@ def _plot_trajectories_by_output( for level_index, level in enumerate(levels): # Get the percentiles that correspond to the credibility level, # as their labels in the `summary`. - lower_label, upper_label = [ + lower_label, upper_label = ( get_percentile_label(percentile) for percentile in _get_level_percentiles(level) - ] + ) # Get the data for each percentile. t_lower, lower_data = _get_statistic_data( summary, @@ -468,8 +469,8 @@ def _plot_trajectories_by_output( def _get_condition_and_output_ids( - summary: Dict[str, PredictionResult], -) -> Tuple[Sequence[str], Sequence[str]]: + summary: dict[str, PredictionResult], +) -> tuple[Sequence[str], Sequence[str]]: """Get all condition and output IDs in a prediction summary. Parameters @@ -515,7 +516,7 @@ def _handle_legends( fig: matplotlib.figure.Figure, axes: matplotlib.axes.Axes, levels: Union[float, Sequence[float]], - labels: Dict[str, str], + labels: dict[str, str], level_opacities: Sequence[float], variable_names: Sequence[str], variable_colors: Sequence[RGB], @@ -525,7 +526,7 @@ def _handle_legends( average: str, add_sd: bool, grouped_measurements: Optional[ - Dict[Tuple[str, str], Sequence[Sequence[float]]] + dict[tuple[str, str], Sequence[Sequence[float]]] ], ) -> None: """Add legends to a sampling prediction trajectories plot. @@ -682,7 +683,7 @@ def _handle_colors( levels: Union[float, Sequence[float]], n_variables: int, reverse: bool = False, -) -> Tuple[Sequence[float], Sequence[RGB]]: +) -> tuple[Sequence[float], Sequence[RGB]]: """Calculate the colors for the prediction trajectories plot. Parameters @@ -719,9 +720,9 @@ def sampling_prediction_trajectories( ensemble_prediction: EnsemblePrediction, levels: Union[float, Sequence[float]], title: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, axes: matplotlib.axes.Axes = None, - labels: Dict[str, str] = None, + labels: dict[str, str] = None, axis_label_padding: int = 50, groupby: str = CONDITION, condition_gap: float = 0.01, @@ -967,7 +968,7 @@ def sampling_parameter_cis( step: float = 0.05, show_median: bool = True, title: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, ax: matplotlib.axes.Axes = None, ) -> matplotlib.axes.Axes: """ @@ -1076,7 +1077,7 @@ def sampling_parameter_traces( stepsize: int = 1, use_problem_bounds: bool = True, suptitle: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, ax: matplotlib.axes.Axes = None, ): """ @@ -1188,7 +1189,7 @@ def sampling_scatter( stepsize: int = 1, suptitle: str = None, diag_kind: str = "kde", - size: Tuple[float, float] = None, + size: tuple[float, float] = None, show_bounds: bool = True, ): """ @@ -1255,7 +1256,7 @@ def sampling_1d_marginals( plot_type: str = "both", bw_method: str = "scott", suptitle: str = None, - size: Tuple[float, float] = None, + size: tuple[float, float] = None, ): """ Plot marginals. @@ -1418,7 +1419,7 @@ def get_data_to_plot( pd_iter = pd.DataFrame(data=indices, columns=["iteration"]) if full_trace: - converged = np.zeros((len(arr_fval))) + converged = np.zeros(len(arr_fval)) converged[_burn_in:] = 1 pd_conv = pd.DataFrame(data=converged, columns=["converged"]) diff --git a/pypesto/visualize/select.py b/pypesto/visualize/select.py index 84d1ed0d9..0a8efde34 100644 --- a/pypesto/visualize/select.py +++ b/pypesto/visualize/select.py @@ -1,6 +1,5 @@ """Visualization routines for model selection with pyPESTO.""" -from typing import Dict, List, Tuple import matplotlib.pyplot as plt import networkx as nx @@ -20,12 +19,12 @@ def default_label_maker(model: Model) -> str: # FIXME supply the problem to automatically detect the correct criterion? def plot_selected_models( - selected_models: List[Model], + selected_models: list[Model], criterion: str = Criterion.AIC, relative: bool = True, fz: int = 14, - size: Tuple[float, float] = (5, 4), - labels: Dict[str, str] = None, + size: tuple[float, float] = (5, 4), + labels: dict[str, str] = None, ax: plt.Axes = None, ) -> plt.Axes: """Plot criterion for calibrated models. @@ -116,12 +115,12 @@ def plot_history_digraph(*args, **kwargs): def plot_calibrated_models_digraph( problem: pypesto_select.Problem, - calibrated_models: Dict[str, Model] = None, + calibrated_models: dict[str, Model] = None, criterion: Criterion = None, optimal_distance: float = 1, relative: bool = True, - options: Dict = None, - labels: Dict[str, str] = None, + options: dict = None, + labels: dict[str, str] = None, ax: plt.Axes = None, ) -> plt.Axes: """Plot all calibrated models in the model space, as a directed graph. diff --git a/pypesto/visualize/spline_approximation.py b/pypesto/visualize/spline_approximation.py index 51ca0e434..5f617db30 100644 --- a/pypesto/visualize/spline_approximation.py +++ b/pypesto/visualize/spline_approximation.py @@ -1,5 +1,6 @@ import warnings -from typing import Optional, Sequence, Union +from collections.abc import Sequence +from typing import Optional, Union import matplotlib.axes import matplotlib.pyplot as plt diff --git a/pypesto/visualize/waterfall.py b/pypesto/visualize/waterfall.py index ffdde2c4a..983b44ea1 100644 --- a/pypesto/visualize/waterfall.py +++ b/pypesto/visualize/waterfall.py @@ -1,4 +1,5 @@ -from typing import List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -23,7 +24,7 @@ def waterfall( results: Union[Result, Sequence[Result]], ax: Optional[plt.Axes] = None, size: Optional[tuple[float, float]] = (18.5, 10.5), - y_limits: Optional[Tuple[float]] = None, + y_limits: Optional[tuple[float]] = None, scale_y: Optional[str] = "log10", offset_y: Optional[float] = None, start_indices: Optional[Union[Sequence[int], int]] = None, @@ -187,7 +188,7 @@ def waterfall( def waterfall_lowlevel( fvals, ax: Optional[plt.Axes] = None, - size: Optional[Tuple[float]] = (18.5, 10.5), + size: Optional[tuple[float]] = (18.5, 10.5), scale_y: str = "log10", offset_y: float = 0.0, colors: Optional[Union[RGBA, Sequence[RGBA]]] = None, @@ -296,7 +297,7 @@ def process_offset_for_list( scale_y: Optional[str], start_indices: Optional[Sequence[int]] = None, references: Optional[Sequence[ReferencePoint]] = None, -) -> Tuple[List[np.ndarray], float]: +) -> tuple[list[np.ndarray], float]: """ Compute common offset_y and add it to `fvals` of results. @@ -345,7 +346,7 @@ def process_offset_for_list( return [fvals + offset_y for fvals in fvals_all], offset_y -def get_ordering_by_start_id(results: Sequence[Result]) -> List[int]: +def get_ordering_by_start_id(results: Sequence[Result]) -> list[int]: """Get an ordering of start IDs. The ordering is generated by taking the best function value for each diff --git a/pyproject.toml b/pyproject.toml index 858e52ba8..20e202cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ lint.select = [ "T20", # flake8-print "W", # pycodestyle Warnings "E", # pycodestyle Errors + "UP", # pyupgrade # "ANN", # flakes-annotations TODO: currently produces ~1500 errors to manual fix ] [tool.ruff.lint.pydocstyle] diff --git a/test/base/test_history.py b/test/base/test_history.py index 0bd94de80..12891c4aa 100644 --- a/test/base/test_history.py +++ b/test/base/test_history.py @@ -3,8 +3,8 @@ import os import tempfile import unittest +from collections.abc import Sequence from stat import S_IMODE, S_IWGRP, S_IWOTH, S_IWRITE -from typing import Sequence import numpy as np import pytest diff --git a/test/select/test_select.py b/test/select/test_select.py index 38c2359ad..2ff86def3 100644 --- a/test/select/test_select.py +++ b/test/select/test_select.py @@ -1,6 +1,5 @@ from functools import partial from pathlib import Path -from typing import List import numpy as np import pandas as pd @@ -77,7 +76,7 @@ def pypesto_select_problem(petab_select_problem) -> pypesto.select.Problem: @pytest.fixture -def initial_models(petab_problem_yaml) -> List[Model]: +def initial_models(petab_problem_yaml) -> list[Model]: """Models that can be used to initialize a search.""" initial_model_1 = Model( model_id="myModel1", diff --git a/test/visualize/test_visualize.py b/test/visualize/test_visualize.py index 93c0031f1..79aee2f30 100644 --- a/test/visualize/test_visualize.py +++ b/test/visualize/test_visualize.py @@ -1,8 +1,8 @@ import functools import logging import os +from collections.abc import Sequence from functools import wraps -from typing import Sequence import matplotlib.pyplot as plt import numpy as np From b791e7f75a3f29d843a87cfe0bd40c468da1945a Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Fri, 5 Apr 2024 09:46:07 +0200 Subject: [PATCH 27/35] Update .git-blame-ignore-revs --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4ad8396d2..61ffc23d9 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,4 +1,6 @@ # .git-blame-ignore-revs +# https://github.com/ICB-DCM/pyPESTO/pull/1352 +321ee9dbd0223da0c08172fc08d61796e0d176aa # Adjusted to ruff code-checks. Timestamp: 05.03.2024 82b7d5da1273b9d442a634e5aabbda9685279d77 # Adjusted to black formatting. Timestamp: 11.01.2022 From 38d91c47ee386712da523fe89a6585c11b65a071 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:37:22 +0200 Subject: [PATCH 28/35] Added deepcopy test for RoadRunner objective (#1347) * Added deepcpy functionality and small test. * Better test and better deepcopy * Update pypesto/objective/roadrunner/road_runner.py Co-authored-by: Daniel Weindl * Removed deepcopy from base function as it should be similar to defaultbehaviour. * Also removed deepcopy functionality. * Update test/base/test_roadrunner.py Co-authored-by: Daniel Weindl --------- Co-authored-by: Daniel Weindl --- pypesto/objective/base.py | 7 ------- test/base/test_roadrunner.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pypesto/objective/base.py b/pypesto/objective/base.py index 0f9cc982a..e5212b31c 100644 --- a/pypesto/objective/base.py +++ b/pypesto/objective/base.py @@ -56,13 +56,6 @@ def __init__( self.pre_post_processor = PrePostProcessor() self.history = NoHistory() - def __deepcopy__(self, memodict=None) -> "ObjectiveBase": - """Create deepcopy of objective object.""" - other = type(self)() # maintain type for derived classes - for attr, val in self.__dict__.items(): - other.__dict__[attr] = copy.deepcopy(val) - return other - # The following has_ properties can be used to find out what values # the objective supports. @property diff --git a/test/base/test_roadrunner.py b/test/base/test_roadrunner.py index 43fd3cd16..bbff88ff7 100644 --- a/test/base/test_roadrunner.py +++ b/test/base/test_roadrunner.py @@ -1,7 +1,9 @@ """Test the roadrunner interface.""" - +import copy import logging +import os +import benchmark_models_petab as models import petab import petabtests import pytest @@ -104,3 +106,33 @@ def _execute_case_rr(case, model_type, version): ) logger.info(f"Case {version}/{model_type}/{case} passed.") + + +def test_deepcopy(): + """Test that deepcopy works as intended""" + model_name = "Boehm_JProteomeRes2014" + petab_problem = petab.Problem.from_yaml( + os.path.join(models.MODELS_DIR, model_name, model_name + ".yaml") + ) + petab_problem.model_name = model_name + importer = objective_rr.PetabImporterRR(petab_problem) + problem_parameters = petab_problem.x_nominal_free_scaled + + problem = importer.create_problem() + obj = problem.objective + + problem_copied = copy.deepcopy(problem) + copied_objective = problem_copied.objective + + assert obj(problem_parameters) == copied_objective(problem_parameters) + + # !!not adviced, only done here for testing purposes!! + obj.roadrunner_instance.removeParameter( + "pSTAT5A_rel", forceRegenerate=False + ) + obj.roadrunner_instance.addParameter("pSTAT5A_rel", 0.0, False) + obj.roadrunner_instance.addAssignmentRule( + "pSTAT5A_rel", "(100 * pApB + 200 * pApA * specC17)" + ) + + assert obj(problem_parameters) != copied_objective(problem_parameters) From 3163687a47ca3e7567f62a143b6a6cef4dad06bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sat, 6 Apr 2024 16:45:47 +0100 Subject: [PATCH 29/35] fix jax deprecations (#1346) * fix #1337 * fix pure callback calls * run test in 32bit and 64bit mode * add vmap compatibility, remove second order support * Update test_objective.py * Apply suggestions from code review Co-authored-by: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> * alter implementation, fix doc and extend tests * Update test_objective.py * expose inner x_names rather than allowing them to be set * Update base.py * Update test_objective.py --------- Co-authored-by: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Co-authored-by: Daniel Weindl --- pypesto/objective/jax/base.py | 260 ++++++++++++++-------------------- test/base/test_objective.py | 83 ++++++++--- 2 files changed, 164 insertions(+), 179 deletions(-) diff --git a/pypesto/objective/jax/base.py b/pypesto/objective/jax/base.py index f5775275a..49327bb37 100644 --- a/pypesto/objective/jax/base.py +++ b/pypesto/objective/jax/base.py @@ -7,20 +7,18 @@ """ import copy -from collections.abc import Sequence from functools import partial -from typing import Callable +from typing import Union import numpy as np -from ...C import FVAL, GRAD, HESS, MODE_FUN, RDATAS, ModeType +from ...C import MODE_FUN, ModeType from ..base import ObjectiveBase, ResultDict try: import jax - import jax.experimental.host_callback as hcb import jax.numpy as jnp - from jax import custom_jvp, grad + from jax import custom_jvp except ImportError: raise ImportError( "Using a jax objective requires an installation of " @@ -28,17 +26,13 @@ "`pip install jax jaxlib`." ) from None -# jax compatible (jittable) objective function using host callback, see -# https://jax.readthedocs.io/en/latest/jax.experimental.host_callback.html +# jax compatible (jit-able) objective function using external callback, see +# https://jax.readthedocs.io/en/latest/notebooks/external_callbacks.html @partial(custom_jvp, nondiff_argnums=(0,)) -def _device_fun(obj: "JaxObjective", x: jnp.array): - """Jax compatible objective function execution using host callback. - - This function does not actually call the underlying objective function, - but instead extracts cached return values. Thus it must only be called - from within obj.call_unprocessed, and obj.cached_base_ret must be populated. +def _device_fun(base_objective: ObjectiveBase, x: jnp.array): + """Jax compatible objective function execution using external callback. Parameters ---------- @@ -46,27 +40,22 @@ def _device_fun(obj: "JaxObjective", x: jnp.array): The wrapped jax objective. x: jax computed input array. - - Note - ---- - This function should rather be implemented as class method of JaxObjective, - but this is not possible at the time of writing as this is not supported - by signature inspection in the underlying bind call. """ - return hcb.call( - obj.cached_fval, + return jax.pure_callback( + partial(base_objective, sensi_orders=(0,)), + jax.ShapeDtypeStruct((), x.dtype), x, - result_shape=jax.ShapeDtypeStruct((), np.float64), ) -@partial(custom_jvp, nondiff_argnums=(0,)) -def _device_fun_grad(obj: "JaxObjective", x: jnp.array): - """Jax compatible objective gradient execution using host callback. +def _device_fun_value_and_grad(base_objective: ObjectiveBase, x: jnp.array): + """Jax compatible objective gradient execution using external callback. - This function does not actually call the underlying objective function, - but instead extracts cached return values. Thus it must only be called - from within obj.call_unprocessed and obj.cached_base_ret must be populated. + This function will be called when computing the gradient of the + `JaxObjective` using `jax.grad` or `jax.value_and_grad`. In the latter + case, the function will return both the function value and the gradient, + so no caching is necessary. For higher order derivatives, caching would + be advantageous, but unclear how to implement this. Parameters ---------- @@ -74,54 +63,27 @@ def _device_fun_grad(obj: "JaxObjective", x: jnp.array): The wrapped jax objective. x: jax computed input array. - - Note - ---- - This function should rather be implemented as class method of JaxObjective, - but this is not possible at the time of writing as this is not supported - by signature inspection in the underlying bind call. """ - return hcb.call( - obj.cached_grad, - x, - result_shape=jax.ShapeDtypeStruct( - obj.cached_base_ret[GRAD].shape, # bootstrap from cached value - np.float64, + return jax.pure_callback( + partial( + base_objective, + sensi_orders=( + 0, + 1, + ), ), - ) - - -def _device_fun_hess(obj: "JaxObjective", x: jnp.array): - """Jax compatible objective Hessian execution using host callback. - - This function does not actually call the underlying objective function, - but instead extracts cached return values. Thus it must only be called - from within obj.call_unprocessed and obj.cached_base_ret must be populated. - - Parameters - ---------- - obj: - The wrapped jax objective. - x: - jax computed input array. - - Note - ---- - This function should rather be implemented as class method of JaxObjective, - but this is not possible at the time of writing as this is not supported - by signature inspection in the underlying bind call. - """ - return hcb.call( - obj.cached_hess, - x, - result_shape=jax.ShapeDtypeStruct( - obj.cached_base_ret[HESS].shape, # bootstrap from cached value - np.float64, + ( + jax.ShapeDtypeStruct((), x.dtype), + jax.ShapeDtypeStruct( + x.shape, # bootstrap from cached value + x.dtype, + ), ), + x, ) -# define custom jvp for device_fun & device_fun_grad to enable autodiff, see +# define custom jvp for device_fun to enable autodiff, see # https://jax.readthedocs.io/en/latest/notebooks/Custom_derivative_rules_for_Python_code.html @@ -132,37 +94,31 @@ def _device_fun_jvp( """JVP implementation for device_fun.""" (x,) = primals (x_dot,) = tangents - return _device_fun(obj, x), _device_fun_grad(obj, x).dot(x_dot) - - -@_device_fun_grad.defjvp -def _device_fun_grad_jvp( - obj: "JaxObjective", primals: jnp.array, tangents: jnp.array -): - """JVP implementation for device_fun_grad.""" - (x,) = primals - (x_dot,) = tangents - return _device_fun_grad(obj, x), _device_fun_hess(obj, x).dot(x_dot) + value, grad = _device_fun_value_and_grad(obj, x) + return value, grad @ x_dot class JaxObjective(ObjectiveBase): - """Objective function that combines pypesto objectives with jax functions. + """Objective function that enables use of pypesto objectives in jax models. - The generated objective function will evaluate objective(jax_fun(x)). + The generated function should generally be compatible with jax, but cannot + compute higher order derivatives and is not vectorized (but still + compatible with jax.vmap) Parameters ---------- objective: - pyPESTO objective - jax_fun: - jax function (not jitted) that computes input to the pyPESTO objective + pyPESTO objective to be wrapped. + + Note + ---- + Currently only implements MODE_FUN and sensi_orders=(0,). Support for + MODE_RES should be straightforward to add. """ def __init__( self, objective: ObjectiveBase, - jax_fun: Callable, - x_names: Sequence[str] = None, ): if not isinstance(objective, ObjectiveBase): raise TypeError("objective must be an ObjectiveBase instance") @@ -170,41 +126,11 @@ def __init__( raise NotImplementedError( f"objective must support mode={MODE_FUN}" ) - super().__init__(x_names) self.base_objective = objective - self.jax_fun = jax_fun - # would be cleaner to also have this as class method, but not supported # by signature inspection in bind call. - def jax_objective(x): - # device fun doesn't actually need the value of y, but we need to - # compute this here for autodiff to work - y = jax_fun(x) - return _device_fun(self, y) - - # jit objective & derivatives (not integrated) - self.jax_objective = jax.jit(jax_objective) - self.jax_objective_grad = jax.jit(grad(jax_objective)) - self.jax_objective_hess = jax.jit(jax.hessian(jax_objective)) - - # jit input function - self.infun = jax.jit(self.jax_fun) - - # temporary storage for evaluation results of objective - self.cached_base_ret: ResultDict = {} - - def cached_fval(self, _): - """Return cached function value.""" - return self.cached_base_ret[FVAL] - - def cached_grad(self, _): - """Return cached gradient.""" - return self.cached_base_ret[GRAD] - - def cached_hess(self, _): - """Return cached Hessian.""" - return self.cached_base_ret[HESS] + self.jax_objective = partial(_device_fun, self.base_objective) def check_mode(self, mode: ModeType) -> bool: """See `ObjectiveBase` documentation.""" @@ -215,7 +141,44 @@ def check_sensi_orders(self, sensi_orders, mode: ModeType) -> bool: if not self.check_mode(mode): return False else: - return self.base_objective.check_sensi_orders(sensi_orders, mode) + return ( + self.base_objective.check_sensi_orders(sensi_orders, mode) + and max(sensi_orders) == 0 + ) + + def __call__( + self, + x: jnp.ndarray, + sensi_orders: tuple[int, ...] = (0,), + mode: ModeType = MODE_FUN, + return_dict: bool = False, + **kwargs, + ) -> Union[jnp.ndarray, tuple, ResultDict]: + """ + See :class:`ObjectiveBase` for more documentation. + + Note that this function delegates pre- and post-processing as well as + history handling to the inner objective. + """ + + if not self.check_mode(mode): + raise ValueError( + f"This Objective cannot be called with mode" f"={mode}." + ) + if not self.check_sensi_orders(sensi_orders, mode): + raise ValueError( + f"This Objective cannot be called with " + f"sensi_orders= {sensi_orders} and mode={mode}." + ) + + # this computes all the results from the inner objective, rendering + # them accessible as cached values for device_fun, etc. + if kwargs.pop("return_dict", False): + raise ValueError( + "return_dict=True is not available for JaxObjective evaluation" + ) + + return self.jax_objective(x) def call_unprocessed( self, @@ -225,48 +188,31 @@ def call_unprocessed( **kwargs, ) -> ResultDict: """ - See `ObjectiveBase` for more documentation. + See :class:`ObjectiveBase` for more documentation. - Main method to overwrite from the base class. It handles and - delegates the actual objective evaluation. + This function is not implemented for JaxObjective as it is not called + in the override for __call__. However, it's marked as abstract so we + need to implement it. """ - # derivative computation in jax always requires lower order - # derivatives, see jvp rules for device_fun and device_fun_grad. - if 2 in sensi_orders: - sensi_orders = (0, 1, 2) - elif 1 in sensi_orders: - sensi_orders = (0, 1) - else: - sensi_orders = (0,) - - # this computes all the results from the inner objective, rendering - # them accessible as cached values for device_fun, etc. - set_return_dict, return_dict = ( - "return_dict" in kwargs, - kwargs.pop("return_dict", False), - ) - self.cached_base_ret = self.base_objective( - self.infun(x), sensi_orders, mode, return_dict=True, **kwargs - ) - if set_return_dict: - kwargs["return_dict"] = return_dict - ret = {} - if RDATAS in self.cached_base_ret: - ret[RDATAS] = self.cached_base_ret[RDATAS] - if 0 in sensi_orders: - ret[FVAL] = float(self.jax_objective(x)) - if 1 in sensi_orders: - ret[GRAD] = self.jax_objective_grad(x) - if 2 in sensi_orders: - ret[HESS] = self.jax_objective_hess(x) - - return ret + pass def __deepcopy__(self, memodict=None): other = JaxObjective( copy.deepcopy(self.base_objective), - copy.deepcopy(self.jax_fun), - copy.deepcopy(self.x_names), ) - return other + + @property + def history(self): + """Exposes the history of the inner objective.""" + return self.base_objective.history + + @property + def pre_post_processor(self): + """Exposes the pre_post_processor of inner objective.""" + return self.base_objective.pre_post_processor + + @property + def x_names(self): + """Exposes the x_names of inner objective.""" + return self.base_objective.x_names diff --git a/test/base/test_objective.py b/test/base/test_objective.py index 73615be32..d2c556927 100644 --- a/test/base/test_objective.py +++ b/test/base/test_objective.py @@ -2,6 +2,7 @@ import copy import numbers +from functools import partial import numpy as np import pytest @@ -214,42 +215,80 @@ def test_aesara(max_sensi_order, integrated): ) -def test_jax(max_sensi_order, integrated): +@pytest.mark.parametrize("enable_x64", [True, False]) +def test_jax(max_sensi_order, integrated, enable_x64): """Test function composition and gradient computation via jax""" import jax import jax.numpy as jnp + if max_sensi_order == 2: + pytest.skip("Not Implemented") + + jax.config.update("jax_enable_x64", enable_x64) + from pypesto.objective.jax import JaxObjective prob = rosen_for_sensi(max_sensi_order, integrated, [0, 1]) - # apply inverse transform such that we evaluate at prob['x'] - x_ref = np.arcsinh(prob["x"]) + x_ref = np.asarray(prob["x"]) - def jac_op(x: jnp.array) -> jnp.array: - return jax.lax.sinh(x) + def jax_op_in(x: jnp.array) -> jnp.array: + # pick a simple function here to avoid numerical issues + return 3.0 * x + + def jax_op_out(x: jnp.array) -> jnp.array: + # pick a simple function here to avoid numerical issues + return 0.5 * x # compose rosenbrock function with sinh transformation - obj = JaxObjective(prob["obj"], jac_op) + obj = JaxObjective(prob["obj"]) + + # evaluate for a couple of random points such that we can assess + # compatibility with vmap + xx = x_ref + np.random.randn(10, x_ref.shape[0]) + rvals_ref = [ + jax_op_out( + prob["obj"](jax_op_in(xxi), sensi_orders=(max_sensi_order,)) + ) + for xxi in xx + ] - # check function values and derivatives, also after copy - for _obj in (obj, copy.deepcopy(obj)): - # function value - assert _obj(x_ref) == prob["fval"] + def _fun(y, pypesto_fun, jax_fun_in, jax_fun_out): + return jax_fun_out(pypesto_fun(jax_fun_in(y))) - # gradient - if max_sensi_order > 0: - assert np.allclose( - _obj(x_ref, sensi_orders=(1,)), prob["grad"] * np.cosh(x_ref) - ) + for _obj in (obj, copy.deepcopy(obj)): + fun = partial( + _fun, + pypesto_fun=_obj, + jax_fun_in=jax_op_in, + jax_fun_out=jax_op_out, + ) - # hessian - if max_sensi_order > 1: - assert np.allclose( - prob["hess"] * (np.diag(np.power(np.cosh(x_ref), 2))) - + np.diag(prob["grad"] * np.sinh(x_ref)), - _obj(x_ref, sensi_orders=(2,)), - ) + if max_sensi_order == 1: + fun = jax.grad(fun) + # check compatibility with vmap and jit + vmapped_fun = jax.vmap(fun) + rvals_jax = vmapped_fun(xx) + atol = 0 + # also need to account for roundoff errors in input, so we + # can't use rtol = 1e-8 for 32bit + rtol = 1e-16 if enable_x64 else 1e-4 + for x, rref, rj in zip(xx, rvals_ref, rvals_jax): + if max_sensi_order == 0: + np.testing.assert_allclose(rref, rj, atol=atol, rtol=rtol) + if max_sensi_order == 1: + # g(x) = b(c(x)) => g'(x) = b'(c(x))) * c'(x) + # f(x) = a(g(x)) => f'(x) = a'(g(x)) * g'(x) + # c: jax_op_in, b: prob["obj"], a: jax_op_out + # g(x) = b(c(x)) + g = prob["obj"](jax_op_in(x)) + # g'(x) = b'(c(x))) * c'(x) + g_prime = prob["obj"]( + jax_op_in(x), sensi_orders=(1,) + ) @ jax.jacfwd(jax_op_in)(x) + # f'(x) = a'(g(x)) * g'(x) + f_prime = jax.jacfwd(jax_op_out)(g) * g_prime + np.testing.assert_allclose(f_prime, rj, atol=atol, rtol=rtol) @pytest.fixture( From 30f7cd443662e4846ea36be1cf8805a16dd809cb Mon Sep 17 00:00:00 2001 From: Jonas Arruda <69197639+arrjon@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:14:09 +0200 Subject: [PATCH 30/35] Fix Chain Burn-in Calculation in Thermodynamic Integration (#1361) * compute burn in for each chain * using all chains to compute evidence --- pypesto/sample/diagnostics.py | 18 ++++--- pypesto/sample/parallel_tempering.py | 71 ++++++++++++++++++++++++---- test/sample/test_sample.py | 7 ++- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/pypesto/sample/diagnostics.py b/pypesto/sample/diagnostics.py index ac593c7de..c4e1fe109 100644 --- a/pypesto/sample/diagnostics.py +++ b/pypesto/sample/diagnostics.py @@ -11,7 +11,9 @@ logger = logging.getLogger(__name__) -def geweke_test(result: Result, zscore: float = 2.0) -> int: +def geweke_test( + result: Result, zscore: float = 2.0, chain_number: int = 0 +) -> int: """ Calculate the burn-in of MCMC chains. @@ -21,6 +23,9 @@ def geweke_test(result: Result, zscore: float = 2.0) -> int: The pyPESTO result object with filled sample result. zscore: The Geweke test threshold. + chain_number: + The chain number to be used for the Geweke test (in a parallel tempering setting). + Usually we are only interested in the first chain. Returns ------- @@ -29,16 +34,17 @@ def geweke_test(result: Result, zscore: float = 2.0) -> int: do not differ significantly regarding Geweke test -> Burn-In """ # Get parameter samples as numpy arrays - chain = np.asarray(result.sample_result.trace_x[0]) + chain = np.asarray(result.sample_result.trace_x[chain_number]) # Calculate burn in index burn_in = burn_in_by_sequential_geweke(chain=chain, zscore=zscore) - # Log - logger.info(f"Geweke burn-in index: {burn_in}") + if chain_number == 0: + # Log + logger.info(f"Geweke burn-in index: {burn_in}") - # Fill in burn-in value into result - result.sample_result.burn_in = burn_in + # Fill in burn-in value into result + result.sample_result.burn_in = burn_in return burn_in diff --git a/pypesto/sample/parallel_tempering.py b/pypesto/sample/parallel_tempering.py index 8f2c88f9f..2d54857f6 100644 --- a/pypesto/sample/parallel_tempering.py +++ b/pypesto/sample/parallel_tempering.py @@ -9,6 +9,7 @@ from ..problem import Problem from ..result import McmcPtResult, Result from ..util import tqdm +from .diagnostics import geweke_test from .sampler import InternalSampler, Sampler logger = logging.getLogger(__name__) @@ -178,8 +179,11 @@ def adjust_betas(self, i_sample: int, swapped: Sequence[bool]): """Adjust temperature values. Default: Do nothing.""" def compute_log_evidence( - self, result: Result, method: str = "trapezoid" - ) -> float: + self, + result: Result, + method: str = "trapezoid", + use_all_chains: bool = True, + ) -> Union[float, None]: """Perform thermodynamic integration to estimate the log evidence. Parameters @@ -188,6 +192,10 @@ def compute_log_evidence( Result object containing the samples. method: Integration method, either 'trapezoid' or 'simpson' (uses scipy for integration). + use_all_chains: + If True, calculate burn-in for each chain and use the maximal burn-in for all chains for the integration. + This will fail if not all chains have converged yet. + Otherwise, use only the converged chains for the integration (might increase the integration error). """ from scipy.integrate import simpson, trapezoid @@ -197,19 +205,62 @@ def compute_log_evidence( f"Carefully check the results. Consider using beta_init='{BETA_DECAY}' for better results." ) - burn_in = result.sample_result.burn_in - trace_loglike = ( - result.sample_result.trace_neglogprior[::-1, burn_in:] - - result.sample_result.trace_neglogpost[::-1, burn_in:] - ) - mean_loglike_per_beta = np.mean(trace_loglike, axis=1) + # compute burn in for all chains but the last one (prior only) + burn_ins = np.zeros(len(self.betas), dtype=int) + for i_chain in range(len(self.betas)): + burn_ins[i_chain] = geweke_test(result, chain_number=i_chain) + max_burn_in = int(np.max(burn_ins)) + + if max_burn_in >= result.sample_result.trace_x.shape[1]: + logger.warning( + f"At least {np.sum(burn_ins >= result.sample_result.trace_x.shape[1])} chains seem to not have " + f"converged yet. You may want to use a larger number of samples." + ) + if use_all_chains: + raise ValueError( + "Not all chains have converged yet. You may want to use a larger number of samples, " + "or try ´use_all_chains=False´, which might increase the integration error." + ) + + if use_all_chains: + # estimate mean of log likelihood for each beta + trace_loglike = ( + result.sample_result.trace_neglogprior[::-1, max_burn_in:] + - result.sample_result.trace_neglogpost[::-1, max_burn_in:] + ) + mean_loglike_per_beta = np.mean(trace_loglike, axis=1) + temps = self.betas[::-1] + else: + # estimate mean of log likelihood for each beta if chain has converged + mean_loglike_per_beta = [] + temps = [] + for i_chain in reversed(range(len(self.betas))): + if burn_ins[i_chain] < result.sample_result.trace_x.shape[1]: + # save temperature-chain as it is converged + temps.append(self.betas[i_chain]) + # calculate mean log likelihood for each beta + trace_loglike_i = ( + result.sample_result.trace_neglogprior[ + i_chain, burn_ins[i_chain] : + ] + - result.sample_result.trace_neglogpost[ + i_chain, burn_ins[i_chain] : + ] + ) + mean_loglike_per_beta.append(np.mean(trace_loglike_i)) + if method == "trapezoid": log_evidence = trapezoid( - y=mean_loglike_per_beta, x=self.betas[::-1] + # integrate from low to high temperature + y=mean_loglike_per_beta, + x=temps, ) elif method == "simpson": log_evidence = simpson( - y=mean_loglike_per_beta, x=self.betas[::-1], even="last" + # integrate from low to high temperature + y=mean_loglike_per_beta, + x=temps, + even="last", ) else: raise ValueError( diff --git a/test/sample/test_sample.py b/test/sample/test_sample.py index b3ca4fd1e..a33adde18 100644 --- a/test/sample/test_sample.py +++ b/test/sample/test_sample.py @@ -787,6 +787,7 @@ def test_dynesty_posterior(): assert not (np.diff(mcmc_sample_result.trace_neglogpost) <= 0).all() +@pytest.mark.flaky(reruns=2) # sometimes not all chains converge def test_thermodynamic_integration(): # test thermodynamic integration problem = gaussian_problem() @@ -806,13 +807,16 @@ def test_thermodynamic_integration(): result = sample.sample( problem, - n_samples=5000, + n_samples=10000, result=result, sampler=sampler, ) # compute the log evidence using trapezoid and simpson rule log_evidence = sampler.compute_log_evidence(result, method="trapezoid") + log_evidence_not_all = sampler.compute_log_evidence( + result, method="trapezoid", use_all_chains=False + ) log_evidence_simps = sampler.compute_log_evidence( result, method="simpson" ) @@ -826,4 +830,5 @@ def test_thermodynamic_integration(): # compare to known value assert np.isclose(log_evidence, np.log(evidence[0]), atol=tol) + assert np.isclose(log_evidence_not_all, np.log(evidence[0]), atol=tol) assert np.isclose(log_evidence_simps, np.log(evidence[0]), atol=tol) From b68a3b570019bcdfaadc61a30a5384640712f1cf Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:19:25 +0200 Subject: [PATCH 31/35] Remove some warnings (#1345) * removed startpoint method from minimize as its depracated and per default in the problem. * Changed offset to one that should definitely catch the warning and test for warning. * When loading a problem, we expect a warning. * Trust-exact cannot handle bounds is an expected warning here. * If hess is not traced, no need to compute imo * Revert last changes, instead expect additional warning. * Better catching of warnings * Catch warnings? * Removed startpoint methods from functions, all were uniform. * added match as parameter to pytest.warns * Removed warning, is already tested elsewhere. --- pypesto/optimize/optimizer.py | 2 +- test/base/test_history.py | 26 +++++++++++++++++--------- test/base/test_store.py | 7 +++++-- test/optimize/test_optimize.py | 31 +++++++++++++------------------ test/profile/test_profile.py | 25 ++++++++++++++++--------- test/visualize/test_visualize.py | 5 ----- 6 files changed, 52 insertions(+), 44 deletions(-) diff --git a/pypesto/optimize/optimizer.py b/pypesto/optimize/optimizer.py index ddf6b2d2b..b7e7bc4f4 100644 --- a/pypesto/optimize/optimizer.py +++ b/pypesto/optimize/optimizer.py @@ -469,7 +469,7 @@ def minimize( "evaluated again. This can lead to increased " "computation times. If possible, separate fun " "and hess.", - stacklevel=1, + stacklevel=2, ) if objective.grad is True: diff --git a/test/base/test_history.py b/test/base/test_history.py index 12891c4aa..57d01a3f2 100644 --- a/test/base/test_history.py +++ b/test/base/test_history.py @@ -433,8 +433,8 @@ def test_trace_grad(self): trace_record_grad=True, trace_record_hess=False, ) - - self.check_history() + with pytest.warns(RuntimeWarning, match="cannot handle bounds"): + self.check_history() def test_trace_grad_integrated(self): self.obj = rosen_for_sensi( @@ -447,13 +447,18 @@ def test_trace_grad_integrated(self): trace_record_grad=True, trace_record_hess=False, ) - - self.check_history() + # Expect RuntimeWarning since we cannot handle bounds and + # UserWarning for integrated=True + with pytest.warns(Warning) as warninfo: + self.check_history() + warns = {warn.category for warn in warninfo} + expected_warns = {RuntimeWarning, UserWarning} + assert warns == expected_warns def test_trace_all(self): self.obj = rosen_for_sensi( max_sensi_order=2, - integrated=True, + integrated=False, )["obj"] self.history_options = HistoryOptions( @@ -464,10 +469,11 @@ def test_trace_all(self): trace_record_sres=True, ) self.fix_pars = False - self.check_history() + with pytest.warns(RuntimeWarning, match="cannot handle bounds"): + self.check_history() def test_trace_all_aggregated(self): - self.obj = rosen_for_sensi(max_sensi_order=2, integrated=True)["obj"] + self.obj = rosen_for_sensi(max_sensi_order=2, integrated=False)["obj"] self.history_options = HistoryOptions( trace_record=True, @@ -478,7 +484,8 @@ def test_trace_all_aggregated(self): ) self.obj = pypesto.objective.AggregatedObjective([self.obj, self.obj]) self.fix_pars = False - self.check_history() + with pytest.warns(RuntimeWarning, match="cannot handle bounds"): + self.check_history() class CRFunModeHistoryTest(HistoryTest): @@ -511,7 +518,8 @@ def test_trace_all(self): ) self.fix_pars = False - self.check_history() + with pytest.warns(RuntimeWarning, match="cannot handle bounds"): + self.check_history() @pytest.fixture(params=["memory", "csv", "hdf5", ""]) diff --git a/test/base/test_store.py b/test/base/test_store.py index d90a8030d..31cb583b3 100644 --- a/test/base/test_store.py +++ b/test/base/test_store.py @@ -4,6 +4,7 @@ import tempfile import numpy as np +import pytest import scipy.optimize as so import pypesto @@ -94,7 +95,8 @@ def test_storage_problem(hdf5_file): problem_writer = ProblemHDF5Writer(hdf5_file) problem_writer.write(problem) problem_reader = ProblemHDF5Reader(hdf5_file) - read_problem = problem_reader.read() + with pytest.warns(UserWarning, match="loading a problem"): + read_problem = problem_reader.read() problem_attrs = [ value for name, value in vars(ProblemHDF5Writer).items() @@ -369,7 +371,8 @@ def test_storage_all(): filename = "test_file.hdf5" try: write_result(result=result, filename=filename) - result_read = read_result(filename=filename) + with pytest.warns(UserWarning, match="loading a problem"): + result_read = read_result(filename=filename) # test optimize for i, opt_res in enumerate(result.optimize_result.list): diff --git a/test/optimize/test_optimize.py b/test/optimize/test_optimize.py index e89506afa..2b7e6f76f 100644 --- a/test/optimize/test_optimize.py +++ b/test/optimize/test_optimize.py @@ -219,7 +219,6 @@ def test_unbounded_minimize(optimizer): problem=problem, optimizer=opt, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=options, progress_bar=False, ) @@ -229,7 +228,6 @@ def test_unbounded_minimize(optimizer): problem=problem, optimizer=opt, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=options, progress_bar=False, ) @@ -295,7 +293,6 @@ def check_minimize(problem, library, solver, allow_failed_starts=False): problem=problem, optimizer=optimizer, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, progress_bar=False, ) @@ -328,7 +325,6 @@ def test_trim_results(problem): problem=prob, optimizer=optimizer, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, progress_bar=False, ) @@ -340,7 +336,6 @@ def test_trim_results(problem): problem=prob, optimizer=optimizer, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, progress_bar=False, ) @@ -369,7 +364,10 @@ def test_mpipoolengine(): ) # read results - result1 = read_result("temp_result.h5", problem=True, optimize=True) + with pytest.warns(UserWarning, match="You are loading a problem."): + result1 = read_result( + "temp_result.h5", problem=True, optimize=True + ) # set optimizer optimizer = optimize.FidesOptimizer(verbose=40) # initialize problem with x_guesses and objective @@ -499,7 +497,6 @@ def test_ess(problem, local_optimizer, ess_type, request): res = ess.minimize( problem=problem, - startpoint_method=pypesto.startpoint.UniformStartpoints(), ) print("ESS result: ", res.summary()) @@ -561,15 +558,15 @@ def test_scipy_integrated_grad(): optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) optimize_options = optimize.OptimizeOptions(allow_failed_starts=False) history_options = pypesto.HistoryOptions(trace_record=True) - result = optimize.minimize( - problem=problem, - optimizer=optimizer, - n_starts=1, - startpoint_method=pypesto.startpoint.uniform, - options=optimize_options, - history_options=history_options, - progress_bar=False, - ) + with pytest.warns(UserWarning, match="fun and hess as one func"): + result = optimize.minimize( + problem=problem, + optimizer=optimizer, + n_starts=1, + options=optimize_options, + history_options=history_options, + progress_bar=False, + ) assert ( len(result.optimize_result.history[0].get_fval_trace()) == result.optimize_result.history[0].n_fval @@ -592,7 +589,6 @@ def test_ipopt_approx_grad(): problem=problem, optimizer=optimizer, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, history_options=history_options, progress_bar=False, @@ -606,7 +602,6 @@ def test_ipopt_approx_grad(): problem=problem2, optimizer=optimizer2, n_starts=1, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, history_options=history_options, progress_bar=False, diff --git a/test/profile/test_profile.py b/test/profile/test_profile.py index 328525914..f422fe8bf 100644 --- a/test/profile/test_profile.py +++ b/test/profile/test_profile.py @@ -113,17 +113,24 @@ def test_engine_profiling(self): pypesto.engine.MultiProcessEngine(), pypesto.engine.MultiThreadEngine(), ] - for engine in engines: + expected_warns = [ + pytest.warns(UserWarning, match="fun and hess as one func"), + pytest.warns(UserWarning, match="fun and hess as one func"), + warnings.catch_warnings(), # No warnings + warnings.catch_warnings(), # No warnings + ] + for engine, expected_warn in zip(engines, expected_warns): # run profiling, profile results get appended # in self.result.profile_result - profile.parameter_profile( - problem=self.problem, - result=self.result, - optimizer=self.optimizer, - next_guess_method="fixed_step", - engine=engine, - progress_bar=False, - ) + with expected_warn: + profile.parameter_profile( + problem=self.problem, + result=self.result, + optimizer=self.optimizer, + next_guess_method="fixed_step", + engine=engine, + progress_bar=False, + ) # check results for count, _engine in enumerate(engines[1:]): diff --git a/test/visualize/test_visualize.py b/test/visualize/test_visualize.py index 79aee2f30..cff700497 100644 --- a/test/visualize/test_visualize.py +++ b/test/visualize/test_visualize.py @@ -172,7 +172,6 @@ def create_optimization_history(): problem=problem, optimizer=optimizer, n_starts=5, - startpoint_method=pypesto.startpoint.uniform, options=optimize_options, history_options=history_options, progress_bar=False, @@ -458,7 +457,6 @@ def test_parameters_hist(): problem=problem, optimizer=optimizer, n_starts=10, - startpoint_method=pypesto.startpoint.uniform, progress_bar=False, ) @@ -772,7 +770,6 @@ def test_optimizer_history_with_options(): reference=ref3, trace_x="time", trace_y="gradnorm", - offset_y=10.0, ) @@ -814,7 +811,6 @@ def test_optimization_stats(): problem=problem, optimizer=optimizer, n_starts=10, - startpoint_method=pypesto.startpoint.uniform, progress_bar=False, ) @@ -822,7 +818,6 @@ def test_optimization_stats(): problem=problem, optimizer=optimizer, n_starts=10, - startpoint_method=pypesto.startpoint.uniform, progress_bar=False, ) From 45edfd2c1d01d8e572de13cbf7c74e48add3f07f Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:45:51 +0200 Subject: [PATCH 32/35] Added a brief roadrunner example. (#1348) * Added a brief roadrunner example. to be extended. * Added roadrunner into tox.ini for notebook 1 * Filled in hyperrefs * Added missing hyperref --------- Co-authored-by: Doresic <85789271+Doresic@users.noreply.github.com> --- doc/example.rst | 1 + doc/example/roadrunner.ipynb | 326 +++++++++++++++++++++++ pypesto/objective/roadrunner/__init__.py | 2 +- test/run_notebook.sh | 1 + tox.ini | 2 +- 5 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 doc/example/roadrunner.ipynb diff --git a/doc/example.rst b/doc/example.rst index a6f694110..4c7a263c8 100644 --- a/doc/example.rst +++ b/doc/example.rst @@ -54,6 +54,7 @@ Algorithms and features example/ordinal_data.ipynb example/censored_data.ipynb example/semiquantitative_data.ipynb + example/roadrunner.ipynb Application examples -------------------- diff --git a/doc/example/roadrunner.ipynb b/doc/example/roadrunner.ipynb new file mode 100644 index 000000000..06b540937 --- /dev/null +++ b/doc/example/roadrunner.ipynb @@ -0,0 +1,326 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + " # RoadRunner in pyPESTO\n", + "\n", + "**After going through this notebook, you will be able to...**\n", + "\n", + "* ... create a pyPESTO problem using [RoadRunner](https://www.libroadrunner.org) as a simulator directly from a PEtab problem.\n", + "* ... perform a parameter estimation using pyPESTO with RoadRunner as a simulator, setting advanced simulator features." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# install pyPESTO with roadrunner support\n", + "# %pip install pypesto[roadrunner,petab] --quiet" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# import\n", + "import random\n", + "import matplotlib as mpl\n", + "import petab\n", + "import pypesto.objective\n", + "import pypesto.optimize as optimize\n", + "import pypesto.objective.roadrunner as pypesto_rr\n", + "import pypesto.sample as sample\n", + "import pypesto.visualize as visualize\n", + "import pypesto.profile as profile\n", + "from IPython.display import Markdown, display\n", + "from pprint import pprint\n", + "\n", + "mpl.rcParams[\"figure.dpi\"] = 100\n", + "mpl.rcParams[\"font.size\"] = 18\n", + "\n", + "random.seed(1912)\n", + "\n", + "\n", + "# name of the model that will also be the name of the python module\n", + "model_name = \"boehm_JProteomeRes2014\"\n", + "\n", + "# output directory\n", + "model_output_dir = \"tmp/\" + model_name" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Creating pyPESTO problem from PEtab\n", + "\n", + "The [PEtab file format](https://petab.readthedocs.io/en/latest/documentation_data_format.html) stores all the necessary information to define a parameter estimation problem. This includes the model, the experimental data, the parameters to estimate, and the experimental conditions. Using the `pypesto_rr.PetabImporterRR` class, we can create a pyPESTO problem directly from a PEtab problem." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "petab_yaml = f\"./{model_name}/{model_name}.yaml\"\n", + "\n", + "petab_problem = petab.Problem.from_yaml(petab_yaml)\n", + "importer = pypesto_rr.PetabImporterRR(petab_problem)\n", + "problem = importer.create_problem()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "We now have a pyPESTO problem that we can use to perform parameter estimation. We can get some information on the RoadRunnerObjective and access the RoadRunner model." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "pprint(problem.objective.get_config())" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# direct simulation of the model using roadrunner\n", + "sim_res = problem.objective.roadrunner_instance.simulate(\n", + " times=[0, 2.5, 5, 10, 20, 50]\n", + ")\n", + "pprint(sim_res)\n", + "problem.objective.roadrunner_instance.plot();" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "For more details on interacting with the roadrunner instance, we refer to the [documentation of libroadrunner](https://libroadrunner.readthedocs.io/en/latest/). However, we point out that while theoretical possible, we **strongly advice against** changing the model in that manner." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "ret = problem.objective(\n", + " petab_problem.get_x_nominal(fixed=False,scaled=True),\n", + " mode=\"mode_fun\",\n", + " return_dict=True,\n", + ")\n", + "pprint(ret)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Optimization\n", + "\n", + "To optimize a problem using a RoadRunner objective, we can set additional solver options for the ODE solver." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "optimizer = optimize.ScipyOptimizer()\n", + "\n", + "solver_options = pypesto_rr.SolverOptions(\n", + " relative_tolerance = 1e-6,\n", + " absolute_tolerance = 1e-12,\n", + " maximum_num_steps = 10000\n", + ")\n", + "engine = pypesto.engine.SingleCoreEngine()\n", + "problem.objective.solver_options = solver_options" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "result = optimize.minimize(\n", + " problem=problem,\n", + " optimizer=optimizer,\n", + " n_starts=5, # usually a value >= 100 should be used\n", + " engine=engine\n", + ")\n", + "display(Markdown(result.summary()))" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "Disclaimer: Currently there are two main things not yet fully supported with roadrunner objectives. One is parallelization of the optimization using MultiProcessEngine. The other is explicit gradients of the objective function. While the former will be added in a near future version, we will show a workaround for the latter, until it is implemented." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Visualization Methods\n", + "\n", + "In order to visualize the optimization, there are a few things possible. For a more extensive explanation we refer to the \"getting started\" notebook." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "visualize.waterfall(result);" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "visualize.parameters(result);" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "visualize.parameters_correlation_matrix(result);" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Sensitivities via finite differences\n", + "\n", + "Some solvers need a way to calculate the sensitivities, which currently RoadRunner Objectives do not suport. For this scenario, we can use the FiniteDifferences objective in pypesto, which wraps a given objective into one, that computes sensitivities via finite differences." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# no support for sensitivities\n", + "try:\n", + " ret = problem.objective(\n", + " petab_problem.get_x_nominal(fixed=False,scaled=True),\n", + " mode=\"mode_fun\",\n", + " return_dict=True,\n", + " sensi_orders=(1,),\n", + " )\n", + " pprint(ret)\n", + "except ValueError as e:\n", + " pprint(e)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "objective_fd = pypesto.objective.FD(problem.objective)\n", + "# support through finite differences\n", + "try:\n", + " ret = objective_fd(\n", + " petab_problem.get_x_nominal(fixed=False,scaled=True),\n", + " mode=\"mode_fun\",\n", + " return_dict=True,\n", + " sensi_orders=(1,),\n", + " )\n", + " pprint(ret)\n", + "except ValueError as e:\n", + " pprint(e)" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pypesto/objective/roadrunner/__init__.py b/pypesto/objective/roadrunner/__init__.py index c2b3325da..3b6e800e2 100644 --- a/pypesto/objective/roadrunner/__init__.py +++ b/pypesto/objective/roadrunner/__init__.py @@ -6,4 +6,4 @@ from .petab_importer_roadrunner import PetabImporterRR from .road_runner import RoadRunnerObjective from .roadrunner_calculator import RoadRunnerCalculator -from .utils import simulation_to_measurement_df +from .utils import ExpData, SolverOptions, simulation_to_measurement_df diff --git a/test/run_notebook.sh b/test/run_notebook.sh index c885350c5..cee778651 100755 --- a/test/run_notebook.sh +++ b/test/run_notebook.sh @@ -29,6 +29,7 @@ nbs_1=( 'censored_data.ipynb' 'semiquantitative_data.ipynb' 'getting_started.ipynb' + 'roadrunner.ipynb' ) # Sampling notebooks diff --git a/tox.ini b/tox.ini index 2aa6d02e8..2c860f336 100644 --- a/tox.ini +++ b/tox.ini @@ -124,7 +124,7 @@ description = [testenv:notebooks1] allowlist_externals = bash -extras = example,amici,petab,pyswarm,pymc3,cma,nlopt,fides +extras = example,amici,petab,pyswarm,pymc3,cma,nlopt,fides,roadrunner commands = bash test/run_notebook.sh 1 description = From 0830e95cc3ea90b68169470465d5e255b14ba3c4 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:19:17 +0200 Subject: [PATCH 33/35] Add support for complex noise formulae in roadrunner (#1363) * Add support for complex noise formulae * Removed comment * removed try...except in favor of if --- .../roadrunner/petab_importer_roadrunner.py | 34 +++++++++++++++---- .../roadrunner/roadrunner_calculator.py | 8 ++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/pypesto/objective/roadrunner/petab_importer_roadrunner.py b/pypesto/objective/roadrunner/petab_importer_roadrunner.py index f952d5b11..18f5f4b5b 100644 --- a/pypesto/objective/roadrunner/petab_importer_roadrunner.py +++ b/pypesto/objective/roadrunner/petab_importer_roadrunner.py @@ -7,6 +7,7 @@ from __future__ import annotations import numbers +import re from collections.abc import Iterable from pathlib import Path from typing import Any @@ -85,20 +86,41 @@ def _check_noise_formulae( # check that edatas are available if edatas is None: edatas = self.create_edatas() + # save formulae that need to be changed + to_change = [] # check that noise formulae are valid - for edata, par_map in zip(edatas, parameter_mapping): - for noise_formula in edata.noise_formulae: + for i_edata, (edata, par_map) in enumerate( + zip(edatas, parameter_mapping) + ): + for j_formula, noise_formula in enumerate(edata.noise_formulae): # constant values are allowed if isinstance(noise_formula, numbers.Number): continue # single parameters are allowed if noise_formula in par_map[1].keys(): continue - raise NotImplementedError( - "Noise formulae must be either constants or single " - "parameters. For more complex noise models, please " - "use amici for now." + # extract the observable name via regex pattern + pattern = r"noiseParameter1_(.*?)($|\s)" + observable_name = re.search(pattern, noise_formula).group(1) + to_change.append((i_edata, j_formula, observable_name)) + # change formulae + formulae_changed = [] + for i_edata, j_formula, obs_name in to_change: + # assign new parameter, formula in RR and parameter into mapping + original_formula = edatas[i_edata].noise_formulae[j_formula] + edatas[i_edata].noise_formulae[ + j_formula + ] = f"noiseFormula_{obs_name}" + # different conditions will have the same noise formula + if (obs_name, original_formula) not in formulae_changed: + self.rr.addParameter(f"noiseFormula_{obs_name}", 0.0, False) + self.rr.addAssignmentRule( + f"noiseFormula_{obs_name}", + original_formula, + forceRegenerate=False, ) + self.rr.regenerateModel() + formulae_changed.append((obs_name, original_formula)) def _write_observables_to_model(self): """Write observables of petab problem to the model.""" diff --git a/pypesto/objective/roadrunner/roadrunner_calculator.py b/pypesto/objective/roadrunner/roadrunner_calculator.py index 0f1958dc0..4b6804c76 100644 --- a/pypesto/objective/roadrunner/roadrunner_calculator.py +++ b/pypesto/objective/roadrunner/roadrunner_calculator.py @@ -230,7 +230,7 @@ def simulate_per_condition( times=timepoints, selections=[TIME] + observables_ids ) - llhs = calculate_llh(sim_res, edata, par_map) + llhs = calculate_llh(sim_res, edata, par_map, roadrunner_instance) # reset the model roadrunner_instance.reset() @@ -349,6 +349,7 @@ def calculate_llh( simulations: np.ndarray, edata: ExpData, parameter_mapping: dict, + roadrunner_instance: roadrunner.RoadRunner, ) -> float: """Calculate the negative log-likelihood for a single condition. @@ -360,6 +361,8 @@ def calculate_llh( ExpData of a single condition. parameter_mapping: Parameter mapping for the condition. + roadrunner_instance: + RoadRunner instance. Needed to retrieve complex formulae. Returns ------- @@ -390,6 +393,9 @@ def _fill_in_noise_formula(noise_formula): # if it is not a number, it is assumed to be a string if noise_formula in parameter_mapping.keys(): return parameter_mapping[noise_formula] + # if the string starts with "noiseFormula_" it is saved in the model + if noise_formula.startswith("noiseFormula_"): + return roadrunner_instance.getValue(noise_formula) # replace noise formula with actual value from mapping noise_formulae = np.array( From 41e08af813b0b1232f7c4d47605fa77e91a3b022 Mon Sep 17 00:00:00 2001 From: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:12:35 +0200 Subject: [PATCH 34/35] prepare release v0.5.0 (#1366) * Changelog and version change * Updated changelog * Updated CHANGELOG.rst --- CHANGELOG.rst | 40 ++++++++++++++++++++++++++++++++++++++++ pypesto/version.py | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db393cf34..55253f377 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,46 @@ Release notes ============= +0.5 series +.......... + + +0.5.0 (2024-04-10) +------------------- + +* General + * Include pymc in the documentation. (#1305) + * Ruff Codechecks (#1307) + * Support RoadRunner as simulator for PEtab problems (#1336, #1347, #1348, #1363) +* Hierarchical + * Semiquant: Fix spline knot initialization (#1313, #1323) + * Semiquant: Add spline knots to the optimization result (#1314) + * Semiquant: fix inner opt tolerance (#1330) + * Relative: Fix return of relative calculator if sim fails (#1315) + * Relative: Hierarchical optimization: fix unnecessary simulation (#1327) + * Relative: Fix return of inner parameters on objective call (#1333) +* Optimize + * Support ipopt with gradient approximation (#1310) + * Deprecate CmaesOptimizer in favor of CmaOptimizer (#1311) + * ESSOptimizer: Respect local_n2 in case of failed initial local search (#1328) + * Remove CESSOptimizer (#1320) + * SacessOptimizer: use 'spawn' start method for multiprocessing (#1353) +* PEtab + * Fix unwanted amici model recompilation in PEtab importer (#1319) +* Sample + * Adding Thermodynamic Integration (#1326, #1361) + * Dynesty warnings added (#1324) + * Dynesty: method to save raw results (#1331) +* Ensembles + * Ensembles: don't expect OptimizerResult.id to be convertible to `int` (#1351) +* Misc + * Updated Code to match dependency updates (#1316, #1344, #1346, #1345) + * Ignore code formatting in git blame (#1317) + * Updated deployment method (#1341) + * add pyupgrade to codechecks (#1352) + * Temporarily require scipy<1.13.0 for pypesto[pymc] (#1360) + + 0.4 series .......... diff --git a/pypesto/version.py b/pypesto/version.py index df1243329..3d187266f 100644 --- a/pypesto/version.py +++ b/pypesto/version.py @@ -1 +1 @@ -__version__ = "0.4.2" +__version__ = "0.5.0" From 64f8205c376b7ae510c4e4addebc849a6455fd86 Mon Sep 17 00:00:00 2001 From: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:17:04 +0100 Subject: [PATCH 35/35] fix #1368 (#1369) --- pypesto/visualize/waterfall.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pypesto/visualize/waterfall.py b/pypesto/visualize/waterfall.py index 983b44ea1..6124255ef 100644 --- a/pypesto/visualize/waterfall.py +++ b/pypesto/visualize/waterfall.py @@ -8,7 +8,7 @@ from pypesto.util import delete_nan_inf -from ..C import WATERFALL_MAX_VALUE +from ..C import ALL, WATERFALL_MAX_VALUE from ..result import Result from .clust_color import RGBA, assign_colors from .misc import ( @@ -98,6 +98,13 @@ def waterfall( # handle `order_by_id` if order_by_id: start_id_ordering = get_ordering_by_start_id(results) + # Set start indices to all, and save actual start indices for later, + # so that all fvals are retrieved by `process_offset_for_list`. + # This enables use of `order_by_id` with `start_indices`. + ordered_start_indices = process_start_indices( + result=results[0], start_indices=start_indices + ) + start_indices = ALL refs = create_references(references=reference) @@ -134,6 +141,7 @@ def waterfall( fvals.append(fvals_raw[start_index]) else: fvals.append(None) + fvals = np.array(fvals)[ordered_start_indices] else: # remove nan or inf values in fvals # also remove extremely large values. These values result in `inf`