Repository: PennLINC/CuBIDS

Issue:
Include variant cluster number/centroid or value in variant renaming recommendations
## Summary
<!--What would you like changed/added and why?-->
I think it would be informative to include the variant's value in the proposed acquisition value.
For example, if you had two echo time variants: one with EchoTimes ranging from 0 - 20  and one with EchoTime = 40 - 60 (assuming the dominant group was 20 - 40), you would get the following variant names:

- acq-VARIANTEchoTime
- acq-VARIANTEchoTime (same)

It would be great to have the proposed variant names be something like the following:

### Using centroid

- acq-VARIANTEchoTime10
- acq-VARIANTEchoTime50

This could be hard with floats though.

### Using cluster number

- acq-VARIANTEchoTime1
- acq-VARIANTEchoTime2

Less informative, but still better than the current approach, I think.

## Additional details
<!--What would be the benefit?-->

## Next steps
<!--Do you have any ideas about the implementation?-->


Discussion:
tsalo: The printed variant names in the failing test don't look quite right. I think you need to convert the cluster numbers to integers and replace periods in other number with a `p`. Not sure why FlipAngle is `nan` in the first one either, but we should fix that.

```
acquisition-HASC55APVARIANTEchoTime0.0FlipAnglenan_datatype-dwi_suffix-dwi
acquisition-HASC55APVARIANTEchoTime0.0_datatype-dwi_suffix-dwi
acquisition-VARIANTFlipAngle70.0_datatype-func_suffix-bold_task-rest
```
tsalo: Dropping the decimal points seems to have solved the bug, weirdly enough.
singlesp: @tsalo do we need to update the files in CuBIDS_Toy_Dataset_Curation?
tsalo: @singlesp I'm not sure where that dataset is used. @mattcieslak or @tientong98 might have a better idea.

Patch:
diff --git a/cubids/utils.py b/cubids/utils.py
index 4eeefb2..a6be9b4 100644
--- a/cubids/utils.py
+++ b/cubids/utils.py
@@ -485,6 +485,10 @@ def format_params(param_group_df, config, modality):
     ``'derived_params'`` is a dictionary of dictionaries, where keys are modalities.
     The modality-wise dictionary's keys are names of BIDS fields to derive from the
     NIfTI header and include in the Parameter Groupings.
+
+    The cluster values assigned by this function are used in variant naming when
+    parameters differ from the dominant group. For example, if EchoTime is clustered
+    and a file is in cluster 2, its variant name will include 'EchoTime2'.
     """
     to_format = config["sidecar_params"][modality]
     to_format.update(config["derived_params"][modality])
@@ -881,6 +885,14 @@ def assign_variants(summary, rename_cols):
     pandas.DataFrame
         The updated summary DataFrame with a new column "RenameEntitySet"
         containing the new entity set names for each file.
+
+    Notes
+    -----
+    Variant names are constructed using the following rules:
+    1. Basic parameters use their actual values (e.g., VARIANTFlipAngle75)
+    2. Clustered parameters use their cluster numbers (e.g., VARIANTEchoTime2)
+    3. Special parameters like HasFieldmap use predefined strings (e.g., VARIANTNoFmap)
+    4. Multiple parameters are concatenated (e.g., VARIANTEchoTime2FlipAngle75)
     """
     # loop through summary tsv and create dom_dict
     dom_dict = {}
@@ -918,7 +930,8 @@ def assign_variants(summary, rename_cols):
 
                 if f"Cluster_{col}" in dom_entity_set.keys():
                     if summary.loc[row, f"Cluster_{col}"] != dom_entity_set[f"Cluster_{col}"]:
-                        acq_str += col
+                        cluster_val = summary.loc[row, f"Cluster_{col}"]
+                        acq_str += f"{col}{int(cluster_val)}"
                 elif summary.loc[row, col] != dom_entity_set[col]:
                     if col == "HasFieldmap":
                         if dom_entity_set[col] == "True":
@@ -931,17 +944,22 @@ def assign_variants(summary, rename_cols):
                         else:
                             acq_str += "IsUsed"
                     else:
-                        acq_str += col
+                        val = summary.loc[row, col]
+                        # If the value is a string float (contains decimal point)
+                        if isinstance(val, str) and "." in val:
+                            val = val.replace(".", "p")
+                        # If the value is an actual float
+                        elif isinstance(val, float):
+                            val = str(val).replace(".", "p")
+                        acq_str += f"{col}{val}"
 
             if acq_str == "VARIANT":
                 acq_str += "Other"
 
             if "acquisition" in entities.keys():
                 acq = f"acquisition-{entities['acquisition'] + acq_str}"
-
                 new_name = summary.loc[row, "EntitySet"].replace(
-                    f"acquisition-{entities['acquisition']}",
-                    acq,
+                    f"acquisition-{entities['acquisition']}", acq
                 )
             else:
                 acq = f"acquisition-{acq_str}"
diff --git a/docs/_static/v0_edited_summary.csv b/docs/_static/v0_edited_summary.csv
index bad6107..947f19f 100644
--- a/docs/_static/v0_edited_summary.csv
+++ b/docs/_static/v0_edited_summary.csv
@@ -1,12 +1,12 @@
 Notes,ManualCheck,MergeInto,RenameEntitySet,KeyParamGroup,EntitySet,ParamGroup,Counts,Dim1Size,Dim2Size,Dim3Size,EchoTime,EffectiveEchoSpacing,FlipAngle,HasFieldmap,EntitySetCount,Modality,NSliceTimes,NumVolumes,Obliquity,PartialFourier,PhaseEncodingDirection,RepetitionTime,TotalReadoutTime,UsedAsFieldmap,VoxelSizeDim1,VoxelSizeDim2,VoxelSizeDim3
 ,,,,datatype-anat_suffix-T1w__1,datatype-anat_suffix-T1w,1,4,180,216,180,0.0029,,8,FALSE,4,anat,0,1.0,FALSE,,i-,2.5,,FALSE,1.0,1.0,1.0
 ,,,,acquisition-HASC55AP_datatype-dwi_suffix-dwi__1,acquisition-HASC55AP_datatype-dwi_suffix-dwi,1,2,36,43,36,0.089,0.00069,90,TRUE,4,dwi,36,61.0,FALSE,,j,4.2,0.072,FALSE,5.0,5.0,5.0
-,,0,acquisition-HASC55APVARIANTNumVolumes_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__2,acquisition-HASC55AP_datatype-dwi_suffix-dwi,2,1,36,43,36,0.089,0.00069,90,TRUE,4,dwi,36,10.0,FALSE,,j,4.2,0.072,FALSE,5.0,5.0,5.0
-,,,acquisition-HASC55APVARIANTEchoTime_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__3,acquisition-HASC55AP_datatype-dwi_suffix-dwi,3,1,36,43,36,0.07,0.00069,90,TRUE,4,dwi,36,61.0,FALSE,,j,4.2,0.072,FALSE,5.0,5.0,5.0
+,,0,acquisition-HASC55APVARIANTNumVolumes10_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__2,acquisition-HASC55AP_datatype-dwi_suffix-dwi,2,1,36,43,36,0.089,0.00069,90,TRUE,4,dwi,36,10.0,FALSE,,j,4.2,0.072,FALSE,5.0,5.0,5.0
+,,,acquisition-HASC55APVARIANTEchoTime2_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__3,acquisition-HASC55AP_datatype-dwi_suffix-dwi,3,1,36,43,36,0.07,0.00069,90,TRUE,4,dwi,36,61.0,FALSE,,j,4.2,0.072,FALSE,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-magnitude1_suffix-magnitude1__1,acquisition-v4_datatype-fmap_fmap-magnitude1_suffix-magnitude1,1,4,36,43,36,0.004,,60,FALSE,4,fmap,0,1.0,FALSE,0.75,j-,1.5,,FALSE,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-magnitude2_suffix-magnitude2__1,acquisition-v4_datatype-fmap_fmap-magnitude2_suffix-magnitude2,1,4,36,43,36,0.006,,60,FALSE,4,fmap,0,1.0,FALSE,0.75,j-,1.5,,FALSE,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-phasediff_suffix-phasediff__1,acquisition-v4_datatype-fmap_fmap-phasediff_suffix-phasediff,1,4,36,43,36,,,60,FALSE,4,fmap,0,1.0,FALSE,0.75,j-,1.5,,TRUE,5.0,5.0,5.0
 ,,,,datatype-fmap_direction-PA_fmap-epi_suffix-epi__1,datatype-fmap_direction-PA_fmap-epi_suffix-epi,1,3,36,43,36,0.089,0.00069,90,FALSE,4,fmap,36,1.0,FALSE,,j-,4.2,0.072,TRUE,5.0,5.0,5.0
 ,,,acquisition-VARIANTUnused_datatype-fmap_direction-PA_fmap-epi_suffix-epi,datatype-fmap_direction-PA_fmap-epi_suffix-epi__2,datatype-fmap_direction-PA_fmap-epi_suffix-epi,2,1,36,43,36,0.089,0.00069,90,FALSE,4,fmap,36,1.0,FALSE,,j-,4.2,0.072,FALSE,5.0,5.0,5.0
 ,,,,datatype-func_suffix-bold_task-rest__1,datatype-func_suffix-bold_task-rest,1,2,36,43,36,0.089,0.00069,70,TRUE,3,func,36,61.0,FALSE,,j,1.0,0.072,FALSE,5.0,5.0,5.0
-,,,acquisition-VARIANTFlipAngle_datatype-func_suffix-bold_task-rest,datatype-func_suffix-bold_task-rest__2,datatype-func_suffix-bold_task-rest,2,1,36,43,36,0.089,0.00069,90,TRUE,3,func,36,61.0,FALSE,,j,1.0,0.072,FALSE,5.0,5.0,5.0
\ No newline at end of file
+,,,acquisition-VARIANTFlipAngle90_datatype-func_suffix-bold_task-rest,datatype-func_suffix-bold_task-rest__2,datatype-func_suffix-bold_task-rest,2,1,36,43,36,0.089,0.00069,90,TRUE,3,func,36,61.0,FALSE,,j,1.0,0.072,FALSE,5.0,5.0,5.0
\ No newline at end of file
diff --git a/docs/_static/v0_summary.csv b/docs/_static/v0_summary.csv
index d19cc94..3d492b4 100644
--- a/docs/_static/v0_summary.csv
+++ b/docs/_static/v0_summary.csv
@@ -1,12 +1,12 @@
 Notes,ManualCheck,MergeInto,RenameEntitySet,KeyParamGroup,EntitySet,ParamGroup,Counts,Dim1Size,Dim2Size,Dim3Size,EchoTime,EffectiveEchoSpacing,FlipAngle,HasFieldmap,EntitySetCount,Modality,NSliceTimes,NumVolumes,Obliquity,PartialFourier,PhaseEncodingDirection,RepetitionTime,TotalReadoutTime,UsedAsFieldmap,VoxelSizeDim1,VoxelSizeDim2,VoxelSizeDim3
 ,,,,datatype-anat_suffix-T1w__1,datatype-anat_suffix-T1w,1,4,180,216,180,0.0029,,8,False,4,anat,0,1.0,False,,i-,2.5,,False,1.0,1.0,1.0
 ,,,,acquisition-HASC55AP_datatype-dwi_suffix-dwi__1,acquisition-HASC55AP_datatype-dwi_suffix-dwi,1,2,36,43,36,0.089,0.00069,90,True,4,dwi,36,61.0,False,,j,4.2,0.072,False,5.0,5.0,5.0
-,,,acquisition-HASC55APVARIANTNumVolumes_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__2,acquisition-HASC55AP_datatype-dwi_suffix-dwi,2,1,36,43,36,0.089,0.00069,90,True,4,dwi,36,10.0,False,,j,4.2,0.072,False,5.0,5.0,5.0
-,,,acquisition-HASC55APVARIANTEchoTime_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__3,acquisition-HASC55AP_datatype-dwi_suffix-dwi,3,1,36,43,36,0.07,0.00069,90,True,4,dwi,36,61.0,False,,j,4.2,0.072,False,5.0,5.0,5.0
+,,,acquisition-HASC55APVARIANTNumVolumes10_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__2,acquisition-HASC55AP_datatype-dwi_suffix-dwi,2,1,36,43,36,0.089,0.00069,90,True,4,dwi,36,10.0,False,,j,4.2,0.072,False,5.0,5.0,5.0
+,,,acquisition-HASC55APVARIANTEchoTime2_datatype-dwi_suffix-dwi,acquisition-HASC55AP_datatype-dwi_suffix-dwi__3,acquisition-HASC55AP_datatype-dwi_suffix-dwi,3,1,36,43,36,0.07,0.00069,90,True,4,dwi,36,61.0,False,,j,4.2,0.072,False,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-magnitude1_suffix-magnitude1__1,acquisition-v4_datatype-fmap_fmap-magnitude1_suffix-magnitude1,1,4,36,43,36,0.004,,60,False,4,fmap,0,1.0,False,0.75,j-,1.5,,False,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-magnitude2_suffix-magnitude2__1,acquisition-v4_datatype-fmap_fmap-magnitude2_suffix-magnitude2,1,4,36,43,36,0.006,,60,False,4,fmap,0,1.0,False,0.75,j-,1.5,,False,5.0,5.0,5.0
 ,,,,acquisition-v4_datatype-fmap_fmap-phasediff_suffix-phasediff__1,acquisition-v4_datatype-fmap_fmap-phasediff_suffix-phasediff,1,4,36,43,36,,,60,False,4,fmap,0,1.0,False,0.75,j-,1.5,,True,5.0,5.0,5.0
 ,,,,datatype-fmap_direction-PA_fmap-epi_suffix-epi__1,datatype-fmap_direction-PA_fmap-epi_suffix-epi,1,3,36,43,36,0.089,0.00069,90,False,4,fmap,36,1.0,False,,j-,4.2,0.072,True,5.0,5.0,5.0
 ,,,acquisition-VARIANTUnused_datatype-fmap_direction-PA_fmap-epi_suffix-epi,datatype-fmap_direction-PA_fmap-epi_suffix-epi__2,datatype-fmap_direction-PA_fmap-epi_suffix-epi,2,1,36,43,36,0.089,0.00069,90,False,4,fmap,36,1.0,False,,j-,4.2,0.072,False,5.0,5.0,5.0
 ,,,,datatype-func_suffix-bold_task-rest__1,datatype-func_suffix-bold_task-rest,1,2,36,43,36,0.089,0.00069,70,True,3,func,36,61.0,False,,j,1.0,0.072,False,5.0,5.0,5.0
-,,,acquisition-VARIANTFlipAngle_datatype-func_suffix-bold_task-rest,datatype-func_suffix-bold_task-rest__2,datatype-func_suffix-bold_task-rest,2,1,36,43,36,0.089,0.00069,90,True,3,func,36,61.0,False,,j,1.0,0.072,False,5.0,5.0,5.0
+,,,acquisition-VARIANTFlipAngle90_datatype-func_suffix-bold_task-rest,datatype-func_suffix-bold_task-rest__2,datatype-func_suffix-bold_task-rest,2,1,36,43,36,0.089,0.00069,90,True,3,func,36,61.0,False,,j,1.0,0.072,False,5.0,5.0,5.0
diff --git a/docs/example.rst b/docs/example.rst
index acb1b03..0ec27a9 100644
--- a/docs/example.rst
+++ b/docs/example.rst
@@ -346,7 +346,13 @@ contains only one scan (see "Counts" column) with only 10 volumes
 Since the majority of DWI scans in this dataset have 61 volumes,
 ``CuBIDS`` assigns this single scan to a "Variant" (i.e. non-dominant) Parameter Group,
 and automatically populates that Parameter Group's "RenameEntitySet" column in ``v0_summary.tsv``
-with a suggested name: ``acquisition-HASC55APVARIANTNumVolumes_datatype-dwi_suffix-dwi``.
+with a suggested name.
+
+.. note::
+   CuBIDS now uses a more explicit variant naming format. In this case, since NumVolumes is a regular
+   (non-clustered) parameter, the actual value is included in the variant name:
+   ``acquisition-HASC55APVARIANTNumVolumes10_datatype-dwi_suffix-dwi``.
+
 This time, though,
 we elect to remove this scan because it does not have enough volumes to be usable for most analyses.
 To do this, we can either use ``cubids purge`` again,
@@ -371,9 +377,14 @@ both validation and ``CuBIDS`` summary —
 we are ready to rename our files based on their RenameEntitySet values and
 apply the requested deletion in ``v0_edited_summary.tsv``.
 The ``cubids apply`` function renames scans in each Variant Parameter Group according
-to the metadata parameters with a flag “VARIANT”,
-which is useful because the user will then be able to see, in each scan's filename,
-which metadata parameters associated with that scan vary from those in the acquisition's Dominant Group.
+to the metadata parameters that differ from the dominant group. The variant name follows
+the format ``acquisition-VARIANT{parameter}{value}_``, where:
+- VARIANT indicates a deviation from the dominant group
+- {parameter} is the name of the differing parameter
+- {value} is either the cluster number (for clustered parameters) or actual value
+
+This naming scheme allows users to easily see which parameters vary from the dominant group
+in each scan's filename.
 If the edited summary and files tsvs are located in the ``bids_dir/code/CuBIDS`` directory,
 the user may just pass in those filenames.
 Otherwise, specifying the path to those files is necessary.
@@ -388,8 +399,9 @@ Checking our git log, we can see that our changes from apply have been saved.
 .. image:: _static/screenshot_7.png
 
 We can check the four grouping tsvs ``cubids apply`` produces (``v1_*``) to ensure they look as expected —
-that all files with variant scanning parameters have been renamed to indicate the parameters
-that vary in the acquisition fields of their filenames.
+that all files with variant scanning parameters have been renamed with the correct format
+(e.g., ``VARIANTEchoTime2`` for clustered parameters, ``VARIANTFlipAngle75`` for regular parameters)
+in their acquisition fields.
 
 
 Exemplar testing
diff --git a/docs/glossary.rst b/docs/glossary.rst
index e86096c..95624f3 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -27,7 +27,8 @@ Glossary
     Rename Entity Set
         Auto-generated, recommended new Entity Set name for Variant Groups.
         Based on the metadata parameters that cause scans in Variant Groups to vary from those
-        in their respective Dominant Groups.
+        in their respective Dominant Groups. For clustered parameters, the cluster number is
+        used in the variant name (e.g., VARIANTEchoTime2 for cluster 2).
 
     Acquisition Group
         A collection of sessions across participants that contains the exact same set of Entity
diff --git a/docs/usage.rst b/docs/usage.rst
index 25b3dba..1d8dfa6 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -180,16 +180,27 @@ Renaming is automatically suggested when the summary.tsv is generated from a ``c
 with the suggested new name listed in the tsv's :term:`Rename Entity Set` column.
 CuBIDS populates this column for all Variant Groups
 (e.g., every Parameter Group except the Dominant one).
-Specifically, CuBIDS will suggest renaming all non-dominant Parameter Group to include ``VARIANT*``
-in their acquisition field where ``*`` is the reason
-the Parameter Group varies from the Dominant Group.
-For example, when CuBIDS encounters a Parameter Group with a repetition time that varies from
-the one present in the Dominant Group,
-it will automatically suggest renaming all scans in that Variant Group to include
-``acquisition-VARIANTRepetitionTime`` in their filenames.
-When the user runs ``cubids apply``,
-filenames will get renamed according to the auto-generated names in the “Rename Entity Set” column
-in the summary.tsv
+
+Specifically, CuBIDS will suggest renaming all non-dominant Parameter Groups to include ``VARIANT*``
+in their acquisition field where ``*`` indicates how the Parameter Group varies from the Dominant Group:
+
+1. For clustered parameters (like EchoTime), the cluster number is used (e.g., ``VARIANTEchoTime2``)
+2. For regular parameters (like FlipAngle), the actual value is used (e.g., ``VARIANTFlipAngle75``)
+3. For special parameters:
+   - HasFieldmap variations use ``NoFmap`` or ``HasFmap``
+   - UsedAsFieldmap variations use ``Unused`` or ``IsUsed``
+
+For example, when CuBIDS encounters a Parameter Group with a clustered EchoTime that varies from
+the one present in the Dominant Group, it will automatically suggest renaming all scans in that
+Variant Group to include ``acquisition-VARIANTEchoTime2`` in their filenames (if the scan belongs
+to cluster 2).
+
+When multiple parameters vary, their names are concatenated (e.g., ``VARIANTEchoTime2FlipAngle75``).
+When the user runs ``cubids apply``, filenames will get renamed according to the auto-generated
+names in the "Rename Entity Set" column in the summary.tsv
+
+.. note::
+   The above behavior is new as of version 1.2.0. Prior to this, the variant name was just ``VARIANT{parameter}``.
 
 
 Deleting a mistake


Test patch:
diff --git a/cubids/tests/data/BIDS_Dataset/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json b/cubids/tests/data/BIDS_Dataset/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
index 14dbbc9..02b99fe 100644
--- a/cubids/tests/data/BIDS_Dataset/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
+++ b/cubids/tests/data/BIDS_Dataset/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
@@ -17,7 +17,7 @@
     "ConversionSoftware": "dcm2niix",
     "ScanningSequence": "EP",
     "Manufacturer": "Siemens",
-    "FlipAngle": 70,
+    "FlipAngle": 70.5,
     "ConversionSoftwareVersion": "v1.0.20170724 (OpenJPEG build) GCC6.1.0",
     "SeriesDescription": "ABCD_dMRI",
     "InstitutionAddress": "Spruce_Street_3400_Philadelphia_Pennsylvania_US_19104",
diff --git a/cubids/tests/data/BIDS_Dataset/sub-04/ses-phdiff/func/sub-04_ses-phdiff_task-rest_bold.json b/cubids/tests/data/BIDS_Dataset/sub-04/ses-phdiff/func/sub-04_ses-phdiff_task-rest_bold.json
index 14dbbc9..02b99fe 100644
--- a/cubids/tests/data/BIDS_Dataset/sub-04/ses-phdiff/func/sub-04_ses-phdiff_task-rest_bold.json
+++ b/cubids/tests/data/BIDS_Dataset/sub-04/ses-phdiff/func/sub-04_ses-phdiff_task-rest_bold.json
@@ -17,7 +17,7 @@
     "ConversionSoftware": "dcm2niix",
     "ScanningSequence": "EP",
     "Manufacturer": "Siemens",
-    "FlipAngle": 70,
+    "FlipAngle": 70.5,
     "ConversionSoftwareVersion": "v1.0.20170724 (OpenJPEG build) GCC6.1.0",
     "SeriesDescription": "ABCD_dMRI",
     "InstitutionAddress": "Spruce_Street_3400_Philadelphia_Pennsylvania_US_19104",
diff --git a/cubids/tests/data/inconsistent/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json b/cubids/tests/data/inconsistent/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
index 498af36..77a6cd5 100644
--- a/cubids/tests/data/inconsistent/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
+++ b/cubids/tests/data/inconsistent/sub-03/ses-phdiff/func/sub-03_ses-phdiff_task-rest_bold.json
@@ -17,7 +17,7 @@
   "ConversionSoftware": "dcm2niix",
   "ScanningSequence": "EP",
   "Manufacturer": "Siemens",
-  "FlipAngle": 70,
+  "FlipAngle": 70.5,
   "ConversionSoftwareVersion": "v1.0.20170724 (OpenJPEG build) GCC6.1.0",
   "SeriesDescription": "ABCD_dMRI",
   "InstitutionAddress": "Spruce_Street_3400_Philadelphia_Pennsylvania_US_19104",
diff --git a/cubids/tests/test_bond.py b/cubids/tests/test_bond.py
index 7b2d60a..26a44b0 100644
--- a/cubids/tests/test_bond.py
+++ b/cubids/tests/test_bond.py
@@ -509,8 +509,9 @@ def test_tsv_merge_changes(tmp_path):
 
     # TESTING RENAMES GOT APPLIED
     applied = pd.read_table(str(tmp_path / "unmodified_summary.tsv"))
-
     applied_f = pd.read_table(str(tmp_path / "unmodified_files.tsv"))
+
+    # Check for inconsistencies between FilePath and KeyParamGroup
     odd = []
     for row in range(len(applied_f)):
         if (
@@ -519,6 +520,7 @@ def test_tsv_merge_changes(tmp_path):
         ):
             odd.append((applied_f.loc[row, "FilePath"]))
 
+    # Track KeyParamGroups for files with inconsistencies
     occurrences = {}
     for row in range(len(applied_f)):
         if applied_f.loc[row, "FilePath"] in odd:
@@ -529,8 +531,10 @@ def test_tsv_merge_changes(tmp_path):
             else:
                 occurrences[applied_f.loc[row, "FilePath"]] = [applied_f.loc[row, "KeyParamGroup"]]
 
+    # Ensure no rows were lost
     assert len(orig) == len(applied)
 
+    # Check for exact matches in EntitySet
     renamed = True
     new_keys = applied["EntitySet"].tolist()
     for row in range(len(orig)):
diff --git a/cubids/tests/test_variants.py b/cubids/tests/test_variants.py
new file mode 100644
index 0000000..e8b8aea
--- /dev/null
+++ b/cubids/tests/test_variants.py
@@ -0,0 +1,81 @@
+"""Tests for variant name generation in CuBIDS.
+
+This module tests the assign_variants function which is responsible for generating
+variant names when files differ from the dominant group. The tests cover:
+
+1. Basic variant name generation
+2. Handling of cluster values (e.g., from parameter clustering)
+3. Special parameter handling (HasFieldmap, UsedAsFieldmap)
+4. Mixed parameter cases (both clustered and non-clustered parameters)
+
+The variant naming follows the format:
+    acquisition-VARIANT{parameter}{value}_
+where:
+- VARIANT indicates a deviation from the dominant group
+- {parameter} is the name of the differing parameter
+- {value} is either the cluster number or actual value
+"""
+
+import pytest
+import pandas as pd
+from cubids.utils import assign_variants
+
+
+@pytest.fixture
+def base_df():
+    """Create a basic DataFrame for testing variant assignments.
+
+    Returns
+    -------
+    pandas.DataFrame
+        A DataFrame with basic structure needed for assign_variants testing
+    """
+    return pd.DataFrame({
+        "ParamGroup": ["1", "2", "2"],
+        "EntitySet": ["task-test", "task-test", "task-test"],
+        "RenameEntitySet": ["", "", ""]
+    })
+
+
+def test_assign_variants_with_cluster_values(base_df):
+    """Test that assign_variants includes cluster values in variant names."""
+    # Add specific columns for this test
+    base_df["EchoTime"] = ["0.03", "0.05", "0.07"]
+    base_df["Cluster_EchoTime"] = ["1", "2", "3"]
+
+    # Run assign_variants
+    result = assign_variants(base_df, ["EchoTime"])
+
+    # Check that variant names include cluster values
+    assert "acquisition-VARIANTEchoTime2_" in result.loc[1, "RenameEntitySet"]
+    assert "acquisition-VARIANTEchoTime3_" in result.loc[2, "RenameEntitySet"]
+
+
+def test_assign_variants_mixed_parameters(base_df):
+    """Test assign_variants with both clustered and non-clustered parameters."""
+    # Add specific columns for this test
+    base_df["EchoTime"] = ["0.03", "0.05", "0.07"]
+    base_df["FlipAngle"] = ["90", "75", "60"]
+    base_df["Cluster_EchoTime"] = ["1", "2", "3"]
+
+    # Run assign_variants
+    result = assign_variants(base_df, ["EchoTime", "FlipAngle"])
+
+    # Check variant names include both cluster values and actual values
+    assert "acquisition-VARIANTEchoTime2FlipAngle75_" in result.loc[1, "RenameEntitySet"]
+    assert "acquisition-VARIANTEchoTime3FlipAngle60_" in result.loc[2, "RenameEntitySet"]
+
+
+def test_assign_variants_special_parameters(base_df):
+    """Test assign_variants handles special parameters correctly."""
+    # Add specific columns for this test
+    base_df["HasFieldmap"] = ["True", "False", "False"]
+    base_df["UsedAsFieldmap"] = ["False", "True", "False"]
+
+    # Run assign_variants
+    result = assign_variants(base_df, ["HasFieldmap", "UsedAsFieldmap"])
+
+    # Check special parameter handling
+    assert "acquisition-VARIANTOther_" in result.loc[0, "RenameEntitySet"]
+    assert "acquisition-VARIANTNoFmapIsUsed_" in result.loc[1, "RenameEntitySet"]
+    assert "acquisition-VARIANTNoFmap_" in result.loc[2, "RenameEntitySet"]
\ No newline at end of file