RadGraph-IT 1.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 (39) hide show
  1. radgraph_it-1.0.0.dist-info/METADATA +159 -0
  2. radgraph_it-1.0.0.dist-info/RECORD +39 -0
  3. radgraph_it-1.0.0.dist-info/WHEEL +4 -0
  4. radgraph_it-1.0.0.dist-info/entry_points.txt +2 -0
  5. radgraph_it-1.0.0.dist-info/licenses/LICENSE +23 -0
  6. radgraph_it-1.0.0.dist-info/licenses/LICENSES/Third-Party.md +41 -0
  7. radgraphit/__init__.py +47 -0
  8. radgraphit/api.py +168 -0
  9. radgraphit/artifacts/__init__.py +5 -0
  10. radgraphit/artifacts/manifest.py +290 -0
  11. radgraphit/artifacts/registry.py +24 -0
  12. radgraphit/artifacts/resolver.py +200 -0
  13. radgraphit/cli.py +131 -0
  14. radgraphit/core/__init__.py +1 -0
  15. radgraphit/core/errors.py +46 -0
  16. radgraphit/core/models.py +116 -0
  17. radgraphit/inference/__init__.py +5 -0
  18. radgraphit/inference/backends/__init__.py +1 -0
  19. radgraphit/inference/backends/base.py +13 -0
  20. radgraphit/inference/backends/dygie_v2/__init__.py +11 -0
  21. radgraphit/inference/backends/dygie_v2/backend.py +32 -0
  22. radgraphit/inference/backends/dygie_v2/device.py +46 -0
  23. radgraphit/inference/backends/dygie_v2/feedforward.py +25 -0
  24. radgraphit/inference/backends/dygie_v2/loader.py +165 -0
  25. radgraphit/inference/backends/dygie_v2/model.py +101 -0
  26. radgraphit/inference/backends/dygie_v2/ner_head.py +75 -0
  27. radgraphit/inference/backends/dygie_v2/pruner.py +27 -0
  28. radgraphit/inference/backends/dygie_v2/relation_head.py +147 -0
  29. radgraphit/inference/backends/dygie_v2/span_extractor.py +51 -0
  30. radgraphit/inference/backends/dygie_v2/spans.py +20 -0
  31. radgraphit/inference/backends/dygie_v2/tokenizer_embedder.py +123 -0
  32. radgraphit/inference/backends/dygie_v2/transformer_block.py +62 -0
  33. radgraphit/inference/backends/dygie_v2/vocab.py +123 -0
  34. radgraphit/inference/decoder.py +53 -0
  35. radgraphit/inference/service.py +80 -0
  36. radgraphit/inference/tokenizer.py +60 -0
  37. radgraphit/py.typed +0 -0
  38. radgraphit/serialization/__init__.py +15 -0
  39. radgraphit/serialization/radgraph_xl.py +135 -0
@@ -0,0 +1,60 @@
1
+ """RadGraph-XL-compatible report tokenizer for Italian reports.
2
+
3
+ This is the exact equivalent of ``radgraph_xl_preprocess_report`` from the upstream RadGraph
4
+ package (``radgraph/utils.py``) — the same normalization substitutions followed by
5
+ ``nltk.tokenize.wordpunct_tokenize`` and the same handful of post-tokenization fixups. The v2
6
+ training pipeline uses byte-for-byte this function for its own tokenization
7
+ (``training_v2/src/check_tokenization_raw_it.py`` documents it as an "EXACT COPY … (inference
8
+ code)"), so reproducing it here is what keeps train-time and inference-time token indices aligned.
9
+
10
+ ``nltk.tokenize.wordpunct_tokenize`` is backed by a pure-regex ``WordPunctTokenizer`` (pattern
11
+ ``\\w+|[^\\w\\s]+``); it needs no downloaded NLTK data and performs no I/O, so importing and calling
12
+ it never touches the network.
13
+
14
+ Indices produced downstream from these tokens are **token-level, zero-based, and inclusive**.
15
+ This module never emits character offsets.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ from nltk.tokenize import wordpunct_tokenize
23
+
24
+ from ..core.models import TokenizedReport
25
+
26
+ # Collapse any run of whitespace to a single space (applied after the literal substitutions).
27
+ _WHITESPACE = re.compile(r"\s+")
28
+
29
+
30
+ def normalize_report(text: str) -> str:
31
+ """Apply the RadGraph-XL normalization + tokenization, returning a space-joined token string.
32
+
33
+ Kept as a separate function (mirroring upstream) so the exact normalized string can be
34
+ inspected or tested independently of the ``.split()`` that yields the token list.
35
+ """
36
+ text = text.replace("\\n", " ")
37
+ text = text.replace("\\f", " ")
38
+ text = text.replace("\\u2122", " ")
39
+ text = text.replace("\n", " ")
40
+ text = text.replace('\\"', "``")
41
+
42
+ text_sub = _WHITESPACE.sub(" ", text)
43
+ tokenized_text = " ".join(wordpunct_tokenize(text_sub))
44
+ tokenized_text = tokenized_text.replace(").", ") .")
45
+ tokenized_text = tokenized_text.replace("%.", "% .")
46
+ tokenized_text = tokenized_text.replace(".'", ". '")
47
+ tokenized_text = tokenized_text.replace("%,", "% ,")
48
+ tokenized_text = tokenized_text.replace("%)", "% )")
49
+ return tokenized_text
50
+
51
+
52
+ def tokenize_report(text: str) -> TokenizedReport:
53
+ """Normalize and tokenize one report into a :class:`TokenizedReport`.
54
+
55
+ The caller is responsible for having validated that ``text`` is a non-empty, non-whitespace
56
+ string (see :func:`radgraphit.inference.service.normalize_reports`); any such string yields at
57
+ least one token, because ``wordpunct_tokenize`` emits a token for every non-whitespace run.
58
+ """
59
+ tokens = normalize_report(text).split()
60
+ return TokenizedReport(tokens=tuple(tokens))
radgraphit/py.typed ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ """Serialization boundary: the single owner of the external RadGraph-XL schema."""
2
+
3
+ from .radgraph_xl import (
4
+ SerializationDiagnostics,
5
+ report_to_radgraph_xl,
6
+ to_radgraph_xl,
7
+ to_radgraph_xl_with_diagnostics,
8
+ )
9
+
10
+ __all__ = [
11
+ "SerializationDiagnostics",
12
+ "report_to_radgraph_xl",
13
+ "to_radgraph_xl",
14
+ "to_radgraph_xl_with_diagnostics",
15
+ ]
@@ -0,0 +1,135 @@
1
+ """The single owner of the external RadGraph-XL schema.
2
+
3
+ Everything about how a predicted graph becomes the canonical RadGraph-XL dictionary lives here and
4
+ nowhere else — no serialization logic is scattered through the neural network or the backend. The
5
+ schema reproduced is the one emitted by the upstream RadGraph package
6
+ (``radgraph/utils.py::postprocess_reports`` / ``get_entity``), with three deliberate, documented
7
+ guarantees added on top so the output is reproducible and never self-inconsistent:
8
+
9
+ 1. **Stable entity ordering.** Entities are sorted by ``(start_ix, end_ix, label)`` *before* ids
10
+ are assigned, so the same graph always serializes to the same ``"1"``, ``"2"``, … numbering
11
+ (upstream numbered them in raw model-output order, which is not reproducible run to run).
12
+ 2. **No dangling edges.** A predicted relation is emitted only if *both* endpoints correspond to
13
+ an emitted NER node. Relations that don't are dropped from the contract and counted in the
14
+ internal :class:`SerializationDiagnostics` instead — the XL contract itself is never altered.
15
+ 3. **Deterministic relation ordering.** Each entity's ``relations`` list is sorted by
16
+ ``(label, target_entity_id)``.
17
+
18
+ The per-report dictionary keeps exactly the upstream keys — ``text``, ``entities``,
19
+ ``data_source`` (``None``), ``data_split`` (``"inference"``) — and adds no others. Relation entries
20
+ are ``[label, target_entity_id]`` with no score (the default contract carries no scores).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Sequence
26
+ from dataclasses import dataclass, field
27
+
28
+ from ..core.models import ReportPrediction
29
+
30
+ DATA_SPLIT = "inference"
31
+
32
+ # The four keys of a per-report RadGraph-XL object, in upstream order. Kept as a constant so tests
33
+ # can assert the contract has neither gained nor lost a key.
34
+ REPORT_KEYS = ("text", "entities", "data_source", "data_split")
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class SerializationDiagnostics:
39
+ """Non-contract, internal-only counters gathered while serializing.
40
+
41
+ ``dropped_dangling_relations`` counts predicted relations discarded because at least one
42
+ endpoint had no emitted NER node. ``per_report`` maps the top-level report key to that report's
43
+ own dropped count.
44
+ """
45
+
46
+ dropped_dangling_relations: int = 0
47
+ per_report: dict[str, int] = field(default_factory=dict)
48
+
49
+
50
+ def _entities_to_dict(pred: ReportPrediction) -> tuple[dict[str, object], int]:
51
+ """Serialize one report's entities (with their relations) and return ``(entities, dropped)``.
52
+
53
+ ``dropped`` is the number of predicted relations not emitted because an endpoint span was not
54
+ an emitted NER node.
55
+ """
56
+ # Stable, reproducible numbering: sort by (start, end, label) then assign 1-based string ids.
57
+ ordered = sorted(pred.entities, key=lambda e: (e.start_ix, e.end_ix, e.label))
58
+
59
+ span_to_id: dict[tuple[int, int], str] = {}
60
+ for position, entity in enumerate(ordered, start=1):
61
+ # First entity at a given span wins (spans are unique in NER output; this is defensive and
62
+ # matches upstream's first-match resolution for duplicate spans).
63
+ span_to_id.setdefault(entity.span, str(position))
64
+
65
+ entities: dict[str, object] = {}
66
+ emitted_relations = 0
67
+ for position, entity in enumerate(ordered, start=1):
68
+ relations: list[list[str]] = []
69
+ for relation in pred.relations:
70
+ if relation.source_span != entity.span:
71
+ continue
72
+ target_id = span_to_id.get(relation.target_span)
73
+ if target_id is None:
74
+ continue # dangling: target span has no emitted node — dropped (counted below)
75
+ relations.append([relation.label, target_id])
76
+ emitted_relations += 1
77
+
78
+ relations.sort(key=lambda label_target: (label_target[0], int(label_target[1])))
79
+ entities[str(position)] = {
80
+ "tokens": " ".join(pred.tokens[entity.start_ix : entity.end_ix + 1]),
81
+ "label": entity.label,
82
+ "start_ix": entity.start_ix,
83
+ "end_ix": entity.end_ix,
84
+ "relations": relations,
85
+ }
86
+
87
+ dropped = len(pred.relations) - emitted_relations
88
+ return entities, dropped
89
+
90
+
91
+ def report_to_radgraph_xl(pred: ReportPrediction) -> tuple[dict[str, object], int]:
92
+ """Serialize a single report to its RadGraph-XL object; return ``(object, dropped_count)``."""
93
+ entities, dropped = _entities_to_dict(pred)
94
+ report: dict[str, object] = {
95
+ "text": pred.text,
96
+ "entities": entities,
97
+ "data_source": None,
98
+ "data_split": DATA_SPLIT,
99
+ }
100
+ return report, dropped
101
+
102
+
103
+ def to_radgraph_xl(predictions: Sequence[ReportPrediction]) -> dict[str, object]:
104
+ """Serialize a batch to the canonical RadGraph-XL dict, keyed by input order (``"0"``, ``"1"``).
105
+
106
+ This is the public contract returned by ``RadGraphIT.predict`` / ``__call__``. Report order is
107
+ preserved: the key is the positional index in ``predictions``, regardless of each prediction's
108
+ own ``doc_key``.
109
+ """
110
+ result, _ = to_radgraph_xl_with_diagnostics(predictions)
111
+ return result
112
+
113
+
114
+ def to_radgraph_xl_with_diagnostics(
115
+ predictions: Sequence[ReportPrediction],
116
+ ) -> tuple[dict[str, object], SerializationDiagnostics]:
117
+ """Like :func:`to_radgraph_xl` but also return :class:`SerializationDiagnostics`.
118
+
119
+ Used internally / by advanced callers that want the dropped-dangling-relation count without
120
+ changing the contract shape.
121
+ """
122
+ result: dict[str, object] = {}
123
+ per_report: dict[str, int] = {}
124
+ total_dropped = 0
125
+ for index, pred in enumerate(predictions):
126
+ key = str(index)
127
+ report, dropped = report_to_radgraph_xl(pred)
128
+ result[key] = report
129
+ per_report[key] = dropped
130
+ total_dropped += dropped
131
+ diagnostics = SerializationDiagnostics(
132
+ dropped_dangling_relations=total_dropped,
133
+ per_report=per_report,
134
+ )
135
+ return result, diagnostics