vortex-nwp 2.0.0__py3-none-any.whl

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 (144) hide show
  1. vortex/__init__.py +159 -0
  2. vortex/algo/__init__.py +13 -0
  3. vortex/algo/components.py +2462 -0
  4. vortex/algo/mpitools.py +1953 -0
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  7. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  8. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  9. vortex/algo/serversynctools.py +171 -0
  10. vortex/config.py +112 -0
  11. vortex/data/__init__.py +19 -0
  12. vortex/data/abstractstores.py +1510 -0
  13. vortex/data/containers.py +835 -0
  14. vortex/data/contents.py +622 -0
  15. vortex/data/executables.py +275 -0
  16. vortex/data/flow.py +119 -0
  17. vortex/data/geometries.ini +2689 -0
  18. vortex/data/geometries.py +799 -0
  19. vortex/data/handlers.py +1230 -0
  20. vortex/data/outflow.py +67 -0
  21. vortex/data/providers.py +487 -0
  22. vortex/data/resources.py +207 -0
  23. vortex/data/stores.py +1390 -0
  24. vortex/data/sync_templates/__init__.py +0 -0
  25. vortex/gloves.py +309 -0
  26. vortex/layout/__init__.py +20 -0
  27. vortex/layout/contexts.py +577 -0
  28. vortex/layout/dataflow.py +1220 -0
  29. vortex/layout/monitor.py +969 -0
  30. vortex/nwp/__init__.py +14 -0
  31. vortex/nwp/algo/__init__.py +21 -0
  32. vortex/nwp/algo/assim.py +537 -0
  33. vortex/nwp/algo/clim.py +1086 -0
  34. vortex/nwp/algo/coupling.py +831 -0
  35. vortex/nwp/algo/eda.py +840 -0
  36. vortex/nwp/algo/eps.py +785 -0
  37. vortex/nwp/algo/forecasts.py +886 -0
  38. vortex/nwp/algo/fpserver.py +1303 -0
  39. vortex/nwp/algo/ifsnaming.py +463 -0
  40. vortex/nwp/algo/ifsroot.py +404 -0
  41. vortex/nwp/algo/monitoring.py +263 -0
  42. vortex/nwp/algo/mpitools.py +694 -0
  43. vortex/nwp/algo/odbtools.py +1258 -0
  44. vortex/nwp/algo/oopsroot.py +916 -0
  45. vortex/nwp/algo/oopstests.py +220 -0
  46. vortex/nwp/algo/request.py +660 -0
  47. vortex/nwp/algo/stdpost.py +1641 -0
  48. vortex/nwp/data/__init__.py +30 -0
  49. vortex/nwp/data/assim.py +380 -0
  50. vortex/nwp/data/boundaries.py +314 -0
  51. vortex/nwp/data/climfiles.py +521 -0
  52. vortex/nwp/data/configfiles.py +153 -0
  53. vortex/nwp/data/consts.py +954 -0
  54. vortex/nwp/data/ctpini.py +149 -0
  55. vortex/nwp/data/diagnostics.py +209 -0
  56. vortex/nwp/data/eda.py +147 -0
  57. vortex/nwp/data/eps.py +432 -0
  58. vortex/nwp/data/executables.py +1045 -0
  59. vortex/nwp/data/fields.py +111 -0
  60. vortex/nwp/data/gridfiles.py +380 -0
  61. vortex/nwp/data/logs.py +584 -0
  62. vortex/nwp/data/modelstates.py +363 -0
  63. vortex/nwp/data/monitoring.py +193 -0
  64. vortex/nwp/data/namelists.py +696 -0
  65. vortex/nwp/data/obs.py +840 -0
  66. vortex/nwp/data/oopsexec.py +74 -0
  67. vortex/nwp/data/providers.py +207 -0
  68. vortex/nwp/data/query.py +206 -0
  69. vortex/nwp/data/stores.py +160 -0
  70. vortex/nwp/data/surfex.py +337 -0
  71. vortex/nwp/syntax/__init__.py +9 -0
  72. vortex/nwp/syntax/stdattrs.py +437 -0
  73. vortex/nwp/tools/__init__.py +10 -0
  74. vortex/nwp/tools/addons.py +40 -0
  75. vortex/nwp/tools/agt.py +67 -0
  76. vortex/nwp/tools/bdap.py +59 -0
  77. vortex/nwp/tools/bdcp.py +41 -0
  78. vortex/nwp/tools/bdm.py +24 -0
  79. vortex/nwp/tools/bdmp.py +54 -0
  80. vortex/nwp/tools/conftools.py +1661 -0
  81. vortex/nwp/tools/drhook.py +66 -0
  82. vortex/nwp/tools/grib.py +294 -0
  83. vortex/nwp/tools/gribdiff.py +104 -0
  84. vortex/nwp/tools/ifstools.py +203 -0
  85. vortex/nwp/tools/igastuff.py +273 -0
  86. vortex/nwp/tools/mars.py +68 -0
  87. vortex/nwp/tools/odb.py +657 -0
  88. vortex/nwp/tools/partitioning.py +258 -0
  89. vortex/nwp/tools/satrad.py +71 -0
  90. vortex/nwp/util/__init__.py +6 -0
  91. vortex/nwp/util/async.py +212 -0
  92. vortex/nwp/util/beacon.py +40 -0
  93. vortex/nwp/util/diffpygram.py +447 -0
  94. vortex/nwp/util/ens.py +279 -0
  95. vortex/nwp/util/hooks.py +139 -0
  96. vortex/nwp/util/taskdeco.py +85 -0
  97. vortex/nwp/util/usepygram.py +697 -0
  98. vortex/nwp/util/usetnt.py +101 -0
  99. vortex/proxy.py +6 -0
  100. vortex/sessions.py +374 -0
  101. vortex/syntax/__init__.py +9 -0
  102. vortex/syntax/stdattrs.py +867 -0
  103. vortex/syntax/stddeco.py +185 -0
  104. vortex/toolbox.py +1117 -0
  105. vortex/tools/__init__.py +20 -0
  106. vortex/tools/actions.py +523 -0
  107. vortex/tools/addons.py +316 -0
  108. vortex/tools/arm.py +96 -0
  109. vortex/tools/compression.py +325 -0
  110. vortex/tools/date.py +27 -0
  111. vortex/tools/ddhpack.py +10 -0
  112. vortex/tools/delayedactions.py +782 -0
  113. vortex/tools/env.py +541 -0
  114. vortex/tools/folder.py +834 -0
  115. vortex/tools/grib.py +738 -0
  116. vortex/tools/lfi.py +953 -0
  117. vortex/tools/listings.py +423 -0
  118. vortex/tools/names.py +637 -0
  119. vortex/tools/net.py +2124 -0
  120. vortex/tools/odb.py +10 -0
  121. vortex/tools/parallelism.py +368 -0
  122. vortex/tools/prestaging.py +210 -0
  123. vortex/tools/rawfiles.py +10 -0
  124. vortex/tools/schedulers.py +480 -0
  125. vortex/tools/services.py +940 -0
  126. vortex/tools/storage.py +996 -0
  127. vortex/tools/surfex.py +61 -0
  128. vortex/tools/systems.py +3976 -0
  129. vortex/tools/targets.py +440 -0
  130. vortex/util/__init__.py +9 -0
  131. vortex/util/config.py +1122 -0
  132. vortex/util/empty.py +24 -0
  133. vortex/util/helpers.py +216 -0
  134. vortex/util/introspection.py +69 -0
  135. vortex/util/iosponge.py +80 -0
  136. vortex/util/roles.py +49 -0
  137. vortex/util/storefunctions.py +129 -0
  138. vortex/util/structs.py +26 -0
  139. vortex/util/worker.py +162 -0
  140. vortex_nwp-2.0.0.dist-info/METADATA +67 -0
  141. vortex_nwp-2.0.0.dist-info/RECORD +144 -0
  142. vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
  143. vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
  144. vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,447 @@
1
+ """
2
+ Usage of the EPyGrAM package to compute diffs.
3
+ """
4
+
5
+ import collections
6
+ import copy
7
+ import functools
8
+ import hashlib
9
+ import io
10
+ import json
11
+ import operator
12
+ import pprint
13
+
14
+ import footprints
15
+ from vortex import sessions
16
+ from . import usepygram
17
+
18
+
19
+ class HGeoDesc:
20
+ """Holds Epygram's horizontal geometry data."""
21
+
22
+ def __init__(self, epyfield):
23
+ """
24
+ :param epyfied: An epygram fild object.
25
+ """
26
+ geo = epyfield.geometry
27
+ self.grid = geo.grid
28
+ self.dimensions = geo.dimensions
29
+ self.name = geo.name
30
+ self.projection = (
31
+ None if not geo.projected_geometry else geo.projection
32
+ )
33
+ sio = io.StringIO()
34
+ geo.what(out=sio, vertical_geometry=False)
35
+ sio.seek(0)
36
+ self._what = sio.readlines()[3:]
37
+
38
+ def __eq__(self, other):
39
+ return (
40
+ (self.grid == other.grid)
41
+ and (self.dimensions == other.dimensions)
42
+ and (self.name == other.name)
43
+ and (self.projection == other.projection)
44
+ )
45
+
46
+ def __str__(self):
47
+ return "".join(self._what)
48
+
49
+
50
+ class DataDesc:
51
+ """Holds information about an Epygram's field data (basic stats + checksum)."""
52
+
53
+ def __init__(self, epyfield):
54
+ """
55
+ :param epyfied: An epygram fild object.
56
+ """
57
+ self.stats = epyfield.stats()
58
+ self.stats.pop("quadmean", None) # We do not want quadmean
59
+ s256 = hashlib.sha256()
60
+ s256.update(epyfield.data.tobytes())
61
+ self.checksum = s256.digest()
62
+
63
+ def __eq__(self, other):
64
+ return self.checksum == other.checksum
65
+
66
+ def __str__(self):
67
+ return ", ".join(
68
+ ["{:s}={!s}".format(k, v) for k, v in self.stats.items()]
69
+ )
70
+
71
+
72
+ class HGeoLibrary:
73
+ """A collection/library of :class:`HGeoDesc` objects."""
74
+
75
+ def __init__(self):
76
+ self._geolist = list()
77
+
78
+ def register(self, hgeo_desc):
79
+ """Check if an :class:`HGeoDesc` object is already in the library.
80
+
81
+ If the *hgeo_desc* object is not already in the library it is inserted.
82
+ In any case, the index of the *hgeo_desc* geometry within the library is
83
+ returned.
84
+ """
85
+ found = (None, None)
86
+ for i, g in enumerate(self._geolist):
87
+ if hgeo_desc == g:
88
+ found = (i, g)
89
+ break
90
+ if found == (None, None):
91
+ found = (len(self._geolist), hgeo_desc)
92
+ self._geolist.append(hgeo_desc)
93
+ return found[0]
94
+
95
+ def __str__(self):
96
+ outstr = ""
97
+ for i, g in enumerate(self._geolist):
98
+ outstr += "HORIZONTAL GEOMETRY #{:d}\n\n".format(i)
99
+ outstr += str(g)
100
+ outstr += "\n"
101
+ return outstr
102
+
103
+
104
+ class FieldDesc:
105
+ """Holds various information about an Epygram field."""
106
+
107
+ def __init__(self, hgeoid, vgeo, datadesc, fid, valid):
108
+ self.hgeoid = hgeoid
109
+ self.vgeo = vgeo
110
+ self.datadesc = datadesc
111
+ self.fid = fid
112
+ self.valid = valid
113
+
114
+ def ranking(self, other):
115
+ """
116
+ Compute the comparison score of the present field with respect to a
117
+ reference one (*other*).
118
+ """
119
+ fidscore = functools.reduce(
120
+ operator.add,
121
+ [
122
+ int(self.fid[k] == other.fid[k])
123
+ for k in self.fid.keys()
124
+ if k in other.fid
125
+ ],
126
+ )
127
+ fidscore = (
128
+ 5.0 * float(fidscore) / float(max(len(self.fid), len(other.fid)))
129
+ )
130
+ return (
131
+ int(self.valid != other.valid) * -5.0
132
+ + int(self.hgeoid != other.hgeoid) * -5.0
133
+ + int(self.vgeo != other.vgeo) * -4.0
134
+ + int(self.datadesc == other.datadesc) * 5.0
135
+ + fidscore
136
+ )
137
+
138
+ def ranking_summary(self, other):
139
+ """Returns detailed comparison information (including the ranking)."""
140
+ return (
141
+ self.datadesc == other.datadesc,
142
+ self.valid == other.valid,
143
+ self.hgeoid == other.hgeoid and self.vgeo == other.vgeo,
144
+ self.ranking(other),
145
+ )
146
+
147
+ def __str__(self):
148
+ out = "HGeo=#{:d} ; Validity={!s} ; metadata are:\n".format(
149
+ self.hgeoid, self.valid
150
+ )
151
+ out += pprint.pformat(self.fid) + "\n"
152
+ out += "Data: {!s}".format(self.datadesc)
153
+ return out
154
+
155
+ def prefixed_str(self, prefix):
156
+ """A representation of this object prefixed with the *prefix* string."""
157
+ return "\n".join([prefix + l for l in str(self).split("\n")])
158
+
159
+
160
+ class FieldBundle:
161
+ """A collection of FieldDesc objects."""
162
+
163
+ def __init__(self, hgeolib):
164
+ self._hgeolib = hgeolib
165
+ self._fields = list()
166
+
167
+ @property
168
+ def fields(self):
169
+ """The list of fields in the present collection."""
170
+ return self._fields
171
+
172
+ def _common_processing(self, fld, fid):
173
+ hgeo = HGeoDesc(fld)
174
+ vgeo = fld.geometry.vcoordinate
175
+ valid = fld.validity.get()
176
+ ddesc = DataDesc(fld)
177
+ hgeo_id = self._hgeolib.register(hgeo)
178
+ fid = copy.copy(fid)
179
+ fid["datebasis"] = fld.validity.getbasis()
180
+ fid["term"] = fld.validity.term()
181
+ fid["cumulativeduration"] = fld.validity.cumulativeduration()
182
+ return FieldDesc(hgeo_id, vgeo, ddesc, fid, valid)
183
+
184
+ @usepygram.epygram_checker.disabled_if_unavailable(version="1.0.0")
185
+ def read_grib(self, filename):
186
+ """Read in a GRIB file."""
187
+ with usepygram.epy_env_prepare(sessions.current()):
188
+ gribdata = footprints.proxy.dataformat(
189
+ filename=filename, openmode="r", format="GRIB"
190
+ )
191
+ fld = gribdata.iter_fields(
192
+ get_info_as_json=("centre", "subCentre")
193
+ )
194
+ while fld:
195
+ fid = fld.fid.get("GRIB2", fld.fid.get("GRIB1"))
196
+ fid.update(json.loads(fld.comment))
197
+ self._fields.append(self._common_processing(fld, fid))
198
+ fld = gribdata.iter_fields(
199
+ get_info_as_json=("centre", "subCentre")
200
+ )
201
+
202
+
203
+ class FieldBundles:
204
+ """A collection of :class:`FieldBundle` objects."""
205
+
206
+ def __init__(self):
207
+ self._hgeolib = HGeoLibrary()
208
+ self._bundles = dict()
209
+
210
+ @property
211
+ def hgeo_library(self):
212
+ """The :class:`HGeoLibrary` onject being used in this collection."""
213
+ return self._hgeolib
214
+
215
+ def new_bundle(self, name):
216
+ """Create a new :class:`FieldBundle` object in this collection."""
217
+ fbd = FieldBundle(self._hgeolib)
218
+ self._bundles[name] = fbd
219
+ return fbd
220
+
221
+ @property
222
+ def bundles(self):
223
+ """The dictionary of bundles in the present collection."""
224
+ return self._bundles
225
+
226
+
227
+ class EpyGribDiff(FieldBundles):
228
+ """A specialised version of :class:`FieldBundles` that deals with GRIB files."""
229
+
230
+ _FMT_COUNTER = "[{:04d}] "
231
+ _HEAD_COUNTER = " " * len(_FMT_COUNTER.format(0))
232
+
233
+ _FMT_SHORT = (
234
+ "#{n:>4d} id={id:16s} l={level:<6d} c={centre:<3d},{scentre:3d}"
235
+ )
236
+ _HEAD_SHORT = "Mess. ParamId/ShortN Level Centre,S "
237
+
238
+ _FMT_MIDDLE = " | {0:1s} {1:1s} {2:1s} {3:6s} | "
239
+ _HEAD_MIDDLE = " | {:1s} {:1s} {:1s} {:5s} | "
240
+ _ELTS_MIDDLE = ("data", "valid", "geo", "score")
241
+
242
+ _SPACER = (
243
+ _HEAD_COUNTER
244
+ + "REF "
245
+ + "-" * (len(_HEAD_SHORT) - 4)
246
+ + " | ----- ----- | "
247
+ + "NEW "
248
+ + "-" * (len(_HEAD_SHORT) - 4)
249
+ )
250
+
251
+ _DETAILED_SUMARY = "Data: {0:1s} ; Validity Date: {1:1s} ; HGeometry: {2:s} ; Score: {3:6s}"
252
+
253
+ def __init__(self, ref, new):
254
+ """
255
+ :param str ref: Path to the reference GRIB file
256
+ :param str new: Path to the new GRIB file
257
+ """
258
+ super().__init__()
259
+ self._new = self.new_bundle("New")
260
+ self._new.read_grib(new)
261
+ self._ref = self.new_bundle("Ref")
262
+ self._ref.read_grib(ref)
263
+
264
+ def _compute_diff(self):
265
+ """Explore all possible field combinations and find the closest match.
266
+
267
+ :return: tuple (newfield_id, list of matching reffield_ids, rankingscore,
268
+ list of ranking_summaries)
269
+ """
270
+ found = set()
271
+ couples = list()
272
+ for i, field in enumerate(self._new.fields):
273
+ rscore = collections.defaultdict(list)
274
+ rsummary = collections.defaultdict(list)
275
+ for j, rfield in enumerate(self._ref.fields):
276
+ tsummary = field.ranking_summary(rfield)
277
+ rscore[tsummary[-1]].append(j)
278
+ rsummary[tsummary[-1]].append(tsummary)
279
+ highest = max(rscore.keys())
280
+ # If the score is >= 3 the fields are paired...
281
+ # Note: Their might be several field combinations with the same
282
+ # ranking score
283
+ if highest >= 3.0:
284
+ refs = rscore[highest]
285
+ couples.append((i, refs, highest, rsummary[highest]))
286
+ found.update(refs)
287
+ else:
288
+ couples.append((i, (), None, None))
289
+ missings = set(range(len(self._ref.fields))) - found
290
+ if missings:
291
+ couples.append((None, list(missings), None, None))
292
+ return couples
293
+
294
+ @classmethod
295
+ def _str_header(cls):
296
+ """Returns the comparison table header."""
297
+ out = cls._SPACER + "\n"
298
+ e_len = max([len(e) for e in cls._ELTS_MIDDLE])
299
+ e_new = [
300
+ ("{:>" + str(e_len) + "s}").format(e.upper())
301
+ for e in cls._ELTS_MIDDLE
302
+ ]
303
+ if e_len > 1:
304
+ for i in range(e_len - 1):
305
+ out += (
306
+ cls._HEAD_COUNTER
307
+ + " " * len(cls._HEAD_SHORT)
308
+ + cls._HEAD_MIDDLE.format(*[e[i] for e in e_new])
309
+ + " " * len(cls._HEAD_SHORT)
310
+ + "\n"
311
+ )
312
+ out += (
313
+ (
314
+ cls._HEAD_COUNTER
315
+ + cls._HEAD_SHORT
316
+ + cls._HEAD_MIDDLE.format(*[e[-1] for e in e_new])
317
+ + cls._HEAD_SHORT
318
+ + "\n"
319
+ )
320
+ + cls._SPACER
321
+ + "\n"
322
+ )
323
+ return out
324
+
325
+ @classmethod
326
+ def _str_field_summary(cls, n, field):
327
+ """Returns a string that summarise a field properties."""
328
+ if "paramId" in field.fid:
329
+ # GRIB1
330
+ sid = str(field.fid["paramId"]) + "/" + field.fid["shortName"]
331
+ else:
332
+ # GRIB2
333
+ sid = (
334
+ str(field.fid["parameterCategory"])
335
+ + "-"
336
+ + str(field.fid["parameterNumber"])
337
+ + "/"
338
+ + field.fid["shortName"]
339
+ )
340
+ if len(sid) > 16: # Truncate if the string is too long
341
+ sid = sid[:15] + "*"
342
+ return cls._FMT_SHORT.format(
343
+ n=n,
344
+ id=sid,
345
+ level=field.fid.get("level", -99),
346
+ centre=field.fid["centre"],
347
+ scentre=field.fid.get("subCentre", -99),
348
+ )
349
+
350
+ @classmethod
351
+ def _str_rsummary_format(cls, rsum, fmt):
352
+ """Format the ranking_summary output."""
353
+ dmap = {True: "=", False: "!"}
354
+ return fmt.format(
355
+ dmap[rsum[0]],
356
+ dmap[rsum[1]],
357
+ dmap[rsum[2]],
358
+ "======" if rsum[3] == 10 else "{:6.2f}".format(rsum[3]),
359
+ )
360
+
361
+ @staticmethod
362
+ def _embedded_counter(c):
363
+ """Return the formatted comparison counter."""
364
+ return "[{:04d}] ".format(c)
365
+
366
+ def format_diff(self, detailed=True):
367
+ """Return a string that contains the comparison results.
368
+
369
+ :param bool detailed: If False, just returns the comparison table.
370
+ """
371
+ out = ""
372
+ counter = 0
373
+ for couple in self._compute_diff():
374
+ if couple[0] is None:
375
+ for n in couple[1]:
376
+ counter += 1
377
+ out += self._embedded_counter(counter)
378
+ if detailed:
379
+ out += "Unmatched reference field\n"
380
+ out += (
381
+ self.bundles["Ref"]
382
+ .fields[n]
383
+ .prefixed_str(" REF| ")
384
+ + "\n"
385
+ )
386
+ else:
387
+ out += self._str_field_summary(
388
+ n, self.bundles["Ref"].fields[n]
389
+ )
390
+ out += (
391
+ self._FMT_MIDDLE.format("?", "?", "?", " ?")
392
+ + "\n"
393
+ )
394
+ else:
395
+ new = self.bundles["New"].fields[couple[0]]
396
+ if len(couple[1]):
397
+ for i, n in enumerate(couple[1]):
398
+ counter += 1
399
+ ref = self.bundles["Ref"].fields[n]
400
+ out += self._embedded_counter(counter)
401
+ if detailed:
402
+ out += (
403
+ self._str_rsummary_format(
404
+ couple[3][i], self._DETAILED_SUMARY
405
+ )
406
+ + "\n"
407
+ )
408
+ out += (
409
+ ref.prefixed_str(" REF| ")
410
+ + "\n vs\n"
411
+ + new.prefixed_str(" NEW| ")
412
+ + "\n"
413
+ )
414
+ else:
415
+ out += self._str_field_summary(n, ref)
416
+ out += self._str_rsummary_format(
417
+ couple[3][i], self._FMT_MIDDLE
418
+ )
419
+ out += (
420
+ self._str_field_summary(couple[0], new)
421
+ if i == 0
422
+ else " idem."
423
+ )
424
+ out += "\n"
425
+ else:
426
+ counter += 1
427
+ out += self._embedded_counter(counter)
428
+ if detailed:
429
+ out += "Unmatched new field \n"
430
+ out += (
431
+ self.bundles["New"]
432
+ .fields[couple[0]]
433
+ .prefixed_str(" NEW| ")
434
+ + "\n"
435
+ )
436
+ else:
437
+ out += " " * len(self._HEAD_SHORT)
438
+ out += self._FMT_MIDDLE.format("?", "?", "?", " ?")
439
+ out += self._str_field_summary(couple[0], new) + "\n"
440
+ out += "\n" if detailed else ""
441
+ if detailed:
442
+ out += "LIST OF HORIZONTAL GEOMETRIES:\n\n"
443
+ out += str(self.hgeo_library)
444
+ return out
445
+
446
+ def __str__(self):
447
+ return self._str_header() + self.format_diff(detailed=False)