.. Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. .. Contributors: :author: Matthias De Lozzo .. _nutshell_design_space: How to deal with design spaces ============================== 1. How is a design space defined? ********************************* 1.a. What is a design space made of? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A design space is defined by: - the number of the variables - the names of the variables - the sizes of the variables - the upper bounds of the variables (optional; by default: :math:`-\infty`) - the lower bounds of the variables (optional; by default: :math:`+\infty`) - the normalization policies of the variables: - bounded float variables are normalizable, - bounded integer variables are normalizable, - unbounded float variables are not normalizable, - unbounded integer variables are not normalizable. .. note:: The normalized version of a given variable :math:`x` is either :math:`\frac{x-lb_x}{ub_x-lb_x}` or :math:`\frac{x}{ub_x-lb_x}`. 1.b. How is a design space implemented in |g|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Design space are implemented in |g| through the :class:`.DesignSpace` class. 1.c. What are the API functions in |g|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A design space can be created from the :meth:`~gemseo.api.create_design_space` and :meth:`~gemseo.api.read_design_space` API functions and then, enhanced by methods of the :class:`.DesignSpace` class. It can be exported to a file by means of the :meth:`~gemseo.api.export_design_space`. 1.d. How does |g| handle integer variables? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When integer variables are included in a :class:`.DesignSpace`, |g| may round its values to keep them consistent with the specified type. This rounding takes place when the variables vector is unnormalized, and just before a function or its gradient are evaluated. Depending on the problem that is being solved and the algorithm that is being used, the results may or may not be affected. - In the case of an :class:`.OptimizationProblem`, the main issue is that this rounding introduces discontinuities. Gradient-based algorithms are very sensitive to discontinuities and do not handle integer variables, they may not crash, but they will probably end up returning local minima, not converging, or exhibit unexpected behavior. Gradient-free algorithms handle rounding a little bit better, in the case of COBYLA it is equivalent to a strong relaxation. Still, we strongly recommend to choose carefully to avoid issues. By default, |g| will not allow the user to run an :class:`.OptimizationProblem` using an algorithm that does not handle integer variables. However, since the potential issues also depend on the problem itself, a user can skip this check passing the argument ``skip_int_check=True`` in ``algo_options``. For updated information about the optimization algorithms that handle integer variables, refer to :ref:`gen_opt_algos`. - In the case of a :class:`.DOEScenario`, there are no convergence issues because we are only evaluating pre-defined samples. Nevertheless, one should remember that the samples generated by the DoE algorithm will be rounded if necessary. This may impact the mathematical properties of the sample distribution. 2. How to read a design space from a file? ****************************************** Case 1: the file contains a header line ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's consider the *design_space.txt* file containing the following lines: .. code:: name lower_bound value upper_bound type x1 -1. 0. 1. float x2 5. 6. 8. float We can read this file by means of the :meth:`~gemseo.api.read_design_space` function API: .. code:: from gemseo.api import read_design_space design_space = read_design_space('design_space.txt') and print it: .. code:: print(design_space) which results in: .. code:: Design Space: +------+-------------+-------+-------------+-------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+-------+ | x1 | -1 | 0 | 1 | float | | x2 | 5 | 6 | 8 | float | +------+-------------+-------+-------------+-------+ Case 2: the file does not contain a header line ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, let's consider the *design_space_without_header.txt* file containing the following lines: .. code:: x1 -1. 0. 1. float x2 5. 6. 8. float We can read this file by means of the :meth:`~gemseo.api.read_design_space` API function with the list of labels as optional argument: .. code:: from gemseo.api import read_design_space design_space = read_design_space( "design_space_without_header.txt", ["name", "lower_bound", "value", "upper_bound", "type"], ) and print it: .. code:: print(design_space) which results in: .. code:: Design Space: +------+-------------+-------+-------------+-------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+-------+ | x1 | -1 | 0 | 1 | float | | x2 | 5 | 6 | 8 | float | +------+-------------+-------+-------------+-------+ .. warning:: - User must provide the following minimal fields in the file defining the design space: :code:`'name'`, :code:`'lower_bound'` and :code:`'upper_bound'`. - The inequality :code:`'lower_bound'` <= :code:`'name'` <= :code:`'upper_bound'` must be satisfied. .. note:: - Available fields are :code:`'name'`, :code:`'lower_bound'`, :code:`'upper_bound'`, :code:`'value'` and :code:`'type'`. - The :code:`'value'` field is optional. By default, it is set at :code:`None`. - The :code:`'type'` field is optional. By default, it is set at :code:`float`. - Each dimension of a variable must be provided. E.g. when the :code:`'size'` of :code:`'x1'` is 2: .. code:: name lower_bound value upper_bound type x1 -1. 0. 1. float x1 -3. -1. 1. float x2 5. 6. 8. float .. note:: - Lower infinite bound is encoded :code:`-inf'` or :code:`'-Inf'`. - Upper infinite bound is encoded :code:`'inf'`, :code:`'Inf'`, :code:`'+inf'` or :code:`'+Inf'`. 3. How to create a design space from scratch? ********************************************* Let's imagine that we want to build a design space with the following requirements: - *x1* is a one-dimensional unbounded float variable, - *x2* is a one-dimensional unbounded integer variable, - *x3* is a two-dimensional unbounded float variable, - *x4* is a one-dimensional float variable with lower bound equal to 1, - *x5* is a one-dimensional float variable with upper bound equal to 1, - *x6* is a one-dimensional unbounded float variable, - *x7* is a two-dimensional bounded integer variable with lower bound equal to -1, upper bound equal to 1 and current values to (0,1), We can create this design space from scratch by means of the :meth:`~gemseo.api.create_design_space` API function and the :meth:`.DesignSpace.add_variable` method of the :class:`.DesignSpace` class: .. code:: from gemseo.api import create_design_space from numpy import ones, array design_space = create_design_space() design_space.add_variable('x1') design_space.add_variable('x2', var_type='integer') design_space.add_variable('x3', size=2) design_space.add_variable('x4', l_b=ones(1)) design_space.add_variable('x5', u_b=ones(1)) design_space.add_variable('x6', value=ones(1)) design_space.add_variable( "x7", size=2, var_type="integer", value=array([0, 1]), l_b=-ones(2), u_b=ones(2) ) and print it: .. code:: print(design_space) which results in: .. code:: Design Space: +------+-------------+-------+-------------+---------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+---------+ | x1 | -inf | None | inf | float | | x2 | -inf | None | inf | integer | | x3 | -inf | None | inf | float | | x3 | -inf | None | inf | float | | x4 | 1 | None | inf | float | | x5 | -inf | None | 1 | float | | x6 | -inf | 1 | inf | float | | x7 | -1 | 0 | 1 | integer | | x7 | -1 | 1 | 1 | integer | +------+-------------+-------+-------------+---------+ .. note:: For a variable whose :code:`'size'` is greater than 1, each dimension of this variable is printed (e.g. :code:`'x3'` and :code:`'x7'`). .. note:: We can get a list of the variable names with theirs indices by means of the :meth:`.DesignSpace.get_indexed_variables_names` method: .. code:: indexed_variables_names = design_space.get_indexed_variables_names() and :code:`print(indexed_variables_names)`: .. code:: ['x1', 'x2', 'x3!0', 'x3!1', 'x4', 'x5', 'x6', 'x7!0', 'x7!1'] We see that the multidimensional variables have an index (here :code:`'0'` and :code:`'1'`) preceded by a :code:`'!'` separator. 4. How to get information about the design space? ************************************************* How to get the size of a design variable? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the size of a variable by means of the :meth:`.DesignSpace.get_size` method: .. code:: x3_size = design_space.get_size('x3') and :code:`print(x3_size)` to see the result: .. code:: 1 How to get the type of a design variable? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the type of a variable by means of the :meth:`.DesignSpace.get_type` method: .. code:: x3_type = design_space.get_type('x3') and :code:`print(x3_type)` to see the result: .. code:: ['float'] How to get the size of a lower or upper bound for a given variable? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the lower and upper bounds of a variable by means of the :meth:`.DesignSpace.get_lower_bound` and :meth:`.DesignSpace.get_upper_bound` methods: .. code:: x3_lb = design_space.get_lower_bound('x3') x3_ub = design_space.get_upper_bound('x3') and :code:`print(x3_lb, x3_ub)` to see the result: .. code:: [-10.], [ 10.] How to get the size of a lower or upper bound for a set of given variables? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the lower and upper bounds of a set of variables by means of the :meth:`.DesignSpace.get_lower_bounds` and :meth:`.DesignSpace.get_upper_bounds` methods: .. code:: x1x3_lb = design_space.get_lower_bounds(['x1', 'x3']) x1x3_ub = design_space.get_upper_bounds(['x1', 'x3']) and :code:`print(x1x3_lb, x1x3_ub)` to see the result: .. code:: [-10. -10.], [ 10. 10.] How to get the current array value of the design parameter vector? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the current value of the design parameters by means of the :meth:`.DesignSpace.get_lower_bounds` and :meth:`.DesignSpace.get_current_value` method: .. code:: current_x = design_space.get_current_value() and :code:`print(current_x)` to see the result: .. code:: [ 3. 1. 1. 1.] How to get the current dictionary value of the design parameter vector? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the current value of the design parameters with :code:`dict` format by means of the :meth:`.DesignSpace.get_lower_bounds` and :meth:`.DesignSpace.get_current_value` method: .. code:: dict_current_x = design_space.get_current_value(as_dict=True) and :code:`print(dict_current_x)` to see the result: .. code:: {'x2': array([1.]), 'x3': array([1.]), 'x1': array([3.]), 'x6': array([1.])} How to get the normalized array value of the design parameter vector? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the normalized current value of the design parameters by means of the :meth:`.DesignSpace.get_lower_bounds` and :meth:`.DesignSpace.get_current_x_normalized` method: .. code:: normalized_current_x = design_space.get_current_value(normalize=True) and :code:`print(normalized_current_x)` to see the result: .. code:: [ 0.65 1. 0.55 0.55] How to get the active bounds at the current design parameter vector or at a given one? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get the active bounds by means of the :meth:`.DesignSpace.get_active_bounds` method, either at current parameter values: .. code:: active_at_current_x = design_space.get_active_bounds() and :code:`print(active_at_current_x)` to see the result: .. code:: ({'x2': array([False], dtype=bool), 'x3': array([False], dtype=bool), 'x1': array([False], dtype=bool), 'x6': array([False], dtype=bool)}, {'x2': array([False], dtype=bool), 'x3': array([False], dtype=bool), 'x1': array([False], dtype=bool), 'x6': array([False], dtype=bool)}) or at a given point: .. code:: active_at_given_point = design_space.get_active_bounds(array([1., 10, 1., 1.])) and :code:`print(active_at_given_point)` to see the result: .. code.. ({'x2': array([False], dtype=bool), 'x3': array([False], dtype=bool), 'x1': array([False], dtype=bool), 'x6': array([False], dtype=bool)}, {'x2': array([ True], dtype=bool), 'x3': array([False], dtype=bool), 'x1': array([False], dtype=bool), 'x6': array([False], dtype=bool)}) 5. How to modify a design space? ******************************** How to remove a variable from a design space? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's consider the previous design space and assume that we want to remove the :code:`'x4'` variable. For that, we can use the :meth:`.DesignSpace.remove_variable` method: .. code:: design_space.remove_variable('x4') and :code:`print(design_space)` to see the result: .. code:: Design Space: +------+-------------+-------+-------------+---------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+---------+ | x1 | -inf | None | inf | float | | x2 | -inf | None | inf | integer | | x3 | -inf | None | inf | float | | x3 | -inf | None | inf | float | | x5 | -inf | None | 1 | float | | x6 | -inf | 1 | inf | float | | x7 | -1 | 0 | 1 | integer | | x7 | -1 | 1 | 1 | integer | +------+-------------+-------+-------------+---------+ How to filter the entries of a design space? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can keep only a subset of variables, e.g. :code:`'x1'`, :code:`'x2'`, :code:`'x3'` and :code:`'x6'`, by means of the :meth:`gemseo.algos.design_space.DesignSpace.filter` method: .. code:: design_space.filter(['x1', 'x2', 'x3', 'x6']) # keep the x1, x2, x3 and x6 variables and :code:`print(design_space)` to see the result: .. code:: Design Space: +------+-------------+-------+-------------+---------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+---------+ | x1 | -inf | None | inf | float | | x2 | -inf | None | inf | integer | | x3 | -inf | None | inf | float | | x3 | -inf | None | inf | float | | x6 | -inf | 1 | inf | float | +------+-------------+-------+-------------+---------+ We can also keep only a subset of components for a given variable, e.g. the first component of the variable :code:`'x3'`, by means of the :meth:`gemseo.algos.design_space.DesignSpace.filter_dim` method: .. code:: design_space.filer_dim('x3', [0]) # keep the first dimension of x3 and :code:`print(design_space)` to see the result: .. code:: Design Space: +------+-------------+-------+-------------+---------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+---------+ | x1 | -inf | None | inf | float | | x2 | -inf | None | inf | integer | | x3 | -inf | None | inf | float | | x6 | -inf | 1 | inf | float | +------+-------------+-------+-------------+---------+ How to modify the data values contained in a design space? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can change the current values and bounds contained in a design space by means of the :meth:`.DesignSpace.set_current_value`, :meth:`.DesignSpace.set_current_variable`, :meth:`.DesignSpace.set_lower_bound` and :meth:`.DesignSpace.set_upper_bound` methods: .. code:: design_space.set_current_value(array([1., 1., 1., 1.])) design_space.set_current_variable('x1', array([3.])) design_space.set_lower_bound('x1', array([-10.])) design_space.set_lower_bound('x2', array([-10.])) design_space.set_lower_bound('x3', array([-10.])) design_space.set_lower_bound('x6', array([-10.])) design_space.set_upper_bound('x1', array([10.])) design_space.set_upper_bound('x2', array([10.])) design_space.set_upper_bound('x3', array([10.])) design_space.set_upper_bound('x6', array([10.])) and :code:`print(design_space)` to see the result: .. code:: Design Space: +------+-------------+-------+-------------+---------+ | name | lower_bound | value | upper_bound | type | +------+-------------+-------+-------------+---------+ | x1 | -10 | 3 | 10 | float | | x2 | -10 | 1 | 10 | integer | | x3 | -10 | 1 | 10 | float | | x6 | -10 | 1 | 10 | float | +------+-------------+-------+-------------+---------+ 6. How to (un)normalize a parameter vector? ******************************************* Let's consider the parameter vector :code:`x_vect = array([1.,10.,1.,1.])`. We can normalize this vector by means of the :meth:`.DesignSpace.normalize_vect`: .. code:: normalized_x_vect = design_space.normalize_vect(x_vect) and :code:`print(normalized_x_vect)`: .. code:: [ 0.55 1. 0.55 0.55] Conversely, we can unnormalize this normalized vector by means of the :meth:`.DesignSpace.unnormalize_vect`: .. code:: unnormalized_x_vect = design_space.unnormalize_vect(x_vect) .. code:: [ 1. 10. 1. 1.] .. note:: Both methods takes an optional argument denoted :code:`'minus_lb'` which is :code:`True` by default. If :code:`True`, the normalization of the normalizable variables is of the form :code:`(x-lb_x)/(ub_x-lb_x)`. Otherwise, it is of the form :code:`x/(ub_x-lb_x)`. Here, when :code:`minus_lb` is :code:`False`, the normalize parameter vector is: .. code:: [ 0.05 0.5 0.05 0.05] 7. How to cast design data? *************************** How to cast a design point from array to dict? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can cast a design point from :code:`array` to :code:`dict` by means of the :meth:`.DesignSpace.array_to_dict` method: .. code:: array_point = array([1, 2, 3, 4]) dict_point = design_space.array_to_dict(array_point) and :code:`print(dict_point)` to see the result: .. code:: {'x2': array([2]), 'x3': array([3]), 'x1': array([1]), 'x6': array([4])} How to cast a design point from dict to array? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can cast a design point from :code:`dict` to :code:`array` by means of the :meth:`.DesignSpace.dict_to_array` method: .. code:: new_array_point = design_space.dict_to_array(dict_point) and :code:`print(new_array_point)` to see the result: .. code:: [1, 2, 3, 4] .. note:: An optional argument denoted :code:`'variable_names'`, which is a list of string and set at :code:`None` by default, lists all of the variables to consider. If :code:`None`, all design variables are considerd. How to cast the current value to complex? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can cast the current value to complex by means of the :meth:`.DesignSpace.to_complex` method: .. code:: print(design_space.get_current_value()) design_space.to_complex() print(design_space.get_current_value()) and the successive printed messages are: .. code:: [ 3. 1. 1. 1.] [ 3.+0.j 1.+0.j 1.+0.j 1.+0.j] How to cast the right component values of a vector to integer? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For a given vector where some components should be integer, it is possible to round them by means of the :meth:`.DesignSpace.round_vect` method: .. code:: vector = array([1.3, 3.4,3.6, -1.4]) rounded_vector = design_space.round_vect(vector) and :code:`print(rounded_vector)` to see the result: .. code:: [ 1.3 3. 3.6 -1.4] 8. How to test if the current value is defined? *********************************************** We can test if the design parameter set has a current :code:`'value'` by means of the :meth:`.DesignSpace.has_current_value`: .. code:: print(design_space.has_current_value()) which results in: .. code:: True .. note:: The result returned by :meth:`.DesignSpace.has_current_value` is :code:`False` as long as at least one component of one variable is :code:`None`. 9. How to project a point into bounds? ************************************** Sometimes, components of a design vector are greater than the upper bounds or lower than the upper bounds. For that, it is possible to project the vector into the bounds by means of the :meth:`.DesignSpace.project_into_bounds`: .. code:: point = array([1.,3,-15.,23.]) p_point = design_space.project_into_bounds(point) and :code:`print(p_point)` to see the result: .. code:: [ 1. 3. -10. 10.] 10. How to export a design space to a file? ******************************************* When the design space is created, it is possible to export it by means of the :meth:`~gemseo.api.export_design_space` API function with arguments: - :code:`design_space`: design space - :code:`output_file`: output file path - :code:`export_hdf`: export to a hdf file (True, default) or a txt file (False) - :code:`fields`: list of fields to export, by default all - :code:`append`: if :code:`True`, appends the data in the file - :code:`table_options`: dictionary of options for the :class:`~gemseo.third_party.prettytable.prettytable.PrettyTable` For example: .. code:: from gemseo.api import export_design_space export_design_space(design_space, 'new_design_space.txt')