@@ -613,3 +613,223 @@ def test_singleton_os_repr_and_str(self):
613613
614614 assert "1" in repr_str and "2" in repr_str
615615 assert "1" in str_str and "2" in str_str
616+
617+
618+ class TestExtendedOutcome :
619+ """Tests for ExtendedOutcome class and best_for method."""
620+
621+ def test_extended_outcome_with_only_outcome (self ):
622+ """Test ExtendedOutcome with only an outcome."""
623+ from negmas .outcomes .common import ExtendedOutcome
624+
625+ outcome = (5 , 10 )
626+ extended = ExtendedOutcome (outcome = outcome )
627+
628+ assert extended .outcome == (5 , 10 )
629+ assert extended .outcome_space is None
630+ assert extended .data is None
631+
632+ def test_extended_outcome_with_outcome_and_data (self ):
633+ """Test ExtendedOutcome with outcome and data."""
634+ from negmas .outcomes .common import ExtendedOutcome
635+
636+ outcome = (5 , 10 )
637+ data = {"text" : "My offer" , "confidence" : 0.9 }
638+ extended = ExtendedOutcome (outcome = outcome , data = data )
639+
640+ assert extended .outcome == (5 , 10 )
641+ assert extended .data ["text" ] == "My offer"
642+ assert extended .data ["confidence" ] == 0.9
643+
644+ def test_extended_outcome_with_outcome_space (self ):
645+ """Test ExtendedOutcome with only an outcome_space (multiple offers)."""
646+ from negmas .outcomes .common import ExtendedOutcome
647+
648+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
649+ os = make_os (issues )
650+ extended = ExtendedOutcome (outcome = None , outcome_space = os )
651+
652+ assert extended .outcome is None
653+ assert extended .outcome_space is os
654+
655+ def test_extended_outcome_with_both_outcome_and_outcome_space (self ):
656+ """Test ExtendedOutcome with both outcome and outcome_space."""
657+ from negmas .outcomes .common import ExtendedOutcome
658+
659+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
660+ os = make_os (issues )
661+ outcome = (2 , 1 )
662+ extended = ExtendedOutcome (outcome = outcome , outcome_space = os )
663+
664+ assert extended .outcome == (2 , 1 )
665+ assert extended .outcome_space is os
666+
667+ def test_best_for_with_only_outcome (self ):
668+ """Test best_for returns the outcome when only outcome is set."""
669+ from negmas .outcomes .common import ExtendedOutcome
670+ from negmas .preferences import LinearUtilityFunction
671+
672+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
673+ ufun = LinearUtilityFunction .random (issues = issues )
674+
675+ outcome = (2 , 1 )
676+ extended = ExtendedOutcome (outcome = outcome )
677+
678+ result = extended .best_for (ufun )
679+ assert result == outcome
680+
681+ def test_best_for_with_only_outcome_space (self ):
682+ """Test best_for returns the best outcome from outcome_space."""
683+ from negmas .outcomes .common import ExtendedOutcome
684+ from negmas .preferences import LinearUtilityFunction
685+
686+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
687+ os = make_os (issues )
688+ # Use positive weights so higher values are better
689+ ufun = LinearUtilityFunction (weights = [1.0 , 1.0 ], issues = issues )
690+
691+ extended = ExtendedOutcome (outcome = None , outcome_space = os )
692+
693+ result = extended .best_for (ufun )
694+ # With positive weights, best outcome should be (4, 2) - max values
695+ assert result == (4 , 2 )
696+ # Verify it's actually the best
697+ assert ufun (result ) == ufun .max ()
698+
699+ def test_best_for_with_both_outcome_and_outcome_space (self ):
700+ """Test best_for returns best from outcome_space when both are set."""
701+ from negmas .outcomes .common import ExtendedOutcome
702+ from negmas .preferences import LinearUtilityFunction
703+
704+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
705+ os = make_os (issues )
706+ # Use positive weights so higher values are better
707+ ufun = LinearUtilityFunction (weights = [1.0 , 1.0 ], issues = issues )
708+
709+ # outcome is NOT the best in the space
710+ outcome = (1 , 0 )
711+ extended = ExtendedOutcome (outcome = outcome , outcome_space = os )
712+
713+ result = extended .best_for (ufun )
714+ # Should return best from outcome_space, not the provided outcome
715+ assert result == (4 , 2 )
716+ assert result != outcome
717+
718+ def test_best_for_with_none_outcome_and_none_space (self ):
719+ """Test best_for returns None when both outcome and outcome_space are None."""
720+ from negmas .outcomes .common import ExtendedOutcome
721+ from negmas .preferences import LinearUtilityFunction
722+
723+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
724+ ufun = LinearUtilityFunction .random (issues = issues )
725+
726+ extended = ExtendedOutcome (outcome = None , outcome_space = None )
727+
728+ result = extended .best_for (ufun )
729+ assert result is None
730+
731+ def test_best_for_with_singleton_outcome_space (self ):
732+ """Test best_for with a SingletonOutcomeSpace (single outcome in space)."""
733+ from negmas .outcomes .common import ExtendedOutcome
734+ from negmas .preferences import LinearUtilityFunction
735+
736+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
737+ ufun = LinearUtilityFunction .random (issues = issues )
738+
739+ single_outcome = (3 , 1 )
740+ singleton_os = SingletonOutcomeSpace (single_outcome , name = "single" )
741+ extended = ExtendedOutcome (outcome = None , outcome_space = singleton_os )
742+
743+ result = extended .best_for (ufun )
744+ assert result == single_outcome
745+
746+ def test_best_for_with_negative_weights (self ):
747+ """Test best_for correctly finds best with negative weights."""
748+ from negmas .outcomes .common import ExtendedOutcome
749+ from negmas .preferences import LinearUtilityFunction
750+
751+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
752+ os = make_os (issues )
753+ # Use negative weights so lower values are better
754+ ufun = LinearUtilityFunction (weights = [- 1.0 , - 1.0 ], issues = issues )
755+
756+ extended = ExtendedOutcome (outcome = None , outcome_space = os )
757+
758+ result = extended .best_for (ufun )
759+ # With negative weights, best outcome should be (0, 0) - min values
760+ assert result == (0 , 0 )
761+ assert ufun (result ) == ufun .max ()
762+
763+ def test_best_for_with_mixed_weights (self ):
764+ """Test best_for correctly finds best with mixed positive/negative weights."""
765+ from negmas .outcomes .common import ExtendedOutcome
766+ from negmas .preferences import LinearUtilityFunction
767+
768+ issues = [make_issue (5 , name = "price" ), make_issue (3 , name = "quantity" )]
769+ os = make_os (issues )
770+ # First issue: higher is better, second issue: lower is better
771+ ufun = LinearUtilityFunction (weights = [1.0 , - 1.0 ], issues = issues )
772+
773+ extended = ExtendedOutcome (outcome = None , outcome_space = os )
774+
775+ result = extended .best_for (ufun )
776+ # Best should be (4, 0) - max first, min second
777+ assert result == (4 , 0 )
778+ assert ufun (result ) == ufun .max ()
779+
780+ def test_extract_outcome_with_extended_outcome (self ):
781+ """Test extract_outcome extracts the outcome from ExtendedOutcome."""
782+ from negmas .outcomes .common import ExtendedOutcome , extract_outcome
783+
784+ outcome = (5 , 10 )
785+ extended = ExtendedOutcome (outcome = outcome , data = {"text" : "test" })
786+
787+ assert extract_outcome (extended ) == (5 , 10 )
788+ assert extract_outcome (outcome ) == (5 , 10 )
789+ assert extract_outcome (None ) is None
790+
791+ def test_extract_text_with_extended_outcome (self ):
792+ """Test extract_text extracts text from ExtendedOutcome data."""
793+ from negmas .outcomes .common import ExtendedOutcome , extract_text
794+
795+ extended_with_text = ExtendedOutcome (
796+ outcome = (5 , 10 ), data = {"text" : "My proposal" }
797+ )
798+ extended_without_text = ExtendedOutcome (outcome = (5 , 10 ), data = {"other" : "data" })
799+ extended_no_data = ExtendedOutcome (outcome = (5 , 10 ))
800+
801+ assert extract_text (extended_with_text ) == "My proposal"
802+ assert extract_text (extended_without_text ) == ""
803+ assert extract_text (extended_no_data ) is None
804+ assert extract_text ((5 , 10 )) is None
805+
806+ def test_extended_outcome_is_frozen (self ):
807+ """Test that ExtendedOutcome is immutable (frozen)."""
808+ from negmas .outcomes .common import ExtendedOutcome
809+
810+ extended = ExtendedOutcome (outcome = (5 , 10 ))
811+
812+ with pytest .raises (AttributeError ):
813+ extended .outcome = (1 , 2 ) # type: ignore
814+
815+ def test_best_for_with_subset_outcome_space (self ):
816+ """Test best_for with an outcome_space that's a subset of the ufun's space."""
817+ from negmas .outcomes .common import ExtendedOutcome
818+ from negmas .preferences import LinearUtilityFunction
819+
820+ # Create ufun with larger outcome space
821+ full_issues = [make_issue (10 , name = "price" ), make_issue (5 , name = "quantity" )]
822+ ufun = LinearUtilityFunction (weights = [1.0 , 1.0 ], issues = full_issues )
823+
824+ # Create a smaller subset outcome space
825+ subset_issues = [make_issue (3 , name = "price" ), make_issue (2 , name = "quantity" )]
826+ subset_os = make_os (subset_issues )
827+
828+ extended = ExtendedOutcome (outcome = None , outcome_space = subset_os )
829+
830+ result = extended .best_for (ufun )
831+ # Best in subset should be (2, 1) - max values in the subset
832+ assert result == (2 , 1 )
833+ # Verify it's the best within the subset
834+ for o in subset_os .enumerate ():
835+ assert ufun (result ) >= ufun (o )
0 commit comments