.. only:: html .. note:: :class: sphx-glr-download-link-note Click :ref:`here ` to download the full example code .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_tutorials_foundations_plot_setup_model.py: Model Definition ---------------- This tutorial describes how to setup a function with random inputs. It also provides examples of how to use model wrappers to time function calls and evaluate a function at multiple samples in parallel. We start by defining a function of two random variables. We will use the Rosenbrock becnhmark. See :func:`pyapprox.benchmarks.benchmarks.setup_rosenbrock_function` .. code-block:: default import pyapprox as pya from pyapprox.benchmarks.benchmarks import setup_benchmark benchmark = setup_benchmark('rosenbrock',nvars=2) Print the attributes of the benchmark with .. code-block:: default print(benchmark.keys()) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none dict_keys(['fun', 'jac', 'hessp', 'variable', 'mean', 'loglike', 'loglike_grad']) Any of these attributes, e.g. the Rosenbrock function (the attribute ``fun`` can be accessed using benchmark.fun. Now lets define the inputs to the function of interest. For independent random variables we use SciPy random variablest to represent each one-dimensional variables. For documentation refer to the `scipy.stats module `_. We define multivariate random variables by specifying each 1D variable in a list. Here we will setup a 2D variable which is the tensor product of two independent and identically distributed uniform random variables .. code-block:: default from scipy import stats univariate_variables = [stats.uniform(-2,4),stats.uniform(-2,4)] variable = pya.IndependentMultivariateRandomVariable(univariate_variables) This variable is also defined in the benchmark.variable attribute. To print a summary of the random variable .. code-block:: default print(variable) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none I.I.D. Variable Number of variables: 2 Unique variables and global id: uniform(loc=-2,scale=4): z0, z1 We can draw random samples from variable and evaluate the function using .. code-block:: default nsamples = 10 samples = pya.generate_independent_random_samples(variable,nsamples) values = benchmark.fun(samples) User defined functions ^^^^^^^^^^^^^^^^^^^^^^ Pyapprox can be used with pretty much any function provided an appropriate interface is defined. Here will show how to setup a simple function. PyApprox requires all functions to take 2D np.ndarray with shape (nvars,nsamples) and requires a function to return a 2D np.ndarray with shape (nsampels,nqoi). nqoi==1 for scalar valued functions and nqoi>1 for vectored value functions. Lets define a function which does not match this criteria and use wrappers provided by PyApprox to convert it to the correct format. Specifically we will define a function that only takes a 1D np.ndarray and returns a scalar .. code-block:: default import numpy as np def fun(sample): assert sample.ndim==1 return np.sum(sample**2) from pyapprox.models.wrappers import evaluate_1darray_function_on_2d_array def pyapprox_fun(samples): values = evaluate_1darray_function_on_2d_array(fun,samples) return values values = pyapprox_fun(samples) The function :func:`pyapprox.models.wrappers.evaluate_1darray_function_on_2d_array` avoids the need to write a for loop but we can do this also and does some checking to make sure values is the correct shape .. code-block:: default values_loop = np.array([np.atleast_1d(fun(s)) for s in samples.T]) assert np.allclose(values,values_loop) Timing function evaluations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is often useful to be able to track the time needed to evaluate a function. We can track this using the :class:`pyapprox.models.wrappers.TimerModelWrapper` and :class:`pyapprox.models.wrappers.WorkTrackingModel` objects which are designed to work together. The former time each evaluation of a function that returns output of shape (nsampels,qoi) and appends the time to the quantities of interest returned by the function, i.e returns a 2D np.ndarray with shape (nsamples,qoi+1). The second extracts the time and removes it from the quantities of interest and returns output with the original shape (nsmaples,nqoi) of the user function. Lets use the class with a function that takes a random amount of time. We will use the previous function but add a random pause between 0 and .1 seconds .. code-block:: default import time def fun_pause(sample): assert sample.ndim==1 time.sleep(np.random.uniform(0,.05)) return np.sum(sample**2) def pyapprox_fun(samples): return evaluate_1darray_function_on_2d_array(fun_pause,samples) from pyapprox.models.wrappers import TimerModelWrapper, WorkTrackingModel timer_fun = TimerModelWrapper(pyapprox_fun) worktracking_fun = WorkTrackingModel(timer_fun) values = worktracking_fun(samples) The :class:`pyapprox.models.wrappers.WorkTrackingModel` has an attribute :class:`pyapprox.models.wrappers.WorkTracker` which stores the execution time of each function evaluation as a dictionary. The key corresponds is the model id. For this example the id will always be the same, but the id can vary and this is useful when evaluating mutiple models, e.g. when using multi-fidelity methods. To print the dictionary use .. code-block:: default costs = worktracking_fun.work_tracker.costs print(costs) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none {(0,): [0.0445559024810791, 0.0033240318298339844, 0.03546309471130371, 0.05207014083862305, 0.037081241607666016, 0.01415395736694336, 0.030753135681152344, 0.001789093017578125, 0.017220020294189453, 0.007019758224487305]} We can also call the work tracker to query the median cost for a model with a given id. The default id is 0. .. code-block:: default fun_id=np.atleast_2d([0]) print(worktracking_fun.work_tracker(fun_id)) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none [0.02398658] Evaluating multiple models ^^^^^^^^^^^^^^^^^^^^^^^^^^ Now let apply this two an ensemble of models to explore the use of model ids. First create a second function. .. code-block:: default def fun_pause_2(sample): time.sleep(np.random.uniform(.05,.1)) return np.sum(sample**2) def pyapprox_fun_2(samples): return evaluate_1darray_function_on_2d_array(fun_pause_2,samples) Now using :class:`pyapprox.control_variate_monte_carlo.ModelEnsemble` we can create a function which takes the random samples plus an additional configure variable which defines which model to evaluate. Lets use half the samples to evaluate the first model and evalaute the second model at the remaining samples .. code-block:: default from pyapprox.control_variate_monte_carlo import ModelEnsemble model_ensemble = ModelEnsemble([pyapprox_fun,pyapprox_fun_2]) timer_fun_ensemble = TimerModelWrapper(model_ensemble) worktracking_fun_ensemble = WorkTrackingModel( timer_fun_ensemble,num_config_vars=1) fun_ids = np.ones(nsamples); fun_ids[:nsamples//2]=0 ensemble_samples = np.vstack([samples,fun_ids]) values = worktracking_fun_ensemble(ensemble_samples) Here we had to pass the number (1) of configure variables to the WorkTrackingModel. PyApprox assumes that the configure variables are the last rows of the samples 2D array Now check that the new values are the same as when using the individual functions directly .. code-block:: default assert np.allclose(values[:nsamples//2],pyapprox_fun(samples[:,:nsamples//2])) assert np.allclose(values[nsamples//2:],pyapprox_fun_2(samples[:,nsamples//2:])) Again we can query the exection times of each model .. code-block:: default costs = worktracking_fun_ensemble.work_tracker.costs print(costs) query_fun_ids = np.atleast_2d([0,1]) print(worktracking_fun_ensemble.work_tracker(query_fun_ids)) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none {(0,): [0.013247966766357422, 0.013377904891967773, 0.020057201385498047, 0.05009603500366211, 0.05231881141662598], (1,): [0.09871411323547363, 0.08461213111877441, 0.06339883804321289, 0.09815406799316406, 0.07094907760620117]} [0.0200572 0.08461213] As expected there are 5 samples tracked for each model and the median evaluation time of the second function is about twice as large as for the first function. Evaluating functions at multiple samples in parallel ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For expensive models it is often useful to be able to evaluate each model concurrently. This can be achieved using :class:`pyapprox.models.wrappers.PoolModel`. Note this function is not intended for use with distributed memory systems, but rather is intended to use all the threads of a personal computer or compute node. See :class:`pyapprox.models.async_model.AsynchronousEvaluationModel` if you are interested in running multiple simulations in parallel on a distributed memory system. PoolModel cannot be used to wrap WorkTrackingModel. However it can still be used with WorkTrackingModel using the sequence of wrappers below. .. code-block:: default from pyapprox.models.wrappers import PoolModel max_eval_concurrency=1#set higher #clear WorkTracker counters pool_model = PoolModel(timer_fun_ensemble,max_eval_concurrency,assert_omp=False) worktracking_fun_ensemble.work_tracker.costs = dict() worktracking_fun_ensemble = WorkTrackingModel( pool_model,num_config_vars=1) #create more samples to notice improvement in wall time nsamples = 10 samples = pya.generate_independent_random_samples(variable,nsamples) fun_ids = np.ones(nsamples); fun_ids[:nsamples//2]=0 ensemble_samples = np.vstack([samples,fun_ids]) import time t0= time.time() values = worktracking_fun_ensemble(ensemble_samples) t1 = time.time() print(f'With {max_eval_concurrency} threads that took {t1-t0} seconds') import os if ('OMP_NUM_THREADS' not in os.environ or int(os.environ['OMP_NUM_THREADS'])!=1): #make sure to set OMP_NUM_THREADS=1 to maximize benefit of pool model print('Warning set OMP_NUM_THREADS=1 for best performance') max_eval_concurrency=4 pool_model.set_max_eval_concurrency(max_eval_concurrency) t0= time.time() values = worktracking_fun_ensemble(ensemble_samples) t1 = time.time() print(f'With {max_eval_concurrency} threads that took {t1-t0} seconds') .. rst-class:: sphx-glr-script-out Out: .. code-block:: none With 1 threads that took 0.5142409801483154 seconds Warning set OMP_NUM_THREADS=1 for best performance With 4 threads that took 0.2192528247833252 seconds Lets print a summary of the costs to make sure individual function evaluation costs are still being recorded correctly .. code-block:: default print(worktracking_fun_ensemble.work_tracker) .. rst-class:: sphx-glr-script-out Out: .. code-block:: none WorkTracker Cost Summary Funtion ID Median Cost (0,) 0.025630712509155273 (1,) 0.09358561038970947 Note ^^^^ PoolModel cannot be used with lambda functions. You will get error similar to pickle.PicklingError: Can't pickle at 0x12b4e6440>: attribute lookup on __main__ failed .. code-block:: default # sphinx_gallery_thumbnail_path = './figures/cantilever-beam.png' .. gallery thumbnail will say broken if no plots are made in this file so .. specify a default file as above .. rst-class:: sphx-glr-timing **Total running time of the script:** ( 0 minutes 2.199 seconds) .. _sphx_glr_download_auto_tutorials_foundations_plot_setup_model.py: .. only :: html .. container:: sphx-glr-footer :class: sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_setup_model.py ` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_setup_model.ipynb ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_