AnalysisDriver#

AnalysisDriver serves to run a number of case samples without optimization. The intent is to provide data about the response of a model’s outputs when various inputs are changed.

AnalysisDriver Options#

import openmdao.api as om
om.show_options_table("openmdao.drivers.analysis_driver.AnalysisDriver")

OptionDefaultAcceptable ValuesAcceptable TypesDescription
batch_size1000N/A['int']Number of samples to distribute among the processors at a time when run_parallel is True. This should be limited when the memory required to store the batch size of samples grows too large.
debug_print[]['desvars', 'nl_cons', 'ln_cons', 'objs', 'totals']['list']List of what type of Driver variables to print at each iteration.
invalid_desvar_behaviorwarn['warn', 'raise', 'ignore']N/ABehavior of driver if the initial value of a design variable exceeds its bounds. The default value may beset using the `OPENMDAO_INVALID_DESVAR_BEHAVIOR` environment variable to one of the valid options.
procs_per_model1N/A['int']Number of processors to give each model under MPI.
run_parallelFalse[True, False]['bool']Set to True to execute samples in parallel.

Specifying Samples for Execution#

Upon initialization, samples for AnalysisDriver can be provided in two ways.

First, if a list or tuple is provided as the samples argument, it contains one or more dictionaries. Each dictionary in the sequence maps the keys (the promoted variable names to be set) to the associated metadata. Key val is required, while keys units and indices are optional.

In the following example, nine samples of values for x and y are provided.

import openmdao.api as om
from openmdao.test_suite.components.paraboloid import Paraboloid

prob = om.Problem()

prob.model.add_subsystem('comp', Paraboloid(), promotes=['*'])

samples_3x3 = [
    {'x': {'val': 0.}, 'y': {'val': 0.}},
    {'x': {'val': .5}, 'y': {'val': 0.}},
    {'x': {'val': 1.}, 'y': {'val': 0.}},

    {'x': {'val': 0.}, 'y': {'val': 0.5}},
    {'x': {'val': .5}, 'y': {'val': 0.5}},
    {'x': {'val': 1.}, 'y': {'val': 0.5}},

    {'x': {'val': 0.}, 'y': {'val': 1.}},
    {'x': {'val': .5}, 'y': {'val': 1.}},
    {'x': {'val': 1.}, 'y': {'val': 1.}},
]

prob.driver = om.AnalysisDriver(samples=samples_3x3)
prob.driver.add_recorder(om.SqliteRecorder('cases.sql'))

prob.driver.add_response('f_xy', units=None, indices=[0])

prob.setup()
prob.run_driver()
prob.cleanup()

cr = om.CaseReader(str(prob.get_outputs_dir() / "cases.sql"))
cases = cr.get_cases(source='driver')

print(f'Recorded {len(cases)} cases.')

print(f'{"x":^6}     {"y":^6}     {"f_xy":^6}')
for case in cases:
    x = case.get_val('x')[0]
    y = case.get_val('y')[0]
    f_xy = case.get_val('f_xy')[0]
    print(f'{x:6.3f}     {y:6.3f}     {f_xy:6.3f}')
Recorded 9 cases.
  x          y         f_xy 
 0.000      0.000     22.000
 0.500      0.000     19.250
 1.000      0.000     17.000
 0.000      0.500     26.250
 0.500      0.500     23.750
 1.000      0.500     21.750
 0.000      1.000     31.000
 0.500      1.000     28.750
 1.000      1.000     27.000

Specifying Responses for Derivative Recording#

Since AnalysisDriver does not rely upon the constraints and objectives defined within a model, if the recording of derivatives is requested when executing the AnalysisDriver, the user needs to specify those outputs (responses) whose derivatives are required.

AnalysisDriver provides an add_response method used to indicate those outputs that should be recorded. When recording derivatives, derivatives will be recorded for all responses.

AnalysisDriver.add_response(name, indices=None, units=None, linear=False, parallel_deriv_color=None, cache_linear_solution=False, flat_indices=None, alias=None)[source]

Add a response variable to the model associated with this AnalysisDriver.

For AnalysisDriver, a response is an “output of interest” that we want to monitor as a result of changes made in the various samples.

The AnalysisDriver.add_response interface does not support any optimization-centric arguments associated with constraints or objectives, such as scaling.

Internally, the driver does add this as an ‘objective’ to the model for the purposes of tracking derivatives.

Parameters:
namestr

Promoted name of the response variable in the system.

indicessequence of int, optional

If variable is an array, these indicate which entries are of interest for this particular response.

unitsstr, optional

Units to convert to before applying scaling.

linearbool

Set to True if constraint is linear. Default is False.

parallel_deriv_colorstr

If specified, this design var will be grouped for parallel derivative calculations with other variables sharing the same parallel_deriv_color.

cache_linear_solutionbool

If True, store the linear solution vectors for this variable so they can be used to start the next linear solution with an initial guess equal to the solution from the previous linear solve.

flat_indicesbool

If True, interpret specified indices as being indices into a flat source array.

aliasstr or None

Alias for this response. Necessary when adding multiple responses on different indices of the same variable.

import openmdao.api as om
from openmdao.test_suite.components.paraboloid import Paraboloid

prob = om.Problem()

prob.model.add_subsystem('comp', Paraboloid(), promotes=['*'])

samples_3x3 = [
    {'x': {'val': 0.}, 'y': {'val': 0.}},
    {'x': {'val': .5}, 'y': {'val': 0.}},
    {'x': {'val': 1.}, 'y': {'val': 0.}},

    {'x': {'val': 0.}, 'y': {'val': 0.5}},
    {'x': {'val': .5}, 'y': {'val': 0.5}},
    {'x': {'val': 1.}, 'y': {'val': 0.5}},

    {'x': {'val': 0.}, 'y': {'val': 1.}},
    {'x': {'val': .5}, 'y': {'val': 1.}},
    {'x': {'val': 1.}, 'y': {'val': 1.}},
]

prob.driver = om.AnalysisDriver(samples=samples_3x3)
prob.driver.add_recorder(om.SqliteRecorder('cases.sql'))
prob.driver.recording_options['record_derivatives'] = True

prob.driver.add_response('f_xy', units=None, indices=[0])

prob.setup()
prob.run_driver()
prob.cleanup()

cr = om.CaseReader(str(prob.get_outputs_dir() / "cases.sql"))
cases = cr.get_cases(source='driver')


print(f'{"x":^6}     {"y":^6}     {"f_xy":^6}     {"df/dx":^6}     {"df/dy":^6}')
for case in cases:
    x = case.get_val('x')[0]
    y = case.get_val('y')[0]
    f_xy = case.get_val('f_xy')[0]
    df_dx = case.derivatives['f_xy', 'x']
    df_dy = case.derivatives['f_xy', 'y']
    print(f'{x:6.3f}     {y:6.3f}     {f_xy:6.3f}     {df_dx[0][0]}     {df_dy[0][0]}')
  x          y         f_xy      df/dx      df/dy 
 0.000      0.000     22.000     -6.0     8.0
 0.500      0.000     19.250     -5.0     8.5
 1.000      0.000     17.000     -4.0     9.0
 0.000      0.500     26.250     -5.5     9.0
 0.500      0.500     23.750     -4.5     9.5
 1.000      0.500     21.750     -3.5     10.0
 0.000      1.000     31.000     -5.0     10.0
 0.500      1.000     28.750     -4.0     10.5
 1.000      1.000     27.000     -3.0     11.0

To quickly record multiple outputs without having an add_response call for each one, the user can use the add_responses method. This method can accept a sequence of strings, to quickly add outputs to be recorded. Alternatively, providing responses as a dictionary allows the units, indices, and other metadata to be added in a manner similar to samples.

AnalysisDriver.add_responses(responses)[source]

Add multiple responses to be recorded by the AnalysisDriver.

Parameters:
responsesSequence or dict or str

A sequence of response names to be recorded. If more metadata needs to be specified, reponses can be provided as a dictionary whose keys are the variables to be recorded, and whose associated values are dictionaries of metadata to be passed on as keyword arguments to add_response.

import openmdao.api as om
from openmdao.test_suite.components.paraboloid import Paraboloid

prob = om.Problem()

prob.model.add_subsystem('comp', Paraboloid(), promotes=['*'])
prob.model.add_subsystem('execcomp', om.ExecComp('z = x + y'), promotes=['*'])
prob.model.set_input_defaults('x', val=1.0)
prob.model.set_input_defaults('y', val=1.0)

samples = [
    {'x': {'val': 0.}, 'y': {'val': 0.}},
    {'x': {'val': .5}, 'y': {'val': 0.25}},
    {'x': {'val': 1.}, 'y': {'val': 0.125}},
]

prob.driver = om.AnalysisDriver(samples=samples)
prob.driver.add_recorder(om.SqliteRecorder('cases.sql'))

prob.driver.add_responses(('f_xy', 'z'))

prob.setup()
prob.run_driver()
prob.cleanup()

cr = om.CaseReader(str(prob.get_outputs_dir() / "cases.sql"))
cases = cr.get_cases(source='driver')

print(f'{"x":^6}     {"y":^6}     {"f_xy":^6}     {"z":^6}')
for case in cases:
    x = case.get_val('x')[0]
    y = case.get_val('y')[0]
    f_xy = case.get_val('f_xy')[0]
    z = case.get_val('z')[0]
    print(f'{x:6.3f}     {y:6.3f}     {f_xy:6.3f}     {z:6.3f}')
  x          y         f_xy        z   
 0.000      0.000     22.000      0.000
 0.500      0.250     21.438      0.750
 1.000      0.125     18.141      1.125

How does AnalysisDriver compare to DOEDriver?#

AnalysisDriver is intended by be a generalization of DOEDriver that is more capable.

DOEDriver was written for a more optimization-centric mindset. By default, it would only perturb variables marked as design variables of the Problem.

AnalysisDriver takes sample values for any input in the model, as well as implicit outputs. Setting the value of an explicit output in the model would have no effect, as the value would be overwritten upon the execution of the model.

Unlike DOEDriver, it also allows units and indices to be specified for the variables in each sample. Essentially, any argument to set_val can be provided as part of the sample data.

DOEDriver also realizes all of the design points to be analyized, while AnalysisDriver’s generators do this in a lazy way, which could potentially save some memory when running large data sets.

Generating Cases#

Creating a sequence of more than a handful of sample cases can be daunting as the number of model inputs increases. In addition, exceedingly large numbers of cases for large models may begin to tax memory resources if they are all kept in memory. To alleviate these issues, samples for AnalysisDriver can be provided by a lazy pythonic generator descended from an AnalysisGenerator.

There are currently three types of AnalysisGenerator included with OpenMDAO

  • ZipGenerator

  • ProductGenerator

  • CSVGenerator

ZipGenerator#

ZipGenerator takes a dictionary that is similar to those given as a sample, except it provids all values to be assumed for each variable. It then uses python’s zip function to create a sample for each value specified.

For instance, the following ZipGenerator will generate three sample cases:

gen = om.ZipGenerator({'x': {'val': [0.0, 1.0, 2.0], 'units': None},
                       'y': {'val': [4.0, 5.0, 6.0], 'units': None}})

for i, sample in enumerate(gen):
    print(f'{i}:', sample)
0: {'x': {'val': 0.0, 'units': None, 'indices': None}, 'y': {'val': 4.0, 'units': None, 'indices': None}}
1: {'x': {'val': 1.0, 'units': None, 'indices': None}, 'y': {'val': 5.0, 'units': None, 'indices': None}}
2: {'x': {'val': 2.0, 'units': None, 'indices': None}, 'y': {'val': 6.0, 'units': None, 'indices': None}}

Note that the number of values given for each variable to be sampled must be the same.

ProductGenerator#

ProductGenerator, like ZipGenerator, takes a dictionary that provides values and optionally units and indices for each variable to be sampled. It uses Python’s itertools.product to produce every possible combination of the values of sampled variables.

For instance, the following example will generate 12 sample cases:

gen = om.ProductGenerator({'x': {'val': [0.0, 1.0, 2.0], 'units': None},
                           'y': {'val': [4.0, 5.0, 6.0, 7.0], 'units': None}})

for i, sample in enumerate(gen):
    print(f'{i}:', sample)
0: {'x': {'val': 0.0, 'units': None, 'indices': None}, 'y': {'val': 4.0, 'units': None, 'indices': None}}
1: {'x': {'val': 0.0, 'units': None, 'indices': None}, 'y': {'val': 5.0, 'units': None, 'indices': None}}
2: {'x': {'val': 0.0, 'units': None, 'indices': None}, 'y': {'val': 6.0, 'units': None, 'indices': None}}
3: {'x': {'val': 0.0, 'units': None, 'indices': None}, 'y': {'val': 7.0, 'units': None, 'indices': None}}
4: {'x': {'val': 1.0, 'units': None, 'indices': None}, 'y': {'val': 4.0, 'units': None, 'indices': None}}
5: {'x': {'val': 1.0, 'units': None, 'indices': None}, 'y': {'val': 5.0, 'units': None, 'indices': None}}
6: {'x': {'val': 1.0, 'units': None, 'indices': None}, 'y': {'val': 6.0, 'units': None, 'indices': None}}
7: {'x': {'val': 1.0, 'units': None, 'indices': None}, 'y': {'val': 7.0, 'units': None, 'indices': None}}
8: {'x': {'val': 2.0, 'units': None, 'indices': None}, 'y': {'val': 4.0, 'units': None, 'indices': None}}
9: {'x': {'val': 2.0, 'units': None, 'indices': None}, 'y': {'val': 5.0, 'units': None, 'indices': None}}
10: {'x': {'val': 2.0, 'units': None, 'indices': None}, 'y': {'val': 6.0, 'units': None, 'indices': None}}
11: {'x': {'val': 2.0, 'units': None, 'indices': None}, 'y': {'val': 7.0, 'units': None, 'indices': None}}

Unlike ZipGenerator, each variable can have a different number of sample values.

CSVGenerator#

CSVGenerator provides the ability to read cases from a comma-separated-values file. That is, this allows the user to create a table of cases to be run in a spreadsheet and then execute those cases via the AnalysisDriver.

The first row of the CSV file should provide the promoted names of the variables to be sampled. The second row may optionally provide units for each variable to be sampled. The next row may optionally provide the indices to be set for each variable to be sampled. Following that, the remainder of rows provide values for each sampled variable.

That is, the following csv file would provide the same nine cases as the ProductGenerator above. The second line contains the units of x and y. The third line provides the indices being set. These metadata values are unnecessary in this case but provided for illustration.

x, y
None, None
[0], [0]
0.0, 4.0
0.0, 5.0
0.0, 6.0
1.0, 4.0
1.0, 5.0
1.0, 6.0
2.0, 4.0
2.0, 5.0
2.0, 6.0

Running AnalysisDriver in Parallel#

To speed execution, AnalysisDriver can execute samples in parallel when multiple processors are available. This is done by setting the run_parallel option to True as shown in the following example and running your script using MPI.

Note

This feature requires MPI, and may not be able to be run on Colab or Binder.

%%px

import openmdao.api as om
from openmdao.test_suite.components.paraboloid import Paraboloid
from openmdao.utils.mpi import MPI

samples_3x3 = [
    {'x': {'val': 0.}, 'y': {'val': 0.}},
    {'x': {'val': .5}, 'y': {'val': 0.}},
    {'x': {'val': 1.}, 'y': {'val': 0.}},

    {'x': {'val': 0.}, 'y': {'val': 0.5}},
    {'x': {'val': .5}, 'y': {'val': 0.5}},
    {'x': {'val': 1.}, 'y': {'val': 0.5}},

    {'x': {'val': 0.}, 'y': {'val': 1.}},
    {'x': {'val': .5}, 'y': {'val': 1.}},
    {'x': {'val': 1.}, 'y': {'val': 1.}},
]

rank = MPI.COMM_WORLD.rank

prob = om.Problem()

prob.model.add_subsystem('comp', Paraboloid(), promotes=['*'])

prob.driver = om.AnalysisDriver(samples=samples_3x3, run_parallel=True)
prob.driver.add_recorder(om.SqliteRecorder('cases.sql'))

prob.driver.add_response('f_xy', units=None, indices=[0])

prob.setup()
prob.run_driver()
prob.cleanup()

cr = om.CaseReader(str(prob.get_outputs_dir() / f'cases.sql_{rank}'))
cases = cr.get_cases(source='driver')

print(f'Recorded {len(cases)} cases on proc {rank}.')

print(f'{"x":^6}     {"y":^6}     {"f_xy":^6}')
for case in cases:
    x = case.get_val('x')[0]
    y = case.get_val('y')[0]
    f_xy = case.get_val('f_xy')[0]
    print(f'{x:6.3f}     {y:6.3f}     {f_xy:6.3f}')
[stdout:2] Note: SqliteRecorder is running on multiple processors. Cases from rank 2 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem_out/cases.sql_2.
Recorded 2 cases on proc 2.
  x          y         f_xy 
 1.000      0.000     17.000
 0.000      1.000     31.000
[stdout:3] Note: SqliteRecorder is running on multiple processors. Cases from rank 3 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem_out/cases.sql_3.
Recorded 2 cases on proc 3.
  x          y         f_xy 
 0.000      0.500     26.250
 0.500      1.000     28.750
[stdout:1] Note: SqliteRecorder is running on multiple processors. Cases from rank 1 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem_out/cases.sql_1.
Recorded 2 cases on proc 1.
  x          y         f_xy 
 0.500      0.000     19.250
 1.000      0.500     21.750
[stdout:0] Note: SqliteRecorder is running on multiple processors. Cases from rank 0 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem_out/cases.sql_0.
Note: Metadata is being recorded separately as /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem_out/cases.sql_meta.
Recorded 3 cases on proc 0.
  x          y         f_xy 
 0.000      0.000     22.000
 0.500      0.500     23.750
 1.000      1.000     27.000

Running MPI-enabled models in parallel with AnalysisDriver.#

If the model being executed supports parallelization, the user should also specify procs_per_model to dictate how many processor cores each model instance uses.

The following example runs a model that can run two systems simultaneously c1 and c2 within a ParallelGroup. In this case, each model can utilize two processors (procs_per_model = 2).

When providing the number of processors via the mpirun -n {num_procs} python myscript.py, the total number of processors must be a multiple of procs_per_model.

%%px

import numpy as np
import openmdao.api as om

class FanInGrouped(om.Group):
    """
    Topology where two components in a Group feed a single component
    outside of that Group.
    """

    def __init__(self):
        super().__init__()

        self.set_input_defaults('x1', 1.0)
        self.set_input_defaults('x2', 1.0)

        self.sub = self.add_subsystem('sub', om.ParallelGroup(),
                                      promotes_inputs=['x1', 'x2'])

        self.sub.add_subsystem('c1', om.ExecComp(['y=-2.0*x']),
                               promotes_inputs=[('x', 'x1')])
        self.sub.add_subsystem('c2', om.ExecComp(['y=5.0*x']),
                               promotes_inputs=[('x', 'x2')])

        self.add_subsystem('c3', om.ExecComp(['y=3.0*x1+7.0*x2']))

        self.connect("sub.c1.y", "c3.x1")
        self.connect("sub.c2.y", "c3.x2")

prob = om.Problem(FanInGrouped())

# Note the absense of adding design varaibles here, compared to DOEGenerator

prob.driver = om.AnalysisDriver(om.ProductGenerator({'x1': {'val': np.linspace(0.0, 1.0, 10)},
                                                     'x2': {'val': np.linspace(0.0, 1.0, 10)}}))

prob.driver.add_recorder(om.SqliteRecorder("cases.sql"))

prob.driver.add_response('c3.y')

# # the FanInGrouped model uses 2 processes, so we can run
# # two instances of the model at a time, each using 2 of our 4 procs
prob.driver.options['run_parallel'] = True
prob.driver.options['procs_per_model'] = procs_per_model = 2

prob.setup()
prob.run_driver()
prob.cleanup()

# a separate case file will be written by rank 0 of each parallel model
# (the top two global ranks)
rank = prob.comm.rank

num_models = prob.comm.size // procs_per_model

if rank < num_models:
    filename = f'cases.sql_{rank}'

    cr = om.CaseReader(prob.get_outputs_dir() / filename)
    cases = cr.list_cases('driver', out_stream=None)

    values = []
    for case in cases:
        outputs = cr.get_case(case).outputs
        values.append((outputs['x1'], outputs['x2'], outputs['c3.y']))

    print("\n"+"\n".join(["x1: %5.2f, x2: %5.2f, c3.y: %6.2f" % (x1, x2, y) for x1, x2, y in values]))
[stdout:0] Note: SqliteRecorder is running on multiple processors. Cases from rank 0 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem2_out/cases.sql_0.
Note: Metadata is being recorded separately as /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem2_out/cases.sql_meta.

x1:  0.00, x2:  0.00, c3.y:   0.00
x1:  0.00, x2:  0.22, c3.y:   7.78
x1:  0.00, x2:  0.44, c3.y:  15.56
x1:  0.00, x2:  0.67, c3.y:  23.33
x1:  0.00, x2:  0.89, c3.y:  31.11
x1:  0.11, x2:  0.00, c3.y:  -0.67
x1:  0.11, x2:  0.22, c3.y:   7.11
x1:  0.11, x2:  0.44, c3.y:  14.89
x1:  0.11, x2:  0.67, c3.y:  22.67
x1:  0.11, x2:  0.89, c3.y:  30.44
x1:  0.22, x2:  0.00, c3.y:  -1.33
x1:  0.22, x2:  0.22, c3.y:   6.44
x1:  0.22, x2:  0.44, c3.y:  14.22
x1:  0.22, x2:  0.67, c3.y:  22.00
x1:  0.22, x2:  0.89, c3.y:  29.78
x1:  0.33, x2:  0.00, c3.y:  -2.00
x1:  0.33, x2:  0.22, c3.y:   5.78
x1:  0.33, x2:  0.44, c3.y:  13.56
x1:  0.33, x2:  0.67, c3.y:  21.33
x1:  0.33, x2:  0.89, c3.y:  29.11
x1:  0.44, x2:  0.00, c3.y:  -2.67
x1:  0.44, x2:  0.22, c3.y:   5.11
x1:  0.44, x2:  0.44, c3.y:  12.89
x1:  0.44, x2:  0.67, c3.y:  20.67
x1:  0.44, x2:  0.89, c3.y:  28.44
x1:  0.56, x2:  0.00, c3.y:  -3.33
x1:  0.56, x2:  0.22, c3.y:   4.44
x1:  0.56, x2:  0.44, c3.y:  12.22
x1:  0.56, x2:  0.67, c3.y:  20.00
x1:  0.56, x2:  0.89, c3.y:  27.78
x1:  0.67, x2:  0.00, c3.y:  -4.00
x1:  0.67, x2:  0.22, c3.y:   3.78
x1:  0.67, x2:  0.44, c3.y:  11.56
x1:  0.67, x2:  0.67, c3.y:  19.33
x1:  0.67, x2:  0.89, c3.y:  27.11
x1:  0.78, x2:  0.00, c3.y:  -4.67
x1:  0.78, x2:  0.22, c3.y:   3.11
x1:  0.78, x2:  0.44, c3.y:  10.89
x1:  0.78, x2:  0.67, c3.y:  18.67
x1:  0.78, x2:  0.89, c3.y:  26.44
x1:  0.89, x2:  0.00, c3.y:  -5.33
x1:  0.89, x2:  0.22, c3.y:   2.44
x1:  0.89, x2:  0.44, c3.y:  10.22
x1:  0.89, x2:  0.67, c3.y:  18.00
x1:  0.89, x2:  0.89, c3.y:  25.78
x1:  1.00, x2:  0.00, c3.y:  -6.00
x1:  1.00, x2:  0.22, c3.y:   1.78
x1:  1.00, x2:  0.44, c3.y:   9.56
x1:  1.00, x2:  0.67, c3.y:  17.33
x1:  1.00, x2:  0.89, c3.y:  25.11
[stdout:1] Note: SqliteRecorder is running on multiple processors. Cases from rank 1 are being written to /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/problem2_out/cases.sql_1.

x1:  0.00, x2:  0.11, c3.y:   3.89
x1:  0.00, x2:  0.33, c3.y:  11.67
x1:  0.00, x2:  0.56, c3.y:  19.44
x1:  0.00, x2:  0.78, c3.y:  27.22
x1:  0.00, x2:  1.00, c3.y:  35.00
x1:  0.11, x2:  0.11, c3.y:   3.22
x1:  0.11, x2:  0.33, c3.y:  11.00
x1:  0.11, x2:  0.56, c3.y:  18.78
x1:  0.11, x2:  0.78, c3.y:  26.56
x1:  0.11, x2:  1.00, c3.y:  34.33
x1:  0.22, x2:  0.11, c3.y:   2.56
x1:  0.22, x2:  0.33, c3.y:  10.33
x1:  0.22, x2:  0.56, c3.y:  18.11
x1:  0.22, x2:  0.78, c3.y:  25.89
x1:  0.22, x2:  1.00, c3.y:  33.67
x1:  0.33, x2:  0.11, c3.y:   1.89
x1:  0.33, x2:  0.33, c3.y:   9.67
x1:  0.33, x2:  0.56, c3.y:  17.44
x1:  0.33, x2:  0.78, c3.y:  25.22
x1:  0.33, x2:  1.00, c3.y:  33.00
x1:  0.44, x2:  0.11, c3.y:   1.22
x1:  0.44, x2:  0.33, c3.y:   9.00
x1:  0.44, x2:  0.56, c3.y:  16.78
x1:  0.44, x2:  0.78, c3.y:  24.56
x1:  0.44, x2:  1.00, c3.y:  32.33
x1:  0.56, x2:  0.11, c3.y:   0.56
x1:  0.56, x2:  0.33, c3.y:   8.33
x1:  0.56, x2:  0.56, c3.y:  16.11
x1:  0.56, x2:  0.78, c3.y:  23.89
x1:  0.56, x2:  1.00, c3.y:  31.67
x1:  0.67, x2:  0.11, c3.y:  -0.11
x1:  0.67, x2:  0.33, c3.y:   7.67
x1:  0.67, x2:  0.56, c3.y:  15.44
x1:  0.67, x2:  0.78, c3.y:  23.22
x1:  0.67, x2:  1.00, c3.y:  31.00
x1:  0.78, x2:  0.11, c3.y:  -0.78
x1:  0.78, x2:  0.33, c3.y:   7.00
x1:  0.78, x2:  0.56, c3.y:  14.78
x1:  0.78, x2:  0.78, c3.y:  22.56
x1:  0.78, x2:  1.00, c3.y:  30.33
x1:  0.89, x2:  0.11, c3.y:  -1.44
x1:  0.89, x2:  0.33, c3.y:   6.33
x1:  0.89, x2:  0.56, c3.y:  14.11
x1:  0.89, x2:  0.78, c3.y:  21.89
x1:  0.89, x2:  1.00, c3.y:  29.67
x1:  1.00, x2:  0.11, c3.y:  -2.11
x1:  1.00, x2:  0.33, c3.y:   5.67
x1:  1.00, x2:  0.56, c3.y:  13.44
x1:  1.00, x2:  0.78, c3.y:  21.22
x1:  1.00, x2:  1.00, c3.y:  29.00
[stderr:1] /tmp/ipykernel_17483/2439004990.py:66: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)
  print("\n"+"\n".join(["x1: %5.2f, x2: %5.2f, c3.y: %6.2f" % (x1, x2, y) for x1, x2, y in values]))
[stderr:0] /tmp/ipykernel_17482/2439004990.py:66: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)
  print("\n"+"\n".join(["x1: %5.2f, x2: %5.2f, c3.y: %6.2f" % (x1, x2, y) for x1, x2, y in values]))