Im going to answer your questions a little out of order:
How can I have a component output an initially unknown size array, and have a second component input a second initially unknown size array when both unknown arrays add up to the size of an initially known array?
As of OpenMDAO V3.6.0 you can't do this. OpenMDAO requires that you tell it the size of all the inputs and output. There is a slight workaround via the shape_by_conn feature which will let downstream components get sized by upstream comps, but ultimately the start of that sizing-chain needs to have a given value at setup time. So, you can't have dynamically size the output values during run time. However, I am going to provide you a work around that you can use instead.
Your questions 1 and 3 can both be address with a trick called over-allocation.
Generally, this means allocating a larger array than you actually need, and adding an extra input that helps you keep track of how much to use. Exactly how you use this trick is a bit problem specific, but here is a generic example that shows the basics:
import numpy as np
import openmdao.api as om
class VectorChain(om.ExplicitComponent):
def initialize(self):
self.options.declare('vec_size', types=int)
self.options.declare('step_size', types=int)
def setup(self):
vec_size = self.options['vec_size']
step_size = self.options['vec_size']
# this is the index of the last valid value in the data array
self.add_input('in_idx', shape=1, val=0)
# NOTE: though this could be done as a discrete variable,
# that will confuse OpenMDAO's derivative system, so just pass it as a float.
# actual data array
self.add_input('in_vec', shape=vec_size, val=np.zeros(vec_size))
self.add_output('out_idx', shape=1)
self.add_output('out_vec', shape=vec_size)
# NOTES: You do **NOT** define derivatives wrt the idx variable!!
self.declare_partials('out_vec', 'in_vec', method='CS')
def compute(self, inputs, outputs):
in_vec = inputs['in_vec']
out_vec = outputs['out_vec']
out_vec[:] = in_vec.copy()
# always use the first two entries to
# indicate the start/end of the valid data
i_start_idx = int(inputs['in_idx'])
i_end_idx = i_start_idx + self.options['step_size']
i_size = i_end_idx - i_start_idx
if i_start_idx == 0:
out_vec[0] = 1 # get the counting started
i_start_idx = 1
# note: careful to account for the open end of the
# interval when computing the end_idx value
# print(self.pathname)
for i in range(i_start_idx,i_start_idx+self.options['step_size']):
out_vec[i] = out_vec[i-1] + 1
o_end_idx = i_start_idx + i_size
outputs['out_idx'] = o_end_idx
if __name__ == "__main__":
p = om.Problem()
VEC_SIZE = 12
STEP_SIZE = 3
c0 = p.model.add_subsystem('c0', VectorChain(vec_size=VEC_SIZE, step_size=STEP_SIZE))
c1 = p.model.add_subsystem('c1', VectorChain(vec_size=VEC_SIZE, step_size=STEP_SIZE))
c2 = p.model.add_subsystem('c2', VectorChain(vec_size=VEC_SIZE, step_size=STEP_SIZE))
p.model.connect('c0.out_idx', 'c1.in_idx')
p.model.connect('c0.out_vec', 'c1.in_vec')
p.model.connect('c1.out_idx', 'c2.in_idx')
p.model.connect('c1.out_vec', 'c2.in_vec')
p.setup()
p.run_model()
p.model.list_outputs(print_arrays=True)
In this toy example, their is a chain of identical components and each one just keeps counting from where the last one left off. It's obviously a trivial example, but the basics are there.
In your case, you would have different components for the laminar and transition calcs, but the idea is the same. Start with the laminar part, loop till you hit the trop condition. Stop there, and note the index you stoped at. Then pass that information downstream to the next component.
A few details here are worth noting:
- If you wanted to you could do both upper and lower surfaces together in one chain of components. It's not required to do that, but less components is generally faster.
- If you had 1000 nodes on the upper surface, you could over-allocate a length 1000 array and pass that through the whole calculation (this is basically what my example does). Alternatively, you might know that the turbulent trip point is always going to be between the 20th and 50th points. In that case you could over-allocate a length 50 array for the first component for laminar calcs, and a length 980 array for the second turbulent component. If you had large grids, where the potential overlap was much more narrow this would save some memory.
A note about derivatives:
This whole concept involves discrete movement of indices which is technically non-differentiable. The way you make this work in continuous derivative context is to assume that for any linearization (any time you compute derivatives) the index values are fixed (and hence don't participate in the derivatives).
In your case the specific index value of the end of the laminar calc will change from one run to the next, but anytime you want to know derivatives you assume it's fixed.
In the strict mathematical sense, if you were using gradient based optimization, this assumption is not valid. The function is really not differentiable. However, in practice we can often count on the assume the design converging and the location of the changing index to become effectively fixed. However, if that settling down doesn't happen (perhaps you have a really-really fine discretization and there is enough noise to keep the trip point jumping around by a few nodes no matter what) then you might have trouble with tight convergence.
I don't think it likely that this discrete-index issue will be a problem for your case, so I encourage you to give it a try! However, if you do find the problem to be too noisy using this approach, then you should consider re-formulating the problem in a more continuous manner.
A more continuous formulation:
using the discrete data along the airfoil, define a continuous interpolation function (you could either use OpenMDAO's MetaModelStructured component, or use a newton-solver to converge a least-squares fit to a pre-defined basis function). The input to your interpolant would not be an x,y location but instead be a parametric location s along the airfoil surface (i.e. a path length from an arbitrary starting point). There are several ways to handle this, but to keep it simple, let's say you pick a discrete point on the nose of the airfoil as 0, then each point backwards on the top of the airfoil is positive s=1,s=2,s=3,s=4, etc. Each point on the lower surface is negative s=-1, s=-2, s=-3, s=-4 etc. Though I've picked integer values, its important to realize that the interpolant is valid for any real value of s in between the positive and negative limits of your grid.

Once you have that interpolation, you can solve for the stagnation point location in the continuous space of the interpolant (i.e. find the value of s that you need). To do this, you would probably need to differentiate the interpolant then solve for where that derivative went to zero. That would be a second implicit component, following the first one that output the coefficients for your smooth fit.
Then you can pass the interpolant coefficients and the location of the stagnation to a laminar component, and perform your marching calculations backwards from that point on the upper and lower surfaces until you hit the transition point.
Though you might choose to march in a discrete step to produce a nice evenly spaced output grid of data, you still want to keep things continuous with regard to the transition point. Say you march with a fixed step in s till the first point that is beyond the transition condition. At that point, you would start a bisection search process between the last two search locations to find the "exact" (within some reasonable tolerance) location of the transition point in s. Just to be clear, the laminar and turbulent calculations would be performed on their own sub-grids, which were developed by using the interpolating function.

Then you could pass the conditions at the exact transition position to the next component downstream (along with its s location) and start another marching calculation with the turbulent calculations.
I will note that my suggestion is similar to the answer that Rob Falck gave, though my approach does not require an implicit relationship. You could set the problem up my explicit way, or his implicit way. There are plusses and minuses to both, which I won't go into here. The key point is that a truly continuous formulation is possible, even with an explicit calculation approach. One nice thing about this approach is that its a lot easier to keep the arrays of fixed length. You can just allocate n discrete points for each of the laminar and turbulent sub-grids and let the physical spacing vary a little as the stagnation point and laminar/turbulent point move around.
In practice, I don't think you have to try use continuous approach. I think it's more correct to differentiate it and would be able to converge more tightly. While mathematical correctness is nice, it's not always practical. In practice I think you could find an appropriate grid to work in the more discrete format and be up and running faster. If you found you had noise problems or difficulty converging, then you could switch approaches later on.
One thing I can think of that might make you sensitive to the noise of the discrete approach would be if you plan to create a feed-back loop between the boundary layer solver and the inviscid solver. Some codes will take the boundary layer thickness as a new airfoils shape and pass that back to the inviscid calculations, then re-compute surface pressure distributions and recompute a new boundary layer on the original airfoil surface. If you are going to try something like this, the noise of the discrete approach will likely cause you more trouble. In that case, the more continuous approach is warranted.
