Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion ledsa/core/file_handling.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
from typing import List, Union

import numpy as np
Expand Down Expand Up @@ -304,6 +305,32 @@ def create_binary_data(channel: int, path: str = '.') -> None:
LEGACY_FIT_COLUMNS = BASE_COLUMNS + LED_CENTER_COLUMNS + FIT_COLUMNS[:-1]


def _find_first_result_file(channel: int) -> str:
"""
Find the step 3 result file with the lowest image ID of a channel.

An image that could not be read has no result file at all, so the lowest analysed image ID is not
necessarily 1 and the result file of image 1 does not have to exist.

:param channel: Channel number whose result files are searched.
:type channel: int
:return: Path of the result file with the lowest image ID.
:rtype: str
:raises FileNotFoundError: If the channel has no result file.
"""
dir_path = os.path.join('analysis', f'channel{channel}')
img_ids = []
if os.path.isdir(dir_path):
for file_name in os.listdir(dir_path):
img = re.search(r"(\d+)_led_positions.csv", file_name)
if img is not None:
img_ids.append(int(img.group(1)))
if not img_ids:
raise FileNotFoundError(f'No analysis results in {dir_path}. Run the data extraction (-s3 or '
f'-s3_fast) before the analysis.')
return os.path.join(dir_path, f'{min(img_ids)}_led_positions.csv')


def _get_column_names(channel: int) -> List[str]:
"""
Get the column names for the specified channel from the number of columns of the CSV files.
Expand All @@ -313,8 +340,9 @@ def _get_column_names(channel: int) -> List[str]:
:return: List of column names.
:rtype: List[str]
:raises ValueError: If the file cannot be parsed or the number of columns matches no known layout.
:raises FileNotFoundError: If the channel has no result file.
"""
file_path = os.path.join('analysis', f'channel{channel}', '1_led_positions.csv')
file_path = _find_first_result_file(channel)
try:
parameters = ledsa.core.file_handling.read_table(file_path, delim=',', silent=True, atleast_2d=True)
except ValueError as error:
Expand Down
13 changes: 13 additions & 0 deletions ledsa/tests/AcceptanceTests/07_test_corrupted_images.robot
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ Analysis Skips Images Without Extraction Results
# mismatch of the time axes and must not be reported as one
Extco Time Axis Should Be Accepted

Analysis Reads The Column Names From The First Existing Result File
[Documentation] The column names of the analysis are read from one result file. An image that could
... not be read has none at all, so an unreadable first image must not keep the analysis
... from reading the names from the first file that does exist.
Corrupt Image test_img_1.jpg truncate
Execute Ledsa -s3_fast
Corrupted Images Report Should List test_img_1.jpg extraction of the LED intensities
# image 1 is the default reference image and has no results here, so image 2 provides the reference
Create And Fill Config Analysis linear num_ref_images=2
Execute Ledsa --analysis
Analysis Should Exclude Image test_img_1.jpg
Extco Time Axis Should Be Accepted

*** Keywords ***
Restore Test Images
Restore Corrupted Images
Expand Down
7 changes: 4 additions & 3 deletions ledsa/tests/AcceptanceTests/LedsaATestLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,11 @@ def create_and_fill_config(self, first=1, last=4):
conf.save()

@keyword
def create_and_fill_config_analysis(self, solver, reference_property='sum_col_val'):
def create_and_fill_config_analysis(self, solver, reference_property='sum_col_val', num_ref_images=1):
"""Write config_analysis.ini. num_ref_images lets a test take the reference from another image than the first."""
conf = ConfigDataAnalysis(load_config_file=False, camera_position=None, num_layers=20, domain_bounds=None,
led_array_indices=0, num_ref_images=1, camera_channels=0, num_cores=1,
reference_property=reference_property,
led_array_indices=0, num_ref_images=int(num_ref_images), camera_channels=0,
num_cores=1, reference_property=reference_property,
average_images=False, solver=solver, weighting_preference=-6e-4,
weighting_curvature=1e-7,
num_iterations=2000, lambda_reg=1e-3)
Expand Down
56 changes: 56 additions & 0 deletions ledsa/tests/UnitTests/test_file_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os

import pytest

from ledsa.core.file_handling import _find_first_result_file, _get_column_names


class TestFindFirstResultFile:

def _write_result_files(self, tmp_path, img_ids):
channel_dir = tmp_path / 'analysis' / 'channel0'
channel_dir.mkdir(parents=True)
for img_id in img_ids:
(channel_dir / f'{img_id}_led_positions.csv').write_text('# header\n', encoding='utf-8')

def test_lowest_image_id_wins(self, tmp_path, monkeypatch):
# the IDs are compared as numbers, so 2 comes before 10
self._write_result_files(tmp_path, [10, 2, 3])
monkeypatch.chdir(tmp_path)
assert _find_first_result_file(0) == os.path.join('analysis', 'channel0', '2_led_positions.csv')

def test_image_without_results_is_skipped(self, tmp_path, monkeypatch):
# image 1 could not be read, so it has no result file at all
self._write_result_files(tmp_path, [2, 4])
monkeypatch.chdir(tmp_path)
assert _find_first_result_file(0) == os.path.join('analysis', 'channel0', '2_led_positions.csv')

def test_other_files_are_ignored(self, tmp_path, monkeypatch):
self._write_result_files(tmp_path, [3])
(tmp_path / 'analysis' / 'channel0' / 'all_parameters.h5').write_text('', encoding='utf-8')
monkeypatch.chdir(tmp_path)
assert _find_first_result_file(0) == os.path.join('analysis', 'channel0', '3_led_positions.csv')

def test_channel_without_results_raises(self, tmp_path, monkeypatch):
self._write_result_files(tmp_path, [])
monkeypatch.chdir(tmp_path)
with pytest.raises(FileNotFoundError, match='Run the data extraction'):
_find_first_result_file(0)

def test_missing_channel_directory_raises(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
with pytest.raises(FileNotFoundError, match='No analysis results'):
_find_first_result_file(0)


class TestColumnNamesWithoutTheFirstImage:

def test_names_are_read_from_the_first_existing_file(self, tmp_path, monkeypatch):
# the reported case: image 1 could not be read, so the analysis has to fall back to image 2
channel_dir = tmp_path / 'analysis' / 'channel0'
channel_dir.mkdir(parents=True)
row = ','.join(str(float(i)) for i in range(6))
(channel_dir / '2_led_positions.csv').write_text(f'# header\n{row}\n{row}\n', encoding='utf-8')
monkeypatch.chdir(tmp_path)
assert _get_column_names(0) == ["img_id", "led_id", "led_array_id", "sum_col_val", "mean_col_val",
"max_col_val", "bgsub_sum_col_val", "width", "height"]
Loading