From 7968da88a015c1820b5f9332edd85ca3e183df55 Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Sat, 14 Jun 2025 19:45:53 +0300 Subject: [PATCH 01/28] tables description for elements data --- atomdb/migration/periodic/elements_data.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 atomdb/migration/periodic/elements_data.py diff --git a/atomdb/migration/periodic/elements_data.py b/atomdb/migration/periodic/elements_data.py new file mode 100644 index 00000000..c4cfdde9 --- /dev/null +++ b/atomdb/migration/periodic/elements_data.py @@ -0,0 +1,29 @@ +import tables as pt + +class basic_properties(pt.IsDescription): + """ + pos: pos of the property in the column + pt.StringCol(2): max num of characters + """ + atnum = pt.Int32Col(pos=0) + symbol = pt.StringCol(2, pos=1) + name = pt.StringCol(25, pos=2) + group = pt.Int32Col(pos=3) + period = pt.Int32Col(pos=4) + mult = pt.Int32Col(pos=5) + + +class property_values(pt.IsDescription): + source = pt.StringCol(30, pos=0) + unit = pt.StringCol(20, pos=1) + value = pt.StringCol(pos=2) + + +class data_info(pt.IsDescription): + property_key = pt.StringCol(20, pos=0) + property_name = pt.StringCol(25, pos=1) + source_key = pt.StringCol(30, pos=2) + property_description = pt.StringCol(250, pos=3) + reference = pt.StringCol(250, pos=4) + doi = pt.StringCol(150, pos=4) + notes = pt.StringCol(600, pos=5) From 4d0654300fbd80f5215fcde4a5c6cfbebf99051f Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Sun, 15 Jun 2025 04:32:47 +0300 Subject: [PATCH 02/28] filter data and creating tables --- atomdb/migration/periodic/elements_data.py | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/atomdb/migration/periodic/elements_data.py b/atomdb/migration/periodic/elements_data.py index c4cfdde9..45c98946 100644 --- a/atomdb/migration/periodic/elements_data.py +++ b/atomdb/migration/periodic/elements_data.py @@ -1,4 +1,7 @@ import tables as pt +import pandas as pd +import numpy as np + class basic_properties(pt.IsDescription): """ @@ -27,3 +30,88 @@ class data_info(pt.IsDescription): reference = pt.StringCol(250, pos=4) doi = pt.StringCol(150, pos=4) notes = pt.StringCol(600, pos=5) + + +property_configs = [ + {'category': 'cov_radius', 'group': 'size', 'table_name': 'cov_radius', 'description': 'Covalent Radius'}, + + {'category': 'vdw_radius', 'group': 'size', 'table_name': 'vdw_radius', 'description': 'Van der Waals Radius'}, + + {'category': 'at_radius', 'group': 'size', 'table_name': 'at_radius', 'description': 'Atomic Radius'}, + + {'category': 'mass', 'group': None, 'table_name': 'mass', 'description': 'Atomic Mass'}, + + {'category': 'pold', 'group': None, 'table_name': 'pold', 'description': 'Polarizability'}, + + {'category': 'c6', 'group': None, 'table_name': 'c6', 'description': 'C6 Dispersion Coefficient'}, + + {'category': 'eneg', 'group': None, 'table_name': 'eneg', 'description': 'Electronegativity'} +] + + + +elements_data_df = pd.read_csv('atomdb/data/elements_data.csv', comment='#', header=None, dtype=str) +elements_data_df.dropna(how='all', inplace=True) + + + +headers = elements_data_df.iloc[0].str.strip().to_list() +unique_headers = [] +header_counts = {} +for header in headers: + if header in header_counts: + header_counts[header] += 1 + unique_headers.append(f"{header}.{header_counts[header]}") + else: + header_counts[header] = 0 + unique_headers.append(header) + +sources = elements_data_df.iloc[1].str.strip().tolist() +units = elements_data_df.iloc[2].str.strip().tolist() +data = elements_data_df.iloc[3:].reset_index(drop=True) +data.columns = unique_headers + + +sources_data = dict(zip(unique_headers, sources)) +units_data = dict(zip(unique_headers, units)) + + + +def create_data_for_tables(hdf5_file, parent_folder, table_name, table_description, row_description, columns, row_data, sources_data, units_data): + table = hdf5_file.create_table(parent_folder, table_name, row_description, table_description) + + for col in columns: + if pd.notna(row_data[col]): + source = sources_data.get(col, 'unknown') + unit = units_data.get(col, 'unknown') + value = np.nan + + if pd.notna(row_data[col]) and str(row_data[col]).strip(): + try: + value = float(row_data[col]) + except (ValueError, TypeError): + value = np.nan + + row = table.row() + row['source'] = source.encode('utf-8') if source else '' + row['unit'] = unit.encode('utf-8') if unit else '' + row['value'] = value + row.append() + + table.flush() + return table + + + + + + + + + + + + + + + From e4bf93502a9bd0f9bd3776176285c18a2de9b36c Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Sun, 15 Jun 2025 19:40:43 +0300 Subject: [PATCH 03/28] migration script for elements_data --- atomdb/migration/periodic/elements_data.py | 120 ++++++++++++++++----- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/atomdb/migration/periodic/elements_data.py b/atomdb/migration/periodic/elements_data.py index 45c98946..05941486 100644 --- a/atomdb/migration/periodic/elements_data.py +++ b/atomdb/migration/periodic/elements_data.py @@ -19,7 +19,7 @@ class basic_properties(pt.IsDescription): class property_values(pt.IsDescription): source = pt.StringCol(30, pos=0) unit = pt.StringCol(20, pos=1) - value = pt.StringCol(pos=2) + value = pt.Float64Col(pos=2) class data_info(pt.IsDescription): @@ -32,25 +32,53 @@ class data_info(pt.IsDescription): notes = pt.StringCol(600, pos=5) +def create_data_for_tables(hdf5_file, parent_folder, table_name, table_description, row_description, columns, row_data, sources_data, units_data): + table = hdf5_file.create_table(parent_folder, table_name, row_description, table_description) + + for col in columns: + if pd.notna(row_data[col]): + source = sources_data.get(col, 'unknown') + unit = units_data.get(col, 'unknown') + value = np.nan + + if pd.notna(row_data[col]) and str(row_data[col]).strip(): + try: + value = float(row_data[col]) + except (ValueError, TypeError): + value = np.nan + + row = table.row + row['source'] = source.encode('utf-8') if source else '' + row['unit'] = unit.encode('utf-8') if unit else '' + row['value'] = value + row.append() + + table.flush() + return table + + +csv_file = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/elements_data.csv' +hdf5_file = "elements_data.h5" + property_configs = [ - {'category': 'cov_radius', 'group': 'size', 'table_name': 'cov_radius', 'description': 'Covalent Radius'}, + {'property': 'cov_radius', 'group': 'size', 'table_name': 'cov_radius', 'description': 'Covalent Radius'}, - {'category': 'vdw_radius', 'group': 'size', 'table_name': 'vdw_radius', 'description': 'Van der Waals Radius'}, + {'property': 'vdw_radius', 'group': 'size', 'table_name': 'vdw_radius', 'description': 'Van der Waals Radius'}, - {'category': 'at_radius', 'group': 'size', 'table_name': 'at_radius', 'description': 'Atomic Radius'}, + {'property': 'at_radius', 'group': 'size', 'table_name': 'at_radius', 'description': 'Atomic Radius'}, - {'category': 'mass', 'group': None, 'table_name': 'mass', 'description': 'Atomic Mass'}, + {'property': 'mass', 'group': None, 'table_name': 'mass', 'description': 'Atomic Mass'}, - {'category': 'pold', 'group': None, 'table_name': 'pold', 'description': 'Polarizability'}, + {'property': 'pold', 'group': None, 'table_name': 'pold', 'description': 'Polarizability'}, - {'category': 'c6', 'group': None, 'table_name': 'c6', 'description': 'C6 Dispersion Coefficient'}, + {'property': 'c6', 'group': None, 'table_name': 'c6', 'description': 'C6 Dispersion Coefficient'}, - {'category': 'eneg', 'group': None, 'table_name': 'eneg', 'description': 'Electronegativity'} + {'property': 'eneg', 'group': None, 'table_name': 'eneg', 'description': 'Electronegativity'} ] -elements_data_df = pd.read_csv('atomdb/data/elements_data.csv', comment='#', header=None, dtype=str) +elements_data_df = pd.read_csv(csv_file, comment='#', header=None, dtype=str) elements_data_df.dropna(how='all', inplace=True) @@ -77,29 +105,65 @@ class data_info(pt.IsDescription): -def create_data_for_tables(hdf5_file, parent_folder, table_name, table_description, row_description, columns, row_data, sources_data, units_data): - table = hdf5_file.create_table(parent_folder, table_name, row_description, table_description) - for col in columns: - if pd.notna(row_data[col]): - source = sources_data.get(col, 'unknown') - unit = units_data.get(col, 'unknown') - value = np.nan +try: + with pt.open_file(hdf5_file, mode='w', title='Periodic Data') as h5file: + elements_group = h5file.create_group('/', 'elements', 'Elements Data' ) + + for idx, row in data.iterrows(): + atnum = int(row['atnum']) if pd.notna(row['atnum']) else 0 + symbol = row['symbol'] if pd.notna(row['symbol']) else '' + name = row['name'] if pd.notna(row['name']) else '' + group = int(row['group']) if pd.notna(row['group']) else 0 + period = int(row['period']) if pd.notna(row['period']) else 0 + mult = int(row['mult']) if pd.notna(row['mult']) else 0 + + element_group_name = f"Element_{atnum:03d}_{name}" + element_group = h5file.create_group(elements_group, element_group_name, f'Data for {name}') + + basic_properties_table =h5file.create_table(element_group, 'basic_properties', basic_properties, 'Basic Properties') + basic_properties_row = basic_properties_table.row + basic_properties_row['atnum'] = atnum + basic_properties_row['symbol'] = symbol.encode('utf-8') if symbol else '' + basic_properties_row['name'] = name.encode('utf-8') if name else '' + basic_properties_row['group'] = group + basic_properties_row['period'] = period + basic_properties_row['mult'] = mult + basic_properties_row.append() + basic_properties_table.flush() + + + + # flag to track if we need to create size folder or it already exists + size_group_created = False + size_group = None + + for config in property_configs: + columns = [col for col in unique_headers if col.startswith(config['property'])] + + if not columns: + continue + + + if config['group']: + if not size_group_created: + size_group = h5file.create_group(element_group, 'size', 'Size Properties') + size_group_created = True + parent = size_group + else: + parent = element_group + + create_data_for_tables(h5file, parent, config['table_name'], config['description'], property_values, columns, row, sources_data, units_data) + + + +except Exception as e: + print(f"Error creating HDF5 file: {e}") + exit(1) + - if pd.notna(row_data[col]) and str(row_data[col]).strip(): - try: - value = float(row_data[col]) - except (ValueError, TypeError): - value = np.nan - row = table.row() - row['source'] = source.encode('utf-8') if source else '' - row['unit'] = unit.encode('utf-8') if unit else '' - row['value'] = value - row.append() - table.flush() - return table From 06457456f65814011260e0d2d691910f1975558a Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Mon, 16 Jun 2025 02:12:08 +0300 Subject: [PATCH 04/28] data_info migration --- atomdb/migration/periodic/elements_data.py | 66 +++++++++++++++------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/atomdb/migration/periodic/elements_data.py b/atomdb/migration/periodic/elements_data.py index 05941486..67a8b913 100644 --- a/atomdb/migration/periodic/elements_data.py +++ b/atomdb/migration/periodic/elements_data.py @@ -36,28 +36,28 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti table = hdf5_file.create_table(parent_folder, table_name, row_description, table_description) for col in columns: - if pd.notna(row_data[col]): - source = sources_data.get(col, 'unknown') - unit = units_data.get(col, 'unknown') - value = np.nan - - if pd.notna(row_data[col]) and str(row_data[col]).strip(): - try: - value = float(row_data[col]) - except (ValueError, TypeError): - value = np.nan - - row = table.row - row['source'] = source.encode('utf-8') if source else '' - row['unit'] = unit.encode('utf-8') if unit else '' - row['value'] = value - row.append() + source = sources_data.get(col, 'unknown') + unit = units_data.get(col, 'unknown') + value = np.nan + + if pd.notna(row_data[col]) and str(row_data[col]).strip(): + try: + value = float(row_data[col]) + except (ValueError, TypeError): + value = np.nan + + row = table.row + row['source'] = source.encode('utf-8') if source else '' + row['unit'] = unit.encode('utf-8') if unit else '' + row['value'] = value + row.append() table.flush() return table -csv_file = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/elements_data.csv' +elements_data_csv = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/elements_data.csv' +data_info_csv = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/data_info.csv' hdf5_file = "elements_data.h5" property_configs = [ @@ -78,11 +78,9 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti -elements_data_df = pd.read_csv(csv_file, comment='#', header=None, dtype=str) +elements_data_df = pd.read_csv(elements_data_csv, comment='#', header=None, dtype=str) elements_data_df.dropna(how='all', inplace=True) - - headers = elements_data_df.iloc[0].str.strip().to_list() unique_headers = [] header_counts = {} @@ -104,11 +102,17 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti units_data = dict(zip(unique_headers, units)) +data_info_df = pd.read_csv(data_info_csv, sep=',', comment=None, dtype=str) +data_info_df.dropna(how='all', inplace=True) +data_info_df.columns = [col.lstrip('#').strip() for col in data_info_df.columns] + + try: with pt.open_file(hdf5_file, mode='w', title='Periodic Data') as h5file: elements_group = h5file.create_group('/', 'elements', 'Elements Data' ) + data_info_group = h5file.create_group('/', 'data_info', 'Data Info') for idx, row in data.iterrows(): atnum = int(row['atnum']) if pd.notna(row['atnum']) else 0 @@ -155,6 +159,28 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti create_data_for_tables(h5file, parent, config['table_name'], config['description'], property_values, columns, row, sources_data, units_data) + property_info_table = h5file.create_table(data_info_group, 'property_info', data_info, + 'Property Information') + + for _, row in data_info_df.iterrows(): + table_row = property_info_table.row + table_row['property_key'] = row['Property key'].encode('utf-8') if pd.notna( + row['Property key']) else '' + table_row['property_name'] = row['Property name'].encode('utf-8') if pd.notna( + row['Property name']) else '' + table_row['source_key'] = row['Source key'].encode('utf-8') if pd.notna( + row['Source key']) else '' + table_row['property_description'] = row['Property description'].encode( + 'utf-8') if pd.notna( + row['Property description']) else '' + table_row['reference'] = row['Reference'].encode('utf-8') if pd.notna( + row['Reference']) else '' + table_row['doi'] = row['doi'].encode('utf-8') if pd.notna(row['doi']) else '' + table_row['notes'] = row['Notes'].encode('utf-8') if pd.notna(row['Notes']) else '' + table_row.append() + + property_info_table.flush() + except Exception as e: From df637afd382d31fe118026ad49d70358285feead Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Tue, 17 Jun 2025 04:08:41 +0300 Subject: [PATCH 05/28] replace pd by the CSV module / renaming --- atomdb/migration/periodic/elements_data.py | 281 ++++++++++++--------- 1 file changed, 159 insertions(+), 122 deletions(-) diff --git a/atomdb/migration/periodic/elements_data.py b/atomdb/migration/periodic/elements_data.py index 67a8b913..030523b2 100644 --- a/atomdb/migration/periodic/elements_data.py +++ b/atomdb/migration/periodic/elements_data.py @@ -1,8 +1,40 @@ +import csv import tables as pt -import pandas as pd +# import pandas as pd ## Note: consider using csv module instead of pandas import numpy as np +from importlib_resources import \ +files ## Note: alternatively use python path library, see utils.py file +import warnings +warnings.filterwarnings('ignore', category=pt.NaturalNameWarning) +## Note: suggestion, place set-up variables at the top of the file +elements_data_csv = files("atomdb.data").joinpath("elements_data.csv") ## Note: replace harcoded paths to make script more portable +data_info_csv = files("atomdb.data").joinpath("data_info.csv") +hdf5_file = "elements_data.h5" + +property_configs = [ + {'property': 'cov_radius', 'group': 'Radius', 'table_name': 'cov_radius', + 'description': 'Covalent Radius'}, + + {'property': 'vdw_radius', 'group': 'Radius', 'table_name': 'vdw_radius', + 'description': 'Van der Waals Radius'}, + + {'property': 'at_radius', 'group': 'Radius', 'table_name': 'at_radius', + 'description': 'Atomic Radius'}, + + {'property': 'mass', 'group': None, 'table_name': 'mass', 'description': 'Atomic Mass'}, + + {'property': 'pold', 'group': None, 'table_name': 'pold', 'description': 'Polarizability'}, + + {'property': 'c6', 'group': None, 'table_name': 'c6', + 'description': 'C6 Dispersion Coefficient'}, + + {'property': 'eneg', 'group': None, 'table_name': 'eneg', 'description': 'Electronegativity'} +] + + +# Periodic table data schema definition class basic_properties(pt.IsDescription): """ pos: pos of the property in the column @@ -32,7 +64,8 @@ class data_info(pt.IsDescription): notes = pt.StringCol(600, pos=5) -def create_data_for_tables(hdf5_file, parent_folder, table_name, table_description, row_description, columns, row_data, sources_data, units_data): +def create_data_for_tables(hdf5_file, parent_folder, table_name, table_description, row_description, + columns, row_data, sources_data, units_data): table = hdf5_file.create_table(parent_folder, table_name, row_description, table_description) for col in columns: @@ -40,7 +73,7 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti unit = units_data.get(col, 'unknown') value = np.nan - if pd.notna(row_data[col]) and str(row_data[col]).strip(): + if col in row_data and row_data[col].strip(): try: value = float(row_data[col]) except (ValueError, TypeError): @@ -53,155 +86,159 @@ def create_data_for_tables(hdf5_file, parent_folder, table_name, table_descripti row.append() table.flush() - return table - + # return table ## Note: not needed, as the table is already created in the hdf5 file -elements_data_csv = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/elements_data.csv' -data_info_csv = '/home/enjy/work/Gsoc/repo/AtomDB/atomdb/data/data_info.csv' -hdf5_file = "elements_data.h5" -property_configs = [ - {'property': 'cov_radius', 'group': 'size', 'table_name': 'cov_radius', 'description': 'Covalent Radius'}, +def read_elements_data_csv(elements_data_csv): + with open(elements_data_csv, 'r') as f: + reader = csv.reader(f) + lines = [line for line in reader if not line[0].startswith('#') and any(line)] - {'property': 'vdw_radius', 'group': 'size', 'table_name': 'vdw_radius', 'description': 'Van der Waals Radius'}, - - {'property': 'at_radius', 'group': 'size', 'table_name': 'at_radius', 'description': 'Atomic Radius'}, - - {'property': 'mass', 'group': None, 'table_name': 'mass', 'description': 'Atomic Mass'}, - - {'property': 'pold', 'group': None, 'table_name': 'pold', 'description': 'Polarizability'}, - - {'property': 'c6', 'group': None, 'table_name': 'c6', 'description': 'C6 Dispersion Coefficient'}, - - {'property': 'eneg', 'group': None, 'table_name': 'eneg', 'description': 'Electronegativity'} -] + headers = [h.strip() for h in lines[0]] + sources = [s.strip() for s in lines[1]] + units = [u.strip() for u in lines[2]] + data_rows = lines[3:] + # Process headers to make them unique + unique_headers = [] + header_counts = {} + for header in headers: + if header in header_counts: + header_counts[header] += 1 + unique_headers.append(f"{header}.{header_counts[header]}") + else: + header_counts[header] = 0 + unique_headers.append(header) + # Create data as list of dictionaries + data = [] + for row in data_rows: + data.append(dict(zip(unique_headers, row))) -elements_data_df = pd.read_csv(elements_data_csv, comment='#', header=None, dtype=str) -elements_data_df.dropna(how='all', inplace=True) + sources_data = dict(zip(unique_headers, sources)) + units_data = dict(zip(unique_headers, units)) -headers = elements_data_df.iloc[0].str.strip().to_list() -unique_headers = [] -header_counts = {} -for header in headers: - if header in header_counts: - header_counts[header] += 1 - unique_headers.append(f"{header}.{header_counts[header]}") - else: - header_counts[header] = 0 - unique_headers.append(header) + return data, unique_headers, sources_data, units_data -sources = elements_data_df.iloc[1].str.strip().tolist() -units = elements_data_df.iloc[2].str.strip().tolist() -data = elements_data_df.iloc[3:].reset_index(drop=True) -data.columns = unique_headers +def read_data_info_csv(data_info_csv): + with open(data_info_csv, 'r') as f: + reader = csv.reader(f) + lines = [] + for line in reader: + if line and not line[0].startswith('#'): + lines.append([item.strip() for item in line]) -sources_data = dict(zip(unique_headers, sources)) -units_data = dict(zip(unique_headers, units)) + # Get headers (first row) + headers = [h.lstrip('#').strip() for h in lines[0]] + data_rows = lines[1:] + # Create list of dictionaries + data_info = [] + for row in data_rows: + data_info.append(dict(zip(headers, row))) -data_info_df = pd.read_csv(data_info_csv, sep=',', comment=None, dtype=str) -data_info_df.dropna(how='all', inplace=True) -data_info_df.columns = [col.lstrip('#').strip() for col in data_info_df.columns] + return data_info +def write_elements_data_to_hdf5(data, unique_headers, sources_data, units_data): + """ + Write the periodic table data to an HDF5 file. + """ -try: - with pt.open_file(hdf5_file, mode='w', title='Periodic Data') as h5file: - elements_group = h5file.create_group('/', 'elements', 'Elements Data' ) + # Open a file in "w"rite mode + # with pt.open_file(hdf5_file, mode='w', title='Periodic Data') as h5file: + h5file = pt.open_file(hdf5_file, mode="w", + title='Periodic Data') ## Note: removing one indentation level + + # Create the Elements group + elements_group = h5file.create_group('/', 'Elements', 'Elements Data') + + for row in data: + atnum = int(row['atnum']) if 'atnum' in row and row['atnum'].strip() else 0 + symbol = row['symbol'] if 'symbol' in row and row['symbol'].strip() else '' + name = row['name'] if 'name' in row and row['name'].strip() else '' + group = int(row['group']) if 'group' in row and row['group'].strip() else 0 + period = int(row['period']) if 'period' in row and row['period'].strip() else 0 + mult = int(row['mult']) if 'mult' in row and row['mult'].strip() else 0 + + # Create a new group + element_group_name = f"{atnum:03d}" ## Note: change to just label by atomic number, e.g. 001 + element_group = h5file.create_group(elements_group, element_group_name, f'Data for {name}') + + # Create the basic properties table and fill it with data + basic_properties_table = h5file.create_table(element_group, 'basic_properties', + basic_properties, 'Basic Properties') + basic_properties_row = basic_properties_table.row + basic_properties_row['atnum'] = atnum + basic_properties_row['symbol'] = symbol.encode('utf-8') if symbol else '' + basic_properties_row['name'] = name.encode('utf-8') if name else '' + basic_properties_row['group'] = group + basic_properties_row['period'] = period + basic_properties_row['mult'] = mult + basic_properties_row.append() + basic_properties_table.flush() + + # flag to track if we need to create size folder or it already exists + radius_group_created = False + radius_group = None + + for config in property_configs: + columns = [col for col in unique_headers if col.startswith(config['property'])] + + if not columns: + continue + + if config['group']: + if not radius_group_created: + radius_group = h5file.create_group(element_group, 'Radius', 'Radius Properties') + radius_group_created = True + parent = radius_group + else: + parent = element_group + + create_data_for_tables(h5file, parent, config['table_name'], config['description'], + property_values, columns, row, sources_data, units_data) + + # Close the file + h5file.close() + + + +def write_data_info_to_hdf5(data_info_list): + with pt.open_file(hdf5_file, mode='a', title='Periodic Data') as h5file: data_info_group = h5file.create_group('/', 'data_info', 'Data Info') - for idx, row in data.iterrows(): - atnum = int(row['atnum']) if pd.notna(row['atnum']) else 0 - symbol = row['symbol'] if pd.notna(row['symbol']) else '' - name = row['name'] if pd.notna(row['name']) else '' - group = int(row['group']) if pd.notna(row['group']) else 0 - period = int(row['period']) if pd.notna(row['period']) else 0 - mult = int(row['mult']) if pd.notna(row['mult']) else 0 - - element_group_name = f"Element_{atnum:03d}_{name}" - element_group = h5file.create_group(elements_group, element_group_name, f'Data for {name}') - - basic_properties_table =h5file.create_table(element_group, 'basic_properties', basic_properties, 'Basic Properties') - basic_properties_row = basic_properties_table.row - basic_properties_row['atnum'] = atnum - basic_properties_row['symbol'] = symbol.encode('utf-8') if symbol else '' - basic_properties_row['name'] = name.encode('utf-8') if name else '' - basic_properties_row['group'] = group - basic_properties_row['period'] = period - basic_properties_row['mult'] = mult - basic_properties_row.append() - basic_properties_table.flush() - - - - # flag to track if we need to create size folder or it already exists - size_group_created = False - size_group = None - - for config in property_configs: - columns = [col for col in unique_headers if col.startswith(config['property'])] - - if not columns: - continue - - - if config['group']: - if not size_group_created: - size_group = h5file.create_group(element_group, 'size', 'Size Properties') - size_group_created = True - parent = size_group - else: - parent = element_group - - create_data_for_tables(h5file, parent, config['table_name'], config['description'], property_values, columns, row, sources_data, units_data) - property_info_table = h5file.create_table(data_info_group, 'property_info', data_info, 'Property Information') - for _, row in data_info_df.iterrows(): + for row in data_info_list: table_row = property_info_table.row - table_row['property_key'] = row['Property key'].encode('utf-8') if pd.notna( - row['Property key']) else '' - table_row['property_name'] = row['Property name'].encode('utf-8') if pd.notna( - row['Property name']) else '' - table_row['source_key'] = row['Source key'].encode('utf-8') if pd.notna( - row['Source key']) else '' - table_row['property_description'] = row['Property description'].encode( - 'utf-8') if pd.notna( - row['Property description']) else '' - table_row['reference'] = row['Reference'].encode('utf-8') if pd.notna( - row['Reference']) else '' - table_row['doi'] = row['doi'].encode('utf-8') if pd.notna(row['doi']) else '' - table_row['notes'] = row['Notes'].encode('utf-8') if pd.notna(row['Notes']) else '' + table_row['property_key'] = row.get('Property key', '').encode('utf-8') + table_row['property_name'] = row.get('Property name', '').encode('utf-8') + table_row['source_key'] = row.get('Source key', '').encode('utf-8') + table_row['property_description'] = row.get('Property description', '').encode('utf-8') + table_row['reference'] = row.get('Reference', '').encode('utf-8') + table_row['doi'] = row.get('doi', '').encode('utf-8') + table_row['notes'] = row.get('Notes', '').encode('utf-8') table_row.append() property_info_table.flush() -except Exception as e: - print(f"Error creating HDF5 file: {e}") - exit(1) - - - - - - - - - - - - - - +if __name__ == "__main__": + # Read the elements data from the CSV file + data, unique_headers, sources_data, units_data = read_elements_data_csv(elements_data_csv) + # Read the provenance data from the CSV file + data_info_df = read_data_info_csv(data_info_csv) + # Write the periodic table data to an HDF5 file + write_elements_data_to_hdf5(data, unique_headers, sources_data, units_data) + # Write the provenance data to the HDF5 file + write_data_info_to_hdf5(data_info_df) From 1bc44ff148882b1c8984195899cdb9984cc94641 Mon Sep 17 00:00:00 2001 From: enjyashraf18 Date: Tue, 17 Jun 2025 04:50:33 +0300 Subject: [PATCH 06/28] fix chopped cols value / pythonic naming --- atomdb/migration/periodic/elements_data.h5 | Bin 0 -> 65123214 bytes atomdb/migration/periodic/elements_data.py | 22 ++++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 atomdb/migration/periodic/elements_data.h5 diff --git a/atomdb/migration/periodic/elements_data.h5 b/atomdb/migration/periodic/elements_data.h5 new file mode 100644 index 0000000000000000000000000000000000000000..782a6a281857cf159ee8ec288ea344ed9b6d84f8 GIT binary patch literal 65123214 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