ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$i_{\mathrm{abc}}\,/\,\mathrm{A}$')
        ax.grid(which='both')
        plt.legend(handles=ax.lines[::3],
                   labels=('Measurement abc', 'Setpoint dq0'))
        fig.show()

    # Define the environment
    env = gym.make(
        'openmodelica_microgrid_gym:ModelicaEnv_test-v1',
        viz_mode='episode',
        viz_cols=[
            PlotTmpl(
                [[f'lcl1.inductor{i}.i'
                  for i in '123'], [f'slave.SPI{i}' for i in 'dq0']],
                callback=update_legend,
                color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                style=[[None], ['--']],
                title=
                'Example of using an timevariant external current reference'),
        ],
        log_level=logging.INFO,
        max_episode_steps=max_episode_steps,
        model_params={
            'rl1.resistor1.R': partial(load_step, gain=20),
            'rl1.resistor2.R': partial(load_step, gain=20),
            'rl1.resistor3.R': partial(load_step, gain=20),
            'rl1.inductor1.L': 0.001,
            'rl1.inductor2.L': 0.001,
            'rl1.inductor3.L': 0.001
        },
        model_path='../omg_grid/grid.network.fmu',
Exemple #2
0
            r_filt.reset()
            l_filt.reset()
            c_filt.reset()

        plotter = PlotManager(agent,
                              save_results=save_results,
                              save_folder=save_folder,
                              show_plots=show_plots)

        env = gym.make(
            'openmodelica_microgrid_gym:ModelicaEnv_test-v1',
            reward_fun=rew.rew_fun_vc,
            viz_cols=[
                PlotTmpl([[f'lc.capacitor{i}.v'
                           for i in '123'], [f'master.SPV{i}' for i in 'abc']],
                         callback=plotter.xylables_v_abc,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl([[f'master.CVV{i}'
                           for i in 'dq0'], [f'master.SPV{i}' for i in 'dq0']],
                         callback=plotter.xylables_v_dq0,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl([[f'lc.inductor{i}.i'
                           for i in '123'], [f'master.SPI{i}' for i in 'abc']],
                         callback=plotter.xylables_i_abc,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl(
                    [
                        [f'master.I_hat{i}' for i in 'abc'],
Exemple #3
0

    def xylables_freq(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$f_{\mathrm{slave}}\,/\,\mathrm{Hz}$')
        ax.grid(which='both')
        time = strftime("%Y-%m-%d %H_%M_%S", gmtime())
        fig.savefig(save_folder + '/f_slave' + time + '.pdf')


    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=xylables_i
                                ),
                       PlotTmpl([f'lc1.capacitor{i}.v' for i in '123'],
                                callback=xylables_v_abc
                                ),
                       PlotTmpl([f'master.instPow'],
                                callback=xylables_P_master
                                ),
                       PlotTmpl([f'slave.instPow'],
                                callback=xylables_P_slave
                                ),
                       PlotTmpl([f'slave.freq'],
                                callback=xylables_freq
                                ),
                       PlotTmpl([f'master.CVV{i}' for i in 'dq0'],
                                callback=xylables_v_dq0
Exemple #4
0
    # - inputs to the models are the connection points to the inverters (see user guide for more details)
    # - model outputs are the the 3 currents through the inductors and the 3 voltages across the capacitors


    def xylables(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$i_{\mathrm{abc}}\,/\,\mathrm{A}$')
        ax.grid(which='both')
        fig.show()
        # fig.savefig('Inductor_currents.pdf')

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=xylables)
                   ],
                   log_level=logging.INFO,
                   viz_mode='episode',
                   max_episode_steps=max_episode_steps,
                   net=net,
                   model_path='../omg_grid/grid.network_singleInverter.fmu',
                   history=FullHistory())

    #####################################
    # Execution of the experiment
    # Using a runner to execute 'num_episodes' different episodes (i.e. SafeOpt iterations)
    runner = Runner(agent, env)

    runner.run(num_episodes, visualise=True)
Exemple #5
0

    def xylables_v_dq0(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$v_{\mathrm{dq0}}\,/\,\mathrm{V}$')
        ax.grid(which='both')
        time = strftime("%Y-%m-%d %H_%M_%S", gmtime())
        fig.savefig(save_folder + '/dq0_voltage' + time + '.pdf')


    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=xylables_i
                                ),
                       PlotTmpl([f'lc1.capacitor{i}.v' for i in '123'],
                                callback=xylables_v_abc
                                ),
                       PlotTmpl([f'master.CVV{i}' for i in 'dq0'],
                                callback=xylables_v_dq0
                                )
                   ],
                   log_level=logging.INFO,
                   viz_mode='episode',
                   max_episode_steps=max_episode_steps,
                   net=net,
                   model_path='../omg_grid/grid.network_singleInverter.fmu',
                   history=FullHistory()
                   )

    def xylables_i_dq0(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$i_{\mathrm{dq0}}\,/\,\mathrm{i}$')
        ax.grid(which='both')


    callback = LoadstepCallback()

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=xylables
                                ),
                       PlotTmpl([f'rl1.resistor{i}.R' for i in '123'],
                                callback=xylables_R
                                ),
                       PlotTmpl([f'master.CVI{s}' for s in 'dq0'],
                                ),
                       PlotTmpl([f'rl1.inductor{i}.L' for i in '123'],
                                callback=xylables_L
                                )
                   ],
                   log_level=logging.INFO,
                   viz_mode='episode',
                   model_params={'rl1.resistor1.R': callback.load_step_resistance,  # see LoadstepCallback(Callback)
                                 'rl1.resistor2.R': callback.load_step_resistance,
                                 'rl1.resistor3.R': callback.load_step_resistance,
Exemple #7
0
if __name__ == '__main__':

    def second_plot(fig):
        ax = fig.gca()
        ax.set_ylabel('y label!')
        ax.set_xlabel('$t\,/\,\mathrm{ms}$')
        fig.savefig('plot2.pdf')

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv-v1',
                   viz_mode='episode',
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=lambda fig: fig.savefig('plot.pdf'),
                                linewidth=4,
                                style=[None, '--', '*'],
                                linestyle=['None', None, None],
                                marker=[r'$\heartsuit$', None, None],
                                c=['pink', None, None],
                                title='test'),
                       PlotTmpl(['lc1.inductor1.i', 'lc1.inductor2.i'],
                                callback=second_plot,
                                legend=[False, True],
                                label=[None, 'something'])
                   ],
                   max_episode_steps=None,
                   net='../net/net.yaml',
                   model_path='../omg_grid/grid.network.fmu')

    env.reset()
    for _ in range(100):
        env.render()
            if t >= .05:
                i_ref[:] = i_ref2
            else:
                i_ref[:] = i_ref1

            return partial(l_load.give_value, n=2)(t)


        env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                       # reward_fun=Reward().rew_fun,
                       reward_fun=rew.rew_fun_c,
                       # time_step=delta_t,
                       viz_cols=[
                           PlotTmpl([[f'lc.inductor{i}.i' for i in '123'], [f'master.SPI{i}' for i in 'abc']],
                                    callback=plotter.xylables_i_abc,
                                    color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                                    style=[[None], ['--']]
                                    ),
                           PlotTmpl([[f'master.m{i}' for i in 'abc']],
                                    callback=lambda fig: plotter.update_axes(fig, title='Simulation',
                                                                             ylabel='$m_{\mathrm{abc}}\,/\,\mathrm{}$')

                                    ),
                           PlotTmpl([[f'master.CVI{i}' for i in 'dq0'], [f'master.SPI{i}' for i in 'dq0']],
                                    callback=plotter.xylables_i_dq0,
                                    color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                                    style=[[None], ['--']]
                                    )
                       ],
                       log_level=logging.INFO,
                       viz_mode='episode',
import pytest

from openmodelica_microgrid_gym.env import PlotTmpl

v = [['a', 'b'], ['c', 'd']]
tmpl = PlotTmpl(v, color=[None, ['C2', 'C1']], style=[None, '--'])


@pytest.mark.parametrize(
    'i,o',
    [[[k for k in PlotTmpl(v)],
      [('a', dict(c='C1')), ('b', dict(c='C2')), ('c', dict(c='C1')),
       ('d', dict(c='C2'))]],
     [[k for k in PlotTmpl(v, c=[None, ['C2', 'C1']])],
      [('a', dict(c='C1')), ('b', dict(c='C2')), ('c', dict(c='C2')),
       ('d', dict(c='C1'))]],
     [[k for k in PlotTmpl(v, color=[None, ['C2', 'C1']])],
      [('a', dict(color='C1')), ('b', dict(color='C2')),
       ('c', dict(color='C2')), ('d', dict(color='C1'))]],
     [tmpl[2],
      ('c', dict(color='C2', style='--'))], [tmpl[1],
                                             ('b', dict(color='C2'))]])
def test_plot_tmpl(i, o):
    assert i == o
Exemple #10
0
        ax.set_ylabel('$R_{\mathrm{load}}\,/\,\mathrm{\Omega}$')
        ax.grid(which='both')
        ax.set_ylim([lower_bound_load - 2, upper_bound_load + 2])
        plt.title(
            'Load example drawn from Ornstein-Uhlenbeck process \n- Clipping outside the shown y-range'
        )
        plt.legend()
        fig.show()

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   net=net,
                   model_params={
                       'rl1.resistor1.R': load_step,
                       'rl1.resistor2.R': load_step,
                       'rl1.resistor3.R': load_step
                   },
                   viz_cols=[
                       PlotTmpl([f'rl1.resistor{i}.R' for i in '123'],
                                callback=xylables)
                   ],
                   model_path='../omg_grid/grid.network.fmu')

    env.reset()
    for _ in range(1000):
        env.render()
        obs, rew, done, info = env.step(
            env.action_space.sample())  # take a random action
        if done:
            break
    env.close()
Exemple #11
0
        ax.grid(which='both')
        time = strftime("%Y-%m-%d %H_%M_%S", gmtime())
        fig.savefig(save_folder + '/dq0_voltage' + time + '.pdf')

    def xylables_L(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')
        ax.set_ylabel('$L_{\mathrm{123}}\,/\,\mathrm{H}$')
        ax.grid(which='both')

    callback = LoadstepCallback()

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'lc1.inductor{i}.i' for i in '123'],
                                callback=xylables_i),
                       PlotTmpl([f'lc1.capacitor{i}.v' for i in '123'],
                                callback=xylables_v_abc),
                       PlotTmpl([f'rl1.resistor{i}.R' for i in '123'],
                                callback=xylables_R),
                       PlotTmpl([f'master.CVi{s}' for s in 'dq0'],
                                callback=xylables_i_dq0),
                       PlotTmpl([f'master.CVV{i}' for i in 'dq0'],
                                callback=xylables_v_dq0),
                       PlotTmpl([f'rl1.inductor{i}.L' for i in '123'],
                                callback=xylables_L)
                   ],
                   log_level=logging.INFO,
                   viz_mode='episode',
                   model_params={
                       'rl1.resistor1.R': callback.load_step_resistance,
Exemple #12
0
            r_filt.reset()
            l_filt.reset()
            c_filt.reset()

        plotter = PlotManager(agent,
                              save_results=save_results,
                              save_folder=save_folder,
                              show_plots=show_plots)

        env = gym.make(
            'openmodelica_microgrid_gym:ModelicaEnv_test-v1',
            reward_fun=rew.rew_fun_v,
            viz_cols=[
                PlotTmpl([[f'lc.capacitor{i}.v'
                           for i in '123'], [f'master.SPV{i}' for i in 'abc']],
                         callback=plotter.xylables_v_abc,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl([[f'master.CVV{i}'
                           for i in 'dq0'], [f'master.SPV{i}' for i in 'dq0']],
                         callback=plotter.xylables_v_dq0,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl([[f'lc.inductor{i}.i'
                           for i in '123'], [f'master.SPI{i}' for i in 'abc']],
                         callback=plotter.xylables_i_abc,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                # PlotTmpl([[f'master.I_hat{i}' for i in 'abc'], [f'r_load.resistor{i}.i' for i in '123'], ],
                #         callback=lambda fig: plotter.update_axes(fig, title='Simulation',
                #                                                  ylabel='$i_{\mathrm{o estimate,abc}}\,/\,\mathrm{A}$'),
Exemple #13
0

    def xylables_L(fig):
        ax = fig.gca()
        ax.set_xlabel(r'$t\,/\,\mathrm{s}$')  # zeit
        ax.set_ylabel('$L_{\mathrm{123}}\,/\,\mathrm{H}$')
        ax.grid(which='both')


    callback = LoadstepCallback()

    env = gym.make('openmodelica_microgrid_gym:ModelicaEnv_test-v1',
                   reward_fun=Reward().rew_fun,
                   viz_cols=[
                       PlotTmpl([f'slave.freq'],
                                callback=xylables_freq
                                ),
                       PlotTmpl([f'master.CVV{i}' for i in 'dq0'],
                                callback=xylables_v_dq0
                                ),
                       PlotTmpl([f'rl1.resistor{i}.R' for i in '123'],  # Plot Widerstand RL
                                callback=xylables_R
                                ),
                       PlotTmpl([f'rl1.inductor{i}.L' for i in '123'],  # Plot Widerstand RL
                                callback=xylables_L
                                ),
                   ],
                   log_level=logging.INFO,
                   viz_mode='episode',
                   max_episode_steps=max_episode_steps,
                   model_params={'rl1.resistor1.R': callback.load_step_resistance,
def run_experiment(len_kp, len_ki):
    if isfile(f'{save_folder}/{len_kp:.4f},{len_ki:.4f}.txt'):
        with open(f'{save_folder}/{len_kp:.4f},{len_ki:.4f}.txt', 'r') as f:
            return strtobool(f.read().strip())

    rew = Reward(i_limit=iLimit,
                 i_nominal=iNominal,
                 mu_c=mu,
                 max_episode_steps=max_episode_steps,
                 obs_dict=[[f'lc.inductor{k}.i' for k in '123'],
                           'master.phase', [f'master.SPI{k}' for k in 'dq0']])

    #####################################
    # Definitions for the GP
    prior_mean = 0  # 2  # mean factor of the GP prior mean which is multiplied with the first performance of the
    # initial set
    noise_var = 0.001  # 0.001 ** 2  # measurement noise sigma_omega
    prior_var = 2  # prior variance of the GP

    bounds = None
    lengthscale = None
    if adjust == 'Kp':
        bounds = [(0.0001, 0.1)]  # bounds on the input variable Kp
        lengthscale = [
            .025
        ]  # length scale for the parameter variation [Kp] for the GP

    # For 1D example, if Ki should be adjusted
    if adjust == 'Ki':
        bounds = [(0, 20)]  # bounds on the input variable Ki
        lengthscale = [
            10
        ]  # length scale for the parameter variation [Ki] for the GP

    # For 2D example, choose Kp and Ki as mutable parameters (below) and define bounds and lengthscale for both of them
    if adjust == 'Kpi':
        bounds = [(0.001, 0.07), (2, 150)]
        lengthscale = [0.012, 30.]

    df_len = pd.DataFrame({
        'lengthscale': lengthscale,
        'bounds': bounds,
        'balanced_load': balanced_load,
        'barrier_param_mu': mu
    })

    # The performance should not drop below the safe threshold, which is defined by the factor safe_threshold times
    # the initial performance: safe_threshold = 0.8 means. Performance measurement for optimization are seen as
    # unsafe, if the new measured performance drops below 20 % of the initial performance of the initial safe (!)
    # parameter set
    safe_threshold = 0
    j_min = cal_j_min(phase_shift, amp_dev)  # Used for normalization

    # The algorithm will not try to expand any points that are below this threshold. This makes the algorithm stop
    # expanding points eventually.
    # The following variable is multiplied with the first performance of the initial set by the factor below:
    explore_threshold = 0

    # Factor to multiply with the initial reward to give back an abort_reward-times higher negative reward in case of
    # limit exceeded
    # has to be negative due to normalized performance (regarding J_init = 1)
    abort_reward = 100 * j_min

    # Definition of the kernel
    kernel = GPy.kern.Matern32(input_dim=len(bounds),
                               variance=prior_var,
                               lengthscale=lengthscale,
                               ARD=True)

    #####################################
    # Definition of the controllers
    mutable_params = None
    current_dqp_iparams = None
    if adjust == 'Kp':
        # mutable_params = parameter (Kp gain of the current controller of the inverter) to be optimized using
        # the SafeOpt algorithm
        mutable_params = dict(currentP=MutableFloat(0.04))

        # Define the PI parameters for the current controller of the inverter
        current_dqp_iparams = PI_params(kP=mutable_params['currentP'],
                                        kI=12,
                                        limits=(-1, 1))

    # For 1D example, if Ki should be adjusted
    elif adjust == 'Ki':
        mutable_params = dict(currentI=MutableFloat(5))
        current_dqp_iparams = PI_params(kP=0.005,
                                        kI=mutable_params['currentI'],
                                        limits=(-1, 1))

    # For 2D example, choose Kp and Ki as mutable parameters
    elif adjust == 'Kpi':
        mutable_params = dict(currentP=MutableFloat(0.04),
                              currentI=MutableFloat(11.8))
        current_dqp_iparams = PI_params(kP=mutable_params['currentP'],
                                        kI=mutable_params['currentI'],
                                        limits=(-1, 1))

    # Define a current sourcing inverter as master inverter using the pi and droop parameters from above
    ctrl = MultiPhaseDQCurrentSourcingController(current_dqp_iparams,
                                                 delta_t,
                                                 undersampling=undersample,
                                                 name='master',
                                                 f_nom=net.freq_nom)

    i_ref = MutableParams([MutableFloat(f) for f in i_ref1])
    #####################################
    # Definition of the optimization agent
    # The agent is using the SafeOpt algorithm by F. Berkenkamp (https://arxiv.org/abs/1509.01066) in this example
    # Arguments described above
    # History is used to store results
    agent = SafeOptAgent(
        mutable_params,
        abort_reward,
        j_min,
        kernel,
        dict(bounds=bounds,
             noise_var=noise_var,
             prior_mean=prior_mean,
             safe_threshold=safe_threshold,
             explore_threshold=explore_threshold),
        [ctrl],
        dict(master=[[f'lc.inductor{k}.i' for k in '123'], i_ref]),
        history=FullHistory(),
    )

    #####################################
    # Definition of the environment using a FMU created by OpenModelica
    # (https://www.openmodelica.org/)
    # Using an inverter supplying a load
    # - using the reward function described above as callable in the env
    # - viz_cols used to choose which measurement values should be displayed (here, only the 3 currents across the
    #   inductors of the inverters are plotted. Labels and grid is adjusted using the PlotTmpl (For more information,
    #   see UserGuide)
    # - inputs to the models are the connection points to the inverters (see user guide for more details)
    # - model outputs are the the 3 currents through the inductors and the 3 voltages across the capacitors

    if include_simulate:

        # Defining unbalanced loads sampling from Gaussian distribution with sdt = 0.2*mean
        # r_load = Load(R, 0.1 * R, balanced=balanced_load, tolerance=0.1)
        # l_load = Load(L, 0.1 * L, balanced=balanced_load, tolerance=0.1)
        # i_noise = Noise([0, 0, 0], [0.0023, 0.0015, 0.0018], 0.0005, 0.32)

        # if no noise should be included:
        r_load = Load(R, 0 * R, balanced=balanced_load)
        l_load = Load(L, 0 * L, balanced=balanced_load)

        def reset_loads():
            r_load.reset()
            l_load.reset()

        plotter = PlotManager(agent,
                              save_results=save_results,
                              save_folder=save_folder,
                              show_plots=show_plots)

        def ugly_foo(t):

            if t >= .05:
                i_ref[:] = i_ref2
            else:
                i_ref[:] = i_ref1

            return partial(l_load.give_value, n=2)(t)

        env = gym.make(
            'openmodelica_microgrid_gym:ModelicaEnv_test-v1',
            # reward_fun=Reward().rew_fun,
            reward_fun=rew.rew_fun_c,
            # time_step=delta_t,
            viz_cols=[
                PlotTmpl([[f'lc.inductor{i}.i'
                           for i in '123'], [f'master.SPI{i}' for i in 'abc']],
                         callback=plotter.xylables_i_abc,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']]),
                PlotTmpl([[f'master.m{i}' for i in 'abc']],
                         callback=lambda fig: plotter.update_axes(
                             fig,
                             title='Simulation',
                             ylabel='$m_{\mathrm{abc}}\,/\,\mathrm{}$')),
                PlotTmpl([[f'master.CVI{i}'
                           for i in 'dq0'], [f'master.SPI{i}' for i in 'dq0']],
                         callback=plotter.xylables_i_dq0,
                         color=[['b', 'r', 'g'], ['b', 'r', 'g']],
                         style=[[None], ['--']])
            ],
            log_level=logging.INFO,
            viz_mode='episode',
            max_episode_steps=max_episode_steps,
            model_params={
                'lc.resistor1.R': partial(r_load.give_value, n=0),
                'lc.resistor2.R': partial(r_load.give_value, n=1),
                'lc.resistor3.R': partial(r_load.give_value, n=2),
                'lc.inductor1.L': partial(l_load.give_value, n=0),
                'lc.inductor2.L': partial(l_load.give_value, n=1),
                'lc.inductor3.L': ugly_foo
            },
            model_path='../../omg_grid/grid.paper.fmu',
            # model_path='../omg_grid/omg_grid.Grids.Paper_SC.fmu',
            net=net,
            history=FullHistory(),
            action_time_delay=1 * undersample)

        runner = MonteCarloRunner(agent, env)

        runner.run(num_episodes,
                   n_mc=n_MC,
                   visualise=True,
                   prepare_mc_experiment=reset_loads)

        with open(f'{save_folder}/{len_kp:.4f},{len_ki:.4f}.txt', 'w') as f:
            print(f'{agent.unsafe}', file=f)

        return agent.unsafe