pyMOTO 1.5.0__tar.gz → 1.5.1__tar.gz

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 (56) hide show
  1. {pymoto-1.5.0 → pymoto-1.5.1}/PKG-INFO +2 -2
  2. {pymoto-1.5.0 → pymoto-1.5.1}/README.md +1 -1
  3. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/PKG-INFO +2 -2
  4. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/__init__.py +1 -1
  5. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/common/domain.py +1 -0
  6. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/common/dyadcarrier.py +1 -1
  7. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/common/mma.py +11 -3
  8. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/core_objects.py +98 -50
  9. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/assembly.py +17 -13
  10. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/io.py +17 -5
  11. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/linalg.py +14 -14
  12. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/routines.py +2 -2
  13. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/iterative.py +20 -16
  14. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/solvers.py +44 -13
  15. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/sparse.py +10 -0
  16. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_dyadcarrier.py +107 -91
  17. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_module_eigensolve.py +27 -40
  18. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_solvers_dense.py +10 -5
  19. pymoto-1.5.1/tests/test_solvers_iterative.py +98 -0
  20. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_solvers_sparse.py +1 -0
  21. pymoto-1.5.0/tests/test_solvers_iterative.py +0 -96
  22. {pymoto-1.5.0 → pymoto-1.5.1}/LICENSE +0 -0
  23. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/SOURCES.txt +0 -0
  24. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/dependency_links.txt +0 -0
  25. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/requires.txt +0 -0
  26. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/top_level.txt +0 -0
  27. {pymoto-1.5.0 → pymoto-1.5.1}/pyMOTO.egg-info/zip-safe +0 -0
  28. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/aggregation.py +0 -0
  29. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/autodiff.py +0 -0
  30. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/complex.py +0 -0
  31. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/filter.py +0 -0
  32. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/generic.py +0 -0
  33. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/modules/scaling.py +0 -0
  34. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/__init__.py +0 -0
  35. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/auto_determine.py +0 -0
  36. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/dense.py +0 -0
  37. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/solvers/matrix_checks.py +0 -0
  38. {pymoto-1.5.0 → pymoto-1.5.1}/pymoto/utils.py +0 -0
  39. {pymoto-1.5.0 → pymoto-1.5.1}/pyproject.toml +0 -0
  40. {pymoto-1.5.0 → pymoto-1.5.1}/setup.cfg +0 -0
  41. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_aggregration.py +0 -0
  42. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_assembly.py +0 -0
  43. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_automod.py +0 -0
  44. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_complex.py +0 -0
  45. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_core.py +0 -0
  46. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_domain.py +0 -0
  47. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_element_operations.py +0 -0
  48. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_elmatrices.py +0 -0
  49. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_filter.py +0 -0
  50. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_linsolve_sparse.py +0 -0
  51. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_module_concatsignal.py +0 -0
  52. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_module_einsum.py +0 -0
  53. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_module_mathgeneral.py +0 -0
  54. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_scaling.py +0 -0
  55. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_static_condenstation.py +0 -0
  56. {pymoto-1.5.0 → pymoto-1.5.1}/tests/test_thermo_mech.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyMOTO
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -51,7 +51,7 @@ automatically calculated.
51
51
 
52
52
  # Quick start installation
53
53
  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),
54
+ [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
55
  [venv](https://realpython.com/python-virtual-environments-a-primer/))
56
56
  2. Install the pymoto Python package (and its dependencies)
57
57
  - Option A (conda): If you are working with Conda, install by `conda install -c aatmdelissen pymoto`
@@ -24,7 +24,7 @@ automatically calculated.
24
24
 
25
25
  # Quick start installation
26
26
  1. Make sure you have Python running in some kind of virtual environment (e.g.
27
- [conda](https://docs.conda.io/projects/conda/en/stable/), [miniconda](https://docs.conda.io/en/latest/miniconda.html),
27
+ [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),
28
28
  [venv](https://realpython.com/python-virtual-environments-a-primer/))
29
29
  2. Install the pymoto Python package (and its dependencies)
30
30
  - Option A (conda): If you are working with Conda, install by `conda install -c aatmdelissen pymoto`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyMOTO
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -51,7 +51,7 @@ automatically calculated.
51
51
 
52
52
  # Quick start installation
53
53
  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),
54
+ [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
55
  [venv](https://realpython.com/python-virtual-environments-a-primer/))
56
56
  2. Install the pymoto Python package (and its dependencies)
57
57
  - Option A (conda): If you are working with Conda, install by `conda install -c aatmdelissen pymoto`
@@ -1,4 +1,4 @@
1
- __version__ = '1.5.0'
1
+ __version__ = '1.5.1'
2
2
 
3
3
  from .common.domain import DomainDefinition
4
4
 
@@ -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
 
@@ -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]
@@ -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
+
@@ -186,14 +186,14 @@ class SignalSlice(Signal):
186
186
  The sliced values are referenced to their original source Signal, such that they can be used and updated in modules.
187
187
  This means that updating the values in this SignalSlice changes the data in its source Signal.
188
188
  """
189
- def __init__(self, orig_signal, sl, tag=None):
190
- self.orig_signal = orig_signal
189
+ def __init__(self, base, sl, tag=None):
190
+ self.base = base
191
191
  self.slice = sl
192
192
  self.keep_alloc = False # Allocation must be False because sensitivity cannot be assigned with [] operator
193
193
 
194
194
  # for s in slice:
195
195
  if tag is None:
196
- self.tag = f"{self.orig_signal.tag}[{fmt_slice(self.slice)}]"
196
+ self.tag = f"{self.base.tag}[{fmt_slice(self.slice)}]"
197
197
  else:
198
198
  self.tag = tag
199
199
 
@@ -203,7 +203,7 @@ class SignalSlice(Signal):
203
203
  @property
204
204
  def state(self):
205
205
  try:
206
- return None if self.orig_signal.state is None else self.orig_signal.state[self.slice]
206
+ return None if self.base.state is None else self.base.state[self.slice]
207
207
  except Exception as e:
208
208
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
209
209
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (getter). Signal details:" +
@@ -212,7 +212,7 @@ class SignalSlice(Signal):
212
212
  @state.setter
213
213
  def state(self, new_state):
214
214
  try:
215
- self.orig_signal.state[self.slice] = new_state
215
+ self.base.state[self.slice] = new_state
216
216
  except Exception as e:
217
217
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
218
218
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (setter). Signal details:" +
@@ -221,7 +221,7 @@ class SignalSlice(Signal):
221
221
  @property
222
222
  def sensitivity(self):
223
223
  try:
224
- return None if self.orig_signal.sensitivity is None else self.orig_signal.sensitivity[self.slice]
224
+ return None if self.base.sensitivity is None else self.base.sensitivity[self.slice]
225
225
  except Exception as e:
226
226
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
227
227
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.sensitivity (getter). Signal details:" +
@@ -230,26 +230,55 @@ class SignalSlice(Signal):
230
230
  @sensitivity.setter
231
231
  def sensitivity(self, new_sens):
232
232
  try:
233
- if self.orig_signal.sensitivity is None:
233
+ if self.base.sensitivity is None:
234
+ # Initialize sensitivity of base-signal
234
235
  if new_sens is None:
235
236
  return # Sensitivity doesn't need to be initialized when it is set to None
236
237
  try:
237
- self.orig_signal.sensitivity = self.orig_signal.state * 0 # Make a new copy with 0 values
238
+ self.base.sensitivity = self.base.state * 0 # Make a new copy with 0 values
238
239
  except TypeError:
239
- if self.orig_signal.state is None:
240
+ if self.base.state is None:
240
241
  raise TypeError("Could not initialize sensitivity because state is not set" + self._err_str())
241
242
  else:
242
- raise TypeError(f"Could not initialize sensitivity for type \'{type(self.orig_signal.state).__name__}\'")
243
+ raise TypeError(f"Could not initialize sensitivity for type \'{type(self.base.state).__name__}\'")
243
244
 
244
245
  if new_sens is None:
245
246
  new_sens = 0 # reset() uses this
246
247
 
247
- self.orig_signal.sensitivity[self.slice] = new_sens
248
+ self.base.sensitivity[self.slice] = new_sens
248
249
  except Exception as e:
249
250
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
250
251
  raise type(e)(str(e) + "\n\t| Above error was raised in SignalSlice.state (setter). Signal details:" +
251
252
  self._err_str()).with_traceback(sys.exc_info()[2])
252
253
 
254
+ def add_sensitivity(self, ds: Any):
255
+ """ Add a new term to internal sensitivity """
256
+ try:
257
+ if ds is None:
258
+ return
259
+ if self.base.sensitivity is None:
260
+ self.base.sensitivity = self.base.state * 0
261
+ # self.sensitivity = copy.deepcopy(ds)
262
+
263
+ if hasattr(self.sensitivity, "add_sensitivity"):
264
+ # Allow user to implement a custom add_sensitivity function instead of __iadd__
265
+ self.sensitivity.add_sensitivity(ds)
266
+ else:
267
+ self.sensitivity += ds
268
+ return self
269
+ except TypeError:
270
+ if isinstance(ds, type(self.sensitivity)):
271
+ raise TypeError(
272
+ f"Cannot add to the sensitivity with type '{type(self.sensitivity).__name__}'" + self._err_str())
273
+ else:
274
+ raise TypeError(
275
+ f"Adding wrong type '{type(ds).__name__}' to the sensitivity '{type(self.sensitivity).__name__}'" + self._err_str())
276
+ except ValueError:
277
+ sens_shape = self.sensitivity.shape if hasattr(self.sensitivity, 'shape') else ()
278
+ ds_shape = ds.shape if hasattr(ds, 'shape') else ()
279
+ raise ValueError(
280
+ f"Cannot add argument of shape {ds_shape} to the sensitivity of shape {sens_shape}" + self._err_str()) from None
281
+
253
282
  def reset(self, keep_alloc: bool = None):
254
283
  """ Reset the sensitivities to zero or None
255
284
  This must be called to clear internal memory of subsequent sensitivity calculations.
@@ -408,7 +437,7 @@ class Module(ABC, RegisteredClass):
408
437
  >> Module([inputs])
409
438
 
410
439
  Using keywords:
411
- >> Module(sig_in=[inputs], sig_out=[outputs]
440
+ >> Module(sig_in=[inputs], sig_out=[outputs])
412
441
  """
413
442
 
414
443
  def _err_str(self, module_signature: bool = True, init: bool = True, fn=None):
@@ -556,67 +585,74 @@ class Module(ABC, RegisteredClass):
556
585
  class Network(Module):
557
586
  """ Binds multiple Modules together as one Module
558
587
 
588
+ Initialize a network with a number of modules that should be executed consecutively
559
589
  >> Network(module1, module2, ...)
560
590
 
561
591
  >> Network([module1, module2, ...])
562
592
 
563
593
  >> Network((module1, module2, ...))
564
594
 
595
+ Modules can also be constructed using a dictionary based on strings
565
596
  >> Network([ {type="module1", sig_in=[sig1, sig2], sig_out=[sig3]},
566
597
  {type="module2", sig_in=[sig3], sig_out=[sig4]} ])
567
598
 
599
+ Appending modules to a network will output the signals automatically
600
+ >> fn = Network()
601
+ >> s_out = fn.append(module1)
602
+
603
+ Args:
604
+ print_timing: Print timing of each module inside this Network
568
605
  """
569
606
  def __init__(self, *args, print_timing=False):
607
+ super().__init__()
570
608
  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
-
609
+ self.mods = [] # Empty module list
610
+ self.append(*args) # Append to module list
597
611
  self.print_timing = print_timing
598
612
 
599
- def timefn(self, fn, prefix='Evaluation'):
613
+ def timefn(self, fn, name=None):
600
614
  start_t = time.time()
601
615
  fn()
602
616
  duration = time.time() - start_t
603
- if duration > .5:
604
- print(f"{prefix} {fn} took {time.time() - start_t} s")
617
+ if name is None:
618
+ name = f"{fn}"
619
+ if isinstance(self.print_timing, bool):
620
+ tmin = 0.0
621
+ else:
622
+ tmin = self.print_timing
623
+ if duration > tmin:
624
+ print(f"{name} took {time.time() - start_t} s")
605
625
 
606
626
  def response(self):
607
- if self.print_timing:
608
- [self.timefn(b.response, prefix='Response') for b in self.mods]
627
+ if self.print_timing is not False:
628
+ start_t = time.time()
629
+ [self.timefn(m.response, name=f"-- Response of \"{type(m).__name__}\"") for m in self.mods]
630
+ duration = time.time() - start_t
631
+ if isinstance(self.print_timing, bool):
632
+ tmin = 0.0
633
+ else:
634
+ tmin = self.print_timing
635
+ if duration > tmin:
636
+ print(f"-- TOTAL Response took {time.time() - start_t} s")
609
637
  else:
610
- [b.response() for b in self.mods]
638
+ [m.response() for m in self.mods]
611
639
 
612
640
  def sensitivity(self):
613
- if self.print_timing:
614
- [self.timefn(b.sensitivity, 'Sensitivity') for b in reversed(self.mods)]
641
+ if self.print_timing is not False:
642
+ start_t = time.time()
643
+ [self.timefn(m.sensitivity, name=f"-- Sensitivity of \"{type(m).__name__}\"") for m in reversed(self.mods)]
644
+ duration = time.time() - start_t
645
+ if isinstance(self.print_timing, bool):
646
+ tmin = 0.0
647
+ else:
648
+ tmin = self.print_timing
649
+ if duration > tmin:
650
+ print(f"-- TOTAL Sensitivity took {time.time() - start_t} s")
615
651
  else:
616
- [b.sensitivity() for b in reversed(self.mods)]
652
+ [m.sensitivity() for m in reversed(self.mods)]
617
653
 
618
654
  def reset(self):
619
- [b.reset() for b in reversed(self.mods)]
655
+ [m.reset() for m in reversed(self.mods)]
620
656
 
621
657
  def _response(self, *args):
622
658
  pass # Unused
@@ -636,13 +672,25 @@ class Network(Module):
636
672
  def __iter__(self):
637
673
  return iter(self.mods)
638
674
 
675
+ def __call__(self, *args):
676
+ return self.append(*args)
677
+
639
678
  def append(self, *newmods):
640
679
  modlist = _parse_to_list(*newmods)
680
+ if len(modlist) == 0:
681
+ return
641
682
 
642
683
  # Check if the blocks are initialized, else create them
684
+ for i, m in enumerate(modlist):
685
+ if isinstance(m, dict):
686
+ exclude_keys = ['type']
687
+ b_ex = {k: m[k] for k in set(list(m.keys())) - set(exclude_keys)}
688
+ modlist[i] = Module.create(m['type'], **b_ex)
689
+
690
+ # Check validity of modules
643
691
  for i, m in enumerate(modlist):
644
692
  if not _is_valid_module(m):
645
- raise TypeError(f"Argument #{i} is not a valid module, type=\'{type(mod).__name__}\'.")
693
+ raise TypeError(f"Argument #{i} is not a valid module, type=\'{type(m).__name__}\'.")
646
694
 
647
695
  # Obtain the internal blocks
648
696
  self.mods.extend(modlist)
@@ -657,4 +705,4 @@ class Network(Module):
657
705
  self.sig_in = _parse_to_list(in_unique)
658
706
  self.sig_out = _parse_to_list(all_out)
659
707
 
660
- return modlist[-1].sig_out[0] if len(modlist[-1].sig_out) == 1 else modlist[-1].sig_out # Returns the output signal
708
+ 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
@@ -202,15 +202,16 @@ class PlotIter(FigModule):
202
202
  show (bool): Show the figure on the screen
203
203
  ylim: Provide y-axis limits for the plot
204
204
  """
205
- def _prepare(self, ylim=None):
205
+ def _prepare(self, ylim=None, log_scale=False):
206
206
  self.minlim = 1e+200
207
207
  self.maxlim = -1e+200
208
208
  self.ylim = ylim
209
+ self.log_scale = log_scale
209
210
 
210
211
  def _response(self, *args):
211
212
  if not hasattr(self, 'ax'):
212
213
  self.ax = self.fig.add_subplot(111)
213
- self.ax.set_yscale('linear')
214
+ self.ax.set_yscale('linear' if not self.log_scale else 'log')
214
215
  self.ax.set_xlabel("Iteration")
215
216
 
216
217
  if not hasattr(self, 'line'):
@@ -233,13 +234,24 @@ class PlotIter(FigModule):
233
234
  self.minlim = min(self.minlim, np.min(xadd))
234
235
  self.maxlim = max(self.maxlim, np.max(xadd))
235
236
 
236
- dy = max((self.maxlim - self.minlim)*0.05, sys.float_info.min)
237
-
238
237
  self.ax.set_xlim([-0.5, self.iter+0.5])
239
238
  if self.ylim is not None:
240
239
  self.ax.set_ylim(self.ylim)
241
240
  elif np.isfinite(self.minlim) and np.isfinite(self.maxlim):
242
- self.ax.set_ylim([self.minlim - dy, self.maxlim + dy])
241
+ if self.log_scale:
242
+ dy = (np.log10(self.maxlim) - np.log10(self.minlim))*0.05
243
+ ll = 10**(np.log10(self.minlim) - dy)
244
+ ul = 10**(np.log10(self.maxlim) + dy)
245
+ else:
246
+ dy = (self.maxlim - self.minlim)*0.05
247
+ ll = self.minlim - dy
248
+ ul = self.maxlim + dy
249
+
250
+ if ll == ul:
251
+ dy = abs(np.nextafter(abs(ll), 1) - abs(ll))
252
+ ll = ll - 1e5*dy
253
+ ul = ul + 1e5*dy
254
+ self.ax.set_ylim([ll, ul])
243
255
 
244
256
  self._update_fig()
245
257
 
@@ -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):
@@ -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