pack-mm 0.0.14__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pack_mm/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Pack molecules in various shapes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("pack-mm")
pack_mm/cli/packmm.py ADDED
@@ -0,0 +1,161 @@
1
+ """Command line for packmm."""
2
+
3
+ # Author; alin m elena, alin@elena.re
4
+ # Contribs;
5
+ # Date: 22-02-2025
6
+ # ©alin m elena,
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+ import typer
12
+
13
+ from pack_mm.core.core import pack_molecules
14
+
15
+
16
+ class InsertionMethod(str, Enum):
17
+ """Insertion options."""
18
+
19
+ ANYWHERE = "anywhere"
20
+ SPHERE = "sphere"
21
+ BOX = "box"
22
+ CYLINDER_Z = "cylinderZ"
23
+ CYLINDER_Y = "cylinderY"
24
+ CYLINDER_X = "cylinderX"
25
+ ELLIPSOID = "ellipsoid"
26
+
27
+
28
+ app = typer.Typer(no_args_is_help=True)
29
+
30
+
31
+ @app.command()
32
+ def packmm(
33
+ system: str | None = typer.Option(
34
+ None,
35
+ help="""The original box in which you want to add particles.
36
+ If not provided, an empty box will be created.""",
37
+ ),
38
+ molecule: str = typer.Option(
39
+ "H2O",
40
+ help="""Name of the molecule to be processed, ASE-recognizable or
41
+ ASE-readable file.""",
42
+ ),
43
+ nmols: int = typer.Option(-1, help="Target number of molecules to insert."),
44
+ ntries: int = typer.Option(
45
+ 50, help="Maximum number of attempts to insert each molecule."
46
+ ),
47
+ seed: int = typer.Option(2025, help="Random seed for reproducibility."),
48
+ where: InsertionMethod = typer.Option(
49
+ InsertionMethod.ANYWHERE,
50
+ help="""Where to insert the molecule. Choices: 'anywhere', 'sphere',
51
+ 'box', 'cylinderZ', 'cylinderY', 'cylinderX', 'ellipsoid'.""",
52
+ ),
53
+ centre: str | None = typer.Option(
54
+ None,
55
+ help="""Centre of the insertion zone in fractional coordinates,
56
+ e.g., '0.12,0.4,0.5'.""",
57
+ ),
58
+ radius: float | None = typer.Option(
59
+ None,
60
+ help="""Radius of the sphere or cylinder in Å,
61
+ depending on the insertion volume.""",
62
+ ),
63
+ height: float | None = typer.Option(
64
+ None, help="Height of the cylinder in fractional coordinates."
65
+ ),
66
+ a: float | None = typer.Option(
67
+ None,
68
+ help="""Side of the box or semi-axis of the ellipsoid, fractional,
69
+ depends on the insertion method.""",
70
+ ),
71
+ b: float | None = typer.Option(
72
+ None,
73
+ help="""Side of the box or semi-axis of the ellipsoid, fractional,
74
+ depends on the insertion method.""",
75
+ ),
76
+ c: float | None = typer.Option(
77
+ None,
78
+ help="""Side of the box or semi-axis of the ellipsoid, fractional,
79
+ depends on the insertion method.""",
80
+ ),
81
+ device: str = typer.Option(
82
+ "cpu", help="Device to run calculations on (e.g., 'cpu' or 'cuda')."
83
+ ),
84
+ model: str = typer.Option("medium-omat-0", help="ML model to use."),
85
+ arch: str = typer.Option("mace_mp", help="MLIP architecture to use."),
86
+ temperature: float = typer.Option(
87
+ 300.0, help="Temperature for the Monte Carlo acceptance rule."
88
+ ),
89
+ cell_a: float = typer.Option(20.0, help="Side of the empty box along the x-axis."),
90
+ cell_b: float = typer.Option(20.0, help="Side of the empty box along the y-axis."),
91
+ cell_c: float = typer.Option(20.0, help="Side of the empty box along the z-axis."),
92
+ fmax: float = typer.Option(
93
+ 0.1, help="force tollerance for optimisation if needed."
94
+ ),
95
+ geometry: bool = typer.Option(
96
+ True, help="Perform geometry optimization at the end."
97
+ ),
98
+ ):
99
+ """Pack molecules into a system based on the specified parameters."""
100
+ print("Script called with following input")
101
+ print(f"{system=}")
102
+ print(f"{nmols=}")
103
+ print(f"{molecule=}")
104
+ print(f"{ntries=}")
105
+ print(f"{seed=}")
106
+ print(f"where={where.value}")
107
+ print(f"{centre=}")
108
+ print(f"{radius=}")
109
+ print(f"{height=}")
110
+ print(f"{a=}")
111
+ print(f"{b=}")
112
+ print(f"{c=}")
113
+ print(f"{cell_a=}")
114
+ print(f"{cell_b=}")
115
+ print(f"{cell_c=}")
116
+ print(f"{arch=}")
117
+ print(f"{model=}")
118
+ print(f"{device=}")
119
+ print(f"{temperature=}")
120
+ print(f"{fmax=}")
121
+ print(f"{geometry=}")
122
+ if nmols == -1:
123
+ print("nothing to do, no molecule to insert")
124
+ raise typer.Exit(0)
125
+
126
+ center = centre
127
+ if centre:
128
+ center = tuple(map(float, centre.split(",")))
129
+ lc = [x < 0.0 for x in center]
130
+ if len(center) != 3 or any(lc):
131
+ err = "Invalid centre 3 coordinates expected!"
132
+ print(f"{err}")
133
+ raise Exception("Invalid centre 3 coordinates expected!")
134
+
135
+ pack_molecules(
136
+ system=system,
137
+ molecule=molecule,
138
+ nmols=nmols,
139
+ arch=arch,
140
+ model=model,
141
+ device=device,
142
+ where=where,
143
+ center=center,
144
+ radius=radius,
145
+ height=height,
146
+ a=a,
147
+ b=b,
148
+ c=c,
149
+ seed=seed,
150
+ temperature=temperature,
151
+ ntries=ntries,
152
+ fmax=fmax,
153
+ geometry=geometry,
154
+ ca=cell_a,
155
+ cb=cell_b,
156
+ cc=cell_c,
157
+ )
158
+
159
+
160
+ if __name__ == "__main__":
161
+ app()
pack_mm/core/core.py ADDED
@@ -0,0 +1,345 @@
1
+ # Author; alin m elena, alin@elena.re
2
+ # Contribs;
3
+ # Date: 16-11-2024
4
+ # ©alin m elena,
5
+ """pack molecules inside various shapes."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from ase import Atoms
12
+ from ase.build import molecule as build_molecule
13
+ from ase.io import read, write
14
+ from janus_core.calculations.geom_opt import GeomOpt
15
+ from janus_core.helpers.mlip_calculators import choose_calculator
16
+ from numpy import cos, exp, pi, random, sin, sqrt
17
+
18
+
19
+ def random_point_in_sphere(c: (float, float, float), r: float) -> (float, float, float):
20
+ """
21
+ Generate a random point inside a sphere of radius r, centered at c.
22
+
23
+ Parameters
24
+ ----------
25
+ c (tuple): The center of the sphere as (x, y, z).
26
+ r (float): The radius of the sphere.
27
+
28
+ Returns
29
+ -------
30
+ tuple: A point (x, y, z) inside the sphere.
31
+ """
32
+ rad = r * random.rand() ** (1 / 3)
33
+
34
+ theta = random.uniform(0, 2 * pi)
35
+ phi = random.uniform(0, pi)
36
+
37
+ x = c[0] + rad * sin(phi) * cos(theta)
38
+ y = c[1] + rad * sin(phi) * sin(theta)
39
+ z = c[2] + rad * cos(phi)
40
+
41
+ return (x, y, z)
42
+
43
+
44
+ def random_point_in_ellipsoid(
45
+ d: (float, float, float), a: float, b: float, c: float
46
+ ) -> (float, float, float):
47
+ """
48
+ Generate a random point inside an ellipsoid with axes a, b, c, centered at d.
49
+
50
+ Parameters
51
+ ----------
52
+ d (tuple): The center of the ellipsoid as (x, y, z).
53
+ a (float): The semi-axis length of the ellipsoid along the x-axis.
54
+ b (float): The semi-axis length of the ellipsoid along the y-axis.
55
+ c (float): The semi-axis length of the ellipsoid along the z-axis.
56
+
57
+ Returns
58
+ -------
59
+ tuple: A point (x, y, z) inside the ellipsoid.
60
+ """
61
+ theta = random.uniform(0, 2 * pi)
62
+ phi = random.uniform(0, pi)
63
+ rad = random.rand() ** (1 / 3)
64
+
65
+ x = d[0] + a * rad * sin(phi) * cos(theta)
66
+ y = d[1] + b * rad * sin(phi) * sin(theta)
67
+ z = d[2] + c * rad * cos(phi)
68
+
69
+ return (x, y, z)
70
+
71
+
72
+ def random_point_in_box(
73
+ d: (float, float, float), a: float, b: float, c: float
74
+ ) -> (float, float, float):
75
+ """
76
+ Generate a random point inside a box with sides a, b, c, centered at d.
77
+
78
+ Parameters
79
+ ----------
80
+ d (tuple): The center of the box as (x, y, z).
81
+ a (float): The length of the box along the x-axis.
82
+ b (float): The length of the box along the y-axis.
83
+ c (float): The length of the box along the z-axis.
84
+
85
+ Returns
86
+ -------
87
+ tuple: A point (x, y, z) inside the box.
88
+ """
89
+ x = d[0] + random.uniform(-a * 0.5, a * 0.5)
90
+ y = d[1] + random.uniform(-b * 0.5, b * 0.5)
91
+ z = d[2] + random.uniform(-c * 0.5, c * 0.5)
92
+
93
+ return (x, y, z)
94
+
95
+
96
+ def random_point_in_cylinder(
97
+ c: (float, float, float), r: float, h: float, d: str
98
+ ) -> (float, float, float):
99
+ """
100
+ Generate a random point inside a cylinder with radius r and height h, centered at c.
101
+
102
+ Parameters
103
+ ----------
104
+ c (tuple): The center of the cylinder as (x, y, z).
105
+ r (float): The radius of the cylinder's base.
106
+ h (float): The height of the cylinder.
107
+ direction (str): direction along which cylinger is oriented
108
+
109
+ Returns
110
+ -------
111
+ tuple: A point (x, y, z) inside the cylinder.
112
+ """
113
+ theta = random.uniform(0, 2 * pi)
114
+ rad = r * sqrt(random.rand())
115
+
116
+ if d == "z":
117
+ z = c[2] + random.uniform(-h * 0.5, h * 0.5)
118
+ x = c[0] + rad * cos(theta)
119
+ y = c[1] + rad * sin(theta)
120
+ elif d == "y":
121
+ y = c[1] + random.uniform(-h * 0.5, h * 0.5)
122
+ x = c[0] + rad * cos(theta)
123
+ z = c[2] + rad * sin(theta)
124
+ elif d == "x":
125
+ x = c[0] + random.uniform(-h * 0.5, h * 0.5)
126
+ y = c[1] + rad * sin(theta)
127
+ z = c[2] + rad * cos(theta)
128
+
129
+ return (x, y, z)
130
+
131
+
132
+ def validate_value(label, x):
133
+ """Validate input value, and raise an exception."""
134
+ if x is not None and x < 0.0:
135
+ err = f"Invalid {label}, needs to be positive"
136
+ print(err)
137
+ raise Exception(err)
138
+
139
+
140
+ def pack_molecules(
141
+ system: str | None,
142
+ molecule: str,
143
+ nmols: int,
144
+ arch: str,
145
+ model: str,
146
+ device: str,
147
+ where: str,
148
+ center: tuple[float, float, float] | None,
149
+ radius: float | None,
150
+ height: float | None,
151
+ a: float | None,
152
+ b: float | None,
153
+ c: float | None,
154
+ seed: int,
155
+ temperature: float,
156
+ ntries: int,
157
+ geometry: bool,
158
+ fmax: float,
159
+ ca: float,
160
+ cb: float,
161
+ cc: float,
162
+ ) -> None:
163
+ """
164
+ Pack molecules into a system based on the specified parameters.
165
+
166
+ Parameters
167
+ ----------
168
+ system (str): Path to the system file or name of the system.
169
+ molecule (str): Path to the molecule file or name of the molecule.
170
+ nmols (int): Number of molecules to insert.
171
+ arch (str): Architecture for the calculator.
172
+ model (str): Path to the model file.
173
+ device (str): Device to run calculations on (e.g., "cpu" or "cuda").
174
+ where (str): Region to insert molecules ("anywhere",
175
+ "sphere", "cylinderZ", etc.).
176
+ center (Optional[Tuple[float, float, float]]): Center of the insertion region.
177
+ radius (Optional[float]): Radius for spherical or cylindrical insertion.
178
+ height (Optional[float]): Height for cylindrical insertion.
179
+ a, b, c (Optional[float]): Parameters for box or ellipsoid insertion.
180
+ seed (int): Random seed for reproducibility.
181
+ temperature (float): Temperature in Kelvin for acceptance probability.
182
+ ntries (int): Maximum number of attempts to insert each molecule.
183
+ geometry (bool): Whether to perform geometry optimization after insertion.
184
+ ca, cb, cc (float): Cell dimensions if system is empty.
185
+ """
186
+ kbt = temperature * 8.6173303e-5 # Boltzmann constant in eV/K
187
+ validate_value("temperature", temperature)
188
+ validate_value("radius", radius)
189
+ validate_value("height", height)
190
+ validate_value("fmax", fmax)
191
+ validate_value("seed", seed)
192
+ validate_value("box a", a)
193
+ validate_value("box b", b)
194
+ validate_value("box c", c)
195
+ validate_value("ntries", ntries)
196
+ validate_value("cell box a", ca)
197
+ validate_value("cell box b", cb)
198
+ validate_value("nmols", nmols)
199
+ validate_value("cell box c", cc)
200
+
201
+ random.seed(seed)
202
+
203
+ try:
204
+ sys = read(system)
205
+ sysname = Path(system).stem
206
+ except Exception:
207
+ sys = Atoms(cell=[ca, cb, cc], pbc=[True, True, True])
208
+ sysname = "empty"
209
+
210
+ cell = sys.cell.lengths()
211
+
212
+ # Print initial information
213
+ print(f"Inserting {nmols} {molecule} molecules in {sysname}.")
214
+ print(f"Using {arch} model {model} on {device}.")
215
+ print(f"Insert in {where}.")
216
+
217
+ # Set center of insertion region
218
+ if center is None:
219
+ center = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
220
+ else:
221
+ center = tuple(ci * cell[i] for i, ci in enumerate(center))
222
+
223
+ # Set parameters based on insertion region
224
+ if where == "anywhere":
225
+ a, b, c = 1, 1, 1
226
+ elif where == "sphere":
227
+ if radius is None:
228
+ radius = min(cell) * 0.5
229
+ elif where in ["cylinderZ", "cylinderY", "cylinderX"]:
230
+ if radius is None:
231
+ if where == "cylinderZ":
232
+ radius = min(cell[0], cell[1]) * 0.5
233
+ elif where == "cylinderY":
234
+ radius = min(cell[0], cell[2]) * 0.5
235
+ elif where == "cylinderX":
236
+ radius = min(cell[2], cell[1]) * 0.5
237
+ if height is None:
238
+ height = 0.5
239
+ elif where == "box":
240
+ a, b, c = a or 1, b or 1, c or 1
241
+ elif where == "ellipsoid":
242
+ a, b, c = a or 0.5, b or 0.5, c or 0.5
243
+
244
+ calc = choose_calculator(
245
+ arch=arch, model_path=model, device=device, default_dtype="float64"
246
+ )
247
+ sys.calc = calc
248
+
249
+ e = sys.get_potential_energy() if len(sys) > 0 else 0.0
250
+
251
+ csys = sys.copy()
252
+ for i in range(nmols):
253
+ accept = False
254
+ for _itry in range(ntries):
255
+ mol = load_molecule(molecule)
256
+ tv = get_insertion_position(where, center, cell, a, b, c, radius, height)
257
+ mol = rotate_molecule(mol)
258
+ mol.translate(tv)
259
+
260
+ tsys = csys.copy() + mol.copy()
261
+ tsys.calc = calc
262
+ en = tsys.get_potential_energy()
263
+ de = en - e
264
+
265
+ acc = exp(-de / kbt)
266
+ u = random.random()
267
+ print(f"Old energy={e}, new energy={en}, {de=}, {acc=}, random={u}")
268
+
269
+ if u <= acc:
270
+ accept = True
271
+ break
272
+
273
+ if accept:
274
+ csys = tsys.copy()
275
+ e = en
276
+ print(f"Inserted particle {i + 1}")
277
+ write(f"{sysname}+{i + 1}{Path(molecule).stem}.cif", csys)
278
+ else:
279
+ print(f"Failed to insert particle {i + 1} after {ntries} tries")
280
+ optimize_geometry(
281
+ f"{sysname}+{i + 1}{Path(molecule).stem}.cif", device, arch, model, fmax
282
+ )
283
+
284
+ # Perform final geometry optimization if requested
285
+ if geometry:
286
+ optimize_geometry(
287
+ f"{sysname}+{nmols}{Path(molecule).stem}.cif", device, arch, model, fmax
288
+ )
289
+
290
+
291
+ def load_molecule(molecule: str):
292
+ """Load a molecule from a file or build it."""
293
+ try:
294
+ return build_molecule(molecule)
295
+ except KeyError:
296
+ return read(molecule)
297
+
298
+
299
+ def get_insertion_position(
300
+ where: str,
301
+ center: tuple[float, float, float],
302
+ cell: list[float],
303
+ a: float,
304
+ b: float,
305
+ c: float,
306
+ radius: float | None,
307
+ height: float | None,
308
+ ) -> tuple[float, float, float]:
309
+ """Get a random insertion position based on the region."""
310
+ if where == "sphere":
311
+ return random_point_in_sphere(center, radius)
312
+ if where == "box":
313
+ return random_point_in_box(center, cell[0] * a, cell[1] * b, cell[2] * c)
314
+ if where == "ellipsoid":
315
+ return random_point_in_ellipsoid(center, cell[0] * a, cell[1] * b, cell[2] * c)
316
+ if where in ["cylinderZ", "cylinderY", "cylinderX"]:
317
+ axis = where[-1].lower()
318
+ return random_point_in_cylinder(center, radius, cell[2] * height, axis)
319
+ # now is anywhere
320
+ return random.random(3) * [a, b, c] * cell
321
+
322
+
323
+ def rotate_molecule(mol):
324
+ """Rotate a molecule randomly."""
325
+ ang = random.random(3)
326
+ mol.euler_rotate(
327
+ phi=ang[0] * 360, theta=ang[1] * 180, psi=ang[2] * 360, center=(0.0, 0.0, 0.0)
328
+ )
329
+ return mol
330
+
331
+
332
+ def optimize_geometry(
333
+ struct_path: str, device: str, arch: str, model: str, fmax: float
334
+ ) -> float:
335
+ """Optimize the geometry of a structure."""
336
+ geo = GeomOpt(
337
+ struct_path=struct_path,
338
+ device=device,
339
+ fmax=fmax,
340
+ calc_kwargs={"model_paths": model, "default_dtype": "float64"},
341
+ filter_kwargs={"hydrostatic_strain": True},
342
+ )
343
+ geo.run()
344
+ write(f"{struct_path}-opt.cif", geo.struct)
345
+ return geo.struct.get_potential_energy()
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.1
2
+ Name: pack-mm
3
+ Version: 0.0.14
4
+ Summary: packing materials and molecules in boxes using for machine learnt interatomic potentials
5
+ Author: Alin M. Elena
6
+ Classifier: Programming Language :: Python
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Project-URL: Repository, https://github.com/ddmms/pack-mm/
15
+ Project-URL: Documentation, https://ddmms.github.io/pack-mm/
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: janus-core>=0.7.2
18
+ Requires-Dist: typer<1.0.0,>=0.12.5
19
+ Requires-Dist: typer-config<2.0.0,>=1.4.2
20
+ Description-Content-Type: text/markdown
21
+
22
+ # pack materials and molecules
23
+
24
+ [![Python versions][python-badge]][python-link]
25
+ [![Build Status][ci-badge]][ci-link]
26
+ [![Coverage Status][cov-badge]][cov-link]
27
+ [![License][license-badge]][license-link]
28
+
29
+ [python-badge]: https://img.shields.io/pypi/pyversions/pack-mm.svg
30
+ [python-link]: https://pypi.org/project/pack-mm/
31
+ [ci-badge]: https://github.com/ddmms/pack-mm/actions/workflows/build.yml/badge.svg?branch=main
32
+ [ci-link]: https://github.com/ddmms/pack-mm/actions
33
+ [cov-badge]: https://coveralls.io/repos/github/ddmms/pack-mm/badge.svg?branch=main
34
+ [cov-link]: https://coveralls.io/github/ddmms/pack-mm?branch=main
35
+ [license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
36
+ [license-link]: https://opensource.org/license/MIT
@@ -0,0 +1,8 @@
1
+ pack_mm-0.0.14.dist-info/METADATA,sha256=KOiX1eZqUv1OmoKdtpFyUC_ueiJ3BYGBhZjv6D8ZPMg,1583
2
+ pack_mm-0.0.14.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ pack_mm-0.0.14.dist-info/entry_points.txt,sha256=ajKA2oehIa_LCVCP2XTRxV0VNgjGl9c2wYkwk0BasrQ,66
4
+ pack_mm-0.0.14.dist-info/licenses/LICENSE,sha256=ZOYkPdn_vQ8wYJqZnjesow79F_grMbVlHcJ9V91G1pE,1100
5
+ pack_mm/__init__.py,sha256=ct7qfCmTDwhLYip6JKYWRLasmmaGYt0ColbK0CpvYZk,150
6
+ pack_mm/cli/packmm.py,sha256=FWiBUQBG6Z5Vi5A1VH9DYUn8iBwIs4NbFR5a8mPtEpM,4797
7
+ pack_mm/core/core.py,sha256=gP8j1NJg3_8dj4it3V2Z80dbAKhy0emwUjd_-wuKvws,10839
8
+ pack_mm-0.0.14.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ packmm = pack_mm.cli.packmm:app
3
+
4
+ [gui_scripts]
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 data-driven materials and molecular science
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.