diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..e7e9d11
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,2 @@
+# Default ignored files
+/workspace.xml
diff --git a/.idea/chromo.iml b/.idea/chromo.iml
new file mode 100644
index 0000000..4f2c9af
--- /dev/null
+++ b/.idea/chromo.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..7a85af6
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..ea896ee
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chromo/Polymer.py b/chromo/Polymer.py
deleted file mode 100644
index a75c112..0000000
--- a/chromo/Polymer.py
+++ /dev/null
@@ -1,243 +0,0 @@
-"""
-Polymer class.
-
-Creates a polymer with a defined length and discretization.
-"""
-from pathlib import Path
-from enum import Enum
-import dataclasses
-import io
-
-import numpy as np
-import pandas as pd
-from .util import dss_params, combine_repeat
-
-
-@dataclasses.dataclass
-class Epigenmark:
- """Information about the chemical properties of an epigenetic mark."""
-
- name: str
- bind_energy: float
- interaction_energy: float
- chemical_potential: float
-
-
-class State:
- """Defines the epigenetic state of a given bead for a single mark."""
-
- pass
-
-
-class HP1_State(State, Enum):
- """HP1 can be bound to none, either, or both nucleosome tails."""
-
- NONE = 0
- LBOUND = 1
- RBOUND = 2
- BOTH = 3
-
-
-def make_epi_collection(epigenmarks):
- """
- Takes a sequence of epigenetic marks and returns a summary DataFrame.
-
- Parameters
- ----------
- epigenmarks : Sequence[Epigenmark]
- The epigenetic marks to be summarized.
-
- Returns
- -------
- pd.DataFrame
- Columns are the properties of Epigenmark.
- """
- df = pd.DataFrame(columns=['name', 'bind_energy', 'interaction_energy',
- 'chemical_potential'])
- for mark in epigenmarks:
- df = df.append(dataclasses.asdict(mark), ignore_index=True)
- return df
-
-
-class Polymer:
- """
- The positions and chemical state of a discrete polymer.
-
- The polymer carries around a set of coordinates ``r`` of shape
- ``(num_beads, 3)``, a triad of material normals ``t_i`` for ``i`` in
- ``{1,2,3}``, and some number of chemical states per bead.
-
- Its material properties are completely defined by these positions, chemical
- states, and the length of polymer (in Kuhn lengths) simulated by each bead.
- TODO: allow differential discretization, and decay to constant
- discretization naturally.
-
- Since this codebase is primarily used to simulate DNA, information about
- the chemical properties of each epigenetic mark are stored in `Epigenmark`
- objects.
- """
-
- def __init__(self, name, r, t_3, t_2, epigenmarks, states,
- length_per_bead):
- """
- Parameters
- ----------
- name : str
- A name for convenient repr. Should be a valid filename.
- r : (N, 3) array_like of float
- The positions of each bead.
- t_3 : (N, 3) array_like of float
- The tangent vector to each bead in global coordinates.
- t_2 : (N, 3) array_like of float
- A material normal to each bead in global coordinates.
- epigenmarks : (M, ) Sequence[Epigenmark]
- Information about the chemical properties of each of the epigenetic
- marks being tracked.
- states : (N, M) array_like of State
- State of each of the M epigenetic marks being tracked for each
- bead.
- length_per_bead : float
- How many Kuhn lengths of polymer are simulated by each bead.
- """
- self.name = name
- self.r = r
- self.t_3 = t_3
- self.t_2 = t_2
- self.epigenmarks = make_epi_collection(epigenmarks)
- self.states = states
- self.length_per_bead = length_per_bead
- # Load the polymer parameters for the conformational energy
- self.sim_type, self.eps_bend, self.eps_par, self.eps_perp, \
- self.gamma, self.eta = self._find_parameters(length_per_bead)
-
- @classmethod
- def from_files(cls, *, r=None, t_3=None, t_2=None, marks=None, states=None,
- lengths=None):
- """
- Instantiate a Polymer from input files.
-
- Each of the r, t_3, t_2, epigenmarks, states, and length_per_bead
- arrays requires a separate input file.
- """
- r = pd.read_csv(r, header=None) if r is not None else None
- t_3 = pd.read_csv(t_3, header=None) if t_3 is not None else None
- t_2 = pd.read_csv(t_2, header=None) if t_2 is not None else None
- states = pd.read_csv(states) if states is not None else None
- marks = pd.read_csv(r, header=None) if r is not None else None
- return cls(r, t_3, t_2, marks, states, lengths)
-
- @classmethod
- def straight_line_in_x(cls, name, marks, states, num_beads,
- length_per_bead):
- r = np.zeros((num_beads, 3))
- r[:, 0] = length_per_bead * np.arange(num_beads)
- t_3 = np.zeros((num_beads, 3))
- t_3[:, 0] = 1
- t_2 = np.zeros((num_beads, 3))
- t_2[:, 1] = 1
- return cls(name, r, t_3, t_2, marks, states, length_per_bead)
-
- def write_marks(self, marks_file, **kwargs):
- self.epigenmarks.to_csv(marks_file, **kwargs)
-
- def __repr__(self):
- s = io.Bytes()
- self.write_repr(s)
- return s.decode()
-
- def write_repr(self, path):
- np.savez(path, r=self.r, t_2=self.t_2, t_3=self.t_3,
- states=self.states, marks=self.epigenmarks.values,
- length_per_bead=self.length_per_bead)
-
- @classmethod
- def from_repr(cls, path):
- npz = np.load(path)
- mark_info_arr = npz['marks']
- marks = [Epigenmark(*info) for info in mark_info_arr]
- return cls(r=npz['r'], t_3=npz['t_3'], t_2=npz['t_2'],
- states=npz['states'], marks=marks,
- length_per_bead=npz['length_per_bead'])
-
- @property
- def num_marks(self):
- return len(self.epigenmarks)
-
- @property
- def num_beads(self):
- return self.r.shape[0]
-
- def __str__(self):
- return f"Polymer<{self.name}, nbeads={self.num_beads}, " \
- f"nmarks={self.num_marks}>"
-
- @staticmethod
- def _find_parameters(length_bead):
- """Determine the parameters for the elastic forces based on ssWLC with twist."""
- sim_type = "sswlc"
- lp_dna = 53 # Persistence length of DNA in nm
- length_dim = length_bead * 0.34 / lp_dna # Non-dimensionalized length per bead
-
- # Determine the parameter values using linear interpolation of the parameter table
- eps_bend = np.interp(length_dim, dss_params[:, 0], dss_params[:, 1]) / length_dim
- gamma = np.interp(length_dim, dss_params[:, 0], dss_params[:, 2]) * length_dim * lp_dna
- eps_par = np.interp(length_dim, dss_params[:, 0], dss_params[:, 3]) / (length_dim * lp_dna**2)
- eps_perp = np.interp(length_dim, dss_params[:, 0], dss_params[:, 4]) / (length_dim * lp_dna**2)
- eta = np.interp(length_dim, dss_params[:, 0], dss_params[:, 5]) / lp_dna
-
- return sim_type, eps_bend, eps_par, eps_perp, gamma, eta
-
- def compute_dE(self, ind0, indf, r_poly_trial, t3_poly_trial, t2_poly_trial,
- states_trial):
- """
- Compute change in energy of polymer state from proposed new state.
- """
- delta_energy_poly = 0
- # Calculate contribution to polymer energy at the ind0 position
- if ind0 != 0:
- delta_r_trial = r_poly_trial[0, :] - self.r[ind0 - 1, :]
- delta_r_par_trial = np.dot(delta_r_trial, self.t_3[ind0 - 1, :])
- delta_r_perp_trial = delta_r_trial - delta_r_par_trial * self.t_3[ind0 - 1, :]
-
- delta_r = self.r[ind0, :] - self.r[ind0 - 1, :]
- delta_r_par = np.dot(delta_r, self.t_3[ind0 - 1, :])
- delta_r_perp = delta_r - delta_r_par * self.t_3[ind0 - 1, :]
-
- bend_vec_trial = (t3_poly_trial[0, :] - self.t_3[ind0 - 1, :]
- - self.eta * delta_r_perp_trial)
- bend_vec = (self.t_3[ind0, :] - self.t_3[ind0 - 1, :]
- - self.eta * delta_r_perp)
-
- delta_energy_poly += (0.5 * self.eps_bend * np.dot(bend_vec_trial, bend_vec_trial)
- + 0.5 * self.eps_par * (delta_r_par_trial - self.gamma) ** 2
- + 0.5 * self.eps_perp * np.dot(delta_r_perp_trial, delta_r_perp_trial))
- delta_energy_poly -= (0.5 * self.eps_bend * np.dot(bend_vec, bend_vec)
- + 0.5 * self.eps_par * (delta_r_par - self.gamma) ** 2
- + 0.5 * self.eps_perp * np.dot(delta_r_perp, delta_r_perp))
-
- # Calculate contribution to polymer energy at the indf position
- if indf != self.num_beads:
-
- delta_r_trial = self.r[indf, :] - r_poly_trial[indf - ind0 - 1, :]
- delta_r_par_trial = np.dot(delta_r_trial, t3_poly_trial[indf - ind0 - 1, :])
- delta_r_perp_trial = delta_r_trial - delta_r_par_trial * t3_poly_trial[indf - ind0 - 1, :]
-
- delta_r = self.r[indf, :] - self.r[indf - 1, :]
- delta_r_par = np.dot(delta_r, self.t_3[indf - 1, :])
- delta_r_perp = delta_r - delta_r_par * self.t_3[indf - 1, :]
-
- bend_vec_trial = (self.t_3[indf, :] - t3_poly_trial[indf - ind0 - 1, :]
- - self.eta * delta_r_perp_trial)
- bend_vec = (self.t_3[indf, :] - self.t_3[indf - 1, :]
- - self.eta * delta_r_perp)
-
- delta_energy_poly += (0.5 * self.eps_bend * np.dot(bend_vec_trial, bend_vec_trial)
- + 0.5 * self.eps_par * (delta_r_par_trial - self.gamma) ** 2
- + 0.5 * self.eps_perp * np.dot(delta_r_perp_trial, delta_r_perp_trial))
- delta_energy_poly -= (0.5 * self.eps_bend * np.dot(bend_vec, bend_vec)
- + 0.5 * self.eps_par * (delta_r_par - self.gamma) ** 2
- + 0.5 * self.eps_perp * np.dot(delta_r_perp, delta_r_perp))
-
- return delta_energy_poly
-
-
diff --git a/chromo/components.py b/chromo/components.py
new file mode 100644
index 0000000..05161ce
--- /dev/null
+++ b/chromo/components.py
@@ -0,0 +1,286 @@
+"""
+Components that will make up our simulation.
+
+Various types of polymers, solvents, and other simulation components should be
+defined here.
+"""
+from enum import Enum
+
+import numpy as np
+import pandas as pd
+
+from .util import dss_params
+
+
+class State(Enum):
+ """Defines the chemical state of a given bead for a single mark."""
+
+ pass
+
+
+class HP1_State(State):
+ """HP1 can be bound to none, either, or both nucleosome tails."""
+
+ NONE = 0
+ LBOUND = 1
+ RBOUND = 2
+ BOTH = 3
+
+
+class Polymer:
+ """
+ The positions and chemical state of a discrete polymer.
+
+ The polymer carries around a set of coordinates ``r`` of shape
+ ``(num_beads, 3)``, a triad of material normals ``t_i`` for ``i`` in
+ ``{1,2,3}``, and some number of chemical states per bead.
+
+ Its material properties are completely defined by these positions, chemical
+ states, and the length of polymer (in Kuhn lengths) simulated by each bead.
+ TODO: allow differential discretization, and decay to constant
+ discretization naturally.
+
+ Since this codebase is primarily used to simulate DNA, information about
+ the chemical properties of each epigenetic mark are stored in `Epigenmark`
+ objects.
+ """
+
+ _arrays = 'r', 't3', 't2', 'states', 'bead_length'
+ """Track which arrays will be saved to file."""
+ _3d_arrays = 'r', 't3', 't2'
+ """Track which arrays need to have multi-indexed values (x, y, z)."""
+
+ def __init__(self, name, r, *, bead_length, t3=None, t2=None,
+ states=None, mark_names=None):
+ """
+ Construct a polymer.
+
+ Parameters
+ ----------
+ name : str
+ A name for convenient repr. Should be a valid filename.
+ r : (N, 3) array_like of float
+ The positions of each bead.
+ t3 : (N, 3) array_like of float
+ The tangent vector to each bead in global coordinates.
+ t2 : (N, 3) array_like of float
+ A material normal to each bead in global coordinates.
+ states : (N, M) array_like of int
+ State of each of the M epigenetic marks being tracked for each
+ bead.
+ mark_names : (M, ) str or Sequence[str]
+ The name of each chemical modification tracked in `states`, for
+ each of tracking which mark is which.
+ bead_length : float or (N,) array_like of float
+ The amount of polymer path length between this bead and the next
+ bead. For now, a constant value is assumed (the first value if an
+ array is passed).
+ """
+ self.name = name
+ self.r = r
+ self.t3 = t3
+ self.t2 = t2
+ self.states = states
+ self.mark_names = mark_names
+ if states is not None:
+ # does what atleast_2d(axis=1) should do (but doesn't)
+ self.states = self.states.reshape(states.shape[0], -1)
+ num_beads, num_marks = self.states.shape
+ if num_marks != len(mark_names):
+ raise ValueError("Each chemical state must be given a name.")
+ if num_beads != len(self.r):
+ raise ValueError("Initial epigenetic state of wrong length.")
+ # first if makes sure len will work in second if
+ if not issubclass(type(bead_length), np.ndarray):
+ bead_length = np.atleast_1d(bead_length)
+ if len(bead_length) == 1:
+ bead_length = np.broadcast_to(bead_length, (self.num_beads, 1))
+ self.bead_length = bead_length
+ # Load the polymer parameters for the conformational energy
+ # for now, the array-values of bead_length are ignored
+ self.eps_bend, self.eps_par, self.eps_perp, self.gamma, self.eta \
+ = self._find_parameters(self.bead_length[0])
+
+ @classmethod
+ def from_csv(cls, csv_file):
+ """Construct Polymer from CSV file."""
+ df = pd.read_csv(csv_file)
+ return cls.from_dataframe(df)
+
+ @classmethod
+ def straight_line_in_x(cls, name, num_beads, bead_length, **kwargs):
+ """Construct polymer initialized uniformly along the positve x-axis."""
+ r = np.zeros((num_beads, 3))
+ r[:, 0] = bead_length * np.arange(num_beads)
+ t3 = np.zeros((num_beads, 3))
+ t3[:, 0] = 1
+ t2 = np.zeros((num_beads, 3))
+ t2[:, 1] = 1
+ return cls(name, r, t3=t3, t2=t2, bead_length=bead_length, **kwargs)
+
+ def to_dataframe(self):
+ """
+ Write canonical CSV representation of the Polymer to file.
+
+ The top-level multiindex values for the columns should correspond to
+ the kwargs, to simplify unpacking the structure (and to make for easy
+ accessing of the dataframe.
+ """
+ arrays = {name: self.__dict__[name] for name in self._arrays
+ if self.__dict__[name] is not None}
+ vector_arrs = {}
+ regular_arrs = {}
+ for name, arr in arrays.items():
+ if name in self._3d_arrays:
+ vector_arrs[name] = arr
+ # special-cased below to get correct epigenmark names
+ elif name != 'states':
+ regular_arrs[name] = arr
+ # first construct the parts of the DataFrame that need a multi-index
+ # TODO: remove "list" after numpy fixes
+ # https://github.com/numpy/numpy/issues/17305
+ vector_arr = np.concatenate(list(vector_arrs.values()), axis=1)
+ # vector_arr = np.concatenate(vector_arrs.values(), axis=1)
+ vector_index = pd.MultiIndex.from_product([vector_arrs.keys(),
+ ('x', 'y', 'z')])
+ vector_df = pd.DataFrame(vector_arr, columns=vector_index)
+ states_index = pd.MultiIndex.from_tuples(
+ [('states', name) for name in self.mark_names])
+ states_df = pd.DataFrame(self.states, columns=states_index)
+ df = pd.concat([vector_df, states_df], axis=1)
+ # now throw in the remaining columns one-by-one
+ for name, arr in regular_arrs.items():
+ df[name] = arr
+ return df
+
+ @classmethod
+ def from_dataframe(cls, df, name=None):
+ """Construct Polymer object from DataFrame. Inverts `.to_dataframe`."""
+ # top-level multiindex values correspond to kwargs
+ kwnames = np.unique(df.columns.get_level_values(0))
+ kwargs = {name: df[name].to_numpy() for name in kwnames}
+ # extract names of each epigenetic state from multi-index
+ if 'states' in df:
+ mark_names = df['states'].columns.to_numpy()
+ kwargs['mark_names'] = mark_names
+ return cls(name, **kwargs)
+
+ @classmethod
+ def from_file(cls, path, name=None):
+ """Construct Polymer object from string representation."""
+ if name is None:
+ name = path.name
+ return cls.from_dataframe(
+ pd.read_csv(path, header=[0, 1], index_col=0),
+ name
+ )
+
+ def to_csv(self, path):
+ """Save Polymer object to CSV file as DataFrame."""
+ return self.to_dataframe().to_csv(path)
+
+ def to_file(self, path):
+ """Synonym for *to_csv* to conform to `make_reproducible` spec."""
+ return self.to_csv(path)
+
+ @property
+ def num_marks(self):
+ """Return number of states tracked per bead."""
+ return self.states.shape[1]
+
+ @property
+ def num_beads(self):
+ """Return number of beads in the polymer."""
+ return self.r.shape[0]
+
+ def __str__(self):
+ """Return string representation of the Polymer."""
+ return f"Polymer<{self.name}, nbeads={self.num_beads}, " \
+ f"nmarks={self.num_marks}>"
+
+ @staticmethod
+ def _find_parameters(length_bead):
+ """Look up elastic parameters of ssWLC for each bead_length."""
+ lp_dna = 53 # Persistence length of DNA in nm
+ # Non-dimensionalized length per bead
+ length_dim = length_bead * 0.34 / lp_dna
+
+ # Determine the parameter values using linear interpolation of the
+ # parameter table
+ eps_bend = np.interp(length_dim, dss_params[:, 0], dss_params[:, 1]) \
+ / length_dim
+ gamma = np.interp(length_dim, dss_params[:, 0], dss_params[:, 2]) \
+ * length_dim * lp_dna
+ eps_par = np.interp(length_dim, dss_params[:, 0], dss_params[:, 3]) \
+ / (length_dim * lp_dna**2)
+ eps_perp = np.interp(length_dim, dss_params[:, 0], dss_params[:, 4]) \
+ / (length_dim * lp_dna**2)
+ eta = np.interp(length_dim, dss_params[:, 0], dss_params[:, 5]) \
+ / lp_dna
+
+ return eps_bend, eps_par, eps_perp, gamma, eta
+
+ def compute_dE(self, ind0, indf, r_poly_trial, t3_poly_trial,
+ t2_poly_trial, states_trial):
+ """Compute change in polymer energy moving to proposed new state."""
+ delta_energy_poly = 0
+ # Calculate contribution to polymer energy at the ind0 position
+ if ind0 != 0:
+ delta_r_trial = r_poly_trial[0, :] - self.r[ind0 - 1, :]
+ delta_r_par_trial = np.dot(delta_r_trial, self.t3[ind0 - 1, :])
+ delta_r_perp_trial = delta_r_trial \
+ - delta_r_par_trial * self.t3[ind0 - 1, :]
+
+ delta_r = self.r[ind0, :] - self.r[ind0 - 1, :]
+ delta_r_par = np.dot(delta_r, self.t3[ind0 - 1, :])
+ delta_r_perp = delta_r - delta_r_par * self.t3[ind0 - 1, :]
+
+ bend_vec_trial = (t3_poly_trial[0, :] - self.t3[ind0 - 1, :]
+ - self.eta * delta_r_perp_trial)
+ bend_vec = (self.t3[ind0, :] - self.t3[ind0 - 1, :]
+ - self.eta * delta_r_perp)
+
+ delta_energy_poly += (
+ 0.5 * self.eps_bend * np.dot(bend_vec_trial, bend_vec_trial)
+ + 0.5 * self.eps_par * (delta_r_par_trial - self.gamma) ** 2
+ + 0.5 * self.eps_perp * np.dot(delta_r_perp_trial,
+ delta_r_perp_trial)
+ )
+ delta_energy_poly -= (
+ 0.5 * self.eps_bend * np.dot(bend_vec, bend_vec)
+ + 0.5 * self.eps_par * (delta_r_par - self.gamma) ** 2
+ + 0.5 * self.eps_perp * np.dot(delta_r_perp, delta_r_perp)
+ )
+
+ # Calculate contribution to polymer energy at the indf position
+ if indf != self.num_beads:
+
+ delta_r_trial = self.r[indf, :] - r_poly_trial[indf - ind0 - 1, :]
+ delta_r_par_trial = np.dot(delta_r_trial,
+ t3_poly_trial[indf - ind0 - 1, :])
+ delta_r_perp_trial = delta_r_trial \
+ - delta_r_par_trial * t3_poly_trial[indf - ind0 - 1, :]
+
+ delta_r = self.r[indf, :] - self.r[indf - 1, :]
+ delta_r_par = np.dot(delta_r, self.t3[indf - 1, :])
+ delta_r_perp = delta_r - delta_r_par * self.t3[indf - 1, :]
+
+ bend_vec_trial = (self.t3[indf, :]
+ - t3_poly_trial[indf - ind0 - 1, :]
+ - self.eta * delta_r_perp_trial)
+ bend_vec = (self.t3[indf, :] - self.t3[indf - 1, :]
+ - self.eta * delta_r_perp)
+
+ delta_energy_poly += (
+ 0.5 * self.eps_bend * np.dot(bend_vec_trial, bend_vec_trial)
+ + 0.5 * self.eps_par * (delta_r_par_trial - self.gamma) ** 2
+ + 0.5 * self.eps_perp * np.dot(delta_r_perp_trial,
+ delta_r_perp_trial)
+ )
+ delta_energy_poly -= (
+ 0.5 * self.eps_bend * np.dot(bend_vec, bend_vec)
+ + 0.5 * self.eps_par * (delta_r_par - self.gamma) ** 2
+ + 0.5 * self.eps_perp * np.dot(delta_r_perp, delta_r_perp)
+ )
+
+ return delta_energy_poly
diff --git a/chromo/Field.py b/chromo/fields.py
similarity index 50%
rename from chromo/Field.py
rename to chromo/fields.py
index d027e0e..1ddaa12 100644
--- a/chromo/Field.py
+++ b/chromo/fields.py
@@ -1,12 +1,12 @@
"""
-Field class
+Fields discretize space to efficiently track changes in Mark energy.
Creates a field object that contains parameters for the field calculations
and functions to generate the densities
-
"""
-
import numpy as np
+import pandas as pd
+
from .util import combine_repeat
@@ -16,10 +16,21 @@ class FieldBase:
Must be subclassed to be useful.
"""
+
+ _field_descriptors = []
+
+ @property
+ def name(self):
+ """For now, there's only one field per sim, so classname works."""
+ return self.__class__.__name__
+
def __init__(self):
+ """Construct a field holding no Polymers, tracking no Marks."""
self.polymers = []
+ self.marks = pd.DataFrame()
def __str__(self):
+ """Print representation of empty field."""
return "Field<>"
def __contains__(self, poly):
@@ -62,9 +73,80 @@ def compute_dE(self, poly, ind0, indf, r, t3, t2, states):
pass
+class Reconstructor:
+ """
+ Defer construction of `Field` until after `Polymer`/`Mark` instances.
+
+ Constructs a kwargs object that can be re-passed to the appropriate `Field`
+ constructor when they become available.
+ """
+
+ def __init__(self, cls, **kwargs):
+ """Construct our Reconstructor."""
+ self.field_constructor = cls
+ self.kwargs = kwargs
+
+ def finalize(self, polymers, marks):
+ """Finish construction of appropriate `Field` object."""
+ return self.field_constructor(polymers=polymers, marks=marks,
+ **self.kwargs)
+
+ @classmethod
+ def from_file(cls, path):
+ """Assume class name is encoded in file name."""
+ constructor = globals()[path.name]
+ # read info as a series
+ kwargs = pd.read_csv(path).iloc[0].to_dict()
+ return cls(constructor, **kwargs)
+
+ def __call__(self, polymers, marks):
+ """Synonym for `Reconstructor.finalize`."""
+ return self.finalize(polymers, marks)
+
+
class UniformDensityField(FieldBase):
- def __init__(self, polymers, x_width, nx, y_width, ny, z_width, nz):
+ """
+ Rectilinear discretization of a rectangular box.
+
+ Computes field energies at the corners of each box. The bead in each box
+ contributes "mass" to each vertex linearly based on its position in the
+ box. This is much more stable numerically than just using the boxes as
+ "bins" as would be done in a e.g. finite-differences discretization.
+ """
+
+ _field_descriptors = ['x_width', 'nx', 'y_width', 'ny', 'z_width', 'nz']
+
+ def __init__(self, polymers, marks, x_width, nx, y_width, ny, z_width,
+ nz):
+ """
+ Construct a UniformDensityField.
+
+ Parameters
+ ----------
+ polymers : Sequence[Polymer]
+ Name of (or object representing) each polymer in the field.
+ marks : pd.DataFrame
+ Output of `chromo.marks.make_mark_collection` for the `Mark`s to be
+ used.
+ x_width : float
+ Width of the box containing the field in the x-direction.
+ nx : int
+ Number of bins in the x-direction.
+ y_width : float
+ Width of the box containing the field in the y-direction.
+ ny : int
+ Number of bins in the y-direction.
+ z_width : float
+ Width of the box containing the field in the z-direction.
+ nz : int
+ Number of bins in the z-direction.
+ """
self.polymers = polymers
+ self.marks = marks
+ for poly in polymers:
+ if poly.num_marks != len(marks):
+ raise NotImplementedError("For now, all polymers must use all"
+ " of the same marks.")
self.x_width = x_width
self.y_width = y_width
self.z_width = z_width
@@ -78,18 +160,58 @@ def __init__(self, polymers, x_width, nx, y_width, ny, z_width, nz):
self.vol_bin = x_width * y_width * z_width / self.n_bins
self.bin_index = UniformDensityField._get_corner_bin_index(
self.nx, self.ny, self.nz)
- num_epigenmarks = np.array([len(p.epigenmarks) for p in polymers])
- if np.any(num_epigenmarks != num_epigenmarks[0]):
- raise ValueError("All Polymers must have the same number of "
- "epigenetic marks for now.")
# one column of density for each state and one for the actual bead
# density of the polymer
- self.density = np.zeros((self.n_bins, num_epigenmarks[0] + 1), 'd')
+ self.num_marks = len(marks)
+ self.density = np.zeros((self.n_bins, self.num_marks + 1), 'd')
self._recompute_field() # initialize the density field
+ def to_file(self, path):
+ """Save Field description and Polymer/Mark names to CSV as Series."""
+ rows = {name: self.__dict__[name] for name in self._field_descriptors}
+ for i, polymer in enumerate(self.polymers):
+ rows[polymer.name] = 'polymer'
+ for i, mark in self.marks.iterrows():
+ # careful! mark.name is the Series.name attribute
+ rows[mark['name']] = 'mark'
+ # prints just key,value for each key in rows
+ return pd.Series(rows).to_csv(path, header=None)
+
+ @classmethod
+ def from_file(cls, path, polymers, marks):
+ """Recover field saved with `.to_file`. Requires Poly/Mark inputs."""
+ # the 0th column will be the index, the 1th column holds the data...
+ field_series = pd.read_csv(path, header=None, index_col=0)[1]
+ kwargs = pd.to_numeric(field_series[cls._field_descriptors]).to_dict()
+ polymer_names = field_series[field_series == 'polymer'].index.values
+ mark_names = field_series[field_series == 'mark'].index.values
+
+ err_prefix = f"Tried to instantiate class:{cls} from file:{path} with "
+ if len(polymers) != len(polymer_names):
+ raise ValueError(err_prefix + f"{len(polymers)} polymers, but "
+ f" there are {len(polymer_names)} listed.")
+ for polymer in polymers:
+ if polymer.name not in polymer_names:
+ raise ValueError(err_prefix + f"polymer:{polymer.name}, but "
+ " this polymer was not present in file.")
+ if len(marks) != len(mark_names):
+ raise ValueError(err_prefix + f"{len(marks)} marks, but "
+ f" there are {len(mark_names)} listed.")
+ for i, mark in marks.iterrows():
+ if mark['name'] not in mark_names:
+ raise ValueError(err_prefix + f"mark:{mark}, but "
+ " this mark was not present in file.")
+
+ return cls(polymers=polymers, marks=marks, **kwargs)
+
@staticmethod
def _get_corner_bin_index(nx, ny, nz):
- """Setup the index array for density bins corners."""
+ """
+ Set up the index array for density bins corners.
+
+ TODO: this should just be a higher-dimensional array to avoid having to
+ do any of this math.
+ """
bin_index = np.zeros((nx * ny * nz, 8), 'd')
count = 0
for index_z in range(nz):
@@ -121,9 +243,10 @@ def _get_corner_bin_index(nx, ny, nz):
return bin_index
def __str__(self):
+ """Print summary of the UniformDensityField."""
n_poly = len(self.polymers)
- return "UniformRectField"
+ return f"UniformDensityField"
def _recompute_field(self, check_consistency=False):
new_density = self.density.copy()
@@ -138,49 +261,63 @@ def _recompute_field(self, check_consistency=False):
" bug in this code.")
def compute_dE(self, poly, ind0, indf, r, t3, t2, states):
+ """
+ Compute change in energy based on proposed new polymer location.
+
+ Requires the polymer to be moved in order to compare to old state.
+ """
# even if the move does not change the states, we cannot ignore them
# because they're needed to compute the energy at any point along the
# polymer
if states is None:
states = poly.states
density_poly, index_xyz = self._calc_density(
- poly.r[ind0:indf, :], poly.states, ind0, indf)
- density_poly_trial, index_xyz_trial = self._calc_density(r, states, ind0, indf)
- delta_density_poly_total = np.concatenate((density_poly_trial, -density_poly))
- delta_index_xyz_total = np.concatenate((index_xyz_trial, index_xyz)).astype(int)
- delta_density, delta_index_xyz = combine_repeat(delta_density_poly_total, delta_index_xyz_total)
-
- delta_energy_epigen = 0
- for i, (_, epi_info) in enumerate(poly.epigenmarks.iterrows()):
- delta_energy_epigen += 0.5 * epi_info.interaction_energy * np.sum(
- (delta_density[:, i + 1] + self.density[delta_index_xyz, i + 1]) ** 2
- - self.density[delta_index_xyz, i + 1] ** 2)
- return delta_energy_epigen
+ poly.r[ind0:indf, :], poly.states, ind0, indf)
+ density_poly_trial, index_xyz_trial = self._calc_density(
+ r, states, ind0, indf)
+ delta_density_poly_total = np.concatenate((
+ density_poly_trial, -density_poly))
+ delta_index_xyz_total = np.concatenate(
+ (index_xyz_trial, index_xyz)).astype(int)
+ delta_density, delta_index_xyz = combine_repeat(
+ delta_density_poly_total, delta_index_xyz_total)
+
+ dE_marks = 0
+ for i, (_, mark_info) in enumerate(self.marks.iterrows()):
+ dE_marks += 0.5 * mark_info.interaction_energy * np.sum(
+ (
+ delta_density[:, i + 1]
+ + self.density[delta_index_xyz, i + 1]
+ ) ** 2
+ - self.density[delta_index_xyz, i + 1] ** 2
+ )
+ return dE_marks
def _calc_density(self, r_poly, states, ind0, indf):
- num_epigenmark = states.shape[1]
+ num_marks = states.shape[1]
# Find the (0,0,0) bins for the beads and the associated weights
- x_poly_box = (r_poly[:, 0] - 0.5 * self.dx
- - self.x_width * np.floor((r_poly[:, 0] - 0.5 * self.dx) / self.x_width))
+ x_poly_box = (r_poly[:, 0] - 0.5 * self.dx - self.x_width * np.floor(
+ (r_poly[:, 0] - 0.5 * self.dx) / self.x_width))
index_x = np.floor(self.nx * x_poly_box / self.x_width).astype(int)
weight_x = 1 - (x_poly_box / self.dx - index_x)
- y_poly_box = (r_poly[:, 1] - 0.5 * self.dy
- - self.y_width * np.floor((r_poly[:, 1] - 0.5 * self.dy) / self.y_width))
+ y_poly_box = (r_poly[:, 1] - 0.5 * self.dy - self.y_width * np.floor(
+ (r_poly[:, 1] - 0.5 * self.dy) / self.y_width))
index_y = np.floor(self.ny * y_poly_box / self.y_width).astype(int)
weight_y = 1 - (y_poly_box / self.dy - index_y)
- z_poly_box = (r_poly[:, 2] - 0.5 * self.dz
- - self.z_width * np.floor((r_poly[:, 2] - 0.5 * self.dz) / self.z_width))
+ z_poly_box = (r_poly[:, 2] - 0.5 * self.dz - self.z_width * np.floor(
+ (r_poly[:, 2] - 0.5 * self.dz) / self.z_width))
index_z = np.floor(self.nz * z_poly_box / self.z_width).astype(int)
weight_z = 1 - (z_poly_box / self.dz - index_z)
- index_x0y0z0 = index_x + self.nx * index_y + self.nx * self.ny * index_z
+ index_x0y0z0 = index_x + self.nx*index_y + self.nx*self.ny*index_z
# Generate the weight array for all of the bins containing the beads
+ #TODO if you clean this up, please use append instead
weight = weight_x * weight_y * weight_z
weight = np.concatenate((weight, (1 - weight_x) * weight_y * weight_z))
weight = np.concatenate((weight, weight_x * (1 - weight_y) * weight_z))
@@ -190,6 +327,7 @@ def _calc_density(self, r_poly, states, ind0, indf):
weight = np.concatenate((weight, weight_x * (1 - weight_y) * (1 - weight_z)))
weight = np.concatenate((weight, (1 - weight_x) * (1 - weight_y) * (1 - weight_z)))
+ #TODO this can be done in one line
index_xyz_total = index_x0y0z0
index_xyz_total = np.concatenate((index_xyz_total, self.bin_index[index_x0y0z0, 1])).astype(int)
index_xyz_total = np.concatenate((index_xyz_total, self.bin_index[index_x0y0z0, 2])).astype(int)
@@ -199,14 +337,15 @@ def _calc_density(self, r_poly, states, ind0, indf):
index_xyz_total = np.concatenate((index_xyz_total, self.bin_index[index_x0y0z0, 6])).astype(int)
index_xyz_total = np.concatenate((index_xyz_total, self.bin_index[index_x0y0z0, 7])).astype(int)
- density_total = np.zeros((8 * (indf - ind0), num_epigenmark + 1), 'd')
+ density_total = np.zeros((8 * (indf - ind0), num_marks + 1), 'd')
density_total[:, 0] = weight
- for ind_epigen in range(num_epigenmark):
+ for ind_mark in range(num_marks):
for ind_corner in range(8):
ind_corner0 = ind_corner * (indf - ind0)
ind_cornerf = ind_corner0 + indf - ind0
- density_total[ind_corner0:ind_cornerf, ind_epigen + 1] = \
- weight[ind_corner0:ind_cornerf] * states[ind0:indf, ind_epigen]
+ density_total[ind_corner0:ind_cornerf, ind_mark + 1] = \
+ weight[ind_corner0:ind_cornerf] \
+ * states[ind0:indf, ind_mark]
# Combine repeat entries in the density array
density, index_xyz = combine_repeat(density_total, index_xyz_total)
diff --git a/chromo/image.py b/chromo/image.py
new file mode 100644
index 0000000..948932f
--- /dev/null
+++ b/chromo/image.py
@@ -0,0 +1,189 @@
+r"""
+Imaging module - Generate image files from simulations for PyMol imaging
+
+Notes
+-----
+
+"""
+
+
+import numpy as np
+
+
+def gen_pymol_file(r_poly, meth_seq = np.array([]), hp1_seq = np.array([]), limit_n = False, n_max = 100000,
+ max_method = 'mid_slice', ind_save = np.array([]),
+ add_com = False, filename='r_poly.pdb', ring=False):
+ r"""
+
+ Parameters
+ ----------
+ r_poly : (num_beads, 3) float
+ Conformation of the chain subjected to active-Brownian forces
+ meth_seq : (num_beads) int
+ Epigenetic sequence (0 = No tails methylated, 1 = 1 tail methylated, 2 = 2 tails methylated)
+ hp1_seq : (num_beads) int
+ Number of tails with HP1 bound
+ limit_n : Boolean
+
+ filename : str
+ File name to write the pdb file
+ ring : bool
+ Boolean to close the polymer into a ring
+
+ Returns
+ -------
+ none
+
+ """
+
+ # Open the file
+ f = open(filename, 'w')
+
+ atomname1 = "A1" # Chain atom type
+ resname = "SSN" # Type of residue (UNKnown/Single Stranded Nucleotide)
+ chain = "A" # Chain identifier
+ resnum = 1
+ numresidues = len(r_poly[:, 0])
+ descrip = "Pseudo atom representation of DNA"
+ chemicalname = "Body and ribbon spatial coordinates"
+ if len(meth_seq) == 0:
+ image_meth_seq = False
+ else:
+ image_meth_seq = True
+
+ # Determine the bead indices to reduce the total represented to n_max if limit_n True
+
+ if not ind_save.size == 0:
+ connect_left, connect_right = find_connect(ind_save, ring)
+ else:
+ if n_max >= numresidues:
+ limit_n = False
+ if limit_n:
+ if max_method == 'mid_slice':
+ ind_save, connect_left, connect_right = find_ind_mid_slice(r_poly, n_max, ring)
+ else:
+ ind_save, connect_left, connect_right = find_ind_total(r_poly, ring)
+
+ # Write the preamble to the pymol file
+
+ f.write('HET %3s %1s%4d %5d %-38s\n' % (resname, chain, resnum, numresidues, descrip))
+ f.write('HETNAM %3s %-50s\n' % (resname, chemicalname))
+ f.write('FORMUL 1 %3s C20 N20 P21\n' % (resname))
+
+ # Write the conformation to the pymol file
+
+ count = 0
+ for ind in range(numresidues):
+ if ind_save[ind] == 1:
+ if image_meth_seq:
+ atomname = 'A' + str(int(meth_seq[ind]))
+ else:
+ atomname = atomname1
+ f.write('ATOM%7d %4s %3s %1s %8.3f%8.3f%8.3f%6.2f%6.2f C\n' %
+ (count + 1, atomname, resname, chain, r_poly[ind, 0], r_poly[ind, 1], r_poly[ind, 2], 1.00, 1.00))
+ count += 1
+ numresidues_save = count
+
+ # Add a nucleus bead to the pymol file
+
+ atomname = 'AN'
+ chain = 'B'
+ r_com = np.mean(r_poly, axis = 0)
+ if add_com:
+ f.write('ATOM%7d %4s %3s %1s %8.3f%8.3f%8.3f%6.2f%6.2f C\n' %
+ (count + 1, atomname, resname, chain, r_com[0], r_com[1], r_com[2], 1.00, 1.00))
+
+ # Define the connectivity in the chain
+
+ count = 0
+ for ind in range(numresidues):
+ if ind_save[ind] == 1:
+ if ind == 0:
+ ind_left = numresidues_save - 1
+ else:
+ ind_left = count - 1
+ if ind == numresidues - 1:
+ ind_right = 0
+ else:
+ ind_right = count + 1
+
+ if connect_left[ind] == 1 and connect_right[ind] == 1:
+ f.write('CONECT%5d%5d%5d\n' % (count + 1, ind_left + 1, ind_right + 1))
+ elif connect_left[ind] == 1 and connect_right[ind] == 0:
+ f.write('CONECT%5d%5d\n' % (count + 1, ind_left + 1))
+ elif connect_left[ind] == 0 and connect_right[ind] == 1:
+ f.write('CONECT%5d%5d\n' % (count + 1, ind_right + 1))
+ elif connect_left[ind] == 0 and connect_right[ind] == 0:
+ f.write('CONECT%5d\n' % (count + 1))
+ count += 1
+
+ # Close the file
+ f.write('END')
+ f.close()
+
+ return
+
+
+def find_ind_mid_slice(r_poly, n_max, ring):
+ r"""
+
+ """
+
+ # Find the value of the x-coordinate and sort the beads according to distance from mean
+ x_ave = np.mean(r_poly[:, 0])
+ delta_x = r_poly[:, 0] - x_ave
+# ind_sort = np.argsort(np.abs(delta_x))
+ ind_sort = np.argsort(-delta_x)
+
+ # Select the first n_max beads based on distance from the mean
+ ind_save = np.zeros(len(r_poly[:, 0]))
+
+ for ind in range(n_max):
+ ind_save[ind_sort[ind]] = 1
+
+ # Determine whether adjacent beads are saved to define connections
+ connect_left, connect_right = find_connect(ind_save, ring)
+
+ return ind_save, connect_left, connect_right
+
+
+def find_ind_total(r_poly, ring):
+ r"""
+
+ """
+
+ ind_save = np.ones(len(r_poly[:, 0]))
+ connect_left, connect_right = find_connect(ind_save, ring)
+
+ return ind_save, connect_left, connect_right
+
+
+def find_connect(ind_save, ring):
+ r"""
+
+
+ """
+ connect_left = np.zeros(len(ind_save))
+ connect_right = np.zeros(len(ind_save))
+
+ for ind in range(len(ind_save)):
+ if ind_save[ind] * ind_save[ind - 1] == 1:
+ connect_left[ind] = 1
+
+ if ind == (len(ind_save) - 1):
+ if ind_save[ind] * ind_save[0] == 1:
+ connect_right[ind] = 1
+ else:
+ if ind_save[ind] * ind_save[ind + 1] == 1:
+ connect_right[ind] = 1
+
+ # Remove end connection if ring False
+
+ if not ring:
+ connect_left[0] = 0
+
+ if not ring:
+ connect_right[-1] = 0
+
+ return connect_left, connect_right
+
diff --git a/chromo/marks.py b/chromo/marks.py
new file mode 100644
index 0000000..25c583f
--- /dev/null
+++ b/chromo/marks.py
@@ -0,0 +1,83 @@
+"""
+Modifications to the polymer's chemical state that affect the energy.
+
+Each simulation type will typically require a particular type of Mark, for
+which there will be a class defined here. In addition, for common, physically
+derived marks, such as known epigenetic modifications to DNA or
+well-characterized modifications to other real polymers, the best-known
+parameters for that mark should be documented here, as instances of the
+appropriate `.Mark` subclass.
+"""
+
+import dataclasses
+import inspect
+import sys
+
+import pandas as pd
+
+
+@dataclasses.dataclass
+class Mark:
+ """In order for our code to work, all marks need a string name."""
+
+ name: str
+
+
+@dataclasses.dataclass
+class Epigenmark(Mark):
+ """Information about the chemical properties of an epigenetic mark."""
+
+ bind_energy: float
+ interaction_energy: float
+ chemical_potential: float
+
+
+hp1 = Epigenmark('HP1', bind_energy=1, interaction_energy=1,
+ chemical_potential=1)
+"""
+Heterochromatin Protein 1, binds H3K9me marks.
+
+For now, we just use filler values for the energies. In the future, this
+documentation string should go through the process of explaining exactly how we
+arrive at the values that we actually use.
+"""
+
+
+def get_by_name(name):
+ """Look up saved mark by name."""
+ all_marks = [obj for name, obj in inspect.getmembers(sys.modules[__name__])
+ if isinstance(obj, Mark)]
+ matching_marks = [mark for mark in all_marks if mark.name == name]
+ if not matching_marks:
+ raise ValueError(f"No marks found in {__name__} with name: {name}")
+ if len(matching_marks) > 1:
+ raise ValueError(f"More than one mark has the name requested: {name}")
+ return matching_marks[0]
+
+
+def make_mark_collection(marks):
+ """
+ Construct summary DataFrame from sequence of marks.
+
+ Parameters
+ ----------
+ marks : Mark or str or Sequence[Mark] or Sequence[str]
+ The marks to be summarized by the DataFrame.
+
+ Returns
+ -------
+ pd.DataFrame
+ Columns are the properties of each Mark.
+ """
+ df = pd.DataFrame(columns=['name', 'bind_energy', 'interaction_energy',
+ 'chemical_potential'])
+ if marks is None:
+ return None
+ input_type = type(marks)
+ if input_type is str or issubclass(input_type, Mark):
+ marks = [marks] # allow the "one mark" case
+ for mark in marks:
+ if type(mark) is str:
+ mark = get_by_name(mark)
+ df = df.append(dataclasses.asdict(mark), ignore_index=True)
+ return df
diff --git a/chromo/mc/__init__.py b/chromo/mc/__init__.py
index 407ac78..8f1af93 100644
--- a/chromo/mc/__init__.py
+++ b/chromo/mc/__init__.py
@@ -1,27 +1,51 @@
"""Monte Carlo simulations of a discrete wormlike chain."""
from pathlib import Path
+import warnings
import numpy as np
from .mc_sim import mc_sim
from .moves import all_moves
+from ..util.reproducibility import make_reproducible
+from ..components import Polymer
+from ..marks import get_by_name, make_mark_collection
+from ..fields import UniformDensityField
-def polymer_in_field(polymers, epigenmarks, field, num_mc_steps, num_save_mc,
- mc_moves=None, output_dir='.'):
+@make_reproducible
+def simple_mc(num_polymers, num_beads, bead_length, num_marks, num_save_mc,
+ num_saves, x_width, nx, y_width, ny, z_width, nz, random_seed=0,
+ output_dir='.'):
+ polymers = [
+ Polymer.straight_line_in_x(
+ f'Polymer-{i}', num_beads, bead_length,
+ states=np.zeros((num_beads, num_marks)),
+ mark_names=num_marks*['HP1']
+ ) for i in range(num_polymers)
+ ]
+ marks = [get_by_name('HP1') for i in range(num_marks)]
+ marks = make_mark_collection(marks)
+ field = UniformDensityField(polymers, marks, x_width, nx, y_width, ny,
+ z_width, nz)
+ return _polymer_in_field(polymers, marks, field, num_save_mc, num_saves,
+ random_seed=random_seed, output_dir=output_dir)
+
+
+def _polymer_in_field(polymers, marks, field, num_save_mc, num_saves,
+ mc_moves=None, random_seed=0, output_dir='.'):
"""
Monte Carlo simulation of a tssWLC in a field.
This example code can be used to understand how to call the codebase, or
- run directly for simple simulations. See the documentation for the
+ run directly for simple simulations.
Parameters
----------
polymers : Sequence[Polymer]
- The polymers to be simulated. Epigenetic marks, discretization, and
- other parameters should be specified before input.
- epigenmarks : Sequence[Epigenmark]
- DEPRECATED. Should be integrated into the "Polymer" class.
+ The polymers to be simulated.
+ epigenmarks : `pd.DataFrame`
+ Output of `chromo.marks.make_mark_collection`. Summarizes the energetic
+ properties of each chemical modification.
field : Field
The discretization of space in which to simulate the polymers.
num_save_mc : int
@@ -31,11 +55,15 @@ def polymer_in_field(polymers, epigenmarks, field, num_mc_steps, num_save_mc,
output_dir : Optional[Path], default: '.'
Directory in which to save the simulation output.
"""
+ warnings.warn("The random seed is currently ignored.", UserWarning)
if mc_moves is None:
mc_moves = all_moves
# Perform Monte Carlo simulation for each save file
- for mc_count in range(num_save_mc):
- mc_sim(polymers, epigenmarks, num_mc_steps, mc_moves, field)
+ for mc_count in range(num_saves):
+ mc_sim(polymers, marks, num_save_mc, mc_moves, field)
for poly in polymers:
- poly.write_repr(output_dir / Path(f"{poly.name}-{mc_count}.npz"))
+ poly.to_csv(output_dir / Path(f"{poly.name}-{mc_count}.csv"))
print("Save point " + str(mc_count) + " completed")
+
+
+polymer_in_field = make_reproducible(_polymer_in_field)
diff --git a/chromo/mc/mc_sim.py b/chromo/mc/mc_sim.py
index 2dafdb7..e4a06b5 100644
--- a/chromo/mc/mc_sim.py
+++ b/chromo/mc/mc_sim.py
@@ -23,7 +23,7 @@ def mc_step(adaptible_move, poly, epigenmarks, field):
if np.random.rand() < np.exp(-dE):
adaptible_move.accept()
poly.r[ind0:indf, :] = r
- poly.t_3[ind0:indf, :] = t3
- poly.t_2[ind0:indf, :] = t2
+ poly.t3[ind0:indf, :] = t3
+ poly.t2[ind0:indf, :] = t2
poly.states[ind0:indf, :] = states
return
diff --git a/chromo/mc/moves.py b/chromo/mc/moves.py
index 6290ef3..14154b7 100644
--- a/chromo/mc/moves.py
+++ b/chromo/mc/moves.py
@@ -7,6 +7,7 @@
import numpy as np
+
class MCAdapter:
"""
Track success rate and adjust parameters for a Monte Carlo move.
@@ -95,7 +96,7 @@ def crank_shaft_move(polymer, amp_move, amp_bead):
rot_angle = amp_move * (np.random.rand() - 0.5)
if ind0 == (indf + 1):
- delta_t3 = polymer.t_3[ind0, :]
+ delta_t3 = polymer.t3[ind0, :]
else:
delta_t3 = polymer.r[indf - 1, :] - polymer.r[ind0, :]
delta_t3 /= np.linalg.norm(delta_t3)
@@ -129,7 +130,8 @@ def crank_shaft_move(polymer, amp_move, amp_bead):
t3_poly_trial = np.zeros((indf - ind0, 3), 'd')
for i_bead in range(ind0, indf):
r_poly_trial[i_bead - ind0, :] = rot_vector + np.matmul(rot_matrix, polymer.r[i_bead, :])
- t3_poly_trial[i_bead - ind0, :] = np.matmul(rot_matrix, polymer.t_3[i_bead, :])
+ t3_poly_trial[i_bead - ind0, :] = np.matmul(rot_matrix, polymer.t3[i_bead, :])
return ind0, indf, r_poly_trial, t3_poly_trial, None, None
all_moves = [MCAdapter(move) for move in [crank_shaft_move]]
+
diff --git a/chromo/util/reproducibility.py b/chromo/util/reproducibility.py
index 14ed934..adb30ba 100644
--- a/chromo/util/reproducibility.py
+++ b/chromo/util/reproducibility.py
@@ -16,8 +16,10 @@
"""
from pathlib import Path
import inspect
+import collections
import pandas as pd
+import numpy as np
# get version number (includes commit hash) from versioneer
from .._version import get_versions
@@ -37,9 +39,11 @@ def make_reproducible(sim):
f"""
Decorator for automagically making simulations reproducible.
- Requires every input to the function to have a __repr__ that is actually
- useful, and that the wrapped function has a keyword argument with name
- {output_dir_param_name}. Also requires you to pass a "random_seed" kwarg.
+ Requires every non-builtin input to the function to implement a
+ ``.to_file`` method and a ``.name`` attribute. These will be used to save
+ the state of the simulation's input to the output folder. The wrapped
+ function must have a keyword argument with name {output_dir_param_name}.
+ Also requires you to pass a "random_seed" kwarg.
The versioneer version (including a commit hash if necessary), the
date/time, a unique folder name where the simulation inputs and output will
@@ -73,11 +77,11 @@ def make_reproducible(sim):
├── paramN
└── (Any output of simulator goes here)
"""
- params = inspect.signature(sim)
+ params = inspect.signature(sim).parameters
if "random_seed" not in params:
raise ValueError("Requires the random seed to be passed in explicitly,"
- " even if the random generator is passed to the "
- "function as well.")
+ " even if the random generator is passed to the"
+ " function as well.")
if output_dir_param_name not in params:
raise ValueError(f"Could not wrap simulation function: {sim}\n"
f"Missing required kwarg: {output_dir_param_name}.")
@@ -90,19 +94,28 @@ def wrapper(*args, **kwargs):
"no default value can be found.")
kwargs[output_dir_param_name] = outdir_param.default
# get a new folder to save simulation into in a thread-safe way
- outdir = kwargs[output_dir_param_name]
- output_subfolder = get_unique_subfolder(Path(outdir)/sim_folder_prefix)
+ outdir = Path(kwargs[output_dir_param_name])
+ # make the requested base output folder first if it doesn't exist tho..
+ outdir.mkdir(parents=True, exist_ok=True)
+ output_subfolder = get_unique_subfolder(outdir / sim_folder_prefix)
kwargs[output_dir_param_name] = output_subfolder
# get the inputs that can be simply saved in our CSV file
simple_params, hard_params = split_builtin_params(sim, *args, **kwargs)
+ # add the extra outputs that we promise users
+ simple_params['version'] = __version__
+ simple_params['start_time'] = pd.Timestamp.now()
# append to the CSV file, unless it doesn't exist
- # TODO make thread-safe
- sim_tracker = output_subfolder / sim_tracker_name
+ sim_tracker = outdir / sim_tracker_name
+ # TODO make writing header thread-safe
header = not sim_tracker.exists()
simple_params = pd.DataFrame(pd.Series(simple_params)).T
simple_params.to_csv(sim_tracker, header=header, index=False, mode='a')
- # TODO add support for non-simple params
- return sim(*args, **kwargs)
+ # the remaining parameters should implement `.to_file`/`.from_file`.
+ to_file_params(hard_params, output_subfolder)
+ sim_out = sim(*args, **kwargs)
+ with open(output_subfolder / Path('__completion_time__'), 'w') as f:
+ f.write(str(pd.Timestamp.now()))
+ return sim_out
return wrapper
@@ -111,8 +124,15 @@ def wrapper(*args, **kwargs):
# builtin_types = tuple(getattr(builtins, t) for t in dir(builtins)
# if isinstance(getattr(builtins, t), type))
builtin_types = [bool, bytes, bytearray, complex, float, int, str]
-# the additional types are not allowed by default because they make loading in
-# the CSV a little trickier
+"""
+Types to be saved as CSV entries by default.
+
+These are basically just the built-ins that permit simple save/load cycling via
+CSV entries with Pandas. Some extras are included in the split_builtin_params
+function itself. Please see that code for a full description.
+"""
+# these additional types are not allowed by default because they make loading
+# in the CSV a little trickier
# ..., frozenset, tuple]
@@ -121,7 +141,8 @@ def split_builtin_params(sim, *args, **kwargs):
builtin_args = {}
non_builtin = {}
for arg_name, value in sig.arguments.items():
- if type(value) in builtin_types:
+ dtype = type(value)
+ if dtype in builtin_types or issubclass(dtype, Path):
builtin_args[arg_name] = value
else:
non_builtin[arg_name] = value
@@ -157,3 +178,36 @@ def get_unique_subfolder(root):
except:
i += 1
return folder_name
+
+
+def to_file_params(non_builtins_kwargs, folder, suffix=''):
+ """
+ Call ``.to_file`` for each parameter, handling sequences.
+
+ Parameters
+ ----------
+ non_builtins_kwargs : Dict[str, RoundTrippable]
+ A RoundTrippable must implement the
+ ``.name``/``.to_file``/``.from_file`` interface, or be a standard type,
+ such as a numpy array or a DataFrame.
+ folder : Path
+ The folder to save the output to.
+ """
+ for arg_name, value in non_builtins_kwargs.items():
+ dtype = type(value)
+ if isinstance(value, collections.Sequence):
+ for i, data in enumerate(value):
+ to_file_params({arg_name: data}, folder, str(i))
+ elif hasattr(value, 'to_file'):
+ value.to_file(folder / Path(value.name + suffix))
+ elif issubclass(dtype, pd.DataFrame) or issubclass(dtype, pd.Series):
+ value.to_csv(folder / Path(arg_name + suffix))
+ elif issubclass(dtype, np.ndarray):
+ np.savetxt(folder / Path(arg_name + suffix), value)
+ else:
+ raise ValueError(f"Argument not understood: {arg_name}={value}.\n"
+ "This simulation cannot be made reproducible.\n"
+ f"Please implement `.to_file` for type: {dtype}.")
+
+
+
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..d0c3cbf
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/make.bat b/doc/make.bat
new file mode 100644
index 0000000..6247f7e
--- /dev/null
+++ b/doc/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/doc/source/bd_sim.rst b/doc/source/bd_sim.rst
new file mode 100644
index 0000000..01a69ab
--- /dev/null
+++ b/doc/source/bd_sim.rst
@@ -0,0 +1,169 @@
+.. _bd_sim:
+
+Brownian Dynamics Simulations
+=============================
+
+Langevin equation for translation
+---------------------------------
+
+We consider a discrete chain of :math:`n_{b}` beads with positions :math:`\vec{r}^{(n)}`,
+where the bead index :math:`n` runs from 0 to :math:`n_{b}-1`.
+We consider a general potential energy :math:`E = E( \{ \vec{r} \} )`, where :math:`\{ \vec{r} \}` indicates
+the full set of positions.
+We define the potential forces on the beads to be
+
+.. math::
+ \vec{f}_{E}^{(n)} = - \frac{\partial E}{\partial \vec{r}^{(n)}}
+
+The Langevin equation for the bead positions is given by
+
+.. math::
+ \xi_{r} \frac{\partial \vec{r}^{(n)}}{\partial t} = \vec{f}_{E}^{(n)} + \vec{f}_{B}^{(n)}
+
+The Brownian force :math:`\vec{f}_{B}^{(n)}` is governed by the fluctuation dissipation theorem,
+which states that :math:`\vec{f}_{B}^{(n)}` is a
+Gaussian-distributed random force
+that satisfies
+
+.. math::
+ \langle \vec{f}_{B}^{(n)}(t) \rangle & = & 0, \\
+ \langle \vec{f}_{B}^{(n)}(t) \vec{f}_{B}^{(n')}(t') \rangle
+ & = &
+ 2 k_{B} T \xi \delta (t - t') \delta_{nn'} \mathbf{I}.
+
+This stochastic differential equation is numerically integrated by
+discretizing time in discrete time steps :math:`\delta t`.
+Thus, the discrete time at the :math:`j`th time step is
+given by :math:`t_{j} = \delta t (j-1)`, where :math:`t_{0} = 0`.
+Based on this time discretization, we numerically integrate the Langevin
+equation as
+
+.. math::
+ \vec{r}^{(n)}_{j+1} =
+ \vec{r}^{(n)}_{j} +
+ \frac{\delta t}{\xi_{r}} \left(
+ \vec{f}_{E_{j}}^{(n)} + \vec{f}_{B_{j}}^{(n)}
+ \right)
+
+which suggests an explicit integration algorithm.
+
+The fluctuation dissipation for the discrete time
+
+.. math::
+ \langle \vec{f}_{B_{j}}^{(n)} \rangle & = & 0, \\
+ \langle \vec{f}_{B_{j}}^{(n)} \vec{f}_{B_{j'}}^{(n')} \rangle
+ & = &
+ \frac{2 k_{B} T \xi}{\delta t} \delta_{jj'} \delta_{nn'} \mathbf{I}.
+
+Thus, the value of :math:`\vec{f}_{B_{j}}^{(n)}` is selected from a Gaussian distribution
+with variance :math:`\frac{2 k_{B} T \xi}{\delta t}`.
+
+Langevin equation for translation and rotation
+----------------------------------------------
+
+We consider a discrete chain of :math:`n_{b}` beads with positions :math:`\vec{r}^{(n)}` and orientation triad :math:`\vec{t}_{i}^{(n)}`,
+where the bead index :math:`n` runs from 0 to :math:`n_{b}-1`.
+We consider a general potential energy :math:`E = E( \{ \vec{r}, \vec{t}_{i} \} )`, where :math:`\{ \vec{r}, \vec{t}_{i} \}` indicates
+the full set of positions and orientations.
+Since the orientation triad forms an orthonormal basis, the dynamics must maintain the following conditions:
+
+.. math::
+ \vec{t}_{i}^{(n)} \cdot \vec{t}_{j}^{(n)} = \delta_{ij}
+
+.. \label{eq:constraint}
+
+for all :math:`i,j` pairs.
+These conditions are enforced through Lagrange constraints. Thus, we define the constrained energy
+
+.. math::
+ E_{\lambda} ( \{ \vec{r}, \vec{t}_{i} \} ) = E( \{ \vec{r}, \vec{t}_{i} \} ) -
+ \sum_{n=0}^{n_{b}-1} \sum_{i,j = 1}^{3}
+ \frac{\lambda_{ij}}{2}
+ \left(
+ \vec{t}_{i}^{(n)} \cdot \vec{t}_{j}^{(n)} - \delta_{ij}
+ \right)
+
+We define the potential forces and torques on the beads to be
+
+.. math::
+ \vec{f}_{E}^{(n)} & = & - \frac{\partial E}{\partial \vec{r}^{(n)}} \\
+ \vec{\tau}_{E,i}^{(n)} & = & - \frac{\partial E}{\partial \vec{t}_{i}^{(n)}}
+
+The Langevin equation for the bead positions is given by
+
+.. math::
+ \xi_{r} \frac{\partial \vec{r}^{(n)}}{\partial t} = \vec{f}_{E}^{(n)} + \vec{f}_{B}^{(n)}
+
+where :math:`\vec{f}_{B}^{(n)}` is a Brownian force (discussed below).
+
+The Langevin equation for the orientation triad must resolve the orthonormal constraints. This leads to the three Langevin equations
+
+.. math::
+ \xi_{t,i} \frac{\partial \vec{t}_{i}^{(n)}}{\partial t} = \vec{\tau}_{E,i}^{(n)} + \vec{\tau}_{B,i}^{(n)}
+ + \sum_{j=1}^{3} \lambda_{ij} \vec{t}_{j}^{(n)}
+
+The Lagrange constraints are satisfied by setting the time derivative of Eq.~\ref{eq:constraint} = 0,
+or we have
+
+.. math::
+ \vec{t}_{i}^{(n)} \cdot \frac{\partial \vec{t}_{j}^{(n)}}{\partial t} +
+ \vec{t}_{j}^{(n)} \cdot \frac{\partial \vec{t}_{i}^{(n)}}{\partial t} = 0
+
+This leads to the solution to the Lagrange multipliers to be
+
+.. math::
+ \lambda_{ij} = - \left( \xi_{t,i} + \xi_{t,j} \right)^{-1}
+ \left[
+ \xi_{t,i} \vec{t}_{i}^{(n)} \cdot \left( \vec{\tau}_{E,j}^{(n)} + \vec{\tau}_{B,j}^{(n)} \right) +
+ \xi_{t,j} \vec{t}_{j}^{(n)} \cdot \left( \vec{\tau}_{E,i}^{(n)} + \vec{\tau}_{B,i}^{(n)} \right)
+ \right]
+
+With this development, we now write the Langevin equations as
+
+.. math::
+ \xi_{t,1} \frac{\partial \vec{t}_{1}^{(n)}}{\partial t} & = &
+ \frac{\xi_{t,1}}{\xi_{t,1}+\xi_{t,2}}
+ \left[
+ \vec{t}_{2}^{(n)} \cdot \left( \vec{\tau}_{E,1}^{(n)} + \vec{\tau}_{B,1}^{(n)} \right) -
+ \vec{t}_{1}^{(n)} \cdot \left( \vec{\tau}_{E,2}^{(n)} + \vec{\tau}_{B,2}^{(n)} \right)
+ \right] \vec{t}_{2}^{(n)} +
+ \nonumber \\
+ & &
+ \frac{\xi_{t,1}}{\xi_{t,1}+\xi_{t,3}}
+ \left[
+ \vec{t}_{3}^{(n)} \cdot \left( \vec{\tau}_{E,1}^{(n)} + \vec{\tau}_{B,1}^{(n)} \right) -
+ \vec{t}_{1}^{(n)} \cdot \left( \vec{\tau}_{E,3}^{(n)} + \vec{\tau}_{B,3}^{(n)} \right)
+ \right] \vec{t}_{3}^{(n)} \\
+ \xi_{t,2} \frac{\partial \vec{t}_{2}^{(n)}}{\partial t} & = &
+ \frac{\xi_{t,2}}{\xi_{t,1}+\xi_{t,2}}
+ \left[
+ \vec{t}_{1}^{(n)} \cdot \left( \vec{\tau}_{E,2}^{(n)} + \vec{\tau}_{B,2}^{(n)} \right) -
+ \vec{t}_{2}^{(n)} \cdot \left( \vec{\tau}_{E,1}^{(n)} + \vec{\tau}_{B,1}^{(n)} \right)
+ \right] \vec{t}_{1}^{(n)} +
+ \nonumber \\
+ & &
+ \frac{\xi_{t,2}}{\xi_{t,2}+\xi_{t,3}}
+ \left[
+ \vec{t}_{3}^{(n)} \cdot \left( \vec{\tau}_{E,2}^{(n)} + \vec{\tau}_{B,2}^{(n)} \right) -
+ \vec{t}_{2}^{(n)} \cdot \left( \vec{\tau}_{E,3}^{(n)} + \vec{\tau}_{B,3}^{(n)} \right)
+ \right] \vec{t}_{3}^{(n)} \\
+ \xi_{t,3} \frac{\partial \vec{t}_{3}^{(n)}}{\partial t} & = &
+ \frac{\xi_{t,3}}{\xi_{t,1}+\xi_{t,3}}
+ \left[
+ \vec{t}_{1}^{(n)} \cdot \left( \vec{\tau}_{E,3}^{(n)} + \vec{\tau}_{B,3}^{(n)} \right) -
+ \vec{t}_{3}^{(n)} \cdot \left( \vec{\tau}_{E,1}^{(n)} + \vec{\tau}_{B,1}^{(n)} \right)
+ \right] \vec{t}_{1}^{(n)} +
+ \nonumber \\
+ & &
+ \frac{\xi_{t,3}}{\xi_{t,2}+\xi_{t,3}}
+ \left[
+ \vec{t}_{2}^{(n)} \cdot \left( \vec{\tau}_{E,3}^{(n)} + \vec{\tau}_{B,3}^{(n)} \right) -
+ \vec{t}_{3}^{(n)} \cdot \left( \vec{\tau}_{E,2}^{(n)} + \vec{\tau}_{B,2}^{(n)} \right)
+ \right] \vec{t}_{2}^{(n)}
+
+
+Brownian dynamics simulations with constrained bond lengths
+-----------------------------------------------------------
+
+Overview of bond constraints
+Ref [Hinch1994]_
\ No newline at end of file
diff --git a/doc/source/code_structure.rst b/doc/source/code_structure.rst
new file mode 100644
index 0000000..fc3c3c9
--- /dev/null
+++ b/doc/source/code_structure.rst
@@ -0,0 +1,9 @@
+.. _code_structure:
+
+Code Overview and Structure
+===========================
+
+We provide an overview of the 'chromo' code structure and the individual classes and modules that
+are responsible for the individual simulation capabilities.
+
+9/18/20 - Bruno will update this page with a code map and discussion
\ No newline at end of file
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 0000000..59bb2d3
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,71 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../'))
+sys.path.insert(1, os.path.abspath('../../'))
+
+# -- Project information -----------------------------------------------------
+
+project = 'chromo'
+copyright = '2020, Andy Spakowitz, Bruno Beltran, Joe Wakim'
+author = 'Andy Spakowitz, Bruno Beltran, Joe Wakim'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'nbsphinx',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.mathjax',
+ 'sphinx.ext.ifconfig',
+ 'sphinx.ext.viewcode',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.githubpages',
+ 'matplotlib.sphinxext.plot_directive'
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'nature'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Autodoc settings
+autosummary_generate = True
+
+# Napoleon settings
+napoleon_google_docstring = False
+napoleon_numpy_docstring = True
diff --git a/doc/source/field.rst b/doc/source/field.rst
new file mode 100644
index 0000000..aa682da
--- /dev/null
+++ b/doc/source/field.rst
@@ -0,0 +1,69 @@
+.. _field:
+
+
+Field-Theoretic Treatment of Interactions
+=========================================
+
+Our treatment of interactions uses a field-theoretic treatment of the densities to determine the interactions between polymer
+segments. Following work by Pike, et al. (Refs. [Pike2009a]_, [Pike2009b]_),
+we define
+
+The simulation has a fixed volume with sides lengths :math:`L_{x}`, :math:`L_{y}`, and :math:`L_{z}`.
+These lengths are discretize into :math:`M_{x}`, :math:`M_{y}`, and :math:`M_{z}` bins of length
+:math:`\Delta_{x} = L_{x}/M_{x}`,
+:math:`\Delta_{y} = L_{y}/M_{y}`, and
+:math:`\Delta_{z} = L_{z}/M_{z}`.
+The bins are defined by the three indices
+:math:`i_{x}`,
+:math:`i_{y}`, and
+:math:`i_{z}` that run from zero to
+:math:`M_{x}-1`,
+:math:`M_{y}-1`, and
+:math:`M_{z}-1`, respectively.
+
+
+We consider the :math:`n`th bead located at position :math:`\vec{r}^{(n)}`.
+We define a weight function :math:`w_{I}(\vec{r}^{(n)})` within the :math:`I`th bin.
+The :math:`I`th index is defined to be a superindex that combines
+:math:`i_{x}`,
+:math:`i_{y}`, and
+:math:`i_{z}` into a single unique index :math:`I= i_{x} + M_{x} i_{y} + M_{x}M_{z} i_{z}` that
+runs from zero to :math:`M_{x}M_{y}M_{z}-1` (total of :math:`M_{x}M_{y}M_{z}` unique indices)
+The total weight on the :math:`I`th bin is given by the contributions from the three cartesian
+directions, \emph{i.e.}
+:math:`w_{I}(\vec{r}^{(n)}) =
+w_{i_{x}}^{(x)}(x^{(n)})
+w_{i_{y}}^{(y)}(y^{(n)})
+w_{i_{z}}^{(z)}(z^{(n)})`.
+Figure~\ref{fig:weight} shows a schematic of the :math:`x`-direction weight function (same method for :math:`y` and :math:`z`).
+This shows a linear interpolation weighting method, consistent with Refs. [Pike2009a]_, [Pike2009b]_.
+
+.. figure:: figures/weight.pdf
+ :width: 600
+ :align: center
+ :alt: Schematic of the weight function :math:`w_{i_{x}}^{(x)}` that gives the weighting of the particle in the :math:`i_{x}` site in the
+ :math:`x`-direction based on a linear interpolation method
+
+ Schematic of the weight function :math:`w_{i_{x}}^{(x)}` that gives the weighting of the particle in the :math:`i_{x}` site in the
+ :math:`x`-direction based on a linear interpolation method
+
+
+The number of epigenetic proteins (\emph{e.g.} HP1) to the :math:`n`th site is given by :math:`N_{I}^{(\alpha)}`, where :math:`\alpha` determines
+the type of epigenetic mark.
+The :math:`\alpha`-protein density within the :math:`I`th bin is given by
+
+.. math::
+ \rho_{I}^{(\alpha)} = \frac{1}{v_{\mathrm{bin}}} \sum_{n=0}^{n_{b} - 1} w_{I}(\vec{r}^{(n)}) N_{I}^{(\alpha)}
+
+where :math:`v_{\mathrm{bin}} = \Delta_{x} \Delta_{y} \Delta_{z}` is the volume of a bin.
+The maximum number of epigenetic proteins bound :math:`N_{\mathrm{max}}^{(\alpha)}` gives an upper bound on the
+number of proteins that can bind to a bead, accounting for coarse graining of a bead to represent multiple nucleosomes.
+For discretization of one nucleosome per bead, the maximum :math:`N_{\mathrm{max}}^{(\alpha)} = 2` implies binding
+of a protein to the two histone tail proteins for the :math:`\alpha` epigenetic mark.
+We define the number of :math:`\alpha` marks on the :math:`I`th bead as :math:`M_{I}^{(\alpha)}`, which can take values from zero
+to :math:`N_{\mathrm{max}}^{(\alpha)}`.
+
+Protein binding to a marked tail results in energy :math:`-\beta \epsilon_{m}` [non-dimensionalized by :math:`\beta = 1/(k_{B}T)`], and protein binding to an unmarked tail is associated with
+energy :math:`-\beta \epsilon_{u}`. The chemical potential of the :math:`\alpha` protein is defined as :math:`\beta \mu^{(\alpha)}`.
+The binding of :math:`N_{I}^{(\alpha)}` proteins to a bead with :math:`M_{I}^{(\alpha)}` marks results in a free energy that
+accounts for all of the combinatoric ways of binding.
diff --git a/doc/source/figures/weight.ai b/doc/source/figures/weight.ai
new file mode 100644
index 0000000..840f5d3
--- /dev/null
+++ b/doc/source/figures/weight.ai
@@ -0,0 +1,1119 @@
+%PDF-1.5
%
+1 0 obj
<>/OCGs[23 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<>stream
+
+
+
+
+ application/pdf
+
+
+ weight
+
+
+ Adobe Illustrator 24.1 (Macintosh)
+ 2020-08-02T19:05:18-07:00
+ 2020-08-02T19:05:18-07:00
+ 2020-08-02T19:05:18-07:00
+
+
+
+ 256
+ 52
+ JPEG
+ /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgANAEAAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYqlv+ItLXV20m
aQwXuxhWZSizAgH9yx+F6VoQN8VTLFXYq7FXYq7FWJeY/wA0fKfl9i169xLaRSCO/v7S3kubazqD
RrmSINwWooTvQ9aYqyXT9QsNRsob7T7iO7srhQ8FzA6yRup6FWUkEYqiMVY/5284W3lPTbbVL2Ll
pz3cNrfXBfgLeOeqLLTi3L97wSlR9qtdqFV5Za/mx5i8/aro+l+WFfQdXNrNcXCyyGWOC8jmMM0V
wvpUkSCKM1VgOTTR04kc1Vep+SIdfh0iZdbMhuTd3DQ+tIJZPSaQkVcKnwluRQcVohUcVpxCq3zd
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
l8fWn2cVf//Z
+
+
+
+ uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7
+ xmp.did:fe646f74-ee7e-4b6e-92b8-1740a48566ef
+ uuid:b5a937fd-0226-ef46-b8bd-32e27ebcdfcb
+ proof:pdf
+
+ uuid:eab21be5-e766-664f-af13-7ab4a523d93d
+ xmp.did:cc4dc041-9a60-4315-bc5d-7e2d77be13d2
+ uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7
+ proof:pdf
+
+
+
+
+ saved
+ xmp.iid:fe646f74-ee7e-4b6e-92b8-1740a48566ef
+ 2020-08-02T16:40:33-07:00
+ Adobe Illustrator 24.1 (Macintosh)
+ /
+
+
+
+ Basic RGB
+ Document
+ AIRobin
+ 1
+ False
+ False
+
+ 6.000000
+ 3.000000
+ Inches
+
+
+
+
+ ChalkboardSE-Regular
+ Chalkboard SE
+ Regular
+ TrueType
+ 13.0d1e2
+ False
+ ChalkboardSE.ttc
+
+
+ MyriadPro-Regular
+ Myriad Pro
+ Regular
+ Open Type
+ Version 2.106;PS 2.000;hotconv 1.0.70;makeotf.lib2.5.58329
+ False
+ MyriadPro-Regular.otf
+
+
+
+
+
+ Cyan
+ Magenta
+ Yellow
+ Black
+
+
+
+
+
+ Default Swatch Group
+ 0
+
+
+
+ White
+ RGB
+ PROCESS
+ 255
+ 255
+ 255
+
+
+ Black
+ RGB
+ PROCESS
+ 0
+ 0
+ 0
+
+
+ RGB Red
+ RGB
+ PROCESS
+ 255
+ 0
+ 0
+
+
+ RGB Yellow
+ RGB
+ PROCESS
+ 255
+ 255
+ 0
+
+
+ RGB Green
+ RGB
+ PROCESS
+ 0
+ 255
+ 0
+
+
+ RGB Cyan
+ RGB
+ PROCESS
+ 0
+ 255
+ 255
+
+
+ RGB Blue
+ RGB
+ PROCESS
+ 0
+ 0
+ 255
+
+
+ RGB Magenta
+ RGB
+ PROCESS
+ 255
+ 0
+ 255
+
+
+ R=193 G=39 B=45
+ RGB
+ PROCESS
+ 193
+ 39
+ 45
+
+
+ R=237 G=28 B=36
+ RGB
+ PROCESS
+ 237
+ 28
+ 36
+
+
+ R=241 G=90 B=36
+ RGB
+ PROCESS
+ 241
+ 90
+ 36
+
+
+ R=247 G=147 B=30
+ RGB
+ PROCESS
+ 247
+ 147
+ 30
+
+
+ R=251 G=176 B=59
+ RGB
+ PROCESS
+ 251
+ 176
+ 59
+
+
+ R=252 G=238 B=33
+ RGB
+ PROCESS
+ 252
+ 238
+ 33
+
+
+ R=217 G=224 B=33
+ RGB
+ PROCESS
+ 217
+ 224
+ 33
+
+
+ R=140 G=198 B=63
+ RGB
+ PROCESS
+ 140
+ 198
+ 63
+
+
+ R=57 G=181 B=74
+ RGB
+ PROCESS
+ 57
+ 181
+ 74
+
+
+ R=0 G=146 B=69
+ RGB
+ PROCESS
+ 0
+ 146
+ 69
+
+
+ R=0 G=104 B=55
+ RGB
+ PROCESS
+ 0
+ 104
+ 55
+
+
+ R=34 G=181 B=115
+ RGB
+ PROCESS
+ 34
+ 181
+ 115
+
+
+ R=0 G=169 B=157
+ RGB
+ PROCESS
+ 0
+ 169
+ 157
+
+
+ R=41 G=171 B=226
+ RGB
+ PROCESS
+ 41
+ 171
+ 226
+
+
+ R=0 G=113 B=188
+ RGB
+ PROCESS
+ 0
+ 113
+ 188
+
+
+ R=46 G=49 B=146
+ RGB
+ PROCESS
+ 46
+ 49
+ 146
+
+
+ R=27 G=20 B=100
+ RGB
+ PROCESS
+ 27
+ 20
+ 100
+
+
+ R=102 G=45 B=145
+ RGB
+ PROCESS
+ 102
+ 45
+ 145
+
+
+ R=147 G=39 B=143
+ RGB
+ PROCESS
+ 147
+ 39
+ 143
+
+
+ R=158 G=0 B=93
+ RGB
+ PROCESS
+ 158
+ 0
+ 93
+
+
+ R=212 G=20 B=90
+ RGB
+ PROCESS
+ 212
+ 20
+ 90
+
+
+ R=237 G=30 B=121
+ RGB
+ PROCESS
+ 237
+ 30
+ 121
+
+
+ R=199 G=178 B=153
+ RGB
+ PROCESS
+ 199
+ 178
+ 153
+
+
+ R=153 G=134 B=117
+ RGB
+ PROCESS
+ 153
+ 134
+ 117
+
+
+ R=115 G=99 B=87
+ RGB
+ PROCESS
+ 115
+ 99
+ 87
+
+
+ R=83 G=71 B=65
+ RGB
+ PROCESS
+ 83
+ 71
+ 65
+
+
+ R=198 G=156 B=109
+ RGB
+ PROCESS
+ 198
+ 156
+ 109
+
+
+ R=166 G=124 B=82
+ RGB
+ PROCESS
+ 166
+ 124
+ 82
+
+
+ R=140 G=98 B=57
+ RGB
+ PROCESS
+ 140
+ 98
+ 57
+
+
+ R=117 G=76 B=36
+ RGB
+ PROCESS
+ 117
+ 76
+ 36
+
+
+ R=96 G=56 B=19
+ RGB
+ PROCESS
+ 96
+ 56
+ 19
+
+
+ R=66 G=33 B=11
+ RGB
+ PROCESS
+ 66
+ 33
+ 11
+
+
+
+
+
+ Cold
+ 1
+
+
+
+ C=56 M=0 Y=20 K=0
+ RGB
+ PROCESS
+ 101
+ 200
+ 208
+
+
+ C=51 M=43 Y=0 K=0
+ RGB
+ PROCESS
+ 131
+ 139
+ 197
+
+
+ C=26 M=41 Y=0 K=0
+ RGB
+ PROCESS
+ 186
+ 155
+ 201
+
+
+
+
+
+ Grays
+ 1
+
+
+
+ R=0 G=0 B=0
+ RGB
+ PROCESS
+ 0
+ 0
+ 0
+
+
+ R=26 G=26 B=26
+ RGB
+ PROCESS
+ 26
+ 26
+ 26
+
+
+ R=51 G=51 B=51
+ RGB
+ PROCESS
+ 51
+ 51
+ 51
+
+
+ R=77 G=77 B=77
+ RGB
+ PROCESS
+ 77
+ 77
+ 77
+
+
+ R=102 G=102 B=102
+ RGB
+ PROCESS
+ 102
+ 102
+ 102
+
+
+ R=128 G=128 B=128
+ RGB
+ PROCESS
+ 128
+ 128
+ 128
+
+
+ R=153 G=153 B=153
+ RGB
+ PROCESS
+ 153
+ 153
+ 153
+
+
+ R=179 G=179 B=179
+ RGB
+ PROCESS
+ 179
+ 179
+ 179
+
+
+ R=204 G=204 B=204
+ RGB
+ PROCESS
+ 204
+ 204
+ 204
+
+
+ R=230 G=230 B=230
+ RGB
+ PROCESS
+ 230
+ 230
+ 230
+
+
+ R=242 G=242 B=242
+ RGB
+ PROCESS
+ 242
+ 242
+ 242
+
+
+
+
+
+
+ Adobe PDF library 15.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
endstream
endobj
3 0 obj
<>
endobj
5 0 obj
<>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 28 0 R/TrimBox[0.0 0.0 432.0 216.0]/Type/Page>>
endobj
25 0 obj
<>stream
+HWKo0WǵYQ`@71:쐭{`-v&`SGESZ.נެZ7
7wG_ۃטgP:FVĭ"/_jBA!.1<#d$x`j}gL6ՎLi
+ہĺp;ĩF0X<(T{gj',zVZ]
+Ca{?Xjq?.#:y:ϭyC "Iu";hmo"lI`c*"7f)z.>QRRuG3urmp#tڀsD;#hBSR{SI*ʾ)dh2ik Faᓕ8!6Jn?7ʃL:KRkUL\!w(b}|,$0&Z0HF5b#I18Kzj
J%jMffl*Ch
vW{>u~?^t%W9tx,inڲ6=K,[l~4kMK7!|=_ g1v⸠х
+F pkQF^2o[8o@