ladim 1.3.4__py3-none-any.whl → 2.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.
ladim/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = '1.3.4'
1
+ __version__ = '2.0.0'
2
2
 
3
3
  from .main import main, run
ladim/config.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ Functions for parsing configuration parameters.
3
+
4
+ The module contains functions for parsing input configuration
5
+ parameters, appending default values and converting between
6
+ different versions of config file formats.
7
+ """
8
+ import numpy as np
9
+
10
+
11
+ def configure(module_conf):
12
+ import yaml
13
+
14
+ # Handle variations of input config type
15
+ if isinstance(module_conf, dict):
16
+ config_dict = module_conf
17
+ else:
18
+ config_dict = yaml.safe_load(module_conf)
19
+
20
+ if 'version' not in config_dict:
21
+ if 'particle_release' in config_dict:
22
+ config_dict['version'] = 1
23
+ else:
24
+ config_dict['version'] = 2
25
+
26
+ return _versioned_configure(config_dict)
27
+
28
+
29
+ def _versioned_configure(config_dict):
30
+ if config_dict['version'] == 1:
31
+ config_dict = _convert_1_to_2(config_dict)
32
+
33
+ return config_dict
34
+
35
+
36
+ def _convert_1_to_2(c):
37
+ # Read timedelta
38
+ dt_value, dt_unit = c['numerics']['dt']
39
+ dt_sec = np.timedelta64(dt_value, dt_unit).astype('timedelta64[s]').astype('int64')
40
+
41
+ # Read output variables
42
+ outvars = dict()
43
+ outvar_names = c['output_variables'].get('particle', []) + c['output_variables'].get('instance', [])
44
+ for v in outvar_names:
45
+ outvars[v] = c['output_variables'][v].copy()
46
+ for v in c['output_variables'].get('particle', []):
47
+ outvars[v]['kind'] = 'initial'
48
+ if 'release_time' in outvars and 'units' in outvars['release_time']:
49
+ outvars['release_time']['units'] = 'seconds since 1970-01-01'
50
+
51
+ # Read release config
52
+ relconf = dict(
53
+ file=c['files']['particle_release_file'],
54
+ frequency=c['particle_release'].get('release_frequency', [0, 's']),
55
+ colnames=c['particle_release']['variables'],
56
+ formats={
57
+ c['particle_release'][v]: v
58
+ for v in c['particle_release']['variables']
59
+ if v in c['particle_release'].keys()
60
+ },
61
+ )
62
+ if c['particle_release'].get('release_type', '') != 'continuous':
63
+ del relconf['frequency']
64
+ ibmvars = c.get('state', dict()).get('ibm_variables', [])
65
+ ibmvars += c.get('ibm', dict()).get('variables', [])
66
+ relconf['defaults'] = {
67
+ k: np.float64(0)
68
+ for k in ibmvars
69
+ if k not in relconf['colnames']
70
+ }
71
+
72
+ # Read ibm config
73
+ ibmconf_legacy = c.get('ibm', dict()).copy()
74
+ if 'module' in ibmconf_legacy:
75
+ ibmconf_legacy['ibm_module'] = ibmconf_legacy.pop('module')
76
+ ibmconf = dict()
77
+ if 'ibm_module' in ibmconf_legacy:
78
+ ibmconf['module'] = 'ladim.ibms.LegacyIBM'
79
+ ibmconf['legacy_module'] = ibmconf_legacy['ibm_module']
80
+ ibmconf['conf'] = dict(
81
+ dt=dt_sec,
82
+ output_instance=c.get('output_variables', {}).get('instance', []),
83
+ nc_attributes={k: v for k, v in outvars.items()}
84
+ )
85
+ ibmconf['conf']['ibm'] = {
86
+ k: v
87
+ for k, v in ibmconf_legacy.items()
88
+ if k != 'ibm_module'
89
+ }
90
+
91
+ config_dict = dict(
92
+ version=2,
93
+ solver=dict(
94
+ start=c['time_control']['start_time'],
95
+ stop=c['time_control']['stop_time'],
96
+ step=dt_sec,
97
+ seed=c['numerics'].get('seed', None),
98
+ order=['release', 'forcing', 'output', 'tracker', 'ibm', 'state'],
99
+ ),
100
+ grid=dict(
101
+ file=c['gridforce']['input_file'],
102
+ legacy_module=c['gridforce']['module'] + '.Grid',
103
+ start_time=np.datetime64(c['time_control']['start_time'], 's'),
104
+ ),
105
+ forcing=dict(
106
+ file=c['gridforce']['input_file'],
107
+ legacy_module=c['gridforce']['module'] + '.Forcing',
108
+ start_time=np.datetime64(c['time_control']['start_time'], 's'),
109
+ stop_time=np.datetime64(c['time_control']['stop_time'], 's'),
110
+ dt=dt_sec,
111
+ ibm_forcing=c['gridforce'].get('ibm_forcing', []),
112
+ ),
113
+ release=relconf,
114
+ output=dict(
115
+ file=c['files']['output_file'],
116
+ frequency=c['output_variables']['outper'],
117
+ variables=outvars,
118
+ ),
119
+ tracker=dict(
120
+ method=c['numerics']['advection'],
121
+ diffusion=c['numerics']['diffusion'],
122
+ ),
123
+ ibm=ibmconf,
124
+ )
125
+ return config_dict
ladim/forcing.py ADDED
@@ -0,0 +1,95 @@
1
+ from .model import Model, Module
2
+
3
+
4
+ class Forcing(Module):
5
+ def __init__(self, model: Model):
6
+ super().__init__(model)
7
+
8
+ def velocity(self, X, Y, Z, tstep=0.0):
9
+ raise NotImplementedError
10
+
11
+
12
+ class RomsForcing(Forcing):
13
+ def __init__(self, model: Model, file, variables=None, **conf):
14
+ """
15
+ Forcing module which uses output data from the ROMS ocean model
16
+
17
+ :param model: Parent model
18
+ :param file: Glob pattern for the input files
19
+ :param variables: A mapping of variable names to interpolation
20
+ specifications. Each interpolaction specification consists of 0-4
21
+ of the letters "xyzt". Coordinates that are listed in the string are
22
+ interpolated linearly, while the remaining ones use nearest-neighbor
23
+ interpolation. Some default configurations are defined:
24
+
25
+ .. code-block:: json
26
+ {
27
+ "temp": "xyzt",
28
+ "salt": "xyzt",
29
+ "u": "xt",
30
+ "v": "yt",
31
+ "w": "zt",
32
+ }
33
+
34
+
35
+ :param conf: Legacy config dict
36
+ """
37
+ super().__init__(model)
38
+
39
+ # Apply default interpolation configs
40
+ variables = variables or dict()
41
+ default_vars = dict(u="xt", v="yt", w="zt", temp="xyzt", salt="xyzt")
42
+ self.variables = {**default_vars, **variables}
43
+
44
+ grid_ref = GridReference(model)
45
+ legacy_conf = dict(
46
+ gridforce=dict(
47
+ input_file=file,
48
+ ),
49
+ ibm_forcing=conf.get('ibm_forcing', []),
50
+ start_time=conf.get('start_time', None),
51
+ stop_time=conf.get('stop_time', None),
52
+ dt=conf.get('dt', None),
53
+ )
54
+
55
+ from .model import load_class
56
+ LegacyForcing = load_class(conf.get('legacy_module', 'ladim.gridforce.ROMS.Forcing'))
57
+
58
+ # Allow gridforce module in current directory
59
+ import sys
60
+ import os
61
+ sys.path.insert(0, os.getcwd())
62
+ # Import correct gridforce_module
63
+ self.forcing = LegacyForcing(legacy_conf, grid_ref)
64
+ # self.steps = self.forcing.steps
65
+ # self.U = self.forcing.U
66
+ # self.V = self.forcing.V
67
+
68
+ def update(self):
69
+ elapsed = self.model.solver.time - self.model.solver.start
70
+ t = elapsed // self.model.solver.step
71
+
72
+ self.forcing.update(t)
73
+
74
+ # Update state variables by sampling the field
75
+ x, y, z = self.model.state['X'], self.model.state['Y'], self.model.state['Z']
76
+ for v in self.variables:
77
+ if v in self.model.state:
78
+ self.model.state[v] = self.field(x, y, z, v)
79
+
80
+ def velocity(self, X, Y, Z, tstep=0.0):
81
+ return self.forcing.velocity(X, Y, Z, tstep=tstep)
82
+
83
+ def field(self, X, Y, Z, name):
84
+ return self.forcing.field(X, Y, Z, name)
85
+
86
+ def close(self):
87
+ return self.forcing.close()
88
+
89
+
90
+ class GridReference:
91
+ def __init__(self, modules: Model):
92
+ self.modules = modules
93
+
94
+ def __getattr__(self, item):
95
+ return getattr(self.modules.grid.grid, item)
ladim/grid.py ADDED
@@ -0,0 +1,79 @@
1
+ from .model import Model, Module
2
+
3
+
4
+ class Grid(Module):
5
+ def __init__(self, model: Model):
6
+ super().__init__(model)
7
+
8
+ def ingrid(self, X, Y):
9
+ raise NotImplementedError
10
+
11
+ def sample_metric(self, X, Y):
12
+ raise NotImplementedError
13
+
14
+ def atsea(self, X, Y):
15
+ raise NotImplementedError
16
+
17
+ def ll2xy(self, lon, lat):
18
+ raise NotImplementedError
19
+
20
+ def xy2ll(self, x, y):
21
+ raise NotImplementedError
22
+
23
+
24
+ class RomsGrid(Grid):
25
+ def __init__(self, model: Model, **conf):
26
+ super().__init__(model)
27
+
28
+ legacy_conf = dict(
29
+ gridforce=dict(
30
+ input_file=conf['file'],
31
+ ),
32
+ start_time=conf.get('start_time', None),
33
+ )
34
+
35
+ from .model import load_class
36
+ LegacyGrid = load_class(conf.get('legacy_module', 'ladim.gridforce.ROMS.Grid'))
37
+
38
+ # Allow gridforce module in current directory
39
+ import sys
40
+ import os
41
+ sys.path.insert(0, os.getcwd())
42
+ # Import correct gridforce_module
43
+ # gridforce_module = importlib.import_module(config["gridforce_module"])
44
+ self.grid = LegacyGrid(legacy_conf)
45
+ self.xmin = self.grid.xmin
46
+ self.xmax = self.grid.xmax
47
+ self.ymin = self.grid.ymin
48
+ self.ymax = self.grid.ymax
49
+
50
+ def sample_metric(self, X, Y):
51
+ """Sample the metric coefficients"""
52
+ return self.grid.sample_metric(X, Y)
53
+
54
+ def sample_depth(self, X, Y):
55
+ """Return the depth of grid cells"""
56
+ return self.grid.sample_depth(X, Y)
57
+
58
+ def lonlat(self, X, Y, method=None):
59
+ """Return the longitude and latitude from grid coordinates"""
60
+ return self.grid.lonlat(X, Y, method=method)
61
+
62
+ def ingrid(self, X, Y):
63
+ """Returns True for points inside the subgrid"""
64
+ return self.grid.ingrid(X, Y)
65
+
66
+ def onland(self, X, Y):
67
+ """Returns True for points on land"""
68
+ return self.grid.onland(X, Y)
69
+
70
+ # Error if point outside
71
+ def atsea(self, X, Y):
72
+ """Returns True for points at sea"""
73
+ return self.grid.atsea(X, Y)
74
+
75
+ def ll2xy(self, lon, lat):
76
+ return self.grid.ll2xy(lon, lat)
77
+
78
+ def xy2ll(self, X, Y):
79
+ return self.grid.xy2ll(X, Y)
ladim/gridforce/ROMS.py CHANGED
@@ -168,6 +168,10 @@ class Grid:
168
168
  I = X.round().astype(int) - self.i0
169
169
  J = Y.round().astype(int) - self.j0
170
170
 
171
+ # Constrain to valid indices
172
+ I = np.minimum(np.maximum(I, 0), self.dx.shape[-1] - 2)
173
+ J = np.minimum(np.maximum(J, 0), self.dx.shape[-2] - 2)
174
+
171
175
  # Metric is conform for PolarStereographic
172
176
  A = self.dx[J, I]
173
177
  return A, A
@@ -207,6 +211,11 @@ class Grid:
207
211
  """Returns True for points at sea"""
208
212
  I = X.round().astype(int) - self.i0
209
213
  J = Y.round().astype(int) - self.j0
214
+
215
+ # Constrain to valid indices
216
+ I = np.minimum(np.maximum(I, 0), self.M.shape[-1] - 1)
217
+ J = np.minimum(np.maximum(J, 0), self.M.shape[-2] - 1)
218
+
210
219
  return self.M[J, I] > 0
211
220
 
212
221
  def xy2ll(self, X, Y):
@@ -239,6 +248,10 @@ class Forcing:
239
248
  # self.config = config["gridforce"]
240
249
  self.ibm_forcing = config["ibm_forcing"]
241
250
 
251
+ config = config.copy()
252
+ config["start_time"] = np.datetime64(config["start_time"])
253
+ config["stop_time"] = np.datetime64(config["stop_time"])
254
+
242
255
  files = self.find_files(config["gridforce"])
243
256
  numfiles = len(files)
244
257
  if numfiles == 0:
@@ -319,12 +332,17 @@ class Forcing:
319
332
  @staticmethod
320
333
  def find_files(force_config):
321
334
  """Find (and sort) the forcing file(s)"""
322
- files = glob.glob(force_config["input_file"])
335
+
336
+ # Use unix-style filenames to provide consistency between windows and linux
337
+ first_file = force_config.get("first_file", "").replace("\\", "/")
338
+ last_file = force_config.get("last_file", "").replace("\\", "/")
339
+ files = [f.replace("\\", "/") for f in glob.glob(force_config["input_file"])]
340
+
323
341
  files.sort()
324
- if force_config.get("first_file", None):
325
- files = [f for f in files if f >= force_config["first_file"]]
326
- if force_config.get("last_file", None):
327
- files = [f for f in files if f <= force_config["last_file"]]
342
+ if first_file:
343
+ files = [f for f in files if f >= first_file]
344
+ if last_file:
345
+ files = [f for f in files if f <= last_file]
328
346
  return files
329
347
 
330
348
  @staticmethod
@@ -713,6 +731,10 @@ def z2s(z_rho, X, Y, Z):
713
731
  I = np.around(X).astype("int")
714
732
  J = np.around(Y).astype("int")
715
733
 
734
+ # Constrain to valid indices
735
+ I = np.minimum(np.maximum(I, 0), z_rho.shape[-1] - 1)
736
+ J = np.minimum(np.maximum(J, 0), z_rho.shape[-2] - 1)
737
+
716
738
  # Vectorized searchsorted
717
739
  K = np.sum(z_rho[:, J, I] < -Z, axis=0)
718
740
  K = K.clip(1, kmax - 1)
@@ -747,6 +769,11 @@ def sample3D(F, X, Y, K, A, method="bilinear"):
747
769
  # Find rho-point as lower left corner
748
770
  I = X.astype("int")
749
771
  J = Y.astype("int")
772
+
773
+ # Constrain to valid indices
774
+ I = np.minimum(np.maximum(I, 0), F.shape[-1] - 2)
775
+ J = np.minimum(np.maximum(J, 0), F.shape[-2] - 2)
776
+
750
777
  P = X - I
751
778
  Q = Y - J
752
779
  W000 = (1 - P) * (1 - Q) * (1 - A)
@@ -1 +0,0 @@
1
- from .legacy import Grid, Forcing
ladim/ibms/__init__.py CHANGED
@@ -1,6 +1,21 @@
1
- class IBM:
2
- def __init__(self, *_, **__):
3
- pass
1
+ from ..model import Model, Module
2
+
3
+
4
+ class IBM(Module):
5
+ def __init__(self, model: Model):
6
+ super().__init__(model)
7
+
8
+
9
+ class LegacyIBM(IBM):
10
+ def __init__(self, model: Model, legacy_module, conf):
11
+ super().__init__(model)
12
+
13
+ from ..model import load_class
14
+ LegacyIbmClass = load_class(legacy_module + '.IBM')
15
+ self._ibm = LegacyIbmClass(conf)
4
16
 
5
17
  def update(self):
6
- pass
18
+ grid = self.model['grid']
19
+ state = self.model['state']
20
+ forcing = self.model['forcing']
21
+ self._ibm.update_ibm(grid, state, forcing)
ladim/main.py CHANGED
@@ -15,7 +15,7 @@ import logging
15
15
 
16
16
  import ladim
17
17
 
18
- from .configuration import configure
18
+ from .config import configure
19
19
  from .model import Model
20
20
 
21
21
 
ladim/model.py CHANGED
@@ -1,22 +1,25 @@
1
1
  import importlib
2
2
 
3
- import ladim.gridforce
4
- import ladim.ibms
5
- import ladim.output
6
- import ladim.release
7
- import ladim.state
8
- import ladim.tracker
9
- import ladim.timestepper
3
+ from typing import TYPE_CHECKING
4
+ if TYPE_CHECKING:
5
+ from ladim.grid import Grid
6
+ from ladim.forcing import Forcing
7
+ from ladim.ibms import IBM
8
+ from ladim.output import Output
9
+ from ladim.release import Releaser
10
+ from ladim.state import State
11
+ from ladim.tracker import Tracker
12
+ from ladim.solver import Solver
10
13
 
11
14
  DEFAULT_MODULES = dict(
12
- grid='ladim.gridforce.Grid',
13
- forcing='ladim.gridforce.Forcing',
14
- release='ladim.release.Releaser',
15
- state='ladim.state.State',
16
- output='ladim.output.Output',
15
+ grid='ladim.grid.RomsGrid',
16
+ forcing='ladim.forcing.RomsForcing',
17
+ release='ladim.release.TextFileReleaser',
18
+ state='ladim.state.DynamicState',
19
+ output='ladim.output.RaggedOutput',
17
20
  ibm='ladim.ibms.IBM',
18
- tracker='ladim.tracker.Tracker',
19
- timestepper='ladim.timestepper.TimeStepper',
21
+ tracker='ladim.tracker.HorizontalTracker',
22
+ solver='ladim.solver.Solver',
20
23
  )
21
24
 
22
25
 
@@ -24,49 +27,62 @@ class Model:
24
27
  def __init__(self, config):
25
28
  module_names = (
26
29
  'grid', 'forcing', 'release', 'state', 'output', 'ibm', 'tracker',
27
- 'timestepper',
30
+ 'solver',
28
31
  )
29
32
 
30
33
  self.modules = dict()
31
34
  for name in module_names:
32
- self.add_module(name, config[name])
35
+ self.add_module(name, config.get(name, dict()))
33
36
 
34
37
  def add_module(self, name, conf):
35
38
  module_name = conf.get('module', DEFAULT_MODULES[name])
36
- Module = load_module(module_name)
37
- self.modules[name] = Module(self, **conf)
39
+ conf_without_module = {
40
+ k: v for k, v in conf.items()
41
+ if k != 'module'
42
+ }
43
+
44
+ cls = load_class(module_name)
45
+ self.modules[name] = cls(self, **conf_without_module)
38
46
 
39
47
  @property
40
- def grid(self) -> ladim.gridforce.Grid:
48
+ def grid(self) -> "Grid":
49
+ # noinspection PyTypeChecker
41
50
  return self.modules.get('grid', None)
42
51
 
43
52
  @property
44
- def forcing(self) -> ladim.gridforce.Forcing:
53
+ def forcing(self) -> "Forcing":
54
+ # noinspection PyTypeChecker
45
55
  return self.modules.get('forcing', None)
46
56
 
47
57
  @property
48
- def release(self) -> ladim.release.Releaser:
58
+ def release(self) -> "Releaser":
59
+ # noinspection PyTypeChecker
49
60
  return self.modules.get('release', None)
50
61
 
51
62
  @property
52
- def state(self) -> ladim.state.State:
63
+ def state(self) -> "State":
64
+ # noinspection PyTypeChecker
53
65
  return self.modules.get('state', None)
54
66
 
55
67
  @property
56
- def output(self) -> ladim.output.Output:
68
+ def output(self) -> "Output":
69
+ # noinspection PyTypeChecker
57
70
  return self.modules.get('output', None)
58
71
 
59
72
  @property
60
- def ibm(self) -> ladim.ibms.IBM:
73
+ def ibm(self) -> "IBM":
74
+ # noinspection PyTypeChecker
61
75
  return self.modules.get('ibm', None)
62
76
 
63
77
  @property
64
- def tracker(self) -> ladim.tracker.Tracker:
78
+ def tracker(self) -> "Tracker":
79
+ # noinspection PyTypeChecker
65
80
  return self.modules.get('tracker', None)
66
81
 
67
82
  @property
68
- def timestepper(self) -> ladim.timestepper.TimeStepper:
69
- return self.modules.get('timestepper', None)
83
+ def solver(self) -> "Solver":
84
+ # noinspection PyTypeChecker
85
+ return self.modules.get('solver', None)
70
86
 
71
87
  def __getitem__(self, item):
72
88
  return self.modules[item]
@@ -75,7 +91,7 @@ class Model:
75
91
  return item in self.modules
76
92
 
77
93
  def run(self):
78
- self.timestepper.run()
94
+ self.solver.run()
79
95
 
80
96
  def close(self):
81
97
  for m in self.modules.values():
@@ -83,6 +99,25 @@ class Model:
83
99
  m.close()
84
100
 
85
101
 
86
- def load_module(name):
102
+ def load_class(name):
87
103
  pkg, cls = name.rsplit(sep='.', maxsplit=1)
88
104
  return getattr(importlib.import_module(pkg), cls)
105
+
106
+
107
+ class Module:
108
+ def __init__(self, model: Model):
109
+ self._model = model
110
+
111
+ @property
112
+ def model(self) -> Model:
113
+ return self._model
114
+
115
+ @model.setter
116
+ def model(self, value: Model):
117
+ self._model = value
118
+
119
+ def update(self):
120
+ pass
121
+
122
+ def close(self):
123
+ pass