Skip to content
Snippets Groups Projects
Commit 11e7b780 authored by Lauri Himanen's avatar Lauri Himanen
Browse files

Parsing of single point calculations underway.

parent 8ffb7733
Branches
Tags
No related merge requests found
from nomadcore.baseclasses import CommonMatcher
from nomadcore.baseclasses import CommonParser
#===============================================================================
class CPMDCommonMatcher(CommonMatcher):
class CPMDCommonParser(CommonParser):
"""
This class is used to store and instantiate common parts of the
hierarchical SimpleMatcher structure used in the parsing of a CPMD
calculation.
"""
def __init__(self, parser_context):
super(CPMDCommonMatcher, self).__init__(parser_context)
super(CPMDCommonParser, self).__init__(parser_context)
#===========================================================================
# onClose triggers
......
from __future__ import absolute_import
from nomadcore.simple_parser import SimpleMatcher as SM
from nomadcore.baseclasses import MainHierarchicalParser
from nomadcore.caching_backend import CachingLevel
from .commonmatcher import CommonMatcher
from nomadcore.baseclasses import SubHierarchicalParser
# from nomadcore.caching_backend import CachingLevel
import logging
logger = logging.getLogger("nomad")
#===============================================================================
class CPMDSinglePointParser(MainHierarchicalParser):
"""The main parser class. Used to parse the CP2K calculation with run types:
-ENERGY
-ENERGY_FORCE
class CPMDInputParser(SubHierarchicalParser):
"""Parses the CPMD input file.
"""
def __init__(self, file_path, parser_context):
"""
"""
super(CPMDSinglePointParser, self).__init__(file_path, parser_context)
self.setup_common_matcher(CommonMatcher(parser_context))
super(CPMDInputParser, self).__init__(file_path, parser_context)
#=======================================================================
# Cache levels
self.caching_level_for_metaname.update({
'x_cp2k_energy_total_scf_iteration': CachingLevel.ForwardAndCache,
'x_cp2k_energy_XC_scf_iteration': CachingLevel.ForwardAndCache,
'x_cp2k_energy_change_scf_iteration': CachingLevel.ForwardAndCache,
'x_cp2k_stress_tensor': CachingLevel.ForwardAndCache,
'x_cp2k_section_stress_tensor': CachingLevel.ForwardAndCache,
})
# self.caching_level_for_metaname.update({
# 'x_cp2k_energy_total_scf_iteration': CachingLevel.ForwardAndCache,
# })
#=======================================================================
# SimpleMatchers
self.root_matcher = SM("",
forwardMatch=True,
sections=['section_run', "section_single_configuration_calculation", "section_system", "section_method"],
otherMetaInfo=["atom_forces"],
subMatchers=[
self.cm.header(),
self.cm.quickstep_header(),
self.cm.quickstep_calculation(),
]
)
from __future__ import absolute_import
from nomadcore.simple_parser import SimpleMatcher as SM
from nomadcore.baseclasses import MainHierarchicalParser
from nomadcore.unit_conversion.unit_conversion import convert_unit
from nomadcore.caching_backend import CachingLevel
from .commonmatcher import CPMDCommonMatcher
from .commonparser import CPMDCommonParser
from .inputparser import CPMDInputParser
import re
import logging
import datetime
import numpy as np
logger = logging.getLogger("nomad")
......@@ -18,7 +21,7 @@ class CPMDMainParser(MainHierarchicalParser):
"""
"""
super(CPMDMainParser, self).__init__(file_path, parser_context)
self.setup_common_matcher(CPMDCommonMatcher(parser_context))
self.setup_common_matcher(CPMDCommonParser(parser_context))
#=======================================================================
# Cache levels
......@@ -32,22 +35,240 @@ class CPMDMainParser(MainHierarchicalParser):
forwardMatch=True,
sections=['section_run', "section_single_configuration_calculation", "section_system", "section_method"],
subMatchers=[
SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
SM( "\s+VERSION (?P<program_version>\d+\.\d+)"),
SM( " THE INPUT FILE IS:\s+(?P<x_cpmd_input_file>{})".format(self.regexs.regex_eol)),
SM( " THIS JOB RUNS ON:\s+(?P<x_cpmd_run_host_name>{})".format(self.regexs.regex_eol)),
SM( " THE JOB WAS SUBMITTED BY:\s+(?P<x_cpmd_run_user_name>{})".format(self.regexs.regex_eol)),
SM( " PROGRAM CPMD STARTED AT",
forwardMatch=True,
sections=["x_cpmd_section_start_information"],
subMatchers=[
SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
SM( "\s+VERSION (?P<program_version>\d+\.\d+)"),
SM( " THE INPUT FILE IS:\s+(?P<x_cpmd_input_filename>{})".format(self.regexs.regex_eol)),
SM( " THIS JOB RUNS ON:\s+(?P<x_cpmd_run_host_name>{})".format(self.regexs.regex_eol)),
SM( " THE PROCESS ID IS:\s+(?P<x_cpmd_process_id>{})".format(self.regexs.regex_i)),
SM( " THE JOB WAS SUBMITTED BY:\s+(?P<x_cpmd_run_user_name>{})".format(self.regexs.regex_eol)),
]
),
SM( " SINGLE POINT DENSITY OPTIMIZATION",
sections=["x_cpmd_section_run_type_information"],
subMatchers=[
SM( " USING SEED\s+{}\s+TO INIT. PSEUDO RANDOM NUMBER GEN.".format(self.regexs.regex_i)),
SM( " PATH TO THE RESTART FILES:\s+{}".format(self.regexs.regex_eol)),
SM( " GRAM-SCHMIDT ORTHOGONALIZATION"),
SM( " MAXIMUM NUMBER OF STEPS:\s+{} STEPS".format(self.regexs.regex_i)),
SM( " MAXIMUM NUMBER OF ITERATIONS FOR SC:\s+{} STEPS".format(self.regexs.regex_i)),
SM( " PRINT INTERMEDIATE RESULTS EVERY\s+{} STEPS".format(self.regexs.regex_i)),
SM( " STORE INTERMEDIATE RESULTS EVERY\s+{} STEPS".format(self.regexs.regex_i)),
SM( " NUMBER OF DISTINCT RESTART FILES:\s+{}".format(self.regexs.regex_i)),
SM( " TEMPERATURE IS CALCULATED ASSUMING EXTENDED BULK BEHAVIOR"),
SM( " FICTITIOUS ELECTRON MASS:\s+{}".format(self.regexs.regex_f)),
SM( " TIME STEP FOR ELECTRONS:\s+{}".format(self.regexs.regex_f)),
SM( " TIME STEP FOR IONS:\s+{}".format(self.regexs.regex_f)),
SM( " CONVERGENCE CRITERIA FOR WAVEFUNCTION OPTIMIZATION:\s+{}".format(self.regexs.regex_f)),
SM( " WAVEFUNCTION OPTIMIZATION BY PRECONDITIONED DIIS"),
SM( " THRESHOLD FOR THE WF-HESSIAN IS\s+{}".format(self.regexs.regex_f)),
SM( " MAXIMUM NUMBER OF VECTORS RETAINED FOR DIIS:\s+{}".format(self.regexs.regex_i)),
SM( " STEPS UNTIL DIIS RESET ON POOR PROGRESS:\s+{}".format(self.regexs.regex_i)),
SM( " FULL ELECTRONIC GRADIENT IS USED".format(self.regexs.regex_i)),
SM( " SPLINE INTERPOLATION IN G-SPACE FOR PSEUDOPOTENTIAL FUNCTIONS",
subMatchers=[
SM( " NUMBER OF SPLINE POINTS:\s+{}".format(self.regexs.regex_i)),
]
),
]
),
SM( " EXCHANGE CORRELATION FUNCTIONALS",
sections=["x_cpmd_section_xc_information"],
subMatchers=[
# SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
]
),
SM( " ***************************** ATOMS ****************************".replace("*", "\*"),
sections=["x_cpmd_section_system_information"],
subMatchers=[
SM( " NR TYPE X(BOHR) Y(BOHR) Z(BOHR) MBL".replace("(", "\(").replace(")", "\)"),
adHoc=self.ad_hoc_atom_information()
),
SM( " CHARGE:\s+(?P<total_charge>{})".format(self.regexs.regex_i)),
]
),
SM( " \| Pseudopotential Report",
sections=["x_cpmd_section_pseudopotential_information"],
subMatchers=[
# SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
]
),
SM( " ************************** SUPERCELL ***************************".replace("*", "\*"),
sections=["x_cpmd_section_supercell"],
subMatchers=[
SM( " SYMMETRY:\s+(?P<x_cpmd_cell_symmetry>{})".format(self.regexs.regex_eol)),
SM( " LATTICE CONSTANT\(a\.u\.\):\s+(?P<x_cpmd_cell_lattice_constant>{})".format(self.regexs.regex_f)),
SM( " CELL DIMENSION:\s+(?P<x_cpmd_cell_dimension>{})".format(self.regexs.regex_eol)),
SM( " VOLUME\(OMEGA IN BOHR\^3\):\s+(?P<x_cpmd_cell_volume>{})".format(self.regexs.regex_f)),
SM( " LATTICE VECTOR A1\(BOHR\):\s+(?P<x_cpmd_lattice_vector_A1>{})".format(self.regexs.regex_eol)),
SM( " LATTICE VECTOR A2\(BOHR\):\s+(?P<x_cpmd_lattice_vector_A2>{})".format(self.regexs.regex_eol)),
SM( " LATTICE VECTOR A3\(BOHR\):\s+(?P<x_cpmd_lattice_vector_A3>{})".format(self.regexs.regex_eol)),
SM( " RECIP\. LAT\. VEC\. B1\(2Pi/BOHR\):\s+(?P<x_cpmd_reciprocal_lattice_vector_B1>{})".format(self.regexs.regex_eol)),
SM( " RECIP\. LAT\. VEC\. B2\(2Pi/BOHR\):\s+(?P<x_cpmd_reciprocal_lattice_vector_B2>{})".format(self.regexs.regex_eol)),
SM( " RECIP\. LAT\. VEC\. B3\(2Pi/BOHR\):\s+(?P<x_cpmd_reciprocal_lattice_vector_B3>{})".format(self.regexs.regex_eol)),
SM( " RECIP\. LAT\. VEC\. B3\(2Pi/BOHR\):\s+(?P<x_cpmd_reciprocal_lattice_vector_B3>{})".format(self.regexs.regex_eol)),
SM( " REAL SPACE MESH:\s+(?P<x_cpmd_real_space_mesh>{})".format(self.regexs.regex_eol)),
SM( " WAVEFUNCTION CUTOFF\(RYDBERG\):\s+(?P<x_cpmd_wave_function_cutoff>{})".format(self.regexs.regex_f)),
SM( " DENSITY CUTOFF\(RYDBERG\): \(DUAL= 4\.00\)\s+(?P<x_cpmd_density_cutoff>{})".format(self.regexs.regex_f)),
SM( " NUMBER OF PLANE WAVES FOR WAVEFUNCTION CUTOFF:\s+(?P<x_cpmd_number_of_planewaves_wave_function>{})".format(self.regexs.regex_i)),
SM( " NUMBER OF PLANE WAVES FOR DENSITY CUTOFF:\s+(?P<x_cpmd_number_of_planewaves_density>{})".format(self.regexs.regex_i)),
]
),
SM( " GENERATE ATOMIC BASIS SET",
sections=["x_cpmd_section_wave_function_initialization"],
subMatchers=[
# SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
]
),
SM( " NFI GEMAX CNORM ETOT DETOT TCPU",
sections=["x_cpmd_section_scf"],
subMatchers=[
SM( "\s+{0}\s+{1}\s+{1}\s+{1}\s+{1}\s+{1}".format(self.regexs.regex_i, self.regexs.regex_f),
sections=["section_scf_iteration"],
repeats=True,
),
]
),
SM( " * FINAL RESULTS *".replace("*", "\*"),
sections=["x_cpmd_section_final_results"],
subMatchers=[
SM( " ATOM COORDINATES GRADIENTS \(-FORCES\)",
adHoc=self.ad_hoc_atom_forces(),
),
SM( " \(K\+E1\+L\+N\+X\) TOTAL ENERGY =\s+(?P<energy_total__hartree>{}) A\.U\.".format(self.regexs.regex_f)),
SM( " \(E1=A-S\+R\) ELECTROSTATIC ENERGY =\s+(?P<energy_electrostatic__hartree>{}) A\.U\.".format(self.regexs.regex_f)),
SM( " \(X\) EXCHANGE-CORRELATION ENERGY =\s+(?P<energy_XC_potential__hartree>{}) A\.U\.".format(self.regexs.regex_f)),
]
),
SM( " * TIMING *".replace("*", "\*"),
sections=["x_cpmd_section_timing"],
subMatchers=[
]
),
SM( " CPU TIME :",
forwardMatch=True,
sections=["x_cpmd_section_end_information"],
subMatchers=[
# SM( " PROGRAM CPMD STARTED AT: (?P<x_cpmd_start_datetime>{})".format(self.regexs.regex_eol)),
]
)
]
)
#=======================================================================
# onClose triggers
def onClose_section_run(self, backend, gIndex, section):
def onClose_x_cpmd_section_start_information(self, backend, gIndex, section):
# Starting date and time
start_datetime = section.get_latest_value("x_cpmd_start_datetime")
start_date_stamp, start_wall_time = self.timestamp_from_string(start_datetime)
backend.addValue("time_run_date_start", start_date_stamp)
backend.addValue("time_run_wall_start", start_wall_time)
# Input file
input_filename = section.get_latest_value("x_cpmd_input_filename")
input_filepath = self.file_service.set_file_id(input_filename, "input")
if input_filepath is not None:
input_parser = CPMDInputParser(input_filepath, self.parser_context)
input_parser.parse()
else:
logger.warning("The input file for the calculation could not be found.")
def onClose_x_cpmd_section_supercell(self, backend, gIndex, section):
# Simulation cell
A1 = section.get_latest_value("x_cpmd_lattice_vector_A1")
A2 = section.get_latest_value("x_cpmd_lattice_vector_A2")
A3 = section.get_latest_value("x_cpmd_lattice_vector_A3")
A1_array = self.vector_from_string(A1)
A2_array = self.vector_from_string(A2)
A3_array = self.vector_from_string(A3)
cell = np.vstack((A1_array, A2_array, A3_array))
backend.addArrayValues("simulation_cell", cell, unit="bohr")
# Plane wave basis
cutoff = section.get_latest_value("x_cpmd_wave_function_cutoff")
si_cutoff = convert_unit(cutoff, "rydberg")
basis_id = backend.openSection("section_basis_set_cell_dependent")
backend.addValue("basis_set_cell_dependent_kind", "plane_waves")
backend.addValue("basis_set_cell_dependent_name", "PW_{}".format(cutoff))
backend.addValue("basis_set_planewave_cutoff", si_cutoff)
backend.closeSection("section_basis_set_cell_dependent", basis_id)
#=======================================================================
# adHoc
def debug(self):
def wrapper(parser):
print("DEBUG")
return wrapper
def ad_hoc_atom_information(self):
"""Parses the atom labels and coordinates.
"""
def wrapper(parser):
# Define the regex that extracts the information
regex_string = r"\s+({0})\s+({1})\s+({2})\s+({2})\s+({2})\s+({0})".format(self.regexs.regex_i, self.regexs.regex_word, self.regexs.regex_f)
regex_compiled = re.compile(regex_string)
match = True
coordinates = []
labels = []
while match:
line = parser.fIn.readline()
result = regex_compiled.match(line)
if result:
match = True
label = result.groups()[1]
labels.append(label)
coordinate = [float(x) for x in result.groups()[2:5]]
coordinates.append(coordinate)
else:
match = False
coordinates = np.array(coordinates)
labels = np.array(labels)
# If anything found, push the results to the correct section
if len(coordinates) != 0:
parser.backend.addArrayValues("atom_positions", coordinates, unit="bohr")
parser.backend.addArrayValues("atom_labels", labels)
parser.backend.addValue("number_of_atoms", coordinates.shape[0])
return wrapper
def ad_hoc_atom_forces(self):
"""Parses the atomic forces from the final results.
"""
def wrapper(parser):
# Define the regex that extracts the information
regex_string = r"\s+({0})\s+({1})\s+({2})\s+({2})\s+({2})\s+({2})\s+({2})\s+({2})".format(self.regexs.regex_i, self.regexs.regex_word, self.regexs.regex_f)
regex_compiled = re.compile(regex_string)
match = True
forces = []
while match:
line = parser.fIn.readline()
result = regex_compiled.match(line)
if result:
match = True
force = [float(x) for x in result.groups()[5:8]]
forces.append(force)
else:
match = False
forces = -np.array(forces)
# If anything found, push the results to the correct section
if len(forces) != 0:
parser.backend.addArrayValues("atom_forces", forces, unit="hartree/bohr")
return wrapper
#=======================================================================
# misc. functions
def timestamp_from_string(self, timestring):
......@@ -59,9 +280,10 @@ class CPMDMainParser(MainHierarchicalParser):
wall_time = hour*3600+minute*60+second+0.001*msec
return date_stamp, wall_time
#=======================================================================
# adHoc
def debug(self):
def wrapper(parser):
print("DEBUG")
return wrapper
def vector_from_string(self, vectorstr):
"""Returns a numpy array from a string comprising of floating
point numbers.
"""
vectorstr = vectorstr.strip().split()
vec_array = np.array([float(x) for x in vectorstr])
return vec_array
......@@ -40,7 +40,7 @@ object CpmdParser extends SimpleExternalParserGenerator(
"parser-cpmd/cpmdparser/generic/__init__.py",
"parser-cpmd/cpmdparser/versions/__init__.py",
"parser-cpmd/cpmdparser/versions/cpmd41/__init__.py",
"parser-cpmd/cpmdparser/versions/cpmd41/commonmatcher.py",
"parser-cpmd/cpmdparser/versions/cpmd41/commonparser.py",
"parser-cpmd/cpmdparser/versions/cpmd41/mainparser.py",
"parser-cpmd/cpmdparser/versions/cpmd41/inputparser.py",
"nomad_meta_info/public.nomadmetainfo.json",
......
......@@ -31,7 +31,7 @@ logging.basicConfig(
logger = logging.getLogger("nomad")
logger.setLevel(logging.CRITICAL)
logging.getLogger("nomadcore.caching_backend").setLevel(logging.CRITICAL)
logger = logging.getLogger("cp2kparser")
logger = logging.getLogger("cpmdparser")
logger.setLevel(logging.ERROR)
......@@ -89,9 +89,76 @@ class TestSinglePoint(unittest.TestCase):
result = self.results["time_run_wall_start"]
self.assertEqual(result, 50706.851)
# def test_program_basis_set_type(self):
# result = self.results["program_basis_set_type"]
# self.assertEqual(result, "plane waves")
def test_program_basis_set_type(self):
result = self.results["program_basis_set_type"]
self.assertEqual(result, "plane waves")
def test_atom_label(self):
atom_labels = self.results["atom_labels"]
expected_labels = np.array(2*["H"])
self.assertTrue(np.array_equal(atom_labels, expected_labels))
def test_total_charge(self):
charge = self.results["total_charge"]
self.assertEqual(charge, 0)
def test_atom_positions(self):
atom_position = self.results["atom_positions"]
expected_position = convert_unit(np.array(
[
[8.259993, 7.558904, 7.558904],
[6.857816, 7.558904, 7.558904],
]
), "bohr")
self.assertTrue(np.array_equal(atom_position, expected_position))
def test_number_of_atoms(self):
n_atoms = self.results["number_of_atoms"]
self.assertEqual(n_atoms, 2)
def test_simulation_cell(self):
cell = self.results["simulation_cell"]
n_vectors = cell.shape[0]
n_dim = cell.shape[1]
self.assertEqual(n_vectors, 3)
self.assertEqual(n_dim, 3)
expected_cell = convert_unit(np.array([[15.1178, 0, 0], [0, 15.1178, 0], [0, 0, 15.1178]]), "bohr")
self.assertTrue(np.array_equal(cell, expected_cell))
def test_basis_set_cell_dependent(self):
kind = self.results["basis_set_cell_dependent_kind"]
name = self.results["basis_set_cell_dependent_name"]
cutoff = self.results["basis_set_planewave_cutoff"]
self.assertEqual(kind, "plane_waves")
self.assertEqual(name, "PW_70.0")
self.assertEqual(cutoff, convert_unit(70.00000, "rydberg"))
def test_atom_forces(self):
result = self.results["atom_forces"]
expected_result = convert_unit(
-np.array([
[1.780E-02, -1.104E-16, -9.425E-17],
[-1.780E-02, -1.867E-16, -1.490E-16],
]),
"hartree/bohr"
)
self.assertTrue(np.array_equal(result, expected_result))
def test_energy_total(self):
result = self.results["energy_total"]
expected_result = convert_unit(np.array(-1.13245953), "hartree")
self.assertTrue(np.array_equal(result, expected_result))
def test_energy_electrostatic(self):
result = self.results["energy_electrostatic"]
expected_result = convert_unit(np.array(-0.47319176), "hartree")
self.assertTrue(np.array_equal(result, expected_result))
def test_energy_XC_potential(self):
result = self.results["energy_XC_potential"]
expected_result = convert_unit(np.array(-0.65031699), "hartree")
self.assertTrue(np.array_equal(result, expected_result))
# def test_energy_total_scf_iteration(self):
# result = self.results["energy_total_scf_iteration"]
......@@ -128,56 +195,12 @@ class TestSinglePoint(unittest.TestCase):
# expected_result = convert_unit(np.array(-9.4555961214), "hartree")
# self.assertTrue(np.array_equal(result[0], expected_result))
# def test_energy_total(self):
# result = self.results["energy_total"]
# expected_result = convert_unit(np.array(-31.297885372811074), "hartree")
# self.assertTrue(np.array_equal(result, expected_result))
# def test_electronic_kinetic_energy(self):
# result = self.results["electronic_kinetic_energy"]
# expected_result = convert_unit(np.array(13.31525592466419), "hartree")
# self.assertTrue(np.array_equal(result, expected_result))
# def test_atom_forces(self):
# result = self.results["atom_forces"]
# expected_result = convert_unit(
# np.array([
# [0.00000000, 0.00000000, 0.00000000],
# [0.00000000, 0.00000001, 0.00000001],
# [0.00000001, 0.00000001, 0.00000000],
# [0.00000001, 0.00000000, 0.00000001],
# [-0.00000001, -0.00000001, -0.00000001],
# [-0.00000001, -0.00000001, -0.00000001],
# [-0.00000001, -0.00000001, -0.00000001],
# [-0.00000001, -0.00000001, -0.00000001],
# ]),
# "forceAu"
# )
# self.assertTrue(np.array_equal(result, expected_result))
# def test_atom_label(self):
# atom_labels = self.results["atom_labels"]
# expected_labels = np.array(8*["Si1"])
# self.assertTrue(np.array_equal(atom_labels, expected_labels))
# def test_simulation_cell(self):
# cell = self.results["simulation_cell"]
# n_vectors = cell.shape[0]
# n_dim = cell.shape[1]
# self.assertEqual(n_vectors, 3)
# self.assertEqual(n_dim, 3)
# expected_cell = convert_unit(np.array([[5.431, 0, 0], [0, 5.431, 0], [0, 0, 5.431]]), "angstrom")
# self.assertTrue(np.array_equal(cell, expected_cell))
# def test_number_of_atoms(self):
# n_atoms = self.results["number_of_atoms"]
# self.assertEqual(n_atoms, 8)
# def test_atom_position(self):
# atom_position = self.results["atom_positions"]
# expected_position = convert_unit(np.array([4.073023, 4.073023, 1.357674]), "angstrom")
# self.assertTrue(np.array_equal(atom_position[-1, :], expected_position))
# def test_x_cp2k_filenames(self):
# input_filename = self.results["x_cp2k_input_filename"]
# expected_input = "si_bulk8.inp"
......@@ -207,9 +230,6 @@ class TestSinglePoint(unittest.TestCase):
# multiplicity = self.results["spin_target_multiplicity"]
# self.assertEqual(multiplicity, 1)
# def test_total_charge(self):
# charge = self.results["total_charge"]
# self.assertEqual(charge, 0)
# def test_section_basis_set_atom_centered(self):
# basis = self.results["section_basis_set_atom_centered"][0]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment