Delete equiformer_ase_calculator.py
Browse files- equiformer_ase_calculator.py +0 -414
equiformer_ase_calculator.py
DELETED
|
@@ -1,414 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
ASE Calculator wrapper for Equiformer model.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from typing import Optional
|
| 6 |
-
import numpy as np
|
| 7 |
-
import yaml
|
| 8 |
-
import os
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
from torch_geometric.data import Data as TGData
|
| 12 |
-
from torch_geometric.data import Batch
|
| 13 |
-
from ase import Atoms
|
| 14 |
-
from ase.calculators.calculator import Calculator, all_changes
|
| 15 |
-
from ase.data import atomic_numbers
|
| 16 |
-
from ase.calculators.singlepoint import SinglePointCalculator as sp
|
| 17 |
-
from ase.constraints import FixAtoms
|
| 18 |
-
from torch_scatter import scatter_mean
|
| 19 |
-
|
| 20 |
-
from ocpmodels.datasets import data_list_collater
|
| 21 |
-
from ocpmodels.preprocessing import AtomsToGraphs
|
| 22 |
-
from ocpmodels.common.relaxation.ase_utils import (
|
| 23 |
-
batch_to_atoms,
|
| 24 |
-
ase_atoms_to_torch_geometric,
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
from nets.equiformer_v2.equiformer_v2_oc20 import EquiformerV2_OC20
|
| 28 |
-
from nets.prediction_utils import compute_extra_props
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class EquiformerASECalculator(Calculator):
|
| 32 |
-
"""
|
| 33 |
-
Equiformer ASE Calculator.
|
| 34 |
-
|
| 35 |
-
Might need to reimplement EquiformerASECalculator based on:
|
| 36 |
-
ocpmodels/common/relaxation/ase_utils.py
|
| 37 |
-
|
| 38 |
-
Args:
|
| 39 |
-
checkpoint_path: Path to the Equiformer model checkpoint
|
| 40 |
-
device: Optional device specification (defaults to auto-detect)
|
| 41 |
-
"""
|
| 42 |
-
|
| 43 |
-
def __init__(
|
| 44 |
-
self,
|
| 45 |
-
checkpoint_path: Optional[str] = None,
|
| 46 |
-
device: Optional[torch.device] = None,
|
| 47 |
-
project_root: str = None,
|
| 48 |
-
**kwargs,
|
| 49 |
-
):
|
| 50 |
-
"""
|
| 51 |
-
Initialize the Equiformer calculator.
|
| 52 |
-
|
| 53 |
-
Args:
|
| 54 |
-
checkpoint_path: Path to the trained Equiformer checkpoint file
|
| 55 |
-
device: Optional device specification (defaults to auto-detect)
|
| 56 |
-
**kwargs: Additional keyword arguments for parent Calculator class
|
| 57 |
-
"""
|
| 58 |
-
Calculator.__init__(self, **kwargs)
|
| 59 |
-
self.results = {}
|
| 60 |
-
|
| 61 |
-
# Set device
|
| 62 |
-
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 63 |
-
|
| 64 |
-
# Load model
|
| 65 |
-
if project_root is None:
|
| 66 |
-
project_root = os.path.dirname(os.path.dirname(__file__))
|
| 67 |
-
config_path = os.path.join(project_root, "configs/equiformer_v2.yaml")
|
| 68 |
-
with open(config_path, "r") as file:
|
| 69 |
-
config = yaml.safe_load(file)
|
| 70 |
-
model_config = config["model"]
|
| 71 |
-
self.model = EquiformerV2_OC20(**model_config)
|
| 72 |
-
|
| 73 |
-
if checkpoint_path is None:
|
| 74 |
-
checkpoint_path = os.path.join(project_root, "ckpt/eqv2.ckpt")
|
| 75 |
-
state_dict = torch.load(checkpoint_path, weights_only=True)["state_dict"]
|
| 76 |
-
state_dict = {k.replace("potential.", ""): v for k, v in state_dict.items()}
|
| 77 |
-
self.model.load_state_dict(state_dict, strict=False)
|
| 78 |
-
self.model = self.model.to(self.device)
|
| 79 |
-
self.model.eval()
|
| 80 |
-
|
| 81 |
-
# Set implemented properties
|
| 82 |
-
self.implemented_properties = ["energy", "forces", "hessian"]
|
| 83 |
-
|
| 84 |
-
# ocpmodels/common/relaxation/ase_utils.py
|
| 85 |
-
self.a2g = AtomsToGraphs(
|
| 86 |
-
max_neigh=self.model.max_neighbors,
|
| 87 |
-
radius=self.model.cutoff,
|
| 88 |
-
r_energy=False,
|
| 89 |
-
r_forces=False,
|
| 90 |
-
r_distances=False,
|
| 91 |
-
r_edges=False,
|
| 92 |
-
r_pbc=True,
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
def forward(self, atoms, hessian=False):
|
| 96 |
-
"""
|
| 97 |
-
Forward pass for the Equiformer calculator.
|
| 98 |
-
If hessian is True, it will compute the Hessian via autograd and eigenvalues/eigenvectors.
|
| 99 |
-
Otherwise, it will only compute the energy and forces.
|
| 100 |
-
"""
|
| 101 |
-
properties = ["energy", "forces"]
|
| 102 |
-
if hessian:
|
| 103 |
-
properties += ["hessian", "eigen"]
|
| 104 |
-
self.calculate(atoms, properties=properties)
|
| 105 |
-
return self.results
|
| 106 |
-
|
| 107 |
-
def calculate(self, atoms=None, properties=None, system_changes=all_changes):
|
| 108 |
-
"""
|
| 109 |
-
Calculate properties for the given atoms.
|
| 110 |
-
|
| 111 |
-
You can get the
|
| 112 |
-
|
| 113 |
-
Args:
|
| 114 |
-
atoms: ASE Atoms object
|
| 115 |
-
properties: List of properties to compute (used by ASE internally)
|
| 116 |
-
system_changes: System changes since last calculation (used by ASE internally)
|
| 117 |
-
"""
|
| 118 |
-
# Call base class to set atoms attribute
|
| 119 |
-
Calculator.calculate(self, atoms)
|
| 120 |
-
|
| 121 |
-
# ocpmodels/common/relaxation/ase_utils.py
|
| 122 |
-
# data_object = self.a2g.convert(atoms)
|
| 123 |
-
# batch = data_list_collater([data_object], otf_graph=True)
|
| 124 |
-
|
| 125 |
-
# Convert ASE atoms to torch_geometric format
|
| 126 |
-
batch = ase_atoms_to_torch_geometric(atoms)
|
| 127 |
-
batch = batch.to(self.device)
|
| 128 |
-
|
| 129 |
-
if properties is None:
|
| 130 |
-
properties = []
|
| 131 |
-
if "eigen" in properties:
|
| 132 |
-
properties.append("hessian")
|
| 133 |
-
|
| 134 |
-
# Prepare batch with extra properties
|
| 135 |
-
batch = compute_extra_props(batch, pos_require_grad="hessian" in properties)
|
| 136 |
-
|
| 137 |
-
# Run prediction
|
| 138 |
-
with torch.enable_grad():
|
| 139 |
-
energy, forces, eigenoutputs = self.model.forward(batch, eigen=True)
|
| 140 |
-
|
| 141 |
-
# Store results
|
| 142 |
-
self.results = {}
|
| 143 |
-
|
| 144 |
-
# Energy is per molecule, extract scalar value
|
| 145 |
-
self.results["energy"] = float(energy.detach().cpu().item())
|
| 146 |
-
|
| 147 |
-
# Forces shape: [n_atoms, 3]
|
| 148 |
-
self.results["forces"] = forces.detach().cpu().numpy()
|
| 149 |
-
|
| 150 |
-
# predicted eigenvalues and eigenvectors of the Hessian
|
| 151 |
-
for key in ["eigval_1", "eigval_2", "eigvec_1", "eigvec_2"]:
|
| 152 |
-
if key in eigenoutputs:
|
| 153 |
-
self.results[key] = eigenoutputs[key].detach().cpu().numpy()
|
| 154 |
-
|
| 155 |
-
# Compute the Hessian via autodiff on the fly
|
| 156 |
-
if "hessian" in properties:
|
| 157 |
-
# 3D coordinates -> 3N^2 Hessian elements
|
| 158 |
-
N = batch.pos.shape[0]
|
| 159 |
-
forces = forces.reshape(-1)
|
| 160 |
-
num_elements = forces.shape[0]
|
| 161 |
-
|
| 162 |
-
def get_vjp(v):
|
| 163 |
-
return torch.autograd.grad(
|
| 164 |
-
outputs=-1 * forces,
|
| 165 |
-
inputs=batch.pos,
|
| 166 |
-
grad_outputs=v,
|
| 167 |
-
retain_graph=True,
|
| 168 |
-
create_graph=False,
|
| 169 |
-
allow_unused=False,
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
I_N = torch.eye(num_elements, device=forces.device)
|
| 173 |
-
hessian = torch.vmap(get_vjp, in_dims=0, out_dims=0, chunk_size=None)(I_N)[
|
| 174 |
-
0
|
| 175 |
-
]
|
| 176 |
-
hessian = hessian.view(N * 3, N * 3)
|
| 177 |
-
self.results["hessian"] = hessian.detach().cpu().numpy()
|
| 178 |
-
|
| 179 |
-
if "eigen" in properties:
|
| 180 |
-
eigenvalues, eigenvectors = torch.linalg.eigh(hessian)
|
| 181 |
-
smallest_eigenvals = eigenvalues[:2]
|
| 182 |
-
smallest_eigenvecs = eigenvectors[:, :2]
|
| 183 |
-
self.results["eigenvalues"] = smallest_eigenvals.detach().cpu().numpy()
|
| 184 |
-
self.results["eigenvectors"] = (
|
| 185 |
-
smallest_eigenvecs.T.view(2, N, 3).detach().cpu().numpy()
|
| 186 |
-
)
|
| 187 |
-
|
| 188 |
-
def get_hessian_autodiff(self, atoms):
|
| 189 |
-
"""
|
| 190 |
-
Get the Hessian matrix for the given atoms via autodiff (on the fly Hessian).
|
| 191 |
-
"""
|
| 192 |
-
self.calculate(atoms, properties=["energy", "forces", "hessian"])
|
| 193 |
-
return self.results["hessian"]
|
| 194 |
-
|
| 195 |
-
def get_eigen_autodiff(self, atoms):
|
| 196 |
-
"""
|
| 197 |
-
Get the eigenvalues and eigenvectors for the given atoms via autodiff (on the fly eigenvalues).
|
| 198 |
-
"""
|
| 199 |
-
self.calculate(atoms, properties=["energy", "forces", "hessian", "eigen"])
|
| 200 |
-
return self.results["eigenvalues"], self.results["eigenvectors"]
|
| 201 |
-
|
| 202 |
-
# Andreas: really not sure if this is correct
|
| 203 |
-
def get_vibrational_analysis(
|
| 204 |
-
self, atoms, filter_threshold=1.0, preserve_imaginary=True
|
| 205 |
-
):
|
| 206 |
-
"""
|
| 207 |
-
Compute vibrational modes using mass-weighted Hessian.
|
| 208 |
-
|
| 209 |
-
Args:
|
| 210 |
-
atoms: ASE Atoms object
|
| 211 |
-
filter_threshold: Threshold for filtering small frequencies (cm^-1).
|
| 212 |
-
Set to 0.0 to keep all modes.
|
| 213 |
-
preserve_imaginary: If True, preserves imaginary frequencies regardless of threshold.
|
| 214 |
-
Important for transition state analysis.
|
| 215 |
-
|
| 216 |
-
Returns:
|
| 217 |
-
dict: Dictionary containing vibrational analysis results:
|
| 218 |
-
- frequencies: vibrational frequencies in cm^-1 (negative = imaginary)
|
| 219 |
-
- normal_modes: normal mode eigenvectors (mass-weighted)
|
| 220 |
-
- reduced_masses: reduced masses for each mode
|
| 221 |
-
- force_constants: force constants for each mode
|
| 222 |
-
"""
|
| 223 |
-
# Get the Hessian matrix
|
| 224 |
-
self.calculate(atoms, properties=["energy", "forces", "hessian"])
|
| 225 |
-
hessian = self.results["hessian"]
|
| 226 |
-
|
| 227 |
-
# Get atomic masses in atomic units
|
| 228 |
-
masses = atoms.get_masses() # in amu
|
| 229 |
-
masses_au = masses * 1822.888486 # Convert amu to atomic units
|
| 230 |
-
|
| 231 |
-
# Create mass matrix (diagonal matrix with masses repeated 3 times for x,y,z)
|
| 232 |
-
N = len(atoms)
|
| 233 |
-
mass_matrix = np.zeros((3 * N, 3 * N))
|
| 234 |
-
for i in range(N):
|
| 235 |
-
mass_matrix[3 * i : 3 * i + 3, 3 * i : 3 * i + 3] = masses_au[i] * np.eye(3)
|
| 236 |
-
|
| 237 |
-
# Create mass-weighted Hessian: H_mw = M^(-1/2) * H * M^(-1/2)
|
| 238 |
-
mass_matrix_sqrt = np.sqrt(mass_matrix)
|
| 239 |
-
mass_matrix_inv_sqrt = np.linalg.inv(mass_matrix_sqrt)
|
| 240 |
-
|
| 241 |
-
# Mass-weighted Hessian
|
| 242 |
-
hessian_mw = mass_matrix_inv_sqrt @ hessian @ mass_matrix_inv_sqrt
|
| 243 |
-
|
| 244 |
-
# Solve eigenvalue problem for mass-weighted Hessian
|
| 245 |
-
eigenvalues, eigenvectors = np.linalg.eigh(hessian_mw)
|
| 246 |
-
|
| 247 |
-
# Convert eigenvalues to frequencies
|
| 248 |
-
# ω = sqrt(λ) where λ are eigenvalues of mass-weighted Hessian
|
| 249 |
-
# Convert from atomic units to cm^-1
|
| 250 |
-
# 1 Hartree = 219474.631 cm^-1
|
| 251 |
-
# 1 atomic unit of frequency = sqrt(Hartree/amu) = 5140.487 cm^-1
|
| 252 |
-
frequencies_cm = np.sqrt(np.abs(eigenvalues)) * 5140.487
|
| 253 |
-
|
| 254 |
-
# Handle negative eigenvalues (imaginary frequencies)
|
| 255 |
-
frequencies_cm = np.where(eigenvalues < 0, -frequencies_cm, frequencies_cm)
|
| 256 |
-
|
| 257 |
-
# Convert eigenvectors back to Cartesian coordinates
|
| 258 |
-
# Normal modes in Cartesian coordinates: Q = M^(-1/2) * q
|
| 259 |
-
normal_modes_cart = mass_matrix_inv_sqrt @ eigenvectors
|
| 260 |
-
|
| 261 |
-
# Calculate reduced masses for each mode
|
| 262 |
-
# μ = 1 / (q^T * q) where q are mass-weighted normal modes
|
| 263 |
-
reduced_masses = 1.0 / np.sum(eigenvectors**2, axis=0)
|
| 264 |
-
|
| 265 |
-
# Calculate force constants
|
| 266 |
-
# k = μ * ω^2
|
| 267 |
-
force_constants = reduced_masses * (frequencies_cm / 5140.487) ** 2
|
| 268 |
-
|
| 269 |
-
# Filter out translational and rotational modes
|
| 270 |
-
# For transition state analysis, preserve imaginary frequencies (negative eigenvalues)
|
| 271 |
-
if filter_threshold > 0.0:
|
| 272 |
-
if preserve_imaginary:
|
| 273 |
-
# Keep imaginary frequencies (negative) and real frequencies above threshold
|
| 274 |
-
vibrational_mask = (frequencies_cm < 0) | (
|
| 275 |
-
frequencies_cm > filter_threshold
|
| 276 |
-
)
|
| 277 |
-
else:
|
| 278 |
-
# Traditional filtering by absolute value
|
| 279 |
-
vibrational_mask = np.abs(frequencies_cm) > filter_threshold
|
| 280 |
-
else:
|
| 281 |
-
# Keep all modes
|
| 282 |
-
vibrational_mask = np.ones(len(frequencies_cm), dtype=bool)
|
| 283 |
-
|
| 284 |
-
print(
|
| 285 |
-
f"Masked {np.sum(~vibrational_mask)} modes (filter_threshold={filter_threshold}, preserve_imaginary={preserve_imaginary})"
|
| 286 |
-
)
|
| 287 |
-
|
| 288 |
-
return {
|
| 289 |
-
"frequencies": frequencies_cm[vibrational_mask],
|
| 290 |
-
"normal_modes": normal_modes_cart[:, vibrational_mask],
|
| 291 |
-
"reduced_masses": reduced_masses[vibrational_mask],
|
| 292 |
-
"force_constants": force_constants[vibrational_mask],
|
| 293 |
-
"all_frequencies": frequencies_cm,
|
| 294 |
-
"all_normal_modes": normal_modes_cart,
|
| 295 |
-
"all_reduced_masses": reduced_masses,
|
| 296 |
-
"all_force_constants": force_constants,
|
| 297 |
-
"vibrational_mask": vibrational_mask,
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
def analyze_stationary_point(self, atoms):
|
| 301 |
-
"""
|
| 302 |
-
Analyze whether the structure is a minimum, transition state, or higher-order saddle point.
|
| 303 |
-
|
| 304 |
-
Args:
|
| 305 |
-
atoms: ASE Atoms object
|
| 306 |
-
|
| 307 |
-
Returns:
|
| 308 |
-
dict: Analysis results containing:
|
| 309 |
-
- point_type: str ("minimum", "transition_state", "higher_order_saddle")
|
| 310 |
-
- n_imaginary: int (number of imaginary frequencies)
|
| 311 |
-
- frequencies: array of all frequencies
|
| 312 |
-
- imaginary_frequencies: array of imaginary frequencies only
|
| 313 |
-
"""
|
| 314 |
-
vib_results = self.get_vibrational_analysis(
|
| 315 |
-
atoms, filter_threshold=1.0, preserve_imaginary=True
|
| 316 |
-
)
|
| 317 |
-
frequencies = vib_results["frequencies"]
|
| 318 |
-
|
| 319 |
-
imaginary_freqs = frequencies[frequencies < 0]
|
| 320 |
-
n_imaginary = len(imaginary_freqs)
|
| 321 |
-
|
| 322 |
-
if n_imaginary == 0:
|
| 323 |
-
point_type = "minimum"
|
| 324 |
-
elif n_imaginary == 1:
|
| 325 |
-
point_type = "transition_state"
|
| 326 |
-
else:
|
| 327 |
-
point_type = "higher_order_saddle"
|
| 328 |
-
|
| 329 |
-
return {
|
| 330 |
-
"point_type": point_type,
|
| 331 |
-
"n_imaginary": n_imaginary,
|
| 332 |
-
"frequencies": frequencies,
|
| 333 |
-
"imaginary_frequencies": imaginary_freqs,
|
| 334 |
-
"all_results": vib_results,
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
if __name__ == "__main__":
|
| 339 |
-
import os
|
| 340 |
-
from ase.vibrations import Vibrations
|
| 341 |
-
|
| 342 |
-
# Create a simple water molecule for testing
|
| 343 |
-
atoms = Atoms(
|
| 344 |
-
symbols="H2O",
|
| 345 |
-
positions=[
|
| 346 |
-
[0.0, 0.0, 0.0], # O
|
| 347 |
-
[0.0, 0.757, 0.587], # H
|
| 348 |
-
[0.0, -0.757, 0.587], # H
|
| 349 |
-
],
|
| 350 |
-
)
|
| 351 |
-
|
| 352 |
-
# Initialize calculator with default checkpoint path
|
| 353 |
-
project_root = os.path.dirname(os.path.dirname(__file__))
|
| 354 |
-
checkpoint_path = os.path.join(project_root, "ckpt/eqv2.ckpt")
|
| 355 |
-
|
| 356 |
-
if not os.path.exists(checkpoint_path):
|
| 357 |
-
print(f"Checkpoint not found at {checkpoint_path}")
|
| 358 |
-
print("Please provide a valid checkpoint path")
|
| 359 |
-
exit()
|
| 360 |
-
|
| 361 |
-
calculator = EquiformerASECalculator(
|
| 362 |
-
checkpoint_path=checkpoint_path, project_root=project_root
|
| 363 |
-
)
|
| 364 |
-
|
| 365 |
-
# Attach calculator to atoms
|
| 366 |
-
# atoms.set_calculator(calculator)
|
| 367 |
-
atoms.calc = calculator
|
| 368 |
-
|
| 369 |
-
# Calculate energy and forces
|
| 370 |
-
energy = atoms.get_potential_energy()
|
| 371 |
-
forces = atoms.get_forces()
|
| 372 |
-
|
| 373 |
-
print(f"Energy: {energy:.6f} eV")
|
| 374 |
-
print(f"Forces shape: {forces.shape}")
|
| 375 |
-
print(f"Forces:\n{forces}")
|
| 376 |
-
|
| 377 |
-
# To get hessian, we need to explicitly calculate it through the calculator
|
| 378 |
-
calculator.calculate(atoms, properties=["energy", "forces", "hessian"])
|
| 379 |
-
hessian = calculator.results["hessian"]
|
| 380 |
-
print(f"Hessian shape: {hessian.shape}")
|
| 381 |
-
|
| 382 |
-
eigenvalues, eigenvectors = calculator.get_eigen_autodiff(atoms)
|
| 383 |
-
print(f"Eigenvalues: {eigenvalues}")
|
| 384 |
-
print(f"Eigenvectors: {eigenvectors.shape}")
|
| 385 |
-
|
| 386 |
-
# Compute vibrational analysis for transition state analysis
|
| 387 |
-
print("\n=== Vibrational Analysis (Transition State Compatible) ===")
|
| 388 |
-
vib_results = calculator.get_vibrational_analysis(atoms, preserve_imaginary=True)
|
| 389 |
-
|
| 390 |
-
print(f"Number of vibrational modes: {len(vib_results['frequencies'])}")
|
| 391 |
-
print(f"Vibrational frequencies (cm^-1):")
|
| 392 |
-
imaginary_count = 0
|
| 393 |
-
for i, freq in enumerate(vib_results["frequencies"]):
|
| 394 |
-
if freq < 0:
|
| 395 |
-
print(f" Mode {i+1}: {freq:.2f} (i)")
|
| 396 |
-
imaginary_count += 1
|
| 397 |
-
else:
|
| 398 |
-
print(f" Mode {i+1}: {freq:.2f}")
|
| 399 |
-
|
| 400 |
-
print(f"\nNumber of imaginary frequencies: {imaginary_count}")
|
| 401 |
-
if imaginary_count == 1:
|
| 402 |
-
print("This appears to be a transition state (1 imaginary frequency)")
|
| 403 |
-
elif imaginary_count == 0:
|
| 404 |
-
print("This appears to be a minimum (0 imaginary frequencies)")
|
| 405 |
-
else:
|
| 406 |
-
print(f"This has {imaginary_count} imaginary frequencies")
|
| 407 |
-
|
| 408 |
-
# Compare with ASE's Vibrations class
|
| 409 |
-
print("\n" + "=" * 40)
|
| 410 |
-
print("Comparison with ASE's Vibrations class")
|
| 411 |
-
vib = Vibrations(atoms)
|
| 412 |
-
vib.run()
|
| 413 |
-
vib.summary()
|
| 414 |
-
vib.clean()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|