Entangled networks#

For these examples, we use the models of a network liquid from Neophytou et al., PNAS, 121, e2406890121 which is known to densify via two successive liquid-liquid phase transitions.

The system of interest is a coarse-grained model of DNA dendrimers, where each dendrimer is represented by a repulsive spherical core decorated with four attractive patches in a tetrahedral arrangement.

  • At low pressures, an unentangled low-density liquid (LDL) forms.

  • At intermediate pressures, an entangled high-density liquid (HDL) forms.

  • At high pressures, an entangled very high-density liquid (vHDL) forms.

[1]:
import math
import random
from collections import defaultdict
from itertools import combinations
from typing import Any, Iterable

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm

import topo_metrics as tm
import topo_metrics.analysis as tm_ana
from topo_metrics._julia_wrappers import run_bond_distance_rdf

%config InlineBackend.figure_format = 'svg'

radial distribution functions by bond separation#

The first measure used to identify the onset of entanglement is to decompose the radial distribution function (RDF) into contributions derived from particles separated by \(D\) bonds. This allows for assessing whether particles close in space are separated by multiple bonds in the network.

[2]:
all_rs = []
all_gD = []
for index in range(1, 101):
    topo = tm.Topology.from_conflink("data/conflink_p2.0.dat", index=-index)
    assert topo.lattice is not None, "Topology must have lattice information."

    rs, gD, _ = run_bond_distance_rdf(
        positions=topo.cartesian_coordinates,
        edges=topo.edges,
        cell_lengths=topo.lattice.lengths,
        cell_angles=topo.lattice.angles,
        dmax=6,
        rmax=50.0,
        dr=0.1,
    )

    all_rs.append(rs)
    all_gD.append(gD)
[3]:
for rs in all_rs[1:]:
    assert np.allclose(rs, all_rs[0])

rs = all_rs[0]
std_gD = np.std(np.array(all_gD), axis=0)
mean_gD = np.mean(np.array(all_gD), axis=0)

fig, ax = plt.subplots()

for D in [1, 2, 3, 4, 5, 6]:
    ax.plot(rs / 8, mean_gD[D - 1], label=f"D={D}", alpha=0.9)
    if D in [1, 2]:
        ax.fill_between(rs / 8, 0, mean_gD[D - 1], alpha=0.1)

ax.set_xlabel("r")
ax.set_ylabel("g(r)")
ax.set_ylim(-0.05, 1.25)
ax.legend(ncol=2, frameon=False)

plt.show()
../_images/examples_entangled_networks_4_0.svg
[ ]:
all_rs = []
all_gD = []
for index in range(1, 101):
    topo = tm.Topology.from_conflink("data/conflink_p20.0.dat", index=-index)
    assert topo.lattice is not None, "Topology must have lattice information."

    rs, gD, _ = run_bond_distance_rdf(
        positions=topo.cartesian_coordinates,
        edges=topo.edges,
        cell_lengths=topo.lattice.lengths,
        cell_angles=topo.lattice.angles,
        dmax=6,
        rmax=50.0,
        dr=0.1,
    )

    all_rs.append(rs)
    all_gD.append(gD)

for rs in all_rs[1:]:
    assert np.allclose(rs, all_rs[0])

rs = all_rs[0]
std_gD = np.std(np.array(all_gD), axis=0)
mean_gD = np.mean(np.array(all_gD), axis=0)

fig, ax = plt.subplots()

for D in [1, 2, 3, 4, 5, 6]:
    ax.plot(rs / 8, mean_gD[D - 1], label=f"D={D}", alpha=0.9)
    if D in [1, 2]:
        ax.fill_between(rs / 8, 0, mean_gD[D - 1], alpha=0.1)

ax.set_xlabel("r")
ax.set_ylabel("g(r)")
ax.set_ylim(-0.05, 1.25)
ax.legend(ncol=2, frameon=False)

plt.show()
../_images/examples_entangled_networks_5_0.svg
[ ]:
all_rs = []
all_gD = []
for index in range(1, 101):
    topo = tm.Topology.from_conflink("data/conflink_p32.0.dat", index=-index)
    assert topo.lattice is not None, "Topology must have lattice information."

    rs, gD, _ = run_bond_distance_rdf(
        positions=topo.cartesian_coordinates,
        edges=topo.edges,
        cell_lengths=topo.lattice.lengths,
        cell_angles=topo.lattice.angles,
        dmax=6,
        rmax=50.0,
        dr=0.1,
    )

    all_rs.append(rs)
    all_gD.append(gD)

for rs in all_rs[1:]:
    assert np.allclose(rs, all_rs[0])

rs = all_rs[0]
std_gD = np.std(np.array(all_gD), axis=0)
mean_gD = np.mean(np.array(all_gD), axis=0)

fig, ax = plt.subplots()

for D in [1, 2, 3, 4, 5, 6]:
    ax.plot(rs / 8, mean_gD[D - 1], label=f"D={D}", alpha=0.9)
    if D in [1, 2]:
        ax.fill_between(rs / 8, 0, mean_gD[D - 1], alpha=0.1)

ax.set_xlabel("r")
ax.set_ylabel("g(r)")
ax.set_ylim(-0.05, 1.25)
ax.legend(ncol=2, frameon=False)

plt.show()
../_images/examples_entangled_networks_6_0.svg

coiled rings#

Coiled rings are cyclic paths in the network whose geometry is strongly distorted away from a planar-like conformation. In this sense, coiled rings exhibit supercoiling in three dimensions, i.e., the closed path winds around itself.

The degree of coiling (and, more generally, self-entanglement of the ring backbone) is quantified by the writhe of the cyclic path \(C_i\),

\[Wr_i \;=\; \frac{1}{4\pi}\oint_{C_i}\!\!\oint_{C_i} \frac{\mathbf r_i' - \mathbf r_i}{\left\lVert \mathbf r_i' - \mathbf r_i \right\rVert^{3}} \cdot \left(d\mathbf r_i' \times d\mathbf r_i\right).\]

Here, \(\mathbf r_i\) and \(\mathbf r_i'\) denote two points on the same closed curve \(C_i\), and \(d\mathbf r_i\), \(d\mathbf r_i'\) are the corresponding infinitesimal tangent vectors along the curve.

Geometrically, \(Wr_i\) measures how many times the closed path loops around itself in 3D. Approximately planar rings have \(Wr_i \approx 0\), while strongly coiled rings have \(|Wr_i|\gtrsim 1\) (with larger \(|Wr_i|\) indicating more pronounced self-winding). When chirality is not of interest, one often reports \(|Wr_i|\), since the sign of \(Wr_i\) encodes handedness.

[6]:
topo = tm.Topology.from_conflink("data/conflink_p2.0.dat")
rings = topo.get_rings(depth=8)
topo
[6]:
Topology(nodes=1000, edges=3982, has_lattice=True)
[7]:
all_writhes = defaultdict(list)
for ring in rings:
    if len(ring) > 10:
        continue

    writhe, _ = ring.writhe_and_acn()  # type: ignore
    all_writhes[len(ring)].append(abs(writhe))

bins = np.linspace(0, 1.25, 50)

fig, ax = tm_ana.plot_writhe_distributions(all_writhes, bins=bins)
ax.set_xlim(0, 1.25)
plt.show()
../_images/examples_entangled_networks_9_0.svg
[8]:
topo = tm.Topology.from_conflink("data/conflink_p20.0.dat")
rings = topo.get_rings(depth=8)

all_writhes = defaultdict(list)
for ring in rings:
    if len(ring) > 10:
        continue

    writhe, _ = ring.writhe_and_acn()  # type: ignore
    all_writhes[len(ring)].append(abs(writhe))

fig, ax = tm_ana.plot_writhe_distributions(all_writhes, bins=bins)
ax.set_xlim(0, 1.25)
plt.show()
../_images/examples_entangled_networks_10_0.svg
[9]:
topo = tm.Topology.from_conflink("data/conflink_p32.0.dat")
rings = topo.get_rings(depth=8)

all_writhes = defaultdict(list)
for ring in rings:
    if len(ring) > 10:
        continue

    writhe, _ = ring.writhe_and_acn()  # type: ignore
    all_writhes[len(ring)].append(abs(writhe))

fig, ax = tm_ana.plot_writhe_distributions(all_writhes, bins=bins)
ax.set_xlim(0, 1.25)
plt.show()
../_images/examples_entangled_networks_11_0.svg

linked rings#

Linked rings are two (or more) disjoint rings (i.e., rings that do not share any vertices) which are entangled with one another in such a way that they cannot be unlinked without at least one bond being broken.

[10]:
from topo_metrics import Node
from topo_metrics.io.lmp import write_rings_lammps_full
from topo_metrics.knots import linking_number_method_1a
from topo_metrics.ring_geometry import RingGeometry


def circle_xy(n=400, r=1.0, center=(0.0, 0.0, 0.0), z=0.0) -> RingGeometry:
    """circle in plane z = center[2] + z."""

    t = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
    x = center[0] + r * np.cos(t)
    y = center[1] + r * np.sin(t)
    zz = np.full_like(t, center[2] + z)

    coords = np.column_stack([x, y, zz])

    return RingGeometry(
        tuple(
            Node(node_id=node_id, cart_coord=pos)
            for node_id, pos in enumerate(coords, start=1)
        )
    )


def circle_xz(n=400, r=1.0, center=(1.0, 0.0, 0.0)):
    """circle in plane y = center[1]"""

    t = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
    x = center[0] + r * np.cos(t)
    y = np.full_like(t, center[1])
    z = center[2] + r * np.sin(t)

    coords = np.column_stack([x, y, z])

    return RingGeometry(
        tuple(
            Node(node_id=node_id, cart_coord=pos)
            for node_id, pos in enumerate(coords, start=1)
        )
    )