"""
This module, `io_snc.py`, contains functions for handling input and output operations related to ArcCheck files.
The module includes the following functions:
- `parse_arccheck_header`: Parses the header information from an ArcCheck file and returns it as a dictionary.
- `parse_arrays_from_file`: Parses the array data from an ArcCheck file and returns it as a dictionary.
- `write_snc_txt_file`: Writes the array data and header into a .txt file in the same format as it was read.
- `parse_acm_file`: Parses an ACM file and returns the frame data, diode data, and background and calibration data.
- `detector_arrays`: Rearranges the detector data from acl file into the one displayed in SNC Patient.
- `diode_numbers_in_snc_array`: Reorganizes the detectors numbers in an acl measurement file into the planar array that is displayed in SNC Patient software.
This module is part of a larger project aimed at analyzing and correcting dose rate dependencies in ArcCheck measurements.
"""
import numpy as np
import os
import pandas as pd
[docs]
def parse_arrays_from_file(file_path):
"""
Parses the array data from an ArcCheck file and returns it as a dictionary.
Parameters
----------
file_path : str
The path to the ArcCheck file.
Returns
-------
dict
A dictionary containing the array data if the file exists and can be read successfully.
Notes
-----
The function reads the file line by line and splits each line at the space character to separate keys and values.
The keys are predefined in the `valid_arrays` list. If a key from the file matches a key in `valid_arrays`,
the corresponding value is updated in the dictionary.
"""
array_data = {}
current_array = None
array_content = []
with open(file_path, 'r') as file:
lines = file.readlines()
# Define the exact names of arrays expected in the file
valid_arrays = [
'Background', 'Calibration Factors', 'Offset', 'Raw Counts', 'Corrected Counts',
'Dose Counts', 'Data Flags', 'Interpolated', 'Dose Interpolated',
'Corrected Counts (No Angular Correction)'
]
for line in lines:
line = line.strip()
# Check if the line matches any of the valid array names exactly
if line in valid_arrays:
if current_array is not None:
# Handle conversion by ensuring all rows are the same length
max_length = max(len(row) for row in array_content)
uniform_content = [row + [None] * (max_length - len(row)) for row in array_content]
array_data[current_array] = np.array(uniform_content, dtype=object) # Use dtype=object for mixed types
current_array = line
array_content = []
elif current_array is not None: # Continue capturing data if we're within an array
parsed_line = line.split()
array_content.append(parsed_line)
# Finalize the last array data capture
if current_array is not None:
max_length = max(len(row) for row in array_content)
uniform_content = [row + [None] * (max_length - len(row)) for row in array_content]
array_data[current_array] = np.array(uniform_content, dtype=object)
return array_data
[docs]
def write_snc_txt_file(array_data, header_data, file_path):
"""
Writes the array data and header into a .txt file in the same format as it was read.
Parameters
----------
array_data : dict
Dictionary containing all the array data.
header_data : dict
Dictionary containing all the header information, including 'Full Header Text'.
file_path : str
Path to the file where the data should be saved.
Notes
-----
The function writes the full header text directly from the header_data dictionary.
Then it writes each array, skipping None values and using tabs as delimiter.
"""
try:
with open(file_path, 'w') as file:
# Write the full header text directly from the header_data dictionary
if 'Full Header Text' in header_data:
file.write(header_data['Full Header Text'])
file.write("\n\n") # Ensure there's a blank line after the header
# Write each array, skipping None values and using tabs as delimiter
for array_name, array_content in array_data.items():
file.write(f"{array_name}\n")
for row in array_content:
# Only write the row if it contains any non-None values
if any(x is not None for x in row):
row_data = '\t'.join(str(x) for x in row if x is not None)
# Add a tab character before 'COL' and 'Xcm'
row_data = row_data.replace('COL', '\tCOL').replace('Xcm', '\tXcm')
file.write(f"{row_data}\n")
file.write("\n") # Separate arrays by a newline for clarity
except Exception as e:
print(f"An error occurred while writing to file: {e}")
[docs]
def parse_acm_file(file_path):
"""
Parses an ACM file and returns the frame data, diode data, and background and calibration data.
Parameters
----------
file_path : str
The path to the ACM file.
Returns
-------
tuple
A tuple containing three pandas DataFrames: frame_data_df, diode_data_df, bkrnd_and_calibration_df.
Notes
-----
The function reads the file line by line and splits each line at the tab character to separate keys and values.
"""
frame_data_keys = [
'UPDATE#', 'TIMETIC1', 'TIMETIC2', 'PULSES',
'STATUS1', 'STATUS2', 'VirtualInclinometer', 'CorrectedAngle', 'FieldSize', 'Reference Diode'
]
diode_data_keys = ['Reference Diode'] + [str(i) for i in range(1, 1387)] # 1386 diodes
# Initialize storage for frame data and diode data
frame_data = []
diode_data = []
with open(file_path, 'r') as file:
lines = file.readlines()
# Skip to line 77 where the data header starts
data_header_index = 76 # 0-based index for line 77
data_header = lines[data_header_index].strip().split('\t')
# Extract Background and Calibration data
background_data = lines[data_header_index + 1].strip().split('\t')[10:] # Skip 'Background' line
calibration_data = lines[data_header_index + 2].strip().split('\t')[10:] # Skip 'Calibration' line
# Create a DataFrame for Background and Calibration data
bkrnd_and_calibration_df = pd.DataFrame({
'Detector Names': diode_data_keys,
'Background': background_data,
'Calibration': calibration_data
})
# Process each line of data after the headers
for line in lines[data_header_index + 3:]: # Skip 'Background' and 'Calibration' lines
row_data = line.strip().split('\t')
if row_data[0] == 'Data:': # Ensure we are reading a data line
# Split frame data and diode data based on the known structure
frame_row = row_data[1:len(frame_data_keys) + 1]
diode_row = row_data[len(frame_data_keys) + 1:]
frame_data.append(frame_row)
diode_data.append(diode_row)
# Convert lists to numpy arrays for easier manipulation later
frame_data = np.array(frame_data, dtype=float)
diode_data = np.array(diode_data, dtype=float)
diode_data_keys = [str(i) for i in range(1, 1387)] # getting rid of the reference diodo key
frame_data_df = pd.DataFrame(frame_data, columns=frame_data_keys)
diode_data_df = pd.DataFrame(diode_data, columns=diode_data_keys)
return frame_data_df, diode_data_df, bkrnd_and_calibration_df
[docs]
def detector_arrays(acl_detectors):
"""
Rearranges the detector data from acl file into the one displayed in SNC Patient.
Parameters
----------
acl_detectors : pandas.DataFrame
The measurement data arranged in acl formal.
Returns
-------
numpy.ndarray
A 3D numpy array created from the differential dose data.
Notes
-----
The function initializes a 3D numpy array with zeros and fills it with the detector data.
"""
# Initialize a 3D numpy array with zeros
arrays = np.zeros((len(acl_detectors), 41, 131))
for row_index in range(len(acl_detectors)):
# Extract the counts for the current row
current_row_counts = acl_detectors.iloc[row_index].values
# Initialize the detector number to match the DataFrame's column indexing
number = 1
for row in range(40, -1, -2): # Start from the last row, move upwards in steps of 2
for col in range(0, 131, 2): # Start from the first column, move right in steps of 2
if number <= 1386:
# Assign the count value corresponding to the detector number
arrays[row_index, row, col] = current_row_counts[
number - 1] # Adjust for 0-based indexing in the array
number += 1
else:
break # Stop if the number exceeds 1386
return arrays
[docs]
def diode_numbers_in_snc_array():
"""
Reorganizes the detectors numbers in an acl measurement file into the planar array
that is displayed in SNC Patient software.
Returns
-------
numpy.ndarray
A 2D numpy array representing the reorganized detectors.
Notes
-----
The function creates an initial array of size 41x131 filled with zeros and fills every second row and column with detector numbers.
"""
# Step 1: Read the acl file and extract the detector numbers
# This step is dependent on the format of the acl file and is not shown here
# Step 2: Create an initial array of size 41x131 filled with zeros
array = np.zeros((41, 131))
# Step 3: Fill every second row and column with detector numbers
# Starting from the bottom left corner (which in array indexing is the last row, first column)
number = 1
for row in range(40, -1, -2): # Start from the last row, move upwards in steps of 2
for col in range(0, 131, 2): # Start from the first column, move right in steps of 2
if number <= 1386:
array[row, col] = number
number += 1
else:
break # Stop if the number exceeds 1386
array = array.astype(int)
return array