Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3c08b5f
feat: enhance DSP accuracy, performance and compliance
jmrplens Jan 7, 2026
e5fd6a3
fix: address linting, typing and coverage gaps
jmrplens Jan 7, 2026
236da6e
fix: resolve review comments and CI failure
jmrplens Jan 7, 2026
4548a63
feat: optimize impulse weighting with Numba
jmrplens Jan 7, 2026
8736543
chore: drop support for Python < 3.11
jmrplens Jan 7, 2026
542e2ec
chore: project-wide cleanup and Python 3.11 upgrade
jmrplens Jan 7, 2026
380ff6c
fix: final linting, typing and code cleanup
jmrplens Jan 7, 2026
2cdd6b6
feat: proactively apply improvements across the codebase
jmrplens Jan 7, 2026
871f888
perf: further optimize Numba kernel and ensure PR comments alignment
jmrplens Jan 7, 2026
12f45c2
fix: resolve dimension-specific TypeError in Numba kernel and update …
jmrplens Jan 7, 2026
d6eec42
refactor: reduce octavefilter parameters to satisfy SonarCloud (S107)
jmrplens Jan 7, 2026
dad8efd
chore: configure Sonar to allow professional API and restore explicit…
jmrplens Jan 7, 2026
6c4aa61
feat: upgrade filter benchmark with advanced signal theory metrics
jmrplens Jan 7, 2026
f545e46
feat: enhance technical report with visual benchmarks and cleaner output
jmrplens Jan 7, 2026
dd432d0
ci: automate assets update with commit amend
jmrplens Jan 7, 2026
86e7d96
Merge dd432d092b33ae09e59f210386465a42bc14381b into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
50a24dd
Merge 86e7d96822f81786e6177926e59e94af64fd23b2 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
8dcb599
Merge 50a24dd97922dd883fed6a3486169a59bab9f7df into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
d094471
Merge 8dcb59999cf33726bdaa0591af23091b8313d8af into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
1eae280
test: automate 100% coverage reporting with conftest.py
jmrplens Jan 7, 2026
d2ad38d
Merge 1eae280c97377d1b6b1d942f6fa0e2713ea64314 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
e7da09d
Merge d2ad38d8baec003e51ad05d637f19dfb5b2e0d3d into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
f8bb1ba
Merge e7da09d70404fdc605271914467f72ff8b956305 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
d895d47
Merge f8bb1bab7aea3f4b18a37df6154e9a50774cc613 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
3abc10e
Merge d895d47fb23054215625862b5cd358be4c7c03bc into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
a0aa737
Merge 3abc10e1b03a0146d74d33f41ca1e4f749d992c4 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
d7ba9dc
Merge a0aa7374669970e0efdcd2f1404909cb262fd245 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
ac261ec
Merge d7ba9dce9db61ae26f33dcd1964a7b39ab142828 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
1322b72
Merge ac261ec5b65444c2f32494c04f9aaa5ece40a713 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
8f8c4a6
Merge 1322b723b379e4f23ff7b7db08be60df4196f18e into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
d503b06
Merge 8f8c4a63a5f728955dd74a414a7ef4eb6ccf5a76 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
ba348af
Merge d503b06822ec336cedb13678053e0f6b2bd836b3 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
9e0fa1e
Merge ba348af2c2e1d076dff8499c8bd549a8e73f6410 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
a7a0ca1
Merge 9e0fa1e5f7165111d3a99710c86bee0d9fcab7cf into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
1f86d6b
Merge a7a0ca1d91e7492886b5ef32376885413ca801f3 into 07d1c0df5d235d25f…
jmrplens Jan 7, 2026
ebe207e
ci: use standard commits for assets in PRs to avoid conflicts
jmrplens Jan 7, 2026
37e7866
fix: resolve ruff E701 and restrict assets generation to main branch
jmrplens Jan 7, 2026
8bba7de
fix: total cleanup of ruff issues and CI logic
jmrplens Jan 7, 2026
e02b244
fix: resolve security issue and ensure PR comments are published
jmrplens Jan 7, 2026
5cf0602
fix: address security notice B607 and prevent CI loops
jmrplens Jan 7, 2026
b291fc5
docs: regenerate assets and technical report [skip ci]
jmrplens Jan 7, 2026
5f341f6
chore: trigger CI for final verification
jmrplens Jan 7, 2026
3e2d046
chore: bump version to 1.1.0
jmrplens Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/images/benchmark/benchmark_crossover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/images/benchmark/benchmark_precision.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/images/benchmark/benchmark_stability.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_bessel_fraction_1_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_bessel_fraction_3_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_butter_fraction_1_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_butter_fraction_3_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_cheby1_fraction_1_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_cheby1_fraction_3_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_cheby2_fraction_1_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_cheby2_fraction_3_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_ellip_fraction_1_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/filter_ellip_fraction_3_order_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/signal_response_fraction_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/images/time_weighting_analysis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 59 additions & 61 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
contents: read
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -55,98 +55,100 @@ jobs:
pip install -r requirements-dev.txt
pip install -e .
- name: Run tests
env:
NUMBA_DISABLE_JIT: 1
run: |
pytest --junitxml=test-results-${{ matrix.python-version }}.xml --cov=src --cov-report=xml
- name: Run Filter Benchmark
run: |
python scripts/benchmark_filters.py
- name: Upload Test Results
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.python-version }}
path: |
test-results-${{ matrix.python-version }}.xml
filter_benchmark_report.md
coverage.xml
if: always()

sonar:
assets:
needs: tests
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download coverage report
uses: actions/download-artifact@v4
with:
name: test-results-3.13
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_GH }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

graphs:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.TOKEN_GH || github.token }}

- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Generate graphs
run: python generate_graphs.py

- name: Upload Graphs
uses: actions/upload-artifact@v4
with:
name: generated-graphs
path: .github/images/*.png

- name: Deploy Images to Assets Branch

- name: Regenerate all assets
run: |
python generate_graphs.py
python scripts/benchmark_filters.py

- name: Commit and Push changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

# Stash generated images temporarily
mkdir -p temp_images
cp .github/images/*.png temp_images/

# Fetch all branches to ensure we can switch to assets if it exists
git fetch origin
# Check for changes in assets
git add .github/images/ filter_benchmark_report.md

# Checkout assets branch or create orphan if it doesn't exist
if git show-ref --verify --quiet refs/remotes/origin/assets; then
git checkout assets
git pull origin assets
if git diff --staged --quiet; then
echo "No changes in assets detected."
else
git checkout --orphan assets
git rm -rf .
echo "Changes detected, pushing updates..."

if [ "${{ github.event_name }}" == "pull_request" ]; then
# Standard commit for PRs (no amend to avoid rebase hell)
git commit -m "chore: update assets for PR [skip ci]"
git push origin HEAD:${{ github.head_ref }}
else
# Clean amend for main branch
# Append [skip ci] to the original message to prevent loops
orig_msg=$(git log -1 --pretty=%B)
if [[ "$orig_msg" != *"[skip ci]"* ]]; then
git commit --amend -m "$orig_msg [skip ci]"
else
git commit --amend --no-edit
fi
git push origin ${{ github.ref_name }} --force
fi
fi

# Create directory for this run
mkdir -p ${{ github.run_id }}
cp temp_images/*.png ${{ github.run_id }}/

git add ${{ github.run_id }}/
git commit -m "Add graphs for run ${{ github.run_id }}"
git push origin assets
continue-on-error: true

sonar:
needs: tests
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download coverage report
uses: actions/download-artifact@v4
with:
name: test-results-3.13
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_GH }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

pr-comment:
needs: [quality, tests, graphs]
needs: [quality, tests]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: always() && github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
Expand All @@ -158,11 +160,7 @@ jobs:
with:
pattern: test-results-*
path: test-results
- name: Download Graphs
uses: actions/download-artifact@v4
with:
name: generated-graphs
path: generated-graphs
continue-on-error: true

- name: Generate Comment Body
run: python .github/scripts/comment_pr.py
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ sonar:
@if [ -f .env ]; then export $$(cat .env | xargs) && $(PNPM) exec sonar-scanner; else $(PNPM) exec sonar-scanner; fi

test:
$(PYTHON) tests/test_basic.py
$(PYTHON) tests/test_multichannel.py
$(PYTHON) tests/test_audio_processing.py
$(PYTHON) -m pytest tests/

coverage:
$(PYTHON) -m pytest --cov=src/pyoctaveband --cov-report=term-missing tests/

check: lint security test
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Python application](https://github.com/jmrplens/PyOctaveBand/actions/workflows/python-app.yml/badge.svg)](https://github.com/jmrplens/PyOctaveBand/actions/workflows/python-app.yml)

# PyOctaveBand
Advanced Octave-Band and Fractional Octave-Band filter bank for signals in the time domain. Fully compliant with **ANSI s1.11-2004** and **IEC 61260-1-2014**.
Advanced Octave-Band and Fractional Octave-Band filter bank for signals in the time domain. Fully compliant with **ANSI S1.11-2004** (Filters) and **IEC 61672-1:2013** (Time Weighting).

This library provides professional-grade tools for acoustic analysis, including frequency weighting (A, C, Z), time ballistics (Fast, Slow, Impulse), and multiple filter architectures.

Expand All @@ -21,7 +21,7 @@ Now available on [PyPI](https://pypi.org/project/PyOctaveBand/).
- [Gallery of Responses](#gallery-of-filter-bank-responses)
3. [🔊 Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z)
4. [⏱️ Time Weighting and Integration](#️-time-weighting-and-integration)
5. [⚡ Performance: OctaveFilterBank](#-performance-octavefilterbank-class)
5. [⚡ Performance: Multichannel & Vectorization](#-performance-multichannel--vectorization)
6. [🔍 Filter Usage and Examples](#-filter-usage-and-examples)
- [1. Butterworth](#1-butterworth-butter)
- [2. Chebyshev I](#2-chebyshev-i-cheby1)
Expand Down Expand Up @@ -124,7 +124,7 @@ spl, freq = octavefilter(signal, fs=fs, fraction=3)
*Example of a 1/3 Octave Band spectrum analysis of a complex signal.*

### Multichannel Support
PyOctaveBand natively supports multichannel signals (e.g., Stereo, 5.1, Microphone Arrays) without loops. Input arrays of shape `(N_channels, N_samples)` are processed in parallel.
PyOctaveBand natively supports multichannel signals (e.g., Stereo, 5.1, Microphone Arrays) using **fully vectorized operations**. Input arrays of shape `(N_channels, N_samples)` are processed in parallel, offering significant performance gains over iterative loops.

<img src="https://raw.githubusercontent.com/jmrplens/PyOctaveBand/main/.github/images/signal_response_multichannel.png" width="80%"></img>

Expand Down Expand Up @@ -186,13 +186,13 @@ c_weighted_signal = weighting_filter(signal, fs, curve='C')

## ⏱️ Time Weighting and Integration

Accurate SPL measurement requires capturing energy over specific time windows.
Accurate SPL measurement requires capturing energy over specific time windows. PyOctaveBand implements exact time constants per **IEC 61672-1:2013**.

<img src="https://raw.githubusercontent.com/jmrplens/PyOctaveBand/main/.github/images/time_weighting_analysis.png" width="80%"></img>

* **Fast (`fast`):** $\tau = 125$ ms. Standard for noise fluctuations.
* **Slow (`slow`):** $\tau = 1000$ ms. Standard for steady noise.
* **Impulse (`impulse`):** 35 ms rise time. For explosive sounds.
* **Impulse (`impulse`):** **Asymmetric** ballistics. 35 ms rise time for rapid onset capture, 1500 ms decay for readability.

```python
from pyoctaveband import time_weighting
Expand All @@ -205,9 +205,9 @@ spl_t = 10 * np.log10(energy_envelope / (2e-5)**2)

---

## ⚡ Performance: OctaveFilterBank Class
## ⚡ Performance: Multichannel & Vectorization

Pre-calculating coefficients saves significant CPU time when processing multiple frames.
The `OctaveFilterBank` class is highly optimized for real-time and batch processing. It uses NumPy vectorization to handle multichannel audio arrays (e.g., 64-channel microphone arrays) without explicit Python loops, ensuring maximum throughput.

```python
from pyoctaveband import OctaveFilterBank
Expand Down
64 changes: 36 additions & 28 deletions filter_benchmark_report.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# Filter Architecture Benchmark Report

This report compares the performance and characteristics of the available filter types.

## 1. Spectral Isolation (at 1kHz)
| Filter Type | Peak SPL (dB) | Atten. -1 Oct (dB) | Atten. +1 Oct (dB) | Atten. -2 Oct (dB) | Atten. +2 Oct (dB) |
|---|---|---|---|---|---|
| butter | 90.96 | 40.0 | 32.3 | 46.8 | 57.7 |
| cheby1 | 90.96 | 39.8 | 40.2 | 46.5 | 57.2 |
| cheby2 | 90.96 | 42.5 | 50.4 | 49.2 | 61.6 |
| ellip | 90.95 | 39.9 | 45.1 | 46.6 | 57.1 |
| bessel | 90.54 | 41.6 | 33.6 | 48.4 | 60.1 |

## 2. Stability and Performance
| Filter Type | Max IR Tail Energy | Stability Status | Avg. Execution Time (s) |
|---|---|---|---|
| butter | 1.29e-09 | ✅ Stable | 0.0353 |
| cheby1 | 2.04e-07 | ✅ Stable | 0.0348 |
| cheby2 | 2.12e-07 | ✅ Stable | 0.0355 |
| ellip | 4.95e-07 | ✅ Stable | 0.0359 |
| bessel | 4.21e-15 | ✅ Stable | 0.0451 |

## 3. Analysis Summary
- **Butterworth:** Best compromise, maximally flat passband.
- **Chebyshev I:** Steeper roll-off than Butterworth but with passband ripple.
- **Chebyshev II:** Flat passband, ripple in the stopband.
- **Elliptic:** Steepest transition but ripples in both passband and stopband.
- **Bessel:** Best phase response and minimal ringing (group delay), but slowest roll-off.
# PyOctaveBand: Technical Benchmark Report

Generated: 2026-01-07 09:42:54

## 1. Test Signal Parameters
- **Sample Rate:** 96.0 kHz
- **Duration:** 10.0 seconds
- **Signal Types:** White Noise (Stability) / Pure Sine (Precision)
- **Precision:** 64-bit Floating Point

## 2. Crossover (Linkwitz-Riley)
![Crossover](.github/images/benchmark/benchmark_crossover.png)

- **Flatness Error:** 0.000000 dB (Target < 0.01)

## 3. Precision & Isolation
![Precision](.github/images/benchmark/benchmark_precision.png)

| Type | Error (dB) | Isolation | Ripple | GD Std (ms) |
|:---|:---:|:---:|:---:|:---:|
| butter | 2.46e-03 | 31.3 dB | 0.2705 dB | 2847.826 |
| cheby1 | 3.38e-03 | 40.5 dB | 0.1000 dB | 3551.677 |
| cheby2 | 3.26e-03 | 57.8 dB | 29.4187 dB | 4790.013 |
| ellip | 9.41e-03 | 54.2 dB | 0.1000 dB | 4700.881 |
| bessel | 5.20e-01 | 32.5 dB | 5.9845 dB | 1380.212 |

## 4. Performance
![Performance](.github/images/benchmark/benchmark_performance.png)

| Channels | Exec Time (s) | Speedup |
|:---|:---:|:---:|
| 1 | 0.542 | 1.00x |
| 2 | 1.060 | 1.02x |
| 4 | 2.091 | 1.04x |
| 8 | 4.170 | 1.04x |
| 16 | 8.398 | 1.03x |
30 changes: 21 additions & 9 deletions generate_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,33 +396,45 @@ def generate_weighting_responses(output_dir: str) -> None:


def generate_time_weighting_plot(output_dir: str) -> None:
"""Visualize Fast and Slow time weighting response to a burst."""
"""Visualize Fast, Slow and Impulse time weighting response to a burst."""
print("Generating time_weighting_analysis.png...")
fs = 1000
t = np.linspace(0, 3, fs * 3, endpoint=False)
t = np.linspace(0, 4, fs * 4, endpoint=False)

# 500ms burst of noise
# 500ms burst of noise starting at 1.0s
rng = np.random.default_rng(42)
x = np.zeros_like(t)
x[fs:fs+int(fs*0.5)] = rng.standard_normal(int(fs*0.5))
start_idx = int(fs * 1.0)
end_idx = int(fs * 1.5)
x[start_idx:end_idx] = rng.standard_normal(end_idx - start_idx)

from pyoctaveband import time_weighting

# Square for energy
x_sq = x**2
fast = time_weighting(x, fs, mode="fast")
slow = time_weighting(x, fs, mode="slow")
impulse = time_weighting(x, fs, mode="impulse")

_, ax = plt.subplots()
ax.plot(t, x_sq, color=COLOR_GRID, alpha=0.5, label="Instantaneous Energy ($x^2$)")
# Normalize for better visualization
# We normalized x_sq to peak at 1 for the plot
peak = np.max(x_sq)
x_sq /= peak
fast /= peak
slow /= peak
impulse /= peak

ax.plot(t, x_sq, color=COLOR_GRID, alpha=0.5, label="Input Burst (Normalized)")
ax.plot(t, fast, color=COLOR_PRIMARY, label="Fast (125ms)")
ax.plot(t, slow, color=COLOR_SECONDARY, label="Slow (1000ms)")
ax.plot(t, impulse, color="purple", linestyle="-.", linewidth=1.5, label="Impulse (35ms/1.5s)")

ax.set_title("Time Weighting Ballistics (Fast vs Slow)", fontweight="bold")
ax.set_title("Time Weighting Ballistics (IEC 61672-1)", fontweight="bold")
ax.set_xlabel("Time [s]")
ax.set_ylabel("Squared Amplitude")
ax.legend(loc="lower right")
ax.set_xlim(0.8, 3.0)
ax.set_ylabel("Normalized Response")
ax.legend(loc="upper right")
ax.set_xlim(0.8, 3.5)
plt.savefig(os.path.join(output_dir, "time_weighting_analysis.png"))
plt.close()

Expand Down
Loading