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, RunSingleCase

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, sep="\n")

print(
    "Off-design result:",
    f"m.K1 = {m.K1}",
    f"m.p_in.x = {m.p_in.x}",
    f"m.p_out.x = {m.p_out.x}",
    sep="\n  ",
)
Off-design problem:
Unknowns
  p_in.x = 1.5
Equations
  p_out.x == expected_output := 0.0
Off-design 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, sep="\n")

print(
    "Design result:",
    f"m.K1 = {m.K1}",
    f"m.p_in.x = {m.p_in.x}",
    f"m.p_out.x = {m.p_out.x}",
    sep="\n  ",
)
Design problem:
Unknowns
  K1 = 3.0
  p_in.x = 2.5
Equations
  p_out.x - p_in.x == dx_design := 0.0
  p_out.x == expected_output := 0.0
Design 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):
        """Design method for `x`.
        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"x = {s.x}",
    f"y = {s.y}",
    sep="\n",
)
x = 12.612677458790806
y = 2.9403902792268246e-14

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"x = {s.x}",
    f"y = {s.y}",
    sep="\n",
)
x = 1.4142135623730951
y = 4.440892098500626e-16

Show information on design method:

[7]:
s.design_x?

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

[ ]: