From 89fdca8c2608144da928a8bc4cef1126b92e7c20 Mon Sep 17 00:00:00 2001
From: Craig Miller <5473482+craigmillernz@users.noreply.github.com>
Date: Tue, 27 Jan 2026 15:25:03 +1300
Subject: [PATCH 01/21] update files with new build from ESNZ
---
CHANGELOG | 2 +
README.md | 86 ++++---
meson.build | 71 ++++++
pygtide/__init__.py | 21 +-
pygtide/core.py | 233 ++++++++++-------
pygtide/update_etpred_data.py | 467 ++++++++++++++++++++++------------
pyproject.toml | 46 ++++
setup.cfg | 4 -
setup.py | 33 ---
9 files changed, 640 insertions(+), 323 deletions(-)
create mode 100644 meson.build
create mode 100644 pyproject.toml
delete mode 100644 setup.cfg
delete mode 100644 setup.py
diff --git a/CHANGELOG b/CHANGELOG
index db42ce8..af23eb7 100755
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+v0.8.0:
+ * modernisation of update script and test script and build system by Craig Miller, ESNZ.
v0.7.1:
* record length was corrected
* length of day (LOD) tide is now properly interpolated
diff --git a/README.md b/README.md
index 3b764c9..c6227a6 100644
--- a/README.md
+++ b/README.md
@@ -14,49 +14,67 @@ There are two options:
## How to install and run
-Instructions:
+### Prerequisites
+
* Download and install [*Anaconda*](https://www.anaconda.com/products/distribution) or [*Miniconda*](https://docs.conda.io/en/latest/miniconda.html)
+* Install required packages:
+ ```
+ conda install numpy pandas requests git
+ ```
-* Make sure the following packages are installed:
- `conda install numpy pandas requests git`
-
-* Download and install pygtide:
- * *Linux* or *MacOS*:
- **NOTE**: Make sure suitable C++ and Fortran compilers are available.
- ```
- pip install pygtide
- ```
-
- * *Windows*:
- Download the correct wheel for your Python version (available for 3.8 to 3.11) from the subfolder "windows" onto your system.
- Then navigate your Anaconda explorer to the download location and execute:
- ```
- pip install [wheel_name_depending_on_python_version]
- ```
-
-* Run tests:
- ```
- python -c "import pygtide; pygtide.test(msg=True)"
- ```
-* The internal database files can be updated as follows:
- ```
- python -c "import pygtide; pygtide.update()"
- ```
-* See `pygtide/tests.py` for example usage:
+### Installation options
+#### Option 1: Pre-built wheels (Windows, Python 3.8–3.11)
+Download the correct wheel for your Python version from the `windows/` subfolder and install:
+```powershell
+pip install [wheel_name_depending_on_python_version]
```
-from pygtide import predict_series
-args = (-20.82071, -70.15288, 830.0, '2020-01-01', 6, 600)
-series = predict_series(*args, statazimut=90, tidalcompo=8)
+
+#### Option 2: Build from source (Linux, macOS, Windows; Python 3.8–3.14)
+
+**Requirements for building:**
+- A Fortran compiler (e.g., `gfortran` via MinGW on Windows; included in Linux/macOS gcc toolchains)
+- Meson build system: automatically installed via `pip` (see below)
+- Ninja (optional but recommended): `conda install ninja` or `pip install ninja`
+
+**Install from GitHub:**
+```bash
+pip install git+https://github.com/hydrogeoscience/pygtide.git
+```
+
+**Install from local repository:**
+```bash
+cd /path/to/pygtide
+pip install -e .
+```
+
+If Meson or Ninja are missing, pip will attempt to install them automatically. For faster builds, pre-install them:
+```bash
+pip install meson-python meson ninja
```
-* Development version: This can be installed by downloading the Github repository and running:
- `pip install download_path`.
- Alternatively, in one step as:
+### After installation
+
+* Run tests to verify installation:
+ ```
+ python -c "import pygtide; pygtide.test(msg=True)"
+ ```
+
+* Update internal database files (downloads latest leap seconds and pole data):
```
- pip install git+https://github.com/hydrogeoscience/pygtide.git
+ python -c "import pygtide; pygtide.update()"
```
+### Example usage
+
+See `pygtide/tests.py` for complete examples. Quick start:
+
+```python
+from pygtide import predict_series
+args = (-20.82071, -70.15288, 830.0, '2020-01-01', 6, 600)
+series = predict_series(*args, statazimut=90, tidalcompo=8)
+```
+
## How to use
An updated user guide is currently in progress ...
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..4d55acf
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,71 @@
+project('pygtide', 'fortran',
+ version : '0.81',
+ license : 'MPL-2.0',
+ meson_version : '>=1.1.0',
+)
+
+# Get Python installation
+py = import('python').find_installation()
+
+# Determine file extension based on OS
+host_os = host_machine.system()
+if host_os == 'windows'
+ ext = 'pyd'
+else
+ ext = 'so'
+endif
+
+# Source file
+src = 'src/etpred.f90'
+
+# Build the Fortran extension module using f2py
+# f2py generates a version-specific filename like etpred.cp314-win_amd64.pyd
+etpred_module = custom_target(
+ 'etpred_build',
+ input : src,
+ output : 'etpred.' + ext,
+ command : [
+ py,
+ '-m', 'numpy.f2py',
+ '-c',
+ '--f90flags=-fPIC -O3',
+ '-m', 'etpred',
+ '@INPUT@',
+ ],
+ build_by_default : true,
+)
+
+# Final copy: after the module is built, copy the versioned file into the
+# package directory with a stable name `pygtide/etpred.{ext}` so imports work.
+copy_code = 'import glob,shutil,os; os.chdir(os.environ.get("MESON_SOURCE_ROOT","../..")); files=glob.glob(os.path.join("build","**","etpred*.' + ext + '"),recursive=True); files and shutil.copy(files[0],os.path.join("pygtide","etpred.' + ext + '")) and print("Copied to pygtide/etpred.' + ext + '")'
+
+final_copy = custom_target(
+ 'copy_etpred_final',
+ input : etpred_module,
+ output : 'copy_etpred.stamp',
+ command : [py, '-c', copy_code],
+ build_by_default : true,
+ depends : etpred_module,
+)
+
+# Install package data (commdat files)
+install_subdir(
+ 'pygtide/commdat',
+ install_dir : py.get_install_dir() / 'pygtide',
+ exclude_files : ['.gitkeep']
+)
+
+# Install the .pyd/.so file that was copied to pygtide/ by the copy_etpred_final target
+install_data(
+ 'pygtide/etpred.' + ext,
+ install_dir : py.get_install_dir() / 'pygtide',
+)
+
+# Install Python source files
+install_data(
+ 'pygtide/__init__.py',
+ 'pygtide/core.py',
+ 'pygtide/tests.py',
+ 'pygtide/update_etpred_data.py',
+ install_dir : py.get_install_dir() / 'pygtide',
+)
diff --git a/pygtide/__init__.py b/pygtide/__init__.py
index ebe224a..a6c5f16 100755
--- a/pygtide/__init__.py
+++ b/pygtide/__init__.py
@@ -2,5 +2,22 @@
from pygtide.core import predict_series, predict_spectrum, predict_table
from pygtide.core import plot_series, plot_spectrum
from pygtide.tests import test
-# from pygtide.update_etpred_data import update
-__version__ = '0.8'
+from pygtide.update_etpred_data import update
+
+from importlib.metadata import version
+
+try:
+ __version__ = version("pygtide")
+except Exception:
+ __version__ = "0.8.0"
+
+__all__ = [
+ "pygtide",
+ "predict_series",
+ "predict_spectrum",
+ "predict_table",
+ "plot_series",
+ "plot_spectrum",
+ "test",
+ "update",
+]
diff --git a/pygtide/core.py b/pygtide/core.py
index 4e069ed..11fec21 100755
--- a/pygtide/core.py
+++ b/pygtide/core.py
@@ -90,83 +90,110 @@
sampling rate was lower than 60 seconds. This was successfully fixed.
===============================================================================
"""
+
import pygtide.etpred as etpred
from datetime import datetime, timedelta, date
from warnings import warn
import numpy as np
import pandas as pd
-from pkg_resources import resource_filename
+from importlib import resources
import os
+
class pygtide(object):
"""
The PyGTide class will initialise internal variables
"""
+
def __init__(self, msg=True):
"""
pygtide.init() initialises the etpred (Fortran) module and sets global variables
"""
self.msg = msg
self.fortran_version = etpred.inout.vers.astype(str)
- self.data_dir = resource_filename('pygtide', 'commdat/')
- etpred.params.comdir = self.data_dir + ' ' * (256 - len(self.data_dir))
- # set OS dependent module output
- etpred.params.nullfile = os.devnull + ' ' * (10 - len(os.devnull))
+ # Resolve package data directory directly from filesystem.
+ # Avoid importlib.resources.as_file() which creates temp dirs that get cleaned up.
+ pkg_dir = os.path.dirname(__file__)
+ self.data_dir = os.path.join(pkg_dir, "commdat")
+ if not self.data_dir.endswith(os.sep):
+ self.data_dir += os.sep
+
+ # Fortran expects a fixed-length string (256 chars)
+ etpred.params.comdir = self.data_dir + " " * (256 - len(self.data_dir))
+
+ # OS-dependent null file
+ etpred.params.nullfile = os.devnull + " " * (10 - len(os.devnull))
+
self.args = []
- # set some common variables for external access
+
+ # Initialise Fortran module
etpred.init()
+
# capture end date of file "etddt.dat" from module
year = int(etpred.inout.etd_start)
self.etddt_start = datetime(year, 1, 1)
year = etpred.inout.etd_end
- self.etddt_end = (datetime(int(year), 1, 1) + timedelta(days=(year - int(year)) * 365))
+ self.etddt_end = datetime(int(year), 1, 1) + timedelta(
+ days=(year - int(year)) * 365
+ )
+
# capture end date of file "etpolut1.dat" from module
self.etpolut1_start = datetime.strptime(str(etpred.inout.etpol_start), "%Y%m%d")
self.etpolut1_end = datetime.strptime(str(etpred.inout.etpol_end), "%Y%m%d")
- self.headers = np.char.strip(etpred.inout.header.astype('str'))
+ self.headers = np.char.strip(etpred.inout.header.astype("str"))
# self.units = ['(m/s)**2','nm/s**2','mas','mm','mm','nstr','nstr','nstr','nstr','nstr','mm']
self.exec = False
- self.wavegroup_def = np.asarray([[0, 10, 1., 0.]])
+ self.wavegroup_def = np.asarray([[0, 10, 1, 0.0]])
self.set_wavegroup(self.wavegroup_def)
- #%% sync the Python object
+ # %% sync the Python object
def update(self):
"""
self.update() refreshes the variables of PyGTide based on the Fortran module etpred
"""
- self.headers = np.char.strip(etpred.inout.header.astype('str'))
+ self.headers = np.char.strip(etpred.inout.header.astype("str"))
self.args = etpred.inout.argsin
- self.unit = etpred.inout.etpunit.astype('str')
+ self.unit = etpred.inout.etpunit.astype("str")
- #%% set wave group parameters
+ # %% set wave group parameters
def set_wavegroup(self, wavedata=None):
- if (wavedata is None):
+ if wavedata is None:
wavedata = self.wavegroup_def
# require at least 4 columns
- if (wavedata.shape[1] != 4):
+ if wavedata.shape[1] != 4:
raise ValueError("The wave group input must have 4 columns!")
return False
# require frequency ranges to increase and not overlap
freq_diffs = np.diff(wavedata[:, 0:1].flatten())
- if ((freq_diffs < 0).any()):
- raise ValueError("Wave group frequency ranges must be increasing and not overlapping!")
+ if (freq_diffs < 0).any():
+ raise ValueError(
+ "Wave group frequency ranges must be increasing and not overlapping!"
+ )
return False
- if ((wavedata[:, 2] < 0).any()):
+ if (wavedata[:, 2] < 0).any():
raise ValueError("Amplitude factors must be positive!")
return False
# set the wave group parameters
- etpred.waves(wavedata[:, 0], wavedata[:, 1], wavedata[:, 2], wavedata[:, 3], int(wavedata.shape[0]))
+ etpred.waves(
+ wavedata[:, 0],
+ wavedata[:, 1],
+ wavedata[:, 2],
+ wavedata[:, 3],
+ int(wavedata.shape[0]),
+ )
return True
- #%% reset the wave group
+ # %% reset the wave group
def reset_wavegroup(self):
self.set_wavegroup(self.wavegroup_def)
return True
# run module etpred and return numbers
- def predict(self, latitude, longitude, height, startdate, duration, samprate, **control):
+ def predict(
+ self, latitude, longitude, height, startdate, duration, samprate, **control
+ ):
"""
self.predict(latitude, longitude, height, startdate, duration, samprate, **control):
-------------------------------------------------------------------------------
@@ -256,77 +283,77 @@ def predict(self, latitude, longitude, height, startdate, duration, samprate, **
# tidal catalog
argsin[10] = 8
# amplitude truncation
- argsin[12] = 1.0E-10
+ argsin[12] = 1.0e-10
# values from: https://dx.doi.org/10.1016/j.jog.2005.08.035
argsin[13] = 1.16
argsin[14] = 1.16
# iterate through optional arguments passed
- if 'statgravit' in control:
- if not (0 <= control['statgravit'] <= 20):
- raise ValueError('Station gravity exceeds permissible range!')
+ if "statgravit" in control:
+ if not (0 <= control["statgravit"] <= 20):
+ raise ValueError("Station gravity exceeds permissible range!")
else:
- argsin[8] = control['statgravit']
- if 'statazimut' in control:
- if not (0 <= control['statazimut'] <= 180):
- raise ValueError('Statazimut exceeds permissible range!')
+ argsin[8] = control["statgravit"]
+ if "statazimut" in control:
+ if not (0 <= control["statazimut"] <= 180):
+ raise ValueError("Statazimut exceeds permissible range!")
else:
- argsin[9] = control['statazimut']
- if 'tidalpoten' in control:
- if control['tidalpoten'] not in range(1,9):
- raise ValueError('Tidalpoten must be an integer between 1 and 8!')
+ argsin[9] = control["statazimut"]
+ if "tidalpoten" in control:
+ if control["tidalpoten"] not in range(1, 9):
+ raise ValueError("Tidalpoten must be an integer between 1 and 8!")
else:
- argsin[10] = control['tidalpoten']
- if 'tidalcompo' in control:
- if control['tidalcompo'] not in range(-1,10):
- raise ValueError('Tidalcompo must be an integer between -1 and 9!')
+ argsin[10] = control["tidalpoten"]
+ if "tidalcompo" in control:
+ if control["tidalcompo"] not in range(-1, 10):
+ raise ValueError("Tidalcompo must be an integer between -1 and 9!")
else:
- argsin[11] = control['tidalcompo']
- if 'amtruncate' in control:
- if not (0 <= control['amtruncate']):
- raise ValueError('Amtruncate must be greater than 0!')
+ argsin[11] = control["tidalcompo"]
+ if "amtruncate" in control:
+ if not (0 <= control["amtruncate"]):
+ raise ValueError("Amtruncate must be greater than 0!")
else:
- argsin[12] = control['amtruncate']
- if 'poltidecor' in control:
- if not (control['poltidecor'] >= 0):
- raise ValueError('Poltidecor must be >= 0!')
+ argsin[12] = control["amtruncate"]
+ if "poltidecor" in control:
+ if not (control["poltidecor"] >= 0):
+ raise ValueError("Poltidecor must be >= 0!")
else:
- argsin[13] = control['poltidecor']
- if 'lodtidecor' in control:
- if not (control['lodtidecor'] >= 0):
- raise ValueError('Lodtidecor must be >= 0!')
+ argsin[13] = control["poltidecor"]
+ if "lodtidecor" in control:
+ if not (control["lodtidecor"] >= 0):
+ raise ValueError("Lodtidecor must be >= 0!")
else:
- argsin[14] = control['lodtidecor']
+ argsin[14] = control["lodtidecor"]
# additional control parameters
- if 'fileprd' in control:
- if control['fileprd'] not in range(0,2):
- raise ValueError('Fileprd flag must be 0 or 1!')
+ if "fileprd" in control:
+ if control["fileprd"] not in range(0, 2):
+ raise ValueError("Fileprd flag must be 0 or 1!")
else:
- argsin[15] = control['fileprd']
- if 'fileprn' in control:
- if control['fileprn'] not in range(0,2):
- raise ValueError('Fileprn flag must be 0 or 1!')
+ argsin[15] = control["fileprd"]
+ if "fileprn" in control:
+ if control["fileprn"] not in range(0, 2):
+ raise ValueError("Fileprn flag must be 0 or 1!")
else:
- argsin[16] = control['fileprn']
- if 'screenout' in control:
- if control['screenout'] not in range(0,2):
- raise ValueError('Screenout flag must be 0 or 1!')
+ argsin[16] = control["fileprn"]
+ if "screenout" in control:
+ if control["screenout"] not in range(0, 2):
+ raise ValueError("Screenout flag must be 0 or 1!")
else:
- argsin[17] = control['screenout']
+ argsin[17] = control["screenout"]
# process required parameters here
if not (-90 <= latitude <= 90):
- raise ValueError('Latitude exceeds permissible range!')
+ raise ValueError("Latitude exceeds permissible range!")
else:
argsin[0] = latitude
if not (-180 <= longitude <= 180):
- raise ValueError('Longitude exceeds permissible range!')
+ raise ValueError("Longitude exceeds permissible range!")
else:
argsin[1] = longitude
if not (-500 <= height <= 5000):
- raise ValueError('Height exceeds permissible range!')
+ raise ValueError("Height exceeds permissible range!")
else:
argsin[2] = height
- if not (0 < duration <= 10*24*365):
+ if not (0 < duration <= 10 * 24 * 365):
raise ValueError("Duration exceeds permissible range!")
else:
argsin[6] = int(duration)
@@ -336,37 +363,51 @@ def predict(self, latitude, longitude, height, startdate, duration, samprate, **
try:
startdate = datetime.strptime(startdate, "%Y-%m-%d")
except ValueError:
- raise ValueError("Startdate has incorrect format (YYYY-MM-DD)!" )
+ raise ValueError("Startdate has incorrect format (YYYY-MM-DD)!")
enddate = startdate + timedelta(hours=duration)
# check if requested prediction series exceeds permissible time
- if (startdate < self.etddt_start):
+ if startdate < self.etddt_start:
fname = str(etpred.params.etddtdat)
- warn("Prediction timeframe is earlier than the available time database (%s). "
- "For details refer to the file '%s'." % (self.etddt_start, fname))
- if (enddate > (self.etddt_end + timedelta(days=365))):
+ warn(
+ "Prediction timeframe is earlier than the available time database (%s). "
+ "For details refer to the file '%s'." % (self.etddt_start, fname)
+ )
+ if enddate > (self.etddt_end + timedelta(days=365)):
fname = str(etpred.params.etddtdat)
- warn("Please consider updating the leap second database '%s' (last value is from %s)." % (fname, self.etddt_end))
+ warn(
+ "Please consider updating the leap second database '%s' (last value is from %s)."
+ % (fname, self.etddt_end)
+ )
# if not (-50*365 < (startdate - dt.datetime.now()).days < 365):
- if ( ((argsin[13] > 0) or (argsin[14] > 0)) and ((startdate < self.etpolut1_start) or (enddate > self.etpolut1_end)) ):
+ if ((argsin[13] > 0) or (argsin[14] > 0)) and (
+ (startdate < self.etpolut1_start) or (enddate > self.etpolut1_end)
+ ):
fname = str(etpred.params.etddtdat)
- warn("Dates exceed permissible range for pole/LOD tide correction (interval %s to %s). Consider update file '%s'." % (self.etpolut1_start, self.etpolut1_end, fname))
- if ( ((argsin[13] > 0) or (argsin[14] > 0)) and (startdate < datetime.strptime('1600-01-01', "%Y-%m-%d")) ):
- raise ValueError("PyGTide should not be used for dates before the year 1600.")
+ warn(
+ "Dates exceed permissible range for pole/LOD tide correction (interval %s to %s). Consider update file '%s'."
+ % (self.etpolut1_start, self.etpolut1_end, fname)
+ )
+ if ((argsin[13] > 0) or (argsin[14] > 0)) and (
+ startdate < datetime.strptime("1600-01-01", "%Y-%m-%d")
+ ):
+ raise ValueError(
+ "PyGTide should not be used for dates before the year 1600."
+ )
# set the start date and time
- argsin[3:6] = [startdate.year,startdate.month,startdate.day]
+ argsin[3:6] = [startdate.year, startdate.month, startdate.day]
# test sammprate validity
- if not (0 < samprate <= 24*3600):
+ if not (0 < samprate <= 24 * 3600):
raise ValueError("samprate exceeds permissible range!")
else:
argsin[7] = int(samprate)
# test that samprate is not larger than duration
- if (samprate/3600 > duration):
+ if samprate / 3600 > duration:
raise ValueError("samprate exceeds duration!")
# print(argsin)
self.args = argsin
if self.msg:
- print('%s is calculating, please wait ...' % (self.fortran_version))
-
+ print("%s is calculating, please wait ..." % (self.fortran_version))
+
# hand over variables
etpred.predict(argsin)
@@ -385,20 +426,20 @@ def results(self, digits=6):
"""
if self.exec:
# get the headers from Fortran
- cols = np.char.strip(etpred.inout.header.astype('str'))
- allcols = np.insert(cols[2:], 0, 'UTC')
+ cols = np.char.strip(etpred.inout.header.astype("str"))
+ allcols = np.insert(cols[2:], 0, "UTC")
etdata = pd.DataFrame(columns=allcols)
# format date and time into padded number strings
etpred_data = np.array(etpred.inout.etpdata)
# print(etpred.inout.etpdata[:, 0])
# catch non-complete container fills from odd duration/sampling pairs
- etpred_data = etpred_data[etpred_data[:,0] > 0, :]
- # convert
- date = np.char.mod("%08.0f ", etpred_data[:,0])
- time = np.char.mod("%06.0f", etpred_data[:,1])
+ etpred_data = etpred_data[etpred_data[:, 0] > 0, :]
+ # convert
+ date = np.char.mod("%08.0f ", etpred_data[:, 0])
+ time = np.char.mod("%06.0f", etpred_data[:, 1])
# merge date and time arrays
datetime = np.core.defchararray.add(date, time)
- etdata['UTC'] = pd.to_datetime(datetime, format="%Y%m%d %H%M%S", utc=True)
+ etdata["UTC"] = pd.to_datetime(datetime, format="%Y%m%d %H%M%S", utc=True)
# obtain header strings from routine and convert
etdata[cols[2:]] = np.around(etpred_data[:, 2:], digits)
return etdata
@@ -443,22 +484,22 @@ def datetime(self):
"""
if self.exec:
# reformat the date and time values obtained from ETERNA
- date = np.char.mod("%08.0f", etpred.inout.etpdata[:,0])
- time = np.char.mod("%06.0f", etpred.inout.etpdata[:,1])
+ date = np.char.mod("%08.0f", etpred.inout.etpdata[:, 0])
+ time = np.char.mod("%06.0f", etpred.inout.etpdata[:, 1])
return np.stack((date, time), axis=1)
else:
return None
def predict_table(*args, msg=False, **kwargs):
- kwargs.setdefault('screenout', int(msg))
+ kwargs.setdefault("screenout", int(msg))
pt = pygtide(msg=msg)
pt.predict(*args, **kwargs)
return pt.results()
def predict_series(*args, msg=False, index=0, **kwargs):
- kwargs.setdefault('screenout', int(msg))
+ kwargs.setdefault("screenout", int(msg))
pt = pygtide(msg=msg)
pt.predict(*args, **kwargs)
return pt.data()[:, index]
@@ -466,6 +507,7 @@ def predict_series(*args, msg=False, index=0, **kwargs):
def predict_spectrum(*args, nfft=None, **kwargs):
from numpy.fft import rfft, rfftfreq
+
sr = args[-1]
data = predict_series(*args, **kwargs)
if nfft is None:
@@ -480,15 +522,18 @@ def plot_series(*args, indices=(0, 1), show=True, **kwargs):
table.plot(*indices)
if show:
import matplotlib.pyplot as plt
+
plt.show()
+
def plot_spectrum(*args, ax=None, show=True, **kwargs):
import matplotlib.pyplot as plt
+
freq, spec = predict_spectrum(*args, **kwargs)
if ax is None:
ax = plt.subplot(111)
ax.plot(freq * 24 * 3600, np.abs(spec))
- ax.set_xlabel('freq (cycles per day)')
- ax.set_ylabel('amplitude')
+ ax.set_xlabel("freq (cycles per day)")
+ ax.set_ylabel("amplitude")
if show:
plt.show()
diff --git a/pygtide/update_etpred_data.py b/pygtide/update_etpred_data.py
index 1192141..774c6ac 100755
--- a/pygtide/update_etpred_data.py
+++ b/pygtide/update_etpred_data.py
@@ -4,108 +4,148 @@
Author: Gabriel C. Rau (gabriel@hydrogeo.science)
Website: https://hydrogeo.science
"""
+
import pygtide.etpred as etpred
-from pkg_resources import resource_filename
+
+from importlib import resources
from pathlib import Path
import numpy as np
import time as tt
import pandas as pd
import datetime as dt
-import urllib, urllib.request, re, os
+import urllib
+import urllib.request
+import re
+import os
+
def timestampToDecyear(ts):
- year=ts.year
- jan1=pd.Timestamp(year,1,1)
- jan1next=pd.Timestamp(year+1,1,1)
- yrlen=(jan1next-jan1).total_seconds()
- return year+(ts-jan1).total_seconds()/yrlen
+ year = ts.year
+ jan1 = pd.Timestamp(year, 1, 1)
+ jan1next = pd.Timestamp(year + 1, 1, 1)
+ yrlen = (jan1next - jan1).total_seconds()
+ return year + (ts - jan1).total_seconds() / yrlen
+
class update_etpred_db(object):
"""
The pygtide_update class will initialise internal variables
"""
+
def __init__(self, msg=True):
self.msg = msg
- self.data_dir = resource_filename('pygtide', 'commdat/')
- etpred.params.comdir = self.data_dir + ' ' * (256 - len(self.data_dir))
+ # Resolve package data directory directly from filesystem.
+ # Avoid importlib.resources.as_file() which creates temp dirs that get cleaned up.
+ pkg_dir = os.path.dirname(__file__)
+ self.data_dir = os.path.join(pkg_dir, "commdat")
+ if not self.data_dir.endswith(os.sep):
+ self.data_dir += os.sep
+
+ # Fortran expects a fixed-length string (256 chars)
+ etpred.params.comdir = self.data_dir + " " * (256 - len(self.data_dir))
+
+ # OS-dependent null file
+ etpred.params.nullfile = os.devnull + " " * (10 - len(os.devnull))
+
# set OS dependent module output
- etpred.params.nullfile = os.devnull + ' ' * (10 - len(os.devnull))
- self.etddt_file = str(etpred.params.etddtdat, 'UTF-8').strip()
- self.etpolut1_dat_file = str(etpred.params.etpolutdat, 'UTF-8').strip()
- self.etpolut1_bin_file = str(etpred.params.etpolutbin, 'UTF-8').strip()
+ etpred.params.nullfile = os.devnull + " " * (10 - len(os.devnull))
+ self.etddt_file = str(etpred.params.etddtdat, "UTF-8").strip()
+ self.etpolut1_dat_file = str(etpred.params.etpolutdat, "UTF-8").strip()
+ self.etpolut1_bin_file = str(etpred.params.etpolutbin, "UTF-8").strip()
- #%% remote data files
+ # %% remote data files
# IERS leap seconds history file
- self.etddt_tmpl = 'etddt_tmpl.dat'
- self.leapsec_rfile = 'https://hpiers.obspm.fr/iers/bul/bulc/Leap_Second_History.dat'
+ self.etddt_tmpl = "etddt_tmpl.dat"
+ self.leapsec_rfile = (
+ "https://hpiers.obspm.fr/iers/bul/bulc/Leap_Second_History.dat"
+ )
# IERS pole coordinate observations
# self.iauhist_rfile = 'http://hpiers.obspm.fr/iers/eop/eopc04/eopc04_IAU2000.62-now'
- self.iauhist_rfile = 'ftp://hpiers.obspm.fr/iers/eop/eopc04/eopc04_IAU2000.62-now'
+ self.iauhist_rfile = (
+ "ftp://hpiers.obspm.fr/iers/eop/eopc04/eopc04_IAU2000.62-now"
+ )
# US navy pole coordinate predictions
- self.iaucurr_rfile = 'https://datacenter.iers.org/data/9/finals2000A.all'
+ self.iaucurr_rfile = "https://datacenter.iers.org/data/9/finals2000A.all"
# self.iaucurr_rfile = 'ftp://cddis.gsfc.nasa.gov/pub/products/iers/finals2000A.all'
-
- #%% update the pole coordinates and UT1 to TAI times
+ # %% update the pole coordinates and UT1 to TAI times
def update_etpolut1(self):
global etpolut
status = True
- etpolut1_file = Path(self.data_dir + '/' + self.etpolut1_dat_file)
- leapsec_file = Path(self.data_dir + '/' + '[raw]_Leap_Second_History.dat')
- iauhist_file = Path(self.data_dir + '/' + '[raw]_eopc04_IAU2000.dat')
- iaucurr_file = Path(self.data_dir + '/' + '[raw]_finals2000A.dat')
+ etpolut1_file = Path(self.data_dir + "/" + self.etpolut1_dat_file)
+ leapsec_file = Path(self.data_dir + "/" + "[raw]_Leap_Second_History.dat")
+ iauhist_file = Path(self.data_dir + "/" + "[raw]_eopc04_IAU2000.dat")
+ iaucurr_file = Path(self.data_dir + "/" + "[raw]_finals2000A.dat")
print("--------------------------------------")
- print("-->> Updating the Earth orientation database '{:s}':".format(etpolut1_file.as_posix()))
+ print(
+ "-->> Updating the Earth orientation database '{:s}':".format(
+ etpolut1_file.as_posix()
+ )
+ )
start = tt.time()
if status:
try:
urllib.request.urlopen(self.leapsec_rfile)
except OSError as error:
- print("ERROR: Could not connect to remote server: {:s}".format(self.leapsec_rfile))
+ print(
+ "ERROR: Could not connect to remote server: {:s}".format(
+ self.leapsec_rfile
+ )
+ )
print("MESSAGE: {0}.".format(error))
- status=False
+ status = False
pass
else:
- print('Start downloading: {:s} ...'.format(self.leapsec_rfile))
+ print("Start downloading: {:s} ...".format(self.leapsec_rfile))
urllib.request.urlretrieve(self.leapsec_rfile, leapsec_file)
end = tt.time()
- print('Finished downloading ({:.1f} s).'.format((end - start)))
+ print("Finished downloading ({:.1f} s).".format((end - start)))
if status:
try:
urllib.request.urlopen(self.iauhist_rfile)
except OSError as error:
- print("ERROR: Could not connect to remote server: {:s}".format(self.iauhist_rfile))
+ print(
+ "ERROR: Could not connect to remote server: {:s}".format(
+ self.iauhist_rfile
+ )
+ )
print("MESSAGE: {0}.".format(error))
- status=False
+ status = False
pass
else:
- print('Start downloading: {:s} ...'.format(self.iauhist_rfile))
+ print("Start downloading: {:s} ...".format(self.iauhist_rfile))
urllib.request.urlretrieve(self.iauhist_rfile, iauhist_file)
end = tt.time()
- print('Finished downloading ({:.1f} s).'.format((end - start)))
+ print("Finished downloading ({:.1f} s).".format((end - start)))
if status:
try:
urllib.request.urlopen(self.iaucurr_rfile)
except OSError as error:
- print("ERROR: Could not connect to remote server: {:s}".format(self.iaucurr_rfile))
+ print(
+ "ERROR: Could not connect to remote server: {:s}".format(
+ self.iaucurr_rfile
+ )
+ )
print("MESSAGE: {0}.".format(error))
- status=False
+ status = False
pass
else:
- print('Start downloading: {:s} ...'.format(self.iaucurr_rfile))
+ print("Start downloading: {:s} ...".format(self.iaucurr_rfile))
urllib.request.urlretrieve(self.iaucurr_rfile, iaucurr_file)
end = tt.time()
- print('Finished downloading ({:.1f} s).'.format((end - start)))
+ print("Finished downloading ({:.1f} s).".format((end - start)))
- #%%
+ # %%
if status:
try:
open(leapsec_file, "r")
except OSError as error:
- print("ERROR: Could not open file: {:s}".format(leapsec_file.as_posix()))
+ print(
+ "ERROR: Could not open file: {:s}".format(leapsec_file.as_posix())
+ )
print("MESSAGE: {0}.".format(error))
status = False
pass
@@ -114,7 +154,9 @@ def update_etpolut1(self):
try:
open(iauhist_file, "r")
except OSError as error:
- print("ERROR: Could not open file: {:s}".format(iauhist_file.as_posix()))
+ print(
+ "ERROR: Could not open file: {:s}".format(iauhist_file.as_posix())
+ )
print("MESSAGE: {0}.".format(error))
status = False
pass
@@ -123,73 +165,117 @@ def update_etpolut1(self):
try:
open(iaucurr_file, "r")
except OSError as error:
- print("ERROR: Could not open file: {:s}".format(iaucurr_file.as_posix()))
+ print(
+ "ERROR: Could not open file: {:s}".format(iaucurr_file.as_posix())
+ )
print("MESSAGE: {0}.".format(error))
status = False
pass
if status:
- #%% read leap second history
- leapsdf = pd.read_csv(leapsec_file, comment='#', header=None, parse_dates= {'date':[1,2,3]}, \
- date_format='%d %m %Y', delimiter=r"\s+")
- leapsdf.columns = ['date', 'MJD', 'leaps']
- leapsdf.drop(['MJD'], axis=1, inplace=True)
+ # %% read leap second history
+ leapsdf = pd.read_csv(
+ leapsec_file,
+ comment="#",
+ header=None,
+ delimiter=r"\s+",
+ names=["MJD", "day", "month", "year", "leaps"],
+ usecols=[0, 1, 2, 3, 4],
+ )
+
+ # Create datetime column from day, month, year
+ leapsdf["date"] = pd.to_datetime(leapsdf[["year", "month", "day"]])
+
+ # Keep only 'date' and 'leaps'
+ leapsdf = leapsdf[["date", "leaps"]]
+
+ # Reindex starting at 1
leapsdf.index += 1
- # insert row at the beginning
- leapsdf.loc[0] = [dt.datetime(1962,1,1), 10]
+
+ # Insert initial row at the beginning
+ leapsdf.loc[0] = [dt.datetime(1962, 1, 1), 10]
+
+ # Sort index to restore correct order
leapsdf.sort_index(inplace=True)
-
- #%% read historic pole coordinates
+
+ # %% read historic pole coordinates
# convert = {3: lambda x: np.around(np.float64(x), 3)}
- iauhist = pd.read_csv(iauhist_file, skiprows=13, header=None, parse_dates= {'date':[0,1,2]}, \
- date_format='%Y %m %d', delimiter=r"\s+", usecols=[0,1,2,3,4,5,6])
- iauhist.columns = ['date', 'MJD', 'x', 'y', 'UT1-UTC']
- #iauhist = iauhist.set_index('date')
-
- #%% read current pole coordinates
- # dateparse = lambda x,y,z: pd.to_datetime(x.zfill(2)+y.zfill(2)+z.zfill(2), format='%y%m%d')
- # convert = {'date': lambda x,y,z: pd.to_datetime(x.zfill(2)+y.zfill(2)+z.zfill(2), format='%y%m%d')}
- fw = [2,2,2,9,3,9,9,10,9,3,10,10]
- iaucurr = pd.read_fwf(iaucurr_file, header=None, widths=fw, usecols=[0,1,2,3,5,7,10])
- iaucurr.iloc[:, 0] = pd.to_datetime(iaucurr.iloc[:, 0].astype(str).str.zfill(2) + '-' + iaucurr.iloc[:, 1].astype(str).str.zfill(2) + '-' + iaucurr.iloc[:, 2].astype(str).str.zfill(2), format='%y-%m-%d')
- iaucurr.drop(iaucurr.columns[[1, 2]], axis=1, inplace=True)
- iaucurr.columns = ['date', 'MJD', 'x', 'y', 'UT1-UTC']
- #iaucurr = iaucurr.set_index('date')
- mask = (iaucurr['date'] > iauhist['date'].values[-1])
- # etpolut = iauhist.append(iaucurr[mask])
- etpolut = pd.concat([iauhist, iaucurr[mask]])
- etpolut = etpolut[np.isfinite(etpolut['x'])]
- #iauhist.combine_first(iaucurr)
-
- #%%
- etpolut['Date'] = etpolut['date'].dt.strftime('%Y%m%d')
- etpolut['Time'] = etpolut['date'].dt.strftime('%H%M%S')
- etpolut['MJD'] = etpolut['MJD'].map('{:8.3f}'.format)
- etpolut['x'] = etpolut['x'].map('{:9.5f}'.format)
- etpolut['y'] = etpolut['y'].map('{:9.5f}'.format)
- etpolut['TAI-UT1'] = etpolut['UT1-UTC']
- etpolut['UT1-UTC'] = etpolut['UT1-UTC'].map('{:9.6f}'.format)
-
- #%%
+ iauhist = pd.read_csv(
+ iauhist_file,
+ skiprows=13,
+ header=None,
+ delimiter=r"\s+",
+ usecols=[0, 1, 2, 3, 4, 5, 6],
+ names=["year", "month", "day", "MJD", "x", "y", "UT1-UTC"],
+ )
+
+ # Combine year, month, day into a single datetime column
+ iauhist["date"] = pd.to_datetime(iauhist[["year", "month", "day"]])
+
+ # Keep only the desired columns
+ iauhist = iauhist[["date", "MJD", "x", "y", "UT1-UTC"]]
+
+ # %% read current pole coordinates
+ fw = [2, 2, 2, 9, 3, 9, 9, 10, 9, 3, 10, 10]
+ iaucurr = pd.read_fwf(
+ iaucurr_file, header=None, widths=fw, usecols=[0, 1, 2, 3, 5, 7, 10]
+ )
+
+ iaucurr_dates = (
+ iaucurr.iloc[:, 0].astype(str).str.zfill(2)
+ + "-"
+ + iaucurr.iloc[:, 1].astype(str).str.zfill(2)
+ + "-"
+ + iaucurr.iloc[:, 2].astype(str).str.zfill(2)
+ )
+
+ iaucurr["date"] = pd.to_datetime(iaucurr_dates, format="%y-%m-%d")
+
+ # Drop old columns used for date
+ iaucurr = iaucurr.drop(columns=iaucurr.columns[:3])
+ # Rename remaining columns
+ iaucurr.columns = ["MJD", "x", "y", "UT1-UTC", "date"]
+ # Move 'date' to first position if needed
+ iaucurr = iaucurr[["date", "MJD", "x", "y", "UT1-UTC"]]
+
+ mask = iaucurr["date"] > iauhist["date"].iloc[-1]
+ etpolut = pd.concat([iauhist, iaucurr.loc[mask]])
+ etpolut = etpolut[np.isfinite(etpolut["x"])]
+
+ # %%
+ etpolut["Date"] = etpolut["date"].dt.strftime("%Y%m%d")
+ etpolut["Time"] = etpolut["date"].dt.strftime("%H%M%S")
+ etpolut["MJD"] = etpolut["MJD"].map("{:8.3f}".format)
+ etpolut["x"] = etpolut["x"].map("{:9.5f}".format)
+ etpolut["y"] = etpolut["y"].map("{:9.5f}".format)
+ etpolut["TAI-UT1"] = etpolut["UT1-UTC"]
+ etpolut["UT1-UTC"] = etpolut["UT1-UTC"].map("{:9.6f}".format)
+
+ # %%
# prepare the last column
for idx, val in leapsdf.iterrows():
# print(idx, val[1])
# find mask for leap seconds
- if idx+1 in leapsdf.index:
- mask = ((etpolut['date'] >= leapsdf['date'].loc[idx]) & (etpolut['date'] < leapsdf['date'].loc[idx+1]))
- #print(mask)
+ if idx + 1 in leapsdf.index:
+ mask = (etpolut["date"] >= leapsdf["date"].loc[idx]) & (
+ etpolut["date"] < leapsdf["date"].loc[idx + 1]
+ )
+ # print(mask)
# subtract leap seconds from UTC
- etpolut.loc[mask, 'TAI-UT1'] = leapsdf['leaps'].loc[idx] - etpolut.loc[mask, 'TAI-UT1']
+ etpolut.loc[mask, "TAI-UT1"] = (
+ leapsdf["leaps"].loc[idx] - etpolut.loc[mask, "TAI-UT1"]
+ )
else:
- mask = (etpolut['date'] >= leapsdf['date'].loc[idx])
- etpolut.loc[mask, 'TAI-UT1'] = leapsdf['leaps'].loc[idx] - etpolut.loc[mask, 'TAI-UT1']
+ mask = etpolut["date"] >= leapsdf["date"].loc[idx]
+ etpolut.loc[mask, "TAI-UT1"] = (
+ leapsdf["leaps"].loc[idx] - etpolut.loc[mask, "TAI-UT1"]
+ )
- etpolut['TAI-UT1'] = etpolut['TAI-UT1'].map('{:9.6f}'.format)
+ etpolut["TAI-UT1"] = etpolut["TAI-UT1"].map("{:9.6f}".format)
- #%%
- #etpolut[0] = etpolut[0].map('${:,.2f}'.format)
- header = \
-"""File : etpolut1.dat
+ # %%
+ # etpolut[0] = etpolut[0].map('${:,.2f}'.format)
+ header = """File : etpolut1.dat
Updated : $1$
Contents : Pole coordinates and earth rotation one day sampling interval,
given at 0 hours UTC. Historic data is combined with predictions.
@@ -202,9 +288,13 @@ def update_etpolut1(self):
Date Time MJD x y UT1-UTC TAI-UT1
["] ["] [sec] [sec]
C****************************************************************\n"""
- header = header.replace("$1$", dt.datetime.utcnow().strftime('%d/%m/%Y'))
- header = header.replace("$2$", etpolut['date'].iloc[0].strftime('%d/%m/%Y') \
- + ' to ' + etpolut['date'].iloc[-1].strftime('%d/%m/%Y'))
+ header = header.replace("$1$", dt.datetime.now(dt.UTC).strftime("%d/%m/%Y"))
+ header = header.replace(
+ "$2$",
+ etpolut["date"].iloc[0].strftime("%d/%m/%Y")
+ + " to "
+ + etpolut["date"].iloc[-1].strftime("%d/%m/%Y"),
+ )
header = header.replace("$3$", self.iauhist_rfile)
header = header.replace("$4$", self.iaucurr_rfile)
header = header.replace("$5$", self.leapsec_rfile)
@@ -213,55 +303,73 @@ def update_etpolut1(self):
# IMPORTANT: newline needs to comply with windows platform!
# https://pythonconquerstheuniverse.wordpress.com/2011/05/08/newline-conversion-in-python-3/
- with open(etpolut1_file, "w", newline='') as myfile:
+ with open(etpolut1_file, "w", newline="") as myfile:
myfile.write(header)
# myfile.write(etpolut['combined'].to_string(index=False, header=False).replace('\n ', '\n'))
# etpolut['combined'].to_string(myfile, index=False, header=False)
# WEIRD PANDAS BUG: to_string() puts white space at beginning of each line
for index, row in etpolut.iterrows():
- string = "{:s} {:s} {:s} {:s} {:s} {:s} {:s}".format(row['Date'], row['Time'], row['MJD'],\
- row['x'], row['y'], row['UT1-UTC'], row['TAI-UT1'])
- myfile.write(string + '\r\n')
+ string = "{:s} {:s} {:s} {:s} {:s} {:s} {:s}".format(
+ row["Date"],
+ row["Time"],
+ row["MJD"],
+ row["x"],
+ row["y"],
+ row["UT1-UTC"],
+ row["TAI-UT1"],
+ )
+ myfile.write(string + "\r\n")
myfile.write("99999999")
myfile.close()
end = tt.time()
# update also bin file
self.etpolut1_dat2bin()
- print('Finished updating {:s} ({:.1f} s).'.format(etpolut1_file.as_posix(), (end - start)))
+ print(
+ "Finished updating {:s} ({:.1f} s).".format(
+ etpolut1_file.as_posix(), (end - start)
+ )
+ )
else:
- print('Update failed!')
+ print("Update failed!")
pass
-
- #%% remove temporary files
+
+ # %% remove temporary files
os.remove(leapsec_file)
os.remove(iauhist_file)
os.remove(iaucurr_file)
return
- #%% update the etpolut1 binary file from the text file
+ # %% update the etpolut1 binary file from the text file
def etpolut1_dat2bin(self):
- etpolut1_dat = Path(self.data_dir + '/' + self.etpolut1_dat_file)
- etpolut1_bin = Path(self.data_dir + '/' + self.etpolut1_bin_file)
+ etpolut1_dat = Path(self.data_dir + "/" + self.etpolut1_dat_file)
+ etpolut1_bin = Path(self.data_dir + "/" + self.etpolut1_bin_file)
header = []
# find the end of the header
with open(etpolut1_dat, "r") as f:
for num, line in enumerate(f, 1):
header.append(line)
- if "C*******" in header[-1]: break
+ if "C*******" in header[-1]:
+ break
# read into dataframe
- cols = ['Date', 'Time', 'MJD', 'x', 'y', 'UT1-UTC', 'TAI-UT1']
- etpolut = pd.read_csv(etpolut1_dat, names=cols, skiprows=num, header=None, delimiter=r"\s+")
+ cols = ["Date", "Time", "MJD", "x", "y", "UT1-UTC", "TAI-UT1"]
+ etpolut = pd.read_csv(
+ etpolut1_dat, names=cols, skiprows=num, header=None, delimiter=r"\s+"
+ )
# drop the last row with EOL ('99999999')
etpolut = etpolut[:-1]
- print("File '{:s}' has {:d} rows.".format(etpolut1_dat.as_posix(), etpolut.shape[0]))
- #%%
+ print(
+ "File '{:s}' has {:d} rows.".format(
+ etpolut1_dat.as_posix(), etpolut.shape[0]
+ )
+ )
+ # %%
# write as binary for use in fortran: each record has 4*8 bytes = 32 bytes
# header contains start date in MJD and number of rows + 1
- head = np.array([np.int32(etpolut.iloc[0, 2]), np.int32(etpolut.shape[0]+1)])
+ head = np.array([np.int32(etpolut.iloc[0, 2]), np.int32(etpolut.shape[0] + 1)])
data = np.float64(etpolut.values[:, 3:])
- #print(data)
- with open(etpolut1_bin,'wb+') as f:
+ # print(data)
+ with open(etpolut1_bin, "wb+") as f:
# write header integers
f.write(head.tobytes())
# advance to next record (32 bytes)
@@ -269,20 +377,27 @@ def etpolut1_dat2bin(self):
# write the flattened matrix (this may have 64 bytes)
f.write(data.flatten().tobytes())
f.close()
- print("File '{:s}' has been updated (Header: {:.0f}, {:d}).".format(etpolut1_bin.as_posix(), etpolut.iloc[0, 2], etpolut.shape[0]+1))
-
+ print(
+ "File '{:s}' has been updated (Header: {:.0f}, {:d}).".format(
+ etpolut1_bin.as_posix(), etpolut.iloc[0, 2], etpolut.shape[0] + 1
+ )
+ )
- #%% update the time conversion database (leap seconds)
+ # %% update the time conversion database (leap seconds)
def update_etddt(self):
global etddt, leapsdf, tmp
- leapsec_file = Path(self.data_dir + '/' + '[raw]_Leap_Second_History.dat')
- old_etddt_file = Path(self.data_dir + '/' + self.etddt_tmpl)
- etddt_file = Path(self.data_dir + '/' + self.etddt_file)
+ leapsec_file = Path(self.data_dir + "/" + "[raw]_Leap_Second_History.dat")
+ old_etddt_file = Path(self.data_dir + "/" + self.etddt_tmpl)
+ etddt_file = Path(self.data_dir + "/" + self.etddt_file)
- #%%
+ # %%
print("--------------------------------------")
- print("-->> Updating time conversion database '{:s}':".format(leapsec_file.as_posix()))
- #%% download leap second history
+ print(
+ "-->> Updating time conversion database '{:s}':".format(
+ leapsec_file.as_posix()
+ )
+ )
+ # %% download leap second history
start = tt.time()
try:
urllib.request.urlopen(self.leapsec_rfile)
@@ -291,12 +406,12 @@ def update_etddt(self):
print("MESSAGE: {0}.".format(error))
pass
else:
- print('Start downloading: {:s} ...'.format(leapsec_file.as_posix()))
+ print("Start downloading: {:s} ...".format(leapsec_file.as_posix()))
urllib.request.urlretrieve(self.leapsec_rfile, leapsec_file)
end = tt.time()
- print('Finished downloading ({:.1f} s).'.format((end - start)))
+ print("Finished downloading ({:.1f} s).".format((end - start)))
- #%% READ THE EXISTING FILE
+ # %% READ THE EXISTING FILE
# print(etddt_file)
# find the end of the header
with open(old_etddt_file, "r") as f:
@@ -304,52 +419,90 @@ def update_etddt(self):
header = []
regex = re.compile(r"^\s*updated\s*\:.*$", re.IGNORECASE)
for num, line in enumerate(f, 1):
- line = regex.sub("Updated : %s" % dt.datetime.utcnow().strftime('%d/%m/%Y'), line)
+ line = regex.sub(
+ "Updated : %s" % dt.datetime.now(dt.UTC).strftime("%d/%m/%Y"),
+ line,
+ )
header.append(line)
- if "C*******" in header[-1]: break
-
- cols = ['year','JD','DDT']
- etddt = pd.read_csv(old_etddt_file, names=cols, skiprows=num, header=None, delimiter=r"\s+")
-
- #%% read leap second history
- leapsdf = pd.read_csv(leapsec_file, comment='#', header=None, parse_dates= {'date':[1,2,3]}, date_format='%d %m %Y', delimiter=r"\s+")
- leapsdf.columns = ['date', 'MJD', 'leaps']
- # leapsdf = leapsdf.set_index('date')
- # DDT = delta-T + delta-UT = leaps + 32.184 s offset
- leapsdf['DDT'] = leapsdf['leaps'] + 32.184
-
- #%%
- leapsdf['JD'] = [dt.to_julian_date() for dt in leapsdf['date']]
- leapsdf['year'] = [timestampToDecyear(dt) for dt in leapsdf['date']]
-
- #%%
- mask = (leapsdf['year'] > etddt['year'].values[-1])
+ if "C*******" in header[-1]:
+ break
+
+ cols = ["year", "JD", "DDT"]
+ etddt = pd.read_csv(
+ old_etddt_file, names=cols, skiprows=num, header=None, delimiter=r"\s+"
+ )
+
+ # %% read leap second history
+ leapsdf = pd.read_csv(
+ leapsec_file,
+ comment="#",
+ header=None,
+ delimiter=r"\s+",
+ names=[
+ "MJD",
+ "day",
+ "month",
+ "year",
+ "leaps",
+ ], # assign proper column names
+ usecols=[0, 1, 2, 3, 4],
+ )
+ # Combine day, month, year into a single datetime column
+ leapsdf["date"] = pd.to_datetime(leapsdf[["year", "month", "day"]])
+
+ # Keep only the relevant columns
+ leapsdf = leapsdf[["date", "MJD", "leaps"]]
+
+ # Compute DDT = leaps + 32.184 s
+ leapsdf["DDT"] = leapsdf["leaps"] + 32.184
+
+ # %%
+ leapsdf["JD"] = [dt.to_julian_date() for dt in leapsdf["date"]]
+ leapsdf["year"] = [timestampToDecyear(dt) for dt in leapsdf["date"]]
+
+ # %%
+ mask = leapsdf["year"] > etddt["year"].values[-1]
indices = leapsdf.index[mask]
# print(indices)
# tmp = []
for i, val in enumerate(indices):
# for each record create a new row
- etddt.loc[len(etddt) + 1] = {'year': leapsdf.loc[val, 'year'], 'JD': leapsdf.loc[val, 'JD'], 'DDT': leapsdf.loc[val, 'DDT']}
+ etddt.loc[len(etddt) + 1] = {
+ "year": leapsdf.loc[val, "year"],
+ "JD": leapsdf.loc[val, "JD"],
+ "DDT": leapsdf.loc[val, "DDT"],
+ }
# number of new records
records = sum(mask)
- if (records > 0):
+ if records > 0:
# write header
- with open(self.data_dir + '/' + self.etddt_file, "w+", newline='\r\n') as f:
+ with open(self.data_dir + "/" + self.etddt_file, "w+", newline="\r\n") as f:
f.write("".join(header))
- #etddt['combined'].to_string(f, index=False, header=False)
- #f.write("\n")
+ # etddt['combined'].to_string(f, index=False, header=False)
+ # f.write("\n")
# WEIRD PANDAS BUG: to_string() puts white space at beginning of each line
for index, row in etddt.iterrows():
- string = "{:.5f} {:.5f} {:8.3f}".format(row['year'], row['JD'], row['DDT'])
+ string = "{:.5f} {:.5f} {:8.3f}".format(
+ row["year"], row["JD"], row["DDT"]
+ )
# print(string)
- f.write(string + '\n')
+ f.write(string + "\n")
f.close()
end = tt.time()
- print('{:d} records were added to the template ({:.1f} s).'.format(records, end - start))
- print("The leap second File ('{:s}') is now up to date ({:.1f} s).".format(self.etddt_file, end - start))
-
-#%% run the update
+ print(
+ "{:d} records were added to the template ({:.1f} s).".format(
+ records, end - start
+ )
+ )
+ print(
+ "The leap second File ('{:s}') is now up to date ({:.1f} s).".format(
+ self.etddt_file, end - start
+ )
+ )
+
+
+# %% run the update
def update(msg=True):
pt = update_etpred_db(msg)
pt.update_etddt()
@@ -357,4 +510,6 @@ def update(msg=True):
pt.update_etpolut1()
print("---------------------")
-update()
\ No newline at end of file
+
+if __name__ == "__main__":
+ update()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a9bc764
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,46 @@
+[build-system]
+requires = [
+ "meson-python",
+ "meson>=1.1.0",
+ "ninja",
+ "numpy>=1.21.0",
+]
+build-backend = "mesonpy"
+
+[project]
+name = "pygtide"
+version = "0.81"
+description = "A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth"
+readme = "README.md"
+license = {text = "MPL-2.0"}
+authors = [
+ {name = "Gabriel C. Rau", email = "gabriel@hydrogeo.science"},
+ {name = "Tom Eulenfeld"},
+]
+requires-python = ">=3.8"
+dependencies = [
+ "numpy>=1.21.0",
+ "pandas",
+ "requests",
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Topic :: Scientific/Engineering :: Physics",
+]
+
+[project.urls]
+Homepage = "https://github.com/hydrogeoscience/pygtide"
+Documentation = "https://hydrogeo.science"
+Repository = "https://github.com/hydrogeoscience/pygtide.git"
+Issues = "https://github.com/hydrogeoscience/pygtide/issues"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index c59cb2a..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-[metadata]
-long_description = file: README.md
-long_description_content_type = text/markdown
-license_files=LICENSE
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 35cd46f..0000000
--- a/setup.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import os.path
-import re
-
-from numpy.distutils.core import setup, Extension
-
-def find_version(*paths):
- fname = os.path.join(os.path.dirname(__file__), *paths)
- with open(fname) as fp:
- code = fp.read()
- match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", code, re.M)
- if match:
- return match.group(1)
- raise RuntimeError("Unable to find version string.")
-
-
-VERSION = find_version('pygtide', '__init__.py')
-
-ext = [Extension(name='pygtide.etpred',
- sources=['src/etpred.f90'])]
-
-setup(
- name='pygtide',
- version=VERSION,
- packages=['pygtide'],
- package_data={'pygtide': ['commdat/*']},
- ext_modules=ext,
- install_requires=['numpy', 'pandas','requests'],
- author='Gabriel C. Rau, Tom Eulenfeld',
- author_email='gabriel@hydrogeo.science',
- url='https://github.com/hydrogeoscience/pygtide',
- description=('A Python module and wrapper for ETERNA PREDICT to compute '
- 'gravitational tides on Earth'),
- )
From 52ffc69ae3e05580583fea7a16404eaf80f6432e Mon Sep 17 00:00:00 2001
From: Craig Miller <5473482+craigmillernz@users.noreply.github.com>
Date: Tue, 27 Jan 2026 15:31:11 +1300
Subject: [PATCH 02/21] update pyproject version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a9bc764..6d74239 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ build-backend = "mesonpy"
[project]
name = "pygtide"
-version = "0.81"
+version = "0.8.0
description = "A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth"
readme = "README.md"
license = {text = "MPL-2.0"}
From 1d98d5264e654a6e761acdbfdb59d0a02c7f7d2c Mon Sep 17 00:00:00 2001
From: Craig Miller <5473482+craigmillernz@users.noreply.github.com>
Date: Thu, 29 Jan 2026 17:27:49 +1300
Subject: [PATCH 03/21] new build system for multiple os
---
README.md | 11 +++++---
meson.build | 79 +++++++++++++++++++++++++++--------------------------
setup.py | 19 +++++++++++++
3 files changed, 67 insertions(+), 42 deletions(-)
create mode 100644 setup.py
diff --git a/README.md b/README.md
index c6227a6..e1570b9 100644
--- a/README.md
+++ b/README.md
@@ -33,19 +33,19 @@ pip install [wheel_name_depending_on_python_version]
#### Option 2: Build from source (Linux, macOS, Windows; Python 3.8–3.14)
**Requirements for building:**
-- A Fortran compiler (e.g., `gfortran` via MinGW on Windows; included in Linux/macOS gcc toolchains)
+- A Fortran compiler (e.g., `gfortran` via MinGW on Windows; included in Linux/macOS gcc toolchains) `conda install gfortran`
- Meson build system: automatically installed via `pip` (see below)
- Ninja (optional but recommended): `conda install ninja` or `pip install ninja`
-**Install from GitHub:**
+**Clone repo from git:**
```bash
-pip install git+https://github.com/hydrogeoscience/pygtide.git
+git clone https://github.com/hydrogeoscience/pygtide.git
```
**Install from local repository:**
```bash
cd /path/to/pygtide
-pip install -e .
+python setup.py
```
If Meson or Ninja are missing, pip will attempt to install them automatically. For faster builds, pre-install them:
@@ -53,6 +53,9 @@ If Meson or Ninja are missing, pip will attempt to install them automatically. F
pip install meson-python meson ninja
```
+Add the repo path to your pythonpath.
+
+
### After installation
* Run tests to verify installation:
diff --git a/meson.build b/meson.build
index 4d55acf..5b225e9 100644
--- a/meson.build
+++ b/meson.build
@@ -1,71 +1,74 @@
project('pygtide', 'fortran',
- version : '0.81',
- license : 'MPL-2.0',
+ version : '0.8.0',
meson_version : '>=1.1.0',
)
-# Get Python installation
py = import('python').find_installation()
-# Determine file extension based on OS
host_os = host_machine.system()
+fc = meson.get_compiler('fortran')
+
if host_os == 'windows'
ext = 'pyd'
+ if fc.get_id() == 'msvc'
+ f90flags = ['/O2']
+ else
+ f90flags = ['-O3']
+ endif
else
ext = 'so'
+ f90flags = ['-fPIC', '-O3']
endif
-# Source file
src = 'src/etpred.f90'
+f90_flag_args = ['--f90flags=' + ' '.join(f90flags)]
-# Build the Fortran extension module using f2py
-# f2py generates a version-specific filename like etpred.cp314-win_amd64.pyd
-etpred_module = custom_target(
+# --------------------------------------------------
+# 1. Build f2py (ABI-tagged output)
+# --------------------------------------------------
+etpred_build = custom_target(
'etpred_build',
input : src,
- output : 'etpred.' + ext,
+ output : 'etpred.build.stamp',
command : [
py,
'-m', 'numpy.f2py',
'-c',
- '--f90flags=-fPIC -O3',
+ ] + f90_flag_args + [
'-m', 'etpred',
'@INPUT@',
],
build_by_default : true,
)
-# Final copy: after the module is built, copy the versioned file into the
-# package directory with a stable name `pygtide/etpred.{ext}` so imports work.
-copy_code = 'import glob,shutil,os; os.chdir(os.environ.get("MESON_SOURCE_ROOT","../..")); files=glob.glob(os.path.join("build","**","etpred*.' + ext + '"),recursive=True); files and shutil.copy(files[0],os.path.join("pygtide","etpred.' + ext + '")) and print("Copied to pygtide/etpred.' + ext + '")'
+# --------------------------------------------------
+# 2. Copy ABI module → pygtide/etpred.pyd
+# --------------------------------------------------
+copy_code = '''
+import glob, os, shutil, sys
-final_copy = custom_target(
- 'copy_etpred_final',
- input : etpred_module,
- output : 'copy_etpred.stamp',
- command : [py, '-c', copy_code],
- build_by_default : true,
- depends : etpred_module,
-)
+build_dir = os.getcwd()
+src_root = r"@0@"
-# Install package data (commdat files)
-install_subdir(
- 'pygtide/commdat',
- install_dir : py.get_install_dir() / 'pygtide',
- exclude_files : ['.gitkeep']
-)
+pattern = os.path.join(build_dir, '**', 'etpred*.@1@')
+matches = glob.glob(pattern, recursive=True)
+
+if not matches:
+ print("ERROR: etpred ABI module not found")
+ sys.exit(1)
-# Install the .pyd/.so file that was copied to pygtide/ by the copy_etpred_final target
-install_data(
- 'pygtide/etpred.' + ext,
- install_dir : py.get_install_dir() / 'pygtide',
+dst = os.path.join(src_root, 'pygtide', 'etpred.@1@')
+shutil.copy(matches[0], dst)
+print(f"Copied {matches[0]} -> {dst}")
+'''.format(
+ meson.project_source_root(),
+ ext
)
-# Install Python source files
-install_data(
- 'pygtide/__init__.py',
- 'pygtide/core.py',
- 'pygtide/tests.py',
- 'pygtide/update_etpred_data.py',
- install_dir : py.get_install_dir() / 'pygtide',
+custom_target(
+ 'copy_etpred',
+ input : etpred_build,
+ output : 'etpred.copy.stamp',
+ command : [py, '-c', copy_code],
+ build_by_default : true,
)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..7432550
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,19 @@
+import subprocess
+import shutil
+import sys
+import os
+
+# --- Step 1: Setup build dir ---
+build_dir = "build"
+
+if not os.path.exists(build_dir):
+ subprocess.check_call(["meson", "setup", build_dir])
+else:
+ print(f"Build directory '{build_dir}' already exists, skipping setup.")
+
+# --- Step 2: Compile ---
+subprocess.check_call(["meson", "compile", "-C", build_dir])
+
+# --- Step 3: Clean up build dir ---
+shutil.rmtree(build_dir)
+print(f"Deleted build directory '{build_dir}'")
From ccbc47aa3799c883e1ef913d7c425b8bcfbb8e72 Mon Sep 17 00:00:00 2001
From: Craig Miller <5473482+craigmillernz@users.noreply.github.com>
Date: Thu, 29 Jan 2026 21:31:36 +1300
Subject: [PATCH 04/21] another build system update
---
MANIFEST.in | 2 ++
README.md | 8 ++++----
build.py | 41 +++++++++++++++++++++++++++++++++++++
pyproject.toml | 32 ++++++++++++++++++-----------
setup.py | 55 ++++++++++++++++++++++++++++++++++++++------------
5 files changed, 109 insertions(+), 29 deletions(-)
create mode 100644 build.py
diff --git a/MANIFEST.in b/MANIFEST.in
index bb3ec5f..418dfc8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1,3 @@
include README.md
+include pygtide/etpred.*
+
diff --git a/README.md b/README.md
index e1570b9..aabdd22 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,10 @@ git clone https://github.com/hydrogeoscience/pygtide.git
**Install from local repository:**
```bash
cd /path/to/pygtide
-python setup.py
+
+python build.py
+
+pip install . --no-build-isolation
```
If Meson or Ninja are missing, pip will attempt to install them automatically. For faster builds, pre-install them:
@@ -53,9 +56,6 @@ If Meson or Ninja are missing, pip will attempt to install them automatically. F
pip install meson-python meson ninja
```
-Add the repo path to your pythonpath.
-
-
### After installation
* Run tests to verify installation:
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..27688fe
--- /dev/null
+++ b/build.py
@@ -0,0 +1,41 @@
+import subprocess
+import shutil
+import os
+import glob
+import sys
+
+# --- Configuration ---
+build_dir = 'build'
+ext = 'pyd' if os.name == 'nt' else 'so'
+
+# --- Step 1: Setup Meson build directory ---
+if not os.path.exists(build_dir):
+ print(f"Setting up Meson build directory '{build_dir}'...")
+ subprocess.check_call(['meson', 'setup', build_dir])
+else:
+ print(f"Build directory '{build_dir}' already exists, skipping setup.")
+
+# --- Step 2: Compile Meson targets ---
+print("Compiling Meson targets...")
+subprocess.check_call(['meson', 'compile', '-C', build_dir])
+
+# --- Step 3: Copy ABI module to pygtide/ ---
+print("Locating ABI module in build directory...")
+build_path = os.path.abspath(build_dir)
+pattern = os.path.join(build_path, '**', f'etpred*.{ext}')
+matches = glob.glob(pattern, recursive=True)
+
+if not matches:
+ print("ERROR: ABI module not found! Did Meson compile succeed?")
+ sys.exit(1)
+
+dst = os.path.join(os.path.abspath('.'), 'pygtide', f'etpred.{ext}')
+shutil.copy(matches[0], dst)
+print(f"Copied ABI module {matches[0]} -> {dst}")
+
+# --- Step 4: Clean up build directory ---
+print(f"Deleting build directory '{build_dir}'...")
+shutil.rmtree(build_dir)
+print("Build complete!")
+
+print("\nNext step: run 'pip install .' or 'pip install -e .' to install the package.")
diff --git a/pyproject.toml b/pyproject.toml
index 6d74239..84ccd7a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,27 +1,25 @@
[build-system]
requires = [
- "meson-python",
- "meson>=1.1.0",
- "ninja",
- "numpy>=1.21.0",
+ "setuptools>=61",
+ "wheel",
+ "numpy>=1.21.0"
]
-build-backend = "mesonpy"
+build-backend = "setuptools.build_meta"
[project]
name = "pygtide"
-version = "0.8.0
+version = "0.8.0"
description = "A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth"
readme = "README.md"
-license = {text = "MPL-2.0"}
+requires-python = ">=3.8"
authors = [
- {name = "Gabriel C. Rau", email = "gabriel@hydrogeo.science"},
- {name = "Tom Eulenfeld"},
+ { name = "Gabriel C. Rau", email = "gabriel@hydrogeo.science" },
+ { name = "Tom Eulenfeld" }
]
-requires-python = ">=3.8"
dependencies = [
"numpy>=1.21.0",
"pandas",
- "requests",
+ "requests"
]
classifiers = [
"Development Status :: 4 - Beta",
@@ -36,7 +34,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
- "Topic :: Scientific/Engineering :: Physics",
+ "Topic :: Scientific/Engineering :: Physics"
]
[project.urls]
@@ -44,3 +42,13 @@ Homepage = "https://github.com/hydrogeoscience/pygtide"
Documentation = "https://hydrogeo.science"
Repository = "https://github.com/hydrogeoscience/pygtide.git"
Issues = "https://github.com/hydrogeoscience/pygtide/issues"
+
+# ----------------------------
+# Setuptools-specific configuration
+# ----------------------------
+[tool.setuptools]
+packages = ["pygtide"] # <-- move packages here
+include-package-data = true # include package data
+
+[tool.setuptools.package-data]
+pygtide = ["etpred.*", "commdat/*"]
diff --git a/setup.py b/setup.py
index 7432550..997ed49 100644
--- a/setup.py
+++ b/setup.py
@@ -1,19 +1,48 @@
-import subprocess
-import shutil
+# setup.py
import sys
import os
+from setuptools import setup, Extension, find_packages
-# --- Step 1: Setup build dir ---
-build_dir = "build"
+# Detect platform-specific ABI extension
+ext = '.pyd' if sys.platform.startswith('win') else '.so'
+etpred_path = os.path.join('pygtide', f'etpred{ext}')
-if not os.path.exists(build_dir):
- subprocess.check_call(["meson", "setup", build_dir])
-else:
- print(f"Build directory '{build_dir}' already exists, skipping setup.")
+if not os.path.exists(etpred_path):
+ raise FileNotFoundError(f"Prebuilt ABI module not found: {etpred_path}\n"
+ "Run build.py first to generate it.")
-# --- Step 2: Compile ---
-subprocess.check_call(["meson", "compile", "-C", build_dir])
+# Define the prebuilt extension
+etpred_module = Extension(
+ name='pygtide.etpred',
+ sources=[], # No sources; using prebuilt ABI
+ extra_objects=[etpred_path],
+)
-# --- Step 3: Clean up build dir ---
-shutil.rmtree(build_dir)
-print(f"Deleted build directory '{build_dir}'")
+# Setup configuration
+setup(
+ name='pygtide',
+ version='0.8.0',
+ description='A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth',
+ author='Gabriel C. Rau',
+ author_email='gabriel@hydrogeo.science',
+ packages=find_packages(), # finds 'pygtide'
+ #ext_modules=[etpred_module],
+ include_package_data=True, # ensures any package data is included
+ package_data={
+ 'pygtide': ['etpred.*', 'commdat/*'],
+ },
+ python_requires='>=3.8',
+ install_requires=[
+ 'numpy>=1.21.0',
+ 'pandas',
+ 'requests',
+ ],
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Topic :: Scientific/Engineering :: Physics",
+ ],
+)
From 7486e0d8e2a6efa6c0890ebfc238c8992e8cdf9d Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 13:51:12 +0100
Subject: [PATCH 05/21] automatically invoke meson build script from setup.py
---
build.py | 41 -------------------------------
build_pygtide_abi.py | 47 ++++++++++++++++++++++++++++++++++++
pyproject.toml | 4 +++-
setup.py | 57 +++++++++++++++++---------------------------
4 files changed, 72 insertions(+), 77 deletions(-)
delete mode 100644 build.py
create mode 100644 build_pygtide_abi.py
diff --git a/build.py b/build.py
deleted file mode 100644
index 27688fe..0000000
--- a/build.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import subprocess
-import shutil
-import os
-import glob
-import sys
-
-# --- Configuration ---
-build_dir = 'build'
-ext = 'pyd' if os.name == 'nt' else 'so'
-
-# --- Step 1: Setup Meson build directory ---
-if not os.path.exists(build_dir):
- print(f"Setting up Meson build directory '{build_dir}'...")
- subprocess.check_call(['meson', 'setup', build_dir])
-else:
- print(f"Build directory '{build_dir}' already exists, skipping setup.")
-
-# --- Step 2: Compile Meson targets ---
-print("Compiling Meson targets...")
-subprocess.check_call(['meson', 'compile', '-C', build_dir])
-
-# --- Step 3: Copy ABI module to pygtide/ ---
-print("Locating ABI module in build directory...")
-build_path = os.path.abspath(build_dir)
-pattern = os.path.join(build_path, '**', f'etpred*.{ext}')
-matches = glob.glob(pattern, recursive=True)
-
-if not matches:
- print("ERROR: ABI module not found! Did Meson compile succeed?")
- sys.exit(1)
-
-dst = os.path.join(os.path.abspath('.'), 'pygtide', f'etpred.{ext}')
-shutil.copy(matches[0], dst)
-print(f"Copied ABI module {matches[0]} -> {dst}")
-
-# --- Step 4: Clean up build directory ---
-print(f"Deleting build directory '{build_dir}'...")
-shutil.rmtree(build_dir)
-print("Build complete!")
-
-print("\nNext step: run 'pip install .' or 'pip install -e .' to install the package.")
diff --git a/build_pygtide_abi.py b/build_pygtide_abi.py
new file mode 100644
index 0000000..7ea53f6
--- /dev/null
+++ b/build_pygtide_abi.py
@@ -0,0 +1,47 @@
+import subprocess
+import shutil
+import os
+import glob
+import sys
+
+
+def build():
+ # --- Configuration ---
+ print('Start Meson build')
+ build_dir = 'build_abi'
+ ext = 'pyd' if os.name == 'nt' else 'so'
+
+ # --- Step 1: Setup Meson build directory ---
+ if os.path.exists(build_dir):
+ print(f"Build directory '{build_dir}' already exists, skipping setup.")
+ else:
+ print(f"Setting up Meson build directory '{build_dir}'...")
+ subprocess.check_call(['meson', 'setup', build_dir])
+ # --- Step 2: Compile Meson targets ---
+ print("Compiling Meson targets...")
+ subprocess.check_call(['meson', 'compile', '-C', build_dir])
+
+ # --- Step 3: Copy ABI module to pygtide/ ---
+ print("Locating ABI module in build directory...")
+ build_path = os.path.abspath(build_dir)
+ pattern = os.path.join(build_path, '**', f'etpred*.{ext}')
+ matches = glob.glob(pattern, recursive=True)
+
+ if not matches:
+ print("ERROR: ABI module not found! Did Meson compile succeed?")
+ sys.exit(1)
+
+ dst = os.path.join(os.path.abspath('.'), 'pygtide', f'etpred.{ext}')
+ shutil.copy(matches[0], dst)
+ print(f"Copied ABI module {matches[0]} -> {dst}")
+
+ # --- Step 4: Clean up build directory ---
+ print(f"Deleting build directory '{build_dir}'...")
+ shutil.rmtree(build_dir)
+ print("Meson build complete!")
+
+ #print("\nNext step: run 'pip install .' or 'pip install -e .' to install the package.")
+
+
+if __name__ == '__main__':
+ build()
diff --git a/pyproject.toml b/pyproject.toml
index 84ccd7a..ab9a4e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,9 @@
requires = [
"setuptools>=61",
"wheel",
- "numpy>=1.21.0"
+ "numpy>=1.21.0",
+ "meson",
+ "ninja"
]
build-backend = "setuptools.build_meta"
diff --git a/setup.py b/setup.py
index 997ed49..e37cc6c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,48 +1,35 @@
# setup.py
+from pathlib import Path
import sys
-import os
-from setuptools import setup, Extension, find_packages
+
+from setuptools import setup, Extension
+
# Detect platform-specific ABI extension
+HERE = Path(__file__).resolve().parent
+
ext = '.pyd' if sys.platform.startswith('win') else '.so'
-etpred_path = os.path.join('pygtide', f'etpred{ext}')
+etpred_path = HERE / 'pygtide' / f'etpred{ext}'
+
+if etpred_path.exists():
+ print(f"Use ABI module {etpred_path}")
+else:
+ print(f"Prebuilt ABI module not found: {etpred_path}\n"
+ "Run build_pygtide_abi.py")
+ sys.path.insert(0, str(HERE))
+ import build_pygtide_abi
+ build_pygtide_abi.build()
+ if not etpred_path.exists():
+ raise FileNotFoundError(f"No ABI module, error in Meson build")
-if not os.path.exists(etpred_path):
- raise FileNotFoundError(f"Prebuilt ABI module not found: {etpred_path}\n"
- "Run build.py first to generate it.")
# Define the prebuilt extension
etpred_module = Extension(
name='pygtide.etpred',
sources=[], # No sources; using prebuilt ABI
- extra_objects=[etpred_path],
+ extra_objects=[str(etpred_path)],
)
-# Setup configuration
-setup(
- name='pygtide',
- version='0.8.0',
- description='A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth',
- author='Gabriel C. Rau',
- author_email='gabriel@hydrogeo.science',
- packages=find_packages(), # finds 'pygtide'
- #ext_modules=[etpred_module],
- include_package_data=True, # ensures any package data is included
- package_data={
- 'pygtide': ['etpred.*', 'commdat/*'],
- },
- python_requires='>=3.8',
- install_requires=[
- 'numpy>=1.21.0',
- 'pandas',
- 'requests',
- ],
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Science/Research",
- "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3",
- "Topic :: Scientific/Engineering :: Physics",
- ],
-)
+
+# Metadata pulled from pyproject.toml
+setup()
From cc2e0bc9d7ec3b94117d7a34e03b5583368b414e Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 13:53:19 +0100
Subject: [PATCH 06/21] update test conda env
---
.github/test_conda_env.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/test_conda_env.yml b/.github/test_conda_env.yml
index 7aaba20..6497b7b 100644
--- a/.github/test_conda_env.yml
+++ b/.github/test_conda_env.yml
@@ -4,4 +4,4 @@ channels:
dependencies:
- numpy
- pandas
- - fortran-compiler
+ - requests
From c84c8af7378b5514c1abf236c30b7f5f9fee3d23 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 13:58:08 +0100
Subject: [PATCH 07/21] use SPDX license string
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index ab9a4e5..0910bab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ name = "pygtide"
version = "0.8.0"
description = "A Python module and wrapper for ETERNA PREDICT to compute gravitational tides on Earth"
readme = "README.md"
+license = "MPL-2.0"
requires-python = ">=3.8"
authors = [
{ name = "Gabriel C. Rau", email = "gabriel@hydrogeo.science" },
@@ -26,7 +27,6 @@ dependencies = [
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
- "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
From 00779c2ea878d722c4455fae072a0b37db6e24de Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:13:46 +0100
Subject: [PATCH 08/21] fix datetime.UTC not found in new python versions
---
pygtide/update_etpred_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pygtide/update_etpred_data.py b/pygtide/update_etpred_data.py
index 774c6ac..a7930ac 100755
--- a/pygtide/update_etpred_data.py
+++ b/pygtide/update_etpred_data.py
@@ -288,7 +288,7 @@ def update_etpolut1(self):
Date Time MJD x y UT1-UTC TAI-UT1
["] ["] [sec] [sec]
C****************************************************************\n"""
- header = header.replace("$1$", dt.datetime.now(dt.UTC).strftime("%d/%m/%Y"))
+ header = header.replace("$1$", dt.datetime.now(dt.timezone.utc).strftime("%d/%m/%Y"))
header = header.replace(
"$2$",
etpolut["date"].iloc[0].strftime("%d/%m/%Y")
From 3863703d261dfb5ce430838d301c3b396862a173 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:19:50 +0100
Subject: [PATCH 09/21] add windows and py 3.14 to test matrix, use newest
conda activate action
---
.github/workflows/tests.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 50c2548..cb60f05 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,9 +8,10 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest, macos-latest]
+ os: [ubuntu-latest, macos-latest, windows-latest]
include:
- python: "3.10"
+ - python: "3.14"
runs-on: ${{ matrix.os }}
defaults:
run:
@@ -18,9 +19,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: setup conda
- uses: conda-incubator/setup-miniconda@v2
+ uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python }}
+ auto-update-conda: true
environment-file: .github/test_conda_env.yml
- name: print conda environment info
run: |
From 1350c356cd7ca01422e354adc273baaf4b6b66e2 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:27:44 +0100
Subject: [PATCH 10/21] try to setup fortran with action
---
.github/workflows/tests.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index cb60f05..9dcfbe1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -18,6 +18,10 @@ jobs:
shell: bash -l {0}
steps:
- uses: actions/checkout@v3
+ - uses: fortran-lang/setup-fortran@v1
+ id: setup-fortran
+ with:
+ compiler: gcc
- name: setup conda
uses: conda-incubator/setup-miniconda@v3
with:
From 73843034ac196eed69787db952f25e7e59a35347 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:32:20 +0100
Subject: [PATCH 11/21] test py 3.10 and 3.14
---
.github/workflows/tests.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9dcfbe1..c101975 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,9 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- include:
- - python: "3.10"
- - python: "3.14"
+ python: ["3.10", "3.14"]
runs-on: ${{ matrix.os }}
defaults:
run:
From 1a1300ef8bce9339c91cb3f35581fd5ab8c25c79 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:42:53 +0100
Subject: [PATCH 12/21] move start download prints to correct place
---
pygtide/update_etpred_data.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/pygtide/update_etpred_data.py b/pygtide/update_etpred_data.py
index a7930ac..c00dae6 100755
--- a/pygtide/update_etpred_data.py
+++ b/pygtide/update_etpred_data.py
@@ -86,6 +86,7 @@ def update_etpolut1(self):
start = tt.time()
if status:
try:
+ print("Start downloading: {:s} ...".format(self.leapsec_rfile))
urllib.request.urlopen(self.leapsec_rfile)
except OSError as error:
print(
@@ -97,13 +98,13 @@ def update_etpolut1(self):
status = False
pass
else:
- print("Start downloading: {:s} ...".format(self.leapsec_rfile))
urllib.request.urlretrieve(self.leapsec_rfile, leapsec_file)
end = tt.time()
print("Finished downloading ({:.1f} s).".format((end - start)))
if status:
try:
+ print("Start downloading: {:s} ...".format(self.iauhist_rfile))
urllib.request.urlopen(self.iauhist_rfile)
except OSError as error:
print(
@@ -115,13 +116,13 @@ def update_etpolut1(self):
status = False
pass
else:
- print("Start downloading: {:s} ...".format(self.iauhist_rfile))
urllib.request.urlretrieve(self.iauhist_rfile, iauhist_file)
end = tt.time()
print("Finished downloading ({:.1f} s).".format((end - start)))
if status:
try:
+ print("Start downloading: {:s} ...".format(self.iaucurr_rfile))
urllib.request.urlopen(self.iaucurr_rfile)
except OSError as error:
print(
@@ -133,7 +134,6 @@ def update_etpolut1(self):
status = False
pass
else:
- print("Start downloading: {:s} ...".format(self.iaucurr_rfile))
urllib.request.urlretrieve(self.iaucurr_rfile, iaucurr_file)
end = tt.time()
print("Finished downloading ({:.1f} s).".format((end - start)))
@@ -400,13 +400,13 @@ def update_etddt(self):
# %% download leap second history
start = tt.time()
try:
+ print("Start downloading: {:s} ...".format(leapsec_file.as_posix()))
urllib.request.urlopen(self.leapsec_rfile)
except OSError as error:
print("ERROR: Could not connect to remote server!")
print("MESSAGE: {0}.".format(error))
pass
else:
- print("Start downloading: {:s} ...".format(leapsec_file.as_posix()))
urllib.request.urlretrieve(self.leapsec_rfile, leapsec_file)
end = tt.time()
print("Finished downloading ({:.1f} s).".format((end - start)))
From 8ec3b8a6d8bf04ba95f45724923ed745ef6617de Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 14:47:02 +0100
Subject: [PATCH 13/21] add timeout to blocking ftp open call
---
pygtide/update_etpred_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pygtide/update_etpred_data.py b/pygtide/update_etpred_data.py
index c00dae6..7e916a5 100755
--- a/pygtide/update_etpred_data.py
+++ b/pygtide/update_etpred_data.py
@@ -105,7 +105,7 @@ def update_etpolut1(self):
if status:
try:
print("Start downloading: {:s} ...".format(self.iauhist_rfile))
- urllib.request.urlopen(self.iauhist_rfile)
+ urllib.request.urlopen(self.iauhist_rfile, timeout=1)
except OSError as error:
print(
"ERROR: Could not connect to remote server: {:s}".format(
From eef0f9c16be1e30b86c60e566590a14837a4d780 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 15:04:40 +0100
Subject: [PATCH 14/21] fix another occurence of dt.UTC
---
pygtide/update_etpred_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pygtide/update_etpred_data.py b/pygtide/update_etpred_data.py
index 7e916a5..5ed09a3 100755
--- a/pygtide/update_etpred_data.py
+++ b/pygtide/update_etpred_data.py
@@ -420,7 +420,7 @@ def update_etddt(self):
regex = re.compile(r"^\s*updated\s*\:.*$", re.IGNORECASE)
for num, line in enumerate(f, 1):
line = regex.sub(
- "Updated : %s" % dt.datetime.now(dt.UTC).strftime("%d/%m/%Y"),
+ "Updated : %s" % dt.datetime.now(dt.timezone.utc).strftime("%d/%m/%Y"),
line,
)
header.append(line)
From 8ec709431d13167069797e48aebac390a169c5f1 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 15:04:47 +0100
Subject: [PATCH 15/21] update readme
---
README.md | 25 ++++++++++++++-----------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index aabdd22..63b31d4 100644
--- a/README.md
+++ b/README.md
@@ -19,23 +19,22 @@ There are two options:
* Download and install [*Anaconda*](https://www.anaconda.com/products/distribution) or [*Miniconda*](https://docs.conda.io/en/latest/miniconda.html)
* Install required packages:
```
- conda install numpy pandas requests git
+ conda install numpy pandas requests
```
### Installation options
-#### Option 1: Pre-built wheels (Windows, Python 3.8–3.11)
-Download the correct wheel for your Python version from the `windows/` subfolder and install:
-```powershell
-pip install [wheel_name_depending_on_python_version]
+#### Option 1: Install and compile source distribution from PyPi (Linux, macOS, Windows; Python 3.8–3.14)
+
+```bash
+pip install pygtide
```
-#### Option 2: Build from source (Linux, macOS, Windows; Python 3.8–3.14)
+#### Option 2: Build from source locally (Linux, macOS, Windows; Python 3.8–3.14)
**Requirements for building:**
- A Fortran compiler (e.g., `gfortran` via MinGW on Windows; included in Linux/macOS gcc toolchains) `conda install gfortran`
-- Meson build system: automatically installed via `pip` (see below)
-- Ninja (optional but recommended): `conda install ninja` or `pip install ninja`
+- Meson build system with ninja: automatically installed via `pip`
**Clone repo from git:**
```bash
@@ -46,9 +45,7 @@ git clone https://github.com/hydrogeoscience/pygtide.git
```bash
cd /path/to/pygtide
-python build.py
-
-pip install . --no-build-isolation
+pip install .
```
If Meson or Ninja are missing, pip will attempt to install them automatically. For faster builds, pre-install them:
@@ -56,6 +53,12 @@ If Meson or Ninja are missing, pip will attempt to install them automatically. F
pip install meson-python meson ninja
```
+#### Option 3: Pre-built wheels (Windows, Python 3.8–3.11)
+Download the correct wheel for your Python version from the `windows/` subfolder and install:
+```powershell
+pip install [wheel_name_depending_on_python_version]
+```
+
### After installation
* Run tests to verify installation:
From 5a52cee2d10a84235b68ea3b5114a4b051a0d993 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 17:22:01 +0100
Subject: [PATCH 16/21] check if CI tests in windows work with ifx compiler
---
.github/workflows/tests.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c101975..399ad44 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -19,7 +19,7 @@ jobs:
- uses: fortran-lang/setup-fortran@v1
id: setup-fortran
with:
- compiler: gcc
+ compiler: ${{ startsWith(matrix.os, 'windows') && 'ifx' || 'gcc' }}
- name: setup conda
uses: conda-incubator/setup-miniconda@v3
with:
From 70aa376ba177528ecbd41607364c7669ac53575b Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 17:25:54 +0100
Subject: [PATCH 17/21] remove requests dep, not used
---
.github/test_conda_env.yml | 1 -
pyproject.toml | 1 -
2 files changed, 2 deletions(-)
diff --git a/.github/test_conda_env.yml b/.github/test_conda_env.yml
index 6497b7b..0af1d61 100644
--- a/.github/test_conda_env.yml
+++ b/.github/test_conda_env.yml
@@ -4,4 +4,3 @@ channels:
dependencies:
- numpy
- pandas
- - requests
diff --git a/pyproject.toml b/pyproject.toml
index 0910bab..5fe53b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,6 @@ authors = [
dependencies = [
"numpy>=1.21.0",
"pandas",
- "requests"
]
classifiers = [
"Development Status :: 4 - Beta",
From 048e6b808d760155a13ab454bc7e139fffcf6811 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 17:30:49 +0100
Subject: [PATCH 18/21] install gfortran runtime on windows
---
.github/workflows/tests.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 399ad44..8e29ec6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -19,13 +19,16 @@ jobs:
- uses: fortran-lang/setup-fortran@v1
id: setup-fortran
with:
- compiler: ${{ startsWith(matrix.os, 'windows') && 'ifx' || 'gcc' }}
+ compiler: gcc
- name: setup conda
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python }}
auto-update-conda: true
environment-file: .github/test_conda_env.yml
+ - name: install gfortran runtime (Windows)
+ if: runner.os == 'Windows'
+ run: conda install -y -c conda-forge gfortran
- name: print conda environment info
run: |
conda info -a
From c935c8cfdc04493f7ca8ac3d24712de5524ebdb4 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Thu, 29 Jan 2026 17:49:01 +0100
Subject: [PATCH 19/21] approach appears to work for windows py3.14, but not
py3.10, check other versions
---
.github/workflows/tests.yml | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 8e29ec6..db17a61 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,6 +10,16 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.10", "3.14"]
+ exclude:
+ - os: windows-latest
+ python: "3.10"
+ include:
+ - os: windows-latest
+ python: "3.11"
+ - os: windows-latest
+ python: "3.12"
+ - os: windows-latest
+ python: "3.13"
runs-on: ${{ matrix.os }}
defaults:
run:
From 8f0a7e2ab904d2e955141baa80a812f6728b6a26 Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Fri, 30 Jan 2026 14:26:27 +0100
Subject: [PATCH 20/21] remove win/py11+13 from CI matrix
---
.github/workflows/tests.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index db17a61..c445197 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,12 +14,8 @@ jobs:
- os: windows-latest
python: "3.10"
include:
- - os: windows-latest
- python: "3.11"
- os: windows-latest
python: "3.12"
- - os: windows-latest
- python: "3.13"
runs-on: ${{ matrix.os }}
defaults:
run:
From 7882889545e2fc184ce8d7cb3b02f150aadb5e4f Mon Sep 17 00:00:00 2001
From: Tom Eulenfeld
Date: Fri, 30 Jan 2026 14:54:51 +0100
Subject: [PATCH 21/21] tested mesonpy build backend
---
pyproject.toml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index 5fe53b5..2d4ca8b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,8 +4,10 @@ requires = [
"wheel",
"numpy>=1.21.0",
"meson",
+# "meson-python",
"ninja"
]
+#build-backend = 'mesonpy'
build-backend = "setuptools.build_meta"
[project]