Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# lammps.py (2011/03/29) 

2# An ASE calculator for the LAMMPS classical MD code available from 

3# http://lammps.sandia.gov/ 

4# The environment variable ASE_LAMMPSRUN_COMMAND must be defined to point to the 

5# LAMMPS binary. 

6# 

7# Copyright (C) 2009 - 2011 Joerg Meyer, joerg.meyer@ch.tum.de 

8# 

9# This library is free software; you can redistribute it and/or 

10# modify it under the terms of the GNU Lesser General Public 

11# License as published by the Free Software Foundation; either 

12# version 2.1 of the License, or (at your option) any later version. 

13# 

14# This library is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

17# Lesser General Public License for more details. 

18# 

19# You should have received a copy of the GNU Lesser General Public 

20# License along with this file; if not, write to the Free Software 

21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 

22# USA or see <http://www.gnu.org/licenses/>. 

23 

24 

25import os 

26import shutil 

27import shlex 

28from subprocess import Popen, PIPE, TimeoutExpired 

29from threading import Thread 

30from re import compile as re_compile, IGNORECASE 

31from tempfile import mkdtemp, NamedTemporaryFile, mktemp as uns_mktemp 

32import inspect 

33import warnings 

34from typing import Dict, Any 

35import numpy as np 

36 

37from ase.parallel import paropen 

38from ase.calculators.calculator import Calculator 

39from ase.calculators.calculator import all_changes 

40from ase.data import chemical_symbols 

41from ase.data import atomic_masses 

42from ase.io.lammpsdata import write_lammps_data 

43from ase.io.lammpsrun import read_lammps_dump 

44from ase.calculators.lammps import Prism 

45from ase.calculators.lammps import write_lammps_in 

46from ase.calculators.lammps import CALCULATION_END_MARK 

47from ase.calculators.lammps import convert 

48 

49__all__ = ["LAMMPS"] 

50 

51 

52class LAMMPS(Calculator): 

53 """The LAMMPS calculators object 

54 

55 files: list 

56 List of files typically containing relevant potentials for the 

57 calculation 

58 parameters: dict 

59 Dictionary of settings to be passed into the input file for calculation. 

60 specorder: list 

61 Within LAAMPS, atoms are identified by an integer value starting from 1. 

62 This variable allows the user to define the order of the indices 

63 assigned to the atoms in the calculation, with the default 

64 if not given being alphabetical 

65 keep_tmp_files: bool 

66 Retain any temporary files created. Mostly useful for debugging. 

67 tmp_dir: str 

68 path/dirname (default None -> create automatically). 

69 Explicitly control where the calculator object should create 

70 its files. Using this option implies 'keep_tmp_files' 

71 no_data_file: bool 

72 Controls whether an explicit data file will be used for feeding 

73 atom coordinates into lammps. Enable it to lessen the pressure on 

74 the (tmp) file system. THIS OPTION MIGHT BE UNRELIABLE FOR CERTAIN 

75 CORNER CASES (however, if it fails, you will notice...). 

76 keep_alive: bool 

77 When using LAMMPS as a spawned subprocess, keep the subprocess 

78 alive (but idling when unused) along with the calculator object. 

79 always_triclinic: bool 

80 Force use of a triclinic cell in LAMMPS, even if the cell is 

81 a perfect parallelepiped. 

82 

83 **Example** 

84 

85Provided that the respective potential file is in the working directory, one 

86can simply run (note that LAMMPS needs to be compiled to work with EAM 

87potentials) 

88 

89:: 

90 

91 from ase import Atom, Atoms 

92 from ase.build import bulk 

93 from ase.calculators.lammpsrun import LAMMPS 

94 

95 parameters = {'pair_style': 'eam/alloy', 

96 'pair_coeff': ['* * NiAlH_jea.eam.alloy H Ni']} 

97 

98 files = ['NiAlH_jea.eam.alloy'] 

99 

100 Ni = bulk('Ni', cubic=True) 

101 H = Atom('H', position=Ni.cell.diagonal()/2) 

102 NiH = Ni + H 

103 

104 lammps = LAMMPS(parameters=parameters, files=files) 

105 

106 NiH.calc = lammps 

107 print("Energy ", NiH.get_potential_energy()) 

108 

109(Remember you also need to set the environment variable 

110``$ASE_LAMMPSRUN_COMMAND``) 

111 

112 """ 

113 

114 name = "lammpsrun" 

115 implemented_properties = ["energy", "free_energy", "forces", "stress", 

116 "energies"] 

117 

118 # parameters to choose options in LAMMPSRUN 

119 ase_parameters: Dict[str, Any] = dict( 

120 specorder=None, 

121 atorder=True, 

122 always_triclinic=False, 

123 keep_alive=True, 

124 keep_tmp_files=False, 

125 no_data_file=False, 

126 tmp_dir=None, 

127 files=[], # usually contains potential parameters 

128 verbose=False, 

129 write_velocities=False, 

130 binary_dump=True, # bool - use binary dump files (full 

131 # precision but long long ids are casted to 

132 # double) 

133 lammps_options="-echo log -screen none -log /dev/stdout", 

134 trajectory_out=None, # file object, if is not None the 

135 # trajectory will be saved in it 

136 ) 

137 

138 # parameters forwarded to LAMMPS 

139 lammps_parameters = dict( 

140 boundary=None, # bounadry conditions styles 

141 units="metal", # str - Which units used; some potentials 

142 # require certain units 

143 atom_style="atomic", 

144 special_bonds=None, 

145 # potential informations 

146 pair_style="lj/cut 2.5", 

147 pair_coeff=["* * 1 1"], 

148 masses=None, 

149 pair_modify=None, 

150 # variables controlling the output 

151 thermo_args=[ 

152 "step", "temp", "press", "cpu", "pxx", "pyy", "pzz", 

153 "pxy", "pxz", "pyz", "ke", "pe", "etotal", "vol", "lx", 

154 "ly", "lz", "atoms", ], 

155 dump_properties=["id", "type", "x", "y", "z", "vx", "vy", 

156 "vz", "fx", "fy", "fz", ], 

157 dump_period=1, # period of system snapshot saving (in MD steps) 

158 ) 

159 

160 default_parameters = dict(ase_parameters, **lammps_parameters) 

161 

162 # legacy parameter persist, when the 'parameters' dictinary is manipulated 

163 # from the outside. All others are rested to the default value 

164 legacy_parameters = [ 

165 "specorder", 

166 "dump_period", 

167 "always_triclinic", 

168 "keep_alive", 

169 "keep_tmp_files", 

170 "tmp_dir", 

171 "parameters", 

172 "no_data_file", 

173 "files", 

174 "write_velocities", 

175 "trajectory_out", 

176 ] 

177 

178 legacy_parameters_map = {"_custom_thermo_args": "thermo_args"} 

179 

180 legacy_warn_string = "You are using an " 

181 legacy_warn_string += "old syntax to set '{}'.\n" 

182 legacy_warn_string += "Please use {}.set().".format(name.upper()) 

183 

184 def __init__(self, label="lammps", **kwargs): 

185 # "Parameters" used to be the dictionary with all parameters forwarded 

186 # to lammps. This clashes with the implementation in Calculator to 

187 # reload an old one. Trying to catch both cases to not break old 

188 # scripts. 

189 if "parameters" in kwargs: 

190 old_parameters = kwargs["parameters"] 

191 if isinstance(old_parameters, dict): 

192 warnings.warn(self.legacy_warn_string.format("parameters")) 

193 del kwargs["parameters"] 

194 else: 

195 old_parameters = None 

196 

197 Calculator.__init__(self, label=label, **kwargs) 

198 

199 if old_parameters and isinstance(old_parameters, dict): 

200 self.set(**old_parameters) 

201 

202 self.prism = None 

203 self.calls = 0 

204 self.forces = None 

205 # thermo_content contains data "written by" thermo_style. 

206 # It is a list of dictionaries, each dict (one for each line 

207 # printed by thermo_style) contains a mapping between each 

208 # custom_thermo_args-argument and the corresponding 

209 # value as printed by lammps. thermo_content will be 

210 # re-populated by the read_log method. 

211 self.thermo_content = [] 

212 

213 if self.parameters.tmp_dir is not None: 

214 # If tmp_dir is pointing somewhere, don't remove stuff! 

215 self.parameters.keep_tmp_files = True 

216 self._lmp_handle = None # To handle the lmp process 

217 

218 if self.parameters.tmp_dir is None: 

219 self.parameters.tmp_dir = mkdtemp(prefix="LAMMPS-") 

220 else: 

221 self.parameters.tmp_dir = os.path.realpath(self.parameters.tmp_dir) 

222 if not os.path.isdir(self.parameters.tmp_dir): 

223 os.mkdir(self.parameters.tmp_dir, 0o755) 

224 

225 for f in self.parameters.files: 

226 shutil.copy( 

227 f, os.path.join(self.parameters.tmp_dir, os.path.basename(f)) 

228 ) 

229 

230 def get_lammps_command(self): 

231 cmd = self.parameters.get('command') 

232 if cmd is None: 

233 envvar = 'ASE_{}_COMMAND'.format(self.name.upper()) 

234 cmd = os.environ.get(envvar) 

235 

236 if cmd is None: 

237 cmd = 'lammps' 

238 

239 opts = self.parameters.get('lammps_options') 

240 

241 if opts is not None: 

242 cmd = '{} {}'.format(cmd, opts) 

243 

244 return cmd 

245 

246 def __setattr__(self, key, value): 

247 """Catch attribute sets to emulate legacy behavior. 

248 

249 Old LAMMPSRUN allows to just override the parameters 

250 dictionary. "Modern" ase calculators can assume that default 

251 parameters are always set, overrides of the 

252 'parameters'-dictionary have to be caught and the default 

253 parameters need to be added first. A check refuses to set 

254 calculator attributes if they are unknown and set outside the 

255 '__init__' functions. 

256 """ 

257 # !TODO: remove and break somebody's code (e.g. the test examples) 

258 if ( 

259 key == "parameters" 

260 and value is not None 

261 and self.parameters is not None 

262 ): 

263 temp_dict = self.get_default_parameters() 

264 if self.parameters: 

265 for l_key in self.legacy_parameters: 

266 try: 

267 temp_dict[l_key] = self.parameters[l_key] 

268 except KeyError: 

269 pass 

270 temp_dict.update(value) 

271 value = temp_dict 

272 if key in self.legacy_parameters and key != "parameters": 

273 warnings.warn(self.legacy_warn_string.format(key)) 

274 self.set(**{key: value}) 

275 elif key in self.legacy_parameters_map: 

276 warnings.warn( 

277 self.legacy_warn_string.format( 

278 "{} for {}".format(self.legacy_parameters_map[key], key) 

279 ) 

280 ) 

281 self.set(**{self.legacy_parameters_map[key]: value}) 

282 # Catch setting none-default attributes 

283 # one test was assigning an useless Attribute, but it still worked 

284 # because the assigned object was before manipulation already handed 

285 # over to the calculator (10/2018) 

286 elif hasattr(self, key) or inspect.stack()[1][3] == "__init__": 

287 Calculator.__setattr__(self, key, value) 

288 else: 

289 raise AttributeError("Setting unknown Attribute '{}'".format(key)) 

290 

291 def __getattr__(self, key): 

292 """Corresponding getattribute-function to emulate legacy behavior. 

293 """ 

294 if key in self.legacy_parameters and key != "parameters": 

295 return self.parameters[key] 

296 if key in self.legacy_parameters_map: 

297 return self.parameters[self.legacy_parameters_map[key]] 

298 return object.__getattribute__(self, key) 

299 

300 def clean(self, force=False): 

301 

302 self._lmp_end() 

303 

304 if not self.parameters.keep_tmp_files or force: 

305 shutil.rmtree(self.parameters.tmp_dir) 

306 

307 def check_state(self, atoms, tol=1.0e-10): 

308 # Transforming the unit cell to conform to LAMMPS' convention for 

309 # orientation (c.f. https://lammps.sandia.gov/doc/Howto_triclinic.html) 

310 # results in some precision loss, so we use bit larger tolerance than 

311 # machine precision here. Note that there can also be precision loss 

312 # related to how many significant digits are specified for things in 

313 # the LAMMPS input file. 

314 return Calculator.check_state(self, atoms, tol) 

315 

316 def calculate(self, atoms=None, properties=None, system_changes=None): 

317 if properties is None: 

318 properties = self.implemented_properties 

319 if system_changes is None: 

320 system_changes = all_changes 

321 Calculator.calculate(self, atoms, properties, system_changes) 

322 self.run() 

323 

324 def _lmp_alive(self): 

325 # Return True if this calculator is currently handling a running 

326 # lammps process 

327 return self._lmp_handle and not isinstance( 

328 self._lmp_handle.poll(), int 

329 ) 

330 

331 def _lmp_end(self): 

332 # Close lammps input and wait for lammps to end. Return process 

333 # return value 

334 if self._lmp_alive(): 

335 # !TODO: handle lammps error codes 

336 try: 

337 self._lmp_handle.communicate(timeout=5) 

338 except TimeoutExpired: 

339 self._lmp_handle.kill() 

340 self._lmp_handle.communicate() 

341 err = self._lmp_handle.poll() 

342 assert err is not None 

343 return err 

344 

345 def set_missing_parameters(self): 

346 """Verify that all necessary variables are set. 

347 """ 

348 symbols = self.atoms.get_chemical_symbols() 

349 # If unspecified default to atom types in alphabetic order 

350 if not self.parameters.specorder: 

351 self.parameters.specorder = sorted(set(symbols)) 

352 

353 # !TODO: handle cases were setting masses actual lead to errors 

354 if not self.parameters.masses: 

355 self.parameters.masses = [] 

356 for type_id, specie in enumerate(self.parameters.specorder): 

357 mass = atomic_masses[chemical_symbols.index(specie)] 

358 self.parameters.masses += [ 

359 "{0:d} {1:f}".format(type_id + 1, mass) 

360 ] 

361 

362 # set boundary condtions 

363 if not self.parameters.boundary: 

364 b_str = " ".join(["fp"[int(x)] for x in self.atoms.get_pbc()]) 

365 self.parameters.boundary = b_str 

366 

367 def run(self, set_atoms=False): 

368 # !TODO: split this function 

369 """Method which explicitly runs LAMMPS.""" 

370 pbc = self.atoms.get_pbc() 

371 if all(pbc): 

372 cell = self.atoms.get_cell() 

373 elif not any(pbc): 

374 # large enough cell for non-periodic calculation - 

375 # LAMMPS shrink-wraps automatically via input command 

376 # "periodic s s s" 

377 # below 

378 cell = 2 * np.max(np.abs(self.atoms.get_positions())) * np.eye(3) 

379 else: 

380 warnings.warn( 

381 "semi-periodic ASE cell detected - translation " 

382 + "to proper LAMMPS input cell might fail" 

383 ) 

384 cell = self.atoms.get_cell() 

385 self.prism = Prism(cell) 

386 

387 self.set_missing_parameters() 

388 self.calls += 1 

389 

390 # change into subdirectory for LAMMPS calculations 

391 tempdir = self.parameters.tmp_dir 

392 

393 # setup file names for LAMMPS calculation 

394 label = "{0}{1:>06}".format(self.label, self.calls) 

395 lammps_in = uns_mktemp( 

396 prefix="in_" + label, dir=tempdir 

397 ) 

398 lammps_log = uns_mktemp( 

399 prefix="log_" + label, dir=tempdir 

400 ) 

401 lammps_trj_fd = NamedTemporaryFile( 

402 prefix="trj_" + label, 

403 suffix=(".bin" if self.parameters.binary_dump else ""), 

404 dir=tempdir, 

405 delete=(not self.parameters.keep_tmp_files), 

406 ) 

407 lammps_trj = lammps_trj_fd.name 

408 if self.parameters.no_data_file: 

409 lammps_data = None 

410 else: 

411 lammps_data_fd = NamedTemporaryFile( 

412 prefix="data_" + label, 

413 dir=tempdir, 

414 delete=(not self.parameters.keep_tmp_files), 

415 mode='w', 

416 encoding='ascii' 

417 ) 

418 write_lammps_data( 

419 lammps_data_fd, 

420 self.atoms, 

421 specorder=self.parameters.specorder, 

422 force_skew=self.parameters.always_triclinic, 

423 velocities=self.parameters.write_velocities, 

424 prismobj=self.prism, 

425 units=self.parameters.units, 

426 atom_style=self.parameters.atom_style 

427 ) 

428 lammps_data = lammps_data_fd.name 

429 lammps_data_fd.flush() 

430 

431 # see to it that LAMMPS is started 

432 if not self._lmp_alive(): 

433 command = self.get_lammps_command() 

434 # Attempt to (re)start lammps 

435 self._lmp_handle = Popen( 

436 shlex.split(command, posix=(os.name == "posix")), 

437 stdin=PIPE, 

438 stdout=PIPE, 

439 ) 

440 lmp_handle = self._lmp_handle 

441 

442 # Create thread reading lammps stdout (for reference, if requested, 

443 # also create lammps_log, although it is never used) 

444 if self.parameters.keep_tmp_files: 

445 lammps_log_fd = open(lammps_log, "wb") 

446 fd = SpecialTee(lmp_handle.stdout, lammps_log_fd) 

447 else: 

448 fd = lmp_handle.stdout 

449 thr_read_log = Thread(target=self.read_lammps_log, args=(fd,)) 

450 thr_read_log.start() 

451 

452 # write LAMMPS input (for reference, also create the file lammps_in, 

453 # although it is never used) 

454 if self.parameters.keep_tmp_files: 

455 lammps_in_fd = open(lammps_in, "wb") 

456 fd = SpecialTee(lmp_handle.stdin, lammps_in_fd) 

457 else: 

458 fd = lmp_handle.stdin 

459 write_lammps_in( 

460 lammps_in=fd, 

461 parameters=self.parameters, 

462 atoms=self.atoms, 

463 prismobj=self.prism, 

464 lammps_trj=lammps_trj, 

465 lammps_data=lammps_data, 

466 ) 

467 

468 if self.parameters.keep_tmp_files: 

469 lammps_in_fd.close() 

470 

471 # Wait for log output to be read (i.e., for LAMMPS to finish) 

472 # and close the log file if there is one 

473 thr_read_log.join() 

474 if self.parameters.keep_tmp_files: 

475 lammps_log_fd.close() 

476 

477 if not self.parameters.keep_alive: 

478 self._lmp_end() 

479 

480 exitcode = lmp_handle.poll() 

481 if exitcode and exitcode != 0: 

482 raise RuntimeError( 

483 "LAMMPS exited in {} with exit code: {}." 

484 "".format(tempdir, exitcode) 

485 ) 

486 

487 # A few sanity checks 

488 if len(self.thermo_content) == 0: 

489 raise RuntimeError("Failed to retrieve any thermo_style-output") 

490 if int(self.thermo_content[-1]["atoms"]) != len(self.atoms): 

491 # This obviously shouldn't happen, but if prism.fold_...() fails, 

492 # it could 

493 raise RuntimeError("Atoms have gone missing") 

494 

495 trj_atoms = read_lammps_dump( 

496 infileobj=lammps_trj, 

497 order=self.parameters.atorder, 

498 index=-1, 

499 prismobj=self.prism, 

500 specorder=self.parameters.specorder, 

501 ) 

502 

503 if set_atoms: 

504 self.atoms = trj_atoms.copy() 

505 

506 self.forces = trj_atoms.get_forces() 

507 # !TODO: trj_atoms is only the last snapshot of the system; Is it 

508 # desirable to save also the inbetween steps? 

509 if self.parameters.trajectory_out is not None: 

510 # !TODO: is it advisable to create here temporary atoms-objects 

511 self.trajectory_out.write(trj_atoms) 

512 

513 tc = self.thermo_content[-1] 

514 self.results["energy"] = convert( 

515 tc["pe"], "energy", self.parameters["units"], "ASE" 

516 ) 

517 self.results["free_energy"] = self.results["energy"] 

518 self.results['forces'] = convert(self.forces.copy(), 

519 'force', 

520 self.parameters['units'], 

521 'ASE') 

522 stress = np.array( 

523 [-tc[i] for i in ("pxx", "pyy", "pzz", "pyz", "pxz", "pxy")] 

524 ) 

525 

526 # We need to apply the Lammps rotation stuff to the stress: 

527 xx, yy, zz, yz, xz, xy = stress 

528 stress_tensor = np.array([[xx, xy, xz], 

529 [xy, yy, yz], 

530 [xz, yz, zz]]) 

531 R = self.prism.rot_mat 

532 stress_atoms = np.dot(R, stress_tensor) 

533 stress_atoms = np.dot(stress_atoms, R.T) 

534 stress_atoms = stress_atoms[[0, 1, 2, 1, 0, 0], 

535 [0, 1, 2, 2, 2, 1]] 

536 stress = stress_atoms 

537 

538 self.results["stress"] = convert( 

539 stress, "pressure", self.parameters["units"], "ASE" 

540 ) 

541 

542 lammps_trj_fd.close() 

543 if not self.parameters.no_data_file: 

544 lammps_data_fd.close() 

545 

546 def __enter__(self): 

547 return self 

548 

549 def __exit__(self, *args): 

550 self._lmp_end() 

551 

552 def read_lammps_log(self, lammps_log=None): 

553 # !TODO: somehow communicate 'thermo_content' explicitly 

554 """Method which reads a LAMMPS output log file.""" 

555 

556 if lammps_log is None: 

557 lammps_log = self.label + ".log" 

558 

559 if isinstance(lammps_log, str): 

560 fileobj = paropen(lammps_log, "wb") 

561 close_log_file = True 

562 else: 

563 # Expect lammps_in to be a file-like object 

564 fileobj = lammps_log 

565 close_log_file = False 

566 

567 # read_log depends on that the first (three) thermo_style custom args 

568 # can be capitalized and matched against the log output. I.e. 

569 # don't use e.g. 'ke' or 'cpu' which are labeled KinEng and CPU. 

570 mark_re = r"^\s*" + r"\s+".join( 

571 [x.capitalize() for x in self.parameters.thermo_args[0:3]] 

572 ) 

573 _custom_thermo_mark = re_compile(mark_re) 

574 

575 # !TODO: regex-magic necessary? 

576 # Match something which can be converted to a float 

577 f_re = r"([+-]?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|nan|inf))" 

578 n_args = len(self.parameters["thermo_args"]) 

579 # Create a re matching exactly N white space separated floatish things 

580 _custom_thermo_re = re_compile( 

581 r"^\s*" + r"\s+".join([f_re] * n_args) + r"\s*$", flags=IGNORECASE 

582 ) 

583 

584 thermo_content = [] 

585 line = fileobj.readline().decode("utf-8") 

586 while line and line.strip() != CALCULATION_END_MARK: 

587 # check error 

588 if 'ERROR:' in line: 

589 if close_log_file: 

590 fileobj.close() 

591 raise RuntimeError(f'LAMMPS exits with error message: {line}') 

592 

593 # get thermo output 

594 if _custom_thermo_mark.match(line): 

595 bool_match = True 

596 while bool_match: 

597 line = fileobj.readline().decode("utf-8") 

598 bool_match = _custom_thermo_re.match(line) 

599 if bool_match: 

600 # create a dictionary between each of the 

601 # thermo_style args and it's corresponding value 

602 thermo_content.append( 

603 dict( 

604 zip( 

605 self.parameters.thermo_args, 

606 map(float, bool_match.groups()), 

607 ) 

608 ) 

609 ) 

610 else: 

611 line = fileobj.readline().decode("utf-8") 

612 

613 if close_log_file: 

614 fileobj.close() 

615 

616 self.thermo_content = thermo_content 

617 

618 

619class SpecialTee: 

620 """A special purpose, with limited applicability, tee-like thing. 

621 

622 A subset of stuff read from, or written to, orig_fd, 

623 is also written to out_fd. 

624 It is used by the lammps calculator for creating file-logs of stuff 

625 read from, or written to, stdin and stdout, respectively. 

626 """ 

627 

628 def __init__(self, orig_fd, out_fd): 

629 self._orig_fd = orig_fd 

630 self._out_fd = out_fd 

631 self.name = orig_fd.name 

632 

633 def write(self, data): 

634 self._orig_fd.write(data) 

635 self._out_fd.write(data) 

636 self.flush() 

637 

638 def read(self, *args, **kwargs): 

639 data = self._orig_fd.read(*args, **kwargs) 

640 self._out_fd.write(data) 

641 return data 

642 

643 def readline(self, *args, **kwargs): 

644 data = self._orig_fd.readline(*args, **kwargs) 

645 self._out_fd.write(data) 

646 return data 

647 

648 def readlines(self, *args, **kwargs): 

649 data = self._orig_fd.readlines(*args, **kwargs) 

650 self._out_fd.write("".join(data)) 

651 return data 

652 

653 def flush(self): 

654 self._orig_fd.flush() 

655 self._out_fd.flush()