pinnx 0.0.3__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.
Files changed (53) hide show
  1. pinnx/__init__.py +42 -0
  2. pinnx/_trainer.py +511 -0
  3. pinnx/callbacks.py +574 -0
  4. pinnx/fnspace.py +367 -0
  5. pinnx/geometry/__init__.py +45 -0
  6. pinnx/geometry/base.py +662 -0
  7. pinnx/geometry/geometry_1d.py +192 -0
  8. pinnx/geometry/geometry_2d.py +1018 -0
  9. pinnx/geometry/geometry_3d.py +181 -0
  10. pinnx/geometry/geometry_nd.py +322 -0
  11. pinnx/geometry/pointcloud.py +97 -0
  12. pinnx/geometry/timedomain.py +175 -0
  13. pinnx/grad.py +370 -0
  14. pinnx/grad_test.py +16 -0
  15. pinnx/icbc/__init__.py +29 -0
  16. pinnx/icbc/base.py +49 -0
  17. pinnx/icbc/boundary_conditions.py +439 -0
  18. pinnx/icbc/initial_conditions.py +89 -0
  19. pinnx/metrics.py +117 -0
  20. pinnx/nn/__init__.py +34 -0
  21. pinnx/nn/base.py +41 -0
  22. pinnx/nn/convert.py +129 -0
  23. pinnx/nn/deeponet.py +336 -0
  24. pinnx/nn/deeponet_strategy.py +158 -0
  25. pinnx/nn/fnn.py +173 -0
  26. pinnx/nn/mionet.py +243 -0
  27. pinnx/nn/model.py +114 -0
  28. pinnx/problem/__init__.py +33 -0
  29. pinnx/problem/base.py +122 -0
  30. pinnx/problem/dataset_function.py +79 -0
  31. pinnx/problem/dataset_general.py +66 -0
  32. pinnx/problem/dataset_mf.py +148 -0
  33. pinnx/problem/dataset_quadruple.py +129 -0
  34. pinnx/problem/dataset_triple.py +139 -0
  35. pinnx/problem/fpde.py +745 -0
  36. pinnx/problem/ide.py +149 -0
  37. pinnx/problem/pde.py +471 -0
  38. pinnx/problem/pde_operator.py +432 -0
  39. pinnx/utils/__init__.py +6 -0
  40. pinnx/utils/_convert.py +70 -0
  41. pinnx/utils/_display.py +109 -0
  42. pinnx/utils/array_ops.py +48 -0
  43. pinnx/utils/external.py +372 -0
  44. pinnx/utils/internal.py +255 -0
  45. pinnx/utils/losses.py +81 -0
  46. pinnx/utils/sampler.py +80 -0
  47. pinnx/utils/sampling.py +1525 -0
  48. pinnx/utils/transformers.py +327 -0
  49. pinnx-0.0.3.dist-info/METADATA +162 -0
  50. pinnx-0.0.3.dist-info/RECORD +53 -0
  51. pinnx-0.0.3.dist-info/WHEEL +5 -0
  52. pinnx-0.0.3.dist-info/licenses/LICENSE +497 -0
  53. pinnx-0.0.3.dist-info/top_level.txt +1 -0
pinnx/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ # Copyright 2024 BrainX Ecosystem Limited. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+
17
+ __version__ = "0.0.3"
18
+ __version_info__ = tuple(map(int, __version__.split(".")))
19
+
20
+ __all__ = [
21
+ "callbacks",
22
+ "fnspace",
23
+ "geometry",
24
+ "grad",
25
+ "icbc",
26
+ "metrics",
27
+ "nn",
28
+ "problem",
29
+ "utils",
30
+ "Trainer",
31
+ ]
32
+
33
+ from . import callbacks
34
+ from . import fnspace
35
+ from . import geometry
36
+ from . import grad
37
+ from . import icbc
38
+ from . import metrics
39
+ from . import nn
40
+ from . import problem
41
+ from . import utils
42
+ from ._trainer import Trainer
pinnx/_trainer.py ADDED
@@ -0,0 +1,511 @@
1
+ # Rewrite of the original file in DeepXDE: https://github.com/lululxvi/deepxde
2
+ # ==============================================================================
3
+
4
+
5
+ import time
6
+ from typing import Union, Sequence, Callable, Optional
7
+
8
+ import braintools
9
+ import brainstate
10
+ import brainunit as u
11
+ import jax.numpy as jnp
12
+ import jax.tree
13
+ import numpy as np
14
+
15
+ from . import metrics as metrics_module
16
+ from . import utils
17
+ from .callbacks import CallbackList, Callback
18
+ from .problem.base import Problem
19
+ from .utils._display import training_display
20
+
21
+ __all__ = [
22
+ "Trainer",
23
+ "TrainState",
24
+ "LossHistory",
25
+ ]
26
+
27
+
28
+ class Trainer:
29
+ """
30
+ A ``Trainer`` trains a neural network on a ``Problem``.
31
+
32
+ Args:
33
+ problem: ``pinnx.problem.Problem`` instance.
34
+ external_trainable_variables: A trainable ``brainstate.ParamState`` object or a list
35
+ of trainable ``brainstate.ParamState`` objects. The unknown parameters in the
36
+ physics systems that need to be recovered.
37
+ """
38
+ __module__ = 'pinnx'
39
+ optimizer: braintools.optim.Optimizer # optimizer
40
+ problem: Problem # problem
41
+ params: brainstate.util.FlattedDict # trainable variables
42
+
43
+ def __init__(
44
+ self,
45
+ problem: Problem,
46
+ external_trainable_variables: Union[brainstate.ParamState, Sequence[brainstate.ParamState]] = None,
47
+ batch_size: Optional[int] = None,
48
+ ):
49
+ # the problem
50
+ self.problem = problem
51
+ assert isinstance(self.problem, Problem), "problem must be a Problem instance."
52
+
53
+ # the approximator
54
+ if self.problem.approximator is None:
55
+ raise ValueError("Problem must define an approximator before training.")
56
+
57
+ # parameters and external trainable variables
58
+ params = brainstate.graph.states(self.problem.approximator, brainstate.ParamState)
59
+ if external_trainable_variables is None:
60
+ external_trainable_variables = []
61
+ else:
62
+ if not isinstance(external_trainable_variables, list):
63
+ external_trainable_variables = [external_trainable_variables]
64
+ for i, var in enumerate(external_trainable_variables):
65
+ assert isinstance(var, brainstate.ParamState), ("external_trainable_variables must be a "
66
+ "list of ParamState instance.")
67
+ params[('external_trainable_variable', i)] = var
68
+ self.params = params
69
+
70
+ # other useful parameters
71
+ self.metrics = None
72
+ self.batch_size = batch_size
73
+
74
+ # training state
75
+ self.train_state = TrainState()
76
+ self.loss_history = LossHistory()
77
+ self.stop_training = False
78
+
79
+ @utils.timing
80
+ def compile(
81
+ self,
82
+ optimizer: braintools.optim.Optimizer,
83
+ metrics: Union[str, Sequence[str]] = None,
84
+ measture_train_step_compile_time: bool = False,
85
+ ):
86
+ """
87
+ Configures the trainer for training.
88
+
89
+ Args:
90
+ optimizer: String name of an optimizer, or an optimizer class instance.
91
+ metrics: List of metrics to be evaluated by the trainer during training.
92
+ """
93
+ print("Compiling trainer...")
94
+
95
+ # optimizer
96
+ assert isinstance(optimizer, braintools.optim.Optimizer), "optimizer must be an Optimizer instance."
97
+ self.optimizer = optimizer
98
+ self.optimizer.register_trainable_weights(self.params)
99
+
100
+ # metrics may use trainer variables such as self.net,
101
+ # and thus are instantiated after compile.
102
+ metrics = metrics or []
103
+ self.metrics = [metrics_module.get(m) for m in metrics]
104
+
105
+ def fn_outputs(training: bool, inputs):
106
+ with brainstate.environ.context(fit=training):
107
+ inputs = jax.tree.map(lambda x: u.math.asarray(x), inputs, is_leaf=u.math.is_quantity)
108
+ return self.problem.approximator(inputs)
109
+
110
+ def fn_outputs_losses(training, inputs, targets, **kwargs):
111
+ with brainstate.environ.context(fit=training):
112
+ # inputs
113
+ inputs = jax.tree.map(lambda x: u.math.asarray(x), inputs, is_leaf=u.math.is_quantity)
114
+
115
+ # outputs
116
+ outputs = self.problem.approximator(inputs)
117
+
118
+ # targets
119
+ if targets is not None:
120
+ targets = jax.tree.map(lambda x: u.math.asarray(x), targets, is_leaf=u.math.is_quantity)
121
+
122
+ # compute losses
123
+ if training:
124
+ losses = self.problem.losses_train(inputs, outputs, targets, **kwargs)
125
+ else:
126
+ losses = self.problem.losses_test(inputs, outputs, targets, **kwargs)
127
+ return outputs, losses
128
+
129
+ def fn_outputs_losses_train(inputs, targets, **aux):
130
+ return fn_outputs_losses(True, inputs, targets, **aux)
131
+
132
+ def fn_outputs_losses_test(inputs, targets, **aux):
133
+ return fn_outputs_losses(False, inputs, targets, **aux)
134
+
135
+ def fn_train_step(inputs, targets, **aux):
136
+ def _loss_fun():
137
+ losses = fn_outputs_losses_train(inputs, targets, **aux)[1]
138
+ return u.math.sum(u.math.asarray([loss.sum() for loss in jax.tree.leaves(losses)]))
139
+
140
+ grads = brainstate.transform.grad(_loss_fun, grad_states=self.params)()
141
+ self.optimizer.update(grads)
142
+
143
+ # Callables
144
+ self.fn_outputs = brainstate.transform.jit(fn_outputs, static_argnums=0)
145
+ self.fn_outputs_losses_train = brainstate.transform.jit(fn_outputs_losses_train)
146
+ self.fn_outputs_losses_test = brainstate.transform.jit(fn_outputs_losses_test)
147
+ self.fn_train_step = brainstate.transform.jit(fn_train_step)
148
+
149
+ if measture_train_step_compile_time:
150
+ t0 = time.time()
151
+ self._compile_training_step(self.batch_size)
152
+ t1 = time.time()
153
+ return self, t1 - t0
154
+
155
+ return self
156
+
157
+ @utils.timing
158
+ def train(
159
+ self,
160
+ iterations: int,
161
+ batch_size: int = None,
162
+ display_every: int = 1000,
163
+ disregard_previous_best: bool = False,
164
+ callbacks: Union[Callback, Sequence[Callback]] = None,
165
+ model_restore_path: str = None,
166
+ model_save_path: str = None,
167
+ measture_train_step_time: bool = False,
168
+ ):
169
+ """
170
+ Trains the trainer.
171
+
172
+ Args:
173
+ iterations (Integer): Number of iterations to train the trainer, i.e., number
174
+ of times the network weights are updated.
175
+ batch_size: Integer, tuple, or ``None``.
176
+
177
+ - If you solve PDEs via ``pinnx.problem.PDE`` or ``pinnx.problem.TimePDE``, do not use `batch_size`,
178
+ and instead use `pinnx.callbacks.PDEPointResampler
179
+ <https://deepxde.readthedocs.io/en/latest/modules/deepxde.html#deepxde.callbacks.PDEPointResampler>`_,
180
+ see an `example <https://github.com/lululxvi/deepxde/blob/master/examples/pinn_forward/diffusion_1d_resample.py>`_.
181
+ - For DeepONet in the format of Cartesian product, if `batch_size` is an Integer,
182
+ then it is the batch size for the branch input;
183
+ if you want to also use mini-batch for the trunk net input,
184
+ set `batch_size` as a tuple, where the fist number is the batch size for the branch net input
185
+ and the second number is the batch size for the trunk net input.
186
+ display_every (Integer): Print the loss and metrics every this steps.
187
+ disregard_previous_best: If ``True``, disregard the previous saved best
188
+ trainer.
189
+ callbacks: List of ``pinnx.callbacks.Callback`` instances. List of callbacks
190
+ to apply during training.
191
+ model_restore_path (String): Path where parameters were previously saved.
192
+ model_save_path (String): Prefix of filenames created for the checkpoint.
193
+ """
194
+
195
+ if measture_train_step_time:
196
+ t0 = time.time()
197
+
198
+ if self.metrics is None:
199
+ raise ValueError("Compile the trainer before training.")
200
+
201
+ # callbacks
202
+ callbacks = CallbackList(callbacks=[callbacks] if isinstance(callbacks, Callback) else callbacks)
203
+ callbacks.set_model(self)
204
+
205
+ # disregard previous best
206
+ if disregard_previous_best:
207
+ self.train_state.disregard_best()
208
+
209
+ # restore
210
+ if model_restore_path is not None:
211
+ self.restore(model_restore_path, verbose=1)
212
+
213
+ print("Training trainer...\n")
214
+ self.stop_training = False
215
+
216
+ # testing
217
+ self.train_state.set_data_train(*self.problem.train_next_batch(batch_size))
218
+ self.train_state.set_data_test(*self.problem.test())
219
+ self._test()
220
+
221
+ # training
222
+ callbacks.on_train_begin()
223
+ self._train(iterations, display_every, batch_size, callbacks)
224
+ callbacks.on_train_end()
225
+
226
+ # summary
227
+ print("")
228
+ training_display.summary(self.train_state)
229
+ if model_save_path is not None:
230
+ self.save(model_save_path, verbose=1)
231
+
232
+ if measture_train_step_time:
233
+ t1 = time.time()
234
+ return self, t1 - t0
235
+ return self
236
+
237
+ def _compile_training_step(self, batch_size=None):
238
+ # get data
239
+ self.train_state.set_data_train(*self.problem.train_next_batch(batch_size))
240
+
241
+ # train one batch
242
+ self.fn_train_step.compile(self.train_state.X_train,
243
+ self.train_state.y_train,
244
+ **self.train_state.Aux_train)
245
+
246
+ def _train(self, iterations, display_every, batch_size, callbacks):
247
+ for i in range(iterations):
248
+ callbacks.on_epoch_begin()
249
+ callbacks.on_batch_begin()
250
+
251
+ # get data
252
+ self.train_state.set_data_train(*self.problem.train_next_batch(batch_size))
253
+
254
+ # train one batch
255
+ self.fn_train_step(self.train_state.X_train, self.train_state.y_train, **self.train_state.Aux_train)
256
+
257
+ self.train_state.epoch += 1
258
+ self.train_state.step += 1
259
+ if self.train_state.step % display_every == 0 or i + 1 == iterations:
260
+ self._test()
261
+
262
+ callbacks.on_batch_end()
263
+ callbacks.on_epoch_end()
264
+
265
+ if self.stop_training:
266
+ break
267
+
268
+ def _test(self):
269
+ # evaluate the training data
270
+ (
271
+ self.train_state.y_pred_train,
272
+ self.train_state.loss_train,
273
+ ) = self.fn_outputs_losses_train(
274
+ self.train_state.X_train,
275
+ self.train_state.y_train,
276
+ **self.train_state.Aux_train,
277
+ )
278
+
279
+ # evaluate the test data
280
+ (
281
+ self.train_state.y_pred_test,
282
+ self.train_state.loss_test
283
+ ) = self.fn_outputs_losses_test(
284
+ self.train_state.X_test,
285
+ self.train_state.y_test,
286
+ **self.train_state.Aux_test,
287
+ )
288
+
289
+ # metrics
290
+ if isinstance(self.train_state.y_test, (list, tuple)):
291
+ self.train_state.metrics_test = [
292
+ m(self.train_state.y_test[i],
293
+ self.train_state.y_pred_test[i])
294
+ for m in self.metrics
295
+ for i in range(len(self.train_state.y_test))
296
+ ]
297
+ else:
298
+ self.train_state.metrics_test = [
299
+ m(self.train_state.y_test,
300
+ self.train_state.y_pred_test)
301
+ for m in self.metrics
302
+ ]
303
+
304
+ # history
305
+ self.train_state.update_best()
306
+ self.loss_history.append(
307
+ self.train_state.step,
308
+ self.train_state.loss_train,
309
+ self.train_state.loss_test,
310
+ self.train_state.metrics_test,
311
+ )
312
+
313
+ # check NaN
314
+ if (
315
+ jnp.isnan(jnp.asarray(jax.tree.leaves(self.train_state.loss_train))).any()
316
+ or jnp.isnan(jnp.asarray(jax.tree.leaves(self.train_state.loss_test))).any()
317
+ ):
318
+ self.stop_training = True
319
+
320
+ # display
321
+ training_display(self.train_state)
322
+
323
+ def predict(
324
+ self,
325
+ xs,
326
+ operator: Optional[Callable] = None,
327
+ callbacks: Union[Callback, Sequence[Callback]] = None,
328
+ ):
329
+ """Generates predictions for the input samples. If `operator` is ``None``,
330
+ returns the network output, otherwise returns the output of the `operator`.
331
+
332
+ Args:
333
+ xs: The network inputs. A Numpy array or a tuple of Numpy arrays.
334
+ operator: A function takes arguments (`neural_net`, `inputs`) and outputs a tensor. `inputs` and
335
+ `outputs` are the network input and output tensors, respectively. `operator` is typically
336
+ chosen as the PDE (used to define `pinnx.problem.PDE`) to predict the PDE residual.
337
+ callbacks: List of ``pinnx.callbacks.Callback`` instances. List of callbacks
338
+ to apply during prediction.
339
+ """
340
+ xs = jax.tree.map(
341
+ lambda x: u.math.asarray(x, dtype=brainstate.environ.dftype()),
342
+ xs,
343
+ is_leaf=u.math.is_quantity
344
+ )
345
+ callbacks = CallbackList(callbacks=[callbacks] if isinstance(callbacks, Callback) else callbacks)
346
+ callbacks.set_model(self)
347
+ callbacks.on_predict_begin()
348
+ ys = self.fn_outputs(False, xs)
349
+ if operator is not None:
350
+ ys = operator(xs, ys)
351
+ callbacks.on_predict_end()
352
+ return ys
353
+
354
+ def save(self, save_path, verbose: int = 0):
355
+ """Saves all variables to a disk file.
356
+
357
+ Args:
358
+ save_path (string): Prefix of filenames to save the trainer file.
359
+ verbose (int): Verbosity mode, 0 or 1.
360
+
361
+ Returns:
362
+ string: Path where trainer is saved.
363
+ """
364
+ import braintools
365
+
366
+ # save path
367
+ save_path = f"{save_path}-{self.train_state.epoch}.msgpack"
368
+
369
+ # avoid the duplicate ParamState save
370
+ model = brainstate.graph.Dict(params=self.params, optimizer=self.optimizer)
371
+
372
+ checkpoint = brainstate.graph.states(model).to_nest()
373
+ braintools.file.msgpack_save(save_path, checkpoint)
374
+
375
+ if verbose > 0:
376
+ print(
377
+ "Epoch {}: saving trainer to {} ...\n".format(
378
+ self.train_state.epoch, save_path
379
+ )
380
+ )
381
+ return save_path
382
+
383
+ def restore(self, save_path, verbose: int = 0):
384
+ """Restore all variables from a disk file.
385
+
386
+ Args:
387
+ save_path (string): Path where trainer was previously saved.
388
+ verbose (int): Verbosity mode, 0 or 1.
389
+ """
390
+ import braintools
391
+ if verbose > 0:
392
+ print("Restoring trainer from {} ...\n".format(save_path))
393
+
394
+ data = brainstate.graph.Dict(params=self.params, optimizer=self.optimizer)
395
+
396
+ checkpoint = brainstate.graph.states(data).to_nest()
397
+ braintools.file.msgpack_load(save_path, target=checkpoint)
398
+
399
+ def saveplot(
400
+ self,
401
+ issave: bool = True,
402
+ isplot: bool = True,
403
+ loss_fname: str = "loss.dat",
404
+ train_fname: str = "train.dat",
405
+ test_fname: str = "test.dat",
406
+ output_dir: str = None,
407
+ ):
408
+ """
409
+ Saves and plots the loss and metrics.
410
+
411
+ Args:
412
+ issave: If ``True``, save the loss and metrics to files.
413
+ isplot: If ``True``, plot the loss and metrics.
414
+ loss_fname: Filename to save the loss.
415
+ train_fname: Filename to save the training metrics.
416
+ test_fname: Filename to save the test metrics.
417
+ output_dir: Directory to save the files.
418
+ """
419
+ utils.saveplot(
420
+ self.loss_history,
421
+ self.train_state,
422
+ issave=issave,
423
+ isplot=isplot,
424
+ loss_fname=loss_fname,
425
+ train_fname=train_fname,
426
+ test_fname=test_fname,
427
+ output_dir=output_dir,
428
+ )
429
+
430
+
431
+ class TrainState:
432
+ __module__ = 'pinnx'
433
+
434
+ def __init__(self):
435
+ self.epoch = 0
436
+ self.step = 0
437
+
438
+ # Current data
439
+ self.X_train = None
440
+ self.y_train = None
441
+ self.Aux_train = dict()
442
+ self.X_test = None
443
+ self.y_test = None
444
+ self.Aux_test = dict()
445
+
446
+ # Results of current step
447
+ # Train results
448
+ self.loss_train = None
449
+ self.y_pred_train = None
450
+ # Test results
451
+ self.loss_test = None
452
+ self.y_pred_test = None
453
+ self.y_std_test = None
454
+ self.metrics_test = None
455
+
456
+ # The best results correspond to the min train loss
457
+ self.best_step = 0
458
+ self.best_loss_train = np.inf
459
+ self.best_loss_test = np.inf
460
+ self.best_y = None
461
+ self.best_ystd = None
462
+ self.best_metrics = None
463
+
464
+ def set_data_train(self, X_train, y_train, *args):
465
+ self.X_train = X_train
466
+ self.y_train = y_train
467
+ if len(args) > 0:
468
+ assert len(args) == 1, "Auxiliary training data must be a single argument."
469
+ assert isinstance(args[0], dict), "Auxiliary training data must be a dictionary."
470
+ self.Aux_train = args[0]
471
+
472
+ def set_data_test(self, X_test, y_test, *args):
473
+ self.X_test = X_test
474
+ self.y_test = y_test
475
+ if len(args) > 0:
476
+ assert len(args) == 1, "Auxiliary test data must be a single argument."
477
+ assert isinstance(args[0], dict), "Auxiliary test data must be a dictionary."
478
+ self.Aux_test = args[0]
479
+
480
+ def update_best(self):
481
+ current_loss_train = jnp.sum(jnp.asarray(jax.tree.leaves(self.loss_train)))
482
+ if self.best_loss_train > current_loss_train:
483
+ self.best_step = self.step
484
+ self.best_loss_train = current_loss_train
485
+ self.best_loss_test = jnp.sum(jnp.asarray(jax.tree.leaves(self.loss_test)))
486
+ self.best_y = self.y_pred_test
487
+ self.best_ystd = self.y_std_test
488
+ self.best_metrics = self.metrics_test
489
+
490
+ def disregard_best(self):
491
+ self.best_loss_train = np.inf
492
+
493
+
494
+ class LossHistory:
495
+ __module__ = 'pinnx'
496
+
497
+ def __init__(self):
498
+ self.steps = []
499
+ self.loss_train = []
500
+ self.loss_test = []
501
+ self.metrics_test = []
502
+
503
+ def append(self, step, loss_train, loss_test, metrics_test):
504
+ self.steps.append(step)
505
+ self.loss_train.append(loss_train)
506
+ if loss_test is None:
507
+ loss_test = self.loss_test[-1]
508
+ if metrics_test is None:
509
+ metrics_test = self.metrics_test[-1]
510
+ self.loss_test.append(loss_test)
511
+ self.metrics_test.append(metrics_test)