diff --git a/docs/cli/interatomic.md b/docs/cli/calculators.md
similarity index 86%
rename from docs/cli/interatomic.md
rename to docs/cli/calculators.md
index 8a96647a..a27d44fb 100644
--- a/docs/cli/interatomic.md
+++ b/docs/cli/calculators.md
@@ -4,7 +4,7 @@
This command generates an averaged EAM potential for an alloy. The basic theory behind averaged alloy EAM potentials is
described in
-[Varvenne, Luque, Nöhring, Curtin, Phys. Rev. B 93, 104201 (2016)](https://doi.org/10.1103/PhysRevB.93.104201). The
+[Varvenne, Luque, Nöhring, Curtin, Phys. Rev. B 93, 104201 (2016)](https://doi.org/10.1103/PhysRevB.93.104201). The
basic usage of the command is
```bash
diff --git a/docs/cli/fracture_mechanics.md b/docs/cli/fracture_mechanics.md
new file mode 100644
index 00000000..d1075eec
--- /dev/null
+++ b/docs/cli/fracture_mechanics.md
@@ -0,0 +1,67 @@
+# Fracture mechanics
+
+## `matscipy-quasistatic-crack`
+
+This command carries out a quasistatic K-field-controlled crack calculation on a small cluster. The outer boundaries of
+the cluster are fixed according to the near field solution of linear elastic fracture mechanics, considering elastic
+anisotropy, as described in [Sih, Paris, Irwin, Int. J. Fract. Mech. 1, 189 (1965)](https://doi.org/10.1007/BF00186854).
+The crack advances by stepwise updates of $K_\textrm{I}$ and and crack tip position.
+
+The command does not take any arguments but can be configured using a `params.py` file in the current directory.
+An example `params.py` file follows:
+```Python
+import numpy as np
+
+import ase.io
+
+from matscipy.fracture_mechanics.clusters import diamond, set_groups
+
+from atomistica import TersoffScr, Tersoff_PRB_39_5566_Si_C__Scr
+
+###
+# Interaction potential
+calc = TersoffScr(**Tersoff_PRB_39_5566_Si_C__Scr)
+
+# Fundamental material properties
+el = 'C'
+a0 = 3.57
+
+# Elastic constants: either specify explicitly or compute automatically
+compute_elastic_constants = True
+# C11 = 1220. # GPa
+# C12 = -3. # GPa
+# C44 = 535. # GPa
+
+# Surface energy
+surface_energy = 2.7326 * 10 # GPa*A = 0.1 J/m^2
+
+# Crack system
+crack_surface = [1, 1, 1] # Normal of crack face
+crack_front = [1, -1, 0] # Direction of crack front
+# bond = (10, 11)
+bondlength = 1.7 # Bond length for crack tip detection
+bulk_nn = 4 # Number of nearest neighbors in the bulk
+
+vacuum = 6.0 # Vacuum surrounding the cracked cluster
+
+# Simulation control
+nsteps = 31
+# Increase stress intensity factor
+k1 = np.linspace(0.8, 1.2, nsteps)
+# Don't move crack tip
+tip_dx = np.zeros_like(k1)
+tip_dz = np.zeros_like(k1)
+
+fmax = 0.05 # Tolerance for quasistatic optimizer
+
+# The actual crack system
+n = [1, 1, 1]
+skin_x, skin_y = 1, 1
+cryst = diamond(el, a0, n, crack_surface, crack_front)
+set_groups(cryst, n, skin_x, skin_y) # Outer fixed atoms
+ase.io.write('cryst.xyz', cryst, format='extxyz') # Dump initial crack system (without notch)
+```
+
+## `matscipy-sinclair-continuation`
+
+## `matscipy-sinclair-crack`
\ No newline at end of file
diff --git a/docs/cli/index.rst b/docs/cli/index.rst
index 08065d71..7b37e5c4 100644
--- a/docs/cli/index.rst
+++ b/docs/cli/index.rst
@@ -14,5 +14,6 @@ whether a default parameter was used, to the log.
diffusion
electrochemistry
- interatomic
+ fracture_mechanics
+ calculators
structure
\ No newline at end of file
diff --git a/matscipy/cli/fracture_mechanics/__init__.py b/matscipy/cli/fracture_mechanics/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/matscipy/tool/meson.build b/matscipy/cli/fracture_mechanics/meson.build
similarity index 53%
rename from matscipy/tool/meson.build
rename to matscipy/cli/fracture_mechanics/meson.build
index 0efaf425..5a8491e0 100644
--- a/matscipy/tool/meson.build
+++ b/matscipy/cli/fracture_mechanics/meson.build
@@ -3,11 +3,14 @@
# Pure Python sources
python_sources = [
'__init__.py',
- 'plot.py'
+ 'quasistatic_crack.py',
+ 'setup_crack.py',
+ 'sinclair_continuation.py',
+ 'sinclair_crack.py'
]
# Install pure Python
python.install_sources(
python_sources,
- subdir: 'matscipy/tool'
+ subdir: 'matscipy/cli/fracture_mechanics'
)
diff --git a/matscipy/cli/fracture_mechanics/quasistatic_crack.py b/matscipy/cli/fracture_mechanics/quasistatic_crack.py
new file mode 100644
index 00000000..965918b5
--- /dev/null
+++ b/matscipy/cli/fracture_mechanics/quasistatic_crack.py
@@ -0,0 +1,177 @@
+#
+# Copyright 2014-2015, 2021 Lars Pastewka (U. Freiburg)
+# 2015 James Kermode (Warwick U.)
+#
+# matscipy - Materials science with Python at the atomic-scale
+# https://github.com/libAtoms/matscipy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+# USAGE:
+#
+# Code imports the file 'params.py' from current working directory. params.py
+# contains simulation parameters. Some parameters can be omitted, see below.
+#
+# Parameters
+# ----------
+# calc : ase.Calculator
+# Calculator object for energy and force computation.
+# tip_x0 : float
+# Initial x-position of crack tip.
+# tip_y0 : float
+# Initial y-position of crack tip.
+# tip_dx : array-like
+# Displacement of tip in x-direction during run. x- and y-positions will be
+# optimized self-consistently if omitted.
+# tip_dy : array-like
+# Displacement of tip in y-direction during run. Position will be optimized
+# self-consistently if omitted.
+
+import sys
+
+import numpy as np
+
+import ase.io
+import ase.constraints
+import ase.optimize
+
+from matscipy import parameter
+from matscipy.logger import screen
+
+from setup_crack import setup_crack
+from ase.optimize.precon import PreconLBFGS
+
+###
+
+def main():
+ # Atom types used for outputting the crack tip position.
+ ACTUAL_CRACK_TIP = 'Au'
+ FITTED_CRACK_TIP = 'Ag'
+
+ ###
+
+ logger = screen
+
+ ###
+
+ a, cryst, crk, k1g, tip_x0, tip_y0, bond1, bond2, boundary_mask, \
+ boundary_mask_bulk, tip_mask = setup_crack(logger=logger)
+ ase.io.write('notch.xyz', a, format='extxyz')
+
+ # Global parameters
+ basename = parameter('basename', 'quasistatic_crack')
+ calc = parameter('calc')
+ fmax = parameter('fmax', 0.01)
+
+ # Determine simulation control
+ k1_list = parameter('k1')
+ old_k1 = k1_list[0]
+ nsteps = len(k1_list)
+ tip_dx_list = parameter('tip_dx', np.zeros(nsteps))
+ tip_dy_list = parameter('tip_dy', np.zeros(nsteps))
+
+ # Run crack calculation.
+ tip_x = tip_x0
+ tip_y = tip_y0
+ a.set_calculator(calc)
+ for i, (k1, tip_dx, tip_dy) in enumerate(zip(k1_list, tip_dx_list,
+ tip_dy_list)):
+ logger.pr('=== k1 = {0}*k1g, tip_dx = {1}, tip_dy = {2} ===' \
+ .format(k1, tip_dx, tip_dy))
+ if tip_dx is None or tip_dy is None:
+ #
+ # Optimize crack tip position
+ #
+ old_y = tip_y + 1.0
+ old_x = tip_x + 1.0
+ while abs(tip_x - old_x) > 1e-6 and abs(tip_y - old_y) > 1e-6:
+ b = cryst.copy()
+ ux, uy = crk.displacements(cryst.positions[:, 0], cryst.positions[:, 1],
+ tip_x, tip_y, k * k1g)
+ b.positions[:, 0] += ux
+ b.positions[:, 1] += uy
+ a.set_constraint(None)
+ a.positions[boundary_mask] = b.positions[boundary_mask]
+ a.set_constraint(ase.constraints.FixAtoms(mask=boundary_mask))
+ logger.pr('Optimizing positions...')
+ opt = ase.optimize.FIRE(a, logfile=None)
+ opt.run(fmax=fmax)
+ logger.pr('...done. Converged within {0} steps.' \
+ .format(opt.get_number_of_steps()))
+
+ old_x = tip_x
+ old_y = tip_y
+ tip_x, tip_y = crk.crack_tip_position(a.positions[:, 0],
+ a.positions[:, 1],
+ cryst.positions[:, 0],
+ cryst.positions[:, 1],
+ tip_x, tip_y, k * k1g,
+ mask=mask)
+ else:
+ #
+ # Do not optimize tip position.
+ #
+ tip_x = tip_x0 + tip_dx
+ tip_y = tip_y0 + tip_dy
+ logger.pr('Setting crack tip position to {0} {1}' \
+ .format(tip_x, tip_y))
+ # Scale strain field and optimize crack
+ a.set_constraint(None)
+ x, y = crk.scale_displacements(a.positions[:len(cryst), 0],
+ a.positions[:len(cryst), 1],
+ cryst.positions[:, 0],
+ cryst.positions[:, 1],
+ old_k1, k1)
+ a.positions[:len(cryst), 0] = x
+ a.positions[:len(cryst), 1] = y
+ # Optimize atoms in center
+ a.set_constraint(ase.constraints.FixAtoms(mask=boundary_mask))
+ logger.pr('Optimizing positions...')
+ opt = PreconLBFGS(a)
+ opt.run(fmax=fmax)
+ logger.pr('...done. Converged within {0} steps.' \
+ .format(opt.get_number_of_steps()))
+ old_k1 = k1
+
+ # Output optimized configuration ot file. Include the mask array in
+ # output, so we know which atoms were used in fitting.
+ a.set_array('atoms_used_for_fitting_crack_tip', tip_mask)
+
+ # Output a file that contains the target crack tip (used for
+ # the displacmenets of the boundary atoms) and the fitted crack tip
+ # positions. The target crack tip is marked by a Hydrogen atom.
+ b = a.copy()
+ b += ase.Atom(ACTUAL_CRACK_TIP, (tip_x, tip_y, b.cell[2, 2] / 2))
+
+ # Measure the true (fitted) crack tip position.
+ try:
+ measured_tip_x, measured_tip_y = \
+ crk.crack_tip_position(a.positions[:, 0], a.positions[:, 1],
+ cryst.positions[:, 0], cryst.positions[:, 1],
+ tip_x, tip_y, k1 * k1g, mask=tip_mask)
+ measured_tip_x %= a.cell[0][0]
+ measured_tip_y %= a.cell[0][0]
+ except:
+ measured_tip_x = 0.0
+ measured_tip_y = 0.0
+
+ # The fitted crack tip is marked by a Helium atom.
+ b += ase.Atom(FITTED_CRACK_TIP, (measured_tip_x, measured_tip_y,
+ b.cell[2, 2] / 2))
+
+ b.info['bond_length'] = a.get_distance(bond1, bond2)
+ b.info['energy'] = a.get_potential_energy()
+ b.info['cell_origin'] = [0, 0, 0]
+ ase.io.write('%s_%4.4i.xyz' % (basename, i), b, format='extxyz')
diff --git a/scripts/fracture_mechanics/setup_crack.py b/matscipy/cli/fracture_mechanics/setup_crack.py
similarity index 83%
rename from scripts/fracture_mechanics/setup_crack.py
rename to matscipy/cli/fracture_mechanics/setup_crack.py
index 9525e62b..07661869 100644
--- a/scripts/fracture_mechanics/setup_crack.py
+++ b/matscipy/cli/fracture_mechanics/setup_crack.py
@@ -18,6 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+
import numpy as np
import ase.optimize
@@ -30,6 +31,7 @@
from matscipy.logger import screen
from matscipy.neighbours import neighbour_list
+
###
def setup_crack(logger=screen):
@@ -56,11 +58,11 @@ def setup_crack(logger=screen):
fmax=elastic_fmax)
log_file.close()
logger.pr('Measured elastic constants (in GPa):')
- logger.pr(np.round(C*10/GPa)/10)
+ logger.pr(np.round(C * 10 / GPa) / 10)
crk = crack.CubicCrystalCrack(parameter('crack_surface'),
parameter('crack_front'),
- Crot=C/GPa)
+ Crot=C / GPa)
else:
if has_parameter('C'):
crk = crack.CubicCrystalCrack(parameter('crack_surface'),
@@ -72,17 +74,16 @@ def setup_crack(logger=screen):
parameter('C11'), parameter('C12'),
parameter('C44'))
-
logger.pr('Elastic constants used for boundary condition (in GPa):')
- logger.pr(np.round(crk.C*10)/10)
+ logger.pr(np.round(crk.C * 10) / 10)
# Get Griffith's k1.
k1g = crk.k1g(parameter('surface_energy'))
logger.pr('Griffith k1 = %f' % k1g)
# Apply initial strain field.
- tip_x = parameter('tip_x', cryst.cell.diagonal()[0]/2)
- tip_y = parameter('tip_y', cryst.cell.diagonal()[1]/2)
+ tip_x = parameter('tip_x', cryst.cell.diagonal()[0] / 2)
+ tip_y = parameter('tip_y', cryst.cell.diagonal()[1] / 2)
bondlength = parameter('bondlength', 2.7)
bulk_nn = parameter('bulk_nn', 4)
@@ -97,19 +98,19 @@ def setup_crack(logger=screen):
# Get surface atoms of cluster with crack
a = hydrogenate(cryst, bondlength, parameter('XH_bondlength'), b=a)
g = a.get_array('groups')
- g[a.numbers==1] = -1
+ g[a.numbers == 1] = -1
a.set_array('groups', g)
cryst = a.copy()
k1 = parameter('k1')
try:
- k1 = k1[0]
+ k1 = k1[0]
except:
- pass
- ux, uy = crk.displacements(cryst.positions[:,0], cryst.positions[:,1],
- tip_x, tip_y, k1*k1g)
- a.positions[:len(cryst),0] += ux
- a.positions[:len(cryst),1] += uy
+ pass
+ ux, uy = crk.displacements(cryst.positions[:, 0], cryst.positions[:, 1],
+ tip_x, tip_y, k1 * k1g)
+ a.positions[:len(cryst), 0] += ux
+ a.positions[:len(cryst), 1] += uy
# Center notched configuration in simulation cell and ensure enough vacuum.
oldr = a[0].position.copy()
@@ -124,7 +125,7 @@ def setup_crack(logger=screen):
parameter('bond', crack.find_tip_coordination(a, bondlength=bondlength, bulk_nn=bulk_nn))
if parameter('center_crack_tip_on_bond', False):
- tip_x, tip_y, dummy = (a.positions[bond1]+a.positions[bond2])/2
+ tip_x, tip_y, dummy = (a.positions[bond1] + a.positions[bond2]) / 2
# Hydrogenate?
coord = np.bincount(neighbour_list('i', a, bondlength), minlength=len(a))
@@ -134,19 +135,19 @@ def setup_crack(logger=screen):
g = a.get_array('groups')
gcryst = cryst.get_array('groups')
coord = a.get_array('coord')
- g[coord!=4] = -1
- gcryst[coord!=4] = -1
+ g[coord != 4] = -1
+ gcryst[coord != 4] = -1
a.set_array('groups', g)
cryst.set_array('groups', gcryst)
if hydrogenate_flag and hydrogenate_crack_face_flag:
# Get surface atoms of cluster with crack
- exclude = np.logical_and(a.get_array('groups')==1, coord!=4)
+ exclude = np.logical_and(a.get_array('groups') == 1, coord != 4)
a.set_array('exclude', exclude)
a = hydrogenate(cryst, bondlength, parameter('XH_bondlength'), b=a,
exclude=exclude)
g = a.get_array('groups')
- g[a.numbers==1] = -1
+ g[a.numbers == 1] = -1
a.set_array('groups', g)
basename = parameter('basename', 'energy_barrier')
ase.io.write('{0}_hydrogenated.xyz'.format(basename), a,
@@ -162,15 +163,15 @@ def setup_crack(logger=screen):
gcryst = cryst.get_array('groups')
logger.pr('Opening bond {0}--{1}, initial bond length {2}'.
- format(bond1, bond2, a.get_distance(bond1, bond2, mic=True)))
+ format(bond1, bond2, a.get_distance(bond1, bond2, mic=True)))
# centre vertically on the opening bond
if parameter('center_cell_on_bond', True):
- a.translate([0., a.cell[1,1]/2.0 -
- (a.positions[bond1, 1] +
- a.positions[bond2, 1])/2.0, 0.])
+ a.translate([0., a.cell[1, 1] / 2.0 -
+ (a.positions[bond1, 1] +
+ a.positions[bond2, 1]) / 2.0, 0.])
a.info['bond1'] = bond1
a.info['bond2'] = bond2
- return a, cryst, crk, k1g, tip_x, tip_y, bond1, bond2, g==0, gcryst==0, g==1
+ return a, cryst, crk, k1g, tip_x, tip_y, bond1, bond2, g == 0, gcryst == 0, g == 1
diff --git a/matscipy/cli/fracture_mechanics/sinclair_continuation.py b/matscipy/cli/fracture_mechanics/sinclair_continuation.py
new file mode 100644
index 00000000..fbb61e40
--- /dev/null
+++ b/matscipy/cli/fracture_mechanics/sinclair_continuation.py
@@ -0,0 +1,246 @@
+#
+# Copyright 2020-2021 James Kermode (Warwick U.)
+#
+# matscipy - Materials science with Python at the atomic-scale
+# https://github.com/libAtoms/matscipy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+import matscipy
+
+import os
+import numpy as np
+
+import h5py
+import ase.io
+from ase.units import GPa
+from ase.constraints import FixAtoms
+# from ase.optimize import LBFGSLineSearch
+from ase.optimize.precon import PreconLBFGS
+
+from matscipy import parameter
+from matscipy.elasticity import fit_elastic_constants
+from matscipy.fracture_mechanics.crack import CubicCrystalCrack, SinclairCrack
+
+from scipy.optimize import fsolve, fminbound
+
+import sys
+
+def main():
+ print(matscipy.__file__)
+
+ sys.path.insert(0, '../../../scripts/fracture_mechanics')
+
+ import params
+
+ calc = parameter('calc')
+ fmax = parameter('fmax', 1e-3)
+ max_opt_steps = parameter('max_opt_steps', 100)
+ max_arc_steps = parameter('max_arc_steps', 10)
+ vacuum = parameter('vacuum', 10.0)
+ flexible = parameter('flexible', True)
+ continuation = parameter('continuation', False)
+ ds = parameter('ds', 1e-2)
+ nsteps = parameter('nsteps', 10)
+ a0 = parameter('a0') # lattice constant
+ k0 = parameter('k0', 1.0)
+ extended_far_field = parameter('extended_far_field', False)
+ alpha0 = parameter('alpha0', 0.0) # initial guess for crack position
+ dump = parameter('dump', False)
+ precon = parameter('precon', False)
+ prerelax = parameter('prerelax', False)
+ traj_file = parameter('traj_file', 'x_traj.h5')
+ restart_file = parameter('restart_file', traj_file)
+ traj_interval = parameter('traj_interval', 1)
+ direction = parameter('direction', +1)
+ ds_max = parameter('ds_max', 0.1)
+ ds_min = parameter('ds_min', 1e-6)
+ ds_aggressiveness = parameter('ds_aggressiveness', 1.25)
+ opt_method = parameter('opt_method', 'krylov')
+
+ # make copies of initial configs
+ cryst = params.cryst.copy()
+ cluster = params.cluster.copy()
+
+ # compute elastic constants
+ cryst.pbc = True
+ cryst.calc = calc
+ C, C_err = fit_elastic_constants(cryst, symmetry=parameter('elastic_symmetry', 'triclinic'))
+
+ # setup the crack
+ crk = CubicCrystalCrack(parameter('crack_surface'),
+ parameter('crack_front'),
+ Crot=C / GPa)
+
+ # get Griffith's stess intensity factor
+ k1g = crk.k1g(parameter('surface_energy'))
+ print('Griffith k1 = %f' % k1g)
+
+ # check for restart files if continuation=True
+ if continuation and not os.path.exists(restart_file):
+ continuation = False
+ if continuation:
+
+ # works only if restart file contains atleast two consecutive flex solutions
+ with h5py.File(restart_file, 'r') as hf:
+ x = hf['x']
+ restart_index = parameter('restart_index', x.shape[0] - 1)
+ x0 = x[restart_index - 1, :]
+ x1 = x[restart_index, :]
+ k0 = x0[-1] / k1g;
+ k1 = x1[-1] / k1g
+ alpha0 = x0[-2] / k1g;
+ alpha1 = x1[-2] / k1g
+ # print restart info
+ print(f'Restarting from k0={k0}, k1={k1} --> alpha0={alpha0}, alpha1={alpha1}')
+
+ else:
+
+ # setup Sinclair boundary conditions with variable_alpha and no variable_k, for approx solution
+ sc = SinclairCrack(crk, cluster, calc,
+ k0 * k1g, alpha=alpha0,
+ vacuum=vacuum,
+ variable_alpha=flexible,
+ extended_far_field=extended_far_field)
+
+ traj = None
+ dk = parameter('dk', 1e-4)
+ dalpha = parameter('dalpha', 1e-3)
+
+ # reuse output from sinclair_crack.py if possible
+ if os.path.exists(f'k_{int(k0 * 1000):04d}.xyz'):
+ print(f'Reading atoms from k_{int(k0 * 1000):04d}.xyz')
+ a = ase.io.read(f'k_{int(k0 * 1000):04d}.xyz')
+ sc.set_atoms(a)
+
+ # setup mask fpr relevant regions
+ mask = sc.regionI | sc.regionII
+ if extended_far_field:
+ mask = sc.regionI | sc.regionII | sc.regionIII
+
+ # estimate the stable K range
+ k_range = parameter('k_range', 'Unknown')
+ if isinstance(k_range, list) and len(k_range) == 2:
+ kmin, kmax = k_range
+ print(f'Using K range from params file: {kmin} < k / k_G < {kmax}')
+ else:
+ print('No k_range=[kmin,kmax] given in params.')
+ print('Running CLE-only approximation to estimate stable K range.')
+
+
+ # first use the CLE-only approximation`: define a function f_alpha0(k, alpha)
+ def f(k, alpha):
+ sc.k = k * k1g
+ sc.alpha = alpha
+ sc.update_atoms()
+ return sc.get_crack_tip_force(mask=mask)
+
+
+ # identify approximate range of stable k
+ alpha_range = parameter('alpha_range', np.linspace(-a0, a0, 20))
+ k = k0 # initial guess for k
+ alpha_k = []
+ for alpha in alpha_range:
+ # look for solution to f(k, alpha) = 0 close to alpha = alpha
+ (k,) = fsolve(f, k, args=(alpha,))
+ print(f'alpha={alpha:.3f} k={k:.3f} ')
+ alpha_k.append((alpha, k))
+ alpha_k = np.array(alpha_k)
+ kmin, kmax = alpha_k[:, 1].min(), alpha_k[:, 1].max()
+ print(f'Estimated stable K range is {kmin} < k / k_G < {kmax}')
+
+ # define a function to relax with static scheme at a given value of k
+ traj = open("traj.xyz", "w")
+
+
+ def g(k, do_abs=True):
+ print(f'Static minimisation with k={k}, alpha={alpha0}.')
+ sc.k = k * k1g
+ sc.alpha = alpha0
+ sc.variable_alpha = False
+ sc.variable_k = False
+ sc.update_atoms()
+ atoms = sc.atoms.copy()
+ atoms.calc = sc.calc
+ atoms.set_constraint(FixAtoms(mask=~sc.regionI))
+ opt = PreconLBFGS(atoms, logfile=None)
+ # opt = LBFGSLineSearch(atoms)
+ opt.run(fmax=1e-3)
+ atoms.write(traj, format="extxyz")
+ sc.set_atoms(atoms)
+ f_alpha = sc.get_crack_tip_force(mask=mask)
+ print(f'Static minimisation with k={k}, alpha={alpha0} --> f_alpha={f_alpha}')
+ if do_abs:
+ f_alpha = abs(f_alpha)
+ return f_alpha
+
+
+ # minimise g(k) in [kmin, kmax]
+ kopt, falpha_min, ierr, funccalls = fminbound(g, kmin, kmax, xtol=1e-8, full_output=True)
+ print(f'Brent minimisation yields f_alpha={falpha_min} for k = {kopt} after {funccalls} calls')
+ traj.close()
+
+ # Find flex1
+ # first optimize with static scheme
+ # sc.k = kopt * k1g
+ k0 = kopt
+ sc.rescale_k(k0 * k1g)
+ sc.alpha = alpha0
+ sc.variable_alpha = False
+ sc.optimize(ftol=1e-3, steps=max_opt_steps, method=opt_method)
+ # then revert to target fmax precision and optimize with flexible scheme
+ sc.variable_alpha = flexible # True
+ sc.optimize(ftol=fmax, steps=max_opt_steps, method=opt_method)
+ # save flex1
+ sc.atoms.write('x0.xyz')
+ x0 = np.r_[sc.get_dofs(), k0 * k1g]
+ alpha0 = sc.alpha
+
+ # Find flex2: a second solution x1 = (u_1, alpha_1, k_1) where
+ # k_1 ~= k_0 and alpha_1 ~= alpha_0
+ # Increase k, and rescale Us accordingly
+ print(f'Rescaling K_I from {sc.k} to {sc.k + dk * k1g}')
+ k1 = k0 + dk
+ sc.rescale_k(k1 * k1g)
+ # optimize at target fmax precision with flexible scheme
+ sc.variable_alpha = flexible # True
+ sc.optimize(ftol=fmax, steps=max_opt_steps, method=opt_method)
+ # save flex2
+ sc.atoms.write('x1.xyz')
+ x1 = np.r_[sc.get_dofs(), k1 * k1g]
+ alpha1 = sc.alpha
+
+ # check crack tip didn't jump too far
+ print(f'k0={k0}, k1={k1} --> alpha0={alpha0}, alpha1={alpha1}')
+ assert abs(alpha1 - alpha0) < dalpha
+
+ # setup new crack with variable_k for full solution
+ scv = SinclairCrack(crk, cluster, calc,
+ k0 * k1g, alpha=alpha0,
+ vacuum=vacuum,
+ variable_alpha=flexible, variable_k=True,
+ extended_far_field=extended_far_field)
+
+ # run full arc length continuation
+ scv.arc_length_continuation(x0, x1, N=nsteps,
+ ds=ds, ftol=fmax, max_steps=max_arc_steps,
+ direction=direction,
+ continuation=continuation,
+ traj_file=traj_file,
+ traj_interval=traj_interval,
+ precon=precon,
+ ds_max=ds_max, ds_min=ds_min,
+ ds_aggressiveness=ds_aggressiveness,
+ opt_method='krylov') # 'ode12r' preconditioning needs debugging
diff --git a/matscipy/cli/fracture_mechanics/sinclair_crack.py b/matscipy/cli/fracture_mechanics/sinclair_crack.py
new file mode 100644
index 00000000..c89155b2
--- /dev/null
+++ b/matscipy/cli/fracture_mechanics/sinclair_crack.py
@@ -0,0 +1,149 @@
+#
+# Copyright 2014, 2020 James Kermode (Warwick U.)
+# 2020 Arnaud Allera (U. Lyon 1)
+# 2014 Lars Pastewka (U. Freiburg)
+#
+# matscipy - Materials science with Python at the atomic-scale
+# https://github.com/libAtoms/matscipy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+import os
+import numpy as np
+
+import ase.io
+from ase.units import GPa
+from ase.constraints import FixAtoms
+from ase.optimize.precon import PreconLBFGS
+
+from matscipy import parameter
+from matscipy.elasticity import fit_elastic_constants
+from matscipy.fracture_mechanics.crack import CubicCrystalCrack, SinclairCrack
+
+import sys
+
+def main():
+ sys.path.insert(0, '../../../scripts/fracture_mechanics')
+ import params
+
+ calc = parameter('calc')
+ fmax = parameter('fmax', 1e-3)
+ vacuum = parameter('vacuum', 10.0)
+ flexible = parameter('flexible', True)
+ extended_far_field = parameter('extended_far_field', False)
+ k0 = parameter('k0', 1.0)
+ alpha0 = parameter('alpha0', 0.0) # initial guess for crack position
+ dump = parameter('dump', False)
+ precon = parameter('precon', False)
+ prerelax = parameter('prerelax', False)
+ lbfgs = parameter('lbfgs', not flexible) # use LBGS by default if not flexible
+
+ # compute elastic constants
+ cryst = params.cryst.copy()
+ cryst.pbc = True
+ cryst.calc = calc
+ C, C_err = fit_elastic_constants(cryst,
+ symmetry=parameter('elastic_symmetry',
+ 'triclinic'))
+
+ crk = CubicCrystalCrack(parameter('crack_surface'),
+ parameter('crack_front'),
+ Crot=C / GPa)
+
+ # Get Griffith's k1.
+ k1g = crk.k1g(parameter('surface_energy'))
+ print('Griffith k1 = %f' % k1g)
+
+ cluster = params.cluster.copy()
+ sc = SinclairCrack(crk, cluster, calc, k0 * k1g,
+ alpha=alpha0,
+ variable_alpha=flexible,
+ vacuum=vacuum,
+ extended_far_field=extended_far_field)
+
+ nsteps = parameter('nsteps')
+ k1_range = parameter('k1_range', np.linspace(0.8, 1.2, nsteps))
+
+ ks = list(k1_range)
+ ks_out = []
+ alphas = []
+
+ max_steps = parameter('max_steps', 10)
+ fit_alpha = parameter('fit_alpha', False)
+
+ for i, k in enumerate(ks):
+ restart_file = parameter('restart_file', f'k_{int(k * 1000):04d}.xyz')
+ if os.path.exists(restart_file):
+ print(f'Restarting from {restart_file}')
+ if restart_file.endswith('.xyz'):
+ a = ase.io.read(restart_file)
+ sc.set_atoms(a)
+ elif restart_file.endswith('.txt'):
+ x = np.loadtxt(restart_file)
+ if len(x) == len(sc):
+ sc.set_dofs(x)
+ elif len(x) == len(sc) + 1:
+ sc.variable_k = True
+ sc.set_dofs(x)
+ sc.variable_k = False
+ elif len(x) == len(sc) + 2:
+ sc.variable_alpha = True
+ sc.variable_k = True
+ sc.set_dofs(x)
+ sc.variable_alpha = False
+ sc.variable_k = False
+ else:
+ raise RuntimeError('cannot guess how to restart with'
+ f' {len(x)} variables for {len(self)} DoFs')
+ else:
+ raise ValueError(f"don't know how to restart from {restart_file}")
+ else:
+ sc.rescale_k(k * k1g)
+
+ if fit_alpha:
+ sc.alpha, = sc.fit_cle(variable_alpha=True, variable_k=False)
+ print(f'Fitted value of alpha: {sc.alpha}')
+ print(f'k = {sc.k / k1g} * k1g')
+ print(f'alpha = {sc.alpha}')
+
+ if lbfgs:
+ print('Optimizing with LBFGS')
+ atoms = sc.atoms.copy()
+ atoms.calc = sc.calc
+ atoms.set_constraint(FixAtoms(mask=np.logical_not(sc.regionI)))
+ opt = PreconLBFGS(atoms)
+ opt.run(fmax=fmax)
+ sc.set_atoms(atoms)
+ else:
+ if prerelax:
+ print('Pre-relaxing with Conjugate-Gradients')
+ sc.optimize(ftol=1e-5, steps=max_steps, dump=dump,
+ method='cg')
+
+ sc.optimize(fmax, steps=max_steps, dump=dump,
+ precon=precon, method='krylov')
+
+ if flexible:
+ print(f'Optimized alpha = {sc.alpha:.3f}')
+ ks_out.append(k)
+ alphas.append(sc.alpha)
+
+ a = sc.atoms
+ a.get_forces()
+ ase.io.write(f'k_{int(k * 1000):04d}.xyz', a)
+ with open('x.txt', 'a') as fh:
+ np.savetxt(fh, [sc.get_dofs()])
+
+ np.savetxt('k_vs_alpha.txt', np.c_[ks_out, alphas])
diff --git a/matscipy/tool/__init__.py b/matscipy/tool/__init__.py
deleted file mode 100644
index 62485b6d..00000000
--- a/matscipy/tool/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# Copyright 2014-2015, 2021 Lars Pastewka (U. Freiburg)
-# 2015-2016 Adrien Gola (KIT)
-# 2014 James Kermode (Warwick U.)
-#
-# matscipy - Materials science with Python at the atomic-scale
-# https://github.com/libAtoms/matscipy
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-# Generic stuff may go here.
-
diff --git a/matscipy/tool/plot.py b/matscipy/tool/plot.py
deleted file mode 100644
index b5181e6c..00000000
--- a/matscipy/tool/plot.py
+++ /dev/null
@@ -1,201 +0,0 @@
-#
-# Copyright 2016, 2021 Lars Pastewka (U. Freiburg)
-# 2016 Adrien Gola (KIT)
-#
-# matscipy - Materials science with Python at the atomic-scale
-# https://github.com/libAtoms/matscipy
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-
-"""
- Usage : python plot.py [filename]
-
- This tool script allow to quicly plot data from text files arranged in column with space as separator. It uses a GUI through Tkinter
- "log.lammp" can be read and splited according to different "run" present in the log file
-
- Adrien Gola // 4.11.2015
-"""
-
-import numpy as np
-import matplotlib
-matplotlib.rcParams['backend'] = "Qt4Agg"
-import matplotlib.pyplot as p
-
-import os,sys
-import os.path
-
-import Tkinter as tk
-import ttk
-
-# ------------------------------------
-# ------- Function definition --------
-# ------------------------------------
-def close_windows():
- root.destroy()
-
-def plot_button():
- global xlabel,ylabel
- x = d[:,Xvar.get()]
- y = d[:,Yvar.get()]
-
- if xlabel.get() != "X-axis label":
- hx = xlabel.get()
- else:
- hx = h[Xvar.get()]
- if ylabel.get() != "Y-axis label":
- hy = ylabel.get()
- else:
- hy = h[Yvar.get()]
-
- p.plot(x, y, 'o')
- p.xlabel(hx)
- p.ylabel(hy)
- p.show()
-
-def process_file():
- global variables_frame,h,d,Xvar,Yvar,skipline,flag_loglammps
- selected_file = sorted(outlogfiles)[Fvar.get()]
- if flag_loglammps:
- h = open(selected_file,'r').readlines()[0].strip('#').strip().split(" ") # headers list
- d = np.loadtxt(selected_file, skiprows=1) # data
- else:
- h = filter(None,open(selected_file,'r').readlines()[0+int(skipline.get())].strip('#').strip().split(" ")) # headers list
- d = np.loadtxt(selected_file, skiprows=1+int(skipline.get())) # data
- try:
- for child in variables_frame.winfo_children():
- child.destroy()
- except:
- pass
- #Xvar
- tk.Label(variables_frame,text="Select X-axis variable to plot",anchor='n').grid(row=next_row+1)
- Xvar = tk.IntVar()
- for i,x in enumerate(h):
- x_row=i+2
- tk.Radiobutton(variables_frame,text=x,variable=Xvar, value=i).grid(row=next_row+x_row)
- #Yvar
- tk.Label(variables_frame,text="Select Y-axis variable to plot",anchor='n').grid(row=next_row+1,column=1)
- Yvar = tk.IntVar()
- for j,x in enumerate(h):
- y_row=j+2
- tk.Radiobutton(variables_frame,text=x,variable=Yvar, value=j).grid(row=next_row+y_row,column=1)
-
-
-mypath = os.getcwd()
-# -------------------------------------------------------
-# ------- Reading and spliting the main log file --------
-# -------------------------------------------------------
-flag_loglammps=0
-# --- file selection ---
-try :
- in_file = sys.argv[1]
-except:
- print("Usage : plot_general.py file_name")
- quit()
-
-if in_file == "log.lammps":
- flag_loglammps=1
- data = open("log.lammps",'r').readlines() # select log.lammps as main log file if it exists
-
- # --- spliting into "out.log.*" files ---
- j = 0
- flag = 0
- flag_out = 0
- step = ""
- for lines in data:
- lines = lines.strip()
- if lines.startswith("run:"):
- step = "."+lines.strip().strip("run: ")
- elif lines.startswith("Memory usage per processor"):
- if len(str(j)) == 1:
- J = "0"+str(j)
- else:
- J = str(j)
- out = open("out.log.%s%s-plt-tmp"%(J,step),'w')
- flag_out = 1
- flag = 1
- elif flag and lines.startswith("Loop") or lines.startswith("kill"):
- flag = 0
- j+=1
- out.close()
- step = ""
- elif flag:
- out.write(lines+'\n')
- if flag_out:
- out.close()
- outlogfiles = [ f for f in os.listdir(mypath) if os.path.isfile(os.path.join(mypath,f)) and f.startswith("out.log") and f.endswith("plt-tmp")] # list of "out.log.*" files
-else:
- outlogfiles = [in_file]
- pass
-
-
-
-
-
-###################
-### MAIN script ###
-###################
-
-root=tk.Tk()
-# Positioning frames
-button_frame = tk.Frame(root,heigh=5)
-button_frame.pack(side='right')
-
-source_frame = tk.Frame(root)
-source_frame.pack()
-
-labels_frame = tk.Frame(root)
-labels_frame.pack(side="bottom")
-
-variables_frame = tk.Frame(root)
-variables_frame.pack(side="bottom")
-
-# Filling frames
-tk.Label(source_frame,text="Select data source",anchor='n').grid(row=0)
-Fvar = tk.IntVar()
-for i,x in enumerate(outlogfiles):
- next_row=i+1
- tk.Radiobutton(source_frame,text=x,variable=Fvar, value=i, command = process_file).grid(row=next_row)
- if not flag_loglammps:
- skipline = tk.Entry(source_frame,width=5,justify="center")
- skipline.grid(row=next_row,column=1)
- skipline.insert(0,0)
- else:
- skipline=0
-next_row+=1
-ttk.Separator(source_frame,orient='horizontal').grid(row=next_row,columnspan=2,sticky="ew")
-
-# Insert X,Y label text field
-ttk.Separator(labels_frame,orient='horizontal').grid(columnspan=2,sticky="ew")
-xlabel = tk.Entry(labels_frame,width=50,justify="center")
-xlabel.grid(row=next_row+1,column=0,columnspan=2)
-xlabel.insert(0,"X-axis label")
-ylabel = tk.Entry(labels_frame,width=50,justify="center")
-ylabel.grid(row=next_row+2,column=0,columnspan=2)
-ylabel.insert(0,"Y-axis label")
-
-close = tk.Button(button_frame, text = "Close",width=10, command = close_windows)
-close.grid(row=1)
-plot = tk.Button(button_frame, text = "Plot",width=10, command = plot_button)
-plot.grid(row=0)
-
-# show GUI
-root.mainloop()
-
-
-# Cleaning tmp files
-if flag_loglammps:
- for f in outlogfiles:
- os.remove(f)
diff --git a/pyproject.toml b/pyproject.toml
index ab976ec9..88b69b46 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,7 +70,7 @@ cli = [
# Generic
matscipy-rms = "matscipy.cli.diffusion.rms:main"
-# Glasses
+# Structure generation
matscipy-quench = "matscipy.cli.glasses.quench:main"
# Electrochemistry
@@ -78,6 +78,11 @@ matscipy-continuous2discrete = "matscipy.cli.electrochemistry.continuous2discret
matscipy-poisson-nernst-planck = "matscipy.cli.electrochemistry.poisson_nernst_planck_solver:main"
matscipy-stericify = "matscipy.cli.electrochemistry.stericify:main"
+# Fracture mechanics
+matscipy-quasistatic-crack = "matscipy.cli.fracture_mechanics.quasistatic_crack:main"
+matscipy-sinclair-continuation = "matscipy.cli.fracture_mechanics.sinclair_continuation:main"
+matscipy-sinclair-crack = "matscipy.cli.fracture_mechanics.sinclair_crack:main"
+
# Interatomic potentials
matscipy-average-eam-potential = "matscipy.cli.calculators.average_eam_potential:main"
diff --git a/scripts/fracture_mechanics/energy_barrier.py b/scripts/fracture_mechanics/energy_barrier.py
index 992ece83..f18abfbb 100644
--- a/scripts/fracture_mechanics/energy_barrier.py
+++ b/scripts/fracture_mechanics/energy_barrier.py
@@ -46,26 +46,21 @@
# Code imports the file 'params.py' from current working directory. params.py
# contains simulation parameters. Some parameters can be omitted, see below.
-from math import sqrt
-
import os
-import sys
import numpy as np
-import ase
from ase.constraints import FixConstraint
import ase.io
import ase.optimize
from ase.data import atomic_numbers
-from ase.units import GPa
from ase.geometry import find_mic
import matscipy.fracture_mechanics.crack as crack
from matscipy import parameter
from matscipy.logger import screen
-from setup_crack import setup_crack
+from matscipy.cli.fracture_mechanics.setup_crack import setup_crack
###
diff --git a/scripts/fracture_mechanics/neb_crack_tip.py b/scripts/fracture_mechanics/neb_crack_tip.py
index 69b8048f..5ce3a63f 100644
--- a/scripts/fracture_mechanics/neb_crack_tip.py
+++ b/scripts/fracture_mechanics/neb_crack_tip.py
@@ -46,11 +46,9 @@
# contains simulation parameters. Some parameters can be omitted, see below.
import os
-import sys
import numpy as np
-import ase
import ase.constraints
import ase.io
#import ase.optimize.precon
@@ -58,14 +56,13 @@
from ase.neb import NEB
from ase.data import atomic_numbers
-from ase.units import GPa
import matscipy.fracture_mechanics.crack as crack
from matscipy import parameter
from matscipy.logger import screen
from matscipy.io import savetbl
-from setup_crack import setup_crack
+from matscipy.cli.fracture_mechanics.setup_crack import setup_crack
from scipy.integrate import cumtrapz
diff --git a/scripts/fracture_mechanics/optimize_crack_tip.py b/scripts/fracture_mechanics/optimize_crack_tip.py
index d4ca8069..93198d62 100644
--- a/scripts/fracture_mechanics/optimize_crack_tip.py
+++ b/scripts/fracture_mechanics/optimize_crack_tip.py
@@ -45,23 +45,18 @@
# Code imports the file 'params.py' from current working directory. params.py
# contains simulation parameters. Some parameters can be omitted, see below.
-import os
-import sys
-
import numpy as np
-import ase
import ase.constraints
import ase.io
import ase.optimize
from ase.data import atomic_numbers
-from ase.units import GPa
import matscipy.fracture_mechanics.crack as crack
from matscipy import parameter
from matscipy.logger import screen
-from setup_crack import setup_crack
+from matscipy.cli.fracture_mechanics.setup_crack import setup_crack
###
diff --git a/scripts/fracture_mechanics/quasistatic_crack.py b/scripts/fracture_mechanics/quasistatic_crack.py
deleted file mode 100644
index 8730f1dd..00000000
--- a/scripts/fracture_mechanics/quasistatic_crack.py
+++ /dev/null
@@ -1,206 +0,0 @@
-#
-# Copyright 2014-2015, 2021 Lars Pastewka (U. Freiburg)
-# 2015 James Kermode (Warwick U.)
-#
-# matscipy - Materials science with Python at the atomic-scale
-# https://github.com/libAtoms/matscipy
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-# ======================================================================
-# matscipy - Python materials science tools
-# https://github.com/libAtoms/matscipy
-#
-# Copyright (2014) James Kermode, King's College London
-# Lars Pastewka, Karlsruhe Institute of Technology
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-# ======================================================================
-
-# USAGE:
-#
-# Code imports the file 'params.py' from current working directory. params.py
-# contains simulation parameters. Some parameters can be omitted, see below.
-#
-# Parameters
-# ----------
-# calc : ase.Calculator
-# Calculator object for energy and force computation.
-# tip_x0 : float
-# Initial x-position of crack tip.
-# tip_y0 : float
-# Initial y-position of crack tip.
-# tip_dx : array-like
-# Displacement of tip in x-direction during run. x- and y-positions will be
-# optimized self-consistently if omitted.
-# tip_dy : array-like
-# Displacement of tip in y-direction during run. Position will be optimized
-# self-consistently if omitted.
-
-import os
-import sys
-
-import numpy as np
-
-import ase
-import ase.io
-import ase.constraints
-import ase.optimize
-from ase.units import GPa
-
-import matscipy.fracture_mechanics.crack as crack
-from matscipy import parameter
-from matscipy.logger import screen
-
-from setup_crack import setup_crack
-from ase.optimize.precon import PreconLBFGS
-
-###
-
-sys.path += ['.', '..']
-import params
-
-###
-
-# Atom types used for outputting the crack tip position.
-ACTUAL_CRACK_TIP = 'Au'
-FITTED_CRACK_TIP = 'Ag'
-
-###
-
-logger = screen
-
-###
-
-a, cryst, crk, k1g, tip_x0, tip_y0, bond1, bond2, boundary_mask, \
- boundary_mask_bulk, tip_mask = setup_crack(logger=logger)
-ase.io.write('notch.xyz', a, format='extxyz')
-
-# Global parameters
-basename = parameter('basename', 'quasistatic_crack')
-calc = parameter('calc')
-fmax = parameter('fmax', 0.01)
-
-# Determine simulation control
-k1_list = parameter('k1')
-old_k1 = k1_list[0]
-nsteps = len(k1_list)
-tip_dx_list = parameter('tip_dx', np.zeros(nsteps))
-tip_dy_list = parameter('tip_dy', np.zeros(nsteps))
-
-# Run crack calculation.
-tip_x = tip_x0
-tip_y = tip_y0
-a.set_calculator(calc)
-for i, ( k1, tip_dx, tip_dy ) in enumerate(zip(k1_list, tip_dx_list,
- tip_dy_list)):
- logger.pr('=== k1 = {0}*k1g, tip_dx = {1}, tip_dy = {2} ===' \
- .format(k1, tip_dx, tip_dy))
- if tip_dx is None or tip_dy is None:
- #
- # Optimize crack tip position
- #
- old_y = tip_y+1.0
- old_x = tip_x+1.0
- while abs(tip_x-old_x) > 1e-6 and abs(tip_y-old_y) > 1e-6:
- b = cryst.copy()
- ux, uy = crk.displacements(cryst.positions[:,0], cryst.positions[:,1],
- tip_x, tip_y, k*k1g)
- b.positions[:,0] += ux
- b.positions[:,1] += uy
- a.set_constraint(None)
- a.positions[boundary_mask] = b.positions[boundary_mask]
- a.set_constraint(ase.constraints.FixAtoms(mask=boundary_mask))
- logger.pr('Optimizing positions...')
- opt = ase.optimize.FIRE(a, logfile=None)
- opt.run(fmax=fmax)
- logger.pr('...done. Converged within {0} steps.' \
- .format(opt.get_number_of_steps()))
-
- old_x = tip_x
- old_y = tip_y
- tip_x, tip_y = crk.crack_tip_position(a.positions[:,0],
- a.positions[:,1],
- cryst.positions[:,0],
- cryst.positions[:,1],
- tip_x, tip_y, k*k1g,
- mask=mask)
- else:
- #
- # Do not optimize tip position.
- #
- tip_x = tip_x0+tip_dx
- tip_y = tip_y0+tip_dy
- logger.pr('Setting crack tip position to {0} {1}' \
- .format(tip_x, tip_y))
- # Scale strain field and optimize crack
- a.set_constraint(None)
- x, y = crk.scale_displacements(a.positions[:len(cryst),0],
- a.positions[:len(cryst),1],
- cryst.positions[:,0],
- cryst.positions[:,1],
- old_k1, k1)
- a.positions[:len(cryst),0] = x
- a.positions[:len(cryst),1] = y
- # Optimize atoms in center
- a.set_constraint(ase.constraints.FixAtoms(mask=boundary_mask))
- logger.pr('Optimizing positions...')
- opt = PreconLBFGS(a)
- opt.run(fmax=fmax)
- logger.pr('...done. Converged within {0} steps.' \
- .format(opt.get_number_of_steps()))
- old_k1 = k1
-
- # Output optimized configuration ot file. Include the mask array in
- # output, so we know which atoms were used in fitting.
- a.set_array('atoms_used_for_fitting_crack_tip', tip_mask)
-
- # Output a file that contains the target crack tip (used for
- # the displacmenets of the boundary atoms) and the fitted crack tip
- # positions. The target crack tip is marked by a Hydrogen atom.
- b = a.copy()
- b += ase.Atom(ACTUAL_CRACK_TIP, (tip_x, tip_y, b.cell[2, 2]/2))
-
- # Measure the true (fitted) crack tip position.
- try:
- measured_tip_x, measured_tip_y = \
- crk.crack_tip_position(a.positions[:,0], a.positions[:,1],
- cryst.positions[:,0], cryst.positions[:,1],
- tip_x, tip_y, k1*k1g, mask=tip_mask)
- measured_tip_x %= a.cell[0][0]
- measured_tip_y %= a.cell[0][0]
- except:
- measured_tip_x = 0.0
- measured_tip_y = 0.0
-
- # The fitted crack tip is marked by a Helium atom.
- b += ase.Atom(FITTED_CRACK_TIP, (measured_tip_x, measured_tip_y,
- b.cell[2, 2]/2))
-
- b.info['bond_length'] = a.get_distance(bond1, bond2)
- b.info['energy'] = a.get_potential_energy()
- b.info['cell_origin'] = [0, 0, 0]
- ase.io.write('%s_%4.4i.xyz' % (basename, i), b, format='extxyz')
\ No newline at end of file
diff --git a/scripts/fracture_mechanics/sinclair_continuation.py b/scripts/fracture_mechanics/sinclair_continuation.py
deleted file mode 100644
index 081d0da4..00000000
--- a/scripts/fracture_mechanics/sinclair_continuation.py
+++ /dev/null
@@ -1,236 +0,0 @@
-#
-# Copyright 2020-2021 James Kermode (Warwick U.)
-#
-# matscipy - Materials science with Python at the atomic-scale
-# https://github.com/libAtoms/matscipy
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-import matscipy; print(matscipy.__file__)
-
-import os
-import numpy as np
-
-import h5py
-import ase.io
-from ase.units import GPa
-from ase.constraints import FixAtoms
-#from ase.optimize import LBFGSLineSearch
-from ase.optimize.precon import PreconLBFGS
-
-from matscipy import parameter
-from matscipy.elasticity import fit_elastic_constants
-from matscipy.fracture_mechanics.crack import CubicCrystalCrack, SinclairCrack
-
-from scipy.optimize import fsolve, fminbound
-from scipy.optimize.nonlin import NoConvergence
-
-import sys
-sys.path.insert(0, '.')
-
-import params
-calc = parameter('calc')
-fmax = parameter('fmax', 1e-3)
-max_opt_steps = parameter('max_opt_steps', 100)
-max_arc_steps = parameter('max_arc_steps', 10)
-vacuum = parameter('vacuum', 10.0)
-flexible = parameter('flexible', True)
-continuation = parameter('continuation', False)
-ds = parameter('ds', 1e-2)
-nsteps = parameter('nsteps', 10)
-a0 = parameter('a0') # lattice constant
-k0 = parameter('k0', 1.0)
-extended_far_field = parameter('extended_far_field', False)
-alpha0 = parameter('alpha0', 0.0) # initial guess for crack position
-dump = parameter('dump', False)
-precon = parameter('precon', False)
-prerelax = parameter('prerelax', False)
-traj_file = parameter('traj_file', 'x_traj.h5')
-restart_file = parameter('restart_file', traj_file)
-traj_interval = parameter('traj_interval', 1)
-direction = parameter('direction', +1)
-ds_max = parameter('ds_max', 0.1)
-ds_min = parameter('ds_min', 1e-6)
-ds_aggressiveness=parameter('ds_aggressiveness', 1.25)
-opt_method=parameter('opt_method', 'krylov')
-
-
-# make copies of initial configs
-cryst = params.cryst.copy()
-cluster = params.cluster.copy()
-
-# compute elastic constants
-cryst.pbc = True
-cryst.calc = calc
-C, C_err = fit_elastic_constants(cryst, symmetry=parameter('elastic_symmetry','triclinic'))
-
-# setup the crack
-crk = CubicCrystalCrack(parameter('crack_surface'),
- parameter('crack_front'),
- Crot=C/GPa)
-
-# get Griffith's stess intensity factor
-k1g = crk.k1g(parameter('surface_energy'))
-print('Griffith k1 = %f' % k1g)
-
-# check for restart files if continuation=True
-if continuation and not os.path.exists(restart_file):
- continuation = False
-if continuation:
-
- # works only if restart file contains atleast two consecutive flex solutions
- with h5py.File(restart_file, 'r') as hf:
- x = hf['x']
- restart_index = parameter('restart_index', x.shape[0] - 1)
- x0 = x[restart_index-1, :]
- x1 = x[restart_index, :]
- k0 = x0[-1] / k1g ; k1 = x1[-1] / k1g
- alpha0 = x0[-2] / k1g ; alpha1 = x1[-2] / k1g
- # print restart info
- print(f'Restarting from k0={k0}, k1={k1} --> alpha0={alpha0}, alpha1={alpha1}')
-
-else:
-
- # setup Sinclair boundary conditions with variable_alpha and no variable_k, for approx solution
- sc = SinclairCrack(crk, cluster, calc,
- k0*k1g, alpha=alpha0,
- vacuum=vacuum,
- variable_alpha=flexible,
- extended_far_field=extended_far_field)
-
- traj = None
- dk = parameter('dk', 1e-4)
- dalpha = parameter('dalpha', 1e-3)
-
- # reuse output from sinclair_crack.py if possible
- if os.path.exists(f'k_{int(k0 * 1000):04d}.xyz'):
- print(f'Reading atoms from k_{int(k0 * 1000):04d}.xyz')
- a = ase.io.read(f'k_{int(k0 * 1000):04d}.xyz')
- sc.set_atoms(a)
-
- # setup mask fpr relevant regions
- mask = sc.regionI | sc.regionII
- if extended_far_field:
- mask = sc.regionI | sc.regionII | sc.regionIII
-
-
- # estimate the stable K range
- k_range= parameter('k_range', 'Unknown')
- if isinstance(k_range,list) and len(k_range)==2:
- kmin, kmax = k_range
- print(f'Using K range from params file: {kmin} < k / k_G < {kmax}')
- else:
- print('No k_range=[kmin,kmax] given in params.')
- print('Running CLE-only approximation to estimate stable K range.')
- # first use the CLE-only approximation`: define a function f_alpha0(k, alpha)
- def f(k, alpha):
- sc.k = k * k1g
- sc.alpha = alpha
- sc.update_atoms()
- return sc.get_crack_tip_force(mask=mask)
-
- # identify approximate range of stable k
- alpha_range = parameter('alpha_range', np.linspace(-a0, a0, 20))
- k = k0 # initial guess for k
- alpha_k = []
- for alpha in alpha_range:
- # look for solution to f(k, alpha) = 0 close to alpha = alpha
- (k,) = fsolve(f, k, args=(alpha,))
- print(f'alpha={alpha:.3f} k={k:.3f} ')
- alpha_k.append((alpha, k))
- alpha_k = np.array(alpha_k)
- kmin, kmax = alpha_k[:, 1].min(), alpha_k[:, 1].max()
- print(f'Estimated stable K range is {kmin} < k / k_G < {kmax}')
-
- # define a function to relax with static scheme at a given value of k
- traj = open("traj.xyz", "w")
- def g(k, do_abs=True):
- print(f'Static minimisation with k={k}, alpha={alpha0}.')
- sc.k = k * k1g
- sc.alpha = alpha0
- sc.variable_alpha = False
- sc.variable_k = False
- sc.update_atoms()
- atoms = sc.atoms.copy()
- atoms.calc = sc.calc
- atoms.set_constraint(FixAtoms(mask=~sc.regionI))
- opt = PreconLBFGS(atoms, logfile=None)
- #opt = LBFGSLineSearch(atoms)
- opt.run(fmax=1e-3)
- atoms.write(traj, format="extxyz")
- sc.set_atoms(atoms)
- f_alpha = sc.get_crack_tip_force(mask=mask)
- print(f'Static minimisation with k={k}, alpha={alpha0} --> f_alpha={f_alpha}')
- if do_abs:
- f_alpha = abs(f_alpha)
- return f_alpha
-
- # minimise g(k) in [kmin, kmax]
- kopt, falpha_min, ierr, funccalls = fminbound(g, kmin, kmax, xtol=1e-8, full_output=True)
- print(f'Brent minimisation yields f_alpha={falpha_min} for k = {kopt} after {funccalls} calls')
- traj.close()
-
- # Find flex1
- # first optimize with static scheme
- #sc.k = kopt * k1g
- k0 = kopt
- sc.rescale_k(k0 * k1g)
- sc.alpha = alpha0
- sc.variable_alpha = False
- sc.optimize(ftol=1e-3, steps=max_opt_steps,method=opt_method)
- # then revert to target fmax precision and optimize with flexible scheme
- sc.variable_alpha = flexible #True
- sc.optimize(ftol=fmax, steps=max_opt_steps,method=opt_method)
- # save flex1
- sc.atoms.write('x0.xyz')
- x0 = np.r_[sc.get_dofs(), k0 * k1g]
- alpha0 = sc.alpha
-
- # Find flex2: a second solution x1 = (u_1, alpha_1, k_1) where
- # k_1 ~= k_0 and alpha_1 ~= alpha_0
- # Increase k, and rescale Us accordingly
- print(f'Rescaling K_I from {sc.k} to {sc.k + dk * k1g}')
- k1 = k0 + dk
- sc.rescale_k(k1 * k1g)
- # optimize at target fmax precision with flexible scheme
- sc.variable_alpha = flexible #True
- sc.optimize(ftol=fmax, steps=max_opt_steps,method=opt_method)
- # save flex2
- sc.atoms.write('x1.xyz')
- x1 = np.r_[sc.get_dofs(), k1 * k1g]
- alpha1 = sc.alpha
-
- # check crack tip didn't jump too far
- print(f'k0={k0}, k1={k1} --> alpha0={alpha0}, alpha1={alpha1}')
- assert abs(alpha1 - alpha0) < dalpha
-
-# setup new crack with variable_k for full solution
-scv = SinclairCrack(crk, cluster, calc,
- k0*k1g, alpha=alpha0,
- vacuum=vacuum,
- variable_alpha=flexible, variable_k=True,
- extended_far_field=extended_far_field)
-
-# run full arc length continuation
-scv.arc_length_continuation(x0, x1, N=nsteps,
- ds=ds, ftol=fmax, max_steps=max_arc_steps,
- direction=direction,
- continuation=continuation,
- traj_file=traj_file,
- traj_interval=traj_interval,
- precon=precon,
- ds_max=ds_max, ds_min=ds_min,
- ds_aggressiveness=ds_aggressiveness,
- opt_method='krylov') # 'ode12r' preconditioning needs debugging
\ No newline at end of file
diff --git a/scripts/fracture_mechanics/sinclair_crack.py b/scripts/fracture_mechanics/sinclair_crack.py
deleted file mode 100644
index bc2518a0..00000000
--- a/scripts/fracture_mechanics/sinclair_crack.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#
-# Copyright 2014, 2020 James Kermode (Warwick U.)
-# 2020 Arnaud Allera (U. Lyon 1)
-# 2014 Lars Pastewka (U. Freiburg)
-#
-# matscipy - Materials science with Python at the atomic-scale
-# https://github.com/libAtoms/matscipy
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-import os
-import numpy as np
-
-import ase.io
-from ase.units import GPa
-from ase.constraints import FixAtoms
-from ase.optimize.precon import PreconLBFGS
-
-from matscipy import parameter
-from matscipy.elasticity import fit_elastic_constants
-from matscipy.fracture_mechanics.crack import CubicCrystalCrack, SinclairCrack
-
-import sys
-
-sys.path.insert(0, '.')
-import params
-
-calc = parameter('calc')
-fmax = parameter('fmax', 1e-3)
-vacuum = parameter('vacuum', 10.0)
-flexible = parameter('flexible', True)
-extended_far_field = parameter('extended_far_field', False)
-k0 = parameter('k0', 1.0)
-alpha0 = parameter('alpha0', 0.0) # initial guess for crack position
-dump = parameter('dump', False)
-precon = parameter('precon', False)
-prerelax = parameter('prerelax', False)
-lbfgs = parameter('lbfgs', not flexible) # use LBGS by default if not flexible
-
-# compute elastic constants
-cryst = params.cryst.copy()
-cryst.pbc = True
-cryst.calc = calc
-C, C_err = fit_elastic_constants(cryst,
- symmetry=parameter('elastic_symmetry',
- 'triclinic'))
-
-crk = CubicCrystalCrack(parameter('crack_surface'),
- parameter('crack_front'),
- Crot=C/GPa)
-
-# Get Griffith's k1.
-k1g = crk.k1g(parameter('surface_energy'))
-print('Griffith k1 = %f' % k1g)
-
-
-cluster = params.cluster.copy()
-sc = SinclairCrack(crk, cluster, calc, k0 * k1g,
- alpha=alpha0,
- variable_alpha=flexible,
- vacuum=vacuum,
- extended_far_field=extended_far_field)
-
-nsteps = parameter('nsteps')
-k1_range = parameter('k1_range', np.linspace(0.8, 1.2, nsteps))
-
-ks = list(k1_range)
-ks_out = []
-alphas = []
-
-max_steps = parameter('max_steps', 10)
-fit_alpha = parameter('fit_alpha', False)
-
-for i, k in enumerate(ks):
- restart_file = parameter('restart_file', f'k_{int(k * 1000):04d}.xyz')
- if os.path.exists(restart_file):
- print(f'Restarting from {restart_file}')
- if restart_file.endswith('.xyz'):
- a = ase.io.read(restart_file)
- sc.set_atoms(a)
- elif restart_file.endswith('.txt'):
- x = np.loadtxt(restart_file)
- if len(x) == len(sc):
- sc.set_dofs(x)
- elif len(x) == len(sc) + 1:
- sc.variable_k = True
- sc.set_dofs(x)
- sc.variable_k = False
- elif len(x) == len(sc) + 2:
- sc.variable_alpha = True
- sc.variable_k = True
- sc.set_dofs(x)
- sc.variable_alpha = False
- sc.variable_k = False
- else:
- raise RuntimeError('cannot guess how to restart with'
- f' {len(x)} variables for {len(self)} DoFs')
- else:
- raise ValueError(f"don't know how to restart from {restart_file}")
- else:
- sc.rescale_k(k * k1g)
-
- if fit_alpha:
- sc.alpha, = sc.fit_cle(variable_alpha=True, variable_k=False)
- print(f'Fitted value of alpha: {sc.alpha}')
- print(f'k = {sc.k / k1g} * k1g')
- print(f'alpha = {sc.alpha}')
-
- if lbfgs:
- print('Optimizing with LBFGS')
- atoms = sc.atoms.copy()
- atoms.calc = sc.calc
- atoms.set_constraint(FixAtoms(mask=np.logical_not(sc.regionI)))
- opt = PreconLBFGS(atoms)
- opt.run(fmax=fmax)
- sc.set_atoms(atoms)
- else:
- if prerelax:
- print('Pre-relaxing with Conjugate-Gradients')
- sc.optimize(ftol=1e-5, steps=max_steps, dump=dump,
- method='cg')
-
- sc.optimize(fmax, steps=max_steps, dump=dump,
- precon=precon, method='krylov')
-
- if flexible:
- print(f'Optimized alpha = {sc.alpha:.3f}')
- ks_out.append(k)
- alphas.append(sc.alpha)
-
- a = sc.atoms
- a.get_forces()
- ase.io.write(f'k_{int(k * 1000):04d}.xyz', a)
- with open('x.txt', 'a') as fh:
- np.savetxt(fh, [sc.get_dofs()])
-
-np.savetxt('k_vs_alpha.txt', np.c_[ks_out, alphas])