ladim 2.0.5__py3-none-any.whl → 2.0.7__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/gridforce/ROMS.py CHANGED
@@ -251,10 +251,11 @@ class Forcing:
251
251
 
252
252
  """
253
253
 
254
- def __init__(self, config, grid):
254
+ def __init__(self, config, _):
255
255
 
256
256
  logger.info("Initiating forcing")
257
257
 
258
+ grid = Grid(config)
258
259
  self._grid = grid # Get the grid object, make private?
259
260
  # self.config = config["gridforce"]
260
261
  self.ibm_forcing = config["ibm_forcing"]
@@ -292,7 +293,12 @@ class Forcing:
292
293
  # --------------
293
294
  # prestep = last forcing step < 0
294
295
  #
296
+ self.has_been_initialized = False
297
+ self.steps = steps
298
+ self._files = files
295
299
 
300
+ def _remaining_initialization(self):
301
+ steps = self.steps
296
302
  V = [step for step in steps if step < 0]
297
303
  if V: # Forcing available before start time
298
304
  prestep = max(V)
@@ -335,9 +341,7 @@ class Forcing:
335
341
  else:
336
342
  # No forcing at start, should already be excluded
337
343
  raise SystemExit(3)
338
-
339
- self.steps = steps
340
- self._files = files
344
+ self.has_been_initialized = True
341
345
 
342
346
  # ===================================================
343
347
  @staticmethod
@@ -435,6 +439,9 @@ class Forcing:
435
439
  def update(self, t):
436
440
  """Update the fields to time step t"""
437
441
 
442
+ if not self.has_been_initialized:
443
+ self._remaining_initialization()
444
+
438
445
  # Read from config?
439
446
  interpolate_velocity_in_time = True
440
447
  interpolate_ibm_forcing_in_time = False
ladim/ibms/__init__.py CHANGED
@@ -1,21 +1,26 @@
1
1
  from ..model import Model, Module
2
+ import numpy as np
2
3
 
3
4
 
4
5
  class IBM(Module):
5
- def __init__(self, model: Model):
6
- super().__init__(model)
6
+ pass
7
7
 
8
8
 
9
9
  class LegacyIBM(IBM):
10
- def __init__(self, model: Model, legacy_module, conf):
11
- super().__init__(model)
12
-
10
+ def __init__(self, legacy_module, conf):
13
11
  from ..model import load_class
14
12
  LegacyIbmClass = load_class(legacy_module + '.IBM')
15
13
  self._ibm = LegacyIbmClass(conf)
16
14
 
17
- def update(self):
18
- grid = self.model['grid']
19
- state = self.model['state']
20
- forcing = self.model['forcing']
15
+ def update(self, model: Model):
16
+ grid = model.grid
17
+ state = model.state
18
+
19
+ state.dt = model.solver.step
20
+ state.timestamp = np.int64(model.solver.time).astype('datetime64[s]')
21
+ state.timestep = (
22
+ (model.solver.time - model.solver.start) // model.solver.step
23
+ )
24
+
25
+ forcing = model.forcing
21
26
  self._ibm.update_ibm(grid, state, forcing)
ladim/main.py CHANGED
@@ -29,7 +29,7 @@ def main(config_stream, loglevel=logging.INFO):
29
29
  # Read configuration
30
30
  config = configure(config_stream)
31
31
 
32
- model = Model(config)
32
+ model = Model.from_config(config)
33
33
  model.run()
34
34
  model.close()
35
35
 
ladim/model.py CHANGED
@@ -27,74 +27,72 @@ DEFAULT_MODULES = dict(
27
27
 
28
28
 
29
29
  class Model:
30
- def __init__(self, config):
30
+ """
31
+ The Model class represents the entire simulation model. The different
32
+ submodules control the simulation behaviour. In particular, the solver
33
+ submodule controls the execution flow while the other submodules are
34
+ called once every time step within the main simulation loop.
35
+ """
36
+
37
+ def __init__(
38
+ self, grid: "Grid", forcing: "Forcing", release: "Releaser",
39
+ state: "State", output: "Output", ibm: "IBM", tracker: "Tracker",
40
+ solver: "Solver",
41
+ ):
42
+ self.grid = grid
43
+ self.forcing = forcing
44
+ self.release = release
45
+ self.state = state
46
+ self.output = output
47
+ self.ibm = ibm
48
+ self.tracker = tracker
49
+ self.solver = solver
50
+
51
+ @staticmethod
52
+ def from_config(config: dict) -> "Model":
53
+ """
54
+ Initialize a model class by supplying the configuration parameters
55
+ of each submodule.
56
+
57
+ :param config: Configuration parameters for each submodule
58
+ :return: An initialized Model class
59
+ """
60
+
61
+ # Create new version of the config dict without the 'model' keyword
62
+ def remove_module_key(d: dict):
63
+ return {k: v for k, v in d.items() if k != 'module'}
64
+
65
+ # Initialize modules
31
66
  module_names = (
32
67
  'grid', 'forcing', 'release', 'state', 'output', 'ibm', 'tracker',
33
68
  'solver',
34
69
  )
35
-
36
- self.modules = dict()
70
+ modules = dict()
37
71
  for name in module_names:
38
- self.add_module(name, config.get(name, dict()))
39
-
40
- def add_module(self, name, conf):
41
- module_name = conf.get('module', DEFAULT_MODULES[name])
42
- conf_without_module = {
43
- k: v for k, v in conf.items()
44
- if k != 'module'
45
- }
46
-
47
- cls = load_class(module_name)
48
- self.modules[name] = cls(self, **conf_without_module)
49
-
50
- @property
51
- def grid(self) -> "Grid":
52
- # noinspection PyTypeChecker
53
- return self.modules.get('grid', None)
54
-
55
- @property
56
- def forcing(self) -> "Forcing":
57
- # noinspection PyTypeChecker
58
- return self.modules.get('forcing', None)
59
-
60
- @property
61
- def release(self) -> "Releaser":
62
- # noinspection PyTypeChecker
63
- return self.modules.get('release', None)
72
+ subconf = config.get(name, dict())
73
+ modules[name] = Module.from_config(
74
+ conf=remove_module_key(subconf),
75
+ module=subconf.get('module', DEFAULT_MODULES[name]),
76
+ )
64
77
 
65
- @property
66
- def state(self) -> "State":
67
- # noinspection PyTypeChecker
68
- return self.modules.get('state', None)
69
-
70
- @property
71
- def output(self) -> "Output":
72
- # noinspection PyTypeChecker
73
- return self.modules.get('output', None)
78
+ # Initialize model
79
+ return Model(**modules)
74
80
 
75
81
  @property
76
- def ibm(self) -> "IBM":
77
- # noinspection PyTypeChecker
78
- return self.modules.get('ibm', None)
79
-
80
- @property
81
- def tracker(self) -> "Tracker":
82
- # noinspection PyTypeChecker
83
- return self.modules.get('tracker', None)
84
-
85
- @property
86
- def solver(self) -> "Solver":
87
- # noinspection PyTypeChecker
88
- return self.modules.get('solver', None)
89
-
90
- def __getitem__(self, item):
91
- return self.modules[item]
92
-
93
- def __contains__(self, item):
94
- return item in self.modules
82
+ def modules(self) -> dict:
83
+ return dict(
84
+ grid=self.grid,
85
+ forcing=self.forcing,
86
+ release=self.release,
87
+ state=self.state,
88
+ output=self.output,
89
+ ibm=self.ibm,
90
+ tracker=self.tracker,
91
+ solver=self.solver,
92
+ )
95
93
 
96
94
  def run(self):
97
- self.solver.run()
95
+ self.solver.run(self)
98
96
 
99
97
  def close(self):
100
98
  for m in self.modules.values():
@@ -127,18 +125,19 @@ def load_class(name):
127
125
 
128
126
 
129
127
  class Module:
130
- def __init__(self, model: Model):
131
- self._model = model
132
-
133
- @property
134
- def model(self) -> Model:
135
- return self._model
136
-
137
- @model.setter
138
- def model(self, value: Model):
139
- self._model = value
140
-
141
- def update(self):
128
+ @staticmethod
129
+ def from_config(conf: dict, module: str) -> "Module":
130
+ """
131
+ Initialize a module using a configuration dict.
132
+
133
+ :param conf: The configuration parameters of the module
134
+ :param module: The fully qualified name of the module
135
+ :return: An initialized module
136
+ """
137
+ cls = load_class(module)
138
+ return cls(**conf)
139
+
140
+ def update(self, model: Model):
142
141
  pass
143
142
 
144
143
  def close(self):
ladim/output.py CHANGED
@@ -4,16 +4,14 @@ import numpy as np
4
4
 
5
5
 
6
6
  class Output(Module):
7
- def __init__(self, model: Model):
8
- super().__init__(model)
7
+ pass
9
8
 
10
9
 
11
10
  class RaggedOutput(Output):
12
- def __init__(self, model: Model, variables: dict, file: str, frequency):
11
+ def __init__(self, variables: dict, file: str, frequency):
13
12
  """
14
13
  Writes simulation output to netCDF file in ragged array format
15
14
 
16
- :param model: Parent model
17
15
  :param variables: Simulation variables to include in output, and their formatting
18
16
  :param file: Name of output file, or empty if a diskless dataset is desired
19
17
  :param frequency: Output frequency in seconds. Alternatively, as a two-element
@@ -21,8 +19,6 @@ class RaggedOutput(Output):
21
19
  unit.
22
20
 
23
21
  """
24
- super().__init__(model)
25
-
26
22
  # Convert output format specification from ladim.yaml config to OutputFormat
27
23
  self._formats = {
28
24
  k: OutputFormat.from_ladim_conf(v)
@@ -47,7 +43,7 @@ class RaggedOutput(Output):
47
43
  freq_unit = 's'
48
44
  self._write_frequency = np.timedelta64(freq_num, freq_unit).astype('timedelta64[s]').astype('int64')
49
45
 
50
- self._dset = None
46
+ self._dset = None # type: nc.Dataset | None
51
47
  self._num_writes = 0
52
48
  self._last_write_time = np.int64(-4611686018427387904)
53
49
 
@@ -56,47 +52,48 @@ class RaggedOutput(Output):
56
52
  """Returns a handle to the netCDF dataset currently being written to"""
57
53
  return self._dset
58
54
 
59
- def update(self):
55
+ def update(self, model: Model):
60
56
  if self._dset is None:
61
57
  self._create_dset()
62
58
 
63
- self._write_init_vars()
64
- self._write_instance_vars()
59
+ self._write_init_vars(model)
60
+ self._write_instance_vars(model)
65
61
 
66
- def _write_init_vars(self):
62
+ def _write_init_vars(self, model):
67
63
  """
68
64
  Write the initial state of new particles
69
65
  """
70
66
 
71
67
  # Check if there are any new particles
72
68
  part_size = self._dset.dimensions['particle'].size
73
- num_new = self.model.state.released - part_size
69
+ num_new = model.state.released - part_size
74
70
  if num_new == 0:
75
71
  return
76
72
 
77
73
  # Write variable data
78
- idx = self.model.state['pid'] > part_size - 1
79
- pid = self.model.state['pid'][idx]
74
+ idx = model.state['pid'] > part_size - 1
75
+ pid = model.state['pid'][idx]
80
76
  for v in set(self._init_vars) - {'release_time'}:
81
77
  # The idx array is not necessarily monotonically increasing by 1
82
78
  # all the way. We therefore copy the data into a temporary,
83
79
  # continuous array.
84
- data_raw = self.model.state[v][idx]
80
+ data_raw = model.state[v][idx]
85
81
  data = np.zeros(num_new, dtype=data_raw.dtype)
86
82
  data[pid - part_size] = data_raw
87
83
  self._dset.variables[v][part_size:part_size + num_new] = data
88
84
 
89
85
  # Write release time variable
90
- data = np.broadcast_to(self.model.solver.time, shape=(num_new, ))
86
+ data = np.broadcast_to(model.solver.time, shape=(num_new, ))
91
87
  self._dset.variables['release_time'][part_size:part_size + num_new] = data
88
+ self._dset.sync()
92
89
 
93
- def _write_instance_vars(self):
90
+ def _write_instance_vars(self, model):
94
91
  """
95
92
  Write the current state of dynamic varaibles
96
93
  """
97
94
 
98
95
  # Check if this is a write time step
99
- current_time = self.model.solver.time
96
+ current_time = model.solver.time
100
97
  elapsed_since_last_write = current_time - self._last_write_time
101
98
  if elapsed_since_last_write < self._write_frequency:
102
99
  return
@@ -109,16 +106,17 @@ class RaggedOutput(Output):
109
106
 
110
107
  # Write variable values
111
108
  inst_size = self._dset.dimensions['particle_instance'].size
112
- inst_num = self.model.state.size
113
- inst_vars = {k: self.model.state[k] for k in set(self._inst_vars) - {'lat', 'lon'}}
109
+ inst_num = model.state.size
110
+ inst_vars = {k: model.state[k] for k in set(self._inst_vars) - {'lat', 'lon'}}
114
111
  if {'lat', 'lon'}.intersection(self._inst_vars):
115
- x, y = self.model.state['X'], self.model.state['Y']
116
- inst_vars['lon'], inst_vars['lat'] = self.model.grid.xy2ll(x, y)
112
+ x, y = model.state['X'], model.state['Y']
113
+ inst_vars['lon'], inst_vars['lat'] = model.grid.xy2ll(x, y)
117
114
  for name, data in inst_vars.items():
118
115
  self._dset.variables[name][inst_size:inst_size + inst_num] = data
119
116
 
120
117
  # Write particle count
121
118
  self._dset.variables['particle_count'][time_size] = inst_num
119
+ self._dset.sync()
122
120
 
123
121
  def _create_dset(self):
124
122
  default_formats = dict(
@@ -161,6 +159,7 @@ class RaggedOutput(Output):
161
159
  )
162
160
 
163
161
  self._dset.variables['instance_offset'][:] = 0
162
+ self._dset.sync()
164
163
 
165
164
  def close(self):
166
165
  if self._dset is not None:
ladim/release.py CHANGED
@@ -11,13 +11,12 @@ logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
13
  class Releaser(Module):
14
- def __init__(self, model: Model):
15
- super().__init__(model)
14
+ pass
16
15
 
17
16
 
18
17
  class TextFileReleaser(Releaser):
19
18
  def __init__(
20
- self, model: Model, file, colnames: list = None, formats: dict = None,
19
+ self, file, colnames: list = None, formats: dict = None,
21
20
  frequency=(0, 's'), defaults=None,
22
21
  ):
23
22
  """
@@ -25,7 +24,6 @@ class TextFileReleaser(Releaser):
25
24
 
26
25
  The text file must be a whitespace-separated csv file
27
26
 
28
- :param model: Parent model
29
27
  :param file: Release file
30
28
 
31
29
  :param colnames: Column names, if the release file does not contain any
@@ -44,8 +42,6 @@ class TextFileReleaser(Releaser):
44
42
  release.
45
43
  """
46
44
 
47
- super().__init__(model)
48
-
49
45
  # Release file
50
46
  self._csv_fname = file # Path name
51
47
  self._csv_column_names = colnames # Column headers
@@ -60,30 +56,31 @@ class TextFileReleaser(Releaser):
60
56
  # Other parameters
61
57
  self._defaults = defaults or dict()
62
58
 
63
- def update(self):
64
- self._add_new()
65
- self._kill_old()
59
+ def update(self, model: Model):
60
+ self._add_new(model)
61
+ self._kill_old(model)
66
62
 
67
- def _kill_old(self):
68
- state = self.model.state
63
+ # noinspection PyMethodMayBeStatic
64
+ def _kill_old(self, model: Model):
65
+ state = model.state
69
66
  if 'alive' in state:
70
67
  alive = state['alive']
71
- alive &= self.model.grid.ingrid(state['X'], state['Y'])
68
+ alive &= model.grid.ingrid(state['X'], state['Y'])
72
69
  state.remove(~alive)
73
70
 
74
- def _add_new(self):
71
+ def _add_new(self, model: Model):
75
72
  # Get the portion of the release dataset that corresponds to
76
73
  # current simulation time
77
74
  df = release_data_subset(
78
75
  dataframe=self.dataframe,
79
- start_time=self.model.solver.time,
80
- stop_time=self.model.solver.time + self.model.solver.step,
76
+ start_time=model.solver.time,
77
+ stop_time=model.solver.time + model.solver.step,
81
78
  ).copy(deep=True)
82
79
 
83
80
  # If there are no new particles, but the state is empty, we should
84
81
  # still initialize the state by adding the appropriate columns
85
- if (len(df) == 0) and ('X' not in self.model.state):
86
- self.model.state.append(df.to_dict(orient='list'))
82
+ if (len(df) == 0) and ('X' not in model.state):
83
+ model.state.append(df.to_dict(orient='list'))
87
84
  self._last_release_dataframe = df
88
85
 
89
86
  # If there are no new particles and we don't use continuous release,
@@ -94,14 +91,14 @@ class TextFileReleaser(Releaser):
94
91
 
95
92
  # If we have continuous release, but there are no new particles and
96
93
  # the last release is recent, we are also done
97
- current_time = self.model.solver.time
94
+ current_time = model.solver.time
98
95
  elapsed_since_last_write = current_time - self._last_release_time
99
96
  last_release_is_recent = (elapsed_since_last_write < self._frequency)
100
97
  if continuous_release and (len(df) == 0) and last_release_is_recent:
101
98
  return
102
99
 
103
100
  # If we are at the final time step, we should not release any more particles
104
- if continuous_release and self.model.solver.time >= self.model.solver.stop:
101
+ if continuous_release and model.solver.time >= model.solver.stop:
105
102
  return
106
103
 
107
104
  # If we have continuous release, but there are no new particles and
@@ -119,7 +116,7 @@ class TextFileReleaser(Releaser):
119
116
  logger.critical("Particle release must have position")
120
117
  raise ValueError()
121
118
  # else
122
- X, Y = self.model.grid.ll2xy(df["lon"].values, df["lat"].values)
119
+ X, Y = model.grid.ll2xy(df["lon"].values, df["lat"].values)
123
120
  df.rename(columns=dict(lon="X", lat="Y"), inplace=True)
124
121
  df["X"] = X
125
122
  df["Y"] = Y
@@ -136,7 +133,7 @@ class TextFileReleaser(Releaser):
136
133
 
137
134
  # Add new particles
138
135
  new_particles = df.to_dict(orient='list')
139
- state = self.model.state
136
+ state = model.state
140
137
  state.append(new_particles)
141
138
 
142
139
  @property
ladim/solver.py CHANGED
@@ -2,9 +2,8 @@ import numpy as np
2
2
 
3
3
 
4
4
  class Solver:
5
- def __init__(self, modules, start, stop, step, order=None, seed=None):
5
+ def __init__(self, start, stop, step, order=None, seed=None):
6
6
  self.order = order or ('release', 'forcing', 'tracker', 'ibm', 'output')
7
- self.modules = modules
8
7
  self.start = np.datetime64(start, 's').astype('int64')
9
8
  self.stop = np.datetime64(stop, 's').astype('int64')
10
9
  self.step = np.timedelta64(step, 's').astype('int64')
@@ -13,11 +12,12 @@ class Solver:
13
12
  if seed is not None:
14
13
  np.random.seed(seed)
15
14
 
16
- def run(self):
17
- modules = [self.modules[k] for k in self.order if k in self.modules]
15
+ def run(self, model):
16
+ modules = model.modules
17
+ ordered_modules = [modules[k] for k in self.order if k in modules]
18
18
 
19
19
  self.time = self.start
20
20
  while self.time <= self.stop:
21
- for m in modules:
22
- m.update()
21
+ for m in ordered_modules:
22
+ m.update(model)
23
23
  self.time += self.step
ladim/state.py CHANGED
@@ -4,19 +4,13 @@ from .model import Model, Module
4
4
 
5
5
 
6
6
  class State(Module):
7
- def __init__(self, model: Model):
8
- """
9
- The state module contains static and dynamic particle properties
10
-
11
- The other modules interact with the state module mostly through
12
- the getitem and setitem methods. For instance, to increase the
13
- depth of all particles by 1, use
14
-
15
- >>> model.state['Z'] += 1
7
+ """
8
+ The state module contains static and dynamic particle properties
16
9
 
17
- :param model: Parent model
18
- """
19
- super().__init__(model)
10
+ The other modules interact with the state module mostly through
11
+ the getitem and setitem methods. For instance, to increase the
12
+ depth of all particles by 1, use state['Z'] += 1
13
+ """
20
14
 
21
15
  @property
22
16
  def size(self):
@@ -65,12 +59,9 @@ class State(Module):
65
59
 
66
60
 
67
61
  class DynamicState(State):
68
- def __init__(self, model: Model):
69
- super().__init__(model)
70
-
62
+ def __init__(self):
71
63
  self._num_released = 0
72
64
  self._varnames = set()
73
-
74
65
  self._data = pd.DataFrame()
75
66
 
76
67
  @property
@@ -123,7 +114,11 @@ class DynamicState(State):
123
114
  return self[item]
124
115
 
125
116
  def __setattr__(self, item, value):
126
- if item in list(self.__dict__.keys()) + ['_data', '_model', '_num_released', '_varnames']:
117
+ excepted_values = [
118
+ '_data', '_model', '_num_released', '_varnames', 'dt', 'timestep',
119
+ 'timestamp'
120
+ ]
121
+ if item in list(self.__dict__.keys()) + excepted_values:
127
122
  super().__setattr__(item, value)
128
123
  elif item in self._data:
129
124
  self._data[item] = value
@@ -132,19 +127,3 @@ class DynamicState(State):
132
127
 
133
128
  def __contains__(self, item):
134
129
  return item in self._data
135
-
136
- @property
137
- def dt(self):
138
- """Backwards-compatibility function for returning model.solver.step"""
139
- return self.model.solver.step
140
-
141
- @property
142
- def timestamp(self):
143
- """Backwards-compatibility function for returning solver time as numpy datetime"""
144
- return np.int64(self.model.solver.time).astype('datetime64[s]')
145
-
146
- @property
147
- def timestep(self):
148
- """Backwards-compatibility function for returning solver time as timestep"""
149
- elapsed = self.model.solver.time - self.model.solver.start
150
- return elapsed // self.model.solver.step
ladim/tracker.py CHANGED
@@ -3,28 +3,25 @@ import numpy as np
3
3
 
4
4
 
5
5
  class Tracker(Module):
6
- def __init__(self, model: Model):
7
- super().__init__(model)
6
+ pass
8
7
 
9
8
 
10
9
  class HorizontalTracker:
11
10
  """The physical particle tracking kernel"""
12
11
 
13
- def __init__(self, model: Model, method, diffusion) -> None:
14
- self.model = model
15
-
12
+ def __init__(self, method, diffusion) -> None:
16
13
  if not diffusion:
17
14
  method += "_nodiff"
18
15
  self.integrator = StochasticDifferentialEquationIntegrator.from_keyword(method)
19
16
  self.D = diffusion # [m2.s-1]
20
17
 
21
- def update(self):
22
- state = self.model.state
23
- grid = self.model.grid
24
- forcing = self.model.forcing
18
+ def update(self, model: Model):
19
+ state = model.state
20
+ grid = model.grid
21
+ forcing = model.forcing
25
22
 
26
- t0 = self.model.solver.time
27
- dt = self.model.solver.step
23
+ t0 = model.solver.time
24
+ dt = model.solver.step
28
25
 
29
26
  act = state['active']
30
27
  X, Y, Z = state['X'][act], state['Y'][act], state['Z'][act]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ladim
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
4
  Summary: Lagrangian Advection and Diffusion Model
5
5
  Home-page: https://github.com/pnsaevik/ladim
6
6
  Author: Bjørn Ådlandsvik
@@ -17,12 +17,14 @@ Classifier: Operating System :: OS Independent
17
17
  Requires-Python: >=3.7
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: numpy
21
- Requires-Dist: pyyaml
22
20
  Requires-Dist: netCDF4
21
+ Requires-Dist: numpy
23
22
  Requires-Dist: pandas
24
- Requires-Dist: xarray
25
23
  Requires-Dist: pyarrow
24
+ Requires-Dist: pyproj
25
+ Requires-Dist: pyyaml
26
+ Requires-Dist: scipy
27
+ Requires-Dist: xarray
26
28
 
27
29
  LADiM – the Lagrangian Advection and Diffusion Model
28
30
  ====================================================