diff --git a/snlohelper/base_function.py b/snlohelper/base_function.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd011c7db9f5e8c1a7136ade3e82c801ff5181e9
--- /dev/null
+++ b/snlohelper/base_function.py
@@ -0,0 +1,137 @@
+import time
+from typing import Any, Optional, Protocol
+
+from .utils import Position, gui, scale, get_content_complete, set_value
+from .main_window import Functions, open_function
+
+
+class BaseFunction(Protocol):
+    """Base class for a function window."""
+
+    _function: Functions
+    # Positions
+    _run_pos: Position  # of the run field
+    _result_pos: Position  # of the results field
+    _close_pos: Position
+    _configuration_pos: dict[str, list[Position]]  # of the configuration fields
+
+    def open(self) -> None:
+        """Open the function."""
+        open_function(self._function)
+
+    def run(self) -> None:
+        """Click 'Run'."""
+        gui.click(*scale(*self._run_pos))
+
+    def close(self) -> None:
+        """Click 'x'."""
+        self.open()
+        gui.click(*scale(*self._close_pos))
+
+    def configure(
+        self, data: Optional[dict[str, Any | list[Any] | tuple[Any, ...]]] = None
+    ) -> None:
+        """Configure the values and leave the config window open.
+
+        If any value is "None", that field will not be changed. This is useful, if you want to
+        change a single value in a row.
+        For example `data={'Wavelengths (nm)': [1064.5, None, None]}` will set the first wavelength
+        to 1064.5 nm while leaving the other wavelengths untouched.
+        """
+        self.open()
+        if data is None:
+            return
+        for key, value in data.items():
+            positions = self._configuration_pos[key]
+            if isinstance(value, (list, tuple)):
+                for i, val in enumerate(value):
+                    if val is not None:
+                        set_value(positions[i], val)
+            else:
+                if value is not None:
+                    set_value(positions[0], value)
+
+    def get_configuration(self) -> dict[str, list[float | str]]:
+        """Read the current configuration."""
+        self.open()
+        data = {}
+        for key, positions in self._configuration_pos.items():
+            d = []
+            for pos in positions:
+                val = get_content_complete(pos)
+                try:
+                    d.append(float(val))
+                except ValueError:
+                    d.append(val)
+            data[key] = d
+        return data
+
+    def read_results(self) -> list[str]:
+        return get_content_complete(self._result_pos).split("\r\n")
+
+    def interpret_results(self, rows: list[str]) -> dict[str, Any]:
+        """Interpret the results and return them as a dictionary."""
+        data = {}
+        for row in rows:
+            text, values = row.split("=")
+            content = []
+            for element in values.split():
+                try:
+                    val = float(element)
+                except ValueError:
+                    val = element
+                content.append(val)
+            data[text.strip()] = content
+        return data
+
+    def run_and_read(
+        self,
+        waiting_time: float = 1,
+        max_tries: int = 10,
+        interval: float = 0.5,
+        waiting_line_count: int = 3,
+    ) -> dict[str, Any]:
+        """Run an analysis and return the result."""
+        self.run()
+        time.sleep(waiting_time)
+        for _ in range(max_tries):
+            rows = self.read_results()
+            if len(rows) > waiting_line_count:
+                break
+            time.sleep(interval)
+
+        # interpret results and save as dictionary:
+        return self.interpret_results(rows)
+
+    def configure_run_read(
+        self, data: Optional[dict[str, Any]] = None, **kwargs
+    ) -> dict[str, Any]:
+        """Configure and run an analysis and return the result."""
+        self.configure(data)
+        return self.run_and_read(**kwargs)
+
+
+def generate_position_dict(
+    first_position: Position,
+    configuration_names: list[str],
+    columns: int = 3,
+    column_distance: int = 60,
+) -> dict[str, list[Position]]:
+    """Generate a position dictionary for functions.
+
+    This utility makes it easier to generate a position matrix with three fields per entry.
+    Adjust it afterwards according to the number of entries.
+
+    :param first_position: Position (tuple) of the top left configuration field.
+    :param configuration_names: List of all the names (in order) of the configuration.
+    """
+    # Notes regarding positions for mix: horizontal distance is 60 px, vertical distance 16 pixels
+    positions = {}
+    i = 0
+    for name in configuration_names:
+        positions[name] = [
+            (first_position[0] + j * column_distance, first_position[1] + 16 * i)
+            for j in range(columns)
+        ]
+        i += 1
+    return positions
diff --git a/snlohelper/mix_methods.py b/snlohelper/mix_methods.py
index bd44c306b1ac28cc12702d78fa265b88ae1247bb..9deae485a728c13362abb8ce64859ab7f21c3d48 100644
--- a/snlohelper/mix_methods.py
+++ b/snlohelper/mix_methods.py
@@ -1,102 +1,26 @@
-import time
-from typing import Any, Optional
+from typing import Any, Optional, Protocol
 
-from .utils import Position, gui, scale, set_value, get_content_complete
-from .main_window import Functions, open_function
+from .utils import Position, gui, scale
+from .base_function import BaseFunction
 
 
-class MixMethods:
+class MixMethods(BaseFunction, Protocol):
     """Parent class for mix methods.
 
     Subclass it for specific methods. You should define the positions and the result interpretation.
     """
 
-    _function: Functions
-    # Positions
     _accept_pos: Position
-    _run_pos: Position
     _change_inputs_pos: Position
-    _result_pos: Position  # of the results field
-    _configuration_pos: dict[str, list[Position]]  # of the configuration fields
-
-    def open(self) -> None:
-        """Open the function."""
-        open_function(self._function)
 
     def accept(self) -> None:
         """Click 'Accept'."""
         gui.click(*scale(*self._accept_pos))
 
-    def run(self) -> None:
-        """Click 'Run'."""
-        gui.click(*scale(*self._run_pos))
-
     def change_inputs(self) -> None:
         """Click 'Change Inputs'."""
         gui.click(*scale(*self._change_inputs_pos))
 
-    def configure(self, data: Optional[dict[str, Any]] = None) -> None:
-        """Configure the values and leave the config window open.
-
-        If any value is "None", that field will not be changed. This is useful, if you want to
-        change a single value in a row.
-        For example `data={'Wavelengths (nm)': [1064.5, None, None]}` will set the first wavelength
-        to 1064.5 nm while leaving the other wavelengths untouched.
-        """
-        self.open()
-        if data is None:
-            return
-        for key, value in data.items():
-            positions = self._configuration_pos[key]
-            for i, val in enumerate(value):
-                if val is not None:
-                    set_value(positions[i], val)
-
-    def get_configuration(self) -> dict[str, Any]:
-        """Read the current configuration."""
-        self.open()
-        data = {}
-        for key, positions in self._configuration_pos.items():
-            d = []
-            for pos in positions:
-                val = get_content_complete(pos)
-                try:
-                    d.append(float(val))
-                except ValueError:
-                    d.append(val)
-            data[key] = d
-        return data
-
-    def interpret_results(self, rows: list[str]) -> dict[str, Any]:
-        """Interpret the results and return them as a dictionary."""
-        data = {}
-        for row in rows:
-            text, values = row.split("=")
-            data[text.strip()] = [float(i) for i in values.split()]
-        return data
-
-    def read_results(self) -> list[str]:
-        return get_content_complete(self._result_pos).split("\r\n")
-
-    def run_and_read(
-        self,
-        waiting_time: float = 1,
-        max_tries: int = 10,
-        interval: float = 0.5,
-        successful_line_count: int = 3,
-    ) -> dict[str, Any]:
-        """Run an analysis and return the result."""
-        self.run()
-        time.sleep(waiting_time)
-        for _ in range(max_tries):
-            rows = self.read_results()
-            if len(rows) > successful_line_count:
-                break
-            time.sleep(interval)
-
-        # interpret results and save as dictionary:
-        return self.interpret_results(rows)
-
     def configure_run_read(
         self, data: Optional[dict[str, Any]] = None, **kwargs
     ) -> dict[str, float | list[float]]:
@@ -104,26 +28,3 @@ class MixMethods:
         self.configure(data)
         self.accept()
         return self.run_and_read(**kwargs)
-
-
-def generate_position_dict(
-    first_configuration_field: Position, configuration_names: list[str]
-) -> dict[str, list[Position]]:
-    """Generate a position dictionary for mix methods.
-
-    This utility makes it easier to generate a position matrix with three fields per entry.
-    Adjust it afterwards according to the number of entries.
-
-    :param first_configuration_field: Position (tuple) of the top left configuration field.
-    :param configuration_names: List of all the names (in order) of the configuration.
-    """
-    # Notes regarding positions: horizontal distance is 60 px, vertical distance 16 pixels
-    positions = {}
-    i = 0
-    for name in configuration_names:
-        positions[name] = [
-            (first_configuration_field[0], first_configuration_field[1] + 16 * i),
-            (first_configuration_field[0] + 60, first_configuration_field[1] + 16 * i),
-            (first_configuration_field[0] + 120, first_configuration_field[1] + 16 * i),
-        ]
-    return positions
diff --git a/snlohelper/ref_index.py b/snlohelper/ref_index.py
new file mode 100644
index 0000000000000000000000000000000000000000..6432588abf483e4723ffa7ede8e27c45bfe1134c
--- /dev/null
+++ b/snlohelper/ref_index.py
@@ -0,0 +1,39 @@
+from typing import Any
+
+from .base_function import BaseFunction, Functions
+
+
+class RefractiveIndex(BaseFunction):
+    _function = Functions.REF_INDEX
+    _run_pos = (295, 187)
+    _result_pos = (160, 260)
+    _close_pos = (365, 41)
+    _configuration_pos = {
+        "Crystal": [(170, 90)],
+        "Temperature": [(290, 90)],
+        "theta": [(170, 146)],
+        "phi": [(300, 146)],
+        "Wavelength": [(170, 190)],
+    }
+
+    def run_and_read(
+        self,
+        waiting_time: float = 0.1,
+        max_tries: int = 10,
+        interval: float = 0.1,
+        waiting_line_count: int = 3,
+    ) -> dict[str, Any]:
+        return super().run_and_read(waiting_time, max_tries, interval, waiting_line_count)
+
+    def refractive_indices(
+        self, Crystal=None, Temperature=None, theta=None, phi=None, Wavelength=None
+    ) -> list[float]:
+        kwargs = {
+            "Crystal": Crystal,
+            "Temperature": Temperature,
+            "theta": theta,
+            "phi": phi,
+            "Wavelength": Wavelength,
+        }
+        results = self.configure_run_read(kwargs)
+        return results["Refractive index (o,e)"]
diff --git a/snlohelper/two_d_mix_lp.py b/snlohelper/two_d_mix_lp.py
index e1abf34df3a5c6ee0d4eaf3a1b7a106aec3477e2..8b8cdb95f37c2a27b3cb7c736a5551cab46c07d3 100644
--- a/snlohelper/two_d_mix_lp.py
+++ b/snlohelper/two_d_mix_lp.py
@@ -8,6 +8,7 @@ class TwoDMixLP(MixMethods):
     _function = Functions.TWOD_MIX_LP
     _accept_pos = (506, 520)
     _run_pos = (140, 220)
+    _close_pos = (540, 140)
     _change_inputs_pos = (373, 166)
     _result_pos = (133, 293)
 
@@ -31,7 +32,7 @@ class TwoDMixLP(MixMethods):
         "beta blue (cm/W)": [(400, 420), (460, 420), (520, 420)],
         "Walkoff angles (mrad)": [(400, 440), (460, 440), (520, 440)],
         "Offset in wo dir. (mm)": [(400, 453), (460, 453), (520, 453)],
-        "Rad. curv. (mm/air“)": [(400, 473), (460, 473), (520, 473)],
+        "Rad. curv. (mm/air)": [(400, 473), (460, 473), (520, 473)],
         "# of integ/grid points": [(400, 486), (460, 486), (520, 486)],
         "Crystal/grid sizes (mm)": [(400, 500), (460, 500), (520, 500)],
         "Deff (pm/V)": [(400, 520)],
diff --git a/snlohelper/two_d_mix_sp.py b/snlohelper/two_d_mix_sp.py
index b15561404e5c1a540a0801c4d6026cb012eb1fa2..f153542720a9e1566321b491da26b30b6bc664ff 100644
--- a/snlohelper/two_d_mix_sp.py
+++ b/snlohelper/two_d_mix_sp.py
@@ -8,6 +8,7 @@ class TwoDMixSP(MixMethods):
     _function = Functions.TWOD_MIX_SP
     _accept_pos = (506, 600)
     _run_pos = (140, 200)
+    _close_pos = (540, 140)
     _change_inputs_pos = (150, 266)
     _result_pos = (133, 293)