Design Methods

Design methods allow component designers to identify, from expert knowledge, the different ways users can design a component from functional requirements.

Declaring a design method in a system

Design methods are declared at System setup, using System.add_design_method. This class method takes the name of the design method as single argument; it will create a new entry in an internal dictionary of MathematicalProblem objects, mapped to their names.

Such objects bear unknowns and equations, declared with methods add_unkown and add_equation:

class MySystem(System):

    def setup(self):
        self.add_inward('x', 1.0)
        self.add_outward('y', 0.0)

        design = self.add_design_method('design_x')     # create problem `design`, and store it with key 'design_x'
        design.add_unknown('x').add_equation('y == 0')  # define problem by declaring unknowns and equations

    def compute(self):
        self.y = self.x**2 - 3

In practice, design methods are mathematical problems that can be activated on demand.

Example

[1]:
from cosapp.base import System, Port

class XPort(Port):
    def setup(self):
        self.add_variable("x", 1.0)

class MultiplyWithDesignMethod(System):

    def setup(self):
        self.add_input(XPort, 'p_in', {'x': 1.})
        self.add_output(XPort, 'p_out', {'x': 1.})

        self.add_inward('K1', 5.)

        # off-design constraints
        self.add_inward('expected_output', 1.0)
        self.add_unknown('p_in.x').add_equation('p_out.x == expected_output')

        # design methods
        self.add_inward('dx_design', 10.)
        self.add_design_method('dx').add_unknown('K1').add_equation('p_out.x - p_in.x == dx_design')

    def compute(self):
        self.p_out.x = self.p_in.x * self.K1

Class MultiplyWithDesignMethod defines two types of mathematical problems, through add_unknown and add_equation:

  1. Unknowns and equations declared directly on the system (that is self.add_unknown and self.add_equation in system setup) are always enforced, for all instances of the class. They are referred to as the off-design problem of the class. Composite systems automatically collect the off-design problems of their sub-systems.

  2. Unknowns and equations declared within a design method define a design problem, which may or may not be activated.

Solving the off-design problem

[2]:
from cosapp.drivers import NonLinearSolver

m = MultiplyWithDesignMethod('m')
# Add solver
solver = m.add_driver(NonLinearSolver('solver', tol=1e-12))

m.K1 = 5
m.expected_output = 7.5
m.run_drivers()

print(
    "Off-design problem:",
    solver.problem,
    "",
    "Result:",
    f"{  m.K1 = }",
    f"{  m.p_in.x = }",
    f"{  m.p_out.x = }",
    sep="\n",
)
Off-design problem:
Unknowns [1]
  p_in.x = 1.5
Equations [1]
  p_out.x == expected_output := 0.0

Result:
  m.K1 = 5
  m.p_in.x = 1.5
  m.p_out.x = 7.5

Activating a design method

Design methods are activated by extending an existing mathematical problem with the predefined design method. In the example below, a single-point design case is created using design method 'dx' of the system of interest.

[3]:
from cosapp.drivers import NonLinearSolver, RunSingleCase

m = MultiplyWithDesignMethod('m')

solver = m.add_driver(NonLinearSolver('solver', tol=1e-12))

# Add design point
case = solver.add_child(RunSingleCase('case'))

# Define case conditions
case.set_values({
    'expected_output': 7.5,
    'dx_design': 5.0,
})

case.design.extend(m.design_methods['dx'])  # activate design method 'dx' of system `m`

m.run_drivers()

print(
    "Design problem:",
    solver.problem,
    "",
    "Result:",
    f"  {m.K1 = }",
    f"  {m.p_in.x = }",
    f"  {m.p_out.x = }",
    sep="\n",
)
Design problem:
Unknowns [2]
  K1 = 3.0
  p_in.x = 2.5
Equations [2]
  p_out.x - p_in.x == dx_design := 0.0
  p_out.x == expected_output := 0.0

Result:
  m.K1 = 3.0
  m.p_in.x = 2.5
  m.p_out.x = 7.5

Promoting sub-system design methods at parent level

Composite systems can take advantage of design methods defined for their sub-systems, and thus construct composite design methods.

class CompositeSystem(System):

    def setup(self):
        a = self.add_child(ComponentA('a'))
        b = self.add_child(ComponentB('b'))

        design = self.add_design_method('design')
        design.extend(a.design_methods['design_this'])
        design.extend(b.design_methods['design_that'])

Write dynamic design problems as system methods

As mentioned earlier, a design method is a predefined mathematical problem, stored in dictionary design_methods, to be used in design problems. The main limitation is that design methods defined at setup are static, that is defined once and for all.

For advanced uses, one may want to create mathematical problems dynamically, with optional parameters, say. This can typically be achieved by writing dedicated object-bound methods creating said problem on-the-fly, using custom arguments. A convenient function, System.new_problem, may be used to this end:

[4]:
import math
from cosapp.base import System


class SystemWithDynamicDesignMethod(System):

    def setup(self):
        self.add_inward('x', 1.0)
        self.add_outward('y', 0.0)

    def compute(self):
        self.y = math.sin(self.x**2 - 2)

    def design_x(self, **options):
        """Compute `x` such that `y == 0`.
        Additional options apply to unknown `x`.
        """
        problem = self.new_problem()
        problem.add_unknown('x', **options)
        problem.add_equation('y == 0')
        return problem

First simulation, with no constraint on unknown x:

[5]:
from cosapp.drivers import NonLinearSolver

s = SystemWithDynamicDesignMethod('s')

solver = s.add_driver(NonLinearSolver('solver'))

solver.extend(s.design_x())

s.x = 0.7  # initial value
s.run_drivers()

print(
    f"{s.x = }",
    f"{s.y = }",
    f"Converged in {solver.results.fres_calls} iterations",
    sep="\n",
)
s.x = 12.612677458790806
s.y = 2.9403902792268246e-14
Converged in 5 iterations

Second try, imposing a maximum step on x at each solver iteration:

[6]:
s = SystemWithDynamicDesignMethod('s')

solver = s.add_driver(NonLinearSolver('solver'))

solver.extend(s.design_x(max_abs_step=0.1))

s.x = 0.7  # initial value
s.run_drivers()

print(
    f"{s.x = }",
    f"{s.y = }",
    f"Converged in {solver.results.fres_calls} iterations",
    sep="\n",
)
s.x = 1.4142135623730951
s.y = 4.440892098500626e-16
Converged in 11 iterations

Show information on design method:

[7]:
help(s.design_x)
Help on method design_x in module __main__:

design_x(**options) method of __main__.SystemWithDynamicDesignMethod instance
    Compute `x` such that `y == 0`.
    Additional options apply to unknown `x`.

Congrats! You are now ready to update your System into a design model with CoSApp!