Skip to main content

AcyclePy: Advanced cyclostratigraphy toolkit

Project description

AcyclePy — Advanced Cyclostratigraphy and Time Series Analysis Toolkit

Version 0.5.1 | PyPI | Repository | MIT License

AcyclePy is the Python library companion to the Acycle desktop application (Li, Hinnov & Kump, 2019). It provides a programmatic API (no GUI required) for scientific time-series analysis: cyclostratigraphy, spectral analysis, wavelet analysis, filtering, age modeling, astronomical calculations, image digitization, and more.

The library follows a Pyleoclim-style fluent design: load a time or depth series, chain preprocessing and analysis calls, then plot or export results — all in a few lines of code.

import acycle as ac

s = ac.Series.from_file("data.txt", x_unit="m", y_name="GR")
s2 = s.clean(sort=True).interpolate(step=0.33).detrend(window=0.3, method="lowess")
psd = s2.spectral(method="mtm", nw=2, noise="robust_ar1", fmax=1.0)
psd.plot(xaxis="period")

Note: This package also bundles the full Acycle desktop GUI tools. Use acycle-imageprocessor, acycle-plot, etc. from the command line.


Installation

pip install acycle

Requirements: Python >= 3.8, numpy, scipy, pandas, matplotlib, astropy, PySide6 (for GUI tools).


Table of Contents

  1. Core Concepts
  2. Quick Start Examples
  3. The Series Object
  4. Data I/O
  5. Preprocessing
  6. Spectral Analysis
  7. Wavelet Analysis
  8. Filtering
  9. Age Modeling & Tuning
  10. Cyclostratigraphy (COCO / eCOCO)
  11. Sedimentation Noise (DYNOT / rho1)
  12. Astronomical Calculations
  13. Image Processing & Digitizing
  14. Plotting
  15. CLI Tools (GUI Compatibility)
  16. All Result Objects
  17. Built-in Example Datasets
  18. Full API Reference

Core Concepts

Concept Class/Function Description
Time series ac.Series Two-column (x, y) data with units and metadata
Multi-series ac.MultiSeries Collection of Series with shared x-axis
Image ac.Image Image for digitization and analysis
Power spectrum ac.PSD Result of spectral() — frequency, power, noise
Wavelet ac.WaveletResult CWT power, period, COI, significance
Filtered signal ac.FilterResult Result of filter() — amplitude, phase
Age model ac.AgeModel Depth-age pairs, sedimentation rate
COCO result ac.CocoResult Correlation coefficient vs sedimentation rate
Sediment noise ac.SedNoiseResult DYNOT / rho1 noise quantification

Every result object supports:

  • result.plot() — generate a matplotlib figure
  • result.to_dataframe() — export to pandas DataFrame
  • result.save("prefix") — save to files
  • result.settings — dict of parameters used

Quick Start Examples

Example 1: Load data, clean, interpolate, detrend, compute spectrum

import acycle as ac
import matplotlib.pyplot as plt

# Load bundled example
s = ac.load_example("la2004_etp")

# Chain preprocessing
s2 = (s.clean(sort=True, dropna=True, duplicate="mean")
      .interpolate(step=2.0, method="linear")
      .detrend(window=0.3, method="lowess"))

# Compute power spectrum with red-noise background
psd = s2.spectral(method="mtm", nw=2, noise="robust_ar1", fmax=1.0)
psd.plot(xaxis="period")
plt.show()

Expected output: A figure with the power spectrum (black), median-smoothed curve (red), and red-noise confidence levels (blue dashed). Peaks above the 99% confidence line are statistically significant.

Example 2: Generate synthetic data and run wavelet analysis

import acycle as ac
import numpy as np

# Create a signal with two periodic components
t = np.arange(0, 1000, 1.0)
y = (np.sin(2 * np.pi * t / 100)     # 100-kyr cycle
     + 0.5 * np.sin(2 * np.pi * t / 41)   # 41-kyr cycle
     + 0.2 * np.sin(2 * np.pi * t / 23)   # 23-kyr cycle
     + np.random.randn(len(t)) * 0.1)     # white noise

s = ac.Series(x=t, y=y, x_name="Time", x_unit="kyr", y_name="Signal")

# Continuous wavelet transform
wav = s.wavelet(mother="MORLET", period_min=4, period_max=256)
# wav is a WaveletResult with power, coi, significance attributes
print(f"Periods: {wav.period[:5]}...")
print(f"Power shape: {wav.power.shape}")

Expected output:

Periods: [4.  4.29 4.6  4.93 5.28]...
Power shape: (79, 1000)

Example 3: COCO — cyclostratigraphic correlation coefficient

import acycle as ac

# Load a depth-domain gamma ray log
s = ac.Series.from_file(
    "data/Examples/Example-WayaoCarnianGR0.txt",
    x_unit="m", y_name="GR"
)

# Clean and interpolate
s = s.clean(sort=True).interpolate(step=0.33)

# COCO analysis: test sedimentation rates 4.29 to 29.89 cm/kyr at 0.2 step
coco = s.coco(
    median_age=230,        # Ma
    sed_rate=(4.29, 29.89, 0.2),  # (min, max, step) cm/kyr
    n_sim=200,             # Monte Carlo simulations
)

best_sr = coco.sed_rate[np.argmax(coco.rho)]
print(f"Best sedimentation rate: {best_sr:.1f} cm/kyr")
print(f"Correlation: {coco.rho.max():.3f}, H0-SL: {coco.h0_sl.min():.4f}")

Example 4: Bandpass filtering

import acycle as ac
import numpy as np

s = ac.Series(x=np.linspace(0, 500, 1000),
              y=np.sin(2*np.pi*np.linspace(0,500,1000)/100),
              x_name="Depth", x_unit="m", y_name="Signal")

# Gaussian bandpass filter around 0.01 cycles/m
result = s.filter(kind="bandpass", method="gaussian", flow=0.005, fhigh=0.015)
filt = result.filtered  # a new Series

print(f"Filtered: {filt}")
print(f"Amplitude available: {result.amplitude is not None}")

Example 5: Image digitization (no GUI)

import acycle as ac
import numpy as np

# Create a simple test image (20x20 RGB)
img_data = np.random.randint(0, 255, (20, 20, 3), dtype=np.uint8)
img = ac.Image(data=img_data, name="Test")

# Convert to grayscale
gray = img.to_grayscale()
print(f"Grayscale shape: {gray.shape}")

# Extract intensity profile
profile = img.profile()
print(f"Profile length: {len(profile['intensity'])} pixels")

The Series Object

Creating a Series

# From raw arrays
s = ac.Series(x=[1, 2, 3, 4, 5], y=[10, 20, 15, 25, 30],
              x_name="Depth", x_unit="m", y_name="GR", y_unit="API")

# From a file
s = ac.Series.from_file(
    "data.txt",
    columns=(0, 1),         # x, y column indices (0-based)
    delimiter=None,         # auto-detect: comma, tab, or whitespace
    x_unit="m", y_name="GR",
    sort=True, dropna=True, duplicate="mean",
)

# Load a built-in example
s = ac.load_example("la2004_etp")
s = ac.load_lr04(start=0, stop=5320)   # LR04 benthic stack
s = ac.load_cenogrid(variable="d18o")  # CENOGRID

Properties

Property Type Description
s.x, s.y ndarray Raw data arrays
s.n int Number of points
s.dt float Median sampling interval
s.x_min, s.x_max float X-axis extent
s.x_name, s.y_name str Axis labels
s.x_unit, s.y_unit str Unit labels
s.history list[dict] Chain of applied operations
s.metadata dict Arbitrary metadata

Chainable Methods

s2 = s.clean(sort=True, dropna=True)          # → Series
s3 = s2.interpolate(step=0.33)                 # → Series
s4 = s3.detrend(window=0.3, method="lowess")   # → Series
s5 = s4.standardize()                          # → Series
s6 = s4.clip_by_threshold(0.5)                 # → Series
s7 = s4.moving_average(10)                     # → Series
psd = s3.spectral()                            # → PSD
wav = s3.wavelet()                             # → WaveletResult
filt = s3.filter(kind="bandpass", flow=0.01, fhigh=0.1)  # → FilterResult
coco = s3.coco(230, (4.29, 29.89, 0.2))       # → CocoResult

Quick Plot

s.plot(kind="line", color="steelblue", line_width=1.5,
       xlabel="Depth (m)", ylabel="Value", grid=True)

Data I/O

Function Description Returns
ac.read_series(path, **kw) Read two-column data file Series
ac.write_series(series, path) Write to delimited file str (path)
ac.extract_columns(path, x_col, y_col) Extract columns from multi-column file Series
ac.new_folder(path) Create directory str
ac.new_text(path) Create empty text file str
ac.save_figure(fig, path) Save matplotlib figure str
s = ac.read_series("data.csv", columns=(1, 3), delimiter=",",
                    x_unit="m", y_name="TOC", sort=True)
ac.write_series(s, "output.txt", sep="\t")
ac.save_figure(fig, "plot.pdf", dpi=300)

Preprocessing

Series.detrend(window=0.35, method="lowess")

Remove long-term trends. Supported methods: "linear", "polynomial", "lowess", "loess", "rlowess", "rloess", "moving_mean", "savgol".

# Remove 35% lowess trend
s_dt = s.detrend(window=0.35, method="lowess")
# Or specify window in axis units
s_dt = s.detrend(window=80, window_unit="axis", method="lowess")
# Get trend as well
s_dt, s_trend = s.detrend(window=0.3, return_trend=True)

Series.clip_by_threshold(threshold, side="above", mode="delete")

# Delete all values above 10
s2 = s.clip_by_threshold(10, side="above", mode="delete")
# Cap values above 10 to exactly 10
s2 = s.clip_by_threshold(10, side="above", mode="cap")
# Set values below -5 to zero
s2 = s.clip_by_threshold(-5, side="below", mode="zero")

Other preprocessing methods

Method Description
s.interpolate(step, method) Uniform resampling
s.interpolate_pro(step, method) Advanced interpolation with gap filling
s.interpolate_to(reference) Resample onto another Series' x-grid
s.select(start, stop) Extract a sub-range
s.standardize() Z-score: (y - mean) / std
s.log10(handle_nonpositive) Base-10 logarithm
s.derivative(order) Numerical derivative
s.remove_sections(sections) Delete sections [(start, stop), ...]
s.add_gaps(gaps) Insert NaN gaps [(position, duration), ...]
s.remove_peaks(ymin, ymax, mode) Cap or set-to-NaN peaks
s.changepoint(method) Detect changepoints in y
s.transform_xy(a, b, c, d) Affine: x_new=ax+b, y_new=cy+d
s.find_extreme(kind) Find maximum or minimum value
s.multiply(other) Multiply by another Series element-wise
s.moving_average(n) Simple moving average
s.gaussian_smooth(n, sigma) Gaussian kernel smooth
s.moving_median(n) Running median filter

Utility functions:

Function Description
ac.merge_series(list) Merge multiple series into DataFrame
ac.pca(data, n_components) Principal component analysis

Spectral Analysis

Series.spectral(method="mtm", **kw)

psd = s.spectral(
    method="mtm",              # "mtm" | "lomb_scargle" | "periodogram"
    nw=2,                      # time-bandwidth product (MTM)
    noise="robust_ar1",        # "classic_ar1" | "robust_ar1" | "power_law" | None
    fmax=1.0,                  # max frequency; "nyquist" for Nyquist
    median_smooth=0.2,         # smoothing window fraction
    confidence=(0.90, 0.95, 0.99, 0.999),
)
# Plot with period axis (log scale)
psd.plot(xaxis="period")
# Export to DataFrame
df = psd.to_dataframe()
# Save to CSV
psd.save("spectrum_output")
# Access settings
print(psd.settings)
# Access noise model
print(f"Rho: {psd.rho}, Noise power: {psd.noise_power[:5]}")

Series.spectral_swa(**kw)

Sliding-window spectral analysis with FDR confidence levels.

psd_swa = s.spectral_swa(fmin=0, fmax=0.1)

Series.evolutionary_spectral(**kw)

Sliding-window (evolutive) spectrogram.

evo = s.evolutionary_spectral(
    method="fft",              # "fft" | "lah_fft" | "mtm" | "lomb_scargle"
    window=0.35,               # window fraction
    step=0.05,                 # sliding step
    nw=2,                      # MTM parameter
)
# evo.x, evo.frequency, evo.power are 2D arrays

Series.prewhiten(method="robust_ar1")

s_pw = s.prewhiten(method="robust_ar1")  # robust AR(1) removal
s_pw = s.prewhiten(method="classic_ar1") # classic AR(1)
s_pw = s.prewhiten(method="user", rho=0.5)  # user-specified rho

Wavelet Analysis

Series.wavelet(**kw)

Continuous wavelet transform (CWT) based on Torrence & Compo (1998).

wav = s.wavelet(
    mother="MORLET",           # "MORLET" | "PAUL" | "DOG"
    period_min=2 * s.dt,       # default: 2 * sampling interval
    period_max=0.5 * s.n * s.dt,  # default: half record length
    dj=0.1,                    # scale resolution
    pad=True,                  # zero-pad to next power of 2
)

# Access results
print(wav.power.shape)         # (n_periods, n_time)
print(wav.period[:5])          # period axis
print(wav.coi)                 # cone of influence
print(wav.significance)        # significance vs chi2 test

Series.wavelet_coherence(other, **kw)

s2 = ac.Series(x=s.x, y=s.y + np.random.randn(len(s.y)) * 0.1)
coherence = s.wavelet_coherence(s2)
print(coherence["coherence"].shape)  # (n_periods, n_time)

Filtering

Series.filter(**kw)

# Bandpass filter
result = s.filter(
    kind="bandpass",           # "bandpass" | "lowpass" | "highpass"
    method="gaussian",         # "gaussian" | "taner" | "butter" | "cheby1" | "ellip"
    flow=0.01, fhigh=0.05,    # frequency bounds
    remove_mean=True,
)

filt = result.filtered        # filtered Series
amp = result.amplitude        # amplitude envelope (taner/gaussian)
phase = result.phase          # instantaneous phase
freq_inst = result.instantaneous_frequency  # instantaneous frequency

Series.dynamic_filter(**kw)

result = s.dynamic_filter(
    window=0.35, fmin=0.005, fmax=0.05,
    lower_bound=[(0, 0.01), (500, 0.02)],  # evolving lower boundary
    upper_bound=[(0, 0.06), (500, 0.04)],  # evolving upper boundary
)

Series.amplitude_modulation(flow, fhigh, **kw)

am = s.amplitude_modulation(flow=0.008, fhigh=0.012)
envelope = am["envelope"]     # Hilbert envelope of filtered band

Age Modeling & Tuning

Series.build_age_model(cycle_period, **kw)

age_model = s.build_age_model(405, anchor="max", start_age=0)
# age_model.depth, age_model.age, age_model.sed_rate

Series.tune(age_model, **kw)

s_tuned = s.tune(age_model, direction="depth_to_time")
# s_tuned is now in the time domain

ac.sedrate_to_age_model(depth, sedrate, **kw)

am = ac.sedrate_to_age_model(depth=d, sedrate=sr, sedrate_unit="cm/kyr")

ac.stratigraphic_correlation(ref, target, tie_points)

result = ac.stratigraphic_correlation(
    (x_ref, y_ref), (x_targ, y_targ),
    tie_points=[(0, 0), (50, 45), (100, 95)],
)

Cyclostratigraphy (COCO / eCOCO)

Series.coco(median_age, sed_rate, **kw)

Correlation COefficient analysis.

coco = s.coco(
    median_age=230,            # Ma
    sed_rate=(4.29, 29.89, 0.2),  # cm/kyr
    n_sim=2000,                # Monte Carlo simulations (2000+ for publication)
    astronomical_periods=None, # auto: Berger(1989) periods
)

best_idx = np.argmax(coco.rho)
print(f"Best SR: {coco.sed_rate[best_idx]:.1f} cm/kyr, rho={coco.rho[best_idx]:.3f}")

Series.ecoco(median_age, sed_rate, **kw)

Evolutionary COCO — sliding-window analysis.

ecoco = s.ecoco(
    median_age=230,
    sed_rate=(4.29, 29.89, 0.2),
    window=0.35,               # sliding window fraction
    n_sim=500,
)
# ecoco.positions, ecoco.rho_2d, ecoco.h0_sl_2d

Sedimentation Noise (DYNOT / rho1)

Series.dynot(**kw)

dynot = s.dynot(
    window_range=(300, 500),   # kyr
    step=5,                    # kyr
    n_sim=1000,                # bootstrap iterations
)
# dynot.age, dynot.median, dynot.quantiles

Series.rho1_noise(**kw)

rho1 = s.rho1_noise(mode="single", window=0.35)
print(f"rho1: {rho1.settings}")

Astronomical Calculations

ac.insolation(start, stop, **kw)

ins = ac.insolation(0, 1000, step=1, day=80, latitude=65, solution="La2004")

ac.astronomical_solution(start, stop, **kw)

etp = ac.astronomical_solution(0, 1000, step=1, output="ETP", normalize=True)
ecc = ac.astronomical_solution(0, 1000, step=1, output="eccentricity")

ac.milankovitch_calculator(**kw)

result = ac.milankovitch_calculator(model="Waltham2015", age=100)

ac.signal_noise(**kw)

sine = ac.signal_noise(start=0, stop=1000, model="sine", period=100, amplitude=5, seed=42)
red = ac.signal_noise(start=0, stop=500, model="red_noise", mean=0, std=1, rho=0.7, seed=42)
white = ac.signal_noise(start=0, stop=200, model="white_noise", mean=0, std=1)
poly = ac.signal_noise(start=0, stop=10, step=0.1, model="polynomial", polynomial_coeffs=[1, 2, 3])

Image Processing & Digitizing

ac.Image(data, path, **kw)

img = ac.Image(path="photo.jpg", name="MyPhoto")

# Display
img.show()

# Grayscale
gray = img.to_grayscale(output="gray.png")

# CIE Lab conversion
lab = img.to_lab()

# Intensity profile along a line
profile = img.profile(control_points=[(0, 10), (100, 50)])

# Digitize data points from an image
points = img.digitize(
    axis_points=[(10, 20), (200, 20), (10, 20), (10, 180)],
    axis_values=[(0, 100), (0, 50)],  # ((x1, x2), (y1, y2))
    color=(255, 0, 0), tolerance=50,
)

Plotting

Single series

s.plot(kind="line", color="steelblue", grid=True, xlabel="Depth (m)")

Multi-series stacked

ac.plot_standardized([s1, s2, s3], method="zscore", offset=2)
ac.plot_offset([s1, s2], method="zscore", offset=2)

Multi-panel

ac.plot_multi([s1, s2], layout=(2, 1), shared_x=True, title="Subplots")

Save figure

ac.save_figure(fig, "output.pdf", dpi=300, format="pdf")

Series helpers

sr = s.sampling_rate(plot=True)        # resolution histogram
vd = s.value_distribution(bins=50)     # distribution + Q-Q plot
s.to_sound("data.wav", repeat=5, sample_rate=8192)  # sonification

CLI Tools (GUI Compatibility)

These commands launch the original Acycle desktop GUI tools:

Command Tool
acycle-imageprocessor Image Processor — digitization and analysis
acycle-plot PlotPro — interactive plotting
acycle-interpolation [file] Interpolation Pro
acycle-data-extractor [file] Data Extractor
acycle-section-remover [file] Section Remover
acycle-gap-adder [file] Gap Adder
acycle-data-clipper [file] Data Clipper
acycle-image-analyzer Image Analyzer

Or from Python:

ac.launch_imageprocessor(image="photo.jpg")
ac.launch_plotpro(files=["data.txt"])
ac.launch_interpolation(data_file="data.txt")

All Result Objects

Object Key Attributes Methods
PSD .frequency, .power, .period, .noise_power, .rho .plot(), .to_dataframe(), .save()
EvolutiveSpectrum .x, .frequency, .power .to_dataframe()
WaveletResult .x, .period, .power, .coi, .significance
FilterResult .filtered, .amplitude, .phase, .instantaneous_frequency
AgeModel .depth, .age, .sed_rate, .tie_points
CocoResult .sed_rate, .rho, .p_value, .h0_sl
SedNoiseResult .age, .median, .quantiles

Built-in Example Datasets

s = ac.load_example("la2004_etp")           # La2004 ETP solution
s = ac.load_example("petm_logfe")           # PETM log-Fe data
s = ac.load_example("wayao_gr")             # Carnian gamma ray log
s = ac.load_example("newark_depth_rank")    # Newark Basin depth ranks
s = ac.load_example("rednoise_0.7_2000")    # Synthetic red noise
s = ac.load_example("guandao_gr")           # Guandao Anisian GR
s = ac.load_example("launa_loa_co2")        # Mauna Loa CO2
s = ac.load_example("csa_extinction")       # Extinction event data

Full API Reference

Top-level imports

from acycle import (
    Series, MultiSeries,                 # core data
    PSD, EvolutiveSpectrum,             # spectral results
    WaveletResult, FilterResult,         # wavelet & filter results
    AgeModel, CocoResult, SedNoiseResult,  # age & cyclostrat results
    Image,                                # image analysis
    # IO
    read_series, write_series, load_example, load_lr04, load_cenogrid,
    new_folder, new_text, save_figure, extract_columns,
    # Basic
    insolation, astronomical_solution, milankovitch_calculator, signal_noise,
    # Preprocessing
    detrend, interpolate_pro, clip_by_threshold, remove_sections,
    add_gaps, remove_peaks, changepoint, merge_series, pca,
    transform_xy, find_extreme, multiply_series,
    # Plotting
    plot_multi, plot_standardized, plot_offset,
    # CLI
    launch_imageprocessor, launch_plotpro, launch_interpolation,
    launch_data_extractor, launch_section_remover, launch_gap_adder,
    launch_data_clipper, launch_image_analyzer,
)

Submodule imports

from acycle.spectral import _mtm_spectrum, _lomb_scargle_spectrum
from acycle.wavelet import cwt, wavelet_coherence
from acycle.filter import apply_filter, dynamic_filter, amplitude_modulation
from acycle.age import build_age_model, tune, sedrate_to_age_model, stratigraphic_correlation
from acycle.astrochron import coco, ecoco, dynot, rho1_noise
from acycle.preprocess import detrend, clip_by_threshold, changepoint, pca
from acycle.plot_api import plot_multi, plot_standardized, save_figure
from acycle.image_api import Image

Citation

Li, M., Hinnov, L., & Kump, L. (2019). Acycle: Time-series analysis software for paleoclimate research and education. Computers & Geosciences, 127, 12-22. https://doi.org/10.1016/j.cageo.2019.02.011


Changelog

  • v0.5.1: Bug fixes (lowess fallback, fmax string handling, wavelet chi2, spectral imports)
  • v0.5.0: Complete programmatic API with 70+ functions (all AcyclePy_API.docx requirements)
  • v0.3.0—0.4.0: CLI wrappers and basic package structure
  • v0.1.x: Original PyPI package with GUI tools only (acycle-imageprocessor, acycle-plot, acycle-interpolation, etc.)

Project details


Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

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

acycle-0.5.2-py3-none-any.whl (820.7 kB view details)

Uploaded Python 3

File details

Details for the file acycle-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: acycle-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 820.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for acycle-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 0ed57fc4a9b092fe0afb21618508ef464ba9eab4870e70efa236e35628f8b8ba
MD5 112e807d066c19c191bf0e0455ae7f8f
BLAKE2b-256 13eacd21e4e3ee694b786acee1d434fe61fedca1654aa101c01f6be6c319e744

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