Skip to main content

anno_eval/eval/
benchmark.rs

1//! Large-scale benchmark datasets for NER evaluation.
2//!
3//! Unlike the synthetic module which provides curated examples, this module
4//! generates large datasets for stress testing and statistical significance.
5//!
6//! # Philosophy (burntsushi-style)
7//!
8//! - **Real** edge cases, not toy examples
9//! - Models should perform **just okay** on these, not perfectly
10//! - Tests statistical significance, not cherry-picked success cases
11//!
12//! # Usage
13//!
14//! ```rust,no_run
15//! use anno_eval::eval::benchmark::{generate_large_dataset, EdgeCaseType};
16//!
17//! // Generate 1000 hard examples
18//! let dataset = generate_large_dataset(1000, EdgeCaseType::All);
19//! assert!(dataset.len() >= 1000);
20//! ```
21
22use super::datasets::GoldEntity;
23use super::synthetic::{AnnotatedExample, Difficulty, Domain};
24use anno::{EntityCategory, EntityType};
25
26/// Types of edge cases to generate
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EdgeCaseType {
29    /// All edge case types
30    All,
31    /// Ambiguous entities (words that could be multiple types)
32    Ambiguous,
33    /// Unicode edge cases (RTL, combining characters, emoji)
34    Unicode,
35    /// Dense text (many entities close together)
36    Dense,
37    /// Sparse text (single entity in long text)
38    Sparse,
39    /// Nested or overlapping entity candidates
40    Nested,
41    /// Unusual capitalization or casing
42    Casing,
43    /// Boundary cases (entities at start/end, with punctuation)
44    Boundary,
45    /// Multi-word entities (proper name + title, company + suffix)
46    MultiWord,
47    /// Numeric pattern edge cases
48    NumericEdge,
49    /// Domain-specific jargon
50    Jargon,
51}
52
53/// Generate a large dataset of challenging examples.
54///
55/// Returns at least `min_count` examples, potentially more due to template expansion.
56#[must_use]
57pub fn generate_large_dataset(
58    min_count: usize,
59    edge_case_type: EdgeCaseType,
60) -> Vec<AnnotatedExample> {
61    let mut examples = Vec::with_capacity(min_count);
62
63    match edge_case_type {
64        EdgeCaseType::All => {
65            let per_type = min_count / 10;
66            examples.extend(generate_ambiguous_examples(per_type));
67            examples.extend(generate_unicode_examples(per_type));
68            examples.extend(generate_dense_examples(per_type));
69            examples.extend(generate_sparse_examples(per_type));
70            examples.extend(generate_nested_examples(per_type));
71            examples.extend(generate_casing_examples(per_type));
72            examples.extend(generate_boundary_examples(per_type));
73            examples.extend(generate_multiword_examples(per_type));
74            examples.extend(generate_numeric_edge_examples(per_type));
75            examples.extend(generate_jargon_examples(per_type));
76        }
77        EdgeCaseType::Ambiguous => examples.extend(generate_ambiguous_examples(min_count)),
78        EdgeCaseType::Unicode => examples.extend(generate_unicode_examples(min_count)),
79        EdgeCaseType::Dense => examples.extend(generate_dense_examples(min_count)),
80        EdgeCaseType::Sparse => examples.extend(generate_sparse_examples(min_count)),
81        EdgeCaseType::Nested => examples.extend(generate_nested_examples(min_count)),
82        EdgeCaseType::Casing => examples.extend(generate_casing_examples(min_count)),
83        EdgeCaseType::Boundary => examples.extend(generate_boundary_examples(min_count)),
84        EdgeCaseType::MultiWord => examples.extend(generate_multiword_examples(min_count)),
85        EdgeCaseType::NumericEdge => examples.extend(generate_numeric_edge_examples(min_count)),
86        EdgeCaseType::Jargon => examples.extend(generate_jargon_examples(min_count)),
87    }
88
89    // Ensure we have at least min_count by duplicating with variations
90    while examples.len() < min_count {
91        let idx = examples.len() % examples.len().max(1);
92        if let Some(ex) = examples.get(idx).cloned() {
93            examples.push(ex);
94        } else {
95            break;
96        }
97    }
98
99    examples
100}
101
102// ============================================================================
103// Ambiguous Examples - Words that could be multiple types
104// ============================================================================
105
106fn generate_ambiguous_examples(count: usize) -> Vec<AnnotatedExample> {
107    // Words that are genuinely ambiguous: "Apple" (company or fruit),
108    // "Washington" (person, city, or state), "Amazon" (company or river)
109    let templates = [
110        // Apple - company vs fruit (this is genuinely hard)
111        ("I love Apple products but prefer android.", vec![]), // Apple = company, but no clear entity marker
112        ("The Apple fell from the tree.", vec![]),             // Apple = fruit, not an entity
113        (
114            "Apple Inc. reported earnings.",
115            vec![("Apple Inc.", EntityType::Organization, 0)],
116        ),
117        (
118            "Steve Jobs founded Apple.",
119            vec![
120                ("Steve Jobs", EntityType::Person, 0),
121                ("Apple", EntityType::Organization, 19),
122            ],
123        ),
124        // Washington - person, city, state, building
125        (
126            "Washington was the first president.",
127            vec![("Washington", EntityType::Person, 0)],
128        ),
129        (
130            "I visited Washington D.C. last week.",
131            vec![("Washington D.C.", EntityType::Location, 10)],
132        ),
133        ("The meeting is at Washington Hotel.", vec![]), // Ambiguous: Location or Organization?
134        (
135            "George Washington crossed the Delaware.",
136            vec![
137                ("George Washington", EntityType::Person, 0),
138                ("Delaware", EntityType::Location, 30),
139            ],
140        ),
141        // Amazon - company vs river
142        (
143            "Amazon ships globally.",
144            vec![("Amazon", EntityType::Organization, 0)],
145        ),
146        (
147            "The Amazon river is massive.",
148            vec![
149                ("Amazon", EntityType::Location, 4), // River name
150            ],
151        ),
152        (
153            "Amazon Prime is popular.",
154            vec![
155                ("Amazon Prime", EntityType::Organization, 0), // Product/service
156            ],
157        ),
158        // Jordan - person vs country
159        (
160            "Michael Jordan played basketball.",
161            vec![("Michael Jordan", EntityType::Person, 0)],
162        ),
163        (
164            "Jordan borders Israel.",
165            vec![
166                ("Jordan", EntityType::Location, 0),
167                ("Israel", EntityType::Location, 15),
168            ],
169        ),
170        (
171            "Jordan Peele directed the film.",
172            vec![("Jordan Peele", EntityType::Person, 0)],
173        ),
174        // Paris - city vs person (Paris Hilton)
175        (
176            "Paris is beautiful in spring.",
177            vec![("Paris", EntityType::Location, 0)],
178        ),
179        (
180            "Paris Hilton attended the event.",
181            vec![("Paris Hilton", EntityType::Person, 0)],
182        ),
183        // China - country vs dinnerware (lowercase = tableware)
184        ("Made in China.", vec![("China", EntityType::Location, 8)]),
185        ("The china cabinet is antique.", vec![]), // lowercase china = porcelain
186        // May - month vs name vs verb
187        (
188            "May is a lovely month.",
189            vec![
190                ("May", EntityType::Date, 0), // Month
191            ],
192        ),
193        (
194            "Theresa May resigned as PM.",
195            vec![("Theresa May", EntityType::Person, 0)],
196        ),
197        ("You may proceed.", vec![]), // verb, not entity
198        // Bill - name vs legislation vs invoice
199        (
200            "Bill Gates founded Microsoft.",
201            vec![
202                ("Bill Gates", EntityType::Person, 0),
203                ("Microsoft", EntityType::Organization, 19),
204            ],
205        ),
206        ("The bill passed the Senate.", vec![]), // legislation, not entity
207        ("Please pay the bill.", vec![]),        // invoice, not entity
208    ];
209
210    generate_from_templates(&templates, count, Domain::News, Difficulty::Hard)
211}
212
213// ============================================================================
214// Unicode Examples - RTL, combining characters, emoji
215// ============================================================================
216
217fn generate_unicode_examples(count: usize) -> Vec<AnnotatedExample> {
218    let templates = [
219        // CJK characters
220        (
221            "株式会社トヨタ自動車 reported earnings.",
222            vec![("株式会社トヨタ自動車", EntityType::Organization, 0)],
223        ),
224        (
225            "Contact 田中太郎 for details.",
226            vec![("田中太郎", EntityType::Person, 8)],
227        ),
228        // Arabic (RTL)
229        (
230            "محمد is a common name.",
231            vec![("محمد", EntityType::Person, 0)],
232        ),
233        // Cyrillic
234        (
235            "Владимир Путин addressed the nation.",
236            vec![("Владимир Путин", EntityType::Person, 0)],
237        ),
238        (
239            "Visit Москва this summer.",
240            vec![("Москва", EntityType::Location, 6)],
241        ),
242        // Greek
243        (
244            "Αθήνα is the capital of Greece.",
245            vec![
246                ("Αθήνα", EntityType::Location, 0),
247                ("Greece", EntityType::Location, 24),
248            ],
249        ),
250        // Mixed scripts
251        (
252            "Sony (ソニー株式会社) announced new products.",
253            vec![
254                ("Sony", EntityType::Organization, 0),
255                ("ソニー株式会社", EntityType::Organization, 6),
256            ],
257        ),
258        // Emoji with entities
259        (
260            "Tim Berners-Lee invented the Web.",
261            vec![
262                ("Tim Berners-Lee", EntityType::Person, 0),
263                (
264                    "Web",
265                    EntityType::custom("Technology", EntityCategory::Misc),
266                    29,
267                ),
268            ],
269        ),
270        // Accented characters
271        (
272            "François Hollande was president.",
273            vec![("François Hollande", EntityType::Person, 0)],
274        ),
275        (
276            "Zürich is in Switzerland.",
277            vec![
278                ("Zürich", EntityType::Location, 0),
279                ("Switzerland", EntityType::Location, 13),
280            ],
281        ),
282        (
283            "São Paulo is Brazil's largest city.",
284            vec![
285                ("São Paulo", EntityType::Location, 0),
286                ("Brazil", EntityType::Location, 13),
287            ],
288        ),
289        // Combining characters
290        (
291            "José García works at Google.",
292            vec![
293                ("José García", EntityType::Person, 0),
294                ("Google", EntityType::Organization, 21),
295            ],
296        ),
297        // Long multi-byte
298        (
299            "北京大学 and 清华大学 are top universities.",
300            vec![
301                ("北京大学", EntityType::Organization, 0),
302                ("清华大学", EntityType::Organization, 9),
303            ],
304        ),
305    ];
306
307    generate_from_templates(&templates, count, Domain::News, Difficulty::Hard)
308}
309
310// ============================================================================
311// Dense Examples - Many entities close together
312// ============================================================================
313
314fn generate_dense_examples(count: usize) -> Vec<AnnotatedExample> {
315    let templates = [
316        // Multiple entities per sentence
317        (
318            "Apple, Google, Microsoft, Amazon, and Meta all reported earnings.",
319            vec![
320                ("Apple", EntityType::Organization, 0),
321                ("Google", EntityType::Organization, 7),
322                ("Microsoft", EntityType::Organization, 15),
323                ("Amazon", EntityType::Organization, 26),
324                ("Meta", EntityType::Organization, 38),
325            ],
326        ),
327        (
328            "John Smith, Jane Doe, and Bob Wilson attended.",
329            vec![
330                ("John Smith", EntityType::Person, 0),
331                ("Jane Doe", EntityType::Person, 12),
332                ("Bob Wilson", EntityType::Person, 26),
333            ],
334        ),
335        (
336            "Contact support@company.com or sales@company.com.",
337            vec![
338                ("support@company.com", EntityType::Email, 8),
339                ("sales@company.com", EntityType::Email, 31),
340            ],
341        ),
342        (
343            "Visit https://example.com and https://test.org.",
344            vec![
345                ("https://example.com", EntityType::Url, 6),
346                ("https://test.org", EntityType::Url, 30),
347            ],
348        ),
349        (
350            "$100, $200, $300, and $400 are the prices.",
351            vec![
352                ("$100", EntityType::Money, 0),
353                ("$200", EntityType::Money, 6),
354                ("$300", EntityType::Money, 12),
355                ("$400", EntityType::Money, 22),
356            ],
357        ),
358        // Very dense entity sequence
359        (
360            "Tokyo, Beijing, Seoul, Bangkok, Singapore.",
361            vec![
362                ("Tokyo", EntityType::Location, 0),
363                ("Beijing", EntityType::Location, 7),
364                ("Seoul", EntityType::Location, 16),
365                ("Bangkok", EntityType::Location, 23),
366                ("Singapore", EntityType::Location, 32),
367            ],
368        ),
369        // Dates and numbers
370        (
371            "January 1, February 2, March 3, April 4, May 5.",
372            vec![
373                ("January 1", EntityType::Date, 0),
374                ("February 2", EntityType::Date, 11),
375                ("March 3", EntityType::Date, 23),
376                ("April 4", EntityType::Date, 32),
377                ("May 5", EntityType::Date, 41),
378            ],
379        ),
380        // Mixed entity types, dense
381        (
382            "Tim Cook (Apple), Sundar Pichai (Google), Satya Nadella (Microsoft).",
383            vec![
384                ("Tim Cook", EntityType::Person, 0),
385                ("Apple", EntityType::Organization, 10),
386                ("Sundar Pichai", EntityType::Person, 18),
387                ("Google", EntityType::Organization, 33),
388                ("Satya Nadella", EntityType::Person, 42),
389                ("Microsoft", EntityType::Organization, 57),
390            ],
391        ),
392    ];
393
394    generate_from_templates(&templates, count, Domain::News, Difficulty::Hard)
395}
396
397// ============================================================================
398// Sparse Examples - Single entity in long text
399// ============================================================================
400
401fn generate_sparse_examples(count: usize) -> Vec<AnnotatedExample> {
402    let templates = [
403        (
404            "The weather has been quite nice lately with temperatures hovering around the mid-seventies and clear skies throughout most of the week. According to the forecast, this trend is expected to continue for at least another few days before any significant changes occur in the atmospheric conditions. Many people have been taking advantage of the pleasant weather to spend time outdoors, enjoying activities such as hiking, biking, and picnicking in the parks. The local news station reported that John Smith was seen at the park yesterday.",
405            vec![("John Smith", EntityType::Person, 493)],
406        ),
407        (
408            "In a move that surprised absolutely no one, the quarterly financial results came in exactly as analysts had predicted, with revenues matching expectations and profit margins holding steady. The company's stock price barely moved on the news, reflecting the market's general sense that everything was proceeding according to plan. After hours of discussion and deliberation, the board decided to maintain the current dividend policy and continue with the existing strategic initiatives. CEO Maria Garcia addressed the shareholders.",
409            vec![("Maria Garcia", EntityType::Person, 490)],
410        ),
411        (
412            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. The meeting is at Google headquarters.",
413            vec![("Google", EntityType::Organization, 210)],
414        ),
415    ];
416
417    generate_from_templates(&templates, count, Domain::News, Difficulty::Medium)
418}
419
420// ============================================================================
421// Nested Examples - Overlapping/nested entity candidates
422// ============================================================================
423
424fn generate_nested_examples(count: usize) -> Vec<AnnotatedExample> {
425    let templates = [
426        // Organization within location name
427        (
428            "University of California, Los Angeles is a great school.",
429            vec![(
430                "University of California, Los Angeles",
431                EntityType::Organization,
432                0,
433            )],
434        ),
435        // Person name contains place name
436        (
437            "Jack London wrote adventure novels.",
438            vec![("Jack London", EntityType::Person, 0)],
439        ),
440        // Location within organization name
441        (
442            "Bank of America reported earnings.",
443            vec![("Bank of America", EntityType::Organization, 0)],
444        ),
445        // Multiple nested candidates
446        (
447            "New York Times published the story.",
448            vec![("New York Times", EntityType::Organization, 0)],
449        ),
450        // Person name that is also a place
451        (
452            "Carolina Herrera designs fashion.",
453            vec![("Carolina Herrera", EntityType::Person, 0)],
454        ),
455        // Compound proper nouns
456        (
457            "The Wall Street Journal reports on Wall Street.",
458            vec![
459                ("Wall Street Journal", EntityType::Organization, 4),
460                ("Wall Street", EntityType::Location, 35),
461            ],
462        ),
463        // University names (nested location)
464        (
465            "Harvard University is in Cambridge, Massachusetts.",
466            vec![
467                ("Harvard University", EntityType::Organization, 0),
468                ("Cambridge", EntityType::Location, 25),
469                ("Massachusetts", EntityType::Location, 36),
470            ],
471        ),
472    ];
473
474    generate_from_templates(&templates, count, Domain::Academic, Difficulty::Hard)
475}
476
477// ============================================================================
478// Casing Examples - Unusual capitalization
479// ============================================================================
480
481fn generate_casing_examples(count: usize) -> Vec<AnnotatedExample> {
482    let templates = [
483        // All caps (headlines, tweets)
484        (
485            "APPLE ANNOUNCES NEW IPHONE",
486            vec![("APPLE", EntityType::Organization, 0)],
487        ),
488        // lowercase (informal text, tweets)
489        (
490            "just saw tim cook at the apple store lol",
491            vec![
492                // These should ideally be detected but many models fail here
493            ],
494        ),
495        // CamelCase (product names, hashtags)
496        (
497            "Download the iPhone app from Apple.",
498            vec![("Apple", EntityType::Organization, 29)],
499        ),
500        // Mixed case (typos, stylistic)
501        (
502            "BREAKING: Amazon CEO resigns",
503            vec![("Amazon", EntityType::Organization, 10)],
504        ),
505        // Title case vs sentence case
506        (
507            "The President Of The United States spoke today.",
508            vec![("United States", EntityType::Location, 21)],
509        ),
510        // Acronyms with periods
511        (
512            "The U.S.A. is large.",
513            vec![("U.S.A.", EntityType::Location, 4)],
514        ),
515        (
516            "Contact the F.B.I. for assistance.",
517            vec![("F.B.I.", EntityType::Organization, 12)],
518        ),
519    ];
520
521    generate_from_templates(&templates, count, Domain::SocialMedia, Difficulty::Hard)
522}
523
524// ============================================================================
525// Boundary Examples - Entities at boundaries, with punctuation
526// ============================================================================
527
528fn generate_boundary_examples(count: usize) -> Vec<AnnotatedExample> {
529    let templates = [
530        // At start
531        (
532            "Apple is a company.",
533            vec![("Apple", EntityType::Organization, 0)],
534        ),
535        // At end
536        (
537            "The company is Apple.",
538            vec![("Apple", EntityType::Organization, 15)],
539        ),
540        // With punctuation
541        (
542            "Is Apple, the company, profitable?",
543            vec![("Apple", EntityType::Organization, 3)],
544        ),
545        // With quotes
546        (
547            "\"Apple\" announced earnings.",
548            vec![("Apple", EntityType::Organization, 1)],
549        ),
550        // With parentheses
551        (
552            "The company (Apple) is large.",
553            vec![("Apple", EntityType::Organization, 13)],
554        ),
555        // With colon
556        (
557            "Company: Apple Inc.",
558            vec![("Apple Inc.", EntityType::Organization, 9)],
559        ),
560        // Hyphenated
561        (
562            "The Apple-Google partnership.",
563            vec![
564                ("Apple", EntityType::Organization, 4),
565                ("Google", EntityType::Organization, 10),
566            ],
567        ),
568        // With apostrophe
569        (
570            "Apple's revenue increased.",
571            vec![("Apple", EntityType::Organization, 0)],
572        ),
573        // Entity is the entire text
574        ("John Smith", vec![("John Smith", EntityType::Person, 0)]),
575        // Entity at very end with period
576        (
577            "The CEO is Tim Cook.",
578            vec![("Tim Cook", EntityType::Person, 11)],
579        ),
580    ];
581
582    generate_from_templates(&templates, count, Domain::News, Difficulty::Medium)
583}
584
585// ============================================================================
586// Multi-Word Examples - Complex multi-token entities
587// ============================================================================
588
589fn generate_multiword_examples(count: usize) -> Vec<AnnotatedExample> {
590    let templates = [
591        // Titles with names
592        (
593            "Dr. John Smith presented the findings.",
594            vec![("Dr. John Smith", EntityType::Person, 0)],
595        ),
596        (
597            "Prof. Maria Garcia leads the department.",
598            vec![("Prof. Maria Garcia", EntityType::Person, 0)],
599        ),
600        // Full company names
601        (
602            "International Business Machines Corporation announced layoffs.",
603            vec![(
604                "International Business Machines Corporation",
605                EntityType::Organization,
606                0,
607            )],
608        ),
609        // Locations with qualifiers
610        (
611            "Greater Los Angeles Area is densely populated.",
612            vec![("Greater Los Angeles Area", EntityType::Location, 0)],
613        ),
614        // Names with suffixes
615        (
616            "John Smith Jr. inherited the company.",
617            vec![("John Smith Jr.", EntityType::Person, 0)],
618        ),
619        (
620            "Robert Kennedy III is running.",
621            vec![("Robert Kennedy III", EntityType::Person, 0)],
622        ),
623        // Organizations with "The"
624        (
625            "The New York Times reported.",
626            vec![("The New York Times", EntityType::Organization, 0)],
627        ),
628        // Long place names
629        (
630            "The United Kingdom of Great Britain and Northern Ireland.",
631            vec![(
632                "United Kingdom of Great Britain and Northern Ireland",
633                EntityType::Location,
634                4,
635            )],
636        ),
637    ];
638
639    generate_from_templates(&templates, count, Domain::News, Difficulty::Hard)
640}
641
642// ============================================================================
643// Numeric Edge Examples - Pattern edge cases
644// ============================================================================
645
646fn generate_numeric_edge_examples(count: usize) -> Vec<AnnotatedExample> {
647    let templates = [
648        // Money edge cases
649        ("The price is $1.", vec![("$1", EntityType::Money, 13)]),
650        ("It costs $0.01.", vec![("$0.01", EntityType::Money, 9)]),
651        (
652            "Worth $1,000,000.",
653            vec![("$1,000,000", EntityType::Money, 6)],
654        ),
655        ("Price: $1.5M.", vec![("$1.5M", EntityType::Money, 7)]),
656        (
657            "Costs between $10-$20.",
658            vec![
659                ("$10", EntityType::Money, 14),
660                ("$20", EntityType::Money, 18),
661            ],
662        ),
663        // Percentage edge cases
664        ("Increased 0.5%.", vec![("0.5%", EntityType::Percent, 10)]),
665        ("Down 100%.", vec![("100%", EntityType::Percent, 5)]),
666        ("About 33.333%.", vec![("33.333%", EntityType::Percent, 6)]),
667        // Date edge cases
668        ("On 1/1/2020.", vec![("1/1/2020", EntityType::Date, 3)]),
669        (
670            "Date: 2020-01-01.",
671            vec![("2020-01-01", EntityType::Date, 6)],
672        ),
673        (
674            "January 1st, 2020.",
675            vec![("January 1st, 2020", EntityType::Date, 0)],
676        ),
677        ("The year 2020.", vec![]), // Year alone may not be a date entity
678        // Time edge cases
679        ("At 9:00 AM.", vec![("9:00 AM", EntityType::Time, 3)]),
680        ("Meeting at 14:30.", vec![("14:30", EntityType::Time, 11)]),
681        ("Around noon.", vec![]), // "noon" may or may not be time entity
682        // Phone edge cases
683        ("Call 555-1234.", vec![("555-1234", EntityType::Phone, 5)]),
684        (
685            "Phone: +1-800-555-1234.",
686            vec![("+1-800-555-1234", EntityType::Phone, 7)],
687        ),
688        ("Ext. 1234.", vec![]), // Extension alone is not a phone
689        // Email edge cases
690        ("Email a@b.co.", vec![("a@b.co", EntityType::Email, 6)]),
691        (
692            "Contact test.user+tag@subdomain.example.com.",
693            vec![("test.user+tag@subdomain.example.com", EntityType::Email, 8)],
694        ),
695        // URL edge cases
696        (
697            "Visit http://x.co.",
698            vec![("http://x.co", EntityType::Url, 6)],
699        ),
700        ("Go to localhost:8080.", vec![]), // localhost is not typically a URL entity
701    ];
702
703    generate_from_templates(&templates, count, Domain::Technical, Difficulty::Hard)
704}
705
706// ============================================================================
707// Jargon Examples - Domain-specific terminology
708// ============================================================================
709
710fn generate_jargon_examples(count: usize) -> Vec<AnnotatedExample> {
711    let templates = [
712        // Medical jargon - entities hard to distinguish from regular words
713        ("The patient has COVID-19.", vec![]), // COVID-19 may be disease, not organization
714        (
715            "Pfizer developed the vaccine.",
716            vec![("Pfizer", EntityType::Organization, 0)],
717        ),
718        // Legal jargon
719        ("Per Brown v. Board of Education.", vec![]), // Case names are complex
720        (
721            "The defendant, John Smith, pleaded.",
722            vec![("John Smith", EntityType::Person, 15)],
723        ),
724        // Tech jargon - product names vs companies
725        ("Install Python 3.10.", vec![]), // Python is language, not entity
726        (
727            "Microsoft released Windows 11.",
728            vec![("Microsoft", EntityType::Organization, 0)],
729        ),
730        ("The React library is popular.", vec![]), // React is library
731        (
732            "Facebook created React.",
733            vec![("Facebook", EntityType::Organization, 0)],
734        ),
735        // Financial jargon
736        ("S&P 500 rose 2%.", vec![("2%", EntityType::Percent, 13)]), // S&P 500 is index
737        ("NASDAQ hit record highs.", vec![]),                        // NASDAQ is index
738        (
739            "Goldman Sachs upgraded the stock.",
740            vec![("Goldman Sachs", EntityType::Organization, 0)],
741        ),
742        // Sports jargon
743        (
744            "The Lakers beat the Celtics 110-105.",
745            vec![
746                ("Lakers", EntityType::Organization, 4),
747                ("Celtics", EntityType::Organization, 20),
748            ],
749        ),
750        (
751            "LeBron James scored 30 points.",
752            vec![("LeBron James", EntityType::Person, 0)],
753        ),
754    ];
755
756    generate_from_templates(&templates, count, Domain::Technical, Difficulty::Hard)
757}
758
759// ============================================================================
760// Helper Functions
761// ============================================================================
762
763#[allow(clippy::type_complexity)]
764fn generate_from_templates(
765    templates: &[(&str, Vec<(&str, EntityType, usize)>)],
766    count: usize,
767    domain: Domain,
768    difficulty: Difficulty,
769) -> Vec<AnnotatedExample> {
770    let mut examples = Vec::with_capacity(count);
771
772    for (text, entity_specs) in templates.iter().cycle().take(count.max(templates.len())) {
773        let entities: Vec<GoldEntity> = entity_specs
774            .iter()
775            .map(|(txt, entity_type, start)| GoldEntity::new(*txt, entity_type.clone(), *start))
776            .collect();
777
778        examples.push(AnnotatedExample {
779            text: (*text).to_string(),
780            entities,
781            domain,
782            difficulty,
783        });
784    }
785
786    examples
787}
788
789/// Statistics about a benchmark dataset.
790#[derive(Debug, Clone)]
791pub struct BenchmarkStats {
792    /// Total number of examples in the dataset.
793    pub total_examples: usize,
794    /// Total number of entities across all examples.
795    pub total_entities: usize,
796    /// Average entities per example.
797    pub avg_entities_per_example: f64,
798    /// Number of examples with no entities (negative examples).
799    pub examples_with_no_entities: usize,
800    /// Distribution of edge case types (if tracked during generation).
801    pub edge_case_distribution: Vec<(EdgeCaseType, usize)>,
802}
803
804impl BenchmarkStats {
805    /// Calculate stats from a dataset
806    pub fn from_dataset(examples: &[AnnotatedExample]) -> Self {
807        let total_examples = examples.len();
808        let total_entities: usize = examples.iter().map(|e| e.entities.len()).sum();
809        let examples_with_no_entities = examples.iter().filter(|e| e.entities.is_empty()).count();
810
811        Self {
812            total_examples,
813            total_entities,
814            avg_entities_per_example: total_entities as f64 / total_examples.max(1) as f64,
815            examples_with_no_entities,
816            edge_case_distribution: vec![], // Would need tracking during generation
817        }
818    }
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824
825    #[test]
826    fn test_generate_large_dataset() {
827        let dataset = generate_large_dataset(100, EdgeCaseType::All);
828        assert!(
829            dataset.len() >= 100,
830            "Should generate at least 100 examples"
831        );
832    }
833
834    #[test]
835    fn test_ambiguous_examples() {
836        let examples = generate_ambiguous_examples(10);
837        assert!(!examples.is_empty());
838        // Verify some examples have no entities (genuinely ambiguous)
839        let no_entity_count = examples.iter().filter(|e| e.entities.is_empty()).count();
840        assert!(
841            no_entity_count > 0,
842            "Should have some ambiguous (no entity) cases"
843        );
844    }
845
846    #[test]
847    fn test_unicode_examples() {
848        let examples = generate_unicode_examples(10);
849        assert!(!examples.is_empty());
850        // Verify we have non-ASCII text
851        let has_unicode = examples.iter().any(|e| !e.text.is_ascii());
852        assert!(has_unicode, "Should have non-ASCII characters");
853    }
854
855    #[test]
856    fn test_entity_offsets_valid() {
857        let dataset = generate_large_dataset(500, EdgeCaseType::All);
858        for example in &dataset {
859            for entity in &example.entities {
860                assert!(
861                    entity.start < example.text.len(),
862                    "Entity '{}' start {} >= text length {} in: {}",
863                    entity.text,
864                    entity.start,
865                    example.text.len(),
866                    example.text
867                );
868                // Note: Using chars().count() for Unicode-safe comparison
869                let char_count = example.text.chars().count();
870                assert!(
871                    entity.end <= char_count,
872                    "Entity '{}' end {} > char count {} in: {}",
873                    entity.text,
874                    entity.end,
875                    char_count,
876                    example.text
877                );
878            }
879        }
880    }
881
882    #[test]
883    fn test_benchmark_stats() {
884        let dataset = generate_large_dataset(100, EdgeCaseType::All);
885        let stats = BenchmarkStats::from_dataset(&dataset);
886        assert!(stats.total_examples >= 100);
887        assert!(stats.total_entities > 0);
888    }
889
890    // Slow test - generates many examples
891    #[test]
892    #[ignore] // Run with: cargo test -- --ignored
893    fn test_large_scale_benchmark() {
894        let dataset = generate_large_dataset(10_000, EdgeCaseType::All);
895        assert!(dataset.len() >= 10_000);
896        let stats = BenchmarkStats::from_dataset(&dataset);
897        println!("Large benchmark stats: {:?}", stats);
898    }
899}