1use super::confidence::Confidence;
39use super::types::MentionType;
40use serde::{Deserialize, Serialize};
41use std::borrow::Cow;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[non_exhaustive]
54pub enum EntityCategory {
55 Agent,
58 Organization,
61 Place,
64 Creative,
67 Temporal,
70 Numeric,
73 Contact,
76 Relation,
80 Misc,
82}
83
84impl EntityCategory {
85 #[must_use]
87 pub const fn requires_ml(&self) -> bool {
88 matches!(
89 self,
90 EntityCategory::Agent
91 | EntityCategory::Organization
92 | EntityCategory::Place
93 | EntityCategory::Creative
94 | EntityCategory::Relation
95 )
96 }
97
98 #[must_use]
100 pub const fn pattern_detectable(&self) -> bool {
101 matches!(
102 self,
103 EntityCategory::Temporal | EntityCategory::Numeric | EntityCategory::Contact
104 )
105 }
106
107 #[must_use]
109 pub const fn is_relation(&self) -> bool {
110 matches!(self, EntityCategory::Relation)
111 }
112
113 #[must_use]
115 pub const fn as_str(&self) -> &'static str {
116 match self {
117 EntityCategory::Agent => "agent",
118 EntityCategory::Organization => "organization",
119 EntityCategory::Place => "place",
120 EntityCategory::Creative => "creative",
121 EntityCategory::Temporal => "temporal",
122 EntityCategory::Numeric => "numeric",
123 EntityCategory::Contact => "contact",
124 EntityCategory::Relation => "relation",
125 EntityCategory::Misc => "misc",
126 }
127 }
128}
129
130impl std::fmt::Display for EntityCategory {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(f, "{}", self.as_str())
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
161#[non_exhaustive]
162pub enum EntityType {
163 Person,
166 Organization,
168 Location,
170
171 Date,
174 Time,
176
177 Money,
180 Percent,
182 Quantity,
184 Cardinal,
186 Ordinal,
188
189 Email,
192 Url,
194 Phone,
196
197 Custom {
200 name: String,
202 category: EntityCategory,
204 },
205}
206
207impl EntityType {
208 #[must_use]
210 pub fn category(&self) -> EntityCategory {
211 match self {
212 EntityType::Person => EntityCategory::Agent,
214 EntityType::Organization => EntityCategory::Organization,
216 EntityType::Location => EntityCategory::Place,
218 EntityType::Date | EntityType::Time => EntityCategory::Temporal,
220 EntityType::Money
222 | EntityType::Percent
223 | EntityType::Quantity
224 | EntityType::Cardinal
225 | EntityType::Ordinal => EntityCategory::Numeric,
226 EntityType::Email | EntityType::Url | EntityType::Phone => EntityCategory::Contact,
228 EntityType::Custom { category, .. } => *category,
230 }
231 }
232
233 #[must_use]
235 pub fn requires_ml(&self) -> bool {
236 self.category().requires_ml()
237 }
238
239 #[must_use]
241 pub fn pattern_detectable(&self) -> bool {
242 self.category().pattern_detectable()
243 }
244
245 #[must_use]
254 pub fn as_label(&self) -> &str {
255 match self {
256 EntityType::Person => "PER",
257 EntityType::Organization => "ORG",
258 EntityType::Location => "LOC",
259 EntityType::Date => "DATE",
260 EntityType::Time => "TIME",
261 EntityType::Money => "MONEY",
262 EntityType::Percent => "PERCENT",
263 EntityType::Quantity => "QUANTITY",
264 EntityType::Cardinal => "CARDINAL",
265 EntityType::Ordinal => "ORDINAL",
266 EntityType::Email => "EMAIL",
267 EntityType::Url => "URL",
268 EntityType::Phone => "PHONE",
269 EntityType::Custom { name, .. } => name.as_str(),
270 }
271 }
272
273 #[must_use]
285 pub fn from_label(label: &str) -> Self {
286 let label = label
288 .strip_prefix("B-")
289 .or_else(|| label.strip_prefix("I-"))
290 .or_else(|| label.strip_prefix("E-"))
291 .or_else(|| label.strip_prefix("S-"))
292 .unwrap_or(label);
293
294 match label.to_uppercase().as_str() {
295 "PER" | "PERSON" => EntityType::Person,
297 "ORG" | "ORGANIZATION" | "COMPANY" | "CORPORATION" => EntityType::Organization,
298 "LOC" | "LOCATION" | "GPE" | "GEO-LOC" => EntityType::Location,
299 "FACILITY" | "FAC" | "BUILDING" => {
301 EntityType::custom("BUILDING", EntityCategory::Place)
302 }
303 "PRODUCT" | "PROD" => EntityType::custom("PRODUCT", EntityCategory::Misc),
304 "EVENT" => EntityType::custom("EVENT", EntityCategory::Creative),
305 "CREATIVE-WORK" | "WORK_OF_ART" | "ART" => {
306 EntityType::custom("CREATIVE_WORK", EntityCategory::Creative)
307 }
308 "GROUP" | "NORP" => EntityType::custom("GROUP", EntityCategory::Agent),
309 "DATE" => EntityType::Date,
311 "TIME" => EntityType::Time,
312 "MONEY" | "CURRENCY" => EntityType::Money,
314 "PERCENT" | "PERCENTAGE" => EntityType::Percent,
315 "QUANTITY" => EntityType::Quantity,
316 "CARDINAL" => EntityType::Cardinal,
317 "ORDINAL" => EntityType::Ordinal,
318 "EMAIL" => EntityType::Email,
320 "URL" | "URI" => EntityType::Url,
321 "PHONE" | "TELEPHONE" => EntityType::Phone,
322 "MISC" | "MISCELLANEOUS" | "OTHER" => EntityType::custom("MISC", EntityCategory::Misc),
324 "DISEASE" | "DISORDER" => EntityType::custom("DISEASE", EntityCategory::Misc),
326 "CHEMICAL" | "DRUG" => EntityType::custom("CHEMICAL", EntityCategory::Misc),
327 "GENE" => EntityType::custom("GENE", EntityCategory::Misc),
328 "PROTEIN" => EntityType::custom("PROTEIN", EntityCategory::Misc),
329 other => EntityType::custom(other, EntityCategory::Misc),
331 }
332 }
333
334 #[must_use]
348 pub fn custom(name: impl Into<String>, category: EntityCategory) -> Self {
349 EntityType::Custom {
350 name: name.into(),
351 category,
352 }
353 }
354}
355
356impl std::fmt::Display for EntityType {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "{}", self.as_label())
359 }
360}
361
362impl std::str::FromStr for EntityType {
363 type Err = std::convert::Infallible;
364
365 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
367 Ok(Self::from_label(s))
368 }
369}
370
371impl Serialize for EntityType {
376 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
377 serializer.serialize_str(self.as_label())
378 }
379}
380
381impl<'de> Deserialize<'de> for EntityType {
382 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
383 struct EntityTypeVisitor;
384
385 impl<'de> serde::de::Visitor<'de> for EntityTypeVisitor {
386 type Value = EntityType;
387
388 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 f.write_str("a string label or a tagged enum object")
390 }
391
392 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<EntityType, E> {
394 Ok(EntityType::from_label(v))
395 }
396
397 fn visit_map<A: serde::de::MapAccess<'de>>(
400 self,
401 mut map: A,
402 ) -> Result<EntityType, A::Error> {
403 let key: String = map
404 .next_key()?
405 .ok_or_else(|| serde::de::Error::custom("empty object"))?;
406 match key.as_str() {
407 "Custom" => {
408 #[derive(Deserialize)]
409 struct CustomFields {
410 name: String,
411 category: EntityCategory,
412 }
413 let fields: CustomFields = map.next_value()?;
414 Ok(EntityType::Custom {
415 name: fields.name,
416 category: fields.category,
417 })
418 }
419 "Other" => {
420 let val: String = map.next_value()?;
422 Ok(EntityType::custom(val, EntityCategory::Misc))
423 }
424 variant => {
426 let _: serde::de::IgnoredAny = map.next_value()?;
428 Ok(EntityType::from_label(variant))
429 }
430 }
431 }
432 }
433
434 deserializer.deserialize_any(EntityTypeVisitor)
435 }
436}
437
438#[derive(Debug, Clone, Default)]
478pub struct TypeMapper {
479 mappings: std::collections::HashMap<String, EntityType>,
480}
481
482impl TypeMapper {
483 #[must_use]
485 pub fn new() -> Self {
486 Self::default()
487 }
488
489 #[must_use]
491 pub fn mit_movie() -> Self {
492 let mut mapper = Self::new();
493 mapper.add("ACTOR", EntityType::Person);
495 mapper.add("DIRECTOR", EntityType::Person);
496 mapper.add("CHARACTER", EntityType::Person);
497 mapper.add(
498 "TITLE",
499 EntityType::custom("WORK_OF_ART", EntityCategory::Creative),
500 );
501 mapper.add("GENRE", EntityType::custom("GENRE", EntityCategory::Misc));
502 mapper.add("YEAR", EntityType::Date);
503 mapper.add("RATING", EntityType::custom("RATING", EntityCategory::Misc));
504 mapper.add("PLOT", EntityType::custom("PLOT", EntityCategory::Misc));
505 mapper
506 }
507
508 #[must_use]
510 pub fn mit_restaurant() -> Self {
511 let mut mapper = Self::new();
512 mapper.add("RESTAURANT_NAME", EntityType::Organization);
513 mapper.add("LOCATION", EntityType::Location);
514 mapper.add(
515 "CUISINE",
516 EntityType::custom("CUISINE", EntityCategory::Misc),
517 );
518 mapper.add("DISH", EntityType::custom("DISH", EntityCategory::Misc));
519 mapper.add("PRICE", EntityType::Money);
520 mapper.add(
521 "AMENITY",
522 EntityType::custom("AMENITY", EntityCategory::Misc),
523 );
524 mapper.add("HOURS", EntityType::Time);
525 mapper
526 }
527
528 #[must_use]
530 pub fn biomedical() -> Self {
531 let mut mapper = Self::new();
532 mapper.add(
533 "DISEASE",
534 EntityType::custom("DISEASE", EntityCategory::Agent),
535 );
536 mapper.add(
537 "CHEMICAL",
538 EntityType::custom("CHEMICAL", EntityCategory::Misc),
539 );
540 mapper.add("DRUG", EntityType::custom("DRUG", EntityCategory::Misc));
541 mapper.add("GENE", EntityType::custom("GENE", EntityCategory::Misc));
542 mapper.add(
543 "PROTEIN",
544 EntityType::custom("PROTEIN", EntityCategory::Misc),
545 );
546 mapper.add("DNA", EntityType::custom("DNA", EntityCategory::Misc));
548 mapper.add("RNA", EntityType::custom("RNA", EntityCategory::Misc));
549 mapper.add(
550 "cell_line",
551 EntityType::custom("CELL_LINE", EntityCategory::Misc),
552 );
553 mapper.add(
554 "cell_type",
555 EntityType::custom("CELL_TYPE", EntityCategory::Misc),
556 );
557 mapper
558 }
559
560 #[must_use]
562 pub fn social_media() -> Self {
563 let mut mapper = Self::new();
564 mapper.add("person", EntityType::Person);
566 mapper.add("corporation", EntityType::Organization);
567 mapper.add("location", EntityType::Location);
568 mapper.add("group", EntityType::Organization);
569 mapper.add(
570 "product",
571 EntityType::custom("PRODUCT", EntityCategory::Misc),
572 );
573 mapper.add(
574 "creative_work",
575 EntityType::custom("WORK_OF_ART", EntityCategory::Creative),
576 );
577 mapper.add("event", EntityType::custom("EVENT", EntityCategory::Misc));
578 mapper
579 }
580
581 #[must_use]
583 pub fn manufacturing() -> Self {
584 let mut mapper = Self::new();
585 mapper.add("MATE", EntityType::custom("MATERIAL", EntityCategory::Misc));
587 mapper.add("MANP", EntityType::custom("PROCESS", EntityCategory::Misc));
588 mapper.add("MACEQ", EntityType::custom("MACHINE", EntityCategory::Misc));
589 mapper.add(
590 "APPL",
591 EntityType::custom("APPLICATION", EntityCategory::Misc),
592 );
593 mapper.add("FEAT", EntityType::custom("FEATURE", EntityCategory::Misc));
594 mapper.add(
595 "PARA",
596 EntityType::custom("PARAMETER", EntityCategory::Misc),
597 );
598 mapper.add("PRO", EntityType::custom("PROPERTY", EntityCategory::Misc));
599 mapper.add(
600 "CHAR",
601 EntityType::custom("CHARACTERISTIC", EntityCategory::Misc),
602 );
603 mapper.add(
604 "ENAT",
605 EntityType::custom("ENABLING_TECHNOLOGY", EntityCategory::Misc),
606 );
607 mapper.add(
608 "CONPRI",
609 EntityType::custom("CONCEPT_PRINCIPLE", EntityCategory::Misc),
610 );
611 mapper.add(
612 "BIOP",
613 EntityType::custom("BIO_PROCESS", EntityCategory::Misc),
614 );
615 mapper.add(
616 "MANS",
617 EntityType::custom("MAN_STANDARD", EntityCategory::Misc),
618 );
619 mapper
620 }
621
622 pub fn add(&mut self, source: impl Into<String>, target: EntityType) {
624 self.mappings.insert(source.into().to_uppercase(), target);
625 }
626
627 #[must_use]
629 pub fn map(&self, label: &str) -> Option<&EntityType> {
630 self.mappings.get(&label.to_uppercase())
631 }
632
633 #[must_use]
637 pub fn normalize(&self, label: &str) -> EntityType {
638 self.map(label)
639 .cloned()
640 .unwrap_or_else(|| EntityType::from_label(label))
641 }
642
643 #[must_use]
645 pub fn contains(&self, label: &str) -> bool {
646 self.mappings.contains_key(&label.to_uppercase())
647 }
648
649 pub fn labels(&self) -> impl Iterator<Item = &String> {
651 self.mappings.keys()
652 }
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
668#[non_exhaustive]
669pub enum ExtractionMethod {
670 Pattern,
673
674 #[default]
677 Neural,
678
679 Consensus,
681
682 Heuristic,
685
686 Unknown,
688}
689
690impl ExtractionMethod {
691 #[must_use]
715 pub const fn is_calibrated(&self) -> bool {
716 match self {
717 ExtractionMethod::Neural => true,
718 ExtractionMethod::Pattern => false,
720 ExtractionMethod::Consensus => false,
721 ExtractionMethod::Heuristic => false,
722 ExtractionMethod::Unknown => false,
723 }
724 }
725
726 #[must_use]
733 pub const fn confidence_interpretation(&self) -> &'static str {
734 match self {
735 ExtractionMethod::Neural => "probability",
736 ExtractionMethod::Pattern => "binary",
737 ExtractionMethod::Heuristic => "heuristic_score",
738 ExtractionMethod::Consensus => "agreement_ratio",
739 ExtractionMethod::Unknown => "unknown",
740 }
741 }
742}
743
744impl std::fmt::Display for ExtractionMethod {
745 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746 match self {
747 ExtractionMethod::Pattern => write!(f, "pattern"),
748 ExtractionMethod::Neural => write!(f, "neural"),
749 ExtractionMethod::Consensus => write!(f, "consensus"),
750 ExtractionMethod::Heuristic => write!(f, "heuristic"),
751 ExtractionMethod::Unknown => write!(f, "unknown"),
752 }
753 }
754}
755
756pub trait Lexicon: Send + Sync {
810 fn lookup(&self, text: &str) -> Option<(EntityType, Confidence)>;
814
815 fn contains(&self, text: &str) -> bool {
817 self.lookup(text).is_some()
818 }
819
820 fn source(&self) -> &str;
822
823 fn len(&self) -> usize;
825
826 fn is_empty(&self) -> bool {
828 self.len() == 0
829 }
830}
831
832#[derive(Debug, Clone)]
837pub struct HashMapLexicon {
838 entries: std::collections::HashMap<String, (EntityType, Confidence)>,
839 source: String,
840}
841
842impl HashMapLexicon {
843 #[must_use]
845 pub fn new(source: impl Into<String>) -> Self {
846 Self {
847 entries: std::collections::HashMap::new(),
848 source: source.into(),
849 }
850 }
851
852 pub fn insert(
854 &mut self,
855 text: impl Into<String>,
856 entity_type: EntityType,
857 confidence: impl Into<Confidence>,
858 ) {
859 self.entries
860 .insert(text.into(), (entity_type, confidence.into()));
861 }
862
863 pub fn from_iter<I, S, C>(source: impl Into<String>, entries: I) -> Self
865 where
866 I: IntoIterator<Item = (S, EntityType, C)>,
867 S: Into<String>,
868 C: Into<Confidence>,
869 {
870 let mut lexicon = Self::new(source);
871 for (text, entity_type, conf) in entries {
872 lexicon.insert(text, entity_type, conf);
873 }
874 lexicon
875 }
876
877 pub fn entries(&self) -> impl Iterator<Item = (&str, &EntityType, Confidence)> {
879 self.entries.iter().map(|(k, (t, c))| (k.as_str(), t, *c))
880 }
881}
882
883impl Lexicon for HashMapLexicon {
884 fn lookup(&self, text: &str) -> Option<(EntityType, Confidence)> {
885 self.entries.get(text).cloned()
886 }
887
888 fn source(&self) -> &str {
889 &self.source
890 }
891
892 fn len(&self) -> usize {
893 self.entries.len()
894 }
895}
896
897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
902pub struct Provenance {
903 pub source: Cow<'static, str>,
905 pub method: ExtractionMethod,
907 pub pattern: Option<Cow<'static, str>>,
909 pub raw_confidence: Option<Confidence>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub model_version: Option<Cow<'static, str>>,
914 #[serde(default, skip_serializing_if = "Option::is_none")]
916 pub timestamp: Option<String>,
917}
918
919impl Provenance {
920 #[must_use]
922 pub fn pattern(pattern_name: &'static str) -> Self {
923 Self {
924 source: Cow::Borrowed("pattern"),
925 method: ExtractionMethod::Pattern,
926 pattern: Some(Cow::Borrowed(pattern_name)),
927 raw_confidence: Some(Confidence::ONE), model_version: None,
929 timestamp: None,
930 }
931 }
932
933 #[must_use]
947 pub fn ml(model_name: impl Into<Cow<'static, str>>, confidence: impl Into<Confidence>) -> Self {
948 Self {
949 source: model_name.into(),
950 method: ExtractionMethod::Neural,
951 pattern: None,
952 raw_confidence: Some(confidence.into()),
953 model_version: None,
954 timestamp: None,
955 }
956 }
957
958 #[must_use]
960 pub fn ensemble(sources: &'static str) -> Self {
961 Self {
962 source: Cow::Borrowed(sources),
963 method: ExtractionMethod::Consensus,
964 pattern: None,
965 raw_confidence: None,
966 model_version: None,
967 timestamp: None,
968 }
969 }
970
971 #[must_use]
973 pub fn with_version(mut self, version: &'static str) -> Self {
974 self.model_version = Some(Cow::Borrowed(version));
975 self
976 }
977
978 #[must_use]
980 pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
981 self.timestamp = Some(timestamp.into());
982 self
983 }
984}
985
986#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1017pub enum Span {
1018 Text {
1023 start: usize,
1025 end: usize,
1027 },
1028 BoundingBox {
1031 x: f32,
1033 y: f32,
1035 width: f32,
1037 height: f32,
1039 page: Option<u32>,
1041 },
1042 Hybrid {
1044 start: usize,
1046 end: usize,
1048 bbox: Box<Span>,
1050 },
1051}
1052
1053impl Span {
1054 #[must_use]
1056 pub const fn text(start: usize, end: usize) -> Self {
1057 Self::Text { start, end }
1058 }
1059
1060 #[must_use]
1062 pub fn bbox(x: f32, y: f32, width: f32, height: f32) -> Self {
1063 Self::BoundingBox {
1064 x,
1065 y,
1066 width,
1067 height,
1068 page: None,
1069 }
1070 }
1071
1072 #[must_use]
1074 pub fn bbox_on_page(x: f32, y: f32, width: f32, height: f32, page: u32) -> Self {
1075 Self::BoundingBox {
1076 x,
1077 y,
1078 width,
1079 height,
1080 page: Some(page),
1081 }
1082 }
1083
1084 #[must_use]
1086 pub const fn is_text(&self) -> bool {
1087 matches!(self, Self::Text { .. } | Self::Hybrid { .. })
1088 }
1089
1090 #[must_use]
1092 pub const fn is_visual(&self) -> bool {
1093 matches!(self, Self::BoundingBox { .. } | Self::Hybrid { .. })
1094 }
1095
1096 #[must_use]
1098 pub const fn text_offsets(&self) -> Option<(usize, usize)> {
1099 match self {
1100 Self::Text { start, end } => Some((*start, *end)),
1101 Self::Hybrid { start, end, .. } => Some((*start, *end)),
1102 Self::BoundingBox { .. } => None,
1103 }
1104 }
1105
1106 #[must_use]
1108 pub fn len(&self) -> usize {
1109 match self {
1110 Self::Text { start, end } => end.saturating_sub(*start),
1111 Self::Hybrid { start, end, .. } => end.saturating_sub(*start),
1112 Self::BoundingBox { .. } => 0,
1113 }
1114 }
1115
1116 #[must_use]
1118 pub fn is_empty(&self) -> bool {
1119 self.len() == 0
1120 }
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1164pub struct DiscontinuousSpan {
1165 segments: Vec<std::ops::Range<usize>>,
1168}
1169
1170impl<'de> serde::Deserialize<'de> for DiscontinuousSpan {
1171 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1172 where
1173 D: serde::Deserializer<'de>,
1174 {
1175 #[derive(serde::Deserialize)]
1177 struct Raw {
1178 segments: Vec<std::ops::Range<usize>>,
1179 }
1180 let raw = Raw::deserialize(deserializer)?;
1181 Ok(Self::new(raw.segments))
1183 }
1184}
1185
1186impl DiscontinuousSpan {
1187 #[must_use]
1192 pub fn new(mut segments: Vec<std::ops::Range<usize>>) -> Self {
1193 segments.retain(|r| r.start < r.end);
1195 segments.sort_by_key(|r| r.start);
1197 let mut merged: Vec<std::ops::Range<usize>> = Vec::with_capacity(segments.len());
1199 for seg in segments {
1200 if let Some(last) = merged.last_mut() {
1201 if seg.start <= last.end {
1202 last.end = last.end.max(seg.end);
1204 continue;
1205 }
1206 }
1207 merged.push(seg);
1208 }
1209 Self { segments: merged }
1210 }
1211
1212 #[must_use]
1217 #[allow(clippy::single_range_in_vec_init)] pub fn contiguous(start: usize, end: usize) -> Self {
1219 let (lo, hi) = if start <= end {
1220 (start, end)
1221 } else {
1222 (end, start)
1223 };
1224 if lo == hi {
1225 Self {
1226 segments: Vec::new(),
1227 }
1228 } else {
1229 Self {
1230 segments: vec![lo..hi],
1231 }
1232 }
1233 }
1234
1235 #[must_use]
1237 pub fn num_segments(&self) -> usize {
1238 self.segments.len()
1239 }
1240
1241 #[must_use]
1243 pub fn is_discontinuous(&self) -> bool {
1244 self.segments.len() > 1
1245 }
1246
1247 #[must_use]
1249 pub fn is_contiguous(&self) -> bool {
1250 self.segments.len() <= 1
1251 }
1252
1253 #[must_use]
1255 pub fn segments(&self) -> &[std::ops::Range<usize>] {
1256 &self.segments
1257 }
1258
1259 #[must_use]
1261 pub fn bounding_range(&self) -> Option<std::ops::Range<usize>> {
1262 if self.segments.is_empty() {
1263 return None;
1264 }
1265 let start = self.segments.first()?.start;
1266 let end = self.segments.last()?.end;
1267 Some(start..end)
1268 }
1269
1270 #[must_use]
1273 pub fn total_len(&self) -> usize {
1274 self.segments.iter().map(|r| r.end - r.start).sum()
1275 }
1276
1277 #[must_use]
1279 pub fn extract_text(&self, text: &str, separator: &str) -> String {
1280 self.segments
1281 .iter()
1282 .map(|r| {
1283 let start = r.start;
1284 let len = r.end.saturating_sub(r.start);
1285 text.chars().skip(start).take(len).collect::<String>()
1286 })
1287 .collect::<Vec<_>>()
1288 .join(separator)
1289 }
1290
1291 #[must_use]
1301 pub fn contains(&self, pos: usize) -> bool {
1302 self.segments.iter().any(|r| r.contains(&pos))
1303 }
1304
1305 #[must_use]
1307 pub fn to_span(&self) -> Option<Span> {
1308 self.bounding_range().map(|r| Span::Text {
1309 start: r.start,
1310 end: r.end,
1311 })
1312 }
1313}
1314
1315impl From<std::ops::Range<usize>> for DiscontinuousSpan {
1316 fn from(range: std::ops::Range<usize>) -> Self {
1317 Self::contiguous(range.start, range.end)
1318 }
1319}
1320
1321impl Default for Span {
1322 fn default() -> Self {
1323 Self::Text { start: 0, end: 0 }
1324 }
1325}
1326
1327#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1339pub struct HierarchicalConfidence {
1340 pub linkage: Confidence,
1343 pub type_score: Confidence,
1345 pub boundary: Confidence,
1348}
1349
1350impl HierarchicalConfidence {
1351 #[must_use]
1356 pub fn new(
1357 linkage: impl Into<Confidence>,
1358 type_score: impl Into<Confidence>,
1359 boundary: impl Into<Confidence>,
1360 ) -> Self {
1361 Self {
1362 linkage: linkage.into(),
1363 type_score: type_score.into(),
1364 boundary: boundary.into(),
1365 }
1366 }
1367
1368 #[must_use]
1371 pub fn from_single(confidence: impl Into<Confidence>) -> Self {
1372 let c = confidence.into();
1373 Self {
1374 linkage: c,
1375 type_score: c,
1376 boundary: c,
1377 }
1378 }
1379
1380 #[must_use]
1383 pub fn combined(&self) -> Confidence {
1384 let product = self.linkage.value() * self.type_score.value() * self.boundary.value();
1385 Confidence::new(product.powf(1.0 / 3.0))
1386 }
1387
1388 #[must_use]
1390 pub fn as_f64(&self) -> f64 {
1391 self.combined().value()
1392 }
1393
1394 #[must_use]
1396 pub fn passes_threshold(&self, linkage_min: f64, type_min: f64, boundary_min: f64) -> bool {
1397 self.linkage >= linkage_min && self.type_score >= type_min && self.boundary >= boundary_min
1398 }
1399}
1400
1401impl Default for HierarchicalConfidence {
1402 fn default() -> Self {
1403 Self {
1404 linkage: Confidence::ONE,
1405 type_score: Confidence::ONE,
1406 boundary: Confidence::ONE,
1407 }
1408 }
1409}
1410
1411impl From<f64> for HierarchicalConfidence {
1412 fn from(confidence: f64) -> Self {
1413 Self::from_single(confidence)
1414 }
1415}
1416
1417impl From<f32> for HierarchicalConfidence {
1418 fn from(confidence: f32) -> Self {
1419 Self::from_single(confidence)
1420 }
1421}
1422
1423impl From<Confidence> for HierarchicalConfidence {
1424 fn from(confidence: Confidence) -> Self {
1425 Self::from_single(confidence)
1426 }
1427}
1428
1429#[derive(Debug, Clone)]
1451pub struct RaggedBatch {
1452 pub token_ids: Vec<u32>,
1455 pub cumulative_offsets: Vec<u32>,
1459 pub max_seq_len: usize,
1461}
1462
1463impl RaggedBatch {
1464 pub fn from_sequences(sequences: &[Vec<u32>]) -> Self {
1466 let total_tokens: usize = sequences.iter().map(|s| s.len()).sum();
1467 let mut token_ids = Vec::with_capacity(total_tokens);
1468 let mut cumulative_offsets = Vec::with_capacity(sequences.len() + 1);
1469 let mut max_seq_len = 0;
1470
1471 cumulative_offsets.push(0);
1472 for seq in sequences {
1473 token_ids.extend_from_slice(seq);
1474 let len = token_ids.len();
1478 if len > u32::MAX as usize {
1479 log::warn!(
1482 "Token count {} exceeds u32::MAX, truncating to {}",
1483 len,
1484 u32::MAX
1485 );
1486 cumulative_offsets.push(u32::MAX);
1487 } else {
1488 cumulative_offsets.push(len as u32);
1489 }
1490 max_seq_len = max_seq_len.max(seq.len());
1491 }
1492
1493 Self {
1494 token_ids,
1495 cumulative_offsets,
1496 max_seq_len,
1497 }
1498 }
1499
1500 #[must_use]
1502 pub fn batch_size(&self) -> usize {
1503 self.cumulative_offsets.len().saturating_sub(1)
1504 }
1505
1506 #[must_use]
1508 pub fn total_tokens(&self) -> usize {
1509 self.token_ids.len()
1510 }
1511
1512 #[must_use]
1514 pub fn doc_range(&self, doc_idx: usize) -> Option<std::ops::Range<usize>> {
1515 if doc_idx + 1 < self.cumulative_offsets.len() {
1516 let start = self.cumulative_offsets[doc_idx] as usize;
1517 let end = self.cumulative_offsets[doc_idx + 1] as usize;
1518 Some(start..end)
1519 } else {
1520 None
1521 }
1522 }
1523
1524 #[must_use]
1526 pub fn doc_tokens(&self, doc_idx: usize) -> Option<&[u32]> {
1527 self.doc_range(doc_idx).map(|r| &self.token_ids[r])
1528 }
1529
1530 #[must_use]
1532 pub fn padding_savings(&self) -> f64 {
1533 let padded_size = self.batch_size() * self.max_seq_len;
1534 if padded_size == 0 {
1535 return 0.0;
1536 }
1537 1.0 - (self.total_tokens() as f64 / padded_size as f64)
1538 }
1539}
1540
1541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1550pub struct SpanCandidate {
1551 pub doc_idx: u32,
1553 pub start: u32,
1555 pub end: u32,
1557}
1558
1559impl SpanCandidate {
1560 #[must_use]
1562 pub const fn new(doc_idx: u32, start: u32, end: u32) -> Self {
1563 Self {
1564 doc_idx,
1565 start,
1566 end,
1567 }
1568 }
1569
1570 #[must_use]
1572 pub const fn width(&self) -> u32 {
1573 self.end.saturating_sub(self.start)
1574 }
1575}
1576
1577pub fn generate_span_candidates(batch: &RaggedBatch, max_width: usize) -> Vec<SpanCandidate> {
1582 let mut candidates = Vec::new();
1583
1584 for doc_idx in 0..batch.batch_size() {
1585 if let Some(range) = batch.doc_range(doc_idx) {
1586 let doc_len = range.len();
1587 for start in 0..doc_len {
1589 let max_end = (start + max_width).min(doc_len);
1590 for end in (start + 1)..=max_end {
1591 candidates.push(SpanCandidate::new(doc_idx as u32, start as u32, end as u32));
1592 }
1593 }
1594 }
1595 }
1596
1597 candidates
1598}
1599
1600pub fn generate_filtered_candidates(
1604 batch: &RaggedBatch,
1605 max_width: usize,
1606 linkage_mask: &[f32],
1607 threshold: f32,
1608) -> Vec<SpanCandidate> {
1609 let mut candidates = Vec::new();
1610 let mut mask_idx = 0;
1611
1612 for doc_idx in 0..batch.batch_size() {
1613 if let Some(range) = batch.doc_range(doc_idx) {
1614 let doc_len = range.len();
1615 for start in 0..doc_len {
1616 let max_end = (start + max_width).min(doc_len);
1617 for end in (start + 1)..=max_end {
1618 if mask_idx < linkage_mask.len() && linkage_mask[mask_idx] >= threshold {
1620 candidates.push(SpanCandidate::new(
1621 doc_idx as u32,
1622 start as u32,
1623 end as u32,
1624 ));
1625 }
1626 mask_idx += 1;
1627 }
1628 }
1629 }
1630 }
1631
1632 candidates
1633}
1634
1635#[derive(Debug, Clone, Serialize)]
1684pub struct Entity {
1685 pub text: String,
1687 pub entity_type: EntityType,
1689 start: usize,
1696 end: usize,
1703 pub confidence: Confidence,
1708 #[serde(default, skip_serializing_if = "Option::is_none")]
1710 pub normalized: Option<String>,
1711 #[serde(default, skip_serializing_if = "Option::is_none")]
1713 pub provenance: Option<Provenance>,
1714 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 pub kb_id: Option<String>,
1718 #[serde(default, skip_serializing_if = "Option::is_none")]
1722 pub canonical_id: Option<super::types::CanonicalId>,
1723 #[serde(default, skip_serializing_if = "Option::is_none")]
1726 pub hierarchical_confidence: Option<HierarchicalConfidence>,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1730 pub visual_span: Option<Span>,
1731 #[serde(default, skip_serializing_if = "Option::is_none")]
1735 pub discontinuous_span: Option<DiscontinuousSpan>,
1736 #[serde(default, skip_serializing_if = "Option::is_none")]
1742 pub mention_type: Option<MentionType>,
1743}
1744
1745impl<'de> Deserialize<'de> for Entity {
1746 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1747 #[derive(Deserialize)]
1751 struct EntityHelper {
1752 text: String,
1753 entity_type: EntityType,
1754 start: usize,
1755 end: usize,
1756 confidence: Confidence,
1757 #[serde(default)]
1758 normalized: Option<String>,
1759 #[serde(default)]
1760 provenance: Option<Provenance>,
1761 #[serde(default)]
1762 kb_id: Option<String>,
1763 #[serde(default)]
1764 canonical_id: Option<super::types::CanonicalId>,
1765 #[serde(default)]
1766 hierarchical_confidence: Option<HierarchicalConfidence>,
1767 #[serde(default)]
1768 visual_span: Option<Span>,
1769 #[serde(default)]
1770 discontinuous_span: Option<DiscontinuousSpan>,
1771 #[serde(default)]
1772 mention_type: Option<MentionType>,
1773 }
1774
1775 let h = EntityHelper::deserialize(deserializer)?;
1776 let mut entity = Entity::new(h.text, h.entity_type, h.start, h.end, h.confidence);
1777 entity.normalized = h.normalized;
1778 entity.provenance = h.provenance;
1779 entity.kb_id = h.kb_id;
1780 entity.canonical_id = h.canonical_id;
1781 entity.hierarchical_confidence = h.hierarchical_confidence;
1782 entity.visual_span = h.visual_span;
1783 entity.discontinuous_span = h.discontinuous_span;
1784 entity.mention_type = h.mention_type;
1785 Ok(entity)
1786 }
1787}
1788
1789impl Entity {
1790 #[must_use]
1801 pub fn new(
1802 text: impl Into<String>,
1803 entity_type: EntityType,
1804 start: usize,
1805 end: usize,
1806 confidence: impl Into<Confidence>,
1807 ) -> Self {
1808 let (start, end) = if start > end {
1810 (end, start)
1811 } else {
1812 (start, end)
1813 };
1814 Self {
1815 text: text.into(),
1816 entity_type,
1817 start,
1818 end,
1819 confidence: confidence.into(),
1820 normalized: None,
1821 provenance: None,
1822 kb_id: None,
1823 canonical_id: None,
1824 hierarchical_confidence: None,
1825 visual_span: None,
1826 discontinuous_span: None,
1827 mention_type: None,
1828 }
1829 }
1830
1831 #[inline]
1833 #[must_use]
1834 pub fn start(&self) -> usize {
1835 self.start
1836 }
1837
1838 #[inline]
1840 #[must_use]
1841 pub fn end(&self) -> usize {
1842 self.end
1843 }
1844
1845 #[inline]
1851 pub fn set_start(&mut self, start: usize) {
1852 debug_assert!(
1853 start <= self.end,
1854 "set_start({start}) would invert span (end={})",
1855 self.end
1856 );
1857 self.start = start;
1858 }
1859
1860 #[inline]
1866 pub fn set_end(&mut self, end: usize) {
1867 debug_assert!(
1868 end >= self.start,
1869 "set_end({end}) would invert span (start={})",
1870 self.start
1871 );
1872 self.end = end;
1873 }
1874
1875 #[must_use]
1877 pub fn with_provenance(
1878 text: impl Into<String>,
1879 entity_type: EntityType,
1880 start: usize,
1881 end: usize,
1882 confidence: impl Into<Confidence>,
1883 provenance: Provenance,
1884 ) -> Self {
1885 let (start, end) = if start > end {
1886 (end, start)
1887 } else {
1888 (start, end)
1889 };
1890 Self {
1891 text: text.into(),
1892 entity_type,
1893 start,
1894 end,
1895 confidence: confidence.into(),
1896 normalized: None,
1897 provenance: Some(provenance),
1898 kb_id: None,
1899 canonical_id: None,
1900 hierarchical_confidence: None,
1901 visual_span: None,
1902 discontinuous_span: None,
1903 mention_type: None,
1904 }
1905 }
1906
1907 #[must_use]
1909 pub fn with_hierarchical_confidence(
1910 text: impl Into<String>,
1911 entity_type: EntityType,
1912 start: usize,
1913 end: usize,
1914 confidence: HierarchicalConfidence,
1915 ) -> Self {
1916 let (start, end) = if start > end {
1917 (end, start)
1918 } else {
1919 (start, end)
1920 };
1921 Self {
1922 text: text.into(),
1923 entity_type,
1924 start,
1925 end,
1926 confidence: Confidence::new(confidence.as_f64()),
1927 normalized: None,
1928 provenance: None,
1929 kb_id: None,
1930 canonical_id: None,
1931 hierarchical_confidence: Some(confidence),
1932 visual_span: None,
1933 discontinuous_span: None,
1934 mention_type: None,
1935 }
1936 }
1937
1938 #[must_use]
1940 pub fn from_visual(
1941 text: impl Into<String>,
1942 entity_type: EntityType,
1943 bbox: Span,
1944 confidence: impl Into<Confidence>,
1945 ) -> Self {
1946 Self {
1947 text: text.into(),
1948 entity_type,
1949 start: 0,
1950 end: 0,
1951 confidence: confidence.into(),
1952 normalized: None,
1953 provenance: None,
1954 kb_id: None,
1955 canonical_id: None,
1956 hierarchical_confidence: None,
1957 visual_span: Some(bbox),
1958 discontinuous_span: None,
1959 mention_type: None,
1960 }
1961 }
1962
1963 #[must_use]
1965 pub fn with_type(
1966 text: impl Into<String>,
1967 entity_type: EntityType,
1968 start: usize,
1969 end: usize,
1970 ) -> Self {
1971 Self::new(text, entity_type, start, end, 1.0)
1972 }
1973
1974 pub fn link_to_kb(&mut self, kb_id: impl Into<String>) {
1984 self.kb_id = Some(kb_id.into());
1985 }
1986
1987 pub fn set_canonical(&mut self, canonical_id: impl Into<super::types::CanonicalId>) {
1991 self.canonical_id = Some(canonical_id.into());
1992 }
1993
1994 #[must_use]
2004 pub fn with_canonical_id(mut self, canonical_id: impl Into<super::types::CanonicalId>) -> Self {
2005 self.canonical_id = Some(canonical_id.into());
2006 self
2007 }
2008
2009 #[must_use]
2011 pub fn is_linked(&self) -> bool {
2012 self.kb_id.is_some()
2013 }
2014
2015 #[must_use]
2017 pub fn has_coreference(&self) -> bool {
2018 self.canonical_id.is_some()
2019 }
2020
2021 #[must_use]
2027 pub fn is_discontinuous(&self) -> bool {
2028 self.discontinuous_span
2029 .as_ref()
2030 .map(|s| s.is_discontinuous())
2031 .unwrap_or(false)
2032 }
2033
2034 #[must_use]
2038 pub fn discontinuous_segments(&self) -> Option<Vec<std::ops::Range<usize>>> {
2039 self.discontinuous_span
2040 .as_ref()
2041 .filter(|s| s.is_discontinuous())
2042 .map(|s| s.segments().to_vec())
2043 }
2044
2045 pub fn set_discontinuous_span(&mut self, span: DiscontinuousSpan) {
2049 if let Some(bounding) = span.bounding_range() {
2051 self.start = bounding.start;
2052 self.end = bounding.end;
2053 }
2054 self.discontinuous_span = Some(span);
2055 }
2056
2057 #[must_use]
2065 pub fn total_len(&self) -> usize {
2066 if let Some(ref span) = self.discontinuous_span {
2067 span.segments().iter().map(|r| r.end - r.start).sum()
2068 } else {
2069 self.end.saturating_sub(self.start)
2070 }
2071 }
2072
2073 pub fn set_normalized(&mut self, normalized: impl Into<String>) {
2085 self.normalized = Some(normalized.into());
2086 }
2087
2088 #[must_use]
2090 pub fn normalized_or_text(&self) -> &str {
2091 self.normalized.as_deref().unwrap_or(&self.text)
2092 }
2093
2094 #[must_use]
2096 pub fn method(&self) -> ExtractionMethod {
2097 self.provenance
2098 .as_ref()
2099 .map_or(ExtractionMethod::Unknown, |p| p.method)
2100 }
2101
2102 #[must_use]
2104 pub fn source(&self) -> Option<&str> {
2105 self.provenance.as_ref().map(|p| p.source.as_ref())
2106 }
2107
2108 #[must_use]
2110 pub fn category(&self) -> EntityCategory {
2111 self.entity_type.category()
2112 }
2113
2114 #[must_use]
2116 pub fn is_structured(&self) -> bool {
2117 self.entity_type.pattern_detectable()
2118 }
2119
2120 #[must_use]
2122 pub fn is_named(&self) -> bool {
2123 self.entity_type.requires_ml()
2124 }
2125
2126 #[must_use]
2128 pub fn overlaps(&self, other: &Entity) -> bool {
2129 !(self.end <= other.start || other.end <= self.start)
2130 }
2131
2132 #[must_use]
2134 pub fn overlap_ratio(&self, other: &Entity) -> f64 {
2135 let intersection_start = self.start.max(other.start);
2136 let intersection_end = self.end.min(other.end);
2137
2138 if intersection_start >= intersection_end {
2139 return 0.0;
2140 }
2141
2142 let intersection = (intersection_end - intersection_start) as f64;
2143 let union = ((self.end - self.start) + (other.end - other.start)
2144 - (intersection_end - intersection_start)) as f64;
2145
2146 if union == 0.0 {
2147 return 1.0;
2148 }
2149
2150 intersection / union
2151 }
2152
2153 pub fn set_hierarchical_confidence(&mut self, confidence: HierarchicalConfidence) {
2155 self.confidence = Confidence::new(confidence.as_f64());
2156 self.hierarchical_confidence = Some(confidence);
2157 }
2158
2159 #[must_use]
2161 pub fn linkage_confidence(&self) -> Confidence {
2162 self.hierarchical_confidence
2163 .map_or(self.confidence, |h| h.linkage)
2164 }
2165
2166 #[must_use]
2168 pub fn type_confidence(&self) -> Confidence {
2169 self.hierarchical_confidence
2170 .map_or(self.confidence, |h| h.type_score)
2171 }
2172
2173 #[must_use]
2175 pub fn boundary_confidence(&self) -> Confidence {
2176 self.hierarchical_confidence
2177 .map_or(self.confidence, |h| h.boundary)
2178 }
2179
2180 #[must_use]
2182 pub fn is_visual(&self) -> bool {
2183 self.visual_span.is_some()
2184 }
2185
2186 #[must_use]
2188 pub const fn text_span(&self) -> (usize, usize) {
2189 (self.start, self.end)
2190 }
2191
2192 #[must_use]
2194 pub const fn span_len(&self) -> usize {
2195 self.end.saturating_sub(self.start)
2196 }
2197
2198 pub fn set_visual_span(&mut self, span: Span) {
2223 self.visual_span = Some(span);
2224 }
2225
2226 #[must_use]
2246 pub fn extract_text(&self, source_text: &str) -> String {
2247 let char_count = source_text.chars().count();
2251 self.extract_text_with_len(source_text, char_count)
2252 }
2253
2254 #[must_use]
2266 pub fn extract_text_with_len(&self, source_text: &str, text_char_count: usize) -> String {
2267 if self.start >= text_char_count || self.end > text_char_count || self.start >= self.end {
2268 return String::new();
2269 }
2270 source_text
2271 .chars()
2272 .skip(self.start)
2273 .take(self.end - self.start)
2274 .collect()
2275 }
2276
2277 #[must_use]
2279 pub fn builder(text: impl Into<String>, entity_type: EntityType) -> EntityBuilder {
2280 EntityBuilder::new(text, entity_type)
2281 }
2282
2283 #[must_use]
2316 pub fn validate(&self, source_text: &str) -> Vec<ValidationIssue> {
2317 let char_count = source_text.chars().count();
2319 self.validate_with_len(source_text, char_count)
2320 }
2321
2322 #[must_use]
2334 pub fn validate_with_len(
2335 &self,
2336 source_text: &str,
2337 text_char_count: usize,
2338 ) -> Vec<ValidationIssue> {
2339 let mut issues = Vec::new();
2340
2341 if self.start >= self.end {
2343 issues.push(ValidationIssue::InvalidSpan {
2344 start: self.start,
2345 end: self.end,
2346 reason: "start must be less than end".to_string(),
2347 });
2348 }
2349
2350 if self.end > text_char_count {
2351 issues.push(ValidationIssue::SpanOutOfBounds {
2352 end: self.end,
2353 text_len: text_char_count,
2354 });
2355 }
2356
2357 if self.start < self.end && self.end <= text_char_count {
2359 let actual = self.extract_text_with_len(source_text, text_char_count);
2360 if actual != self.text {
2361 issues.push(ValidationIssue::TextMismatch {
2362 expected: self.text.clone(),
2363 actual,
2364 start: self.start,
2365 end: self.end,
2366 });
2367 }
2368 }
2369
2370 if let EntityType::Custom { ref name, .. } = self.entity_type {
2374 if name.is_empty() {
2375 issues.push(ValidationIssue::InvalidType {
2376 reason: "Custom entity type has empty name".to_string(),
2377 });
2378 }
2379 }
2380
2381 if let Some(ref disc_span) = self.discontinuous_span {
2383 for (i, seg) in disc_span.segments().iter().enumerate() {
2384 if seg.start >= seg.end {
2385 issues.push(ValidationIssue::InvalidSpan {
2386 start: seg.start,
2387 end: seg.end,
2388 reason: format!("discontinuous segment {} is invalid", i),
2389 });
2390 }
2391 if seg.end > text_char_count {
2392 issues.push(ValidationIssue::SpanOutOfBounds {
2393 end: seg.end,
2394 text_len: text_char_count,
2395 });
2396 }
2397 }
2398 }
2399
2400 issues
2401 }
2402
2403 #[must_use]
2407 pub fn is_valid(&self, source_text: &str) -> bool {
2408 self.validate(source_text).is_empty()
2409 }
2410
2411 #[must_use]
2431 pub fn validate_batch(
2432 entities: &[Entity],
2433 source_text: &str,
2434 ) -> std::collections::HashMap<usize, Vec<ValidationIssue>> {
2435 entities
2436 .iter()
2437 .enumerate()
2438 .filter_map(|(idx, entity)| {
2439 let issues = entity.validate(source_text);
2440 if issues.is_empty() {
2441 None
2442 } else {
2443 Some((idx, issues))
2444 }
2445 })
2446 .collect()
2447 }
2448}
2449
2450#[derive(Debug, Clone, PartialEq)]
2452pub enum ValidationIssue {
2453 InvalidSpan {
2455 start: usize,
2457 end: usize,
2459 reason: String,
2461 },
2462 SpanOutOfBounds {
2464 end: usize,
2466 text_len: usize,
2468 },
2469 TextMismatch {
2471 expected: String,
2473 actual: String,
2475 start: usize,
2477 end: usize,
2479 },
2480 InvalidConfidence {
2482 value: f64,
2484 },
2485 InvalidType {
2487 reason: String,
2489 },
2490}
2491
2492impl std::fmt::Display for ValidationIssue {
2493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2494 match self {
2495 ValidationIssue::InvalidSpan { start, end, reason } => {
2496 write!(f, "Invalid span [{}, {}): {}", start, end, reason)
2497 }
2498 ValidationIssue::SpanOutOfBounds { end, text_len } => {
2499 write!(f, "Span end {} exceeds text length {}", end, text_len)
2500 }
2501 ValidationIssue::TextMismatch {
2502 expected,
2503 actual,
2504 start,
2505 end,
2506 } => {
2507 write!(
2508 f,
2509 "Text mismatch at [{}, {}): expected '{}', got '{}'",
2510 start, end, expected, actual
2511 )
2512 }
2513 ValidationIssue::InvalidConfidence { value } => {
2514 write!(f, "Confidence {} outside [0.0, 1.0]", value)
2515 }
2516 ValidationIssue::InvalidType { reason } => {
2517 write!(f, "Invalid entity type: {}", reason)
2518 }
2519 }
2520 }
2521}
2522
2523#[derive(Debug, Clone)]
2538pub struct EntityBuilder {
2539 text: String,
2540 entity_type: EntityType,
2541 start: usize,
2542 end: usize,
2543 confidence: Confidence,
2544 normalized: Option<String>,
2545 provenance: Option<Provenance>,
2546 kb_id: Option<String>,
2547 canonical_id: Option<super::types::CanonicalId>,
2548 hierarchical_confidence: Option<HierarchicalConfidence>,
2549 visual_span: Option<Span>,
2550 discontinuous_span: Option<DiscontinuousSpan>,
2551 mention_type: Option<MentionType>,
2552}
2553
2554impl EntityBuilder {
2555 #[must_use]
2557 pub fn new(text: impl Into<String>, entity_type: EntityType) -> Self {
2558 let text = text.into();
2559 let end = text.chars().count();
2560 Self {
2561 text,
2562 entity_type,
2563 start: 0,
2564 end,
2565 confidence: Confidence::ONE,
2566 normalized: None,
2567 provenance: None,
2568 kb_id: None,
2569 canonical_id: None,
2570 hierarchical_confidence: None,
2571 visual_span: None,
2572 discontinuous_span: None,
2573 mention_type: None,
2574 }
2575 }
2576
2577 #[must_use]
2579 pub const fn span(mut self, start: usize, end: usize) -> Self {
2580 self.start = start;
2581 self.end = end;
2582 self
2583 }
2584
2585 #[must_use]
2587 pub fn confidence(mut self, confidence: impl Into<Confidence>) -> Self {
2588 self.confidence = confidence.into();
2589 self
2590 }
2591
2592 #[must_use]
2594 pub fn hierarchical_confidence(mut self, confidence: HierarchicalConfidence) -> Self {
2595 self.confidence = Confidence::new(confidence.as_f64());
2596 self.hierarchical_confidence = Some(confidence);
2597 self
2598 }
2599
2600 #[must_use]
2602 pub fn normalized(mut self, normalized: impl Into<String>) -> Self {
2603 self.normalized = Some(normalized.into());
2604 self
2605 }
2606
2607 #[must_use]
2609 pub fn provenance(mut self, provenance: Provenance) -> Self {
2610 self.provenance = Some(provenance);
2611 self
2612 }
2613
2614 #[must_use]
2616 pub fn kb_id(mut self, kb_id: impl Into<String>) -> Self {
2617 self.kb_id = Some(kb_id.into());
2618 self
2619 }
2620
2621 #[must_use]
2623 pub const fn canonical_id(mut self, canonical_id: u64) -> Self {
2624 self.canonical_id = Some(super::types::CanonicalId::new(canonical_id));
2625 self
2626 }
2627
2628 #[must_use]
2630 pub fn visual_span(mut self, span: Span) -> Self {
2631 self.visual_span = Some(span);
2632 self
2633 }
2634
2635 #[must_use]
2639 pub fn discontinuous_span(mut self, span: DiscontinuousSpan) -> Self {
2640 if let Some(bounding) = span.bounding_range() {
2642 self.start = bounding.start;
2643 self.end = bounding.end;
2644 }
2645 self.discontinuous_span = Some(span);
2646 self
2647 }
2648
2649 #[must_use]
2651 pub fn mention_type(mut self, mention_type: MentionType) -> Self {
2652 self.mention_type = Some(mention_type);
2653 self
2654 }
2655
2656 #[must_use]
2660 pub fn build(self) -> Entity {
2661 let (start, end) = if self.start <= self.end {
2662 (self.start, self.end)
2663 } else {
2664 (self.end, self.start)
2665 };
2666 Entity {
2667 text: self.text,
2668 entity_type: self.entity_type,
2669 start,
2670 end,
2671 confidence: self.confidence,
2672 normalized: self.normalized,
2673 provenance: self.provenance,
2674 kb_id: self.kb_id,
2675 canonical_id: self.canonical_id,
2676 hierarchical_confidence: self.hierarchical_confidence,
2677 visual_span: self.visual_span,
2678 discontinuous_span: self.discontinuous_span,
2679 mention_type: self.mention_type,
2680 }
2681 }
2682}
2683
2684#[derive(Debug, Clone, Serialize, Deserialize)]
2710pub struct Relation {
2711 pub head: Entity,
2713 pub tail: Entity,
2715 pub relation_type: String,
2717 pub trigger_span: Option<(usize, usize)>,
2720 pub confidence: Confidence,
2722}
2723
2724impl Relation {
2725 #[must_use]
2727 pub fn new(
2728 head: Entity,
2729 tail: Entity,
2730 relation_type: impl Into<String>,
2731 confidence: impl Into<Confidence>,
2732 ) -> Self {
2733 Self {
2734 head,
2735 tail,
2736 relation_type: relation_type.into(),
2737 trigger_span: None,
2738 confidence: confidence.into(),
2739 }
2740 }
2741
2742 #[must_use]
2744 pub fn with_trigger(
2745 head: Entity,
2746 tail: Entity,
2747 relation_type: impl Into<String>,
2748 trigger_start: usize,
2749 trigger_end: usize,
2750 confidence: impl Into<Confidence>,
2751 ) -> Self {
2752 Self {
2753 head,
2754 tail,
2755 relation_type: relation_type.into(),
2756 trigger_span: Some((trigger_start, trigger_end)),
2757 confidence: confidence.into(),
2758 }
2759 }
2760
2761 #[must_use]
2763 pub fn as_triple(&self) -> String {
2764 format!(
2765 "({}, {}, {})",
2766 self.head.text, self.relation_type, self.tail.text
2767 )
2768 }
2769
2770 #[must_use]
2773 pub fn span_distance(&self) -> usize {
2774 if self.head.end <= self.tail.start {
2775 self.tail.start.saturating_sub(self.head.end)
2776 } else if self.tail.end <= self.head.start {
2777 self.head.start.saturating_sub(self.tail.end)
2778 } else {
2779 0 }
2781 }
2782}
2783
2784#[cfg(test)]
2785mod tests {
2786 #![allow(clippy::unwrap_used)] use super::*;
2788
2789 #[test]
2790 fn entity_new_swaps_inverted_span() {
2791 let e = Entity::new("test", EntityType::Person, 10, 5, 0.9);
2792 assert_eq!(e.start(), 5);
2793 assert_eq!(e.end(), 10);
2794 }
2795
2796 #[test]
2797 fn entity_deserialize_swaps_inverted_span() {
2798 let json = r#"{"text":"test","entity_type":"PER","start":10,"end":5,"confidence":0.9}"#;
2799 let e: Entity = serde_json::from_str(json).unwrap();
2800 assert_eq!(e.start(), 5);
2801 assert_eq!(e.end(), 10);
2802 }
2803
2804 #[test]
2805 fn entity_serde_round_trip() {
2806 let original = Entity::new("Berlin", EntityType::Location, 10, 16, 0.95);
2807 let json = serde_json::to_string(&original).unwrap();
2808 let restored: Entity = serde_json::from_str(&json).unwrap();
2809 assert_eq!(restored.text, original.text);
2810 assert_eq!(restored.entity_type, original.entity_type);
2811 assert_eq!(restored.start(), original.start());
2812 assert_eq!(restored.end(), original.end());
2813 assert!((restored.confidence.value() - original.confidence.value()).abs() < f64::EPSILON);
2814 }
2815
2816 #[test]
2817 fn test_entity_type_roundtrip() {
2818 let types = [
2819 EntityType::Person,
2820 EntityType::Organization,
2821 EntityType::Location,
2822 EntityType::Date,
2823 EntityType::Money,
2824 EntityType::Percent,
2825 ];
2826
2827 for t in types {
2828 let label = t.as_label();
2829 let parsed = EntityType::from_label(label);
2830 assert_eq!(t, parsed);
2831 }
2832 }
2833
2834 #[test]
2835 fn test_entity_overlap() {
2836 let e1 = Entity::new("John", EntityType::Person, 0, 4, 0.9);
2837 let e2 = Entity::new("Smith", EntityType::Person, 5, 10, 0.9);
2838 let e3 = Entity::new("John Smith", EntityType::Person, 0, 10, 0.9);
2839
2840 assert!(!e1.overlaps(&e2)); assert!(e1.overlaps(&e3)); assert!(e3.overlaps(&e2)); }
2844
2845 #[test]
2846 fn test_confidence_clamping() {
2847 let e1 = Entity::new("test", EntityType::Person, 0, 4, 1.5);
2848 assert!((e1.confidence - 1.0).abs() < f64::EPSILON);
2849
2850 let e2 = Entity::new("test", EntityType::Person, 0, 4, -0.5);
2851 assert!(e2.confidence.abs() < f64::EPSILON);
2852 }
2853
2854 #[test]
2855 fn test_entity_categories() {
2856 assert_eq!(EntityType::Person.category(), EntityCategory::Agent);
2858 assert_eq!(
2859 EntityType::Organization.category(),
2860 EntityCategory::Organization
2861 );
2862 assert_eq!(EntityType::Location.category(), EntityCategory::Place);
2863 assert!(EntityType::Person.requires_ml());
2864 assert!(!EntityType::Person.pattern_detectable());
2865
2866 assert_eq!(EntityType::Date.category(), EntityCategory::Temporal);
2868 assert_eq!(EntityType::Time.category(), EntityCategory::Temporal);
2869 assert!(EntityType::Date.pattern_detectable());
2870 assert!(!EntityType::Date.requires_ml());
2871
2872 assert_eq!(EntityType::Money.category(), EntityCategory::Numeric);
2874 assert_eq!(EntityType::Percent.category(), EntityCategory::Numeric);
2875 assert!(EntityType::Money.pattern_detectable());
2876
2877 assert_eq!(EntityType::Email.category(), EntityCategory::Contact);
2879 assert_eq!(EntityType::Url.category(), EntityCategory::Contact);
2880 assert_eq!(EntityType::Phone.category(), EntityCategory::Contact);
2881 assert!(EntityType::Email.pattern_detectable());
2882 }
2883
2884 #[test]
2885 fn test_new_types_roundtrip() {
2886 let types = [
2887 EntityType::Time,
2888 EntityType::Email,
2889 EntityType::Url,
2890 EntityType::Phone,
2891 EntityType::Quantity,
2892 EntityType::Cardinal,
2893 EntityType::Ordinal,
2894 ];
2895
2896 for t in types {
2897 let label = t.as_label();
2898 let parsed = EntityType::from_label(label);
2899 assert_eq!(t, parsed, "Roundtrip failed for {}", label);
2900 }
2901 }
2902
2903 #[test]
2904 fn test_custom_entity_type() {
2905 let disease = EntityType::custom("DISEASE", EntityCategory::Agent);
2906 assert_eq!(disease.as_label(), "DISEASE");
2907 assert!(disease.requires_ml());
2908
2909 let product_id = EntityType::custom("PRODUCT_ID", EntityCategory::Misc);
2910 assert_eq!(product_id.as_label(), "PRODUCT_ID");
2911 assert!(!product_id.requires_ml());
2912 assert!(!product_id.pattern_detectable());
2913 }
2914
2915 #[test]
2916 fn test_entity_normalization() {
2917 let mut e = Entity::new("Jan 15", EntityType::Date, 0, 6, 0.95);
2918 assert!(e.normalized.is_none());
2919 assert_eq!(e.normalized_or_text(), "Jan 15");
2920
2921 e.set_normalized("2024-01-15");
2922 assert_eq!(e.normalized.as_deref(), Some("2024-01-15"));
2923 assert_eq!(e.normalized_or_text(), "2024-01-15");
2924 }
2925
2926 #[test]
2927 fn test_entity_helpers() {
2928 let named = Entity::new("John", EntityType::Person, 0, 4, 0.9);
2929 assert!(named.is_named());
2930 assert!(!named.is_structured());
2931 assert_eq!(named.category(), EntityCategory::Agent);
2932
2933 let structured = Entity::new("$100", EntityType::Money, 0, 4, 0.95);
2934 assert!(!structured.is_named());
2935 assert!(structured.is_structured());
2936 assert_eq!(structured.category(), EntityCategory::Numeric);
2937 }
2938
2939 #[test]
2940 fn test_knowledge_linking() {
2941 let mut entity = Entity::new("Marie Curie", EntityType::Person, 0, 11, 0.95);
2942 assert!(!entity.is_linked());
2943 assert!(!entity.has_coreference());
2944
2945 entity.link_to_kb("Q7186"); assert!(entity.is_linked());
2947 assert_eq!(entity.kb_id.as_deref(), Some("Q7186"));
2948
2949 entity.set_canonical(42);
2950 assert!(entity.has_coreference());
2951 assert_eq!(
2952 entity.canonical_id,
2953 Some(crate::core::types::CanonicalId::new(42))
2954 );
2955 }
2956
2957 #[test]
2958 fn test_relation_creation() {
2959 let head = Entity::new("Marie Curie", EntityType::Person, 0, 11, 0.95);
2960 let tail = Entity::new("Sorbonne", EntityType::Organization, 24, 32, 0.90);
2961
2962 let relation = Relation::new(head.clone(), tail.clone(), "WORKED_AT", 0.85);
2963 assert_eq!(relation.relation_type, "WORKED_AT");
2964 assert_eq!(relation.as_triple(), "(Marie Curie, WORKED_AT, Sorbonne)");
2965 assert!(relation.trigger_span.is_none());
2966
2967 let relation2 = Relation::with_trigger(head, tail, "EMPLOYMENT", 13, 19, 0.85);
2969 assert_eq!(relation2.trigger_span, Some((13, 19)));
2970 }
2971
2972 #[test]
2973 fn test_relation_span_distance() {
2974 let head = Entity::new("Marie Curie", EntityType::Person, 0, 11, 0.95);
2976 let tail = Entity::new("Sorbonne", EntityType::Organization, 24, 32, 0.90);
2977 let relation = Relation::new(head, tail, "WORKED_AT", 0.85);
2978 assert_eq!(relation.span_distance(), 13);
2979 }
2980
2981 #[test]
2982 fn test_relation_category() {
2983 let rel_type = EntityType::custom("CEO_OF", EntityCategory::Relation);
2985 assert_eq!(rel_type.category(), EntityCategory::Relation);
2986 assert!(rel_type.category().is_relation());
2987 assert!(rel_type.requires_ml()); }
2989
2990 #[test]
2995 fn test_span_text() {
2996 let span = Span::text(10, 20);
2997 assert!(span.is_text());
2998 assert!(!span.is_visual());
2999 assert_eq!(span.text_offsets(), Some((10, 20)));
3000 assert_eq!(span.len(), 10);
3001 assert!(!span.is_empty());
3002 }
3003
3004 #[test]
3005 fn test_span_bbox() {
3006 let span = Span::bbox(0.1, 0.2, 0.3, 0.4);
3007 assert!(!span.is_text());
3008 assert!(span.is_visual());
3009 assert_eq!(span.text_offsets(), None);
3010 assert_eq!(span.len(), 0); }
3012
3013 #[test]
3014 fn test_span_bbox_with_page() {
3015 let span = Span::bbox_on_page(0.1, 0.2, 0.3, 0.4, 5);
3016 if let Span::BoundingBox { page, .. } = span {
3017 assert_eq!(page, Some(5));
3018 } else {
3019 panic!("Expected BoundingBox");
3020 }
3021 }
3022
3023 #[test]
3024 fn test_span_hybrid() {
3025 let bbox = Span::bbox(0.1, 0.2, 0.3, 0.4);
3026 let hybrid = Span::Hybrid {
3027 start: 10,
3028 end: 20,
3029 bbox: Box::new(bbox),
3030 };
3031 assert!(hybrid.is_text());
3032 assert!(hybrid.is_visual());
3033 assert_eq!(hybrid.text_offsets(), Some((10, 20)));
3034 assert_eq!(hybrid.len(), 10);
3035 }
3036
3037 #[test]
3042 fn test_hierarchical_confidence_new() {
3043 let hc = HierarchicalConfidence::new(0.9, 0.8, 0.7);
3044 assert!((hc.linkage - 0.9).abs() < f64::EPSILON);
3045 assert!((hc.type_score - 0.8).abs() < f64::EPSILON);
3046 assert!((hc.boundary - 0.7).abs() < f64::EPSILON);
3047 }
3048
3049 #[test]
3050 fn test_hierarchical_confidence_clamping() {
3051 let hc = HierarchicalConfidence::new(1.5, -0.5, 0.5);
3052 assert_eq!(hc.linkage, 1.0);
3053 assert_eq!(hc.type_score, 0.0);
3054 assert_eq!(hc.boundary, 0.5);
3055 }
3056
3057 #[test]
3058 fn test_hierarchical_confidence_from_single() {
3059 let hc = HierarchicalConfidence::from_single(0.8);
3060 assert!((hc.linkage - 0.8).abs() < f64::EPSILON);
3061 assert!((hc.type_score - 0.8).abs() < f64::EPSILON);
3062 assert!((hc.boundary - 0.8).abs() < f64::EPSILON);
3063 }
3064
3065 #[test]
3066 fn test_hierarchical_confidence_combined() {
3067 let hc = HierarchicalConfidence::new(1.0, 1.0, 1.0);
3068 assert!((hc.combined() - 1.0).abs() < f64::EPSILON);
3069
3070 let hc2 = HierarchicalConfidence::new(0.8, 0.8, 0.8);
3071 assert!((hc2.combined() - 0.8).abs() < 0.001);
3072
3073 let hc3 = HierarchicalConfidence::new(0.5, 0.5, 0.5);
3075 assert!((hc3.combined() - 0.5).abs() < 0.001);
3076 }
3077
3078 #[test]
3079 fn test_hierarchical_confidence_threshold() {
3080 let hc = HierarchicalConfidence::new(0.9, 0.8, 0.7);
3081 assert!(hc.passes_threshold(0.5, 0.5, 0.5));
3082 assert!(hc.passes_threshold(0.9, 0.8, 0.7));
3083 assert!(!hc.passes_threshold(0.95, 0.8, 0.7)); assert!(!hc.passes_threshold(0.9, 0.85, 0.7)); }
3086
3087 #[test]
3088 fn test_hierarchical_confidence_from_f64() {
3089 let hc: HierarchicalConfidence = 0.85_f64.into();
3090 assert!((hc.linkage - 0.85).abs() < 0.001);
3091 }
3092
3093 #[test]
3098 fn test_ragged_batch_from_sequences() {
3099 let seqs = vec![vec![1, 2, 3], vec![4, 5], vec![6, 7, 8, 9]];
3100 let batch = RaggedBatch::from_sequences(&seqs);
3101
3102 assert_eq!(batch.batch_size(), 3);
3103 assert_eq!(batch.total_tokens(), 9);
3104 assert_eq!(batch.max_seq_len, 4);
3105 assert_eq!(batch.cumulative_offsets, vec![0, 3, 5, 9]);
3106 }
3107
3108 #[test]
3109 fn test_ragged_batch_doc_range() {
3110 let seqs = vec![vec![1, 2, 3], vec![4, 5]];
3111 let batch = RaggedBatch::from_sequences(&seqs);
3112
3113 assert_eq!(batch.doc_range(0), Some(0..3));
3114 assert_eq!(batch.doc_range(1), Some(3..5));
3115 assert_eq!(batch.doc_range(2), None);
3116 }
3117
3118 #[test]
3119 fn test_ragged_batch_doc_tokens() {
3120 let seqs = vec![vec![1, 2, 3], vec![4, 5]];
3121 let batch = RaggedBatch::from_sequences(&seqs);
3122
3123 assert_eq!(batch.doc_tokens(0), Some(&[1, 2, 3][..]));
3124 assert_eq!(batch.doc_tokens(1), Some(&[4, 5][..]));
3125 }
3126
3127 #[test]
3128 fn test_ragged_batch_padding_savings() {
3129 let seqs = vec![vec![1, 2, 3], vec![4, 5], vec![6, 7, 8, 9]];
3133 let batch = RaggedBatch::from_sequences(&seqs);
3134 let savings = batch.padding_savings();
3135 assert!((savings - 0.25).abs() < 0.001);
3136 }
3137
3138 #[test]
3143 fn test_span_candidate() {
3144 let sc = SpanCandidate::new(0, 5, 10);
3145 assert_eq!(sc.doc_idx, 0);
3146 assert_eq!(sc.start, 5);
3147 assert_eq!(sc.end, 10);
3148 assert_eq!(sc.width(), 5);
3149 }
3150
3151 #[test]
3152 fn test_generate_span_candidates() {
3153 let seqs = vec![vec![1, 2, 3]]; let batch = RaggedBatch::from_sequences(&seqs);
3155 let candidates = generate_span_candidates(&batch, 2);
3156
3157 assert_eq!(candidates.len(), 5);
3160
3161 for c in &candidates {
3163 assert_eq!(c.doc_idx, 0);
3164 assert!(c.end as usize <= 3);
3165 assert!(c.width() as usize <= 2);
3166 }
3167 }
3168
3169 #[test]
3170 fn test_generate_filtered_candidates() {
3171 let seqs = vec![vec![1, 2, 3]];
3172 let batch = RaggedBatch::from_sequences(&seqs);
3173
3174 let mask = vec![0.9, 0.9, 0.1, 0.1, 0.1];
3177 let candidates = generate_filtered_candidates(&batch, 2, &mask, 0.5);
3178
3179 assert_eq!(candidates.len(), 2);
3180 }
3181
3182 #[test]
3187 fn test_entity_builder_basic() {
3188 let entity = Entity::builder("John", EntityType::Person)
3189 .span(0, 4)
3190 .confidence(0.95)
3191 .build();
3192
3193 assert_eq!(entity.text, "John");
3194 assert_eq!(entity.entity_type, EntityType::Person);
3195 assert_eq!(entity.start(), 0);
3196 assert_eq!(entity.end(), 4);
3197 assert!((entity.confidence - 0.95).abs() < f64::EPSILON);
3198 }
3199
3200 #[test]
3201 fn test_entity_builder_full() {
3202 let entity = Entity::builder("Marie Curie", EntityType::Person)
3203 .span(0, 11)
3204 .confidence(0.95)
3205 .kb_id("Q7186")
3206 .canonical_id(42)
3207 .normalized("Marie Salomea Skłodowska Curie")
3208 .provenance(Provenance::ml("bert", 0.95))
3209 .build();
3210
3211 assert_eq!(entity.text, "Marie Curie");
3212 assert_eq!(entity.kb_id.as_deref(), Some("Q7186"));
3213 assert_eq!(
3214 entity.canonical_id,
3215 Some(crate::core::types::CanonicalId::new(42))
3216 );
3217 assert_eq!(
3218 entity.normalized.as_deref(),
3219 Some("Marie Salomea Skłodowska Curie")
3220 );
3221 assert!(entity.provenance.is_some());
3222 }
3223
3224 #[test]
3225 fn test_entity_builder_hierarchical() {
3226 let hc = HierarchicalConfidence::new(0.9, 0.8, 0.7);
3227 let entity = Entity::builder("test", EntityType::Person)
3228 .span(0, 4)
3229 .hierarchical_confidence(hc)
3230 .build();
3231
3232 assert!(entity.hierarchical_confidence.is_some());
3233 assert!((entity.linkage_confidence() - 0.9).abs() < 0.001);
3234 assert!((entity.type_confidence() - 0.8).abs() < 0.001);
3235 assert!((entity.boundary_confidence() - 0.7).abs() < 0.001);
3236 }
3237
3238 #[test]
3239 fn test_entity_builder_visual() {
3240 let bbox = Span::bbox(0.1, 0.2, 0.3, 0.4);
3241 let entity = Entity::builder("receipt item", EntityType::Money)
3242 .visual_span(bbox)
3243 .confidence(0.9)
3244 .build();
3245
3246 assert!(entity.is_visual());
3247 assert!(entity.visual_span.is_some());
3248 }
3249
3250 #[test]
3255 fn test_entity_hierarchical_confidence_helpers() {
3256 let mut entity = Entity::new("test", EntityType::Person, 0, 4, 0.8);
3257
3258 assert!((entity.linkage_confidence() - 0.8).abs() < 0.001);
3260 assert!((entity.type_confidence() - 0.8).abs() < 0.001);
3261 assert!((entity.boundary_confidence() - 0.8).abs() < 0.001);
3262
3263 entity.set_hierarchical_confidence(HierarchicalConfidence::new(0.95, 0.85, 0.75));
3265 assert!((entity.linkage_confidence() - 0.95).abs() < 0.001);
3266 assert!((entity.type_confidence() - 0.85).abs() < 0.001);
3267 assert!((entity.boundary_confidence() - 0.75).abs() < 0.001);
3268 }
3269
3270 #[test]
3271 fn test_entity_from_visual() {
3272 let entity = Entity::from_visual(
3273 "receipt total",
3274 EntityType::Money,
3275 Span::bbox(0.5, 0.8, 0.2, 0.05),
3276 0.92,
3277 );
3278
3279 assert!(entity.is_visual());
3280 assert_eq!(entity.start(), 0);
3281 assert_eq!(entity.end(), 0);
3282 assert!((entity.confidence - 0.92).abs() < f64::EPSILON);
3283 }
3284
3285 #[test]
3286 fn test_entity_span_helpers() {
3287 let entity = Entity::new("test", EntityType::Person, 10, 20, 0.9);
3288 assert_eq!(entity.text_span(), (10, 20));
3289 assert_eq!(entity.span_len(), 10);
3290 }
3291
3292 #[test]
3297 fn test_provenance_pattern() {
3298 let prov = Provenance::pattern("EMAIL");
3299 assert_eq!(prov.method, ExtractionMethod::Pattern);
3300 assert_eq!(prov.pattern.as_deref(), Some("EMAIL"));
3301 assert_eq!(prov.raw_confidence, Some(Confidence::new(1.0))); }
3303
3304 #[test]
3305 fn test_provenance_ml() {
3306 let prov = Provenance::ml("bert-ner", 0.87);
3307 assert_eq!(prov.method, ExtractionMethod::Neural);
3308 assert_eq!(prov.source.as_ref(), "bert-ner");
3309 assert_eq!(prov.raw_confidence, Some(Confidence::new(0.87)));
3310 }
3311
3312 #[test]
3313 fn test_provenance_with_version() {
3314 let prov = Provenance::ml("gliner", 0.92).with_version("v2.1.0");
3315
3316 assert_eq!(prov.model_version.as_deref(), Some("v2.1.0"));
3317 assert_eq!(prov.source.as_ref(), "gliner");
3318 }
3319
3320 #[test]
3321 fn test_provenance_with_timestamp() {
3322 let prov = Provenance::pattern("DATE").with_timestamp("2024-01-15T10:30:00Z");
3323
3324 assert_eq!(prov.timestamp.as_deref(), Some("2024-01-15T10:30:00Z"));
3325 }
3326
3327 #[test]
3328 fn test_provenance_builder_chain() {
3329 let prov = Provenance::ml("modernbert-ner", 0.95)
3330 .with_version("v1.0.0")
3331 .with_timestamp("2024-11-27T12:00:00Z");
3332
3333 assert_eq!(prov.method, ExtractionMethod::Neural);
3334 assert_eq!(prov.source.as_ref(), "modernbert-ner");
3335 assert_eq!(prov.raw_confidence, Some(Confidence::new(0.95)));
3336 assert_eq!(prov.model_version.as_deref(), Some("v1.0.0"));
3337 assert_eq!(prov.timestamp.as_deref(), Some("2024-11-27T12:00:00Z"));
3338 }
3339
3340 #[test]
3341 fn test_provenance_serialization() {
3342 let prov = Provenance::ml("test", 0.9)
3343 .with_version("v1.0")
3344 .with_timestamp("2024-01-01");
3345
3346 let json = serde_json::to_string(&prov).unwrap();
3347 assert!(json.contains("model_version"));
3348 assert!(json.contains("v1.0"));
3349
3350 let restored: Provenance = serde_json::from_str(&json).unwrap();
3351 assert_eq!(restored.model_version.as_deref(), Some("v1.0"));
3352 assert_eq!(restored.timestamp.as_deref(), Some("2024-01-01"));
3353 }
3354
3355 #[test]
3356 fn entity_serde_roundtrip_no_temporal_fields() {
3357 let entity = Entity::new("Berlin", EntityType::Location, 0, 6, 0.95);
3358 let json = serde_json::to_string(&entity).unwrap();
3359 assert!(!json.contains("valid_from"));
3361 assert!(!json.contains("valid_until"));
3362 assert!(!json.contains("phi_features"));
3363 let recovered: Entity = serde_json::from_str(&json).unwrap();
3365 assert_eq!(recovered.text, "Berlin");
3366 assert_eq!(recovered.start(), 0);
3367 assert_eq!(recovered.end(), 6);
3368 }
3369
3370 #[test]
3371 fn entity_deserialize_ignores_unknown_fields() {
3372 let json = r#"{"text":"Berlin","entity_type":"LOC","start":0,"end":6,"confidence":0.95,"valid_from":null,"phi_features":null}"#;
3373 let entity: Entity = serde_json::from_str(json).unwrap();
3374 assert_eq!(entity.text, "Berlin");
3375 }
3376}
3377
3378#[cfg(test)]
3379mod proptests {
3380 #![allow(clippy::unwrap_used)] use super::*;
3382 use proptest::prelude::*;
3383
3384 proptest! {
3385 #[test]
3386 fn confidence_always_clamped(conf in -10.0f64..10.0) {
3387 let e = Entity::new("test", EntityType::Person, 0, 4, conf);
3388 prop_assert!(e.confidence >= 0.0);
3389 prop_assert!(e.confidence <= 1.0);
3390 }
3391
3392 #[test]
3393 fn entity_type_roundtrip(label in "[A-Z]{3,10}") {
3394 let et = EntityType::from_label(&label);
3395 let back = EntityType::from_label(et.as_label());
3396 let is_custom = matches!(back, EntityType::Custom { .. });
3398 prop_assert!(is_custom || back == et);
3399 }
3400
3401 #[test]
3402 fn overlap_is_symmetric(
3403 s1 in 0usize..100,
3404 len1 in 1usize..50,
3405 s2 in 0usize..100,
3406 len2 in 1usize..50,
3407 ) {
3408 let e1 = Entity::new("a", EntityType::Person, s1, s1 + len1, 1.0);
3409 let e2 = Entity::new("b", EntityType::Person, s2, s2 + len2, 1.0);
3410 prop_assert_eq!(e1.overlaps(&e2), e2.overlaps(&e1));
3411 }
3412
3413 #[test]
3414 fn overlap_ratio_bounded(
3415 s1 in 0usize..100,
3416 len1 in 1usize..50,
3417 s2 in 0usize..100,
3418 len2 in 1usize..50,
3419 ) {
3420 let e1 = Entity::new("a", EntityType::Person, s1, s1 + len1, 1.0);
3421 let e2 = Entity::new("b", EntityType::Person, s2, s2 + len2, 1.0);
3422 let ratio = e1.overlap_ratio(&e2);
3423 prop_assert!(ratio >= 0.0);
3424 prop_assert!(ratio <= 1.0);
3425 }
3426
3427 #[test]
3428 fn self_overlap_ratio_is_one(s in 0usize..100, len in 1usize..50) {
3429 let e = Entity::new("test", EntityType::Person, s, s + len, 1.0);
3430 let ratio = e.overlap_ratio(&e);
3431 prop_assert!((ratio - 1.0).abs() < 1e-10);
3432 }
3433
3434 #[test]
3435 fn hierarchical_confidence_always_clamped(
3436 linkage in -2.0f32..2.0,
3437 type_score in -2.0f32..2.0,
3438 boundary in -2.0f32..2.0,
3439 ) {
3440 let hc = HierarchicalConfidence::new(linkage, type_score, boundary);
3441 prop_assert!(hc.linkage >= 0.0 && hc.linkage <= 1.0);
3442 prop_assert!(hc.type_score >= 0.0 && hc.type_score <= 1.0);
3443 prop_assert!(hc.boundary >= 0.0 && hc.boundary <= 1.0);
3444 prop_assert!(hc.combined() >= 0.0 && hc.combined() <= 1.0);
3445 }
3446
3447 #[test]
3448 fn span_candidate_width_consistent(
3449 doc in 0u32..10,
3450 start in 0u32..100,
3451 end in 1u32..100,
3452 ) {
3453 let actual_end = start.max(end);
3454 let sc = SpanCandidate::new(doc, start, actual_end);
3455 prop_assert_eq!(sc.width(), actual_end.saturating_sub(start));
3456 }
3457
3458 #[test]
3459 fn ragged_batch_preserves_tokens(
3460 seq_lens in proptest::collection::vec(1usize..10, 1..5),
3461 ) {
3462 let mut counter = 0u32;
3464 let seqs: Vec<Vec<u32>> = seq_lens.iter().map(|&len| {
3465 let seq: Vec<u32> = (counter..counter + len as u32).collect();
3466 counter += len as u32;
3467 seq
3468 }).collect();
3469
3470 let batch = RaggedBatch::from_sequences(&seqs);
3471
3472 prop_assert_eq!(batch.batch_size(), seqs.len());
3474 prop_assert_eq!(batch.total_tokens(), seq_lens.iter().sum::<usize>());
3475
3476 for (i, seq) in seqs.iter().enumerate() {
3478 let doc_tokens = batch.doc_tokens(i).unwrap();
3479 prop_assert_eq!(doc_tokens, seq.as_slice());
3480 }
3481 }
3482
3483 #[test]
3484 fn span_text_offsets_consistent(start in 0usize..100, len in 0usize..50) {
3485 let end = start + len;
3486 let span = Span::text(start, end);
3487 let (s, e) = span.text_offsets().unwrap();
3488 prop_assert_eq!(s, start);
3489 prop_assert_eq!(e, end);
3490 prop_assert_eq!(span.len(), len);
3491 }
3492
3493 #[test]
3499 fn entity_span_validity(
3500 start in 0usize..10000,
3501 len in 1usize..500,
3502 conf in 0.0f64..=1.0,
3503 ) {
3504 let end = start + len;
3505 let text_content: String = "x".repeat(end);
3507 let entity_text: String = text_content.chars().skip(start).take(len).collect();
3508 let e = Entity::new(&entity_text, EntityType::Person, start, end, conf);
3509 let issues = e.validate(&text_content);
3510 for issue in &issues {
3512 match issue {
3513 ValidationIssue::InvalidSpan { .. } => {
3514 prop_assert!(false, "start < end should never produce InvalidSpan");
3515 }
3516 ValidationIssue::SpanOutOfBounds { .. } => {
3517 prop_assert!(false, "span within text should never produce SpanOutOfBounds");
3518 }
3519 _ => {} }
3521 }
3522 }
3523
3524 #[test]
3526 fn entity_type_label_roundtrip_standard(
3527 idx in 0usize..13,
3528 ) {
3529 let standard_types = [
3530 EntityType::Person,
3531 EntityType::Organization,
3532 EntityType::Location,
3533 EntityType::Date,
3534 EntityType::Time,
3535 EntityType::Money,
3536 EntityType::Percent,
3537 EntityType::Quantity,
3538 EntityType::Cardinal,
3539 EntityType::Ordinal,
3540 EntityType::Email,
3541 EntityType::Url,
3542 EntityType::Phone,
3543 ];
3544 let et = &standard_types[idx];
3545 let label = et.as_label();
3546 let roundtripped = EntityType::from_label(label);
3547 prop_assert_eq!(&roundtripped, et,
3548 "from_label(as_label()) must roundtrip for {:?} (label={:?})", et, label);
3549 }
3550
3551 #[test]
3553 fn span_containment_property(
3554 a_start in 0usize..5000,
3555 a_len in 1usize..5000,
3556 b_offset in 0usize..5000,
3557 b_len in 1usize..5000,
3558 ) {
3559 let a_end = a_start + a_len;
3560 let b_start = a_start + (b_offset % a_len); let b_end_candidate = b_start + b_len;
3562
3563 if b_start >= a_start && b_end_candidate <= a_end {
3565 prop_assert!(a_start <= b_start);
3567 prop_assert!(a_end >= b_end_candidate);
3568
3569 let ea = Entity::new("a", EntityType::Person, a_start, a_end, 1.0);
3571 let eb = Entity::new("b", EntityType::Person, b_start, b_end_candidate, 1.0);
3572 prop_assert!(ea.overlaps(&eb),
3573 "containing span must overlap contained span");
3574 }
3575 }
3576
3577 #[test]
3579 fn entity_serde_roundtrip(
3580 start in 0usize..10000,
3581 len in 1usize..500,
3582 conf in 0.0f64..=1.0,
3583 type_idx in 0usize..5,
3584 ) {
3585 let end = start + len;
3586 let types = [
3587 EntityType::Person,
3588 EntityType::Organization,
3589 EntityType::Location,
3590 EntityType::Date,
3591 EntityType::Email,
3592 ];
3593 let et = types[type_idx].clone();
3594 let text = format!("entity_{}", start);
3595 let e = Entity::new(&text, et, start, end, conf);
3596
3597 let json = serde_json::to_string(&e).unwrap();
3598 let e2: Entity = serde_json::from_str(&json).unwrap();
3599
3600 prop_assert_eq!(&e.text, &e2.text);
3601 prop_assert_eq!(&e.entity_type, &e2.entity_type);
3602 prop_assert_eq!(e.start(), e2.start());
3603 prop_assert_eq!(e.end(), e2.end());
3604 prop_assert!((e.confidence - e2.confidence).abs() < 1e-10,
3606 "confidence roundtrip: {} vs {}", e.confidence, e2.confidence);
3607 prop_assert_eq!(&e.normalized, &e2.normalized);
3608 prop_assert_eq!(&e.kb_id, &e2.kb_id);
3609 }
3610
3611 #[test]
3614 fn discontinuous_span_total_length(
3615 segments in proptest::collection::vec(
3616 (0usize..5000, 1usize..500),
3617 1..6
3618 ),
3619 ) {
3620 let ranges: Vec<std::ops::Range<usize>> = segments.iter()
3621 .map(|&(start, len)| start..start + len)
3622 .collect();
3623 let span = DiscontinuousSpan::new(ranges);
3624 let expected_sum: usize = span.segments().iter().map(|r| r.end - r.start).sum();
3626 prop_assert_eq!(span.total_len(), expected_sum,
3627 "total_len must equal sum of merged segment lengths");
3628 for w in span.segments().windows(2) {
3630 prop_assert!(w[0].end <= w[1].start,
3631 "segments must not overlap: {:?} vs {:?}", w[0], w[1]);
3632 }
3633 }
3634 }
3635
3636 #[test]
3641 fn test_entity_category_requires_ml() {
3642 assert!(EntityCategory::Agent.requires_ml());
3643 assert!(EntityCategory::Organization.requires_ml());
3644 assert!(EntityCategory::Place.requires_ml());
3645 assert!(EntityCategory::Creative.requires_ml());
3646 assert!(EntityCategory::Relation.requires_ml());
3647
3648 assert!(!EntityCategory::Temporal.requires_ml());
3649 assert!(!EntityCategory::Numeric.requires_ml());
3650 assert!(!EntityCategory::Contact.requires_ml());
3651 assert!(!EntityCategory::Misc.requires_ml());
3652 }
3653
3654 #[test]
3655 fn test_entity_category_pattern_detectable() {
3656 assert!(EntityCategory::Temporal.pattern_detectable());
3657 assert!(EntityCategory::Numeric.pattern_detectable());
3658 assert!(EntityCategory::Contact.pattern_detectable());
3659
3660 assert!(!EntityCategory::Agent.pattern_detectable());
3661 assert!(!EntityCategory::Organization.pattern_detectable());
3662 assert!(!EntityCategory::Place.pattern_detectable());
3663 assert!(!EntityCategory::Creative.pattern_detectable());
3664 assert!(!EntityCategory::Relation.pattern_detectable());
3665 assert!(!EntityCategory::Misc.pattern_detectable());
3666 }
3667
3668 #[test]
3669 fn test_entity_category_is_relation() {
3670 assert!(EntityCategory::Relation.is_relation());
3671
3672 assert!(!EntityCategory::Agent.is_relation());
3673 assert!(!EntityCategory::Organization.is_relation());
3674 assert!(!EntityCategory::Place.is_relation());
3675 assert!(!EntityCategory::Temporal.is_relation());
3676 assert!(!EntityCategory::Numeric.is_relation());
3677 assert!(!EntityCategory::Contact.is_relation());
3678 assert!(!EntityCategory::Creative.is_relation());
3679 assert!(!EntityCategory::Misc.is_relation());
3680 }
3681
3682 #[test]
3683 fn test_entity_category_as_str() {
3684 assert_eq!(EntityCategory::Agent.as_str(), "agent");
3685 assert_eq!(EntityCategory::Organization.as_str(), "organization");
3686 assert_eq!(EntityCategory::Place.as_str(), "place");
3687 assert_eq!(EntityCategory::Creative.as_str(), "creative");
3688 assert_eq!(EntityCategory::Temporal.as_str(), "temporal");
3689 assert_eq!(EntityCategory::Numeric.as_str(), "numeric");
3690 assert_eq!(EntityCategory::Contact.as_str(), "contact");
3691 assert_eq!(EntityCategory::Relation.as_str(), "relation");
3692 assert_eq!(EntityCategory::Misc.as_str(), "misc");
3693 }
3694
3695 #[test]
3696 fn test_entity_category_display() {
3697 assert_eq!(format!("{}", EntityCategory::Agent), "agent");
3698 assert_eq!(format!("{}", EntityCategory::Temporal), "temporal");
3699 assert_eq!(format!("{}", EntityCategory::Relation), "relation");
3700 }
3701
3702 #[test]
3707 fn test_entity_type_serializes_to_flat_string() {
3708 assert_eq!(
3709 serde_json::to_string(&EntityType::Person).unwrap(),
3710 r#""PER""#
3711 );
3712 assert_eq!(
3713 serde_json::to_string(&EntityType::Organization).unwrap(),
3714 r#""ORG""#
3715 );
3716 assert_eq!(
3717 serde_json::to_string(&EntityType::Location).unwrap(),
3718 r#""LOC""#
3719 );
3720 assert_eq!(
3721 serde_json::to_string(&EntityType::Date).unwrap(),
3722 r#""DATE""#
3723 );
3724 assert_eq!(
3725 serde_json::to_string(&EntityType::Money).unwrap(),
3726 r#""MONEY""#
3727 );
3728 }
3729
3730 #[test]
3731 fn test_custom_entity_type_serializes_flat() {
3732 let misc = EntityType::custom("MISC", EntityCategory::Misc);
3733 assert_eq!(serde_json::to_string(&misc).unwrap(), r#""MISC""#);
3734
3735 let disease = EntityType::custom("DISEASE", EntityCategory::Agent);
3736 assert_eq!(serde_json::to_string(&disease).unwrap(), r#""DISEASE""#);
3737 }
3738
3739 #[test]
3740 fn test_entity_type_deserializes_from_flat_string() {
3741 let per: EntityType = serde_json::from_str(r#""PER""#).unwrap();
3742 assert_eq!(per, EntityType::Person);
3743
3744 let org: EntityType = serde_json::from_str(r#""ORG""#).unwrap();
3745 assert_eq!(org, EntityType::Organization);
3746
3747 let misc: EntityType = serde_json::from_str(r#""MISC""#).unwrap();
3748 assert_eq!(misc, EntityType::custom("MISC", EntityCategory::Misc));
3749 }
3750
3751 #[test]
3752 fn test_entity_type_deserializes_backward_compat_custom() {
3753 let json = r#"{"Custom":{"name":"MISC","category":"Misc"}}"#;
3755 let et: EntityType = serde_json::from_str(json).unwrap();
3756 assert_eq!(et, EntityType::custom("MISC", EntityCategory::Misc));
3757 }
3758
3759 #[test]
3760 fn test_entity_type_deserializes_backward_compat_other() {
3761 let json = r#"{"Other":"foo"}"#;
3763 let et: EntityType = serde_json::from_str(json).unwrap();
3764 assert_eq!(et, EntityType::custom("foo", EntityCategory::Misc));
3765 }
3766
3767 #[test]
3768 fn test_entity_type_serde_roundtrip() {
3769 let types = vec![
3770 EntityType::Person,
3771 EntityType::Organization,
3772 EntityType::Location,
3773 EntityType::Date,
3774 EntityType::Time,
3775 EntityType::Money,
3776 EntityType::Percent,
3777 EntityType::Quantity,
3778 EntityType::Cardinal,
3779 EntityType::Ordinal,
3780 EntityType::Email,
3781 EntityType::Url,
3782 EntityType::Phone,
3783 EntityType::custom("MISC", EntityCategory::Misc),
3784 EntityType::custom("DISEASE", EntityCategory::Agent),
3785 ];
3786
3787 for t in &types {
3788 let json = serde_json::to_string(t).unwrap();
3789 let back: EntityType = serde_json::from_str(&json).unwrap();
3790 assert_eq!(
3793 t.as_label(),
3794 back.as_label(),
3795 "roundtrip failed for {:?}",
3796 t
3797 );
3798 }
3799 }
3800}