Error on initialization of a connected variable in WISDEM

I am getting started with WISDEM and have set up a basic optimization problem based on the ExpicitComponent “PlantFinance” in plant_finance.py.
The object variable is the PlantFinance output “lcoe”, and the only design variable is the input “turbine_aep”. The PlantFinance input “tcc_per_kW” is the “turbine_cost_kW” output of a subsystem/instance of the ExplicitComponent “TurbineCostAdder2015” in nrel_csm_cost_2015.py, so “tcc_per_kW” and “turbine_cost_kW” are connected together. The remaining inputs of PlantFinance are input at the problem setup, or are kept at their default values in PlantFinance. This is the code:

import numpy as np
import openmdao.api as om
from wisdem.inputs import load_yaml
from wisdem.plant_financese.plant_finance import PlantFinance
from wisdem.nrelcsm.nrel_csm_cost_2015 import TurbineCostAdder2015, RotorCostAdder2015, BladeCost2015
from wisdem.nrelcsm.nrel_csm_mass_2015 import BladeMass

#==============================================================================

# =============================================================================
class call_to_group(om.Group):
    
#    def initialize(self):
#        
#        pass

    def setup(self):
        
  
        self.add_subsystem("bladeMass_calc", BladeMass(),
                           promotes_inputs=['rotor_diameter'],
                           promotes_outputs=['blade_mass']
                           )
        
        self.add_subsystem('bladeCost_calc', BladeCost2015(), 
                           promotes_inputs=['blade_mass'],
                           promotes_outputs=['blade_cost']
                           )
        
        self.add_subsystem("rotorCostAndMass_calc", RotorCostAdder2015(),
                           promotes_inputs=['blade_cost', 'blade_mass'],
                           promotes_outputs=['rotor_cost', 'rotor_mass_tcc']
                           )
        
        self.add_subsystem("turbineCapitalCosts_calc", TurbineCostAdder2015(),
                           promotes_inputs=['rotor_cost', 'rotor_mass_tcc', 'machine_rating'] #,
#                           promotes_outputs=['turbine_cost_kW']
                           )
     #  
        
        inputsToPlantFinance = [
                "machine_rating",
                "turbine_number",
                "bos_per_kW",
                "opex_per_kW",
                "turbine_aep",
                ]
        
        self.add_subsystem('LCoE_calc', PlantFinance(verbosity=False), 
                           promotes_inputs=inputsToPlantFinance,
                           promotes_outputs=["lcoe"])
        
        self.connect('turbineCapitalCosts_calc.turbine_cost_kW', 'LCoE_calc.tcc_per_kW')
        
# ============================入力============================================

design_options = load_yaml('windTurbine_design_options.yaml') 
##
rotorDiameter = design_options["assembly"]["rotor_diameter"] # [m]
        #
machine_rating = design_options["assembly"]["rated_power"] # [W] !!!
machine_rating /=1e3 # [kW] kWが必要だから。
turbine_number = design_options["costs"]["turbine_number"]
print(' turbine number=', turbine_number, '\n')
         
#----------- call to group---------------------------------

case = om.Problem()
case.model = call_to_group()#

case.driver = om.ScipyOptimizeDriver()
case.driver.options['optimizer'] = 'SLSQP' # this is not the cause of the optimization failure

case.model.add_design_var('turbine_aep', lower=1e6, upper=1.55e6) # , scaler=1e-1


case.model.add_objective('lcoe', scaler=1) # netAEPの最大化

#+++++++++++++++++++++#
 +  Area 1 
#+++++++++++++++++++++#

case.setup()

#+++++++++++++++++++++#
 +  Area 2 
#+++++++++++++++++++++#

case.check_partials(compact_print=True)

I am running check_partials before the optimization problem, but the check fails in the following manners:

  1. if, in Area 1 of the code, I set the inputs to the subsystem/instance of PlantFinance as case.model.set_input_defaults('turbine_number', val=turbine_number) # case.model.set_input_defaults('machine_rating', val=machine_rating) # case.model.set_input_defaults('tcc_per_kW', val=500.0) # case.model.set_input_defaults('bos_per_kW', val=500.0) # case.model.set_input_defaults('opex_per_kW', val=5.0) case.model.set_input_defaults('rotor_diameter', val=rotorDiameter, units='m' ) case.model.set_input_defaults('turbine_aep', val=1e6, units="kW*h") #
    I get the following error:
  1. if, in addition to the set_input_defaults lines in Area 1, I use set_val in Area 2 of the code as:
case.set_val('turbine_number', turbine_number)
case.set_val('tcc_per_kW', 500.0)
case.set_val('bos_per_kW', 500.0)
case.set_val('opex_per_kW', 5.0)
case.set_val('rotor_diameter', rotorDiameter)
case.set_val('machine_rating', machine_rating)

I get the following error:

If I comment out the line “case.set_val(‘tcc_per_kW’, 500.0)” the error becomes:

  1. If I comment out all the set_input_defaults in Area 1 of the code and keep only set_val in Area 2, nothing changes respect to point (2)

So I have understood that the value of “tcc_per_kW” must be initialized even if the variable is connected to the output of another subsystem, but set_input_defaults and/or set_val do not find “tcc_per_kW”.
What am I missing and/or mistaking?

Thank you for your time and attention

You are very, very close! Looking at your code, there is no variable called “tcc_per_kW” in the top-level namespace. That variable is within the “LCoE_calc” namespace. Add “tcc_per_kW” to the promoted variables in the “inputsToPlantFinance” list. You will then need to modify the variable connection line to:

self.connect('turbineCapitalCosts_calc.turbine_cost_kW', 'tcc_per_kW')

After that things should run.

With this correction I can run check_partials without any error or warning.
Thank you.

After this, I tried to run the optimization, and this is the subject of my next post (should I, perhaps, start a brand new topic for this?)

I run the optimization, and the output is:

LCoE should be impacted by turbine_aep, because there is the specific partial derivative defined by the jacobian in plant_finance.py (detected also by check_partials).
Then, I run the openmdao check on my code and the result is

The inputs not connected are all the variables that I left at the default values, but also the variables that I set in my optimization. I do not think that I have to care about the default values, but the inputs that I set resulting not connected is a problem, and I add IndepVarComp outputs to the group setup like this:

class call_to_group(om.Group):
    
#    def initialize(self):
#        
#        pass

    def setup(self):
        
        indeps = self.add_subsystem("indeps", om.IndepVarComp(), promotes=["*"])
        indeps.add_output('rotor_diameter', val=1.0, units='m')
        indeps.add_discrete_output('turbine_number', val=1)
        indeps.add_output('machine_rating', val=1.0, units='kW')
        indeps.add_output('bos_per_kW', val=1.0, units="USD/kW")
        indeps.add_output('opex_per_kW', val=1.0, units="USD/kW/yr")
        indeps.add_output('turbine_aep', val=1e5, units='kW*h')
        #        
  
        self.add_subsystem("bladeMass_calc", BladeMass(),
                           promotes_inputs=['rotor_diameter'],
                           promotes_outputs=['blade_mass']
                           )
        
        self.add_subsystem('bladeCost_calc', BladeCost2015(), 
                           promotes_inputs=['blade_mass'],
                           promotes_outputs=['blade_cost']
                           )
        
        self.add_subsystem("rotorCostAndMass_calc", RotorCostAdder2015(),
                           promotes_inputs=['blade_cost', 'blade_mass'],
                           promotes_outputs=['rotor_cost', 'rotor_mass_tcc']
                           )
        
        self.add_subsystem("turbineCapitalCosts_calc", TurbineCostAdder2015(),
                           promotes_inputs=['rotor_cost', 'rotor_mass_tcc', 'machine_rating'] #,
#                           promotes_outputs=['turbine_cost_kW']
                           )
     #  
        
        inputsToPlantFinance = [
                "machine_rating",
                "turbine_number",
                "bos_per_kW",
                "opex_per_kW",
                "turbine_aep",
                "tcc_per_kW",
                ]
        
        self.add_subsystem('LCoE_calc', PlantFinance(verbosity=False), 
                           promotes_inputs=inputsToPlantFinance,
                           promotes_outputs=["lcoe"])
        
        self.connect('turbineCapitalCosts_calc.turbine_cost_kW', 'tcc_per_kW')

I run again the openmadao check, and only the variables kept at the default value are found as inputs not connected:

I run again the optimization, but the result is still the same:

Why I have that DerivativesWarning, and the optimization does not even start?
Can I have some help also on this problem?

Thank you

Hello Henny,

I also get the same warnings as you. This might be because the WISDEM code does not provide analytical partial derivatives, so OpenMDAO is assuming that all derivatives are zero. I’m not 100% positive though and that would be a better question on the OpenMDAO StackOverflow boards. You need to declare that you are using finite differential either for the partials (within each component) or for the totals. I have added a line to use finite differencing for the total derivatives. Note, I also removed your lines setting inputs for “tcc_per_kW”, since that is a connected dependent variable and shouldn’t be set like an independent variable, and put in better scaling on the design variable and objective to make the SLSQP method better behaved.

import numpy as np
import openmdao.api as om
from wisdem.inputs import load_yaml
from wisdem.plant_financese.plant_finance import PlantFinance
from wisdem.nrelcsm.nrel_csm_cost_2015 import TurbineCostAdder2015, RotorCostAdder2015, BladeCost2015
from wisdem.nrelcsm.nrel_csm_mass_2015 import BladeMass

#==============================================================================

# =============================================================================
class call_to_group(om.Group):

    def setup(self):
       
 
        self.add_subsystem("bladeMass_calc", BladeMass(),
                           promotes_inputs=['rotor_diameter'],
                           promotes_outputs=['blade_mass']
                           )
       
        self.add_subsystem('bladeCost_calc', BladeCost2015(),
                           promotes_inputs=['blade_mass'],
                           promotes_outputs=['blade_cost']
                           )
       
        self.add_subsystem("rotorCostAndMass_calc", RotorCostAdder2015(),
                           promotes_inputs=['blade_cost', 'blade_mass'],
                           promotes_outputs=['rotor_cost', 'rotor_mass_tcc']
                           )
       
        self.add_subsystem("turbineCapitalCosts_calc", TurbineCostAdder2015(),
                           promotes_inputs=['rotor_cost', 'rotor_mass_tcc', 'machine_rating'] #,
#                           promotes_outputs=['turbine_cost_kW']
                           )
       
        inputsToPlantFinance = [
            "machine_rating",
            "turbine_number",
            "bos_per_kW",
            "opex_per_kW",
            "turbine_aep",
            "tcc_per_kW",
        ]
       
        self.add_subsystem('LCoE_calc', PlantFinance(verbosity=False),
                           promotes_inputs=inputsToPlantFinance,
                           promotes_outputs=["lcoe"])
       
        self.connect('turbineCapitalCosts_calc.turbine_cost_kW', 'tcc_per_kW')
       
# ============================入力============================================

design_options = load_yaml('windTurbine_design_options.yaml')
rotorDiameter = design_options["assembly"]["rotor_diameter"] # [m]
machine_rating = design_options["assembly"]["rated_power"] # [W] !!!
machine_rating /=1e3 # [kW] kWが必要だから。
turbine_number = design_options["costs"]["turbine_number"]
print(' turbine number=', turbine_number, '\n')
         
#----------- call to group---------------------------------

case = om.Problem()
case.model = call_to_group()

case.driver = om.ScipyOptimizeDriver()
case.driver.options['optimizer'] = 'SLSQP' # this is not the cause of the optimization failure

case.model.add_design_var('turbine_aep', lower=1e6, upper=1.55e6, ref=1e6) # , scaler=1e-1


case.model.add_objective('lcoe', ref=0.1) # netAEPの最大化

#+++++++++++++++++++++#
# +  Area 1
#+++++++++++++++++++++#
case.model.set_input_defaults('turbine_number', val=turbine_number) #
case.model.set_input_defaults('machine_rating', val=machine_rating) #
case.model.set_input_defaults('bos_per_kW', val=500.0) #
case.model.set_input_defaults('opex_per_kW', val=5.0)
case.model.set_input_defaults('rotor_diameter', val=rotorDiameter, units='m' )
case.model.set_input_defaults('turbine_aep', val=1e6, units="kW*h") #

case.setup()

#+++++++++++++++++++++#
# +  Area 2
#+++++++++++++++++++++#
case.set_val('turbine_number', turbine_number)
case.set_val('bos_per_kW', 500.0)
case.set_val('opex_per_kW', 5.0)
case.set_val('rotor_diameter', rotorDiameter)
case.set_val('machine_rating', machine_rating)
case.set_val('turbine_aep', 1e6, units="kW*h")

#case.check_partials(compact_print=True)
print(case['turbine_aep'], case['lcoe'])
case.model.approx_totals(method="fd")
case.run_driver()
case.model.list_inputs()
case.model.list_outputs()

With this code, I get the following output:

Thinking about the problem further, you are minimizing LCOE = (FCR*CapEx + OpEx) / AEP. You have declared AEP to be an independent variable so there is no connection between AEP & turbine cost. Therefore, LCOE will be minimized when AEP, the denominator, is maximized. That is why the optimizer sets the value to the AEP upper bound.

Hello Garret,

thank you for the code corrections: with them I also get rid of the warning about no impact by design variable on the optimization objective.
Now, just to correctly understand how WISDEM works in OpenMDAO, you write

so what I read in plant_financese/plant_finance.py::PlantFinance from line 136 to line 145 is not the declaration of the analytical partial derivatives of the “lcoe” variable? This declaration is in the “compute” method, but there is also a single J = self.J line in the “compute_partials” method of the PlantFinance class: doesn’t this mean that the analytical partial derivatives are provided?

Then, about

I was expecting to see the optimization succesfull at the maximum AEP, and intentionally chose a design variable that affected only LCoE and not the turbine cost. Before running a case where the design variable impact on the objective is indirect, I wanted to try a simpler case where the impact is direct to be sure that I get the ojective really mimimized with what I write.

Thank you for your time and attention.

We haven’t maintained the partial derivative declarations in plant_financese in a while, so I cannot guarantee that they are correct or fed to OpenMDAO properly. The other Components you are using also do not provide analytic derivatives.

Yes, the ExplicitComponents BladeMass, BladeCost2015, RotorCostAdder2015, and TurbineCostAdder2015 do not provide analytic derivatives, and, actually they even do not define any partial derivative… I mean, there is no setup_partials method, nor any declare_partials instruction in their definitions.
Then, for what I have understood, none of the inputs to those Components can be set as design variable for the optimization objective LCoE because the change of the design variable is not connected through to the change of LCoE.
If so, how design variables affecting the blade mass can impact the optimization objective LCoE ? I am thinking here about blade twist, chord, and sparcap thickness in the Blade Aero-Structural Optimization Example of the WISDEM documentation, where the merit_figure in the analysis_options_aerostruct.yaml file is just LCoE.

I realize that I am implicitly assuming that the only way to calculate LCoE in WISDEM is by using the Components in wisdem\nrelcsm and wisdem\plant_financese, is this assumtpion correct or there are actually other Components?

Thank you for your time and attention

All variable can be labelled a design variable in OpenMDAO. If partial derivatives are not written out analytically, they can be computed through finite differencing.

WISDEM offers many canned example problems to demonstrate turbine design optimization. I know you’ve taken a look at some of those example problems already. For the blade, there are simplistic mass and cost estimates available through the NREL CSM and more more detailed ones available through RotorSE. You are correct that plant_financese is the Component where the final evaluation of LCOE occurs.

Thank you very much for the explanation of this important point.
Indeed, calling approx_totals() on the group used as model you can set any input as design variable, even if no partial derivative is declared in any of the subsystems of the group (http://openmdao.org/twodocs/versions/3.6.0/features/core_features/working_with_derivatives/approximating_totals.html).

As a try, I have just replaced turbine_aep with rotor_diameter as design variable in my code, and the LCoE optimization completed successfully with the expected result.