Je vais répondre à vos questions un peu dans le désordre :
Comment puis-je faire en sorte qu'un composant produise un tableau de taille initialement inconnue, et qu'un second composant introduise un second tableau de taille initialement inconnue lorsque les deux tableaux inconnus s'additionnent à la taille d'un tableau initialement connu ?
Depuis OpenMDAO V3.6.0, vous ne pouvez plus le faire. OpenMDAO exige que vous lui indiquiez la taille de toutes les entrées et sorties. Il existe une légère solution de contournement via l'option caractéristique shape_by_conn ce qui permettra aux composants en aval d'être dimensionnés par les comps en amont, mais en fin de compte le début de cette chaîne de dimensionnement doit avoir une valeur donnée au moment de la configuration. Vous ne pouvez donc pas dimensionner dynamiquement les valeurs de sortie pendant l'exécution. Cependant, je vais vous fournir une solution de contournement que vous pouvez utiliser à la place.
Vos questions 1 et 3 peuvent toutes deux être traitées par une astuce appelée surallocation. En général, cela signifie allouer un tableau plus grand que ce dont vous avez réellement besoin, et ajouter une entrée supplémentaire qui vous aide à garder la trace de la quantité à utiliser. La manière exacte d'utiliser cette astuce est un peu spécifique au problème, mais voici un exemple générique qui montre les bases :
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)
Dans cet exemple de jouet, il y a une chaîne de composants identiques et chacun d'eux continue à compter à partir de l'endroit où le dernier s'est arrêté. C'est évidemment un exemple trivial, mais les bases sont là. Dans votre cas, vous auriez des composants différents pour les calculs laminaires et de transition, mais l'idée est la même. Commencez par la partie laminaire, bouclez jusqu'à ce que vous atteigniez la condition trop. Arrêtez-vous là, et notez l'indice auquel vous vous êtes arrêté. Puis transmettez cette information en aval au composant suivant.
Quelques détails méritent d'être notés :
- Si vous le vouliez, vous pourriez faire les surfaces supérieure et inférieure ensemble dans une chaîne de composants. Il n'est pas nécessaire de le faire, mais moins de composants est généralement plus rapide.
- Si vous aviez 1000 nœuds sur la surface supérieure, vous pourriez surallouer un tableau de longueur 1000 et le faire passer dans tout le calcul (c'est en gros ce que fait mon exemple). Par ailleurs, vous pouvez savoir que le point de déclenchement de la turbulence se situera toujours entre les 20e et 50e points. Dans ce cas, vous pourriez sur-allouer un tableau de longueur 50 pour la première composante pour les calculs laminaires, et un tableau de longueur 980 pour la seconde composante turbulente. Si vous aviez de grandes grilles, où le chevauchement potentiel était beaucoup plus étroit, cela permettrait d'économiser de la mémoire.
Une remarque sur les produits dérivés :
Tout ce concept implique un mouvement discret d'indices qui est techniquement non différentiable. La façon dont vous faites fonctionner cela dans le contexte des dérivées continues est de supposer que pour toute linéarisation (à chaque fois que vous calculez les dérivées), les valeurs des indices sont fixes (et ne participent donc pas aux dérivées). Dans votre cas, la valeur de l'indice spécifique de la fin du calcul laminaire changera d'une exécution à l'autre, mais à chaque fois que vous voulez connaître les dérivées, vous supposez qu'elle est fixe.
Au sens mathématique strict, si vous utilisiez une optimisation basée sur le gradient, cette hypothèse n'est pas valable. La fonction n'est vraiment pas différentiable. Cependant, dans la pratique, nous pouvons souvent compter sur l'hypothèse que le modèle converge et que l'emplacement de l'indice changeant devient effectivement fixe. Cependant, si cette stabilisation ne se produit pas (peut-être avez-vous une discrétisation vraiment très fine et qu'il y a suffisamment de bruit pour que le point de déclenchement continue à sauter de quelques nœuds quoi qu'il arrive), vous risquez d'avoir des problèmes de convergence étroite.
Je ne pense pas qu'il soit probable que cette question de l'indice discret soit un problème pour votre cas, et je vous encourage donc à essayer ! Cependant, si vous trouvez que le problème est trop bruyant en utilisant cette approche, alors vous devriez envisager de reformuler le problème d'une manière plus continue.
Une formulation plus continue : en utilisant les données discrètes le long de l'aile, définissez une fonction d'interpolation continue (vous pouvez utiliser la fonction d'interpolation continue d'OpenMDAO). MétaModèleStructuré ou utiliser un solveur de Newton pour faire converger un ajustement des moindres carrés vers une fonction de base prédéfinie. ). L'entrée de votre interpolant ne serait pas un emplacement x,y mais plutôt un emplacement paramétrique s le long de la surface de l'aile (c'est-à-dire une longueur de trajectoire à partir d'un point de départ arbitraire). Il y a plusieurs façons de gérer cela, mais pour rester simple, disons que vous choisissez un point discret sur le nez de l'aile comme 0, puis chaque point vers l'arrière sur le dessus de l'aile est positif s=1, s=2, s=3, s=4, etc. Chaque point sur la surface inférieure est négatif s=-1, s=-2, s=-3, s=-4, etc. Bien que j'aie choisi des valeurs entières, il est important de réaliser que l'interpolant est valable pour toute valeur réelle de s entre les limites positives et négatives de votre grille.
Une fois que vous avez cette interpolation, vous pouvez résoudre l'emplacement du point de stagnation dans l'espace continu de l'interpolant (c'est-à-dire trouver la valeur de s dont vous avez besoin). Pour ce faire, vous devrez probablement différencier l'interpolant puis résoudre l'endroit où cette dérivée devient nulle. Il s'agirait d'une deuxième composante implicite, après la première qui fournit les coefficients de votre ajustement lisse.
Ensuite, vous pouvez transmettre les coefficients interpolants et l'emplacement de la stagnation à une composante laminaire, et effectuer vos calculs à rebours à partir de ce point sur les surfaces supérieure et inférieure jusqu'à ce que vous atteigniez le point de transition. Bien que vous puissiez choisir de procéder par étapes discrètes pour produire une grille de données de sortie uniformément espacée, vous souhaitez que les choses restent continues en ce qui concerne le point de transition. Supposons que vous marchiez avec un pas fixe en s jusqu'au premier point qui est au-delà de la condition de transition. À ce moment-là, vous lancerez un processus de recherche par bissection entre les deux derniers emplacements de recherche pour trouver l'emplacement "exact" (dans une certaine tolérance raisonnable) du point de transition dans la zone de transition. s . Pour être clair, les calculs laminaires et turbulents seraient effectués sur leurs propres sous-grilles, qui ont été développées en utilisant la fonction d'interpolation.
Ensuite, vous pourriez transmettre les conditions à la position exacte de la transition au composant suivant en aval (ainsi que son s ) et commencer un autre calcul de marche avec les calculs de turbulence.
Je noterai que ma suggestion est similaire à la réponse donnée par Rob Falck, bien que mon approche ne nécessite pas de relation implicite. Vous pouvez résoudre le problème de ma façon explicite ou de sa façon implicite. Les deux approches présentent des avantages et des inconvénients que je n'aborderai pas ici. Le point essentiel est qu'une formulation véritablement continue est possible, même avec une approche de calcul explicite. L'un des avantages de cette approche est qu'il est beaucoup plus facile de conserver des tableaux de longueur fixe. Vous pouvez simplement allouer n points discrets pour chacune des sous-grilles laminaire et turbulente et laisser l'espacement physique varier un peu lorsque le point de stagnation et le point laminaire/turbulent se déplacent.
En pratique, je ne pense pas que vous deviez essayer d'utiliser l'approche continue. Je pense qu'il est plus correct de la différencier et qu'elle serait capable de converger plus étroitement. Si l'exactitude mathématique est agréable, elle n'est pas toujours pratique. En pratique, je pense que vous pourriez trouver une grille appropriée pour travailler dans un format plus discret et être opérationnel plus rapidement. Si vous constatez que vous avez des problèmes de bruit ou des difficultés à converger, vous pourrez changer d'approche par la suite.
Une chose à laquelle je pense qui pourrait vous rendre sensible au bruit de l'approche discrète serait si vous prévoyez de créer une boucle de rétroaction entre le solveur de couche limite et le solveur inviscide. Certains codes prennent l'épaisseur de la couche limite comme une nouvelle forme d'aile et la renvoient aux calculs inviscides, puis recalculent les distributions de pression de surface et recalculent une nouvelle couche limite sur la surface de l'aile originale. Si vous voulez essayer quelque chose comme ça, le bruit de l'approche discrète vous causera probablement plus de problèmes. Dans ce cas, l'approche plus continue est justifiée.