materials_for_mc 0.2.0

Material builder for Monte Carlo simulations
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
import pytest
from materials_for_mc import Nuclide

def test_be9_not_fissionable():
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    assert hasattr(nuc, 'fissionable'), "Nuclide should have a 'fissionable' attribute"
    assert nuc.fissionable is False, "Be9 should not be fissionable"

def test_fe58_not_fissionable():
    nuc = Nuclide('Fe58')
    nuc.read_nuclide_from_json('tests/Fe58.json')
    assert hasattr(nuc, 'fissionable'), "Nuclide should have a 'fissionable' attribute"
    assert nuc.fissionable is False, "Fe58 should not be fissionable"
from materials_for_mc import Nuclide

def test_read_li6_nuclide():
    nuc1 = Nuclide('Li6')
    nuc1.read_nuclide_from_json('tests/Li6.json')
    assert nuc1.element.lower() == 'lithium'
    assert nuc1.atomic_symbol == "Li"
    assert nuc1.atomic_number == 3
    assert nuc1.mass_number == 6
    assert nuc1.neutron_number == 3
    assert nuc1.available_temperatures == ['294']
    # We don't expect any specific order of MT numbers, just check they're all ints
    assert all(isinstance(mt, int) for mt in nuc1.reaction_mts)

    cs = nuc1.reactions['294'][2]['cross_section']

    for entry in cs:
        assert isinstance(entry, float)
        assert isinstance(entry, float)

    expected_li6 = [1, 2, 3, 4, 5, 24, 27, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 101, 102, 103, 105, 203, 204, 205, 206, 207, 301, 444]
    assert nuc1.reaction_mts == expected_li6

def test_read_li7_nuclide():
    nuc1 = Nuclide('Li7')
    nuc1.read_nuclide_from_json('tests/Li7.json')
    assert nuc1.element.lower() == 'lithium'
    assert nuc1.atomic_symbol == "Li"
    assert nuc1.atomic_number == 3
    assert nuc1.mass_number == 7
    assert nuc1.neutron_number == 4
    assert nuc1.available_temperatures == ['294']
    # We don't expect any specific order of MT numbers, just check they're all ints
    assert all(isinstance(mt, int) for mt in nuc1.reaction_mts)

    cs = nuc1.reactions['294'][2]['cross_section']
    
    for entry in cs:
        assert isinstance(entry, float)
        assert isinstance(entry, float)

    expected_li7 = [1, 2, 3, 4, 5, 16, 24, 25, 27, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 101, 102, 104, 203, 204, 205, 206, 207, 301, 444]
    assert nuc1.reaction_mts == expected_li7


def test_read_be9_available_and_loaded_temperatures():
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    assert nuc.available_temperatures == ['294', '300']
    # By current implementation, all temps are loaded eagerly
    assert hasattr(nuc, 'loaded_temperatures'), "loaded_temperatures attribute missing"
    assert nuc.loaded_temperatures == ['294', '300']
    # Reactions dict should contain both temperatures
    assert '294' in nuc.reactions
    assert '300' in nuc.reactions


def test_read_be9_mt_numbers_per_temperature():
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    mts_294 = sorted(int(mt) for mt in nuc.reactions['294'].keys())
    mts_300 = sorted(int(mt) for mt in nuc.reactions['300'].keys())
    expected_294 = sorted([
        1,2,3,16,27,101,102,103,104,105,107,203,204,205,207,301,444,
        875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890
    ])
    expected_300 = sorted([
        1,2,3,16,27,101,102,103,104,105,107,203,204,205,207,301
    ])
    assert mts_294 == expected_294, f"Be9 294K MT list mismatch: {mts_294}"
    assert mts_300 == expected_300, f"Be9 300K MT list mismatch: {mts_300}"
    # Ensure 300K list is subset of 294K list
    assert set(mts_300).issubset(set(mts_294))


def test_read_be9_selective_single_temperature():
    # Ensure only the specified temperature (300) is retained in reactions and loaded_temperatures
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json', temperatures=['300'])
    assert nuc.available_temperatures == ['294', '300'], "available_temperatures should list all temps present in file"
    assert nuc.loaded_temperatures == ['300'], f"loaded_temperatures should be only ['300'], got {nuc.loaded_temperatures}"
    assert '300' in nuc.reactions, "300K reactions missing after selective load"
    assert '294' not in nuc.reactions, "294K reactions should not be loaded when selectively requesting only 300K"
    # MT list at 300 should match subset expectation
    mts_300 = sorted(int(mt) for mt in nuc.reactions['300'].keys())
    expected_300 = sorted([
        1,2,3,16,27,101,102,103,104,105,107,203,204,205,207,301
    ])
    assert mts_300 == expected_300, f"Selective load 300K MT list mismatch: {mts_300}"


def test_read_nuclide_from_json_keyword():
    from materials_for_mc import Nuclide
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json("tendl-21")

def test_read_nuclide_from_json_local_path():
    from materials_for_mc import Nuclide
    nuc = Nuclide('Li6')
    # Should not raise TypeError when passing local path
    nuc.read_nuclide_from_json("tests/Li6.json")


def test_microscopic_cross_section_with_temperature():
    """Test microscopic_cross_section with explicit temperature."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Test with specific temperature
    xs, energy = nuc.microscopic_cross_section(reaction=2, temperature='294')
    assert len(xs) > 0, "Cross section data should not be empty"
    assert len(energy) > 0, "Energy data should not be empty"
    assert len(xs) == len(energy), "Cross section and energy arrays should have same length"
    
    # Test with different temperature
    xs_300, energy_300 = nuc.microscopic_cross_section(reaction=2, temperature='300')
    assert len(xs_300) > 0, "Cross section data should not be empty for 300K"
    assert len(energy_300) > 0, "Energy data should not be empty for 300K"
    
    # Test different MT numbers
    xs_mt3, energy_mt3 = nuc.microscopic_cross_section(reaction=3, temperature='294')
    assert len(xs_mt3) > 0, "MT=3 cross section data should not be empty"
    assert len(energy_mt3) > 0, "MT=3 energy data should not be empty"


def test_microscopic_cross_section_without_temperature():
    """Test microscopic_cross_section with single loaded temperature."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    # Load only one temperature
    nuc.read_nuclide_from_json('tests/Be9.json', temperatures=['294'])
    
    # Should work without specifying temperature since only one is loaded
    xs, energy = nuc.microscopic_cross_section(2)
    assert len(xs) > 0, "Cross section data should not be empty"
    assert len(energy) > 0, "Energy data should not be empty"
    assert len(xs) == len(energy), "Cross section and energy arrays should have same length"


def test_microscopic_cross_section_multiple_temperatures_error():
    """Test that microscopic_cross_section raises error when multiple temperatures loaded without specifying."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')  # Loads both 294 and 300
    
    # Should raise error when no temperature specified with multiple loaded
    with pytest.raises(Exception) as exc_info:
        nuc.microscopic_cross_section(2)
    error_msg = str(exc_info.value)
    assert "Multiple temperatures loaded" in error_msg
    assert "294" in error_msg and "300" in error_msg


def test_microscopic_cross_section_invalid_temperature():
    """Test error handling for invalid temperature."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Should raise error for non-existent temperature
    with pytest.raises(Exception) as exc_info:
        nuc.microscopic_cross_section(reaction=2, temperature='500')
    error_msg = str(exc_info.value)
    assert "Temperature '500' not found" in error_msg
    assert "Available temperatures:" in error_msg
    assert "294" in error_msg and "300" in error_msg


def test_microscopic_cross_section_invalid_mt():
    """Test error handling for invalid MT number."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Should raise error for non-existent MT
    with pytest.raises(Exception) as exc_info:
        nuc.microscopic_cross_section(reaction=9999, temperature='294')
    error_msg = str(exc_info.value)
    assert "MT 9999 not found" in error_msg
    assert "Available MTs:" in error_msg


def test_microscopic_cross_section_multiple_mt_numbers():
    """Test microscopic_cross_section with various MT numbers."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Test common MT numbers that should exist in Be9
    test_mts = [1, 2, 3, 16, 27, 101, 102]  # Common reaction types
    
    for mt in test_mts:
        try:
            xs, energy = nuc.microscopic_cross_section(reaction=mt, temperature='294')
            assert len(xs) > 0, f"MT={mt} should have cross section data"
            assert len(energy) > 0, f"MT={mt} should have energy data"
            assert len(xs) == len(energy), f"MT={mt} data length mismatch"
            assert all(e > 0 for e in energy), f"MT={mt} energy values should be positive"
            assert all(x >= 0 for x in xs), f"MT={mt} cross sections should be non-negative"
        except Exception:
            # Some MT numbers might not exist, which is fine
            pass


def test_microscopic_cross_section_lithium():
    """Test microscopic_cross_section with Li6 data."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Li6 should have only one temperature, so no temperature needed
    xs, energy = nuc.microscopic_cross_section(reaction=2)  # Elastic scattering
    assert len(xs) > 0, "Li6 elastic scattering data should not be empty"
    assert len(energy) > 0, "Li6 energy data should not be empty"
    
    # Test with explicit temperature too
    xs_explicit, energy_explicit = nuc.microscopic_cross_section(reaction=2, temperature='294')
    assert xs == xs_explicit, "Results should be identical with/without explicit temperature"
    assert energy == energy_explicit, "Energy should be identical with/without explicit temperature"


def test_auto_loading_from_config():
    """Test that microscopic_cross_section can auto-load data from config when nuclide is empty"""
    from materials_for_mc import Config
    
    # Set up config for auto-loading
    config = Config()
    config.set_cross_sections({'Be9': 'tests/Be9.json'})
    
    # Create empty nuclide with name but no data loaded
    nuc = Nuclide('Be9')
    assert nuc.loaded_temperatures == [], "Should start with no loaded temperatures"
    
    # Call microscopic_cross_section - should auto-load data
    xs, energy = nuc.microscopic_cross_section(reaction=2, temperature='294')
    assert len(xs) > 0, "Auto-loaded cross section data should not be empty"
    assert len(energy) > 0, "Auto-loaded energy data should not be empty"
    assert len(xs) == len(energy), "Cross section and energy arrays should have same length"
    
    # Note: loaded_temperatures won't be updated in the Python object due to immutable API
    # The auto-loading happens internally but doesn't modify the original object


def test_auto_loading_additional_temperature():
    """Test that microscopic_cross_section can auto-load additional temperatures"""
    from materials_for_mc import Config
    
    # Set up config for auto-loading
    config = Config()
    config.set_cross_sections({'Be9': 'tests/Be9.json'})
    
    # Load Be9 with only 294K initially
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json', ['294'])
    
    assert nuc.loaded_temperatures == ['294'], "Should only have 294K loaded initially"
    assert '300' in nuc.available_temperatures, "Should know 300K is available"
    
    # Request 300K data - should auto-load additional temperature
    xs, energy = nuc.microscopic_cross_section(reaction=2, temperature='300')
    assert len(xs) > 0, "Auto-loaded 300K cross section data should not be empty"
    assert len(energy) > 0, "Auto-loaded 300K energy data should not be empty"
    
    # The original nuclide object still shows only 294K due to immutable API
    # But the internal auto-loading worked to provide the 300K data
    

def test_auto_loading_without_config_fails():
    """Test that auto-loading fails gracefully when no config is available"""
    from materials_for_mc import Config
    
    # Clear any existing config by setting an empty dict
    config = Config()
    original_configs = config.get_cross_sections()
    
    # Temporarily clear all configurations
    config.set_cross_sections({})
    
    try:
        # Create empty nuclide with name but no config
        nuc = Nuclide('TestNuclide')
        
        # Call microscopic_cross_section - should fail with helpful error
        try:
            nuc.microscopic_cross_section(reaction=2, temperature='294')
            assert False, "Auto-loading without config should fail"
        except Exception as e:
            error_msg = str(e)
            # The error could be either "No configuration found" or a download error
            # Both are acceptable since there's no valid config
            assert ("No configuration found" in error_msg or 
                    "Failed to download" in error_msg or
                    "404 Not Found" in error_msg), f"Error should indicate missing or invalid configuration: {error_msg}"
            assert "TestNuclide" in error_msg or "TestNuclide" in str(e.__class__), f"Error should be related to TestNuclide: {error_msg}"
    
    finally:
        # Restore original configuration
        if original_configs:
            config.set_cross_sections(original_configs)


def test_auto_loading_multiple_calls_consistent():
    """Test that multiple auto-loading calls give consistent results"""
    from materials_for_mc import Config
    
    # Set up config for auto-loading
    config = Config()
    config.set_cross_sections({'Be9': 'tests/Be9.json'})
    
    # Create empty nuclide
    nuc = Nuclide('Be9')
    
    # Call microscopic_cross_section multiple times
    xs1, energy1 = nuc.microscopic_cross_section(reaction=2, temperature='294')
    xs2, energy2 = nuc.microscopic_cross_section(reaction=2, temperature='294')
    xs3, energy3 = nuc.microscopic_cross_section(reaction=102, temperature='300')
    
    # First two calls should give identical results
    assert xs1 == xs2, "Multiple calls with same parameters should give identical results"
    assert energy1 == energy2, "Multiple calls with same parameters should give identical energy"
    
    # Third call should work too (different MT and temperature)
    assert len(xs3) > 0, "Auto-loading different MT and temperature should work"
    assert len(energy3) > 0, "Auto-loading different MT and temperature should provide energy"


def test_auto_loading_with_manual_loading_combined():
    """Test combining manual loading with auto-loading for additional data"""
    from materials_for_mc import Config
    
    # Set up config for auto-loading
    config = Config()
    config.set_cross_sections({'Be9': 'tests/Be9.json'})
    
    # Manually load some data first
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json', ['294'])
    
    # Verify manual loading worked
    assert '294' in nuc.loaded_temperatures, "Manual loading should work"
    assert '300' in nuc.available_temperatures, "Should know other temperatures are available"
    
    # Now use auto-loading for data that was manually loaded
    xs_manual, energy_manual = nuc.microscopic_cross_section(reaction=2, temperature='294')
    assert len(xs_manual) > 0, "Should get data for manually loaded temperature"
    
    # And use auto-loading for additional temperature not manually loaded  
    xs_auto, energy_auto = nuc.microscopic_cross_section(reaction=2, temperature='300')
    assert len(xs_auto) > 0, "Should auto-load additional temperature"
    
    # Test with a temperature-specific MT - try MT=444 which should only be available at 294K
    xs_specific, energy_specific = nuc.microscopic_cross_section(reaction=444, temperature='294')
    assert len(xs_specific) > 0, "Should get temperature-specific MT data"
    
    # Try to get MT=444 at 300K - this should fail since it's not available at that temperature
    try:
        nuc.microscopic_cross_section(reaction=444, temperature='300')
        # If we get here without exception, that's fine too - means 444 exists at both temps
        print("MT=444 exists at both temperatures")
    except ValueError as e:
        # This is expected if MT=444 is not available at 300K
        assert "MT 444 not found" in str(e), f"Should get MT not found error, got: {e}"
    
    # Note: For Be9 MT=2, the cross sections at 294K and 300K might be identical
    # This is fine - the important thing is that both calls succeeded


def test_fendl_3_2c_keyword():
    """Test that the fendl-3.2c keyword is recognized and works correctly."""
    import materials_for_mc
    
    # Test that the keyword is recognized (this tests the Rust backend)
    try:
        # This should not raise an exception if the keyword is recognized
        # We'll create a dummy config entry to test keyword recognition
        from materials_for_mc import Config
        config = Config()
        
        # Test setting cross sections with the keyword - this should not fail
        # if the keyword is recognized in the backend
        config.set_cross_sections({'Li6': 'fendl-3.2c'})
        
        # Verify we can retrieve it
        cross_sections = config.get_cross_sections()
        assert 'Li6' in cross_sections, "Li6 should be in cross sections config"
        assert cross_sections['Li6'] == 'fendl-3.2c', "Should store fendl-3.2c keyword correctly"
        
        # Test that keyword expansion would work (without actually downloading)
        # This implicitly tests the URL cache functionality  
        print("fendl-3.2c keyword test passed - keyword is recognized")
        
    except Exception as e:
        pytest.fail(f"fendl-3.2c keyword should be recognized by the system: {e}")


def test_auto_loading_with_global_keyword():
    """Test that auto-loading works with global keyword configuration"""
    from materials_for_mc import Config, Nuclide
    
    # Set global keyword configuration
    config = Config()
    config.set_cross_sections('fendl-3.2c')
    
    # Verify config is set correctly
    assert config.get_cross_section('Li6') == 'fendl-3.2c', "Global config should apply to Li6"
    
    # Create empty nuclide
    nuc = Nuclide('Li6')
    assert nuc.loaded_temperatures == [], "Should start with no loaded temperatures"
    
    # Call microscopic_cross_section - should auto-load data from global config
    try:
        xs, energy = nuc.microscopic_cross_section(reaction=1, temperature='294')
        assert len(xs) > 0, "Auto-loaded cross section data should not be empty"
        assert len(energy) > 0, "Auto-loaded energy data should not be empty"
        assert len(xs) == len(energy), "Cross section and energy arrays should have same length"
        print("Auto-loading with global keyword test passed!")
        
    except Exception as e:
        # If we can't download (no internet or URL issues), that's OK for this test
        # The important thing is that the config lookup worked
        if "No configuration found" in str(e):
            pytest.fail(f"Config lookup failed - auto-loading should work with global keywords: {e}")
        else:
            print(f"Note: Auto-loading test skipped due to download issue: {e}")
            # This is acceptable - we verified the config lookup works


def test_microscopic_cross_section_by_name():
    """Test microscopic_cross_section with reaction names."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Test elastic scattering using reaction name
    xs_name, energy_name = nuc.microscopic_cross_section("(n,elastic)", temperature='294')
    assert len(xs_name) > 0, "Elastic scattering cross section should not be empty"
    assert len(energy_name) > 0, "Energy data should not be empty"
    
    # Compare with MT number approach (MT=2 is elastic scattering)
    xs_mt, energy_mt = nuc.microscopic_cross_section(reaction=2, temperature='294')
    
    # Should get identical results
    assert xs_name == xs_mt, "Reaction name and MT number should give identical cross sections"
    assert energy_name == energy_mt, "Reaction name and MT number should give identical energy grids"
    
    # Test other common reactions
    test_reactions = [
        ("(n,gamma)", 102),   # Radiative capture
        ("(n,a)", 107),       # Alpha production
        ("(n,total)", 1),     # Total cross section
    ]
    
    for reaction_name, mt_num in test_reactions:
        try:
            xs_name, energy_name = nuc.microscopic_cross_section(reaction_name, temperature='294')
            xs_mt, energy_mt = nuc.microscopic_cross_section(reaction=mt_num, temperature='294')
            
            assert len(xs_name) > 0, f"{reaction_name} should have cross section data"
            assert len(energy_name) > 0, f"{reaction_name} should have energy data"
            assert xs_name == xs_mt, f"{reaction_name} and MT={mt_num} should give identical results"
            assert energy_name == energy_mt, f"{reaction_name} and MT={mt_num} should give identical energy"
            
        except Exception as e:
            # Some reactions might not exist for Be9, which is acceptable
            if "not found" in str(e).lower():
                print(f"Note: {reaction_name} (MT={mt_num}) not available in Be9 data")
            else:
                raise e


def test_microscopic_cross_section_by_name_invalid_reaction():
    """Test error handling for invalid reaction names."""
    from materials_for_mc import Nuclide
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Test with invalid reaction name
    with pytest.raises(Exception) as exc_info:
        nuc.microscopic_cross_section("invalid_reaction", temperature='294')
    error_msg = str(exc_info.value)
    assert "not found in reaction mapping" in error_msg or "Unknown reaction" in error_msg


def test_microscopic_cross_section_by_name_fission():
    """Test that the special 'fission' alias works."""
    from materials_for_mc import Nuclide
    
    # Use Li6 which might have fission data, or test the error handling
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    try:
        xs_fission, energy_fission = nuc.microscopic_cross_section("fission", temperature='294')
        xs_mt18, energy_mt18 = nuc.microscopic_cross_section(18, temperature='294')
        
        # Should get identical results since fission maps to MT=18
        assert xs_fission == xs_mt18, "fission and MT=18 should give identical results"
        assert energy_fission == energy_mt18, "fission and MT=18 should give identical energy"
        
    except Exception as e:
        # Li6 might not have fission data, which is acceptable
        if "MT 18 not found" in str(e) or "not found" in str(e).lower():
            print("Note: Li6 does not have fission data (expected)")
        else:
            raise e


def test_sample_reaction_basic():
    """Test basic functionality of sample_reaction method."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Test sampling at a typical neutron energy
    reaction = nuc.sample_reaction(energy=1.0, temperature='294', seed=42)
    
    if reaction is not None:
        # Check that all expected fields are present
        assert 'mt_number' in reaction, "Reaction should have mt_number field"
        assert 'cross_section' in reaction, "Reaction should have cross_section field"
        assert 'threshold_idx' in reaction, "Reaction should have threshold_idx field"
        assert 'interpolation' in reaction, "Reaction should have interpolation field"
        assert 'energy' in reaction, "Reaction should have energy field"
        
        # Check data types
        assert isinstance(reaction['mt_number'], int), "MT number should be integer"
        assert isinstance(reaction['cross_section'], list), "Cross section should be list"
        assert isinstance(reaction['energy'], list), "Energy should be list"
        assert isinstance(reaction['threshold_idx'], int), "Threshold index should be integer"
        assert isinstance(reaction['interpolation'], list), "Interpolation should be list"
        
        # Check data validity
        assert reaction['mt_number'] > 0, "MT number should be positive"
        assert len(reaction['cross_section']) > 0, "Cross section should not be empty"
        assert len(reaction['energy']) > 0, "Energy grid should not be empty"
        assert reaction['threshold_idx'] >= 0, "Threshold index should be non-negative"
        assert all(x >= 0 for x in reaction['cross_section']), "Cross sections should be non-negative"
        assert all(e > 0 for e in reaction['energy']), "Energy values should be positive"
    else:
        # If no reaction is sampled, that's also valid (zero total cross section)
        print("Note: No reaction sampled (possibly zero total cross section)")


def test_sample_reaction_reproducibility():
    """Test that sample_reaction gives reproducible results with same seed."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Sample with same seed multiple times
    reaction1 = nuc.sample_reaction(energy=1.0, temperature='294', seed=123)
    reaction2 = nuc.sample_reaction(energy=1.0, temperature='294', seed=123)
    reaction3 = nuc.sample_reaction(energy=1.0, temperature='294', seed=456)
    
    if reaction1 is not None and reaction2 is not None:
        # Same seed should give same result
        assert reaction1['mt_number'] == reaction2['mt_number'], "Same seed should give same MT number"
        assert reaction1['cross_section'] == reaction2['cross_section'], "Same seed should give same cross section data"
        assert reaction1['energy'] == reaction2['energy'], "Same seed should give same energy data"
        
        # Different seed might give different result (but not guaranteed)
        if reaction3 is not None:
            # We can't guarantee different results, but at least verify the structure is correct
            assert isinstance(reaction3['mt_number'], int), "Different seed should still give valid MT number"
    else:
        print("Note: No reaction sampled in reproducibility test")


def test_sample_reaction_different_energies():
    """Test sample_reaction at different neutron energies."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Test various energies from thermal to fast neutron range
    test_energies = [1e-3, 1e-2, 1e-1, 1.0, 10.0, 100.0, 1000.0]
    sampled_mts = set()
    
    for energy in test_energies:
        reaction = nuc.sample_reaction(energy=energy, temperature='294', seed=42)
        
        if reaction is not None:
            sampled_mts.add(reaction['mt_number'])
            
            # Verify reaction structure at each energy
            assert reaction['mt_number'] > 0, f"Valid MT number at energy {energy}"
            assert len(reaction['cross_section']) > 0, f"Non-empty cross section at energy {energy}"
            assert len(reaction['energy']) > 0, f"Non-empty energy grid at energy {energy}"
    
    # Should sample at least one reaction across all energies (unless total XS is zero everywhere)
    if len(sampled_mts) == 0:
        print("Warning: No reactions sampled across all test energies")
    else:
        print(f"Sampled {len(sampled_mts)} different reaction types across energy range")


def test_sample_reaction_be9():
    """Test sample_reaction with Be9 nuclide (different reaction channels)."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')
    
    # Be9 has different reaction channels than Li6
    reaction = nuc.sample_reaction(energy=1.0, temperature='294', seed=42)
    
    if reaction is not None:
        # Verify structure
        assert 'mt_number' in reaction, "Be9 reaction should have mt_number field"
        assert isinstance(reaction['mt_number'], int), "Be9 MT number should be integer"
        assert reaction['mt_number'] > 0, "Be9 MT number should be positive"
        
        # Check that MT number is valid for Be9
        assert reaction['mt_number'] in nuc.reaction_mts, f"MT {reaction['mt_number']} should be in available MTs for Be9"
        
        print(f"Be9 sampled reaction: MT {reaction['mt_number']}")
    else:
        print("Note: No Be9 reaction sampled")


def test_sample_reaction_without_seed():
    """Test that sample_reaction works without specifying a seed."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Sample without seed (should use system random state)
    reaction1 = nuc.sample_reaction(energy=1.0, temperature='294')
    reaction2 = nuc.sample_reaction(energy=1.0, temperature='294')
    
    # Both should work (might be same or different)
    if reaction1 is not None:
        assert isinstance(reaction1['mt_number'], int), "No-seed reaction 1 should be valid"
    
    if reaction2 is not None:
        assert isinstance(reaction2['mt_number'], int), "No-seed reaction 2 should be valid"
    
    print("No-seed sampling test completed")


def test_sample_reaction_multiple_temperatures():
    """Test sample_reaction with different temperatures."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Be9')
    nuc.read_nuclide_from_json('tests/Be9.json')  # Has both 294K and 300K
    
    # Sample at both temperatures
    reaction_294 = nuc.sample_reaction(energy=1.0, temperature='294', seed=42)
    reaction_300 = nuc.sample_reaction(energy=1.0, temperature='300', seed=42)
    
    # Both should work
    if reaction_294 is not None:
        assert isinstance(reaction_294['mt_number'], int), "294K reaction should be valid"
        
    if reaction_300 is not None:
        assert isinstance(reaction_300['mt_number'], int), "300K reaction should be valid"
    
    # Results might be different due to temperature-dependent cross sections
    # but we can't guarantee this, so just verify they both work
    print("Multiple temperature sampling test completed")


def test_sample_reaction_invalid_temperature():
    """Test error handling for invalid temperature in sample_reaction."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Test with invalid temperature - should handle gracefully
    # The current implementation uses fallback behavior rather than strict errors
    reaction = nuc.sample_reaction(energy=1.0, temperature='999', seed=42)
    
    # The method should either work (using fallback) or return None
    if reaction is not None:
        assert isinstance(reaction['mt_number'], int), "Fallback temperature reaction should be valid"
        print("Invalid temperature handled with fallback")
    else:
        print("Invalid temperature resulted in no reaction sampled")


def test_sample_reaction_edge_cases():
    """Test sample_reaction with edge case energies."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Test very low energy
    reaction_low = nuc.sample_reaction(energy=1e-5, temperature='294', seed=42)
    
    # Test very high energy
    reaction_high = nuc.sample_reaction(energy=1e7, temperature='294', seed=42)
    
    # Both should either work or return None (no error)
    if reaction_low is not None:
        assert isinstance(reaction_low['mt_number'], int), "Very low energy should give valid reaction"
    
    if reaction_high is not None:
        assert isinstance(reaction_high['mt_number'], int), "Very high energy should give valid reaction"
    
    print("Edge case energy test completed")


def test_sample_reaction_zero_energy():
    """Test sample_reaction with zero energy."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    # Test with zero energy (edge case)
    reaction = nuc.sample_reaction(energy=0.0, temperature='294', seed=42)
    
    # Should either work or return None, not crash
    if reaction is not None:
        assert isinstance(reaction['mt_number'], int), "Zero energy should give valid reaction if any"
    
    print("Zero energy test completed")


def test_sample_reaction_consistency_with_available_mts():
    """Test that sampled reactions are from available MT numbers."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    available_mts = set(nuc.reaction_mts)
    sampled_mts = set()
    
    # Sample many times to get a good distribution
    for i in range(100):
        reaction = nuc.sample_reaction(energy=1.0, temperature='294', seed=i)
        if reaction is not None:
            sampled_mts.add(reaction['mt_number'])
    
    # All sampled MTs should be in the available set
    for mt in sampled_mts:
        assert mt in available_mts, f"Sampled MT {mt} should be in available MTs {available_mts}"
    
    if sampled_mts:
        print(f"Sampled MTs {sorted(sampled_mts)} are all in available MTs {sorted(available_mts)}")
    else:
        print("No reactions sampled in consistency test")


def test_sample_reaction_return_type():
    """Test that sample_reaction returns correct Python types."""
    from materials_for_mc import Nuclide
    
    nuc = Nuclide('Li6')
    nuc.read_nuclide_from_json('tests/Li6.json')
    
    reaction = nuc.sample_reaction(energy=1.0, temperature='294', seed=42)
    
    if reaction is not None:
        # Check that it's a dictionary
        assert isinstance(reaction, dict), "Reaction should be a dictionary"
        
        # Check specific field types
        assert isinstance(reaction['mt_number'], int), "mt_number should be int"
        assert isinstance(reaction['cross_section'], list), "cross_section should be list"
        assert isinstance(reaction['energy'], list), "energy should be list"
        assert isinstance(reaction['threshold_idx'], int), "threshold_idx should be int"
        assert isinstance(reaction['interpolation'], list), "interpolation should be list"
        
        # Check list element types
        if reaction['cross_section']:
            assert isinstance(reaction['cross_section'][0], float), "Cross section values should be float"
        
        if reaction['energy']:
            assert isinstance(reaction['energy'][0], float), "Energy values should be float"
        
        if reaction['interpolation']:
            assert isinstance(reaction['interpolation'][0], int), "Interpolation values should be int"
    
    else:
        # None is also a valid return type
        assert reaction is None, "Return should be None or dict"
    
    print("Return type test completed")


def test_nuclide_different_data_sources():
    """Test that loading the same nuclide from different sources gives different results."""
    
    # Load Li7 from different sources
    li7_tendl = Nuclide("Li7")
    li7_tendl.read_nuclide_from_json("tendl-21")
    
    li7_fendl = Nuclide("Li7")
    li7_fendl.read_nuclide_from_json("fendl-3.2c")
    
    # Get cross sections from both
    xs_tendl, energy_tendl = li7_tendl.microscopic_cross_section("(n,gamma)")
    xs_fendl, energy_fendl = li7_fendl.microscopic_cross_section("(n,gamma)")
    
    # Should have different data
    data_different = (len(xs_tendl) != len(xs_fendl) or 
                     any(abs(a - b) > 1e-10 for a, b in zip(xs_tendl, xs_fendl)))
    
    assert data_different, "TENDL and FENDL data should be different"
    print(f"TENDL Li7: {len(xs_tendl)} points, FENDL Li7: {len(xs_fendl)} points")


def test_nuclide_file_vs_keyword_sources():
    """Test that file paths and keywords can coexist."""
    
    # Load Li6 from local file
    li6_file = Nuclide("Li6")
    li6_file.read_nuclide_from_json("tests/Li6.json")
    
    # Load Li7 from keyword
    li7_keyword = Nuclide("Li7")
    li7_keyword.read_nuclide_from_json("tendl-21")
    
    assert li6_file.name == "Li6"
    assert li7_keyword.name == "Li7"
    
    # Verify we can load cross sections from both
    xs_file, _ = li6_file.microscopic_cross_section("(n,gamma)")
    xs_keyword, _ = li7_keyword.microscopic_cross_section("(n,gamma)")
    
    assert len(xs_file) > 0 and len(xs_keyword) > 0, "Both should have cross section data"


def test_nuclide_cache_respects_data_source_boundaries():
    """Test that the cache properly separates different data sources."""
    
    # Load Li7 from TENDL first time
    li7_tendl_1 = Nuclide("Li7")
    li7_tendl_1.read_nuclide_from_json("tendl-21")
    
    # Load Li7 from FENDL
    li7_fendl = Nuclide("Li7")
    li7_fendl.read_nuclide_from_json("fendl-3.2c")
    
    # Load Li7 from TENDL again (should use cache)
    li7_tendl_2 = Nuclide("Li7")
    li7_tendl_2.read_nuclide_from_json("tendl-21")
    
    # Get cross sections
    xs_tendl_1, _ = li7_tendl_1.microscopic_cross_section("(n,gamma)")
    xs_fendl, _ = li7_fendl.microscopic_cross_section("(n,gamma)")
    xs_tendl_2, _ = li7_tendl_2.microscopic_cross_section("(n,gamma)")
    
    # TENDL loads should be identical (cache working)
    assert xs_tendl_1 == xs_tendl_2, "TENDL loads should be identical (cache working)"
    
    # TENDL vs FENDL should be different (different data sources)
    tendl_vs_fendl_different = (len(xs_tendl_1) != len(xs_fendl) or 
                               any(abs(a - b) > 1e-10 for a, b in zip(xs_tendl_1, xs_fendl)))
    assert tendl_vs_fendl_different, "TENDL and FENDL should have different data"


def test_nuclide_path_normalization():
    """Test that different path formats for same file give same results."""
    import os
    
    # Load Li6 with relative path
    li6_rel = Nuclide("Li6")
    li6_rel.read_nuclide_from_json("tests/Li6.json")
    
    # Load Li6 with absolute path
    li6_abs = Nuclide("Li6")
    abs_path = os.path.abspath("tests/Li6.json")
    li6_abs.read_nuclide_from_json(abs_path)
    
    # Should give identical results (same file)
    xs_rel, _ = li6_rel.microscopic_cross_section("(n,gamma)")
    xs_abs, _ = li6_abs.microscopic_cross_section("(n,gamma)")
    
    assert xs_rel == xs_abs, "Relative and absolute paths to same file should give identical results"