Skip to content

Commit 08b3721

Browse files
authored
Add CombineRasrCachesJob and CombineRasrLogsJob (#613)
Jobs to combine a set of "original" caches with the information from a set of "updated" caches.
1 parent 989e90e commit 08b3721

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

rasr/util.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
"RemapSegmentsWithBundlesJob",
44
"ClusterMapToSegmentListJob",
55
"RemapSegmentsJob",
6+
"CombineRasrCachesJob",
7+
"CombineRasrLogsJob",
68
]
79

810
import collections
911
import logging
12+
from typing import Dict, Optional
1013
import xml.etree.ElementTree as ET
14+
import subprocess as sp
1115

1216
from sisyphus import *
1317

18+
1419
Path = setup_path(__package__)
1520

1621
from i6_core.util import *
@@ -184,3 +189,162 @@ def run(self):
184189
) as f:
185190
for b in bs:
186191
f.write("%s\n" % tk.uncached_path(self.cache_paths[b]))
192+
193+
194+
class CombineRasrCachesJob(Job):
195+
"""
196+
Uses the RASR archiver binary in `combine` mode in order to combine the information
197+
from `original_caches[task_id]` and `updated_caches[task_id]`
198+
(giving priority to `updated_caches[task_id]`), and dumps the result into `final_caches[task_id]`.
199+
200+
This job is meant to **work with RASR caches whose information inside is composed of key-value pairs**
201+
(key: segment full name, value: information about the segment such as features, alignments...),
202+
and requires that `original_caches.keys() == updated_caches.keys()`.
203+
204+
The job's output is similar to the following python pseudocode:
205+
```
206+
final_cache_contents[segment_full_name] = (
207+
updated_cache_contents[segment_full_name]
208+
if segment_full_name in updated_cache
209+
else original_cache_contents[segment_full_name]
210+
)
211+
```
212+
213+
**The user is responsible of the content of the original and the updated cache**.
214+
For assistance, if using multiple split files, one can use the :class:`i6_core.corpus.segments.SegmentCorpusJob`
215+
along with the :class:`i6_core.corpus.filter.FilterSegmentsByListJob`
216+
(which ensures a correspondence between original and updated segment splits)
217+
in order to obtain an exact subset of each original split files as follows:
218+
```
219+
# Obtain all segments from the updated corpus.
220+
updated_corpus_segments_job = i6_core.corpus.segments.SegmentCorpusJob(
221+
updated_corpus, num_segments=num_original_corpus_splits
222+
)
223+
all_updated_corpus_segments = i6_core.text.processing.ConcatenateJob(
224+
list(updated_corpus_segments_job.out_single_segment_files.values()), zip_out=False
225+
).out
226+
# Filter the segments from the original corpus and keep the same split structure.
227+
updated_corpus_filtered_segments_job = i6_core.corpus.filter.FilterSegmentsByListJob(
228+
segment_files=individual_segment_files_dict, filter_list=all_updated_corpus_segments, invert_match=True
229+
)
230+
```
231+
232+
More information about the archiver is available here:
233+
https://www-i6.informatik.rwth-aachen.de/rwth-asr/manual/index.php/Archiver.
234+
235+
Note: the term "RASR cache" refers to a file in binary format that supports compression,
236+
or equivalently, a flow node with the `generic-cache` filter. More information available here:
237+
- https://github.com/rwth-i6/rasr/blob/master/src/Flow/Cache.hh
238+
- https://github.com/rwth-i6/rasr/blob/master/src/Flow/Cache.cc
239+
- https://github.com/rwth-i6/i6_core/blob/main/lib/rasr_cache.py (python interface).
240+
"""
241+
242+
def __init__(
243+
self, original_caches: Dict[int, tk.Path], updated_caches: Dict[int, tk.Path], rasr_archiver_exe: tk.Path
244+
):
245+
"""
246+
:param original_caches: Caches that must be overwritten.
247+
The internal key-value pairs inside the `i`-th final cache
248+
will have the value provided in the `i`-th original cache
249+
only if the key is not found in the corresponding `i`-th updated cache.
250+
:param updated_caches: Caches with which the contents inside the respective original caches should be updated.
251+
The internal key-value pairs inside the `i`-th final cache
252+
will always prioritize the corresponding key-value pairs inside the `i`-th updated cache.
253+
:param rasr_archiver_exe: Executable for the compiled RASR archiver.
254+
"""
255+
256+
set_original_caches_keys = set(original_caches.keys())
257+
set_updated_caches_keys = set(updated_caches.keys())
258+
assert set_original_caches_keys == set_updated_caches_keys, (
259+
"Original and updated dictionaries don't have a 1-to-1 correspondence:\n"
260+
f"Original - updated: {set_original_caches_keys.difference(set_updated_caches_keys)}\n"
261+
f"Updated - original: {set_updated_caches_keys.difference(set_original_caches_keys)}"
262+
)
263+
self.original_caches = original_caches
264+
self.updated_caches = updated_caches
265+
self.rasr_archiver_exe = rasr_archiver_exe
266+
267+
self.out_final_single_caches = {
268+
i: self.output_path(f"cache.{i}", cached=True) for i in range(1, len(original_caches) + 1)
269+
}
270+
271+
self.rqmt = {"cpu": 1, "mem": 1.0, "time": 1.0}
272+
273+
def tasks(self):
274+
yield Task("run", resume="run", rqmt=self.rqmt, args=list(self.original_caches.keys()))
275+
276+
def run(self, task_id: int):
277+
sp.check_call(
278+
[
279+
self.rasr_archiver_exe,
280+
"--mode",
281+
"combine",
282+
self.out_final_single_caches[task_id].get_path(),
283+
self.updated_caches[task_id].get_path(),
284+
self.original_caches[task_id].get_path(),
285+
]
286+
)
287+
288+
289+
class CombineRasrLogsJob(Job):
290+
"""
291+
Outputs the information from `<segment>` tags in the updated logs if available.
292+
If not, outputs the same information from the original logs.
293+
294+
The code treats the log contents as a key-value mapping, in which the key is the segment's full name
295+
and the value is the information stored in the log for such a segment.
296+
Following the example from this dict-based structure,
297+
triggering this job would achieve the same result as `original_log.update(updated_log)`,
298+
or the following explicit pseudocode:
299+
```
300+
final_log[segment_full_name] = (
301+
updated_log[segment_full_name]
302+
if segment_full_name in updated_log
303+
else original_log[segment_full_name]
304+
)
305+
```
306+
"""
307+
308+
def __init__(self, original_logs: Dict[int, tk.Path], updated_logs: Dict[int, tk.Path]):
309+
"""
310+
:param original_logs: Logs whose corresponding information must be overwritten.
311+
:param updated_logs: Logs whose information will overwrite the original caches.
312+
"""
313+
self.original_logs = original_logs
314+
self.updated_logs = updated_logs
315+
set_original_logs_keys = set(original_logs.keys())
316+
set_updated_logs_keys = set(updated_logs.keys())
317+
assert set_original_logs_keys == set_updated_logs_keys, (
318+
"Original and updated dictionaries don't have a 1-to-1 correspondence:\n"
319+
f"Original - updated: {set_original_logs_keys.difference(set_updated_logs_keys)}\n"
320+
f"Updated - original: {set_updated_logs_keys.difference(set_original_logs_keys)}"
321+
)
322+
323+
self.out_final_logs = {i: self.output_path(f"log.{i}.gz") for i in range(1, len(self.original_logs) + 1)}
324+
325+
self.rqmt = {"cpu": 1, "mem": 1.0, "time": 1.0}
326+
327+
def tasks(self):
328+
yield Task("run", resume="run", rqmt=self.rqmt, args=list(self.original_logs.keys()))
329+
330+
def run(self, task_id: int):
331+
seg_name_to_xml_content = {}
332+
# Read the updated logs and store the information listed there.
333+
with uopen(self.updated_logs[task_id], "rt") as f:
334+
document = ET.parse(f)
335+
_seg_list = document.findall(".//segment")
336+
for seg in _seg_list:
337+
seg_name_to_xml_content[seg.attrib["full-name"]] = seg
338+
339+
# Read the original logs and overwrite them with the updated ones if required.
340+
with uopen(self.original_logs[task_id], "rt") as f:
341+
document = ET.parse(f)
342+
_rec_list = document.findall(".//recording")
343+
for rec in _rec_list:
344+
for i, child in enumerate(rec):
345+
if child.tag == "segment" and child.attrib.get("full-name") in seg_name_to_xml_content:
346+
rec[i] = seg_name_to_xml_content[child.attrib["full-name"]]
347+
348+
# Dump the final logs.
349+
with uopen(self.out_final_logs[task_id].get_path(), "wb") as f:
350+
document.write(f, encoding="UTF-8", xml_declaration=True)

0 commit comments

Comments
 (0)