fargopy 0.4.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fargopy/sys.py CHANGED
@@ -17,52 +17,67 @@ from select import select
17
17
  # Classes
18
18
  ###############################################################
19
19
 
20
- #/////////////////////////////////////
20
+
21
+ # /////////////////////////////////////
21
22
  # UTIL CLASS
22
- #/////////////////////////////////////
23
+ # /////////////////////////////////////
23
24
  class Sys(object):
25
+ """System utilities for command execution, directory locking, and resource monitoring.
26
+
27
+ The ``Sys`` class provides a collection of static methods to interact with
28
+ the operating system. It handles shell command execution with output capturing,
29
+ directory locking mechanisms for concurrent safety, and system pause functionalities.
30
+ """
24
31
 
25
32
  QERROR = True
26
- STDERR = ''
27
- STDOUT = ''
28
- OUT = ''
33
+ STDERR = ""
34
+ STDOUT = ""
35
+ OUT = ""
29
36
 
30
37
  @staticmethod
31
- def run(cmd,quiet=True):
32
- """Run a system command
33
-
34
- Parameters:
35
- cmd: string:
36
- Command to run
37
- quiet: boolean, default = True:
38
- When False the output of the command is shown.
39
-
40
- Output:
41
- error: integer:
42
- Error code (0,-1,>0)
43
-
44
- output: list:
45
- List with output.
46
- If error == 0, output[:-1] will contain
47
- the output line by line.
38
+ def run(cmd, quiet=True):
39
+ """Run a system shell command.
40
+
41
+ Executes a command string in the shell and captures its stdout and stderr.
42
+ Useful for running FARGO3D executables or other system tools.
43
+
44
+ Parameters
45
+ ----------
46
+ cmd : str
47
+ The command string to execute.
48
+ quiet : bool, optional
49
+ If True, suppress printing output to stdout (default: True).
50
+
51
+ Returns
52
+ -------
53
+ tuple
54
+ (error_code, output_list)
55
+ - error_code (int): 0 for success, non-zero for error.
56
+ - output_list (list): List of output lines (strings).
57
+
58
+ Examples
59
+ --------
60
+ Run a simple shell command:
61
+
62
+ >>> err, out = fp.Sys.run("ls -l")
48
63
  """
49
64
  fargopy.Debug.trace(f"sysrun::cmd = {cmd}")
50
65
 
51
- out=[]
66
+ out = []
52
67
  for path in Sys._run(cmd):
53
68
  try:
54
69
  if not quiet:
55
- print(path.decode('utf-8'))
56
- out += [path.decode('utf-8')]
70
+ print(path.decode("utf-8"))
71
+ out += [path.decode("utf-8")]
57
72
  except:
58
- out += [(path[0],path[1].decode('utf-8'))]
59
-
60
- Sys.STDOUT = ''
61
- if len(out)>1:
62
- Sys.STDOUT = '\n'.join(out[:-1])
63
-
73
+ out += [(path[0], path[1].decode("utf-8"))]
74
+
75
+ Sys.STDOUT = ""
76
+ if len(out) > 1:
77
+ Sys.STDOUT = "\n".join(out[:-1])
78
+
64
79
  Sys.STDERR = out[-1][1]
65
- if len(Sys.STDERR)>0:
80
+ if len(Sys.STDERR) > 0:
66
81
  Sys.QERROR = out[-1][0]
67
82
  if Sys.QERROR == 0:
68
83
  Sys.QERROR = -1
@@ -74,30 +89,36 @@ class Sys(object):
74
89
 
75
90
  if fargopy.Debug.VERBOSE:
76
91
  error = out[-1][0]
77
- if Sys.QERROR>0:
92
+ if Sys.QERROR > 0:
78
93
  fargopy.Debug.trace(f"sysrun::Error check Sys.STDERR.")
79
- elif Sys.QERROR<0:
80
- fargopy.Debug.trace(f"sysrun::Done. Still some issues must be check. Check Sys.STDOUT and Sys.STDERR for details.")
81
- elif Sys.QERROR==0:
82
- fargopy.Debug.trace(f"sysrun::Done. You're great. Check Sys.STDOUT for details.")
83
-
94
+ elif Sys.QERROR < 0:
95
+ fargopy.Debug.trace(
96
+ f"sysrun::Done. Still some issues must be check. Check Sys.STDOUT and Sys.STDERR for details."
97
+ )
98
+ elif Sys.QERROR == 0:
99
+ fargopy.Debug.trace(
100
+ f"sysrun::Done. You're great. Check Sys.STDOUT for details."
101
+ )
102
+
84
103
  Sys.OUT = out
85
- return Sys.QERROR,out
104
+ return Sys.QERROR, out
86
105
 
87
106
  @staticmethod
88
107
  def _run(cmd):
89
- p=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
108
+ p = subprocess.Popen(
109
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
110
+ )
90
111
  while True:
91
112
  line = p.stdout.readline().rstrip()
92
113
  if not line:
93
114
  break
94
115
  yield line
95
- (output,error)=p.communicate()
96
- yield p.returncode,error
116
+ (output, error) = p.communicate()
117
+ yield p.returncode, error
97
118
 
98
119
  @staticmethod
99
120
  def simple(cmd):
100
- return os.system(cmd)
121
+ return subprocess.call(cmd, shell=True)
101
122
 
102
123
  @staticmethod
103
124
  def get_memory():
@@ -105,21 +126,38 @@ class Sys(object):
105
126
  return svmem
106
127
 
107
128
  @staticmethod
108
- def lock(dir,content=dict()):
109
- """Lock a directory using content information
129
+ def lock(dir, content=dict()):
130
+ """Create a lock file in a directory.
131
+
132
+ Creates a ``fargopy.lock`` file containing the provided content to
133
+ signal that the directory is in use or processing.
134
+
135
+ Parameters
136
+ ----------
137
+ dir : str
138
+ Path to the directory to lock.
139
+ content : dict, optional
140
+ Dictionary of metadata to store in the lock file.
110
141
  """
111
142
  if not os.path.isdir(dir):
112
143
  print(f"Locking directory '{dir}' not found.")
113
144
  return
114
-
145
+
115
146
  filename = f"{dir}/fargopy.lock"
116
- with open(filename,'w') as file_object:
117
- file_object.write(json.dumps(content,default=lambda obj:'<not serializable>'))
147
+ with open(filename, "w") as file_object:
148
+ file_object.write(
149
+ json.dumps(content, default=lambda obj: "<not serializable>")
150
+ )
118
151
  file_object.close()
119
152
 
120
153
  @staticmethod
121
154
  def unlock(dir):
122
- """UnLock a directory
155
+ """Remove the lock file from a directory.
156
+
157
+ Parameters
158
+ ----------
159
+ dir : str
160
+ Path to the directory to unlock.
123
161
  """
124
162
  if not os.path.isdir(dir):
125
163
  print(f"Locking directory '{dir}' not found.")
@@ -127,9 +165,9 @@ class Sys(object):
127
165
  filename = f"{dir}/fargopy.lock"
128
166
  if os.path.isfile(filename):
129
167
  fargopy.Sys.simple(f"rm -rf {filename}")
130
-
168
+
131
169
  @staticmethod
132
- def is_locked(dir,verbose=False):
170
+ def is_locked(dir, verbose=False):
133
171
  if not os.path.isdir(dir):
134
172
  if verbose:
135
173
  print(f"Locking directory '{dir}' not found.")
@@ -141,21 +179,34 @@ class Sys(object):
141
179
  with open(filename) as file_handler:
142
180
  info = json.load(file_handler)
143
181
  return info
144
-
145
- @staticmethod
146
- def sleep_timeout(timeout=5,msg=None):
147
- """This routine sleeps for a 'time'. In the meanwhile checks if there is a keyboard interrupt
148
- (Enter or Ctrl+C) and interrupt sleeping
149
182
 
150
- Examples:
151
- >>> Sys.sleep_timeout()
152
- >>> Sys.sleep_timeout(10)
153
-
154
- >>> for i in range(10):
155
- print(f"i = {i}")
156
- if Sys.sleep_timeout():break
183
+ @staticmethod
184
+ def sleep_timeout(timeout=5, msg=None):
185
+ """Sleep for a specified duration, interruptible by user input.
186
+
187
+ Sleeps for ``timeout`` seconds. Checks for keyboard interrupt (Enter or Ctrl+C)
188
+ during the sleep period.
189
+
190
+ Parameters
191
+ ----------
192
+ timeout : int, optional
193
+ Sleep duration in seconds (default: 5).
194
+ msg : str, optional
195
+ Message to display before sleeping.
196
+
197
+ Returns
198
+ -------
199
+ bool
200
+ True if interrupted, False if timeout completed.
201
+
202
+ Examples
203
+ --------
204
+ Sleep for 10 seconds or until user interrupt:
205
+
206
+ >>> if fp.Sys.sleep_timeout(10, "Press Enter to skip"):
207
+ ... print("Skipped")
157
208
  """
158
- try:
209
+ try:
159
210
  if msg:
160
211
  print(msg)
161
212
  rlist, wlist, xlist = select([sys.stdin], [], [], timeout)
@@ -165,4 +216,4 @@ class Sys(object):
165
216
  return False
166
217
  except KeyboardInterrupt:
167
218
  print("Interrupting")
168
- return True
219
+ return True
@@ -0,0 +1,8 @@
1
+ # fargopy/tests/test___interp.py
2
+ import os
3
+ import numpy as np
4
+ import pytest
5
+ import fargopy as fp
6
+
7
+ FILE = __file__
8
+ ROOTDIR = os.path.abspath(os.path.dirname(FILE))
@@ -0,0 +1,76 @@
1
+ import os
2
+ import numpy as np
3
+ import pytest
4
+ import fargopy as fp
5
+
6
+ FILE = __file__
7
+ ROOTDIR = os.path.abspath(os.path.dirname(FILE))
8
+
9
+
10
+ @pytest.fixture(scope="session")
11
+ def sim():
12
+ # Load the local test simulation only once per test session
13
+ fp.Simulation.download_precomputed("p3disoj")
14
+ return fp.Simulation(output_dir=f"/tmp/p3disoj")
15
+
16
+
17
+ def test_sphere_tessellation_properties():
18
+ # basic sanity checks on sphere tessellation
19
+ s = fp.flux.Surface(type="sphere", radius=1.0, subdivisions=1)
20
+ # number of triangles for 1 subdivision: 20*(4**1) = 80
21
+ assert s.num_triangles == 20 * (4**1)
22
+ assert s.centers.shape == (s.num_triangles, 3)
23
+ assert s.normals.shape == (s.num_triangles, 3)
24
+ assert s.areas.shape == (s.num_triangles,)
25
+ # areas positive
26
+ assert np.all(s.areas > 0)
27
+ # normals are unit length
28
+ norms = np.linalg.norm(s.normals, axis=1)
29
+ assert np.allclose(norms, 1.0, atol=1e-6)
30
+ # total area approximates sphere area 4*pi*r^2
31
+ total_area = np.sum(s.areas)
32
+ assert np.isfinite(total_area)
33
+ assert np.isclose(total_area, 4 * np.pi * s.radius**2, rtol=0.12)
34
+
35
+
36
+ def test_plane_tessellation_and_areas():
37
+ # plane tessellation with subdivisions should yield correct area sum
38
+ s = fp.flux.Surface(type="plane", radius=1.0, subdivisions=4, width=2.0, length=3.0)
39
+ assert s.centers.shape[0] == s.subdivisions**2
40
+ assert np.all(s.areas > 0)
41
+ total_area = np.sum(s.areas)
42
+ assert np.isclose(total_area, 2.0 * 3.0, rtol=1e-8)
43
+
44
+
45
+ def test_cylinder_tessellation_basic():
46
+ # cylinder tessellation exposes top/bottom/lateral arrays
47
+ s = fp.flux.Surface(type="cylinder", radius=0.5, height=1.0, subdivisions=8)
48
+ # ensure expected attributes exist and have consistent sizes
49
+ assert hasattr(s, "top_centers")
50
+ assert hasattr(s, "bottom_centers")
51
+ assert hasattr(s, "lateral_centers")
52
+ assert s.top_centers.shape[0] == s.bottom_centers.shape[0]
53
+ assert s.lateral_centers.shape[0] > 0
54
+
55
+
56
+ # FAILED
57
+ def test_total_mass_grid_integration(sim):
58
+ # Use the test simulation to run total_mass on a small sphere
59
+ s = fp.flux.Surface(
60
+ type="sphere", radius=0.1, subdivisions=0, center=(0.0, 0.0, 0.0)
61
+ )
62
+ # compute mass for a single snapshot (should be finite and non-negative)
63
+ mass = s.total_mass(sim, field="gasdens", snapshot=1, follow_planet=False)
64
+ assert np.isfinite(mass)
65
+ assert mass >= 0.0
66
+
67
+
68
+ def test_total_mass_multiple_snapshots_returns_array(sim):
69
+ s = fp.flux.Surface(
70
+ type="sphere", radius=0.1, subdivisions=0, center=(0.0, 0.0, 0.0)
71
+ )
72
+ masses = s.total_mass(sim, field="gasdens", snapshot=[1, 2], follow_planet=False)
73
+ assert isinstance(masses, np.ndarray)
74
+ assert masses.shape[0] == 2
75
+ assert np.all(np.isfinite(masses))
76
+ assert np.all(masses >= 0.0)
@@ -0,0 +1,132 @@
1
+ # fargopy/tests/test___interp.py
2
+ import os
3
+ import numpy as np
4
+ import pytest
5
+ import fargopy as fp
6
+
7
+
8
+ @pytest.fixture(scope="session")
9
+ def sim():
10
+ # Load the local test simulation only once per test session
11
+ fp.Simulation.download_precomputed("p3disoj")
12
+ return fp.Simulation(output_dir=f"/tmp/p3disoj")
13
+
14
+
15
+ def test_interpolacion_1d_point(sim):
16
+ data = sim.load_field(
17
+ fields="gasdens",
18
+ slice="phi=0,theta=1.56",
19
+ snapshot=(1, 2),
20
+ interpolate=True,
21
+ )
22
+ x = 1.2
23
+ valor = data.evaluate(1.2, var1=x)
24
+ # If it returns a 0-d array or a scalar, both are acceptable; it must be finite
25
+ assert np.isfinite(valor).all(), "1D interpolation must return a finite value"
26
+
27
+
28
+ def test_interpolacion_1d_array(sim):
29
+ data = sim.load_field(
30
+ fields="gasdens",
31
+ slice="phi=0,theta=1.56",
32
+ snapshot=(1, 2),
33
+ interpolate=True,
34
+ )
35
+ x = np.array([1.2, 1.3, 1.4])
36
+ valor = data.evaluate(1.2, var1=x)
37
+ assert np.asarray(valor).shape == x.shape, (
38
+ "1D interpolation must preserve the input shape"
39
+ )
40
+ assert np.isfinite(valor).any(), (
41
+ "At least some points should be finite if they lie inside the domain"
42
+ )
43
+
44
+
45
+ def test_interpolacion_2d_point(sim):
46
+ data = sim.load_field(
47
+ fields="gasdens",
48
+ slice="theta=1.56",
49
+ snapshot=(1, 2),
50
+ interpolate=True,
51
+ )
52
+ # Conservative point (typically inside the domain)
53
+ x = 1.2
54
+ y = 0.14
55
+ valor = data.evaluate(
56
+ 1.2, var1=x, var2=y, interpolator="griddata", method="nearest"
57
+ )
58
+ assert np.isscalar(valor) or np.asarray(valor).shape == (), (
59
+ "2D point interpolation must return a scalar/0-d value"
60
+ )
61
+ assert np.isfinite(valor), (
62
+ "2D point interpolation must return a finite value (nearest)"
63
+ )
64
+
65
+
66
+ def test_interpolacion_2d_array(sim):
67
+ data = sim.load_field(
68
+ fields="gasdens",
69
+ slice="theta=1.56",
70
+ snapshot=(1, 2),
71
+ interpolate=True,
72
+ )
73
+
74
+ # Your y=[1.3,1.4,1.5] values are very likely outside the domain -> NaNs (linear) or spurious values.
75
+ # Here we use "safe" points and, additionally, nearest-neighbor interpolation to avoid NaNs
76
+ # due to the convex hull limitation.
77
+ x = np.array([0.9, 1.0, 1.1])
78
+ y = np.array([0.05, 0.10, 0.15])
79
+
80
+ valor = data.evaluate(
81
+ 1.2, var1=x, var2=y, interpolator="griddata", method="nearest"
82
+ )
83
+ valor = np.asarray(valor)
84
+
85
+ assert valor.shape == x.shape, (
86
+ "2D interpolation must return an array with the same shape as the input"
87
+ )
88
+ assert np.isfinite(valor).all(), (
89
+ "With nearest and in-domain points, NaNs should not appear"
90
+ )
91
+
92
+
93
+ # FAILING
94
+ def test_interpolacion_3d_point(sim):
95
+ data = sim.load_field(
96
+ fields="gasdens",
97
+ snapshot=(1, 2),
98
+ interpolate=True,
99
+ )
100
+ x, y, z = 1.2, 1.3, 1.4
101
+ valor = data.evaluate(
102
+ 1.2, var1=x, var2=y, var3=z, interpolator="griddata", method="nearest"
103
+ )
104
+ assert np.isscalar(valor) or np.asarray(valor).shape == (), (
105
+ "3D point interpolation must return a scalar/0-d value"
106
+ )
107
+ assert np.isfinite(valor), (
108
+ "3D point interpolation must return a finite value (nearest)"
109
+ )
110
+
111
+
112
+ def test_interpolacion_3d_array(sim):
113
+ data = sim.load_field(
114
+ fields="gasdens",
115
+ snapshot=(1, 2),
116
+ interpolate=True,
117
+ )
118
+ x = np.array([1.2, 1.3, 1.4])
119
+ y = np.array([1.3, 1.4, 1.5])
120
+ z = np.array([0.024, 0.14, 0.2])
121
+
122
+ valor = data.evaluate(
123
+ 1.2, var1=x, var2=y, var3=z, interpolator="griddata", method="nearest"
124
+ )
125
+ valor = np.asarray(valor)
126
+
127
+ assert valor.shape == x.shape, (
128
+ "3D interpolation must return an array with the same shape as the input"
129
+ )
130
+ assert np.isfinite(valor).all(), (
131
+ "With nearest, NaNs should not appear except for extremely out-of-domain points"
132
+ )
@@ -0,0 +1,91 @@
1
+ #!python
2
+ import sys
3
+ import os
4
+ import subprocess
5
+ import argparse
6
+
7
+
8
+ def get_version():
9
+ try:
10
+ try:
11
+ from importlib.metadata import version
12
+ return version("fargopy")
13
+ except ImportError:
14
+ # Fallback for Python < 3.8
15
+ import pkg_resources
16
+ return pkg_resources.get_distribution("fargopy").version
17
+ except Exception:
18
+ # Fallback if package not installed or in development
19
+ try:
20
+ import fargopy.version
21
+ return fargopy.version.version
22
+ except ImportError:
23
+ return "unknown"
24
+
25
+ def run_verify():
26
+ try:
27
+ import fargopy
28
+ v = get_version()
29
+ print(f"fargopy {v} is successfully installed.")
30
+ print(f"Location: {os.path.dirname(fargopy.__file__)}")
31
+ except ImportError:
32
+ print("Error: fargopy is not installed or cannot be imported.")
33
+ sys.exit(1)
34
+
35
+ def run_tests():
36
+ try:
37
+ import fargopy
38
+ package_dir = os.path.dirname(fargopy.__file__)
39
+ test_dir = os.path.join(package_dir, 'tests')
40
+ if not os.path.exists(test_dir):
41
+ print(f"Error: tests directory not found at {test_dir}")
42
+ sys.exit(1)
43
+
44
+ print(f"Running tests in {test_dir}...")
45
+ subprocess.check_call([sys.executable, "-m", "pytest", test_dir])
46
+ except ImportError:
47
+ print("Error: fargopy is not installed.")
48
+ sys.exit(1)
49
+ except subprocess.CalledProcessError as e:
50
+ sys.exit(e.returncode)
51
+
52
+ def main():
53
+ parser = argparse.ArgumentParser(description="fargopy interactive shell and utilities")
54
+ parser.add_argument("--verify", action="store_true", help="Verify installation and show version")
55
+ parser.add_argument("--test", action="store_true", help="Run the distributed tests")
56
+
57
+ # If no arguments are provided, or just unknown args (which arguably should be passed to ipython?
58
+ # but for now let's stick to the requested logic: optional args for cmd, else launch ipython)
59
+ # The issue is that argparse will parse all args. If the user wants to pass args to ipython, this might conflict.
60
+ # However, standard pattern is usually wrapper handles its flags, pass others.
61
+ # But ifargopy historically just wraps ipython.
62
+
63
+ # Let's check if we have specific flags.
64
+ if "--verify" in sys.argv:
65
+ run_verify()
66
+ return
67
+ if "--test" in sys.argv:
68
+ run_tests()
69
+ return
70
+
71
+ # Original behavior: launch IPython
72
+ user_home = os.path.expanduser("~")
73
+ fp_dotdir = os.path.join(user_home, ".fargopy")
74
+
75
+ if not os.path.isdir(fp_dotdir):
76
+ # First time initialization logic
77
+ init_script = "/tmp/ifargopy_initialize.py"
78
+ with open(init_script, "w") as f:
79
+ f.write("import fargopy as fp\n")
80
+ f.write("fp.initialize('configure')\n")
81
+ f.write("print('We have configured fargopy for the first time. Run it again.')\n")
82
+
83
+ subprocess.call(["ipython", "-i", init_script])
84
+ else:
85
+ startup_script = os.path.join(fp_dotdir, "ifargopy.py")
86
+ # Pass all arguments to ipython except the script name itself
87
+ cmd = ["ipython", "-i", startup_script] + sys.argv[1:]
88
+ subprocess.call(cmd)
89
+
90
+ if __name__ == "__main__":
91
+ main()