AcyclePy: Advanced cyclostratigraphy, time series analysis, and paleoclimate toolkit
Project description
AcyclePy -- Cyclostratigraphy and Time Series Analysis Toolkit
AcyclePy is the Python library companion to the Acycle desktop application, providing a programmatic API for cyclostratigraphy, time series analysis, and paleoclimate research. It also bundles the full Acycle desktop GUI tools.
Reference: Li, M., Hinnov, L., & Kump, L. (2019). Acycle: Time-series analysis software for paleoclimate research and education. Computers & Geosciences, 127, 12-22.
Installation
ash
pip install acycle
Requirements: Python >= 3.8. Core dependencies are installed automatically.
Optional dependencies:
ash
pip install "acycle[dev]" # pytest, black, flake8
pip install "acycle[full]" # sounddevice, h5py, netCDF4
pip install statsmodels # better lowess performance
Verify:
python
import acycle as ac
s = ac.Series(x=[1,2,3], y=[10,20,30])
print(s) # Series(n=3, x=[1, 3] , y_name='y')
Result Objects
All analysis functions return structured result objects with o_dataframe(), save(), and plot() methods:
| Class | Description |
|---|---|
| PSD | Power spectral density |
| EvolutiveSpectrum | Evolutionary (time-varying) spectrum |
| WaveletResult | Continuous wavelet transform |
| FilterResult | Filtered signal with optional amplitude envelope |
| AgeModel | Age-depth model with sedimentation rate |
| CocoResult | COCO/eCOCO correlation analysis |
| SedNoiseResult | DYNOT sedimentation noise |
python
psd = PSD(frequency=freq, power=power)
psd.to_dataframe() # pandas DataFrame
psd.save("my_spectrum") # writes my_spectrum_spectrum.csv
psd.plot() # matplotlib figure
psd.settings # parameters used
Example 1: Spectral Analysis of the LR04 Benthic Stack
This example walks through loading a classic paleoclimate dataset, preprocessing it, computing its power spectrum, and identifying Milankovitch cycles.
`python import acycle as ac import numpy as np
Step 1: Load the LR04 benthic d18O stack (Lisiecki & Raymo, 2005).
This is a 0-5320 ka record with 2115 unevenly-spaced data points.
lr04 = ac.load_example("lr04") print(f"Loaded: {lr04.n} points, span {lr04.x_min:.0f}-{lr04.x_max:.0f} ka")
-> Loaded: 2115 points, span 0-5320 ka
Step 2: Interpolate to a uniform 1-kyr grid.
The original data has variable spacing (median ~2.5 kyr).
Linear interpolation onto a 1-kyr grid gives 5321 evenly-spaced points.
lr04_uniform = lr04.interpolate(step=1.0, method="linear") print(f"Interpolated: {lr04_uniform.n} points, dt={lr04_uniform.dt:.2f} ka")
-> Interpolated: 5321 points, dt=1.00 ka
Step 3: Detrend with a 400-kyr lowess window.
This removes long-term trends (>400 kyr) while preserving orbital cycles.
lr04_dt = lr04_uniform.detrend(window=400, window_unit="value", method="lowess") print(f"Detrended: mean={lr04_dt.y.mean():.4f}, std={lr04_dt.y.std():.4f}")
Step 4: Compute the multitaper power spectrum.
nw=3 gives 5 tapers (2*nw-1), balancing frequency resolution and variance.
from acycle import spectral, PSD freq, power = spectral._mtm_spectrum(lr04_dt.y, dt=lr04_dt.dt, nw=3) period = 1.0 / freq # convert frequency to period (kyr)
Step 5: Identify the top 10 spectral peaks.
peak_idx = np.argsort(power)[-10:][::-1] print("\nTop 10 spectral peaks:") for i in peak_idx: if period[i] > 1: # skip the zero-frequency component print(f" Period: {period[i]:7.1f} kyr Power: {power[i]:.4f}")
Typical output shows peaks near 405, 100, 41, 23, 19 kyr.
Step 6: Save the spectrum.
psd = PSD(frequency=freq, power=power, period=period, method="MTM(nw=3)") psd.save("lr04_spectrum") # saves to lr04_spectrum_spectrum.csv psd.plot() # displays the power spectrum `
Example 2: Continuous Wavelet Transform
This example shows how to perform wavelet analysis to see how spectral power evolves through time, useful for detecting non-stationary behavior in climate records.
`python import acycle as ac import numpy as np
Step 1: Load and prepare the LR04 0-2000 ka section.
lr04 = ac.load_example("lr04") s = lr04.select(0, 2000).interpolate(step=1.0)
Step 2: Detrend to remove the long-term d18O trend.
s_dt = s.detrend(window=200, window_unit="value", method="lowess")
Step 3: Compute the continuous wavelet transform (CWT).
Morlet wavelet (default), scanning periods from 2-500 kyr.
dj=0.05 gives fine period resolution (~100 scale steps per octave).
pad=True zero-pads to the next power of 2 for faster FFT.
from acycle import wavelet, WaveletResult period, power, coi, significance = wavelet.cwt( s_dt.y, dt=s_dt.dt, period_min=2, period_max=500, dj=0.05, mother="MORLET", pad=True )
Step 4: Wrap in a WaveletResult for easy access.
wr = WaveletResult( x=s_dt.x, period=period, power=power, coi=coi, significance=significance, mother="MORLET" )
Step 5: Export to DataFrame (periods as rows, time as columns).
df = wr.to_dataframe() print(f"Wavelet power shape: {df.shape}")
Step 6: Extract the time-averaged (global) wavelet spectrum.
global_power = power.mean(axis=1)
Step 7: Find dominant periods.
peak_idx = np.argsort(global_power)[-5:][::-1] print("\nDominant periods (global wavelet):") for i in peak_idx: print(f" {period[i]:.1f} kyr: power={global_power[i]:.4f}")
Step 8: Plot and save.
wr.plot(cmap="viridis") # time-period heatmap with cone of influence wr.save("lr04_wavelet") # saves to lr04_wavelet.csv
Step 9: Cross-wavelet coherence between two series segments.
s1 = s.select(0, 1000) s2 = s.select(500, 1500) coh = wavelet.wavelet_coherence(s1.y[:500], s2.y[-500:], dt=s1.dt) print(f"Coherence keys: {list(coh.keys())}")
-> ['period', 'coherence', 'phase', 'coi', 'dt']
`
Example 3: Full Age Modeling and Tuning Workflow
This example demonstrates building an age model from cycle counting, tuning a depth-domain series to the time domain, and computing sedimentation rates.
`python import acycle as ac import numpy as np
Step 1: Generate a synthetic depth-domain series with 405-kyr cycles.
np.random.seed(42) depth = np.arange(0, 200, 0.1) # 0-200 meters at 10-cm spacing
405 kyr * ~2 cm/kyr = ~8.1 m per cycle -> ~24.7 cycles over 200 m
signal = np.sin(2 * np.pi * depth / 8.1) noise = 0.3 * np.random.randn(len(depth)) y = signal + noise s = ac.Series(x=depth, y=y, x_name="Depth", x_unit="m", y_name="Proxy")
Step 2: Interpolate to uniform spacing.
s = s.interpolate(step=0.1, method="linear")
Step 3: Build an age model by counting 405-kyr cycles.
anchor="max" counts from eccentricity maxima.
start_age=0 sets the first counted cycle to 0 ka.
from acycle import age, AgeModel model = age.build_age_model( s.x, s.y, cycle_period=405, anchor="max", start_age=0, age_direction="increasing" )
Step 4: Examine the age model.
print(f"Tie points: {len(model['tie_points'])}") print(f"Depth range: {model['depth'][0]:.1f}-{model['depth'][-1]:.1f} m") print(f"Age range: {model['age'][0]:.1f}-{model['age'][-1]:.1f} ka")
Expected: ~24 tie points covering ~0-9720 ka
Step 5: Compute sedimentation rate (m/kyr).
sed_rates = model["sed_rate"] print(f"Mean sedimentation rate: {sed_rates.mean():.3f} m/kyr") print(f" = {sed_rates.mean()*100:.1f} cm/kyr")
Step 6: Tune the depth-domain series to time domain.
tuned_x, tuned_y = age.tune(s.x, s.y, model) tuned_series = ac.Series(x=tuned_x, y=tuned_y, x_name="Age", x_unit="ka") print(f"Tuned series: {tuned_series.n} pts, dt={tuned_series.dt:.2f} ka")
Step 7: Verify tuning by checking the 405 kyr peak.
tuned_dt = tuned_series.detrend(window=400, method="lowess") freq, power = ac.spectral._mtm_spectrum(tuned_dt.y, dt=tuned_dt.dt, nw=2) period = 1.0 / freq peak = period[np.argmax(power[1:])+1] print(f"Dominant spectral period: {peak:.1f} kyr (expected ~405)")
Step 8: Create and save an AgeModel result object.
am = AgeModel( depth=model["depth"], age=model["age"], sed_rate=model["sed_rate"] ) am.to_dataframe() am.plot() # Harker diagram: depth vs age am.save("my_age_model") `
Quick Reference
Reading data:
python
s = ac.read_series("file.txt") # whitespace
s = ac.read_series("file.csv", delimiter=",") # comma
lr04 = ac.load_example("lr04") # built-in dataset
Series processing:
python
s.clean(sort=True).interpolate(step=1.0).detrend(window=400).standardize()
Spectral analysis:
python
freq, power = ac.spectral._mtm_spectrum(y, dt=1.0, nw=3)
freq, power = ac.spectral._lomb_scargle_spectrum(x, y)
Wavelet analysis:
python
period, power, coi, sig = ac.wavelet.cwt(y, dt=1.0, period_min=2, period_max=500)
coh = ac.wavelet.wavelet_coherence(y1, y2, dt=1.0)
Filtering:
python
result = ac.filter.apply_filter(y, dt=1.0, kind="bandpass", flow=0.01, fhigh=0.05)
result = ac.filter.amplitude_modulation(x, y, flow=0.01, fhigh=0.05)
Age modeling:
python
model = ac.age.build_age_model(depth, y, cycle_period=405)
tuned_x, tuned_y = ac.age.tune(depth, y, model)
Preprocessing:
python
yd, trend = ac.preprocess.detrend(x, y, window=0.35, method="lowess")
x, y = ac.preprocess.multiply_series(x, y1, y2)
df = ac.preprocess.merge_series([(x1,y1,"A"), (x2,y2,"B")])
GUI Tools
ash
acycle-imageprocessor acycle-interpolation acycle-gap-adder
acycle-plot acycle-data-extractor acycle-data-clipper
acycle-image-analyzer acycle-section-remover
Notes
pandas >= 2.0: Tested with pandas 2.x. The removed delim_whitespace parameter has been replaced with sep=r"\s+".
Headless servers: The programmatic API works without any display. GUI tools require a graphical environment.
statsmodels: Install statsmodels for faster lowess/loess detrending. Falls back to pure-SciPy if unavailable.
File encoding on Windows: Chinese Windows uses cp936/GBK by default. For UTF-8 files, pass encoding="utf-8" to ead_series().
Large files: For files > 100 MB, load with pandas and construct Series:
python
import pandas as pd
df = pd.read_csv("large.csv", usecols=[0,1])
s = ac.Series(x=df.iloc[:,0].values, y=df.iloc[:,1].values)
Built-in Datasets
| Name | Description | Points | Reference |
|---|---|---|---|
| lr04 | LR04 benthic d18O | 2,115 | Lisiecki & Raymo (2005) |
| cenogrid_d18o | CENOGRID d18O | 23,659 | Westerhold et al. (2020) |
| cenogrid_d13c | CENOGRID d13C | 23,666 | Westerhold et al. (2020) |
| la2004_etp | La2004 ETP | 2,001 | Laskar et al. (2004) |
| petm_logfe | Svalbard PETM logFe | 392 | Charles et al. (2011) |
| wayao_gr | Wayao Carnian GR | 499 | Li et al. (2018) |
| guandao_gr | Guandao Anisian GR | 1,071 | Li et al. (2018) |
| ewark_depth_rank | Newark Depth Rank | -- | Olsen & Kent (1999) | | ednoise_0.7_2000 | Synthetic red noise | 2,000 | -- | | launa_loa_co2 | Mauna Loa CO2 | 722 | NOAA | | csa_extinction | CSA extinction | 10 | -- |
Citation
Li, M., Hinnov, L., & Kump, L. (2019). Acycle: Time-series analysis software for paleoclimate research and education. Computers & Geosciences, 127, 12-22.
License
MIT
Links
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file acycle-0.6.9.tar.gz.
File metadata
- Download URL: acycle-0.6.9.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bbc6b045e523aa168c5a64152d8ac668813da3d91f8d01034a94b0a05803599
|
|
| MD5 |
2ee60c43b33970d30105bcf072c35c2f
|
|
| BLAKE2b-256 |
f161ca2240d76dd59d454d666e66da44500aaf6ef6c25f590523b7125ce74a59
|
File details
Details for the file acycle-0.6.9-py3-none-any.whl.
File metadata
- Download URL: acycle-0.6.9-py3-none-any.whl
- Upload date:
- Size: 1.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a04ed6ac7e4fa184b216b9f275422decdf2c080e74d207ac990e4c806da32546
|
|
| MD5 |
878001d42badaa9d74d474bbb4e5514f
|
|
| BLAKE2b-256 |
4acdb8b1119fde490d38d7aadc532378f1e98b28cd31856addbd498fd8afe1ef
|