diff --git a/toffy/settings.py b/toffy/settings.py index efe83e29..beae614f 100644 --- a/toffy/settings.py +++ b/toffy/settings.py @@ -1,6 +1,6 @@ # tiled regions -REGION_PARAM_FIELDS = ['region_start_row', 'region_start_col', 'fov_num_row', 'fov_num_col', - 'row_fov_size', 'col_fov_size', 'region_rand'] +REGION_PARAM_FIELDS = ['region_name', 'region_start_row', 'region_start_col', + 'fov_num_row', 'fov_num_col', 'row_fov_size', 'col_fov_size', 'region_rand'] # mibitracker MIBITRACKER_BACKEND = 'https://backend-dot-mibitracker-angelolab.appspot.com' diff --git a/toffy/tiling_utils.py b/toffy/tiling_utils.py index 8177ac33..623194cf 100644 --- a/toffy/tiling_utils.py +++ b/toffy/tiling_utils.py @@ -278,8 +278,11 @@ def read_tiled_region_inputs(region_corners, region_params): A `dict` mapping each region-specific parameter to a list of values per FOV """ - # read in the data for each fov (region_start from region_corners_path, all others from user) + # read in the data for each region (region_start from region_corners_path, others from user) for fov in region_corners['fovs']: + # append the name of the region + region_params['region_name'].append(fov['name']) + # append the starting row and column coordinates region_params['region_start_row'].append(fov['centerPointMicrons']['y']) region_params['region_start_col'].append(fov['centerPointMicrons']['x']) @@ -289,7 +292,7 @@ def read_tiled_region_inputs(region_corners, region_params): # verify that the micron size specified is valid if fov['fovSizeMicrons'] <= 0: - raise ValueError("The fovSizeMicrons field for region %s must be positive" + raise ValueError("The fovSizeMicrons field for FOVs in region %s must be positive" % fov['name']) print("Using FOV step size of %d microns for both row (y) and column (x) axis of region %s" @@ -331,7 +334,7 @@ def read_tiled_region_inputs(region_corners, region_params): def set_tiled_region_params(region_corners_path): - """Given a file specifying FOV regions, set the MIBI tiling parameters. + """Given a file specifying top-left FOVs for a set of regions, set the MIBI tiling parameters. User inputs will be required for many values. Units used are microns. @@ -547,7 +550,8 @@ def generate_tiled_region_fov_list(tiling_params, moly_path): row_col_pairs = generate_x_y_fov_pairs(row_range, col_range) # name the FOVs according to MIBI conventions - fov_names = ['R%dC%d' % (y + 1, x + 1) for x in range(region_info['fov_num_row']) + fov_names = ['%s_R%dC%d' % (region_info['region_name'], y + 1, x + 1) + for x in range(region_info['fov_num_row']) for y in range(region_info['fov_num_col'])] # randomize pairs list if specified diff --git a/toffy/tiling_utils_test.py b/toffy/tiling_utils_test.py index 67adad53..d4c0cc31 100644 --- a/toffy/tiling_utils_test.py +++ b/toffy/tiling_utils_test.py @@ -210,12 +210,12 @@ def test_save_coreg_params(): cases=test_cases.TiledRegionReadCases, glob='*_no_moly_param') def test_read_tiled_region_inputs(monkeypatch, fov_coords, fov_names, fov_sizes, user_inputs, base_param_values, full_param_set): - # define a sample fovs list + # define a sample fovs list to define the top-left corners of each tiled region sample_fovs_list = test_utils.generate_sample_fovs_list( fov_coords=fov_coords, fov_names=fov_names, fov_sizes=fov_sizes ) - # define sample region_params to read data into + # define sample_region_params to read data into sample_region_params = {rpf: [] for rpf in settings.REGION_PARAM_FIELDS} # generate the user inputs @@ -269,14 +269,14 @@ def test_generate_region_info(): @parametrize('region_corners_file', [param('bad_region_corners.json', marks=file_missing_err), param('tiled_region_corners.json')]) @parametrize_with_cases( - 'fov_coords, fov_names, fov_sizes, user_inputs, base_param_values,full_param_set', + 'fov_coords, fov_names, fov_sizes, user_inputs, base_param_values, full_param_set', cases=test_cases.TiledRegionReadCases, glob='*_with_moly_param' ) @parametrize('moly_interval_val', [0, 1]) def test_set_tiled_region_params(monkeypatch, region_corners_file, fov_coords, fov_names, fov_sizes, user_inputs, base_param_values, full_param_set, moly_interval_val): - # define a sample set of fovs + # define a sample set of fovs to define the top-left corners of each tiled region sample_fovs_list = test_utils.generate_sample_fovs_list( fov_coords=fov_coords, fov_names=fov_names, fov_sizes=fov_sizes ) @@ -351,19 +351,21 @@ def test_generate_x_y_fov_pairs_rhombus(coords, actual_pairs): @parametrize_with_cases( - 'moly_path, moly_region_setting, moly_interval_setting, moly_interval_value, ' - 'moly_insert_indices, fov_1_end_pos', cases=test_cases.TiledRegionMolySettingCases + 'moly_path, moly_roi_setting, moly_interval_setting, moly_interval_value, ' + 'moly_insert_indices, roi_1_end_pos', cases=test_cases.TiledRegionMolySettingCases ) @parametrize('randomize_setting', [['N', 'N'], ['N', 'Y'], ['Y', 'Y']]) -def test_generate_tiled_region_fov_list(moly_path, moly_region_setting, +def test_generate_tiled_region_fov_list(moly_path, moly_roi_setting, moly_interval_setting, moly_interval_value, - moly_insert_indices, fov_1_end_pos, randomize_setting): - sample_fovs_list = test_utils.generate_sample_fovs_list( - fov_coords=[(0, 0), (100, 100)], fov_names=["TheFirstFOV", "TheSecondFOV"], + moly_insert_indices, roi_1_end_pos, randomize_setting): + # define a set of fovs defining the upper-left corners of each region + sample_roi_fovs_list = test_utils.generate_sample_fovs_list( + fov_coords=[(0, 0), (100, 100)], fov_names=['TheFirstROI', 'TheSecondROI'], fov_sizes=[5, 10] ) sample_region_inputs = { + 'region_name': ['TheFirstROI', 'TheSecondROI'], 'region_start_row': [100, 150], 'region_start_col': [0, 50], 'fov_num_row': [2, 4], @@ -377,7 +379,7 @@ def test_generate_tiled_region_fov_list(moly_path, moly_region_setting, sample_tiling_params = { 'fovFormatVersion': '1.5', - 'fovs': sample_fovs_list['fovs'], + 'fovs': sample_roi_fovs_list['fovs'], 'region_params': sample_region_params } @@ -390,7 +392,7 @@ def test_generate_tiled_region_fov_list(moly_path, moly_region_setting, with open(sample_moly_path, 'w') as smp: json.dump(sample_moly_point, smp) - sample_tiling_params['moly_region'] = moly_region_setting + sample_tiling_params['moly_region'] = moly_roi_setting sample_tiling_params['region_params'][0]['region_rand'] = randomize_setting[0] sample_tiling_params['region_params'][1]['region_rand'] = randomize_setting[1] @@ -398,18 +400,23 @@ def test_generate_tiled_region_fov_list(moly_path, moly_region_setting, if moly_interval_setting: sample_tiling_params['moly_interval'] = moly_interval_value - fov_regions = tiling_utils.generate_tiled_region_fov_list( + fov_list = tiling_utils.generate_tiled_region_fov_list( sample_tiling_params, os.path.join(td, moly_path) ) # assert none of the metadata keys explicitly added by set_tiling_params appear for k in ['region_params', 'moly_region', 'moly_interval']: - assert k not in fov_regions + assert k not in fov_list # retrieve the center points center_points = [ (fov['centerPointMicrons']['x'], fov['centerPointMicrons']['y']) - for fov in fov_regions['fovs'] + for fov in fov_list['fovs'] + ] + + # retrieve the fov names + fov_names = [ + fov['name'] for fov in fov_list['fovs'] ] # define the center points sorted @@ -419,47 +426,73 @@ def test_generate_tiled_region_fov_list(moly_path, moly_region_setting, (x, y) for x in np.arange(50, 90, 10) for y in list(reversed(np.arange(140, 160, 10))) ] + # define the corresponding FOV names + actual_fov_names = [ + 'TheFirstROI_R%dC%d' % (x, y) for y in np.arange(1, 3) for x in np.arange(1, 5) + ] + [ + 'TheSecondROI_R%dC%d' % (x, y) for y in np.arange(1, 5) for x in np.arange(1, 3) + ] + for mi in moly_insert_indices: actual_center_points_sorted.insert(mi, (14540, -10830)) + actual_fov_names.insert(mi, 'MoQC') - # easiest case: the center points should be sorted + # easiest case: the center points and FOV names should be sorted if randomize_setting == ['N', 'N']: assert center_points == actual_center_points_sorted - # if only the second fov is randomized + assert fov_names == actual_fov_names + + # if only the second ROI is randomized elif randomize_setting == ['N', 'Y']: - # ensure the fov 1 center points are the same for both sorted and random - assert center_points[:fov_1_end_pos] == actual_center_points_sorted[:fov_1_end_pos] + # ensure the ROI 1 center points and FOV names are the same for both sorted and random + assert center_points[:roi_1_end_pos] == actual_center_points_sorted[:roi_1_end_pos] + assert fov_names[:roi_1_end_pos] == actual_fov_names[:roi_1_end_pos] - # ensure the random center points for fov 2 contain the same elements + # ensure the random center points and fov names for ROI 2 contain the same elements # as its sorted version misc_utils.verify_same_elements( - computed_center_points=center_points[fov_1_end_pos:], - actual_center_points=actual_center_points_sorted[fov_1_end_pos:] + computed_center_points=center_points[roi_1_end_pos:], + actual_center_points=actual_center_points_sorted[roi_1_end_pos:] + ) + misc_utils.verify_same_elements( + computed_fov_names=fov_names[roi_1_end_pos:], + actual_fov_names=actual_fov_names[roi_1_end_pos:] ) - # however, fov 2 sorted entries should NOT equal fov 2 random entries - assert center_points[fov_1_end_pos:] != actual_center_points_sorted[fov_1_end_pos:] + # however, ROI 2 sorted entries should NOT equal ROI 2 random entries + assert center_points[roi_1_end_pos:] != actual_center_points_sorted[roi_1_end_pos:] + assert fov_names[roi_1_end_pos:] != actual_fov_names[roi_1_end_pos:] # if both fovs are randomized elif randomize_setting == ['Y', 'Y']: - # ensure the random center points for fov 1 contain the same elements + # ensure the random center points and fov names for ROI 1 contain the same elements # as its sorted version misc_utils.verify_same_elements( - computed_center_points=center_points[:fov_1_end_pos], - actual_center_points=actual_center_points_sorted[:fov_1_end_pos] + computed_center_points=center_points[:roi_1_end_pos], + actual_center_points=actual_center_points_sorted[:roi_1_end_pos] + ) + misc_utils.verify_same_elements( + computed_fov_names=fov_names[:roi_1_end_pos], + actual_fov_names=actual_fov_names[:roi_1_end_pos] ) - # however, fov 1 sorted entries should NOT equal fov 1 random entries - assert center_points[:fov_1_end_pos] != actual_center_points_sorted[:fov_1_end_pos] + # however, ROI 1 sorted entries should NOT equal ROI 1 random entries + assert center_points[:roi_1_end_pos] != actual_center_points_sorted[:roi_1_end_pos] + assert fov_names[:roi_1_end_pos] != actual_fov_names[:roi_1_end_pos] - # ensure the random center points for fov 2 contain the same elements + # ensure the random center points for ROI 2 contain the same elements # as its sorted version misc_utils.verify_same_elements( - computed_center_points=center_points[fov_1_end_pos:], - actual_center_points=actual_center_points_sorted[fov_1_end_pos:] + computed_center_points=center_points[roi_1_end_pos:], + actual_center_points=actual_center_points_sorted[roi_1_end_pos:] + ) + misc_utils.verify_same_elements( + computed_fov_names=fov_names[roi_1_end_pos:], + actual_fov_names=actual_fov_names[roi_1_end_pos:] ) - # however, fov 2 sorted entries should NOT equal fov 2 random entries - assert center_points[fov_1_end_pos:] != actual_center_points_sorted[fov_1_end_pos:] + # however, ROI 2 sorted entries should NOT equal ROI 2 random entries + assert center_points[roi_1_end_pos:] != actual_center_points_sorted[roi_1_end_pos:] + assert fov_names[roi_1_end_pos:] != actual_fov_names[roi_1_end_pos:] @parametrize_with_cases('top_left, top_right, bottom_left, bottom_right', diff --git a/toffy/tiling_utils_test_cases.py b/toffy/tiling_utils_test_cases.py index 657b0103..2584efc3 100644 --- a/toffy/tiling_utils_test_cases.py +++ b/toffy/tiling_utils_test_cases.py @@ -40,56 +40,66 @@ def case_reentry_different_type(self): return generate_fiducial_read_vals(user_input_type='diff_types') -# this function assumes that FOV 2's corresponding values are linearly spaced from FOV 1's +# define the list of region start coords and names +_TILED_REGION_ROI_COORDS = [(50, 150), (100, 300)] +_TILED_REGION_ROI_NAMES = ["TheFirstROI", "TheSecondROI"] +_TILED_REGION_ROI_SIZES = [1000, 2000] + + +# this function assumes that ROI 2's corresponding values are linearly spaced from ROI 1's # NOTE: x and y correspond to column and row index respectively as specified in the JSON spec file -def generate_tiled_region_params(start_x_fov_1=50, start_y_fov_1=150, - num_row_fov_1=4, num_col_fov_1=2, - row_size_fov_1=2, col_size_fov_1=1, num_fovs=2): +def generate_tiled_region_params(start_x_roi_1=50, start_y_roi_1=150, + num_row_roi_1=4, num_col_roi_1=2, + row_size_roi_1=2, col_size_roi_1=1, num_rois=2, + roi_names=deepcopy(_TILED_REGION_ROI_NAMES)): # define this dictionary for testing purposes to ensure that function calls # equal what would be placed in param_set_values base_param_values = { - 'region_start_row': start_y_fov_1, - 'region_start_col': start_x_fov_1, - 'fov_num_row': num_row_fov_1, - 'fov_num_col': num_col_fov_1, - 'row_fov_size': row_size_fov_1, - 'col_fov_size': col_size_fov_1 + 'region_start_row': start_y_roi_1, + 'region_start_col': start_x_roi_1, + 'fov_num_row': num_row_roi_1, + 'fov_num_col': num_col_roi_1, + 'row_fov_size': row_size_roi_1, + 'col_fov_size': col_size_roi_1 } - # define the values for each param that should be contained for each FOV + # define the values for each param that should be contained for each ROI full_param_set = { param: list(np.arange( base_param_values[param], - base_param_values[param] * (num_fovs + 1), + base_param_values[param] * (num_rois + 1), base_param_values[param] )) for param in base_param_values } + # set the names for each ROI + full_param_set['region_name'] = roi_names + # TODO: might want to return just one and have the test function generate the other return base_param_values, full_param_set # test tiled region parameter setting and FOV generation -# a helper function for generating params specific to each FOV for TiledRegionReadCases -# NOTE: the param moly_region applies across all FOVs, so it's not set here -def generate_tiled_region_cases(fov_coord_list, fov_name_list, fov_sizes, - user_input_type='none', num_row_fov_1=4, num_col_fov_1=2, - random_fov_1='n', random_fov_2='Y'): +# a helper function for generating params specific to each ROI for TiledRegionReadCases +# NOTE: the param moly_roi applies across all ROIs, so it's not set here +def generate_tiled_region_cases(roi_coord_list, roi_name_list, roi_sizes, + user_input_type='none', num_row_roi_1=4, num_col_roi_1=2, + random_roi_1='n', random_roi_2='Y'): # define the base value for each parameter to use for testing # as well as the full set of parameters for each FOV base_param_values, full_param_set = generate_tiled_region_params( - fov_coord_list[0][0], fov_coord_list[0][1], num_row_fov_1, num_col_fov_1, - fov_sizes[0], fov_sizes[0], len(fov_coord_list) + roi_coord_list[0][0], roi_coord_list[0][1], num_row_roi_1, num_col_roi_1, + roi_sizes[0], roi_sizes[0], len(roi_coord_list) ) full_param_set['region_rand'] = ['N', 'Y'] # define the list of user inputs to pass into the input functions for tiled regions user_inputs = [ - num_row_fov_1, num_col_fov_1, random_fov_1, - num_row_fov_1 * 2, num_col_fov_1 * 2, random_fov_2 + num_row_roi_1, num_col_roi_1, random_roi_1, + num_row_roi_1 * 2, num_col_roi_1 * 2, random_roi_2 ] # insert some bad inputs for the desire test type @@ -104,13 +114,7 @@ def generate_tiled_region_cases(fov_coord_list, fov_name_list, fov_sizes, for i in np.arange(0, len(user_inputs), 2): user_inputs.insert(int(i), bad_inputs_to_insert[int(i / 2)]) - return fov_coord_list, fov_name_list, fov_sizes, user_inputs, base_param_values, full_param_set - - -# define the list of region start coords and names -_TILED_REGION_FOV_COORDS = [(50, 150), (100, 300)] -_TILED_REGION_FOV_NAMES = ["TheFirstFOV", "TheSecondFOV"] -_TILED_REGION_FOV_SIZES = [1000, 2000] + return roi_coord_list, roi_name_list, roi_sizes, user_inputs, base_param_values, full_param_set # NOTE: because of the way the moly_interval param is handled @@ -118,25 +122,25 @@ def generate_tiled_region_cases(fov_coord_list, fov_name_list, fov_sizes, class TiledRegionReadCases: def case_no_reentry_no_moly_param(self): return generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES) + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES) ) def case_no_reentry_with_moly_param(self): fcl, fnl, fs, ui, bpv, fps = generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES) + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES) ) return fcl, fnl, fs, ui + ['Y'], bpv, fps def case_reentry_same_type_no_moly_param(self): return generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES), + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES), user_input_type='same_types' ) def case_reentry_same_type_with_moly_param(self): fcl, fnl, fs, ui, bpv, fps = generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES), + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES), user_input_type='same_types' ) @@ -144,13 +148,13 @@ def case_reentry_same_type_with_moly_param(self): def case_reentry_different_type_no_moly_param(self): return generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES), + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES), user_input_type='diff_types' ) def case_reentry_different_type_with_moly_param(self): fcl, fnl, fs, ui, bpv, fps = generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES), + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES), user_input_type='diff_types' ) @@ -159,7 +163,7 @@ def case_reentry_different_type_with_moly_param(self): @xfail(raises=ValueError, strict=True) def case_bad_fov_size_value_no_moly_param(self): fcl, fnl, fs, ui, bpv, fps = generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES) + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES) ) fs[0] = -5 @@ -169,7 +173,7 @@ def case_bad_fov_size_value_no_moly_param(self): @xfail(raises=ValueError, strict=True) def case_bad_fov_size_value_moly_param(self): fcl, fnl, fs, ui, bpv, fps = generate_tiled_region_cases( - _TILED_REGION_FOV_COORDS, _TILED_REGION_FOV_NAMES, deepcopy(_TILED_REGION_FOV_SIZES) + _TILED_REGION_ROI_COORDS, _TILED_REGION_ROI_NAMES, deepcopy(_TILED_REGION_ROI_SIZES) ) fs[0] = -5