BeeperTeeper
BeeperTeeper

Reputation: 161

Is it possible to group ExplicitComponents inside an ExplicitComponent?

I have an ExplicitComponent, 'CSTSurface', which takes some weights and generates a curve. I'd like to group two of these together to generate a two-surface aerofoil ('CSTAerofoil').

Currently, I use a group with aliased promoted variables. I'd rather use a component that just took one vector of input weights (like an ExplicitComponent) and then dispatched them to two CSTSurface components.

Is this possible? What would be the best way to achieve this?

My code (excuse the lazy 'fd' for now!):

import openmdao.api as om
import numpy as np
import scipy.special as scsp
from types import FunctionType


class CSTSurface(om.ExplicitComponent):

    def initialize(self):
        self.options.declare('nBP', types=int)
        self.options.declare('nPoints', types=int)
        self.options.declare('classFunc', default=lambda x: (x**0.5)*(1-x), types=FunctionType, recordable=False)  # A little experimental, allow class functions to be passed as an option?

    def setup(self):
        nPoints = self.options['nPoints']
        nBP = self.options['nBP']

        self.add_input('xArray', shape=nPoints)
        self.add_input('weights', shape=nBP)  # Must be a row vector for multiplication to work right.

        self.add_output('surfaceArray', shape=nPoints)

        self.declare_partials('surfaceArray', ['xArray', 'weights'], method='fd')

    def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
        x = inputs['xArray']
        weights = np.append(inputs['weights'], [0.0, 0.0])
        C = self.options['classFunc']
        nBP = self.options['nBP']
        nPoints = self.options['nPoints']

        # Build bezier matrix
        B = np.zeros((nPoints, nBP + 2))
        for r in range(nBP):
            S = scsp.binom(nBP, r)*(x**r)*((1-x)**(nBP-r))  # Shape function
            B[:, r] = C(x)*S
        B[:, nBP] = x  # Trailing edge thickness terms
        B[:, nBP+1] = x*((1-x)**0.5)*((1-x)**nBP)  # Leading edge modification
        # Multiply by weights
        B = B * weights
        # Sum each row to get points
        surfaceArray = np.sum(B, axis=1)

        outputs['surfaceArray'] = surfaceArray


class CSTAerofoil(om.Group):

    def initialize(self):
        self.options.declare('nBPUpper', types=int)
        self.options.declare('nBPLower', types=int)
        self.options.declare('nPoints', types=int)
        self.options.declare('classFunc', default=lambda x: (x**0.5)*(1-x), types=FunctionType, recordable=False)  # A little experimental, allow class functions to be passed as an option?

    def setup(self):
        nPoints = self.options['nPoints']
        nBPUpper = self.options['nBPUpper']
        nBPLower = self.options['nBPLower']
        C = self.options['classFunc']

        self.add_subsystem('upperCSTSurface', CSTSurface(nBP=nBPUpper, nPoints=nPoints, classFunc=C),
                           promotes_inputs=[('weights', 'upperWeights'), 'xArray'],
                           promotes_outputs=[('surfaceArray', 'upperSurfaceArray')])
        self.add_subsystem('lowerCSTSurface', CSTSurface(nBP=nBPLower, nPoints=nPoints, classFunc=C),
                           promotes_inputs=[('weights', 'lowerWeights'), 'xArray'],
                           promotes_outputs=[('surfaceArray', 'lowerSurfaceArray')])

Upvotes: 0

Views: 80

Answers (1)

Justin Gray
Justin Gray

Reputation: 5710

When you start to learn how to build things in OpenMDAO, it is sometimes tempting to try and use the OpenMDAO structure for everything. For example, in OpenMDAO the component represents the smallest unit of computational work, and you can stamp out multiple copies as needed. Then groups allow you to organize those copies as you need. This what you did, but as you noted it wasn't exactly how you wanted to structure you wanted out of your component, so you wonder about nesting components within themselves.

It is not technically impossible to nest component instances within the compute of other component instances, but I would not recommend it. You'd have to do a bunch of work to construct extra dictionaries and pass them around. It's just not worth it.

Instead, we can revert to a more basic object composition design using a basic a python class with methods (i.e. compute and compute_partials) that uses normal arguments. The helper class can handle the CST calculations, then a component has two instances of it that pull from the weights array as needed.

Something like this:

import openmdao.api as om
import numpy as np
import scipy.special as scsp
from types import FunctionType


class CSTSurface():

    def __init__(self, n_Bp, n_points, class_func):
       self.n_Bp = n_Bp
       self.n_points = n_points 
       self.class_func = class_func

    def compute_surface(self, x_array, weights):
        x = x_array
        weights = np.append(weights, [0.0, 0.0])
        C = self.class_func
        n_BP = self.n_Bp
        n_points = self.n_points

        # Build bezier matrix
        B = np.zeros((n_points, n_BP + 2))
        for r in range(n_BP):
            S = scsp.binom(n_BP, r)*(x**r)*((1-x)**(n_BP-r))  # Shape function
            B[:, r] = C(x)*S
        B[:, n_BP] = x  # Trailing edge thickness terms
        B[:, n_BP+1] = x*((1-x)**0.5)*((1-x)**n_BP)  # Leading edge modification
        # Multiply by weights
        B = B * weights
        # Sum each row to get points
        surface_array = np.sum(B, axis=1)

        return surface_array


class CSTAerofoil(om.ExplicitComponent):

    def initialize(self):
        self.options.declare('n_Bp_upper', types=int)
        self.options.declare('n_Bp_lower', types=int)
        self.options.declare('n_points', types=int)
        self.options.declare('class_func', default=lambda x: (x**0.5)*(1-x), types=FunctionType, recordable=False)  # A little experimental, allow class functions to be passed as an option?


    def setup(self):
        n_points = self.options['n_points']
        n_Bp_upper = self.options['n_Bp_upper']
        n_Bp_lower = self.options['n_Bp_lower']
        C = self.options['class_func']

        self.cst_upper = CSTSurface(n_Bp_upper, n_points, C)
        self.cst_lower = CSTSurface(n_Bp_lower, n_points, C)

        self.add_input('x_array', shape=n_points)

        self.add_input('weights', shape=n_Bp_upper+n_Bp_lower)

        self.add_output('surface', shape=2*n_points)

    def compute(self, inputs, outputs): 
        n_points = self.options['n_points']
        n_Bp_upper = self.options['n_Bp_upper']
        n_Bp_lower = self.options['n_Bp_lower']

        outputs['surface'][:n_points] = self.cst_upper.compute_surface(inputs['x_array'], inputs['weights'][:n_Bp_upper])
        outputs['surface'][n_points:] = self.cst_lower.compute_surface(inputs['x_array'], inputs['weights'][n_Bp_upper:])


if __name__ == "__main__": 

    p = om.Problem()

    p.model.add_subsystem('cst', CSTAerofoil(n_points=10, n_Bp_upper=3, n_Bp_lower=4))

    p.setup()

    p['cst.weights'] = [1,2,3,1,2,3,4]
    p['cst.x_array'] = np.linspace(0,1,10)

    p.run_model()


    p.model.list_outputs(print_arrays=True)

Upvotes: 2

Related Questions