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