femagtools 1.8.1__py3-none-any.whl → 1.8.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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: