"""This module contains the `SRIMDB` class."""
from dataclasses import dataclass, field
from pathlib import Path
from irradiapy import config
from irradiapy.database import Database
from irradiapy.enums import Phases
from irradiapy.materials.component import Component
from irradiapy.materials.element import Element
from irradiapy.srim.backscat import Backscat
from irradiapy.srim.collision import Collision
from irradiapy.srim.e2recoil import E2Recoil
from irradiapy.srim.ioniz import Ioniz
from irradiapy.srim.lateral import Lateral
from irradiapy.srim.novac import Novac
from irradiapy.srim.phonon import Phonon
from irradiapy.srim.range import Range
from irradiapy.srim.range3d import Range3D
from irradiapy.srim.sputter import Sputter
from irradiapy.srim.transmit import Transmit
from irradiapy.srim.trimdat import Trimdat
from irradiapy.srim.vacancy import Vacancy
[docs]
@dataclass(kw_only=True)
class SRIMDB(Database):
"""Base class for storing SRIM output data in a SQLite database.
Attributes
----------
path : Path
Output database path.
target : list[Component] | None, optional (default=None)
SRIM target. Do not provide this argument for read only.
calculation : str | None, optional (default=None)
SRIM calculation. Do not provide this argument for read only.
Accepted values are: "quick", "full" and "mono".
seed : int (default=0)
Seed for SRIM randomness.
srim_dir : Path (default=config.get_srim_dir())
Directory where SRIM is installed.
backscat : Backscat
Class storing `BACKSCAT.txt` data.
e2recoil : E2Recoil
Class storing `E2RECOIL.txt` data.
ioniz : Ioniz
Class storing `IONIZ.txt` data.
lateral : Lateral
Class storing `LATERAL.txt` data.
phonon : Phonon
Class storing `PHONON.txt` data.
range : Range
Class storing `RANGE.txt` data.
range3d : Range3D
Class storing `RANGE_3D.txt` data.
sputter : Sputter
Class storing `SPUTTER.txt` data.
transmit : Transmit
Class storing `TRANSMIT.txt` data.
trimdat : Trimdat
Class storing `TRIM.DAT` data.
vacancy : Vacancy
Class storing `VACANCY.txt` data.
"""
target: list[Component] | None = None
calculation: str | None = None
seed: int = 0
srim_dir: Path = field(default_factory=config.get_srim_dir)
plot_type: int = 5
xmin: float = 0.0
xmax: float = 0.0
do_ranges: int = 1
do_backscatt: int = 1
do_transmit: int = 1
do_sputtered: int = 1
do_collisions: int = 1
exyz: float = 0.0
autosave: int = 0
reminders: int = field(default=0, init=False)
bragg: int = field(default=1, init=False)
backscat: Backscat = field(init=False)
e2recoil: E2Recoil = field(init=False)
ioniz: Ioniz = field(init=False)
lateral: Lateral = field(init=False)
phonon: Phonon = field(init=False)
range: Range = field(init=False)
range3d: Range3D = field(init=False)
sputter: Sputter = field(init=False)
transmit: Transmit = field(init=False)
trimdat: Trimdat = field(init=False)
vacancy: Vacancy = field(init=False)
collision: Collision = field(init=False)
nions: int = field(init=False, default=0)
def __post_init__(self) -> None:
"""Initializes the `SRIMDB` object.
"""
super().__post_init__()
self.backscat = Backscat(self)
self.e2recoil = E2Recoil(self)
self.ioniz = Ioniz(self)
self.lateral = Lateral(self)
self.phonon = Phonon(self)
self.range = Range(self)
self.range3d = Range3D(self)
self.sputter = Sputter(self)
self.transmit = Transmit(self)
self.trimdat = Trimdat(self)
self.vacancy = Vacancy(self)
self.collision = Collision(self)
if self.calculation not in ["quick", "full", "mono", None]:
raise ValueError("Invalid calculation mode.")
self.nions = self.get_nions()
if self.target and self.calculation and not self.table_exists("calculation"):
self.__save_target_calculation()
elif (
not self.target
and not self.calculation
and self.table_exists("calculation")
):
self.load_target_calculation()
elif (self.target and not self.calculation) or (
not self.target and self.calculation
):
raise ValueError(
"Both `srim_target` and `calculation` must be provided or None."
)
if self.calculation != "quick":
self.novac = Novac(self)
def __save_component(self, component: Component) -> None:
"""Save component into the recoils database."""
cur = self.cursor()
cur.execute(
(
"INSERT INTO components ("
"name, phase, density, "
"x0, y0, z0, width, height, length, "
"ax, ay, az, c, structure, "
"ed_min, ed_avr, b_arc, c_arc, "
"srim_el, srim_es, srim_phase, srim_bragg) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "
"?, ?, ?, ?, ?)"
),
(
component.name,
component.phase.to_int(),
component.density,
component.x0,
component.y0,
component.z0,
component.width,
component.height,
component.length,
component.ax,
component.ay,
component.az,
component.c,
component.structure,
component.ed_min,
component.ed_avr,
component.b_arc,
component.c_arc,
component.srim_el,
component.srim_es,
component.srim_phase,
component.srim_bragg,
),
)
# Save elements
component_id = cur.lastrowid
for element, stoich in zip(component.elements, component.stoichs):
cur.execute(
(
"INSERT INTO elements (component_id, atomic_number, "
"atomic_weight, symbol, stoich, ed_min, ed_avr, b_arc, c_arc, srim_el, srim_es) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
),
(
component_id,
element.atomic_number,
element.atomic_weight,
element.symbol,
stoich,
element.ed_min,
element.ed_avr,
element.b_arc,
element.c_arc,
element.srim_el,
element.srim_es,
),
)
cur.close()
self.commit()
return component_id
def __save_target_calculation(self) -> None:
"""Saves the target and calculation parameters into the database."""
cur = self.cursor()
cur.execute(
(
"CREATE TABLE IF NOT EXISTS components ("
"component_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, "
"phase INTEGER, density REAL, x0 REAL, y0 REAL, z0 REAL, "
"width REAL, height REAL, length REAL, "
"ax REAL, ay REAL, az REAL, c REAL, structure TEXT, "
"ed_min REAL, ed_avr REAL, b_arc REAL, c_arc REAL, calculate_energies BOOLEAN, "
"srim_el REAL, srim_es REAL, srim_phase INTEGER, srim_bragg INTEGER)"
)
)
cur.execute(
(
"CREATE TABLE IF NOT EXISTS elements ("
"component_id INTEGER, atomic_number INTEGER, atomic_weight REAL, "
"symbol TEXT, stoich REAL, ed_min REAL, ed_avr REAL, b_arc REAL, c_arc REAL, "
"srim_el REAL, srim_es REAL)"
)
)
for component in self.target:
self.__save_component(component)
cur.execute(
(
"CREATE TABLE IF NOT EXISTS calculation("
"mode INTEGER, seed INTEGER, reminders INTEGER, plot_type INTEGER,"
"xmin REAL, xmax REAL, ranges INTEGER, backscatt INTEGER,"
"transmit INTEGER, sputtered INTEGER, collisions INTEGER, exyz REAL,"
"bragg INTEGER, autosave INTEGER)"
)
)
if self.calculation == "quick":
calculation = 4
elif self.calculation == "full":
calculation = 5
else:
calculation = 7
cur.execute(
(
"INSERT INTO calculation"
"(mode, seed, reminders, plot_type, xmin, xmax, ranges, backscatt,"
"transmit, sputtered, collisions, exyz, bragg, autosave)"
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
),
[
calculation,
self.seed,
self.reminders,
self.plot_type,
self.xmin,
self.xmax,
self.do_ranges,
self.do_backscatt,
self.do_transmit,
self.do_sputtered,
self.do_collisions,
self.exyz,
self.bragg,
self.autosave,
],
)
cur.close()
self.commit()
[docs]
def load_target_calculation(self) -> list[Component]:
"""Loads the target and calculation parameters from the database."""
cur = self.cursor()
cur.execute(
"SELECT component_id, name, phase, density, width, height, length, ax, structure, srim_bragg FROM components"
)
components = list(cur.fetchall())
target = []
for (
component_id,
name,
phase,
density,
width,
height,
length,
ax,
structure,
srim_bragg,
) in components:
cur.execute(
(
"SELECT component_id, atomic_number, "
"atomic_weight, symbol, stoich, ed_min, ed_avr, b_arc, c_arc, srim_el, srim_es "
"FROM elements WHERE component_id = ?"
),
(component_id,),
)
db_elements = list(cur.fetchall())
elements = []
for (
_component_id,
atomic_number,
atomic_weight,
symbol,
_stoich,
ed_min,
ed_avr,
b_arc,
c_arc,
srim_el,
srim_es,
) in db_elements:
element = Element(
atomic_number=atomic_number,
atomic_weight=atomic_weight,
symbol=symbol,
ed_min=ed_min,
b_arc=b_arc,
c_arc=c_arc,
ed_avr=ed_avr,
srim_el=srim_el,
srim_es=srim_es,
)
elements.append(element)
stoichs = [db_element[4] for db_element in db_elements]
component = Component(
elements=elements,
stoichs=stoichs,
name=name,
phase=Phases.from_int(phase),
density=density,
width=width,
height=height,
length=length,
ax=ax,
structure=structure,
srim_bragg=srim_bragg,
)
target.append(component)
self.target = target
cur.execute("SELECT * FROM calculation")
db_calculation = cur.fetchone()
cur.close()
self.seed = db_calculation[1]
self.reminders = db_calculation[2]
self.plot_type = db_calculation[3]
self.xmin = db_calculation[4]
self.xmax = db_calculation[5]
self.do_ranges = db_calculation[6]
self.do_backscatt = db_calculation[7]
self.do_transmit = db_calculation[8]
self.do_sputtered = db_calculation[9]
self.do_collisions = db_calculation[10]
self.exyz = db_calculation[11]
self.bragg = db_calculation[12]
self.autosave = db_calculation[13]
mode = db_calculation[0]
if mode == 4:
self.calculation = "quick"
elif mode == 5:
self.calculation = "full"
else:
self.calculation = "mono"
cur.close()
return target
[docs]
def get_nions(self) -> int:
"""Gets the number of ions in the simulation based on trimdat table."""
if self.table_exists("trimdat"):
cur = self.cursor()
cur.execute("SELECT COUNT(1) FROM trimdat")
nions = cur.fetchone()[0]
cur.close()
return nions
return 0
[docs]
def append_output(self) -> None:
"""Appends SRIM output files into the database."""
self.trimdat.process_file(self.srim_dir / "TRIM.DAT") # This must be first
self.backscat.process_file(self.srim_dir / "SRIM Outputs/BACKSCAT.txt")
self.collision.process_file(self.srim_dir / "SRIM Outputs/COLLISON.txt")
self.e2recoil.process_file(self.srim_dir / "E2RECOIL.txt")
self.ioniz.process_file(self.srim_dir / "IONIZ.txt")
self.lateral.process_file(self.srim_dir / "LATERAL.txt")
self.phonon.process_file(self.srim_dir / "PHONON.txt")
self.range3d.process_file(self.srim_dir / "SRIM Outputs/RANGE_3D.txt")
self.range.process_file(self.srim_dir / "RANGE.txt")
self.sputter.process_file(self.srim_dir / "SRIM Outputs/SPUTTER.txt")
self.transmit.process_file(self.srim_dir / "SRIM Outputs/TRANSMIT.txt")
self.nions = self.get_nions()
self.vacancy.process_file(self.srim_dir / "VACANCY.txt")
if self.calculation in ["full", "mono"]:
self.novac.process_file(self.srim_dir / "NOVAC.txt")