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: \(-\infty\))

  • the lower bounds of the variables (optional; by default: \(+\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 \(x\) is either \(\frac{x-lb_x}{ub_x-lb_x}\) or \(\frac{x}{ub_x-lb_x}\).

1.b. How is a design space implemented in GEMSEO?

Design space are implemented in GEMSEO through the DesignSpace class.

1.c. What are the API functions in GEMSEO?

A design space can be created from the create_design_space() and read_design_space() API functions and then, enhanced by methods of the DesignSpace class. It can be exported to a file by means of the export_design_space().

1.d. How does GEMSEO handle integer variables?

When integer variables are included in a DesignSpace, GEMSEO 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 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, GEMSEO will not allow the user to run an 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 Options for Optimization algorithms.

  • In the case of a 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:

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 read_design_space() function API:

from gemseo.api import read_design_space

design_space = read_design_space('design_space.txt')

and print it:

print(design_space)

which results in:

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:

x1 -1. 0. 1. float
x2 5. 6. 8. float

We can read this file by means of the read_design_space() API function with the list of labels as optional argument:

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:

print(design_space)

which results in:

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: 'name', 'lower_bound' and 'upper_bound'.

  • The inequality 'lower_bound' <= 'name' <= 'upper_bound' must be satisfied.

Note

  • Available fields are 'name', 'lower_bound', 'upper_bound', 'value' and 'type'.

  • The 'value' field is optional. By default, it is set at None.

  • The 'type' field is optional. By default, it is set at float.

  • Each dimension of a variable must be provided. E.g. when the 'size' of 'x1' is 2:

    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 -inf' or '-Inf'.

  • Upper infinite bound is encoded 'inf', 'Inf', '+inf' or '+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 create_design_space() API function and the DesignSpace.add_variable() method of the DesignSpace class:

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:

print(design_space)

which results in:

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 'size' is greater than 1, each dimension of this variable is printed (e.g. 'x3' and 'x7').

Note

We can get a list of the variable names with theirs indices by means of the DesignSpace.get_indexed_variables_names() method:

indexed_variables_names = design_space.get_indexed_variables_names()

and print(indexed_variables_names):

['x1', 'x2', 'x3!0', 'x3!1', 'x4', 'x5', 'x6', 'x7!0', 'x7!1']

We see that the multidimensional variables have an index (here '0' and '1') preceded by a '!' 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 DesignSpace.get_size() method:

x3_size = design_space.get_size('x3')

and print(x3_size) to see the result:

1

How to get the type of a design variable?

We can get the type of a variable by means of the DesignSpace.get_type() method:

x3_type = design_space.get_type('x3')

and print(x3_type) to see the result:

['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 DesignSpace.get_lower_bound() and DesignSpace.get_upper_bound() methods:

x3_lb = design_space.get_lower_bound('x3')
x3_ub = design_space.get_upper_bound('x3')

and print(x3_lb, x3_ub) to see the result:

[-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 DesignSpace.get_lower_bounds() and DesignSpace.get_upper_bounds() methods:

x1x3_lb = design_space.get_lower_bounds(['x1', 'x3'])
x1x3_ub = design_space.get_upper_bounds(['x1', 'x3'])

and print(x1x3_lb, x1x3_ub) to see the result:

[-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 DesignSpace.get_lower_bounds() and DesignSpace.get_current_value() method:

current_x = design_space.get_current_value()

and print(current_x) to see the result:

[ 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 dict format by means of the DesignSpace.get_lower_bounds() and DesignSpace.get_current_x_dict() method:

dict_current_x = design_space.get_current_value(as_dict=True)

and print(dict_current_x) to see the result:

{'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 DesignSpace.get_lower_bounds() and DesignSpace.get_current_x_normalized() method:

normalized_current_x = design_space.get_current_value(normalize=True)

and print(normalized_current_x) to see the result:

[ 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 DesignSpace.get_active_bounds() method, either at current parameter values:

active_at_current_x = design_space.get_active_bounds()

and print(active_at_current_x) to see the result:

({'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:

active_at_given_point = design_space.get_active_bounds(array([1., 10, 1., 1.]))

and print(active_at_given_point) to see the result:

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 'x4' variable.

For that, we can use the DesignSpace.remove_variable() method:

design_space.remove_variable('x4')

and print(design_space) to see the result:

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. 'x1', 'x2', 'x3' and 'x6', by means of the gemseo.algos.design_space.DesignSpace.filter() method:

design_space.filter(['x1', 'x2', 'x3', 'x6']) # keep the x1, x2, x3 and x6 variables

and print(design_space) to see the result:

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 'x3', by means of the gemseo.algos.design_space.DesignSpace.filter_dim() method:

design_space.filer_dim('x3', [0]) # keep the first dimension of x3

and print(design_space) to see the result:

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 DesignSpace.set_current_value(), DesignSpace.set_current_variable(), DesignSpace.set_lower_bound() and DesignSpace.set_upper_bound() methods:

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 print(design_space) to see the result:

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 x_vect = array([1.,10.,1.,1.]). We can normalize this vector by means of the DesignSpace.normalize_vect():

normalized_x_vect = design_space.normalize_vect(x_vect)

and print(normalized_x_vect):

[  0.55  1.     0.55   0.55]

Conversely, we can unnormalize this normalized vector by means of the DesignSpace.unnormalize_vect():

unnormalized_x_vect = design_space.unnormalize_vect(x_vect)
[  1.  10.   1.   1.]

Note

Both methods takes an optional argument denoted 'minus_lb' which is True by default. If True, the normalization of the normalizable variables is of the form (x-lb_x)/(ub_x-lb_x). Otherwise, it is of the form x/(ub_x-lb_x). Here, when minus_lb is False, the normalize parameter vector is:

[  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 array to dict by means of the DesignSpace.array_to_dict() method:

array_point = array([1, 2, 3, 4])
dict_point = design_space.array_to_dict(array_point)

and print(dict_point) to see the result:

{'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 dict to array by means of the DesignSpace.dict_to_array() method:

new_array_point = design_space.dict_to_array(dict_point)

and print(new_array_point) to see the result:

[1, 2, 3, 4]

Note

  • An optional argument denoted 'all_vars', which is a boolean and set at True by default, indicates if all design variables shall be in the dict passed in argument.

  • An optional argument denoted 'all_var_list', which is a list of string and set at None by default, list all of the variables to consider. If 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 DesignSpace.to_complex() method:

print(design_space.get_current_value())
design_space.to_complex()
print(design_space.get_current_value())

and the successive printed messages are:

[ 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 DesignSpace.round_vect() method:

vector = array([1.3, 3.4,3.6, -1.4])
rounded_vector =  design_space.round_vect(vector)

and print(rounded_vector) to see the result:

[ 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 'value' by means of the DesignSpace.has_current_value():

print(design_space.has_current_value())

which results in:

True

Note

The result returned by DesignSpace.has_current_value() is False as long as at least one component of one variable is 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 DesignSpace.project_into_bounds():

point = array([1.,3,-15.,23.])
p_point = design_space.project_into_bounds(point)

and print(p_point) to see the result:

[  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 export_design_space() API function with arguments:

  • design_space: design space

  • output_file: output file path

  • export_hdf: export to a hdf file (True, default) or a txt file (False)

  • fields: list of fields to export, by default all

  • append: if True, appends the data in the file

  • table_options: dictionary of options for the PrettyTable

For example:

from gemseo.api import export_design_space

export_design_space(design_space, 'new_design_space.txt')