msreport 0.0.31__tar.gz → 0.0.32__tar.gz

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 (53) hide show
  1. {msreport-0.0.31 → msreport-0.0.32}/PKG-INFO +1 -1
  2. {msreport-0.0.31 → msreport-0.0.32}/msreport/__init__.py +1 -1
  3. {msreport-0.0.31 → msreport-0.0.32}/msreport/export.py +1 -1
  4. {msreport-0.0.31 → msreport-0.0.32}/msreport/helper/maxlfq.py +3 -3
  5. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/comparison.py +7 -2
  6. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/multivariate.py +34 -15
  7. {msreport-0.0.31 → msreport-0.0.32}/msreport/reader.py +146 -14
  8. {msreport-0.0.31 → msreport-0.0.32}/msreport.egg-info/PKG-INFO +1 -1
  9. {msreport-0.0.31 → msreport-0.0.32}/pyproject.toml +8 -0
  10. {msreport-0.0.31 → msreport-0.0.32}/tests/test_plot.py +4 -0
  11. {msreport-0.0.31 → msreport-0.0.32}/LICENSE.txt +0 -0
  12. {msreport-0.0.31 → msreport-0.0.32}/README.md +0 -0
  13. {msreport-0.0.31 → msreport-0.0.32}/msreport/aggregate/__init__.py +0 -0
  14. {msreport-0.0.31 → msreport-0.0.32}/msreport/aggregate/condense.py +0 -0
  15. {msreport-0.0.31 → msreport-0.0.32}/msreport/aggregate/pivot.py +0 -0
  16. {msreport-0.0.31 → msreport-0.0.32}/msreport/aggregate/summarize.py +0 -0
  17. {msreport-0.0.31 → msreport-0.0.32}/msreport/analyze.py +0 -0
  18. {msreport-0.0.31 → msreport-0.0.32}/msreport/errors.py +0 -0
  19. {msreport-0.0.31 → msreport-0.0.32}/msreport/fasta.py +0 -0
  20. {msreport-0.0.31 → msreport-0.0.32}/msreport/helper/__init__.py +0 -0
  21. {msreport-0.0.31 → msreport-0.0.32}/msreport/helper/calc.py +0 -0
  22. {msreport-0.0.31 → msreport-0.0.32}/msreport/helper/table.py +0 -0
  23. {msreport-0.0.31 → msreport-0.0.32}/msreport/helper/temp.py +0 -0
  24. {msreport-0.0.31 → msreport-0.0.32}/msreport/impute.py +0 -0
  25. {msreport-0.0.31 → msreport-0.0.32}/msreport/isobar.py +0 -0
  26. {msreport-0.0.31 → msreport-0.0.32}/msreport/normalize.py +0 -0
  27. {msreport-0.0.31 → msreport-0.0.32}/msreport/peptidoform.py +0 -0
  28. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/__init__.py +0 -0
  29. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/_partial_plots.py +0 -0
  30. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/distribution.py +0 -0
  31. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/quality.py +0 -0
  32. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/style.py +0 -0
  33. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/style_sheets/msreport-notebook.mplstyle +0 -0
  34. {msreport-0.0.31 → msreport-0.0.32}/msreport/plot/style_sheets/seaborn-whitegrid.mplstyle +0 -0
  35. {msreport-0.0.31 → msreport-0.0.32}/msreport/qtable.py +0 -0
  36. {msreport-0.0.31 → msreport-0.0.32}/msreport/rinterface/__init__.py +0 -0
  37. {msreport-0.0.31 → msreport-0.0.32}/msreport/rinterface/limma.py +0 -0
  38. {msreport-0.0.31 → msreport-0.0.32}/msreport/rinterface/rinstaller.py +0 -0
  39. {msreport-0.0.31 → msreport-0.0.32}/msreport/rinterface/rscripts/limma.R +0 -0
  40. {msreport-0.0.31 → msreport-0.0.32}/msreport.egg-info/SOURCES.txt +0 -0
  41. {msreport-0.0.31 → msreport-0.0.32}/msreport.egg-info/dependency_links.txt +0 -0
  42. {msreport-0.0.31 → msreport-0.0.32}/msreport.egg-info/requires.txt +0 -0
  43. {msreport-0.0.31 → msreport-0.0.32}/msreport.egg-info/top_level.txt +0 -0
  44. {msreport-0.0.31 → msreport-0.0.32}/setup.cfg +0 -0
  45. {msreport-0.0.31 → msreport-0.0.32}/setup.py +0 -0
  46. {msreport-0.0.31 → msreport-0.0.32}/tests/test_analyze.py +0 -0
  47. {msreport-0.0.31 → msreport-0.0.32}/tests/test_export.py +0 -0
  48. {msreport-0.0.31 → msreport-0.0.32}/tests/test_helper.py +0 -0
  49. {msreport-0.0.31 → msreport-0.0.32}/tests/test_impute.py +0 -0
  50. {msreport-0.0.31 → msreport-0.0.32}/tests/test_isobar.py +0 -0
  51. {msreport-0.0.31 → msreport-0.0.32}/tests/test_maxlfq.py +0 -0
  52. {msreport-0.0.31 → msreport-0.0.32}/tests/test_peptidoform.py +0 -0
  53. {msreport-0.0.31 → msreport-0.0.32}/tests/test_qtable.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msreport
3
- Version: 0.0.31
3
+ Version: 0.0.32
4
4
  Summary: Post processing and analysis of quantitative proteomics data
5
5
  Author-email: "David M. Hollenstein" <hollenstein.david@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -8,4 +8,4 @@ from msreport.fasta import import_protein_database
8
8
  from msreport.qtable import Qtable
9
9
  from msreport.reader import FragPipeReader, MaxQuantReader, SpectronautReader
10
10
 
11
- __version__ = "0.0.31"
11
+ __version__ = "0.0.32"
@@ -502,7 +502,7 @@ def _find_covered_region_boundaries(
502
502
  Examples:
503
503
  >>> coverage_mask = [True, True, False, False, True]
504
504
  >>> _find_covered_region_boundaries(coverage_mask)
505
- ... [(0, 1), (4, 4)]
505
+ [(0, 1), (4, 4)]
506
506
  """
507
507
  start = []
508
508
  stop = []
@@ -113,9 +113,9 @@ def calculate_pairwise_mode_log_ratio_matrix(
113
113
  ... ]
114
114
  ... )
115
115
  >>> calculate_pairwise_mode_log_ratio_matrix(array)
116
- array([[ 0. , -0.0849625, -1. ],
117
- [ 0.0849625, 0. , -1. ],
118
- [ 1. , 1. , 0. ]])
116
+ array([[ 0. , -0.08496251, -1. ],
117
+ [ 0.08496251, 0. , -1. ],
118
+ [ 1. , 1. , 0. ]])
119
119
  """
120
120
  ratio_marix = _calculate_pairwise_centered_log_ratio_matrix(
121
121
  array, msreport.helper.mode, log_transformed=log_transformed
@@ -77,10 +77,15 @@ def volcano_ma(
77
77
  )
78
78
  special_entries = list(special_entries) + list(special_proteins)
79
79
 
80
- data = qtable.get_data(exclude_invalid=exclude_invalid)
81
- if annotation_column not in data.columns:
80
+ if annotation_column not in qtable.data.columns:
82
81
  annotation_column = qtable.id_column
83
82
 
83
+ data = qtable.get_data(exclude_invalid=exclude_invalid)
84
+ mask = np.ones(data.shape[0], dtype=bool)
85
+ for tag in [ratio_tag, expression_tag, pvalue_tag]:
86
+ mask = mask & np.isfinite(data[f"{tag} {comparison_group}"])
87
+ data = data[mask]
88
+
84
89
  scatter_size = 2 / (max(min(data.shape[0], 10000), 1000) / 1000)
85
90
 
86
91
  masks = {
@@ -21,6 +21,7 @@ def sample_pca(
21
21
  pc_x: str = "PC1",
22
22
  pc_y: str = "PC2",
23
23
  exclude_invalid: bool = True,
24
+ exclude_missing: bool = False,
24
25
  ) -> tuple[plt.Figure, list[plt.Axes]]:
25
26
  """Figure to compare sample similarities with a principle component analysis.
26
27
 
@@ -44,11 +45,14 @@ def sample_pca(
44
45
  samples.
45
46
  exclude_invalid: If True, rows are filtered according to the Boolean entries of
46
47
  the "Valid" column.
48
+ exclude_missing: If True, only rows without any missing values are used.
47
49
 
48
50
  Returns:
49
51
  A matplotlib Figure and a list of Axes objects, containing the PCA plots.
50
52
  """
51
53
  design = qtable.get_design()
54
+ samples = qtable.get_samples()
55
+
52
56
  if design.shape[0] < 3:
53
57
  fig, ax = plt.subplots(1, 1, figsize=(2, 1.3))
54
58
  fig.suptitle(f'PCA of "{tag}" values', y=1.1)
@@ -65,13 +69,22 @@ def sample_pca(
65
69
  return fig, np.array([ax])
66
70
 
67
71
  table = qtable.make_sample_table(
68
- tag, samples_as_columns=True, exclude_invalid=exclude_invalid
72
+ tag, samples_as_columns=True, exclude_invalid=False
69
73
  )
74
+
75
+ inclusion_mask = np.ones(qtable.data.shape[0], dtype=bool)
76
+ if exclude_invalid:
77
+ inclusion_mask = inclusion_mask & qtable["Valid"]
78
+ if exclude_missing:
79
+ _non_missing_masks = [(qtable[f"Missing {s}"] == 0) for s in samples]
80
+ inclusion_mask = inclusion_mask & (np.all(_non_missing_masks, axis=0))
81
+ table = table[inclusion_mask]
82
+
70
83
  table = table.replace({0: np.nan})
71
84
  table = table[np.isfinite(table).sum(axis=1) > 0]
72
85
  if not msreport.helper.intensities_in_logspace(table):
73
86
  table = np.log2(table)
74
- table[table.isna()] = 0
87
+ table = table.fillna(0)
75
88
 
76
89
  table = table.transpose()
77
90
  sample_index = table.index.tolist()
@@ -203,6 +216,7 @@ def sample_pca(
203
216
  def expression_clustermap(
204
217
  qtable: Qtable,
205
218
  exclude_invalid: bool = True,
219
+ exclude_missing: bool = False,
206
220
  remove_imputation: bool = True,
207
221
  mean_center: bool = False,
208
222
  cluster_samples: bool = True,
@@ -218,6 +232,7 @@ def expression_clustermap(
218
232
  qtable: A `Qtable` instance, which data is used for plotting.
219
233
  exclude_invalid: If True, rows are filtered according to the Boolean entries of
220
234
  the "Valid" column.
235
+ exclude_missing: If True, only rows without any missing values are used.
221
236
  remove_imputation: If True, imputed values are set to 0 before clustering.
222
237
  Defaults to True.
223
238
  mean_center: If True, the data is mean-centered before clustering. Defaults to
@@ -242,25 +257,29 @@ def expression_clustermap(
242
257
  if len(samples) < 2:
243
258
  raise ValueError("At least two samples are required to generate a clustermap.")
244
259
 
245
- data = qtable.make_expression_table(samples_as_columns=True)
260
+ data = qtable.make_expression_table(samples_as_columns=True, exclude_invalid=False)
246
261
  data = data[samples]
262
+ data = data.fillna(0)
247
263
 
248
- for sample in samples:
249
- if remove_imputation:
250
- data.loc[qtable.data[f"Missing {sample}"], sample] = 0
251
- data[sample] = data[sample].fillna(0)
252
-
253
- if not mean_center:
254
- # Hide missing values in the heatmap, making them appear white
255
- mask_values = qtable.data[
264
+ if not mean_center: # Hide missing values in the heatmap, making them appear white
265
+ hide_values_mask = qtable.data[
256
266
  [f"Missing {sample}" for sample in samples]
257
267
  ].to_numpy()
258
268
  else:
259
- mask_values = np.zeros(data.shape, dtype=bool)
269
+ hide_values_mask = np.zeros(data.shape, dtype=bool)
270
+
271
+ if remove_imputation:
272
+ for sample in samples:
273
+ data.loc[qtable.data[f"Missing {sample}"], sample] = 0
260
274
 
275
+ inclusion_mask = np.ones(data.shape[0], dtype=bool)
261
276
  if exclude_invalid:
262
- data = data[qtable.data["Valid"]]
263
- mask_values = mask_values[qtable.data["Valid"]]
277
+ inclusion_mask = inclusion_mask & qtable["Valid"]
278
+ if exclude_missing:
279
+ _non_missing_masks = [(qtable[f"Missing {s}"] == 0) for s in samples]
280
+ inclusion_mask = inclusion_mask & (np.all(_non_missing_masks, axis=0))
281
+ hide_values_mask = hide_values_mask[inclusion_mask]
282
+ data = data[inclusion_mask]
264
283
 
265
284
  color_wheel = ColorWheelDict()
266
285
  for exp in experiments:
@@ -314,7 +333,7 @@ def expression_clustermap(
314
333
  col_cluster=cluster_samples,
315
334
  col_colors=sample_colors,
316
335
  row_colors=["#000000" for _ in range(len(data))],
317
- mask=mask_values,
336
+ mask=hide_values_mask,
318
337
  method=cluster_method,
319
338
  metric="euclidean",
320
339
  **heatmap_args,
@@ -545,7 +545,12 @@ class FragPipeReader(ResultReader):
545
545
  """FragPipe result reader.
546
546
 
547
547
  Methods:
548
- import_design: Reads a "fragpipe-files.fp-manifest" file and returns a
548
+ import_design: Depending on the quantification strategy, imports either the
549
+ manifest file or the experiment annotation file and returns a processed
550
+ design dataframe.
551
+ import_manifest: Reads a "fragpipe-files.fp-manifest" file and returns a
552
+ processed design dataframe.
553
+ import_experiment_annotation: Reads a "experiment_annotation" file and returns a
549
554
  processed design dataframe.
550
555
  import_proteins: Reads a "combined_protein.tsv" or "protein.tsv" file and
551
556
  returns a processed dataframe, conforming to the MsReport naming
@@ -589,12 +594,8 @@ class FragPipeReader(ResultReader):
589
594
  "ions": "combined_ion.tsv",
590
595
  "ion_evidence": "ion.tsv",
591
596
  "psm_evidence": "psm.tsv",
592
- "design": "fragpipe-files.fp-manifest",
593
- }
594
- isobar_filenames: dict[str, str] = {
595
- "proteins": "protein.tsv",
596
- "peptides": "peptide.tsv",
597
- "ions": "ion.tsv",
597
+ "manifest": "fragpipe-files.fp-manifest",
598
+ "experiment_annotation": "experiment_annotation.tsv",
598
599
  }
599
600
  sil_filenames: dict[str, str] = {
600
601
  "proteins": "combined_protein_label_quant.tsv",
@@ -675,14 +676,27 @@ class FragPipeReader(ResultReader):
675
676
  self._isobar: bool = isobar
676
677
  self._sil: bool = sil
677
678
  self._contaminant_tag: str = contaminant_tag
678
- if isobar:
679
- self.filenames = self.isobar_filenames
680
- elif sil:
681
- self.filenames = self.sil_filenames
679
+
680
+ self.filenames = self.default_filenames.copy()
681
+ if sil:
682
+ self.filenames.update(self.sil_filenames)
683
+
684
+ def import_design(self, sort: bool = False) -> pd.DataFrame:
685
+ """Reads the experimental design file and returns a processed design dataframe.
686
+
687
+ Depending on the quantification strategy (isobaric or label-free/sil), either
688
+ the experiment annotation file or the manifest file is imported.
689
+
690
+ Args:
691
+ sort: If True, the design dataframe is sorted by "Experiment" and
692
+ "Replicate"; default False.
693
+ """
694
+ if self._isobar:
695
+ return self.import_experiment_annotation(sort=sort)
682
696
  else:
683
- self.filenames = self.default_filenames
697
+ return self.import_manifest(sort=sort)
684
698
 
685
- def import_design(
699
+ def import_manifest(
686
700
  self, filename: Optional[str] = None, sort: bool = False
687
701
  ) -> pd.DataFrame:
688
702
  """Read a 'fp-manifest' file and returns a processed design dataframe.
@@ -709,7 +723,7 @@ class FragPipeReader(ResultReader):
709
723
  FileNotFoundError: If the specified manifest file does not exist.
710
724
  """
711
725
  if filename is None:
712
- filepath = os.path.join(self.data_directory, self.filenames["design"])
726
+ filepath = os.path.join(self.data_directory, self.filenames["manifest"])
713
727
  else:
714
728
  filepath = os.path.join(self.data_directory, filename)
715
729
  if not os.path.exists(filepath):
@@ -748,6 +762,63 @@ class FragPipeReader(ResultReader):
748
762
  design.reset_index(drop=True, inplace=True)
749
763
  return design
750
764
 
765
+ def import_experiment_annotation(
766
+ self, filename: Optional[str] = None, sort: bool = False
767
+ ) -> pd.DataFrame:
768
+ """Read a 'experiment_annotation' file and returns a processed design dataframe.
769
+
770
+ The annotation columns "sample", "channel", and "plex" are mapped to the design
771
+ table columns "Sample", "Channel", and "Plex". The "Experiment" and "Replicate"
772
+ columns are extracted from the "Sample" column by splitting at the last
773
+ underscore, if there is no underscore, "Replicate" is set to an empty string.
774
+
775
+ Note that this convention of splitting the "Sample" column does confirm to the
776
+ FragPipe convention, but FragPipe does not enforce it for the experiment
777
+ annotation file.
778
+
779
+ Args:
780
+ filename: Allows specifying an alternative filename, otherwise the default
781
+ filename is used.
782
+ sort: If True, the design dataframe is sorted by "Experiment" and
783
+ "Replicate"; default False.
784
+
785
+ Returns:
786
+ A dataframe containing the processed design table with columns:
787
+ "Sample", "Experiment", "Replicate", "Channel", and "Plex".
788
+
789
+ Raises:
790
+ FileNotFoundError: If the specified manifest file does not exist.
791
+ """
792
+ if filename is None:
793
+ filepath = os.path.join(
794
+ self.data_directory, self.filenames["experiment_annotation"]
795
+ )
796
+ else:
797
+ filepath = os.path.join(self.data_directory, filename)
798
+ if not os.path.exists(filepath):
799
+ raise FileNotFoundError(
800
+ f"File '{filepath}' does not exist. Please check the file path."
801
+ )
802
+
803
+ annotation = pd.read_csv(filepath, sep="\t")
804
+
805
+ design = pd.DataFrame(
806
+ {
807
+ "Sample": annotation["sample"],
808
+ "Experiment": annotation["sample"].str.rsplit("_", n=1).str[0],
809
+ "Replicate": annotation["sample"].str.rsplit("_", n=1).str[1],
810
+ "Channel": annotation["channel"],
811
+ "Plex": annotation["plex"],
812
+ }
813
+ )
814
+ design["Replicate"] = design["Replicate"].fillna("")
815
+
816
+ if sort:
817
+ design.sort_values(by=["Experiment", "Replicate"], inplace=True)
818
+ design.reset_index(drop=True, inplace=True)
819
+
820
+ return design
821
+
751
822
  def import_proteins(
752
823
  self,
753
824
  filename: Optional[str] = None,
@@ -1034,6 +1105,7 @@ class FragPipeReader(ResultReader):
1034
1105
  )
1035
1106
  df["Modified sequence"] = mod_entries["Modified sequence"]
1036
1107
  df["Modifications"] = mod_entries["Modifications"]
1108
+ df = self._add_modification_localization_string_to_psm_evidence(df)
1037
1109
  return df
1038
1110
 
1039
1111
  def _add_protein_entries(self, df: pd.DataFrame) -> pd.DataFrame:
@@ -1207,6 +1279,66 @@ class FragPipeReader(ResultReader):
1207
1279
  new_df[new_column] = localization_strings
1208
1280
  return new_df
1209
1281
 
1282
+ def _add_modification_localization_string_to_psm_evidence(
1283
+ self, df: pd.DataFrame
1284
+ ) -> pd.DataFrame:
1285
+ """Adds a modification localization string column to a PSM evidence table.
1286
+
1287
+ Extracts localization probabilities from all columns in the form
1288
+ f"{aa:modification}", converts them into the standardized modification
1289
+ localization string format used by msreport, and adds a new column
1290
+ "Modification localization string".
1291
+
1292
+ Probabilities are written in the format
1293
+ "Mod1@Site1:Probability1,Site2:Probability2;Mod2@Site3:Probability3",
1294
+ e.g. "15.9949@11:1.000;79.9663@3:0.200,4:0.800". Refer to
1295
+ `msreport.peptidoform.make_localization_string` for details.
1296
+
1297
+ Args:
1298
+ df: A dataframe containing PSM tables from FragPipe.
1299
+
1300
+ Returns:
1301
+ A copy of the input dataframe with the added
1302
+ "Modification localization string" column.
1303
+ """
1304
+ new_df = df.copy()
1305
+ _search_tag = " Best Localization"
1306
+ mod_localization_columns = [
1307
+ c.strip(_search_tag) for c in new_df.columns if c.endswith(_search_tag)
1308
+ ]
1309
+ if not mod_localization_columns:
1310
+ new_df["Modification localization string"] = ""
1311
+ return new_df
1312
+
1313
+ df[mod_localization_columns] = (
1314
+ df[mod_localization_columns].astype(str).replace("nan", "")
1315
+ )
1316
+ row_mod_probabilities: list[dict[str, dict[int, float]]] = [
1317
+ {} for i in range(df.shape[0])
1318
+ ]
1319
+ for mod_localization_column in mod_localization_columns:
1320
+ modification = mod_localization_column.split(":")[1]
1321
+ for modification_probabilities, probability_sequence in zip(
1322
+ row_mod_probabilities, df[mod_localization_column]
1323
+ ):
1324
+ if not probability_sequence:
1325
+ continue
1326
+ _, probabilities = msreport.peptidoform.parse_modified_sequence(
1327
+ probability_sequence, "(", ")"
1328
+ )
1329
+ modification_probabilities[modification] = {
1330
+ site: float(probability) for site, probability in probabilities
1331
+ }
1332
+
1333
+ localization_strings = []
1334
+ for localization_probabilities in row_mod_probabilities:
1335
+ localization_string = msreport.peptidoform.make_localization_string(
1336
+ localization_probabilities
1337
+ )
1338
+ localization_strings.append(localization_string)
1339
+ new_df["Modification localization string"] = localization_strings
1340
+ return new_df
1341
+
1210
1342
 
1211
1343
  class SpectronautReader(ResultReader):
1212
1344
  """Spectronaut result reader.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msreport
3
- Version: 0.0.31
3
+ Version: 0.0.32
4
4
  Summary: Post processing and analysis of quantitative proteomics data
5
5
  Author-email: "David M. Hollenstein" <hollenstein.david@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -112,3 +112,11 @@ module = [
112
112
  "yaml.*",
113
113
  ]
114
114
  follow_untyped_imports = true
115
+
116
+ [tool.pytest.ini_options]
117
+ addopts = "--doctest-modules"
118
+ doctest_optionflags = "NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
119
+ testpaths = [
120
+ "tests",
121
+ "msreport",
122
+ ]
@@ -1,3 +1,4 @@
1
+ import matplotlib
1
2
  import numpy as np
2
3
  import pandas as pd
3
4
  import pytest
@@ -5,6 +6,9 @@ import pytest
5
6
  import msreport.plot
6
7
  import msreport.qtable
7
8
 
9
+ # Use the 'Agg' backend for plotting tests to prevent TclError in headless environments.
10
+ matplotlib.use("Agg")
11
+
8
12
 
9
13
  @pytest.fixture
10
14
  def example_data():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes