Skip to main content

General purpose exponential function fitting

Project description

bicfit

bicfit is a lightweight Python library for fitting damped cosine and exponential functions. It combines the Generalized Pencil-of-Function method [^1] (to generate robust initial guesses) with standard optimization routines (to refine the fit).

This makes it well-suited for researchers and practitioners dealing with decaying oscillations, resonance signals, or exponential relaxations in noisy data.

Currently, bicfit supports fitting:

  • Complex exponential decay $f(t) = x_0 + \sum_k A_k \exp((j\omega_k - \kappa_k) t)$
  • Damped cosine $f(t) = x_0 + \sum_k A_k \exp(-\kappa_k t) \cos(\omega_k t + \phi_k)$
  • Exponential decay $f(t) = x_0 + \sum_k A_k \exp(-\kappa_k t)$

[^1]: Generalized Pencil-of-Function Method for Extracting Poles of an EM System from Its Transient Response Hua and Sarkar (IEEE TRANSACTIONS ON ANTENNAS AND PROPAGATION, VOL. 37, NO. 2, FEBRUARY 1989)

Installation

You can install bicfit via pip:

pip install bicfit

Usage

Start with

import numpy as np

w = 0.2
kappa = 0.05
offset = 1 + 1j
sigma_noise = 0.03
n_points = 100

Complex exponential decay

from bicfit import fit_complex_exponential

times = np.linspace(0, 150, n_points)
noise = np.random.normal(0, sigma_noise, n_points) + 1j * np.random.normal(0, sigma_noise, n_points)
signal = offset + np.exp((1j * w - kappa) * times) + noise

result = fit_complex_exponential(times, signal)
result.plot()

# you can call `result` directly to evaluate it
fitted_signal = result(times)

result

Complex exponential_fit

In this example, result is an object of type ComplexResult that has the following attributes:

  • offset: the offset of the data
  • amplitudes: the amplitudes of the modes
  • pulsations: the pulsations of the modes
  • decay_rates: the decay rates of the modes
  • times: the times of the data
  • signal: the original signal
  • frequencies: (read-only) the frequencies of the modes, computed as pulsations / (2 * np.pi)

As a convenience, the result object also has a plot() method that plots the original signal and the fitted function. It also exposes a modes property that returns a list of Mode objects, each containing the amplitude pulsation and decay rate of the mode.

The ComplexResult and ComplexMode classes (as all Result and Mode classes) are callable so you can use them to evaluate the fitted function at any time.

Damped cosine

from bicfit import fit_damped_cosine

signal = signal.real

result = fit_damped_cosine(times, signal)
result.plot()
result

Damped cosine fit

Here the result is an object of type DampedCosineResult that has similar attributes to the ComplexResult class:

  • offset: the offset of the data
  • amplitudes: the amplitudes of the modes
  • phases: the phases of the modes
  • pulsations: the pulsations of the modes
  • decay_rates: the decay rates of the modes
  • times: the times of the data
  • signal: the original signal
  • frequencies: (read-only) the frequencies of the modes, computed as pulsations / (2 * np.pi)

The DampedCosineResult object also has a plot() method that plots the original signal and the fitted function. It exposes a modes property that returns a list of DampedCosineMode objects, each containing the amplitude, pulsation, decay rate, and phase of the mode.

Exponential decay

from bicfit import fit_exponential_decay

noise = np.random.normal(0, sigma_noise, n_points) + 1j * np.random.normal(0, sigma_noise, n_points)
signal = offset + np.exp(- kappa * times) + noise

result = fit_exponential_decay(times, signal, is_complex=True)
result.plot()
result

Exponential decay fit

⚠️ Fitting an exponential can be tricky in general. If you do not have data long enough that the exponential plateaus, the fit will not work well. If you know where the exponential plateaus, you do not need as much data but bicfit does not exploit this knowledge yet.

Multiple modes

a1, a2 = 1.0, 0.5
w1, w2 = 0.2, 0.4
kappa1, kappa2 = 0.05, 0.01

noise = np.random.normal(0, sigma_noise, n_points) + 1j * np.random.normal(0, sigma_noise, n_points)
signal = offset + a1 * np.exp((1j * w1 - kappa1) * times) + a2 * np.exp((1j * w2 - kappa2) * times) + noise

result = fit_complex_exponential(times, signal, n_modes=2, post_fit=True)
result.plot()
result

Multiple modes fit

Here the result is an object of type ExponentialDecayResultthat has similar attributes to the ComplexResult class:

  • offset: the offset of the data
  • amplitudes: the amplitudes of the modes
  • pulsations: the decay rates of the modes
  • times: the times of the data
  • signal: the original signal

It also has the plot() function, modes attribute and is callable like the other result classes.

Fit with no offset

If you know that your data has no offset, you can set the post_fit parameter to bicfit.NoOffset() to force the fit to have no offset.

result = fit_complex_exponential(times, signal, post_fit=bicfit.NoOffset())

Coming features

There are a few features that are not implemented yet but could be in the future, if there is a demand for them:

  • exploiting knowledge of the plateau of an exponential decay
  • fitting non uniformly sampled data
  • port fit functions to JAX
  • [performance] amplitude fitting is probably overkill and could be done from the pencil eigenvectors

Development

You can run tests using pytest:

uv sync --dev
uv run pytest .

Feedback

We welcome any feedback, suggestions, or bug reports to help improve bicfit.

If you find a case where the fit fails, please include your data in CSV format for reproducibility.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bicfit-0.3.10.tar.gz (416.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bicfit-0.3.10-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

File details

Details for the file bicfit-0.3.10.tar.gz.

File metadata

  • Download URL: bicfit-0.3.10.tar.gz
  • Upload date:
  • Size: 416.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for bicfit-0.3.10.tar.gz
Algorithm Hash digest
SHA256 405bbac86e692d62a76b56cc81cdd67b34a7c6002496a4b7c42403ad65926fd5
MD5 f8de351ce42425541de9e00fa03845dc
BLAKE2b-256 b05f70d59f1bbf1ae9fada6790aa329ae8dc0dc17ea5d94553eb434e25999f83

See more details on using hashes here.

File details

Details for the file bicfit-0.3.10-py3-none-any.whl.

File metadata

  • Download URL: bicfit-0.3.10-py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for bicfit-0.3.10-py3-none-any.whl
Algorithm Hash digest
SHA256 f4ad1af7c348c4f02dd1ba5eabb9303c86e551d4a54b5b8dfb671222e5d74a40
MD5 f105a9804b11bfd12ade67c29d1043fd
BLAKE2b-256 997ba3acd28580d330ebcdfafb312310cfa5ec7dfc4247292c11349ad1ce7087

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page