В своих предыдущих постах я исследовал, как реализовать модели регрессии гауссовского процесса, имитировать лазер и выполнить байесовскую оптимизацию с использованием Python. В этом посте мы соберем все вместе и используем байесовскую оптимизацию для оптимизации лазера!

Оптимизация лазера

Лазеры чрезвычайно сложны и непостоянны: их стабилизация занимает много времени, они зависят от большого количества параметров и требуют большого количества ресурсов просто для настройки. Например, в Linac Coherent Light Source технические специалисты должны активно настраивать несколько параметров управления для достижения определенной характеристики луча. Этот процесс настройки отнимает драгоценное время, которое можно было бы потратить на эксперименты!

Большинство алгоритмов оптимизации не подходят для настройки лазера, отчасти из-за чрезмерного количества требуемых вычислительных ресурсов, отчасти из-за уровня сложности, связанного с лазером, и отчасти из-за того, что многие алгоритмы оптимизации легко застревают в локальных минимумах. . Следовательно, чтобы настроить лазер, нам нужно выполнить оптимизацию дорогостоящей функции черного ящика, что и делает байесовская оптимизация!

В следующем разделе мы рассмотрим, как использовать байесовскую оптимизацию для оптимизации мощности накачки лазера для заданной выходной мощности.

Код Python для байесовской оптимизации лазера

Мы используем библиотеку bayes_opt для выполнения байесовской оптимизации. Лазер моделируется с помощью fdtd1d_laser, доступного в моем GitHub FDTD-репозитории.

import numpy as np
import matplotlib.pyplot as plt
from fdtd import fdtd1d_laser
from bayes_opt import BayesianOptimization, UtilityFunction

Учитывая некоторую целевую выходную мощность, мы оптимизируем наилучшую мощность насоса D0 для использования. Поэтому мы определяем функцию для запуска и измерения выходной мощности лазера для некоторой мощности накачки D0.

def run_and_measure_laser(D0, n_iter = 50000, t_measure = 10000):
    # Run 1D FDTD laser simulation for pump strength D0.
    fdtd = fdtd1d_laser(D0 = D0)
    fdtd.run(initiate_pulse = True, n_iter = n_iter)
    # "Measure" the output power.
    E = np.abs(fdtd.E_measure)[-t_measure:]
    t = np.arange(0, len(E) * fdtd.dt, fdtd.dt)
    P = np.trapz(E, t)
    return P

Затем мы определяем функцию, которая принимает входные данные target_power, а также количество итераций оптимизации для запуска. Функция запускает лазер для некоторого пробного значения D0, измеряет выходную мощность лазера и сравнивает ее с target_power. Затем оптимизатор итеративно корректирует D0, сводя к минимуму разницу между выходной мощностью и target_power.

def optimize_laser(target_power, n_iter = 15):
    # Initialize the optimizer.
    pbounds = {"D0": [1, 20]} # Search bounds.
    optimizer = BayesianOptimization(f = None, pbounds = pbounds,
                                     random_state = 42)
    # Upper confidence bounds utility function.
    utility = UtilityFunction(kind = "ucb", kappa = 1.96, xi = 0.01)
    D0s = np.array([np.nan] * n_iter)
    lasing_powers = D0s.copy()
    targets = D0s.copy()
    # Iterate the optimizer for n_iter iterations.
    for i in range(n_iter):
        # Optimizer suggests a value of D0 to try based on 
        # historical results.
        next_point = optimizer.suggest(utility)
        # Perform the lasing measurements.
        lasing_power = run_and_measure_laser(**next_point)
        # Minimize the difference between lasing power and
        # target_power.
        target = -np.abs(lasing_power - target_power)
        # Store the results for output.
        D0s[i] = next_point["D0"]
        lasing_powers[i] = lasing_power
        targets[i] = target
        try:
            # Update the optimizer with the evaluation results.
            # This needs to be in try-except structure in order
            # to prevent repeat errors from occuring.
            optimizer.register(params = next_point, target = target)
        except:
            pass
    return optimizer, D0s, lasing_powers, targets

Мы запускаем оптимизатор на 20 итераций для целевой мощности 215233.

target_power = 215233
n_iter = 20
optimization_results = optimize_laser(target_power, n_iter)
optimizer = optimization_results[0]
D0s = optimization_results[1]
lasing_powers = optimization_results[2]
targets = optimization_results[3]
# Get the best results.
D0 = optimizer.max["params"]["D0"]
target = optimizer.max["target"]
lasing_power = int(lasing_powers[np.where(D0s == D0)[0]][0])
# Plot the optimization iterations.
plt.figure(figsize = (15, 5))
plt.plot(range(1, 1+len(optimizer.space.target)), 
         optimizer.space.target, "-o")
plt.grid(True)
plt.xlabel("Iteration", fontsize = 14)
plt.ylabel("-|lasing_power - target_power|", fontsize = 14)
plt.xticks(range(1, 1+len(optimizer.space.target)))
plt.show()

Мы обнаруживаем, что в течение 20 итераций оптимизатору удалось определить, что при силе накачки D0 = 9.943 мы получаем выходную мощность lasing_power = 214871, что очень близко к исходной цели 215233.

print("Optimized pump strength value: {:.3f}.".format(D0))
print("Output power: {}.".format(lasing_power))

>>> Optimized pump strength value: 9.943. 
>>> Output power: 214871.

Заключительные замечания

В этой статье мы использовали байесовскую оптимизацию, чтобы оптимизировать мощность накачки смоделированного одномерного лазера для некоторой целевой выходной мощности. Это очень простой пример, в котором мы оптимизируем одну входную переменную для одной целевой переменной. В действительности лазеры намного сложнее, и несколько целевых переменных будут оптимизированы одновременно для набора нескольких входных переменных. Для этого нам нужно будет использовать многоцелевую оптимизацию, которая является более сложной, чем одноцелевая оптимизация, выполненная выше.

Рекомендации

Дж. Дурис, Д. Кеннеди, А. Ханука, Дж. Шталенкова, А. Эделен, А. Эггер, Т. Коуп, Д. Ратнер. Байесовская оптимизация лазера на свободных электронах, Физ. Преподобный Летт. 124, 124801, 2020.