Source code for opencmp.solvers.time_integration_schemes

########################################################################################################################
# Copyright 2021 the authors (see AUTHORS file for full list).                                                         #
#                                                                                                                      #
# This file is part of OpenCMP.                                                                                        #
#                                                                                                                      #
# OpenCMP is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public  #
# License as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any     #
# later version.                                                                                                       #
#                                                                                                                      #
# OpenCMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied        #
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more  #
# details.                                                                                                             #
#                                                                                                                      #
# You should have received a copy of the GNU Lesser General Public License along with OpenCMP. If not, see             #
# <https://www.gnu.org/licenses/>.                                                                                     #
########################################################################################################################

import ngsolve as ngs
from ngsolve import GridFunction, BilinearForm, LinearForm, Parameter
from opencmp.models import Model
from typing import List, Tuple
from ngsolve import dx


[docs]def explicit_euler(model: Model, gfu_0: List[GridFunction], dt: List[Parameter])\ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Explicit Euler time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear forms a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a.append(a_tmp) # Construct the linear forms L : List[BilinearForm] = [] L_ode = model.construct_bilinear_time_ODE(gfu_lst[1], V, tmp_dt, 1) L_lst = model.construct_linear(V, gfu_lst[1], tmp_dt, 1) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += -1.0 * L_ode[i] L_tmp += L_lst[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'explicit euler') return a, L
[docs]def implicit_euler(model: Model, gfu_0: List[GridFunction], dt: List[Parameter], step: int = 0) \ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Implicit Euler time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. step: Used for adaptive_three_step to ensure the correct boundary condition and model function values are used when the half steps are taken. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[step] # Construct the bilinear form a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, step) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, step) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear form L : List[BilinearForm] = [] L_lst = model.construct_linear(V, None, tmp_dt, step) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += L_lst[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'implicit euler') return a, L
[docs]def crank_nicolson(model: Model, gfu_0: List[GridFunction], dt: List[Parameter])\ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Crank Nicolson (trapezoidal rule) time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear form a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = ngs.BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += 0.5 * a_ode[i] a.append(a_tmp) # Construct the linear form L: List[BilinearForm] = [] L_ode = model.construct_bilinear_time_ODE(gfu_lst[1], V, tmp_dt, 1) L_lst_0 = model.construct_linear(V, gfu_lst[0], tmp_dt, 0) L_lst_1 = model.construct_linear(V, gfu_lst[1], tmp_dt, 1) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += -0.5 * L_ode[i] L_tmp += 0.5 * L_lst_0[i] L_tmp += 0.5 * L_lst_1[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'crank nicolson') return a, L
[docs]def euler_IMEX(model: Model, gfu_0: List[GridFunction], dt: List[Parameter])\ -> Tuple[List[BilinearForm], List[LinearForm]]: """ First order IMEX time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear form a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear form L : List[BilinearForm] = [] L_lst = model.construct_linear(V, None, tmp_dt, 0) L_imex = model.construct_imex_explicit(V, gfu_lst[1], tmp_dt, 0) for i in range(model.num_weak_forms): L_tmp = ngs.LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += L_imex[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'implicit euler') return a, L
[docs]def CNLF(model: Model, gfu_0: List[GridFunction], dt: List[Parameter]) \ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Crank Nicolson Leap Frog IMEX time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear forms a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += 2.0 * a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear form L : List[BilinearForm] = [] L_lst = model.construct_linear(V, gfu_lst[2], tmp_dt, 2) L_ode = model.construct_bilinear_time_ODE(gfu_lst[2], V, tmp_dt, 2) L_imex = model.construct_imex_explicit(V, gfu_lst[1], tmp_dt, 1) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += -1.0 * L_ode[i] L_tmp += 2.0 * L_imex[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'CNLF') return a, L
[docs]def SBDF(model: Model, gfu_0: List[GridFunction], dt: List[Parameter]) \ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Third order semi-implicit backwards differencing time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear forms a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = ngs.BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear form L : List[BilinearForm] = [] L_lst = model.construct_linear(V, gfu_lst[0], tmp_dt, 0) L_imex_1 = model.construct_imex_explicit(V, gfu_lst[1], tmp_dt, 1) L_imex_2 = model.construct_imex_explicit(V, gfu_lst[2], tmp_dt, 2) L_imex_3 = model.construct_imex_explicit(V, gfu_lst[3], tmp_dt, 3) for i in range(model.num_weak_forms): L_tmp = ngs.LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += 3.0 * L_imex_1[i] L_tmp += -3.0 * L_imex_2[i] L_tmp += L_imex_3[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'SBDF') return a, L
[docs]def adaptive_IMEX_pred(model: Model, gfu_0: List[GridFunction], dt: List[Parameter]) \ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Predictor for the adaptive time-stepping IMEX time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of previous time steps ordered from most recent to oldest. dt: List of timestep sizes ordered from most recent to oldest. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # Create list of the previous time-step's result # NOTE: It is assumed that pressure is always the 2nd in the list gfu_prev = [gfu_lst[0][i] for i in range(len(gfu_lst[0]))] # Zero out pressure gfu_prev[1] = 0.0 # Operators specific to this time integration scheme. w = dt[0] / dt[1] E = model.construct_gfu().components[0] E_expr = (1.0 + w) * gfu_lst[0][0] - w * gfu_lst[1][0] E.Set(E_expr) # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear forms a: List[BilinearForm] = [] a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear forms L : List[BilinearForm] = [] # TODO: Why doesn't using E work? Numerical error? L_lst = model.construct_linear(V, gfu_lst[0], tmp_dt, 0) L_imex = model.construct_imex_explicit(V, gfu_prev, tmp_dt, 0) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += L_imex[i] L.append(L_tmp) # Add time discretization terms _add_dt_terms(a, L, gfu_lst, model, 'crank nicolson') return a, L
[docs]def RK_222(model: Model, gfu_0: List[GridFunction], dt: List[Parameter], step: int) \ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Two-step second-order Runge Kutta IMEX time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of intermediate steps in reverse step order (ie: [step n+1 sol, step 2 sol, step 1 sol, step n sol]). dt: List of current time step size. step: Which step of the Runge Kutta scheme should be assembled. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # Define scheme-specific constants. These might eventually become user-specified constants. gamma = 0.5 * (2.0 - ngs.sqrt(2.0)) delta = 1.0 - 1.0 / (2.0 - ngs.sqrt(2.0)) # Define the variables to hold the linear and bilinear forms a: List[BilinearForm] = [] L: List[BilinearForm] = [] if step == 1: # All the terms use the same dt. tmp_dt = dt[1] # Construct the bilinear forms a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 1) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 1) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear forms L_lst = model.construct_linear(V, None, tmp_dt, 1) L_imex = model.construct_imex_explicit(V, gfu_lst[2], tmp_dt, 2) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += L_imex[i] L.append(L_tmp) elif step == 2: # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear forms a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += gamma * a_ode[i] a.append(a_tmp) # Construct the linear form L_lst_0 = model.construct_linear(V, gfu_lst[0], tmp_dt, 0) L_lst_1 = model.construct_linear(V, gfu_lst[1], tmp_dt, 1) L_imex_1 = model.construct_imex_explicit(V, gfu_lst[1], tmp_dt, 1) L_imex_2 = model.construct_imex_explicit(V, gfu_lst[2], tmp_dt, 2) L_ode = model.construct_bilinear_time_ODE(gfu_lst[1], V, tmp_dt, 1) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += gamma * L_lst_0[i] L_tmp += (1.0 - gamma) * L_lst_1[i] L_tmp += (1.0 - delta) * L_imex_1[i] L_tmp += delta * L_imex_2[i] L_tmp += -(1.0 - gamma) * L_ode[i] L.append(L_tmp) else: raise ValueError('RK 222 only has 2 steps. Step \'{}\' is invalid'.format(step)) _add_dt_terms(a, L, gfu_lst, model, 'RK 222') return a, L
[docs]def RK_232(model: Model, gfu_0: List[GridFunction], dt: List[Parameter], step: int)\ -> Tuple[List[BilinearForm], List[LinearForm]]: """ Three-step second-order Runge Kutta IMEX time integration scheme. This function constructs the final bilinear and linear forms for the time integration scheme by adding the necessary time-dependent terms to the model's stationary terms. The returned bilinear and linear forms have NOT been assembled. Args: model: The model to solve. gfu_0: List of the solutions of intermediate steps in reverse step order (ie: [step n+1 sol, step 2 sol, step 1 sol, step n sol]). dt: List of current time step size. step: Which step of the Runge Kutta scheme should be assembled. Returns: Tuple[BilinearForm, LinearForm]: - a: A list of the final bilinear forms (as a BilinearForm but not assembled). - L: A list of the final linear forms (as a LinearForm but not assembled). """ U, V = model.get_trial_and_test_functions() gfu_lst = _split_gfu(gfu_0) # Define scheme-specific constants. These might eventually become user-specified constants. gamma = 0.5 * (2.0 - ngs.sqrt(2.0)) delta = -2.0 * ngs.sqrt(2.0) / 3.0 # Define the variables to hold the linear and bilinear forms a: List[BilinearForm] = [] L: List[BilinearForm] = [] if step == 1: # All the terms use the same dt. tmp_dt = dt[2] # Construct the bilinear form a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 2) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 2) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += a_ode[i] a.append(a_tmp) # Construct the linear form L_lst = model.construct_linear(V, None, tmp_dt, 2) L_imex = model.construct_imex_explicit(V, gfu_lst[3], tmp_dt, 3) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += L_lst[i] L_tmp += L_imex[i] L.append(L_tmp) elif step == 2: # All the terms use the same dt. tmp_dt = dt[1] # Construct the bilinear form a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 1) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 1) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += gamma * a_ode[i] a.append(a_tmp) # Construct the linear form L_lst_1 = model.construct_linear(V, None, tmp_dt, 1) L_lst_2 = model.construct_linear(V, gfu_lst[2], tmp_dt, 2) L_imex_2 = model.construct_imex_explicit(V, gfu_lst[2], tmp_dt, 2) L_imex_3 = model.construct_imex_explicit(V, gfu_lst[3], tmp_dt, 3) L_ode = model.construct_bilinear_time_ODE(gfu_lst[2], V, tmp_dt, 2) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += gamma * L_lst_1[i] L_tmp += (1.0 - gamma) * L_lst_2[i] L_tmp += (1.0 - delta) * L_imex_2[i] L_tmp += delta * L_imex_3[i] L_tmp += -(1.0 - gamma) * L_ode[i] L.append(L_tmp) elif step == 3: # All the terms use the same dt. tmp_dt = dt[0] # Construct the bilinear form a_lst = model.construct_bilinear_time_coefficient(U, V, tmp_dt, 0) a_ode = model.construct_bilinear_time_ODE(U, V, tmp_dt, 0) for i in range(model.num_weak_forms): a_tmp = BilinearForm(model.fes) a_tmp += a_lst[i] a_tmp += gamma * a_ode[i] a.append(a_tmp) # Construct the linear form L_lst_0 = model.construct_linear(V, gfu_lst[0], tmp_dt, 0) L_lst_2 = model.construct_linear(V, gfu_lst[2], tmp_dt, 2) L_imex_1 = model.construct_imex_explicit(V, gfu_lst[1], tmp_dt, 1) L_imex_2 = model.construct_imex_explicit(V, gfu_lst[2], tmp_dt, 2) L_ode = model.construct_bilinear_time_ODE(gfu_lst[2], V, tmp_dt, 2) for i in range(model.num_weak_forms): L_tmp = LinearForm(model.fes) L_tmp += gamma * L_lst_0[i] L_tmp += (1.0 - gamma) * L_lst_2[i] L_tmp += delta * L_imex_1[i] L_tmp += (1.0 - delta) * L_imex_2[i] L_tmp += -(1.0 - gamma) * L_ode[i] L.append(L_tmp) else: raise ValueError('RK 232 only has 3 steps. Step \'{}\' is invalid'.format(step)) _add_dt_terms(a, L, gfu_lst, model, 'RK 232') return a, L
[docs]def _add_dt_terms(a: List[BilinearForm], L: List[BilinearForm], gfu_lst: List[List[GridFunction]], model: Model, time_discretization_scheme: str) -> None: """ Function to handle the adding of the time discretization terms to the linear and bilinear forms. Args: a: List of all of the bilinear forms for the model L: List of all of the linear forms for the model gfu_lst: List of the solutions of previous time steps in reverse chronological order. model: time_discretization_scheme: The name of the time integration discretization being used. """ # Construct the time discretization terms a_dt, L_dt = model.time_derivative_terms(gfu_lst, time_discretization_scheme) # When adding the time discretization term, multiply by the phase field if using the diffuse interface method. if model.DIM: for i in range(len(a_dt)): a_dt[i] *= model.DIM_solver.phi_gfu L_dt[i] *= model.DIM_solver.phi_gfu for i in range(model.num_weak_forms): a[i] += a_dt[i] * dx L[i] += L_dt[i] * dx
[docs]def _split_gfu(gfu: List[GridFunction]) -> List[List[GridFunction]]: """ Function to separate out the various components of each gridfunction solution to a previous timestep. The first entry in gfu_lst is None so that its indexing matches the indexing for dt and the step argument in the functions for constructing the weak form (ie: gfu_lst[1], dt[1] and step=1 all refer to values at time n). The final result should be gfu_lst = [None, [component 0, component 1...] at t^n, [component 0, component 1...] at t^n-1, ...]. Args: gfu: List of gridfunctions to split up Returns: [[component 0, component 1...] at t^n, [component 0, component 1...] at t^n-1, ...] """ # NOTE: First entry is supposed to be None, see function docstring gfu_lst: List[List[GridFunction]] = [None] for i in range(len(gfu)): if len(gfu[i].components) > 0: gfu_lst.append([gfu[i].components[j] for j in range(len(gfu[i].components))]) else: gfu_lst.append([gfu[i]]) return gfu_lst