This repository contains the implementation of the Maximum Likelihood Estimation (MLE) and Least Squares (LS) routines, and links to open-source data deposited in a Zenodo folder.
Understanding the complexity of frequency and phase angle fluctuations in power grids
Alessandro Lonardi, Jacques M. Maritz, Leonardo Rydin Gorjão, and Christian Beck
[arXiv]
The code is made available for the public, if you make use of it please cite our work in the form of the reference above.
Below, we explain how to run the code. You can fit synthetic data (which are also used for tests) and visualize real-world data collected with the PMUs used in our study.
The code was developed using Python 3.12 and can be downloaded and used locally as-is.
To install the necessary packages, you can follow these steps:
- Install Poetry
- Clone this repository to your machine
- Install the dependencies with Poetry using
poetry install --no-rootThe core folders and files implementing the numerical methods are the following:
-
domaincontains all thedistributionsandfunctionsfitted via MLE and LS -
paramscontains a json file specifying the MLE parameters boundaries -
src-
likelihood.pycontains the negative log-likelihood function$\ell(\boldsymbol{\theta})$ -
mle.pycontains the MLE routine. We usescipy.optimize.minimize()for parameters optimization with the Nelder-Mead optimizer that clips$\boldsymbol{\theta}$ inside its boundaries. As the optimizer does not require gradients, we approximate standard deviations of$\boldsymbol{\theta}$ by calculating the Hessian of the negative log-likelihood and inverting it (if$\mathbf{H}$ is non-invertible we regularize it with Tikhonov regularization and invert the regularized Hessian):
$\mathbf{H}(\boldsymbol{\theta}) = \frac{\partial^2 (\ell(\boldsymbol{\theta}))}{\partial \boldsymbol{\theta} \partial \boldsymbol{\theta}^\top} \quad \to \quad \mathrm{Cov}({\boldsymbol{\theta}}^*) \approx \mathbf{H}{({\boldsymbol{\theta}}^*)}^{-1} \quad \to \quad \mathrm{std}(\theta_i^*) = \sqrt{[\mathrm{Cov}(\boldsymbol{\theta}^*)]_{ii}} $ -
mle_multiple_inits.pycontains a function to run MLE sequentially for multiple initializations of$\boldsymbol{\theta}$ and choose the solution with the highest likelihood -
ls_multiple_inits.pycontains the LS routine. The function is a wrapper ofscipy.optimize.curve_fit()that runs the fit for multiple initializations and chooses the solution with the lowest sum of squared residuals.
-
We test the code on a series of synthetic datasets that reproduce the measurements collected by PMUs. Specifically, we use rejection sampling (see utils/sampling) to generate:
- Data following the deadband angular velocity distribution
$p(\omega) \propto \sinh (\omega / a) / \omega$ for$\omega \in [-\omega_0, \omega_0]$ - Tail data sampled from the right-hand side (values above the mean) of a Gaussian and
$q$ -Gaussian distribution - Data following the tilted washboard phase-angle difference distribution
$p(x) \propto \exp (a x + b \cos(x))$
We also generate two datasets following single- and double-exponential decay and perturb their values using Gaussian noise with a small amplitude.
We fit the data using the MLE and LS routines. Detailed examples on how to run the code on all synthetic datasets are in notebooks/synthetic_data
Synthetic data serve as a validation for our method. For a more robust check, we implement the fit discussed in the notebooks as pytests in tests
To run the tests locally with Poetry, you can execute:
poetry run pytest tests/We open-source measurements recorded from our PMUs over the month of August 2025. The data have a 0.1 s resolution and include:
- Frequency measurements recorded in London
- Phase angle measurements recorded in Stellenbosch
- Phase angle measurements recorded in Bloemfontein
Important: Due to their large size, the files are not directly uploaded on GitHub. You can find them deposited in a Zenodo folder.
Once you dowload the data, add them to the data folder as:
- data/frequency_london.csv
- data/phase_angle_stellenbosch.csv
- data/phase_angle_bloemfontein.csv
Doing so allows running the notebooks in notebooks/real_data to reproduce useful paper plots using the open-sourced data.