Example #1
0
    def __ms_nlp_vars(self, options, model, V, P):
        """Rearrange decision variables to dae-compatible form,
        allowing for parallel function evaluations

        @param model awebox model
        @param V nlp decision variables
        @param P nlp parameters
        """

        # interval parameters
        param_at_time = model.parameters(cas.vertcat(P['theta0'], V['phi']))
        ms_params = cas.repmat(param_at_time, 1, self.__n_k)

        if options['parallelization']['include']:
            # use function map for rootfinder parallellization
            G_map = self.__dae.rootfinder.map(
                'G_map', options['parallelization']['type'], self.__n_k, [],
                [])
            x_root = []
            z_root = []
            p_root = []

        else:
            # compute implicit vars in for loop
            z_implicit = []

        # compute explicit values of implicit variables
        ms_vars0 = []
        for kdx in range(self.__n_k):
            # get vars at time
            var_at_time = struct_op.get_variables_at_time(
                options, V, None, model.variables, kdx)
            ms_vars0 += [var_at_time]
            # get dae vars at time
            x, z, p = self.__dae.fill_in_dae_variables(var_at_time,
                                                       param_at_time)

            if not options['parallelization']['include']:
                # compute implicit vars in for loop
                z_at_time = self.__dae.z(self.__dae.rootfinder(z, x, p))
                z_implicit = cas.horzcat(z_implicit, z_at_time)
            else:
                # store vars for parallelization
                x_root = cas.horzcat(x_root, x)
                z_root = cas.horzcat(z_root, z)
                p_root = cas.horzcat(p_root, p)

        if options['parallelization']['include']:
            # compute implicit vars in parallel fashion
            z_implicit = G_map(z_root, x_root, p_root)

        # construct list of all interval variables
        ms_vars = []
        ms_x = []
        ms_z = []
        ms_p = []

        for kdx in range(self.__n_k):
            # fill in non-lifted vars
            var_at_time = self.__set_implicit_variables(
                options, ms_vars0[kdx], param_at_time,
                self.__dae.z(z_implicit[:, kdx]))
            # update dae vars at time
            x, z, p = self.__dae.fill_in_dae_variables(var_at_time,
                                                       param_at_time)

            # store result
            ms_vars = cas.horzcat(ms_vars, var_at_time)
            ms_x = cas.horzcat(ms_x, x)
            ms_z = cas.horzcat(ms_z, z)
            ms_p = cas.horzcat(ms_p, p)

        self.__ms_params = ms_params
        self.__ms_vars = ms_vars
        self.__ms_x = ms_x
        self.__ms_z = ms_z
        self.__ms_z0 = z_implicit
        self.__ms_p = ms_p

        return None
Example #2
0
    def collocate_constraints(self, options, model, formulation, V, P, Xdot):
        """ Generate collocation and path constraints on all nodes, provide integral outputs and
            integral constraints on all nodes
        """
        # extract discretization information
        N_coll = self.__n_k * self.__d  # collocation points

        # extract model information
        variables = model.variables
        parameters = model.parameters

        # construct list of all collocation node variables and parameters
        coll_vars = []
        for kdx in range(self.__n_k):
            for ddx in range(self.__d):
                var_at_time = struct_op.get_variables_at_time(
                    options, V, Xdot, model, kdx, ddx)
                coll_vars = cas.horzcat(coll_vars, var_at_time)

        coll_params = cas.repmat(
            parameters(cas.vertcat(P['theta0'], V['phi'])), 1, N_coll)

        # evaluate dynamics and constraint functions on all intervals
        if options['parallelization']['include']:

            # use function map for parallellization
            parallellization = options['parallelization']['type']
            dynamics = model.dynamics.map('dynamics_map', parallellization,
                                          N_coll, [], [])
            path_constraints_fun = model.constraints_fun.map(
                'constraints_map', parallellization, N_coll, [], [])
            integral_outputs_fun = model.integral_outputs_fun.map(
                'integral_outputs_map', parallellization, N_coll, [], [])
            outputs_fun = model.outputs_fun.map('outputs_fun',
                                                parallellization, N_coll, [],
                                                [])

            # extract formulation information
            constraints_fun_ineq = formulation.constraints_fun['integral'][
                'inequality'].map('integral_constraints_map_ineq', 'serial',
                                  N_coll, [], [])
            constraints_fun_eq = formulation.constraints_fun['integral'][
                'equality'].map('integral_constraints_map_eq', 'serial',
                                N_coll, [], [])

            # evaluate functions
            coll_dynamics = dynamics(coll_vars, coll_params)
            coll_constraints = path_constraints_fun(coll_vars, coll_params)
            coll_outputs = outputs_fun(coll_vars, coll_params)
            integral_outputs_deriv = integral_outputs_fun(
                coll_vars, coll_params)
            integral_constraints = OrderedDict()
            integral_constraints['inequality'] = constraints_fun_ineq(
                coll_vars, coll_params)
            integral_constraints['equality'] = constraints_fun_eq(
                coll_vars, coll_params)

        else:

            # initialize function evaluations
            coll_dynamics = []
            coll_constraints = []
            coll_outputs = []
            integral_outputs_deriv = []
            integral_constraints = OrderedDict()
            integral_constraints['inequality'] = []
            integral_constraints['equality'] = []

            # evaluate functions in for loop
            for i in range(N_coll):
                coll_dynamics = cas.horzcat(
                    coll_dynamics,
                    model.dynamics(coll_vars[:, i], coll_params[:, i]))
                coll_constraints = cas.horzcat(
                    coll_constraints,
                    model.constraints_fun(coll_vars[:, i], coll_params[:, i]))
                coll_outputs = cas.horzcat(
                    coll_outputs,
                    model.outputs_fun(coll_vars[:, i], coll_params[:, i]))
                integral_outputs_deriv = cas.horzcat(
                    integral_outputs_deriv,
                    model.integral_outputs_fun(coll_vars[:, i],
                                               coll_params[:, i]))
                integral_constraints['inequality'] = cas.horzcat(
                    integral_constraints['inequality'],
                    formulation.constraints_fun['integral']['inequality'](
                        coll_vars[:, i], coll_params[:, i]))
                integral_constraints['equality'] = cas.horzcat(
                    integral_constraints['equality'],
                    formulation.constraints_fun['integral']['equality'](
                        coll_vars[:, i], coll_params[:, i]))

        # integrate integral outputs
        Integral_outputs_list = [np.zeros(model.integral_outputs.cat.shape[0])]
        Integral_constraints_list = []
        for kdx in range(self.__n_k):
            tf = struct_op.calculate_tf(options, V, kdx)
            Integral_outputs_list = self.__integrate_integral_outputs(
                Integral_outputs_list,
                integral_outputs_deriv[:, kdx * self.__d:(kdx + 1) * self.__d],
                model, tf)
            Integral_constraints_list += [
                self.__integrate_integral_constraints(integral_constraints,
                                                      kdx, tf)
            ]

        return coll_dynamics, coll_constraints, coll_outputs, Integral_outputs_list, Integral_constraints_list
Example #3
0
def discretize(nlp_numerics_options, model, formulation):

    # -----------------------------------------------------------------------------
    # discretization setup
    # -----------------------------------------------------------------------------
    nk = nlp_numerics_options['n_k']

    if nlp_numerics_options['discretization'] == 'direct_collocation':
        direct_collocation = True
        ms = False
        d = nlp_numerics_options['collocation']['d']
        scheme = nlp_numerics_options['collocation']['scheme']
        Collocation = collocation.Collocation(nk, d, scheme)
        Multiple_shooting = None

    elif nlp_numerics_options['discretization'] == 'multiple_shooting':
        direct_collocation = False
        ms = True
        Collocation = None
        dae = model.get_dae()
        Multiple_shooting = multiple_shooting.Multiple_shooting(
            nk, dae, nlp_numerics_options['integrator'])
        slacks = None

    # --------------------------------------
    # prepare model variables and dynamics
    # --------------------------------------
    variables = model.variables
    parameters = model.parameters
    variables_dict = model.variables_dict

    #---------------------------------------
    # prepare constraints structure
    #---------------------------------------
    form_constraints = formulation.constraints
    constraints_fun = formulation.constraints_fun  # initial, terminal and periodicity constraints
    path_constraints = model.constraints
    path_constraints_fun = model.constraints_fun
    g_struct = constraints.setup_constraint_structure(
        nlp_numerics_options, model,
        formulation)  # empty struct for collocated constraints

    #-------------------------------------------
    # DISCRETIZE VARIABLES, CREATE NLP PARAMETERS
    #-------------------------------------------
    V = setup_nlp_v(nlp_numerics_options, model, formulation, Collocation)
    P = setup_nlp_p(V, model)
    if direct_collocation:
        Xdot = Collocation.get_xdot(nlp_numerics_options, V, model)

    # construct time grids for this nlp
    time_grids = construct_time_grids(nlp_numerics_options)

    # ---------------------------------------
    # prepare outputs structure
    # ---------------------------------------
    outputs = model.outputs
    outputs_fun = model.outputs_fun

    [form_outputs, form_outputs_dict
     ] = performance.collect_performance_outputs(nlp_numerics_options, model,
                                                 V)
    form_outputs_fun = cas.Function('form_outputs_fun', [V, P],
                                    [form_outputs.cat])

    Outputs_struct = setup_output_structure(nlp_numerics_options, outputs,
                                            form_outputs)

    #-------------------------------------------
    # COLLOCATE CONSTRAINTS, OUTPUTS
    #-------------------------------------------

    # prepare listing of outputs and constraints
    Outputs_list = []
    g_list = []
    g_bounds = {'lb': [], 'ub': []}

    # extract model.parameters from V
    param_at_time = parameters(cas.vertcat(P['theta0'], V['phi']))
    xi = V['xi']  #TODO: don't hard code!

    # parallellize constraints on collocation nodes
    if direct_collocation:
        [
            coll_dynamics, coll_constraints, coll_outputs,
            Integral_outputs_list, Integral_constraint_list
        ] = Collocation.collocate_constraints(nlp_numerics_options, model,
                                              formulation, V, P, Xdot)

    # parallellize constraints on interval nodes
    if ms:
        [
            ms_xf, ms_z0, Xdot, ms_constraints, ms_outputs,
            Integral_outputs_list, Integral_constraint_list
        ] = Multiple_shooting.discretize_constraints(nlp_numerics_options,
                                                     model, formulation, V, P)

    # Construct list of constraints (+ bounds) and outputs
    for kdx in range(nk):

        # time constant of the following interval
        tf = struct_op.calculate_tf(nlp_numerics_options, V, kdx)

        if kdx == 0:

            # extract initial (reference) variables
            var_initial = struct_op.get_variables_at_time(
                nlp_numerics_options, V, Xdot, model, 0)
            var_ref_initial = struct_op.get_var_ref_at_time(
                nlp_numerics_options, P, V, Xdot, model, 0)

            # add initial constraints
            [g_list, g_bounds] = constraints.append_initial_constraints(
                g_list, g_bounds, form_constraints, constraints_fun,
                var_initial, var_ref_initial, xi)

        if (ms) or (direct_collocation and scheme != 'radau'):

            # at each interval node, algebraic constraints should be satisfied
            [g_list, g_bounds] = constraints.append_algebraic_constraints(
                g_list, g_bounds, dae.z(ms_z0[:, kdx]), V, kdx)

            # at each interval node, path constraints should be satisfied
            if 'us' in list(V.keys()):  # slack path constraints
                slacks = V['us', kdx]

            [g_list, g_bounds] = constraints.append_path_constraints(
                g_list, g_bounds, path_constraints, ms_constraints[:, kdx],
                slacks)

            # compute outputs for this time interval
            Outputs_list.append(ms_outputs[:, kdx])

        if direct_collocation:

            # add constraints and outputs on collocation nodes
            for ddx in range(d):

                # at each (except for first node) collocation point dynamics should meet
                [g_list,
                 g_bounds] = constraints.append_collocation_constraints(
                     g_list, g_bounds, coll_dynamics[:, kdx * d + ddx])

                # at each (except for first node) collocation node, path constraints should be satisfied
                [g_list, g_bounds] = constraints.append_path_constraints(
                    g_list, g_bounds, path_constraints,
                    coll_constraints[:, kdx * d + ddx])

                # compute outputs for this time interval
                Outputs_list.append(coll_outputs[:, kdx * d + ddx])

            # endpoint should match next start point
            [g_list, g_bounds] = Collocation.append_continuity_constraint(
                g_list, g_bounds, V, kdx)

        elif ms:

            # endpoint should match next start point
            [g_list,
             g_bounds] = Multiple_shooting.append_continuity_constraint(
                 g_list, g_bounds, ms_xf, V, kdx)

    # extract terminal (reference) variables
    var_terminal = struct_op.get_variables_at_final_time(
        nlp_numerics_options, V, Xdot, model)
    var_ref_terminal = struct_op.get_var_ref_at_final_time(
        nlp_numerics_options, P, Xdot, model)

    # add terminal and periodicity constraints
    [g_list, g_bounds] = constraints.append_terminal_constraints(
        g_list, g_bounds, form_constraints, constraints_fun, var_terminal,
        var_ref_terminal, xi)
    [g_list, g_bounds] = constraints.append_periodic_constraints(
        g_list, g_bounds, form_constraints, constraints_fun, var_initial,
        var_terminal)

    if direct_collocation:
        [g_list, g_bounds] = constraints.append_integral_constraints(
            nlp_numerics_options, g_list, g_bounds, Integral_constraint_list,
            form_constraints, constraints_fun, V, Xdot, model,
            formulation.integral_constants)

    Outputs_list.append(form_outputs_fun(V, P))

    # Create Outputs struct and function
    Outputs = Outputs_struct(cas.vertcat(*Outputs_list))
    Outputs_fun = cas.Function('Outputs_fun', [V, P], [Outputs.cat])

    # Create Integral outputs struct and function
    Integral_outputs_struct = setup_integral_output_structure(
        nlp_numerics_options, model.integral_outputs)
    Integral_outputs = Integral_outputs_struct(
        cas.vertcat(*Integral_outputs_list))
    Integral_outputs_fun = cas.Function('Integral_outputs_fun', [V, P],
                                        [Integral_outputs.cat])

    # Create g struct and functions and g_bounds vectors
    [g, g_fun, g_jacobian_fun,
     g_bounds] = constraints.create_constraint_outputs(g_list, g_bounds,
                                                       g_struct, V, P)

    Xdot_struct = struct_op.construct_Xdot_struct(nlp_numerics_options, model)
    Xdot_fun = cas.Function('Xdot_fun', [V], [Xdot])

    return V, P, Xdot_struct, Xdot_fun, g_struct, g_fun, g_jacobian_fun, g_bounds, Outputs_struct, Outputs_fun, Integral_outputs_struct, Integral_outputs_fun, time_grids, Collocation, Multiple_shooting
Example #4
0
def test_integrators():

    # ===========================================
    # SET-UP DIRECT COLLOCATION PROBLEM AND SOLVE
    # ===========================================

    # make default options object
    base_options = awe.Options(True) # True refers to internal access switch

    # choose simplest model
    base_options['user_options']['system_model']['architecture'] = {1:0}
    base_options['user_options']['system_model']['kite_dof'] = 3
    base_options['user_options']['kite_standard'] = awe.ampyx_data.data_dict()
    base_options['user_options']['tether_drag_model'] = 'split'
    base_options['user_options']['induction_model'] = 'not_in_use'
    
    # specify direct collocation options
    base_options['nlp']['n_k'] = 40
    base_options['nlp']['discretization'] = 'direct_collocation'
    base_options['nlp']['collocation']['u_param'] = 'zoh'
    base_options['nlp']['collocation']['scheme'] = 'radau'
    base_options['nlp']['collocation']['d'] = 4

    base_options['model']['tether']['control_var'] = 'dddl_t'

     # homotopy tuning
    base_options['solver']['mu_hippo'] = 1e-4
    base_options['solver']['tol_hippo'] = 1e-4

    # make trial, build and run
    trial = awe.Trial(name = 'test', seed = base_options)
    trial.build()
    trial.optimize()

    # extract solution data
    V_final = trial.optimization.V_opt
    P       = trial.optimization.p_fix_num
    Int_outputs = trial.optimization.integral_output_vals[1]
    model   = trial.model
    dae     = model.get_dae()

    # build dae variables for t = 0 within first shooting interval
    variables0 = struct_op.get_variables_at_time(base_options['nlp'], V_final, None, model.variables, 0)
    parameters = model.parameters(vertcat(P['theta0'], V_final['phi']))
    x0, z0, p  = dae.fill_in_dae_variables(variables0, parameters)

    # ===================================
    # TEST COLLOCATION INTEGRATOR
    # ===================================

    # set discretization to multiple shooting
    base_options['nlp']['discretization'] = 'multiple_shooting'
    base_options['nlp']['integrator']['type'] = 'collocation'
    base_options['nlp']['integrator']['collocation_scheme'] = base_options['nlp']['collocation']['scheme']
    base_options['nlp']['integrator']['interpolation_order'] = base_options['nlp']['collocation']['d']
    base_options['nlp']['integrator']['num_steps'] = 1

    # switch off expand to allow for use of integrator in NLP
    base_options['solver']['expand_overwrite'] = False

    # build MS trial
    trialColl = awe.Trial(name = 'testColl', seed = base_options)
    trialColl.build()

    # multiple shooting dae integrator
    F = trialColl.nlp.Multiple_shooting.F

    # integrate over one interval
    Ff = F(x0 = x0, z0 = z0, p = p)
    xf = Ff['xf']
    zf = Ff['zf']
    qf = Ff['qf']

    # evaluate integration error
    err_coll_x = np.max(np.abs(np.divide((xf - V_final['xd',1]), V_final['xd',1]).full()))
    xa = dae.z(zf)['xa']
    err_coll_z = np.max(np.abs(np.divide(dae.z(zf)['xa'] - V_final['coll_var',0, -1, 'xa'], V_final['coll_var',0, -1, 'xa']).full()))
    err_coll_q = np.max(np.abs(np.divide((qf - Int_outputs['int_out',1]), Int_outputs['int_out',1]).full()))

    tolerance = 1e-8

    # values should match up to nlp solver accuracy
    assert(err_coll_x < tolerance)
    assert(err_coll_z < tolerance)
    assert(err_coll_q < tolerance)

    # ===================================
    # TEST RK4-ROOT INTEGRATOR
    # ===================================

    # set discretization to multiple shooting
    base_options['nlp']['integrator']['type'] = 'rk4root'
    base_options['nlp']['integrator']['num_steps'] = 20

    # build MS trial
    trialRK = awe.Trial(name = 'testRK', seed = base_options)
    trialRK.build()

    # multiple shooting dae integrator
    F = trialRK.nlp.Multiple_shooting.F

    # integrate over one interval
    Ff = F(x0 = x0, z0 = z0, p = p)
    xf = Ff['xf']
    zf = Ff['zf']
    qf = Ff['qf']

    # evaluate 
    err_rk_x = np.max(np.abs(np.divide((xf - V_final['xd',1]), V_final['xd',1]).full()))
    xa = dae.z(zf)['xa']
    err_rk_z = np.max(np.abs(np.divide(dae.z(zf)['xa'] - V_final['coll_var',0, -1, 'xa'], V_final['coll_var',0, -1, 'xa']).full()))
    err_rk_q = np.max(np.abs(np.divide((qf - Int_outputs['int_out',1]), Int_outputs['int_out',1]).full()))

    # error should be below 1%
    assert(err_rk_x < 1e-2)
    assert(err_rk_z < 1e-2)
    assert(err_rk_q < 2e-2)
Example #5
0
def get_strength_constraint(options, V, Outputs, model):

    n_k = options['n_k']
    d = options['collocation']['d']

    comparison_labels = options['induction']['comparison_labels']
    wake_nodes = options['induction']['vortex_wake_nodes']
    rings = wake_nodes - 1
    kite_nodes = model.architecture.kite_nodes

    strength_scale = tools.get_strength_scale(options)

    Xdot = struct_op.construct_Xdot_struct(options, model.variables_dict)(0.)

    cstr_list = cstr_op.ConstraintList()

    any_vor = any(label[:3] == 'vor' for label in comparison_labels)
    if any_vor:

        for kite in kite_nodes:
            for ring in range(rings):
                wake_node = ring

                for ndx in range(n_k):
                    for ddx in range(d):

                        local_name = 'vortex_strength_' + str(kite) + '_' + str(ring) + '_' + str(ndx) + '_' + str(ddx)

                        variables = struct_op.get_variables_at_time(options, V, Xdot, model.variables, ndx, ddx)
                        wg_local = tools.get_ring_strength(variables, kite, ring)

                        ndx_shed = n_k - 1 - wake_node
                        ddx_shed = d - 1

                        # working out:
                        # n_k = 3
                        # if ndx = 0 and ddx = 0 -> shed: wn >= n_k
                        #     wn: 0 sheds at ndx = 2, ddx = -1 : unshed,    period = 0
                        #     wn: 1 sheds at ndx = 1, ddx = -1 : unshed,    period = 0
                        #     wn: 2 sheds at ndx = 0, ddx = -1 : unshed,    period = 0
                        #     wn: 3 sheds at ndx = -1, ddx = -1 : SHED      period = 1
                        #     wn: 4 sheds at ndx = -2,                      period = 1
                        #     wn: 5 sheds at ndx = -3                       period = 1
                        #     wn: 6 sheds at ndx = -4                       period = 2
                        # if ndx = 1 and ddx = 0 -> shed: wn >= n_k - ndx
                        #     wn: 0 sheds at ndx = 2, ddx = -1 : unshed,
                        #     wn: 1 sheds at ndx = 1, ddx = -1 : unshed,
                        #     wn: 2 sheds at ndx = 0, ddx = -1 : SHED,
                        #     wn: 3 sheds at ndx = -1, ddx = -1 : SHED
                        # if ndx = 0 and ddx = -1 -> shed:
                        #     wn: 0 sheds at ndx = 2, ddx = -1 : unshed,
                        #     wn: 1 sheds at ndx = 1, ddx = -1 : unshed,
                        #     wn: 2 sheds at ndx = 0, ddx = -1 : SHED,
                        #     wn: 3 sheds at ndx = -1, ddx = -1 : SHED

                        already_shed = False
                        if (ndx > ndx_shed):
                            already_shed = True
                        elif ((ndx == ndx_shed) and (ddx == ddx_shed)):
                            already_shed = True

                        if already_shed:

                            # working out:
                            # n_k = 3
                            # period_0 -> wn 0, wn 1, wn 2 -> floor(ndx_shed / n_k)
                            # period_1 -> wn 3, wn 4, wn 5

                            period_number = int(np.floor(float(ndx_shed)/float(n_k)))
                            ndx_shed_w_periodicity = ndx_shed - period_number * n_k

                            gamma_val = Outputs['coll_outputs', ndx_shed_w_periodicity, ddx_shed, 'aerodynamics', 'circulation' + str(kite)]
                            wg_ref = 1. * gamma_val / strength_scale
                        else:
                            wg_ref = 0.

                        local_resi = (wg_local - wg_ref)

                        local_cstr = cstr_op.Constraint(expr = local_resi,
                                                        name = local_name,
                                                        cstr_type='eq')
                        cstr_list.append(local_cstr)

    return cstr_list
Example #6
0
def get_fixing_constraint(options, V, Outputs, model):

    n_k = options['n_k']

    comparison_labels = options['induction']['comparison_labels']
    wake_nodes = options['induction']['vortex_wake_nodes']
    kite_nodes = model.architecture.kite_nodes
    wingtips = ['ext', 'int']

    Xdot = struct_op.construct_Xdot_struct(options, model.variables_dict)(0.)

    cstr_list = cstr_op.ConstraintList()

    any_vor = any(label[:3] == 'vor' for label in comparison_labels)
    if any_vor:

        for kite in kite_nodes:
            for tip in wingtips:
                for wake_node in range(wake_nodes):
                    local_name = 'wake_fixing_' + str(kite) + '_' + str(tip) + '_' + str(wake_node)

                    if wake_node < n_k:

                        # working out:
                        # n_k = 3
                        # wn:0, n_k-1=2
                        # wn:1, n_k-2=1
                        # wn:2=n_k-1, n_k-3=0
                        # ... switch to periodic fixing

                        reverse_index = n_k - 1 - wake_node
                        variables_at_shed = struct_op.get_variables_at_time(options, V, Xdot, model.variables,
                                                                            reverse_index, -1)

                        wx_local = tools.get_wake_node_position_si(options, variables_at_shed, kite, tip, wake_node)
                        wingtip_pos = Outputs[
                            'coll_outputs', reverse_index, -1, 'aerodynamics', 'wingtip_' + tip + str(kite)]

                        local_resi = wx_local - wingtip_pos

                        local_cstr = cstr_op.Constraint(expr = local_resi,
                                                        name = local_name,
                                                        cstr_type='eq')
                        cstr_list.append(local_cstr)

                    else:

                        # working out:
                        # n_k = 3
                        # wn:0, n_k-1=2
                        # wn:1, n_k-2=1
                        # wn:2=n_k-1, n_k-3=0
                        # ... switch to periodic fixing
                        # wn:3 at ndx = 0 must be equal to -> wn:0 at ndx = -1, ddx = -1
                        # wn:4 at ndx = 0 must be equal to -> wn:1 at ndx = -1, ddx = -1

                        variables_at_initial = struct_op.get_variables_at_time(options, V, Xdot, model.variables, 0)
                        variables_at_final = struct_op.get_variables_at_time(options, V, Xdot, model.variables, -1, -1)

                        upstream_node = wake_node - n_k
                        wx_local = tools.get_wake_node_position_si(options, variables_at_initial, kite, tip, wake_node)
                        wx_upstream = tools.get_wake_node_position_si(options, variables_at_final, kite, tip, upstream_node)

                        local_resi = wx_local - wx_upstream
                        local_cstr = cstr_op.Constraint(expr = local_resi,
                                                        name = local_name,
                                                        cstr_type='eq')
                        cstr_list.append(local_cstr)

    return cstr_list