Skip to content

Commit

Permalink
Merge pull request #7 from philippemiron/modernisation
Browse files Browse the repository at this point in the history
Modernisation
  • Loading branch information
philippemiron authored Jan 28, 2025
2 parents 92bc82e + ed7c6ac commit 9ce60fb
Show file tree
Hide file tree
Showing 19 changed files with 1,794 additions and 410 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python-version: ["3.9"]
python-version: ["3.12"]
steps:
- uses: actions/checkout@v2
- uses: mamba-org/provision-with-micromamba@main
- uses: mamba-org/setup-micromamba@v1
with:
environment-file: environment.yml
environment-name: pygtm
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
12 changes: 12 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"python.testing.unittestArgs": [
"-v",
"discover",
"-s",
"./tests",
"-p",
"*test*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
2 changes: 2 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ dependencies:
- scikit-learn
- netCDF4
- cmocean
- ipykernel
- ruff
26 changes: 16 additions & 10 deletions pygtm/dataset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Module containing the trajectory class."""

import numpy as np
from scipy.interpolate import interp1d

Expand All @@ -17,13 +19,14 @@ def __init__(self, x, y, t, ids):

@staticmethod
def monotonic(x):
"""
Test if an array is in a monotonic order
"""Test if an array is in a monotonic order.
Args:
x: array
Returns:
True or False
"""
# True if array is monotonic.
# monotonic(X) returns True (False) if X is (not) monotonic.
Expand All @@ -41,8 +44,8 @@ def monotonic(x):

@staticmethod
def trajectory_interpolation(t, x, y, s):
"""
Interpolation function x(t), y(t) describe the locations at time t of a trajectory
"""Interpolation function x(t), y(t) describe the locations at time t of a trajectory.
Args:
t: time t of a trajectory
x: longitude of the trajectory
Expand All @@ -53,6 +56,7 @@ def trajectory_interpolation(t, x, y, s):
ti: time of the interpolated trajectory
fx(ti): longitude of the trajectory at the interpolated time
fy(ti): latitude of the trajectory at the interpolated time
"""
# interpolation functions
fx = interp1d(t, x)
Expand All @@ -65,23 +69,24 @@ def trajectory_interpolation(t, x, y, s):

@staticmethod
def intersection_ratio(x1, x2):
"""
Function used to interpolate trajectories at the ±180 meridian
"""Interpolate trajectories at the ±180 meridian.
Args:
x1: longitude on one side of the ±180 meridian
x2: longitude on the other side of the ±180 meridian
Returns:
the ratio between x1 and ±180 meridian over x1 and x2
"""
if x1 < 0:
return (x1 + 180) / (360 - np.abs(x1 - x2))
else:
return (180 - x1) / (360 - np.abs(x1 - x2))

def create_segments(self, T):
"""
Subdivide full trajectories into a list of segments of T days
"""Subdivide full trajectories into a list of segments of T days.
Args:
T: transition time
Expand Down Expand Up @@ -237,8 +242,8 @@ def create_segments(self, T):
self.yt = y0

def filtering(self, x_range=None, y_range=None, t_range=None, complete_track=True):
"""
Returns trajectory in a spatial domain and/or temporal range
"""Filter trajectory in a spatial domain and/or temporal range.
Args:
x_range: longitudinal range (ascending order)
y_range: meridional range (ascending order)
Expand All @@ -254,6 +259,7 @@ def filtering(self, x_range=None, y_range=None, t_range=None, complete_track=Tru
segs_t [Ns]: time associated to each segments
segs_ind [Nt,2]: indices of the first and last segment of a trajectory
ex: trajectory 0 contains the segs[segs_ind[0,0]:segs_ind[0,1]]
"""
if x_range is None:
x_range = [np.min(self.x), np.max(self.x)]
Expand Down
58 changes: 35 additions & 23 deletions pygtm/matrix.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from pygtm import tools
"""Module to calculate the transition matrix and its properties."""

import numpy as np
import scipy as sc
import scipy.linalg as sla
import scipy.sparse.linalg as ssla
from sklearn.preprocessing import maxabs_scale
from scipy.sparse.csgraph import connected_components
from sklearn.cluster import KMeans
from sklearn.preprocessing import maxabs_scale

from pygtm import tools


class matrix_space:
Expand All @@ -28,8 +31,8 @@ def __init__(self, domain):
self.ccs = None

def fill_transition_matrix(self, data):
"""
Calculate the transition matrix of all points (x0, y0) to (xT, yT) on grid domain.bins
"""Calculate the transition matrix of all points of the domain.
Args:
data: object containing initial and final points of trajectories segments
data.x0: array longitude of segment initial points
Expand All @@ -41,6 +44,7 @@ def fill_transition_matrix(self, data):
B: containing particles at initial time for each bin
P: transition matrix
M: number of particles per bins at time t
"""
# Function to evaluate the transition Matrix
# For each elements id [1:N]
Expand Down Expand Up @@ -99,13 +103,14 @@ def fill_transition_matrix(self, data):
return

def transition_matrix_extras(self, data):
"""
Miscellaneous operations perform after the calculations of the transition matrix
"""Miscellaneous operations perform after the calculations of the transition matrix.
Args:
data: trajectory object
Returns:
None
"""
d = self.domain
in_domain = np.all(
Expand Down Expand Up @@ -142,9 +147,7 @@ def transition_matrix_extras(self, data):
self.fo = 1 - np.sum(self.P, 1)

def largest_connected_components(self):
"""
Restrict the matrix to the strongly connected components
"""
"""Restrict the matrix to the strongly connected components."""
# a set of nodes is consider strongly connected if there
# is a connection between each pair of nodes in the set
_, self.ccs = connected_components(self.P, directed=True, connection="strong")
Expand All @@ -167,15 +170,16 @@ def largest_connected_components(self):

@staticmethod
def eigenvectors(m, n):
"""
Calculate n real eigenvalues and eigenvectors of the matrix mat
"""Calculate n real eigenvalues and eigenvectors of the matrix mat.
Args:
m: square matrix
n: number of eigenvalues and eigenvectors to calculate
Returns:
d: top n real eigenvalues in descending order
v: top n eigenvectors associated with eigenvalues d
"""
if n is None:
d, v = sla.eig(m) # all eigenvectors
Expand All @@ -195,8 +199,8 @@ def eigenvectors(m, n):
return d, v

def left_and_right_eigenvectors(self, n=None):
"""
Function to call eigenvectors() for left and right calculations
"""Call eigenvectors for left and right calculations.
Args:
n: number of eigenvectors to calculate (default all)
Expand All @@ -205,34 +209,37 @@ def left_and_right_eigenvectors(self, n=None):
eigL: left eigenvalues (equal to eigR in our case)
R: right eigenvectors
L: left eigenvectors
"""
self.eigR, self.R = self.eigenvectors(self.P, n)
self.eigL, self.L = self.eigenvectors(np.transpose(self.P), n)

def lagrangian_geography(self, selected_vec, n_clusters):
"""
Cluster the eigenvectors to evaluate the Lagrangian Geography
"""Cluster the eigenvectors to evaluate the Lagrangian Geography.
Args:
selected_vec: list of selected eigenvectors
n_clusters: number of clusters
Returns:
model.labels_: array corresponding to the cluster associated with each bin
"""
vectors_geo = self.R[:, selected_vec]
model = KMeans(n_clusters=n_clusters, random_state=1).fit(vectors_geo)

return model.labels_

def push_forward(self, d0, exp):
"""
Dispersion of a initial distribution
"""Dispersion of a initial distribution.
Args:
d0: initial distribution
exp: proportional to the time to evolve the density (time = exp*T)
Returns:
d: evolved density at t0 + exp*T
"""
# for loop is faster than using matrix_power() with big matrix
d = np.copy(d0)
Expand All @@ -241,13 +248,14 @@ def push_forward(self, d0, exp):
return d

def matrix_to_graph(self, mat=None):
"""
Transform the transition matrix into a graph
"""Transform the transition matrix into a graph.
Args:
mat: transition matrix
Returns:
graph: dictionary where each key is a node and the values are its connection(s)
"""
graph = {}

Expand All @@ -264,12 +272,14 @@ def matrix_to_graph(self, mat=None):
return graph

def residence_time(self, target):
"""
Calculate residence time from P matrix and a subset element list
"""Calculate residence time from P matrix and a subset element list.
Args:
target: bin indices in the zone of interest
Returns:
array [N]: residence time
"""
c = np.zeros(len(self.P))
a = sc.sparse.eye(len(target)) - self.P[np.ix_(target, target)]
Expand All @@ -280,12 +290,14 @@ def residence_time(self, target):
return c

def hitting_time(self, target):
"""
Calculate hitting time from P matrix and a subset element list
"""Calculate hitting time from P matrix and a subset element list.
Args:
target: bin indices in the zone to hit
Returns:
array [N]: hitting time
"""
# The hitting time of set A is equal to the residence time
# of set B, with B set as the completent of set A
Expand Down
Loading

0 comments on commit 9ce60fb

Please sign in to comment.