From 4248dcc01436d5baeebfa7dda33f94f3f389f7e4 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Wed, 28 Jan 2026 15:14:07 +0000 Subject: [PATCH 01/17] Make the releases automatic --- .github/actions/update-version/action.yml | 30 ++++++++++++ .github/workflows/distribution.yml | 57 +++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/actions/update-version/action.yml diff --git a/.github/actions/update-version/action.yml b/.github/actions/update-version/action.yml new file mode 100644 index 00000000000..dce3d27c0f8 --- /dev/null +++ b/.github/actions/update-version/action.yml @@ -0,0 +1,30 @@ +name: Update Version in setup.py +description: "Updates release flag and version string in setup.py." + +inputs: + version: + description: 'The version string to set.' + required: true + release: + description: 'The boolean state for the release flag (true/false).' + required: true + +runs: + using: "composite" + steps: + - name: Update setup.py + shell: bash + run: | + # Convert release input to Python boolean (True/False) + RELEASE_BOOL=$(echo "${{ inputs.release }}" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}') + python3 -c " + import sys, re + version = sys.argv[1] + release_flag = sys.argv[2] + with open('setup.py', 'r') as f: + content = f.read() + content = re.sub(r'^release = .*', f'release = {release_flag}', content, flags=re.MULTILINE) + content = re.sub(r'^version = .*', f\"version = '{version}'\", content, flags=re.MULTILINE) + with open('setup.py', 'w') as f: + f.write(content) + " "${{ inputs.version }}" "$RELEASE_BOOL" \ No newline at end of file diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index eacb2ccb071..14d8f6154a8 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -21,6 +21,15 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.11' + - name: Set release version environment variable + if: startsWith(github.ref, 'refs/tags/v') + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Update version file for release + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/update-version + with: + version: ${{ env.RELEASE_VERSION }} + release: 'true' - name: Install cibuildwheel run: python -m pip install cibuildwheel - name: Build wheels @@ -51,6 +60,15 @@ jobs: pattern: wheel-* merge-multiple: true path: dist/ + - name: Set release version environment variable + if: startsWith(github.ref, 'refs/tags/v') + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Update version file for release + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/update-version + with: + version: ${{ env.RELEASE_VERSION }} + release: 'true' - name: Build source distribution run: | python -c 'import setuptools ; print("setuptools version", setuptools.__version__)' @@ -62,3 +80,42 @@ jobs: uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 with: password: ${{ secrets.pypi_password }} + + bump_version: + name: Bump version to dev + needs: deploy_pypi + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: master + + - name: Calculate and set next version + id: versioning + run: | + # Extract version from tag (e.g. v1.2.3 -> 1.2.3) + VERSION=${GITHUB_REF#refs/tags/v} + IFS='.' read -r -a parts <<< "$VERSION" + MAJOR=${parts[0]} + MINOR=${parts[1]} + PATCH=${parts[2]} + NEXT_PATCH=$((PATCH + 1)) + NEXT_VERSION="${MAJOR}.${MINOR}.dev${NEXT_PATCH}" + echo "next_version=${NEXT_VERSION}" >> $GITHUB_OUTPUT + + - name: Update version to next dev + uses: ./.github/actions/update-version + with: + version: ${{ steps.versioning.outputs.next_version }} + release: 'false' + + - name: Commit and push version bump + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add setup.py + git commit -m "Back to development: ${{ steps.versioning.outputs.next_version }}" + git push origin master From cc7c4e39c74d426e0973772bdd86a1239bdbe759 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Wed, 28 Jan 2026 15:50:00 +0000 Subject: [PATCH 02/17] Update release instructions --- docs/release.rst | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/release.rst b/docs/release.rst index eeeb74b0a94..d520371e7d1 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -13,16 +13,10 @@ Creating the release on GitHub To create a new PyCBC release: -#. Make sure that the setup.py file contains the correct version number, which should be in the format ``x.y.z`` (where x, y, and z are the major, minor, and patch levels of the release) in PyCBC's setup.py file. -#. Set ``release = True`` in the PyCBC's setup.py file. -#. Commit the changed setup.py file and push to commit to the repository. #. Go to the `PyCBC release page `_ and click on ``Draft a new release``. #. Enter a tag version in the format ``vx.y.z``. Note the ``v`` in front of the major, minor, and patch numbers. #. Enter a descriptive release title and write a description of the release in the text box provided. #. Click on ``Publish release`` to create the release. -#. Update the setup.py file with an incremented major or minor version number and make sure that the string ``dev`` appears in that version. For example, if you just released ``1.2.1`` then change the string to ``1.3.dev0`` or ``2.0.dev0`` as appropriate. This is needed to ensure that if someone is building from source, it always takes precedence over an older release version. -#. Set ``release = False`` in PyCBC's setup.py file. -#. Commit these changes and push them to the repository. .. note:: @@ -30,7 +24,7 @@ To create a new PyCBC release: unless you are back-porting a bug fix from a new release series to an old production release series. -Creating the release will trigger a Travis build that updates CVMFS, Docker, and PyPI with the release. +Creating the release will trigger a CI build that updates CVMFS, Docker, and PyPI with the release, and increments the patch number. Please ensure that you check the outputs of these builds and urgently report any errors to other maintainers, you will be emailed if the builds fails. ------------------------------------------------ @@ -39,7 +33,7 @@ Backporting Bug Fixes to Previous Release Series Branches should only be created when bug fixes from master need to be back ported to an old release series (e.g. adding a bug fix from the 1.4 series to -the 1.3 series. +the 1.3 series. ) To create a branch for the bug fix, make a new branch from the last release tag and cherry pick the changes to that branch. For example, to create a From 2415dd91c64314c81452cc4fecc52a34a9d9a4f3 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Wed, 28 Jan 2026 16:23:56 +0000 Subject: [PATCH 03/17] Update .github/workflows/distribution.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/distribution.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index 14d8f6154a8..c6c7292758a 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -98,10 +98,21 @@ jobs: run: | # Extract version from tag (e.g. v1.2.3 -> 1.2.3) VERSION=${GITHUB_REF#refs/tags/v} + # Ensure the version is in the expected MAJOR.MINOR.PATCH format with numeric components + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Tag '$GITHUB_REF' is not in the supported 'vMAJOR.MINOR.PATCH' format." >&2 + echo "Got VERSION='$VERSION'. Example of a valid tag: v1.2.3" >&2 + exit 1 + fi IFS='.' read -r -a parts <<< "$VERSION" MAJOR=${parts[0]} MINOR=${parts[1]} PATCH=${parts[2]} + # PATCH is guaranteed numeric by the regex above, but we keep this guard for extra safety + if ! [[ "$PATCH" =~ ^[0-9]+$ ]]; then + echo "Error: Patch component '$PATCH' from tag '$GITHUB_REF' is not numeric." >&2 + exit 1 + fi NEXT_PATCH=$((PATCH + 1)) NEXT_VERSION="${MAJOR}.${MINOR}.dev${NEXT_PATCH}" echo "next_version=${NEXT_VERSION}" >> $GITHUB_OUTPUT From d7b62d946a30dcfc61993088a23ab46be2ac6dd4 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Wed, 28 Jan 2026 16:26:54 +0000 Subject: [PATCH 04/17] Update .github/actions/update-version/action.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/actions/update-version/action.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/actions/update-version/action.yml b/.github/actions/update-version/action.yml index dce3d27c0f8..85cc7eded4f 100644 --- a/.github/actions/update-version/action.yml +++ b/.github/actions/update-version/action.yml @@ -23,8 +23,10 @@ runs: release_flag = sys.argv[2] with open('setup.py', 'r') as f: content = f.read() - content = re.sub(r'^release = .*', f'release = {release_flag}', content, flags=re.MULTILINE) - content = re.sub(r'^version = .*', f\"version = '{version}'\", content, flags=re.MULTILINE) + content, release_count = re.subn(r'^release = .*', f'release = {release_flag}', content, flags=re.MULTILINE) + content, version_count = re.subn(r'^version = .*', f\"version = '{version}'\", content, flags=re.MULTILINE) + if release_count != 1 or version_count != 1: + raise SystemExit(f\"Expected exactly 1 replacement for each of 'release' and 'version' lines, got release={release_count}, version={version_count}\") with open('setup.py', 'w') as f: f.write(content) " "${{ inputs.version }}" "$RELEASE_BOOL" \ No newline at end of file From 8ac6db19935e59dc202da3bc6eed551a4df3314f Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Wed, 28 Jan 2026 16:25:32 +0000 Subject: [PATCH 05/17] use startsWith(github.ref, 'refs/tags') consistently --- .github/workflows/distribution.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index c6c7292758a..5b0f4afd888 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -22,10 +22,10 @@ jobs: with: python-version: '3.11' - name: Set release version environment variable - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags') run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Update version file for release - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags') uses: ./.github/actions/update-version with: version: ${{ env.RELEASE_VERSION }} @@ -61,10 +61,10 @@ jobs: merge-multiple: true path: dist/ - name: Set release version environment variable - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags') run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Update version file for release - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags') uses: ./.github/actions/update-version with: version: ${{ env.RELEASE_VERSION }} @@ -84,7 +84,7 @@ jobs: bump_version: name: Bump version to dev needs: deploy_pypi - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest permissions: contents: write From 66caf7699163ddb2f411115b541c1e9bf22b6a82 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Thu, 29 Jan 2026 09:09:44 +0000 Subject: [PATCH 06/17] check if the release is from default branch, then bump development version number if it is --- .github/workflows/distribution.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index 5b0f4afd888..e11d8ebc46b 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -91,9 +91,21 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: master + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + + - name: Check if tag is on default branch + id: check_branch + run: | + if git merge-base --is-ancestor ${{ github.sha }} HEAD; then + echo "is_default=true" >> $GITHUB_OUTPUT + else + echo "Tag ${{ github.ref }} is not on ${{ github.event.repository.default_branch }}. Skipping bump." + echo "is_default=false" >> $GITHUB_OUTPUT + fi - name: Calculate and set next version + if: steps.check_branch.outputs.is_default == 'true' id: versioning run: | # Extract version from tag (e.g. v1.2.3 -> 1.2.3) @@ -118,15 +130,17 @@ jobs: echo "next_version=${NEXT_VERSION}" >> $GITHUB_OUTPUT - name: Update version to next dev + if: steps.check_branch.outputs.is_default == 'true' uses: ./.github/actions/update-version with: version: ${{ steps.versioning.outputs.next_version }} release: 'false' - name: Commit and push version bump + if: steps.check_branch.outputs.is_default == 'true' run: | git config user.name "GitHub Actions" git config user.email "actions@github.com" git add setup.py git commit -m "Back to development: ${{ steps.versioning.outputs.next_version }}" - git push origin master + git push origin ${{ github.event.repository.default_branch }} From 0396bfe358a6984528aa6791a497a8ea68f1d24a Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Thu, 29 Jan 2026 09:25:13 +0000 Subject: [PATCH 07/17] (safety) Check if the next version is the one already in the repository --- .github/workflows/distribution.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index e11d8ebc46b..a6959971f5d 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -82,7 +82,7 @@ jobs: password: ${{ secrets.pypi_password }} bump_version: - name: Bump version to dev + name: Bump version to development needs: deploy_pypi if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest @@ -94,7 +94,7 @@ jobs: ref: ${{ github.event.repository.default_branch }} fetch-depth: 0 - - name: Check if tag is on default branch + - name: Check if the tag is on the default branch id: check_branch run: | if git merge-base --is-ancestor ${{ github.sha }} HEAD; then @@ -104,7 +104,7 @@ jobs: echo "is_default=false" >> $GITHUB_OUTPUT fi - - name: Calculate and set next version + - name: Calculate the next version if: steps.check_branch.outputs.is_default == 'true' id: versioning run: | @@ -142,5 +142,9 @@ jobs: git config user.name "GitHub Actions" git config user.email "actions@github.com" git add setup.py - git commit -m "Back to development: ${{ steps.versioning.outputs.next_version }}" - git push origin ${{ github.event.repository.default_branch }} + if git diff --staged --quiet; then + echo "No changes to commit." + else + git commit -m "Set back to development: ${{ steps.versioning.outputs.next_version }}" + git push origin ${{ github.event.repository.default_branch }} + fi From 2d22593716023b6d2efda042a0339716bd3283ca Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Thu, 29 Jan 2026 09:26:37 +0000 Subject: [PATCH 08/17] versioning match to PEP440 --- .github/workflows/distribution.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index a6959971f5d..f69d3fd360f 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -126,7 +126,7 @@ jobs: exit 1 fi NEXT_PATCH=$((PATCH + 1)) - NEXT_VERSION="${MAJOR}.${MINOR}.dev${NEXT_PATCH}" + NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev0" echo "next_version=${NEXT_VERSION}" >> $GITHUB_OUTPUT - name: Update version to next dev From e0c68bf16fa53d3726153066e990f90ef72a093a Mon Sep 17 00:00:00 2001 From: pracchia <78790650+pracchia@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:45:51 +0100 Subject: [PATCH 09/17] Float64 cast in map pixels sum (#5271) --- pycbc/distributions/sky_location.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycbc/distributions/sky_location.py b/pycbc/distributions/sky_location.py index 53a3c187bc2..6ecf6522091 100644 --- a/pycbc/distributions/sky_location.py +++ b/pycbc/distributions/sky_location.py @@ -321,10 +321,10 @@ def __init__(self, **params): # Sanity-check the probabilities, and ensure they are normalized # correctly (sum to one). assert type(self.pix_probs) == numpy.ndarray - sum_pix_probs = sum(self.pix_probs) + sum_pix_probs = self.pix_probs.sum(dtype=numpy.float64) if not numpy.isclose(sum_pix_probs, 1): warnings.warn( - f'Sum of probs in HEALPix map is {sum(self.pix_probs)}, ' + f'Sum of probs in HEALPix map is {self.pix_probs.sum(dtype=numpy.float64)}, ' 'far from 1. Something might be wrong with that map' ) self.pix_probs /= sum_pix_probs From 79cc894a6ab70d637b82bd318b481aab6a2b580c Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 09:42:20 +0000 Subject: [PATCH 10/17] Change timeseries, frequencyseries epoch to be float64 rather than ligotimegps (#5237) * add constants module to fall back on astropy/numpy values if lal is not installed * Update timeseries to use a float64 rather than ligotimegps * inject constants * Use import_optional to avoid repeating try/excepts * unused imports * More removal of LAL constants/routines * More movement away from LAL * Need a couple more checks in timeseries * removing lal from tests * Use astropy rather than lal gpstime in live plotting * Move gaussian noise module away from lal, add test coverage for that module * psd reading, results/versioning shouldnt require lal * More work * copilot review suggestions * Unused imports * Add some logic to allow LIGOTimeGPS to be passed into frequencyseries or timeseries * testing print * Allow wider variation in psd * Fix to eventmgr which assumes a ligotimegps * epoch_float64: use LIGOTimeGPS for epoch (switch epoch handling to float64) * epoch_float64: move epoch debug instrumentation and tracing helpers here (origins buffer, trace helpers, test instrumentation) * re moving unrelated changes * Removing unrelated changes * more unrelated stuff * Set coinc, stat and test live coinc back to master * Cleanup of this branch * Cleanup of this branch * I think this is the right stuff to make a start again * Fix stable sort problem with test_live_coinc_compare * Dont know where this change came from., but its wrong * Apply suggestion from @GarethCabournDavies * I cant remember why this change was made - lets revert it and run the tests * Handle slicing with None epochs for timeseries * Move epoch logic to a shared function, in-depth explanation logic of how epochs are input/converted --- pycbc/results/versioning.py | 100 ++++++++++++++++-------------- pycbc/types/array.py | 5 +- pycbc/types/frequencyseries.py | 49 +++++---------- pycbc/types/timeseries.py | 47 ++++++-------- pycbc/types/utils.py | 78 +++++++++++++++++++++++ pycbc/waveform/generator.py | 23 ++++--- test/fft_base.py | 32 +++++----- test/test_frame.py | 8 +-- test/test_frequencyseries.py | 78 ++++++++++++++++++----- test/test_live_coinc_compare.py | 28 +++++++-- test/test_timeseries.py | 38 +++++++++--- test/validation_code/old_coinc.py | 2 +- 12 files changed, 316 insertions(+), 172 deletions(-) create mode 100644 pycbc/types/utils.py diff --git a/pycbc/results/versioning.py b/pycbc/results/versioning.py index 03db3408207..ebc47a4130e 100644 --- a/pycbc/results/versioning.py +++ b/pycbc/results/versioning.py @@ -18,10 +18,13 @@ import subprocess import urllib.parse -import lal -import lalframe - import pycbc.version +from pycbc.libutils import import_optional + +lal = import_optional('lal') +lalframe = import_optional('lalframe') +lalsimulation = import_optional('lalsimulation') + logger = logging.getLogger('pycbc.results.versioning') @@ -42,53 +45,54 @@ def add_info_new_version(info_dct, curr_module, extra_str): info_dct['Committer'] = vcs_object.vcsCommitter info_dct['Date'] = vcs_object.vcsDate - lalinfo = {} - lalinfo['Name'] = 'LAL' - try: - lalinfo['ID'] = lal.VCSId - lalinfo['Status'] = lal.VCSStatus - lalinfo['Version'] = lal.VCSVersion - lalinfo['Tag'] = lal.VCSTag - lalinfo['Author'] = lal.VCSAuthor - lalinfo['Branch'] = lal.VCSBranch - lalinfo['Committer'] = lal.VCSCommitter - lalinfo['Date'] = lal.VCSDate - except AttributeError: - add_info_new_version(lalinfo, lal, '') - library_list.append(lalinfo) + if lal is not None: + lalinfo = {} + lalinfo['Name'] = 'LAL' + try: + lalinfo['ID'] = lal.VCSId + lalinfo['Status'] = lal.VCSStatus + lalinfo['Version'] = lal.VCSVersion + lalinfo['Tag'] = lal.VCSTag + lalinfo['Author'] = lal.VCSAuthor + lalinfo['Branch'] = lal.VCSBranch + lalinfo['Committer'] = lal.VCSCommitter + lalinfo['Date'] = lal.VCSDate + except AttributeError: + add_info_new_version(lalinfo, lal, '') + library_list.append(lalinfo) + + if lalframe is not None: + lalframeinfo = {} + try: + lalframeinfo['Name'] = 'LALFrame' + lalframeinfo['ID'] = lalframe.FrameVCSId + lalframeinfo['Status'] = lalframe.FrameVCSStatus + lalframeinfo['Version'] = lalframe.FrameVCSVersion + lalframeinfo['Tag'] = lalframe.FrameVCSTag + lalframeinfo['Author'] = lalframe.FrameVCSAuthor + lalframeinfo['Branch'] = lalframe.FrameVCSBranch + lalframeinfo['Committer'] = lalframe.FrameVCSCommitter + lalframeinfo['Date'] = lalframe.FrameVCSDate + except AttributeError: + add_info_new_version(lalframeinfo, lalframe, 'Frame') + library_list.append(lalframeinfo) - lalframeinfo = {} - try: - lalframeinfo['Name'] = 'LALFrame' - lalframeinfo['ID'] = lalframe.FrameVCSId - lalframeinfo['Status'] = lalframe.FrameVCSStatus - lalframeinfo['Version'] = lalframe.FrameVCSVersion - lalframeinfo['Tag'] = lalframe.FrameVCSTag - lalframeinfo['Author'] = lalframe.FrameVCSAuthor - lalframeinfo['Branch'] = lalframe.FrameVCSBranch - lalframeinfo['Committer'] = lalframe.FrameVCSCommitter - lalframeinfo['Date'] = lalframe.FrameVCSDate - except AttributeError: - add_info_new_version(lalframeinfo, lalframe, 'Frame') - library_list.append(lalframeinfo) + if lalsimulation is not None: + lalsimulationinfo = {} + lalsimulationinfo['Name'] = 'LALSimulation' + try: + lalsimulationinfo['ID'] = lalsimulation.SimulationVCSId + lalsimulationinfo['Status'] = lalsimulation.SimulationVCSStatus + lalsimulationinfo['Version'] = lalsimulation.SimulationVCSVersion + lalsimulationinfo['Tag'] = lalsimulation.SimulationVCSTag + lalsimulationinfo['Author'] = lalsimulation.SimulationVCSAuthor + lalsimulationinfo['Branch'] = lalsimulation.SimulationVCSBranch + lalsimulationinfo['Committer'] = lalsimulation.SimulationVCSCommitter + lalsimulationinfo['Date'] = lalsimulation.SimulationVCSDate + except AttributeError: + add_info_new_version(lalsimulationinfo, lalsimulation, 'Simulation') - lalsimulationinfo = {} - lalsimulationinfo['Name'] = 'LALSimulation' - try: - import lalsimulation - lalsimulationinfo['ID'] = lalsimulation.SimulationVCSId - lalsimulationinfo['Status'] = lalsimulation.SimulationVCSStatus - lalsimulationinfo['Version'] = lalsimulation.SimulationVCSVersion - lalsimulationinfo['Tag'] = lalsimulation.SimulationVCSTag - lalsimulationinfo['Author'] = lalsimulation.SimulationVCSAuthor - lalsimulationinfo['Branch'] = lalsimulation.SimulationVCSBranch - lalsimulationinfo['Committer'] = lalsimulation.SimulationVCSCommitter - lalsimulationinfo['Date'] = lalsimulation.SimulationVCSDate - except AttributeError: - add_info_new_version(lalsimulationinfo, lalsimulation, 'Simulation') - except ImportError: - pass - library_list.append(lalsimulationinfo) + library_list.append(lalsimulationinfo) pycbcinfo = {} pycbcinfo['Name'] = 'PyCBC' diff --git a/pycbc/types/array.py b/pycbc/types/array.py index 9959a3700d4..dc646c43f97 100644 --- a/pycbc/types/array.py +++ b/pycbc/types/array.py @@ -33,7 +33,7 @@ from functools import wraps import h5py -import lal as _lal + import numpy as _numpy from numpy import float32, float64, complex64, complex128, ones from numpy.linalg import norm @@ -41,6 +41,9 @@ import pycbc.scheme as _scheme from pycbc.scheme import schemed, cpuonly from pycbc.opt import LimitedSizeDict +from pycbc.libutils import import_optional + +_lal = import_optional('lal') #! FIXME: the uint32 datatype has not been fully tested, # we should restrict any functions that do not allow an diff --git a/pycbc/types/frequencyseries.py b/pycbc/types/frequencyseries.py index 47e65870f49..b4cb25afdd7 100644 --- a/pycbc/types/frequencyseries.py +++ b/pycbc/types/frequencyseries.py @@ -19,10 +19,15 @@ """ import os as _os import h5py -from pycbc.types.array import Array, _convert, zeros, _noreal -import lal as _lal import numpy as _numpy +from pycbc.types.array import Array, _convert, zeros, _noreal +from pycbc.types.utils import determine_epoch +from pycbc.types import float64 +from pycbc.libutils import import_optional + +_lal = import_optional('lal') + class FrequencySeries(Array): """Models a frequency series consisting of uniformly sampled scalar values. @@ -32,7 +37,7 @@ class FrequencySeries(Array): Array containing sampled data. delta_f : float Frequency between consecutive samples in Hertz. - epoch : {None, lal.LIGOTimeGPS}, optional + epoch : {None, lal.LIGOTimeGPS, float64}, optional Start time of the associated time domain data in seconds. dtype : {None, data-type}, optional Sample data type. @@ -50,33 +55,10 @@ def __init__(self, initial_array, delta_f=None, epoch="", dtype=None, copy=True) raise TypeError('must provide either an initial_array with a delta_f attribute, or a value for delta_f') if not delta_f > 0: raise ValueError('delta_f must be a positive number') - # We gave a nonsensical default value to epoch so we can test if it's been set. - # If the user passes in an initial_array that has an 'epoch' attribute and doesn't - # pass in a value of epoch, then our new object's epoch comes from initial_array. - # But if the user passed in a value---even 'None'---that will take precedence over - # anything set in initial_array. Finally, if the user passes in something without - # an epoch attribute *and* doesn't pass in a value of epoch, it becomes 'None' - if not isinstance(epoch,_lal.LIGOTimeGPS): - if epoch == "": - if isinstance(initial_array,FrequencySeries): - epoch = initial_array._epoch - else: - epoch = _lal.LIGOTimeGPS(0) - elif epoch is not None: - try: - if isinstance(epoch, _numpy.generic): - # In python3 lal LIGOTimeGPS will not work on numpy - # types as input. A quick google on how to generically - # convert numpy floats/ints to the python equivalent - # https://stackoverflow.com/questions/9452775/ - epoch = _lal.LIGOTimeGPS(epoch.item()) - else: - epoch = _lal.LIGOTimeGPS(epoch) - except: - raise TypeError('epoch must be either None or a lal.LIGOTimeGPS') + Array.__init__(self, initial_array, dtype=dtype, copy=copy) self._delta_f = delta_f - self._epoch = epoch + self._epoch = determine_epoch(epoch, initial_array) def _return(self, ary): return FrequencySeries(ary, self._delta_f, epoch=self._epoch, copy=False) @@ -100,11 +82,12 @@ def get_delta_f(self): doc="Frequency between consecutive samples in Hertz.") def get_epoch(self): - """Return frequency series epoch as a LIGOTimeGPS. + """Return frequency series epoch """ return self._epoch + epoch = property(get_epoch, - doc="Frequency series epoch as a LIGOTimeGPS.") + doc="Frequency series epoch.") def get_sample_frequencies(self): """Return an Array containing the sample frequencies. @@ -138,7 +121,7 @@ def start_time(self): def start_time(self, time): """ Set the start time """ - self._epoch = _lal.LIGOTimeGPS(time) + self._epoch = float64(time) @property def end_time(self): @@ -334,7 +317,7 @@ def lal(self): LAL frequency series object containing the same data as self. The actual type depends on the sample's dtype. If the epoch of self was 'None', the epoch of the returned LAL object will be - LIGOTimeGPS(0,0); otherwise, the same as that of self. + LIGOTimeGPS(0,0); otherwise, convert it to LIGOTimeGPS Raises ------ @@ -346,7 +329,7 @@ def lal(self): if self._epoch is None: ep = _lal.LIGOTimeGPS(0,0) else: - ep = self._epoch + ep = _lal.LIGOTimeGPS(self._epoch) if self._data.dtype == _numpy.float32: lal_data = _lal.CreateREAL4FrequencySeries("",ep,0,self.delta_f,_lal.SecondUnit,len(self)) diff --git a/pycbc/types/timeseries.py b/pycbc/types/timeseries.py index 6c1bb187218..f81e26a58d4 100644 --- a/pycbc/types/timeseries.py +++ b/pycbc/types/timeseries.py @@ -19,14 +19,18 @@ """ import os as _os import h5py + +import numpy as _numpy +from scipy.io.wavfile import write as write_wav + from pycbc.types.array import Array, _convert, complex_same_precision_as, zeros +from pycbc.types.utils import determine_epoch from pycbc.types.array import _nocomplex from pycbc.types.frequencyseries import FrequencySeries from pycbc.types import float32, float64 -import lal as _lal -import numpy as _numpy -from scipy.io.wavfile import write as write_wav +from pycbc.libutils import import_optional +_lal = import_optional('lal') class TimeSeries(Array): """Models a time series consisting of uniformly sampled scalar values. @@ -46,7 +50,7 @@ class TimeSeries(Array): """ def __init__(self, initial_array, delta_t=None, - epoch=None, dtype=None, copy=True): + epoch="", dtype=None, copy=True): if len(initial_array) < 1: raise ValueError('initial_array must contain at least one sample.') if delta_t is None: @@ -57,23 +61,10 @@ def __init__(self, initial_array, delta_t=None, if not delta_t > 0: raise ValueError('delta_t must be a positive number') - # Get epoch from initial_array if epoch not given (or is None) - # If initialy array has no epoch, set epoch to 0. - # If epoch is provided, use that. - if not isinstance(epoch, _lal.LIGOTimeGPS): - if epoch is None: - if isinstance(initial_array, TimeSeries): - epoch = initial_array._epoch - else: - epoch = _lal.LIGOTimeGPS(0) - elif epoch is not None: - try: - epoch = _lal.LIGOTimeGPS(epoch) - except: - raise TypeError('epoch must be either None or a lal.LIGOTimeGPS') + self._epoch = determine_epoch(epoch, initial_array) + Array.__init__(self, initial_array, dtype=dtype, copy=copy) self._delta_t = delta_t - self._epoch = epoch def to_astropy(self, name='pycbc'): """ Return an astropy.timeseries.TimeSeries instance @@ -91,6 +82,8 @@ def to_astropy(self, name='pycbc'): def epoch_close(self, other): """ Check if the epoch is close enough to allow operations """ + if self._epoch is None or other._epoch is None: + return False dt = abs(float(self.start_time - other.start_time)) return dt <= 1e-7 @@ -125,8 +118,8 @@ def _typecheck(self, other): self.start_time, other.start_time)) def _getslice(self, index): - # Set the new epoch---note that index.start may also be None - if index.start is None: + # Set the new epoch - index.start or self._epoch may be None + if index.start is None or self._epoch is None: new_epoch = self._epoch else: if index.start < 0: @@ -215,7 +208,7 @@ def delta_f(self): @property def start_time(self): - """Return time series start time as a LIGOTimeGPS. + """Return time series start time. """ return self._epoch @@ -223,14 +216,14 @@ def start_time(self): def start_time(self, time): """ Set the start time """ - self._epoch = _lal.LIGOTimeGPS(time) + self._epoch = float64(time) def get_end_time(self): - """Return time series end time as a LIGOTimeGPS. + """Return time series end time. """ return self._epoch + self.get_duration() end_time = property(get_end_time, - doc="Time series end time as a LIGOTimeGPS.") + doc="Time series end time.") def get_sample_times(self): """Return an Array containing the sample times. @@ -483,7 +476,7 @@ def lal(self): LAL time series object containing the same data as self. The actual type depends on the sample's dtype. If the epoch of self is 'None', the epoch of the returned LAL object will be - LIGOTimeGPS(0,0); otherwise, the same as that of self. + LIGOTimeGPS(0,0); Raises ------ @@ -491,7 +484,7 @@ def lal(self): If time series is stored in GPU memory. """ lal_data = None - ep = self._epoch + ep = _lal.LIGOTimeGPS(self._epoch) if self._data.dtype == _numpy.float32: lal_data = _lal.CreateREAL4TimeSeries("",ep,0,self.delta_t,_lal.SecondUnit,len(self)) diff --git a/pycbc/types/utils.py b/pycbc/types/utils.py new file mode 100644 index 00000000000..37b001911c6 --- /dev/null +++ b/pycbc/types/utils.py @@ -0,0 +1,78 @@ +import logging +import numpy as _numpy +from numpy import float64 + +from pycbc.libutils import import_optional + +logger = logging.getLogger('pycbc.type.utils') + +_lal = import_optional('lal') + +def determine_epoch(epoch, initial_array): + """ + Determine what the value should be given the epoch input + and initial array input to creating an array. + Errors giving TypeError if the type cannot be determined. + + We gave a nonsensical default value ("") to FrequencySeries + and TimeSeries epoch so we can test if it has been set. + + If this function receives this default value, then we test + `initial_array`; if `initial_array` has an 'epoch' attribute, + we use that, otherwise return zero + + But if the user passed in any value to FrequencySeries or Timeseries + - even 'None' - then that will take precedence over anything set in + the initial_array. None values are returned directly, all others + we try to convert to float64 first. + + Parameters + ---------- + epoch: + float64/number-type, LIGOTimeGPS, None + initial_array: + Array - only really matters if this has an _epoch set already + + Returns + ------- + epoch: float64 or None - see logic above + """ + + + if isinstance(epoch, float64) or epoch is None: + return epoch + + if epoch == "": + # The default has been given, try these: + try: + # inherit epoch from initial array + return initial_array._epoch + except AttributeError: + # default epoch given, and we can't grab the epoch + # from the initial array - fall back to zero + return float64(0) + + # If we reach here, then the epoch has been given + # but is not already a float64 or None, so we try to do conversions + + # LIGOTimeGPS is a special case, as numpy.isscalar fails, but + # it can be converted using float64(). + # We require lal to be imported to do this check + is_ltg = _lal is not None and isinstance(epoch, _lal.LIGOTimeGPS) + + # It looks like this is an array/list/tuple, so float conversion could + # succeed, but we shouldn't be trying it + if not is_ltg and not _numpy.isscalar(epoch): + # Its not a + raise TypeError("epoch must be a number, not array-like") + + try: + # Okay we have gone through the special cases now, just try it and see + return float64(epoch) + except TypeError as e: + # Give something helpful before failing. + logger.warning( + "epoch cannot be determined: " + f"type: {type(epoch)}, value: {epoch}" + ) + raise e \ No newline at end of file diff --git a/pycbc/waveform/generator.py b/pycbc/waveform/generator.py index 435b70ab3be..396681b0a59 100644 --- a/pycbc/waveform/generator.py +++ b/pycbc/waveform/generator.py @@ -40,7 +40,6 @@ ceilpow2, apply_fd_time_shift from pycbc.detector import Detector from pycbc.pool import use_mpi -import lal as _lal from pycbc import strain @@ -466,7 +465,7 @@ class BaseFDomainDetFrameGenerator(metaclass=ABCMeta): must be included in either the variable args or the frozen params. If None, the generate function will just return the plus polarization returned by the rFrameGeneratorClass shifted by any desired time shift. - epoch : {float, lal.LIGOTimeGPS + epoch : float The epoch start time to set the waveform to. A time shift = tc - epoch is applied to waveforms before returning. variable_args : {(), list or tuple} @@ -557,7 +556,7 @@ def epoch(self): function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch`` """ - return _lal.LIGOTimeGPS(self._epoch) + return self._epoch @abstractmethod def generate(self, **kwargs): @@ -589,7 +588,7 @@ class FDomainDetFrameGenerator(BaseFDomainDetFrameGenerator): must be included in either the variable args or the frozen params. If None, the generate function will just return the plus polarization returned by the rFrameGeneratorClass shifted by any desired time shift. - epoch : {float, lal.LIGOTimeGPS + epoch : float The epoch start time to set the waveform to. A time shift = tc - epoch is applied to waveforms before returning. variable_args : {(), list or tuple} @@ -608,7 +607,7 @@ class FDomainDetFrameGenerator(BaseFDomainDetFrameGenerator): detector_names : list The list of detector names. If no detectors were provided, then this will be ['RF'] for "radiation frame". - epoch : lal.LIGOTimeGPS + epoch : float The GPS start time of the frequency series returned by the generate function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch``. @@ -737,7 +736,7 @@ class FDomainDetFrameTwoPolGenerator(BaseFDomainDetFrameGenerator): must be included in either the variable args or the frozen params. If None, the generate function will just return the plus polarization returned by the rFrameGeneratorClass shifted by any desired time shift. - epoch : {float, lal.LIGOTimeGPS + epoch : float The epoch start time to set the waveform to. A time shift = tc - epoch is applied to waveforms before returning. variable_args : {(), list or tuple} @@ -756,7 +755,7 @@ class FDomainDetFrameTwoPolGenerator(BaseFDomainDetFrameGenerator): detector_names : list The list of detector names. If no detectors were provided, then this will be ['RF'] for "radiation frame". - epoch : lal.LIGOTimeGPS + epoch : float The GPS start time of the frequency series returned by the generate function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch``. @@ -884,7 +883,7 @@ class FDomainDetFrameTwoPolNoRespGenerator(BaseFDomainDetFrameGenerator): must be included in either the variable args or the frozen params. If None, the generate function will just return the plus polarization returned by the rFrameGeneratorClass shifted by any desired time shift. - epoch : {float, lal.LIGOTimeGPS + epoch : float The epoch start time to set the waveform to. A time shift = tc - epoch is applied to waveforms before returning. variable_args : {(), list or tuple} @@ -903,7 +902,7 @@ class FDomainDetFrameTwoPolNoRespGenerator(BaseFDomainDetFrameGenerator): detector_names : list The list of detector names. If no detectors were provided, then this will be ['RF'] for "radiation frame". - epoch : lal.LIGOTimeGPS + epoch : float The GPS start time of the frequency series returned by the generate function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch``. @@ -985,7 +984,7 @@ class FDomainDetFrameModesGenerator(BaseFDomainDetFrameGenerator): must be included in either the variable args or the frozen params. If None, the generate function will just return the plus polarization returned by the rFrameGeneratorClass shifted by any desired time shift. - epoch : {float, lal.LIGOTimeGPS + epoch : float The epoch start time to set the waveform to. A time shift = tc - epoch is applied to waveforms before returning. variable_args : {(), list or tuple} @@ -1004,7 +1003,7 @@ class FDomainDetFrameModesGenerator(BaseFDomainDetFrameGenerator): detector_names : list The list of detector names. If no detectors were provided, then this will be ['RF'] for "radiation frame". - epoch : lal.LIGOTimeGPS + epoch : float The GPS start time of the frequency series returned by the generate function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch``. @@ -1180,7 +1179,7 @@ def epoch(self): function. A time shift is applied to the waveform equal to tc-epoch. Update by using ``set_epoch`` """ - return _lal.LIGOTimeGPS(self._epoch) + return self._epoch @staticmethod def select_rframe_generator(approximant): diff --git a/test/fft_base.py b/test/fft_base.py index b823639918d..be355dc6b2e 100644 --- a/test/fft_base.py +++ b/test/fft_base.py @@ -59,17 +59,21 @@ 'Default' and once under its own name. """ -import pycbc -import pycbc.scheme -import pycbc.types -from pycbc.types import Array as ar, TimeSeries as ts, FrequencySeries as fs +import unittest import numpy from numpy import dtype, float32, float64, complex64, complex128, zeros, real from numpy.random import randn + import pycbc.fft from pycbc.fft.backend_support import set_backend -import unittest -from lal import LIGOTimeGPS as LTG +import pycbc +import pycbc.scheme +from pycbc.types import ( + Array as ar, + TimeSeries as ts, + FrequencySeries as fs, + zeros +) # Because we run many similar tests where we only vary dtypes, precisions, # or Array/TimeSeries/FrequencySeries, it is helpful to define the following @@ -238,7 +242,7 @@ def _test_random(test_case, inarr, outarr, tol): inarr *= outarr._delta_t elif isinstance(outarr, fs): inarr *= outarr._delta_f - if type(inarr) == pycbc.types.Array: + if type(inarr) == ar: # An Array FFTed and then IFFTEd will be scaled by its length # Frequency and TimeSeries have no scaling inarr /= len(inarr) @@ -290,7 +294,7 @@ def _test_random(test_case, inarr, outarr, tol): outarr *= inarr._delta_t elif isinstance(inarr, fs): outarr *= inarr._delta_f - if type(inarr) == pycbc.types.Array: + if type(inarr) == ar: # An Array FFTed and then IFFTEd will be scaled by its length # Frequency and TimeSeries have no scaling outarr /= len(inarr) @@ -328,9 +332,9 @@ def class_fft(inarr, outarr): fft_class.execute() outty = type(outarr) - outzer = pycbc.types.zeros(len(outarr)) + outzer = zeros(len(outarr)) # If we give an output array that is wrong only in length, raise ValueError: - out_badlen = outty(pycbc.types.zeros(len(outarr)+1), + out_badlen = outty(zeros(len(outarr)+1), dtype=outarr.dtype, **other_args) args = [inarr, out_badlen] tc.assertRaises(ValueError, pycbc.fft.fft, *args) @@ -381,10 +385,10 @@ def class_ifft(inarr, outarr): ifft_class.execute() outty = type(outarr) - outzer = pycbc.types.zeros(len(outarr)) + outzer = zeros(len(outarr)) # If we give an output array that is wrong only in length, # raise ValueError: - out_badlen = outty(pycbc.types.zeros(len(outarr)+1), + out_badlen = outty(zeros(len(outarr)+1), dtype=outarr.dtype, **other_args) args = [inarr, out_badlen] tc.assertRaises(ValueError, pycbc.fft.ifft, *args) @@ -409,7 +413,7 @@ def class_ifft(inarr, outarr): except KeyError: delta = new_args.pop('delta_f') new_args.update({'delta_t' : delta}) - in_badkind = type(inarr)(pycbc.types.zeros(len(inarr)), + in_badkind = type(inarr)(zeros(len(inarr)), dtype=_bad_dtype[dtype(outarr).type], **new_args) args = [in_badkind, outarr] @@ -467,7 +471,7 @@ def setUp(self): self.in_c2c_rev = [3.0-1.0j,-1.0+3.0j] self.out_c2c_rev = [2.0+2.0j,4.0-4.0j] # For Time/FrequencySeries, we want to test with a non-trivial epoch - self.epoch = LTG(3,4) + self.epoch = 3 + 4 * 1e-9 # When we need a delta_t or delta_f for input, use this. # Output-appropriate variable is computed. self.delta = 1.0/4096.0 diff --git a/test/test_frame.py b/test/test_frame.py index 5bc47299264..27902fbfaba 100644 --- a/test/test_frame.py +++ b/test/test_frame.py @@ -26,12 +26,12 @@ ''' -import pycbc import unittest -import pycbc.frame import numpy from astropy.utils.data import download_file -import lal + +import pycbc +import pycbc.frame from pycbc.types import TimeSeries from utils import parse_args_cpu_only, simple_exit @@ -53,7 +53,7 @@ def setUp(self): self.data2 += numpy.random.rand(self.size) * 1j self.delta_t = .5 - self.epoch = lal.LIGOTimeGPS(123456,0) + self.epoch = 123456.0 self.expected_data1 = TimeSeries(self.data1,dtype=self.dtype, epoch=self.epoch,delta_t=self.delta_t) self.expected_data2 = TimeSeries(self.data2,dtype=self.dtype, diff --git a/test/test_frequencyseries.py b/test/test_frequencyseries.py index e174f69386d..5d20a04b23a 100644 --- a/test/test_frequencyseries.py +++ b/test/test_frequencyseries.py @@ -26,14 +26,15 @@ ''' import unittest +import numpy +import os +import tempfile + from pycbc.types import ( Array, FrequencySeries, float32, complex64, float64, complex128, ) from pycbc.scheme import DefaultScheme -import numpy -import lal -import os -import tempfile + from utils import array_base, parse_args_all_schemes, simple_exit _scheme, _context = parse_args_all_schemes("FrequencySeries") @@ -99,16 +100,41 @@ def setUp(self): # Finally, we want to have an array that we shouldn't be able to operate on, # because the precision is wrong, and one where the length is wrong. - self.bad = FrequencySeries([1,1,1], 0.1, epoch=self.epoch, dtype = self.other_precision[self.odtype]) - self.bad2 = FrequencySeries([1,1,1,1], 0.1, epoch=self.epoch, dtype = self.dtype) + self.bad = FrequencySeries( + [1,1,1], + 0.1, + epoch=self.epoch, + dtype = self.other_precision[self.odtype] + ) + self.bad2 = FrequencySeries( + [1,1,1,1], + 0.1, + epoch=self.epoch, + dtype = self.dtype + ) # These are FrequencySeries that have problems specific to FrequencySeries - self.bad3 = FrequencySeries([1,1,1], 0.2, epoch=self.epoch, dtype = self.dtype) + self.bad3 = FrequencySeries( + [1,1,1], + 0.2, + epoch=self.epoch, + dtype = self.dtype + ) # This next one is actually okay for frequencyseries if self.epoch is None: - self.bad4 = FrequencySeries([1,1,1], 0.1, epoch = lal.LIGOTimeGPS(1000, 1000), dtype = self.dtype) + self.bad4 = FrequencySeries( + [1,1,1], + 0.1, + epoch = (1000 + 1e-6), + dtype = self.dtype + ) else: - self.bad4 = FrequencySeries([1,1,1], 0.1, epoch=None, dtype = self.dtype) + self.bad4 = FrequencySeries( + [1,1,1], + 0.1, + epoch=None, + dtype = self.dtype + ) def test_numpy_init(self): with self.context: @@ -264,8 +290,8 @@ def test_array_init(self): in2-=1 # Giving complex input and specifying a real dtype should raise an error else: - self.assertRaises(TypeError, FrequencySeries, in1,0.1, dtype = self.dtype) - self.assertRaises(TypeError, FrequencySeries, in2,0.1, dtype = self.dtype) + self.assertRaises(TypeError, FrequencySeries, in1, 0.1, dtype = self.dtype) + self.assertRaises(TypeError, FrequencySeries, in2, 0.1, dtype = self.dtype) # Also, when it is unspecified out3 = FrequencySeries(in1,0.1,epoch=self.epoch) @@ -281,7 +307,12 @@ def test_array_init(self): self.assertEqual(out3._epoch, self.epoch) # We should also be able to create from a CPU Array - out4 = FrequencySeries(cpuarray,0.1, dtype=self.dtype, epoch=self.epoch) + out4 = FrequencySeries( + cpuarray, + 0.1, + dtype=self.dtype, + epoch=self.epoch + ) self.assertTrue(type(out4._scheme) == type(self.context)) self.assertTrue(type(out4._data) is SchemeArray) @@ -316,8 +347,18 @@ def test_array_init(self): # Also checking that a cpu array can't be made out of another scheme without copying if self.scheme != 'cpu': - self.assertRaises(TypeError, FrequencySeries, out4, 0.1, copy=False) - out6 = FrequencySeries(out4, 0.1, dtype=self.dtype, epoch=self.epoch) + self.assertRaises( + TypeError, + FrequencySeries, + out4, 0.1, + copy=False + ) + out6 = FrequencySeries( + out4, + 0.1, + dtype=self.dtype, + epoch=self.epoch + ) self.assertTrue(type(out6._scheme) == DefaultScheme) self.assertTrue(type(out6._data) is CPUArray) self.assertEqual(out6[0],1) @@ -330,7 +371,12 @@ def test_array_init(self): def test_list_init(self): with self.context: # When specified - out1 = FrequencySeries([5,3,1],0.1, dtype=self.dtype, epoch=self.epoch) + out1 = FrequencySeries( + [5,3,1], + 0.1, + dtype=self.dtype, + epoch=self.epoch + ) self.assertTrue(type(out1._scheme) == type(self.context)) self.assertTrue(type(out1._data) is SchemeArray) @@ -535,7 +581,7 @@ def __init__(self, *args): suite = unittest.TestSuite() # Unlike the regular array tests, we will need to test with an epoch, and with none -epochs = [lal.LIGOTimeGPS(1000, 1000),None] +epochs = [(1000 + 1e-6),None] i = 0 for t,otypes in types: diff --git a/test/test_live_coinc_compare.py b/test/test_live_coinc_compare.py index 90359cb848c..be0312a81c2 100644 --- a/test/test_live_coinc_compare.py +++ b/test/test_live_coinc_compare.py @@ -140,13 +140,29 @@ def assess_same_output(newout, oldout): self.assertTrue(key not in oldout) else: self.assertTrue(key in oldout) - if type(newout[key]) is np.ndarray: - self.assertTrue(len(newout[key]) == len(oldout[key])) - self.assertTrue( - np.isclose(newout[key], oldout[key]).all() - ) + + a = newout[key] + b = oldout[key] + + if isinstance(a, np.ndarray): + # compare shapes and values + self.assertEqual(len(a), len(b)) + + a_comp = a + b_comp = b + + # For background/stat, order by time as the sort is not stable + if key == 'background/stat' and len(a) > 1: + tnew = newout.get('background/time', None) + told = oldout.get('background/time', None) + idx_new = np.argsort(tnew, kind='stable') + idx_old = np.argsort(told, kind='stable') + a_comp = a[idx_new] + b_comp = b[idx_old] + + self.assertTrue(np.isclose(a_comp, b_comp).all()) else: - self.assertTrue(newout[key] == oldout[key]) + self.assertEqual(a,b) for i in range(self.num_iterations): logging.info("Iteration %d", i) diff --git a/test/test_timeseries.py b/test/test_timeseries.py index 39fde81e4be..d28b62d1b05 100644 --- a/test/test_timeseries.py +++ b/test/test_timeseries.py @@ -26,14 +26,16 @@ ''' import unittest +import numpy +import os +import tempfile + from pycbc.types import float32, float64, complex64, complex128 from pycbc.types import Array, TimeSeries from pycbc.scheme import DefaultScheme -import numpy -import lal + from utils import array_base, parse_args_all_schemes, simple_exit -import os -import tempfile + _scheme, _context = parse_args_all_schemes("TimeSeries") @@ -104,9 +106,19 @@ def setUp(self): # These are timeseries that have problems specific to timeseries self.bad3 = TimeSeries([1,1,1], 0.2, epoch=self.epoch, dtype = self.dtype) if self.epoch == 0: - self.bad4 = TimeSeries([1,1,1], self.delta_t, epoch = lal.LIGOTimeGPS(1000, 1000), dtype = self.dtype) + self.bad4 = TimeSeries( + [1,1,1], + self.delta_t, + epoch = 1000 + 1e-6, + dtype = self.dtype + ) else: - self.bad4 = TimeSeries([1,1,1], self.delta_t, epoch=None, dtype = self.dtype) + self.bad4 = TimeSeries( + [1,1,1], + self.delta_t, + epoch=None, + dtype = self.dtype + ) def test_numpy_init(self): with self.context: @@ -483,7 +495,10 @@ def test_at_time(self): self.assertAlmostEqual(a.at_time(0.6, nearest_sample=True), 1.0) self.assertAlmostEqual(a.at_time(0.5, interpolate='linear'), 0.5) self.assertAlmostEqual(a.at_time([2.5], interpolate='quadratic'), 2.5) - self.assertAlmostEqual(a.at_time(lal.LIGOTimeGPS(2.1)), 2.0) + self.assertAlmostEqual( + a.at_time(2.1), + 2.0 + ) i = numpy.array([-0.2, 0.5, 1.5, 7.0]) @@ -505,7 +520,10 @@ def test_at_time(self): # Check that the output corresponds to input being scalar/array. self.assertEqual(numpy.ndim(a.at_time(0.5)), 0) - self.assertEqual(numpy.ndim(a.at_time(lal.LIGOTimeGPS(2.1))), 0) + self.assertEqual( + numpy.ndim(a.at_time(2.1)), + 0 + ) self.assertEqual(numpy.ndim(a.at_time(i)), 1) def test_inject(self): @@ -574,7 +592,7 @@ class TestTimeSeries(TestTimeSeriesBase): def __init__(self, *args): self.dtype = dtype self.odtype = odtype - self.epoch = epoch if epoch is not None else lal.LIGOTimeGPS(0, 0) + self.epoch = epoch if epoch is not None else 0 unittest.TestCase.__init__(self, *args) TestTimeSeries.__name__ = _scheme + " " + dtype.__name__ + " with " + odtype.__name__ return TestTimeSeries @@ -585,7 +603,7 @@ def __init__(self, *args): suite = unittest.TestSuite() # Unlike the regular array tests, we will need to test with an epoch, and with none -epochs = [lal.LIGOTimeGPS(1000, 1000), None] +epochs = [1000 + 1e-6, None] i = 0 for t,otypes in types: diff --git a/test/validation_code/old_coinc.py b/test/validation_code/old_coinc.py index 21a2ebc34b1..65ce1dbb23b 100644 --- a/test/validation_code/old_coinc.py +++ b/test/validation_code/old_coinc.py @@ -934,7 +934,7 @@ def _add_singles_to_buffer(self, results, ifos): for ifo in ifos: trigs = results[ifo] - if len(trigs['snr'] > 0): + if len(trigs['snr']) > 0: trigsc = copy.copy(trigs) trigsc['chisq'] = trigs['chisq'] * trigs['chisq_dof'] trigsc['chisq_dof'] = (trigs['chisq_dof'] + 2) / 2 From 6a5a42d6419bbfbee4de689e2796c816fba84184 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 13:43:21 +0000 Subject: [PATCH 11/17] I'm not sure that the version info is needed for the docker and CVMFS buils, but lets test it --- .github/workflows/build_venv.yml | 9 +++++++++ .github/workflows/docker-build.yml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/build_venv.yml b/.github/workflows/build_venv.yml index 6336f84f52e..c009ab0ad3d 100644 --- a/.github/workflows/build_venv.yml +++ b/.github/workflows/build_venv.yml @@ -12,6 +12,15 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Set release version environment variable + if: startsWith(github.ref, 'refs/tags') + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Update version file for release + if: startsWith(github.ref, 'refs/tags') + uses: ./.github/actions/update-version + with: + version: ${{ env.RELEASE_VERSION }} + release: 'true' - env: OSG_ACCESS: "${{secrets.OSG_ACCESS}}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fa7136f9a68..2fdd7fdb0e4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -13,6 +13,15 @@ jobs: - uses: actions/checkout@v1 - + - name: Set release version environment variable + if: startsWith(github.ref, 'refs/tags') + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Update version file for release + if: startsWith(github.ref, 'refs/tags') + uses: ./.github/actions/update-version + with: + version: ${{ env.RELEASE_VERSION }} + release: 'true' name: "Preparing a host container" run: "docker build -t pycbc-docker-tmp ." - From db3721463689fdbc905fb7e5b03009e37649ed11 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 14:14:03 +0000 Subject: [PATCH 12/17] try this other method for bumping the development version number --- .github/workflows/distribution.yml | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index f69d3fd360f..e4f1f96c6cd 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -136,15 +136,43 @@ jobs: version: ${{ steps.versioning.outputs.next_version }} release: 'false' - - name: Commit and push version bump + - name: Create bump branch, commit, and push + id: create_branch if: steps.check_branch.outputs.is_default == 'true' run: | git config user.name "GitHub Actions" git config user.email "actions@github.com" + BRANCH="bump-version-${{ github.run_id }}-${{ github.sha::8 }}" + git checkout -b "${BRANCH}" git add setup.py if git diff --staged --quiet; then echo "No changes to commit." - else - git commit -m "Set back to development: ${{ steps.versioning.outputs.next_version }}" - git push origin ${{ github.event.repository.default_branch }} + # Export an empty branch_name so downstream steps can handle it + echo "branch_name=" >> $GITHUB_OUTPUT + exit 0 fi + git commit -m "Set back to development: ${{ steps.versioning.outputs.next_version }}" + git push --set-upstream origin "${BRANCH}" + # Export the branch name as a step output so other steps can reference it + echo "branch_name=${BRANCH}" >> $GITHUB_OUTPUT + - name: Create Pull Request (github-script) + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branch = process.env.BUMP_BRANCH; + if (!branch) core.setFailed('BUMP_BRANCH not set'); + const title = `Automated version bump to ${process.env.NEXT_VERSION}`; + const body = `This PR was created automatically by CI to set the project back to ${process.env.NEXT_VERSION}.`; + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + head: branch, + base: context.payload.repository.default_branch, + body, + }); + core.setOutput('pr_number', pr.number); + env: + NEXT_VERSION: ${{ steps.versioning.outputs.next_version }} + BUMP_BRANCH: ${{ steps.create_branch.outputs.branch_name }} From e62088c3ffd4d5e7ca1c23dbc0a06497b895d286 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 14:20:46 +0000 Subject: [PATCH 13/17] fixes to make docker build work --- .github/workflows/docker-build.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2fdd7fdb0e4..e6e7117ac2f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,9 +10,8 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - + - name: Checkout uses: actions/checkout@v1 - - - name: Set release version environment variable if: startsWith(github.ref, 'refs/tags') run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV @@ -30,12 +29,11 @@ jobs: - env: DOCKER_IMG: pycbc/pycbc-el8 - name: "Running docker commit" - run: "bash -e docker/etc/docker_commit.sh" - - + run: bash -e docker/etc/docker_commit.sh + - name: (optional) Push docker image env: DOCKER_IMG: pycbc/pycbc-el8 DOCKER_PASSWORD: "${{secrets.DOCKERHUB_PASSWORD}}" DOCKER_USERNAME: "${{secrets.DOCKERHUB_USERNAME}}" - name: "Pushing docker image" + - name: "Pushing docker image" run: "bash -e docker/etc/push_image.sh" From e9a1fb65930139f84a226ed3942a2b4f8c2bc648 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 14:28:16 +0000 Subject: [PATCH 14/17] Fix bump --- .github/workflows/distribution.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index e4f1f96c6cd..01693d6b1b2 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -142,7 +142,8 @@ jobs: run: | git config user.name "GitHub Actions" git config user.email "actions@github.com" - BRANCH="bump-version-${{ github.run_id }}-${{ github.sha::8 }}" + # Use runner environment variables and shell substring to get the short SHA + BRANCH="bump-version-${GITHUB_RUN_ID}-${GITHUB_SHA::8}" git checkout -b "${BRANCH}" git add setup.py if git diff --staged --quiet; then @@ -161,7 +162,10 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const branch = process.env.BUMP_BRANCH; - if (!branch) core.setFailed('BUMP_BRANCH not set'); + if (!branch) { + core.info('No branch was created (no changes). Skipping PR creation.'); + return; + } const title = `Automated version bump to ${process.env.NEXT_VERSION}`; const body = `This PR was created automatically by CI to set the project back to ${process.env.NEXT_VERSION}.`; const { data: pr } = await github.rest.pulls.create({ From a94973573a62fdafbe3b2e17c0c2c342502772c6 Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 14:33:30 +0000 Subject: [PATCH 15/17] try to get docker build working --- .github/workflows/docker-build.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e6e7117ac2f..1a21f6e9cb4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -12,28 +12,34 @@ jobs: steps: - name: Checkout uses: actions/checkout@v1 + - name: Set release version environment variable if: startsWith(github.ref, 'refs/tags') run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Update version file for release if: startsWith(github.ref, 'refs/tags') uses: ./.github/actions/update-version with: version: ${{ env.RELEASE_VERSION }} release: 'true' - name: "Preparing a host container" - run: "docker build -t pycbc-docker-tmp ." - - - name: "Installing PyCBC and dependencies" - run: "docker run --privileged --name pycbc_inst -v `pwd`:/scratch pycbc-docker-tmp /bin/bash -c /scratch/docker/etc/docker-install.sh" - - + + - name: Preparing a host container + run: docker build -t pycbc-docker-tmp . + + - name: Installing PyCBC and dependencies + run: docker run --privileged --name pycbc_inst -v ${{ github.workspace }}:/scratch pycbc-docker-tmp /bin/bash -c /scratch/docker/etc/docker-install.sh + + - name: Running docker commit env: DOCKER_IMG: pycbc/pycbc-el8 run: bash -e docker/etc/docker_commit.sh + - name: (optional) Push docker image env: DOCKER_IMG: pycbc/pycbc-el8 DOCKER_PASSWORD: "${{secrets.DOCKERHUB_PASSWORD}}" DOCKER_USERNAME: "${{secrets.DOCKERHUB_USERNAME}}" + - name: "Pushing docker image" run: "bash -e docker/etc/push_image.sh" From eafaa4c873e9fa700e1734958c65d154ad2824dc Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Fri, 30 Jan 2026 14:39:42 +0000 Subject: [PATCH 16/17] try this --- .github/workflows/docker-build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1a21f6e9cb4..b4976acc2e5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -35,11 +35,9 @@ jobs: DOCKER_IMG: pycbc/pycbc-el8 run: bash -e docker/etc/docker_commit.sh - - name: (optional) Push docker image + - name: Push docker image env: DOCKER_IMG: pycbc/pycbc-el8 DOCKER_PASSWORD: "${{secrets.DOCKERHUB_PASSWORD}}" DOCKER_USERNAME: "${{secrets.DOCKERHUB_USERNAME}}" - - - name: "Pushing docker image" run: "bash -e docker/etc/push_image.sh" From 6bbb865c7776557696831377ca52f2c575b413ad Mon Sep 17 00:00:00 2001 From: Gareth S Cabourn Davies Date: Mon, 2 Feb 2026 10:52:22 +0000 Subject: [PATCH 17/17] bump tp development version needs a permission --- .github/workflows/distribution.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml index 01693d6b1b2..0d29624074a 100644 --- a/.github/workflows/distribution.yml +++ b/.github/workflows/distribution.yml @@ -88,6 +88,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + pull-requests: write steps: - uses: actions/checkout@v4 with: