From 234ec1524df2b95bd635c40554ca6eb87c5d602b Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Wed, 11 Mar 2026 14:38:52 +0100 Subject: [PATCH 1/6] Fix ADF15 parser Update regex patterns to handle other raw files (e.g. pec40#w_ic#w0.dat) --- cherab/openadas/parse/adf15.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index 12aa01a9..a1b60219 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -38,6 +38,13 @@ 11: 'O', 12: 'Q', 13: 'R', + 14: 'T', + 15: 'U', + 16: 'V', + 17: 'W', + 18: 'X', + 19: 'Y', + 20: 'Z', } @@ -120,7 +127,7 @@ def _scrape_metadata_hydrogen(file, element, charge): wavelength = float(match.groups()[1]) / 10 # convert Angstroms to nm upper_level = int(match.groups()[2]) lower_level = int(match.groups()[3]) - rate_type_adas = match.groups()[4] + rate_type_adas = match.groups()[4].upper() if rate_type_adas == 'EXCIT': rate_type = 'excitation' elif rate_type_adas == 'RECOM': @@ -147,14 +154,14 @@ def _scrape_metadata_hydrogen_like(file, element, charge): file.seek(0) lines = file.readlines() - pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' + pec_index_header_match = r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE' while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): lines.pop(0) index_lines = lines for i in range(len(index_lines)): - pec_full_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' + pec_full_transition_match = r'^C\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -163,7 +170,7 @@ def _scrape_metadata_hydrogen_like(file, element, charge): wavelength = float(match.groups()[1]) / 10 # convert Angstroms to nm upper_level = int(match.groups()[2]) lower_level = int(match.groups()[3]) - rate_type_adas = match.groups()[4] + rate_type_adas = match.groups()[4].upper() if rate_type_adas == 'EXCIT': rate_type = 'excitation' elif rate_type_adas == 'RECOM': @@ -193,10 +200,10 @@ def _scrape_metadata_full(file, element, charge): configuration_lines = [] configuration_dict = {} - configuration_header_match = r'^C\s*Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy \(cm\*\*-1\)$' + configuration_header_match = r'^C\s*(?:lv\s+)?Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy\s*\(cm(?:\*\*|\^)-1\)\s*$' while not re.match(configuration_header_match, lines[0], re.IGNORECASE): lines.pop(0) - pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' + pec_index_header_match = r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE' while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): configuration_lines.append(lines[0]) lines.pop(0) @@ -204,7 +211,7 @@ def _scrape_metadata_full(file, element, charge): for i in range(len(configuration_lines)): - configuration_string_match = r"^C\s*([0-9]*)\s*((?:[0-9][SPDFG][0-9]\s)*)\s*\(([0-9]*\.?[0-9]*)\)([0-9]*)\(\s*([0-9]*\.?[0-9]*)\)" + configuration_string_match = r'^[Cc]\s*([0-9]+)\s+(\S+)\s+\(([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+)\(\s*([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+(?:\.[0-9]+)?)?\s*$' match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) if not match: continue @@ -231,7 +238,7 @@ def _scrape_metadata_full(file, element, charge): upper_level = configuration_dict[upper_level_id] lower_level_id = int(match.groups()[3]) lower_level = configuration_dict[lower_level_id] - rate_type_adas = match.groups()[4] + rate_type_adas = match.groups()[4].upper() if rate_type_adas == 'EXCIT': rate_type = 'excitation' elif rate_type_adas == 'RECOM': From b2cdca5e0e5fb79cf2ff81d32560b3949aca863c Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Wed, 11 Mar 2026 14:52:57 +0100 Subject: [PATCH 2/6] Fix regressions for config regex pattern in _scrape_metadata_full --- cherab/openadas/parse/adf15.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index a1b60219..12067a28 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -211,7 +211,11 @@ def _scrape_metadata_full(file, element, charge): for i in range(len(configuration_lines)): - configuration_string_match = r'^[Cc]\s*([0-9]+)\s+(\S+)\s+\(([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+)\(\s*([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+(?:\.[0-9]+)?)?\s*$' + configuration_string_match = ( + r'^[Cc]\s*([0-9]+)\s+([0-9A-Za-z#]+(?:\s+[0-9A-Za-z#]+)*)\s+' + r'\(([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+)\(\s*([0-9]+(?:\.[0-9]+)?)\)' + r'\s*(?:[0-9]+(?:\.[0-9]+)?)?\s*$' + ) match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) if not match: continue From 6157a17462786aa060ebac69af1ece9e98423125 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Sun, 22 Mar 2026 00:48:19 +0100 Subject: [PATCH 3/6] Move regex pattern definition out of for-loop --- cherab/openadas/parse/adf15.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index 12067a28..2a864592 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -116,9 +116,8 @@ def _scrape_metadata_hydrogen(file, element, charge): lines.pop(0) index_lines = lines + pec_hydrogen_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)' for i in range(len(index_lines)): - - pec_hydrogen_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)' match = re.match(pec_hydrogen_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -159,9 +158,8 @@ def _scrape_metadata_hydrogen_like(file, element, charge): lines.pop(0) index_lines = lines + pec_full_transition_match = r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' for i in range(len(index_lines)): - - pec_full_transition_match = r'^C\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -209,13 +207,8 @@ def _scrape_metadata_full(file, element, charge): lines.pop(0) index_lines = lines + configuration_string_match = r'^[cC]\s*([0-9]*)\s*((?:[0-9][SPDFG][0-9]\s)*)\s*\(([0-9]*\.?[0-9]*)\)([0-9]*)\(\s*([0-9]*\.?[0-9]*)\)' for i in range(len(configuration_lines)): - - configuration_string_match = ( - r'^[Cc]\s*([0-9]+)\s+([0-9A-Za-z#]+(?:\s+[0-9A-Za-z#]+)*)\s+' - r'\(([0-9]+(?:\.[0-9]+)?)\)\s*([0-9]+)\(\s*([0-9]+(?:\.[0-9]+)?)\)' - r'\s*(?:[0-9]+(?:\.[0-9]+)?)?\s*$' - ) match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) if not match: continue @@ -229,9 +222,8 @@ def _scrape_metadata_full(file, element, charge): configuration_dict[config_id] = (electron_configuration + " " + spin_multiplicity + total_orbital_quantum_number + total_angular_momentum_quantum_number) + pec_full_transition_match = r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' for i in range(len(index_lines)): - - pec_full_transition_match = r'^C\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) if not match: continue From 04ddbd77c5e8d52429d9198a9a42a220f7e97af9 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Sun, 22 Mar 2026 01:43:45 +0100 Subject: [PATCH 4/6] Refine regex pattern for configuration string in _scrape_metadata_full --- cherab/openadas/parse/adf15.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index 2a864592..6ecd424c 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -207,7 +207,13 @@ def _scrape_metadata_full(file, element, charge): lines.pop(0) index_lines = lines - configuration_string_match = r'^[cC]\s*([0-9]*)\s*((?:[0-9][SPDFG][0-9]\s)*)\s*\(([0-9]*\.?[0-9]*)\)([0-9]*)\(\s*([0-9]*\.?[0-9]*)\)' + configuration_string_match = ( + r'^[cC]\s*([0-9]+)\s*' + r'((?:[0-9][SPDFG][0-9](?:\s+[0-9][SPDFG][0-9])*)|(?:[0-9A-Z]+))\s*' + r'\(([0-9]*\.?[0-9]+)\)' + r'\s*([0-9]+)' + r'\(\s*([0-9]*\.?[0-9]+)\)' + ) for i in range(len(configuration_lines)): match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) if not match: From 45100343dfd2e3ec4c58ac4c16015888df6352ca Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 18 May 2026 12:17:41 +0200 Subject: [PATCH 5/6] Add unit tests for ADF15 parser with mock data for hydrogen, carbon, and tungsten formats --- cherab/openadas/parse/adf15.py | 72 ++++--- cherab/openadas/tests/test_adf15.py | 296 ++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 38 deletions(-) create mode 100644 cherab/openadas/tests/test_adf15.py diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index 6ecd424c..3d4acd62 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -17,11 +17,29 @@ # under the Licence. import re + import numpy as np -from cherab.core.atomic import hydrogen, Element + +from cherab.core.atomic import Element, hydrogen from cherab.core.utility import RecursiveDict from cherab.core.utility.conversion import Cm3ToM3, PerCm3ToPerM3 +# Compiled regex patterns for ADF15 file parsing +_ADF_HEADER_MATCH = re.compile(r'^\s*(\d*) {4}/(.*)/?\s*$') +_PEC_INDEX_HEADER_MATCH_STANDARD = re.compile(r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE', re.IGNORECASE) +_PEC_HYDROGEN_TRANSITION_MATCH = re.compile(r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)', re.IGNORECASE) +_PEC_FULL_TRANSITION_MATCH = re.compile(r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)', re.IGNORECASE) +_CONFIGURATION_HEADER_MATCH = re.compile(r'^C\s*(?:lv\s+)?Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy\s*\(cm(?:\*\*|\^)-1\)\s*$', re.IGNORECASE) +_CONFIGURATION_STRING_MATCH = re.compile( + r'^[cC]\s*([0-9]+)\s*' + r'((?:[0-9][SPDFG][0-9](?:\s+[0-9][SPDFG][0-9])*)|(?:[0-9A-Z]+))\s*' + r'\(([0-9]*\.?[0-9]+)\)' + r'\s*([0-9]+)' + r'\(\s*([0-9]*\.?[0-9]+)\)', + re.IGNORECASE, +) +_WAVELENGTH_MATCH = re.compile(r"^\s*[0-9]*\.[0-9]* ?a?\s+[0-9]+\s+[0-9]+.*?/isel *= *[0-9]+$", re.IGNORECASE) +_BLOCK_ID_MATCH = re.compile(r"^\s*[0-9]*\.[0-9]* ?a?\s*([0-9]*)\s*([0-9]*).*/type *= *([a-zA-Z]*).*/isel *= * ([0-9]*)$", re.IGNORECASE) _L_LOOKUP = { 0: 'S', @@ -64,10 +82,9 @@ def parse_adf15(element, charge, adf_file_path, header_format=None): charge = int(charge) with open(adf_file_path, "r") as file: - # for check header line header = file.readline() - if not re.match(r'^\s*(\d*) {4}/(.*)/?\s*$', header): + if not _ADF_HEADER_MATCH.match(header): raise ValueError('The specified path does not point to a valid ADF15 file.') # scrape transition information and wavelength @@ -111,14 +128,11 @@ def _scrape_metadata_hydrogen(file, element, charge): file.seek(0) lines = file.readlines() - pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' - while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): + while not _PEC_INDEX_HEADER_MATCH_STANDARD.match(lines[0]): lines.pop(0) index_lines = lines - - pec_hydrogen_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)' for i in range(len(index_lines)): - match = re.match(pec_hydrogen_transition_match, index_lines[i], re.IGNORECASE) + match = _PEC_HYDROGEN_TRANSITION_MATCH.match(index_lines[i]) if not match: continue @@ -153,14 +167,11 @@ def _scrape_metadata_hydrogen_like(file, element, charge): file.seek(0) lines = file.readlines() - pec_index_header_match = r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE' - while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): + while not _PEC_INDEX_HEADER_MATCH_STANDARD.match(lines[0]): lines.pop(0) index_lines = lines - - pec_full_transition_match = r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' for i in range(len(index_lines)): - match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) + match = _PEC_FULL_TRANSITION_MATCH.match(index_lines[i]) if not match: continue @@ -198,24 +209,14 @@ def _scrape_metadata_full(file, element, charge): configuration_lines = [] configuration_dict = {} - configuration_header_match = r'^C\s*(?:lv\s+)?Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy\s*\(cm(?:\*\*|\^)-1\)\s*$' - while not re.match(configuration_header_match, lines[0], re.IGNORECASE): + while not _CONFIGURATION_HEADER_MATCH.match(lines[0]): lines.pop(0) - pec_index_header_match = r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE' - while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): + while not _PEC_INDEX_HEADER_MATCH_STANDARD.match(lines[0]): configuration_lines.append(lines[0]) lines.pop(0) index_lines = lines - - configuration_string_match = ( - r'^[cC]\s*([0-9]+)\s*' - r'((?:[0-9][SPDFG][0-9](?:\s+[0-9][SPDFG][0-9])*)|(?:[0-9A-Z]+))\s*' - r'\(([0-9]*\.?[0-9]+)\)' - r'\s*([0-9]+)' - r'\(\s*([0-9]*\.?[0-9]+)\)' - ) for i in range(len(configuration_lines)): - match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) + match = _CONFIGURATION_STRING_MATCH.match(configuration_lines[i]) if not match: continue @@ -225,12 +226,10 @@ def _scrape_metadata_full(file, element, charge): total_orbital_quantum_number = _L_LOOKUP[int(match.groups()[3])] # L total_angular_momentum_quantum_number = match.groups()[4] # J - configuration_dict[config_id] = (electron_configuration + " " + spin_multiplicity + - total_orbital_quantum_number + total_angular_momentum_quantum_number) + configuration_dict[config_id] = electron_configuration + " " + spin_multiplicity + total_orbital_quantum_number + total_angular_momentum_quantum_number - pec_full_transition_match = r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' for i in range(len(index_lines)): - match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) + match = _PEC_FULL_TRANSITION_MATCH.match(index_lines[i]) if not match: continue @@ -264,11 +263,8 @@ def _extract_rate(file, block_num): # search from start of file file.seek(0) - wavelength_match = r"^\s*[0-9]*\.[0-9]* ?a? +.*$" - block_id_match = r"^\s*[0-9]*\.[0-9]* ?a?\s*([0-9]*)\s*([0-9]*).*/type *= *([a-zA-Z]*).*/isel *= * ([0-9]*)$" - - for block in _group_by_block(file, wavelength_match): - match = re.match(block_id_match, block[0], re.IGNORECASE) + for block in _group_by_block(file, _WAVELENGTH_MATCH): + match = _BLOCK_ID_MATCH.match(block[0]) if not match: continue @@ -326,18 +322,18 @@ def _extract_rate(file, block_num): raise RuntimeError('Block number {} was not found in the ADF15 file.'.format(block_num)) -def _group_by_block(source_file, match_string): +def _group_by_block(source_file, match_pattern): """ Generator the splits the ADF15 file into blocks. - Groups lines of file into blocks based on precursor ' 6561.9A 24...' + Groups lines of file into blocks based on wavelength pattern match. Note: comment section not filtered out of last block, don't over-read! """ buffer = [] for line in source_file: - if re.match(match_string, line, re.IGNORECASE): + if match_pattern.match(line): if buffer: yield buffer buffer = [line] diff --git a/cherab/openadas/tests/test_adf15.py b/cherab/openadas/tests/test_adf15.py new file mode 100644 index 00000000..cea0ec48 --- /dev/null +++ b/cherab/openadas/tests/test_adf15.py @@ -0,0 +1,296 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import os +import tempfile +import unittest + +import numpy as np + +from cherab.core.atomic import carbon, hydrogen +from cherab.openadas.parse.adf15 import parse_adf15 + + +class MockADF15Files: + """Helper class to create mock ADF15 files for testing.""" + + @staticmethod + def create_hydrogen_adf15(): + """Create a mock ADF15 file in hydrogen format.""" + content = """ 0 /test hydrogen format/ +C +C TEST FILE FOR HYDROGEN +C +C ISEL WAVELENGTH TRANSITION TYPE +C +C 1. 656.3 N= 2 - N= 1 EXCIT +C 2. 486.1 N= 3 - N= 2 RECOM +C 3. 434.0 N= 4 - N= 2 CHEXC +C +C PHOTON EMISSIVITY COEFFICIENTS +C +656.3A 2 2 /type = excit /isel = 1 +1.0E+08 1.0E+09 +1000.0 5000.0 +1.0E-12 2.0E-12 3.0E-12 4.0E-12 +486.1A 2 2 /type = recom /isel = 2 +1.1E+08 1.1E+09 +2000.0 6000.0 +1.1E-12 2.1E-12 3.1E-12 4.1E-12 +434.0A 2 2 /type = chexc /isel = 3 +1.2E+08 1.2E+09 +3000.0 7000.0 +1.2E-12 2.2E-12 3.2E-12 4.2E-12 +""" + return content + + @staticmethod + def create_carbon_adf15(): + """Create a mock ADF15 file in carbon (full configuration) format.""" + content = """ 0 /test carbon format/ +C +C TEST FILE FOR CARBON +C +C lv Configuration (2S+1)L(w-1/2) Energy (cm^-1) +C --- ------------------- -------------- -------------- +c 1 60964A52B (1) 0( 0.0) 0.0 +c 2 60963A52B51C (3) 2( 3.0) 16720.4 +c 3 60964A51C51D (5) 4( 4.5) 32456.8 +c 4 60963A52B51D (2) 1( 1.5) 48932.1 +C +C ISEL WVLEN(A) TRANSITION TYPE ISPB NSPB +C ISPP NSPP SZ TG PR WR +C ----- ---------- ----------------------------------- ----- ---- ---- -- -- -- -- +C 1 1560.70 1(3)1( 1.0)- 2(2)2( 2.0) excit 1 1 12 569 12 1 +C 2 1657.80 1(3)1( 1.0)- 3(5)4( 4.5) excit 1 1 12 1068 7 2 +C 3 1329.50 1(3)1( 1.0)- 4(2)1( 1.5) excit 1 1 12 778 31 3 +C +C PHOTON EMISSIVITY COEFFICIENTS +C +1560.70A 2 2 /type = excit /isel = 1 +1.0E+08 1.0E+09 +1000.0 5000.0 +1.0E-12 2.0E-12 3.0E-12 4.0E-12 +1657.80A 2 2 /type = excit /isel = 2 +1.1E+08 1.1E+09 +2000.0 6000.0 +1.1E-12 2.1E-12 3.1E-12 4.1E-12 +1329.50A 2 2 /type = excit /isel = 3 +1.2E+08 1.2E+09 +3000.0 7000.0 +1.2E-12 2.2E-12 3.2E-12 4.2E-12 +""" + return content + + @staticmethod + def create_tungsten_adf15(): + """Create a mock ADF15 file in tungsten (extended) format.""" + content = """ 0 /test tungsten format/ +C +C TEST FILE FOR TUNGSTEN +C +C lv Configuration (2S+1)L(w-1/2) Energy (cm^-1) +C --- ------------------- -------------- -------------- +c 1074 60964A52B (1) 0( 0.0) 0.0 +c 1075 60963A52B51C (3) 2( 3.0) 16720.4 +c 414 60964A51C51D (5) 4( 4.5) 32456.8 +c 365 60963A52B51D (2) 1( 1.5) 48932.1 +C +C ISEL WVLEN(A) TRANSITION TYPE ISPB NSPB +C ISPP NSPP SZ TG PR WR +C ----- ---------- ----------------------------------- ----- ---- ---- -- -- -- -- +C 1 56.5300 1074(1)0( 0.0)- 1075(3)2( 3.0) excit 1 1 12 569 12 1 +C 2 56.5520 1075(3)2( 3.0)- 414(5)4( 4.5) excit 1 1 12 1068 7 2 +C 3 70.1877 414(5)4( 4.5)- 365(2)1( 1.5) excit 1 1 12 778 31 3 +C 4 70.5640 365(2)1( 1.5)- 1074(1)0( 0.0) excit 1 1 12 275 36 4 +C +C PHOTON EMISSIVITY COEFFICIENTS +C +56.5300A 2 2 /type = excit /isel = 1 +1.0E+08 1.0E+09 +1000.0 5000.0 +1.0E-12 2.0E-12 3.0E-12 4.0E-12 +56.5520A 2 2 /type = excit /isel = 2 +1.1E+08 1.1E+09 +2000.0 6000.0 +1.1E-12 2.1E-12 3.1E-12 4.1E-12 +70.1877A 2 2 /type = excit /isel = 3 +1.2E+08 1.2E+09 +3000.0 7000.0 +1.2E-12 2.2E-12 3.2E-12 4.2E-12 +70.5640A 2 2 /type = excit /isel = 4 +1.3E+08 1.3E+09 +4000.0 8000.0 +1.3E-12 2.3E-12 3.3E-12 4.3E-12 +""" + return content + + +class TestADF15Parser(unittest.TestCase): + """Unit tests for ADF15 parser.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + """Clean up temporary files.""" + for filename in os.listdir(self.temp_dir): + filepath = os.path.join(self.temp_dir, filename) + if os.path.isfile(filepath): + os.unlink(filepath) + os.rmdir(self.temp_dir) + + def _create_test_file(self, filename, content): + """Helper to create a test file.""" + filepath = os.path.join(self.temp_dir, filename) + with open(filepath, 'w') as f: + f.write(content) + return filepath + + def test_parse_hydrogen_adf15(self): + """Test parsing of hydrogen format ADF15 file.""" + content = MockADF15Files.create_hydrogen_adf15() + filepath = self._create_test_file('test_h.adf15', content) + + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + + # Check that rates were extracted + self.assertIn('excitation', rates) + self.assertIn(hydrogen, rates['excitation']) + self.assertIn(0, rates['excitation'][hydrogen]) + + # Check that wavelengths were extracted + self.assertIn(hydrogen, wavelengths) + self.assertIn(0, wavelengths[hydrogen]) + + # Check specific transitions + self.assertIn((2, 1), rates['excitation'][hydrogen][0]) + self.assertIn((3, 2), rates['recombination'][hydrogen][0]) + self.assertIn((4, 2), rates['thermalcx'][hydrogen][0]) + + def test_parse_carbon_adf15_full_config(self): + """Test parsing of carbon format ADF15 file with full configuration.""" + content = MockADF15Files.create_carbon_adf15() + filepath = self._create_test_file('test_c.adf15', content) + + rates, wavelengths = parse_adf15(carbon, 0, filepath) + + # Check that rates were extracted + self.assertIn('excitation', rates) + self.assertIn(carbon, rates['excitation']) + self.assertIn(0, rates['excitation'][carbon]) + + # Check that wavelengths were extracted + self.assertIn(carbon, wavelengths) + + # Verify rate data has correct shape + transitions = list(rates['excitation'][carbon][0].keys()) + self.assertGreater(len(transitions), 0) + + for transition, rate_data in rates['excitation'][carbon][0].items(): + self.assertIn('ne', rate_data) + self.assertIn('te', rate_data) + self.assertIn('rate', rate_data) + self.assertTrue(isinstance(rate_data['ne'], np.ndarray)) + self.assertTrue(isinstance(rate_data['te'], np.ndarray)) + self.assertTrue(isinstance(rate_data['rate'], np.ndarray)) + + def test_parse_tungsten_adf15(self): + """Test parsing of tungsten format ADF15 file.""" + # Tungsten (W) has atomic number 74 + # Create a mock tungsten element for testing + from cherab.core.atomic import tungsten + + content = MockADF15Files.create_tungsten_adf15() + filepath = self._create_test_file('test_w.adf15', content) + + rates, wavelengths = parse_adf15(tungsten, 0, filepath) + + # Check that rates were extracted + self.assertIn('excitation', rates) + self.assertIn(tungsten, rates['excitation']) + self.assertIn(0, rates['excitation'][tungsten]) + + # Check that wavelengths were extracted + self.assertIn(tungsten, wavelengths) + + def test_rate_data_structure(self): + """Test that rate data has correct structure and units.""" + content = MockADF15Files.create_hydrogen_adf15() + filepath = self._create_test_file('test_structure.adf15', content) + + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + + # Extract first rate + first_transition = list(rates['excitation'][hydrogen][0].keys())[0] + rate_data = rates['excitation'][hydrogen][0][first_transition] + + # Check structure + self.assertEqual(set(rate_data.keys()), {'ne', 'te', 'rate'}) + + # Check that units were converted (values should be large after conversion from cm^-3 to m^-3) + self.assertTrue(np.all(rate_data['ne'] >= 1e14)) # Should be in m^-3 + self.assertTrue(np.all(rate_data['te'] > 0)) # Temperature should be positive + self.assertTrue(np.all(rate_data['rate'] > 0)) # Rate should be positive + + # Check array dimensions match + ne_count = len(rate_data['ne']) + te_count = len(rate_data['te']) + rate_shape = rate_data['rate'].shape + self.assertEqual(rate_shape, (ne_count, te_count)) + + def test_invalid_adf15_file(self): + """Test that invalid ADF15 file raises appropriate error.""" + invalid_content = "This is not a valid ADF15 file\n" + filepath = self._create_test_file('invalid.adf15', invalid_content) + + with self.assertRaises(ValueError) as context: + parse_adf15(hydrogen, 0, filepath) + + self.assertIn('valid ADF15 file', str(context.exception)) + + def test_wavelength_conversion(self): + """Test that wavelengths are correctly converted from Angstroms to nm.""" + content = MockADF15Files.create_hydrogen_adf15() + filepath = self._create_test_file('test_wavelength.adf15', content) + + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + + # Check specific wavelengths (656.3 Angstrom = 65.63 nm) + transition_21 = (2, 1) + if transition_21 in wavelengths[hydrogen][0]: + wl = wavelengths[hydrogen][0][transition_21] + # Should be around 65.63 nm (converted from 656.3 Angstrom) + self.assertAlmostEqual(wl, 65.63, places=1) + + def test_multiple_rate_types(self): + """Test parsing file with multiple rate types.""" + content = MockADF15Files.create_hydrogen_adf15() + filepath = self._create_test_file('test_multitypes.adf15', content) + + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + + # Check both rate types exist + self.assertIn('excitation', rates) + self.assertIn('recombination', rates) + self.assertIn('thermalcx', rates) + + +if __name__ == '__main__': + unittest.main() From 51e85bf49c4404f6d0e888c57c6767d6a5ccc080 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 18 May 2026 14:42:18 +0200 Subject: [PATCH 6/6] Refactor string literals to use double quotes in ADF15 parser and tests --- cherab/openadas/parse/adf15.py | 116 ++++++++++++++-------------- cherab/openadas/tests/test_adf15.py | 92 +++++++++++----------- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index 3d4acd62..0b05f42f 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -25,44 +25,44 @@ from cherab.core.utility.conversion import Cm3ToM3, PerCm3ToPerM3 # Compiled regex patterns for ADF15 file parsing -_ADF_HEADER_MATCH = re.compile(r'^\s*(\d*) {4}/(.*)/?\s*$') -_PEC_INDEX_HEADER_MATCH_STANDARD = re.compile(r'^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE', re.IGNORECASE) -_PEC_HYDROGEN_TRANSITION_MATCH = re.compile(r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)', re.IGNORECASE) -_PEC_FULL_TRANSITION_MATCH = re.compile(r'^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)', re.IGNORECASE) -_CONFIGURATION_HEADER_MATCH = re.compile(r'^C\s*(?:lv\s+)?Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy\s*\(cm(?:\*\*|\^)-1\)\s*$', re.IGNORECASE) +_ADF_HEADER_MATCH = re.compile(r"^\s*(\d*) {4}/(.*)/?\s*$") +_PEC_INDEX_HEADER_MATCH_STANDARD = re.compile(r"^C\s*ISEL\s*(?:WAVELENGTH|WVLEN\(A\))\s*TRANSITION\s*TYPE", re.IGNORECASE) +_PEC_HYDROGEN_TRANSITION_MATCH = re.compile(r"^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)", re.IGNORECASE) +_PEC_FULL_TRANSITION_MATCH = re.compile(r"^[cC]\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)", re.IGNORECASE) +_CONFIGURATION_HEADER_MATCH = re.compile(r"^C\s*(?:lv\s+)?Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy\s*\(cm(?:\*\*|\^)-1\)\s*$", re.IGNORECASE) _CONFIGURATION_STRING_MATCH = re.compile( - r'^[cC]\s*([0-9]+)\s*' - r'((?:[0-9][SPDFG][0-9](?:\s+[0-9][SPDFG][0-9])*)|(?:[0-9A-Z]+))\s*' - r'\(([0-9]*\.?[0-9]+)\)' - r'\s*([0-9]+)' - r'\(\s*([0-9]*\.?[0-9]+)\)', + r"^[cC]\s*([0-9]+)\s*" + r"((?:[0-9][SPDFG][0-9](?:\s+[0-9][SPDFG][0-9])*)|(?:[0-9A-Z]+))\s*" + r"\(([0-9]*\.?[0-9]+)\)" + r"\s*([0-9]+)" + r"\(\s*([0-9]*\.?[0-9]+)\)", re.IGNORECASE, ) _WAVELENGTH_MATCH = re.compile(r"^\s*[0-9]*\.[0-9]* ?a?\s+[0-9]+\s+[0-9]+.*?/isel *= *[0-9]+$", re.IGNORECASE) _BLOCK_ID_MATCH = re.compile(r"^\s*[0-9]*\.[0-9]* ?a?\s*([0-9]*)\s*([0-9]*).*/type *= *([a-zA-Z]*).*/isel *= * ([0-9]*)$", re.IGNORECASE) _L_LOOKUP = { - 0: 'S', - 1: 'P', - 2: 'D', - 3: 'F', - 4: 'G', - 5: 'H', - 6: 'I', - 7: 'K', - 8: 'L', - 9: 'M', - 10: 'N', - 11: 'O', - 12: 'Q', - 13: 'R', - 14: 'T', - 15: 'U', - 16: 'V', - 17: 'W', - 18: 'X', - 19: 'Y', - 20: 'Z', + 0: "S", + 1: "P", + 2: "D", + 3: "F", + 4: "G", + 5: "H", + 6: "I", + 7: "K", + 8: "L", + 9: "M", + 10: "N", + 11: "O", + 12: "Q", + 13: "R", + 14: "T", + 15: "U", + 16: "V", + 17: "W", + 18: "X", + 19: "Y", + 20: "Z", } @@ -77,7 +77,7 @@ def parse_adf15(element, charge, adf_file_path, header_format=None): """ if not isinstance(element, Element): - raise TypeError('The element must be an Element object.') + raise TypeError("The element must be an Element object.") charge = int(charge) @@ -85,17 +85,17 @@ def parse_adf15(element, charge, adf_file_path, header_format=None): # for check header line header = file.readline() if not _ADF_HEADER_MATCH.match(header): - raise ValueError('The specified path does not point to a valid ADF15 file.') + raise ValueError("The specified path does not point to a valid ADF15 file.") # scrape transition information and wavelength # use simple electron configuration structure for hydrogen-like ions - if header_format == 'hydrogen' or element == hydrogen: + if header_format == "hydrogen" or element == hydrogen: config = _scrape_metadata_hydrogen(file, element, charge) - elif header_format == 'hydrogen-like': + elif header_format == "hydrogen-like": config = _scrape_metadata_hydrogen_like(file, element, charge) elif element.atomic_number - charge == 1: config = _scrape_metadata_hydrogen_like(file, element, charge) - if not config and 'bnd#' in adf_file_path: + if not config and "bnd#" in adf_file_path: # ADF15 files with the "bnd" suffix may have metadata in the "hydrogen" format config = _scrape_metadata_hydrogen(file, element, charge) else: @@ -106,14 +106,14 @@ def parse_adf15(element, charge, adf_file_path, header_format=None): # process rate data rates = RecursiveDict() - for cls in ('excitation', 'recombination', 'thermalcx'): + for cls in ("excitation", "recombination", "thermalcx"): for element, charge_states in config[cls].items(): for charge, transitions in charge_states.items(): for transition in transitions.keys(): block_num = config[cls][element][charge][transition] rates[cls][element][charge][transition] = _extract_rate(file, block_num) - wavelengths = config['wavelength'] + wavelengths = config["wavelength"] return rates, wavelengths @@ -141,12 +141,12 @@ def _scrape_metadata_hydrogen(file, element, charge): upper_level = int(match.groups()[2]) lower_level = int(match.groups()[3]) rate_type_adas = match.groups()[4].upper() - if rate_type_adas == 'EXCIT': - rate_type = 'excitation' - elif rate_type_adas == 'RECOM': - rate_type = 'recombination' - elif rate_type_adas == 'CHEXC': - rate_type = 'thermalcx' + if rate_type_adas == "EXCIT": + rate_type = "excitation" + elif rate_type_adas == "RECOM": + rate_type = "recombination" + elif rate_type_adas == "CHEXC": + rate_type = "thermalcx" else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -180,12 +180,12 @@ def _scrape_metadata_hydrogen_like(file, element, charge): upper_level = int(match.groups()[2]) lower_level = int(match.groups()[3]) rate_type_adas = match.groups()[4].upper() - if rate_type_adas == 'EXCIT': - rate_type = 'excitation' - elif rate_type_adas == 'RECOM': - rate_type = 'recombination' - elif rate_type_adas == 'CHEXC': - rate_type = 'thermalcx' + if rate_type_adas == "EXCIT": + rate_type = "excitation" + elif rate_type_adas == "RECOM": + rate_type = "recombination" + elif rate_type_adas == "CHEXC": + rate_type = "thermalcx" else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -240,12 +240,12 @@ def _scrape_metadata_full(file, element, charge): lower_level_id = int(match.groups()[3]) lower_level = configuration_dict[lower_level_id] rate_type_adas = match.groups()[4].upper() - if rate_type_adas == 'EXCIT': - rate_type = 'excitation' - elif rate_type_adas == 'RECOM': - rate_type = 'recombination' - elif rate_type_adas == 'CHEXC': - rate_type = 'thermalcx' + if rate_type_adas == "EXCIT": + rate_type = "excitation" + elif rate_type_adas == "RECOM": + rate_type = "recombination" + elif rate_type_adas == "CHEXC": + rate_type = "thermalcx" else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -316,10 +316,10 @@ def _extract_rate(file, block_num): density = PerCm3ToPerM3.to(density) rates = Cm3ToM3.to(rates) - return {'ne': density, 'te': temperature, 'rate': rates} + return {"ne": density, "te": temperature, "rate": rates} # If code gets to here, block wasn't found. - raise RuntimeError('Block number {} was not found in the ADF15 file.'.format(block_num)) + raise RuntimeError("Block number {} was not found in the ADF15 file.".format(block_num)) def _group_by_block(source_file, match_pattern): diff --git a/cherab/openadas/tests/test_adf15.py b/cherab/openadas/tests/test_adf15.py index cea0ec48..ad333d63 100644 --- a/cherab/openadas/tests/test_adf15.py +++ b/cherab/openadas/tests/test_adf15.py @@ -159,57 +159,57 @@ def tearDown(self): def _create_test_file(self, filename, content): """Helper to create a test file.""" filepath = os.path.join(self.temp_dir, filename) - with open(filepath, 'w') as f: + with open(filepath, "w") as f: f.write(content) return filepath def test_parse_hydrogen_adf15(self): """Test parsing of hydrogen format ADF15 file.""" content = MockADF15Files.create_hydrogen_adf15() - filepath = self._create_test_file('test_h.adf15', content) + filepath = self._create_test_file("test_h.adf15", content) - rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format="hydrogen") # Check that rates were extracted - self.assertIn('excitation', rates) - self.assertIn(hydrogen, rates['excitation']) - self.assertIn(0, rates['excitation'][hydrogen]) + self.assertIn("excitation", rates) + self.assertIn(hydrogen, rates["excitation"]) + self.assertIn(0, rates["excitation"][hydrogen]) # Check that wavelengths were extracted self.assertIn(hydrogen, wavelengths) self.assertIn(0, wavelengths[hydrogen]) # Check specific transitions - self.assertIn((2, 1), rates['excitation'][hydrogen][0]) - self.assertIn((3, 2), rates['recombination'][hydrogen][0]) - self.assertIn((4, 2), rates['thermalcx'][hydrogen][0]) + self.assertIn((2, 1), rates["excitation"][hydrogen][0]) + self.assertIn((3, 2), rates["recombination"][hydrogen][0]) + self.assertIn((4, 2), rates["thermalcx"][hydrogen][0]) def test_parse_carbon_adf15_full_config(self): """Test parsing of carbon format ADF15 file with full configuration.""" content = MockADF15Files.create_carbon_adf15() - filepath = self._create_test_file('test_c.adf15', content) + filepath = self._create_test_file("test_c.adf15", content) rates, wavelengths = parse_adf15(carbon, 0, filepath) # Check that rates were extracted - self.assertIn('excitation', rates) - self.assertIn(carbon, rates['excitation']) - self.assertIn(0, rates['excitation'][carbon]) + self.assertIn("excitation", rates) + self.assertIn(carbon, rates["excitation"]) + self.assertIn(0, rates["excitation"][carbon]) # Check that wavelengths were extracted self.assertIn(carbon, wavelengths) # Verify rate data has correct shape - transitions = list(rates['excitation'][carbon][0].keys()) + transitions = list(rates["excitation"][carbon][0].keys()) self.assertGreater(len(transitions), 0) - for transition, rate_data in rates['excitation'][carbon][0].items(): - self.assertIn('ne', rate_data) - self.assertIn('te', rate_data) - self.assertIn('rate', rate_data) - self.assertTrue(isinstance(rate_data['ne'], np.ndarray)) - self.assertTrue(isinstance(rate_data['te'], np.ndarray)) - self.assertTrue(isinstance(rate_data['rate'], np.ndarray)) + for transition, rate_data in rates["excitation"][carbon][0].items(): + self.assertIn("ne", rate_data) + self.assertIn("te", rate_data) + self.assertIn("rate", rate_data) + self.assertTrue(isinstance(rate_data["ne"], np.ndarray)) + self.assertTrue(isinstance(rate_data["te"], np.ndarray)) + self.assertTrue(isinstance(rate_data["rate"], np.ndarray)) def test_parse_tungsten_adf15(self): """Test parsing of tungsten format ADF15 file.""" @@ -218,14 +218,14 @@ def test_parse_tungsten_adf15(self): from cherab.core.atomic import tungsten content = MockADF15Files.create_tungsten_adf15() - filepath = self._create_test_file('test_w.adf15', content) + filepath = self._create_test_file("test_w.adf15", content) rates, wavelengths = parse_adf15(tungsten, 0, filepath) # Check that rates were extracted - self.assertIn('excitation', rates) - self.assertIn(tungsten, rates['excitation']) - self.assertIn(0, rates['excitation'][tungsten]) + self.assertIn("excitation", rates) + self.assertIn(tungsten, rates["excitation"]) + self.assertIn(0, rates["excitation"][tungsten]) # Check that wavelengths were extracted self.assertIn(tungsten, wavelengths) @@ -233,44 +233,44 @@ def test_parse_tungsten_adf15(self): def test_rate_data_structure(self): """Test that rate data has correct structure and units.""" content = MockADF15Files.create_hydrogen_adf15() - filepath = self._create_test_file('test_structure.adf15', content) + filepath = self._create_test_file("test_structure.adf15", content) - rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format="hydrogen") # Extract first rate - first_transition = list(rates['excitation'][hydrogen][0].keys())[0] - rate_data = rates['excitation'][hydrogen][0][first_transition] + first_transition = list(rates["excitation"][hydrogen][0].keys())[0] + rate_data = rates["excitation"][hydrogen][0][first_transition] # Check structure - self.assertEqual(set(rate_data.keys()), {'ne', 'te', 'rate'}) + self.assertEqual(set(rate_data.keys()), {"ne", "te", "rate"}) # Check that units were converted (values should be large after conversion from cm^-3 to m^-3) - self.assertTrue(np.all(rate_data['ne'] >= 1e14)) # Should be in m^-3 - self.assertTrue(np.all(rate_data['te'] > 0)) # Temperature should be positive - self.assertTrue(np.all(rate_data['rate'] > 0)) # Rate should be positive + self.assertTrue(np.all(rate_data["ne"] >= 1e14)) # Should be in m^-3 + self.assertTrue(np.all(rate_data["te"] > 0)) # Temperature should be positive + self.assertTrue(np.all(rate_data["rate"] > 0)) # Rate should be positive # Check array dimensions match - ne_count = len(rate_data['ne']) - te_count = len(rate_data['te']) - rate_shape = rate_data['rate'].shape + ne_count = len(rate_data["ne"]) + te_count = len(rate_data["te"]) + rate_shape = rate_data["rate"].shape self.assertEqual(rate_shape, (ne_count, te_count)) def test_invalid_adf15_file(self): """Test that invalid ADF15 file raises appropriate error.""" invalid_content = "This is not a valid ADF15 file\n" - filepath = self._create_test_file('invalid.adf15', invalid_content) + filepath = self._create_test_file("invalid.adf15", invalid_content) with self.assertRaises(ValueError) as context: parse_adf15(hydrogen, 0, filepath) - self.assertIn('valid ADF15 file', str(context.exception)) + self.assertIn("valid ADF15 file", str(context.exception)) def test_wavelength_conversion(self): """Test that wavelengths are correctly converted from Angstroms to nm.""" content = MockADF15Files.create_hydrogen_adf15() - filepath = self._create_test_file('test_wavelength.adf15', content) + filepath = self._create_test_file("test_wavelength.adf15", content) - rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format="hydrogen") # Check specific wavelengths (656.3 Angstrom = 65.63 nm) transition_21 = (2, 1) @@ -282,15 +282,15 @@ def test_wavelength_conversion(self): def test_multiple_rate_types(self): """Test parsing file with multiple rate types.""" content = MockADF15Files.create_hydrogen_adf15() - filepath = self._create_test_file('test_multitypes.adf15', content) + filepath = self._create_test_file("test_multitypes.adf15", content) - rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format='hydrogen') + rates, wavelengths = parse_adf15(hydrogen, 0, filepath, header_format="hydrogen") # Check both rate types exist - self.assertIn('excitation', rates) - self.assertIn('recombination', rates) - self.assertIn('thermalcx', rates) + self.assertIn("excitation", rates) + self.assertIn("recombination", rates) + self.assertIn("thermalcx", rates) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()