pyMOTO 1.5.0__py3-none-any.whl → 1.5.1b1__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.
pymoto/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '1.5.0'
1
+ __version__ = '1.5.1-beta1'
2
2
 
3
3
  from .common.domain import DomainDefinition
4
4
 
pymoto/common/domain.py CHANGED
@@ -91,6 +91,7 @@ class DomainDefinition:
91
91
  if self.nelz is None:
92
92
  self.nelz = 0
93
93
  self.unitx, self.unity, self.unitz = unitx, unity, unitz
94
+ self.origin = np.array([0.0, 0.0, 0.0])
94
95
 
95
96
  self.dim = 1 if (self.nelz == 0 and self.nely == 0) else (2 if self.nelz == 0 else 3)
96
97
 
@@ -287,7 +288,7 @@ class DomainDefinition:
287
288
  patch.set_path(self.get_path(x + u, y + v))
288
289
 
289
290
  # flake8: noqa: C901
290
- def write_to_vti(self, vectors: dict, filename="out.vti", scale=1.0, origin=(0.0, 0.0, 0.0)):
291
+ def write_to_vti(self, vectors: dict, filename="out.vti", scale=1.0):
291
292
  """ Write all given vectors to a Paraview (VTI) file
292
293
 
293
294
  The size of the vectors should be a multiple of ``nel`` or ``nnodes``. Based on their size they are marked as
@@ -299,7 +300,6 @@ class DomainDefinition:
299
300
  vectors: A dictionary of vectors to write. Keys are used as vector names.
300
301
  filename (str): The file loction
301
302
  scale: Uniform scaling of the gridpoints
302
- origin: Origin of the domain
303
303
  """
304
304
  ext = '.vti'
305
305
  if ext not in os.path.splitext(filename)[-1].lower():
@@ -336,7 +336,7 @@ class DomainDefinition:
336
336
  file.write(f"<ImageData WholeExtent=\"0 {self.nelx} 0 {self.nely} 0 {self.nelz}\"".encode())
337
337
 
338
338
  # Origin of domain
339
- file.write(f" Origin=\"{origin[0]*scale} {origin[1]*scale} {origin[2]*scale}\"".encode())
339
+ file.write(f" Origin=\"{self.origin[0]*scale} {self.origin[1]*scale} {self.origin[2]*scale}\"".encode())
340
340
 
341
341
  # Spacing of points (dx, dy, dz)
342
342
  dx, dy, dz = self.element_size[0:3]*scale
@@ -393,7 +393,7 @@ class DyadCarrier(object):
393
393
  exprvars = (rowvar, colvar) if mat is None else (rowvar, matvar, colvar)
394
394
  expr = ','.join(exprvars) + '->' + batchvar
395
395
 
396
- val = 0.0 if batchsize is None else np.zeros(batchsize)
396
+ val = 0.0 if batchsize is None else np.zeros(batchsize, dtype=np.result_type(mat, self.dtype))
397
397
  for ui, vi in zip(self.u, self.v):
398
398
  uarg = ui if rows is None else ui[rows]
399
399
  varg = vi if cols is None else vi[cols]
pymoto/common/mma.py CHANGED
@@ -305,8 +305,12 @@ class MMA:
305
305
 
306
306
  # Setting up for constriants
307
307
  self.m = len(self.responses) - 1
308
- self.a = np.zeros(self.m)
309
- self.c = self.cCoef * np.ones(self.m)
308
+ self.a = kwargs.get("a", np.zeros(self.m))
309
+ if len(self.a) != self.m:
310
+ raise RuntimeError(f"Length of the a vector ({len(self.a)}) should be equal to # constraints ({self.m}).")
311
+ self.c = kwargs.get("c", np.full(self.m, self.cCoef, dtype=float))
312
+ if len(self.c) != self.m:
313
+ raise RuntimeError(f"Length of the c vector ({len(self.c)}) should be equal to # constraints ({self.m}).")
310
314
  self.d = np.ones(self.m)
311
315
  self.gold1 = np.zeros(self.m + 1)
312
316
  self.gold2 = self.gold1.copy()
@@ -371,11 +375,15 @@ class MMA:
371
375
  # Calculate response
372
376
  self.funbl.response()
373
377
 
378
+ xval, _ = _concatenate_to_array([s.state for s in self.variables])
379
+
374
380
  # Save response
375
381
  f = ()
376
382
  for s in self.responses:
377
383
  if np.size(s.state) != 1:
378
384
  raise TypeError("State of responses must be scalar.")
385
+ if np.iscomplexobj(s.state):
386
+ raise TypeError("Responses must be real-valued.")
379
387
  f += (s.state, )
380
388
 
381
389
  # Check function change convergence criterion
@@ -564,4 +572,4 @@ class MMA:
564
572
  print(f" | Changes: {', '.join(change_msgs)}")
565
573
 
566
574
  return xmma, change
567
-
575
+
pymoto/core_objects.py CHANGED
@@ -5,6 +5,7 @@ import time
5
5
  import copy
6
6
  from typing import Union, List, Any
7
7
  from abc import ABC, abstractmethod
8
+ from collections.abc import Callable
8
9
  from .utils import _parse_to_list, _concatenate_to_array, _split_from_array
9
10
 
10
11
 
@@ -186,14 +187,14 @@ class SignalSlice(Signal):
186
187
  The sliced values are referenced to their original source Signal, such that they can be used and updated in modules.
187
188
  This means that updating the values in this SignalSlice changes the data in its source Signal.
188
189
  """
189
- def __init__(self, orig_signal, sl, tag=None):
190
- self.orig_signal = orig_signal
190
+ def __init__(self, base, sl, tag=None):
191
+ self.base = base
191
192
  self.slice = sl
192
193
  self.keep_alloc = False # Allocation must be False because sensitivity cannot be assigned with [] operator
193
194
 
194
195
  # for s in slice:
195
196
  if tag is None:
196
- self.tag = f"{self.orig_signal.tag}[{fmt_slice(self.slice)}]"
197
+ self.tag = f"{self.base.tag}[{fmt_slice(self.slice)}]"
197
198
  else:
198
199
  self.tag = tag
199
200
 
@@ -203,7 +204,7 @@ class SignalSlice(Signal):
203
204
  @property
204
205
  def state(self):
205
206
  try:
206
- return None if self.orig_signal.state is None else self.orig_signal.state[self.slice]
207
+ return None if self.base.state is None else self.base.state[self.slice]
207
208
  except Exception as e:
208
209
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
209
210
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (getter). Signal details:" +
@@ -212,7 +213,7 @@ class SignalSlice(Signal):
212
213
  @state.setter
213
214
  def state(self, new_state):
214
215
  try:
215
- self.orig_signal.state[self.slice] = new_state
216
+ self.base.state[self.slice] = new_state
216
217
  except Exception as e:
217
218
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
218
219
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (setter). Signal details:" +
@@ -221,7 +222,7 @@ class SignalSlice(Signal):
221
222
  @property
222
223
  def sensitivity(self):
223
224
  try:
224
- return None if self.orig_signal.sensitivity is None else self.orig_signal.sensitivity[self.slice]
225
+ return None if self.base.sensitivity is None else self.base.sensitivity[self.slice]
225
226
  except Exception as e:
226
227
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
227
228
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.sensitivity (getter). Signal details:" +
@@ -230,26 +231,55 @@ class SignalSlice(Signal):
230
231
  @sensitivity.setter
231
232
  def sensitivity(self, new_sens):
232
233
  try:
233
- if self.orig_signal.sensitivity is None:
234
+ if self.base.sensitivity is None:
235
+ # Initialize sensitivity of base-signal
234
236
  if new_sens is None:
235
237
  return # Sensitivity doesn't need to be initialized when it is set to None
236
238
  try:
237
- self.orig_signal.sensitivity = self.orig_signal.state * 0 # Make a new copy with 0 values
239
+ self.base.sensitivity = self.base.state * 0 # Make a new copy with 0 values
238
240
  except TypeError:
239
- if self.orig_signal.state is None:
241
+ if self.base.state is None:
240
242
  raise TypeError("Could not initialize sensitivity because state is not set" + self._err_str())
241
243
  else:
242
- raise TypeError(f"Could not initialize sensitivity for type \'{type(self.orig_signal.state).__name__}\'")
244
+ raise TypeError(f"Could not initialize sensitivity for type \'{type(self.base.state).__name__}\'")
243
245
 
244
246
  if new_sens is None:
245
247
  new_sens = 0 # reset() uses this
246
248
 
247
- self.orig_signal.sensitivity[self.slice] = new_sens
249
+ self.base.sensitivity[self.slice] = new_sens
248
250
  except Exception as e:
249
251
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
250
252
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (setter). Signal details:" +
251
253
  self._err_str()).with_traceback(sys.exc_info()[2])
252
254
 
255
+ def add_sensitivity(self, ds: Any):
256
+ """ Add a new term to internal sensitivity """
257
+ try:
258
+ if ds is None:
259
+ return
260
+ if self.base.sensitivity is None:
261
+ self.base.sensitivity = self.base.state * 0
262
+ # self.sensitivity = copy.deepcopy(ds)
263
+
264
+ if hasattr(self.sensitivity, "add_sensitivity"):
265
+ # Allow user to implement a custom add_sensitivity function instead of __iadd__
266
+ self.sensitivity.add_sensitivity(ds)
267
+ else:
268
+ self.sensitivity += ds
269
+ return self
270
+ except TypeError:
271
+ if isinstance(ds, type(self.sensitivity)):
272
+ raise TypeError(
273
+ f"Cannot add to the sensitivity with type '{type(self.sensitivity).__name__}'" + self._err_str())
274
+ else:
275
+ raise TypeError(
276
+ f"Adding wrong type '{type(ds).__name__}' to the sensitivity '{type(self.sensitivity).__name__}'" + self._err_str())
277
+ except ValueError:
278
+ sens_shape = self.sensitivity.shape if hasattr(self.sensitivity, 'shape') else ()
279
+ ds_shape = ds.shape if hasattr(ds, 'shape') else ()
280
+ raise ValueError(
281
+ f"Cannot add argument of shape {ds_shape} to the sensitivity of shape {sens_shape}" + self._err_str()) from None
282
+
253
283
  def reset(self, keep_alloc: bool = None):
254
284
  """ Reset the sensitivities to zero or None
255
285
  This must be called to clear internal memory of subsequent sensitivity calculations.
@@ -296,30 +326,54 @@ def _is_valid_module(mod: Any):
296
326
  return False
297
327
 
298
328
 
299
- def _check_function_signature(fn, signals):
329
+ # Type definition for bound method
330
+ class BoundMethod:
331
+ __self__: object
332
+
333
+
334
+ BoundMethodT = Union[Callable, BoundMethod]
335
+
336
+
337
+ def _check_function_signature(fn: BoundMethodT, signals: list = None) -> (int, int):
300
338
  """ Checks the function signature against given signal list
301
- :param fn: response_ or sensitivity_ function of a Module
302
- :param signals: The signals involved
339
+
340
+ - Only positional-only or positional-or-keyword arguments are allowed
341
+ (https://peps.python.org/pep-0362/#parameter-object)
342
+ - If `signals` is provided, the number of them is compared with the allowed min/max number of arguments
343
+
344
+ Args:
345
+ fn: response_ or sensitivity_ function of a Module
346
+ signals (optional): The signals involved, which are checked with the function signature
347
+
348
+ Returns:
349
+ (min_args, max_args): Minimum and maximum number of accepted arguments to the function;
350
+ `None` denotes an unlimited number of maximum arguments (caused by `*args`)
303
351
  """
304
352
  min_args, max_args = 0, 0
305
353
  callstr = f"{type(fn.__self__).__name__}.{fn.__name__}{inspect.signature(fn)}"
354
+ # Loop over all the parameters defined for the function
306
355
  for s, p in inspect.signature(fn).parameters.items():
307
356
  if p.kind == inspect.Parameter.POSITIONAL_ONLY or p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
308
- if p.default != inspect.Parameter.empty:
309
- raise SyntaxError(f"{callstr} must not have default values \"{p}\"")
310
- min_args += 1
311
- if max_args >= 0:
357
+ if p.default == inspect.Parameter.empty:
358
+ # No default argument is provided, a value MUST be provided
359
+ min_args += 1
360
+ if max_args is not None:
312
361
  max_args += 1
313
362
  elif p.kind == inspect.Parameter.VAR_POSITIONAL:
314
- max_args = -1
363
+ # If *args is passed, the maximum number of arguments is unknown. Does not affect minimum number
364
+ max_args = None
315
365
  elif p.kind == inspect.Parameter.KEYWORD_ONLY:
316
- raise SyntaxError(f"{callstr} may not contain keyword arguments \"{p}\"")
366
+ raise SyntaxError(f"{callstr} may not contain keyword-only arguments \"{p}\"")
317
367
  elif p.kind == inspect.Parameter.VAR_KEYWORD:
318
- raise SyntaxError(f"{callstr} may not contain \"{p}\"")
319
- if len(signals) < min_args:
320
- raise TypeError(f"Not enough arguments ({len(signals)}) for {callstr}")
321
- if max_args >= 0 and len(signals) > max_args:
322
- raise TypeError(f"Too many arguments ({len(signals)}) for {callstr}")
368
+ raise SyntaxError(f"{callstr} may not contain dict of keyword-only arguments \"{p}\"")
369
+
370
+ # Check with signal list
371
+ if signals is not None:
372
+ if len(signals) < min_args:
373
+ raise TypeError(f"Not enough arguments ({len(signals)}) for {callstr}; expected {min_args}")
374
+ if max_args is not None and len(signals) > max_args:
375
+ raise TypeError(f"Too many arguments ({len(signals)}) for {callstr}; expected {max_args}")
376
+ return min_args, max_args
323
377
 
324
378
 
325
379
  class RegisteredClass(object):
@@ -408,7 +462,7 @@ class Module(ABC, RegisteredClass):
408
462
  >> Module([inputs])
409
463
 
410
464
  Using keywords:
411
- >> Module(sig_in=[inputs], sig_out=[outputs]
465
+ >> Module(sig_in=[inputs], sig_out=[outputs])
412
466
  """
413
467
 
414
468
  def _err_str(self, module_signature: bool = True, init: bool = True, fn=None):
@@ -493,8 +547,12 @@ class Module(ABC, RegisteredClass):
493
547
  raise type(e)(str(e) + "\n\t| Above error was raised when calling response(). Module details:" +
494
548
  self._err_str(fn=self._response)).with_traceback(sys.exc_info()[2])
495
549
 
496
- def __call__(self):
497
- return self.response()
550
+ def __call__(self, *args):
551
+ if len(args) == 0 and len(self.sig_in) > 0:
552
+ inp = [s.state for s in self.sig_in]
553
+ return self._response(*inp)
554
+ else:
555
+ return self._response(*args)
498
556
 
499
557
  def sensitivity(self):
500
558
  """ Calculate sensitivities using backpropagation
@@ -556,67 +614,74 @@ class Module(ABC, RegisteredClass):
556
614
  class Network(Module):
557
615
  """ Binds multiple Modules together as one Module
558
616
 
617
+ Initialize a network with a number of modules that should be executed consecutively
559
618
  >> Network(module1, module2, ...)
560
619
 
561
620
  >> Network([module1, module2, ...])
562
621
 
563
622
  >> Network((module1, module2, ...))
564
623
 
624
+ Modules can also be constructed using a dictionary based on strings
565
625
  >> Network([ {type="module1", sig_in=[sig1, sig2], sig_out=[sig3]},
566
626
  {type="module2", sig_in=[sig3], sig_out=[sig4]} ])
567
627
 
628
+ Appending modules to a network will output the signals automatically
629
+ >> fn = Network()
630
+ >> s_out = fn.append(module1)
631
+
632
+ Args:
633
+ print_timing: Print timing of each module inside this Network
568
634
  """
569
635
  def __init__(self, *args, print_timing=False):
636
+ super().__init__()
570
637
  self._init_loc = get_init_str()
571
-
572
- # Obtain the internal blocks
573
- self.mods = _parse_to_list(*args)
574
-
575
- # Check if the blocks are initialized, else create them
576
- for i, b in enumerate(self.mods):
577
- if isinstance(b, dict):
578
- exclude_keys = ['type']
579
- b_ex = {k: b[k] for k in set(list(b.keys())) - set(exclude_keys)}
580
- self.mods[i] = Module.create(b['type'], **b_ex)
581
-
582
- # Check validity of modules
583
- for m in self.mods:
584
- if not _is_valid_module(m):
585
- raise TypeError(f"Argument is not a valid Module, type=\'{type(mod).__name__}\'.")
586
-
587
- # Gather all the input and output signals of the internal blocks
588
- all_in = set()
589
- all_out = set()
590
- [all_in.update(b.sig_in) for b in self.mods]
591
- [all_out.update(b.sig_out) for b in self.mods]
592
- in_unique = all_in - all_out
593
-
594
- # Initialize the parent module, with correct inputs and outputs
595
- super().__init__(list(in_unique), list(all_out))
596
-
638
+ self.mods = [] # Empty module list
639
+ self.append(*args) # Append to module list
597
640
  self.print_timing = print_timing
598
641
 
599
- def timefn(self, fn, prefix='Evaluation'):
642
+ def timefn(self, fn, name=None):
600
643
  start_t = time.time()
601
644
  fn()
602
645
  duration = time.time() - start_t
603
- if duration > .5:
604
- print(f"{prefix} {fn} took {time.time() - start_t} s")
646
+ if name is None:
647
+ name = f"{fn}"
648
+ if isinstance(self.print_timing, bool):
649
+ tmin = 0.0
650
+ else:
651
+ tmin = self.print_timing
652
+ if duration > tmin:
653
+ print(f"{name} took {time.time() - start_t} s")
605
654
 
606
655
  def response(self):
607
- if self.print_timing:
608
- [self.timefn(b.response, prefix='Response') for b in self.mods]
656
+ if self.print_timing is not False:
657
+ start_t = time.time()
658
+ [self.timefn(m.response, name=f"-- Response of \"{type(m).__name__}\"") for m in self.mods]
659
+ duration = time.time() - start_t
660
+ if isinstance(self.print_timing, bool):
661
+ tmin = 0.0
662
+ else:
663
+ tmin = self.print_timing
664
+ if duration > tmin:
665
+ print(f"-- TOTAL Response took {time.time() - start_t} s")
609
666
  else:
610
- [b.response() for b in self.mods]
667
+ [m.response() for m in self.mods]
611
668
 
612
669
  def sensitivity(self):
613
- if self.print_timing:
614
- [self.timefn(b.sensitivity, 'Sensitivity') for b in reversed(self.mods)]
670
+ if self.print_timing is not False:
671
+ start_t = time.time()
672
+ [self.timefn(m.sensitivity, name=f"-- Sensitivity of \"{type(m).__name__}\"") for m in reversed(self.mods)]
673
+ duration = time.time() - start_t
674
+ if isinstance(self.print_timing, bool):
675
+ tmin = 0.0
676
+ else:
677
+ tmin = self.print_timing
678
+ if duration > tmin:
679
+ print(f"-- TOTAL Sensitivity took {time.time() - start_t} s")
615
680
  else:
616
- [b.sensitivity() for b in reversed(self.mods)]
681
+ [m.sensitivity() for m in reversed(self.mods)]
617
682
 
618
683
  def reset(self):
619
- [b.reset() for b in reversed(self.mods)]
684
+ [m.reset() for m in reversed(self.mods)]
620
685
 
621
686
  def _response(self, *args):
622
687
  pass # Unused
@@ -636,13 +701,25 @@ class Network(Module):
636
701
  def __iter__(self):
637
702
  return iter(self.mods)
638
703
 
704
+ def __call__(self, *args):
705
+ return self.append(*args)
706
+
639
707
  def append(self, *newmods):
640
708
  modlist = _parse_to_list(*newmods)
709
+ if len(modlist) == 0:
710
+ return
641
711
 
642
712
  # Check if the blocks are initialized, else create them
713
+ for i, m in enumerate(modlist):
714
+ if isinstance(m, dict):
715
+ exclude_keys = ['type']
716
+ b_ex = {k: m[k] for k in set(list(m.keys())) - set(exclude_keys)}
717
+ modlist[i] = Module.create(m['type'], **b_ex)
718
+
719
+ # Check validity of modules
643
720
  for i, m in enumerate(modlist):
644
721
  if not _is_valid_module(m):
645
- raise TypeError(f"Argument #{i} is not a valid module, type=\'{type(mod).__name__}\'.")
722
+ raise TypeError(f"Argument #{i} is not a valid module, type=\'{type(m).__name__}\'.")
646
723
 
647
724
  # Obtain the internal blocks
648
725
  self.mods.extend(modlist)
@@ -657,4 +734,4 @@ class Network(Module):
657
734
  self.sig_in = _parse_to_list(in_unique)
658
735
  self.sig_out = _parse_to_list(all_out)
659
736
 
660
- return modlist[-1].sig_out[0] if len(modlist[-1].sig_out) == 1 else modlist[-1].sig_out # Returns the output signal
737
+ return modlist[-1].sig_out[0] if len(modlist[-1].sig_out) == 1 else modlist[-1].sig_out
@@ -57,20 +57,21 @@ class AssembleGeneral(Module):
57
57
  self.bc = bc
58
58
  self.bcdiagval = np.max(element_matrix) if bcdiagval is None else bcdiagval
59
59
  if bc is not None:
60
- self.bcselect = np.argwhere(np.bitwise_not(np.bitwise_or(np.isin(self.rows, self.bc),
61
- np.isin(self.cols, self.bc)))).flatten()
62
-
63
- self.rows = np.concatenate((self.rows[self.bcselect], self.bc))
64
- self.cols = np.concatenate((self.cols[self.bcselect], self.bc))
60
+ bc_inds = np.bitwise_or(np.isin(self.rows, self.bc), np.isin(self.cols, self.bc))
61
+ self.bcselect = np.argwhere(np.bitwise_not(bc_inds)).flatten()
62
+ self.bcrows = np.concatenate((self.rows[self.bcselect], self.bc))
63
+ self.bccols = np.concatenate((self.cols[self.bcselect], self.bc))
65
64
  else:
66
65
  self.bcselect = None
66
+ self.bcrows = self.rows
67
+ self.bccols = self.cols
67
68
 
68
69
  self.add_constant = add_constant
69
70
 
70
71
  def _response(self, xscale: np.ndarray):
71
72
  nel = self.dofconn.shape[0]
72
73
  assert xscale.size == nel, f"Input vector wrong size ({xscale.size}), must be of size #nel ({nel})"
73
- scaled_el = ((self.elmat.flatten()[np.newaxis]).T * xscale).flatten(order='F')
74
+ scaled_el = (self.elmat.flatten() * xscale[..., np.newaxis]).flatten()
74
75
 
75
76
  # Set boundary conditions
76
77
  if self.bc is not None:
@@ -80,7 +81,7 @@ class AssembleGeneral(Module):
80
81
  mat_values = scaled_el
81
82
 
82
83
  try:
83
- mat = self.matrix_type((mat_values, (self.rows, self.cols)), shape=(self.n, self.n))
84
+ mat = self.matrix_type((mat_values, (self.bcrows, self.bccols)), shape=(self.n, self.n))
84
85
  except TypeError as e:
85
86
  raise type(e)(str(e) + "\n\tInvalid matrix_type={}. Either scipy.sparse.cscmatrix or "
86
87
  "scipy.sparse.csrmatrix are supported"
@@ -96,14 +97,16 @@ class AssembleGeneral(Module):
96
97
  if self.bc is not None:
97
98
  dgdmat[self.bc, :] = 0.0
98
99
  dgdmat[:, self.bc] = 0.0
100
+ dx = np.zeros_like(self.sig_in[0].state)
99
101
  if isinstance(dgdmat, np.ndarray):
100
- dx = np.zeros_like(self.sig_in[0].state)
101
102
  for i in range(len(dx)):
102
103
  indu, indv = np.meshgrid(self.dofconn[i], self.dofconn[i], indexing='ij')
103
- dx[i] = einsum("ij,ij->", self.elmat, dgdmat[indu, indv])
104
- return dx
104
+ dxi = einsum("ij,ij->", self.elmat, dgdmat[indu, indv])
105
+ dx[i] = np.real(dxi) if np.isrealobj(dx) else dxi
105
106
  elif isinstance(dgdmat, DyadCarrier):
106
- return dgdmat.contract(self.elmat, self.dofconn, self.dofconn)
107
+ dxi = dgdmat.contract(self.elmat, self.dofconn, self.dofconn)
108
+ dx[:] = np.real(dxi) if np.isrealobj(dx) else dxi
109
+ return dx
107
110
 
108
111
 
109
112
  def get_B(dN_dx, voigt=True):
@@ -123,7 +126,7 @@ def get_B(dN_dx, voigt=True):
123
126
  """
124
127
  n_dim, n_shapefn = dN_dx.shape
125
128
  n_strains = int((n_dim * (n_dim+1))/2) # Triangular number: ndim=3 -> nstrains = 3+2+1
126
- B = np.zeros((n_strains, n_shapefn*n_dim))
129
+ B = np.zeros((n_strains, n_shapefn*n_dim), dtype=dN_dx.dtype)
127
130
  if n_dim == 1:
128
131
  for i in range(n_shapefn):
129
132
  B[i, 0] = dN_dx[i, 0]
@@ -222,7 +225,8 @@ class AssembleStiffness(AssembleGeneral):
222
225
  ndof = nnode*domain.dim
223
226
 
224
227
  # Element stiffness matrix
225
- self.stiffness_element = np.zeros((ndof, ndof))
228
+ dtype = np.result_type(D, domain.element_size.dtype)
229
+ self.stiffness_element = np.zeros((ndof, ndof), dtype=dtype)
226
230
 
227
231
  # Numerical integration
228
232
  siz = domain.element_size
pymoto/modules/io.py CHANGED
@@ -1,13 +1,9 @@
1
1
  import os
2
- import platform
3
2
  import numbers
4
- import sys
5
3
  from pathlib import Path
6
4
  import numpy as np
7
- if platform.system() == 'Darwin': # Avoid "Python is not installed as a framework (Mac OS X)" error
8
- # Change backend
9
- import matplotlib
10
- matplotlib.use('TkAgg')
5
+ import matplotlib
6
+ matplotlib.use('TkAgg') # Change default backend -- TkAgg does not freeze during calculations
11
7
  import matplotlib.pyplot as plt
12
8
 
13
9
  from pymoto import Module
@@ -202,15 +198,16 @@ class PlotIter(FigModule):
202
198
  show (bool): Show the figure on the screen
203
199
  ylim: Provide y-axis limits for the plot
204
200
  """
205
- def _prepare(self, ylim=None):
201
+ def _prepare(self, ylim=None, log_scale=False):
206
202
  self.minlim = 1e+200
207
203
  self.maxlim = -1e+200
208
204
  self.ylim = ylim
205
+ self.log_scale = log_scale
209
206
 
210
207
  def _response(self, *args):
211
208
  if not hasattr(self, 'ax'):
212
209
  self.ax = self.fig.add_subplot(111)
213
- self.ax.set_yscale('linear')
210
+ self.ax.set_yscale('linear' if not self.log_scale else 'log')
214
211
  self.ax.set_xlabel("Iteration")
215
212
 
216
213
  if not hasattr(self, 'line'):
@@ -233,13 +230,24 @@ class PlotIter(FigModule):
233
230
  self.minlim = min(self.minlim, np.min(xadd))
234
231
  self.maxlim = max(self.maxlim, np.max(xadd))
235
232
 
236
- dy = max((self.maxlim - self.minlim)*0.05, sys.float_info.min)
237
-
238
233
  self.ax.set_xlim([-0.5, self.iter+0.5])
239
234
  if self.ylim is not None:
240
235
  self.ax.set_ylim(self.ylim)
241
236
  elif np.isfinite(self.minlim) and np.isfinite(self.maxlim):
242
- self.ax.set_ylim([self.minlim - dy, self.maxlim + dy])
237
+ if self.log_scale:
238
+ dy = (np.log10(self.maxlim) - np.log10(self.minlim))*0.05
239
+ ll = 10**(np.log10(self.minlim) - dy)
240
+ ul = 10**(np.log10(self.maxlim) + dy)
241
+ else:
242
+ dy = (self.maxlim - self.minlim)*0.05
243
+ ll = self.minlim - dy
244
+ ul = self.maxlim + dy
245
+
246
+ if ll == ul:
247
+ dy = abs(np.nextafter(abs(ll), 1) - abs(ll))
248
+ ll = ll - 1e5*dy
249
+ ul = ul + 1e5*dy
250
+ self.ax.set_ylim([ll, ul])
243
251
 
244
252
  self._update_fig()
245
253
 
pymoto/modules/linalg.py CHANGED
@@ -268,7 +268,7 @@ class LinSolve(Module):
268
268
  if not isinstance(self.solver, LDAWrapper) and self.use_lda_solver:
269
269
  lda_kwargs = dict(hermitian=self.ishermitian, symmetric=self.issymmetric)
270
270
  if hasattr(self.solver, 'tol'):
271
- lda_kwargs['tol'] = self.solver.tol * 2
271
+ lda_kwargs['tol'] = self.solver.tol * 5
272
272
  self.solver = LDAWrapper(self.solver, **lda_kwargs)
273
273
 
274
274
  # Update solver with new matrix
@@ -371,9 +371,11 @@ class EigenSolve(Module):
371
371
  Bqi = qi if B is None else B@qi
372
372
 
373
373
  normval = np.sqrt(qi @ Bqi)
374
- avgval = np.average(qi)/normval
375
-
376
- sf = np.sign(np.real(avgval)) / normval
374
+ sgn = 1 if np.real(np.average(qi)) >= 0 else -1
375
+ sf = sgn / normval
376
+ assert np.isfinite(sf)
377
+ if sf == 0.0:
378
+ warnings.warn(f"Scaling factor of mode {i} is zero!")
377
379
  qi *= sf
378
380
  return W, Q
379
381
 
@@ -423,8 +425,9 @@ class EigenSolve(Module):
423
425
  if self.is_hermitian:
424
426
  return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma, mode=self.mode)
425
427
  else:
426
- # TODO
427
- raise NotImplementedError('Non-Hermitian sparse matrix not supported')
428
+ if self.mode.lower() not in ['normal']:
429
+ raise NotImplementedError('Only `normal` mode can be selected for non-hermitian matrix')
430
+ return spsla.eigs(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma)
428
431
 
429
432
  def _dense_sens(self, A, B, dW, dQ):
430
433
  """ Calculates all (eigenvector and eigenvalue) sensitivities for dense matrix """
@@ -465,17 +468,14 @@ class EigenSolve(Module):
465
468
  qi = Q[:, i]
466
469
  qmq = qi@qi if B is None else qi @ (B @ qi)
467
470
  dA_u = (dwi/qmq) * qi
468
- if np.isrealobj(A):
469
- dA += DyadCarrier([np.real(dA_u), -np.imag(dA_u)], [np.real(qi), np.imag(qi)])
470
- else:
471
- dA += DyadCarrier(dA_u, qi)
471
+ dAi = DyadCarrier(dA_u, qi)
472
+ dA += np.real(dAi) if np.isrealobj(A) else dAi
472
473
 
473
474
  if dB is not None:
474
475
  dB_u = (wi*dwi/qmq) * qi
475
- if np.isrealobj(B):
476
- dB -= DyadCarrier([np.real(dB_u), -np.imag(dB_u)], [np.real(qi), np.imag(qi)])
477
- else:
478
- dB -= DyadCarrier(dB_u, qi)
476
+ dBi = DyadCarrier(dB_u, qi)
477
+ dB -= np.real(dBi) if np.isrealobj(B) else dBi
478
+
479
479
  return dA, dB
480
480
 
481
481
  def _sparse_eigvec_sens(self, A, B, dW, dQ):
pymoto/routines.py CHANGED
@@ -10,10 +10,10 @@ from scipy.sparse import issparse
10
10
  def _has_signal_overlap(sig1: List[Signal], sig2: List[Signal]):
11
11
  for s1 in sig1:
12
12
  while isinstance(s1, SignalSlice):
13
- s1 = s1.orig_signal
13
+ s1 = s1.base
14
14
  for s2 in sig2:
15
15
  while isinstance(s2, SignalSlice):
16
- s2 = s2.orig_signal
16
+ s2 = s2.base
17
17
  if s1 == s2:
18
18
  return True
19
19
  return False
@@ -132,7 +132,7 @@ class GeometricMultigrid(Preconditioner):
132
132
  assert cycle.lower() in self._available_cycles, f"Cycle ({cycle}) is not available. Options are {self._available_cycles}"
133
133
  self.cycle = cycle
134
134
  self.inner_level = None if inner_level is None else inner_level
135
- self.smoother = DampedJacobi(w=0.5) if smoother is None else None
135
+ self.smoother = DampedJacobi(w=0.5) if smoother is None else smoother
136
136
  self.smooth_steps = smooth_steps
137
137
  self.R = None
138
138
  self.sub_domain = DomainDefinition(domain.nelx // 2, domain.nely // 2, domain.nelz // 2,
@@ -299,7 +299,7 @@ class CG(LinearSolver):
299
299
  self.A = A
300
300
  self.preconditioner.update(A)
301
301
  if self.verbosity >= 1:
302
- print(f"Preconditioner set up in {np.round(time.perf_counter() - tstart,3)}s")
302
+ print(f"CG Preconditioner set up in {np.round(time.perf_counter() - tstart, 3)}s")
303
303
 
304
304
  def solve(self, rhs, x0=None, trans='N'):
305
305
  if trans == 'N':
@@ -321,10 +321,18 @@ class CG(LinearSolver):
321
321
  x = x.reshape((x.size, 1))
322
322
 
323
323
  r = b - A@x
324
+ tval = np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)
325
+ if self.verbosity >= 2:
326
+ print(f"CG Initial (max) residual = {tval.max()}")
327
+
328
+ if tval.max() <= self.tol:
329
+ if self.verbosity >= 1:
330
+ print(f"CG Converged in 0 iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final (max) residual {tval.max()}")
331
+
332
+ return x.flatten() if rhs.ndim == 1 else x
333
+
324
334
  z = self.preconditioner.solve(r, trans=trans)
325
335
  p = orth(z, normalize=True)
326
- if self.verbosity >= 2:
327
- print(f"Initial residual = {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
328
336
 
329
337
  for i in range(self.maxit):
330
338
  q = A @ p
@@ -333,16 +341,15 @@ class CG(LinearSolver):
333
341
  alpha = pq_inv @ (p.conj().T @ r)
334
342
 
335
343
  x += p @ alpha
336
- if i % 50 == 0: # Explicit restart
344
+ if i % self.restart == 0: # Explicit restart
337
345
  r = b - A@x
338
346
  else:
339
347
  r -= q @ alpha
340
348
 
349
+ tval = np.linalg.norm(r, axis=0)/np.linalg.norm(b, axis=0)
341
350
  if self.verbosity >= 2:
342
- print(f"i = {i}, residuals = {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
343
-
344
- tval = np.linalg.norm(r)/np.linalg.norm(b)
345
- if tval <= self.tol:
351
+ print(f"CG i = {i}, residuals = {tval}")
352
+ if tval.max() <= self.tol:
346
353
  break
347
354
 
348
355
  z = self.preconditioner.solve(r, trans=trans)
@@ -350,12 +357,9 @@ class CG(LinearSolver):
350
357
  beta = -pq_inv @ (q.conj().T @ z)
351
358
  p = orth(z + p@beta, normalize=False)
352
359
 
353
- if tval > self.tol:
354
- warnings.warn(f'Maximum iterations ({self.maxit}) reached, with final residual {tval}')
360
+ if tval.max() > self.tol:
361
+ warnings.warn(f'CG Maximum iterations ({self.maxit}) reached, with final residuals {tval}')
355
362
  elif self.verbosity >= 1:
356
- print(f"Converged in {i} iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final residuals {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
363
+ print(f"CG Converged in {i} iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final (max) residual {tval.max()}")
357
364
 
358
- if rhs.ndim == 1:
359
- return x.flatten()
360
- else:
361
- return x
365
+ return x.flatten() if rhs.ndim == 1 else x
pymoto/solvers/solvers.py CHANGED
@@ -75,6 +75,17 @@ class LinearSolver:
75
75
  return np.linalg.norm(mat@x - b, axis=0) / np.linalg.norm(b, axis=0)
76
76
 
77
77
 
78
+ def get_diagonal_indices(mat):
79
+ """ Get the row/column indices for entries in the matrix that are diagonal (has all zeros in its row and column) """
80
+ assert mat.ndim == 2, "Not a matrix"
81
+ n = min(*mat.shape) # Matrix size
82
+ bmat = mat != 0
83
+ has_diag = bmat.diagonal()
84
+ nnz_rows = np.array(bmat.sum(axis=0)).flatten()[:n]
85
+ nnz_cols = np.array(bmat.sum(axis=1)).flatten()[:n]
86
+ return np.logical_and(has_diag, nnz_rows <= 1, nnz_cols <= 1)
87
+
88
+
78
89
  class LDAWrapper(LinearSolver):
79
90
  r""" Linear dependency aware solver (LDAS)
80
91
 
@@ -105,6 +116,8 @@ class LDAWrapper(LinearSolver):
105
116
  self.xadj_stored = []
106
117
  self.badj_stored = []
107
118
  self.A = None
119
+ self.diagonal_idx = []
120
+ self.nondiagonal_idx = slice(None)
108
121
  self._did_solve = False # For debugging purposes
109
122
  self._last_rtol = 0.
110
123
  self.hermitian = hermitian
@@ -123,6 +136,9 @@ class LDAWrapper(LinearSolver):
123
136
  self.hermitian = matrix_is_hermitian(A)
124
137
 
125
138
  self.A = A
139
+ diags = get_diagonal_indices(A)
140
+ self.diagonal_idx = np.argwhere(diags).flatten()
141
+ self.nondiagonal_idx = np.argwhere(~diags).flatten()
126
142
  self.x_stored.clear()
127
143
  self.b_stored.clear()
128
144
  self.xadj_stored.clear()
@@ -130,6 +146,9 @@ class LDAWrapper(LinearSolver):
130
146
  self.solver.update(A)
131
147
 
132
148
  def _do_solve_1rhs(self, A, rhs, x_data, b_data, solve_fn, x0=None):
149
+ isel = self.nondiagonal_idx
150
+ idia = self.diagonal_idx
151
+
133
152
  dtype = np.result_type(A, rhs)
134
153
  if rhs.ndim == 1:
135
154
  rhs_loc = np.zeros((rhs.size, 1), dtype=dtype)
@@ -139,11 +158,16 @@ class LDAWrapper(LinearSolver):
139
158
  rhs_loc[:] = rhs
140
159
  sol = np.zeros_like(rhs_loc, dtype=dtype)
141
160
 
142
- # Check linear dependencies in the rhs using modified Gram-Schmidt
161
+ # Diagonal part of the matrix
162
+ sol[idia, ...] = rhs_loc[idia, ...] / A.diagonal()[idia, None]
163
+ rhs_loc[idia, ...] = 0
164
+
165
+ # Non-diagonal part of the matrix
166
+ # Reconstruct using the database using modified Gram-Schmidt
143
167
  for (x, b) in zip(x_data, b_data):
144
168
  assert x.ndim == b.ndim == 1
145
- assert x.size == b.size == rhs_loc.shape[0]
146
- alpha = rhs_loc.T @ b.conj() / (b.conj() @ b)
169
+ # assert x.size == b.size == rhs_loc.shape[0]
170
+ alpha = rhs_loc[isel, ...].T @ b.conj() / (b.conj() @ b)
147
171
 
148
172
  rem_rhs = alpha * b[:, None]
149
173
  add_sol = alpha * x[:, None]
@@ -162,34 +186,41 @@ class LDAWrapper(LinearSolver):
162
186
  warnings.warn('LDAS: Complex vector cannot be added to real solution')
163
187
  continue
164
188
 
165
- rhs_loc -= rem_rhs
166
- sol += add_sol
189
+ rhs_loc[isel, ...] -= rem_rhs
190
+ sol[isel, ...] += add_sol
191
+
192
+ assert np.all(rhs_loc[idia, ...] == 0)
167
193
 
168
194
  # Check tolerance
169
- self._last_rtol = np.ones(rhs_loc.shape[-1]) if len(x_data) == 0 else self.residual(A, sol, rhs if rhs.ndim > 1 else rhs.reshape(-1, 1))
195
+ self._last_rtol = self.residual(A, sol, rhs if rhs.ndim > 1 else rhs.reshape(-1, 1))
170
196
  self._did_solve = self._last_rtol > self.tol
197
+ # If tolerance too large, use the solver for new values
171
198
  if np.any(self._did_solve):
172
199
  if x0 is not None:
173
200
  if x0.ndim == 1:
174
201
  x0_loc = x0.reshape(-1, 1).copy()
175
202
  else:
176
203
  x0_loc = x0[..., self._did_solve].copy()
204
+ x0_loc[idia, ...] = 0
177
205
  for x in x_data:
178
- beta = x0_loc.T @ x.conj() / (x.conj() @ x)
179
- x0_loc -= beta * x
206
+ beta = x0_loc[isel, ...].T @ x.conj() / (x.conj() @ x)
207
+ x0_loc[isel, ...] -= beta * x
180
208
  else:
181
209
  x0_loc = None
182
210
 
183
- # Calculate a new solution
211
+ # Calculate a new solution (for the vectors that have too high residual)
184
212
  xnew = solve_fn(rhs_loc[..., self._did_solve], x0_loc)
185
- self.residual(A, xnew, rhs_loc[..., self._did_solve])
186
- sol[..., self._did_solve] += xnew
213
+
214
+ if isinstance(isel, slice):
215
+ sol[isel, self._did_solve] += xnew[isel, ...]
216
+ else:
217
+ sol[np.ix_(isel, self._did_solve)] += xnew[isel, ...]
187
218
 
188
219
  # Add to database
189
220
  for i in range(xnew.shape[-1]):
190
221
  # Remove all previous components that are already in the database (orthogonalize)
191
- xadd = xnew[..., i]
192
- badd = A @ xadd
222
+ xadd = xnew[isel, i]
223
+ badd = (A @ xnew[..., i])[isel, ...]
193
224
  for x, b in zip(x_data, b_data):
194
225
  beta = badd @ b.conj() / (b.conj() @ b)
195
226
  badd -= beta * b
pymoto/solvers/sparse.py CHANGED
@@ -140,6 +140,9 @@ class SolverSparsePardiso(LinearSolver):
140
140
  Returns:
141
141
  Solution of the system of linear equations, same shape as input b
142
142
  """
143
+ if b.dtype != np.float64: # Only float64 is supported --> this is also done in _check_b, but fails for int8
144
+ warnings.warn(f"Array b's data type was converted from {b.dtype} to float64")
145
+ b = b.astype(np.float64)
143
146
  if trans == 'N':
144
147
  return self._pardiso_solver.solve(self.A, b)
145
148
  elif trans == 'T' or trans == 'H':
@@ -200,6 +203,13 @@ class SolverSparsePardiso(LinearSolver):
200
203
  if v != 0:
201
204
  print(f"{i+1}: {v} ({k})") # i+1 because of 1-based numbering
202
205
 
206
+ def __del__(self):
207
+ try:
208
+ self._pardiso_solver.free_memory(everything=True)
209
+ except ImportError:
210
+ # To prevent ImportError: sys.meta_path is None, Python is likely shutting down
211
+ pass
212
+
203
213
 
204
214
  # ------------------------------------ LU Solver -----------------------------------
205
215
  try:
@@ -1,29 +1,37 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyMOTO
3
- Version: 1.5.0
4
- Summary: A modular approach for topology optimization
5
- Home-page: https://github.com/aatmdelissen/pyMOTO
6
- Author: Arnoud Delissen
7
- Author-email: arnouddelissen+pymoto@gmail.com
8
- License: MIT License
9
- Keywords: topology optimization,generative design,structural,sensitivities,derivatives,framework,modular,blocks,pipeline
3
+ Version: 1.5.1b1
4
+ Summary: A modular approach to topology optimization
5
+ Author-email: Arnoud Delissen <arnouddelissen+pymoto@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pymoto.readthedocs.io/
8
+ Project-URL: Code, https://github.com/aatmdelissen/pyMOTO
9
+ Project-URL: Documentation, https://pymoto.readthedocs.io/
10
+ Project-URL: Issues, https://github.com/aatmdelissen/pyMOTO/issues
11
+ Project-URL: Release notes, https://github.com/aatmdelissen/pyMOTO/releases/latest
12
+ Keywords: topology optimization,generative design,sensitivity analysis,gradients,finite element method,structural optimization,mechanics,engineering
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Natural Language :: English
10
17
  Classifier: Programming Language :: Python :: 3
11
18
  Classifier: Operating System :: OS Independent
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Topic :: Scientific/Engineering
19
+ Requires-Python: >=3.8
14
20
  Description-Content-Type: text/markdown
15
21
  License-File: LICENSE
16
22
  Requires-Dist: numpy
17
- Requires-Dist: scipy >=1.7
23
+ Requires-Dist: scipy>=1.7
18
24
  Requires-Dist: sympy
19
25
  Requires-Dist: matplotlib
20
26
  Provides-Extra: dev
21
- Requires-Dist: flake8 ; extra == 'dev'
22
- Requires-Dist: pytest ; extra == 'dev'
23
- Requires-Dist: cvxopt ; extra == 'dev'
24
- Requires-Dist: scikit-sparse ; extra == 'dev'
25
- Requires-Dist: pypardiso ; extra == 'dev'
26
- Requires-Dist: jax[cpu] ; extra == 'dev'
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Requires-Dist: flake8; extra == "dev"
29
+ Requires-Dist: cvxopt; extra == "dev"
30
+ Requires-Dist: scikit-sparse; extra == "dev"
31
+ Requires-Dist: pypardiso; extra == "dev"
32
+ Requires-Dist: opt-einsum; extra == "dev"
33
+ Requires-Dist: jax[cpu]; extra == "dev"
34
+ Dynamic: license-file
27
35
 
28
36
  [![10.5281/zenodo.8138859](https://zenodo.org/badge/DOI/10.5281/zenodo.8138859.svg)](https://doi.org/10.5281/zenodo.8138859)
29
37
  [![anaconda.org/aatmdelissen/pymoto](https://anaconda.org/aatmdelissen/pymoto/badges/version.svg)](https://anaconda.org/aatmdelissen/pymoto)
@@ -51,7 +59,7 @@ automatically calculated.
51
59
 
52
60
  # Quick start installation
53
61
  1. Make sure you have Python running in some kind of virtual environment (e.g.
54
- [conda](https://docs.conda.io/projects/conda/en/stable/), [miniconda](https://docs.conda.io/en/latest/miniconda.html),
62
+ [uv](https://docs.astral.sh/uv/guides/install-python/), [conda](https://docs.conda.io/projects/conda/en/stable/), [miniconda](https://docs.conda.io/en/latest/miniconda.html),
55
63
  [venv](https://realpython.com/python-virtual-environments-a-primer/))
56
64
  2. Install the pymoto Python package (and its dependencies)
57
65
  - Option A (conda): If you are working with Conda, install by `conda install -c aatmdelissen pymoto`
@@ -0,0 +1,28 @@
1
+ pymoto/__init__.py,sha256=csgRTVQ2B0pIbQZYvpmL4RDcKYVoyc-vaio3UzlaQ-s,2064
2
+ pymoto/core_objects.py,sha256=Amxn-338Igm2BT1O76wr6GgftQ3vgldj-aqHhJodGfA,28579
3
+ pymoto/routines.py,sha256=inz5GbSnvmbD3WKmaNKATTc_SthKsAMPuRc8Jaub4xs,15568
4
+ pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
+ pymoto/common/domain.py,sha256=lstnJ-ITqtjLMQljoc5gN3XVgdFa7gXlMNWiG7IfcR4,18118
6
+ pymoto/common/dyadcarrier.py,sha256=Q0MCIP4b-W_RuYxynvY-voCHfeZ4oF12ykAuVUnmLq4,19433
7
+ pymoto/common/mma.py,sha256=SaFdZhpL5uoQRLWxQrElcjRMzGG4CL3BcPc77z3eMVE,24612
8
+ pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
9
+ pymoto/modules/assembly.py,sha256=o_HZ4bcyK6cMNWVsrmxvv2z6oDgQcrBmB6dquQCWGC8,23216
10
+ pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
11
+ pymoto/modules/complex.py,sha256=B_Obk-ABdV66lEudZ5s8o6qG9NsmYlBsX-PbWvbphhc,4429
12
+ pymoto/modules/filter.py,sha256=6X9FaQMWYZ_TpHVTFiEibzlmAwmSWbydYM93LFrJ0Wo,25490
13
+ pymoto/modules/generic.py,sha256=YzsGZ8J0oLCORt78Bf2p0v4GuqpWRI77NLoCk7gqidw,10666
14
+ pymoto/modules/io.py,sha256=nWhXlDc3l-g-5GEY50yRLbbDAy7pay5Tm92pSgLfUIw,13553
15
+ pymoto/modules/linalg.py,sha256=GIMqcIfVsMib5nIwrhx4sPT6-auLiHBJjbKCAdulLLE,22005
16
+ pymoto/modules/scaling.py,sha256=uq88HHW9rP16XLz7UGc3CNBBpY2Z1glo8yjYxZEnXUg,2327
17
+ pymoto/solvers/__init__.py,sha256=9JUeD2SgZbkYFullA7s7s6SuAVv0onqAqJ8hFvNOs2g,1033
18
+ pymoto/solvers/auto_determine.py,sha256=X8MEG7h6jLfAV1inpja45_-suG8qQFMfLMDfW2ryQqQ,5134
19
+ pymoto/solvers/dense.py,sha256=9fKPCwNxRKAEk5k1A7fdLrr9ngeVssGlw-sbjWCm4iU,11235
20
+ pymoto/solvers/iterative.py,sha256=hfMRw2LChupr4sQf8qUKG6OHjhpeVAp6C9N7M4-845M,13179
21
+ pymoto/solvers/matrix_checks.py,sha256=bbrfjpTSWWnuQW3xY0_CYE8yrh5gA9K5b1LzHEOFAxI,1663
22
+ pymoto/solvers/solvers.py,sha256=Srn44oRonZIjJq-XVpJY9KoTWV0R7zBWFaSmXKWsUCw,11870
23
+ pymoto/solvers/sparse.py,sha256=BPw-2Q4lgbmtjCl8eLEmDdWhjqD7RSLrVYy_kQN_LdU,17144
24
+ pymoto-1.5.1b1.dist-info/licenses/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
25
+ pymoto-1.5.1b1.dist-info/METADATA,sha256=Xt0HY8PX1nI1W1_xiWZNkin3vVXNenx-mvhMhn0F2wY,5510
26
+ pymoto-1.5.1b1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ pymoto-1.5.1b1.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
28
+ pymoto-1.5.1b1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,29 +0,0 @@
1
- pymoto/__init__.py,sha256=YLMAiO2PZHAC6nYWXVh03rhZnZkc_Rc2z7SGQj1T8I4,2058
2
- pymoto/core_objects.py,sha256=88AOo041wrcRSPoLCRwBXUoYANGX-b2SAA0Nuf6sn2Y,25252
3
- pymoto/routines.py,sha256=yjvcQDcWU47ZM6ZpZoX8VwrJoN9JDO_25DSa9oVcKec,15582
4
- pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
- pymoto/common/domain.py,sha256=-eFuYRLehQ17Ai-cV59f4I9FbEM-DJAj6kjjVfj31X0,18120
6
- pymoto/common/dyadcarrier.py,sha256=VwMbqPr0NMDPfpsH0BwvXp8M1dmh8ijFDpF6yoyTmto,19394
7
- pymoto/common/mma.py,sha256=Pof3clOHA8PG51TmUjs11dkjSP96kovZjsPv62tI2Ec,24055
8
- pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
9
- pymoto/modules/assembly.py,sha256=quuR8QpB2w-O0zly-xS6PK6wZQMY6S5TWh15Y9wuh14,22974
10
- pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
11
- pymoto/modules/complex.py,sha256=B_Obk-ABdV66lEudZ5s8o6qG9NsmYlBsX-PbWvbphhc,4429
12
- pymoto/modules/filter.py,sha256=6X9FaQMWYZ_TpHVTFiEibzlmAwmSWbydYM93LFrJ0Wo,25490
13
- pymoto/modules/generic.py,sha256=YzsGZ8J0oLCORt78Bf2p0v4GuqpWRI77NLoCk7gqidw,10666
14
- pymoto/modules/io.py,sha256=LcFvJ-cPgg5ee-aag8kaxHw5RzQ-ggxOM5jk7PeJ1r8,13140
15
- pymoto/modules/linalg.py,sha256=BNkih4nvvkYuQpm4bG5U38dAjkfD5EFi3MjANpBEPPI,21927
16
- pymoto/modules/scaling.py,sha256=uq88HHW9rP16XLz7UGc3CNBBpY2Z1glo8yjYxZEnXUg,2327
17
- pymoto/solvers/__init__.py,sha256=9JUeD2SgZbkYFullA7s7s6SuAVv0onqAqJ8hFvNOs2g,1033
18
- pymoto/solvers/auto_determine.py,sha256=X8MEG7h6jLfAV1inpja45_-suG8qQFMfLMDfW2ryQqQ,5134
19
- pymoto/solvers/dense.py,sha256=9fKPCwNxRKAEk5k1A7fdLrr9ngeVssGlw-sbjWCm4iU,11235
20
- pymoto/solvers/iterative.py,sha256=CIxJHjGnCaIjXbtO2NxV60yeDpcCbSD6Bp0xR-7vOf0,12944
21
- pymoto/solvers/matrix_checks.py,sha256=bbrfjpTSWWnuQW3xY0_CYE8yrh5gA9K5b1LzHEOFAxI,1663
22
- pymoto/solvers/solvers.py,sha256=RwHjZYYlE3oA0U9k7ukla2gOdmq57rSSJQvHqjaM7JU,10626
23
- pymoto/solvers/sparse.py,sha256=w8XBlFBIfOpNnfRdLWhLzzqtD8YVxMnDBuhIabFfQQc,16664
24
- pyMOTO-1.5.0.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
25
- pyMOTO-1.5.0.dist-info/METADATA,sha256=hC38SdgeKEK5NkNDh-gwc4Gz2JOFSymJB-eAMXl7HX4,5006
26
- pyMOTO-1.5.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
27
- pyMOTO-1.5.0.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
28
- pyMOTO-1.5.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
- pyMOTO-1.5.0.dist-info/RECORD,,
@@ -1 +0,0 @@
1
-