Source code for fastga_he.models.propulsion.assemblers.performances_from_pt_file

# This file is part of FAST-OAD_CS23-HE : A framework for rapid Overall Aircraft Design of Hybrid
# Electric Aircraft.
# Copyright (C) 2025 ISAE-SUPAERO

import openmdao.api as om
import fastoad.api as oad

from fastga_he.powertrain_builder.powertrain import (
    FASTGAHEPowerTrainConfigurator,
)
from fastga_he.models.propulsion.assemblers.energy_consumption_from_pt_file import (
    EnergyConsumptionFromPTFile,
)

import fastga_he.models.propulsion.components as he_comp

from .constants import SUBMODEL_POWER_TRAIN_PERF, SUBMODEL_THRUST_DISTRIBUTOR

PERFORMANCE_FROM_PT_FILE = "fastga_he.submodel.propulsion.performances.from_pt_file"
oad.RegisterSubmodel.active_models[SUBMODEL_POWER_TRAIN_PERF] = PERFORMANCE_FROM_PT_FILE


[docs] @oad.RegisterSubmodel(SUBMODEL_POWER_TRAIN_PERF, PERFORMANCE_FROM_PT_FILE) class PowerTrainPerformancesFromFile(om.Group): def __init__(self, **kwargs): super().__init__(**kwargs) self.configurator = FASTGAHEPowerTrainConfigurator()
[docs] def initialize(self): self.options.declare( name="power_train_file_path", default=None, desc="Path to the file containing the description of the power", allow_none=False, ) self.options.declare( "number_of_points", default=1, desc="number of equilibrium to be treated" ) self.options.declare( name="add_solver", default=True, desc="Boolean to add solvers to the power train performance group. Default is False " "it can be turned off when used jointly with the mission to save computation time", allow_none=False, ) self.options.declare( name="pre_condition_pt", default=False, desc="Boolean to pre_condition the different components of the PT, " "can save some time in specific cases", allow_none=False, ) self.options.declare( name="sort_component", default=False, desc="Boolean to sort the component with proper order for adding subsystem operations", allow_none=False, ) self.options.declare( name="sspc_closed_list", default=[], types=list, desc="List of the states of the SSPC specified in the sspc_names_list option, " "each element must be either True or False, when nothing is specified, " "the default state from the PT file is used", allow_none=True, ) self.options.declare( name="sspc_names_list", default=[], types=list, desc="Contains the list of the SSPC name which state need to be changed. If this list " "is empty, nothing will be done.", allow_none=False, )
[docs] def setup(self): number_of_points = self.options["number_of_points"] self.configurator.load(self.options["power_train_file_path"]) propulsor_names = self.configurator.get_thrust_element_list() source_names = self.configurator.get_energy_consumption_list() ( components_name, components_name_id, _, components_om_type, components_options, components_connection_outputs, components_connection_inputs, components_promotes, sspc_list, sspc_state, ) = self.configurator.get_performances_element_lists() # We decide on the SSPCs state, we take the default state unless the options specify # otherwise if self.options["sspc_names_list"]: for sspc_name, sspc_closed in zip( self.options["sspc_names_list"], self.options["sspc_closed_list"] ): sspc_state[sspc_name] = sspc_closed # We check the value the resulting states to see if it agrees with the logic and change # it if it is not the case sspc_state = self.configurator.check_sspc_states(sspc_state) options = { "power_train_file_path": self.options["power_train_file_path"], "number_of_points": number_of_points, } self.add_subsystem( name="thrust_splitter", subsys=oad.RegisterSubmodel.get_submodel(SUBMODEL_THRUST_DISTRIBUTOR, options=options), promotes=["data:*", "thrust"], ) if self.options["sort_component"]: ( components_name, components_name_id, components_om_type, components_options, components_promotes, ) = self.configurator.reorder_components( components_name, components_name_id, components_om_type, components_options, components_promotes, ) # Enforces SSPC are added last, not done before because it might breaks the connections # necessary to ensure the coherence of SSPC states when connected to both end of a cable ( components_name, components_name_id, components_om_type, components_options, components_promotes, ) = self.configurator.enforce_sspc_last( components_name, components_name_id, components_om_type, components_options, components_promotes, ) for ( component_name, component_name_id, component_om_type, component_option, component_promote, ) in zip( components_name, components_name_id, components_om_type, components_options, components_promotes, ): local_sub_sys = he_comp.__dict__["Performances" + component_om_type]() local_sub_sys.options[component_name_id] = component_name local_sub_sys.options["number_of_points"] = number_of_points if component_name in sspc_list.keys(): local_sub_sys.options["at_bus_output"] = sspc_list[component_name] local_sub_sys.options["closed"] = sspc_state[component_name] if component_option: for option_name in component_option: local_sub_sys.options[option_name] = component_option[option_name] self.add_subsystem( name=component_name, subsys=local_sub_sys, promotes=["data:*"] + component_promote, ) self.add_subsystem( name="energy_consumption", subsys=EnergyConsumptionFromPTFile( number_of_points=number_of_points, power_train_file_path=self.options["power_train_file_path"], ), promotes=["non_consumable_energy_t_econ", "fuel_consumed_t_econ"], ) for propulsor_name in propulsor_names: self.connect( "thrust_splitter." + propulsor_name + "_thrust", propulsor_name + ".thrust" ) for om_output, om_input in zip(components_connection_outputs, components_connection_inputs): self.connect(om_output, om_input) for source_name in source_names: self.connect( source_name + ".non_consumable_energy_t", "energy_consumption." + source_name + "_non_consumable_energy_t", ) self.connect( source_name + ".fuel_consumed_t", "energy_consumption." + source_name + "_fuel_consumed_t", ) if self.options["add_solver"]: # Solvers setup self.nonlinear_solver = om.NewtonSolver(solve_subsystems=True) self.nonlinear_solver.linesearch = om.ArmijoGoldsteinLS() self.nonlinear_solver.options["iprint"] = 2 self.nonlinear_solver.options["maxiter"] = 200 self.nonlinear_solver.options["rtol"] = 1e-4 self.linear_solver = om.DirectSolver()
# The performances watcher was moved at the same level as the mission performances # watcher so that it is not opened as much, they could be merged eventually
[docs] def guess_nonlinear( self, inputs, outputs, residuals, discrete_inputs=None, discrete_outputs=None ): # We need to reinstate this check on the coherence of voltage because if we run it on its # own we prefer having a warning as well. Though it begs the question of pre # conditioning, voltage power and current here as well even if it is faster at mission # level # TODO: Think about that # This one will be passed in before going into the first pt components number_of_points = self.options["number_of_points"] # Let's first check the coherence of the voltage self.configurator.check_voltage_coherence(inputs=inputs, number_of_points=number_of_points)