[docs]defwrite_xyz(filename:str,systems:List[System],capabilities:ModelCapabilities,predictions:Dict[str,TensorMap],)->None:"""An ase-based xyz file writer. Writes the systems and predictions to an xyz file. According to ASE practice, arrays which have a dimension corresponding to each atom are saved inside atoms.arrays, while any other arrays are saved inside atoms.info. :param filename: name of the file to save to. :param systems: structures to be written to the file. :param: capabilities: capabilities of the model. :param predictions: prediction values to be written to the file. """# we first split the predictions by structurepredictions_by_structure:List[Dict[str,TensorMap]]=[{}for_insystems]split_labels=[Labels(names=["system"],values=torch.tensor([[i_system]]))fori_systeminrange(len(systems))]fortarget_name,target_tensor_mapinpredictions.items():# split this target by structuretarget_tensor_map=target_tensor_map.to("cpu")split_target=metatensor.torch.split(target_tensor_map,"samples",split_labels)fori_system,system_targetinenumerate(split_target):# add the split target to the dict corresponding to the structurepredictions_by_structure[i_system][target_name]=system_targetframes=[]forsystem,system_predictionsinzip(systems,predictions_by_structure):info={}arrays={}fortarget_name,target_mapinsystem_predictions.items():iflen(target_map.keys)!=1:raiseValueError("Only single-block `TensorMap`s can be ""written to xyz files for the moment.")block=target_map.block()if"atom"inblock.samples.names:# save inside arraysvalues=block.values.detach().cpu().numpy()arrays[target_name]=values.reshape(values.shape[0],-1)# reshaping here is necessary because `arrays` only accepts 2D arrayselse:# save inside infoifblock.values.numel()==1:info[target_name]=block.values.item()else:info[target_name]=block.values.detach().cpu().numpy().squeeze(0)# squeeze the sample dimension, which corresponds to the systemforgradient_name,gradient_blockinblock.gradients():# here, we assume that gradients are always an array, and never a scalarinternal_name=f"{target_name}_{gradient_name}_gradients"external_name=to_external_name(internal_name,capabilities.outputs)if"forces"inexternal_name:arrays[external_name]=(# squeeze the property dimension-gradient_block.values.detach().cpu().squeeze(-1).numpy())elif"virial"inexternal_name:# in this case, we write both the virial and the stressexternal_name_virial=external_nameexternal_name_stress=external_name.replace("virial","stress")strain_derivatives=(# squeeze the property dimensiongradient_block.values.detach().cpu().squeeze(-1).numpy())ifnottorch.any(system.cell!=0):raiseValueError("stresses cannot be written for non-periodic systems.")cell_volume=torch.det(system.cell).item()ifcell_volume==0:raiseValueError("stresses cannot be written for systems with zero volume.")info[external_name_virial]=-strain_derivativesinfo[external_name_stress]=strain_derivatives/cell_volumeelse:info[external_name]=(# squeeze the property dimensiongradient_block.values.detach().cpu().squeeze(-1).numpy())atoms=ase.Atoms(symbols=system.types,positions=system.positions.detach(),info=info)# assign cell and pbcsiftorch.any(system.cell!=0):atoms.pbc=Trueatoms.cell=system.cell.detach().cpu().numpy()# assign arraysforarray_name,arrayinarrays.items():atoms.arrays[array_name]=arrayframes.append(atoms)ase.io.write(filename,frames)