Search code examples
openmdao

Recomendations (functions/solution) to apply in OpenMDAO instead of boolean conditions (if/else)


I have been working for a couple of months with OpenMDAO and I find myself struggling with my code when I want to impose conditions for trying to replicate a physical/engineering behaviour.

I have tried using sigmoid functions, but I am still not convinced with that, due to the difficulty about trading off sensibility and numerical stabilization. Most of times I found overflows in exp so I end up including other conditionals (like np.where) so loosing linearity.

outputs['sigmoid'] = 1 / (1 + np.exp(-x))

I was looking for another kind of step function or something like that, able to keep linearity and derivability to the ease of the optimization. I don't know if something like that exists or if there is any strategy that can help me. If it helps, I am working with an OpenConcept benchmark, which uses vectorized computations ans Simpson's rule numerical integration.

Thank you very much.

PD: This is my first ever question in stackoverflow, so I would like to apologyze in advance for any error or bad practice commited. Hope to eventually collaborate and become active in the community.

Update after Justin answer:

I will take the opportunity to define a little bit more my problem and the strategy I tried. I am trying to monitorize and control thermodynamics conditions inside a tank. One of the things is to take actions when pressure P1 reaches certein threshold P2, for defining this:

eval= (inputs['P1'] - inputs['P2']) / (inputs['P1'] + inputs['P2'])

# P2 = threshold [Pa]
# P1 = calculated pressure [Pa]

k=100  #steepness control
outputs['sigmoid'] = (1 / (1 + np.exp(-eval * k)))

eval was defined in order avoid overflows normalizing the values, so when the threshold is recahed, corrections are taken. In a very similar way, I defined a function to check if there is still mass (so flowing can continue between systems):

eval= inputs['mass']/inputs['max'] 
k=50
outputs['sigmoid'] = (1 / (1 + np.exp(-eval*k)))**3

maxis also used for normalizing the value and the exponent is added for reaching zero before entering in the negative domain.

PLot (sorry it seems I cannot post images yet for my reputation)

It may be important to highlight that both mass and pressure are calculated from coupled ODE integration, in which this activation functions take part. I guess OpenConcept nature 'explore' a lot of possible values before arriving the solution, so most of the times giving negative infeasible values for massand pressure and creating overflows. For that sometimes I try to include:

eval[np.where(eval > 1.5)] = 1.5
    eval[np.where(eval < -1.5)] = -1.5

That is not a beautiful but sometimes effective solution. I try to avoid using it since I taste that this bounds difficult solver and optimizer work.


Solution

  • I could give you a more complete answer if you distilled your question down to a specific code example of the function you're wrestling with and its expected input range. If you provide that code-sample, I'll update my answer.

    Broadly, this is a common challenge when using gradient based optimization. You want some kind of behavior like an if-condition to turn something on/off and in many cases thats a fundamentally discontinuous function.

    To work around that we often use sigmoid functions, but these do have some of the numerical challenges you pointed out. You could try a hyberbolic tangent as an alternative, though it may suffer the same kinds of problems.

    I will give you two broad options:

    Option 1

    sometimes its ok (even if not ideal) to leave the purely discrete conditional in the code. Lets say you wanted to represent a kind of simple piecewise function:

    y = 2x; x>=0 
    
    y = 0; x < 0
    

    There is a sharp corner in that function right at 0. That corner is not differentiable, but the function is fine everywhere else. This is very much like the absolute value function in practice, though you might not draw the analogy looking at the piecewise definition of the function because the piecewise nature of abs is often hidden from you.

    If you know (or at least can check after the fact) that your final answer will no lie right on or very near to that C1 discontinuity, then its probably fine to leave the code the way is is. Your derivatives will be well defined everywhere but right at 0 and you can simply pick the left or the right answer for 0.

    Its not strictly mathematically correct, but it works fine as long as you're not ending up stuck right there.

    Option 2

    Apply a smoothing function. This can be a sigmoid, or a simple polynomial. The exact nature of the smoothing function is highly specific to the kind of discontinuity you are trying to approximate.

    In the case of the piecewise function above, you might be tempted to define that function as:

    2x*sig(x)

    That would give you roughly the correct behavior, and would be differentiable everywhere. But wolfram alpha shows that it actually undershoots a little. Thats probably undesirable, so you can increase the exponent to mitigate that. This however, is where you start to get underflow and overflow problems.

    So to work around that, and make a better behaved function all around, you could instead defined a three part piecewise polynomial:

    undershoot effect of the sigmoid function

    y = 2x; x>=a 
    
    y = c0 + c1*x + c2*x**2; -a <= x < a
    
    y = 0 x < -a
    

    you can solve for the coefficients as a function of a (please double check my algebra before using this!):

    c0 = 1.5a
    
    c1 = 2 
    
    c2 = 1/(2a)
    

    The nice thing about this approach is that it will never overshoot and go negative. You can also make a reasonably small and still get decent numerics. But if you try to make it too small, c2 will obviously blow up.

    In general, I consider the sigmoid function to be a bit of a blunt instrument. It works fine in many cases, but if you try to make it approximate a step function too closely, its a nightmare. If you want to represent physical processes, I find polynomial fillet functions work more nicely.

    It takes a little effort to derive that polynomial, because you want it to be c1 continuous on both sides of the curve. So you have to construct the system of equations to solve for it as a function of the polynomial order and the specific relaxation you want (0.1 here).