Skip to content

Commit 09aee82

Browse files
committed
feat(meta): add DataPointMention for quantitative data points mentioned in text
Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>
1 parent f8a36e9 commit 09aee82

4 files changed

Lines changed: 563 additions & 5 deletions

File tree

docling_core/types/doc/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
BaseMeta,
2222
BasePrediction,
2323
CodeMetaField,
24+
DataPointDirection,
25+
DataPointMention,
26+
DataPointPrecision,
27+
DataPointScale,
2428
DescriptionMetaField,
2529
EntitiesMetaField,
2630
EntityMention,

docling_core/types/doc/common/meta.py

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Metadata models attached to document nodes."""
22

33
from enum import Enum
4-
from typing import Annotated, Any, Final, Optional
4+
from typing import Annotated, Any, Final, Literal, Optional, Union
55

66
from pydantic import (
77
BaseModel,
@@ -10,6 +10,7 @@
1010
Field,
1111
FieldSerializationInfo,
1212
field_serializer,
13+
field_validator,
1314
model_validator,
1415
)
1516
from typing_extensions import Self
@@ -159,10 +160,161 @@ class EntityMention(BasePrediction):
159160
] = None
160161

161162

163+
DataPointPrecision = Literal["exact", "approximate", "lower_bound", "upper_bound", "range_low", "range_high"]
164+
"""Valid values for DataPointMention.precision."""
165+
166+
DataPointDirection = Literal["increase", "decrease", "neutral"]
167+
"""Valid values for DataPointMention.direction."""
168+
169+
DataPointScale = Literal[
170+
"hundred",
171+
"thousand",
172+
"k",
173+
"million",
174+
"m",
175+
"billion",
176+
"b",
177+
"bn",
178+
"trillion",
179+
"t",
180+
"quadrillion",
181+
]
182+
"""Valid values for DataPointMention.scale. Each value maps to a known numeric multiplier."""
183+
184+
185+
class DataPointMention(EntityMention):
186+
"""A quantitative data point mentioned in text, extending EntityMention with a numeric breakdown.
187+
188+
Inherited fields: text (normalised form), orig (source text), label (category),
189+
charspan, confidence, created_by.
190+
"""
191+
192+
value: Annotated[
193+
Optional[float],
194+
Field(
195+
description="Numeric value as written, before scale application. E.g. 4.0 for '$4B'.",
196+
examples=[4.0, 30.0, 3.0],
197+
),
198+
] = None
199+
200+
unit: Annotated[
201+
Optional[str],
202+
Field(
203+
description="Dimensional unit, e.g. 'USD', '°C', '%'. ISO 4217 recommended for currencies.",
204+
examples=["USD", "°C", "%"],
205+
),
206+
] = None
207+
208+
scale: Annotated[
209+
Optional[DataPointScale],
210+
Field(
211+
description="Magnitude multiplier from the source text, e.g. 'billion', 'million', 'k'.",
212+
examples=["billion", "million", "k"],
213+
),
214+
] = None
215+
216+
normalized_value: Annotated[
217+
Optional[float],
218+
Field(
219+
description="value * scale_factor in base units, e.g. 4e9 for '$4B'. Derived convenience field.",
220+
examples=[4_000_000_000.0, 30.0],
221+
),
222+
] = None
223+
224+
range_end: Annotated[
225+
Optional[float],
226+
Field(
227+
description=(
228+
"Upper bound when the data point expresses a range, e.g. 20.0 for 'between 10 and 20°C'. "
229+
"Use with precision='range_low'; value holds the lower bound."
230+
),
231+
examples=[20.0],
232+
),
233+
] = None
234+
235+
display_dp: Annotated[
236+
Optional[int],
237+
Field(
238+
ge=0,
239+
description=(
240+
"Decimal places as written in the source: 0 for '3%', 1 for '3.0%', 2 for '3.00%'. "
241+
"None when not determinable."
242+
),
243+
examples=[0, 1, 2],
244+
),
245+
] = None
246+
247+
precision: Annotated[
248+
Optional[DataPointPrecision],
249+
Field(
250+
description=(
251+
"Author's epistemic qualification: 'exact', 'approximate', "
252+
"'lower_bound', 'upper_bound', 'range_low', 'range_high'."
253+
),
254+
examples=["approximate", "lower_bound"],
255+
),
256+
] = None
257+
258+
direction: Annotated[
259+
Optional[DataPointDirection],
260+
Field(
261+
description=(
262+
"Direction of change for delta/growth values: 'increase', 'decrease', 'neutral'. "
263+
"E.g. 'revenue grew 3%' → 'increase'."
264+
),
265+
examples=["increase", "decrease"],
266+
),
267+
] = None
268+
269+
@field_validator("range_end", mode="after")
270+
@classmethod
271+
def _validate_range_end(cls, v: Optional[float], info: Any) -> Optional[float]:
272+
if v is not None and info.data.get("value") is None:
273+
raise ValueError("range_end requires value to be set")
274+
return v
275+
276+
@property
277+
def scale_factor(self) -> Optional[float]:
278+
"""Numeric multiplier for scale, or None if absent or unrecognised."""
279+
if self.scale is None:
280+
return None
281+
_SCALE_MAP: dict[str, float] = {
282+
"hundred": 1e2,
283+
"thousand": 1e3,
284+
"k": 1e3,
285+
"million": 1e6,
286+
"m": 1e6,
287+
"billion": 1e9,
288+
"b": 1e9,
289+
"bn": 1e9,
290+
"trillion": 1e12,
291+
"t": 1e12,
292+
"quadrillion": 1e15,
293+
}
294+
return _SCALE_MAP.get(self.scale)
295+
296+
def compute_normalized_value(self) -> Optional[float]:
297+
"""Return value * scale_factor without caching.
298+
299+
Returns None when value is absent or scale is present but unrecognised.
300+
Use this to derive or refresh normalized_value after construction.
301+
"""
302+
if self.value is None:
303+
return None
304+
factor = self.scale_factor
305+
if factor is None and self.scale is not None:
306+
return None
307+
return self.value * (factor if factor is not None else 1.0)
308+
309+
162310
class EntitiesMetaField(_ExtraAllowingModel):
163-
"""Container for extracted entity mentions."""
311+
"""Container for extracted entity mentions.
312+
313+
The mentions list accepts both plain EntityMention objects and the richer
314+
DataPointMention subclass; use isinstance(m, DataPointMention) to filter.
315+
"""
164316

165-
mentions: Annotated[list[EntityMention], Field(min_length=1)]
317+
mentions: Annotated[list[Union[DataPointMention, EntityMention]], Field(min_length=1)]
166318

167319

168320
class KeywordsMetaField(_ExtraAllowingModel):

0 commit comments

Comments
 (0)