|
|
@@ -72,6 +72,7 @@ incompatible_save_bypass = false
|
|
|
|
missing_events_bypass = false
|
|
|
|
missing_events_bypass = false
|
|
|
|
analysis_clearing_bypass = false
|
|
|
|
analysis_clearing_bypass = false
|
|
|
|
folding_bypass = false
|
|
|
|
folding_bypass = false
|
|
|
|
|
|
|
|
advanced_parameters = false
|
|
|
|
|
|
|
|
|
|
|
|
[Terminal]
|
|
|
|
[Terminal]
|
|
|
|
|
|
|
|
|
|
|
@@ -84,102 +85,119 @@ SECTIONS = [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Preprocessing",
|
|
|
|
"title": "Preprocessing",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "DOWNSAMPLE", "default": True, "type": bool, "help": "Should the snirf files be downsampled? If this is set to True, DOWNSAMPLE_FREQUENCY will be used as the target frequency to downsample to."},
|
|
|
|
{"name": "DOWNSAMPLE", "default": True, "type": bool, "advanced": False, "help": "Should the snirf files be downsampled? If this is set to True, DOWNSAMPLE_FREQUENCY will be used as the target frequency to downsample to."},
|
|
|
|
{"name": "DOWNSAMPLE_FREQUENCY", "default": 25, "type": int, "depends_on": "DOWNSAMPLE", "help": "Frequency (Hz) to downsample to. If this is set higher than the input data, new data will be interpolated."},
|
|
|
|
{"name": "DOWNSAMPLE_FREQUENCY", "default": 25, "type": int, "depends_on": "DOWNSAMPLE", "advanced": False, "help": "Frequency (Hz) to downsample to. If this is set higher than the input data, new data will be interpolated."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Trimming",
|
|
|
|
"title": "Trimming",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "TRIM", "default": True, "type": bool, "help": "Should the start of the files be trimmed?"},
|
|
|
|
{"name": "TRIM", "default": True, "type": bool, "advanced": False, "help": "Should the start of the files be trimmed?"},
|
|
|
|
{"name": "SECONDS_TO_KEEP", "default": 5.0, "type": float, "depends_on": "TRIM", "help": "Seconds to keep at the beginning of all loaded snirf files before the first annotation/event occurs. Calculation is done seperatly on all loaded snirf files. Setting this to 0 will have the first annotation/event be at time point 0. Only used if TRIM is set to True."},
|
|
|
|
{"name": "SECONDS_TO_KEEP", "default": 5.0, "type": float, "advanced": False, "depends_on": "TRIM", "help": "Seconds to keep at the beginning of all loaded snirf files before the first annotation/event occurs. Calculation is done seperatly on all loaded snirf files. Setting this to 0 will have the first annotation/event be at time point 0. Only used if TRIM is set to True."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Verify Optode Placement",
|
|
|
|
"title": "Verify Optode Placement",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "OPTODE_PLACEMENT", "default": True, "type": bool, "help": "Should an image be generated for each participant outlining their optode placement on a head?"},
|
|
|
|
{"name": "OPTODE_PLACEMENT", "default": True, "type": bool, "advanced": False, "help": "Should an image be generated for each participant outlining their optode placement on a head?"},
|
|
|
|
{"name": "SHOW_OPTODE_NAMES", "default": True, "type": bool, "depends_on": "OPTODE_PLACEMENT", "help": "Should the optode names be written next to their location in the image?"},
|
|
|
|
{"name": "SHOW_OPTODE_NAMES", "default": True, "type": bool, "advanced": False, "depends_on": "OPTODE_PLACEMENT", "help": "Should the optode names be written next to their location in the image?"},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Short/Long Channels",
|
|
|
|
"title": "Short/Long Channels",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "SHORT_CHANNELS", "default": True, "type": bool, "help": "This should be set to True if the data has a short channel present in the data. For more information about short channels, please visit the Wiki."},
|
|
|
|
{"name": "SHORT_CHANNELS", "default": True, "type": bool, "advanced": False, "help": "This should be set to True if the data has a short channel present in the data. For more information about short channels, please visit the Wiki."},
|
|
|
|
{"name": "SHORT_CHANNELS_THRESHOLD", "default": 0.015, "type": float, "depends_on": "SHORT_CHANNELS", "help": "The maximum distance the short channel can be in metres before it is no longer considered a short channel."},
|
|
|
|
{"name": "LONG_CHANNELS", "default": True, "type": bool, "advanced": True, "help": "Should channels exceeding the maximum allowed distance be removed?"},
|
|
|
|
{"name": "LONG_CHANNELS_THRESHOLD", "default": 0.045, "type": float, "help": "The maximum distance channels can be in metres. Any channel longer than this distance will be discarded."},
|
|
|
|
{"name": "SHORT_CHANNELS_THRESHOLD", "default": 0.015, "type": float, "depends_on": "SHORT_CHANNELS", "advanced": False, "help": "The maximum distance the short channel can be in metres before it is no longer considered a short channel."},
|
|
|
|
|
|
|
|
{"name": "LONG_CHANNELS_THRESHOLD", "default": 0.045, "type": float, "advanced": False, "help": "The maximum distance channels can be in metres. Any channel longer than this distance will be discarded."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Heart Rate",
|
|
|
|
"title": "Heart Rate",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "HEART_RATE", "default": True, "type": bool, "help": "Should an attempt be made to calculate the participants heart rate?"},
|
|
|
|
{"name": "HEART_RATE", "default": True, "type": bool, "advanced": False, "help": "Should an attempt be made to calculate the participants heart rate?"},
|
|
|
|
{"name": "SECONDS_TO_STRIP_HR", "default": 5, "type": int, "depends_on": "HEART_RATE", "help": "Will remove this many seconds from the start and end of the file. Useful if recording before cap is firmly placed, or participant removes cap while still recording."},
|
|
|
|
{"name": "SECONDS_TO_STRIP_HR", "default": 5, "type": int, "depends_on": "HEART_RATE", "advanced": False, "help": "Will remove this many seconds from the start and end of the file. Useful if recording before cap is firmly placed, or participant removes cap while still recording."},
|
|
|
|
{"name": "MAX_LOW_HR", "default": 40, "type": int, "depends_on": "HEART_RATE", "help": "Any heart rate windows that average below this value will be rounded up to this value."},
|
|
|
|
{"name": "HR_LOW_FREQ", "default": 0.8, "type": float, "depends_on": "HEART_RATE", "advanced": True, "help": "Lower frequency bound for heart rate detection (Hz). Used to isolate cardiac frequencies before peak detection."},
|
|
|
|
{"name": "MAX_HIGH_HR", "default": 200, "type": int, "depends_on": "HEART_RATE", "help": "Any heart rate windows that average above this value will be rounded down to this value."},
|
|
|
|
{"name": "HR_HIGH_FREQ", "default": 2.5, "type": float, "depends_on": "HEART_RATE", "advanced": True, "help": "Upper frequency bound for heart rate detection (Hz). Used to isolate cardiac frequencies before peak detection."},
|
|
|
|
{"name": "SMOOTHING_WINDOW_HR", "default": 100, "type": int, "depends_on": "HEART_RATE", "help": "How many individual data points to be used to create a single data point/window."},
|
|
|
|
{"name": "HR_SEARCH_MIN", "default": 30, "type": int, "depends_on": "HEART_RATE", "advanced": True, "help": "Minimum heart rate considered during spectral analysis (BPM)."},
|
|
|
|
{"name": "HEART_RATE_WINDOW", "default": 25, "type": int, "depends_on": "HEART_RATE", "help": "Only used for visualization. Shows the 'range' of the calculated heart rate, which is just the average +- this value."},
|
|
|
|
{"name": "HR_SEARCH_MAX", "default": 200, "type": int, "depends_on": "HEART_RATE", "advanced": True, "help": "Maximum heart rate considered during spectral analysis (BPM)."},
|
|
|
|
|
|
|
|
{"name": "MAX_LOW_HR", "default": 40, "type": int, "depends_on": "HEART_RATE", "advanced": False, "help": "Any heart rate windows that average below this value will be rounded up to this value."},
|
|
|
|
|
|
|
|
{"name": "MAX_HIGH_HR", "default": 200, "type": int, "depends_on": "HEART_RATE", "advanced": False, "help": "Any heart rate windows that average above this value will be rounded down to this value."},
|
|
|
|
|
|
|
|
{"name": "SMOOTHING_WINDOW_HR", "default": 100, "type": int, "depends_on": "HEART_RATE", "advanced": True, "help": "Number of individual heart rate samples used to create each smoothed value."},
|
|
|
|
|
|
|
|
{"name": "HEART_RATE_WINDOW", "default": 25, "type": int, "depends_on": "HEART_RATE", "advanced": False, "help": "Visualization window around the estimated heart rate (BPM)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Scalp Coupling Index",
|
|
|
|
"title": "Scalp Coupling Index",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "SCI", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Scalp Coupling Index. This metric calculates the quality of the connection between the optode and the scalp."},
|
|
|
|
{"name": "SCI", "default": True, "type": bool, "advanced": False, "help": "Calculate and mark channels bad based on their Scalp Coupling Index. This metric calculates the quality of the connection between the optode and the scalp."},
|
|
|
|
{"name": "SCI_TIME_WINDOW", "default": 3, "type": int, "depends_on": "SCI", "help": "Independent SCI calculations will be perfomed in a time window for the duration of the value provided, until the end of the file is reached."},
|
|
|
|
{"name": "SCI_USE_HEART_RATE_BAND", "default": True, "type": bool, "depends_on": "SCI", "advanced": False, "help": "Adjust the SCI frequency band using the participant's estimated heart rate."},
|
|
|
|
{"name": "SCI_THRESHOLD", "default": 0.6, "type": float, "depends_on": "SCI", "help": "SCI threshold on a scale of 0-1. A value of 0 is bad coupling while a value of 1 is perfect coupling. Any channels lower than this value will be marked as bad."},
|
|
|
|
{"name": "SCI_LOW_FREQ", "default": 0.7, "type": float, "depends_on": "SCI_USE_HEART_RATE_BAND", "depends_value": False,"advanced": True, "help": "Lower frequency cutoff for SCI bandpass filtering (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SCI_HIGH_FREQ", "default": 1.5, "type": float, "depends_on": "SCI_USE_HEART_RATE_BAND", "depends_value": False, "advanced": True, "help": "Upper frequency cutoff for SCI bandpass filtering (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SCI_TIME_WINDOW", "default": 3, "type": int, "depends_on": "SCI", "advanced": False, "help": "Duration of each independent SCI calculation window in seconds."},
|
|
|
|
|
|
|
|
{"name": "SCI_THRESHOLD", "default": 0.6, "type": float, "depends_on": "SCI", "advanced": False, "help": "SCI threshold on a scale of 0-1. Channels below this value are marked bad."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Signal to Noise Ratio",
|
|
|
|
"title": "Signal to Noise Ratio",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "SNR", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Signal to Noise Ratio. This metric calculates how much of the observed signal was noise versus how much of it was a useful signal."},
|
|
|
|
{"name": "SNR", "default": True, "type": bool, "advanced": False, "help": "Calculate and mark channels bad based on their Signal to Noise Ratio. This metric calculates how much of the observed signal was noise versus how much of it was a useful signal."},
|
|
|
|
{"name": "SNR_THRESHOLD", "default": 5.0, "type": float, "depends_on": "SNR", "help": "SNR threshold (dB). A typical scale would be 0-25, but it is possible for values to be both above and below this range. Higher values correspond to a better signal. If SNR is True, any channels lower than this value will be marked as bad."},
|
|
|
|
{"name": "SNR_THRESHOLD", "default": 5.0, "type": float, "depends_on": "SNR", "advanced": False, "help": "SNR threshold (dB). Channels below this value will be marked as bad."},
|
|
|
|
|
|
|
|
{"name": "SNR_SIGNAL_LOW_FREQ", "default": 0.01, "type": float, "depends_on": "SNR", "advanced": True, "help": "Lower frequency bound for the signal band used in SNR calculation (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SNR_SIGNAL_HIGH_FREQ", "default": 0.5, "type": float, "depends_on": "SNR", "advanced": True, "help": "Upper frequency bound for the signal band used in SNR calculation (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SNR_NOISE_LOW_FREQ", "default": 1.0, "type": float, "depends_on": "SNR", "advanced": True, "help": "Lower frequency bound for the noise band used in SNR calculation (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SNR_NOISE_HIGH_FREQ", "default": 10.0, "type": float, "depends_on": "SNR", "advanced": True, "help": "Upper frequency bound for the noise band used in SNR calculation (Hz)."},
|
|
|
|
|
|
|
|
{"name": "SNR_PLOT_MAX", "default": 20.0, "type": float, "depends_on": "SNR", "advanced": True, "help": "Maximum SNR value displayed on the plot scale (dB)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Peak Spectral Power",
|
|
|
|
"title": "Peak Spectral Power",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "PSP", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Peak Spectral Power. This metric calculates the amplitude or strength of a frequency component that is most prominent in a particular frequency range or spectrum."},
|
|
|
|
{"name": "PSP", "default": True, "type": bool, "advanced": False, "help": "Calculate and mark channels bad based on their Peak Spectral Power. This metric calculates the amplitude or strength of the most prominent frequency component in a specified spectral range."},
|
|
|
|
{"name": "PSP_TIME_WINDOW", "default": 3, "type": int, "depends_on": "PSP", "help": "Independent PSP calculations will be perfomed in a time window for the duration of the value provided, until the end of the file is reached."},
|
|
|
|
{"name": "PSP_TIME_WINDOW", "default": 3, "type": int, "depends_on": "PSP", "advanced": False, "help": "Length of each independent PSP calculation window in seconds."},
|
|
|
|
{"name": "PSP_THRESHOLD", "default": 0.1, "type": float, "depends_on": "PSP", "help": "PSP threshold. A typical scale would be 0-0.5, but it is possible for values to be above this range. Higher values correspond to a better signal. If PSP is True, any channels lower than this value will be marked as bad."},
|
|
|
|
{"name": "PSP_THRESHOLD", "default": 0.1, "type": float, "depends_on": "PSP", "advanced": False, "help": "Channels with average PSP values below this threshold will be marked as bad."},
|
|
|
|
|
|
|
|
{"name": "PSP_LOW_FREQ", "default": 0.7, "type": float, "depends_on": "PSP", "advanced": True, "help": "Lower frequency cutoff for PSP bandpass filtering (Hz)."},
|
|
|
|
|
|
|
|
{"name": "PSP_HIGH_FREQ", "default": 1.5, "type": float, "depends_on": "PSP", "advanced": True, "help": "Upper frequency cutoff for PSP bandpass filtering (Hz)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Coefficient of Variation",
|
|
|
|
"title": "Coefficient of Variation",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "COEFF_VAR", "default": True, "type": bool, "help": "Identifies bad channels using the Coefficient of Variation."},
|
|
|
|
{"name": "COEFF_VAR", "default": True, "type": bool, "advanced": False, "help": "Identifies bad channels using the Coefficient of Variation."},
|
|
|
|
{"name": "COEFF_VAR_THRESHOLD", "default": 20, "type": int, "depends_on": "COEFF_VAR", "help": "Noise threshold (%)."},
|
|
|
|
{"name": "COEFF_VAR_THRESHOLD", "default": 20, "type": int, "depends_on": "COEFF_VAR", "advanced": False, "help": "Noise threshold (%)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Median Absolute Deviation",
|
|
|
|
"title": "Median Absolute Deviation",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "MAD", "default": True, "type": bool, "help": "Identifies bad channels using Median Absolute Deviation."},
|
|
|
|
{"name": "MAD", "default": True, "type": bool, "advanced": False, "help": "Identifies bad channels using Median Absolute Deviation."},
|
|
|
|
{"name": "MAD_THRESHOLD", "default": 4, "type": int, "depends_on": "MAD", "help": "Amount of deviations before the channel is flagged bad."},
|
|
|
|
{"name": "MAD_THRESHOLD", "default": 4, "type": int, "depends_on": "MAD", "advanced": False, "help": "Amount of deviations before the channel is flagged bad."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Power Spectral Density Noise",
|
|
|
|
"title": "Power Spectral Density Noise",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "PSD_NOISE", "default": True, "type": bool, "help": "Identifies bad channels based on their excessive power at high frequencies."},
|
|
|
|
{"name": "PSD_NOISE", "default": True, "type": bool, "advanced": False, "help": "Identifies bad channels based on excessive power at high frequencies."},
|
|
|
|
{"name": "TARGET_FREQ_DIV", "default": 4, "type": int, "depends_on": "PSD_NOISE", "help": "What frequency to check for excessive power. Will take the recording frequency and divide by this number. Has to be greater than 2."},
|
|
|
|
{"name": "TARGET_FREQ_DIV", "default": 4, "type": int, "depends_on": "PSD_NOISE", "advanced": False, "help": "Target frequency is calculated by dividing the recording frequency by this value. Must be greater than 2."},
|
|
|
|
{"name": "DB_LIMIT", "default": -60, "type": int, "depends_on": "PSD_NOISE", "help": "What db level the power level needs to be below at the target frequency."},
|
|
|
|
{"name": "DB_LIMIT", "default": -60, "type": float, "depends_on": "PSD_NOISE", "advanced": False, "help": "Power threshold in dB. Channels exceeding this value near the target frequency are marked bad."},
|
|
|
|
|
|
|
|
{"name": "PSD_MIN_FREQ", "default": 0.1, "type": float, "depends_on": "PSD_NOISE", "advanced": True, "help": "Minimum frequency included when calculating the PSD (Hz)."},
|
|
|
|
|
|
|
|
{"name": "PSD_TARGET_BANDWIDTH", "default": 0.2, "type": float, "depends_on": "PSD_NOISE", "advanced": True, "help": "Frequency window around the target frequency used when averaging PSD power (Hz)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Sensor Dropout",
|
|
|
|
"title": "Sensor Dropout",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "SENSOR_DROPOUT", "default": True, "type": bool, "help": "Identifies bad channels based on comparing the variance of the first 25% of the data to the last 25%."},
|
|
|
|
{"name": "SENSOR_DROPOUT", "default": True, "type": bool, "advanced": False, "help": "Identifies bad channels based on comparing the variance of the first 25% of the data to the last 25%."},
|
|
|
|
{"name": "SENSOR_DROPOUT_VARIANCE_THRESHOLD", "default": 0.05, "type": float, "depends_on": "SENSOR_DROPOUT", "help": "If the end variance is less than this % of the start variance, the channel will be marked as bad."},
|
|
|
|
{"name": "SENSOR_DROPOUT_VARIANCE_THRESHOLD", "default": 0.05, "type": float, "depends_on": "SENSOR_DROPOUT", "advanced": False, "help": "If the end variance is less than this % of the start variance, the channel will be marked as bad."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Bad Channels Handling",
|
|
|
|
"title": "Bad Channels Handling",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "BAD_CHANNELS_HANDLING", "default": ["Interpolate"], "type": list, "options": ["Interpolate", "Remove", "None"], "exclusive": True, "help": "How should we deal with the bad channels that occurred? Note: Some analysis options will only work when this is set to 'Interpolate'."},
|
|
|
|
{"name": "BAD_CHANNELS_HANDLING", "default": ["Interpolate"], "type": list, "options": ["Interpolate", "Remove", "None"], "exclusive": True, "advanced": False, "help": "How should we deal with the bad channels that occurred? Note: Some analysis options will only work when this is set to 'Interpolate'."},
|
|
|
|
{"name": "MAX_DIST", "default": 0.03, "type": float, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Interpolate", "help": "The maximum distance to look for neighbours when interpolating. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
|
|
|
{"name": "MAX_DIST", "default": 0.03, "type": float, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Interpolate", "advanced": True, "help": "The maximum distance to look for neighbours when interpolating. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
|
|
|
{"name": "MIN_NEIGHBORS", "default": 2, "type": int, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Interpolate", "help": "The minimumn amount of neighbours needed within the MAX_DIST parameter. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
|
|
|
{"name": "MIN_NEIGHBORS", "default": 2, "type": int, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Interpolate", "advanced": True, "help": "The minimumn amount of neighbours needed within the MAX_DIST parameter. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
|
|
|
{"name": "MAX_BAD_CHANNELS", "default": 12, "type": int, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Remove", "help": "Maximum amount of bad channels before the participant as a whole is marked as bad (exclusive). If this occurs, the participant will be prevented from processing any further. Used only when BAD_CHANNELS_HANDLING is set to 'Remove'."},
|
|
|
|
{"name": "MAX_BAD_CHANNELS", "default": 12, "type": int, "depends_on": "BAD_CHANNELS_HANDLING", "depends_value": "Remove", "advanced": False, "help": "Maximum amount of bad channels before the participant as a whole is marked as bad (exclusive). If this occurs, the participant will be prevented from processing any further. Used only when BAD_CHANNELS_HANDLING is set to 'Remove'."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
@@ -191,105 +209,119 @@ SECTIONS = [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Temporal Derivative Distribution Repair filtering",
|
|
|
|
"title": "Temporal Derivative Distribution Repair filtering",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "TDDR", "default": True, "type": bool, "help": "Apply Temporal Derivitave Distribution Repair filtering - a method that removes baseline shift and spike artifacts from the data."},
|
|
|
|
{"name": "TDDR", "default": True, "type": bool, "advanced": False, "help": "Apply Temporal Derivitave Distribution Repair filtering - a method that removes baseline shift and spike artifacts from the data."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Wavelet filtering",
|
|
|
|
"title": "Wavelet filtering",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "WAVELET", "default": True, "type": bool, "help": "Apply Wavelet filtering. It is a method to filter involving decomposition, threholding, and reconstruction."},
|
|
|
|
{"name": "WAVELET", "default": True, "type": bool, "advanced": False, "help": "Apply Wavelet filtering. It is a method to filter involving decomposition, threholding, and reconstruction."},
|
|
|
|
{"name": "IQR", "default": 1.5, "type": float, "depends_on": "WAVELET", "help": "Scaling factor for the Inter-Quartile Range."},
|
|
|
|
{"name": "IQR", "default": 1.5, "type": float, "depends_on": "WAVELET", "advanced": False, "help": "Scaling factor for the Inter-Quartile Range."},
|
|
|
|
{"name": "WAVELET_TYPE", "default": "db4", "type": str, "depends_on": "WAVELET", "help": "Wavelet type. Valid values are ['bior1.1', 'bior1.3', 'bior1.5', 'bior2.2', 'bior2.4', 'bior2.6', 'bior2.8', 'bior3.1', 'bior3.3', 'bior3.5', 'bior3.7', 'bior3.9', 'bior4.4', 'bior5.5', 'bior6.8', 'coif1', 'coif2', 'coif3', 'coif4', 'coif5', 'coif6', 'coif7', 'coif8', 'coif9', 'coif10', 'coif11', 'coif12', 'coif13', 'coif14', 'coif15', 'coif16', 'coif17', 'db1', 'db2', 'db3', 'db4', 'db5', 'db6', 'db7', 'db8', 'db9', 'db10', 'db11', 'db12', 'db13', 'db14', 'db15', 'db16', 'db17', 'db18', 'db19', 'db20', 'db21', 'db22', 'db23', 'db24', 'db25', 'db26', 'db27', 'db28', 'db29', 'db30', 'db31', 'db32', 'db33', 'db34', 'db35', 'db36', 'db37', 'db38', 'dmey', 'haar', 'rbio1.1', 'rbio1.3', 'rbio1.5', 'rbio2.2', 'rbio2.4', 'rbio2.6', 'rbio2.8', 'rbio3.1', 'rbio3.3', 'rbio3.5', 'rbio3.7', 'rbio3.9', 'rbio4.4', 'rbio5.5', 'rbio6.8', 'sym2', 'sym3', 'sym4', 'sym5', 'sym6', 'sym7', 'sym8', 'sym9', 'sym10', 'sym11', 'sym12', 'sym13', 'sym14', 'sym15', 'sym16', 'sym17', 'sym18', 'sym19', 'sym20']"},
|
|
|
|
{"name": "WAVELET_TYPE", "default": "db4", "type": str, "depends_on": "WAVELET", "advanced": False, "help": "Wavelet type. Valid values are ['bior1.1', 'bior1.3', 'bior1.5', 'bior2.2', 'bior2.4', 'bior2.6', 'bior2.8', 'bior3.1', 'bior3.3', 'bior3.5', 'bior3.7', 'bior3.9', 'bior4.4', 'bior5.5', 'bior6.8', 'coif1', 'coif2', 'coif3', 'coif4', 'coif5', 'coif6', 'coif7', 'coif8', 'coif9', 'coif10', 'coif11', 'coif12', 'coif13', 'coif14', 'coif15', 'coif16', 'coif17', 'db1', 'db2', 'db3', 'db4', 'db5', 'db6', 'db7', 'db8', 'db9', 'db10', 'db11', 'db12', 'db13', 'db14', 'db15', 'db16', 'db17', 'db18', 'db19', 'db20', 'db21', 'db22', 'db23', 'db24', 'db25', 'db26', 'db27', 'db28', 'db29', 'db30', 'db31', 'db32', 'db33', 'db34', 'db35', 'db36', 'db37', 'db38', 'dmey', 'haar', 'rbio1.1', 'rbio1.3', 'rbio1.5', 'rbio2.2', 'rbio2.4', 'rbio2.6', 'rbio2.8', 'rbio3.1', 'rbio3.3', 'rbio3.5', 'rbio3.7', 'rbio3.9', 'rbio4.4', 'rbio5.5', 'rbio6.8', 'sym2', 'sym3', 'sym4', 'sym5', 'sym6', 'sym7', 'sym8', 'sym9', 'sym10', 'sym11', 'sym12', 'sym13', 'sym14', 'sym15', 'sym16', 'sym17', 'sym18', 'sym19', 'sym20']"},
|
|
|
|
{"name": "WAVELET_LEVEL", "default": 3, "type": int, "depends_on": "WAVELET", "help": "Wavelet Decomposition level (must be >= 0)."},
|
|
|
|
{"name": "WAVELET_LEVEL", "default": 3, "type": int, "depends_on": "WAVELET", "advanced": False, "help": "Wavelet Decomposition level (must be >= 0)."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Haemoglobin Concentration",
|
|
|
|
"title": "Haemoglobin Concentration",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
# NOTE: Intentionally empty
|
|
|
|
{"name": "OVERRIDE_PPF", "default": False, "type": bool, "advanced": True, "help": "Override the dynamic PPF calculation based on age to instead use the same values for all participants."},
|
|
|
|
# TODO: Manual override of PPF?
|
|
|
|
{"name": "PPF_LOWER_WAVELENGTH", "default": 6.0, "type": float, "advanced": True, "depends_on": "OVERRIDE_PPF", "help": "PPF value to use for the lower wavelength."},
|
|
|
|
|
|
|
|
{"name": "PPF_UPPER_WAVELENGTH", "default": 6.0, "type": float, "advanced": True, "depends_on": "OVERRIDE_PPF", "help": "PPF value to use for the upper wavelength."}
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Enhance Negative Correlation",
|
|
|
|
"title": "Enhance Negative Correlation",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "ENHANCE_NEGATIVE_CORRELATION", "default": False, "type": bool, "help": "Apply Enhance Negative Correlation."},
|
|
|
|
{"name": "ENHANCE_NEGATIVE_CORRELATION", "default": False, "type": bool, "advanced": False, "help": "Apply Enhance Negative Correlation."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Filtering",
|
|
|
|
"title": "Filtering",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "FILTER", "default": True, "type": bool, "help": "Should the data be bandpass filtered?"},
|
|
|
|
{"name": "FILTER", "default": True, "type": bool, "advanced": False, "help": "Should the data be bandpass filtered?"},
|
|
|
|
{"name": "L_FREQ", "default": 0.005, "type": float, "depends_on": "FILTER", "help": "Any frequencies lower than this value will be removed."},
|
|
|
|
{"name": "FILTER_ALGORITHM", "default": ["FIR"], "type": list, "options": ["FIR", "IIR"], "exclusive": True, "advanced": False, "help": "Filtering algorithm."},
|
|
|
|
{"name": "H_FREQ", "default": 0.3, "type": float, "depends_on": "FILTER", "help": "Any frequencies higher than this value will be removed."},
|
|
|
|
{"name": "L_FREQ", "default": 0.005, "type": float, "depends_on": "FILTER", "advanced": False, "help": "Any frequencies lower than this value will be removed."},
|
|
|
|
{"name": "L_TRANS_BANDWIDTH", "default": 0.002, "type": float, "depends_on": "FILTER", "help": "How wide the transitional period should be so the data doesn't just drop off on the lower bound."},
|
|
|
|
{"name": "H_FREQ", "default": 0.3, "type": float, "depends_on": "FILTER", "advanced": False, "help": "Any frequencies higher than this value will be removed."},
|
|
|
|
{"name": "H_TRANS_BANDWIDTH", "default": 0.002, "type": float, "depends_on": "FILTER", "help": "How wide the transitional period should be so the data doesn't just drop off on the upper bound."},
|
|
|
|
{"name": "L_TRANS_BANDWIDTH", "default": 0.002, "type": float, "depends_on": "FILTER", "advanced": True, "help": "Width of the lower transition band to prevent abrupt filter cutoff."},
|
|
|
|
|
|
|
|
{"name": "H_TRANS_BANDWIDTH", "default": 0.002, "type": float, "depends_on": "FILTER", "advanced": True, "help": "Width of the upper transition band to prevent abrupt filter cutoff."},
|
|
|
|
|
|
|
|
{"name": "IIR_TYPE", "default": ["butterworth"], "type": list, "options": ["butterworth", "chebyshev1", "chebyshev2", "elliptic", "bessel"], "exclusive": True, "depends_on": "FILTER_ALGORITHM", "depends_value": "IIR", "advanced": True, "help": "IIR filter design."},
|
|
|
|
|
|
|
|
{"name": "IIR_ORDER", "default": 4, "type": int, "depends_on": "FILTER_ALGORITHM", "depends_value": "IIR", "advanced": True, "help": "Order of the IIR filter."},
|
|
|
|
|
|
|
|
{"name": "FILTER_LENGTH", "default": "auto", "type": str, "depends_on": "FILTER_ALGORITHM", "depends_value": "FIR", "advanced": True, "help": "Length of the FIR filter. 'auto' allows automatic selection."},
|
|
|
|
|
|
|
|
{"name": "FILTER_PHASE", "default": ["zero"], "type": list, "options": ["zero", "zero-double", "minimum", "minimum-half"], "exclusive": True, "depends_on": "FILTER_ALGORITHM", "depends_value": "FIR", "advanced": True, "help": "Phase response of the FIR filter."},
|
|
|
|
|
|
|
|
{"name": "FIR_WINDOW", "default": ["hamming"], "type": list, "options": ["hamming", "hann", "blackman"], "exclusive": True, "depends_on": "FILTER_ALGORITHM", "depends_value": "FIR", "advanced": True, "help": "Window function used when designing the FIR filter."},
|
|
|
|
|
|
|
|
{"name": "FIR_DESIGN", "default": ["firwin"], "type": list, "options": ["firwin", "firwin2"], "exclusive": True, "depends_on": "FILTER_ALGORITHM", "depends_value": "FIR", "advanced": True, "help": "Method used to design the FIR filter."},
|
|
|
|
|
|
|
|
{"name": "IIR_OUTPUT", "default": ["sos"], "type": list, "options": ["sos", "ba", "zpk"], "exclusive": True, "depends_on": "FILTER_ALGORITHM", "depends_value": "IIR", "advanced": True, "help": "Representation used for IIR filter coefficients."},
|
|
|
|
|
|
|
|
{"name": "PASSBAND_RIPPLE", "default": 1.0, "type": float, "depends_on": "IIR_TYPE", "depends_value": ["chebyshev1", "elliptic"], "advanced": True, "help": "Maximum allowed ripple in the passband (dB)."},
|
|
|
|
|
|
|
|
{"name": "STOPBAND_ATTENUATION", "default": 40.0, "type": float, "depends_on": "IIR_TYPE", "depends_value": ["chebyshev2", "elliptic"], "advanced": True, "help": "Minimum attenuation in the stopband (dB)."},
|
|
|
|
|
|
|
|
{"name": "FILTER_PAD", "default": ["reflect_limited"], "type": list, "options": ["reflect_limited", "reflect", "edge", "constant"], "exclusive": True, "depends_on": "FILTER", "advanced": True, "help": "Padding strategy used during filtering to reduce edge artifacts."},
|
|
|
|
|
|
|
|
{"name": "SKIP_BY_ANNOTATION", "default": ["edge", "bad_acq_skip"], "type": list, "depends_on": "FILTER", "advanced": True, "help": "Annotations that should be skipped when applying the filter."},
|
|
|
|
|
|
|
|
{"name": "FILTER_N_JOBS", "default": 1, "type": int, "advanced": True, "help": "Number of parallel jobs used during filtering. Use -1 to use all available CPUs."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Extracting Events*",
|
|
|
|
"title": "Extracting Events*",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
|
|
|
{"name": "EVENTS", "default": True, "type": bool, "advanced": False, "help": "Extract events from annotations for visualization and downstream event-based analysis."},
|
|
|
|
|
|
|
|
{"name": "EVENT_ID", "default": "auto", "type": str, "advanced": True, "help": "Controls how annotation descriptions are converted into event identifiers. Use 'auto' for automatic event detection."},
|
|
|
|
|
|
|
|
{"name": "EVENT_REGEX", "default": "^(?![Bb][Aa][Dd]|[Ee][Dd][Gg][Ee]).*$", "type": str, "advanced": True, "help": "Regular expression used to select which annotations are converted into events. By default, bad and edge annotations are ignored."},
|
|
|
|
|
|
|
|
{"name": "EVENT_CHUNK_DURATION", "default": None, "type": float, "advanced": True, "help": "If provided, creates repeated events at this interval within longer annotations instead of only using annotation onset times."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Epoch Calculations",
|
|
|
|
"title": "Epoch Calculations",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
# TODO: implement drop
|
|
|
|
{"name": "EPOCHS", "default": True, "type": bool, "advanced": False, "help": "Create epochs around extracted events for condition-based analysis."},
|
|
|
|
{"name": "EPOCH_HANDLING", "default": ["shift"], "type": list, "options": ["shift", "strict"], "help": "What to do if two unique events occur at the same time. Shift will automatically move one event to the first valid free index. Strict will raise an error processing the file. Drop will remove one of the events."},
|
|
|
|
{"name": "EPOCH_HANDLING", "default": ["shift"], "type": list, "options": ["shift", "strict", "drop"], "exclusive": True, "advanced": False, "help": "How to handle events occurring at the same sample. Shift moves conflicting events forward, strict raises an error, and drop removes conflicting events."},
|
|
|
|
{"name": "MAX_SHIFT", "default": 5, "type": int, "depends_on": "EPOCH_HANDLING", "depends_value": "shift", "help": "Amount of indexes to look ahead and see if there is a valid one to shift to. If none were found, will fall back to 'strict' behaviour."},
|
|
|
|
{"name": "MAX_SHIFT", "default": 5, "type": int, "depends_on": "EPOCH_HANDLING", "depends_value": "shift", "advanced": True, "help": "Maximum number of samples to shift conflicting events before failing."},
|
|
|
|
#{"name": "REJECT_BY_ANNOTATIONS", "default": True, "type": bool, "help": "Help."},
|
|
|
|
{"name": "T_MIN", "default": -5.0, "type": float, "advanced": False, "help": "Time in seconds before each event to include in the epoch."},
|
|
|
|
#{"name": "MAX_SHIFT", "default": 5, "type": int, "depends_on": "EPOCH_HANDLING", "depends_value": "shift", "help": "Amount of indexes to look ahead and see if there is a valid one to shift to. If none were found, will fall back to 'strict' behaviour."},
|
|
|
|
{"name": "T_MAX", "default": 15.0, "type": float, "advanced": False, "help": "Time in seconds after each event to include in the epoch."},
|
|
|
|
#{"name": "MAX_SHIFT", "default": 5, "type": int, "depends_on": "EPOCH_HANDLING", "depends_value": "shift", "help": "Amount of indexes to look ahead and see if there is a valid one to shift to. If none were found, will fall back to 'strict' behaviour."},
|
|
|
|
{"name": "BASELINE", "default": ["pre_event"], "type": list, "options": ["none", "pre_event"], "exclusive": True, "advanced": False, "help": "Baseline correction applied to epochs. Pre-event uses the period before the event as baseline."},
|
|
|
|
{"name": "T_MIN", "default": -5, "type": int, "help": "Seconds before the epoch to be used."},
|
|
|
|
{"name": "REJECT_EPOCHS", "default": True, "type": bool, "advanced": False, "help": "Automatically reject epochs containing excessively large haemoglobin amplitude changes."},
|
|
|
|
{"name": "T_MAX", "default": 15, "type": int, "help": "Seconds after the epoch to be used."},
|
|
|
|
{"name": "REJECT_HBO_THRESHOLD", "default": 80e-7, "type": float, "depends_on": "REJECT_EPOCHS", "advanced": True, "help": "Maximum allowed HbO amplitude before an epoch is rejected."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Design Matrix",
|
|
|
|
"title": "Design Matrix",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "RESAMPLE", "default": True, "type": bool, "help": "Should the data be resampled before calculating the design matrix? Downsampling is useful for speeding up calculations without losing overall data shape."},
|
|
|
|
{"name": "RESAMPLE", "default": True, "type": bool, "advanced": False, "help": "Resample the data before creating the design matrix. Lower frequencies can reduce computation time while preserving the overall signal shape."},
|
|
|
|
{"name": "RESAMPLE_FREQ", "default": 1, "type": int, "help": "The frequency the data should be resampled to."},
|
|
|
|
{"name": "RESAMPLE_FREQ", "default": 1, "type": int, "depends_on": "RESAMPLE", "advanced": False, "help": "Sampling frequency (Hz) used when resampling the data before design matrix calculation."},
|
|
|
|
{"name": "HRF_MODEL", "default": ["fir"], "type": list, "options": ["fir", "glover", "spm", "spm + derivative", "spm + derivative + dispersion", "glover + derivative", "glover + derivative + dispersion"], "exclusive": True, "help": "Specifies the haemodynamic response function."},
|
|
|
|
{"name": "HRF_MODEL", "default": ["fir"], "type": list, "options": ["fir", "glover", "spm", "spm + derivative", "spm + derivative + dispersion", "glover + derivative", "glover + derivative + dispersion"], "exclusive": True, "advanced": False, "help": "Haemodynamic response function model used to create regressors from event timings."},
|
|
|
|
{"name": "STIM_DUR", "default": 0.5, "type": float, "help": "The length of your stimulus. If your HRF_MODEL is fir, this dictates how wide a bin should be."},
|
|
|
|
{"name": "STIM_DUR", "default": 0.5, "type": float, "advanced": False, "help": "Expected duration of each stimulus/event in seconds. For FIR models, determines the width of each event bin."},
|
|
|
|
{"name": "DRIFT_MODEL", "default": ["cosine"], "type": list, "options": ["cosine", "polynomial"], "help": "Specifies the desired drift model."},
|
|
|
|
{"name": "FIR_DELAYS", "default": 15, "type": range, "depends_on": "HRF_MODEL", "depends_value": "fir", "advanced": True, "help": "Number of delayed regressors used for FIR models. Defines how long after an event the response is modelled."},
|
|
|
|
{"name": "HIGH_PASS", "default": 0.01, "type": float, "help": "High-pass frequency in case of a cosine model (in Hz)."},
|
|
|
|
{"name": "DRIFT_MODEL", "default": ["cosine"], "type": list, "options": ["cosine", "polynomial"], "exclusive": True, "advanced": True, "help": "Model used to account for slow baseline signal drift."},
|
|
|
|
{"name": "DRIFT_ORDER", "default": 1, "type": int, "help": "Order of the drift model (in case it is polynomial)"},
|
|
|
|
{"name": "HIGH_PASS", "default": 0.01, "type": float, "depends_on": "DRIFT_MODEL", "depends_value": "cosine", "advanced": True, "help": "High-pass cutoff frequency (Hz) for cosine drift removal."},
|
|
|
|
{"name": "FIR_DELAYS", "default": 15, "type": range, "depends_on": "HRF_MODEL", "depends_value": "fir", "help": "In case of FIR design, yields the array of delays used in the FIR model (in scans)."},
|
|
|
|
{"name": "DRIFT_ORDER", "default": 1, "type": int, "depends_on": "DRIFT_MODEL", "depends_value": "polynomial", "advanced": True, "help": "Polynomial order used to model slow drift."},
|
|
|
|
{"name": "MIN_ONSET", "default": -24, "type": int, "help": "Minimal onset relative to frame times (in seconds)"},
|
|
|
|
{"name": "MIN_ONSET", "default": -24, "type": int, "advanced": True, "help": "Minimum event onset relative to the sampled frame times in seconds."},
|
|
|
|
{"name": "OVERSAMPLING", "default": 50, "type": int, "help": "Oversampling factor used in temporal convolutions."},
|
|
|
|
{"name": "OVERSAMPLING", "default": 50, "type": int, "advanced": True, "help": "Temporal oversampling factor used during HRF convolution."},
|
|
|
|
# TODO: Re-implement this without causing a memory leak
|
|
|
|
{"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "depends_on": "SHORT_CHANNELS", "advanced": False, "help": "Add short channel signals to the design matrix as nuisance regressors to reduce superficial physiological noise."},
|
|
|
|
# {"name": "REMOVE_EVENTS", "default": "None", "type": list, "help": "Remove events matching the names provided before generating the Design Matrix"},
|
|
|
|
|
|
|
|
{"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "depends_on": "SHORT_CHANNELS", "help": "Should short channel regression be used to create the design matrix? This will use the 'signal' from the short channel and regress it out of all other channels."},
|
|
|
|
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "General Linear Model",
|
|
|
|
"title": "General Linear Model",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "NOISE_MODEL", "default": "ar1", "type": str, "help": "The temporal variance model. Defaults to first order auto regressive model 'ar1'. The AR model can be set to any integer value by modifying the value of N. E.g. use ar5 for a fifth order model. If the string auto is provided a model with order 4 times the sample rate will be used."},
|
|
|
|
{"name": "NOISE_MODEL", "default": "ar1", "type": str, "advanced": False, "help": "The temporal variance model. Defaults to first order auto regressive model 'ar1'. The AR model can be set to any integer value by modifying the value of N. E.g. use ar5 for a fifth order model. If the string auto is provided a model with order 4 times the sample rate will be used."},
|
|
|
|
{"name": "BINS", "default": 0, "type": int, "help": "Maximum number of discrete bins for the AR coef histogram/clustering. By default the value is 0, which will set the number of bins to the number of channels, effectively estimating the AR model for each channel."},
|
|
|
|
{"name": "BINS", "default": 0, "type": int, "advanced": True, "help": "Maximum number of discrete bins for the AR coef histogram/clustering. By default the value is 0, which will set the number of bins to the number of channels, effectively estimating the AR model for each channel."},
|
|
|
|
{"name": "N_JOBS", "default": 1, "type": int, "help": "The number of CPUs to use to do the GLM computation. -1 means 'all CPUs'."},
|
|
|
|
{"name": "N_JOBS", "default": 1, "type": int, "advanced": True, "help": "The number of CPUs to use to do the GLM computation. -1 means 'all CPUs'."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Region of Interest",
|
|
|
|
"title": "Region of Interest",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "JSON_LOCATION", "default": "", "type": "json_file", "help": "Location of the JSON file containing region of interest results for significance calculations."},
|
|
|
|
{"name": "JSON_LOCATION", "default": "", "type": "json_file", "advanced": False, "help": "Location of the JSON file containing region of interest results for significance calculations."},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Finishing Touches",
|
|
|
|
"title": "Contrast",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
# Intentionally empty (TODO)
|
|
|
|
{"name": "CONTRAST_BASELINE", "default": True, "type": bool, "advanced": True, "help": "Calculate contrasts comparing each condition against zero/baseline. This may not always represent a meaningful physiological baseline in fNIRS."},
|
|
|
|
|
|
|
|
{"name": "CONTRAST_PAIRWISE", "default": True, "type": bool, "advanced": True, "help": "Calculate contrasts comparing each experimental condition against every other condition. The number of contrasts increases with the number of conditions."}
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"title": "Other",
|
|
|
|
"title": "Other",
|
|
|
|
"params": [
|
|
|
|
"params": [
|
|
|
|
{"name": "TIME_WINDOW_START", "default": 0, "type": int, "help": "Where to start averaging the fir model bins. Only affects the significance and contrast images."},
|
|
|
|
{"name": "MAX_WORKERS", "default": 6, "type": int, "advanced": False, "help": "Number of files to be processed at once. Setting this to a small integer value may help on underpowered systems. Remove the value to use an automatic amount."},
|
|
|
|
{"name": "TIME_WINDOW_END", "default": 15, "type": int, "help": "Where to end averaging the fir model bins. Only affects the significance and contrast images."},
|
|
|
|
{"name": "VERBOSITY", "default": False, "type": bool, "advanced": True, "help": "Setting this to True will log lots of debugging information to the log file. Setting this to False will log minimal data."},
|
|
|
|
{"name": "MAX_WORKERS", "default": 6, "type": int, "help": "Number of files to be processed at once. Setting this to a small integer value may help on underpowered systems. Remove the value to use an automatic amount."},
|
|
|
|
|
|
|
|
{"name": "VERBOSITY", "default": False, "type": bool, "help": "Setting this to True will log lots of debugging information to the log file. Setting this to False will log minimal data."},
|
|
|
|
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]
|
|
|
|
]
|
|
|
@@ -425,6 +457,35 @@ class GroupAssignmentDialog(QDialog):
|
|
|
|
buttons.rejected.connect(self.reject)
|
|
|
|
buttons.rejected.connect(self.reject)
|
|
|
|
layout.addWidget(buttons)
|
|
|
|
layout.addWidget(buttons)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def run_for_field(cls, parent, file_metadata: dict, field_name: str = "AGE"):
|
|
|
|
|
|
|
|
"""Extracts values, checks for data, presents the dialog, and returns mappings."""
|
|
|
|
|
|
|
|
# A. Collect unique non-empty values
|
|
|
|
|
|
|
|
unique_values = sorted(
|
|
|
|
|
|
|
|
list(
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
str(meta.get(field_name, "")).strip()
|
|
|
|
|
|
|
|
for meta in file_metadata.values()
|
|
|
|
|
|
|
|
if str(meta.get(field_name, "")).strip()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not unique_values:
|
|
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
|
|
parent,
|
|
|
|
|
|
|
|
"No Groupable Metadata",
|
|
|
|
|
|
|
|
f"No '{field_name}' values were found in the metadata to group.",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# B. Instantiate and show dialog
|
|
|
|
|
|
|
|
dialog = cls(parent, unique_values=unique_values, field_name=field_name)
|
|
|
|
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
|
|
|
|
|
|
return dialog.get_mappings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _add_group(self):
|
|
|
|
def _add_group(self):
|
|
|
|
"""Adds a new group to the available options."""
|
|
|
|
"""Adds a new group to the available options."""
|
|
|
|
name = self.group_input.text().strip()
|
|
|
|
name = self.group_input.text().strip()
|
|
|
@@ -608,8 +669,10 @@ class MainApplication(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
self.about = None
|
|
|
|
self.about = None
|
|
|
|
self.help = None
|
|
|
|
self.help = None
|
|
|
|
|
|
|
|
self.regroup_metadata = None
|
|
|
|
self.optodes = None
|
|
|
|
self.optodes = None
|
|
|
|
self.events = None
|
|
|
|
self.events = None
|
|
|
|
|
|
|
|
self.events_blazes = None
|
|
|
|
self.terminal = None
|
|
|
|
self.terminal = None
|
|
|
|
self.bubble_widgets = {}
|
|
|
|
self.bubble_widgets = {}
|
|
|
|
self.param_sections = []
|
|
|
|
self.param_sections = []
|
|
|
@@ -621,6 +684,7 @@ class MainApplication(QMainWindow):
|
|
|
|
self.missing_events_bypass = False
|
|
|
|
self.missing_events_bypass = False
|
|
|
|
self.analysis_clearing_bypass = False
|
|
|
|
self.analysis_clearing_bypass = False
|
|
|
|
self.folding_bypass = False
|
|
|
|
self.folding_bypass = False
|
|
|
|
|
|
|
|
self.advanced_parameters = False
|
|
|
|
self.json_location = ""
|
|
|
|
self.json_location = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -880,6 +944,7 @@ class MainApplication(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
options_actions = [
|
|
|
|
options_actions = [
|
|
|
|
("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")),
|
|
|
|
("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")),
|
|
|
|
|
|
|
|
("Regroup Files from Metadata", "F3", self.group_metadata, resource_path("icons/help_24dp_1F1F1F.svg")),
|
|
|
|
("Check for Updates", "F5", self.updater.manual_check_for_updates, resource_path("icons/update_24dp_1F1F1F.svg")),
|
|
|
|
("Check for Updates", "F5", self.updater.manual_check_for_updates, resource_path("icons/update_24dp_1F1F1F.svg")),
|
|
|
|
("Show Update Changelog", "F6", self.show_update_changelog, resource_path("icons/article_shortcut_24dp_1F1F1.svg")),
|
|
|
|
("Show Update Changelog", "F6", self.show_update_changelog, resource_path("icons/article_shortcut_24dp_1F1F1.svg")),
|
|
|
|
("Update events in snirf file (BORIS)...", "F7", self.update_event_markers, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
|
|
|
|
("Update events in snirf file (BORIS)...", "F7", self.update_event_markers, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
|
|
|
@@ -891,7 +956,7 @@ class MainApplication(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
for i, (name, shortcut, slot, icon) in enumerate(options_actions):
|
|
|
|
for i, (name, shortcut, slot, icon) in enumerate(options_actions):
|
|
|
|
options_menu.addAction(make_action(name, shortcut, slot, icon=icon))
|
|
|
|
options_menu.addAction(make_action(name, shortcut, slot, icon=icon))
|
|
|
|
if i == 2 or i == 5 or i == 6 or i == 7:
|
|
|
|
if i == 3 or i == 6 or i == 7:
|
|
|
|
options_menu.addSeparator()
|
|
|
|
options_menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
|
|
self.pref_actions = {}
|
|
|
|
self.pref_actions = {}
|
|
|
@@ -903,6 +968,7 @@ class MainApplication(QMainWindow):
|
|
|
|
("Missing Events Bypass", "", self.missing_events_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "missing_events_bypass"),
|
|
|
|
("Missing Events Bypass", "", self.missing_events_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "missing_events_bypass"),
|
|
|
|
("Analysis Clearing Bypass", "", self.analysis_clearing_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "analysis_clearing_bypass"),
|
|
|
|
("Analysis Clearing Bypass", "", self.analysis_clearing_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "analysis_clearing_bypass"),
|
|
|
|
("Folding Bypass", "", self.folding_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "folding_bypass"),
|
|
|
|
("Folding Bypass", "", self.folding_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "folding_bypass"),
|
|
|
|
|
|
|
|
("Show Advanced Parameters", "", self.advanced_parameters_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "advanced_parameters"),
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for name, shortcut, slot, icon, config_key in preferences_actions:
|
|
|
|
for name, shortcut, slot, icon, config_key in preferences_actions:
|
|
|
@@ -923,24 +989,28 @@ class MainApplication(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_sections(self, index):
|
|
|
|
def update_sections(self, index):
|
|
|
|
# Clear previous sections
|
|
|
|
self.current_section_index = index
|
|
|
|
for i in reversed(range(self.rows_layout.count())):
|
|
|
|
|
|
|
|
widget = self.rows_layout.itemAt(i).widget()
|
|
|
|
|
|
|
|
if widget is not None:
|
|
|
|
|
|
|
|
widget.deleteLater()
|
|
|
|
|
|
|
|
self.param_sections.clear()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.global_param_widgets = {}
|
|
|
|
# Build sections ONCE to avoid destroying/re-creating widgets in C++
|
|
|
|
|
|
|
|
if not hasattr(self, "_sections_built") or not self._sections_built:
|
|
|
|
|
|
|
|
self.param_sections.clear()
|
|
|
|
|
|
|
|
self.global_param_widgets = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Add ParamSection widgets from SECTIONS
|
|
|
|
for section in SECTIONS:
|
|
|
|
for section in SECTIONS:
|
|
|
|
section_widget = ParamSection(section, self.global_param_widgets)
|
|
|
|
self.section_widget = ParamSection(section, self.global_param_widgets)
|
|
|
|
self.rows_layout.addWidget(section_widget)
|
|
|
|
self.rows_layout.addWidget(self.section_widget)
|
|
|
|
self.param_sections.append(section_widget)
|
|
|
|
|
|
|
|
|
|
|
|
self.param_sections.append(self.section_widget)
|
|
|
|
self._sections_built = True
|
|
|
|
|
|
|
|
|
|
|
|
for sec in self.param_sections:
|
|
|
|
# Defensive dictionary lookup for the preference action
|
|
|
|
sec.update_dependencies()
|
|
|
|
pref_action = getattr(self, "pref_actions", {}).get("advanced_parameters")
|
|
|
|
|
|
|
|
show_advanced = pref_action.isChecked() if pref_action is not None else False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Toggle visibility on all built sections
|
|
|
|
|
|
|
|
for sec in self.param_sections:
|
|
|
|
|
|
|
|
sec.set_advanced_visible(show_advanced)
|
|
|
|
|
|
|
|
sec.update_dependencies()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_all(self):
|
|
|
|
def clear_all(self):
|
|
|
@@ -1255,6 +1325,11 @@ class MainApplication(QMainWindow):
|
|
|
|
self.folding_bypass = checked
|
|
|
|
self.folding_bypass = checked
|
|
|
|
self._update_config_setting("folding_bypass", checked)
|
|
|
|
self._update_config_setting("folding_bypass", checked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def advanced_parameters_func(self, checked):
|
|
|
|
|
|
|
|
self.advanced_parameters = checked
|
|
|
|
|
|
|
|
self._update_config_setting("advanced_parameters", checked)
|
|
|
|
|
|
|
|
self.update_sections(0)
|
|
|
|
|
|
|
|
|
|
|
|
def about_window(self):
|
|
|
|
def about_window(self):
|
|
|
|
if self.about is None or not self.about.isVisible():
|
|
|
|
if self.about is None or not self.about.isVisible():
|
|
|
|
self.about = AboutWindow(self)
|
|
|
|
self.about = AboutWindow(self)
|
|
|
@@ -1265,6 +1340,23 @@ class MainApplication(QMainWindow):
|
|
|
|
self.help = UserGuideWindow(self)
|
|
|
|
self.help = UserGuideWindow(self)
|
|
|
|
self.help.show()
|
|
|
|
self.help.show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def group_metadata(self):
|
|
|
|
|
|
|
|
if self.regroup_metadata is None or not self.regroup_metadata.isVisible():
|
|
|
|
|
|
|
|
file_meta = getattr(self, "file_metadata", {})
|
|
|
|
|
|
|
|
if any(bool(meta) for meta in file_meta.values()):
|
|
|
|
|
|
|
|
mappings = GroupAssignmentDialog.run_for_field(
|
|
|
|
|
|
|
|
self, file_meta, field_name="AGE"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if mappings:
|
|
|
|
|
|
|
|
self._apply_group_mappings(mappings, field_name="AGE")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
|
|
None,
|
|
|
|
|
|
|
|
"No Data",
|
|
|
|
|
|
|
|
"This action is not available at this time.",
|
|
|
|
|
|
|
|
QMessageBox.Ok
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def terminal_gui(self):
|
|
|
|
def terminal_gui(self):
|
|
|
|
if self.terminal is None or not self.terminal.isVisible():
|
|
|
|
if self.terminal is None or not self.terminal.isVisible():
|
|
|
|
self.terminal = TerminalWindow(self)
|
|
|
|
self.terminal = TerminalWindow(self)
|
|
|
@@ -1281,9 +1373,9 @@ class MainApplication(QMainWindow):
|
|
|
|
self.events.show()
|
|
|
|
self.events.show()
|
|
|
|
|
|
|
|
|
|
|
|
def update_event_markers_blazes(self):
|
|
|
|
def update_event_markers_blazes(self):
|
|
|
|
if self.events is None or not self.events.isVisible():
|
|
|
|
if self.events_blazes is None or not self.events_blazes.isVisible():
|
|
|
|
self.events = UpdateEventsBlazesWindow(self, EventUpdateMode.WRITE_SNIRF, "Manual SNIRF Edit")
|
|
|
|
self.events_blazes = UpdateEventsBlazesWindow(self, EventUpdateMode.WRITE_SNIRF, "Manual SNIRF Edit")
|
|
|
|
self.events.show()
|
|
|
|
self.events_blazes.show()
|
|
|
|
|
|
|
|
|
|
|
|
def show_update_changelog(self):
|
|
|
|
def show_update_changelog(self):
|
|
|
|
welcome = WelcomeDialog(self, direct=False)
|
|
|
|
welcome = WelcomeDialog(self, direct=False)
|
|
|
@@ -1340,7 +1432,7 @@ class MainApplication(QMainWindow):
|
|
|
|
self.missing_events_bypass = file_cfg.getboolean("Preferences", "missing_events_bypass", fallback=False)
|
|
|
|
self.missing_events_bypass = file_cfg.getboolean("Preferences", "missing_events_bypass", fallback=False)
|
|
|
|
self.analysis_clearing_bypass = file_cfg.getboolean("Preferences", "analysis_clearing_bypass", fallback=False)
|
|
|
|
self.analysis_clearing_bypass = file_cfg.getboolean("Preferences", "analysis_clearing_bypass", fallback=False)
|
|
|
|
self.folding_bypass = file_cfg.getboolean("Preferences", "folding_bypass", fallback=False)
|
|
|
|
self.folding_bypass = file_cfg.getboolean("Preferences", "folding_bypass", fallback=False)
|
|
|
|
|
|
|
|
self.advanced_parameters = file_cfg.getboolean("Preferences", "advanced_parameters", fallback=False)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Sync the UI Menu checkmarks visually
|
|
|
|
# 2. Sync the UI Menu checkmarks visually
|
|
|
|
if hasattr(self, 'pref_actions'):
|
|
|
|
if hasattr(self, 'pref_actions'):
|
|
|
@@ -1349,6 +1441,10 @@ class MainApplication(QMainWindow):
|
|
|
|
self.pref_actions["missing_events_bypass"].setChecked(self.missing_events_bypass)
|
|
|
|
self.pref_actions["missing_events_bypass"].setChecked(self.missing_events_bypass)
|
|
|
|
self.pref_actions["analysis_clearing_bypass"].setChecked(self.analysis_clearing_bypass)
|
|
|
|
self.pref_actions["analysis_clearing_bypass"].setChecked(self.analysis_clearing_bypass)
|
|
|
|
self.pref_actions["folding_bypass"].setChecked(self.folding_bypass)
|
|
|
|
self.pref_actions["folding_bypass"].setChecked(self.folding_bypass)
|
|
|
|
|
|
|
|
self.pref_actions["advanced_parameters"].setChecked(self.advanced_parameters)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.advanced_parameters:
|
|
|
|
|
|
|
|
self.update_sections(0)
|
|
|
|
|
|
|
|
|
|
|
|
if hasattr(self, 'recent_files_menu'):
|
|
|
|
if hasattr(self, 'recent_files_menu'):
|
|
|
|
self.update_recent_files_menu()
|
|
|
|
self.update_recent_files_menu()
|
|
|
@@ -2587,6 +2683,30 @@ class MainApplication(QMainWindow):
|
|
|
|
self.metadata_processed.emit(file_path, session_id)
|
|
|
|
self.metadata_processed.emit(file_path, session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def populate_metadata_fields(self, file_path: str):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Populates the right-column metadata QLineEdits for the given file_path
|
|
|
|
|
|
|
|
without triggering textChanged sync events during the population.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not file_path:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Normalize path to match key format in file_metadata
|
|
|
|
|
|
|
|
norm_path = os.path.normpath(file_path)
|
|
|
|
|
|
|
|
meta = self.file_metadata.get(
|
|
|
|
|
|
|
|
norm_path, self.file_metadata.get(file_path, {})
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Loop through all dynamically created fields (AGE, GENDER, GROUP, etc.)
|
|
|
|
|
|
|
|
for key, line_edit in self.meta_fields.items():
|
|
|
|
|
|
|
|
val = str(meta.get(key, "")).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Block textChanged signals so sync_bubble_data doesn't fire redundant loops
|
|
|
|
|
|
|
|
line_edit.blockSignals(True)
|
|
|
|
|
|
|
|
line_edit.setText(val)
|
|
|
|
|
|
|
|
line_edit.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _remove_file_from_pipeline(self, file_path):
|
|
|
|
def _remove_file_from_pipeline(self, file_path):
|
|
|
|
"""Completely cleans up and removes all references to a file that failed to load."""
|
|
|
|
"""Completely cleans up and removes all references to a file that failed to load."""
|
|
|
|
# 1. Decrement pending file count
|
|
|
|
# 1. Decrement pending file count
|
|
|
@@ -2626,12 +2746,8 @@ class MainApplication(QMainWindow):
|
|
|
|
if self.pending_files_count <= 0:
|
|
|
|
if self.pending_files_count <= 0:
|
|
|
|
self._cleanup_executor()
|
|
|
|
self._cleanup_executor()
|
|
|
|
self.statusbar.showMessage("All files loaded sucessfully.")
|
|
|
|
self.statusbar.showMessage("All files loaded sucessfully.")
|
|
|
|
has_metadata = any(
|
|
|
|
file_meta = getattr(self, "file_metadata", {})
|
|
|
|
bool(meta_dict)
|
|
|
|
if any(bool(meta) for meta in file_meta.values()):
|
|
|
|
for meta_dict in getattr(self, "file_metadata", {}).values()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if has_metadata:
|
|
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
|
reply = QMessageBox.question(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
"Metadata Detected",
|
|
|
|
"Metadata Detected",
|
|
|
@@ -2641,55 +2757,34 @@ class MainApplication(QMainWindow):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
# A. Collect all unique non-empty AGE values across loaded files
|
|
|
|
mappings = GroupAssignmentDialog.run_for_field(
|
|
|
|
unique_ages = sorted(
|
|
|
|
self, file_meta, field_name="AGE"
|
|
|
|
list(
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
str(meta.get("AGE", "")).strip()
|
|
|
|
|
|
|
|
for meta in getattr(
|
|
|
|
|
|
|
|
self, "file_metadata", {}
|
|
|
|
|
|
|
|
).values()
|
|
|
|
|
|
|
|
if str(meta.get("AGE", "")).strip()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
if mappings:
|
|
|
|
|
|
|
|
self._apply_group_mappings(mappings, field_name="AGE")
|
|
|
|
|
|
|
|
|
|
|
|
if not unique_ages:
|
|
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
|
|
self,
|
|
|
|
|
|
|
|
"No Groupable Metadata",
|
|
|
|
|
|
|
|
"No 'AGE' values were found in the metadata to group.",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# B. Open Group Assignment Dialog
|
|
|
|
def _apply_group_mappings(self, mappings: dict, field_name: str = "AGE"):
|
|
|
|
dialog = GroupAssignmentDialog(
|
|
|
|
"""Applies group mappings to metadata and updates all UI widgets."""
|
|
|
|
self, unique_values=unique_ages, field_name="AGE"
|
|
|
|
# C. Update 'GROUP' in self.file_metadata for matching files
|
|
|
|
)
|
|
|
|
for path_key, meta in self.file_metadata.items():
|
|
|
|
|
|
|
|
val = str(meta.get(field_name, "")).strip()
|
|
|
|
|
|
|
|
if val in mappings:
|
|
|
|
|
|
|
|
meta["GROUP"] = mappings[val]
|
|
|
|
|
|
|
|
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
|
|
# D. Update text on ALL bubble widgets
|
|
|
|
mappings = dialog.get_mappings()
|
|
|
|
for path_key, b_widget in self.bubble_widgets.items():
|
|
|
|
|
|
|
|
normalized_k = os.path.normpath(path_key)
|
|
|
|
# C. Update 'GROUP' in self.file_metadata for matching files
|
|
|
|
meta_dict = self.file_metadata.get(
|
|
|
|
for path_key, meta in self.file_metadata.items():
|
|
|
|
normalized_k, self.file_metadata.get(path_key, {})
|
|
|
|
file_age = str(meta.get("AGE", "")).strip()
|
|
|
|
)
|
|
|
|
if file_age in mappings:
|
|
|
|
suffix = self.get_suffix_from_meta_fields(meta_dict)
|
|
|
|
meta["GROUP"] = mappings[file_age]
|
|
|
|
b_widget.setSuffixText(suffix)
|
|
|
|
|
|
|
|
|
|
|
|
# D. Update the text on ALL bubbles immediately
|
|
|
|
|
|
|
|
for path_key, b_widget in self.bubble_widgets.items():
|
|
|
|
|
|
|
|
normalized_k = os.path.normpath(path_key)
|
|
|
|
|
|
|
|
meta_dict = self.file_metadata.get(
|
|
|
|
|
|
|
|
normalized_k, self.file_metadata.get(path_key, {})
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
suffix = self.get_suffix_from_meta_fields(meta_dict)
|
|
|
|
|
|
|
|
b_widget.setSuffixText(suffix)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# E. Refresh form fields if a file is currently active in UI
|
|
|
|
|
|
|
|
current_active = getattr(self, "current_file", None)
|
|
|
|
|
|
|
|
if current_active:
|
|
|
|
|
|
|
|
self.populate_metadata_fields(current_active)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# E. Refresh form fields for currently active file
|
|
|
|
|
|
|
|
current_active = getattr(self, "current_file", None)
|
|
|
|
|
|
|
|
if current_active:
|
|
|
|
|
|
|
|
self.populate_metadata_fields(current_active)
|
|
|
|
|
|
|
|
|
|
|
|
def _cleanup_executor(self):
|
|
|
|
def _cleanup_executor(self):
|
|
|
|
"""Safely shuts down the executor and clears the reference."""
|
|
|
|
"""Safely shuts down the executor and clears the reference."""
|
|
|
|