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.
- radgraph_it-1.0.0.dist-info/METADATA +159 -0
- radgraph_it-1.0.0.dist-info/RECORD +39 -0
- radgraph_it-1.0.0.dist-info/WHEEL +4 -0
- radgraph_it-1.0.0.dist-info/entry_points.txt +2 -0
- radgraph_it-1.0.0.dist-info/licenses/LICENSE +23 -0
- radgraph_it-1.0.0.dist-info/licenses/LICENSES/Third-Party.md +41 -0
- radgraphit/__init__.py +47 -0
- radgraphit/api.py +168 -0
- radgraphit/artifacts/__init__.py +5 -0
- radgraphit/artifacts/manifest.py +290 -0
- radgraphit/artifacts/registry.py +24 -0
- radgraphit/artifacts/resolver.py +200 -0
- radgraphit/cli.py +131 -0
- radgraphit/core/__init__.py +1 -0
- radgraphit/core/errors.py +46 -0
- radgraphit/core/models.py +116 -0
- radgraphit/inference/__init__.py +5 -0
- radgraphit/inference/backends/__init__.py +1 -0
- radgraphit/inference/backends/base.py +13 -0
- radgraphit/inference/backends/dygie_v2/__init__.py +11 -0
- radgraphit/inference/backends/dygie_v2/backend.py +32 -0
- radgraphit/inference/backends/dygie_v2/device.py +46 -0
- radgraphit/inference/backends/dygie_v2/feedforward.py +25 -0
- radgraphit/inference/backends/dygie_v2/loader.py +165 -0
- radgraphit/inference/backends/dygie_v2/model.py +101 -0
- radgraphit/inference/backends/dygie_v2/ner_head.py +75 -0
- radgraphit/inference/backends/dygie_v2/pruner.py +27 -0
- radgraphit/inference/backends/dygie_v2/relation_head.py +147 -0
- radgraphit/inference/backends/dygie_v2/span_extractor.py +51 -0
- radgraphit/inference/backends/dygie_v2/spans.py +20 -0
- radgraphit/inference/backends/dygie_v2/tokenizer_embedder.py +123 -0
- radgraphit/inference/backends/dygie_v2/transformer_block.py +62 -0
- radgraphit/inference/backends/dygie_v2/vocab.py +123 -0
- radgraphit/inference/decoder.py +53 -0
- radgraphit/inference/service.py +80 -0
- radgraphit/inference/tokenizer.py +60 -0
- radgraphit/py.typed +0 -0
- radgraphit/serialization/__init__.py +15 -0
- 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
|