diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 31140b8..347e0db 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -21,6 +21,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + tempo2-version: ['2021.07.1-correct', '2023.05.1', '2024.11.1', '2025.02.1'] steps: - name: Checkout repository @@ -34,11 +35,11 @@ jobs: run: | brew unlink gcc && brew link gcc brew install automake libtool - ./install_tempo2.sh + ./install_tempo2.sh -v ${{ matrix.tempo2-version }} - name: Install tempo2 on linux if: runner.os == 'Linux' run: | - ./install_tempo2.sh + ./install_tempo2.sh -v ${{ matrix.tempo2-version }} - name: Install dependencies and package run: | python -m pip install --upgrade pip diff --git a/install_tempo2.sh b/install_tempo2.sh index 44eeb69..338c0b9 100755 --- a/install_tempo2.sh +++ b/install_tempo2.sh @@ -1,21 +1,46 @@ #!/bin/bash -e -# get install location -if [ $# -eq 0 ] - then - echo 'No install location defined, using' $HOME'/.local/' - prefix=$HOME/.local/ - else - prefix=$1 - echo 'Will install in' $prefix +# default Tempo2 version +tempo2version="2021.07.1-correct" + +usage() { echo "Usage: $0 [-p ] [-v ]" 1>&2; exit 1; } + +# default install location +prefix=$HOME/.local/ +if [[ $# -eq 1 && "$1" != "-h" ]]; then + # interpret single argument as install location + prefix=$1 + echo 'Will install in' $prefix +else + # allow arguments + while getopts ":p:v:" o; do + case "${o}" in + p) + prefix=${OPTARG} + ;; + v) + tempo2version=${OPTARG} + ;; + *) + usage + ;; + esac +done fi +echo 'Will install in' $prefix +echo 'Will attempt to install tempo2 version' $tempo2version + # make a destination directory for runtime files export TEMPO2=$prefix/share/tempo2 mkdir -p $TEMPO2 -curl -O https://bitbucket.org/psrsoft/tempo2/get/2021.07.1-correct.tar.gz -tar zxvf 2021.07.1-correct.tar.gz +dl=$(curl -Of https://bitbucket.org/psrsoft/tempo2/get/${tempo2version}.tar.gz || echo $?) +if [ $dl -ne 0 ]; then + echo 'Version '${tempo2version}' of Tempo2 does not exist. Please see, e.g., https://github.com/mattpitkin/tempo2/tags for a list of allowed version tags.' + exit 1 +fi +tar -zxvf ${tempo2version}.tar.gz cd psrsoft-tempo2-* @@ -33,5 +58,5 @@ cp -r T2runtime/* $TEMPO2 cd .. rm -rf psrsoft-tempo2-* -rm -rf 2021.07.1-correct.tar.gz +rm -rf ${tempo2version}.tar.gz echo "Set TEMPO2 environment variable to ${TEMPO2} to make things run more smoothly." diff --git a/libstempo/libstempo.pyx b/libstempo/libstempo.pyx index cd30843..6b83478 100644 --- a/libstempo/libstempo.pyx +++ b/libstempo/libstempo.pyx @@ -6,13 +6,20 @@ from packaging import version import collections from collections import OrderedDict + # what is the default encoding here? -string = lambda s: s.decode() +def string(buf): + try: + return buf.decode('utf-8') + except UnicodeDecodeError: + return buf.decode('utf-8', errors='replace') + + string_dtype = 'U' from libc cimport stdlib, stdio -from libc.string cimport strncpy, memset +from libc.string cimport strcpy, strncpy, memset from cython cimport view @@ -150,23 +157,21 @@ cdef extern from "tempo2.h": long double sat_day # Just the Day part long double sat_sec # Just the Sec part long double bat # barycentric arrival time - long double bbat # barycentric arrival time long double batCorr #update from sat-> bat + long double bbat # barycentric arrival time long double pet # pulsar emission time int clockCorr # = 1 for clock corrections to be applied, = 0 for BAT int delayCorr # = 1 for time delay corrections to be applied, = 0 for BAT int deleted # 1 if observation deleted, -1 if not in fit long double prefitResidual long double residual - double toaErr # error on TOA (in us) - double origErr # original error on TOA after reading tim file (in us) - double toaDMErr # error on TOA due to DM (in us) - char **flagID # ID of flags - char **flagVal # Value of flags - int nFlags # Number of flags set double freq # frequency of observation (in MHz) double freqSSB # frequency of observation in barycentric frame (in Hz) - char fname[MAX_FILELEN] # name of datafile giving TOA + double toaErr # error on TOA (in us) + double toaDMErr # error on TOA due to DM (in us) + double origErr # original error on TOA after reading tim file (in us) + double phaseOffset # Phase offset + char fname[MAX_FILELEN + 1] # name of datafile giving TOA char telID[100] # telescope ID double sun_ssb[6] # Sun wrt SSB double earth_ssb[6] # Earth center wrt SSB @@ -174,18 +179,21 @@ cdef extern from "tempo2.h": double observatory_earth[6] # Obs wrt Earth center double psrPos[3] # Unit vector to the pulsar position double zenith[3] # Zenith vector, in BC frame. Length=geodetic height - long double torb # Combined binary delay - long long pulseN # Pulse number - long double roemer # Roemer delay double shapiroDelaySun # Shapiro delay caused by the Sun - double phaseOffset # Phase offset + long double roemer # Roemer delay + long double torb # Combined binary delay long double phase # the phase (cycles) - double efac # Error multiplication factor - double equad # Value to add in quadrature + long long pulseN # Pulse number + char flagID[MAX_FLAGS][MAX_FLAG_LEN] # ID of flags + char flagVal[MAX_FLAGS][MAX_FLAG_LEN] # Value of flags + int nFlags # Number of flags set int jump[MAX_FLAGS] # Jump region + #double jumpScale[MAX_FLAGS] int obsNjump # Number of jumps for this observation int fdjump[MAX_FLAGS] int obsNfdjump + double efac # Error multiplication factor + double equad # Value to add in quadrature ctypedef int param_label @@ -204,27 +212,44 @@ cdef extern from "tempo2.h": double height_grs80 # GRS80 geodetic height ctypedef struct pulsar: + char name[100] parameter param[MAX_PARAMS] - observation *obsn - char *name - int nobs - int rescaleErrChisq - int noWarnings - double fitChisq + char binaryModel[100] + + double posPulsar[3] # 3-unitvector pointing at the pulsar + double ne_sw + int nJumps char fjumpID[16] double jumpVal[MAX_JUMPS] # char jumpSAT[MAX_JUMPS] int fitJump[MAX_JUMPS] double jumpValErr[MAX_JUMPS] - char *binaryModel + # char jumpScaled[MAX_JUMPS] + + # new parameters for fdjumps + int nfdJumps + char ffdjumpID[16] + double fdjumpVal[MAX_JUMPS] + int fdjumpIdx[MAX_JUMPS] + int fitfdJump[MAX_JUMPS] + double fdjumpValErr[MAX_JUMPS] + char fdjump_log + + double fitChisq + int rescaleErrChisq + + observation *obsn + int nobs int t2cMethod # How to transform from terrestrial to celestial coords. Set in parfile with T2CMETHOD # tempo2 supports T2C_IAU2000B (default) and T2C_TEMPO - char *JPL_EPHEMERIS - char *ephemeris + int noWarnings + + char JPL_EPHEMERIS[MAX_FILELEN] + char ephemeris[MAX_FILELEN] int useCalceph + char tzrsite[100] int eclCoord # = 1 for ecliptic coords otherwise celestial coords - double posPulsar[3] # 3-unitvector pointing at the pulsar # long double phaseJump[MAX_JUMPS] # Time of phase jump (Deprecated. WHY?) int phaseJumpID[MAX_JUMPS] # ID of closest point to phase jump int phaseJumpDir[MAX_JUMPS] # Size and direction of phase jump @@ -232,39 +257,22 @@ cdef extern from "tempo2.h": double rmsPost char clock[16] FitInfo fitinfo - - double ne_sw - double ne_sw_ifuncT[MAX_IFUNC] - double ne_sw_ifuncV[MAX_IFUNC] - double ne_sw_ifuncE[MAX_IFUNC] - int ne_sw_ifuncN - - # new parameters for fdjumps - int nfdJumps - char ffdjumpID[16] - double fdjumpVal[MAX_JUMPS] - int fdjumpIdx[MAX_JUMPS] - int fitfdJump[MAX_JUMPS] - double fdjumpValErr[MAX_JUMPS] - char fdjump_log # noise parameters follow - # T2EFAC + # T2EFAC/EQUAD int nT2efac + int nT2equad char T2efacFlagID[MAX_T2EFAC][MAX_FLAG_LEN] char T2efacFlagVal[MAX_T2EFAC][MAX_FLAG_LEN] double T2efacVal[MAX_T2EFAC] - - # GLOBAL_EFAC in timfile??? - double T2globalEfac - - # T2EQUAD - int nT2equad char T2equadFlagID[MAX_T2EQUAD][MAX_FLAG_LEN] char T2equadFlagVal[MAX_T2EQUAD][MAX_FLAG_LEN] double T2equadVal[MAX_T2EQUAD] + # GLOBAL_EFAC in timfile??? + double T2globalEfac + # TNEF int nTNEF char TNEFFlagID[MAX_TNEF][MAX_FLAG_LEN] @@ -305,7 +313,12 @@ cdef extern from "tempo2.h": # set reference observation char refphs - char tzrsite[100] + + double ne_sw_ifuncT[MAX_IFUNC] + double ne_sw_ifuncV[MAX_IFUNC] + double ne_sw_ifuncE[MAX_IFUNC] + int ne_sw_ifuncN + void initialise(pulsar *psr, int noWarnings) void destroyOne(pulsar *psr) @@ -804,14 +817,23 @@ cdef class tempopulsar: else: raise IOError("Cannot find timfile {0}.".format(timfile)) - parfile_bytes, timfile_bytes = parfile.encode(), timfile.encode() + parfile_bytes, timfile_bytes = (parfile + "\0").encode("utf-8", errors="ignore"), (timfile + "\0").encode("utf-8", errors="ignore") - for checkfile in [parfile_bytes,timfile_bytes]: - if len(checkfile) > MAX_FILELEN - 1: + for checkfile in [parfile_bytes, timfile_bytes]: + if len(checkfile) > MAX_FILELEN: raise IOError("Filename {0} is too long for tempo2.".format(checkfile)) - stdio.sprintf(parFile[0],"%s",parfile_bytes) - stdio.sprintf(timFile[0],"%s",timfile_bytes) + cdef const char *par_file_c_str = parfile_bytes + cdef const char *tim_file_c_str = timfile_bytes + + cdef size_t pflen = sizeof(char) * len(parfile_bytes) + cdef size_t tflen = sizeof(char) * len(timfile_bytes) + + #strcpy(parFile[0], par_file_c_str) + #strcpy(timFile[0], tim_file_c_str) + + stdio.snprintf(parFile[0], pflen, "%s", par_file_c_str) + stdio.snprintf(timFile[0], tflen, "%s", tim_file_c_str) readParfile(self.psr,parFile,timFile,self.npsr) # load the parameters (all pulsars) @@ -949,10 +971,13 @@ cdef class tempopulsar: return array def _setstring(self,char* string,maxlen,value): - value_bytes = value.encode() + value_bytes = (value + "\0").encode("utf-8", errors="ignore") + cdef const char *value_c_str = value_bytes + cdef size_t vblen = sizeof(char) * len(value_bytes) if len(value_bytes) < maxlen: - stdio.sprintf(string,"%s",value_bytes) + stdio.snprintf(string, vblen, "%s", value_c_str) + #strcpy(string, value_c_str) else: raise ValueError @@ -1034,7 +1059,7 @@ cdef class tempopulsar: for i in range(self.psr[0].nobs): # make sure fname array is empty memset(&(self.psr[0].obsn[i].fname[0]), 0, MAX_FILELEN * sizeof(char)) - strncpy(&(self.psr[0].obsn[i].fname[0]), "FAKE", 4 * sizeof(char)) + strncpy(&(self.psr[0].obsn[i].fname[0]), "FAKE\0", 5 * sizeof(char)) self._inputtoaerrs() self._inputobservatory() @@ -1113,7 +1138,8 @@ cdef class tempopulsar: # set the observatories for i in range(self.psr[0].nobs): - strncpy(&(self.psr[0].obsn[i].telID[0]), obsv[i], 100 * sizeof(char)) + obstr = obsv[i][:99] + b"\0" # append null character + strncpy(&(self.psr[0].obsn[i].telID[0]), obstr, len(obstr) * sizeof(char)) # set which corrections to apply (taken from TEMPO2 readTimfile.C) if obsv[i][0] == "@" or obsv[i] == "bat": @@ -1161,10 +1187,13 @@ cdef class tempopulsar: def __set__(self,value): # this is OK in both Python 2 and 3 - name_bytes = value.encode() + name_bytes = (value + "\0").encode("utf-8", errors="ignore") + cdef const char *name_c_str = name_bytes + cdef size_t nlen = sizeof(char) * len(name_bytes) if len(name_bytes) < 100: - stdio.sprintf(self.psr[0].name,"%s",name_bytes) + #strcpy(self.psr[0].name, name_c_str) + stdio.snprintf(self.psr[0].name, nlen, "%s", name_c_str) else: raise ValueError @@ -1175,10 +1204,13 @@ cdef class tempopulsar: return string(self.psr[0].binaryModel) def __set__(self,value): - model_bytes = value.encode() + model_bytes = (value + "\0").encode("utf-8", errors="ignore") + cdef const char *model_c_str = model_bytes + cdef size_t mblen = sizeof(char) * len(model_bytes) if len(model_bytes) < 100: - stdio.sprintf(self.psr[0].binaryModel,"%s",model_bytes) + #strcpy(self.psr[0].binaryModel, model_c_str) + stdio.snprintf(self.psr[0].binaryModel, mblen, "%s", model_c_str) else: raise ValueError @@ -1190,13 +1222,17 @@ cdef class tempopulsar: def __set__(self,value): def seteph(filename,usecalceph=False): - model_bytes = filename.encode() + model_bytes = (filename + "\0").encode("utf-8", errors="ignore") + cdef const char *model_c_str = model_bytes + cdef size_t mblen = sizeof(char) * len(model_bytes) if len(model_bytes) < MAX_FILELEN: - stdio.sprintf(self.psr[0].JPL_EPHEMERIS,"%s",model_bytes) + #strcpy(self.psr[0].JPL_EPHEMERIS, model_c_str) + stdio.snprintf(self.psr[0].JPL_EPHEMERIS, mblen, "%s", model_c_str) # older tempo2 versions use ephemeris instead of JPL_EPHEMERIS for calceph. - stdio.sprintf(self.psr[0].ephemeris, "%s",model_bytes) + #strcpy(self.psr[0].ephemeris, model_c_str) + stdio.snprintf(self.psr[0].ephemeris, mblen, "%s", model_c_str) self.psr[0].useCalceph = int(usecalceph) @@ -1236,11 +1272,14 @@ cdef class tempopulsar: def __get__(self): return string(self.psr[0].clock) - def __set__(self,value): - value_bytes = value.encode() + def __set__(self, value): + value_bytes = (value + "\0").encode("utf-8", errors="ignore") + cdef const char *value_c_str = value_bytes + cdef size_t vlen = sizeof(char) * len(value_bytes) if len(value_bytes) < 16: - stdio.sprintf(self.psr[0].clock,"%s",value_bytes) + #strcpy(self.psr[0].clock, value_c_str) + stdio.snprintf(self.psr[0].clock, vlen, "%s", value_c_str) else: raise ValueError("CLK name '{}' is too long.".format(value)) @@ -1698,7 +1737,8 @@ cdef class tempopulsar: # set reference epoch self["TZRMJD"].val = epoch if site is not None: - strncpy(&(self.psr[0].tzrsite[0]), str.encode(site), 100 * sizeof(char)) + sitestr = str(site).encode("utf-8", errors="ignore") + b"\0" # append null character + strncpy(&(self.psr[0].tzrsite[0]), sitestr, len(sitestr) * sizeof(char)) if freq is not None: self["TZRFRQ"] = freq self.psr[0].refphs = REFPHS_TZR @@ -2189,12 +2229,16 @@ cdef class tempopulsar: if not parfile: parfile = self.parfile - parfile_bytes = parfile.encode() + parfile_bytes = (parfile + "\0").encode("utf-8", errors="ignore") - if len(parfile_bytes) > MAX_FILELEN - 1: + cdef const char *parfile_c_bytes = parfile_bytes + + if len(parfile_bytes) > MAX_FILELEN: raise IOError("Parfile name {0} too long for tempo2!".format(parfile)) - stdio.sprintf(parFile,"%s",parfile_bytes) + #cdef const char parFile + cdef size_t pflen = sizeof(char) * len(parfile_bytes) + stdio.snprintf(parFile, pflen, "%s", parfile_c_bytes) # void textOutput(pulsar *psr,int npsr, # double globalParameter, -- ? @@ -2219,12 +2263,16 @@ cdef class tempopulsar: if not timfile: timfile = self.timfile - timfile_bytes = timfile.encode() + timfile_bytes = (timfile + "\0").encode("utf-8", errors="ignore") + + cdef const char *timfile_c_bytes = timfile_bytes - if len(timfile_bytes) > MAX_FILELEN - 1: + if len(timfile_bytes) > MAX_FILELEN: raise IOError("Timfile name {0} too long for tempo2!".format(timfile)) - stdio.sprintf(timFile,"%s",timfile_bytes) + #cdef const char *timFile = timfile_bytes + cdef size_t tflen = sizeof(char) * len(timfile_bytes) + stdio.snprintf(timFile, tflen, "%s", timfile_c_bytes) writeTim(timFile,&(self.psr[0]),'tempo2') @@ -2257,11 +2305,11 @@ def rewritetim(timfile): # encodes are needed here because file is open in binary mode if m: - out.write('{0} {1}/{2}\n'.format(m.group(1),os.path.dirname(timfile),m.group(2)).encode()) + out.write('{0} {1}/{2}\n'.format(m.group(1),os.path.dirname(timfile),m.group(2)).encode("utf-8", errors="ignore")) else: - out.write(line.encode()) + out.write(line.encode("utf-8", errors="ignore")) else: - out.write(line.encode()) + out.write(line.encode("utf-8", errors="ignore")) return out.name diff --git a/pyproject.toml b/pyproject.toml index 091ce44..cdf05a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,9 @@ [build-system] requires = [ - "setuptools>=61", + "packaging>=24.2", + 'setuptools>=77, <80; python_version > "3.8"', + 'setuptools>=74; python_version == "3.8"', "setuptools_scm[toml]>=6.2", "wheel", # ephem package likes to have wheel installed "cython", @@ -25,9 +27,8 @@ description = "A Python wrapper for tempo2" authors = [{name = "Michele Vallisneri", email = "vallis@vallis.org"}] urls = { Homepage = "https://github.com/vallis/libstempo" } readme = "README.md" -license = "MIT" -license-files = [ "LICENSE" ] -classifiers=[ +license = { text = "MIT" } + classifiers=[ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: MacOS", @@ -71,6 +72,7 @@ dev = [ [tool.setuptools] include-package-data = true +license-files = [ "LICENSE" ] [tool.setuptools.packages.find] include = [