|
| 1 | +from copy import deepcopy |
| 2 | + |
1 | 3 | import numpy as np |
2 | 4 | import pytest |
3 | 5 | import openmdao.api as om |
|
8 | 10 | from h2integrate.storage.simple_storage_auto_sizing import StorageAutoSizingModel |
9 | 11 | from h2integrate.control.control_strategies.optimized_pyomo_controller import ( |
10 | 12 | OptimizedDispatchController, |
| 13 | + OptimizedDispatchControllerConfig, |
11 | 14 | ) |
12 | 15 |
|
13 | 16 |
|
@@ -245,7 +248,6 @@ def test_min_operating_cost_load_following_battery_dispatch( |
245 | 248 | assert pytest.approx(prob.model.get_val("battery.SOC")[0], rel=1e-2) == 50 |
246 | 249 |
|
247 | 250 | # Find where the signal increases, decreases, and stays at zero |
248 | | - print("SOC", prob.model.get_val("battery.SOC")) |
249 | 251 | indx_soc_increase = np.argwhere( |
250 | 252 | np.diff(prob.model.get_val("battery.SOC", units="unitless"), prepend=True) > 0 |
251 | 253 | ).flatten() |
@@ -627,3 +629,239 @@ def test_optimal_dispatch_with_autosizing_storage_demand_is_avg_in( |
627 | 629 | expected_charge, |
628 | 630 | rtol=1e-6, |
629 | 631 | ) |
| 632 | + |
| 633 | + |
| 634 | +@pytest.mark.regression |
| 635 | +def test_optimal_control_config_with_commodity_buying(subtests): |
| 636 | + config_data = { |
| 637 | + "tech_name": "h2_storage", |
| 638 | + "max_charge_rate": 10.0, |
| 639 | + "charge_efficiency": 1.0, |
| 640 | + "discharge_efficiency": 1.0, |
| 641 | + "commodity": "hydrogen", |
| 642 | + "commodity_rate_units": "kg/h", |
| 643 | + "max_capacity": 40.0, |
| 644 | + "init_soc_fraction": 0.2, |
| 645 | + "max_soc_fraction": 1.0, |
| 646 | + "min_soc_fraction": 0.1, |
| 647 | + "cost_per_charge": 0.03, # USD/kg |
| 648 | + "cost_per_discharge": 0.05, # USD/kg |
| 649 | + "demand_met_value": 0.1, # USD/kg |
| 650 | + "cost_per_production": 0.0, # USD/kg |
| 651 | + "time_weighting_factor": 0.995, |
| 652 | + "system_commodity_interface_limit": 10.0, |
| 653 | + "n_control_window": 24, |
| 654 | + "allow_commodity_buying": False, |
| 655 | + } |
| 656 | + |
| 657 | + config = OptimizedDispatchControllerConfig.from_dict(config_data) |
| 658 | + |
| 659 | + with subtests.test("check commodity_buy_price is None"): |
| 660 | + assert config.commodity_buy_price is None |
| 661 | + with subtests.test("check max_system_capacity is None"): |
| 662 | + assert config.max_system_capacity is None |
| 663 | + |
| 664 | + config_data["allow_commodity_buying"] = True |
| 665 | + |
| 666 | + with subtests.test("with invalid commodity_buy_price"): |
| 667 | + with pytest.raises(ValueError): |
| 668 | + data = deepcopy(config_data) |
| 669 | + data["allow_commodity_buying"] = True |
| 670 | + OptimizedDispatchControllerConfig.from_dict(data) |
| 671 | + |
| 672 | + with pytest.raises(ValueError): |
| 673 | + data = deepcopy(config_data) |
| 674 | + data["allow_commodity_buying"] = True |
| 675 | + data["commodity_buy_price"] = 0.4 |
| 676 | + OptimizedDispatchControllerConfig.from_dict(data) |
| 677 | + |
| 678 | + with pytest.raises(ValueError): |
| 679 | + data = deepcopy(config_data) |
| 680 | + data["allow_commodity_buying"] = True |
| 681 | + data["commodity_buy_price"] = 0.4 |
| 682 | + data["max_system_capacity"] = 0.0 |
| 683 | + OptimizedDispatchControllerConfig.from_dict(data) |
| 684 | + |
| 685 | + |
| 686 | +@pytest.mark.regression |
| 687 | +def test_optimal_control_with_commodity_buying_generic_storage( |
| 688 | + plant_config_h2_storage, tech_config_generic, subtests |
| 689 | +): |
| 690 | + commodity_demand = np.full(48, 5.0) |
| 691 | + commodity_in = np.tile(np.concat([np.zeros(3), np.cumsum(np.ones(15)), np.full(6, 4.0)]), 2) |
| 692 | + commodity_buy_price = np.tile(np.concat([np.arange(-3, 9), np.arange(8, -4, -1)]), 2) |
| 693 | + max_system_capacity = 7 |
| 694 | + |
| 695 | + # Set grid charging parameters |
| 696 | + tech_config_generic["technologies"]["h2_storage"]["model_inputs"]["control_parameters"] = { |
| 697 | + "tech_name": "h2_storage", |
| 698 | + "cost_per_charge": 0.03, # USD/kg |
| 699 | + "cost_per_discharge": 0.05, # USD/kg |
| 700 | + "demand_met_value": 0.1, # USD/kg |
| 701 | + "cost_per_production": 0.0, # USD/kg |
| 702 | + "time_weighting_factor": 0.995, |
| 703 | + "system_commodity_interface_limit": 10.0, |
| 704 | + "n_control_window": 24, |
| 705 | + "allow_commodity_buying": True, |
| 706 | + "commodity_buy_price": 1, |
| 707 | + "max_system_capacity": max_system_capacity, |
| 708 | + } |
| 709 | + |
| 710 | + # Setup the OpenMDAO problem and add subsystems |
| 711 | + prob = om.Problem() |
| 712 | + |
| 713 | + prob.model.add_subsystem( |
| 714 | + "h2_storage_optimized_load_following_controller", |
| 715 | + OptimizedDispatchController( |
| 716 | + plant_config=plant_config_h2_storage, |
| 717 | + tech_config=tech_config_generic["technologies"]["h2_storage"], |
| 718 | + ), |
| 719 | + promotes=["*"], |
| 720 | + ) |
| 721 | + |
| 722 | + prob.model.add_subsystem( |
| 723 | + "h2_storage", |
| 724 | + StoragePerformanceModel( |
| 725 | + plant_config=plant_config_h2_storage, |
| 726 | + tech_config=tech_config_generic["technologies"]["h2_storage"], |
| 727 | + ), |
| 728 | + promotes=["*"], |
| 729 | + ) |
| 730 | + |
| 731 | + # Setup the system and required values |
| 732 | + prob.setup() |
| 733 | + prob.set_val("h2_storage.hydrogen_in", commodity_in) |
| 734 | + prob.set_val("h2_storage.hydrogen_demand", commodity_demand) |
| 735 | + prob.set_val("hydrogen_buy_price", commodity_buy_price) |
| 736 | + |
| 737 | + # Run the model |
| 738 | + prob.run_model() |
| 739 | + |
| 740 | + charge_rate = prob.get_val("h2_storage.max_charge_rate", units="kg/h")[0] |
| 741 | + discharge_rate = prob.get_val("h2_storage.max_charge_rate", units="kg/h")[0] |
| 742 | + capacity = prob.get_val("h2_storage.storage_capacity", units="kg")[0] |
| 743 | + |
| 744 | + print("outputs: ", prob.get_val("storage_hydrogen_out")) |
| 745 | + print("discharge: ", prob.get_val("h2_storage.storage_hydrogen_discharge")) |
| 746 | + print("charge: ", prob.get_val("h2_storage.storage_hydrogen_charge")) |
| 747 | + print("commodity in: ", prob.get_val("h2_storage.hydrogen_in")) |
| 748 | + print("demand: ", prob.get_val("h2_storage.hydrogen_demand")) |
| 749 | + print("commodity_buy: ", prob.get_val("hydrogen_buy_price")) |
| 750 | + print("hydrogen_out: ", prob.get_val("hydrogen_out")) |
| 751 | + |
| 752 | + # Test that discharge is always positive |
| 753 | + with subtests.test("Discharge is always positive"): |
| 754 | + assert np.all(prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h") >= 0) |
| 755 | + with subtests.test("Charge is always negative"): |
| 756 | + assert np.all(prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h") <= 0) |
| 757 | + with subtests.test("Charge + Discharge == storage_hydrogen_out"): |
| 758 | + charge_plus_discharge = prob.get_val( |
| 759 | + "h2_storage.storage_hydrogen_charge", units="kg/h" |
| 760 | + ) + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h") |
| 761 | + np.testing.assert_allclose( |
| 762 | + charge_plus_discharge, prob.get_val("storage_hydrogen_out", units="kg/h"), rtol=1e-6 |
| 763 | + ) |
| 764 | + with subtests.test("Initial SOC is correct"): |
| 765 | + assert ( |
| 766 | + pytest.approx(prob.model.get_val("h2_storage.SOC", units="unitless")[0], rel=1e-6) |
| 767 | + == 0.375 |
| 768 | + ) |
| 769 | + |
| 770 | + indx_soc_increase = np.argwhere( |
| 771 | + np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=True) > 0 |
| 772 | + ).flatten() |
| 773 | + indx_soc_decrease = np.argwhere( |
| 774 | + np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=False) < 0 |
| 775 | + ).flatten() |
| 776 | + indx_soc_same = np.argwhere( |
| 777 | + np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=True) == 0.0 |
| 778 | + ).flatten() |
| 779 | + |
| 780 | + with subtests.test("SOC increases when charging"): |
| 781 | + assert np.all( |
| 782 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_increase] < 0 |
| 783 | + ) |
| 784 | + assert np.all( |
| 785 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_decrease] == 0 |
| 786 | + ) |
| 787 | + assert np.all( |
| 788 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_same] == 0 |
| 789 | + ) |
| 790 | + |
| 791 | + with subtests.test("SOC decreases when discharging"): |
| 792 | + assert np.all( |
| 793 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_decrease] |
| 794 | + > 0 |
| 795 | + ) |
| 796 | + assert np.all( |
| 797 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_increase] |
| 798 | + == 0 |
| 799 | + ) |
| 800 | + assert np.all( |
| 801 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_same] == 0 |
| 802 | + ) |
| 803 | + |
| 804 | + with subtests.test("Max SOC <= Max storage percent"): |
| 805 | + assert prob.get_val("h2_storage.SOC", units="unitless").max() <= 1.0 |
| 806 | + |
| 807 | + with subtests.test("Min SOC >= Min storage percent"): |
| 808 | + assert prob.get_val("h2_storage.SOC", units="unitless").min() >= 0.1 |
| 809 | + |
| 810 | + with subtests.test("Charge never exceeds charge rate"): |
| 811 | + assert ( |
| 812 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h").min() |
| 813 | + >= -1 * charge_rate |
| 814 | + ) |
| 815 | + |
| 816 | + with subtests.test("Discharge never exceeds discharge rate"): |
| 817 | + assert ( |
| 818 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h").max() |
| 819 | + <= discharge_rate |
| 820 | + ) |
| 821 | + |
| 822 | + with subtests.test("Discharge never exceeds demand"): |
| 823 | + np.testing.assert_allclose( |
| 824 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h").max(), |
| 825 | + commodity_demand, |
| 826 | + rtol=1e-6, |
| 827 | + ) |
| 828 | + |
| 829 | + with subtests.test("Sometimes discharges"): |
| 830 | + assert any( |
| 831 | + k > 1e-3 for k in prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h") |
| 832 | + ) |
| 833 | + |
| 834 | + with subtests.test("Sometimes charges"): |
| 835 | + assert any( |
| 836 | + k < -1e-3 for k in prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h") |
| 837 | + ) |
| 838 | + |
| 839 | + with subtests.test("Cumulative charge/discharge does not exceed storage capacity"): |
| 840 | + assert np.cumsum(prob.get_val("storage_hydrogen_out", units="kg/h")).max() <= capacity |
| 841 | + assert np.cumsum(prob.get_val("storage_hydrogen_out", units="kg/h")).min() >= -1 * capacity |
| 842 | + |
| 843 | + with subtests.test("Expected discharge from hour 10-30"): |
| 844 | + expected_discharge = np.concat( |
| 845 | + [np.zeros(8), np.ones(3), np.zeros(3), [5, 0], np.arange(5, 1, -1)] |
| 846 | + ) |
| 847 | + np.testing.assert_allclose( |
| 848 | + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[10:30], |
| 849 | + expected_discharge, |
| 850 | + rtol=1e-6, |
| 851 | + atol=1e-6, |
| 852 | + ) |
| 853 | + |
| 854 | + with subtests.test("Expected charge hour 0-20"): |
| 855 | + expected_charge = np.concat([np.ones(3) * -7, np.zeros(5), [-1, -2], np.zeros(10)]) |
| 856 | + np.testing.assert_allclose( |
| 857 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[0:20], |
| 858 | + expected_charge, |
| 859 | + rtol=1e-6, |
| 860 | + ) |
| 861 | + |
| 862 | + with subtests.test("Output never exceeds system commodity draw limit"): |
| 863 | + np.testing.assert_allclose( |
| 864 | + prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h").min(), |
| 865 | + -max_system_capacity, |
| 866 | + rtol=1e-6, |
| 867 | + ) |
0 commit comments