diff --git a/fitgrid/epochs.py b/fitgrid/epochs.py
index 9a33eba..f252bc9 100644
--- a/fitgrid/epochs.py
+++ b/fitgrid/epochs.py
@@ -60,7 +60,9 @@ class Epochs:
             raise FitGridError('Duplicate column names not allowed.')
 
         # make our own copy so we are immune to modification to original table
-        table = epochs_table.copy().reset_index().set_index(EPOCH_ID)
+        table = (
+            epochs_table.copy().reset_index().set_index(EPOCH_ID).sort_index()
+        )
         assert table.index.names == [EPOCH_ID]
 
         snapshots = table.groupby(TIME)
diff --git a/fitgrid/tools.py b/fitgrid/tools.py
index d0a2717..fabc9ed 100644
--- a/fitgrid/tools.py
+++ b/fitgrid/tools.py
@@ -182,3 +182,63 @@ class single_threaded:
                     f'to {self.old_n_threads} threads (previous value).'
                 )
                 raise RuntimeError(message)
+
+
+def design_matrix_is_constant(df, columns, time):
+    """Check that values in columns of df do not change within any epoch.
+
+    See Notes for more details.
+
+    Parameters
+    ----------
+    df : pandas.DataFrame
+        dataframe to check
+    columns : list of str
+        list of column names to be checked
+    time : str
+        name of the time column
+
+    Returns
+    -------
+    result : bool
+        True if values in specified columns don't change, False otherwise
+
+
+    Notes
+    -----
+    We check that from timepoint to timepoint, each epoch has the same value in
+    a given column:
+
+                epoch1
+        ==============
+             A |    B
+        ==============
+             1      x
+             1      x
+             1      x
+             1      x
+             1      x
+
+                epoch2
+        ==============
+             A |    B
+        ==============
+             2      y
+             2      y
+             2      y
+             2      y
+             2      y
+
+    This is helpful when performing linear regression on an epochs table where
+    the predictors vary with epochs (as they are expected to) but stay constant
+    from sample to sample, because we can do our modeling much faster.
+    """
+    gb = df.groupby(time)
+    _, group = next(iter(gb))  # first group
+
+    df_columns_values = df[columns].values
+    group_columns_values = group[columns].values
+
+    expected_df_values = np.repeat(group_columns_values, len(gb), axis=0)
+
+    return (df_columns_values == expected_df_values).all()

add design matrix check
if the design matrix, given a formula, doesn't change across time/channels, we can run certain computations much faster
diff --git a/tests/test_tools.py b/tests/test_tools.py
index e001ff5..b98c7d2 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -87,3 +87,109 @@ def test_single_threaded_change_before():
         assert blas.get_n_threads() == 1
 
     assert blas.get_n_threads() == BEFORE
+
+
+def test_design_matrix_is_constant():
+
+    df = pd.DataFrame(
+        dict(
+            epoch_id=[
+                0,  # epoch 0
+                0,
+                0,
+                0,
+                1,  # epoch 1
+                1,
+                1,
+                1,
+                2,  # epoch 2
+                2,
+                2,
+                2,
+            ],
+            time=[
+                0,  # epoch 0
+                1,
+                2,
+                3,
+                0,  # epoch 1
+                1,
+                2,
+                3,
+                0,  # epoch 2
+                1,
+                2,
+                3,
+            ],
+            b=[
+                'a',  # epoch 0
+                'a',
+                'a',
+                'a',
+                'b',  # epoch 1
+                'b',
+                'b',
+                'b',
+                'c',  # epoch 2
+                'c',
+                'c',
+                'c',
+            ],
+            c=[
+                10,  # epoch 0
+                10,
+                10,
+                10,
+                20,  # epoch 1
+                20,
+                20,
+                20,
+                30,  # epoch 2
+                30,
+                30,
+                30,
+            ],
+        )
+    ).set_index(['epoch_id', 'time'])
+
+    assert tools.design_matrix_is_constant(df, ['b', 'c'], 'time')
+
+    assert not tools.design_matrix_is_constant(
+        df.sample(frac=1), ['b', 'c'], 'time'
+    )
+
+    assert tools.design_matrix_is_constant(
+        df.sample(frac=1).sort_index(), ['b', 'c'], 'time'
+    )
+
+    df['c'] = [
+        0,  # epoch 0
+        1,
+        2,
+        3,
+        4,  # epoch 1
+        5,
+        6,
+        7,
+        8,  # epoch 2
+        9,
+        10,
+        11,
+    ]
+    assert not tools.design_matrix_is_constant(df, ['b', 'c'], 'time')
+
+    df['c'] = [
+        0,  # epoch 0
+        0,
+        0,
+        0,
+        1,  # epoch 1
+        1,
+        10000,
+        1,
+        2,  # epoch 2
+        2,
+        2,
+        2,
+    ]
+    assert not tools.design_matrix_is_constant(df, ['b', 'c'], 'time')

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc git ssh-client
3.11
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
blosc2==3.2.1
certifi==2025.1.31
charset-normalizer==3.4.1
contourpy==1.3.1
cycler==0.12.1
-e git+https://github.com/kutaslab/fitgrid.git@a6774dc90334ddfac77ff7aff901cc1ef7f75920#egg=fitgrid
fonttools==4.56.0
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
kiwisolver==1.4.8
matplotlib==3.10.1
msgpack==1.1.0
ndindex==1.9.2
numexpr==2.10.2
numpy==2.2.4
packaging @ file:///croot/packaging_1734472117206/work
pandas==2.2.3
patsy==1.0.1
pillow==11.1.0
platformdirs==4.3.7
pluggy @ file:///croot/pluggy_1733169602837/work
py-cpuinfo==9.0.0
pyparsing==3.2.3
pytest @ file:///croot/pytest_1738938843180/work
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.32.3
scipy==1.15.2
six==1.17.0
statsmodels==0.14.4
tables==3.10.2
tqdm==4.67.1
typing_extensions==4.13.0
tzdata==2025.2
urllib3==2.3.0

name: fitgrid
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - bzip2=1.0.8=h5eee18b_6
  - ca-certificates=2025.2.25=h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - libuuid=1.41.5=h5eee18b_0
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py311h06a4308_0
  - pip=25.0=py311h06a4308_0
  - pluggy=1.5.0=py311h06a4308_0
  - pytest=8.3.4=py311h06a4308_0
  - python=3.11.11=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py311h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py311h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - blosc2==3.2.1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - contourpy==1.3.1
      - cycler==0.12.1
      - fonttools==4.56.0
      - idna==3.10
      - kiwisolver==1.4.8
      - matplotlib==3.10.1
      - msgpack==1.1.0
      - ndindex==1.9.2
      - numexpr==2.10.2
      - numpy==2.2.4
      - pandas==2.2.3
      - patsy==1.0.1
      - pillow==11.1.0
      - platformdirs==4.3.7
      - py-cpuinfo==9.0.0
      - pyparsing==3.2.3
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.32.3
      - scipy==1.15.2
      - six==1.17.0
      - statsmodels==0.14.4
      - tables==3.10.2
      - tqdm==4.67.1
      - typing-extensions==4.13.0
      - tzdata==2025.2
      - urllib3==2.3.0
prefix: /opt/conda/envs/fitgrid

tests/test_tools.py::test_design_matrix_is_constant
tests/test_tools.py::test_blas_getter tests/test_tools.py::test_single_threaded_no_change tests/test_tools.py::test_single_threaded_change_before
tests/test_tools.py::test_duplicate_index_printer tests/test_tools.py::test_deduplicate_list
fitgrid/tools.py fitgrid/epochs.py
fitgrid/tools.py fitgrid/epochs.py