experiment_control_window

This module defines the ExperimentCtlWindow class, a Toplevel window in the GUI responsible for allowing the user to assign specific substances (stimuli) to the different valves available on the experimental rig.

It generates labels and input fields for these stimuli based on the number of stimuli defined in the experiment variables and ensures symmetrical assignment between corresponding valves on opposing sides. This ensures that each simulus used is used on both sides. It also includes the button to trigger the generation of the experiment schedule after stimuli assignments are complete.

  1"""
  2This module defines the ExperimentCtlWindow class, a Toplevel window in the GUI responsible
  3for allowing the user to assign specific substances (stimuli) to the different valves available
  4on the experimental rig.
  5
  6It generates labels and input fields for these stimuli based on the number of stimuli
  7defined in the experiment variables and ensures symmetrical assignment between corresponding valves
  8on opposing sides. This ensures that each simulus used is used on both sides. It also includes the button to trigger the
  9generation of the experiment schedule after stimuli assignments are complete.
 10"""
 11
 12import tkinter as tk
 13import logging
 14from tkinter import ttk
 15from typing import Callable, Dict, List
 16
 17from models.experiment_process_data import ExperimentProcessData
 18from models.stimuli_data import StimuliData
 19from views.gui_common import GUIUtils
 20
 21# Get the logger in use to log info here
 22logger = logging.getLogger()
 23
 24
 25class ExperimentCtlWindow(tk.Toplevel):
 26    """
 27    This class defines a Toplevel window for managing experiment stimuli assignments to specific valves.
 28    It allows users to input the substance associated with each valve, ensures symmetry between
 29    corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button
 30    to trigger the generation of the experiment schedule based on these assignments and other experiment variables.
 31
 32    Attributes
 33    ----------
 34    - **exp_process_data** (*ExperimentProcessData*): Reference to the main `models.exp_process_data` model, used to access
 35      variables like 'Num Stimuli' and trigger schedule generation.
 36    - **stimuli_data** (*StimuliData*): Reference to the model holding stimuli/valve assignment data. Used to
 37      get default values and update the model when user input changes.
 38    - **trigger** (*Callable[[str], None]*): A callback function from StateMachine class.
 39      that is called when the 'Generate Schedule' button is pressed to transition the application state to `GENERATE SCHEDULE` state.
 40    - **stimuli_entries** (*Dict[str, tk.StringVar]*): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance")
 41      to `tk.StringVar` objects. These variables are linked to the corresponding Entry widgets.
 42    - **stimuli_frame** (*ttk.Frame*): The main container frame within the Toplevel window.
 43    - **stimuli_entry_frame** (*tk.Frame*): A frame nested within `stimuli_frame`, specifically holding the
 44      labeled entry widgets for valve substance input, organized into two columns.
 45    - **ui_components** (*Dict[str, List[tk.Widget]]*): A dictionary storing lists of created UI widgets
 46      (frames, labels, entries) within `populate_stimuli_frame`. This allows for potential future access or modification.
 47
 48    Methods
 49    -------
 50    - `show()`
 51        Makes the window visible (`deiconify`). It also checks if the 'Num Stimuli' variable in `exp_process_data` has
 52        changed since the UI was last built; if so, it re-initializes the content using `init_content()`.
 53    - `generate_button_method()`
 54        Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within the
 55        `exp_process_data` model. If successful, it hides the current window (`withdraw`) and calls the `trigger`
 56        function with the "GENERATE SCHEDULE" state/event.
 57    - `init_content()`
 58        Builds or rebuilds the main content of the window, including the `stimuli_frame`, populating it with valve
 59        entries using `populate_stimuli_frame`, and creating the 'Generate Schedule' button.
 60    - `fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar)`
 61        A callback function attached via `trace_add` to the `tk.StringVar`s in `stimuli_entries`. When the text in
 62        a valve's entry widget (linked to `source_var`) changes, this function automatically updates the `tk.StringVar`
 63        (`mapped_var`) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable).
 64    - `populate_stimuli_frame()`
 65        Areates and arranges the `tk.Label` and `tk.Entry` widgets for each relevant valve within the
 66        `stimuli_entry_frame`. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli'
 67        variable. It also sets up the `trace_add` callbacks using `fill_reverse_stimuli` to link corresponding valves.
 68        Stores created widgets in `self.ui_components`.
 69    """
 70
 71    def __init__(
 72        self,
 73        exp_data: ExperimentProcessData,
 74        stimuli_data: StimuliData,
 75        trigger: Callable[[str], None],
 76    ):
 77        """
 78        Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback,
 79        window attributes (title, close protocol, bindings), Tkinter variables for stimuli,
 80        links variables to the model, initializes UI content, and hides the window.
 81
 82        Parameters
 83        ----------
 84        - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods.
 85        - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic.
 86        - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE").
 87        """
 88        super().__init__()
 89        self.exp_process_data = exp_data
 90        self.stimuli_data = stimuli_data
 91
 92        # a callback function to immediately show the program window upon genration of schec
 93        self.trigger = trigger
 94
 95        # init window attributes
 96        self.title("Stimuli / Valves")
 97        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
 98        # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion,
 99        # but we don't so we do not use it. still, it is required to capture it
100        self.bind("<Control-w>", lambda event: self.withdraw())
101        self.resizable(False, False)
102
103        # set grid row and col zero to expand to fill the available space
104        self.grid_rowconfigure(0, weight=1)
105        self.grid_columnconfigure(0, weight=1)
106
107        # tkinter variables for stimuli variables
108        self.stimuli_entries = {
109            "Valve 1 Substance": tk.StringVar(),
110            "Valve 2 Substance": tk.StringVar(),
111            "Valve 3 Substance": tk.StringVar(),
112            "Valve 4 Substance": tk.StringVar(),
113            "Valve 5 Substance": tk.StringVar(),
114            "Valve 6 Substance": tk.StringVar(),
115            "Valve 7 Substance": tk.StringVar(),
116            "Valve 8 Substance": tk.StringVar(),
117        }
118
119        # fill the exp_var entry boxes with their default values as configured in self.exp_data
120        for key in self.stimuli_entries.keys():
121            self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key))
122
123        for key, value in self.stimuli_entries.items():
124            value.trace_add(
125                "write",
126                lambda *args, key=key, value=value: self.stimuli_data.update_model(
127                    key, GUIUtils.safe_tkinter_get(value)
128                ),
129            )
130
131        self.init_content()
132
133        # on init, we just want to create the window so that we don't have to later, but then we want to hide it
134        # until the user chooses to show this window
135        self.withdraw()
136
137        logger.info("Experiment Control Window created, but hidden for now.")
138
139    def show(self):
140        """
141        Makes the window visible. Checks if the number of stimuli has changed and rebuilds
142        the UI content if necessary before showing the window.
143        """
144        prev_num_stim = len(self.ui_components["entries"])
145        curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"]
146        if prev_num_stim != curr_num_stimuli:
147            self.init_content()
148        self.deiconify()
149
150    def generate_button_method(self):
151        """
152        Handles the 'Generate Schedule' button click. Triggers schedule generation
153        in the data model, hides the window, and signals the main application via the trigger callback.
154        """
155        # generate the program schedule df and fill it with stimuli that will be used
156        if not self.exp_process_data.generate_schedule():
157            return
158
159        self.withdraw()
160        self.trigger("GENERATE SCHEDULE")
161
162    def init_content(self) -> None:
163        """
164        Initializes or re-initializes the main UI elements of the window.
165        Creates the main frame, populates it with stimuli/valve entry fields,
166        and adds the 'Generate Schedule' button.
167        """
168        try:
169            # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized
170            self.stimuli_frame = ttk.Frame(self)
171            self.stimuli_frame.grid(row=1, column=0, sticky="nsew")
172
173            self.populate_stimuli_frame()
174
175            GUIUtils.create_button(
176                self,
177                "Generate Schedule",
178                lambda: self.generate_button_method(),
179                "green",
180                0,
181                0,
182            )
183
184            logger.info("Filled contents of experiment control window.")
185        except Exception as e:
186            logger.error(f"Error filling experiment control window: {e}")
187            raise
188
189    def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar):
190        """
191        Callback triggered when a stimulus entry's StringVar is modified.
192        It reads the new value from the source variable and sets the corresponding
193        (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented
194        on both sides of the experiment.
195
196        Parameters
197        ----------
198        - **source_var** (*tk.StringVar*): The variable that was just changed by the user.
199        - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side.
200        """
201        try:
202            new_value = source_var.get()
203            # Update the mapped variable with the new value
204            mapped_var.set(new_value)
205            logger.debug(f"Filled reverse stimuli: {new_value}")
206        except Exception as e:
207            logger.error(f"Error filling reverse stimuli: {e}")
208            raise
209
210    def populate_stimuli_frame(self) -> None:
211        """
212        Creates and places the Label and Entry widgets for valve substance assignments
213        within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'.
214        Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.).
215        """
216        try:
217            row = 1
218            self.ui_components: Dict[str, List[tk.Widget]] = {
219                "frames": [],
220                "labels": [],
221                "entries": [],
222            }
223
224            self.stimuli_entry_frame = tk.Frame(
225                self.stimuli_frame, highlightthickness=2, highlightbackground="black"
226            )
227            self.stimuli_entry_frame.grid(
228                row=0, column=0, pady=5, padx=5, sticky="nsew"
229            )
230            self.stimuli_entry_frame.grid_rowconfigure(0, weight=1)
231
232            side_one_label = tk.Label(
233                self.stimuli_entry_frame,
234                text="Side One",
235                bg="light blue",
236                font=("Helvetica", 24),
237                highlightthickness=2,
238                highlightbackground="dark blue",
239            )
240            side_one_label.grid(row=0, column=0, pady=5)
241            side_two_label = tk.Label(
242                self.stimuli_entry_frame,
243                pady=0,
244                text="Side Two",
245                bg="light blue",
246                font=("Helvetica", 24),
247                highlightthickness=2,
248                highlightbackground="dark blue",
249            )
250            side_two_label.grid(row=0, column=1, pady=5)
251            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
252                column = 0
253
254                frame, label, entry = GUIUtils.create_labeled_entry(
255                    self.stimuli_entry_frame,
256                    f"Valve {i + 1}",
257                    self.stimuli_entries[f"Valve {i + 1} Substance"],
258                    row,
259                    column,
260                )
261
262                source_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
263                mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
264
265                self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add(
266                    "write",
267                    lambda name,
268                    index,
269                    mode,
270                    source_var=source_var,
271                    mapped_var=mapped_var: self.fill_reverse_stimuli(
272                        source_var, mapped_var
273                    ),
274                )
275                self.ui_components["frames"].append(frame)
276                self.ui_components["labels"].append(label)
277                self.ui_components["entries"].append(entry)
278
279                row += 1
280
281            row = 1
282            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
283                column = 1
284                frame, label, entry = GUIUtils.create_labeled_entry(
285                    self.stimuli_entry_frame,
286                    f"Valve {i + 5}",
287                    self.stimuli_entries[f"Valve {i + 5} Substance"],
288                    row,
289                    column,
290                )
291
292                source_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
293                mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
294
295                self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add(
296                    "write",
297                    lambda name,
298                    index,
299                    mode,
300                    source_var=source_var,
301                    mapped_var=mapped_var: self.fill_reverse_stimuli(
302                        source_var, mapped_var
303                    ),
304                )
305                self.ui_components["frames"].append(frame)
306                self.ui_components["labels"].append(label)
307                self.ui_components["entries"].append(entry)
308
309                row += 1
310
311            logger.info("Stimuli frame populated.")
312        except Exception as e:
313            logger.error(f"Error populating stimuli frame: {e}")
314            raise
logger = <RootLogger root (INFO)>
class ExperimentCtlWindow(tkinter.Toplevel):
 26class ExperimentCtlWindow(tk.Toplevel):
 27    """
 28    This class defines a Toplevel window for managing experiment stimuli assignments to specific valves.
 29    It allows users to input the substance associated with each valve, ensures symmetry between
 30    corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button
 31    to trigger the generation of the experiment schedule based on these assignments and other experiment variables.
 32
 33    Attributes
 34    ----------
 35    - **exp_process_data** (*ExperimentProcessData*): Reference to the main `models.exp_process_data` model, used to access
 36      variables like 'Num Stimuli' and trigger schedule generation.
 37    - **stimuli_data** (*StimuliData*): Reference to the model holding stimuli/valve assignment data. Used to
 38      get default values and update the model when user input changes.
 39    - **trigger** (*Callable[[str], None]*): A callback function from StateMachine class.
 40      that is called when the 'Generate Schedule' button is pressed to transition the application state to `GENERATE SCHEDULE` state.
 41    - **stimuli_entries** (*Dict[str, tk.StringVar]*): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance")
 42      to `tk.StringVar` objects. These variables are linked to the corresponding Entry widgets.
 43    - **stimuli_frame** (*ttk.Frame*): The main container frame within the Toplevel window.
 44    - **stimuli_entry_frame** (*tk.Frame*): A frame nested within `stimuli_frame`, specifically holding the
 45      labeled entry widgets for valve substance input, organized into two columns.
 46    - **ui_components** (*Dict[str, List[tk.Widget]]*): A dictionary storing lists of created UI widgets
 47      (frames, labels, entries) within `populate_stimuli_frame`. This allows for potential future access or modification.
 48
 49    Methods
 50    -------
 51    - `show()`
 52        Makes the window visible (`deiconify`). It also checks if the 'Num Stimuli' variable in `exp_process_data` has
 53        changed since the UI was last built; if so, it re-initializes the content using `init_content()`.
 54    - `generate_button_method()`
 55        Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within the
 56        `exp_process_data` model. If successful, it hides the current window (`withdraw`) and calls the `trigger`
 57        function with the "GENERATE SCHEDULE" state/event.
 58    - `init_content()`
 59        Builds or rebuilds the main content of the window, including the `stimuli_frame`, populating it with valve
 60        entries using `populate_stimuli_frame`, and creating the 'Generate Schedule' button.
 61    - `fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar)`
 62        A callback function attached via `trace_add` to the `tk.StringVar`s in `stimuli_entries`. When the text in
 63        a valve's entry widget (linked to `source_var`) changes, this function automatically updates the `tk.StringVar`
 64        (`mapped_var`) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable).
 65    - `populate_stimuli_frame()`
 66        Areates and arranges the `tk.Label` and `tk.Entry` widgets for each relevant valve within the
 67        `stimuli_entry_frame`. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli'
 68        variable. It also sets up the `trace_add` callbacks using `fill_reverse_stimuli` to link corresponding valves.
 69        Stores created widgets in `self.ui_components`.
 70    """
 71
 72    def __init__(
 73        self,
 74        exp_data: ExperimentProcessData,
 75        stimuli_data: StimuliData,
 76        trigger: Callable[[str], None],
 77    ):
 78        """
 79        Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback,
 80        window attributes (title, close protocol, bindings), Tkinter variables for stimuli,
 81        links variables to the model, initializes UI content, and hides the window.
 82
 83        Parameters
 84        ----------
 85        - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods.
 86        - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic.
 87        - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE").
 88        """
 89        super().__init__()
 90        self.exp_process_data = exp_data
 91        self.stimuli_data = stimuli_data
 92
 93        # a callback function to immediately show the program window upon genration of schec
 94        self.trigger = trigger
 95
 96        # init window attributes
 97        self.title("Stimuli / Valves")
 98        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
 99        # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion,
100        # but we don't so we do not use it. still, it is required to capture it
101        self.bind("<Control-w>", lambda event: self.withdraw())
102        self.resizable(False, False)
103
104        # set grid row and col zero to expand to fill the available space
105        self.grid_rowconfigure(0, weight=1)
106        self.grid_columnconfigure(0, weight=1)
107
108        # tkinter variables for stimuli variables
109        self.stimuli_entries = {
110            "Valve 1 Substance": tk.StringVar(),
111            "Valve 2 Substance": tk.StringVar(),
112            "Valve 3 Substance": tk.StringVar(),
113            "Valve 4 Substance": tk.StringVar(),
114            "Valve 5 Substance": tk.StringVar(),
115            "Valve 6 Substance": tk.StringVar(),
116            "Valve 7 Substance": tk.StringVar(),
117            "Valve 8 Substance": tk.StringVar(),
118        }
119
120        # fill the exp_var entry boxes with their default values as configured in self.exp_data
121        for key in self.stimuli_entries.keys():
122            self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key))
123
124        for key, value in self.stimuli_entries.items():
125            value.trace_add(
126                "write",
127                lambda *args, key=key, value=value: self.stimuli_data.update_model(
128                    key, GUIUtils.safe_tkinter_get(value)
129                ),
130            )
131
132        self.init_content()
133
134        # on init, we just want to create the window so that we don't have to later, but then we want to hide it
135        # until the user chooses to show this window
136        self.withdraw()
137
138        logger.info("Experiment Control Window created, but hidden for now.")
139
140    def show(self):
141        """
142        Makes the window visible. Checks if the number of stimuli has changed and rebuilds
143        the UI content if necessary before showing the window.
144        """
145        prev_num_stim = len(self.ui_components["entries"])
146        curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"]
147        if prev_num_stim != curr_num_stimuli:
148            self.init_content()
149        self.deiconify()
150
151    def generate_button_method(self):
152        """
153        Handles the 'Generate Schedule' button click. Triggers schedule generation
154        in the data model, hides the window, and signals the main application via the trigger callback.
155        """
156        # generate the program schedule df and fill it with stimuli that will be used
157        if not self.exp_process_data.generate_schedule():
158            return
159
160        self.withdraw()
161        self.trigger("GENERATE SCHEDULE")
162
163    def init_content(self) -> None:
164        """
165        Initializes or re-initializes the main UI elements of the window.
166        Creates the main frame, populates it with stimuli/valve entry fields,
167        and adds the 'Generate Schedule' button.
168        """
169        try:
170            # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized
171            self.stimuli_frame = ttk.Frame(self)
172            self.stimuli_frame.grid(row=1, column=0, sticky="nsew")
173
174            self.populate_stimuli_frame()
175
176            GUIUtils.create_button(
177                self,
178                "Generate Schedule",
179                lambda: self.generate_button_method(),
180                "green",
181                0,
182                0,
183            )
184
185            logger.info("Filled contents of experiment control window.")
186        except Exception as e:
187            logger.error(f"Error filling experiment control window: {e}")
188            raise
189
190    def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar):
191        """
192        Callback triggered when a stimulus entry's StringVar is modified.
193        It reads the new value from the source variable and sets the corresponding
194        (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented
195        on both sides of the experiment.
196
197        Parameters
198        ----------
199        - **source_var** (*tk.StringVar*): The variable that was just changed by the user.
200        - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side.
201        """
202        try:
203            new_value = source_var.get()
204            # Update the mapped variable with the new value
205            mapped_var.set(new_value)
206            logger.debug(f"Filled reverse stimuli: {new_value}")
207        except Exception as e:
208            logger.error(f"Error filling reverse stimuli: {e}")
209            raise
210
211    def populate_stimuli_frame(self) -> None:
212        """
213        Creates and places the Label and Entry widgets for valve substance assignments
214        within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'.
215        Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.).
216        """
217        try:
218            row = 1
219            self.ui_components: Dict[str, List[tk.Widget]] = {
220                "frames": [],
221                "labels": [],
222                "entries": [],
223            }
224
225            self.stimuli_entry_frame = tk.Frame(
226                self.stimuli_frame, highlightthickness=2, highlightbackground="black"
227            )
228            self.stimuli_entry_frame.grid(
229                row=0, column=0, pady=5, padx=5, sticky="nsew"
230            )
231            self.stimuli_entry_frame.grid_rowconfigure(0, weight=1)
232
233            side_one_label = tk.Label(
234                self.stimuli_entry_frame,
235                text="Side One",
236                bg="light blue",
237                font=("Helvetica", 24),
238                highlightthickness=2,
239                highlightbackground="dark blue",
240            )
241            side_one_label.grid(row=0, column=0, pady=5)
242            side_two_label = tk.Label(
243                self.stimuli_entry_frame,
244                pady=0,
245                text="Side Two",
246                bg="light blue",
247                font=("Helvetica", 24),
248                highlightthickness=2,
249                highlightbackground="dark blue",
250            )
251            side_two_label.grid(row=0, column=1, pady=5)
252            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
253                column = 0
254
255                frame, label, entry = GUIUtils.create_labeled_entry(
256                    self.stimuli_entry_frame,
257                    f"Valve {i + 1}",
258                    self.stimuli_entries[f"Valve {i + 1} Substance"],
259                    row,
260                    column,
261                )
262
263                source_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
264                mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
265
266                self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add(
267                    "write",
268                    lambda name,
269                    index,
270                    mode,
271                    source_var=source_var,
272                    mapped_var=mapped_var: self.fill_reverse_stimuli(
273                        source_var, mapped_var
274                    ),
275                )
276                self.ui_components["frames"].append(frame)
277                self.ui_components["labels"].append(label)
278                self.ui_components["entries"].append(entry)
279
280                row += 1
281
282            row = 1
283            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
284                column = 1
285                frame, label, entry = GUIUtils.create_labeled_entry(
286                    self.stimuli_entry_frame,
287                    f"Valve {i + 5}",
288                    self.stimuli_entries[f"Valve {i + 5} Substance"],
289                    row,
290                    column,
291                )
292
293                source_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
294                mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
295
296                self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add(
297                    "write",
298                    lambda name,
299                    index,
300                    mode,
301                    source_var=source_var,
302                    mapped_var=mapped_var: self.fill_reverse_stimuli(
303                        source_var, mapped_var
304                    ),
305                )
306                self.ui_components["frames"].append(frame)
307                self.ui_components["labels"].append(label)
308                self.ui_components["entries"].append(entry)
309
310                row += 1
311
312            logger.info("Stimuli frame populated.")
313        except Exception as e:
314            logger.error(f"Error populating stimuli frame: {e}")
315            raise

This class defines a Toplevel window for managing experiment stimuli assignments to specific valves. It allows users to input the substance associated with each valve, ensures symmetry between corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button to trigger the generation of the experiment schedule based on these assignments and other experiment variables.

Attributes

  • exp_process_data (ExperimentProcessData): Reference to the main models.exp_process_data model, used to access variables like 'Num Stimuli' and trigger schedule generation.
  • stimuli_data (StimuliData): Reference to the model holding stimuli/valve assignment data. Used to get default values and update the model when user input changes.
  • trigger (Callable[[str], None]): A callback function from StateMachine class. that is called when the 'Generate Schedule' button is pressed to transition the application state to GENERATE SCHEDULE state.
  • stimuli_entries (Dict[str, tk.StringVar]): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance") to tk.StringVar objects. These variables are linked to the corresponding Entry widgets.
  • stimuli_frame (ttk.Frame): The main container frame within the Toplevel window.
  • stimuli_entry_frame (tk.Frame): A frame nested within stimuli_frame, specifically holding the labeled entry widgets for valve substance input, organized into two columns.
  • ui_components (Dict[str, List[tk.Widget]]): A dictionary storing lists of created UI widgets (frames, labels, entries) within populate_stimuli_frame. This allows for potential future access or modification.

Methods

  • show() Makes the window visible (deiconify). It also checks if the 'Num Stimuli' variable in exp_process_data has changed since the UI was last built; if so, it re-initializes the content using init_content().
  • generate_button_method() Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within the exp_process_data model. If successful, it hides the current window (withdraw) and calls the trigger function with the "GENERATE SCHEDULE" state/event.
  • init_content() Builds or rebuilds the main content of the window, including the stimuli_frame, populating it with valve entries using populate_stimuli_frame, and creating the 'Generate Schedule' button.
  • fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar) A callback function attached via trace_add to the tk.StringVars in stimuli_entries. When the text in a valve's entry widget (linked to source_var) changes, this function automatically updates the tk.StringVar (mapped_var) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable).
  • populate_stimuli_frame() Areates and arranges the tk.Label and tk.Entry widgets for each relevant valve within the stimuli_entry_frame. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli' variable. It also sets up the trace_add callbacks using fill_reverse_stimuli to link corresponding valves. Stores created widgets in self.ui_components.
ExperimentCtlWindow( exp_data: models.experiment_process_data.ExperimentProcessData, stimuli_data: models.stimuli_data.StimuliData, trigger: Callable[[str], NoneType])
 72    def __init__(
 73        self,
 74        exp_data: ExperimentProcessData,
 75        stimuli_data: StimuliData,
 76        trigger: Callable[[str], None],
 77    ):
 78        """
 79        Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback,
 80        window attributes (title, close protocol, bindings), Tkinter variables for stimuli,
 81        links variables to the model, initializes UI content, and hides the window.
 82
 83        Parameters
 84        ----------
 85        - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods.
 86        - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic.
 87        - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE").
 88        """
 89        super().__init__()
 90        self.exp_process_data = exp_data
 91        self.stimuli_data = stimuli_data
 92
 93        # a callback function to immediately show the program window upon genration of schec
 94        self.trigger = trigger
 95
 96        # init window attributes
 97        self.title("Stimuli / Valves")
 98        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
 99        # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion,
100        # but we don't so we do not use it. still, it is required to capture it
101        self.bind("<Control-w>", lambda event: self.withdraw())
102        self.resizable(False, False)
103
104        # set grid row and col zero to expand to fill the available space
105        self.grid_rowconfigure(0, weight=1)
106        self.grid_columnconfigure(0, weight=1)
107
108        # tkinter variables for stimuli variables
109        self.stimuli_entries = {
110            "Valve 1 Substance": tk.StringVar(),
111            "Valve 2 Substance": tk.StringVar(),
112            "Valve 3 Substance": tk.StringVar(),
113            "Valve 4 Substance": tk.StringVar(),
114            "Valve 5 Substance": tk.StringVar(),
115            "Valve 6 Substance": tk.StringVar(),
116            "Valve 7 Substance": tk.StringVar(),
117            "Valve 8 Substance": tk.StringVar(),
118        }
119
120        # fill the exp_var entry boxes with their default values as configured in self.exp_data
121        for key in self.stimuli_entries.keys():
122            self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key))
123
124        for key, value in self.stimuli_entries.items():
125            value.trace_add(
126                "write",
127                lambda *args, key=key, value=value: self.stimuli_data.update_model(
128                    key, GUIUtils.safe_tkinter_get(value)
129                ),
130            )
131
132        self.init_content()
133
134        # on init, we just want to create the window so that we don't have to later, but then we want to hide it
135        # until the user chooses to show this window
136        self.withdraw()
137
138        logger.info("Experiment Control Window created, but hidden for now.")

Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback, window attributes (title, close protocol, bindings), Tkinter variables for stimuli, links variables to the model, initializes UI content, and hides the window.

Parameters

  • exp_data (ExperimentProcessData): Instance holding experiment variables and methods.
  • stimuli_data (StimuliData): Instance holding stimuli assignment data and logic.
  • trigger (Callable[[str], None]): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE").
exp_process_data
stimuli_data
trigger
stimuli_entries
def show(self):
140    def show(self):
141        """
142        Makes the window visible. Checks if the number of stimuli has changed and rebuilds
143        the UI content if necessary before showing the window.
144        """
145        prev_num_stim = len(self.ui_components["entries"])
146        curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"]
147        if prev_num_stim != curr_num_stimuli:
148            self.init_content()
149        self.deiconify()

Makes the window visible. Checks if the number of stimuli has changed and rebuilds the UI content if necessary before showing the window.

def generate_button_method(self):
151    def generate_button_method(self):
152        """
153        Handles the 'Generate Schedule' button click. Triggers schedule generation
154        in the data model, hides the window, and signals the main application via the trigger callback.
155        """
156        # generate the program schedule df and fill it with stimuli that will be used
157        if not self.exp_process_data.generate_schedule():
158            return
159
160        self.withdraw()
161        self.trigger("GENERATE SCHEDULE")

Handles the 'Generate Schedule' button click. Triggers schedule generation in the data model, hides the window, and signals the main application via the trigger callback.

def init_content(self) -> None:
163    def init_content(self) -> None:
164        """
165        Initializes or re-initializes the main UI elements of the window.
166        Creates the main frame, populates it with stimuli/valve entry fields,
167        and adds the 'Generate Schedule' button.
168        """
169        try:
170            # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized
171            self.stimuli_frame = ttk.Frame(self)
172            self.stimuli_frame.grid(row=1, column=0, sticky="nsew")
173
174            self.populate_stimuli_frame()
175
176            GUIUtils.create_button(
177                self,
178                "Generate Schedule",
179                lambda: self.generate_button_method(),
180                "green",
181                0,
182                0,
183            )
184
185            logger.info("Filled contents of experiment control window.")
186        except Exception as e:
187            logger.error(f"Error filling experiment control window: {e}")
188            raise

Initializes or re-initializes the main UI elements of the window. Creates the main frame, populates it with stimuli/valve entry fields, and adds the 'Generate Schedule' button.

def fill_reverse_stimuli(self, source_var: tkinter.StringVar, mapped_var: tkinter.StringVar):
190    def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar):
191        """
192        Callback triggered when a stimulus entry's StringVar is modified.
193        It reads the new value from the source variable and sets the corresponding
194        (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented
195        on both sides of the experiment.
196
197        Parameters
198        ----------
199        - **source_var** (*tk.StringVar*): The variable that was just changed by the user.
200        - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side.
201        """
202        try:
203            new_value = source_var.get()
204            # Update the mapped variable with the new value
205            mapped_var.set(new_value)
206            logger.debug(f"Filled reverse stimuli: {new_value}")
207        except Exception as e:
208            logger.error(f"Error filling reverse stimuli: {e}")
209            raise

Callback triggered when a stimulus entry's StringVar is modified. It reads the new value from the source variable and sets the corresponding (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented on both sides of the experiment.

Parameters

  • source_var (tk.StringVar): The variable that was just changed by the user.
  • mapped_var (tk.StringVar): The variable for the corresponding valve on the other side.
def populate_stimuli_frame(self) -> None:
211    def populate_stimuli_frame(self) -> None:
212        """
213        Creates and places the Label and Entry widgets for valve substance assignments
214        within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'.
215        Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.).
216        """
217        try:
218            row = 1
219            self.ui_components: Dict[str, List[tk.Widget]] = {
220                "frames": [],
221                "labels": [],
222                "entries": [],
223            }
224
225            self.stimuli_entry_frame = tk.Frame(
226                self.stimuli_frame, highlightthickness=2, highlightbackground="black"
227            )
228            self.stimuli_entry_frame.grid(
229                row=0, column=0, pady=5, padx=5, sticky="nsew"
230            )
231            self.stimuli_entry_frame.grid_rowconfigure(0, weight=1)
232
233            side_one_label = tk.Label(
234                self.stimuli_entry_frame,
235                text="Side One",
236                bg="light blue",
237                font=("Helvetica", 24),
238                highlightthickness=2,
239                highlightbackground="dark blue",
240            )
241            side_one_label.grid(row=0, column=0, pady=5)
242            side_two_label = tk.Label(
243                self.stimuli_entry_frame,
244                pady=0,
245                text="Side Two",
246                bg="light blue",
247                font=("Helvetica", 24),
248                highlightthickness=2,
249                highlightbackground="dark blue",
250            )
251            side_two_label.grid(row=0, column=1, pady=5)
252            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
253                column = 0
254
255                frame, label, entry = GUIUtils.create_labeled_entry(
256                    self.stimuli_entry_frame,
257                    f"Valve {i + 1}",
258                    self.stimuli_entries[f"Valve {i + 1} Substance"],
259                    row,
260                    column,
261                )
262
263                source_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
264                mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
265
266                self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add(
267                    "write",
268                    lambda name,
269                    index,
270                    mode,
271                    source_var=source_var,
272                    mapped_var=mapped_var: self.fill_reverse_stimuli(
273                        source_var, mapped_var
274                    ),
275                )
276                self.ui_components["frames"].append(frame)
277                self.ui_components["labels"].append(label)
278                self.ui_components["entries"].append(entry)
279
280                row += 1
281
282            row = 1
283            for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2):
284                column = 1
285                frame, label, entry = GUIUtils.create_labeled_entry(
286                    self.stimuli_entry_frame,
287                    f"Valve {i + 5}",
288                    self.stimuli_entries[f"Valve {i + 5} Substance"],
289                    row,
290                    column,
291                )
292
293                source_var = self.stimuli_entries[f"Valve {i + 5} Substance"]
294                mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"]
295
296                self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add(
297                    "write",
298                    lambda name,
299                    index,
300                    mode,
301                    source_var=source_var,
302                    mapped_var=mapped_var: self.fill_reverse_stimuli(
303                        source_var, mapped_var
304                    ),
305                )
306                self.ui_components["frames"].append(frame)
307                self.ui_components["labels"].append(label)
308                self.ui_components["entries"].append(entry)
309
310                row += 1
311
312            logger.info("Stimuli frame populated.")
313        except Exception as e:
314            logger.error(f"Error populating stimuli frame: {e}")
315            raise

Creates and places the Label and Entry widgets for valve substance assignments within the stimuli_entry_frame. Organizes entries into 'Side One' and 'Side Two'. Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.).