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")
Option | Default | Acceptable Values | Acceptable Types | Description |
---|---|---|---|---|
batch_size | 1000 | N/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_behavior | warn | ['warn', 'raise', 'ignore'] | N/A | Behavior 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_model | 1 | N/A | ['int'] | Number of processors to give each model under MPI. |
run_parallel | False | [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]))