a2p2 0.2.14__py3-none-any.whl → 0.7.4__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 (47) hide show
  1. a2p2/__main__.py +40 -1
  2. a2p2/chara/facility.py +54 -4
  3. a2p2/chara/gui.py +31 -5
  4. a2p2/client.py +158 -31
  5. a2p2/facility.py +14 -1
  6. a2p2/gui.py +46 -12
  7. a2p2/instrument.py +3 -0
  8. a2p2/jmmc/__init__.py +7 -0
  9. a2p2/jmmc/catalogs.py +129 -0
  10. a2p2/jmmc/generated_models.py +191 -0
  11. a2p2/jmmc/models.py +104 -0
  12. a2p2/jmmc/services.py +16 -0
  13. a2p2/jmmc/utils.py +130 -0
  14. a2p2/jmmc/webservices.py +48 -0
  15. a2p2/ob.py +98 -9
  16. a2p2/samp.py +20 -0
  17. a2p2/version.py +210 -131
  18. a2p2/vlti/conf/GRAVITY_ditTable.json +21 -19
  19. a2p2/vlti/conf/GRAVITY_rangeTable.json +200 -28
  20. a2p2/vlti/conf/MATISSE_rangeTable.json +58 -22
  21. a2p2/vlti/conf/PIONIER_ditTable.json +1 -1
  22. a2p2/vlti/conf/PIONIER_rangeTable.json +16 -18
  23. a2p2/vlti/facility.py +160 -43
  24. a2p2/vlti/gravity.py +243 -311
  25. a2p2/vlti/gui.py +165 -39
  26. a2p2/vlti/instrument.py +266 -49
  27. a2p2/vlti/matisse.py +61 -147
  28. a2p2/vlti/pionier.py +34 -157
  29. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/METADATA +34 -20
  30. a2p2-0.7.4.dist-info/RECORD +39 -0
  31. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/WHEEL +1 -1
  32. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/entry_points.txt +0 -1
  33. a2p2/vlti/confP104/GRAVITY_ditTable.json +0 -122
  34. a2p2/vlti/confP104/GRAVITY_rangeTable.json +0 -202
  35. a2p2/vlti/confP104/MATISSE_ditTable.json +0 -2
  36. a2p2/vlti/confP104/MATISSE_rangeTable.json +0 -202
  37. a2p2/vlti/confP104/PIONIER_ditTable.json +0 -77
  38. a2p2/vlti/confP104/PIONIER_rangeTable.json +0 -118
  39. a2p2/vlti/confP105/GRAVITY_ditTable.json +0 -37
  40. a2p2/vlti/confP105/GRAVITY_rangeTable.json +0 -42
  41. a2p2/vlti/confP105/MATISSE_ditTable.json +0 -2
  42. a2p2/vlti/confP105/MATISSE_rangeTable.json +0 -44
  43. a2p2/vlti/confP105/PIONIER_ditTable.json +0 -25
  44. a2p2/vlti/confP105/PIONIER_rangeTable.json +0 -38
  45. a2p2-0.2.14.dist-info/RECORD +0 -44
  46. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/LICENSE +0 -0
  47. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/top_level.txt +0 -0
a2p2/vlti/instrument.py CHANGED
@@ -8,12 +8,16 @@ import json
8
8
  import os
9
9
  # import ast
10
10
  import re
11
+ import logging
11
12
 
12
13
  import numpy as np
13
14
  from astropy.coordinates import SkyCoord
15
+ import astropy.units as u
14
16
 
15
17
  from a2p2.instrument import Instrument
16
18
 
19
+ logger = logging.getLogger(__name__)
20
+
17
21
 
18
22
  class VltiInstrument(Instrument):
19
23
 
@@ -26,13 +30,41 @@ class VltiInstrument(Instrument):
26
30
  self.rangeTable = None
27
31
  self.ditTable = None
28
32
 
33
+ # warnings
34
+ self.warnings = []
35
+ self.errors = []
36
+
37
+ def isScience(self, objtype):
38
+ return "SCI" in objtype
39
+
40
+ def isCalibrator(self, objtype):
41
+ return "SCI" not in objtype
42
+
29
43
  def get(self, obj, fieldname, defaultvalue):
30
44
  if fieldname in obj._fields:
31
45
  return getattr(obj, fieldname)
32
46
  else:
33
47
  return defaultvalue
34
48
 
35
- # must be defined by each instruments
49
+ def getSequence(self, ob):
50
+ # We may look at ob.observationSchedule but at present tuime schedule still is computed on a2p2 side
51
+ sci = []
52
+ cal = []
53
+ for observationConfiguration in ob.observationConfiguration:
54
+ if 'SCIENCE' in observationConfiguration.type:
55
+ sci.append(observationConfiguration)
56
+ else:
57
+ cal.append(observationConfiguration)
58
+ if len(sci)+len(cal) <= 2: # return [CAL] [SCI]
59
+ return cal+sci
60
+ # else return CAL1 SCI CAL2 [SCI CAL3] [SCI CAL4]
61
+ seq = []
62
+ for c in cal:
63
+ seq.append(c)
64
+ seq += sci
65
+ return seq[0:-1]
66
+
67
+ # checkOB() must be defined by each instruments
36
68
  # def checkOB(self, ob, p2container=None):
37
69
  # consider dryMode if p2container is None else check again and submit on p2 side
38
70
 
@@ -42,30 +74,49 @@ class VltiInstrument(Instrument):
42
74
 
43
75
  # create new container
44
76
  obsconflist = ob.observationConfiguration
77
+
78
+ # Aspro2 always send SCI at first position (or CAL if no SCI)
45
79
  folderName = obsconflist[0].SCTarget.name
46
80
  folderName = re.sub('[^A-Za-z0-9]+', '_', folderName.strip())
47
-
48
- # force a top folder in demo.
81
+ # prefix with username to make test folder clearer
49
82
  if p2container.isRoot() and self.facility.isTutorialAccount():
50
- ui.addToLog(
51
- "create pre folder (required for tutorial account) ", False)
52
- folder, _ = api.createFolder(p2container.containerId, folderName)
83
+ folderName += "_" + self.facility.a2p2client.preferences.getP2UserCommentName()
84
+
85
+ # do create a folder or concatenation only for SM or tutorial account
86
+ # should we create a concatenation only if we have multiple objects
87
+ if p2container.isRoot():
88
+ # create a concatenation if service mode (even if we do not have any cal at this stage)
89
+ if p2container.isServiceModeRun():
90
+ # TODO fix bug when we keep the last selection that is a Concatenation. Concatenation can't be nested.
91
+ # use parent container or ask user to choose another container location ?
92
+ folder, _ = api.createConcatenation(
93
+ p2container.containerId, folderName)
94
+ ui.addToLog(f"concatenation '{folderName}' created")
95
+ else:
96
+ folder, _ = api.createFolder(
97
+ p2container.containerId, folderName)
98
+ ui.addToLog(f"folder '{folderName}' created")
99
+
100
+ # update parent tree since we created a new subfolder
101
+ ui.updateTree(p2container.run, p2container.containerId)
102
+
53
103
  p2container.containerId = folder['containerId']
54
104
 
55
- # create a concatenation if service mode
56
- if p2container.isServiceModeRun():
57
- folder, _ = api.createConcatenation(
58
- p2container.containerId, folderName)
59
105
  else:
60
- folder, _ = api.createFolder(p2container.containerId, folderName)
61
- p2container.containerId = folder['containerId']
106
+ ui.addToLog("concatenation or folder not created")
62
107
 
63
108
  ui.addToLog("OB checked / preparing submission...")
64
109
  self.checkOB(ob, p2container)
65
110
 
111
+ # refresh and select last created element tree
112
+ ui.updateTree(p2container.run, p2container.containerId)
113
+ ui.selectTreeItem(p2container.containerId)
114
+ ui.addToLog(
115
+ "OB submitted! Please check logs and fix last details on P2 web.")
116
+
66
117
  def getCoords(self, target, requirePrecision=True):
67
118
  """
68
- Format coordinates from given target to be VLTI compliant.
119
+ Format HMS DMS coordinates from given target to be VLTI compliant.
69
120
  Throws an exception if requirePrecision is true and given inputs have less than 3 (RA) or 2 (DEC) digits.
70
121
  """
71
122
 
@@ -93,36 +144,63 @@ class VltiInstrument(Instrument):
93
144
 
94
145
  def getPMCoords(self, target, defaultPMRA=0.0, defaultPMDEC=0.0):
95
146
  """
96
- Returns PMRA, PMDEC as float values rounded to 4 decimal digits. 0.0 is used as default if not present.
147
+ Returns PMRA, PMDEC in arcsec/year as float values rounded to 4 decimal digits. 0.0 is used as default if not present.
97
148
  """
98
149
  PMRA = self.get(target, "PMRA", defaultPMRA)
99
150
  PMDEC = self.get(target, "PMDEC", defaultPMDEC)
100
151
  return round(float(PMRA) / 1000.0, 4), round(float(PMDEC) / 1000.0, 4)
101
152
 
153
+ def getPARALLAX(self, target, defaultPARALLAX=0.0):
154
+ """
155
+ Returns PARALLAX in arcsec as float value rounded to 4 decimal digits. 0.0 is used as default if not present.
156
+ """
157
+ PARALLAX = self.get(target, "PARALLAX", defaultPARALLAX)
158
+ return round(float(PARALLAX) / 1000.0, 4)
159
+
160
+
102
161
  def getFlux(self, target, flux):
103
162
  """
104
163
  Returns Flux as float values rounded to 3 decimal digits.
105
164
 
106
165
  flux in 'V', 'J', 'H'...
107
166
  """
108
- return round(float(getattr(target, "FLUX_" + flux)), 3)
109
-
110
- def getBaselineCode(self, baseline):
111
- # as of P104
112
- # take care if you change next branch order
113
- if "B2" in baseline:
114
- return "small"
115
- elif "G2" in baseline:
116
- return "medium"
117
- elif "J3" in baseline:
118
- return "large"
119
- elif "K0" in baseline:
120
- return "astrometric"
121
- elif "U" in baseline:
122
- return "UTs"
167
+ try:
168
+ return round(float(getattr(target, "FLUX_" + flux)), 3)
169
+ except:
170
+ raise ValueError(
171
+ f"Missing {flux} flux for target { getattr(target,'name') }") from None
172
+
173
+ def getBaselineCode(self, ob):
174
+ confAltName = ob.get(ob.interferometerConfiguration, "confAltName")
175
+ stations = ob.get(ob.interferometerConfiguration, "stations")
176
+ if confAltName:
177
+ return confAltName
123
178
  else:
124
179
  raise ValueError(
125
- "Can't detect Interferometric Array type from given baseline : %s)" % (baseline))
180
+ "Can't detect alt name of Interferometric Array type from given baseline : %s)" % (stations))
181
+
182
+ def getAcquisitionType(self):
183
+ return self.facility.ui.getAcquisitionType()
184
+ return "onaxis"
185
+ return "offaxis"
186
+ return "wide"
187
+
188
+ def checkIssVltiType(self, acqTSF):
189
+ # TODO improve handling of this keyword using input from Aspro2's OB
190
+
191
+ # use GUI live checkboxes instead of preferences
192
+ vltitypesVars = self.facility.ui.getIssVltitypeVars()
193
+ vltitypes = [v for v in vltitypesVars
194
+ if vltitypesVars[v].get() and self.isInRange(acqTSF.tpl, "ISS.VLTITYPE", v)]
195
+ if vltitypes:
196
+ self.ui.addToLog(
197
+ f"Set template's ISS_VLTITYPE to {vltitypes}")
198
+ acqTSF.ISS_VLTITYPE = vltitypes
199
+ else:
200
+ # TODO throw an warning ?
201
+ self.ui.addToLog(
202
+ f"Warning: no compatible ISS_VLTITYPE selected in the GUI")
203
+ pass
126
204
 
127
205
  def getA2p2Comments(self):
128
206
  return 'Generated by ' + self.facility.a2p2client.preferences.getP2UserCommentName() + \
@@ -147,7 +225,7 @@ class VltiInstrument(Instrument):
147
225
  self.ditTable = json.load(open(f))
148
226
  return self.ditTable
149
227
 
150
- def getDit(self, tel, spec, pol, K, dualFeed=False, showWarning=False):
228
+ def getDit(self, tel, spec, pol, K, dualFeed=False, targetName="", showWarning=False):
151
229
  """
152
230
  finds DIT according to ditTable and K magnitude K
153
231
 
@@ -172,7 +250,7 @@ class VltiInstrument(Instrument):
172
250
  if tel == "UT":
173
251
  dK += ditTable["AT"]['Kut']
174
252
  for i, d in enumerate(dits):
175
- if mags[i] < (K - dK) and (K - dK) <= mags[i + 1]:
253
+ if mags[i] <= (K - dK) and (K - dK) <= mags[i + 1]:
176
254
  return d
177
255
 
178
256
  # handle out of bounds
@@ -181,11 +259,19 @@ class VltiInstrument(Instrument):
181
259
  for i, d in enumerate(dits):
182
260
  kmin = min(kmin, mags[i] + dK)
183
261
  kmax = max(kmax, mags[i + 1] + dK)
184
- if kmin == K:
185
- return minDIT
186
- raise ValueError(
187
- "K mag (%f) is out of ranges [%f,%f]\n for this mode (tel=%s, spec=%s, pol=%s, dualFeed=%s)" % (
188
- K, kmin, kmax, tel, spec, pol, dualFeed))
262
+
263
+ if showWarning:
264
+ self.warnings.append(
265
+ f"K mag ({K}) is out of range [{kmin},{kmax}]\n mode ( ∆K={dK} tel={tel}, spec={spec}, pol={pol}, dualFeed={dualFeed}) \n for target '{targetName}'")
266
+
267
+ # always return a value to avoid unsupported operations
268
+ if K < kmin:
269
+ return min(dits)
270
+ return max(dits)
271
+
272
+ # raise ValueError(
273
+ # "K mag (%f) of '%s' is out of ranges [%f,%f]\n for this mode (tel=%s, spec=%s, pol=%s, dualFeed=%s)" % (
274
+ # K, targetName, kmin, kmax, tel, spec, pol, dualFeed))
189
275
 
190
276
  def getRangeTable(self):
191
277
  if self.rangeTable:
@@ -220,6 +306,14 @@ class VltiInstrument(Instrument):
220
306
  return value >= rangeTable[_tpl][key]['min'] and \
221
307
  value <= rangeTable[_tpl][key]['max']
222
308
  if 'list' in rangeTable[_tpl][key].keys():
309
+ #
310
+ # hack set to check for coordinates convention define by p2
311
+ # vlue will be checked by p2 later...
312
+ if "ra" in rangeTable[_tpl][key]['list']:
313
+ return True
314
+ if "dec" in rangeTable[_tpl][key]['list']:
315
+ return True
316
+
223
317
  # return value in rangeTable[_tpl][key]['list']
224
318
  if type(value) is list:
225
319
  for v in value:
@@ -280,7 +374,7 @@ class VltiInstrument(Instrument):
280
374
  if not key in rangeTable[_tpl].keys():
281
375
  raise ValueError(
282
376
  "unknown keyword '%s' in template '%s'" % (key, tpl))
283
- if 'default' in rangeTable[_tpl][key].keys() :
377
+ if 'default' in rangeTable[_tpl][key].keys():
284
378
  return rangeTable[_tpl][key]['default']
285
379
  return None
286
380
 
@@ -304,13 +398,15 @@ class VltiInstrument(Instrument):
304
398
  res[key] = rangeTable[_tpl][key]["default"]
305
399
  return res
306
400
 
401
+ def getSkySeparation(self, ra1, dec1, ra2, dec2):
402
+ target1 = SkyCoord(ra1, dec1, frame='icrs', unit=(u.hourangle, u.deg))
403
+ target2 = SkyCoord(ra2, dec2, frame='icrs', unit=(u.hourangle, u.deg))
404
+ return target1.separation(target2)
307
405
 
308
- def getSkyDiff(self, ra, dec, ftra, ftdec):
309
- science = SkyCoord(ra, dec, frame='icrs', unit='deg')
310
- ft = SkyCoord(ftra, ftdec, frame='icrs', unit='deg')
311
- ra_offset = (science.ra - ft.ra) * np.cos(ft.dec.to('radian'))
312
- dec_offset = (science.dec - ft.dec)
313
- return [ra_offset.deg * 3600 * 1000, dec_offset.deg * 3600 * 1000] # in mas
406
+ def getSkyOffset(self, ra, dec, originRa, originDec):
407
+ science = SkyCoord(ra, dec, frame='icrs', unit=(u.hourangle, u.deg))
408
+ origin = SkyCoord(originRa, originDec, frame='icrs', unit=(u.hourangle, u.deg))
409
+ return origin.spherical_offsets_to(science)
314
410
 
315
411
  def getSiderealTimeConstraints(self, LSTINTERVAL):
316
412
  # by default, above 40 degree. Will generate a WAIVERABLE ERROR if not.
@@ -330,9 +426,10 @@ class VltiInstrument(Instrument):
330
426
  intervals.append({'from': lstStartSex, 'to': lstEndSex})
331
427
  return intervals
332
428
 
333
- def saveSiderealTimeConstraints(self, api, obId, LSTINTERVAL):
429
+ def saveSiderealTimeConstraints(self, api, ob, LSTINTERVAL):
334
430
  # wait for next Aspro release to only send constraints set by user
335
431
  return
432
+ obId = ob['obId']
336
433
  intervals = self.getSiderealTimeConstraints(LSTINTERVAL)
337
434
  if intervals:
338
435
  sidTCs, stcVersion = api.getSiderealTimeConstraints(obId)
@@ -362,7 +459,35 @@ class VltiInstrument(Instrument):
362
459
 
363
460
  return s
364
461
 
365
- def showP2Response(self, response, ob, obId):
462
+ def formatRangeTable(self):
463
+ rangeTable = self.getRangeTable()
464
+ buffer = ""
465
+ for l in rangeTable.keys():
466
+ buffer += l + "\n"
467
+ for k in rangeTable[l].keys():
468
+ constraint = rangeTable[l][k]
469
+ keys = constraint.keys()
470
+ buffer += ' %30s :' % (k)
471
+ if 'min' in keys and 'max' in keys:
472
+ buffer += ' %f ... %f ' % (
473
+ constraint['min'], constraint['max'])
474
+ elif 'list' in keys:
475
+ buffer += str(constraint['list'])
476
+ elif "spaceseparatedlist" in keys:
477
+ buffer += ' ' + " ".join(constraint['spaceseparatedlist'])
478
+ if 'default' in keys:
479
+ buffer += ' (' + str(constraint['default']) + ')'
480
+ else:
481
+ buffer += ' -no default-'
482
+ buffer += "\n"
483
+ return buffer
484
+
485
+ def showP2Response(self, response, ob,):
486
+ if not ob:
487
+ return
488
+
489
+ obId = ob['obId']
490
+
366
491
  if response['observable']:
367
492
  msg = 'OB ' + \
368
493
  str(obId) + ' submitted successfully on P2\n' + \
@@ -371,9 +496,93 @@ class VltiInstrument(Instrument):
371
496
  msg = 'OB ' + str(obId) + ' submitted successfully on P2\n' + ob[
372
497
  'name'] + ' has WARNING.\n see LOG for details.'
373
498
  self.ui.addToLog('\n')
374
- self.ui.ShowInfoMessage(msg)
499
+ # self.ui.ShowInfoMessage(msg)
500
+ self.ui.addToLog(msg)
375
501
  self.ui.addToLog('\n'.join(response['messages']) + '\n\n')
376
502
 
503
+ def createOB(self, p2container, obTarget, obConstraints, OBJTYPE, instrumentMode, LSTINTERVAL, tsfs):
504
+ """ Creates an OB on P2 and attach a template for every given tsf."""
505
+
506
+ ui = self.ui
507
+ ui.setProgress(0.1)
508
+
509
+ goodName = re.sub('[^A-Za-z0-9]+', '_', obTarget.name)
510
+ OBS_DESCR = '_'.join(
511
+ (OBJTYPE[0:3], goodName, self.getName(), instrumentMode))
512
+ # removed from template name acqTSF.ISS_BASELINE[0]
513
+
514
+ # dev code to debug without interracting with P2
515
+ if False:
516
+ self.ui.addToLog(f"Skip ob creation : {OBS_DESCR}")
517
+ for tsf in tsfs:
518
+ self.ui.addToLog(f"Skip {tsf.getP2Name()} template creation")
519
+ ui.setProgress(1.0)
520
+ return None
521
+ else:
522
+ self.ui.addToLog(f"Creating new ob from p2 : {OBS_DESCR}")
523
+
524
+ api = self.facility.getAPI()
525
+
526
+ ob, obVersion = api.createOB(p2container.containerId, OBS_DESCR)
527
+
528
+ # we use obId to populate OB
529
+ ob['obsDescription']['name'] = OBS_DESCR[0:min(len(OBS_DESCR), 31)]
530
+ ob['obsDescription']['userComments'] = self.getA2p2Comments()
531
+
532
+ # copy target info
533
+ targetInfo = obTarget.getDict()
534
+ for key in targetInfo:
535
+ ob['target'][key] = targetInfo[key]
536
+
537
+ # copy constraints info
538
+ constraints = obConstraints.getDict()
539
+ for k in constraints:
540
+ ob['constraints'][k] = constraints[k]
541
+
542
+ self.ui.addToLog("New OB saved to p2\n%s" % ob, False)
543
+ ob, obVersion = api.saveOB(ob, obVersion)
544
+
545
+ # set time constraints if present
546
+ self.saveSiderealTimeConstraints(api, ob, LSTINTERVAL)
547
+ ui.addProgress()
548
+
549
+ for tsf in tsfs:
550
+ ui.addProgress()
551
+ self.createTemplate(ob,tsf)
552
+
553
+ # verify OB online
554
+ response = self.verifyOB(ob)
555
+ ui.setProgress(1.0)
556
+
557
+ self.showP2Response(response, ob)
558
+
559
+ return ob
560
+
561
+ def createTemplate(self, ob, tsf, templateName=None):
562
+ if not templateName:
563
+ templateName = tsf.getP2Name()
564
+
565
+ if not ob:
566
+ self.ui.addToLog(f"Request for new template ignored '{templateName}'")
567
+ return
568
+ self.ui.addToLog(f"Creating new template '{templateName}'")
569
+ obId = ob['obId']
570
+ api = self.facility.getAPI()
571
+ tpl, tplVersion = api.createTemplate(obId, templateName)
572
+ values = tsf.getDict()
573
+ tpl, tplVersion = api.setTemplateParams(
574
+ obId, tpl, values, tplVersion)
575
+
576
+ def verifyOB(self, ob):
577
+ if not ob:
578
+ self.ui.addToLog(f"Request to verify OB ignored")
579
+ return
580
+
581
+ api = self.facility.getAPI()
582
+ response, _=api.verifyOB(ob['obId'], True)
583
+ return response
584
+
585
+
377
586
 
378
587
  # TemplateSignatureFile
379
588
  # use new style class to get __getattr__ advantage
@@ -410,6 +619,9 @@ class TSF(object):
410
619
  def getDict(self):
411
620
  return self.tsfParams
412
621
 
622
+ def getName(self):
623
+ return self.tpl
624
+
413
625
  def getP2Name(self):
414
626
  return self.tpl[0:-4]
415
627
 
@@ -488,9 +700,14 @@ class FixedDict(object):
488
700
 
489
701
  class OBTarget(FixedDict):
490
702
 
491
- def __init__(self):
703
+ def __init__(self, instrument, scienceTarget):
492
704
  FixedDict.__init__(
493
705
  self, ('name', 'ra', 'dec', 'properMotionRa', 'properMotionDec'))
706
+ # Target name can include any alphanumeric character, and space, dot, plus or minus signs [a-z][A-Z][0-9][.+- ]
707
+ # ( https://www.eso.org/sci/observing/phase2/p2intro/p2-tutorials/p2-ImportTargetList.html )
708
+ self.name = re.sub(r'[^a-zA-Z0-9.\+\- ]+','',scienceTarget.name.strip())
709
+ self.ra, self.dec = instrument.getCoords(scienceTarget)
710
+ self.properMotionRa, self.properMotionDec = instrument.getPMCoords(scienceTarget)
494
711
 
495
712
 
496
713
  class OBConstraints(TSF):
@@ -498,4 +715,4 @@ class OBConstraints(TSF):
498
715
  def __init__(self, instrument):
499
716
  TSF.__init__(self, instrument, "instrumentConstraints.tsf")
500
717
  # WORKARROUND missing param in p2 json but valid and used in our code
501
- instrument.getRangeTable()[self.tpl]["name"]={}
718
+ instrument.getRangeTable()[self.tpl]["name"] = {}