femagtools 1.8.1__py3-none-any.whl → 1.8.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 (49) hide show
  1. femagtools/__init__.py +1 -1
  2. femagtools/dxfsl/area.py +110 -1
  3. femagtools/dxfsl/areabuilder.py +93 -45
  4. femagtools/dxfsl/conv.py +5 -0
  5. femagtools/dxfsl/converter.py +85 -27
  6. femagtools/dxfsl/fslrenderer.py +5 -4
  7. femagtools/dxfsl/functions.py +14 -6
  8. femagtools/dxfsl/geom.py +135 -149
  9. femagtools/dxfsl/journal.py +1 -1
  10. femagtools/dxfsl/machine.py +161 -9
  11. femagtools/dxfsl/shape.py +46 -1
  12. femagtools/dxfsl/svgparser.py +1 -1
  13. femagtools/dxfsl/symmetry.py +143 -38
  14. femagtools/femag.py +64 -61
  15. femagtools/fsl.py +15 -12
  16. femagtools/isa7.py +3 -2
  17. femagtools/machine/__init__.py +5 -4
  18. femagtools/machine/afpm.py +79 -33
  19. femagtools/machine/effloss.py +29 -18
  20. femagtools/machine/sizing.py +192 -13
  21. femagtools/machine/sm.py +34 -36
  22. femagtools/machine/utils.py +2 -2
  23. femagtools/mcv.py +58 -29
  24. femagtools/model.py +4 -3
  25. femagtools/multiproc.py +79 -80
  26. femagtools/parstudy.py +11 -5
  27. femagtools/plot/nc.py +2 -2
  28. femagtools/semi_fea.py +108 -0
  29. femagtools/templates/basic_modpar.mako +0 -3
  30. femagtools/templates/fe-contr.mako +18 -18
  31. femagtools/templates/ld_lq_fast.mako +3 -0
  32. femagtools/templates/mult_cal_fast.mako +3 -0
  33. femagtools/templates/pm_sym_f_cur.mako +4 -1
  34. femagtools/templates/pm_sym_fast.mako +3 -0
  35. femagtools/templates/pm_sym_loss.mako +3 -0
  36. femagtools/templates/psd_psq_fast.mako +3 -0
  37. femagtools/templates/torq_calc.mako +3 -0
  38. femagtools/tks.py +23 -20
  39. femagtools/zmq.py +213 -0
  40. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/METADATA +3 -3
  41. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/RECORD +49 -47
  42. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/WHEEL +1 -1
  43. tests/test_afpm.py +15 -6
  44. tests/test_femag.py +1 -1
  45. tests/test_fsl.py +4 -4
  46. tests/test_mcv.py +21 -15
  47. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/LICENSE +0 -0
  48. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/entry_points.txt +0 -0
  49. {femagtools-1.8.1.dist-info → femagtools-1.8.3.dist-info}/top_level.txt +0 -0
femagtools/femag.py CHANGED
@@ -27,6 +27,7 @@ import femagtools.fsl
27
27
  import femagtools.config
28
28
  import femagtools.ecloss
29
29
  import femagtools.forcedens
30
+ import femagtools.zmq
30
31
 
31
32
  from femagtools import ntib
32
33
 
@@ -180,15 +181,22 @@ class BaseFemag(object):
180
181
  def copy_winding_file(self, name, wdg):
181
182
  wdg.write(name, self.workdir)
182
183
 
183
- def copy_magnetizing_curves(self, model, dir=None, recsin=''):
184
+ def copy_magnetizing_curves(self, model, dir=None, recsin='', feloss=''):
184
185
  """extract mc names from model and write files into workdir or dir if given
185
186
 
186
187
  Return:
187
188
  list of extracted mc names (:obj:`list`)
188
189
  """
189
190
  dest = dir if dir else self.workdir
191
+ if isinstance(feloss, (int, float)):
192
+ try:
193
+ feloss = {1: 'jordan', 11: 'bertotti'}[int(feloss)]
194
+ except KeyError:
195
+ feloss = ''
190
196
  return [self.magnetizingCurves.writefile(m[0], dest,
191
- fillfac=m[1], recsin=recsin)
197
+ fillfac=m[1],
198
+ recsin=recsin,
199
+ feloss=feloss)
192
200
  for m in model.set_magcurves(
193
201
  self.magnetizingCurves, self.magnets)]
194
202
 
@@ -212,9 +220,11 @@ class BaseFemag(object):
212
220
  self.model = femagtools.model.MachineModel(machine)
213
221
  self.modelname = self.model.name
214
222
  recsin = ''
223
+ feloss = ''
215
224
  if simulation:
216
225
  recsin = simulation.get('recsin', '')
217
- self.copy_magnetizing_curves(self.model, recsin=recsin)
226
+ feloss = simulation.get('calc_fe_loss', '')
227
+ self.copy_magnetizing_curves(self.model, recsin=recsin, feloss=feloss)
218
228
  try:
219
229
  if 'wdgdef' in self.model.winding:
220
230
  self.model.winding['wdgfile'] = self.create_wdg_def(
@@ -429,12 +439,15 @@ class BaseFemag(object):
429
439
  """
430
440
  Read the modelname from the Femag Log file
431
441
  """
432
- with open(os.path.join(self.workdir, 'FEMAG-FSL.log')) as f:
433
- for l in f:
434
- if l.startswith('New model') or l.startswith('Load model'):
435
- return l.split('"')[1]
442
+ try:
443
+ with open(os.path.join(self.workdir, 'FEMAG-FSL.log')) as f:
444
+ for l in f:
445
+ if l.startswith('New model') or l.startswith('Load model'):
446
+ return l.split('"')[1]
447
+ except FileNotFoundError:
448
+ pass
436
449
 
437
- raise ValueError(f"Model not found in {self.workdir}/'FEMAG-FSL.log'")
450
+ return list(pathlib.Path(self.workdir).glob('*.PROT'))[0].stem
438
451
 
439
452
  def readResult(self, simulation, bch=None):
440
453
  if simulation:
@@ -715,55 +728,6 @@ class FemagTask(threading.Thread):
715
728
  self.returncode = self.proc.wait()
716
729
 
717
730
 
718
- class SubscriberTask(threading.Thread):
719
- def __init__(self, port, host, notify):
720
- threading.Thread.__init__(self)
721
- context = zmq.Context.instance()
722
- self.subscriber = context.socket(zmq.SUB)
723
- if not host:
724
- host = 'localhost'
725
- self.subscriber.connect(f'tcp://{host}:{port}')
726
- self.subscriber.setsockopt(zmq.SUBSCRIBE, b'')
727
- self.controller = zmq.Context.instance().socket(zmq.PULL)
728
- self.controller_url = 'inproc://publisher'
729
- self.controller.bind(self.controller_url)
730
- self.poller = zmq.Poller()
731
- self.poller.register(self.subscriber, zmq.POLLIN)
732
- self.poller.register(self.controller, zmq.POLLIN)
733
- self.logger = logger
734
- self.notify = notify
735
-
736
- def stop(self):
737
- socket = zmq.Context.instance().socket(zmq.PUSH)
738
- socket.connect(self.controller_url)
739
- socket.send(b"quit")
740
- socket.close()
741
-
742
- def run(self):
743
- self.logger.info("subscriber is ready")
744
- while True:
745
- socks = dict(self.poller.poll())
746
- if socks.get(self.subscriber) == zmq.POLLIN:
747
- try:
748
- response = self.subscriber.recv_multipart()
749
- # Sometimes femag send messages with only len = 1. These messages must be ignored
750
- if len(response) < 2:
751
- continue
752
- self.notify([s.decode('latin1') for s in response])
753
-
754
- except Exception:
755
- self.logger.error(
756
- "error in subscription message processing", exc_info=True)
757
-
758
- if socks.get(self.controller) == zmq.POLLIN:
759
- req = self.controller.recv()
760
- self.logger.info("subscriber %s", req)
761
- break
762
- self.subscriber.close()
763
- self.controller.close()
764
- self.logger.debug("subscriber stopped")
765
-
766
-
767
731
  class ZmqFemag(BaseFemag):
768
732
  """Invoke and control execution of FEMAG with ZeroMQ
769
733
 
@@ -838,8 +802,8 @@ class ZmqFemag(BaseFemag):
838
802
  """attaches a notify function"""
839
803
  logger.info("Subscribe on '%s' port %d", self.femaghost, self.port+1)
840
804
  if self.subscriber is None:
841
- self.subscriber = SubscriberTask(
842
- self.port+1, self.femaghost, notify)
805
+ self.subscriber = femagtools.zmq.SubscriberTask(
806
+ port=self.port+1, host=self.femaghost, notify=notify)
843
807
  self.subscriber.start()
844
808
  else:
845
809
  # reattach?
@@ -1149,6 +1113,38 @@ class ZmqFemag(BaseFemag):
1149
1113
  logger.warning(response[0])
1150
1114
  return [s.decode('latin1') for s in response]
1151
1115
 
1116
+ def airgap_flux_density(self, pmod):
1117
+ # try to read bag.dat
1118
+ agr = self.getfile("bag.dat")
1119
+ status = json.loads(agr[0])['status']
1120
+ if status == 'ok':
1121
+ datfile = os.path.join(self.workdir, 'bag.dat')
1122
+ with open(datfile, 'wb') as bagfile:
1123
+ bagfile.write(agr[1])
1124
+ agi = ag.read(datfile, pmod)
1125
+ else:
1126
+ import numpy as np
1127
+ # try to read model file (TODO download with getfile)
1128
+ nc = self.read_nc()
1129
+ ag_elmnts = nc.airgap_center_elements
1130
+ logger.info("Airgap elements %d scale_factor %f",
1131
+ len(ag_elmnts), nc.scale_factor())
1132
+ if len(ag_elmnts) < 1:
1133
+ raise ValueError("Missing airgap elements")
1134
+ scf = 360/nc.scale_factor()/ag_elmnts[-1].center[0]
1135
+ pos = np.array([e.center[0]*scf for e in ag_elmnts])
1136
+ bxy = np.array([e.flux_density() for e in ag_elmnts]).T
1137
+ if np.max(bxy[0]) > np.max(bxy[1]):
1138
+ agi = ag.fft(pos, bxy[0], pmod)
1139
+ else:
1140
+ agi = ag.fft(pos, bxy[1], pmod)
1141
+ return dict(Bamp=agi['Bamp'],
1142
+ phi0=agi['phi0'],
1143
+ angle=agi['pos'],
1144
+ angle_fft=agi['pos'],
1145
+ B=agi['B'],
1146
+ B_fft=agi['B_fft'])
1147
+
1152
1148
  def interrupt(self):
1153
1149
  """send push message to control port to stop current calculation"""
1154
1150
  context = zmq.Context.instance()
@@ -1165,7 +1161,7 @@ class ZmqFemag(BaseFemag):
1165
1161
  wdg.write(name, self.workdir)
1166
1162
  self.upload(os.path.join(self.workdir, name+'.WID'))
1167
1163
 
1168
- def copy_magnetizing_curves(self, model, dir=None, recsin=''):
1164
+ def copy_magnetizing_curves(self, model, dir=None, recsin='', feloss=''):
1169
1165
  """extract mc names from model and write files into workdir or dir if given
1170
1166
  and upload to Femag
1171
1167
 
@@ -1175,9 +1171,16 @@ class ZmqFemag(BaseFemag):
1175
1171
  dest = dir if dir else self.workdir
1176
1172
  mc_names = [m for m in model.set_magcurves(
1177
1173
  self.magnetizingCurves, self.magnets)]
1174
+ if isinstance(feloss, (int, float)):
1175
+ try:
1176
+ feloss = {1: 'jordan', 11: 'bertotti'}[int(feloss)]
1177
+ except KeyError:
1178
+ feloss = ''
1178
1179
  for m in mc_names:
1179
1180
  f = self.magnetizingCurves.writefile(m[0], dest,
1180
- fillfac=m[1], recsin=recsin)
1181
+ fillfac=m[1],
1182
+ recsin=recsin,
1183
+ feloss=feloss)
1181
1184
  self.upload(os.path.join(dest, f))
1182
1185
  return mc_names
1183
1186
 
femagtools/fsl.py CHANGED
@@ -158,13 +158,13 @@ class Builder:
158
158
  if templ == 'statorFsl':
159
159
  # obsolete
160
160
  th_props = [' ']
161
- try:
161
+ try:
162
162
  th_props = [f'stator_density = {model.stator["density"]}',
163
163
  f'stator_thcond = {model.stator["thcond"]}',
164
164
  f'stator_thcap = {model.stator["thcap"]}',
165
165
  ]
166
- except:
167
- pass
166
+ except:
167
+ pass
168
168
  if 'parameter' in model.stator['statorFsl']:
169
169
  return self.render_template(
170
170
  model.stator['statorFsl']['content_template'],
@@ -226,20 +226,20 @@ class Builder:
226
226
  self.fsl_rotor = True
227
227
  # obsolete
228
228
  th_props = [' ']
229
- try:
229
+ try:
230
230
  logger.info(model.magnet)
231
231
  th_props = [f'rotor_density = {model["magnet"]["density"]}',
232
232
  f'rotor_thcond = {model["magnet"]["thcond"]}',
233
233
  f'rotor_thcap = {model["magnet"]["thcap"]}'
234
234
  ]
235
- except:
236
- pass
235
+ except:
236
+ pass
237
237
  if 'parameter' in model.magnet['magnetFsl']:
238
238
  return mcv + self.render_template(
239
239
  model.magnet['magnetFsl']['content_template'],
240
240
  model.magnet['magnetFsl']['parameter']) + th_props
241
241
  elif model.magnet['magnetFsl'].get('content'):
242
- return mcv + model.magnet['magnetFsl']['content'].split('\n')
242
+ return mcv + model.magnet['magnetFsl']['content'].split('\n')
243
243
  if isinstance(model.magnet['magnetFsl']
244
244
  ['content_template'], str):
245
245
  with open(model.magnet['magnetFsl']
@@ -248,7 +248,7 @@ class Builder:
248
248
  else:
249
249
  templ = model.magnet['magnetFsl']['content_template']
250
250
  return mcv + self.render_template(
251
- '\n'.join(templ),
251
+ '\n'.join(templ),
252
252
  model.magnet['magnetFsl'])
253
253
 
254
254
  templ = model.magnettype()
@@ -364,8 +364,8 @@ class Builder:
364
364
  """return connect_model if rotating machine and incomplete model
365
365
  (Note: femag bug with connect model)"
366
366
  """
367
- if (model.get('move_action') == 0 and (
368
- model.connect_full or
367
+ if (model.connect_full or (
368
+ model.get('move_action', 0) == 0 and
369
369
  model.stator['num_slots'] > model.stator['num_slots_gen'])):
370
370
  fslcmds = ['pre_models("connect_models")\n']
371
371
  if 'thcond' in model.stator:
@@ -556,7 +556,7 @@ class Builder:
556
556
  if 'fsl_rotor' in conv:
557
557
  self.fsl_rotor = True
558
558
  th_props = ['']
559
- logger.info(model['magnet'])
559
+ logger.debug(model['magnet'])
560
560
  if hasattr(model, 'magnet'):
561
561
  if model['magnet'].get('thcond', 0):
562
562
  th_props = [f'rotor_density = {model["magnet"]["density"]}',
@@ -730,7 +730,7 @@ class Builder:
730
730
  custom_fefunc = ['']
731
731
  if pfefunc:
732
732
  sim['loss_funct'] = 1 # 3?
733
- if pfefunc == 'bertotti' or 'modified_steinmetz':
733
+ if pfefunc in ('bertotti', 'modified_steinmetz'):
734
734
  custom_fefunc = self.__render(sim['PVFE_FSL'], pfefunc)
735
735
  else:
736
736
  custom_fefunc = pfefunc.split('\n')
@@ -750,6 +750,9 @@ class Builder:
750
750
  revert_displ = self.create_displ_stator_rotor(
751
751
  sim['eccentricity'])
752
752
 
753
+ if sim.get('calculationMode') == 'pm_sym_f_cur':
754
+ if 'nload_ex_cur' in sim.keys(): # convert obsolete key
755
+ sim['noload_ex_cur'] = sim.pop('nload_ex_cur')
753
756
  felosses = custom_fefunc + self.create_fe_losses(sim)
754
757
  fslcalc = (displ_stator_rotor
755
758
  + self.__render(sim, sim.get('calculationMode'))
femagtools/isa7.py CHANGED
@@ -815,8 +815,9 @@ class Isa7(object):
815
815
  airgap_positions.append(axy[0][0])
816
816
  self.airgap_center_elements.append(se.elements)
817
817
 
818
- if len(self.airgap_center_elements) == 1:
819
- self.airgap_center_elements = self.airgap_center_elements[0]
818
+ if self.airgap_center_elements:
819
+ # TODO check airgap center
820
+ self.airgap_center_elements = self.airgap_center_elements[-1]
820
821
  if horiz:
821
822
  airgap_positions.append(np.min(nxy[:, 1]))
822
823
  else:
@@ -68,8 +68,9 @@ def create_from_eecpars(temp, eecpars, lfe=1, wdg=1):
68
68
  psid = rwdg*rlfe*dqp['psid']
69
69
  psiq = rwdg*rlfe*dqp['psiq']
70
70
  losses = __scale_losses(dqp['losses'], rlfe)
71
- losses['ef'] = dqpars[-1]['losses']['ef']
72
- losses['eh'] = dqpars[-1]['losses']['ef']
71
+ losses['ef'] = dqpars[-1]['losses'].get('ef', [2.0, 2.0])
72
+ losses['hf'] = dqpars[-1]['losses'].get('hf', [1.0, 1.0])
73
+ # TODO handle bertotti excess loss factor
73
74
 
74
75
  if 'psidq' in eecpars:
75
76
  machine = PmRelMachinePsidq(
@@ -140,7 +141,7 @@ def create(bch, r1, ls, lfe=1, wdg=1):
140
141
  try:
141
142
  losses = __scale_losses(bch.psidq['losses'], rlfe)
142
143
  losses['ef'] = bch.lossPar.get('ef', [2.0, 2.0])
143
- losses['eh'] = bch.lossPar.get('eh', [1.0, 1.0])
144
+ losses['hf'] = bch.lossPar.get('hf', [1.0, 1.0])
144
145
  except KeyError:
145
146
  losses = {}
146
147
  if 'ex_current' in bch.machine:
@@ -162,7 +163,7 @@ def create(bch, r1, ls, lfe=1, wdg=1):
162
163
  try:
163
164
  losses = __scale_losses(bch.ldq['losses'], rlfe)
164
165
  losses['ef'] = bch.lossPar.get('ef', [2.0, 2.0])
165
- losses['eh'] = bch.lossPar.get('eh', [1.0, 1.0])
166
+ losses['hf'] = bch.lossPar.get('hf', [1.0, 1.0])
166
167
  except KeyError:
167
168
  losses = {}
168
169
  if 'ex_current' in bch.machine:
@@ -25,6 +25,30 @@ AFM_TYPES = (
25
25
  "S2R1_all" # 2 stator, 1 rotor, all simulated
26
26
  )
27
27
 
28
+ def num_agnodes(Q, p, pw, ag):
29
+ """return total number of nodes in airgap per pole
30
+ Args:
31
+ Q: (int) number of slots
32
+ p: (int) number of pole pairs
33
+ pw: (float) pole width
34
+ ag: (float) airgap height
35
+ """
36
+ num_nodes = np.arange(6, 120, 6)
37
+ i = np.argmin(np.abs(pw/num_nodes - ag/2))
38
+ nag = num_nodes[i-1]
39
+ if p*nag % Q:
40
+ lcm = np.lcm(Q, 2*p)//p
41
+ nmin, nmax = 1, num_nodes[-1]//lcm
42
+ num_nodes = np.array(
43
+ [i*lcm for i in range(nmin, nmax) if i*lcm % 6 == 0])
44
+ i = np.argmin(np.abs(pw/num_nodes - ag/2))
45
+ if i > 0:
46
+ nag = num_nodes[i-1]
47
+ else:
48
+ nag = num_nodes[i]
49
+ # TODO nodedist 0.5, 2, 4, 6
50
+ return nag
51
+
28
52
  def _integrate(radius, pos, val):
29
53
  interp = RegularGridInterpolator((radius, pos), val)
30
54
  def func(x, y):
@@ -141,11 +165,8 @@ def parident(workdir, engine, temp, machine,
141
165
 
142
166
  if "num_agnodes" not in machine:
143
167
  for pw in pole_width:
144
- machine['num_agnodes'] = 6*round(pw/machine['airgap']/4)
145
-
146
- #if machine['num_agnodes'] < nper:
147
- # machine['num_agnodes'] = 8*round(pw/machine['airgap']/4)
148
-
168
+ machine['num_agnodes'] = num_agnodes(Q1, p//2, pw,
169
+ machine['airgap'])
149
170
  nlparvardef = {
150
171
  "decision_vars": [
151
172
  {"values": pole_width,
@@ -165,13 +186,13 @@ def parident(workdir, engine, temp, machine,
165
186
  parvardef = {
166
187
  "decision_vars": [
167
188
  # {"values": sorted(2*temp), "name": "magn_temp"},
168
- {"steps": num_beta_steps, "bounds": [beta_min, 0], "name": "angl_i_up"},
189
+ {"steps": num_beta_steps, "bounds": [beta_min, 0],
190
+ "name": "angl_i_up"},
169
191
  {"steps": num_cur_steps,
170
192
  "bounds": [i1_max/num_cur_steps, i1_max], "name": "current"}
171
193
  ]
172
194
  }
173
195
 
174
-
175
196
  ldq = []
176
197
  for magtemp in temp:
177
198
  nlcalc = dict(
@@ -189,7 +210,8 @@ def parident(workdir, engine, temp, machine,
189
210
 
190
211
  nlresults = pstudy(nlparvardef, machine, nlcalc, engine)
191
212
  if nlresults['status'].count('C') != len(nlresults['status']):
192
- raise ValueError('Noload simulation failed %s', nlresults['status'])
213
+ raise ValueError(
214
+ f"Noload simulation failed {nlresults['status']}")
193
215
  else:
194
216
  nlresults = {"x": [], "f": []}
195
217
  i = 0
@@ -210,7 +232,7 @@ def parident(workdir, engine, temp, machine,
210
232
  nlresults['f'].append({k: v for k, v in r.items()})
211
233
  i = i + 1
212
234
  nlresults.update(process(lfe, pole_width, machine, nlresults['f']))
213
-
235
+ weights = nlresults['weights']
214
236
  current_angles = nlresults['f'][0]['current_angles']
215
237
  results = []
216
238
  i = 0
@@ -325,7 +347,7 @@ def parident(workdir, engine, temp, machine,
325
347
  (-1, num_beta_steps)),
326
348
  axis=1).T.tolist()})
327
349
  ldq.append({'temperature': magtemp,
328
- 'i1':i1, 'beta':beta,
350
+ 'i1': i1, 'beta': beta,
329
351
  'psid': psid.tolist(), 'psiq': psiq.tolist(),
330
352
  'ld': ld, 'lq': lq,
331
353
  'torque': torque.tolist(),
@@ -334,7 +356,8 @@ def parident(workdir, engine, temp, machine,
334
356
  #iq, id = femagtools.machine.utils.iqd(*np.meshgrid(beta, i1))
335
357
 
336
358
  return {'m': machine[wdgk]['num_phases'],
337
- 'p': machine['poles']//2,
359
+ 'p': machine['poles']//2, 'weights': weights,
360
+ 'rotor_mass': sum(weights[1]), "kfric_b": 1,
338
361
  'ls1': 0, 'r1': r1, 'ldq': ldq}
339
362
 
340
363
 
@@ -362,7 +385,7 @@ def process(lfe, pole_width, machine, bch):
362
385
  num_slots = machine['stator']['num_slots']
363
386
  mmod = model.MachineModel(machine)
364
387
  slots_gen = mmod.stator['num_slots_gen']
365
- scale_factor =_get_scale_factor(model_type, num_slots, slots_gen)
388
+ scale_factor = _get_scale_factor(model_type, num_slots, slots_gen)
366
389
  endpos = [2*pw*1e3 for pw in pole_width]
367
390
  displ = [[d for d in r['linearForce'][0]['displ']
368
391
  if d < e*(1+1/len(r['linearForce'][0]['displ']))]
@@ -373,11 +396,14 @@ def process(lfe, pole_width, machine, bch):
373
396
  currents = [bch[0]['flux'][k][0]['current_k'][:n]
374
397
  for k in bch[0]['flux']]
375
398
  if len(pole_width) > 1:
399
+ # check homogenity:
400
+ if np.diff([len(d) for d in displ]).any():
401
+ raise ValueError(
402
+ f"inhomogenous number of steps: {[len(d) for d in displ]}")
403
+ lfx = [r['linearForce'][0]['force_x'] for r in bch]
376
404
  torque = _integrate(radius, rotpos[0], np.array(
377
405
  [r*scale_factor*np.array(fx[:-1])/l
378
- for l, r, fx in zip(lfe, radius,
379
- [r['linearForce'][0]['force_x']
380
- for r in bch])]))
406
+ for l, r, fx in zip(lfe, radius, lfx)]))
381
407
 
382
408
  voltage = {k: [scale_factor * np.array(ux[:-1])/l
383
409
  for l, ux in zip(lfe, [r['flux'][k][0]['voltage_dpsi']
@@ -385,6 +411,13 @@ def process(lfe, pole_width, machine, bch):
385
411
  for k in bch[0]['flux']}
386
412
  emf = [_integrate(radius, rotpos[0], np.array(voltage[k]))
387
413
  for k in voltage]
414
+
415
+ fluxxy = {k: [scale_factor * np.array(flx[:-1])/l
416
+ for l, flx in zip(lfe, [r['flux'][k][0]['flux_k']
417
+ for r in bch])]
418
+ for k in bch[0]['flux']}
419
+ flux = [_integrate(radius, rotpos[0], np.array(fluxxy[k]))
420
+ for k in fluxxy]
388
421
  else:
389
422
  r = radius[0]
390
423
  torque = [r*scale_factor*fx
@@ -393,6 +426,7 @@ def process(lfe, pole_width, machine, bch):
393
426
  for ux in bch[0]['flux'][k][0]['voltage_dpsi'][:-1]]
394
427
  for k in bch[0]['flux']}
395
428
  emf = [voltage[k][:n] for k in voltage]
429
+ flux = [fluxxy[k][:n] for k in fluxxy]
396
430
 
397
431
  pos = (rotpos[0]/np.pi*180)
398
432
  emffft = utils.fft(pos, emf[0])
@@ -442,10 +476,20 @@ def process(lfe, pole_width, machine, bch):
442
476
  mmod.outer_diam, mmod.inner_diam)
443
477
  i1 = np.mean([np.max(c) for c in currents])/np.sqrt(2)
444
478
  plcu = mmod.winding['num_phases']*i1**2*r1
479
+ weights = np.array([[0, 0, 0], [0, 0, 0]])
480
+ try:
481
+ for b in bch:
482
+ weights = weights + b['weights']
483
+ weights *= scale_factor
484
+ except KeyError as exc:
485
+ #logger.warning("missing key %s", exc)
486
+ pass
445
487
 
446
488
  return {
489
+ 'weights': weights.tolist(),
447
490
  'pos': pos.tolist(), 'r1': r1,
448
491
  'torque': torque,
492
+ 'flux_k': flux,
449
493
  'emf': emf,
450
494
  'emf_amp': emffft['a'], 'emf_angle': emffft['alfa0'],
451
495
  'freq': freq,
@@ -563,10 +607,10 @@ class AFPM:
563
607
  condMat: conductor material
564
608
  """
565
609
  def __init__(self, workdir, magnetizingCurves='.', magnetMat='',
566
- condMat=''):
610
+ condMat='', cmd=None):
567
611
  self.parstudy = parstudy.List(
568
612
  workdir, condMat=condMat, magnets=magnetMat,
569
- magnetizingCurves=magnetizingCurves)
613
+ magnetizingCurves=magnetizingCurves, cmd=cmd)
570
614
 
571
615
  def __call__(self, engine, machine, simulation, num_slices):
572
616
  """ run FE simulation
@@ -603,14 +647,12 @@ class AFPM:
603
647
  for pw in pole_width]
604
648
 
605
649
  if "num_agnodes" not in machine:
650
+ Q1 = machine['stator']['num_slots']
651
+ p = machine['poles']
606
652
  for pw in pole_width:
607
- machine['num_agnodes'] = 6*round(pw/machine['airgap']/4)
608
- #Q = machine['stator']['num_slots']
609
- #p = machine['poles']
610
- #nper = np.lcm(Q, p)
611
- #if machine['num_agnodes'] < nper:
612
- # machine['num_agnodes'] = 8*round(pw/machine['airgap']/4)
613
-
653
+ machine['num_agnodes'] = num_agnodes(Q1, p//2, pw,
654
+ machine['airgap'])
655
+ logger.info("Num agnodes/pole %d", machine['num_agnodes'])
614
656
  parvardef = {
615
657
  "decision_vars": [
616
658
  {"values": pole_width,
@@ -622,21 +664,24 @@ class AFPM:
622
664
  }
623
665
  machine['pole_width'] = np.pi * machine['inner_diam']/machine['poles']
624
666
  machine['lfe'] = machine['outer_diam'] - machine['inner_diam']
625
-
667
+ simulation['skew_displ'] = (simulation.get('skew_angle', 0)/180 * np.pi
668
+ * machine['inner_diam'])
626
669
  nlresults = {}
627
- if (simulation['calculationMode'] != 'cogg_calc' and
628
- 'poc' not in simulation):
670
+ if (simulation['calculationMode'] != 'cogg_calc'
671
+ and 'poc' not in simulation):
629
672
  nlcalc = dict(
630
673
  calculationMode="cogg_calc",
631
674
  magn_temp=simulation.get('magn_temp', 20),
632
675
  num_move_steps=60,
676
+ skew_linear=0,
677
+ skew_steps=0,
633
678
  poc=poc.Poc(machine['pole_width']),
634
679
  speed=0)
635
680
  logging.info("Noload simulation")
636
681
  nlresults = self.parstudy(parvardef,
637
682
  machine, nlcalc, engine)
638
683
  if nlresults['status'].count('C') != len(nlresults['status']):
639
- raise ValueError('Noload simulation failed %s', nlresults['status'])
684
+ raise ValueError(f'Noload simulation failed {nlresults["status"]}')
640
685
  nlresults.update(process(lfe, pole_width, machine, nlresults['f']))
641
686
 
642
687
  current_angles = nlresults['f'][0]['current_angles']
@@ -645,14 +690,15 @@ class AFPM:
645
690
  parameters={
646
691
  'phi_voltage_winding': current_angles})
647
692
  logger.info("Current angles: %s", current_angles)
648
- elif (simulation['calculationMode'] == 'cogg_calc' and
649
- 'poc' not in simulation):
693
+ elif (simulation['calculationMode'] == 'cogg_calc'
694
+ and 'poc' not in simulation):
650
695
  simulation['poc'] = poc.Poc(machine['pole_width'])
651
696
 
652
697
  lresults = self.parstudy(
653
- parvardef,
654
- {k: machine[k] for k in machine if k!= 'afm_rotor'},
655
- simulation, engine) # Note: imcomplete machine prevents rebuild
698
+ parvardef, machine, simulation, engine)
699
+ if lresults['status'].count('C') != len(lresults['status']):
700
+ raise ValueError(
701
+ f'{simulation["calculationMode"]} failed {lresults["status"]}')
656
702
 
657
703
  results = process(lfe, pole_width, machine, lresults['f'])
658
704
  if nlresults: