IMP logo
IMP Reference Guide  2.11.0
The Integrative Modeling Platform
pmi/io/__init__.py
1 """@namespace IMP.pmi.io
2  Utility classes and functions for reading and storing PMI files
3 """
4 
5 from __future__ import print_function
6 import IMP
7 import IMP.algebra
8 import IMP.atom
9 import IMP.pmi
10 import IMP.rmf
11 import IMP.pmi.analysis
12 import IMP.pmi.output
13 import IMP.pmi.tools
14 import RMF
15 import sys,os
16 import numpy as np
17 import re
18 from collections import defaultdict
19 import itertools
20 
21 def parse_dssp(dssp_fn, limit_to_chains='',name_map=None):
22  """Read a DSSP file, and return secondary structure elements (SSEs).
23  Values are all PDB residue numbering.
24  @param dssp_fn The file to read
25  @param limit_to_chains Only read/return these chain IDs
26  @param name_map If passed, return tuples organized by molecule name
27  (name_map should be a dictionary with chain IDs as keys and
28  molecule names as values).
29 
30  @return a dictionary with keys 'helix', 'beta', 'loop'.
31  Each contains a list of SSEs.
32  Each SSE is a list of elements (e.g. strands in a sheet).
33  Each element is a tuple (residue start, residue end, chain).
34 
35  Example for a structure with helix A:5-7 and Beta strands A:1-3,A:9-11:
36 
37  @code{.py}
38  ret = { 'helix' : [ [ (5,7,'A') ], ...]
39  'beta' : [ [ (1,3,'A'),
40  (9,11,'A'), ...], ...]
41  'loop' : same format as helix
42  }
43  @endcode
44  """
45 
46  def convert_chain(ch):
47  if name_map is None:
48  return ch
49  else:
50  return name_map.get(ch, ch)
51 
52  # setup
53  helix_classes = 'GHI'
54  strand_classes = 'EB'
55  loop_classes = [' ', '', 'T', 'S']
56  sse_dict = {}
57  for h in helix_classes:
58  sse_dict[h] = 'helix'
59  for s in strand_classes:
60  sse_dict[s] = 'beta'
61  for l in loop_classes:
62  sse_dict[l] = 'loop'
63  sses = {'helix':[],
64  'beta':[],
65  'loop':[]}
66 
67  # read file and parse
68  start = False
69 
70  # temporary beta dictionary indexed by DSSP's ID
71  beta_dict = IMP.pmi.tools.OrderedDefaultDict(list)
72  prev_sstype = None
73  prev_beta_id = None
74 
75  with open(dssp_fn, 'r') as fh:
76  for line in fh:
77  fields = line.split()
78  chain_break = False
79  if len(fields) < 2:
80  continue
81  if fields[1] == "RESIDUE":
82  # Start parsing from here
83  start = True
84  continue
85  if not start:
86  continue
87  if line[9] == " ":
88  chain_break = True
89  elif limit_to_chains != '' and line[11] not in limit_to_chains:
90  continue
91 
92  # gather line info
93  if not chain_break:
94  pdb_res_num = int(line[5:10])
95  chain = line[11]
96  sstype = sse_dict[line[16]]
97  beta_id = line[33]
98 
99  # decide whether to extend or store the SSE
100  if prev_sstype is None:
101  cur_sse = [pdb_res_num,pdb_res_num,convert_chain(chain)]
102  elif sstype != prev_sstype or chain_break:
103  # add cur_sse to the right place
104  if prev_sstype in ['helix', 'loop']:
105  sses[prev_sstype].append([cur_sse])
106  else: # prev_sstype == 'beta'
107  beta_dict[prev_beta_id].append(cur_sse)
108  cur_sse = [pdb_res_num,pdb_res_num,convert_chain(chain)]
109  else:
110  cur_sse[1] = pdb_res_num
111  if chain_break:
112  prev_sstype = None
113  prev_beta_id = None
114  else:
115  prev_sstype = sstype
116  prev_beta_id = beta_id
117 
118  # final SSE processing
119  if prev_sstype in ['helix', 'loop']:
120  sses[prev_sstype].append([cur_sse])
121  elif prev_sstype == 'beta':
122  beta_dict[prev_beta_id].append(cur_sse)
123  # gather betas
124  for beta_sheet in beta_dict:
125  sses['beta'].append(beta_dict[beta_sheet])
126  return sses
127 
129  out_dir,
130  stat_files,
131  number_of_best_scoring_models=10,
132  get_every=1,
133  score_key="SimplifiedModel_Total_Score_None",
134  feature_keys=None,
135  rmf_file_key="rmf_file",
136  rmf_file_frame_key="rmf_frame_index",
137  override_rmf_dir=None):
138  """Given a list of stat files, read them all and find the best models.
139  Save to a single RMF along with a stat file.
140  @param model The IMP Model
141  @param out_dir The output directory. Will save 3 files (RMF, stat, summary)
142  @param stat_files List of all stat files to collect
143  @param number_of_best_scoring_models Num best models to gather
144  @param get_every Skip frames
145  @param score_key Used for the ranking
146  @param feature_keys Keys to keep around
147  @param rmf_file_key The key that says RMF file name
148  @param rmf_file_frame_key The key that says RMF frame number
149  @param override_rmf_dir For output, change the name of the RMF directory (experiment)
150  """
151 
152  # start by splitting into jobs
153  try:
154  from mpi4py import MPI
155  comm = MPI.COMM_WORLD
156  rank = comm.Get_rank()
157  number_of_processes = comm.size
158  except ImportError:
159  rank = 0
160  number_of_processes = 1
161  my_stat_files=IMP.pmi.tools.chunk_list_into_segments(
162  stat_files,number_of_processes)[rank]
163 
164  # filenames
165  out_stat_fn = os.path.join(out_dir,"top_"+str(number_of_best_scoring_models)+".out")
166  out_rmf_fn = os.path.join(out_dir,"top_"+str(number_of_best_scoring_models)+".rmf3")
167 
168  # extract all the models
169  all_fields=[]
170  for nsf,sf in enumerate(my_stat_files):
171 
172  # get list of keywords
173  root_directory_of_stat_file = os.path.dirname(os.path.dirname(sf))
174  print("getting data from file %s" % sf)
176  all_keys = [score_key,
177  rmf_file_key,
178  rmf_file_frame_key]
179  for k in po.get_keys():
180  for fk in feature_keys:
181  if fk in k:
182  all_keys.append(k)
183  fields = po.get_fields(all_keys,
184  get_every=get_every)
185 
186  # check that all lengths are all equal
187  length_set = set([len(fields[f]) for f in fields])
188  minlen = min(length_set)
189  # if some of the fields are missing, truncate
190  # the feature files to the shortest one
191  if len(length_set) > 1:
192  print("get_best_models: the statfile is not synchronous")
193  minlen = min(length_set)
194  for f in fields:
195  fields[f] = fields[f][0:minlen]
196  if nsf==0:
197  all_fields=fields
198  else:
199  for k in fields:
200  all_fields[k]+=fields[k]
201 
202  if override_rmf_dir is not None:
203  for i in range(minlen):
204  all_fields[rmf_file_key][i]=os.path.join(
205  override_rmf_dir,os.path.basename(all_fields[rmf_file_key][i]))
206 
207  # gather info, sort, write
208  if number_of_processes!=1:
209  comm.Barrier()
210  if rank!=0:
211  comm.send(all_fields, dest=0, tag=11)
212  else:
213  for i in range(1,number_of_processes):
214  data_tmp = comm.recv(source=i, tag=11)
215  for k in all_fields:
216  all_fields[k]+=data_tmp[k]
217 
218  # sort by total score
219  order = sorted(range(len(all_fields[score_key])),
220  key=lambda i: float(all_fields[score_key][i]))
221 
222  # write the stat and RMF files
223  stat = open(out_stat_fn,'w')
224  rh0 = RMF.open_rmf_file_read_only(
225  os.path.join(root_directory_of_stat_file,all_fields[rmf_file_key][0]))
226  prots = IMP.rmf.create_hierarchies(rh0,model)
227  del rh0
228  outf = RMF.create_rmf_file(out_rmf_fn)
229  IMP.rmf.add_hierarchies(outf,prots)
230  for nm,i in enumerate(order[:number_of_best_scoring_models]):
231  dline=dict((k,all_fields[k][i]) for k in all_fields)
232  dline['orig_rmf_file']=dline[rmf_file_key]
233  dline['orig_rmf_frame_index']=dline[rmf_file_frame_key]
234  dline[rmf_file_key]=out_rmf_fn
235  dline[rmf_file_frame_key]=nm
236  rh = RMF.open_rmf_file_read_only(
237  os.path.join(root_directory_of_stat_file,all_fields[rmf_file_key][i]))
238  IMP.rmf.link_hierarchies(rh,prots)
240  RMF.FrameID(all_fields[rmf_file_frame_key][i]))
241  IMP.rmf.save_frame(outf)
242  del rh
243  stat.write(str(dline)+'\n')
244  stat.close()
245  print('wrote stats to',out_stat_fn)
246  print('wrote rmfs to',out_rmf_fn)
247 
248 class _TempProvenance(object):
249  """Placeholder to track provenance information added to the IMP model.
250  This is since we typically don't preserve the IMP::Model object
251  throughout a PMI protocol."""
252  def __init__(self, provclass, particle_name, *args, **kwargs):
253  self.provclass = provclass
254  self.particle_name = particle_name
255  self.args = args
256  self.kwargs = kwargs
257 
258  def get_decorator(self, model):
259  """Instantiate and return an IMP Provenance object in the model."""
260  pi = model.add_particle(self.particle_name)
261  return self.provclass.setup_particle(model, pi, *self.args,
262  **self.kwargs)
263 
264 
265 class ClusterProvenance(_TempProvenance):
266  def __init__(self, *args, **kwargs):
267  _TempProvenance.__init__(self, IMP.core.ClusterProvenance,
268  "clustering", *args, **kwargs)
269 
270 
271 class FilterProvenance(_TempProvenance):
272  def __init__(self, *args, **kwargs):
273  _TempProvenance.__init__(self, IMP.core.FilterProvenance,
274  "filtering", *args, **kwargs)
275 
276 
277 class CombineProvenance(_TempProvenance):
278  def __init__(self, *args, **kwargs):
279  _TempProvenance.__init__(self, IMP.core.CombineProvenance,
280  "combine runs", *args, **kwargs)
281 
282 
283 def add_provenance(prov, hiers):
284  """Add provenance information in `prov` (a list of _TempProvenance
285  objects) to each of the IMP hierarchies provided.
286  Note that we do this all at once since we typically don't preserve
287  the IMP::Model object throughout a PMI protocol."""
288  for h in hiers:
289  IMP.pmi.tools._add_pmi_provenance(h)
290  m = h.get_model()
291  for p in prov:
292  IMP.core.add_provenance(m, h, p.get_decorator(m))
293 
294 def get_best_models(stat_files,
295  score_key="SimplifiedModel_Total_Score_None",
296  feature_keys=None,
297  rmf_file_key="rmf_file",
298  rmf_file_frame_key="rmf_frame_index",
299  prefiltervalue=None,
300  get_every=1, provenance=None):
301  """ Given a list of stat files, read them all and find the best models.
302  Returns the best rmf filenames, frame numbers, scores, and values for feature keywords
303  """
304  rmf_file_list=[] # best RMF files
305  rmf_file_frame_list=[] # best RMF frames
306  score_list=[] # best scores
307  feature_keyword_list_dict=defaultdict(list) # best values of the feature keys
308  statistics = IMP.pmi.output.OutputStatistics()
309  for sf in stat_files:
310  root_directory_of_stat_file = os.path.dirname(os.path.abspath(sf))
311  if sf[-4:]=='rmf3':
312  root_directory_of_stat_file = os.path.dirname(os.path.abspath(root_directory_of_stat_file))
313  print("getting data from file %s" % sf)
315 
316  try:
317  file_keywords = po.get_keys()
318  except:
319  continue
320 
321  keywords = [score_key,
322  rmf_file_key,
323  rmf_file_frame_key]
324 
325  # check all requested keys are in the file
326  # this looks weird because searching for "*requested_key*"
327  if feature_keys:
328  for requested_key in feature_keys:
329  for file_k in file_keywords:
330  if requested_key in file_k:
331  if file_k not in keywords:
332  keywords.append(file_k)
333 
334  if prefiltervalue is None:
335  fields = po.get_fields(keywords,
336  get_every=get_every,
337  statistics=statistics)
338  else:
339  fields = po.get_fields(keywords,
340  filtertuple=(score_key,"<",prefiltervalue),
341  get_every=get_every,
342  statistics=statistics)
343 
344  # check that all lengths are all equal
345  length_set = set()
346  for f in fields:
347  length_set.add(len(fields[f]))
348 
349  # if some of the fields are missing, truncate
350  # the feature files to the shortest one
351  if len(length_set) > 1:
352  print("get_best_models: the statfile is not synchronous")
353  minlen = min(length_set)
354  for f in fields:
355  fields[f] = fields[f][0:minlen]
356 
357  # append to the lists
358  score_list += fields[score_key]
359  for rmf in fields[rmf_file_key]:
360  rmf=os.path.normpath(rmf)
361  if root_directory_of_stat_file not in rmf:
362  rmf_local_path=os.path.join(os.path.basename(os.path.dirname(rmf)),os.path.basename(rmf))
363  rmf=os.path.join(root_directory_of_stat_file,rmf_local_path)
364  rmf_file_list.append(rmf)
365 
366  rmf_file_frame_list += fields[rmf_file_frame_key]
367 
368  for k in keywords:
369  feature_keyword_list_dict[k] += fields[k]
370 
371  # Record combining and filtering operations in provenance, if requested
372  if provenance is not None:
373  if len(stat_files) > 1:
374  provenance.append(CombineProvenance(len(stat_files),
375  statistics.total))
376  if get_every != 1:
377  provenance.append(FilterProvenance("Keep fraction",
378  0., statistics.passed_get_every))
379  if prefiltervalue is not None:
380  provenance.append(FilterProvenance("Total score",
381  prefiltervalue,
382  statistics.passed_filtertuple))
383 
384  return rmf_file_list,rmf_file_frame_list,score_list,feature_keyword_list_dict
385 
386 def get_trajectory_models(stat_files,
387  score_key="SimplifiedModel_Total_Score_None",
388  rmf_file_key="rmf_file",
389  rmf_file_frame_key="rmf_frame_index",
390  get_every=1):
391  """ Given a list of stat files, read them all and find a trajectory of models.
392  Returns the rmf filenames, frame numbers, scores, and values for feature keywords
393  """
394  rmf_file_list=[] # best RMF files
395  rmf_file_frame_list=[] # best RMF frames
396  score_list=[] # best scores
397  for sf in stat_files:
398  root_directory_of_stat_file = os.path.dirname(os.path.dirname(sf))
399  print("getting data from file %s" % sf)
401  keywords = po.get_keys()
402 
403  feature_keywords = [score_key,
404  rmf_file_key,
405  rmf_file_frame_key]
406 
407  fields = po.get_fields(feature_keywords,
408  get_every=get_every)
409 
410  # check that all lengths are all equal
411  length_set = set()
412  for f in fields:
413  length_set.add(len(fields[f]))
414 
415  # if some of the fields are missing, truncate
416  # the feature files to the shortest one
417  if len(length_set) > 1:
418  print("get_best_models: the statfile is not synchronous")
419  minlen = min(length_set)
420  for f in fields:
421  fields[f] = fields[f][0:minlen]
422 
423  # append to the lists
424  score_list += fields[score_key]
425  for rmf in fields[rmf_file_key]:
426  rmf_file_list.append(os.path.join(root_directory_of_stat_file,rmf))
427 
428  rmf_file_frame_list += fields[rmf_file_frame_key]
429 
430  return rmf_file_list,rmf_file_frame_list,score_list
431 
432 
434  rmf_tuples,
435  alignment_components=None,
436  rmsd_calculation_components=None,
437  state_number=0):
438  """ Read in coordinates of a set of RMF tuples.
439  Returns the coordinates split as requested (all, alignment only, rmsd only) as well as
440  RMF file names (as keys in a dictionary, with values being the rank number) and just a plain list
441  @param model The IMP model
442  @param rmf_tuples [score,filename,frame number,original order number, rank]
443  @param alignment_components Tuples to specify what you're aligning on
444  @param rmsd_calculation_components Tuples to specify what components are used for RMSD calc
445  """
446  all_coordinates = []
447  rmsd_coordinates = []
448  alignment_coordinates = []
449  all_rmf_file_names = []
450  rmf_file_name_index_dict = {} # storing the features
451 
452  for cnt, tpl in enumerate(rmf_tuples):
453  rmf_file = tpl[1]
454  frame_number = tpl[2]
455  if cnt==0:
456  prots = IMP.pmi.analysis.get_hiers_from_rmf(model,
457  frame_number,
458  rmf_file)
459  else:
460  IMP.pmi.analysis.link_hiers_to_rmf(model,prots,frame_number,rmf_file)
461 
462  if not prots:
463  continue
464  if IMP.pmi.get_is_canonical(prots[0]):
465  states = IMP.atom.get_by_type(prots[0],IMP.atom.STATE_TYPE)
466  prot = states[state_number]
467  else:
468  prot = prots[state_number]
469 
470  # getting the particles
472  all_particles = [pp for key in part_dict for pp in part_dict[key]]
473  all_ps_set = set(all_particles)
474  model_coordinate_dict = {}
475  template_coordinate_dict={}
476  rmsd_coordinate_dict={}
477 
478  for pr in part_dict:
479  model_coordinate_dict[pr] = np.array(
480  [np.array(IMP.core.XYZ(i).get_coordinates()) for i in part_dict[pr]])
481  # for each file, get (as floats) a list of all coordinates
482  # of all requested tuples, organized as dictionaries.
483  for tuple_dict,result_dict in zip((alignment_components,rmsd_calculation_components),
484  (template_coordinate_dict,rmsd_coordinate_dict)):
485 
486  if tuple_dict is None:
487  continue
488 
489  # PMI2: do selection of resolution and name at the same time
490  if IMP.pmi.get_is_canonical(prot):
491  for pr in tuple_dict:
492  ps = IMP.pmi.tools.select_by_tuple_2(prot,tuple_dict[pr],resolution=1)
493  result_dict[pr] = [list(map(float,IMP.core.XYZ(p).get_coordinates()))
494  for p in ps]
495  else:
496  for pr in tuple_dict:
497  if type(tuple_dict[pr]) is str:
498  name=tuple_dict[pr]
499  s=IMP.atom.Selection(prot,molecule=name)
500  elif type(tuple_dict[pr]) is tuple:
501  name=tuple_dict[pr][2]
502  rend=tuple_dict[pr][1]
503  rbegin=tuple_dict[pr][0]
504  s=IMP.atom.Selection(prot,molecule=name,residue_indexes=range(rbegin,rend+1))
505  ps=s.get_selected_particles()
506  filtered_particles=[p for p in ps if p in all_ps_set]
507  result_dict[pr] = \
508  [list(map(float,IMP.core.XYZ(p).get_coordinates())) for p in filtered_particles]
509 
510  all_coordinates.append(model_coordinate_dict)
511  alignment_coordinates.append(template_coordinate_dict)
512  rmsd_coordinates.append(rmsd_coordinate_dict)
513  frame_name = rmf_file + '|' + str(frame_number)
514  all_rmf_file_names.append(frame_name)
515  rmf_file_name_index_dict[frame_name] = tpl[4]
516  return all_coordinates,alignment_coordinates,rmsd_coordinates,rmf_file_name_index_dict,all_rmf_file_names
517 
518 def get_bead_sizes(model,rmf_tuple,rmsd_calculation_components=None,state_number=0):
519  '''
520  @param model The IMP model
521  @param rmf_tuple score,filename,frame number,original order number, rank
522  @param rmsd_calculation_components Tuples to specify what components are used for RMSD calc
523  '''
524  if rmsd_calculation_components is None:
525  return {}
526 
527  rmf_file = rmf_tuple[1]
528  frame_number = rmf_tuple[2]
529  prots = IMP.pmi.analysis.get_hiers_from_rmf(model,
530  frame_number,
531  rmf_file)
532 
533  if IMP.pmi.get_is_canonical(prots[0]):
534  states = IMP.atom.get_by_type(prots[0],IMP.atom.STATE_TYPE)
535  prot = states[state_number]
536  else:
537  prot = prots[state_number]
538 
539  rmsd_bead_size_dict = {}
540 
541  # PMI2: do selection of resolution and name at the same time
542  if IMP.pmi.get_is_canonical(prot):
543  for pr in rmsd_calculation_components:
544  ps = IMP.pmi.tools.select_by_tuple_2(prot,rmsd_calculation_components[pr],resolution=1)
545  rmsd_bead_size_dict[pr] = [len(IMP.pmi.tools.get_residue_indexes(p))
546  for p in ps]
547  else:
548  # getting the particles
550  all_particles=[pp for key in part_dict for pp in part_dict[key]]
551  all_ps_set=set(all_particles)
552 
553  # getting the coordinates
554  for pr in rmsd_calculation_components:
555  if type(rmsd_calculation_components[pr]) is str:
556  name=rmsd_calculation_components[pr]
557  s=IMP.atom.Selection(prot,molecule=name)
558  elif type(rmsd_calculation_components[pr]) is tuple:
559  name=rmsd_calculation_components[pr][2]
560  rend=rmsd_calculation_components[pr][1]
561  rbegin=rmsd_calculation_components[pr][0]
562  s=IMP.atom.Selection(prot,molecule=name,residue_indexes=range(rbegin,rend+1))
563  ps=s.get_selected_particles()
564  filtered_particles=[p for p in ps if p in all_ps_set]
565  rmsd_bead_size_dict[pr] = \
566  [len(IMP.pmi.tools.get_residue_indexes(p)) for p in filtered_particles]
567 
568  return rmsd_bead_size_dict
569 
570 
571 class TotalScoreOutput(object):
572  """A helper output for model evaluation"""
573  def __init__(self,model):
574  self.model = model
575  self.rs = IMP.pmi.tools.get_restraint_set(self.model)
576 
577  def get_output(self):
578  score = self.rs.evaluate(False)
579  output = {}
580  output["Total_Score"] = str(score)
581  return output
def get_restraint_set
Get a RestraintSet containing all PMI restraints added to the model.
Definition: tools.py:105
A class for reading stat files (either rmf or ascii v1 and v2)
Definition: output.py:769
atom::Hierarchies create_hierarchies(RMF::FileConstHandle fh, Model *m)
RMF::FrameID save_frame(RMF::FileHandle file, std::string name="")
Save the current state of the linked objects as a new RMF frame.
def parse_dssp
Read a DSSP file, and return secondary structure elements (SSEs).
def get_best_models
Given a list of stat files, read them all and find the best models.
A helper output for model evaluation.
Miscellaneous utilities.
Definition: tools.py:1
Collect statistics from ProcessOutput.get_fields().
Definition: output.py:758
Track creation of a system fragment by combination.
Definition: provenance.h:280
def get_trajectory_models
Given a list of stat files, read them all and find a trajectory of models.
def select_by_tuple_2
New tuple format: molname OR (start,stop,molname,copynum,statenum) Copy and state are optional...
Definition: tools.py:664
Track creation of a system fragment by filtering.
Definition: provenance.h:340
void load_frame(RMF::FileConstHandle file, RMF::FrameID frame)
Load the given RMF frame into the state of the linked objects.
A decorator for a particle with x,y,z coordinates.
Definition: XYZ.h:30
def get_particles_at_resolution_one
Get particles at res 1, or any beads, based on the name.
void add_hierarchies(RMF::NodeHandle fh, const atom::Hierarchies &hs)
Tools for clustering and cluster analysis.
Definition: pmi/Analysis.py:1
bool get_is_canonical(atom::Hierarchy h)
Walk up a PMI2 hierarchy/representations and check if the root is named System.
Definition: utilities.h:91
Classes for writing output files and processing them.
Definition: output.py:1
def save_best_models
Given a list of stat files, read them all and find the best models.
General purpose algebraic and geometric methods that are expected to be used by a wide variety of IMP...
Track creation of a system fragment from clustering.
Definition: provenance.h:426
void link_hierarchies(RMF::FileConstHandle fh, const atom::Hierarchies &hs)
def read_coordinates_of_rmfs
Read in coordinates of a set of RMF tuples.
Python classes to represent, score, sample and analyze models.
Functionality for loading, creating, manipulating and scoring atomic structures.
void add_provenance(Model *m, ParticleIndex pi, Provenance p)
Add provenance to part of the model.
Select hierarchy particles identified by the biological name.
Definition: Selection.h:66
Support for the RMF file format for storing hierarchical molecular data and markup.
def get_residue_indexes
Retrieve the residue indexes for the given particle.
Definition: tools.py:882
def add_provenance
Add provenance information in prov (a list of _TempProvenance objects) to each of the IMP hierarchies...
Store objects in order they were added, but with default type.
Definition: tools.py:1223