From 5c396c17f63039891927a13a551d76b7ca69bf8e Mon Sep 17 00:00:00 2001 From: bastonero Date: Tue, 18 Jun 2024 17:07:40 +0000 Subject: [PATCH 1/3] PhParser: allow for pattern initialization The ph.x should be parallelized by setting in the input parameters `start_irr` and `last_irr` to 0. This allows the program to exit smoothly and it further avoids to wait for the rewriting of the wavefunctions, which can be a rather long and intensive IO operation, not really suited for initialization runs. The parser is then adjusted to account for this option, as for some reason the line having `JOB DONE` is not printed in such cases. A simple specialized parser is also added to store the number of q-points and their values, which can be later on used to parallelize over q-points by specifying `last_q` and `start_q`. --- .../parsers/parse_raw/ph.py | 34 ++++++++++ src/aiida_quantumespresso/parsers/ph.py | 23 ++++++- .../fixtures/ph/initialization/aiida.out | 67 +++++++++++++++++++ tests/parsers/test_ph.py | 20 +++++- .../test_ph/test_ph_initialization.yml | 26 +++++++ tests/workflows/ph/test_base.py | 10 ++- 6 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 tests/parsers/fixtures/ph/initialization/aiida.out create mode 100644 tests/parsers/test_ph/test_ph_initialization.yml diff --git a/src/aiida_quantumespresso/parsers/parse_raw/ph.py b/src/aiida_quantumespresso/parsers/parse_raw/ph.py index 91cb377bf..be1662eaf 100644 --- a/src/aiida_quantumespresso/parsers/parse_raw/ph.py +++ b/src/aiida_quantumespresso/parsers/parse_raw/ph.py @@ -438,3 +438,37 @@ def parse_ph_dynmat(data, logs, lattice_parameter=None, also_eigenvectors=False, parsed_data['eigenvectors'] = eigenvectors return parsed_data + + +def parse_initialization_qpoints(stdout: str) -> dict: + """Return the number of q-points from an initialization run. + + Here, the initialization run refers to the one performed by specifying + `start_irr` and `last_irr` to 0 in the inputs. + """ + import re + + parameters = {} + + # Regular expression to match `N` in `( N q-points)` + pattern = r'\(\s*(\d+)\s*q-points\)' + match = re.search(pattern, stdout) + if match: + parameters.update({'number_of_qpoints': int(match.group(1))}) + + # Regular expression pattern to match the q-points section + pattern = r'\(\s*\d+\s*q-points\):\s*\n\s*N\s*xq\(1\)\s*xq\(2\)\s*xq\(3\)\s*\n((?:\s*\d+\s*[\d\.\-\s]+\n?)*)' + match = re.search(pattern, stdout) + + if match: + q_points_block = match.group(1) + + # Regular expression to match each line of coordinates + coord_pattern = r'\s*\d+\s*([\d\.\-]+)\s*([\d\.\-]+)\s*([\d\.\-]+)' + + coords = re.findall(coord_pattern, q_points_block) # Find all coordinates in the block + q_points = [list(map(float, coord)) for coord in coords] + + parameters.update({'q_points': q_points}) + + return parameters diff --git a/src/aiida_quantumespresso/parsers/ph.py b/src/aiida_quantumespresso/parsers/ph.py index 636b3695b..b34f08f2a 100644 --- a/src/aiida_quantumespresso/parsers/ph.py +++ b/src/aiida_quantumespresso/parsers/ph.py @@ -6,12 +6,25 @@ from aiida import orm from aiida_quantumespresso.calculations.ph import PhCalculation -from aiida_quantumespresso.parsers.parse_raw.ph import parse_raw_ph_output +from aiida_quantumespresso.parsers.parse_raw.ph import parse_initialization_qpoints, parse_raw_ph_output from aiida_quantumespresso.utils.mapping import get_logging_container from .base import BaseParser +def _is_initialization(parameters: dict) -> bool: + """Return whether the `ph.x` was run with (patterns) initialization options. + + When `ph.x` is used with `start_irr` and `last_irr` set to 0, the binary doesn't + produce the usual `JOB DONE` statement, and immediately exits the job. This is + used to quickly generate the displacement patterns needed for a correct parallelization + of the code over both q-points and irreducible representations (irreps). + """ + if 'start_irr' in parameters['INPUTPH'] and 'last_irr' in parameters['INPUTPH']: + return parameters['INPUTPH']['start_irr'] == parameters['INPUTPH']['last_irr'] == 0 + return False + + class PhParser(BaseParser): """``Parser`` implementation for the ``PhCalculation`` calculation job class.""" @@ -28,6 +41,14 @@ def parse(self, **kwargs): stdout, parsed_data, logs = self.parse_stdout_from_retrieved(logs) + # When `start_irr` and `last_irr` are set to 0, `JOB DONE` is not in stdout (expected behaviour). + # Though, we at least expect that `stdout` is not empty, otherwise something went wrong. + if stdout and _is_initialization(self.node.inputs.parameters.get_dict()): + parameters = parse_initialization_qpoints(stdout) + if parameters: + self.out('output_parameters', orm.Dict(parameters)) + return + # If the scheduler detected OOW, simply keep that exit code by not returning anything more specific. if self.node.exit_status == PhCalculation.exit_codes.ERROR_SCHEDULER_OUT_OF_WALLTIME: return diff --git a/tests/parsers/fixtures/ph/initialization/aiida.out b/tests/parsers/fixtures/ph/initialization/aiida.out new file mode 100644 index 000000000..620b83ccb --- /dev/null +++ b/tests/parsers/fixtures/ph/initialization/aiida.out @@ -0,0 +1,67 @@ + + Program PHONON v.7.2 starts on 18Jun2024 at 18:33:27 + + This program is part of the open-source Quantum ESPRESSO suite + for quantum simulation of materials; please cite + "P. Giannozzi et al., J. Phys.:Condens. Matter 21 395502 (2009); + "P. Giannozzi et al., J. Phys.:Condens. Matter 29 465901 (2017); + "P. Giannozzi et al., J. Chem. Phys. 152 154105 (2020); + URL http://www.quantum-espresso.org", + in publications or presentations arising from this work. More details at + http://www.quantum-espresso.org/quote + + Parallel version (MPI), running on 8 processors + + MPI processes distributed on 1 nodes + 37704 MiB available memory on the printing compute node when the environment starts + + Reading input from ph.init.in + + Reading xml data from directory: + + ./tmp/graphene.save/ + + R & G space division: proc/nbgrp/npool/nimage = 8 + Subspace diagonalization in iterative solution of the eigenvalue problem: + a serial algorithm will be used + + + IMPORTANT: XC functional enforced from input : + Exchange-correlation= PZ + ( 1 1 0 0 0 0 0) + Any further DFT definition will be discarded + Please, verify this is what you really want + + + Parallelization info + -------------------- + sticks: dense smooth PW G-vecs: dense smooth PW + Min 30 30 10 3040 3040 660 + Max 31 31 11 3065 3065 691 + Sum 241 241 85 24369 24369 5409 + + Using Slab Decomposition + + ----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D + The code is running with the 2D cutoff + Please refer to: + Sohier, T., Calandra, M., & Mauri, F. (2017), + Density functional perturbation theory for gated two-dimensional heterostructu + res: + Theoretical developments and application to flexural phonons in graphene. + Physical Review B, 96(7), 75448. https://doi.org/10.1103/PhysRevB.96.075448 + ----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D + Reading collected, re-writing distributed wavefunctions in ./tmp/ + + + Dynamical matrices for ( 2, 2, 2) uniform grid of q-points + ( 8 q-points): + N xq(1) xq(2) xq(3) + 1 0.000000000 0.000000000 0.000000000 + 2 0.000000000 0.000000000 -0.061660223 + 3 0.000000000 -0.577350269 0.000000000 + 4 0.000000000 -0.577350269 -0.061660223 + 5 -0.500000000 -0.288675135 0.000000000 + 6 -0.500000000 -0.288675135 -0.061660223 + 7 -0.500000000 -0.866025404 0.000000000 + 8 -0.500000000 -0.866025404 -0.061660223 diff --git a/tests/parsers/test_ph.py b/tests/parsers/test_ph.py index 49d4a9a40..54809e739 100644 --- a/tests/parsers/test_ph.py +++ b/tests/parsers/test_ph.py @@ -6,7 +6,7 @@ def generate_inputs(): """Return only those inputs that the parser will expect to be there.""" - return {} + return {'parameters': orm.Dict({'INPUTPH': {}})} @pytest.mark.parametrize('test_name', ['default', 'single_qpoint', 'no_modes_printed']) @@ -61,6 +61,24 @@ def test_ph_out_of_walltime(fixture_localhost, generate_calc_job_node, generate_ data_regression.check(results['output_parameters'].get_dict()) +def test_ph_initialization(fixture_localhost, generate_calc_job_node, generate_parser, data_regression): + """Test a `ph.x` calculation performed with `start_irr` and `last_irr` set to 0.""" + name = 'initialization' + entry_point_calc_job = 'quantumespresso.ph' + entry_point_parser = 'quantumespresso.ph' + + inputs = {'parameters': orm.Dict({'INPUTPH': {'start_irr': 0, 'last_irr': 0}})} + + node = generate_calc_job_node(entry_point_calc_job, fixture_localhost, name, inputs) + parser = generate_parser(entry_point_parser) + results, calcfunction = parser.parse_from_node(node, store_provenance=False) + + assert calcfunction.is_finished, calcfunction.exception + assert calcfunction.is_finished_ok, calcfunction.exit_message + assert 'output_parameters' in results + data_regression.check(results['output_parameters'].get_dict()) + + def test_ph_failed_computing_cholesky(fixture_localhost, generate_calc_job_node, generate_parser): """Test the parsing of a calculation that failed during cholesky factorization. diff --git a/tests/parsers/test_ph/test_ph_initialization.yml b/tests/parsers/test_ph/test_ph_initialization.yml new file mode 100644 index 000000000..5a62dd333 --- /dev/null +++ b/tests/parsers/test_ph/test_ph_initialization.yml @@ -0,0 +1,26 @@ +number_of_qpoints: 8 +q_points: +- - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - -0.061660223 +- - 0.0 + - -0.577350269 + - 0.0 +- - 0.0 + - -0.577350269 + - -0.061660223 +- - -0.5 + - -0.288675135 + - 0.0 +- - -0.5 + - -0.288675135 + - -0.061660223 +- - -0.5 + - -0.866025404 + - 0.0 +- - -0.5 + - -0.866025404 + - -0.061660223 diff --git a/tests/workflows/ph/test_base.py b/tests/workflows/ph/test_base.py index 8a23d04f0..8b6fcb101 100644 --- a/tests/workflows/ph/test_base.py +++ b/tests/workflows/ph/test_base.py @@ -10,6 +10,11 @@ from aiida_quantumespresso.workflows.ph.base import PhBaseWorkChain +def generate_inputs(): + """Return only those inputs that the parser will expect to be there.""" + return {'parameters': orm.Dict({'INPUTPH': {}})} + + @pytest.fixture def generate_ph_calc_job_node(generate_calc_job_node, fixture_localhost): """Generate a ``CalcJobNode`` that would have been created by a ``PhCalculation``.""" @@ -221,9 +226,10 @@ def test_merge_outputs( entry_point_calc_job = 'quantumespresso.ph' parser = generate_parser('quantumespresso.ph') + inputs = generate_inputs() node_1 = generate_calc_job_node( - entry_point_name=entry_point_calc_job, computer=fixture_localhost, test_name=f'{name}_1' + entry_point_name=entry_point_calc_job, computer=fixture_localhost, test_name=f'{name}_1', inputs=inputs ) results_1, calcjob_1 = parser.parse_from_node(node_1, store_provenance=False) @@ -236,7 +242,7 @@ def test_merge_outputs( assert calcjob_1.exit_status == PhCalculation.exit_codes.ERROR_OUT_OF_WALLTIME.status node_2 = generate_calc_job_node( - entry_point_name=entry_point_calc_job, computer=fixture_localhost, test_name=f'{name}_2' + entry_point_name=entry_point_calc_job, computer=fixture_localhost, test_name=f'{name}_2', inputs=inputs ) results_2, calcjob_2 = parser.parse_from_node(node_2, store_provenance=False) From f72e4b80b565956699e2caae51c370f4a0109e80 Mon Sep 17 00:00:00 2001 From: bastonero Date: Thu, 20 Jun 2024 22:47:00 +0000 Subject: [PATCH 2/3] Address review --- .../parsers/parse_raw/ph.py | 17 ++++- src/aiida_quantumespresso/parsers/ph.py | 5 +- .../ph/initialization_failed/aiida.out | 66 +++++++++++++++++++ tests/parsers/test_ph.py | 17 +++++ 4 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/parsers/fixtures/ph/initialization_failed/aiida.out diff --git a/src/aiida_quantumespresso/parsers/parse_raw/ph.py b/src/aiida_quantumespresso/parsers/parse_raw/ph.py index be1662eaf..636e17d6d 100644 --- a/src/aiida_quantumespresso/parsers/parse_raw/ph.py +++ b/src/aiida_quantumespresso/parsers/parse_raw/ph.py @@ -4,6 +4,8 @@ The function that needs to be called from outside is parse_raw_ph_output(). Ideally, the functions should work even without aiida and will return a dictionary with parsed keys. """ +from __future__ import annotations + import numpy from qe_tools import CONSTANTS @@ -440,11 +442,16 @@ def parse_ph_dynmat(data, logs, lattice_parameter=None, also_eigenvectors=False, return parsed_data -def parse_initialization_qpoints(stdout: str) -> dict: +def parse_initialization_qpoints(stdout: str) -> tuple[dict, bool]: """Return the number of q-points from an initialization run. Here, the initialization run refers to the one performed by specifying `start_irr` and `last_irr` to 0 in the inputs. + + :return: (parsed dictionary, error found), the last is a boolean + regarding whether an expected quantity has not been properly + parse; it also checks that the number of q-points parsed within + parenthesis and the amount of q-points parsed from coordinaes coincide. """ import re @@ -471,4 +478,10 @@ def parse_initialization_qpoints(stdout: str) -> dict: parameters.update({'q_points': q_points}) - return parameters + if 'number_of_qpoints' not in parameters and 'q_points' not in parameters: + return parameters, False + + if parameters['number_of_qpoints'] != len(parameters['q_points']): + return parameters, False + + return parameters, True diff --git a/src/aiida_quantumespresso/parsers/ph.py b/src/aiida_quantumespresso/parsers/ph.py index b34f08f2a..abe6d6b3e 100644 --- a/src/aiida_quantumespresso/parsers/ph.py +++ b/src/aiida_quantumespresso/parsers/ph.py @@ -44,10 +44,11 @@ def parse(self, **kwargs): # When `start_irr` and `last_irr` are set to 0, `JOB DONE` is not in stdout (expected behaviour). # Though, we at least expect that `stdout` is not empty, otherwise something went wrong. if stdout and _is_initialization(self.node.inputs.parameters.get_dict()): - parameters = parse_initialization_qpoints(stdout) - if parameters: + parameters, parsed_ok = parse_initialization_qpoints(stdout) + if parsed_ok: self.out('output_parameters', orm.Dict(parameters)) return + # we let it go, as it should exit with STDOUT_INCOMPLETE or similar # If the scheduler detected OOW, simply keep that exit code by not returning anything more specific. if self.node.exit_status == PhCalculation.exit_codes.ERROR_SCHEDULER_OUT_OF_WALLTIME: diff --git a/tests/parsers/fixtures/ph/initialization_failed/aiida.out b/tests/parsers/fixtures/ph/initialization_failed/aiida.out new file mode 100644 index 000000000..17cb066b0 --- /dev/null +++ b/tests/parsers/fixtures/ph/initialization_failed/aiida.out @@ -0,0 +1,66 @@ + + Program PHONON v.7.2 starts on 18Jun2024 at 18:33:27 + + This program is part of the open-source Quantum ESPRESSO suite + for quantum simulation of materials; please cite + "P. Giannozzi et al., J. Phys.:Condens. Matter 21 395502 (2009); + "P. Giannozzi et al., J. Phys.:Condens. Matter 29 465901 (2017); + "P. Giannozzi et al., J. Chem. Phys. 152 154105 (2020); + URL http://www.quantum-espresso.org", + in publications or presentations arising from this work. More details at + http://www.quantum-espresso.org/quote + + Parallel version (MPI), running on 8 processors + + MPI processes distributed on 1 nodes + 37704 MiB available memory on the printing compute node when the environment starts + + Reading input from ph.init.in + + Reading xml data from directory: + + ./tmp/graphene.save/ + + R & G space division: proc/nbgrp/npool/nimage = 8 + Subspace diagonalization in iterative solution of the eigenvalue problem: + a serial algorithm will be used + + + IMPORTANT: XC functional enforced from input : + Exchange-correlation= PZ + ( 1 1 0 0 0 0 0) + Any further DFT definition will be discarded + Please, verify this is what you really want + + + Parallelization info + -------------------- + sticks: dense smooth PW G-vecs: dense smooth PW + Min 30 30 10 3040 3040 660 + Max 31 31 11 3065 3065 691 + Sum 241 241 85 24369 24369 5409 + + Using Slab Decomposition + + ----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D + The code is running with the 2D cutoff + Please refer to: + Sohier, T., Calandra, M., & Mauri, F. (2017), + Density functional perturbation theory for gated two-dimensional heterostructu + res: + Theoretical developments and application to flexural phonons in graphene. + Physical Review B, 96(7), 75448. https://doi.org/10.1103/PhysRevB.96.075448 + ----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D----2D + Reading collected, re-writing distributed wavefunctions in ./tmp/ + + + Dynamical matrices for ( 2, 2, 2) uniform grid of q-points + ( 8 q-points): + N xq(1) xq(2) xq(3) + 1 0.000000000 0.000000000 0.000000000 + 2 0.000000000 0.000000000 -0.061660223 + 3 0.000000000 -0.577350269 0.000000000 + 4 0.000000000 -0.577350269 -0.061660223 + 5 -0.500000000 -0.288675135 0.000000000 + 6 -0.500000000 -0.288675135 -0.061660223 + 7 -0.500000000 -0.866025404 0.000000000 diff --git a/tests/parsers/test_ph.py b/tests/parsers/test_ph.py index 54809e739..70023bd39 100644 --- a/tests/parsers/test_ph.py +++ b/tests/parsers/test_ph.py @@ -79,6 +79,23 @@ def test_ph_initialization(fixture_localhost, generate_calc_job_node, generate_p data_regression.check(results['output_parameters'].get_dict()) +def test_ph_initialization_failed(fixture_localhost, generate_calc_job_node, generate_parser): + """Test a failed `ph.x` calculation performed with `start_irr` and `last_irr` set to 0.""" + name = 'initialization_failed' + entry_point_calc_job = 'quantumespresso.ph' + entry_point_parser = 'quantumespresso.ph' + + inputs = {'parameters': orm.Dict({'INPUTPH': {'start_irr': 0, 'last_irr': 0}})} + + node = generate_calc_job_node(entry_point_calc_job, fixture_localhost, name, inputs) + parser = generate_parser(entry_point_parser) + _, calcfunction = parser.parse_from_node(node, store_provenance=False) + + assert calcfunction.is_finished, calcfunction.exception + assert calcfunction.is_failed, calcfunction.exit_status + assert calcfunction.exit_status == node.process_class.exit_codes.ERROR_OUTPUT_STDOUT_INCOMPLETE.status + + def test_ph_failed_computing_cholesky(fixture_localhost, generate_calc_job_node, generate_parser): """Test the parsing of a calculation that failed during cholesky factorization. From 1f554939c373378ee4d1edf0fc8dc05a480e4ae1 Mon Sep 17 00:00:00 2001 From: bastonero Date: Wed, 26 Jun 2024 22:06:27 +0000 Subject: [PATCH 3/3] Address review 2 --- .../parsers/parse_raw/ph.py | 25 +++++++++---------- src/aiida_quantumespresso/parsers/ph.py | 7 +++--- .../test_ph/test_ph_initialization.yml | 25 ------------------- 3 files changed, 16 insertions(+), 41 deletions(-) diff --git a/src/aiida_quantumespresso/parsers/parse_raw/ph.py b/src/aiida_quantumespresso/parsers/parse_raw/ph.py index 636e17d6d..2dc2d5ddb 100644 --- a/src/aiida_quantumespresso/parsers/parse_raw/ph.py +++ b/src/aiida_quantumespresso/parsers/parse_raw/ph.py @@ -442,16 +442,16 @@ def parse_ph_dynmat(data, logs, lattice_parameter=None, also_eigenvectors=False, return parsed_data -def parse_initialization_qpoints(stdout: str) -> tuple[dict, bool]: +def parse_initialization_qpoints(stdout: str) -> dict: """Return the number of q-points from an initialization run. Here, the initialization run refers to the one performed by specifying `start_irr` and `last_irr` to 0 in the inputs. - :return: (parsed dictionary, error found), the last is a boolean - regarding whether an expected quantity has not been properly - parse; it also checks that the number of q-points parsed within - parenthesis and the amount of q-points parsed from coordinaes coincide. + :return: parsed dictionary + + :raise: `RuntimeError` if the number of q-points cannot be parsed or it + differs from the number of q-points in the stdout list. """ import re @@ -462,6 +462,8 @@ def parse_initialization_qpoints(stdout: str) -> tuple[dict, bool]: match = re.search(pattern, stdout) if match: parameters.update({'number_of_qpoints': int(match.group(1))}) + else: + raise RuntimeError('the number of q-points cannot be parsed') # Regular expression pattern to match the q-points section pattern = r'\(\s*\d+\s*q-points\):\s*\n\s*N\s*xq\(1\)\s*xq\(2\)\s*xq\(3\)\s*\n((?:\s*\d+\s*[\d\.\-\s]+\n?)*)' @@ -475,13 +477,10 @@ def parse_initialization_qpoints(stdout: str) -> tuple[dict, bool]: coords = re.findall(coord_pattern, q_points_block) # Find all coordinates in the block q_points = [list(map(float, coord)) for coord in coords] + else: + raise RuntimeError('the list of q-points cannot be parsed') - parameters.update({'q_points': q_points}) - - if 'number_of_qpoints' not in parameters and 'q_points' not in parameters: - return parameters, False - - if parameters['number_of_qpoints'] != len(parameters['q_points']): - return parameters, False + if parameters['number_of_qpoints'] != len(q_points): + raise RuntimeError('the number of q-points do not coincde with the number of listed q-points') - return parameters, True + return parameters diff --git a/src/aiida_quantumespresso/parsers/ph.py b/src/aiida_quantumespresso/parsers/ph.py index abe6d6b3e..5b8fc3d25 100644 --- a/src/aiida_quantumespresso/parsers/ph.py +++ b/src/aiida_quantumespresso/parsers/ph.py @@ -44,11 +44,12 @@ def parse(self, **kwargs): # When `start_irr` and `last_irr` are set to 0, `JOB DONE` is not in stdout (expected behaviour). # Though, we at least expect that `stdout` is not empty, otherwise something went wrong. if stdout and _is_initialization(self.node.inputs.parameters.get_dict()): - parameters, parsed_ok = parse_initialization_qpoints(stdout) - if parsed_ok: + try: + parameters = parse_initialization_qpoints(stdout) self.out('output_parameters', orm.Dict(parameters)) return - # we let it go, as it should exit with STDOUT_INCOMPLETE or similar + except RuntimeError as exc: + logs.error.append('ERROR_OUTPUT_STDOUT_INCOMPLETE') # If the scheduler detected OOW, simply keep that exit code by not returning anything more specific. if self.node.exit_status == PhCalculation.exit_codes.ERROR_SCHEDULER_OUT_OF_WALLTIME: diff --git a/tests/parsers/test_ph/test_ph_initialization.yml b/tests/parsers/test_ph/test_ph_initialization.yml index 5a62dd333..1945c22a3 100644 --- a/tests/parsers/test_ph/test_ph_initialization.yml +++ b/tests/parsers/test_ph/test_ph_initialization.yml @@ -1,26 +1 @@ number_of_qpoints: 8 -q_points: -- - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - -0.061660223 -- - 0.0 - - -0.577350269 - - 0.0 -- - 0.0 - - -0.577350269 - - -0.061660223 -- - -0.5 - - -0.288675135 - - 0.0 -- - -0.5 - - -0.288675135 - - -0.061660223 -- - -0.5 - - -0.866025404 - - 0.0 -- - -0.5 - - -0.866025404 - - -0.061660223