Phase-Amplitude Coupling#

The comodulogram is a matrix representing the strength of the coupling between the phase of driver frequencies and the amplitude of signal frequencies. The ‘pac’ method allows to extract pairs of driver/signal frequencies that exhibit higher scores of coupling

from biotuner.biotuner_object import compute_biotuner
!pip install pactools
import numpy as np
# load data
data = np.load('../data/EEG_example.npy')

biotuning = compute_biotuner(1000, data = data[10], peaks_function = 'harmonic_recurrence', precision = 0.1, n_harm = 10,
                    ratios_n_harms = 5, ratios_inc_fit = False, ratios_inc = True, scale_cons_limit = 0.1) # Initialize biotuner object
pac_freqs, _ = biotuning.pac(n_values = 30, plot=True, drive_precision = 0.1, max_drive_freq = 7, method = 'duprelatour')
print(pac_freqs)
Looking in indexes: https://pypi.org/simple, https://pypi.ngc.nvidia.com
Requirement already satisfied: pactools in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (0.3.1)
Requirement already satisfied: numpy in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (2.1.3)
Requirement already satisfied: scipy in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (1.15.2)
Requirement already satisfied: matplotlib in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (3.10.1)
Requirement already satisfied: scikit-learn in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (1.6.1)
Requirement already satisfied: mne in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (1.9.0)
Requirement already satisfied: h5py in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pactools) (3.13.0)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (1.3.1)
Requirement already satisfied: cycler>=0.10 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (4.56.0)
Requirement already satisfied: kiwisolver>=1.3.1 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (1.4.8)
Requirement already satisfied: packaging>=20.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (24.2)
Requirement already satisfied: pillow>=8 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (11.1.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (3.2.1)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from matplotlib->pactools) (2.9.0.post0)
Requirement already satisfied: decorator in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from mne->pactools) (5.2.1)
Requirement already satisfied: jinja2 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from mne->pactools) (3.1.5)
Requirement already satisfied: lazy-loader>=0.3 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from mne->pactools) (0.4)
Requirement already satisfied: pooch>=1.5 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from mne->pactools) (1.8.2)
Requirement already satisfied: tqdm in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from mne->pactools) (4.67.1)
Requirement already satisfied: joblib>=1.2.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from scikit-learn->pactools) (1.4.2)
Requirement already satisfied: threadpoolctl>=3.1.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from scikit-learn->pactools) (3.5.0)
Requirement already satisfied: platformdirs>=2.5.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pooch>=1.5->mne->pactools) (4.3.6)
Requirement already satisfied: requests>=2.19.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from pooch>=1.5->mne->pactools) (2.32.3)
Requirement already satisfied: six>=1.5 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from python-dateutil>=2.7->matplotlib->pactools) (1.17.0)
Requirement already satisfied: MarkupSafe>=2.0 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from jinja2->mne->pactools) (3.0.2)
Requirement already satisfied: colorama in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from tqdm->mne->pactools) (0.4.6)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from requests>=2.19.0->pooch>=1.5->mne->pactools) (3.4.1)
Requirement already satisfied: idna<4,>=2.5 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from requests>=2.19.0->pooch>=1.5->mne->pactools) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from requests>=2.19.0->pooch>=1.5->mne->pactools) (2.3.0)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\skite\miniconda3\envs\biotuner_env\lib\site-packages (from requests>=2.19.0->pooch>=1.5->mne->pactools) (2025.1.31)
c:\Users\skite\miniconda3\envs\biotuner_env\Lib\site-packages\IPython\utils\_process_win32.py:138: ResourceWarning: unclosed file <_io.BufferedWriter name=4>
  res = process_handler(cmd, _system_body)
ResourceWarning: Enable tracemalloc to get the object allocation traceback
c:\Users\skite\miniconda3\envs\biotuner_env\Lib\site-packages\IPython\utils\_process_win32.py:138: ResourceWarning: unclosed file <_io.BufferedReader name=5>
  res = process_handler(cmd, _system_body)
ResourceWarning: Enable tracemalloc to get the object allocation traceback
c:\Users\skite\miniconda3\envs\biotuner_env\Lib\site-packages\IPython\utils\_process_win32.py:138: ResourceWarning: unclosed file <_io.BufferedReader name=6>
  res = process_handler(cmd, _system_body)
ResourceWarning: Enable tracemalloc to get the object allocation traceback
[[np.float64(6.4), np.float64(43.0)], [np.float64(6.300000000000001), np.float64(43.0)], [np.float64(6.300000000000001), np.float64(46.0)], [np.float64(6.4), np.float64(44.0)], [np.float64(6.5), np.float64(34.0)], [np.float64(6.300000000000001), np.float64(45.0)], [np.float64(6.300000000000001), np.float64(44.0)], [np.float64(6.300000000000001), np.float64(47.0)], [np.float64(6.4), np.float64(47.0)], [np.float64(3.2), np.float64(44.0)], [np.float64(6.300000000000001), np.float64(42.0)], [np.float64(3.0), np.float64(15.0)], [np.float64(6.5), np.float64(31.0)], [np.float64(6.300000000000001), np.float64(41.0)], [np.float64(3.1), np.float64(40.0)], [np.float64(6.4), np.float64(48.0)], [np.float64(3.0), np.float64(16.0)], [np.float64(6.300000000000001), np.float64(48.0)], [np.float64(6.4), np.float64(42.0)], [np.float64(3.4), np.float64(43.0)], [np.float64(6.5), np.float64(32.0)], [np.float64(6.5), np.float64(35.0)], [np.float64(6.4), np.float64(46.0)], [np.float64(3.0), np.float64(14.0)], [np.float64(6.4), np.float64(45.0)], [np.float64(3.8), np.float64(27.0)], [np.float64(6.5), np.float64(33.0)], [np.float64(3.8), np.float64(26.0)], [np.float64(5.2), np.float64(27.0)], [np.float64(5.4), np.float64(36.0)]]
../../_images/cc01744cf7c1c7c5d4b749e99cd5844b4511803b7835acadee19386a82949963.png

Different methods can be used to compute the PAC: methods = [‘ozkurt’, ‘canolty’, ‘tort’, ‘penny’, ‘vanwijk’, ‘duprelatour’, ‘colgin’, ‘sigl’, ‘bispectrum’]

pac_freqs, _ = biotuning.pac(plot=True, drive_precision = 0.1, n_values = 10, max_drive_freq = 7, method = 'ozkurt')
pac_freqs
[[np.float64(3.4), np.float64(15.0)],
 [np.float64(3.4), np.float64(14.0)],
 [np.float64(3.4), np.float64(19.0)],
 [np.float64(3.1), np.float64(34.0)],
 [np.float64(3.1), np.float64(35.0)],
 [np.float64(3.1), np.float64(44.0)],
 [np.float64(3.4), np.float64(46.0)],
 [np.float64(3.1), np.float64(42.0)],
 [np.float64(3.4), np.float64(23.0)],
 [np.float64(3.1), np.float64(18.0)]]
../../_images/5a54b489f924ac38c7112bea8d7cdec9e92ba920f55f3ae684d29101aa3f9fa6.png

Deriving tunings from PAC information#

By computing the most frequent phase and amplitude frequencies from the ‘pac_freqs’ lists, we can derive a series of ratios by coupling each phase with each amplitude frequencies

from biotuner.biotuner_utils import pairs_most_frequent
pac_frequent = pairs_most_frequent(pac_freqs, 3)
pac_frequent
[[np.float64(18.0), np.float64(23.0), np.float64(42.0)],
 [np.float64(3.1), np.float64(3.4)]]
from biotuner.biotuner_utils import rebound
ratios = []
for i in range(len(pac_frequent[0])):
    for j in range(len(pac_frequent[1])):
        ratios.append(rebound(pac_frequent[0][i]/pac_frequent[1][j]))
        
ratios = sorted(ratios)
ratios
[np.float64(1.3235294117647058),
 np.float64(1.4516129032258065),
 np.float64(1.5441176470588236),
 np.float64(1.6911764705882353),
 np.float64(1.6935483870967742),
 np.float64(1.8548387096774193)]

Another approach to derive tuning based on the information of the Phase-Amplitude Coupling would be to compute the ratios of each pairs of phase/amplitude frequencies, and then to apply the ‘scale_reduction’ function to extract the most consonant intervals. This is what the pac_mode function does.

from biotuner.scale_construction import pac_mode
from biotuner.metrics import dyad_similarity
pac_mode(pac_freqs, n=6, function=dyad_similarity)
[np.float64(1.3709677419354838),
 np.float64(1.411290322580645),
 np.float64(1.4516129032258065),
 np.float64(1.6911764705882353),
 np.float64(1.6935483870967742),
 np.float64(1.7741935483870968)]
pac_mode(pac_freqs, n=10)
[np.float64(1.0294117647058825),
 np.float64(1.1029411764705883),
 np.float64(1.3709677419354838),
 np.float64(1.397058823529412),
 np.float64(1.411290322580645),
 np.float64(1.4516129032258065),
 np.float64(1.6911764705882353),
 np.float64(1.6911764705882353),
 np.float64(1.6935483870967742),
 np.float64(1.7741935483870968)]

Using coupled frequencies as generator interval#

This code analyzes phase amplitude coupling (PAC) and constructs a musical scale based on the frequency ratios associated with the PAC.

First, the code calculates the frequency ratio between the two oscillatory signals that exhibit PAC, stored in the pac_freqs variable. The frequency ratio is then converted to a rational number using the sp.Rational() function and its denominator is limited to 1000 using .limit_denominator().

Next, the code generates the Stern-Brocot interval associated with the rational number using gen_interval_to_stern_brocot(). The interval represents a sequence of fractions that converge to the rational number and is used to determine the generator interval tuning. A generator interval is a specific type of musical interval that can be used to generate a scale.

The number of steps in the interval is limited to 16 using .limit_denominator(), and the generator interval tuning is calculated based on the interval and number of steps using generator_interval_tuning(). The resulting gen_int_tuning variable contains the musical scale based on the PAC frequency ratio, with limit_steps number of steps in the scale and an octave of 2. The tuning is generated by sorting the generator interval tuning in ascending order using sorted().

# Import the necessary functions from the biotuner.scale_construction module
from biotuner.scale_construction import gen_interval_to_stern_brocot, generator_interval_tuning
import sympy as sp
from fractions import Fraction

# Calculate the frequency ratio between the two oscillatory signals that exhibit phase amplitude coupling (stored in pac_freqs variable) and convert it to a rational number with a denominator limited to 1000
ratio = rebound(pac_freqs[0][1]/pac_freqs[0][0])
print(sp.Rational(ratio).limit_denominator(1000))

# Set the limit on the number of steps in the interval to 16 and generate the Stern-Brocot interval based on the frequency ratio
limit_steps = 16
stern_brocot_ratio = gen_interval_to_stern_brocot(ratio)

# Limit the number of steps in the interval to 16 and calculate the steps needed for the generator interval tuning
steps = Fraction(stern_brocot_ratio).limit_denominator(16).denominator

# Calculate the generator interval tuning based on the interval and number of steps, and sort the tuning in ascending order
gen_int_tuning = sorted(generator_interval_tuning(interval = ratio, steps = steps, octave = 2))

# Store the resulting musical scale in the gen_int_tuning variable
gen_int_tuning
75/68
[[np.float64(1.0),
  np.float64(1.1029411764705883),
  np.float64(1.2164792387543255),
  np.float64(1.3417050427437414),
  np.float64(1.4798217383203032),
  np.float64(1.6321563290297463),
  np.float64(1.800172421723985)],
 [np.float64(1.0),
  np.float64(1.1029411764705883),
  np.float64(1.2164792387543255),
  np.float64(1.3417050427437414),
  np.float64(1.4798217383203032),
  np.float64(1.6321563290297463),
  np.float64(1.800172421723985)]]

Deriving euclidian rhythms from PAC information#

This code constructs a musical scale based on the phase amplitude coupling (PAC) frequencies and uses it to generate a consonant rhythmic pattern. The PAC scale is generated using the pac_mode() function and the consonant rhythmic pattern is generated using the consonant_euclid() function. The resulting rhythm is represented as a list of strings and can be played using the Euclidean rhythm. This code could be useful for creating music that is based on the natural rhythms of biological systems

# Import the necessary dictionaries and functions from the biotuner module
from biotuner.dictionaries import *
from biotuner.rhythm_construction import *

# Generate a musical scale based on the phase amplitude coupling (PAC) frequencies using the pac_mode() function
pac_scale = pac_mode(pac_freqs, 10, function=dyad_similarity)

# Generate a consonant Euclidean rhythm based on the PAC scale using the consonant_euclid() function
euclid_final, cons = consonant_euclid(pac_scale, n_steps_down = 3, limit_denom = 4, 
                                      limit_cons =1, limit_denom_final = 100)

# Generate a list of interval vectors based on the Euclidean rhythm
interval_vectors = [interval_vector(x) for x in euclid_final]

# Convert the list of interval vectors to a list of strings using the interval_vec_to_string() function
strings = interval_vec_to_string(interval_vectors)

# Convert the rhythmic pattern strings to their corresponding reference rhythms based on the dict_rhythms dictionary using the euclid_string_to_referent() function
euclid_referent = euclid_string_to_referent(strings, dict_rhythms)

# Store the resulting Euclidean rhythm, interval vectors, and reference rhythms in the euclid_final, interval_vectors, and euclid_referent variables, respectively
euclid_final, interval_vectors, euclid_referent
c:\Users\skite\miniconda3\envs\biotuner_env\Lib\fractions.py:712: RuntimeWarning: overflow encountered in scalar multiply
  self._denominator * other.numerator)
([[1, 0, 0],
  [1, 0, 0, 0],
  [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
  [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
  [1, 1, 1, 0],
  [1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0]],
 [[3], [4], [4, 4, 4], [3, 3, 3, 3], [1, 1, 2], [1, 1, 2, 1, 1, 2, 1, 1, 2]],
 ['None',
  'None',
  'None',
  'It is periodic with four repetitions of E(1,3) = [100]. It is the (12/8)-time Fandago clapping pattern in the Flamenco music of southern Spain, where 1 denotes a loud clap and 0 soft clap.',
  'It is the archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad. It is also a thirteenth century Persian rhythm called Khalif-e-saghil, as well as the trochoid choreic rhythmic pattern of ancient Greece.',
  'None'])