Skip to content

MOO module

t3co.moopack.moo

ACCEL_GRADE_OVERSHOOT = 'accel_grade_overshoot_tol_constraint' module-attribute

ALGORITHMS = [ALGO_NSGA2, ALGO_NelderMead, ALGO_PatternSearch, ALGO_PSO] module-attribute

ALGO_NSGA2 = 'NSGA2' module-attribute

ALGO_NelderMead = 'NelderMead' module-attribute

ALGO_PSO = 'PSO' module-attribute

ALGO_PatternSearch = 'PatternSearch' module-attribute

CONSTRAINTS = [RANGE, ACCEL30, ACCEL60, GRADE125, GRADE6, ACCEL_GRADE_OVERSHOOT, TRACE_MISS_DIST_PERCENT, C_RATE, PHEV_MINIMIZE_FUEL_USE_CONSTRAINT] module-attribute

C_RATE = 'c_rate' module-attribute

EXCEPTION_THROWN = 3 module-attribute

KNOBS = [KNOB_CDA, KNOB_FCMAXKW, KNOB_ess_max_kwh, KNOB_mc_max_kw, KNOB_FCPEAKEFF, KNOB_WTDELTAPERC, KNOB_fs_kwh] module-attribute

KNOB_CDA = 'CdA_perc_imp' module-attribute

KNOB_FCMAXKW = 'fcMaxOutKw' module-attribute

KNOB_FCPEAKEFF = 'fc_peak_eff' module-attribute

KNOB_WTDELTAPERC = 'wt_delta_perc' module-attribute

KNOB_ess_max_kwh = 'ess_max_kwh' module-attribute

KNOB_fs_kwh = 'fs_kwh' module-attribute

KNOB_mc_max_kw = 'mc_max_kw' module-attribute

OBJECTIVES = [TCO, PHEV_MINIMIZE_FUEL_USE_OBJECTIVE] module-attribute

OPTIMIZATION_FAILED_TO_CONVERGE = 2 module-attribute

OPTIMIZATION_SUCCEEDED = 1 module-attribute

PHEV_MINIMIZE_FUEL_USE_CONSTRAINT = 'PHEV_MINIMIZE_CD_FUEL_USE_PERCENTAGE_CONSTRAINT' module-attribute

PHEV_MINIMIZE_FUEL_USE_OBJECTIVE = 'objective_phev_minimize_fuel_use' module-attribute

TCO = 'tot_cost' module-attribute

TRACE_MISS_DIST_PERCENT = 'constraint_trace_miss_dist_percent_on' module-attribute

T3CODisplay

Bases: Output

This class contains the display object for Pymoo optimization printouts - pymoo.util.display.Display

Parameters:

Name Type Description Default
Output pymoo.util.display.output.Output

Pymoo minimize display object

required
Source code in t3co/moopack/moo.py
Python
class T3CODisplay(Output):
    """
    This class contains the display object for Pymoo optimization printouts - pymoo.util.display.Display

    Args:
        Output (pymoo.util.display.output.Output): Pymoo minimize display object
    """

    def __init__(self, **kwargs) -> None:
        """
        This constructor initializes the pymoo.util.display.Display object
        """
        super().__init__(**kwargs)
        self.term = MultiObjectiveSpaceTermination()

    def _do(self, problem: T3COProblem, evaluator: float, algorithm: str) -> None:
        """
        This constructor creates the output printouts

        Args:
            problem (moo.T3COProblem): Pymoo optimization
            evaluator (float): evaluator
            algorithm (str): algorithm name
        """
        super()._do(problem, evaluator, algorithm)
        if problem.n_constr > 0:
            max_cv = np.max(algorithm.pop.get("G"))
            self.output.append("CV max", f"{max_cv:.3g}", width=10)
            # self.output.append("CV key",
            #     problem.constr_list[np.argmax(algorithm.pop.get('G')) % problem.n_constr] if max_cv > 0 else '-----',
            #     width=15
            # )
        if "tot_cost" in problem.obj_list:
            tco_start_idx = problem.obj_list.index("tot_cost")  # noqa: F841
            # tco_min_for_pop = min(algorithm.pop.get('F')[tco_start_idx::len(problem.obj_list)])[0]
            # self.output.append("min(TCO)", f"{tco_min_for_pop:.5g}")
        self.output.append("obj max", f"{np.max(algorithm.pop.get('F')):.5g}", width=10)
        self.output.append("n_nds", len(algorithm.opt), width=7)

        self.term.do_continue(algorithm)

        max_from, eps = "-", "-"
        if len(self.term.metrics) > 0:
            metric = self.term.metrics[-1]
            tol = self.term.tol
            delta_ideal, delta_nadir, delta_f = (
                metric["delta_ideal"],
                metric["delta_nadir"],
                metric["delta_f"],
            )
            if delta_ideal > tol:
                max_from = "ideal"
                eps = delta_ideal
            elif delta_nadir > tol:
                max_from = "nadir"
                eps = delta_nadir
            else:
                max_from = "f"
                eps = delta_f

        self.output.append("eps", eps)
        self.output.append("indicator", max_from)

term = MultiObjectiveSpaceTermination() instance-attribute

__init__(**kwargs) -> None

This constructor initializes the pymoo.util.display.Display object

Source code in t3co/moopack/moo.py
Python
def __init__(self, **kwargs) -> None:
    """
    This constructor initializes the pymoo.util.display.Display object
    """
    super().__init__(**kwargs)
    self.term = MultiObjectiveSpaceTermination()

T3COProblem

Bases: ElementwiseProblem

Class for creating PyMoo problem.

Source code in t3co/moopack/moo.py
Python
 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
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
class T3COProblem(ElementwiseProblem):
    """
    Class for creating PyMoo problem.
    """

    moobasevehicle: fastsim.vehicle.Vehicle
    mooadvancedvehicle: fastsim.vehicle.Vehicle
    opt_scenario: run_scenario.Scenario
    designcycle: fastsim.cycle.Cycle
    config: run_scenario.Config
    do_input_validation: bool

    def setup_opt_records(self):
        """
        This method sets up the empty optimization record arrays
        """
        #

        # objectives
        self.r_tcos = []
        self.r_cd_fc_kwh_percent = []
        self.r_cd_fc_kwh_used = []
        self.r_cd_elec_kwh_used = []

        self.reporting_vars = None
        self.r_grade_6s = []
        self.r_grade_1p25s = []
        self.r_accel_30l = []
        self.r_accel_60l = []
        self.r_ranges = []
        self.r_fuel_efficiencies = []
        self.r_wt_delta_perc_guess = []
        self.r_CdA_reduction_perc = []
        self.r_fc_peak_eff_guess = []
        self.r_fc_max_out_kw_guess = []
        self.r_fs_kwh_guess = []
        self.r_max_ess_kwh_guess = []
        self.r_max_motor_kw_guess = []
        # constraint records
        self.accel_30_constraint = []
        self.accel_60_constraint = []
        self.grade_6_constraint = []
        self.grade_1p25_constraint = []
        self.range_constraint = []
        self.grade_accel_overshoot_tol_constraint = []
        self.c_rate_constraint = []
        self.trace_miss_distance_percent_constraint_record = []
        self.phev_min_fuel_use_prcnt_const_record = []

    def __init__(
        self,
        knobs_bounds: dict,
        vnum: float,
        optimize_pt: str,
        obj_list: list = None,
        constr_list: list = None,
        verbose: bool = False,
        config: run_scenario.Config = None,
        do_input_validation: bool = False,
        **kwargs,
    ) -> None:
        """
        This constructor initializes optimization input variables

        Args:
            knobs_bounds (dict): Dictionary containing knobs bounds for optimization
            vnum (float): Vehicle selection number
            optimize_pt (str, optional): Vehicle powertrain type - Conv, BEV, HEV, PHEV. Defaults to gl.BEV.
            obj_list (list, optional): List of objectives. Defaults to None.
            constr_list (list, optional): List of constraints. Defaults to None.
            verbose (bool, optional): if True, prints process steps. Defaults to False.
            config (run_scenario.Config, optional): T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

        """
        self.setup_opt_records()

        # TODO there should probably not be any default values for kwargs

        # TODO: figure out parallelization and then modify the following line accordingly
        _ = kwargs.pop("parallelization", None)

        # possible TODO: make this a dict for grade, accel, and range tolerance
        self.range_overshoot_tol = kwargs.pop("range_overshoot_tol", None)
        self.grade_accel_overshoot_tol = kwargs.pop(
            "grade_accel_overshoot_tol", 0.01
        )  # TODO default of 1% # user should explicitly turn this on and set this outside moo.py

        # MOO optimization interpolation arrays and coefficients
        # engine peak efficiecy percents
        self.fc_eff_array = kwargs.pop(
            "fc_eff_array", np.array([46, 46, 51, 51, 53, 53, 60]) / 100
        )
        # additional $ cost per kw of engine per engine efficiency percent
        self.fc_cost_coeff_array = kwargs.pop(
            "fc_cost_coeff_array", np.array([0, 0, 7, 16, 22, 40, 74])
        )
        # glider_kg light-weighted percents
        self.ltwt_delta_percs = kwargs.pop(
            "ltwt_delta_percs", np.array([0, 5, 5, 10, 10, 25]) / 100
        )
        # additional $ cost per kg light-weighted
        self.ltwt_dol_per_kg_costs = kwargs.pop(
            "ltwt_dol_per_kg_costs", np.array([4.41, 8.82, 13.23, 17.64, 22.05, 35.27])
        )
        # , if you want to keep default values, they should be 52.1 and 6.7
        # To make it like what I sent before, Arthur can recalculate (should be enough decimals):
        # a = 100*a
        # b = 10000*b - Alicia
        self.cda_cost_coeff_a = kwargs.pop("cda_cost_coeff_a", 52.053)
        self.cda_cost_coeff_b = kwargs.pop("cda_cost_coeff_b", 6.7018)
        self.cda_mass_coeff_a = kwargs.pop(
            "cda_mass_coeff_a", 0
        )  # default formula doesn't need a_M coeff, wt = 17967.9*p^2
        self.cda_mass_coeff_b = kwargs.pop("cda_mass_coeff_b", 1.79679)
        self.cda_perc_imp_at_which_wt_penalty_maxes_out = kwargs.pop(
            "cda_perc_imp_at_which_wt_penalty_maxes_out", 9999999999
        )

        # just checker values, knobs_bounds may not actually have all of these populated, so [0,0] is never used beyond the asserts
        max_fc_bound = knobs_bounds.get(KNOB_FCPEAKEFF, (0, 0))[1]
        max_ltwt_bound = knobs_bounds.get(KNOB_WTDELTAPERC, (0, 0))[1]
        CdA_perc_imp_knob_max = knobs_bounds.get(KNOB_CDA, (0, 0))[1]

        self.knobs_bounds = knobs_bounds

        knobs = [key for key in knobs_bounds.keys()]
        lower_bounds = anp.array([val[0] for bound, val in knobs_bounds.items()])
        upper_bounds = anp.array([val[1] for bound, val in knobs_bounds.items()])

        self.write_tsv = kwargs.pop("write_tsv", False)

        self.obj_list = obj_list
        if obj_list is None:
            # create default objective list
            self.obj_list = [TCO]

        self.constr_list = constr_list

        # add acceleration and grade overshoot tolerance constraint implicitly if user is apply grade or acceleration constraints
        if len(set(self.constr_list) & set([ACCEL30, ACCEL60, GRADE125, GRADE6])):
            self.constr_list.append(ACCEL_GRADE_OVERSHOOT)

        n_constr = len(self.constr_list)
        # input (f"moo.py: T3COProblem __init__\n{self.constr_list}, \neffective constraints: {n_constr}")
        # make it so that knobs always need to be specified.
        self.knobs = knobs

        self.verbose = verbose

        self.optimize_pt = optimize_pt

        self.instantiate_moo_vehicles_and_scenario(vnum, config, do_input_validation)

        # time dilation options, turned on for fuel efficiency cycle
        if "missed_trace_correction" in kwargs:
            self.opt_scenario.missed_trace_correction = kwargs.pop(
                "missed_trace_correction"
            )
            self.opt_scenario.max_time_dilation = kwargs.pop("max_time_dilation")
            self.opt_scenario.min_time_dilation = kwargs.pop("min_time_dilation")
            self.opt_scenario.time_dilation_tol = kwargs.pop("time_dilation_tol")

        n_obj = len(self.obj_list)

        n_args = len(knobs)

        # asserts
        assert (
            self.optimize_pt == self.mooadvancedvehicle.veh_pt_type
        ), "instantiate_moo_vehicles_and_scenario should have handled this"
        for knob in self.knobs:
            assert knob in KNOBS, f"knob {knob} not in defined parameters: {KNOBS}"
        for constraint in self.constr_list:
            assert (
                constraint in CONSTRAINTS
            ), f"constraint {constraint} not in defined constraints: {CONSTRAINTS}"
            if constraint == PHEV_MINIMIZE_FUEL_USE_CONSTRAINT:
                assert (
                    self.opt_scenario.constraint_phev_minimize_fuel_use_percent > 0
                    and self.opt_scenario.constraint_phev_minimize_fuel_use_percent < 1
                )
        assert len(OBJECTIVES) >= 1, "enable at least one objective"
        for obj in self.obj_list:
            assert (
                obj in OBJECTIVES
            ), f"objective {obj} not in defined objectives: {OBJECTIVES}"
        assert (
            set(self.constr_list) & set(self.obj_list) == set()
        ), f"cannot have constraints and objectives overlap constraints {self.constr_list} & {self.obj_list}"
        if KNOB_fs_kwh in self.knobs:
            assert (
                self.optimize_pt in [gl.CONV, gl.HEV, gl.PHEV]
            ), f"input knob bounds: {knobs_bounds}\naltering fuel store (tank, not battery) kwh size but vehicle powertrain to be optimized is {self.optimize_pt}"
        if KNOB_FCMAXKW in self.knobs:
            assert (
                self.optimize_pt in [gl.CONV, gl.HEV, gl.PHEV]
            ), f"input knob bounds: {knobs_bounds}\naltering fuel converter size but vehicle powertrain is {self.optimize_pt}"
        if KNOB_ess_max_kwh in self.knobs:
            assert (
                self.optimize_pt in [gl.BEV, gl.HEV, gl.PHEV]
            ), f"input knob bounds: {knobs_bounds}\naltering battery size but vehicle powertrain to be optimized is {self.optimize_pt}"
        if KNOB_mc_max_kw in self.knobs:
            assert (
                self.optimize_pt in [gl.BEV, gl.HEV, gl.PHEV]
            ), f"input knob bounds: {knobs_bounds}\naltering motor size but vehicle powertrain to be optimized is {self.optimize_pt}"
        assert (
            max_fc_bound <= max(self.fc_eff_array)
        ), f"max eng eff knob val greater than max of eng eff array {max_fc_bound} > {self.fc_eff_array}, could yield free efficiency"
        assert (
            max_ltwt_bound <= max(self.ltwt_delta_percs)
        ), f"max light-weight knob val greater than max of light-weighting percent array {max_ltwt_bound} > {self.ltwt_delta_percs}, could yield free light-weighting"
        assert (
            max_fc_bound <= 1
        ), f"knob bounds must be decimal format, max_fc_bound: {max_fc_bound}"
        assert (
            max_ltwt_bound <= 1
        ), f"knob bounds must be decimal format, max_ltwt_bound: {max_ltwt_bound}"
        assert (
            CdA_perc_imp_knob_max <= 1
        ), f"knob bounds must be decimal format, CdA_perc_imp_knob_max: {CdA_perc_imp_knob_max}"

        # n_ieq_constr, number of constraints that must yield < 0
        super().__init__(
            n_var=n_args,
            n_obj=n_obj,
            n_ieq_constr=n_constr,
            # lower bounds
            xl=lower_bounds,
            # upper bounds
            xu=upper_bounds,
            **kwargs,
        )

        if len(kwargs) > 0:
            warnings.warn(
                f"Possible unused/invalid kwargs provided:\n {list(kwargs.keys())}"
            )

    def compile_reporting_vars(self) -> None:
        """
        This method creates an output dictionary containing optimization results
        """
        d = {
            "objective_TCOs": self.r_tcos,
            "objective_fc_khw_percent": self.r_cd_fc_kwh_percent,
            "objective_fc_khw_used": self.r_cd_fc_kwh_used,
            "objective_elec_khw_used": self.r_cd_elec_kwh_used,
            "r_mph_ach_grade_6s": self.r_grade_6s,
            "r_mph_ach_grade_1p25s": self.r_grade_1p25s,
            "r_sec_to_ach_30mph_ldd": self.r_accel_30l,
            "r_sec_to_ach_60mph_ldd": self.r_accel_60l,
            "r_ach_ranges_mi": self.r_ranges,
            "target_mph_grade_6s": [
                self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph
            ]
            * len(self.r_grade_6s),
            "target_mph_grade_1p25s": [
                self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
            ]
            * len(self.r_grade_6s),
            "target_sec_to_30mph_ldd": [self.opt_scenario.max_time_0_to_30mph_at_gvwr_s]
            * len(self.r_grade_6s),
            "target_sec_to_60mph_ldd": [self.opt_scenario.max_time_0_to_60mph_at_gvwr_s]
            * len(self.r_grade_6s),
            "target_range_mi": [self.opt_scenario.target_range_mi]
            * len(self.r_grade_6s),
            "r_ach_fuel_efficiencies": self.r_fuel_efficiencies,
            "accel_30_constraint_vals": self.accel_30_constraint,
            "accel_60_constraint_vals": self.accel_60_constraint,
            "grade_6_constraint_vals": self.grade_6_constraint,
            "grade_1p25_constraint_vals": self.grade_1p25_constraint,
            "range_constraint_vals": self.range_constraint,
            "grade_accel_overshoot_tol_constraint_vals": self.grade_accel_overshoot_tol_constraint,
            "trace_miss_dist_percent_constraint_vals": self.trace_miss_distance_percent_constraint_record,
            "phev_min_fuel_use_pct_constraint_vals": self.phev_min_fuel_use_prcnt_const_record,
            "r_wt_delta_perc_guess": self.r_wt_delta_perc_guess,
            "r_CdA_reduction_perc": self.r_CdA_reduction_perc,
            "r_fc_peak_eff_guess": self.r_fc_peak_eff_guess,
            "r_fc_max_out_kw_guess": self.r_fc_max_out_kw_guess,
            "r_fs_kwh_guess": self.r_fs_kwh_guess,
            "r_max_ess_kwh_guess": self.r_max_ess_kwh_guess,
            "r_max_motor_kw_guess": self.r_max_motor_kw_guess,
        }
        self.reporting_vars = pd.DataFrame(data=d)

    def instantiate_moo_vehicles_and_scenario(
        self,
        vnum: int,
        config: run_scenario.Config = None,
        do_input_validation: bool = False,
    ) -> None:
        """
        This method instantiates the multi-objective optimization problem vehicles and scenarios, starting with the baseline Conventional vehicle.

        Args:
            vnum (int): vehicle selection number
            config (run_scenario.Config, optional): T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

        Raises:
            TypeError: Invalid optimize_pt selection
        """
        self.moobasevehicle = run_scenario.get_vehicle(
            vnum,
            veh_input_path=config.vehicle_file,
        )

        self.opt_scenario, self.designcycle = run_scenario.get_scenario_and_cycle(
            vnum,
            config.scenario_file,
            config=config,
            do_input_validation=do_input_validation,
        )

        if (
            self.opt_scenario.fuel_type == "diesel and electricity"
            and self.moobasevehicle.veh_pt_type == gl.HEV
        ):
            self.opt_scenario.fuel_type = "diesel"

        # save baseline values for optimization diffs
        self.opt_scenario.originalGliderPrice = (
            self.opt_scenario.vehicle_glider_cost_dol
        )
        self.opt_scenario.originalglider_kg = self.moobasevehicle.glider_kg
        self.opt_scenario.originalIceDolPerKw = self.opt_scenario.fc_ice_cost_dol_per_kw
        self.opt_scenario.origfc_eff_map = self.moobasevehicle.fc_eff_map.copy()
        self.opt_scenario.originalcargo_kg = self.moobasevehicle.cargo_kg
        self.opt_scenario.originaldrag_coef = self.moobasevehicle.drag_coef

        self.mooadvancedvehicle = copy.copy(self.moobasevehicle)

        if self.optimize_pt == gl.BEV:
            self.mooadvancedvehicle.veh_pt_type = gl.BEV
            run_scenario.set_max_fuel_converter_kw(self.mooadvancedvehicle, 0)
            # change to 0, based on Excel version direction when trying to run as EV
            self.mooadvancedvehicle.fc_max_out_kw = 0
            run_scenario.set_fuel_store_kwh(self.mooadvancedvehicle, 0)
        elif self.optimize_pt == gl.CONV:
            self.mooadvancedvehicle.veh_pt_type = gl.CONV
            run_scenario.set_max_motor_kw(
                self.mooadvancedvehicle, scenario=None, max_motor_kw=0
            )
            run_scenario.set_max_battery_kwh(self.mooadvancedvehicle, max_ess_kwh=0)
        elif self.optimize_pt == gl.HEV:
            self.mooadvancedvehicle.veh_pt_type = gl.HEV
            # nothing to zero out for HEV/FCEV
        elif self.optimize_pt == gl.PHEV:
            self.mooadvancedvehicle.veh_pt_type = gl.PHEV
        else:
            raise TypeError(f"invalid optimize_pt selection {self.optimize_pt}")

        # establish payload opp cost calc module
        # TODO, this should include FCEV, and this TCO element should be True/False activated
        # if self.opt_scenario.activate_tco_payload_cap_cost_multiplier or self.opt_scenario.activate_tco_fueling_dwell_time_cost or self.opt_scenario.activate_mr_downtime_cost:
        #     self.oppcostobj = opportunity_cost.OpportunityCost(self.opt_scenario)

    # --------- optimizer parameter application methods ---------
    def cda_percent_delta_knob(
        self, CdA_perc_reduction: str, optvehicle: fastsim.vehicle.Vehicle
    ) -> None:
        """
        This method sets the drag_coef based on aero improvement curve and glider_kg based on cda_cost_coeff_a and cda_cost_coeff_b

        Args:
            CdA_perc_reduction (str): Name of aero improvement curve file
            optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object for optimization vehicle

        """

        # check value of CdA_reduction
        # print(CdA_perc_reduction)

        # functions that return cost, weight, and possibly other penalties w.r.t. various improvements

        def get_cost_per_CdA_delta(x):
            """
            Returns cost penalty given percent improvement in CdA.
            """
            # COST:  a * cda_perc + b * cda_perc^2
            a_C = self.cda_cost_coeff_a
            b_C = self.cda_cost_coeff_b
            x = x * 100
            return a_C * x + b_C * x**2

        def get_mass_per_CdA_delta(x):
            """
            Returns weight penalty [Lb converted to kg] given % improvement in CdA.
            These formulas are developed in pounds. Converted to kg for FASTSim at the end.
            """
            # MASS: a * cda_perc + b * cda_perc^2
            a_M = (
                self.cda_mass_coeff_a
            )  # default formula doesn't need a_M coeff, wt = 17967.9*p^2
            b_M = self.cda_mass_coeff_b
            x = x * 100
            CdA_perc_imp_at_which_wt_penalty_maxes_out = (
                self.cda_perc_imp_at_which_wt_penalty_maxes_out
            )
            return min(
                gl.lbs_to_kgs(
                    a_M * CdA_perc_imp_at_which_wt_penalty_maxes_out
                    + b_M * CdA_perc_imp_at_which_wt_penalty_maxes_out**2
                ),
                gl.lbs_to_kgs(a_M * x + b_M * x**2),
            )

        # reset drag coefficient based on percent improvement
        optvehicle.drag_coef = max(
            0.01,  # this is an additional hard-coded bound on drag coefficient that should probably be removed (throw error or warning if drag_coef < 0.01 instead of running and replacing with this default value)
            self.opt_scenario.originaldrag_coef * (1 - CdA_perc_reduction),
        )
        # glider cost penalty due to CdA improvement
        CdA_cost = get_cost_per_CdA_delta(CdA_perc_reduction)
        self.opt_scenario.vehicle_glider_cost_dol = (
            self.opt_scenario.vehicle_glider_cost_dol + CdA_cost
        )
        # mass adjustments. Add CdA mass FIRST, if applicable. Then limit cargo_kg if overweight from battery.
        optvehicle.glider_kg = optvehicle.glider_kg + get_mass_per_CdA_delta(
            CdA_perc_reduction
        )
        optvehicle.set_veh_mass()

    def weight_delta_percent_knob(
        self, wt_perc_reduction: float, optvehicle: fastsim.vehicle.Vehicle
    ) -> None:
        """
        This method sets the knob from the lightweighting curve

        Args:
            wt_perc_reduction (float): Weight reduction percentage value from lightweighting curve
            optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object of the optimization vehicle
        """
        wt_delta_cost_per_kg = np.interp(
            x=wt_perc_reduction, xp=self.ltwt_delta_percs, fp=self.ltwt_dol_per_kg_costs
        )
        wt_delta_kg = optvehicle.glider_kg * wt_perc_reduction
        # Calculate lightweight cost from curve as an integral
        x_new = (
            self.ltwt_delta_percs[self.ltwt_delta_percs <= wt_perc_reduction]
            * optvehicle.glider_kg
        )
        y_new = self.ltwt_dol_per_kg_costs[self.ltwt_delta_percs <= wt_perc_reduction]
        if wt_delta_kg not in x_new:
            x_new = np.append(x_new, wt_delta_kg)
            y_new = np.append(y_new, wt_delta_cost_per_kg)
        self.opt_scenario.vehicle_glider_cost_dol = (
            self.opt_scenario.vehicle_glider_cost_dol + np.trapz(y_new, x_new)
        )
        optvehicle.glider_kg = optvehicle.glider_kg - wt_delta_kg
        optvehicle.set_veh_mass()

    def fc_peak_eff_knob(
        self, fc_peak_eff: float, optvehicle: fastsim.vehicle.Vehicle
    ) -> None:
        """
        This method sets the knob from the engine efficiency curve

        Args:
            fc_peak_eff (float): Fuel converter peak effiency override from engine efficiency improvement curve
            optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object for optimization vehicle
        """
        fc_eff_array = self.fc_eff_array
        cost_coeff_array = self.fc_cost_coeff_array
        d_eff_dol_per_kw = np.interp(fc_peak_eff, fc_eff_array, cost_coeff_array)
        self.opt_scenario.fc_ice_cost_dol_per_kw = (
            self.opt_scenario.originalIceDolPerKw + d_eff_dol_per_kw
        )
        self.adjust_fc_peak_eff(fc_peak_eff, self.opt_scenario, optvehicle)

    def get_objs(
        self, x: dict, write_tsv: bool = False
    ) -> Tuple[np.array, np.array, dict]:
        """
        This method gets called when PyMoo calls _evaluate. It initializes objectives and constraints and runs vehicle_scenario_sweep

        x optimization knobs = [max motor kw, battery kwh, drag coeff % improvement]
        Function for running FE cycles and accel tests then returning
        fuel consumption and zero-to-sixty times.

        x is a set of genes (or parameters), so kwh size is a gene
        chromosome is a full gene, all values in x

        Args:
            x (dict): Dictionary containing optimization knobs - {max motor kw, battery kwh, drag coeff % improvement}
            write_tsv (bool, optional): if True, save intermediate dataframes. Defaults to False.

        Returns:
            obj_arr_F (np.array): Array of objectives - tot_cost and phev_cd_fuel_used_kwh
            constraint_results_G (np.array): Array of constraints
            rs_sweep (dict): Output dictionary from vehicle_scenario_sweep
        """

        # dict for providing mechanism for making sure all knobs get used
        x_dict = {knob: x[self.knobs.index(knob)] for knob in self.knobs}

        # if self.optimize_pt not in [gl.CONV, gl.BEV, gl.HEV]:
        #     raise TypeError(f"optimize_pt is not configured for {self.optimize_pt}")
        designcycle = self.designcycle
        optvehicle = self.mooadvancedvehicle

        # reset glider price and weight for light-weighting and/or CdA percent improvement
        self.opt_scenario.vehicle_glider_cost_dol = (
            self.opt_scenario.originalGliderPrice
        )
        optvehicle.glider_kg = self.opt_scenario.originalglider_kg

        wt_delta_perc_guess = x_dict.pop(KNOB_WTDELTAPERC, None)
        CdA_reduction_perc = x_dict.pop(KNOB_CDA, None)
        fc_peak_eff_guess = x_dict.pop(KNOB_FCPEAKEFF, None)
        fc_max_out_kw_guess = x_dict.pop(KNOB_FCMAXKW, None)
        max_ess_kwh_guess = x_dict.pop(KNOB_ess_max_kwh, None)
        max_motor_kw_guess = x_dict.pop(KNOB_mc_max_kw, None)
        fs_kwh_guess = x_dict.pop(KNOB_fs_kwh, None)
        # set knobs
        if "wt_delta_perc" in self.knobs:
            # confirmed with Alicia and Jason on 8/11/2021 that light-weighting should occur before CdA adjustment
            self.weight_delta_percent_knob(wt_delta_perc_guess, optvehicle)
        if "CdA_perc_imp" in self.knobs:
            self.cda_percent_delta_knob(CdA_reduction_perc, optvehicle)
        if "fc_peak_eff" in self.knobs:
            self.fc_peak_eff_knob(fc_peak_eff_guess, optvehicle)
        if "fcMaxOutKw" in self.knobs:
            run_scenario.set_max_fuel_converter_kw(optvehicle, fc_max_out_kw_guess)
        if KNOB_fs_kwh in self.knobs:
            run_scenario.set_fuel_store_kwh(optvehicle, fs_kwh_guess)
        if "ess_max_kwh" in self.knobs:
            run_scenario.set_max_battery_kwh(optvehicle, max_ess_kwh_guess)
        if "mc_max_kw" in self.knobs:
            run_scenario.set_max_motor_kw(
                optvehicle, self.opt_scenario, max_motor_kw_guess
            )

        # enforce 0 <= cargo kg <= initial cargo kg for BEV and HEV optimizations
        if self.optimize_pt in [gl.BEV, gl.HEV]:
            run_scenario.limit_cargo_kg_for_moo_hev_bev(self.opt_scenario, optvehicle)

        # # TODO, this calculation and application of payload loss is still not finalized.
        # this also needs to be used for FCEV, would be nice if there were an FCEV veh_pt_type!!!
        # also it needs to have a Scenario File on/off activation

        assert len(x_dict) == 0, f"Unapplied knobs: {list(x_dict.keys())}"

        # calculate objectives
        get_accel_loaded = False
        get_grade = False
        obj_arr_F = []
        g6_acvhd, g125_acvhd, z60l_acvhd, z30l_acvhd, tco_acvhd, mpgge, range_achvd = [
            None
        ] * 7
        if GRADE6 in self.constr_list or GRADE125 in self.constr_list:
            get_grade = True
        if ACCEL30 in self.constr_list or ACCEL60 in self.constr_list:
            get_accel_loaded = True

        rs_sweep = run_scenario.vehicle_scenario_sweep(
            optvehicle,
            self.opt_scenario,
            designcycle,
            verbose=self.verbose,
            get_accel=False,  # don't want non-loaded accel values, for now
            get_accel_loaded=get_accel_loaded,
            get_gradeability=get_grade,
            write_tsv=write_tsv,
        )

        mpgge = rs_sweep["mpgge"]
        g6_acvhd, g125_acvhd = (
            rs_sweep["grade_6_mph_ach"],
            rs_sweep["grade_1_25_mph_ach"],
        )
        z60l_acvhd, z30l_acvhd = (
            rs_sweep["zero_to_60_loaded"],
            rs_sweep["zero_to_30_loaded"],
        )
        tco_acvhd = rs_sweep["tot_cost"]
        if optvehicle.veh_pt_type in [gl.BEV, gl.CONV, gl.HEV]:
            range_achvd = rs_sweep["primary_fuel_range_mi"]
        elif optvehicle.veh_pt_type == gl.PHEV:
            # need range from PHEV that is used to compare to target_range_mi
            range_achvd = rs_sweep["cd_aer_phev_range_mi"]

        if self.verbose:
            print(
                f"grade_6_mph_ach {g6_acvhd} grade_1_25_mph_ach {g125_acvhd} zero_to_60_loaded {z60l_acvhd} "
                f"zero_to_30_loaded {z30l_acvhd} tco {tco_acvhd} mpgge {mpgge} range {range_achvd}"
            )

        # PHEV fuel usage stats
        pct_fc_kwh = None
        phev_cd_fuel_used_kwh = None
        phev_cd_battery_used_kwh = None
        if (
            PHEV_MINIMIZE_FUEL_USE_OBJECTIVE in self.obj_list
            or PHEV_MINIMIZE_FUEL_USE_CONSTRAINT in self.constr_list
        ):
            phev_cd_fuel_used_kwh = rs_sweep["mpgge"]["cd_fuel_used_kwh_total"]
            phev_cd_battery_used_kwh = rs_sweep["mpgge"]["cd_battery_used_kwh"]
            pct_fc_kwh = round(
                phev_cd_fuel_used_kwh
                / (phev_cd_battery_used_kwh + phev_cd_fuel_used_kwh),
                2,
            )
        #                                                           #
        # ********************** objectives  ********************** #
        #                                                           #
        if TCO in self.obj_list:
            obj_arr_F.append(tco_acvhd)
        if PHEV_MINIMIZE_FUEL_USE_OBJECTIVE in self.obj_list:
            obj_arr_F.append(phev_cd_fuel_used_kwh)
        #                                                           #
        # ******************** end objectives  ******************** #
        #                                                           #

        #                                                           #
        # ********************** constraints ********************** #
        #
        #                                                           #

        self.accel_30_constraint.append(None)
        self.accel_60_constraint.append(None)
        self.grade_6_constraint.append(None)
        self.grade_1p25_constraint.append(None)
        self.range_constraint.append(None)
        self.grade_accel_overshoot_tol_constraint.append(None)
        self.c_rate_constraint.append(None)
        self.trace_miss_distance_percent_constraint_record.append(None)
        self.phev_min_fuel_use_prcnt_const_record.append(None)

        constraint_results_G = []
        # calculate constraint violations
        # speed at grade minus target should be negative when constraint is met
        if GRADE6 in self.constr_list:
            constraint_results_G.append(
                self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph - g6_acvhd
            )
        if GRADE125 in self.constr_list:
            constraint_results_G.append(
                self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph - g125_acvhd
            )
        if ACCEL60 in self.constr_list:
            # zero-to-speed time should minus max allowable (target value) should
            # be negative when constraint is met
            # 9 sec achvd - 10 sec target = -1
            constraint_results_G.append(
                z60l_acvhd - self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
            )
        if ACCEL30 in self.constr_list:
            constraint_results_G.append(
                z30l_acvhd - self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
            )

        # calculate limiting grade/accel requirement if all constraints met
        if ACCEL_GRADE_OVERSHOOT in self.constr_list:
            constr_perc = {}
            if GRADE125 in self.constr_list:
                # todo, -abs() for all of these?
                g125c = (
                    -(
                        g125_acvhd
                        - self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
                    )
                    / self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
                )
                constr_perc[GRADE125] = g125c
                self.grade_1p25_constraint[-1] = g125c
            if GRADE6 in self.constr_list:
                g6c = (
                    -(g6_acvhd - self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph)
                    / self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph
                )
                constr_perc[GRADE6] = g6c
                self.grade_6_constraint[-1] = g6c
            if ACCEL60 in self.constr_list:
                z60c = (
                    z60l_acvhd - self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
                ) / self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
                constr_perc[ACCEL60] = z60c
                self.accel_60_constraint[-1] = z60c
            if ACCEL30 in self.constr_list:
                z30c = (
                    z30l_acvhd - self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
                ) / self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
                constr_perc[ACCEL30] = z30c
                self.accel_30_constraint[-1] = z30c

            # if all constraints meet target, then find closest overshoot and ensure it's below tolerance
            if (len(constr_perc) > 0) & (
                np.array(list(constr_perc.values())) < 0
            ).all():
                # say largest negative percent is -.1 (closest to target, 10% exceeding)
                # tolerance is .05
                # -(-.1) - 0.05 = 0.05, >= 0, constraint is violated. Constraints are in bounds if they return < 0
                min_grade_accel_excess = (
                    -constr_perc[max(constr_perc, key=constr_perc.get)]
                    - self.grade_accel_overshoot_tol
                )
            else:
                min_grade_accel_excess = -1  # no constraint to satisfy
            constraint_results_G.append(min_grade_accel_excess)
            self.grade_accel_overshoot_tol_constraint[-1] = min_grade_accel_excess

        if RANGE in self.constr_list:
            # if you fall short of range target
            if self.range_overshoot_tol is None:
                range_mi_cv = (
                    self.opt_scenario.target_range_mi - range_achvd
                )  # pos return, failed
            else:
                if range_achvd <= self.opt_scenario.target_range_mi:
                    range_mi_cv = (
                        self.opt_scenario.target_range_mi - range_achvd
                    )  # pos return, failed
                else:
                    range_mi_cv = range_achvd - (
                        self.opt_scenario.target_range_mi
                        * (1 + self.range_overshoot_tol)
                    )

            constraint_results_G.append(range_mi_cv)
            self.range_constraint[-1] = range_mi_cv

        # c rate constraint
        if C_RATE in self.constr_list:
            self.c_rate_constraint[-1] = (
                self.mooadvancedvehicle.ess_max_kw / self.mooadvancedvehicle.ess_max_kwh
                - np.interp(
                    self.mooadvancedvehicle.ess_max_kwh,
                    # TODO, this 2D array needs to be an input
                    [1.0, 10.0, 188.0, 660.0],  # battery sizes kwh
                    [24.0, 12.0, 2.0, 0.7],  # c rates (kw/kwh)
                )
            )
            constraint_results_G.append(self.c_rate_constraint[-1])

        # # trace miss constraint
        if TRACE_MISS_DIST_PERCENT in self.constr_list:
            assert (
                self.opt_scenario.trace_miss_dist_percent > 0
                and self.opt_scenario.trace_miss_dist_percent < 1
            ), "scenario file input trace_miss_dist_percent must be decimal value greater than 0 and less than 1"
            cycle_records = rs_sweep["design_cycle_sim_drive_record"]
            max_dist_frac_result = max(
                sdr.trace_miss_dist_frac for sdr in cycle_records
            )
            # .1 -> 10%
            max_dist_frac_miss = self.opt_scenario.trace_miss_dist_percent
            constraint_results_G.append(max_dist_frac_result - max_dist_frac_miss)
            self.trace_miss_distance_percent_constraint_record[-1] = (
                max_dist_frac_result - max_dist_frac_miss
            )

        if PHEV_MINIMIZE_FUEL_USE_CONSTRAINT in self.constr_list:
            assert (
                self.opt_scenario.constraint_phev_minimize_fuel_use_percent > 0
            ), "scenario.constraint_phev_minimize_fuel_use_percent must be value > 0 and < 1"
            assert (
                self.opt_scenario.constraint_phev_minimize_fuel_use_percent < 1
            ), "scenario.constraint_phev_minimize_fuel_use_percent must be value > 0 and < 1"
            constraint_results_G.append(
                pct_fc_kwh - self.opt_scenario.constraint_phev_minimize_fuel_use_percent
            )
            self.phev_min_fuel_use_prcnt_const_record[-1] = (
                pct_fc_kwh - self.opt_scenario.constraint_phev_minimize_fuel_use_percent
            )

        #                                                           #
        # ******************** end constraints ******************** #
        #                                                           #

        # append reporting variables
        # obj vars
        self.r_tcos.append(tco_acvhd)
        self.r_cd_fc_kwh_percent.append(pct_fc_kwh)
        self.r_cd_fc_kwh_used.append(phev_cd_fuel_used_kwh)
        self.r_cd_elec_kwh_used.append(phev_cd_battery_used_kwh)

        self.r_grade_6s.append(g6_acvhd)
        self.r_grade_1p25s.append(g125_acvhd)
        self.r_accel_60l.append(z60l_acvhd)
        self.r_accel_30l.append(z30l_acvhd)
        self.r_fuel_efficiencies.append(mpgge)
        self.r_ranges.append(range_achvd)
        self.r_wt_delta_perc_guess.append(wt_delta_perc_guess)
        self.r_CdA_reduction_perc.append(CdA_reduction_perc)
        self.r_fc_peak_eff_guess.append(fc_peak_eff_guess)
        self.r_fc_max_out_kw_guess.append(fc_max_out_kw_guess)
        self.r_fs_kwh_guess.append(fs_kwh_guess)
        self.r_max_ess_kwh_guess.append(max_ess_kwh_guess)
        self.r_max_motor_kw_guess.append(max_motor_kw_guess)

        return np.array(obj_arr_F), np.array(constraint_results_G), rs_sweep

    def _evaluate(self, x: dict, out: dict, *args, **kwargs) -> None:
        """
        This method runs T3COProblem.get_objs() when running Pymoo optimization

        Args:
            x (dict): Dictionary containing optimization knobs
            out (dict): Dictionary containing TCO results for optimization runs
        """
        obj_arr_F, constr_arr, _ = self.get_objs(x)
        out["F"] = obj_arr_F

        if len(constr_arr) > 0:
            out["G"] = constr_arr

    def adjust_fc_peak_eff(
        self,
        fc_peak_eff: float,
        scenario: run_scenario.Scenario,
        optvehicle: fastsim.vehicle.Vehicle,
    ) -> None:
        """
        This method augments an advanced vehicle fc_eff_array based on new fc_peak_eff using baseline fc_eff_array


        Args:
            fc_peak_eff (float): Fuel converter peak efficiency override
            scenario (run_scenario.Scenario): Scenario object of current selection
            optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object of optimization vehicle
        """
        old_peak_eff = scenario.origfc_eff_map.max()
        optvehicle.fc_eff_map = scenario.origfc_eff_map * (
            1 + (fc_peak_eff - old_peak_eff) / old_peak_eff
        )
        optvehicle.set_derived()

    # ------------------------------------ utility functions ------------------------------------
    def sweep_knob(
        self,
        knob: list,
        definition: int = 100,
        plot: bool = False,
        optres: float = None,
        **kwargs,
    ) -> list:
        """
        This method sweeps the optimization knob of vehicle from lbound to ubound, return TCO \
            plot optres to see if there's agreement from opt solution and your sweep

        Args:
            knob (list): list of knobs names for optimization
            definition (int, optional): Number of points. Defaults to 100.
            plot (bool, optional): if True, saves plot of bounds and TCOs. Defaults to False.
            optres (float, optional): Optimization resolution. Defaults to None.

        Returns:
            tcos (list): List of TCOs of length=definition
        """
        label = kwargs.get("label", "")
        sweep_knob_accel_test = kwargs.get("sweep_knob_accel_test", False)
        # save knobs and obj list
        temp_obj_list = self.obj_list
        self.knobs = [knob]
        self.obj_list = [TCO]
        lbound, ubound = self.knobs_bounds[knob]
        tcos = [self.get_objs([x])[0] for x in np.linspace(lbound, ubound, definition)]

        if sweep_knob_accel_test:
            opt_res_kw = np.inf
            if optres is not None:
                opt_res_kw = optres
            assert knob in [KNOB_mc_max_kw, KNOB_FCMAXKW]
            self.obj_list = [ACCEL30, ACCEL60]
            # objs.extend([z60l, z30l]) {(60, 30),...(60, 30)}
            thirty_mph_times = []
            sixty_mph_times = []
            largest_infeasible_30s_size = None
            failed_30_s_time = None
            largest_infeasible_60s_size = None
            failed_60_s_time = None
            for x in np.linspace(lbound, ubound, definition):
                z60l, z30l = self.get_objs([x])
                thirty_mph_times.append([z30l, x])
                sixty_mph_times.append([z60l, x])
            for res in sixty_mph_times:
                z60l, x = res
                if (
                    z60l > self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
                    and x < opt_res_kw
                ):
                    largest_infeasible_60s_size = x
                    failed_60_s_time = z60l
            for res in sixty_mph_times:
                z30l, x = res
                if (
                    z30l > self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
                    and x < opt_res_kw
                ):
                    largest_infeasible_30s_size = x
                    failed_30_s_time = z30l

        # todo, figure out how to get mpgge in the other y axis
        if plot:
            resdir = gl.MOO_KNOB_SWEEP_PLOTS_DIR
            if not resdir.exists():
                resdir.mkdir()
            ts = strftime("%Y-%m-%d_%H-%M-%S", gmtime())
            plt.figure(figsize=(7, 4))
            plt.ylabel("$TCO")
            plt.title(f"{label}_{ts}_{knob}")
            plt.xlabel(f"knob value for {definition} pts")
            plt.plot(
                np.linspace(lbound, ubound, definition),
                tcos,
                label=f"TCO from global optimum, sweeping {knob}",
            )
            if optres is not None:
                plt.vlines(
                    optres,
                    min(tcos),
                    max(tcos),
                    color="red",
                    label=f"MOO {knob} solution: {optres}",
                )
            if sweep_knob_accel_test:
                if largest_infeasible_30s_size is not None:
                    plt.vlines(
                        largest_infeasible_30s_size,
                        min(tcos),
                        max(tcos),
                        color="orange",
                        label=f"failed accel test 0 to 30 res: {failed_30_s_time} target: {self.opt_scenario.max_time_0_to_30mph_at_gvwr_s} {knob} size: {largest_infeasible_30s_size}",
                    )
                if largest_infeasible_60s_size is not None:
                    plt.vlines(
                        largest_infeasible_60s_size,
                        min(tcos),
                        max(tcos),
                        color="yellow",
                        label=f"failed accel test 0 to 60 res: {failed_60_s_time} target: {self.opt_scenario.max_time_0_to_60mph_at_gvwr_s} {knob} size: {largest_infeasible_30s_size}",
                    )
            plt.legend()
            plt.savefig(resdir / f"{knob}_{label}_{ts}.png")
            print(f"saving {knob} results to ", resdir)
            plt.show()
        self.knobs = self.knobs_bounds.keys()
        self.obj_list = temp_obj_list
        return tcos

    def get_tco_from_moo_advanced_result(self, x: dict) -> dict:
        """
        This method is a utility function to get detailed TCO information from optimized MOO result

        Args:
            x (dict): Dictionary containing optimization knobs - [max motor kw, battery kwh, drag coeff % improvement]

        Returns:
            out (dict): Dictionary containing TCO results for optimization runs
        """
        x_dict = {knob: x[self.knobs.index(knob)] for knob in self.knobs}
        wt_delta_perc_guess = x_dict.pop(KNOB_WTDELTAPERC, None)
        CdA_reduction_perc = x_dict.pop(KNOB_CDA, None)
        fc_peak_eff_guess = x_dict.pop(KNOB_FCPEAKEFF, None)
        fc_max_out_kw_guess = x_dict.pop(KNOB_FCMAXKW, None)
        max_ess_kwh_guess = x_dict.pop(KNOB_ess_max_kwh, None)
        max_motor_kw_guess = x_dict.pop(KNOB_mc_max_kw, None)
        fs_kwh_guess = x_dict.pop(KNOB_fs_kwh, None)

        print("MOO Final Solution:")
        if wt_delta_perc_guess is not None:
            print(KNOB_WTDELTAPERC.rjust(20, " "), f":{round(wt_delta_perc_guess, 4)}")
        if CdA_reduction_perc is not None:
            print(KNOB_CDA.rjust(20, " "), f":{round(CdA_reduction_perc, 4)}")
        if fc_peak_eff_guess is not None:
            print(KNOB_FCPEAKEFF.rjust(20, " "), f":{round(fc_peak_eff_guess, 4)}")
        if fc_max_out_kw_guess is not None:
            print(KNOB_FCMAXKW.rjust(20, " "), f":{round(fc_max_out_kw_guess, 4)}")
        if max_ess_kwh_guess is not None:
            print(KNOB_ess_max_kwh.rjust(20, " "), f":{round(max_ess_kwh_guess, 4)}")
        if max_motor_kw_guess is not None:
            print(KNOB_mc_max_kw.rjust(20, " "), f":{round(max_motor_kw_guess, 4)}")
        if fs_kwh_guess is not None:
            print(KNOB_fs_kwh.rjust(20, " "), f":{round(fs_kwh_guess, 4)}")

        _, _, out = self.get_objs(x, write_tsv=False)

        return out

cda_cost_coeff_a = kwargs.pop('cda_cost_coeff_a', 52.053) instance-attribute

cda_cost_coeff_b = kwargs.pop('cda_cost_coeff_b', 6.7018) instance-attribute

cda_mass_coeff_a = kwargs.pop('cda_mass_coeff_a', 0) instance-attribute

cda_mass_coeff_b = kwargs.pop('cda_mass_coeff_b', 1.79679) instance-attribute

cda_perc_imp_at_which_wt_penalty_maxes_out = kwargs.pop('cda_perc_imp_at_which_wt_penalty_maxes_out', 9999999999) instance-attribute

config: run_scenario.Config instance-attribute

constr_list = constr_list instance-attribute

designcycle: fastsim.cycle.Cycle instance-attribute

do_input_validation: bool instance-attribute

fc_cost_coeff_array = kwargs.pop('fc_cost_coeff_array', np.array([0, 0, 7, 16, 22, 40, 74])) instance-attribute

fc_eff_array = kwargs.pop('fc_eff_array', np.array([46, 46, 51, 51, 53, 53, 60]) / 100) instance-attribute

grade_accel_overshoot_tol = kwargs.pop('grade_accel_overshoot_tol', 0.01) instance-attribute

knobs = knobs instance-attribute

knobs_bounds = knobs_bounds instance-attribute

ltwt_delta_percs = kwargs.pop('ltwt_delta_percs', np.array([0, 5, 5, 10, 10, 25]) / 100) instance-attribute

ltwt_dol_per_kg_costs = kwargs.pop('ltwt_dol_per_kg_costs', np.array([4.41, 8.82, 13.23, 17.64, 22.05, 35.27])) instance-attribute

mooadvancedvehicle: fastsim.vehicle.Vehicle instance-attribute

moobasevehicle: fastsim.vehicle.Vehicle instance-attribute

obj_list = obj_list instance-attribute

opt_scenario: run_scenario.Scenario instance-attribute

optimize_pt = optimize_pt instance-attribute

range_overshoot_tol = kwargs.pop('range_overshoot_tol', None) instance-attribute

verbose = verbose instance-attribute

write_tsv = kwargs.pop('write_tsv', False) instance-attribute

__init__(knobs_bounds: dict, vnum: float, optimize_pt: str, obj_list: list = None, constr_list: list = None, verbose: bool = False, config: run_scenario.Config = None, do_input_validation: bool = False, **kwargs) -> None

This constructor initializes optimization input variables

Parameters:

Name Type Description Default
knobs_bounds dict

Dictionary containing knobs bounds for optimization

required
vnum float

Vehicle selection number

required
optimize_pt str

Vehicle powertrain type - Conv, BEV, HEV, PHEV. Defaults to gl.BEV.

required
obj_list list

List of objectives. Defaults to None.

None
constr_list list

List of constraints. Defaults to None.

None
verbose bool

if True, prints process steps. Defaults to False.

False
config run_scenario.Config

T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

None
Source code in t3co/moopack/moo.py
Python
def __init__(
    self,
    knobs_bounds: dict,
    vnum: float,
    optimize_pt: str,
    obj_list: list = None,
    constr_list: list = None,
    verbose: bool = False,
    config: run_scenario.Config = None,
    do_input_validation: bool = False,
    **kwargs,
) -> None:
    """
    This constructor initializes optimization input variables

    Args:
        knobs_bounds (dict): Dictionary containing knobs bounds for optimization
        vnum (float): Vehicle selection number
        optimize_pt (str, optional): Vehicle powertrain type - Conv, BEV, HEV, PHEV. Defaults to gl.BEV.
        obj_list (list, optional): List of objectives. Defaults to None.
        constr_list (list, optional): List of constraints. Defaults to None.
        verbose (bool, optional): if True, prints process steps. Defaults to False.
        config (run_scenario.Config, optional): T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

    """
    self.setup_opt_records()

    # TODO there should probably not be any default values for kwargs

    # TODO: figure out parallelization and then modify the following line accordingly
    _ = kwargs.pop("parallelization", None)

    # possible TODO: make this a dict for grade, accel, and range tolerance
    self.range_overshoot_tol = kwargs.pop("range_overshoot_tol", None)
    self.grade_accel_overshoot_tol = kwargs.pop(
        "grade_accel_overshoot_tol", 0.01
    )  # TODO default of 1% # user should explicitly turn this on and set this outside moo.py

    # MOO optimization interpolation arrays and coefficients
    # engine peak efficiecy percents
    self.fc_eff_array = kwargs.pop(
        "fc_eff_array", np.array([46, 46, 51, 51, 53, 53, 60]) / 100
    )
    # additional $ cost per kw of engine per engine efficiency percent
    self.fc_cost_coeff_array = kwargs.pop(
        "fc_cost_coeff_array", np.array([0, 0, 7, 16, 22, 40, 74])
    )
    # glider_kg light-weighted percents
    self.ltwt_delta_percs = kwargs.pop(
        "ltwt_delta_percs", np.array([0, 5, 5, 10, 10, 25]) / 100
    )
    # additional $ cost per kg light-weighted
    self.ltwt_dol_per_kg_costs = kwargs.pop(
        "ltwt_dol_per_kg_costs", np.array([4.41, 8.82, 13.23, 17.64, 22.05, 35.27])
    )
    # , if you want to keep default values, they should be 52.1 and 6.7
    # To make it like what I sent before, Arthur can recalculate (should be enough decimals):
    # a = 100*a
    # b = 10000*b - Alicia
    self.cda_cost_coeff_a = kwargs.pop("cda_cost_coeff_a", 52.053)
    self.cda_cost_coeff_b = kwargs.pop("cda_cost_coeff_b", 6.7018)
    self.cda_mass_coeff_a = kwargs.pop(
        "cda_mass_coeff_a", 0
    )  # default formula doesn't need a_M coeff, wt = 17967.9*p^2
    self.cda_mass_coeff_b = kwargs.pop("cda_mass_coeff_b", 1.79679)
    self.cda_perc_imp_at_which_wt_penalty_maxes_out = kwargs.pop(
        "cda_perc_imp_at_which_wt_penalty_maxes_out", 9999999999
    )

    # just checker values, knobs_bounds may not actually have all of these populated, so [0,0] is never used beyond the asserts
    max_fc_bound = knobs_bounds.get(KNOB_FCPEAKEFF, (0, 0))[1]
    max_ltwt_bound = knobs_bounds.get(KNOB_WTDELTAPERC, (0, 0))[1]
    CdA_perc_imp_knob_max = knobs_bounds.get(KNOB_CDA, (0, 0))[1]

    self.knobs_bounds = knobs_bounds

    knobs = [key for key in knobs_bounds.keys()]
    lower_bounds = anp.array([val[0] for bound, val in knobs_bounds.items()])
    upper_bounds = anp.array([val[1] for bound, val in knobs_bounds.items()])

    self.write_tsv = kwargs.pop("write_tsv", False)

    self.obj_list = obj_list
    if obj_list is None:
        # create default objective list
        self.obj_list = [TCO]

    self.constr_list = constr_list

    # add acceleration and grade overshoot tolerance constraint implicitly if user is apply grade or acceleration constraints
    if len(set(self.constr_list) & set([ACCEL30, ACCEL60, GRADE125, GRADE6])):
        self.constr_list.append(ACCEL_GRADE_OVERSHOOT)

    n_constr = len(self.constr_list)
    # input (f"moo.py: T3COProblem __init__\n{self.constr_list}, \neffective constraints: {n_constr}")
    # make it so that knobs always need to be specified.
    self.knobs = knobs

    self.verbose = verbose

    self.optimize_pt = optimize_pt

    self.instantiate_moo_vehicles_and_scenario(vnum, config, do_input_validation)

    # time dilation options, turned on for fuel efficiency cycle
    if "missed_trace_correction" in kwargs:
        self.opt_scenario.missed_trace_correction = kwargs.pop(
            "missed_trace_correction"
        )
        self.opt_scenario.max_time_dilation = kwargs.pop("max_time_dilation")
        self.opt_scenario.min_time_dilation = kwargs.pop("min_time_dilation")
        self.opt_scenario.time_dilation_tol = kwargs.pop("time_dilation_tol")

    n_obj = len(self.obj_list)

    n_args = len(knobs)

    # asserts
    assert (
        self.optimize_pt == self.mooadvancedvehicle.veh_pt_type
    ), "instantiate_moo_vehicles_and_scenario should have handled this"
    for knob in self.knobs:
        assert knob in KNOBS, f"knob {knob} not in defined parameters: {KNOBS}"
    for constraint in self.constr_list:
        assert (
            constraint in CONSTRAINTS
        ), f"constraint {constraint} not in defined constraints: {CONSTRAINTS}"
        if constraint == PHEV_MINIMIZE_FUEL_USE_CONSTRAINT:
            assert (
                self.opt_scenario.constraint_phev_minimize_fuel_use_percent > 0
                and self.opt_scenario.constraint_phev_minimize_fuel_use_percent < 1
            )
    assert len(OBJECTIVES) >= 1, "enable at least one objective"
    for obj in self.obj_list:
        assert (
            obj in OBJECTIVES
        ), f"objective {obj} not in defined objectives: {OBJECTIVES}"
    assert (
        set(self.constr_list) & set(self.obj_list) == set()
    ), f"cannot have constraints and objectives overlap constraints {self.constr_list} & {self.obj_list}"
    if KNOB_fs_kwh in self.knobs:
        assert (
            self.optimize_pt in [gl.CONV, gl.HEV, gl.PHEV]
        ), f"input knob bounds: {knobs_bounds}\naltering fuel store (tank, not battery) kwh size but vehicle powertrain to be optimized is {self.optimize_pt}"
    if KNOB_FCMAXKW in self.knobs:
        assert (
            self.optimize_pt in [gl.CONV, gl.HEV, gl.PHEV]
        ), f"input knob bounds: {knobs_bounds}\naltering fuel converter size but vehicle powertrain is {self.optimize_pt}"
    if KNOB_ess_max_kwh in self.knobs:
        assert (
            self.optimize_pt in [gl.BEV, gl.HEV, gl.PHEV]
        ), f"input knob bounds: {knobs_bounds}\naltering battery size but vehicle powertrain to be optimized is {self.optimize_pt}"
    if KNOB_mc_max_kw in self.knobs:
        assert (
            self.optimize_pt in [gl.BEV, gl.HEV, gl.PHEV]
        ), f"input knob bounds: {knobs_bounds}\naltering motor size but vehicle powertrain to be optimized is {self.optimize_pt}"
    assert (
        max_fc_bound <= max(self.fc_eff_array)
    ), f"max eng eff knob val greater than max of eng eff array {max_fc_bound} > {self.fc_eff_array}, could yield free efficiency"
    assert (
        max_ltwt_bound <= max(self.ltwt_delta_percs)
    ), f"max light-weight knob val greater than max of light-weighting percent array {max_ltwt_bound} > {self.ltwt_delta_percs}, could yield free light-weighting"
    assert (
        max_fc_bound <= 1
    ), f"knob bounds must be decimal format, max_fc_bound: {max_fc_bound}"
    assert (
        max_ltwt_bound <= 1
    ), f"knob bounds must be decimal format, max_ltwt_bound: {max_ltwt_bound}"
    assert (
        CdA_perc_imp_knob_max <= 1
    ), f"knob bounds must be decimal format, CdA_perc_imp_knob_max: {CdA_perc_imp_knob_max}"

    # n_ieq_constr, number of constraints that must yield < 0
    super().__init__(
        n_var=n_args,
        n_obj=n_obj,
        n_ieq_constr=n_constr,
        # lower bounds
        xl=lower_bounds,
        # upper bounds
        xu=upper_bounds,
        **kwargs,
    )

    if len(kwargs) > 0:
        warnings.warn(
            f"Possible unused/invalid kwargs provided:\n {list(kwargs.keys())}"
        )

adjust_fc_peak_eff(fc_peak_eff: float, scenario: run_scenario.Scenario, optvehicle: fastsim.vehicle.Vehicle) -> None

This method augments an advanced vehicle fc_eff_array based on new fc_peak_eff using baseline fc_eff_array

Parameters:

Name Type Description Default
fc_peak_eff float

Fuel converter peak efficiency override

required
scenario run_scenario.Scenario

Scenario object of current selection

required
optvehicle fastsim.vehicle.Vehicle

FASTSim vehicle object of optimization vehicle

required
Source code in t3co/moopack/moo.py
Python
def adjust_fc_peak_eff(
    self,
    fc_peak_eff: float,
    scenario: run_scenario.Scenario,
    optvehicle: fastsim.vehicle.Vehicle,
) -> None:
    """
    This method augments an advanced vehicle fc_eff_array based on new fc_peak_eff using baseline fc_eff_array


    Args:
        fc_peak_eff (float): Fuel converter peak efficiency override
        scenario (run_scenario.Scenario): Scenario object of current selection
        optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object of optimization vehicle
    """
    old_peak_eff = scenario.origfc_eff_map.max()
    optvehicle.fc_eff_map = scenario.origfc_eff_map * (
        1 + (fc_peak_eff - old_peak_eff) / old_peak_eff
    )
    optvehicle.set_derived()

cda_percent_delta_knob(CdA_perc_reduction: str, optvehicle: fastsim.vehicle.Vehicle) -> None

This method sets the drag_coef based on aero improvement curve and glider_kg based on cda_cost_coeff_a and cda_cost_coeff_b

Parameters:

Name Type Description Default
CdA_perc_reduction str

Name of aero improvement curve file

required
optvehicle fastsim.vehicle.Vehicle

FASTSim vehicle object for optimization vehicle

required
Source code in t3co/moopack/moo.py
Python
def cda_percent_delta_knob(
    self, CdA_perc_reduction: str, optvehicle: fastsim.vehicle.Vehicle
) -> None:
    """
    This method sets the drag_coef based on aero improvement curve and glider_kg based on cda_cost_coeff_a and cda_cost_coeff_b

    Args:
        CdA_perc_reduction (str): Name of aero improvement curve file
        optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object for optimization vehicle

    """

    # check value of CdA_reduction
    # print(CdA_perc_reduction)

    # functions that return cost, weight, and possibly other penalties w.r.t. various improvements

    def get_cost_per_CdA_delta(x):
        """
        Returns cost penalty given percent improvement in CdA.
        """
        # COST:  a * cda_perc + b * cda_perc^2
        a_C = self.cda_cost_coeff_a
        b_C = self.cda_cost_coeff_b
        x = x * 100
        return a_C * x + b_C * x**2

    def get_mass_per_CdA_delta(x):
        """
        Returns weight penalty [Lb converted to kg] given % improvement in CdA.
        These formulas are developed in pounds. Converted to kg for FASTSim at the end.
        """
        # MASS: a * cda_perc + b * cda_perc^2
        a_M = (
            self.cda_mass_coeff_a
        )  # default formula doesn't need a_M coeff, wt = 17967.9*p^2
        b_M = self.cda_mass_coeff_b
        x = x * 100
        CdA_perc_imp_at_which_wt_penalty_maxes_out = (
            self.cda_perc_imp_at_which_wt_penalty_maxes_out
        )
        return min(
            gl.lbs_to_kgs(
                a_M * CdA_perc_imp_at_which_wt_penalty_maxes_out
                + b_M * CdA_perc_imp_at_which_wt_penalty_maxes_out**2
            ),
            gl.lbs_to_kgs(a_M * x + b_M * x**2),
        )

    # reset drag coefficient based on percent improvement
    optvehicle.drag_coef = max(
        0.01,  # this is an additional hard-coded bound on drag coefficient that should probably be removed (throw error or warning if drag_coef < 0.01 instead of running and replacing with this default value)
        self.opt_scenario.originaldrag_coef * (1 - CdA_perc_reduction),
    )
    # glider cost penalty due to CdA improvement
    CdA_cost = get_cost_per_CdA_delta(CdA_perc_reduction)
    self.opt_scenario.vehicle_glider_cost_dol = (
        self.opt_scenario.vehicle_glider_cost_dol + CdA_cost
    )
    # mass adjustments. Add CdA mass FIRST, if applicable. Then limit cargo_kg if overweight from battery.
    optvehicle.glider_kg = optvehicle.glider_kg + get_mass_per_CdA_delta(
        CdA_perc_reduction
    )
    optvehicle.set_veh_mass()

compile_reporting_vars() -> None

This method creates an output dictionary containing optimization results

Source code in t3co/moopack/moo.py
Python
def compile_reporting_vars(self) -> None:
    """
    This method creates an output dictionary containing optimization results
    """
    d = {
        "objective_TCOs": self.r_tcos,
        "objective_fc_khw_percent": self.r_cd_fc_kwh_percent,
        "objective_fc_khw_used": self.r_cd_fc_kwh_used,
        "objective_elec_khw_used": self.r_cd_elec_kwh_used,
        "r_mph_ach_grade_6s": self.r_grade_6s,
        "r_mph_ach_grade_1p25s": self.r_grade_1p25s,
        "r_sec_to_ach_30mph_ldd": self.r_accel_30l,
        "r_sec_to_ach_60mph_ldd": self.r_accel_60l,
        "r_ach_ranges_mi": self.r_ranges,
        "target_mph_grade_6s": [
            self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph
        ]
        * len(self.r_grade_6s),
        "target_mph_grade_1p25s": [
            self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
        ]
        * len(self.r_grade_6s),
        "target_sec_to_30mph_ldd": [self.opt_scenario.max_time_0_to_30mph_at_gvwr_s]
        * len(self.r_grade_6s),
        "target_sec_to_60mph_ldd": [self.opt_scenario.max_time_0_to_60mph_at_gvwr_s]
        * len(self.r_grade_6s),
        "target_range_mi": [self.opt_scenario.target_range_mi]
        * len(self.r_grade_6s),
        "r_ach_fuel_efficiencies": self.r_fuel_efficiencies,
        "accel_30_constraint_vals": self.accel_30_constraint,
        "accel_60_constraint_vals": self.accel_60_constraint,
        "grade_6_constraint_vals": self.grade_6_constraint,
        "grade_1p25_constraint_vals": self.grade_1p25_constraint,
        "range_constraint_vals": self.range_constraint,
        "grade_accel_overshoot_tol_constraint_vals": self.grade_accel_overshoot_tol_constraint,
        "trace_miss_dist_percent_constraint_vals": self.trace_miss_distance_percent_constraint_record,
        "phev_min_fuel_use_pct_constraint_vals": self.phev_min_fuel_use_prcnt_const_record,
        "r_wt_delta_perc_guess": self.r_wt_delta_perc_guess,
        "r_CdA_reduction_perc": self.r_CdA_reduction_perc,
        "r_fc_peak_eff_guess": self.r_fc_peak_eff_guess,
        "r_fc_max_out_kw_guess": self.r_fc_max_out_kw_guess,
        "r_fs_kwh_guess": self.r_fs_kwh_guess,
        "r_max_ess_kwh_guess": self.r_max_ess_kwh_guess,
        "r_max_motor_kw_guess": self.r_max_motor_kw_guess,
    }
    self.reporting_vars = pd.DataFrame(data=d)

fc_peak_eff_knob(fc_peak_eff: float, optvehicle: fastsim.vehicle.Vehicle) -> None

This method sets the knob from the engine efficiency curve

Parameters:

Name Type Description Default
fc_peak_eff float

Fuel converter peak effiency override from engine efficiency improvement curve

required
optvehicle fastsim.vehicle.Vehicle

FASTSim vehicle object for optimization vehicle

required
Source code in t3co/moopack/moo.py
Python
def fc_peak_eff_knob(
    self, fc_peak_eff: float, optvehicle: fastsim.vehicle.Vehicle
) -> None:
    """
    This method sets the knob from the engine efficiency curve

    Args:
        fc_peak_eff (float): Fuel converter peak effiency override from engine efficiency improvement curve
        optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object for optimization vehicle
    """
    fc_eff_array = self.fc_eff_array
    cost_coeff_array = self.fc_cost_coeff_array
    d_eff_dol_per_kw = np.interp(fc_peak_eff, fc_eff_array, cost_coeff_array)
    self.opt_scenario.fc_ice_cost_dol_per_kw = (
        self.opt_scenario.originalIceDolPerKw + d_eff_dol_per_kw
    )
    self.adjust_fc_peak_eff(fc_peak_eff, self.opt_scenario, optvehicle)

get_objs(x: dict, write_tsv: bool = False) -> Tuple[np.array, np.array, dict]

This method gets called when PyMoo calls _evaluate. It initializes objectives and constraints and runs vehicle_scenario_sweep

x optimization knobs = [max motor kw, battery kwh, drag coeff % improvement] Function for running FE cycles and accel tests then returning fuel consumption and zero-to-sixty times.

x is a set of genes (or parameters), so kwh size is a gene chromosome is a full gene, all values in x

Parameters:

Name Type Description Default
x dict

Dictionary containing optimization knobs - {max motor kw, battery kwh, drag coeff % improvement}

required
write_tsv bool

if True, save intermediate dataframes. Defaults to False.

False

Returns:

Name Type Description
obj_arr_F np.array

Array of objectives - tot_cost and phev_cd_fuel_used_kwh

constraint_results_G np.array

Array of constraints

rs_sweep dict

Output dictionary from vehicle_scenario_sweep

Source code in t3co/moopack/moo.py
Python
def get_objs(
    self, x: dict, write_tsv: bool = False
) -> Tuple[np.array, np.array, dict]:
    """
    This method gets called when PyMoo calls _evaluate. It initializes objectives and constraints and runs vehicle_scenario_sweep

    x optimization knobs = [max motor kw, battery kwh, drag coeff % improvement]
    Function for running FE cycles and accel tests then returning
    fuel consumption and zero-to-sixty times.

    x is a set of genes (or parameters), so kwh size is a gene
    chromosome is a full gene, all values in x

    Args:
        x (dict): Dictionary containing optimization knobs - {max motor kw, battery kwh, drag coeff % improvement}
        write_tsv (bool, optional): if True, save intermediate dataframes. Defaults to False.

    Returns:
        obj_arr_F (np.array): Array of objectives - tot_cost and phev_cd_fuel_used_kwh
        constraint_results_G (np.array): Array of constraints
        rs_sweep (dict): Output dictionary from vehicle_scenario_sweep
    """

    # dict for providing mechanism for making sure all knobs get used
    x_dict = {knob: x[self.knobs.index(knob)] for knob in self.knobs}

    # if self.optimize_pt not in [gl.CONV, gl.BEV, gl.HEV]:
    #     raise TypeError(f"optimize_pt is not configured for {self.optimize_pt}")
    designcycle = self.designcycle
    optvehicle = self.mooadvancedvehicle

    # reset glider price and weight for light-weighting and/or CdA percent improvement
    self.opt_scenario.vehicle_glider_cost_dol = (
        self.opt_scenario.originalGliderPrice
    )
    optvehicle.glider_kg = self.opt_scenario.originalglider_kg

    wt_delta_perc_guess = x_dict.pop(KNOB_WTDELTAPERC, None)
    CdA_reduction_perc = x_dict.pop(KNOB_CDA, None)
    fc_peak_eff_guess = x_dict.pop(KNOB_FCPEAKEFF, None)
    fc_max_out_kw_guess = x_dict.pop(KNOB_FCMAXKW, None)
    max_ess_kwh_guess = x_dict.pop(KNOB_ess_max_kwh, None)
    max_motor_kw_guess = x_dict.pop(KNOB_mc_max_kw, None)
    fs_kwh_guess = x_dict.pop(KNOB_fs_kwh, None)
    # set knobs
    if "wt_delta_perc" in self.knobs:
        # confirmed with Alicia and Jason on 8/11/2021 that light-weighting should occur before CdA adjustment
        self.weight_delta_percent_knob(wt_delta_perc_guess, optvehicle)
    if "CdA_perc_imp" in self.knobs:
        self.cda_percent_delta_knob(CdA_reduction_perc, optvehicle)
    if "fc_peak_eff" in self.knobs:
        self.fc_peak_eff_knob(fc_peak_eff_guess, optvehicle)
    if "fcMaxOutKw" in self.knobs:
        run_scenario.set_max_fuel_converter_kw(optvehicle, fc_max_out_kw_guess)
    if KNOB_fs_kwh in self.knobs:
        run_scenario.set_fuel_store_kwh(optvehicle, fs_kwh_guess)
    if "ess_max_kwh" in self.knobs:
        run_scenario.set_max_battery_kwh(optvehicle, max_ess_kwh_guess)
    if "mc_max_kw" in self.knobs:
        run_scenario.set_max_motor_kw(
            optvehicle, self.opt_scenario, max_motor_kw_guess
        )

    # enforce 0 <= cargo kg <= initial cargo kg for BEV and HEV optimizations
    if self.optimize_pt in [gl.BEV, gl.HEV]:
        run_scenario.limit_cargo_kg_for_moo_hev_bev(self.opt_scenario, optvehicle)

    # # TODO, this calculation and application of payload loss is still not finalized.
    # this also needs to be used for FCEV, would be nice if there were an FCEV veh_pt_type!!!
    # also it needs to have a Scenario File on/off activation

    assert len(x_dict) == 0, f"Unapplied knobs: {list(x_dict.keys())}"

    # calculate objectives
    get_accel_loaded = False
    get_grade = False
    obj_arr_F = []
    g6_acvhd, g125_acvhd, z60l_acvhd, z30l_acvhd, tco_acvhd, mpgge, range_achvd = [
        None
    ] * 7
    if GRADE6 in self.constr_list or GRADE125 in self.constr_list:
        get_grade = True
    if ACCEL30 in self.constr_list or ACCEL60 in self.constr_list:
        get_accel_loaded = True

    rs_sweep = run_scenario.vehicle_scenario_sweep(
        optvehicle,
        self.opt_scenario,
        designcycle,
        verbose=self.verbose,
        get_accel=False,  # don't want non-loaded accel values, for now
        get_accel_loaded=get_accel_loaded,
        get_gradeability=get_grade,
        write_tsv=write_tsv,
    )

    mpgge = rs_sweep["mpgge"]
    g6_acvhd, g125_acvhd = (
        rs_sweep["grade_6_mph_ach"],
        rs_sweep["grade_1_25_mph_ach"],
    )
    z60l_acvhd, z30l_acvhd = (
        rs_sweep["zero_to_60_loaded"],
        rs_sweep["zero_to_30_loaded"],
    )
    tco_acvhd = rs_sweep["tot_cost"]
    if optvehicle.veh_pt_type in [gl.BEV, gl.CONV, gl.HEV]:
        range_achvd = rs_sweep["primary_fuel_range_mi"]
    elif optvehicle.veh_pt_type == gl.PHEV:
        # need range from PHEV that is used to compare to target_range_mi
        range_achvd = rs_sweep["cd_aer_phev_range_mi"]

    if self.verbose:
        print(
            f"grade_6_mph_ach {g6_acvhd} grade_1_25_mph_ach {g125_acvhd} zero_to_60_loaded {z60l_acvhd} "
            f"zero_to_30_loaded {z30l_acvhd} tco {tco_acvhd} mpgge {mpgge} range {range_achvd}"
        )

    # PHEV fuel usage stats
    pct_fc_kwh = None
    phev_cd_fuel_used_kwh = None
    phev_cd_battery_used_kwh = None
    if (
        PHEV_MINIMIZE_FUEL_USE_OBJECTIVE in self.obj_list
        or PHEV_MINIMIZE_FUEL_USE_CONSTRAINT in self.constr_list
    ):
        phev_cd_fuel_used_kwh = rs_sweep["mpgge"]["cd_fuel_used_kwh_total"]
        phev_cd_battery_used_kwh = rs_sweep["mpgge"]["cd_battery_used_kwh"]
        pct_fc_kwh = round(
            phev_cd_fuel_used_kwh
            / (phev_cd_battery_used_kwh + phev_cd_fuel_used_kwh),
            2,
        )
    #                                                           #
    # ********************** objectives  ********************** #
    #                                                           #
    if TCO in self.obj_list:
        obj_arr_F.append(tco_acvhd)
    if PHEV_MINIMIZE_FUEL_USE_OBJECTIVE in self.obj_list:
        obj_arr_F.append(phev_cd_fuel_used_kwh)
    #                                                           #
    # ******************** end objectives  ******************** #
    #                                                           #

    #                                                           #
    # ********************** constraints ********************** #
    #
    #                                                           #

    self.accel_30_constraint.append(None)
    self.accel_60_constraint.append(None)
    self.grade_6_constraint.append(None)
    self.grade_1p25_constraint.append(None)
    self.range_constraint.append(None)
    self.grade_accel_overshoot_tol_constraint.append(None)
    self.c_rate_constraint.append(None)
    self.trace_miss_distance_percent_constraint_record.append(None)
    self.phev_min_fuel_use_prcnt_const_record.append(None)

    constraint_results_G = []
    # calculate constraint violations
    # speed at grade minus target should be negative when constraint is met
    if GRADE6 in self.constr_list:
        constraint_results_G.append(
            self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph - g6_acvhd
        )
    if GRADE125 in self.constr_list:
        constraint_results_G.append(
            self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph - g125_acvhd
        )
    if ACCEL60 in self.constr_list:
        # zero-to-speed time should minus max allowable (target value) should
        # be negative when constraint is met
        # 9 sec achvd - 10 sec target = -1
        constraint_results_G.append(
            z60l_acvhd - self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
        )
    if ACCEL30 in self.constr_list:
        constraint_results_G.append(
            z30l_acvhd - self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
        )

    # calculate limiting grade/accel requirement if all constraints met
    if ACCEL_GRADE_OVERSHOOT in self.constr_list:
        constr_perc = {}
        if GRADE125 in self.constr_list:
            # todo, -abs() for all of these?
            g125c = (
                -(
                    g125_acvhd
                    - self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
                )
                / self.opt_scenario.min_speed_at_1p25pct_grade_in_5min_mph
            )
            constr_perc[GRADE125] = g125c
            self.grade_1p25_constraint[-1] = g125c
        if GRADE6 in self.constr_list:
            g6c = (
                -(g6_acvhd - self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph)
                / self.opt_scenario.min_speed_at_6pct_grade_in_5min_mph
            )
            constr_perc[GRADE6] = g6c
            self.grade_6_constraint[-1] = g6c
        if ACCEL60 in self.constr_list:
            z60c = (
                z60l_acvhd - self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
            ) / self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
            constr_perc[ACCEL60] = z60c
            self.accel_60_constraint[-1] = z60c
        if ACCEL30 in self.constr_list:
            z30c = (
                z30l_acvhd - self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
            ) / self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
            constr_perc[ACCEL30] = z30c
            self.accel_30_constraint[-1] = z30c

        # if all constraints meet target, then find closest overshoot and ensure it's below tolerance
        if (len(constr_perc) > 0) & (
            np.array(list(constr_perc.values())) < 0
        ).all():
            # say largest negative percent is -.1 (closest to target, 10% exceeding)
            # tolerance is .05
            # -(-.1) - 0.05 = 0.05, >= 0, constraint is violated. Constraints are in bounds if they return < 0
            min_grade_accel_excess = (
                -constr_perc[max(constr_perc, key=constr_perc.get)]
                - self.grade_accel_overshoot_tol
            )
        else:
            min_grade_accel_excess = -1  # no constraint to satisfy
        constraint_results_G.append(min_grade_accel_excess)
        self.grade_accel_overshoot_tol_constraint[-1] = min_grade_accel_excess

    if RANGE in self.constr_list:
        # if you fall short of range target
        if self.range_overshoot_tol is None:
            range_mi_cv = (
                self.opt_scenario.target_range_mi - range_achvd
            )  # pos return, failed
        else:
            if range_achvd <= self.opt_scenario.target_range_mi:
                range_mi_cv = (
                    self.opt_scenario.target_range_mi - range_achvd
                )  # pos return, failed
            else:
                range_mi_cv = range_achvd - (
                    self.opt_scenario.target_range_mi
                    * (1 + self.range_overshoot_tol)
                )

        constraint_results_G.append(range_mi_cv)
        self.range_constraint[-1] = range_mi_cv

    # c rate constraint
    if C_RATE in self.constr_list:
        self.c_rate_constraint[-1] = (
            self.mooadvancedvehicle.ess_max_kw / self.mooadvancedvehicle.ess_max_kwh
            - np.interp(
                self.mooadvancedvehicle.ess_max_kwh,
                # TODO, this 2D array needs to be an input
                [1.0, 10.0, 188.0, 660.0],  # battery sizes kwh
                [24.0, 12.0, 2.0, 0.7],  # c rates (kw/kwh)
            )
        )
        constraint_results_G.append(self.c_rate_constraint[-1])

    # # trace miss constraint
    if TRACE_MISS_DIST_PERCENT in self.constr_list:
        assert (
            self.opt_scenario.trace_miss_dist_percent > 0
            and self.opt_scenario.trace_miss_dist_percent < 1
        ), "scenario file input trace_miss_dist_percent must be decimal value greater than 0 and less than 1"
        cycle_records = rs_sweep["design_cycle_sim_drive_record"]
        max_dist_frac_result = max(
            sdr.trace_miss_dist_frac for sdr in cycle_records
        )
        # .1 -> 10%
        max_dist_frac_miss = self.opt_scenario.trace_miss_dist_percent
        constraint_results_G.append(max_dist_frac_result - max_dist_frac_miss)
        self.trace_miss_distance_percent_constraint_record[-1] = (
            max_dist_frac_result - max_dist_frac_miss
        )

    if PHEV_MINIMIZE_FUEL_USE_CONSTRAINT in self.constr_list:
        assert (
            self.opt_scenario.constraint_phev_minimize_fuel_use_percent > 0
        ), "scenario.constraint_phev_minimize_fuel_use_percent must be value > 0 and < 1"
        assert (
            self.opt_scenario.constraint_phev_minimize_fuel_use_percent < 1
        ), "scenario.constraint_phev_minimize_fuel_use_percent must be value > 0 and < 1"
        constraint_results_G.append(
            pct_fc_kwh - self.opt_scenario.constraint_phev_minimize_fuel_use_percent
        )
        self.phev_min_fuel_use_prcnt_const_record[-1] = (
            pct_fc_kwh - self.opt_scenario.constraint_phev_minimize_fuel_use_percent
        )

    #                                                           #
    # ******************** end constraints ******************** #
    #                                                           #

    # append reporting variables
    # obj vars
    self.r_tcos.append(tco_acvhd)
    self.r_cd_fc_kwh_percent.append(pct_fc_kwh)
    self.r_cd_fc_kwh_used.append(phev_cd_fuel_used_kwh)
    self.r_cd_elec_kwh_used.append(phev_cd_battery_used_kwh)

    self.r_grade_6s.append(g6_acvhd)
    self.r_grade_1p25s.append(g125_acvhd)
    self.r_accel_60l.append(z60l_acvhd)
    self.r_accel_30l.append(z30l_acvhd)
    self.r_fuel_efficiencies.append(mpgge)
    self.r_ranges.append(range_achvd)
    self.r_wt_delta_perc_guess.append(wt_delta_perc_guess)
    self.r_CdA_reduction_perc.append(CdA_reduction_perc)
    self.r_fc_peak_eff_guess.append(fc_peak_eff_guess)
    self.r_fc_max_out_kw_guess.append(fc_max_out_kw_guess)
    self.r_fs_kwh_guess.append(fs_kwh_guess)
    self.r_max_ess_kwh_guess.append(max_ess_kwh_guess)
    self.r_max_motor_kw_guess.append(max_motor_kw_guess)

    return np.array(obj_arr_F), np.array(constraint_results_G), rs_sweep

get_tco_from_moo_advanced_result(x: dict) -> dict

This method is a utility function to get detailed TCO information from optimized MOO result

Parameters:

Name Type Description Default
x dict

Dictionary containing optimization knobs - [max motor kw, battery kwh, drag coeff % improvement]

required

Returns:

Name Type Description
out dict

Dictionary containing TCO results for optimization runs

Source code in t3co/moopack/moo.py
Python
def get_tco_from_moo_advanced_result(self, x: dict) -> dict:
    """
    This method is a utility function to get detailed TCO information from optimized MOO result

    Args:
        x (dict): Dictionary containing optimization knobs - [max motor kw, battery kwh, drag coeff % improvement]

    Returns:
        out (dict): Dictionary containing TCO results for optimization runs
    """
    x_dict = {knob: x[self.knobs.index(knob)] for knob in self.knobs}
    wt_delta_perc_guess = x_dict.pop(KNOB_WTDELTAPERC, None)
    CdA_reduction_perc = x_dict.pop(KNOB_CDA, None)
    fc_peak_eff_guess = x_dict.pop(KNOB_FCPEAKEFF, None)
    fc_max_out_kw_guess = x_dict.pop(KNOB_FCMAXKW, None)
    max_ess_kwh_guess = x_dict.pop(KNOB_ess_max_kwh, None)
    max_motor_kw_guess = x_dict.pop(KNOB_mc_max_kw, None)
    fs_kwh_guess = x_dict.pop(KNOB_fs_kwh, None)

    print("MOO Final Solution:")
    if wt_delta_perc_guess is not None:
        print(KNOB_WTDELTAPERC.rjust(20, " "), f":{round(wt_delta_perc_guess, 4)}")
    if CdA_reduction_perc is not None:
        print(KNOB_CDA.rjust(20, " "), f":{round(CdA_reduction_perc, 4)}")
    if fc_peak_eff_guess is not None:
        print(KNOB_FCPEAKEFF.rjust(20, " "), f":{round(fc_peak_eff_guess, 4)}")
    if fc_max_out_kw_guess is not None:
        print(KNOB_FCMAXKW.rjust(20, " "), f":{round(fc_max_out_kw_guess, 4)}")
    if max_ess_kwh_guess is not None:
        print(KNOB_ess_max_kwh.rjust(20, " "), f":{round(max_ess_kwh_guess, 4)}")
    if max_motor_kw_guess is not None:
        print(KNOB_mc_max_kw.rjust(20, " "), f":{round(max_motor_kw_guess, 4)}")
    if fs_kwh_guess is not None:
        print(KNOB_fs_kwh.rjust(20, " "), f":{round(fs_kwh_guess, 4)}")

    _, _, out = self.get_objs(x, write_tsv=False)

    return out

instantiate_moo_vehicles_and_scenario(vnum: int, config: run_scenario.Config = None, do_input_validation: bool = False) -> None

This method instantiates the multi-objective optimization problem vehicles and scenarios, starting with the baseline Conventional vehicle.

Parameters:

Name Type Description Default
vnum int

vehicle selection number

required
config run_scenario.Config

T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

None

Raises:

Type Description
TypeError

Invalid optimize_pt selection

Source code in t3co/moopack/moo.py
Python
def instantiate_moo_vehicles_and_scenario(
    self,
    vnum: int,
    config: run_scenario.Config = None,
    do_input_validation: bool = False,
) -> None:
    """
    This method instantiates the multi-objective optimization problem vehicles and scenarios, starting with the baseline Conventional vehicle.

    Args:
        vnum (int): vehicle selection number
        config (run_scenario.Config, optional): T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

    Raises:
        TypeError: Invalid optimize_pt selection
    """
    self.moobasevehicle = run_scenario.get_vehicle(
        vnum,
        veh_input_path=config.vehicle_file,
    )

    self.opt_scenario, self.designcycle = run_scenario.get_scenario_and_cycle(
        vnum,
        config.scenario_file,
        config=config,
        do_input_validation=do_input_validation,
    )

    if (
        self.opt_scenario.fuel_type == "diesel and electricity"
        and self.moobasevehicle.veh_pt_type == gl.HEV
    ):
        self.opt_scenario.fuel_type = "diesel"

    # save baseline values for optimization diffs
    self.opt_scenario.originalGliderPrice = (
        self.opt_scenario.vehicle_glider_cost_dol
    )
    self.opt_scenario.originalglider_kg = self.moobasevehicle.glider_kg
    self.opt_scenario.originalIceDolPerKw = self.opt_scenario.fc_ice_cost_dol_per_kw
    self.opt_scenario.origfc_eff_map = self.moobasevehicle.fc_eff_map.copy()
    self.opt_scenario.originalcargo_kg = self.moobasevehicle.cargo_kg
    self.opt_scenario.originaldrag_coef = self.moobasevehicle.drag_coef

    self.mooadvancedvehicle = copy.copy(self.moobasevehicle)

    if self.optimize_pt == gl.BEV:
        self.mooadvancedvehicle.veh_pt_type = gl.BEV
        run_scenario.set_max_fuel_converter_kw(self.mooadvancedvehicle, 0)
        # change to 0, based on Excel version direction when trying to run as EV
        self.mooadvancedvehicle.fc_max_out_kw = 0
        run_scenario.set_fuel_store_kwh(self.mooadvancedvehicle, 0)
    elif self.optimize_pt == gl.CONV:
        self.mooadvancedvehicle.veh_pt_type = gl.CONV
        run_scenario.set_max_motor_kw(
            self.mooadvancedvehicle, scenario=None, max_motor_kw=0
        )
        run_scenario.set_max_battery_kwh(self.mooadvancedvehicle, max_ess_kwh=0)
    elif self.optimize_pt == gl.HEV:
        self.mooadvancedvehicle.veh_pt_type = gl.HEV
        # nothing to zero out for HEV/FCEV
    elif self.optimize_pt == gl.PHEV:
        self.mooadvancedvehicle.veh_pt_type = gl.PHEV
    else:
        raise TypeError(f"invalid optimize_pt selection {self.optimize_pt}")

setup_opt_records()

This method sets up the empty optimization record arrays

Source code in t3co/moopack/moo.py
Python
def setup_opt_records(self):
    """
    This method sets up the empty optimization record arrays
    """
    #

    # objectives
    self.r_tcos = []
    self.r_cd_fc_kwh_percent = []
    self.r_cd_fc_kwh_used = []
    self.r_cd_elec_kwh_used = []

    self.reporting_vars = None
    self.r_grade_6s = []
    self.r_grade_1p25s = []
    self.r_accel_30l = []
    self.r_accel_60l = []
    self.r_ranges = []
    self.r_fuel_efficiencies = []
    self.r_wt_delta_perc_guess = []
    self.r_CdA_reduction_perc = []
    self.r_fc_peak_eff_guess = []
    self.r_fc_max_out_kw_guess = []
    self.r_fs_kwh_guess = []
    self.r_max_ess_kwh_guess = []
    self.r_max_motor_kw_guess = []
    # constraint records
    self.accel_30_constraint = []
    self.accel_60_constraint = []
    self.grade_6_constraint = []
    self.grade_1p25_constraint = []
    self.range_constraint = []
    self.grade_accel_overshoot_tol_constraint = []
    self.c_rate_constraint = []
    self.trace_miss_distance_percent_constraint_record = []
    self.phev_min_fuel_use_prcnt_const_record = []

sweep_knob(knob: list, definition: int = 100, plot: bool = False, optres: float = None, **kwargs) -> list

This method sweeps the optimization knob of vehicle from lbound to ubound, return TCO plot optres to see if there's agreement from opt solution and your sweep

Parameters:

Name Type Description Default
knob list

list of knobs names for optimization

required
definition int

Number of points. Defaults to 100.

100
plot bool

if True, saves plot of bounds and TCOs. Defaults to False.

False
optres float

Optimization resolution. Defaults to None.

None

Returns:

Name Type Description
tcos list

List of TCOs of length=definition

Source code in t3co/moopack/moo.py
Python
def sweep_knob(
    self,
    knob: list,
    definition: int = 100,
    plot: bool = False,
    optres: float = None,
    **kwargs,
) -> list:
    """
    This method sweeps the optimization knob of vehicle from lbound to ubound, return TCO \
        plot optres to see if there's agreement from opt solution and your sweep

    Args:
        knob (list): list of knobs names for optimization
        definition (int, optional): Number of points. Defaults to 100.
        plot (bool, optional): if True, saves plot of bounds and TCOs. Defaults to False.
        optres (float, optional): Optimization resolution. Defaults to None.

    Returns:
        tcos (list): List of TCOs of length=definition
    """
    label = kwargs.get("label", "")
    sweep_knob_accel_test = kwargs.get("sweep_knob_accel_test", False)
    # save knobs and obj list
    temp_obj_list = self.obj_list
    self.knobs = [knob]
    self.obj_list = [TCO]
    lbound, ubound = self.knobs_bounds[knob]
    tcos = [self.get_objs([x])[0] for x in np.linspace(lbound, ubound, definition)]

    if sweep_knob_accel_test:
        opt_res_kw = np.inf
        if optres is not None:
            opt_res_kw = optres
        assert knob in [KNOB_mc_max_kw, KNOB_FCMAXKW]
        self.obj_list = [ACCEL30, ACCEL60]
        # objs.extend([z60l, z30l]) {(60, 30),...(60, 30)}
        thirty_mph_times = []
        sixty_mph_times = []
        largest_infeasible_30s_size = None
        failed_30_s_time = None
        largest_infeasible_60s_size = None
        failed_60_s_time = None
        for x in np.linspace(lbound, ubound, definition):
            z60l, z30l = self.get_objs([x])
            thirty_mph_times.append([z30l, x])
            sixty_mph_times.append([z60l, x])
        for res in sixty_mph_times:
            z60l, x = res
            if (
                z60l > self.opt_scenario.max_time_0_to_60mph_at_gvwr_s
                and x < opt_res_kw
            ):
                largest_infeasible_60s_size = x
                failed_60_s_time = z60l
        for res in sixty_mph_times:
            z30l, x = res
            if (
                z30l > self.opt_scenario.max_time_0_to_30mph_at_gvwr_s
                and x < opt_res_kw
            ):
                largest_infeasible_30s_size = x
                failed_30_s_time = z30l

    # todo, figure out how to get mpgge in the other y axis
    if plot:
        resdir = gl.MOO_KNOB_SWEEP_PLOTS_DIR
        if not resdir.exists():
            resdir.mkdir()
        ts = strftime("%Y-%m-%d_%H-%M-%S", gmtime())
        plt.figure(figsize=(7, 4))
        plt.ylabel("$TCO")
        plt.title(f"{label}_{ts}_{knob}")
        plt.xlabel(f"knob value for {definition} pts")
        plt.plot(
            np.linspace(lbound, ubound, definition),
            tcos,
            label=f"TCO from global optimum, sweeping {knob}",
        )
        if optres is not None:
            plt.vlines(
                optres,
                min(tcos),
                max(tcos),
                color="red",
                label=f"MOO {knob} solution: {optres}",
            )
        if sweep_knob_accel_test:
            if largest_infeasible_30s_size is not None:
                plt.vlines(
                    largest_infeasible_30s_size,
                    min(tcos),
                    max(tcos),
                    color="orange",
                    label=f"failed accel test 0 to 30 res: {failed_30_s_time} target: {self.opt_scenario.max_time_0_to_30mph_at_gvwr_s} {knob} size: {largest_infeasible_30s_size}",
                )
            if largest_infeasible_60s_size is not None:
                plt.vlines(
                    largest_infeasible_60s_size,
                    min(tcos),
                    max(tcos),
                    color="yellow",
                    label=f"failed accel test 0 to 60 res: {failed_60_s_time} target: {self.opt_scenario.max_time_0_to_60mph_at_gvwr_s} {knob} size: {largest_infeasible_30s_size}",
                )
        plt.legend()
        plt.savefig(resdir / f"{knob}_{label}_{ts}.png")
        print(f"saving {knob} results to ", resdir)
        plt.show()
    self.knobs = self.knobs_bounds.keys()
    self.obj_list = temp_obj_list
    return tcos

weight_delta_percent_knob(wt_perc_reduction: float, optvehicle: fastsim.vehicle.Vehicle) -> None

This method sets the knob from the lightweighting curve

Parameters:

Name Type Description Default
wt_perc_reduction float

Weight reduction percentage value from lightweighting curve

required
optvehicle fastsim.vehicle.Vehicle

FASTSim vehicle object of the optimization vehicle

required
Source code in t3co/moopack/moo.py
Python
def weight_delta_percent_knob(
    self, wt_perc_reduction: float, optvehicle: fastsim.vehicle.Vehicle
) -> None:
    """
    This method sets the knob from the lightweighting curve

    Args:
        wt_perc_reduction (float): Weight reduction percentage value from lightweighting curve
        optvehicle (fastsim.vehicle.Vehicle): FASTSim vehicle object of the optimization vehicle
    """
    wt_delta_cost_per_kg = np.interp(
        x=wt_perc_reduction, xp=self.ltwt_delta_percs, fp=self.ltwt_dol_per_kg_costs
    )
    wt_delta_kg = optvehicle.glider_kg * wt_perc_reduction
    # Calculate lightweight cost from curve as an integral
    x_new = (
        self.ltwt_delta_percs[self.ltwt_delta_percs <= wt_perc_reduction]
        * optvehicle.glider_kg
    )
    y_new = self.ltwt_dol_per_kg_costs[self.ltwt_delta_percs <= wt_perc_reduction]
    if wt_delta_kg not in x_new:
        x_new = np.append(x_new, wt_delta_kg)
        y_new = np.append(y_new, wt_delta_cost_per_kg)
    self.opt_scenario.vehicle_glider_cost_dol = (
        self.opt_scenario.vehicle_glider_cost_dol + np.trapz(y_new, x_new)
    )
    optvehicle.glider_kg = optvehicle.glider_kg - wt_delta_kg
    optvehicle.set_veh_mass()

run_optimization(pop_size: int, n_max_gen: int, knobs_bounds: dict, vnum: int, x_tol: float, f_tol: float, nth_gen: int, n_last: int, algo: str, obj_list: list = None, config: run_scenario.Config = None, do_input_validation=True, **kwargs) -> Tuple[pymoo.core.result.Result, T3COProblem, bool]

This method creates and runs T3COProblem minimization

Parameters:

Name Type Description Default
pop_size int

Population size for optimization

required
n_max_gen int

maximum number of generations for optimization

required
knobs_bounds dict

Dictionary containing knobs and bounds

required
vnum int

vehicle selection number

required
x_tol float

tolerance in parameter space

required
f_tol float

tolerance in objective space

required
nth_gen int

number of generations to evaluate if convergence occurs

required
n_last int

number of generations to look back for termination

required
algo str

algorithm name

required
obj_list list

list of objectives - TCO or PHEV_MINIMIZE_FUEL_USE_OBJECTIVE. Defaults to None.

None
config run_scenario.Config

T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

None

Returns:

Name Type Description
res pymoo.core.result.Result

Pymoo optimization result object

problem moo.T3COProblem

T3COProblem ElementwiseProblem object

OPTIMIZATION_SUCCEEDED bool

if True, pymoo.minimize succeeded

Source code in t3co/moopack/moo.py
Python
def run_optimization(
    pop_size: int,
    n_max_gen: int,
    knobs_bounds: dict,
    vnum: int,
    x_tol: float,
    f_tol: float,
    nth_gen: int,
    n_last: int,
    algo: str,
    obj_list: list = None,
    config: run_scenario.Config = None,
    do_input_validation=True,
    **kwargs,
) -> Tuple[pymoo.core.result.Result, T3COProblem, bool]:
    """
    This method creates and runs T3COProblem minimization

    Args:
        pop_size (int): Population size for optimization
        n_max_gen (int): maximum number of generations for optimization
        knobs_bounds (dict): Dictionary containing knobs and bounds
        vnum (int): vehicle selection number
        x_tol (float): tolerance in parameter space
        f_tol (float): tolerance in objective space
        nth_gen (int): number of generations to evaluate if convergence occurs
        n_last (int): number of generations to look back for termination
        algo (str): algorithm name
        obj_list (list, optional): list of objectives - TCO or PHEV_MINIMIZE_FUEL_USE_OBJECTIVE. Defaults to None.
        config (run_scenario.Config, optional): T3CO Config object containing analysis attributes and scenario attribute overrides. Defaults to None.

    Returns:
        res (pymoo.core.result.Result): Pymoo optimization result object
        problem (moo.T3COProblem): T3COProblem ElementwiseProblem object
        OPTIMIZATION_SUCCEEDED (bool): if True, pymoo.minimize succeeded
    """

    verbose = kwargs.pop("verbose", False)
    optimize_pt = kwargs.pop("optimize_pt")
    return_least_infeasible = kwargs.pop("optimize_pt", False)
    skip_optimization = kwargs.pop("skip_optimization", False)

    if verbose:
        print("Running optimization.")

    if verbose:
        print(knobs_bounds)

    problem = T3COProblem(
        parallelization=("threads", 1),
        knobs_bounds=knobs_bounds,
        vnum=vnum,
        obj_list=obj_list,
        optimize_pt=optimize_pt,
        verbose=verbose,
        config=config,
        do_input_validation=do_input_validation,
        **kwargs,
    )

    if skip_optimization:
        return None, problem, None, None

    print(
        f"moo.run_optimization algo {algo}, x_tol {x_tol}, f_tol {f_tol}, nth_gen {nth_gen}, n_last {n_last}, n_max_gen {n_max_gen}, pop_size {pop_size}"
    )

    assert (
        algo in ALGORITHMS
    ), f"{algo} not in T3CO list of optimization algorithms {str(ALGORITHMS)}"

    if algo == ALGO_NSGA2:
        algorithm = NSGA2(
            pop_size=pop_size,
            eliminate_duplicates=True,
            # sampling=get_sampling(kwargs.pop('sampling', 'real_lhs'))
            sampling=kwargs.pop("sampling", LHS()),
        )
    elif algo == ALGO_NelderMead:
        print("moo.run_optimization Nelder Mead")
        algorithm = NelderMead()
    elif algo == ALGO_PatternSearch:
        print("moo.run_optimization PatternSearch")
        algorithm = PatternSearch()
    elif algo == ALGO_PSO:
        print("moo.run_optimization Particle Swarm")
        algorithm = PSO()
    # elif algo == 'LocalSearch':
    #     print('moo.run_optimization LocalSearch')
    #     algorithm = LocalSearch()
    t0 = time.time()

    termination = MODT(
        xtol=x_tol,
        ftol=f_tol,
        # n_last=n_last, these are expected in MODT... which is weird bc the docs seem to say it is
        # nth_gen=nth_gen,
        n_max_gen=n_max_gen,
        n_max_evals=None,
    )

    # this check no longer works now that kwargs are pass to T3COProblem and dict types are immutable
    # assert len(kwargs) == 0, f'Invalid kwargs: {list(kwargs.keys())}'
    try:
        res = minimize(
            problem,
            algorithm,
            termination=termination,
            seed=1,
            verbose=True,
            save_history=True,
            return_least_infeasible=return_least_infeasible,
            #    display=T3CODisplay()
        )
    except Exception:
        logging.exception(
            f"moo.run_optimization: Optimization errored out for algorithm {algo}"
        )
        res, problem = None, None
        return res, problem, EXCEPTION_THROWN

    t1 = time.time()
    print(f"\nElapsed time for optimization: {t1 - t0} s")
    if verbose:
        print("\nParameter pareto sets:")
    if res.X is None:
        print("moo.run_optimization: moo failed to converge")
        return res, problem, OPTIMIZATION_FAILED_TO_CONVERGE

    # res.X holds results of optimization
    # successful optimization could be 1D (one solution)
    # [1.0, 2.0, 3.0, 4.0, 5.0]
    # or nD, multiple solutions on a pareto front
    # [[1.0, 2.0, 3.0, 4.0, 5.0],
    # [ 6.0, 7.0, 8.0, 9.0, 10.0],
    # [ 11.0,12.0,13.0,14.0,15.0]]
    return res, problem, OPTIMIZATION_SUCCEEDED