Custom generators

A familiar example

Consider the following example which does basic dupin detection using a freud generator, however, this time we shall read it from a xyz file:

[1]:
import warnings

import freud
import matplotlib.pyplot as plt
import numpy as np
import ruptures as rpt

import dupin as du

warnings.simplefilter("ignore")
ls = 6
FILENAME = "../data/lj-sim.xyz"
steinhardt = freud.order.Steinhardt(l=ls)
nlist_kwargs = {"num_neighbors": 12, "exclude_ii": True}
pipeline = du.data.freud.FreudDescriptor(
    compute=steinhardt, attrs={"particle_order": f"Q{ls}"}
).pipe(du.data.reduce.NthGreatest((10, 50)))
signal_aggregator = du.data.aggregate.SignalAggregator(pipeline)
with open(FILENAME) as file:
    # Read the number of atoms
    N = int(file.readline().strip())
    # Read the comment line
    comment = file.readline().strip()
    box_params = float_list = [float(x) for x in comment.split()]
box_params = freud.Box.from_box(box_params)
traj = np.genfromtxt(FILENAME, skip_header=2, invalid_raise=False)[
    :, 1:4
].reshape(-1, N, 3)
box = freud.box.Box.cube(L=20)

for frame_positions in traj:
    signal_aggregator.accumulate((box, frame_positions), nlist_kwargs)
lin_regress_cost = du.detect.CostLinearFit()
dynp = rpt.Dynp(custom_cost=lin_regress_cost)
sweep_detector = du.detect.SweepDetector(
    dynp, max_change_points=6, tolerance=0.001
)
change_points = sweep_detector.fit(signal_aggregator.to_dataframe())
plt.plot(sweep_detector.costs_)
plt.title("Detection of change points\nusing linear regression cost")
plt.xlabel("Frame")
plt.ylabel("Cost")
plt.axvline(sweep_detector.opt_n_change_points_, color="k", linestyle="--")
plt.show()
# add change points as vlines
plt.plot(
    signal_aggregator.to_dataframe().to_numpy(),
    label=signal_aggregator.to_dataframe().columns.to_list(),
)
for change_point in change_points:
    plt.axvline(change_point, color="k", linestyle="--", label="change point")
plt.title("Results plotted on\naggregated signal")
plt.xlabel("Frame")
plt.ylabel("reduced order parameters")
plt.legend()
plt.show()
../../_images/tutorials_01-Custom-generators_Custom-generator_2_0.png
../../_images/tutorials_01-Custom-generators_Custom-generator_2_1.png

Custom generator example

Now let’s transcribe the above example with a custom generator:

[2]:
import warnings

import freud
import matplotlib.pyplot as plt
import numpy as np
import ruptures as rpt

import dupin as du

warnings.simplefilter("ignore")
ls = 6
FILENAME = "../data/lj-sim.xyz"


def steinhardt_custom_generator(system):
    steinhardt = freud.order.Steinhardt(l=ls)
    nlist_kwargs = {"num_neighbors": 12, "exclude_ii": True}
    steinhardt.compute(system, neighbors=nlist_kwargs)
    return {f"Q{ls}": steinhardt.particle_order}


pipeline = du.data.base.CustomGenerator(steinhardt_custom_generator).pipe(
    du.data.reduce.NthGreatest((10, 50))
)
signal_aggregator = du.data.aggregate.SignalAggregator(pipeline)
with open(FILENAME) as file:
    # Read the number of atoms
    N = int(file.readline().strip())
    # Read the comment line
    comment = file.readline().strip()
    box_params = float_list = [float(x) for x in comment.split()]
box_params = freud.Box.from_box(box_params)
traj = np.genfromtxt(FILENAME, skip_header=2, invalid_raise=False)[
    :, 1:4
].reshape(-1, N, 3)
box = freud.box.Box.cube(L=20)

for frame_positions in traj:
    signal_aggregator.accumulate((box, frame_positions))
lin_regress_cost = du.detect.CostLinearFit()
dynp = rpt.Dynp(custom_cost=lin_regress_cost)
sweep_detector = du.detect.SweepDetector(
    dynp, max_change_points=6, tolerance=0.001
)
change_points = sweep_detector.fit(signal_aggregator.to_dataframe())
plt.plot(sweep_detector.costs_)
plt.title("Detection of change points\nusing linear regression cost")
plt.xlabel("Frame")
plt.ylabel("Cost")
plt.axvline(sweep_detector.opt_n_change_points_, color="k", linestyle="--")
plt.show()
# add change points as vlines
plt.plot(
    signal_aggregator.to_dataframe().to_numpy(),
    label=signal_aggregator.to_dataframe().columns.to_list(),
)
for change_point in change_points:
    plt.axvline(change_point, color="k", linestyle="--", label="change point")
plt.title("Results plotted on\naggregated signal")
plt.xlabel("Frame")
plt.ylabel("reduced order parameters")
plt.legend()
plt.show()
../../_images/tutorials_01-Custom-generators_Custom-generator_4_0.png
../../_images/tutorials_01-Custom-generators_Custom-generator_4_1.png

They are exactly the same! As is expected. The freud generators are convenience functions for easier handling of freud data.

A More complicated custom generator example

Now let’s try computing two different l’s for Steinhardt order parameter using a custom generator:

[3]:
import warnings

import freud
import matplotlib.pyplot as plt
import numpy as np
import ruptures as rpt

import dupin as du

warnings.simplefilter("ignore")
ls = [6, 12]
FILENAME = "../data/lj-sim.xyz"

steinhardt = freud.order.Steinhardt(l=ls)
nlist_kwargs = {"num_neighbors": 12, "exclude_ii": True}


def steinhardt_custom_generator(system):
    steinhardt = freud.order.Steinhardt(l=ls)
    nlist_kwargs = {"num_neighbors": 12, "exclude_ii": True}
    steinhardt.compute(system, neighbors=nlist_kwargs)
    keys = [f"Q{l}" for l in ls]
    properties_dict = {}
    for i, key in enumerate(keys):
        properties_dict[key] = steinhardt.particle_order.T[i]
    return properties_dict


pipeline = du.data.base.CustomGenerator(steinhardt_custom_generator).pipe(
    du.data.reduce.NthGreatest((10, 50))
)
signal_aggregator = du.data.aggregate.SignalAggregator(pipeline)
with open(FILENAME) as file:
    # Read the number of atoms
    N = int(file.readline().strip())
    # Read the comment line
    comment = file.readline().strip()
    box_params = float_list = [float(x) for x in comment.split()]
box_params = freud.Box.from_box(box_params)
traj = np.genfromtxt(FILENAME, skip_header=2, invalid_raise=False)[
    :, 1:4
].reshape(-1, N, 3)
box = freud.box.Box.cube(L=20)

for frame_positions in traj:
    signal_aggregator.accumulate((box, frame_positions))
lin_regress_cost = du.detect.CostLinearFit()
dynp = rpt.Dynp(custom_cost=lin_regress_cost)
sweep_detector = du.detect.SweepDetector(
    dynp, max_change_points=6, tolerance=0.001
)
change_points = sweep_detector.fit(signal_aggregator.to_dataframe())
plt.plot(sweep_detector.costs_)
plt.title("Detection of change points\nusing linear regression cost")
plt.xlabel("Frame")
plt.ylabel("Cost")
plt.axvline(sweep_detector.opt_n_change_points_, color="k", linestyle="--")
plt.show()
# add change points as vlines
plt.plot(
    signal_aggregator.to_dataframe().to_numpy(),
    label=signal_aggregator.to_dataframe().columns.to_list(),
)
for change_point in change_points:
    plt.axvline(change_point, color="k", linestyle="--", label="change point")
plt.title("Results plotted on\naggregated signal")
plt.xlabel("Frame")
plt.ylabel("reduced order parameters")
plt.legend()
plt.show()
../../_images/tutorials_01-Custom-generators_Custom-generator_6_0.png
../../_images/tutorials_01-Custom-generators_Custom-generator_6_1.png

Minkowski structure metrics using custom generator

A very useful set of order parameter which uses Voronoi weighted Steinhardt order parameter computation is often referred to as Minkowski order parameters. To compute Minkowski order parameters the custom generator function is slightly more complicated: Now lets do a more complicated example: minkowski!

[4]:
import warnings

import freud
import matplotlib.pyplot as plt
import numpy as np
import ruptures as rpt

import dupin as du

warnings.simplefilter("ignore")
ls = 6
FILENAME = "../data/lj-sim.xyz"


# custom generator function
def minkowski_generator(system):
    # compute voronoi neighbors and get nlist
    vor = freud.locality.Voronoi()
    vor.compute(system)
    nlist = vor.nlist
    # compute steinhardt
    steinhardt = freud.order.Steinhardt(l=ls, weighted=True)
    steinhardt.compute(system, neighbors=nlist)
    return {f"M{ls}": steinhardt.particle_order}


pipeline = du.data.base.CustomGenerator(minkowski_generator).pipe(
    du.data.reduce.NthGreatest((-10, -50))
)
signal_aggregator = du.data.aggregate.SignalAggregator(pipeline)
with open(FILENAME) as file:
    # Read the number of atoms
    N = int(file.readline().strip())
    # Read the comment line
    comment = file.readline().strip()
    box_params = float_list = [float(x) for x in comment.split()]
box_params = freud.Box.from_box(box_params)
traj = np.genfromtxt(FILENAME, skip_header=2, invalid_raise=False)[
    :, 1:4
].reshape(-1, N, 3)
box = freud.box.Box.cube(L=20)

for frame_positions in traj:
    signal_aggregator.accumulate((box, frame_positions))
lin_regress_cost = du.detect.CostLinearFit()
dynp = rpt.Dynp(custom_cost=lin_regress_cost)
sweep_detector = du.detect.SweepDetector(
    dynp, max_change_points=6, tolerance=0.001
)
change_points = sweep_detector.fit(signal_aggregator.to_dataframe())
plt.plot(sweep_detector.costs_)
plt.title("Detection of change points\nusing linear regression cost")
plt.xlabel("Frame")
plt.ylabel("Cost")
plt.axvline(sweep_detector.opt_n_change_points_, color="k", linestyle="--")
plt.show()
# add change points as vlines
plt.plot(
    signal_aggregator.to_dataframe().to_numpy(),
    label=signal_aggregator.to_dataframe().columns.to_list(),
)
for change_point in change_points:
    plt.axvline(change_point, color="k", linestyle="--", label="change point")
plt.title("Results plotted on\naggregated signal")
plt.xlabel("Frame")
plt.ylabel("reduced order parameters")
plt.legend(loc="upper right")
plt.show()
../../_images/tutorials_01-Custom-generators_Custom-generator_8_0.png
../../_images/tutorials_01-Custom-generators_Custom-generator_8_1.png

Other packages example

The custom generator function should accept a set of variables that will be used by the custom generator. In case of freud this is a system tuple containing box and particle coordinates. Custom generators can be used with other python packages which might require different sets of variables for computation of order parameters/quantities.