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('find_root')    # create problem `design`, and store it with key 'find_root'
        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.0})
        self.add_output(XPort, 'p_out', {'x': 1.0})

        self.add_inward('K1', 5.0)

        # intrinsic 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_design_method('dx').add_unknown('K1').add_equation('p_out.x - p_in.x == 5')

    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 in the system definition (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 intrinsic problem of the class. Composite systems automatically collect the intrinsic 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 intrinsic problem

[2]:
from cosapp.drivers import NonLinearSolver

# Create system
m = MultiplyWithDesignMethod('m')

# Add solver
solver = m.add_driver(NonLinearSolver('solver'))

# Initialize & solve system
m.K1 = 5.0
m.expected_output = 7.5
m.run_drivers()

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

Result:
  m.K1 = 5.0
  m.p_in.x = np.float64(1.5)
  m.p_out.x = np.float64(7.5)

Activating a design method

Design methods are activated by extending an existing mathematical problem with the predefined design method.

[3]:
from cosapp.drivers import NonLinearSolver

m = MultiplyWithDesignMethod('m')

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

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

# Initialize & solve system
m.K1 = 5.0
m.expected_output = 7.5
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 == 5 := 0.0
  p_out.x == expected_output := 0.0

Result:
  m.K1 = np.float64(3.0)
  m.p_in.x = np.float64(2.5)
  m.p_out.x = np.float64(7.5)

Reusing 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.

Aggregation of design problems by extension

In the example below, design method “design” merges design constraints from sub-systems a and b:

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'])

Promotion of sub-system design methods

One can also use method System.pull_design_method to promote sub-system design methods at parent level. Arguments are (1) the sub-system (or list thereof) from which design methods are promoted, and (2) the list of design method names, or a name mapping, if required (rules for variable pulling and connections apply - see tutorial on Systems).

In the example below, design method “design_x”, merging design problems from sub-systems a and b, is created at parent level. Additionaly, design method “design_y” of sub-system a is exposed at parent level as “design_foo”.

class CompositeSystem(System):

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

        self.pull_design_method([a, b], 'design_x')
        self.pull_design_method(a, {'design_y': 'design_foo'})

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
from cosapp.core import MathematicalProblem


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 find_root(self, **options) -> MathematicalProblem:
        """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.find_root())

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 = np.float64(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.find_root(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 = np.float64(1.4142135623730951)
s.y = 4.440892098500626e-16
Converged in 11 iterations

Show information on design method:

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

find_root(**options) -> cosapp.core.numerics.basics.MathematicalProblem 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!