POPC

1-palmitoyl-2-oleoylphosphatidylcholine (POPC) is a phospholipid commonly used in biomolecular simulations due to its prevalence in biological membranes. Optimizing its parameters is crucial for accurately representing its behavior in molecular dynamics simulations. (Full scripts used in this example can be found in here.)

Optimization Objective

The primary objective of this optimization process is to fine-tune the force field parameters of the POPC system to match its behavior in molecular dynamics simulations with experimental data and fine-grained simulation. Key properties targeted for optimization include membrane thickness, area per lipid, and potential of mean force U(r).

Note

To fit the microscopic properties from fine-grained simulaiton, it is needed to prepare the fine-grained trajectory and topology file in advance.

Optimization Workflow

  1. System Setup: The molecular topology of the POPC system is generated based on provided structural information. Bond and angle parameters are adjusted to ensure the proper representation of POPC molecules.

  2. Objective Function Definition: An objective function is defined to quantify the deviation between simulation results and target vaules. This function computes various terms, including force match loss, U(r) Boltzmann inversion loss, membrane thickness loss, area per lipid loss.

  3. Optimization Execution: The optimization process is executed using the Particle Swarm Optimization technique. The optimizer iteratively adjusts the force field parameters to minimize the total loss function.

  4. Result Analysis: The optimized parameters and corresponding loss values are recorded. Additionally, the running time of the optimization task is logged for evaluation.

Environment Setup and Module Importing

The script sets up the environment by adding the path to the GROMACS executable directory to the PATH environment variable. It imports necessary modules from the AMOFMS package for optimization, force field parameter handling, and utility functions.

import os
import time

os.environ['PATH'] += ':/home/xiaoyedi/data/research/tools/gromacs-2023.3/bin'

from AMOFMS.Optimization import OptParametersProcess, ParticleSwarmOptimizer, Particle
from AMOFMS.ObjectiveFunction import BottomUpObjectiveFunction
from AMOFMS.tools.utilies import mkdir
from AMOFMS.tools.math_tools import square_diff_of_elements_in_2D_list
from AMOFMS.tools.properties import MembraneProperties
from AMOFMS.tools.gromacs import find_gmx_executable

Initialization and Configuration

Paths, parameters, and options for the optimization task are initialized and configured. Initial settings include paths to input files, output folders, simulation parameters, and optimization parameters.

time_start = int(time.time())
timeArray_start = time.localtime(time_start)
start_time = time.strftime("%Y-%m-%d %H:%M:%S", timeArray_start)
print(f'\nStarting Optimization Task at {start_time} \n')

fg_topology = 'aa/prod/prod.tpr'
fg_trajectory = 'aa/prod/prod_whole.xtc'
gmx = find_gmx_executable()

opt_folder = './opt'
opt_folder = os.path.realpath(opt_folder)
loss_dat = os.path.join(opt_folder, 'loss.dat')
mdp_folder = './mdp'
initial_gro = 'packmol/mix.gro'
initial_pdb = 'packmol/mix.pdb'
index_file = './index.ndx'
run_em = True
em_double = False
run_anneal = False
run_eq = True
run_prod = True

Objective Function Definition

An objective function (opt_loss_function) is defined to compute the loss during optimization. The function calculates various terms of the loss, including force match loss, U(r) Boltzmann inversion loss, membrane thickness loss, area per lipid loss.

tmp_result = os.path.join(opt_folder, 'iter_result.dat')
with open(tmp_result, 'w') as f:
    line = f'{"Iteration":<12}  {"Bead_id":<12}  {"Ur_loss":<12}  {"APL(Angstrom**-2, exp:63)":<12}  {"Thickness(Angstrom, exp:37)":<12}  ' \
           f'{"Ka(mN/m, exp:255)":<12} \n'
    f.write(line)

def opt_loss_function(particle: Particle):
    print(f'\n{particle.iter}-iter:  {particle.idx}-th particle processing...')
    new_topology = opt_para.unpack_updated_parameters_to_top(updated_parameters_array=particle.position)
    iter_folder = os.path.join(opt_folder, f'iters/iter_{particle.iter}')
    idx_folder = os.path.join(iter_folder, f'{particle.idx}')

    bottom_up_obj.update_system_topology(new_system_top=new_topology)
    bottom_up_obj.update_opt_folder(new_opt_folder=idx_folder)

    total_loss = 0
    each_term_loss = {}

    print('\nComputing Ur Boltzmann inversion loss...')
    bottom_up_obj.run_cg_simulation(initial_gro=initial_gro, fg_resname_list=resname_cg_from_fg_coord,
                                    mdp_folder=mdp_folder, index_file=index_file, table_file=None,
                                    cg_simulation_folder=idx_folder, em=run_em, em_double_version=em_double, anneal=run_anneal,
                                    gpu_acceleration=False,
                                    eq=run_eq, prod=run_prod, nt=cpu_nt, gpu_id=None)
    cg_pair_Ur_list = bottom_up_obj.Boltzmann_inversion(rdf_pairs_list=cg_rdf_pairs_list, tag='cg',
                                                        Temperature=temperature, bin_width=rdf_binwidth, max_distance=rdf_cutoff)
    Ur_loss = Ur_loss_ratio * square_diff_of_elements_in_2D_list(list1=fg_pair_Ur_list, list2=cg_pair_Ur_list)
    each_term_loss.update({'Ur': Ur_loss})
    total_loss += Ur_loss


    print('\nComputing membrane properties...')
    mem = MembraneProperties(topology=bottom_up_obj.cg_topology,
                             trajectory=bottom_up_obj.cg_trajectory)
    cg_apl, _ = mem.compute_apl(headgroup_selection=head_group_expression)
    cg_thickness, _ = mem.compute_membrane_thickness(headgroup_selection=head_group_expression)
    cg_ka = mem.compute_Ka(head_group_expression, temperature)

    print(f'{particle.iter}-iter {particle.idx}-th particle APL(Angstrom**-2): \nexp: {exp_apl} cg: {cg_apl}')
    print(f'{particle.iter}-iter {particle.idx}-th particle Thickness(Angstrom): \nexp: {exp_thickness} cg: {cg_thickness}')
    print(f'{particle.iter}-iter {particle.idx}-th particle Ka(mN/m): \nexp: {exp_ka} cg: {cg_ka}')

    apl_loss = apl_loss_ratio * abs(exp_apl - cg_apl)  # https://doi.org/10.1021/acs.jpcb.6b01870
    thickness_loss = thickness_loss_ratio * abs(exp_thickness - cg_thickness)  # https://doi.org/10.1063/1.4936909
    ka_loss = ka_loss_ratio * abs(exp_ka - cg_ka)

    each_term_loss.update({'apl': apl_loss})
    each_term_loss.update({'thickness': thickness_loss})
    each_term_loss.update({'ka': ka_loss})
    total_loss = total_loss + apl_loss + thickness_loss + ka_loss

    with open(tmp_result, 'a+') as f:
        line = f'{particle.iter:<12}  {particle.idx:<12}  {Ur_loss:<12}  {cg_apl:<12}  {cg_thickness:<12} {cg_ka:<12}\n'
        f.write(line)

    print(f'\n{particle.iter}-iter:  {particle.idx}-th particle Done!')
    return total_loss, each_term_loss

Optimization Execution

The ParticleSwarmOptimizer is utilized for the optimization process. The optimizer iteratively adjusts the optimization parameters to minimize the total loss function.

optimizer = ParticleSwarmOptimizer(objective_function=opt_loss_function, update_boundary_frequency=max_iter,
                                   bounds=opt_para_boundary, num_particles=num_particles, max_iter=max_iter, max_no_improvement_iters=max_iter)

best_para, best_score, recorder = optimizer.optimize_mpi(max_processes=max_processes)
recorder.write_losses_to_file(filepath=loss_dat)

Conclusion

This script provides a framework for conducting optimization tasks involving molecular dynamics simulations and force field parameter optimization for the POPC system. It integrates various functionalities from the AMOFMS package and allows for flexible customization based on specific optimization requirements.