Skip to main content

anno_eval/eval/
bridging.rs

1//! Bridging Anaphora Resolution.
2//!
3//! # Overview
4//!
5//! Bridging anaphora involves inferential links where the anaphor is
6//! *associated* with but not *identical* to its antecedent.
7//!
8//! # Examples
9//!
10//! ```text
11//! "The car broke down. The engine had overheated."
12//!                       ^^^^^^^^^^ bridge (part-whole)
13//! ```
14//!
15//! The definite NP "The engine" is interpreted via a bridging inference
16//! (part-whole relation) to "The car" in the previous sentence.
17//!
18//! # Bridging Types
19//!
20//! | Type | Example | Relation |
21//! |------|---------|----------|
22//! | **Part-Whole** | car → engine | Meronymy |
23//! | **Set-Membership** | students → one student | Set containment |
24//! | **Role** | company → CEO | Functional role |
25//! | **Attribute** | house → price | Property |
26//! | **Other** | concert → audience | General association |
27//!
28//! # Corpora
29//!
30//! - **ISNotes**: OntoNotes-layer bridging annotations
31//! - **BASHI**: bridging subtypes (definite/indefinite/comparative)
32//! - **ARRAU RST**: bridging in discourse structure contexts
33//!
34//! # Annotation Challenges
35//!
36//! Inter-annotator agreement for bridging can be substantially lower than for
37//! identity coreference. This reflects genuine ambiguity in what counts as a
38//! “bridge” vs discourse coherence.
39//!
40//! # References
41//!
42//! - Hou et al. (2018): "Unrestricted Bridging Resolution"
43//! - Rösiger et al. (2018): "Bridging Resolution"
44
45use serde::{Deserialize, Serialize};
46
47/// Type of bridging relation between anaphor and antecedent.
48#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub enum BridgingType {
50    /// Part-whole (meronymy): "the car" → "the engine"
51    PartWhole,
52    /// Set-membership: "the students" → "one student"
53    SetMembership,
54    /// Functional role: "the company" → "the CEO"
55    Role,
56    /// Attribute/property: "the house" → "the price"
57    Attribute,
58    /// Producer-product: "the author" → "the book"
59    ProducerProduct,
60    /// Event participant: "the wedding" → "the bride"
61    EventParticipant,
62    /// Location: "the city" → "the downtown area"
63    Location,
64    /// Comparative: "this report" → "the previous one"
65    Comparative,
66    /// Other associative relation
67    Other(String),
68}
69
70impl Default for BridgingType {
71    fn default() -> Self {
72        Self::Other(String::new())
73    }
74}
75
76impl BridgingType {
77    /// Create from string label.
78    pub fn from_label(label: &str) -> Self {
79        match label.to_lowercase().as_str() {
80            "part-whole" | "meronymy" | "part_whole" => Self::PartWhole,
81            "set-membership" | "set_membership" | "set" => Self::SetMembership,
82            "role" | "functional" => Self::Role,
83            "attribute" | "property" => Self::Attribute,
84            "producer-product" | "producer_product" => Self::ProducerProduct,
85            "event-participant" | "event_participant" | "participant" => Self::EventParticipant,
86            "location" | "spatial" => Self::Location,
87            "comparative" => Self::Comparative,
88            other => Self::Other(other.to_string()),
89        }
90    }
91
92    /// Get canonical label.
93    pub fn as_label(&self) -> &str {
94        match self {
95            Self::PartWhole => "part-whole",
96            Self::SetMembership => "set-membership",
97            Self::Role => "role",
98            Self::Attribute => "attribute",
99            Self::ProducerProduct => "producer-product",
100            Self::EventParticipant => "event-participant",
101            Self::Location => "location",
102            Self::Comparative => "comparative",
103            Self::Other(s) => s.as_str(),
104        }
105    }
106}
107
108/// A bridging link between an anaphor and its antecedent.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct BridgingLink {
111    /// The bridging anaphor (e.g., "the engine")
112    pub anaphor: BridgingMention,
113    /// The antecedent (e.g., "the car")
114    pub antecedent: BridgingMention,
115    /// Type of bridging relation
116    pub bridging_type: BridgingType,
117    /// Confidence in this link
118    pub confidence: f64,
119    /// Whether this is a lexical (predictable) or referential bridge
120    pub is_lexical: bool,
121}
122
123impl BridgingLink {
124    /// Create a new bridging link.
125    pub fn new(
126        anaphor: BridgingMention,
127        antecedent: BridgingMention,
128        bridging_type: BridgingType,
129    ) -> Self {
130        Self {
131            anaphor,
132            antecedent,
133            bridging_type,
134            confidence: 1.0,
135            is_lexical: false,
136        }
137    }
138
139    /// Check if this is a part-whole bridge.
140    pub fn is_part_whole(&self) -> bool {
141        matches!(self.bridging_type, BridgingType::PartWhole)
142    }
143
144    /// Check if this is a set-membership bridge.
145    pub fn is_set_membership(&self) -> bool {
146        matches!(self.bridging_type, BridgingType::SetMembership)
147    }
148}
149
150/// A mention in a bridging relation.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct BridgingMention {
153    /// The mention text
154    pub text: String,
155    /// Start character offset
156    pub start: usize,
157    /// End character offset (exclusive)
158    pub end: usize,
159    /// Head word (for head-match evaluation)
160    pub head: Option<String>,
161    /// Sentence index (for cross-sentence bridging)
162    pub sentence_idx: Option<usize>,
163}
164
165impl BridgingMention {
166    /// Create a new bridging mention.
167    pub fn new(text: &str, start: usize, end: usize) -> Self {
168        Self {
169            text: text.to_string(),
170            start,
171            end,
172            head: None,
173            sentence_idx: None,
174        }
175    }
176
177    /// Set the head word.
178    pub fn with_head(mut self, head: &str) -> Self {
179        self.head = Some(head.to_string());
180        self
181    }
182
183    /// Set the sentence index.
184    pub fn with_sentence(mut self, idx: usize) -> Self {
185        self.sentence_idx = Some(idx);
186        self
187    }
188}
189
190/// Bridging resolution result for a document.
191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192pub struct BridgingDocument {
193    /// Document ID
194    pub id: String,
195    /// Document text
196    pub text: String,
197    /// Detected bridging links
198    pub links: Vec<BridgingLink>,
199}
200
201impl BridgingDocument {
202    /// Create a new bridging document.
203    pub fn new(id: &str, text: &str) -> Self {
204        Self {
205            id: id.to_string(),
206            text: text.to_string(),
207            links: Vec::new(),
208        }
209    }
210
211    /// Add a bridging link.
212    pub fn add_link(&mut self, link: BridgingLink) {
213        self.links.push(link);
214    }
215
216    /// Number of bridging links.
217    pub fn len(&self) -> usize {
218        self.links.len()
219    }
220
221    /// Check if empty.
222    pub fn is_empty(&self) -> bool {
223        self.links.is_empty()
224    }
225
226    /// Get links by type.
227    pub fn links_by_type(&self, bridging_type: &BridgingType) -> Vec<&BridgingLink> {
228        self.links
229            .iter()
230            .filter(|l| &l.bridging_type == bridging_type)
231            .collect()
232    }
233}
234
235/// Evaluation metrics for bridging resolution.
236#[derive(Debug, Clone, Default, Serialize, Deserialize)]
237pub struct BridgingMetrics {
238    /// Precision: correct / predicted
239    pub precision: f64,
240    /// Recall: correct / gold
241    pub recall: f64,
242    /// F1 score
243    pub f1: f64,
244    /// Number of predicted links
245    pub predicted: usize,
246    /// Number of gold links
247    pub gold: usize,
248    /// Number of correct links
249    pub correct: usize,
250    /// Breakdown by bridging type
251    pub by_type: std::collections::HashMap<String, (f64, f64, f64)>, // (P, R, F1)
252}
253
254impl BridgingMetrics {
255    /// Compute metrics from predicted and gold documents.
256    pub fn compute(predicted: &[BridgingDocument], gold: &[BridgingDocument]) -> Self {
257        let mut total_pred = 0;
258        let mut total_gold = 0;
259        let mut total_correct = 0;
260        let mut by_type: std::collections::HashMap<String, (usize, usize, usize)> =
261            std::collections::HashMap::new();
262
263        for (pred_doc, gold_doc) in predicted.iter().zip(gold.iter()) {
264            total_pred += pred_doc.links.len();
265            total_gold += gold_doc.links.len();
266
267            // Match links (exact span match)
268            for pred_link in &pred_doc.links {
269                let type_key = pred_link.bridging_type.as_label().to_string();
270                by_type.entry(type_key.clone()).or_insert((0, 0, 0)).0 += 1;
271
272                for gold_link in &gold_doc.links {
273                    if Self::links_match(pred_link, gold_link) {
274                        total_correct += 1;
275                        by_type.entry(type_key.clone()).or_insert((0, 0, 0)).2 += 1;
276                        break;
277                    }
278                }
279            }
280
281            for gold_link in &gold_doc.links {
282                let type_key = gold_link.bridging_type.as_label().to_string();
283                by_type.entry(type_key).or_insert((0, 0, 0)).1 += 1;
284            }
285        }
286
287        let precision = if total_pred > 0 {
288            total_correct as f64 / total_pred as f64
289        } else {
290            0.0
291        };
292        let recall = if total_gold > 0 {
293            total_correct as f64 / total_gold as f64
294        } else {
295            0.0
296        };
297        let f1 = if precision + recall > 0.0 {
298            2.0 * precision * recall / (precision + recall)
299        } else {
300            0.0
301        };
302
303        // Compute per-type metrics
304        let by_type_metrics: std::collections::HashMap<String, (f64, f64, f64)> = by_type
305            .into_iter()
306            .map(|(k, (pred, gold, corr))| {
307                let p = if pred > 0 {
308                    corr as f64 / pred as f64
309                } else {
310                    0.0
311                };
312                let r = if gold > 0 {
313                    corr as f64 / gold as f64
314                } else {
315                    0.0
316                };
317                let f = if p + r > 0.0 {
318                    2.0 * p * r / (p + r)
319                } else {
320                    0.0
321                };
322                (k, (p, r, f))
323            })
324            .collect();
325
326        Self {
327            precision,
328            recall,
329            f1,
330            predicted: total_pred,
331            gold: total_gold,
332            correct: total_correct,
333            by_type: by_type_metrics,
334        }
335    }
336
337    /// Check if two links match (exact span match).
338    fn links_match(a: &BridgingLink, b: &BridgingLink) -> bool {
339        a.anaphor.start == b.anaphor.start
340            && a.anaphor.end == b.anaphor.end
341            && a.antecedent.start == b.antecedent.start
342            && a.antecedent.end == b.antecedent.end
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_bridging_type_from_label() {
352        assert_eq!(
353            BridgingType::from_label("part-whole"),
354            BridgingType::PartWhole
355        );
356        assert_eq!(
357            BridgingType::from_label("SET-MEMBERSHIP"),
358            BridgingType::SetMembership
359        );
360        assert_eq!(BridgingType::from_label("role"), BridgingType::Role);
361    }
362
363    #[test]
364    fn test_bridging_link_creation() {
365        let anaphor = BridgingMention::new("the engine", 20, 30);
366        let antecedent = BridgingMention::new("The car", 0, 7);
367        let link = BridgingLink::new(anaphor, antecedent, BridgingType::PartWhole);
368
369        assert!(link.is_part_whole());
370        assert!(!link.is_set_membership());
371        assert_eq!(link.bridging_type.as_label(), "part-whole");
372    }
373
374    #[test]
375    fn test_bridging_document() {
376        let mut doc = BridgingDocument::new("doc1", "The car broke down. The engine failed.");
377
378        let anaphor = BridgingMention::new("The engine", 20, 30).with_sentence(1);
379        let antecedent = BridgingMention::new("The car", 0, 7).with_sentence(0);
380        let link = BridgingLink::new(anaphor, antecedent, BridgingType::PartWhole);
381
382        doc.add_link(link);
383
384        assert_eq!(doc.len(), 1);
385        assert!(!doc.is_empty());
386        assert_eq!(doc.links_by_type(&BridgingType::PartWhole).len(), 1);
387    }
388
389    #[test]
390    fn test_bridging_metrics() {
391        let mut pred_doc = BridgingDocument::new("doc1", "");
392        pred_doc.add_link(BridgingLink::new(
393            BridgingMention::new("the engine", 20, 30),
394            BridgingMention::new("The car", 0, 7),
395            BridgingType::PartWhole,
396        ));
397
398        let mut gold_doc = BridgingDocument::new("doc1", "");
399        gold_doc.add_link(BridgingLink::new(
400            BridgingMention::new("the engine", 20, 30),
401            BridgingMention::new("The car", 0, 7),
402            BridgingType::PartWhole,
403        ));
404
405        let metrics = BridgingMetrics::compute(&[pred_doc], &[gold_doc]);
406
407        assert_eq!(metrics.precision, 1.0);
408        assert_eq!(metrics.recall, 1.0);
409        assert_eq!(metrics.f1, 1.0);
410    }
411}