|
18 | 18 | from pathlib import Path |
19 | 19 | from typing import Generator |
20 | 20 |
|
| 21 | +import numpy as np |
21 | 22 | import pytest |
22 | 23 |
|
23 | 24 | from speciesnet.utils import file_exists |
| 25 | +from speciesnet.utils import limit_float_precision |
| 26 | +from speciesnet.utils import load_json |
24 | 27 | from speciesnet.utils import load_partial_predictions |
25 | 28 | from speciesnet.utils import load_rgb_image |
26 | 29 | from speciesnet.utils import ModelInfo |
27 | 30 | from speciesnet.utils import prepare_instances_dict |
28 | 31 | from speciesnet.utils import save_predictions |
| 32 | +from speciesnet.utils import write_json |
29 | 33 |
|
30 | 34 | # fmt: off |
31 | 35 | # pylint: disable=line-too-long |
@@ -514,3 +518,168 @@ def test_failed_saving(self, tmp_path) -> None: |
514 | 518 | } |
515 | 519 | with pytest.raises(TypeError): |
516 | 520 | save_predictions(predictions, tmp_path) |
| 521 | + |
| 522 | + |
| 523 | +class TestPrecisionLimiting: |
| 524 | + """Tests for precision limiting functionality in JSON operations.""" |
| 525 | + |
| 526 | + def test_limit_float_precision_simple_float(self) -> None: |
| 527 | + """Test precision limiting for simple floats.""" |
| 528 | + assert limit_float_precision(3.14159265359, 2) == 3.14 |
| 529 | + assert limit_float_precision(3.14159265359, 4) == 3.1416 |
| 530 | + assert limit_float_precision(2.0, 2) == 2.0 |
| 531 | + |
| 532 | + def test_limit_float_precision_numpy_float(self) -> None: |
| 533 | + """Test precision limiting for numpy floats.""" |
| 534 | + assert limit_float_precision(np.float32(3.14159265359), 2) == 3.14 |
| 535 | + assert limit_float_precision(np.float64(3.14159265359), 4) == 3.1416 |
| 536 | + assert limit_float_precision(np.float16(2.0), 2) == 2.0 |
| 537 | + |
| 538 | + def test_limit_float_precision_non_float_types(self) -> None: |
| 539 | + """Test that non-float types are unchanged.""" |
| 540 | + assert limit_float_precision("string", 2) == "string" |
| 541 | + assert limit_float_precision(42, 2) == 42 |
| 542 | + assert limit_float_precision(True, 2) is True |
| 543 | + assert limit_float_precision(None, 2) is None |
| 544 | + |
| 545 | + def test_limit_float_precision_simple_list(self) -> None: |
| 546 | + """Test precision limiting for lists with floats.""" |
| 547 | + input_list = [1.23456, 2.0, "string", 42, 9.87654] |
| 548 | + expected = [1.23, 2.0, "string", 42, 9.88] |
| 549 | + assert limit_float_precision(input_list, 2) == expected |
| 550 | + |
| 551 | + def test_limit_float_precision_simple_dict(self) -> None: |
| 552 | + """Test precision limiting for dictionaries with floats.""" |
| 553 | + input_dict = { |
| 554 | + "float_val": 3.14159, |
| 555 | + "string_val": "test", |
| 556 | + "int_val": 42, |
| 557 | + "another_float": 2.718281828, |
| 558 | + } |
| 559 | + expected = { |
| 560 | + "float_val": 3.14, |
| 561 | + "string_val": "test", |
| 562 | + "int_val": 42, |
| 563 | + "another_float": 2.72, |
| 564 | + } |
| 565 | + assert limit_float_precision(input_dict, 2) == expected |
| 566 | + |
| 567 | + def test_limit_float_precision_nested_dict_in_list(self) -> None: |
| 568 | + """Test precision limiting for dictionaries nested in lists.""" |
| 569 | + input_data = [ |
| 570 | + {"score": 0.123456, "name": "item1"}, |
| 571 | + {"score": 0.987654, "name": "item2"}, |
| 572 | + "string_item", |
| 573 | + 42, |
| 574 | + ] |
| 575 | + expected = [ |
| 576 | + {"score": 0.123, "name": "item1"}, |
| 577 | + {"score": 0.988, "name": "item2"}, |
| 578 | + "string_item", |
| 579 | + 42, |
| 580 | + ] |
| 581 | + assert limit_float_precision(input_data, 3) == expected |
| 582 | + |
| 583 | + def test_limit_float_precision_nested_list_in_dict(self) -> None: |
| 584 | + """Test precision limiting for lists nested in dictionaries.""" |
| 585 | + input_data = { |
| 586 | + "scores": [0.12345, 0.67890, 0.99999], |
| 587 | + "names": ["A", "B", "C"], |
| 588 | + "metadata": {"threshold": 0.54321, "version": "1.0"}, |
| 589 | + } |
| 590 | + expected = { |
| 591 | + "scores": [0.12, 0.68, 1.0], |
| 592 | + "names": ["A", "B", "C"], |
| 593 | + "metadata": {"threshold": 0.54, "version": "1.0"}, |
| 594 | + } |
| 595 | + assert limit_float_precision(input_data, 2) == expected |
| 596 | + |
| 597 | + def test_limit_float_precision_deeply_nested(self) -> None: |
| 598 | + """Test precision limiting for deeply nested structures.""" |
| 599 | + input_data = { |
| 600 | + "level1": { |
| 601 | + "level2": { |
| 602 | + "level3": [ |
| 603 | + {"deep_float": 3.14159265359, "items": [1.23456, 7.89012]}, |
| 604 | + {"another_deep": 2.71828, "values": [9.87654, 5.43210]}, |
| 605 | + ] |
| 606 | + } |
| 607 | + } |
| 608 | + } |
| 609 | + expected = { |
| 610 | + "level1": { |
| 611 | + "level2": { |
| 612 | + "level3": [ |
| 613 | + {"deep_float": 3.1416, "items": [1.2346, 7.8901]}, |
| 614 | + {"another_deep": 2.7183, "values": [9.8765, 5.4321]}, |
| 615 | + ] |
| 616 | + } |
| 617 | + } |
| 618 | + } |
| 619 | + assert limit_float_precision(input_data, 4) == expected |
| 620 | + |
| 621 | + def test_limit_float_precision_tuples(self) -> None: |
| 622 | + """Test precision limiting for tuples.""" |
| 623 | + input_tuple = (1.23456, "string", 7.89012, 42) |
| 624 | + expected = (1.23, "string", 7.89, 42) |
| 625 | + assert limit_float_precision(input_tuple, 2) == expected |
| 626 | + |
| 627 | + def test_limit_float_precision_mixed_numpy_types(self) -> None: |
| 628 | + """Test precision limiting with mixed numpy and Python floats.""" |
| 629 | + input_data = { |
| 630 | + "python_float": 3.14159, |
| 631 | + "numpy_float32": np.float32(2.71828), |
| 632 | + "numpy_float64": np.float64(1.41421), |
| 633 | + "list_mixed": [1.23456, np.float32(9.87654), "string", np.float64(5.55555)], |
| 634 | + } |
| 635 | + expected = { |
| 636 | + "python_float": 3.14, |
| 637 | + "numpy_float32": 2.72, |
| 638 | + "numpy_float64": 1.41, |
| 639 | + "list_mixed": [1.23, 9.88, "string", 5.56], |
| 640 | + } |
| 641 | + assert limit_float_precision(input_data, 2) == expected |
| 642 | + |
| 643 | + def test_write_json_with_precision(self, tmp_path) -> None: |
| 644 | + """Test write_json function with precision parameter.""" |
| 645 | + test_data = { |
| 646 | + "predictions": [ |
| 647 | + { |
| 648 | + "filepath": "test.jpg", |
| 649 | + "scores": [0.123456789, 0.987654321], |
| 650 | + "bbox": [0.111111, 0.222222, 0.333333, 0.444444], |
| 651 | + "confidence": 0.876543210, |
| 652 | + "nested": { |
| 653 | + "value": 1.414213562, |
| 654 | + "items": [2.718281828, 3.141592654], |
| 655 | + }, |
| 656 | + } |
| 657 | + ] |
| 658 | + } |
| 659 | + |
| 660 | + output_file = tmp_path / "test_precision.json" |
| 661 | + write_json(test_data, output_file, num_decimals=3) |
| 662 | + |
| 663 | + # Read the file back and verify precision was limited |
| 664 | + loaded_data = load_json(output_file) |
| 665 | + prediction = loaded_data["predictions"][0] |
| 666 | + |
| 667 | + assert prediction["scores"] == [0.123, 0.988] |
| 668 | + assert prediction["bbox"] == [0.111, 0.222, 0.333, 0.444] |
| 669 | + assert prediction["confidence"] == 0.877 |
| 670 | + assert prediction["nested"]["value"] == 1.414 |
| 671 | + assert prediction["nested"]["items"] == [2.718, 3.142] |
| 672 | + |
| 673 | + def test_write_json_without_precision(self, tmp_path) -> None: |
| 674 | + """Test write_json function without precision parameter. |
| 675 | +
|
| 676 | + Should preserve original precision. |
| 677 | + """ |
| 678 | + test_data = {"value": 3.14159265359, "scores": [0.123456789, 0.987654321]} |
| 679 | + |
| 680 | + output_file = tmp_path / "test_no_precision.json" |
| 681 | + write_json(test_data, output_file) |
| 682 | + |
| 683 | + loaded_data = load_json(output_file) |
| 684 | + assert loaded_data["value"] == 3.14159265359 |
| 685 | + assert loaded_data["scores"] == [0.123456789, 0.987654321] |
0 commit comments