Reputation: 161
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
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