CoSAppLogo CoSApp examples

Replacing a sub-system dynamically

Utility function cosapp.utils.swap_system allows one to replace on the fly an existing sub-system by another System instance.

[1]:
from cosapp.utils import swap_system

help(swap_system)
Help on function swap_system in module cosapp.utils.swap_system:

swap_system(old_system: 'System', new_system: 'System', init_values=True)
    Replace `old_system` by `new_system`.

    Parameters
    ----------
    - old_system [System]: System to replace
    - new_system [System]: Replacement system
    - init_values [bool, optional]: If `True` (default), original
        system values are copied into the replacement system.

    Returns
    -------
    old_system [System]: the original system, devoid of parent.

Example

[2]:
from cosapp.base import System
from cosapp.drivers import NonLinearSolver


class NominalComponent(System):
    def setup(self):
        self.add_inward('a', 1.0)
        self.add_inward('x', 1.0)
        self.add_outward('y', 0.0)

    def compute(self) -> None:
        self.y = self.a * self.x**2 - 1


class DegradedComponent(System):
    def setup(self):
        self.add_inward('a', 1.0)
        self.add_inward('x', 1.0)
        self.add_outward('y', 0.0)

    def compute(self) -> None:
        self.y = self.x - self.a


class CompositeSystem(System):

    def setup(self):
        a = self.add_child(NominalComponent('a'), pulling='x')
        b = self.add_child(NominalComponent('b'), pulling='y')

        self.connect(a, b, {'y': 'x'})  # a.y -> b.x

[3]:
from cosapp.utils import swap_system

head = CompositeSystem('head')

solver = head.add_driver(NonLinearSolver('solver'))
solver.add_unknown('x', max_abs_step=0.25).add_equation('y == 0')

head.run_drivers()

print(
    "Original config:\n",
    solver.problem,
    sep="\n",
)

Original config:

Unknowns [1]
  x = 1.414213562373095
Equations [1]
  y == 0 := -8.881784197001252e-16

Next, we swap head.a with a newly created system of type DegradedComponent, and retrieve the original sub-system as original_a. After the replacement, original_a is a parentless, stand-alone system.

[4]:
original_a = swap_system(head.a, DegradedComponent('a'))

# Checks
print(
    f"{head.a.parent = }",
    f"{original_a.parent = }",
    f"{type(head.a) = }",
    sep="\n",
)
head.a.parent = head - CompositeSystem
original_a.parent = None
type(head.a) = <class '__main__.DegradedComponent'>

In the process, existing connectors within the parent system are maintained:

[5]:
head.connectors()
[5]:
{'b.outwards -> outwards': Connector(head.outwards <- b.outwards, ['y']),
 'a.outwards -> b.inwards': Connector(b.inwards <- a.outwards, {'x': 'y'}),
 'inwards -> a.inwards': Connector(a.inwards <- head.inwards, ['x'])}

If we rerun the model, we can see that the mathematical problem is maintained. However, the obtained solution differs from the previous one, since the overall behaviour of system head has changed.

[6]:
# Re-run;
head.run_drivers()

print(
    "Modified config:\n",
    solver.problem,
    sep="\n",
)
Modified config:

Unknowns [1]
  x = 2.0
Equations [1]
  y == 0 := 0.0

We can revert to the original configuration, by re-swapping current head.a with previously stored object original_a:

[7]:
# Revert to original sub-system
swap_system(head.a, original_a)

head.run_drivers()

print(
    "Recovered config:\n",
    solver.problem,
    sep="\n",
)
Recovered config:

Unknowns [1]
  x = 1.4142135623730954
Equations [1]
  y == 0 := 1.7763568394002505e-15

Function swap_system can be useful in the context of an event-driven transition, for instance (see tutorial on discrete events):

[8]:
from cosapp.base import System
from cosapp.utils import swap_system


class ThreasholdSystem(System):

    def setup(self):
        a = self.add_child(NominalComponent('a'), pulling='x')
        b = self.add_child(NominalComponent('b'), pulling='y')

        self.connect(a, b, {'y': 'x'})

        self.add_inward('y_max', 3.14)
        self.add_event('failure', trigger="y > y_max")
        self.add_event('recovery', trigger="y < y_max")

    def transition(self):
        if self.failure.present:
            swap_system(self.a, DegradedComponent('a'))

        if self.recovery.present:
            swap_system(self.a, NominalComponent('a'))

Ground rules

To avoid undesired side-effects, the substitute system (second argument) must not be part of an existing system tree (i.e. its parent should be None):

[9]:
head1 = CompositeSystem('head1')
head2 = CompositeSystem('head2')

def print_exception(error: Exception):
    print(f"{type(error).__name__}: {error!s}")

try:
    swap_system(head1.a, head2.b)

except Exception as error:
    print_exception(error)
ValueError: System 'head2.b' already belongs to a system tree.

Oppositely, the swapped system (first argument) must be the child of a higher-level system:

[10]:
try:
    swap_system(head1, CompositeSystem('new'))

except Exception as error:
    print_exception(error)
ValueError: Cannot replace top system 'head1'.