rotational_path.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import numpy as np
  2. from dataclasses import dataclass, field
  3. import quaternionic
  4. from charged_shells import expansion, interactions, mapping
  5. from charged_shells.parameters import ModelParams
  6. import matplotlib.pyplot as plt
  7. from pathlib import Path
  8. from functools import partial
  9. Quaternion = quaternionic.array
  10. Array = np.ndarray
  11. Expansion = expansion.Expansion
  12. @dataclass
  13. class PairRotationalPath:
  14. rotations1: list[Quaternion] = field(default_factory=list)
  15. rotations2: list[Quaternion] = field(default_factory=list)
  16. x_axis: list[Array] = field(default_factory=list)
  17. ticks: dict[float, str] = field(default_factory=dict)
  18. overlapping_last: bool = True
  19. _default_x_axis: Array = None
  20. def add(self, rotation1: Quaternion, rotation2: Quaternion, x_axis: Array = None,
  21. start_name: str | float = None, end_name: str | float = None):
  22. rotation1, rotation2 = np.broadcast_arrays(rotation1, rotation2)
  23. self.rotations1.append(Quaternion(rotation1))
  24. self.rotations2.append(Quaternion(rotation2))
  25. if x_axis is None:
  26. x_axis = np.arange(len(rotation1)) if self._default_x_axis is None else self._default_x_axis
  27. self.add_x_axis(x_axis, start_name, end_name)
  28. def add_x_axis(self, x_axis: Array, start_name: str | float = None, end_name: str | float = None):
  29. try:
  30. last_x_val = self.x_axis[-1][-1]
  31. except IndexError:
  32. last_x_val = 0
  33. if self.overlapping_last:
  34. self.x_axis.append(x_axis + last_x_val)
  35. else:
  36. raise NotImplementedError('Currently only overlapping end points for x-axes are supported.')
  37. # adding ticks to x_axis
  38. s_n = x_axis[0] + last_x_val if start_name is None else start_name # defaults to just numbers
  39. e_n = x_axis[-1] + last_x_val if end_name is None else end_name
  40. start_position = float(self.x_axis[-1][0])
  41. end_position = float(self.x_axis[-1][-1])
  42. if start_position not in self.ticks or start_name is not None: # allows overwriting previous end with new start
  43. self.ticks[start_position] = s_n
  44. self.ticks[end_position] = e_n
  45. def set_default_x_axis(self, default_x_axis: Array):
  46. self._default_x_axis = default_x_axis
  47. def add_euler(self, *, alpha1: Array = 0, beta1: Array = 0, gamma1: Array = 0,
  48. alpha2: Array = 0, beta2: Array = 0, gamma2: Array = 0, **add_kwargs):
  49. R1_euler = quaternionic.array.from_euler_angles(alpha1, beta1, gamma1)
  50. R2_euler = quaternionic.array.from_euler_angles(alpha2, beta2, gamma2)
  51. self.add(Quaternion(R1_euler), Quaternion(R2_euler), **add_kwargs)
  52. def stack_rotations(self) -> (Quaternion, Quaternion):
  53. return Quaternion(np.vstack(self.rotations1)), Quaternion(np.vstack(self.rotations2))
  54. def stack_x_axes(self) -> Array:
  55. return np.hstack(self.x_axis)
  56. def plot_style(self, fig, ax,
  57. energy_units: interactions.EnergyUnit = 'kT',
  58. legend: bool = True,
  59. size: tuple | None = (4, 1.7),
  60. legend_loc: str = 'upper left'):
  61. ax.axhline(y=0, c='k', linestyle=':')
  62. if legend:
  63. ax.legend(fontsize=10, frameon=False, loc=legend_loc, bbox_to_anchor=(0.57, 1))
  64. ax.tick_params(which='both', direction='in', top=True, right=True, labelsize=11)
  65. # ax.set_xlabel('angle', fontsize=15)
  66. if energy_units == 'kT':
  67. energy_units = 'k_B T'
  68. ax.set_ylabel(f'$V [{energy_units}]$', fontsize=11)
  69. ax.set_xticks(list(self.ticks.keys()), list(self.ticks.values()), fontsize=11)
  70. if size is not None:
  71. fig.set_size_inches(size)
  72. for line in ax.get_lines():
  73. line.set_linewidth(1.2)
  74. # plt.tight_layout()
  75. plt.subplots_adjust(left=0.12, right=0.97, top=0.95, bottom=0.15)
  76. @dataclass
  77. class PathEnergyPlot:
  78. """Path comparison for a pair of charge distributions, possibly at different parameter values."""
  79. expansion1: Expansion
  80. expansion2: Expansion
  81. rot_path: PairRotationalPath
  82. dist: float | Array
  83. params: ModelParams
  84. match_expansion_axis_to_params: int | None = None
  85. units: interactions.EnergyUnit = 'kT'
  86. def __post_init__(self):
  87. if not isinstance(self.dist, Array):
  88. self.dist = np.array([self.dist])
  89. # we add 1 to match_expansion_axis as rotations take the new leading axis
  90. if self.match_expansion_axis_to_params is not None:
  91. self.match_expansion_axis_to_params += 1
  92. def plot_style(self, fig, ax, energy_units: interactions.EnergyUnit = 'kT', legend: bool = True,
  93. size: tuple = (8.25, 4.125)):
  94. self.rot_path.plot_style(fig, ax, energy_units, legend=legend, size=size)
  95. def evaluate_energy(self):
  96. energy = []
  97. for dist in self.dist:
  98. energy_fn = mapping.parameter_map_two_expansions(partial(interactions.charged_shell_energy,
  99. dist=dist, units=self.units),
  100. self.match_expansion_axis_to_params)
  101. energy.append(energy_fn(self.expansion1, self.expansion2, self.params))
  102. return np.squeeze(np.stack(energy, axis=-1))
  103. def path_energy(self):
  104. rotations1, rotations2 = self.rot_path.stack_rotations()
  105. self.expansion1.rotate(rotations1)
  106. self.expansion2.rotate(rotations2)
  107. return self.evaluate_energy()
  108. def section_energies(self):
  109. energy_list = []
  110. for rot1, rot2 in zip(self.rot_path.rotations1, self.rot_path.rotations2):
  111. self.expansion1.rotate(rot1)
  112. self.expansion2.rotate(rot2)
  113. energy_list.append(self.evaluate_energy())
  114. return energy_list
  115. def normalization(self, norm_euler_angles: dict):
  116. if norm_euler_angles is None:
  117. return np.array([1.])
  118. self.expansion1.rotate_euler(alpha=norm_euler_angles.get('alpha1', 0),
  119. beta=norm_euler_angles.get('beta1', 0),
  120. gamma=norm_euler_angles.get('gamma1', 0))
  121. self.expansion2.rotate_euler(alpha=norm_euler_angles.get('alpha2', 0),
  122. beta=norm_euler_angles.get('beta2', 0),
  123. gamma=norm_euler_angles.get('gamma2', 0))
  124. return np.abs(self.evaluate_energy())
  125. def evaluate_path(self, norm_euler_angles: dict = None):
  126. energy = self.path_energy()
  127. normalization = self.normalization(norm_euler_angles)
  128. energy = energy / normalization[None, ...]
  129. energy = energy.reshape(energy.shape[0], -1)
  130. return np.squeeze(energy)
  131. def plot(self, labels: list[str] = None, norm_euler_angles: dict = None, save_as: Path = None):
  132. energy = self.evaluate_path(norm_euler_angles=norm_euler_angles)
  133. x_axis = self.rot_path.stack_x_axes()
  134. print(plt.figaspect(0.5) * 8.25)
  135. fig, ax = plt.subplots(figsize=plt.figaspect(0.5) * 8.25)
  136. ax.plot(x_axis, np.squeeze(energy), label=labels)
  137. self.plot_style(fig, ax, energy_units=self.units)
  138. if save_as is not None:
  139. plt.savefig(save_as, dpi=600)
  140. plt.show()
  141. def plot_sections(self, norm_euler_angles: dict = None, save_as: Path = None):
  142. energy_list = self.section_energies()
  143. normalization = self.normalization(norm_euler_angles)
  144. fig, ax = plt.subplots()
  145. for x_axis, energy in zip(self.rot_path.x_axis, energy_list):
  146. energy, norm = np.broadcast_arrays(energy, normalization)
  147. ax.plot(x_axis, energy / norm)
  148. self.plot_style(fig, ax, energy_units=self.units, legend=False)
  149. if save_as is not None:
  150. plt.savefig(save_as, dpi=600)
  151. plt.show()
  152. @dataclass
  153. class PathExpansionComparison:
  154. """Path comparison for different charge distribution models."""
  155. ex_list: list[Expansion]
  156. rot_path: PairRotationalPath
  157. dist: float
  158. params: ModelParams
  159. path_list: list[PathEnergyPlot] = field(default_factory=list)
  160. def __post_init__(self):
  161. for ex in self.ex_list:
  162. self.path_list.append(PathEnergyPlot(ex, ex.clone(), self.rot_path, self.dist, self.params))
  163. def evaluate_path(self, norm_euler_angles: dict = None):
  164. path_vals = []
  165. for path in self.path_list:
  166. path_vals.append(path.evaluate_path(norm_euler_angles))
  167. return np.squeeze(np.stack(path_vals))
  168. def plot(self, labels: list[str] = None, norm_euler_angles: dict = None, save_as: Path = None):
  169. energy = self.evaluate_path(norm_euler_angles=norm_euler_angles)
  170. x_axis = self.rot_path.stack_x_axes()
  171. fig, ax = plt.subplots()
  172. ax.axhline(y=0, c='k', linestyle=':')
  173. ax.plot(x_axis, energy, label=labels)
  174. ax.legend(fontsize=12)
  175. ax.tick_params(which='both', direction='in', top=True, right=True, labelsize=12)
  176. ax.set_xlabel('angle', fontsize=13)
  177. ax.set_ylabel('U', fontsize=13)
  178. plt.tight_layout()
  179. if save_as is not None:
  180. plt.savefig(save_as, dpi=600)
  181. plt.show()