Skip to content

csp_workflow_manager

Classes:

PTRWorkflowManager

PTRWorkflowManager(placements)

Bases: SolarWorkflowManager

__init_(self, placements)

Initialization of an instance of the generic SolarWorkflowManager clas

Parameters:

  • placements

    (pandas Dataframe) –
         The locations that the simulation should be run for.
         Columns must include "lon", "lat"
    

Returns:

  • SolarWorkflorManager

Methods:

Source code in reskit/csp/workflows/csp_workflow_manager.py
def __init__(self, placements):
    """

    __init_(self, placements)

    Initialization of an instance of the generic SolarWorkflowManager clas

    Parameters
    ----------
    placements : pandas Dataframe
                 The locations that the simulation should be run for.
                 Columns must include "lon", "lat"

    Returns
    -------
    SolarWorkflorManager

    """
    # Do basic workflow construction
    super().__init__(placements)
    self._time_sel_ = None
    self._time_index_ = None
    # self.module = None
    self.sim_data_daily = dict()

    self.check_placements()

adjust_variable_to_long_run_average

adjust_variable_to_long_run_average(
    variable: str,
    source_long_run_average: Union[str, float, ndarray],
    real_long_run_average: Union[str, float, ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
    nodata_fallback: str = "nan",
    nodata_fallback_scaling: float = 1,
    allow_nans: bool = True,
)

Adjusts the average mean of the specified variable to a known long-run-average

Note:

uses the equation: variable[t] = variable[t] * real_long_run_average / source_long_run_average

Parameters:

  • variable

    (str) –

    The variable to be adjusted

  • source_long_run_average

    (Union[str, float, ndarray]) –

    The variable's native long run average (the average in the weather file) - If a string is given, it is expected to be a path to a raster file which can be used to look up the average values from using the coordinates in .placements - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements) or at least (placements)

  • real_long_run_average

    (Union[str, float, ndarray]) –

    The variables 'true' long run average - If a string is given, it is expected to be a path to a raster file which can be used to look up the average values from using the coordinates in .placements - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements) or at least (placements)

  • real_lra_scaling

    (float, default: 1 ) –

    An optional scaling factor to apply to the values derived from real_long_run_average. - This is primarily useful when real_long_run_average is a path to a raster file - By default 1

  • spatial_interpolation

    (str, default: 'linear-spline' ) –

    When either source_long_run_average or real_long_run_average are a path to a raster file, this input specifies which interpolation algorithm should be used - Options are: "near", "linear-spline", "cubic-spline", "average" - By default "linear-spline" - See for more info: geokit.raster.interpolateValues

  • nodata_fallback

    (str, default: 'nan' ) –

    When real_long_run_average has no data, one can decide between different fallback options, by default np.nan: - np.nan or None : return np.nan for missing values in real_long_run_average - float : Apply this float value as a scaling factor for all no-data locations only: source_long_run_average * nodata_fallback. NOTE: A value of 1.0 will return the source lra value in case of missing real lra values (no additional nodata_fallback_scaling applied). - str : Will be interpreted as a filepath to a raster with alternative real_long_run_average values, scaled by nodata_fallback_scaling. - callable : any callable method taking the arguments (all iterables): 'locs' and 'source_long_run_average_value' (the locations as gk.geom.point objects and original value from source data). The output values will be considered as the new real_long_run_average for missing locations only (absolute data, no additional nodata_fallback_scaling applied).

    NOTE: np.nan will also be returned in case that the nodata fallback does not yield values either.

  • nodata_fallback_scaling

    (float, default: 1 ) –

    An optional scaling factor to apply to the values derived from nodata_fallback. - This is primarily useful when nodata_fallback is a path to a raster file - By default 1

  • allow_nans

    (boolean, default: True ) –

    If True, NaN values may remain after scaling, else an error will raised. By default True.

Returns:

Source code in reskit/workflow_manager.py
def adjust_variable_to_long_run_average(
    self,
    variable: str,
    source_long_run_average: Union[str, float, np.ndarray],
    real_long_run_average: Union[str, float, np.ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
    nodata_fallback: str = "nan",
    nodata_fallback_scaling: float = 1,
    allow_nans: bool = True,
):
    """Adjusts the average mean of the specified variable to a known long-run-average

    Note:
    -----
    uses the equation: variable[t] = variable[t] * real_long_run_average / source_long_run_average

    Parameters
    ----------
    variable : str
        The variable to be adjusted

    source_long_run_average : Union[str, float, np.ndarray]
        The variable's native long run average (the average in the weather file)
        - If a string is given, it is expected to be a path to a raster file which can be
        used to look up the average values from using the coordinates in `.placements`
        - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements)
        or at least (placements)

    real_long_run_average : Union[str, float, np.ndarray]
        The variables 'true' long run average
        - If a string is given, it is expected to be a path to a raster file which can be
        used to look up the average values from using the coordinates in `.placements`
        - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements)
        or at least (placements)

    real_lra_scaling : float, optional
        An optional scaling factor to apply to the values derived from `real_long_run_average`.
        - This is primarily useful when `real_long_run_average` is a path to a raster file
        - By default 1

    spatial_interpolation : str, optional
        When either `source_long_run_average` or `real_long_run_average` are a path to a raster
        file, this input specifies which interpolation algorithm should be used
        - Options are: "near", "linear-spline", "cubic-spline", "average"
        - By default "linear-spline"
        - See for more info: geokit.raster.interpolateValues

    nodata_fallback: float, str, callable, optional
        When real_long_run_average has no data, one can decide between different fallback options, by default np.nan:
        - np.nan or None : return np.nan for missing values in real_long_run_average
        - float : Apply this float value as a scaling factor for all no-data locations only: source_long_run_average * nodata_fallback.
        NOTE: A value of 1.0 will return the source lra value in case of missing real lra values (no additional nodata_fallback_scaling applied).
        - str : Will be interpreted as a filepath to a raster with alternative real_long_run_average values, scaled by nodata_fallback_scaling.
        - callable : any callable method taking the arguments (all iterables): 'locs' and 'source_long_run_average_value'
        (the locations as gk.geom.point objects and original value from source data). The output values will be considered as
        the new real_long_run_average for missing locations only (absolute data, no additional nodata_fallback_scaling applied).

        NOTE: np.nan will also be returned in case that the nodata fallback does not yield values either.

    nodata_fallback_scaling: float
        An optional scaling factor to apply to the values derived from `nodata_fallback`.
        - This is primarily useful when `nodata_fallback` is a path to a raster file
        - By default 1

    allow_nans : boolean, optional
        If True, NaN values may remain after scaling, else an error will raised. By default True.

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)
    """
    if not (nodata_fallback is None or callable(nodata_fallback) or isinstance(nodata_fallback, (float, int, str))):
        raise TypeError(f"'nodata_fallback' must be a float or a Callable.")

    # first get source values
    if isinstance(source_long_run_average, str):
        # assume raster fp
        source_lra = self.get_scalar_values_from_raster(
            fp=source_long_run_average, spatial_interpolation="linear-spline"
        )
    else:
        source_lra = source_long_run_average

    # then get lng-run average values for scaling
    if isinstance(real_long_run_average, str):
        # assume a raster path
        real_lra = self.get_scalar_values_from_raster(
            fp=real_long_run_average, spatial_interpolation=spatial_interpolation
        )
    else:
        real_lra = real_long_run_average

    # replace missing values with no-data fallback if needed
    if isinstance(nodata_fallback, str) and nodata_fallback.lower() == "source":
        warnings.warn(
            "'source' value for 'nodata_fallback' is deprecated and will be removed soon. Use 1.0 instead.",
            DeprecationWarning,
        )
        nodata_fallback = 1.0
    if isinstance(nodata_fallback, str) and nodata_fallback.lower() == "nan":
        warnings.warn(
            "'nan' value for 'nodata_fallback' is deprecated and will be removed soon. Use np.nan instead.",
            DeprecationWarning,
        )
        nodata_fallback = np.nan
    if any(np.isnan(real_lra)):  # TODO currently all real_lra are replaced by fallback, is this intentional?
        # we are lacking long-run average values
        if nodata_fallback is None or (isinstance(nodata_fallback, float) and np.isnan(nodata_fallback)):
            # nans will be returned for missing lra values
            fallback_lra = np.array([np.nan] * len(real_lra))
        elif isinstance(nodata_fallback, (int, float)):
            # apply factor to source_lra to scale missing values
            fallback_lra = nodata_fallback * source_lra  # no additional scaling
        elif callable(nodata_fallback):
            # apply function to calculate missing values
            fallback_lra = nodata_fallback(
                locs=self.locs, source_long_run_average_value=source_lra
            )  # no additional scaling
        elif isinstance(nodata_fallback, str):
            # assume this is yet another raster path as fallback and extract missing values
            fallback_lra = (
                self.get_scalar_values_from_raster(fp=nodata_fallback, spatial_interpolation=spatial_interpolation)
                * nodata_fallback_scaling
            )

        # divide by real_lra_scaling once to compensate later multiplication below for
        # scaling factor, nodata_fallback should not be multiplied by real_lra_scaling
        fallback_lra = fallback_lra / real_lra_scaling
        # set fallback values where real_lra is nan
        real_lra[np.isnan(real_lra)] = fallback_lra[np.isnan(real_lra)]

    # save LRA as attribute
    self.real_lra = real_lra

    # calculate scaling factor:
    # nan result will stay nan results, as these placements cannot be calculated any more
    factors = real_lra * real_lra_scaling / source_lra
    if any(np.isnan(real_lra)):
        if allow_nans:
            warnings.warn(f"NaN values remaining in real lra after application of nodata_fallback.")
        else:
            raise ValueError(f"Missing values for variable '{variable}' and NaNs not allowed.")

    # write info with missing values to sim_data:
    self.placements[f"missing_values_{basename(real_long_run_average)}_nodata_fallback{nodata_fallback}"] = (
        np.isnan(factors)
    )

    self.sim_data[variable] = factors * self.sim_data[variable]
    self.placements[f"LRA_factor_{variable}"] = factors
    return self

applyHTFHeatLossModel

applyHTFHeatLossModel(
    calculationmethod: str = "gafurov2013",
    params: dict = {},
)

Calculate the heat losses of the HTF and determines the Heat output of the solar field

Args: calculationmethod (str, optional): [calculation method for heat losses. Choose from 'zero' or 'gafurov2013']. Defaults to 'zero'. params (dict, optional): [Parameters for the heat models as dict. For 'gafurov2013' use relHeatLosses and ratedFieldOutputHeat_W]. Defaults to {}.

Raises:

  • error: [description]

Returns:

  • [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['HeattoPlant_W'][timeserie_iter, location_iter] as np.narray
Source code in reskit/csp/workflows/csp_workflow_manager.py
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
def applyHTFHeatLossModel(self, calculationmethod: str = "gafurov2013", params: dict = {}):
    """Calculate the heat losses of the HTF and determines the Heat output of the solar field

    Args:
        calculationmethod (str, optional): [calculation method for heat losses. Choose from 'zero' or 'gafurov2013']. Defaults to 'zero'.
        params (dict, optional): [Parameters for the heat models as dict. For 'gafurov2013' use relHeatLosses and ratedFieldOutputHeat_W]. Defaults to {}.

    Raises
    ------
        error: [description]

    Returns
    -------
        [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['HeattoPlant_W'][timeserie_iter, location_iter] as np.narray
    """
    # check for input
    assert "HeattoHTF_W" in self.sim_data.keys()
    calculationmethod = calculationmethod.lower()

    if calculationmethod == "zero":
        _losses = np.zeros_like(self.sim_data["HeattoHTF_W"], dtype=float)

    elif calculationmethod == "gafurov2013":
        # check for input
        assert "relHeatLosses" in params.keys()
        assert "ratedFieldOutputHeat_W" in params.keys()

        _losses = (
            np.ones_like(self.sim_data["HeattoHTF_W"], dtype=float)
            * params["relHeatLosses"]
            * params["ratedFieldOutputHeat_W"]
        )
        self.sim_data["HeattoPlant_W"] = self.sim_data["HeattoHTF_W"] - _losses
        self.sim_data["HeatLosses_W"] = _losses

    elif calculationmethod == "dersch2018":
        # check for input
        assert "b" in params.keys()
        assert "relTMplant" in params.keys()
        assert "maxHTFTemperature" in params.keys()
        assert "JITaccelerate" in params.keys()
        assert "minHTFTemperature" in params.keys()
        assert "inletHTFTemperature" in params.keys()
        assert params["b"].shape == (5,)
        assert "add_losses_coefficient" in params.keys()

        assert "IAM" in self.sim_data.keys()
        assert "theta" in self.sim_data.keys()
        assert "surface_air_temperature" in self.sim_data.keys()
        assert "HeattoHTF_W" in self.sim_data.keys()
        assert "direct_normal_irradiance" in self.sim_data.keys()

        # set up arrays
        # timedelta in seconds
        deltat = self.time_index[1] - self.time_index[0]
        deltat = deltat.total_seconds()  # seconds
        # temperature
        _temperature = np.zeros_like(self.sim_data["HeattoHTF_W"])
        # initial temperature
        _temperature[0, :] = self.sim_data["surface_air_temperature"][0, :] + 100
        # losses
        _losses = np.zeros_like(self.sim_data["HeattoHTF_W"])
        # K = cos(theta) * IAM
        _K = np.cos(np.deg2rad(self.sim_data["theta"])) * self.sim_data["IAM"]
        # heating power needed:
        _P_heating = np.zeros_like(self.sim_data["HeattoHTF_W"])
        # heat to Plant
        _HeattoPlant = np.zeros_like(self.sim_data["HeattoHTF_W"])

        if params["discretizationmethod"] == "euler explicit":

            def simulation(
                HeattoHTF: np.ndarray,
                temperature: np.ndarray,
                ambient_temperature: np.ndarray,
                losses: np.ndarray,
                K: np.ndarray,
                DNI: np.ndarray,
                A: np.ndarray,
                b: np.ndarray,
                relTMplant: float,
                deltat: float,
                maxHTFTemperature: float,
                minHTFTemperature: float,
                inletHTFTemperature: float,
                P_heating: np.ndarray,
                HeattoPlant: np.ndarray,
                add_losses_coefficient: float,
            ):
                """[Transient simulation of the HTF fluid temperature. Calculate losses by empiric formulation as in Greenius]

                Args:
                    HeattoHTF (np.ndarray): [description]
                    temperature (np.ndarray): [description]
                    ambient_temperature (np.ndarray): [description]
                    losses (np.ndarray): [description]
                    K (np.ndarray): [description]
                    DNI (np.ndarray): [description]
                    A (float): [description]
                    b (np.ndarray): [description]
                    TM_plant (float): [description]
                    deltat (float): [description]
                    maxHTFTemperature (float): [description]

                Returns
                -------
                    [type]: [description]
                """
                maxHTFmeanTemperature = (maxHTFTemperature + inletHTFTemperature) / 2
                # because plant is not at operation, there is no normal outlet temperature
                minHTFmeanTemperature = minHTFTemperature

                for i in range(0, temperature.shape[0] - 1):
                    # 1) Delta T
                    # deltaT = (T_out - T in) / - T_amb  ...  (in, out from the solar field view, defined in Greenius)
                    deltaT = temperature[i, :] - ambient_temperature[i, :]

                    # 2) Losses
                    # loss formula from greenius
                    losses[i, :] = K[i, :] * b[0] * deltaT * A * DNI[i, :] + A * (
                        b[1] * deltaT**1 + b[2] * deltaT**2 + b[3] * deltaT**3 + b[4] * deltaT**4
                    )
                    # additional loss factor:
                    losses[i, :] = losses[i, :] + add_losses_coefficient * deltaT * A
                    # losses[i, :] = losses[i, :] * heatlossfactor + heatlossconstant

                    # calculate temperature from energy balance around all thermal masses which need to be heated up (see sam manual)
                    temperature[i + 1, :] = temperature[i, :] + (HeattoHTF[i + 1, :] - losses[i, :]) * deltat / (
                        relTMplant * A
                    )
                    # The heat to plant is neglected in this formulation. For this will be accouintet in the next lines

                    # The temperature should always stay between max temperature and min temperature
                    # 1) When the Temperatrue is at the operation point, excess heat will be used in the plant --> Normal operation
                    # 2) When the temperature is at the minimum point, missing heat will be provided from electric heating

                    # 1) Operation Point
                    # maximal temperature is achieved, when outlet temperature is at max temperature.
                    temperature[i + 1, :] = np.minimum(temperature[i + 1, :], maxHTFmeanTemperature)
                    # Heat to plant equals Heat input minus losses, when the plant is in operation mode (temperature = max temperature)

                    HeattoPlant[i, :] = (HeattoHTF[i, :] - losses[i, :]) * (
                        temperature[i, :] == maxHTFmeanTemperature
                    )
                    # In the first time step with max temp, not all energy can be used in the plant, as some energy was used for heating the htf.
                    # The htf heating losses are subtracted for the first time step
                    is_first_max_heat = np.logical_and(
                        temperature[i - 1, :] != maxHTFmeanTemperature,
                        temperature[i, :] == maxHTFmeanTemperature,
                    )
                    heat_flux_htf_heatup_last_timestep = (
                        (temperature[i, :] - temperature[i - 1, :]) * relTMplant * A / deltat
                    )

                    # manipulate HeattoPlant, so that heat flux into htf is subtracted from HeattoPlant
                    HeattoPlant[i, :] = np.maximum(
                        HeattoPlant[i, :] - is_first_max_heat * heat_flux_htf_heatup_last_timestep,
                        0,
                    )
                    # because of discretization uncertainties, HeattoPlant can get negative. So this will be prevented here. Looks odd, but is true

                    # 2) Freeze protection
                    # when temperature is below minimal temperature, there will be an electrical heating for the HTF so that:
                    # 2.1) temperatere is locked at min temp
                    temperature[i + 1, :] = np.maximum(temperature[i + 1, :], minHTFmeanTemperature)
                    # 2.2) there are parasitic losses for heating
                    # Heating equals the heat losses - the heat input from the field, if the plant is in freeze protection mode(temperature = min temperature)
                    P_heating[i, :] = np.maximum(
                        (losses[i, :] - HeattoHTF[i, :]) * (temperature[i, :] == minHTFmeanTemperature),
                        0,
                    )
                    # because of discretization uncertainties, P_heating can get negative. So this will be prevented here. Looks odd, but is true

                return temperature, losses, P_heating, HeattoPlant

        elif params["discretizationmethod"] == "euler implicit":

            def simulation(
                HeattoHTF: np.ndarray,
                temperature: np.ndarray,
                ambient_temperature: np.ndarray,
                losses: np.ndarray,
                K: np.ndarray,
                DNI: np.ndarray,
                A: np.ndarray,
                b: np.ndarray,
                relTMplant: float,
                deltat: float,
                maxHTFTemperature: float,
                minHTFTemperature: float,
                inletHTFTemperature: float,
                P_heating: np.ndarray,
                HeattoPlant: np.ndarray,
                add_losses_coefficient: float,
            ):
                """[Transient simulation of the HTF fluid temperature. Calculate losses by empiric formulation as in Greenius]

                Args:
                    HeattoHTF (np.ndarray): [description]
                    temperature (np.ndarray): [description]
                    ambient_temperature (np.ndarray): [description]
                    losses (np.ndarray): [description]
                    K (np.ndarray): [description]
                    DNI (np.ndarray): [description]
                    A (float): [description]
                    b (np.ndarray): [description]
                    TM_plant (float): [description]
                    deltat (float): [description]
                    maxHTFTemperature (float): [description]

                Returns
                -------
                    [type]: [description]
                """
                maxHTFmeanTemperature = (maxHTFTemperature + inletHTFTemperature) / 2
                # because plant is not at operation, there is no normal outlet temperature
                minHTFmeanTemperature = minHTFTemperature

                for i in range(0, temperature.shape[0] - 1):
                    # 1) Delta T
                    # deltaT = (T_out - T in) / - T_amb  ...  (in, out from the solar field view, defined in Greenius)

                    # 2) Losses
                    # get polynomial factors of eueler forward discretized equation:
                    # iterate through locations
                    for i_loc in range(0, HeattoHTF.shape[1]):
                        p0 = -HeattoHTF[i + 1, i_loc] - (relTMplant * A) / deltat * (
                            temperature[i, i_loc] - ambient_temperature[i, i_loc]
                        )
                        p1 = (
                            (relTMplant * A) / deltat
                            + K[i + 1, i_loc] * b[0] * A * DNI[i + 1, i_loc]
                            + A * b[1]
                            + add_losses_coefficient * A
                        )
                        p2 = A * b[2]
                        p3 = A * b[3]
                        p4 = A * b[4]

                        # solve polynomial for zeros:
                        solutions_T_star = np.roots(np.array([p4, p3, p2, p1, p0]).astype(np.complex128))
                        solutions_T = solutions_T_star - ambient_temperature[i + 1, i_loc]

                        # filter out solutions which are real and between -200 and 1000 °C
                        solutions_T_filtered = []
                        for solution in solutions_T:
                            if solution.imag < 1e-10 and solution.imag > -1e-10:
                                if solution.real > -200 and solution.real < 1000:
                                    solutions_T_filtered.append(solution)

                        if len(solutions_T_filtered) != 1:
                            print(solutions_T)
                            print("Severe error: Multiple or no zeros for temperature calculation were found!!")
                            print("Timestep: " + str(i) + ", Location: " + str(i_loc))
                            # error('Severe error: Multiple or no zeros for temperature calculation were found!!')

                        # set temperature
                        temperature[i + 1, i_loc] = solutions_T_filtered[0].real

                    # Set temperature limits
                    temperature[i + 1, :] = np.minimum(temperature[i + 1, :], maxHTFmeanTemperature)
                    temperature[i + 1, :] = np.maximum(temperature[i + 1, :], minHTFmeanTemperature)

                    # calculate delta T
                    deltaT = temperature[i + 1, :] - ambient_temperature[i + 1, :]

                    # loss formula from greenius
                    losses[i + 1, :] = K[i, :] * b[0] * deltaT * A * DNI[i, :] + A * (
                        b[1] * deltaT**1 + b[2] * deltaT**2 + b[3] * deltaT**3 + b[4] * deltaT**4
                    )
                    # additional loss factor:
                    losses[i + 1, :] = losses[i + 1, :] + add_losses_coefficient * deltaT * A
                    # losses[i, :] = losses[i, :] * heatlossfactor + heatlossconstant

                    # calculate temperature from energy balance around all thermal masses which need to be heated up (see sam manual)
                    ########
                    # temperature[i+1, :] = temperature[i, :] + (HeattoHTF[i+1, :] - losses[i, :]) * deltat / (relTMplant * A)
                    #
                    # The heat to plant is neglected in this formulation. For this will be accouintet in the next lines

                    # The temperature should always stay between max temperature and min temperature
                    # 1) When the Temperatrue is at the operation point, excess heat will be used in the plant --> Normal operation
                    # 2) When the temperature is at the minimum point, missing heat will be provided from electric heating

                    # 1) Operation Point
                    # maximal temperature is achieved, when outlet temperature is at max temperature.
                    temperature[i + 1, :] = np.minimum(temperature[i + 1, :], maxHTFmeanTemperature)
                    # Heat to plant equals Heat input minus losses, when the plant is in operation mode (temperature = max temperature)
                    HeattoPlant[i, :] = np.maximum(
                        (HeattoHTF[i, :] - losses[i, :]) * (temperature[i, :] == maxHTFmeanTemperature),
                        0,
                    )
                    # because of discretization uncertainties, HeattoPlant can get negative. So this will be prevented here. Looks odd, but is true

                    # 2) Freeze protection
                    # when temperature is below minimal temperature, there will be an electrical heating for the HTF so that:
                    # 2.1) temperatere is locked at min temp
                    temperature[i + 1, :] = np.maximum(temperature[i + 1, :], minHTFmeanTemperature)
                    # 2.2) there are parasitic losses for heating
                    # Heating equals the heat losses - the heat input from the field, if the plant is in freeze protection mode(temperature = min temperature)
                    P_heating[i, :] = np.maximum(
                        (losses[i, :] - HeattoHTF[i, :]) * (temperature[i, :] == minHTFmeanTemperature),
                        0,
                    )
                    # because of discretization uncertainties, P_heating can get negative. So this will be prevented here. Looks odd, but is true

                return temperature, losses, P_heating, HeattoPlant

        # NUMBA JIT the simulation
        tic = time.time()
        simulation_jitted = jit(nopython=True)(simulation)

        # Run the simulation
        if not params["JITaccelerate"]:
            _temperature, _losses, _P_heating, _HeattoPlant = simulation(
                HeattoHTF=self.sim_data["HeattoHTF_W"],
                temperature=_temperature,
                ambient_temperature=self.sim_data["surface_air_temperature"],
                losses=_losses,
                K=_K,
                DNI=self.sim_data["direct_normal_irradiance"],
                A=self.placements["aperture_area_m2"].values,
                b=params["b"],
                relTMplant=params["relTMplant"],
                deltat=deltat,
                maxHTFTemperature=params["maxHTFTemperature"],
                minHTFTemperature=params["minHTFTemperature"],
                inletHTFTemperature=params["inletHTFTemperature"],
                P_heating=_P_heating,
                HeattoPlant=_HeattoPlant,
                add_losses_coefficient=params["add_losses_coefficient"],
            )

        else:
            _temperature, _losses, _P_heating, _HeattoPlant = simulation_jitted(
                HeattoHTF=self.sim_data["HeattoHTF_W"],
                temperature=_temperature,
                ambient_temperature=self.sim_data["surface_air_temperature"],
                losses=_losses,
                K=_K,
                DNI=self.sim_data["direct_normal_irradiance"],
                A=self.placements["aperture_area_m2"].values,
                b=params["b"],
                relTMplant=params["relTMplant"],
                deltat=deltat,
                maxHTFTemperature=params["maxHTFTemperature"],
                minHTFTemperature=params["minHTFTemperature"],
                inletHTFTemperature=params["inletHTFTemperature"],
                P_heating=_P_heating,
                HeattoPlant=_HeattoPlant,
                add_losses_coefficient=params["add_losses_coefficient"],
            )

        toc = time.time()
        print("Core simulation time: " + str(toc - tic) + "s.")

        # store data
        self.sim_data["HTF_mean_temperature_C"] = _temperature
        self.sim_data["Heat_Losses_W"] = _losses
        self.sim_data["P_heating_W"] = _P_heating
        self.sim_data["HeattoPlant_W"] = _HeattoPlant

        if len(self.placements) > 1:
            assert (
                (self.sim_data["HeattoPlant_W"].mean(axis=0) / self.sim_data["HeattoHTF_W"].mean(axis=0)) < 1
            ).all()
        else:
            assert (self.sim_data["HeattoPlant_W"].mean(axis=0) / self.sim_data["HeattoHTF_W"].mean(axis=0)) < 1

    else:
        warn("Wrong calculation for heat losses of heat transfer fluid selected. Losses will be set to zero.")
        _losses = np.zeros_like(self.sim_data["HeattoHTF_W"], dtype=float)
        self.sim_data["HeattoPlant_W"] = self.sim_data["HeattoHTF_W"] - _losses
        self.sim_data["Heat_Losses_W"] = _losses

    return self

apply_DIRINT_model

apply_DIRINT_model(
    use_pressure=False, use_dew_temperature=False
)

apply_DIRINT_model(self, use_pressure=False, use_dew_temperature=False)

Determines direct normal irradiance (DNI) using the pvlib.irradiance.dirint() function [1].

Parameters:

  • use_pressure

          Default: False
    
  • use_dew_temperature

                 Default: False
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'global_horizontal_irradiance', 'surface_pressure', 'surface_dew_temperature', 'apparent_solar_zenith', 'air_mass' and 'extra_terrestrial_irradiance'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.dirint.html

[2] Perez, R., P. Ineichen, E. Maxwell, R. Seals and A. Zelenka, (1992). “Dynamic Global-to-Direct Irradiance Conversion Models”. ASHRAE Transactions-Research Series, pp. 354-369

[3] Maxwell, E. L., “A Quasi-Physical Model for Converting Hourly Global Horizontal to Direct Normal Insolation”, Technical Report No. SERI/TR-215-3087, Golden, CO: Solar Energy Research Institute, 1987.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def apply_DIRINT_model(self, use_pressure=False, use_dew_temperature=False):
    """

    apply_DIRINT_model(self, use_pressure=False, use_dew_temperature=False)

    Determines direct normal irradiance (DNI) using the pvlib.irradiance.dirint() function [1].


    Parameters
    ----------
    use_pressure: boolian, optional
                  Default: False

    use_dew_temperature: boolian, optional
                         Default: False

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'global_horizontal_irradiance', 'surface_pressure',
    'surface_dew_temperature', 'apparent_solar_zenith', 'air_mass' and 'extra_terrestrial_irradiance'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.dirint.html

    [2]	Perez, R., P. Ineichen, E. Maxwell, R. Seals and A. Zelenka, (1992). “Dynamic Global-to-Direct Irradiance Conversion Models”. ASHRAE Transactions-Research Series, pp. 354-369

    [3]	Maxwell, E. L., “A Quasi-Physical Model for Converting Hourly Global Horizontal to Direct Normal Insolation”, Technical Report No. SERI/TR-215-3087, Golden, CO: Solar Energy Research Institute, 1987.


    """
    assert "global_horizontal_irradiance" in self.sim_data
    assert "surface_pressure" in self.sim_data
    assert "surface_dew_temperature" in self.sim_data
    assert "apparent_solar_zenith" in self.sim_data
    assert "air_mass" in self.sim_data
    assert "extra_terrestrial_irradiance" in self.sim_data

    # self.sim_data["direct_normal_irradiance"] = solarpower.myDirint(
    #     ghi=self.sim_data['global_horizontal_irradiance'],
    #     zenith=self.sim_data["apparent_solar_zenith"],
    #     pressure=self.sim_data["surface_pressure"],
    #     amRel=self.sim_data["air_mass"],
    #     I0=self.sim_data["extra_terrestrial_irradiance"],
    #     temp_dew=self.sim_data["surface_dew_temperature"],
    #     use_delta_kt_prime=True,)

    use_pressure = True
    use_dew_temperature = True

    g = self.sim_data["global_horizontal_irradiance"].flatten()
    z = self.sim_data["apparent_solar_zenith"].flatten()
    p = self.sim_data["surface_pressure"].flatten() if use_pressure else None
    td = self.sim_data["surface_dew_temperature"].flatten() if use_dew_temperature else None
    times = pd.DatetimeIndex(np.column_stack([self._time_index_ for x in range(self._sim_shape_[1])]).flatten())

    self.sim_data["direct_normal_irradiance"] = (
        pvlib.irradiance.dirint(ghi=g, solar_zenith=z, times=times, pressure=p, temp_dew=td)
        .fillna(0)
        .values.reshape(self._sim_shape_)
    )

    return self

apply_angle_of_incidence_losses_to_poa

apply_angle_of_incidence_losses_to_poa()

apply_angle_of_incidence_losses_to_poa(self)

Applies the angle of incidence losses to the plane-of-array irradiance using the pvlib.pvsystem.iam.physical() function [1].

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'poa_direct', 'poa_ground_diffuse' and 'poa_sky_diffuse'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.iam.physical.html

Source code in reskit/solar/workflows/solar_workflow_manager.py
def apply_angle_of_incidence_losses_to_poa(self):
    """
    apply_angle_of_incidence_losses_to_poa(self)

    Applies the angle of incidence losses to the plane-of-array irradiance using the pvlib.pvsystem.iam.physical() function [1].

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'poa_direct', 'poa_ground_diffuse' and 'poa_sky_diffuse'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.iam.physical.html


    """
    assert "poa_direct" in self.sim_data
    assert "poa_ground_diffuse" in self.sim_data
    assert "poa_sky_diffuse" in self.sim_data

    tilt = self.sim_data.get("system_tilt", self.placements["tilt"].values)

    self.sim_data["poa_direct"] *= pvlib.pvsystem.iam.physical(
        aoi=self.sim_data["angle_of_incidence"],
        n=1.526,  # PVLIB v0.7.2 default
        K=4.0,  # PVLIB v0.7.2 default
        L=0.002,  # PVLIB v0.7.2 default
    )

    # Effective angle of incidence values from "Solar-Engineering-of-Thermal-Processes-4th-Edition"
    self.sim_data["poa_ground_diffuse"] *= pvlib.pvsystem.iam.physical(
        aoi=(90 - 0.5788 * tilt + 0.002693 * np.power(tilt, 2)),
        n=1.526,  # PVLIB v0.7.2 default
        K=4.0,  # PVLIB v0.7.2 default
        L=0.002,  # PVLIB v0.7.2 default
    )

    self.sim_data["poa_sky_diffuse"] *= pvlib.pvsystem.iam.physical(
        aoi=(59.7 - 0.1388 * tilt + 0.001497 * np.power(tilt, 2)),
        n=1.526,  # PVLIB v0.7.2 default
        K=4.0,  # PVLIB v0.7.2 default
        L=0.002,  # PVLIB v0.7.2 default
    )

    self.sim_data["poa_diffuse"] = self.sim_data["poa_ground_diffuse"] + self.sim_data["poa_sky_diffuse"]
    self.sim_data["poa_global"] = self.sim_data["poa_direct"] + self.sim_data["poa_diffuse"]

    assert (self.sim_data["poa_global"] < 1600).all(), "POA is too large"

    return self

apply_azimuth

apply_azimuth()

Applies the azimuth angle for each placement. Three options: 'northsouth': azimuth is always 180° (N-S) 'eastwerst': azimuth is always 90° (E-W) 'song2013': azimuth determined by lat: if |lat|<46: N-S, else: E-W

params: orientation: str see above

Source code in reskit/csp/workflows/csp_workflow_manager.py
def apply_azimuth(self):
    """
    Applies the azimuth angle for each placement. Three options:
    'northsouth':
        azimuth is always 180° (N-S)
    'eastwerst':
        azimuth is always 90° (E-W)
    'song2013':
        azimuth determined by lat: if |lat|<46: N-S, else: E-W

    params:
    orientation: str
        see above
    """
    orientation = self.ptr_data["orientation"]
    accepted_orientation = ["northsouth", "eastwest", "song2013"]
    assert orientation in accepted_orientation

    if orientation == "northsouth":
        self.placements["azimuth"] = 180
    elif orientation == "eastwest":
        self.placements["azimuth"] = 90
    elif orientation == "song2013":

        def apply_song2013(lat):
            """Apply song 2013: if latitude is between -46° and +46°, use northsouth orientation, else eastwest orientation"""
            if lat < 46.06 and lat > -46.06:
                # northsouth
                return 180
            else:
                # eastwest
                return 90

        self.placements["azimuth"] = self.placements["lat"].apply(apply_song2013)

apply_elevation

apply_elevation(elev)

apply_elevation(self)

Adds an elevation (name: 'elev') column to the placements data frame.

Parameters:

  • elev

    If a string is given it must be a path to a rasterfile including the elevations. If a list is given it has to include the elevations at each location.

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
Source code in reskit/csp/workflows/csp_workflow_manager.py
def apply_elevation(self, elev):
    """

    apply_elevation(self)

    Adds an elevation (name: 'elev') column to the placements data frame.

    Parameters
    ----------
    elev: str, list
          If a string is given it must be a path to a rasterfile including the elevations.
          If a list is given it has to include the elevations at each location.


    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    """
    if "elev" in self.placements.columns:
        return self

    if elev == None:
        self.placements["elev"] = 0

    elif isinstance(elev, str):
        clipped_elev = self.ext.pad(0.5).rasterMosaic(elev)
        self.placements["elev"] = gk.raster.interpolateValues(clipped_elev, self.locs)
    else:
        self.placements["elev"] = elev

    return self

apply_inverter_losses

apply_inverter_losses(inverter, method='sandia')

apply_inverter_losses(self, inverter, method="sandia", )

Applies inverter losses using the pvlib.pvsystem.snlinverter() function [1], the pvlib.pvsystem.retrieve_sam() function [2] and the pvlib.pvsystem.adrinverter() function [3].

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'module_dc_power_at_mpp' and 'module_dc_voltage_at_mpp'. Required data in the placements dataframe are 'modules_per_string' and 'strings_per_inverter'. Cannot simultaneously provide 'capacity' and inverter-string parameters.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.snlinverter.html

[2] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.retrieve_sam.html

[3] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.adrinverter.html

[4] SAND2007-5036, “Performance Model for Grid-Connected Photovoltaic Inverters by D. King, S. Gonzalez, G. Galbraith, W. Boyson

[5] System Advisor Model web page. https://sam.nrel.gov.

[6] Beyond the Curves: Modeling the Electrical Efficiency of Photovoltaic Inverters, PVSC 2008, Anton Driesse et. al.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def apply_inverter_losses(
    self,
    inverter,
    method="sandia",
):
    """
     apply_inverter_losses(self, inverter, method="sandia", )

     Applies inverter losses using the pvlib.pvsystem.snlinverter() function [1], the pvlib.pvsystem.retrieve_sam() function [2] and the
     pvlib.pvsystem.adrinverter() function [3].


    Parameters
    ----------
     inverter: str
               Describes the inverter.
               [TODO: Add a more detailed description following the example of 'configure_cec_module']
     method: str
             Options:
             "scandia"
             "driesse"
             Describes the used method to apply the inverter losses.

    Returns
    -------
     Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
     Required data in the sim_data dictionary are 'module_dc_power_at_mpp' and 'module_dc_voltage_at_mpp'.
     Required data in the placements dataframe are 'modules_per_string' and 'strings_per_inverter'.
     Cannot simultaneously provide 'capacity' and inverter-string parameters.


    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.snlinverter.html

    [2] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.retrieve_sam.html

    [3] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.adrinverter.html

    [4]	SAND2007-5036, “Performance Model for Grid-Connected Photovoltaic Inverters by D. King, S. Gonzalez, G. Galbraith, W. Boyson

    [5]	System Advisor Model web page. https://sam.nrel.gov.

    [6]	Beyond the Curves: Modeling the Electrical Efficiency of Photovoltaic Inverters, PVSC 2008, Anton Driesse et. al.

    """
    """method can be: 'sandia' or 'driesse'

    TODO: Make it work with multiplt inverter definitions
    """

    assert "module_dc_power_at_mpp" in self.sim_data
    assert "module_dc_voltage_at_mpp" in self.sim_data
    assert hasattr(self, "module")
    assert "modules_per_string" in self.placements.columns
    assert "strings_per_inverter" in self.placements.columns
    assert not "capacity" in self.placements.columns, (
        "Cannot simultaneously provide 'capacity' and inverter-string parameters"
    )

    if method == "sandia":
        if isinstance(inverter, str):
            db = pvlib.pvsystem.retrieve_sam("SandiaInverter")
            inverter = getattr(db, inverter)

        self.sim_data["inverter_ac_power_at_mpp"] = pvlib.inverter.sandia(
            v_dc=self.sim_data["module_dc_voltage_at_mpp"]
            * np.broadcast_to(self.placements.modules_per_string, self._sim_shape_),
            p_dc=self.sim_data["module_dc_power_at_mpp"]
            * np.broadcast_to(
                self.placements.modules_per_string * self.placements.strings_per_inverter,
                self._sim_shape_,
            ),
            inverter=inverter,
        )

    elif method == "driesse":
        if isinstance(inverter, str):
            db = pvlib.pvsystem.retrieve_sam("CECInverter")
            inverter = getattr(db, inverter)

        self.sim_data["inverter_ac_power_at_mpp"] = pvlib.pvsystem.adrinverter(
            v_dc=self.sim_data["module_dc_voltage_at_mpp"]
            * np.broadcast_to(self.placements.modules_per_string, self._sim_shape_),
            p_dc=self.sim_data["module_dc_power_at_mpp"]
            * np.broadcast_to(
                self.placements.modules_per_string * self.placements.strings_per_inverter,
                self._sim_shape_,
            ),
            inverter=inverter,
        )

    number_of_inverters = getattr(self.placements, "number_of_inverters", 1)
    self.sim_data["total_system_generation"] = self.sim_data["inverter_ac_power_at_mpp"] * np.broadcast_to(
        number_of_inverters, self._sim_shape_
    )

    total_capacity = (
        self.module.I_mp_ref
        * self.module.V_mp_ref
        * self.placements.modules_per_string
        * self.placements.strings_per_inverter
        * number_of_inverters
    )

    self.sim_data["capacity_factor"] = self.sim_data["total_system_generation"] / np.broadcast_to(
        total_capacity, self._sim_shape_
    )

    return self

apply_loss_factor

apply_loss_factor(
    loss: Union[float, ndarray, FunctionType],
    variables: Union[str, List[str]] = ["capacity_factor"],
)

Applies a loss factor onto a specified variable

Parameters:

  • loss

    (Union[float, ndarray, FunctionType]) –

    The loss factor(s) to be applied - If a float or a numpy ndarray is given, then the following operation is performed:

    variable = variable * (1 - loss) - If a function is given, then the following operation is performed: variable = variable * (1 - loss(variable) ) - If a numpy ndarray is given, it must be broadcastable to the variable's shape in .sim_data

  • variables

    (Union[str, List[str]], default: ['capacity_factor'] ) –

    The variable or variables to apply the loss factor to - By default ["capacity_factor"]

Returns:

Source code in reskit/workflow_manager.py
def apply_loss_factor(
    self,
    loss: Union[float, np.ndarray, FunctionType],
    variables: Union[str, List[str]] = ["capacity_factor"],
):
    """Applies a loss factor onto a specified variable

    Parameters
    ----------
    loss : Union[float, np.ndarray, FunctionType]
        The loss factor(s) to be applied
        - If a float or a numpy ndarray is given, then the following operation is performed:
        > variable = variable * (1 - loss)
        - If a function is given, then  the following operation is performed:
        > variable = variable * (1 - loss(variable) )
        - If a numpy ndarray is given, it must be broadcastable to the variable's shape in
        `.sim_data`

    variables : Union[str, List[str]], optional
        The variable or variables to apply the loss factor to
        - By default ["capacity_factor"]

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)
    """
    # filter only existing variables
    _variables = [_var for _var in variables if _var in self.sim_data.keys()]
    if len(_variables) < len(variables):
        warnings.warn(
            f"Loss factor could not be applied to the following requested variables because variables are not in sim_data: {', '.join(sorted(set(variables) - set(_variables)))}"
        )

    for var in _variables:
        if isinstance(loss, FunctionType):
            self.sim_data[var] *= 1 - loss(self.sim_data[var])
        else:
            self.sim_data[var] *= 1 - loss

    return self

calculateEconomics_Plant_Storage_4D

calculateEconomics_Plant_Storage_4D()

Calculate the Capex for the Plant for placements, and variations of TES and SM

Parameters:

  • params

    ([type]) –

    [description]

Returns:

  • [type]

    [description]

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateEconomics_Plant_Storage_4D(self):
    """Calculate the Capex for the Plant for placements, and variations of TES and SM

    Parameters
    ----------
    params : [type]
        [description]

    Returns
    -------
    [type]
        [description]
    """
    assert "CAPEX_plant_cost_EUR_per_kW" in self.ptr_data.index
    assert "CAPEX_storage_cost_EUR_per_kWh" in self.ptr_data.index
    assert "CAPEX_indirect_cost_perc_CAPEX" in self.ptr_data.index
    assert "eta_powerplant_1" in self.ptr_data.index

    # Cost estiamtations for plant and storage:
    # dimensions: [SM, TES]
    sm_2D = np.tile(self.sm, (self.opt_data["dimensions"][3], 1)).T
    tes_2D = np.tile(self.tes, (self.opt_data["dimensions"][2], 1))

    # CAPEX_Plant_Storage per Solar field size
    # dimensions: [SM, TES]
    CAPEX_EUR_per_kW_SF_2D = (
        self.ptr_data["CAPEX_plant_cost_EUR_per_kW"] / sm_2D * self.ptr_data["eta_powerplant_1"]
        + self.ptr_data["CAPEX_storage_cost_EUR_per_kWh"] * tes_2D / sm_2D
    ) * (1 + self.ptr_data["CAPEX_indirect_cost_perc_CAPEX"] / 100)

    # yearly cost of storage and plant
    # dimensions: [SM, TES]
    CAPEX_EUR_per_a_kW_SF_2D = CAPEX_EUR_per_kW_SF_2D * self.sim_data["annuity"]
    varOPEX_EUR_per_a_kW_SF_2D = CAPEX_EUR_per_kW_SF_2D * self.ptr_data["OPEX_perc_CAPEX"] / 100
    fixOPEX_EUR_per_a_kW_SF_2D = 0
    del CAPEX_EUR_per_kW_SF_2D

    # dimensions: [SM, TES]
    TOTEX_Plant_storage_EUR_per_a_kw_SF_2D = (
        CAPEX_EUR_per_a_kW_SF_2D + varOPEX_EUR_per_a_kW_SF_2D + fixOPEX_EUR_per_a_kW_SF_2D
    )
    del (
        CAPEX_EUR_per_a_kW_SF_2D,
        varOPEX_EUR_per_a_kW_SF_2D,
        fixOPEX_EUR_per_a_kW_SF_2D,
    )

    # TOTEX plant and storage per year (absolute)
    # Q_sf_des / 1000 * speccosts_EUR_per_kw_sf_2D
    # dimensions: [placements, SM, TES]
    TOTEX_Plant_storage_EUR_per_a_3D = np.tensordot(
        self.placements["capacity_sf_W_th"] / 1000,
        TOTEX_Plant_storage_EUR_per_a_kw_SF_2D,
        axes=0,
    )

    # costs of field:
    # dimensions: [placements, SM, TES]
    TOTEX_SF_EUR_per_a_3D = np.tensordot(
        np.tensordot(
            self.placements["Totex_SF_EUR_per_a"].values,
            np.ones(self.opt_data["dimensions"][2]),
            axes=0,
        ),
        np.ones(self.opt_data["dimensions"][3]),
        axes=0,
    )
    # dimensions: [placements, SM, TES]
    TOTEX_CSP_EUR_per_a_3D = TOTEX_SF_EUR_per_a_3D + TOTEX_Plant_storage_EUR_per_a_3D

    # cost per heat
    # dimensions: [placements, SM, TES]
    # LCO_Heat_EUR_per_Wh = TOTEX_CSP_EUR_per_a_3D / self.opt_data['annualHeat_Wh_3D']

    # TODO: remove
    self.opt_data["TOTEX_CSP_EUR_per_a_3D"] = TOTEX_CSP_EUR_per_a_3D
    # TODO: remove
    self.opt_data["TOTEX_SF_EUR_per_a_3D"] = TOTEX_SF_EUR_per_a_3D
    self.opt_data["TOTEX_Plant_storage_EUR_per_a_3D"] = TOTEX_Plant_storage_EUR_per_a_3D

    return self

calculateEconomics_SolarField

calculateEconomics_SolarField(
    WACC: float = 8,
    lifetime: float = 25,
    calculationmethod: str = "franzmann2021",
    params: dict = {},
)

Calculating the cost for internal heat from CSP. CAPEX: Contains solar field cost, land cost, indirect cost for solar field OPEX: fixOPEX is 2%/a of CAPEX and varCAPEX is electricity demand for solar field pumping

Parameters:

  • WACC

    (float, default: 8 ) –

    [description], by default 8

  • lifetime

    (float, default: 25 ) –

    [description], by default 25

  • calculationmethod

    (str, default: 'franzmann2021' ) –

    [description], by default 'franzmann2021'

  • params

    (dict, default: {} ) –

    [description], by default {}

Returns:

  • [type]

    [description]

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateEconomics_SolarField(
    self,
    WACC: float = 8,
    lifetime: float = 25,
    calculationmethod: str = "franzmann2021",
    params: dict = {},
):
    """Calculating the cost for internal heat from CSP.
    CAPEX: Contains solar field cost, land cost, indirect cost for solar field
    OPEX: fixOPEX is 2%/a of CAPEX and varCAPEX is electricity demand for solar field pumping

    Parameters
    ----------
    WACC : float, optional
        [description], by default 8
    lifetime : float, optional
        [description], by default 25
    calculationmethod : str, optional
        [description], by default 'franzmann2021'
    params : dict, optional
        [description], by default {}

    Returns
    -------
    [type]
        [description]
    """
    assert "HeattoPlant_W" in self.sim_data.keys()
    assert "Parasitics_solarfield_W_el" in self.sim_data.keys()
    assert "CAPEX_solar_field_EUR_per_m^2_aperture" in params.keys()
    assert "CAPEX_land_EUR_per_m^2_land" in params.keys()
    assert "CAPEX_indirect_cost_perc_CAPEX" in params.keys()
    assert "OPEX_perc_CAPEX" in params.keys()

    # calculate from percent to abs value
    if WACC > 1:
        WACC = WACC / 100
    # Calculate annuity factor from WACC and lifetime like in Heuser
    self.sim_data["annuity"] = (WACC * (1 + WACC) ** lifetime) / ((1 + WACC) ** lifetime - 1)

    dt = (self._time_index_[1] - self._time_index_[0]) / pd.Timedelta(hours=1)
    # calculate the average annual heat production
    self.placements["annualHeatfromSF_Wh"] = self.sim_data["HeattoPlant_W"].mean(axis=0) * (
        (self._time_index_[-1] - self._time_index_[0]) / pd.Timedelta(hours=1)
    )

    if calculationmethod == "franzmann2021":
        # assert 'CAPEX_solar_field_EUR_per_m^2_aperture' in params.keys(), "'CAPEX_solar_field_EUR_per_m^2_aperture' needs to be in params"
        # assert 'CAPEX_land_EUR_per_m^2_land' in params.keys(), "'CAPEX_land_EUR_per_m^2_land' needs to be in params"
        # assert 'fixOPEX_%_CAPEX_per_a' in params.keys(), "'_CAPEX_per_a' needs to be in params"
        # assert 'indirect_cost_%_CAPEX' in params.keys(), "'indirect_cost_EUR' needs to be in params"

        self.placements["CAPEX_SF_EUR"] = (
            self.placements["aperture_area_m2"] * params["CAPEX_solar_field_EUR_per_m^2_aperture"]
            + self.placements["land_area_m2"] * params["CAPEX_land_EUR_per_m^2_land"]
        ) * (1 + params["CAPEX_indirect_cost_perc_CAPEX"] / 100)

    elif False:
        pass

    # calculate annual Costs
    Capex_SF_EUR_per_a = self.placements["CAPEX_SF_EUR"] * self.sim_data["annuity"]
    opexFix_SF_EUR_per_a = self.placements["CAPEX_SF_EUR"] * params["OPEX_perc_CAPEX"] / 100

    # calculate opex
    dt = (self._time_index_[1] - self._time_index_[0]) / pd.Timedelta(hours=1)
    opexVar_SF_EUR_per_a = (
        self.sim_data["Parasitics_solarfield_W_el"].sum(axis=0)
        / 1000
        * dt
        * params["electricity_price_EUR_per_kWh"]
    )

    # calculate annual Totex
    self.placements["Totex_SF_EUR_per_a"] = Capex_SF_EUR_per_a + opexFix_SF_EUR_per_a + opexVar_SF_EUR_per_a

    # Cost relative to Heat
    self.placements["LCO_Heat_SF_EURct_per_kWh"] = (
        self.placements["Totex_SF_EUR_per_a"] / self.placements["annualHeatfromSF_Wh"] * 1e2 * 1e3
    )  # EUR/Wh to EURct/kWh

    return self

calculateHeattoHTF

calculateHeattoHTF(
    eta_ptr_max: float = 0.742,
    eta_cleaness: float = 1,
    eta_other: float = 0.99,
)

Calculates the heat from Collector to heat transfer fluid. The result is before the heat losses of the HTF.

Args: eta_ptr_max (float, optional): [Value for optical efficiency of through mirror and absorber]. Defaults to 0.742. eta_cleaness (float, optional): Cleannes factor of the solar receiver (mirrors) A_aperture_sf (int, optional): [Size of the solar field in m^2]. Defaults to 909060.

Returns:

  • [type]: [description]
Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateHeattoHTF(
    self,
    eta_ptr_max: float = 0.742,
    eta_cleaness: float = 1,
    eta_other: float = 0.99,
):
    """Calculates the heat from Collector to heat transfer fluid. The result is before the heat losses of the HTF.

    Args:
        eta_ptr_max (float, optional): [Value for optical efficiency of through mirror and absorber]. Defaults to 0.742.
        eta_cleaness (float, optional): Cleannes factor of the solar receiver (mirrors)
        A_aperture_sf (int, optional): [Size of the solar field in m^2]. Defaults to 909060.

    Returns
    -------
        [type]: [description]
    """
    # units
    # eta_ptr_max: 1
    # costheta: 1
    # IAM: 1
    # eta_shdw: 1
    # A_aperture_sf: m^2
    # direct_normal_irradiance: W/m^2
    assert ~np.isnan(self.sim_data["direct_normal_irradiance"]).any()

    self.sim_data["HeattoHTF_W"] = (
        eta_ptr_max
        * eta_cleaness
        * eta_other
        * np.cos(np.deg2rad(self.sim_data["theta"]))
        * self.sim_data["IAM"]
        * self.sim_data["eta_shdw"]
        * self.sim_data["eta_wind"]
        * self.sim_data["eta_degradation"]
        * self.placements["aperture_area_m2"].values
        * self.sim_data["direct_normal_irradiance"]
    )

    assert ~np.isnan(self.sim_data["HeattoHTF_W"]).any()

    # self.sim_data['P_DNI'] = self.placements['aperture_area_m2'].values * self.sim_data['direct_normal_irradiance']

    # self.sim_data['P_DNI_eta_opt'] = self.placements['aperture_area_m2'].values * self.sim_data['direct_normal_irradiance'] * eta_ptr_max

    return self

calculateIAM

calculateIAM(
    a1: float = 0.000884,
    a2: float = 5.369e-05,
    a3: float = 0,
)

Calculates the IAM (Incident Angle Modifier) angle modifier from incidence angle. Formula and default values are from: [1] GAFUROV, Tokhir, Julio USAOLA, and Milan PRODANOVIC. Modelling of concentrating solar power plant for power system reliability studies [online]. IET Renewable Power Generation. 2015, 9(2), 120-130. Available from: 10.1049/iet-rpg.2013.0377.

Args: a1 (float, optional): IAM-modifier1. Defaults to 0.000884. a2 (float, optional): IAM-modifier1. Defaults to 0.00005369. a3 (float, optional): IAM-modifier1. Defaults to 0.

Returns:

  • [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['IAM'][timeserie_iter, location_iter]
Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateIAM(self, a1: float = 0.000884, a2: float = 0.00005369, a3: float = 0):
    """Calculates the IAM (Incident Angle Modifier) angle modifier from incidence angle. Formula and default values are from:
    [1]	GAFUROV, Tokhir, Julio USAOLA, and Milan PRODANOVIC. Modelling of concentrating
    solar power plant for power system reliability studies [online]. IET Renewable Power
    Generation. 2015, 9(2), 120-130. Available from: 10.1049/iet-rpg.2013.0377.

    Args:
        a1 (float, optional): IAM-modifier1. Defaults to 0.000884.
        a2 (float, optional): IAM-modifier1. Defaults to 0.00005369.
        a3 (float, optional): IAM-modifier1. Defaults to 0.

    Returns
    -------
        [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['IAM'][timeserie_iter, location_iter]
    """
    # check for input data availability
    assert "theta" in list(self.sim_data.keys())

    _theta = self.sim_data["theta"]  # deg

    # calculate with formula for IAM: IAM = 1 + 'sum over i'  a_i * (theta^i / cos (theta))

    # a_i, theta in deg
    _IAM = (
        1
        - a1 * np.power(_theta, 1) / np.cos(np.deg2rad(_theta))
        - a2 * np.power(_theta, 2) / np.cos(np.deg2rad(_theta))
        - a3 * np.power(_theta, 3) / np.cos(np.deg2rad(_theta))
    )

    # replace nan with zero
    self.sim_data["IAM"] = np.maximum(np.nan_to_num(_IAM, nan=0), 0)

    return self

calculateParasitics

calculateParasitics(
    calculationmethod: str = "gafurov2013",
    params: dict = {},
)

Calculating the parasitic losses of the plant

Parameters:

  • calculationmethod

    (str, default: 'gafurov2013' ) –

    [description], by default 'gafurov2013'

  • params

    (dict, default: {} ) –

    For calculationmethod gafurov013: PL_plant_fix: Fixed plant losses in % of design point power output of the plant PL_sf_track: Fixed solar field losses in % of design point power output of the field PL_sf_pumping: Solar field pumping losses in % of design point power output PL_plant_other: Plant Pumping losses in % of design point power output

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateParasitics(self, calculationmethod: str = "gafurov2013", params: dict = {}):
    """Calculating the parasitic losses of the plant

    Parameters
    ----------
    calculationmethod : str, optional
        [description], by default 'gafurov2013'
    params : dict, optional
        For calculationmethod gafurov013:
            PL_plant_fix: Fixed plant losses in % of design point power output of the plant
            PL_sf_track: Fixed solar field losses in % of design point power output of the field
            PL_sf_pumping: Solar field pumping losses in % of design point power output
            PL_plant_other: Plant Pumping losses in % of design point power output

    """
    # estimate design parameters:

    # Q_sf,des is the design point power output of the solar field
    # P_pb_des is the design point power output of the plant
    nominal_efficiency_power_block = self.ptr_data["eta_powerplant_1"]
    SM = 2

    Q_sf_des = self.placements["capacity_sf_W_th"].values  # W
    P_pb_des = Q_sf_des * nominal_efficiency_power_block / SM

    P_pb = self.sim_data["HeattoPlant_W"] * nominal_efficiency_power_block

    if calculationmethod == "gafurov2013":
        assert "PL_plant_fix" in params.keys()
        assert "PL_sf_track" in params.keys()
        assert "PL_sf_pumping" in params.keys()
        assert "PL_plant_pumping" in params.keys()
        assert "PL_plant_other" in params.keys()

        # CALCULATION

        # PL_csp,fix
        PL_plant_fix = params["PL_plant_fix"] * P_pb

        # PL_sf_track
        PL_sf_track = params["PL_sf_track"] * P_pb_des * (self.sim_data["solar_zenith_degree"] < 90)

        # PL_sf_pumping
        PL_sf_pumping = params["PL_sf_pumping"] * Q_sf_des * np.power(self.sim_data["HeattoPlant_W"] / Q_sf_des, 3)
        # PL_plant_pumping
        PL_plant_pumping = params["PL_plant_pumping"] * self.sim_data["HeattoPlant_W"]

        # PL_plant_other
        PL_plant_other = params["PL_plant_other"] * P_pb

        # self.sim_data['PL_sf_track'] = PL_sf_track
        # self.sim_data['PL_sf_pumping'] = PL_sf_pumping

        # + self.sim_data['P_heating_W']#issue #13
        self.sim_data["Parasitics_solarfield_W_el"] = PL_sf_track + PL_sf_pumping
        self.sim_data["Parasitics_plant_W_el"] = PL_plant_fix + PL_plant_pumping + PL_plant_other

    elif calculationmethod == "dersch2018":
        assert "PL_sf_fixed_W_per_m^2_ap" in params.keys()
        assert "PL_sf_pumping_W_per_m^2_ap" in params.keys()
        assert "PL_plant_fix" in params.keys()
        assert "PL_plant_pumping" in params.keys()
        assert "PL_plant_other" in params.keys()

        PL_sf_track = (
            params["PL_sf_fixed_W_per_m^2_ap"]
            * self.placements["aperture_area_m2"].values
            * (self.sim_data["HeattoHTF_W"] > 0)
        )
        PL_sf_pumping = (
            params["PL_sf_pumping_W_per_m^2_ap"]
            * self.placements["aperture_area_m2"].values
            * np.power(self.sim_data["HeattoPlant_W"] / Q_sf_des, 2)
        )  # * 830/self.placements['I_DNI_nom_W_per_m2'].values), 2) #used for valiadaton, as 830 as DNI_des is used in reference data

        # + self.sim_data['P_heating_W']#issue #13
        self.sim_data["Parasitics_solarfield_W_el"] = PL_sf_track + PL_sf_pumping

        # Plant from Gaforov
        PL_plant_fix = params["PL_plant_fix"] * P_pb
        PL_plant_pumping = params["PL_plant_pumping"] * self.sim_data["HeattoPlant_W"]
        PL_plant_other = params["PL_plant_other"] * P_pb
        self.sim_data["Parasitics_plant_W_el"] = PL_plant_fix + PL_plant_pumping + PL_plant_other

    else:
        raise ValueError('calculationmethod for parasitic losses not known. Use "gafurov2013" or "dersch2018"')

    self.sim_data["Parasitics_W_el"] = (
        self.sim_data["Parasitics_solarfield_W_el"] + self.sim_data["Parasitics_plant_W_el"]
    )
    self.placements["Parasitics_solarfield_Wh_el_per_a"] = self.sim_data["Parasitics_solarfield_W_el"].sum(axis=0)
    self.placements["Parasitics_plant_Wh_el_per_a"] = self.sim_data["Parasitics_plant_W_el"].sum(axis=0)

    assert not np.isnan(self.sim_data["Parasitics_solarfield_W_el"]).any()
    assert not np.isnan(self.sim_data["Parasitics_plant_W_el"]).any()

    return self

calculateShadowLosses

calculateShadowLosses(
    SF_density: float = 0.383, method: str = "wagner2011"
)

Estimates shadow losses from solar field density and solar altitude

Args: SF_density (float, optional): [description]. Defaults to 0.383. method(str, optional): [choose from 'wagner2011' and 'gafurov2015']

Returns:

  • [type]: [description]
References

[1] WAGNER, Michael J. and Paul GILMAN. Technical Manual for the SAM Physical Trough Model, 2011. [2] GAFUROV, Tokhir, Julio USAOLA, and Milan PRODANOVIC. Modelling of concentrating solar power plant for power system reliability studies [online]. IET Renewable Power Generation. 2015, 9(2), 120-130. Available from: 10.1049/iet-rpg.2013.0377.

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateShadowLosses(self, SF_density: float = 0.383, method: str = "wagner2011"):
    """Estimates shadow losses from solar field density and solar altitude

    Args:
        SF_density (float, optional): [description]. Defaults to 0.383.
        method(str, optional): [choose from 'wagner2011' and 'gafurov2015']

    Returns
    -------
        [type]: [description]

    References
    ----------
    [1]	WAGNER, Michael J. and Paul GILMAN. Technical Manual for the SAM Physical Trough Model, 2011.
    [2]	GAFUROV, Tokhir, Julio USAOLA, and Milan PRODANOVIC. Modelling of concentrating
    solar power plant for power system reliability studies [online]. IET Renewable Power
    Generation. 2015, 9(2), 120-130. Available from: 10.1049/iet-rpg.2013.0377.
    """
    method = method.lower()

    if method == "wagner2011":
        # equation 2.38 from [1]	WAGNER, Michael J. and Paul GILMAN. Technical Manual for the SAM Physical Trough Model, 2011.
        # keep in mind, that cos(zenith) is replaced by sin(solar altitude angle)
        # value output is limited to 0... 1
        # self.sim_data['eta_shdw'] = np.minimum(np.abs(np.sin(np.deg2rad(self.sim_data['solar_altitude_angle_degree']))) / SF_density, 1)
        assert "tracking_angle" in self.sim_data.keys()
        assert "solar_zenith_degree" in self.sim_data.keys()

        self.sim_data["eta_shdw"] = np.minimum(
            np.abs(np.cos(np.deg2rad(self.sim_data["tracking_angle"]))) / SF_density,
            1,
        )  # TODO
        self.sim_data["eta_shdw"][self.sim_data["solar_zenith_degree"] > 90] = 0

    elif method == "gafurov2015":
        warning("The method gafurov2015 for shadow losses is not fully implemented!")
        assert "solar_altitude_angle_degree" in self.sim_data.keys()
        self.sim_data["eta_shdw"] = np.sin(np.deg2rad(self.sim_data["solar_altitude_angle_degree"])) / (
            SF_density * np.cos(np.deg2rad(self.sim_data["theta"]))
        )

    return self

calculateSolarPosition

calculateSolarPosition()

Calculates the solar position in terms of hour angle and declination from time series and location series of the current object

Returns:

  • [CSPWorkflowManager]: Updated CSPWorkflowManager with new values for sim_data['values'][timeserie_iter, location_iter]. The calculated values are:
    • solar_zenith_degree: solar zenith angle
    • solar_altitude_angle_degree: solar altitude (elevation) angle in degrees
    • aoi_northsouth: angle of incidence for northsouth-orientation of through
    • aoi_eastwest: angle of incidence for eastwest-orientation of through
Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateSolarPosition(self):
    """Calculates the solar position in terms of hour angle and declination from time series and location series of the current object

    Returns
    -------
        [CSPWorkflowManager]: Updated CSPWorkflowManager with new values for sim_data['values'][timeserie_iter, location_iter]. The calculated values are:
                                - solar_zenith_degree: solar zenith angle
                                - solar_altitude_angle_degree: solar altitude (elevation) angle in degrees
                                - aoi_northsouth: angle of incidence for northsouth-orientation of through
                                - aoi_eastwest: angle of incidence for eastwest-orientation of through

    """
    # check for inputs
    assert "lat" in self.placements.columns
    assert "lon" in self.placements.columns
    assert "elev" in self.placements.columns
    assert "azimuth" in self.placements.columns
    assert hasattr(self, "time_index")
    assert self.ptr_data["SF_density_direct"] < 1

    # set up empty array
    self.sim_data["solar_zenith_degree"] = np.empty(shape=(self._numtimesteps, self._numlocations))
    self.sim_data["theta"] = np.empty(shape=(self._numtimesteps, self._numlocations))
    self.sim_data["tracking_angle"] = np.empty(shape=(self._numtimesteps, self._numlocations))

    # iterate through all location
    for location_iter, row in enumerate(self.placements[["lon", "lat", "elev", "azimuth"]].itertuples()):
        # calculate the solar position
        _solarpos = pvlib.solarposition.get_solarposition(
            time=self.time_index,
            latitude=row.lat,
            longitude=row.lon,
            altitude=row.elev,
            # pressure=self.sim_data["surface_pressure"][:, location_iter], #TODO: insert here
            # temperature=self.sim_data["surface_air_temperature"][:, location_iter], #TODO: insert here
            method="nrel_numba",
        )

        self.sim_data["solar_zenith_degree"][:, location_iter] = np.nan_to_num(
            _solarpos["apparent_zenith"].values, 0
        )

        # calculate aoi
        truetracking_angles = pvlib.tracking.singleaxis(
            apparent_zenith=_solarpos["apparent_zenith"],
            apparent_azimuth=_solarpos["azimuth"],
            axis_tilt=0,
            axis_azimuth=row.azimuth,
            max_angle=90,
            backtrack=False,  # for true-tracking
            gcr=self.ptr_data["SF_density_direct"],
        )  # irrelevant for true-tracking

        self.sim_data["theta"][:, location_iter] = np.nan_to_num(truetracking_angles["aoi"].values, 0)
        self.sim_data["tracking_angle"][:, location_iter] = np.nan_to_num(
            truetracking_angles["tracker_theta"].values, 0
        )

        # from [1]	KALOGIROU, Soteris A. Environmental Characteristics. In: Soteris Kalogirou, ed. Solar energy engineering. Processes and systems. Waltham, Mass: Academic Press, 2014, pp. 51-123.
        # fromula 2.12

    self.sim_data["solar_altitude_angle_degree"] = np.rad2deg(
        np.arcsin(np.cos(np.deg2rad(self.sim_data["solar_zenith_degree"])))
    )

    return self

calculateSolarPositionfaster

calculateSolarPositionfaster()

DOES NOT WORK PV LIP DOES NOT SUPPORT MULTIPLE LOCATIONS calculates the solar position in terms of hour angle and declination from time series and location series of the current object

Returns:

  • CSPWorkflowManager: Updated CSPWorkflowManager with new values for

    sim_data['hour_angle'][timeserie_iter, location_iter] and sim_data['declination_angle'][timeserie_iter, location_iter].

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateSolarPositionfaster(self):
    """
    DOES NOT WORK PV LIP DOES NOT SUPPORT MULTIPLE LOCATIONS
    calculates the solar position in terms of hour angle and declination from time series and location series of the current object

    Returns
    -------
        CSPWorkflowManager: Updated CSPWorkflowManager with new values for
        ``sim_data['hour_angle'][timeserie_iter, location_iter]`` and
        ``sim_data['declination_angle'][timeserie_iter, location_iter]``.
    """
    assert "lat" in self.placements.columns
    assert "lon" in self.placements.columns
    assert "elev" in self.placements.columns

    # Shape Input data to 1D
    _time = pd.DatetimeIndex(np.tile(self.time_index.values, self._numlocations))
    # _dayoftheyear = pd.DataFrame(np.tile(self.time_index.day_of_year.values, self._numlocations))
    _latitute = pd.DataFrame(self.placements["lat"].values.repeat(self._numtimesteps))
    _longitude = pd.DataFrame(self.placements["lon"].values.repeat(self._numtimesteps))
    _elevation = pd.DataFrame(self.placements["elev"].values.repeat(self._numtimesteps))

    _solarpos = pvlib.solarposition.spa_python(
        _time.values.squeeze(),
        latitude=_latitute.values.squeeze(),
        longitude=_longitude.values.squeeze(),
        # altitude=_elevation.values.squeeze(),
    )

    self.sim_data["solar_zenith_degree_fast"] = np.reshape(
        a=_solarpos["apparent_zenith"].values,
        newshape=(self._numlocations, self._numtimesteps),
    ).T

    # calculate aoi
    truetracking_angles = pvlib.tracking.singleaxis(
        apparent_zenith=_solarpos["apparent_zenith"],
        apparent_azimuth=_solarpos["azimuth"],
        axis_tilt=0,
        axis_azimuth=180,
        max_angle=90,
        backtrack=False,  # for true-tracking
        gcr=0.5,
    )  # irrelevant for true-tracking

    _aoi_northsouth = np.nan_to_num(truetracking_angles["aoi"].values)
    self.sim_data["aoi_northsouth_fast"] = np.reshape(
        a=_aoi_northsouth, newshape=(self._numlocations, self._numtimesteps)
    ).T

    # calculate aoi
    truetracking_angles = pvlib.tracking.singleaxis(
        apparent_zenith=_solarpos["apparent_zenith"],
        apparent_azimuth=_solarpos["azimuth"],
        axis_tilt=0,
        axis_azimuth=90,
        max_angle=180,
        backtrack=False,  # for true-tracking
        gcr=0.5,
    )  # irrelevant for true-tracking

    _aoi_eastwest = np.nan_to_num(truetracking_angles["aoi"].values)
    self.sim_data["aoi_eastwest_fast"] = np.reshape(
        a=_aoi_eastwest, newshape=(self._numlocations, self._numtimesteps)
    ).T

    # from [1]	KALOGIROU, Soteris A. Environmental Characteristics. In: Soteris Kalogirou, ed. Solar energy engineering. Processes and systems. Waltham, Mass: Academic Press, 2014, pp. 51-123.
    # fromula 2.12

    self.sim_data["solar_altitude_angle_degree_fast"] = np.rad2deg(
        np.arcsin(np.cos(np.deg2rad(self.sim_data["solar_zenith_degree_fast"])))
    )

    return self

calculateWindspeedLosses

calculateWindspeedLosses(
    max_windspeed_threshold: float = 14,
)

If windspeed is above threshold, the efficiency is set to zero.

Args: max_windspeed_threshold (float, optional): [description]. Defaults to 9999.

Returns:

  • [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['eta_wind'][timeserie_iter, location_iter] as np.narray
Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculateWindspeedLosses(self, max_windspeed_threshold: float = 14):
    """If windspeed is above threshold, the efficiency is set to zero.

    Args:
        max_windspeed_threshold (float, optional): [description]. Defaults to 9999.

    Returns
    -------
        [CSPWorkflowManager]: [Updated CSPWorkflowManager with new value for sim_data['eta_wind'][timeserie_iter, location_iter] as np.narray
    """
    assert "surface_wind_speed" in self.sim_data.keys()

    self.sim_data["eta_wind"] = np.less_equal(self.sim_data["surface_wind_speed"], max_windspeed_threshold).astype(
        int
    )

    return self

calculate_LCOE

calculate_LCOE()

Calculates the LCOE from plant and storage sizes, SF totex and Net power output

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculate_LCOE(self):
    """Calculates the LCOE from plant and storage sizes, SF totex and Net power output"""
    # calculate_economics
    TOTEX_EUR_per_a = self._get_totex_from_self()

    CAPEX_total_EUR = self._get_capex(
        A_aperture_m2=self.placements["aperture_area_m2"],
        A_land_m2=self.placements["land_area_m2"],
        Qdot_field_des_W=self.placements["capacity_sf_W_th"],
        eta_des_power_plant=self.ptr_data["eta_powerplant_1"],
        sm=self.placements["sm_opt"],
        tes=self.placements["tes_opt"],
        c_field_per_aperture_area_EUR_per_m2=self.ptr_data["CAPEX_solar_field_EUR_per_m^2_aperture"],
        c_land_per_land_area_EUR_per_m2=self.ptr_data["CAPEX_land_EUR_per_m^2_land"],
        c_storage_EUR_per_kWh_th=self.ptr_data["CAPEX_storage_cost_EUR_per_kWh"],
        c_plant_EUR_per_kW_el=self.ptr_data["CAPEX_plant_cost_EUR_per_kW"],
        c_indirect_cost_perc_per_direct_Capex=self.ptr_data["CAPEX_indirect_cost_perc_CAPEX"],
    )
    #     #annual cost
    # CAPEX_total_EUR_per_a = CAPEX_total_EUR * self.sim_data['annuity']

    # OPEX_EUR_per_a = self._get_opex(
    #     CAPEX_total_EUR=CAPEX_total_EUR,
    #     OPEX_fix_perc_CAPEX_per_a=self.ptr_data['OPEX_%_CAPEX'],
    #     auxilary_power_Wh_per_a=self.sim_data['Parasitics_solarfield_W_el'].sum(axis=0),
    #     electricity_price_EUR_per_kWh=self.ptr_data['electricity_price_EUR_per_kWh'],
    # )

    # TOTEX_EUR_per_a = self._get_totex(
    #     CAPEX_total_EUR_per_a=CAPEX_total_EUR_per_a,
    #     OPEX_EUR_per_a=OPEX_EUR_per_a,
    # )

    self.placements["CAPEX_total_EUR"] = CAPEX_total_EUR
    self.placements["lcoe_EURct_per_kWh_el"] = (
        TOTEX_EUR_per_a / self.placements["Power_net_total_Wh_per_a"] * 1e2 * 1e3
    )  # EUR/WH to EURct/kWh

calculate_electrical_output

calculate_electrical_output(
    onlynightuse=True, debug_vars=False
)

From sm and tes opt, calculate the electrical output. idea: as much energy as possible will be stored to be flexible, the rest is forced to be depending on solar radiation

Source code in reskit/csp/workflows/csp_workflow_manager.py
def calculate_electrical_output(self, onlynightuse=True, debug_vars=False):
    """From sm and tes opt, calculate the electrical output.
    idea: as much energy as possible will be stored to be flexible,
    the rest is forced to be depending on solar radiation
    """
    assert "storage_capacity_kWh_th" in self.placements.columns
    assert "power_plant_capacity_W_el" in self.placements.columns

    pass

    # aggregate_by_day
    dt = (self._time_index_[1] - self._time_index_[0]) / pd.Timedelta(hours=1)
    if hasattr(self, "aggregate_by_day"):
        aggregate_by_day = self.aggregate_by_day
    else:
        aggregate_by_day = np.eye(365).repeat(24, axis=1)
    HeattoPlant_per_day_Wh = np.einsum("ij,jk", aggregate_by_day, self.sim_data["HeattoPlant_W"]) * dt
    Parasitics_plant_per_day_Wh_el = (
        np.einsum("ij,jk", aggregate_by_day, self.sim_data["Parasitics_W_el"]) * dt
    )  # issue #13
    Heat_heating_sf_daily_Wh_th = np.einsum("ij,jk", aggregate_by_day, self.sim_data["P_heating_W"])  # issue #13

    # max thermal input the power plant is capable of
    if onlynightuse:
        operationalhours_per_day = np.einsum("ij,jk", aggregate_by_day, (self.sim_data["solar_zenith_degree"] > 90))
        # for placements in the north, there might be days withoutnight. catch that
        operationalhours_per_day = np.maximum(operationalhours_per_day, 3)
    else:
        operationalhours_per_day = 24
    if isinstance(operationalhours_per_day, int):
        assert operationalhours_per_day > 0
    else:
        assert (operationalhours_per_day > 0).all()
    power_plant_max_heat_Wh = (
        self.placements["power_plant_capacity_W_el"].values
        / self.ptr_data["eta_powerplant_1"]
        * operationalhours_per_day
    )

    # calculate stored and directly used heat per day
    # heat transferred into the storage (preferred, as max dispatchability is good)
    # limit by storage size
    Heat_stored_per_day_Wh = np.minimum(HeattoPlant_per_day_Wh, self.placements["storage_capacity_kWh_th"] * 1000)
    Heat_from_storage_per_day_Wh = np.maximum(Heat_stored_per_day_Wh * self.ptr_data["storage_efficiency_1"], 0)
    # auxiliary heating
    Heat_from_storage_net_per_day_Wh = np.maximum(Heat_from_storage_per_day_Wh - Heat_heating_sf_daily_Wh_th, 0)
    self.sim_data_daily["P_backup_heating_daily_Wh_el"] = np.maximum(
        Heat_heating_sf_daily_Wh_th - Heat_from_storage_per_day_Wh, 0
    )
    # limit by plant size
    Heat_from_storage_used_per_day_Wh = np.minimum(Heat_from_storage_net_per_day_Wh, power_plant_max_heat_Wh)

    # heat transferred directly to the plant (2nd option)
    if onlynightuse:
        Heat_directly_per_day_Wh = 0
    else:
        Heat_directly_per_day_Wh = np.minimum(
            HeattoPlant_per_day_Wh - Heat_stored_per_day_Wh,  # maximum heat available
            # maximum heat capable for the plant (cf_day=1)
            power_plant_max_heat_Wh - Heat_from_storage_used_per_day_Wh,
        )
    # # heat output from the storage
    # Heat_from_storage_per_day_Wh = np.maximum(Heat_stored_per_day_Wh * self.ptr_data['storage_efficiency_1'],0) #13
    # Heat_from_storage_usable_per_day_Wh = np.maximum(Heat_from_storage_per_day_Wh - Heat_heating_sf_daily_Wh_th,0)
    # self.sim_data_daily['P_backup_heating_daily_Wh_el'] = np.maximum(Heat_heating_sf_daily_Wh_th - Heat_stored_per_day_Wh * self.ptr_data['storage_efficiency_1'],0) #13
    # self.placements['P_backup_heating_Wh_el'] = self.sim_data_daily['P_backup_heating_daily_Wh_el'].sum(axis=0)
    # self.placements['Q_sf_heating_from_storage_Wh_th'] = Heat_heating_sf_daily_Wh_th.sum(axis=0) - self.placements['P_backup_heating_Wh_el']
    # #Parasitics_plant_per_day_Wh_el += P_backup_heating_daily_Wh_el

    # total heat usable
    Heat_total_per_day_Wh = Heat_from_storage_used_per_day_Wh + Heat_directly_per_day_Wh
    assert (Heat_total_per_day_Wh <= power_plant_max_heat_Wh * 1.001).all()
    if debug_vars:
        self.placements["avrg_sf_efficiency_1"] = self.sim_data["HeattoPlant_W"].sum(axis=0) / (
            self.placements["aperture_area_m2"] * self.sim_data["direct_normal_irradiance"].sum(axis=0)
        )
        self.placements["Heat_after_curtailment_1"] = Heat_total_per_day_Wh.sum(axis=0) / self.sim_data[
            "HeattoPlant_W"
        ].sum(axis=0)

        self.placements["heat_dni_Wh"] = self.placements["aperture_area_m2"] * self.sim_data[
            "direct_normal_irradiance"
        ].sum(axis=0)
        self.placements["heat_losses_sf_Wh"] = (
            self.placements["aperture_area_m2"] * self.sim_data["direct_normal_irradiance"].sum(axis=0)
        ) - self.sim_data["HeattoPlant_W"].sum(axis=0)
        self.placements["heat_losses_curtailment_storage_Wh"] = (self.sim_data["HeattoPlant_W"]).sum(axis=0) - (
            Heat_stored_per_day_Wh
        ).sum(axis=0)
        self.placements["heating_sf_from_storage"] = Heat_from_storage_per_day_Wh.sum(
            axis=0
        ) - Heat_from_storage_net_per_day_Wh.sum(axis=0)
        self.placements["heating_sf_from_elec"] = self.sim_data_daily["P_backup_heating_daily_Wh_el"].sum(axis=0)
        self.placements["heat_losses_curtailment_plant_Wh"] = (
            Heat_from_storage_net_per_day_Wh - Heat_from_storage_used_per_day_Wh
        ).sum(axis=0)

    # calculate rel load and efficiency

    # rel load is defined as the ratio of daily output to maximal output.
    # As the Poweplant won't output the total power over the whole day, the formula is corrected by:
    # rel_load* = 0.5 * 0.5 + rel_load
    rel_load_plant = Heat_total_per_day_Wh / power_plant_max_heat_Wh
    assert ~np.isnan(rel_load_plant).any()

    # Gafurov2015:
    # rel_efficiency [1] = 54.92 + 112.73 * rel - 104.63 * rel^2 + 37.05 * rel^3
    # dimensions: [time(days), placements, SM, TES]
    efficiency_daily_averaged_1 = self._get_plant_efficiency(
        rel_load_plant=rel_load_plant, eta_nom=self.ptr_data["eta_powerplant_1"]
    )
    del rel_load_plant

    # calculate gross power output: Heat * daily efficiency
    # calculate net power output : power gross - parasitic * share (share distributes the parasitic evenly to each output power)

    # bound
    Power_gross_bound_per_day_Wh = Heat_directly_per_day_Wh * efficiency_daily_averaged_1
    # with np.seterr(divide='ignore', invalid='ignore'):
    share = np.nan_to_num(Heat_directly_per_day_Wh / Heat_total_per_day_Wh, 0)
    Power_net_bound_per_day_Wh = np.maximum(
        Power_gross_bound_per_day_Wh - (Parasitics_plant_per_day_Wh_el * share), 0
    )

    # dispatchable
    Power_gross_dispatchable_per_day_Wh = Heat_from_storage_used_per_day_Wh * efficiency_daily_averaged_1
    # with np.seterr(divide='ignore', invalid='ignore'):
    share = np.nan_to_num(Heat_from_storage_used_per_day_Wh / Heat_total_per_day_Wh, 0)
    Power_net_dispatchable_per_day_Wh = np.maximum(
        Power_gross_dispatchable_per_day_Wh - (Parasitics_plant_per_day_Wh_el * share),
        0,
    )

    # add up for total output
    Power_net_total_per_day_Wh = Power_net_bound_per_day_Wh + Power_net_dispatchable_per_day_Wh

    if debug_vars:
        self.placements["mean_gross_turbine_efficiency_1"] = (
            Power_gross_dispatchable_per_day_Wh.sum(axis=0) + Power_gross_bound_per_day_Wh.sum(axis=0)
        ) / Heat_total_per_day_Wh.sum(axis=0)
        self.placements["turbine_gross_to_net"] = Power_net_total_per_day_Wh.sum(axis=0) / (
            Power_gross_dispatchable_per_day_Wh.sum(axis=0) + Power_gross_bound_per_day_Wh.sum(axis=0)
        )

        self.placements["heat_losses_turbine_plant_Wh"] = Heat_total_per_day_Wh.sum(axis=0) - (
            Power_gross_dispatchable_per_day_Wh.sum(axis=0) + Power_gross_bound_per_day_Wh.sum(axis=0)
        )
        self.placements["heat_losses_turbine_aux_Wh"] = (
            Power_gross_dispatchable_per_day_Wh.sum(axis=0) + Power_gross_bound_per_day_Wh.sum(axis=0)
        ) - Power_net_total_per_day_Wh.sum(axis=0)

    # get avrg cf
    Power_net_total_Wh_per_a = Power_net_total_per_day_Wh.sum(axis=0)
    steps_per_year = pd.Timedelta(hours=8760) / (self._time_index_[1] - self._time_index_[0])
    Power_cf = Power_net_total_Wh_per_a / (self.placements["power_plant_capacity_W_el"].values * steps_per_year)

    # save data
    self.sim_data_daily["Power_net_total_per_day_Wh"] = Power_net_total_per_day_Wh
    self.sim_data_daily["Power_net_bound_per_day_Wh"] = Power_net_bound_per_day_Wh
    self.placements["Power_net_total_Wh_per_a"] = Power_net_total_Wh_per_a
    self.placements["Power_net_bound_perc_per_a"] = (
        np.nan_to_num(Power_net_bound_per_day_Wh.sum(axis=0) / Power_net_total_Wh_per_a) * 100
    )  # %

cell_temperature_from_sapm

cell_temperature_from_sapm(mounting='glass_open_rack')

cell_temperature_from_sapm(self, mounting="glass_open_rack")

Calculates the cell temperature based on the pvlib.temperature.sapm_cell() function [1].

Parameters:

  • mounting

      Options:
      "glass_open_rack" [1]
      "glass_close_roof" [1]
      "polymer_open_rack" [1]
      "polymer_insulated_back" [1]
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'surface_wind_speed', 'surface_air_temperature' and 'poa_global'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.temperature.sapm_cell.html

Source code in reskit/solar/workflows/solar_workflow_manager.py
def cell_temperature_from_sapm(self, mounting="glass_open_rack"):
    """
    cell_temperature_from_sapm(self, mounting="glass_open_rack")

    Calculates the cell temperature based on the pvlib.temperature.sapm_cell() function [1].


    Parameters
    ----------
    mounting: str
              Options:
              "glass_open_rack" [1]
              "glass_close_roof" [1]
              "polymer_open_rack" [1]
              "polymer_insulated_back" [1]

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'surface_wind_speed', 'surface_air_temperature' and 'poa_global'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.temperature.sapm_cell.html


    """
    assert "surface_wind_speed" in self.sim_data
    assert "surface_air_temperature" in self.sim_data
    assert "poa_global" in self.sim_data

    if mounting == "glass_open_rack":
        a, b, dT = -3.47, -0.0594, 3
    elif mounting == "glass_close_roof":
        a, b, dT = -2.98, -0.0471, 1
    elif mounting == "polymer_open_rack":
        a, b, dT = -3.56, -0.075, 3
    elif mounting == "polymer_insulated_back":
        a, b, dT = -2.81, -0.0455, 0
    else:
        raise RuntimeError(
            "mounting not one of: 'glass_open_rack', 'glass_close_roof', 'polymer_open_rack', or 'polymer_insulated_back'"
        )

    self.sim_data["cell_temperature"] = pvlib.temperature.sapm_cell(
        self.sim_data["poa_global"],
        self.sim_data["surface_air_temperature"],
        self.sim_data["surface_wind_speed"],
        a=a,
        b=b,
        deltaT=dT,
        irrad_ref=1000,
    )

    return self

check_ERA5_input

check_ERA5_input()

Check inputs

Source code in reskit/csp/workflows/csp_workflow_manager.py
def check_ERA5_input(self):
    """Check inputs"""
    assert (self.sim_data["direct_horizontal_irradiance"].mean(axis=0) < 1500).all()

    if (self.sim_data["surface_wind_speed"].mean(axis=0) > 50).any():
        self.sim_data["surface_wind_speed"][self.sim_data["surface_air_temperature"] > 50] = 25
    if (self.sim_data["surface_air_temperature"].mean(axis=0) > 100).any():
        self.sim_data["surface_air_temperature"][self.sim_data["surface_air_temperature"] > 100] = 25

configure_cec_module

configure_cec_module(
    module="WINAICO WSx-240P6", tech_year=2050
)

configure_cec_module(self, module="WINAICO WSx-240P6")

Configures CEC of a module based on the outputs of the pvlib.pvsystem.retrieve_sam() function [1].

Parameters:

  • module

    Must be one of: * A module found in the pvlib.pvsystem.retrieve_sam("CECMod") database * "WINAICO WSx-240P6" -> Good for open-field applications * "LG Electronics LG370Q1C-A5" -> Good for rooftop applications * A dict containing a set of module parameters, including: T_NOCT, A_c, N_s, I_sc_ref, V_oc_ref, I_mp_ref, V_mp_ref, alpha_sc, beta_oc, a_ref, I_L_ref, I_o_ref, R_s, R_sh_ref, Adjust, gamma_r, PTC

  • tech_year

    (int, default: 2050 ) –

    If given in combination with the projected module str names "WINAICO WSx-240P6" or "LG Electronics LG370Q1C-A5", the effifiency will be scaled linearly to the given year. Must then be between year of market comparison in analysis (2019) and 2050. Will be ignored when non-projected existing module names or specific parameters are given, can then be None. By default 2050.

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.retrieve_sam.html

Source code in reskit/solar/workflows/solar_workflow_manager.py
def configure_cec_module(
    self,
    module="WINAICO WSx-240P6",
    tech_year=2050,
):
    """
    configure_cec_module(self, module="WINAICO WSx-240P6")

    Configures CEC of a module based on the outputs of the pvlib.pvsystem.retrieve_sam() function [1].

    Parameters
    ----------
    module: str or dict
        Must be one of:
            * A module found in the pvlib.pvsystem.retrieve_sam("CECMod") database
            * "WINAICO WSx-240P6" -> Good for open-field applications
            * "LG Electronics LG370Q1C-A5" -> Good for rooftop applications
            * A dict containing a set of module parameters, including:
                T_NOCT, A_c, N_s, I_sc_ref, V_oc_ref, I_mp_ref, V_mp_ref, alpha_sc,
                beta_oc, a_ref, I_L_ref, I_o_ref, R_s, R_sh_ref, Adjust, gamma_r, PTC
    tech_year : int, optional
        If given in combination with the projected module str names "WINAICO WSx-240P6" or
        "LG Electronics LG370Q1C-A5", the effifiency will be scaled linearly to the given
        year. Must then be between year of market comparison in analysis (2019) and 2050.
        Will be ignored when non-projected existing module names or specific parameters
        are given, can then be None. By default 2050.

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.retrieve_sam.html


    """

    def _interpolate_module_params(projected_module, original_module_name, tech_year, start_year):
        if not isinstance(tech_year, int):
            raise TypeError(f"tech_year must be an integer when projected module is selected")
        # avoid extrapolations
        if not start_year <= tech_year <= 2050:
            raise ValueError(f"tech_year must be between {start_year} and 2050 (max. projection) for this module")

        # get the original (unprojected) module parameters
        db = pvlib.pvsystem.retrieve_sam("CECMod")
        original_module = getattr(db, original_module_name)
        # scale module parameters to tech_year
        module = pd.Series(index=projected_module.index, dtype="float64")
        for param, val_proj in zip(projected_module.index, projected_module):
            if param == "Date":
                module[param] = str(tech_year)
            elif param in ["Version"]:
                # ignore, set dummy nan
                module[param] = np.nan
            elif isinstance(val_proj, (int, float, np.integer)):
                module[param] = original_module[param] + (val_proj - original_module[param]) * (
                    tech_year - start_year
                ) / (2050 - start_year)
            else:
                assert val_proj == original_module[param], (
                    f"parameter '{param}' is not the same for original ({original_module[param]}) and projected ({val_proj}) modules"
                )
                module[param] = val_proj

        return module

    if isinstance(module, str):
        self.register_workflow_parameter("module_name", module)

        if module == "WINAICO WSx-240P6":
            # define projected module parameters
            module_2050 = pd.Series(
                dict(
                    BIPV="N",
                    Date="6/2/2014",
                    T_NOCT=43,
                    A_c=1.663,
                    N_s=60,
                    I_sc_ref=8.41,
                    V_oc_ref=37.12,
                    I_mp_ref=7.96,
                    V_mp_ref=30.2,
                    alpha_sc=0.001164,
                    beta_oc=-0.12357,
                    a_ref=1.6704,
                    I_L_ref=8.961,
                    I_o_ref=1.66e-11,
                    R_s=0.405,
                    R_sh_ref=326.74,
                    Adjust=4.747,
                    gamma_r=-0.383,
                    Version="NRELv1",
                    PTC=220.2,
                    Technology="Multi-c-Si",
                )
            )

            # scale module parameters to tech_year
            module = _interpolate_module_params(
                projected_module=module_2050,
                original_module_name="WINAICO_WSx_240P6",
                tech_year=tech_year,
                start_year=2019,
            )

            module.name = "WINAICO WSx-240P6"

        elif module == "LG Electronics LG370Q1C-A5":
            # define projected module parameters
            module_2050 = pd.Series(
                dict(
                    BIPV="N",
                    Date="12/14/2016",
                    T_NOCT=45.7,
                    A_c=1.673,
                    N_s=60,
                    I_sc_ref=10.82,
                    V_oc_ref=42.8,
                    I_mp_ref=10.01,
                    V_mp_ref=37,
                    alpha_sc=0.003246,
                    beta_oc=-0.10272,
                    a_ref=1.5532,
                    I_L_ref=10.829,
                    I_o_ref=1.12e-11,
                    R_s=0.079,
                    R_sh_ref=92.96,
                    Adjust=14,
                    gamma_r=-0.32,
                    Version="NRELv1",
                    PTC=347.2,
                    Technology="Mono-c-Si",
                )
            )

            # scale module parameters to tech_year
            module = _interpolate_module_params(
                projected_module=module_2050,
                original_module_name="LG_Electronics_Inc__LG370Q1C_A5",
                tech_year=tech_year,
                start_year=2019,
            )

            module.name = "LG Electronics LG370Q1C-A5"

        elif isinstance(module, str):
            if tech_year is not None:
                warnings.warn(
                    f"NOTE: The tech_year argument is ignored when a specific module is given. Set tech_year to None to silence this warning."
                )
            # Extract module parameters
            db = pvlib.pvsystem.retrieve_sam("CECMod")
            try:
                module = getattr(db, module)
            except:
                raise RuntimeError("The module '{}' is not in the CEC database".format(module))
    else:
        if tech_year is not None:
            print(f"NOTE: The tech_year argument is ignored when specific module parameters are given.")
        module = pd.Series(module)
        assert "T_NOCT" in module.index
        assert "A_c" in module.index
        assert "N_s" in module.index
        assert "I_sc_ref" in module.index
        assert "V_oc_ref" in module.index
        assert "I_mp_ref" in module.index
        assert "V_mp_ref" in module.index
        assert "alpha_sc" in module.index
        assert "beta_oc" in module.index
        assert "a_ref" in module.index
        assert "I_L_ref" in module.index
        assert "I_o_ref" in module.index
        assert "R_s" in module.index
        assert "R_sh_ref" in module.index
        assert "Adjust" in module.index
        assert "gamma_r" in module.index
        assert "PTC" in module.index

        try:
            module_desc = json.dumps(module)
        except:
            module_desc = "user-configured"
        self.register_workflow_parameter("module_desc", module_desc)

    # # Check if we need to add the Desoto parameters
    # # defaults for EgRef and dEgdT taken from the note in the docstring for
    # #  'pvlib.pvsystem.calcparams_desoto'
    # if not "EgRef" in module:
    #     module['EgRef'] = 1.121
    # if not "dEgdT" in module:
    #     module['dEgdT'] = -0.0002677

    self.module = module

    return self

determine_air_mass

determine_air_mass(model='kastenyoung1989')

determine_air_mass(self, model='kastenyoung1989')

Determines air mass using the pvlib function pvlib.atmosphere.get_relative_airmass() [1].

Parameters:

  • model

    default 'kastenyoung1989' [1]

    'simple' - secant(apparent zenith angle) - Note that this gives -inf at zenith=90 [2] 'kasten1966' - See reference [2] - requires apparent sun zenith [2] 'youngirvine1967' - See reference [3] - requires true sun zenith [2] 'kastenyoung1989' - See reference [4] - requires apparent sun zenith [2] 'gueymard1993' - See reference [5] - requires apparent sun zenith [2] 'young1994' - See reference [6] - requires true sun zenith [2] 'pickering2002' - See reference [7] - requires apparent sun zenith [2]

Returns:

  • Nothing is returned.
Notes

Required data in the sim_data dictionary are 'apparent_solar_zenith'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.atmosphere.get_relative_airmass.html

[2] Fritz Kasten. “A New Table and Approximation Formula for the Relative Optical Air Mass”. Technical Report 136, Hanover, N.H.: U.S. Army Material Command, CRREL.

[3] A. T. Young and W. M. Irvine, “Multicolor Photoelectric Photometry of the Brighter Planets,” The Astronomical Journal, vol. 72, pp. 945-950, 1967.

[4] Fritz Kasten and Andrew Young. “Revised optical air mass tables and approximation formula”. Applied Optics 28:4735-4738

[5] C. Gueymard, “Critical analysis and performance assessment of clear sky solar irradiance models using theoretical and measured data,” Solar Energy, vol. 51, pp. 121-138, 1993.

[6] A. T. Young, “AIR-MASS AND REFRACTION,” Applied Optics, vol. 33, pp. 1108-1110, Feb 1994.

[7] Keith A. Pickering. “The Ancient Star Catalog”. DIO 12:1, 20,

[8] Matthew J. Reno, Clifford W. Hansen and Joshua S. Stein, “Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis” Sandia Report, (2012).

Source code in reskit/solar/workflows/solar_workflow_manager.py
def determine_air_mass(self, model="kastenyoung1989"):
    """

    determine_air_mass(self, model='kastenyoung1989')

    Determines air mass using the pvlib function pvlib.atmosphere.get_relative_airmass() [1].


    Parameters
    ----------
    model: str, optional
           default 'kastenyoung1989' [1]

           'simple' - secant(apparent zenith angle) - Note that this gives -inf at zenith=90 [2]
           'kasten1966' - See reference [2] - requires apparent sun zenith [2]
           'youngirvine1967' - See reference [3] - requires true sun zenith [2]
           'kastenyoung1989' - See reference [4] - requires apparent sun zenith [2]
           'gueymard1993' - See reference [5] - requires apparent sun zenith [2]
           'young1994' - See reference [6] - requires true sun zenith [2]
           'pickering2002' - See reference [7] - requires apparent sun zenith [2]



    Returns
    -------
    Nothing is returned.

    Notes
    -----
    Required data in the sim_data dictionary are 'apparent_solar_zenith'.



    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.atmosphere.get_relative_airmass.html

    [2]	Fritz Kasten. “A New Table and Approximation Formula for the Relative Optical Air Mass”. Technical Report 136, Hanover, N.H.: U.S. Army Material Command, CRREL.

    [3]	A. T. Young and W. M. Irvine, “Multicolor Photoelectric Photometry of the Brighter Planets,” The Astronomical Journal, vol. 72, pp. 945-950, 1967.

    [4]	Fritz Kasten and Andrew Young. “Revised optical air mass tables and approximation formula”. Applied Optics 28:4735-4738

    [5]	C. Gueymard, “Critical analysis and performance assessment of clear sky solar irradiance models using theoretical and measured data,” Solar Energy, vol. 51, pp. 121-138, 1993.

    [6]	A. T. Young, “AIR-MASS AND REFRACTION,” Applied Optics, vol. 33, pp. 1108-1110, Feb 1994.

    [7]	Keith A. Pickering. “The Ancient Star Catalog”. DIO 12:1, 20,

    [8]	Matthew J. Reno, Clifford W. Hansen and Joshua S. Stein, “Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis” Sandia Report, (2012).


    """
    assert "apparent_solar_zenith" in self.sim_data

    # 29 because that what the function seems to max out at as zenith approaches 90
    self.sim_data["air_mass"] = np.full_like(self.sim_data["apparent_solar_zenith"], 29)

    s = self.sim_data["apparent_solar_zenith"] < 90
    self.sim_data["air_mass"][s] = pvlib.atmosphere.get_relative_airmass(
        self.sim_data["apparent_solar_zenith"][s], model=model
    )

determine_angle_of_incidence

determine_angle_of_incidence()

determine_angle_of_incidence(self)

Determines the angle of incidence [TODO: credit the PVLib function as you've done in previous examples].

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'apparent_solar_zenith' and 'solar_azimuth'.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def determine_angle_of_incidence(self):
    """

    determine_angle_of_incidence(self)

    Determines the angle of incidence [TODO: credit the PVLib function as you've done in previous examples].

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'apparent_solar_zenith' and 'solar_azimuth'.

    """
    """tracking can be: 'fixed' or 'singleaxis'"""
    assert "apparent_solar_zenith" in self.sim_data
    assert "solar_azimuth" in self.sim_data

    azimuth = self.sim_data.get("system_azimuth", self.placements["azimuth"].values)
    tilt = self.sim_data.get("system_tilt", self.placements["tilt"].values)

    self.sim_data["angle_of_incidence"] = np.nan_to_num(
        pvlib.irradiance.aoi(
            tilt,
            azimuth,
            self.sim_data["apparent_solar_zenith"],
            self.sim_data["solar_azimuth"],
        ),
        0,
    )

    return self

determine_area

determine_area()

Determines the land area, aperture area from given placement dataframe. If only 'area' is given, it will be assumed as land area.

Source code in reskit/csp/workflows/csp_workflow_manager.py
def determine_area(self):
    """Determines the land area, aperture area from given placement dataframe.
    If only 'area' is given, it will be assumed as land area.
    """
    assert hasattr(self, "ptr_data")
    assert "SF_density_total" in self.ptr_data.index
    columns = self.placements.columns

    # if only area in placements:
    if "area" in columns and not "aperture_area_m2" in columns and not "land_area_m2" in columns:
        warn('Key "area" is assumed to be the land area. Abort if wrong!')
        self.placements["land_area_m2"] = self.placements["area"]
        self.placements.drop("area", axis=1)
        self.placements["aperture_area_m2"] = self.placements["land_area_m2"] * self.ptr_data["SF_density_total"]

    if "area_m2" in columns and not "aperture_area_m2" in columns and not "land_area_m2" in columns:
        warn('Key "area" is assumed to be the land area. Abort if wrong!')
        self.placements["land_area_m2"] = self.placements["area_m2"]
        self.placements.drop("area_m2", axis=1)
        self.placements["aperture_area_m2"] = self.placements["land_area_m2"] * self.ptr_data["SF_density_total"]

    # only aperture_area_m2 in placements
    elif "aperture_area_m2" in columns and not "land_area_m2" in columns:
        self.placements["aperture_area_m2"] = self.placements["land_area_m2"] * self.ptr_data["SF_density_total"]

    # only land_area_m2 in placements
    elif "land_area_m2" in columns and not "aperture_area_m2" in columns:
        self.placements["land_area_m2"] = self.placements["aperture_area_m2"] / self.ptr_data["SF_density_total"]

determine_extra_terrestrial_irradiance

determine_extra_terrestrial_irradiance(**kwargs)

determine_extra_terrestrial_irradiance(self, **kwargs)

Determines extra terrestrial irradiance using the pvlib.irradiance.get_extra_radiation() function [1].

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.get_extra_radiation.html

[2] M. Reno, C. Hansen, and J. Stein, “Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis”, Sandia National Laboratories, SAND2012-2389, 2012.

[3] https://web.archive.org/web/20230529210255/http://solardat.uoregon.edu/SolarRadiationBasics.html, Eqs. SR1 and SR2 (the UO SRML moved to solardata.uoregon.edu and dropped this page; the archived copy is cited because it carries the numbered equations)

[4] Partridge, G. W. and Platt, C. M. R. 1976. Radiative Processes in Meteorology and Climatology.

[5] Duffie, J. A. and Beckman, W. A. 1991. Solar Engineering of Thermal Processes, 2nd edn. J. Wiley and Sons, New York.

[6] ASCE, 2005. The ASCE Standardized Reference Evapotranspiration Equation, Environmental and Water Resources Institute of the American Civil Engineers, Ed. R. G. Allen et al.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def determine_extra_terrestrial_irradiance(self, **kwargs):
    """

    determine_extra_terrestrial_irradiance(self, **kwargs)

    Determines extra terrestrial irradiance using the pvlib.irradiance.get_extra_radiation() function [1].

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.


    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.get_extra_radiation.html

    [2]	M. Reno, C. Hansen, and J. Stein, “Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis”, Sandia National Laboratories, SAND2012-2389, 2012.

    [3]	<https://web.archive.org/web/20230529210255/http://solardat.uoregon.edu/SolarRadiationBasics.html>, Eqs. SR1 and SR2
    (the UO SRML moved to solardata.uoregon.edu and dropped this page; the
    archived copy is cited because it carries the numbered equations)

    [4]	Partridge, G. W. and Platt, C. M. R. 1976. Radiative Processes in Meteorology and Climatology.

    [5]	Duffie, J. A. and Beckman, W. A. 1991. Solar Engineering of Thermal Processes, 2nd edn. J. Wiley and Sons, New York.

    [6]	ASCE, 2005. The ASCE Standardized Reference Evapotranspiration Equation, Environmental and Water Resources Institute of the American Civil Engineers, Ed. R. G. Allen et al.

    """
    dni_extra = pvlib.irradiance.get_extra_radiation(self._time_index_, **kwargs).values

    shape = len(self._time_index_), self.locs.count
    self.sim_data["extra_terrestrial_irradiance"] = np.broadcast_to(dni_extra.reshape((shape[0], 1)), shape)

    return self

determine_solar_position

determine_solar_position(
    lon_rounding=1, lat_rounding=1, elev_rounding=-2
)

determine_solar_position(self, lon_rounding=1, lat_rounding=1, elev_rounding=-2)

Calculates azimuth and apparent zenith for each location using the pvlib function pvlib.solarposition.spa_python() [1]. Adds azimuth and apparent zenit to the sim_data dictionary.

Parameters:

  • lon_rounding

          Decimal places that the longitude should be rounded to. Default is 1.
    
  • lat_rounding

          Decimal places that the latitude should be rounded to. Default is 1.
    
  • elev_rounding

          Decimal places that the elevation should be rounded to. Default is -2.
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
Notes

Required columns in the placements dataframe to use this functions are 'lon', 'lat' and 'elev'. Required data in the sim_data dictionary are 'surface_pressure' and 'surface_air_temperature'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.solarposition.spa_python.html

[2] I. Reda and A. Andreas, Solar position algorithm for solar radiation applications. Solar Energy, vol. 76, no. 5, pp. 577-589, 2004.

[3] I. Reda and A. Andreas, Corrigendum to Solar position algorithm for solar radiation applications. Solar Energy, vol. 81, no. 6, p. 838, 2007.

[4] USNO delta T: http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term

Source code in reskit/solar/workflows/solar_workflow_manager.py
def determine_solar_position(self, lon_rounding=1, lat_rounding=1, elev_rounding=-2):
    """

    determine_solar_position(self, lon_rounding=1, lat_rounding=1, elev_rounding=-2)

    Calculates azimuth and apparent zenith for each location using the pvlib function pvlib.solarposition.spa_python() [1].
    Adds azimuth and apparent zenit to the sim_data dictionary.


    Parameters
    ----------
    lon_rounding: int, optional
                  Decimal places that the longitude should be rounded to. Default is 1.

    lat_rounding: int, optional
                  Decimal places that the latitude should be rounded to. Default is 1.

    elev_rounding: int, optional
                  Decimal places that the elevation should be rounded to. Default is -2.

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    Notes
    -----
    Required columns in the placements dataframe to use this functions are 'lon', 'lat' and 'elev'.
    Required data in the sim_data dictionary are 'surface_pressure' and 'surface_air_temperature'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.solarposition.spa_python.html

    [2] I. Reda and A. Andreas, Solar position algorithm for solar
        radiation applications. Solar Energy, vol. 76, no. 5, pp. 577-589, 2004.

    [3] I. Reda and A. Andreas, Corrigendum to Solar position algorithm for
        solar radiation applications. Solar Energy, vol. 81, no. 6, p. 838,
        2007.

    [4] USNO delta T:
        http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term


    """
    assert "lon" in self.placements.columns
    assert "lat" in self.placements.columns
    assert "elev" in self.placements.columns
    assert "surface_pressure" in self.sim_data
    assert "surface_air_temperature" in self.sim_data

    rounded_locs = pd.DataFrame()
    rounded_locs["lon"] = np.round(self.placements["lon"].values, lon_rounding)
    rounded_locs["lat"] = np.round(self.placements["lat"].values, lat_rounding)
    rounded_locs["elev"] = np.round(self.placements["elev"].values, elev_rounding)

    solar_position_library = dict()

    # pd.DataFrame(np.nan, index=self.time_index, columns=self.locs)
    self.sim_data["solar_azimuth"] = np.full_like(self.sim_data["surface_pressure"], np.nan)
    # pd.DataFrame(np.nan, index=self.time_index, columns=self.locs)
    self.sim_data["apparent_solar_zenith"] = np.full_like(self.sim_data["surface_pressure"], np.nan)
    # self.sim_data['apparent_solar_elevation'] = np.full_like(self.sim_data['surface_pressure'], np.nan)  # pd.DataFrame(np.nan, index=self.time_index, columns=self.locs)

    for loc, row in enumerate(rounded_locs.itertuples()):
        key = (row.lon, row.lat, row.elev)
        if key in solar_position_library:
            _solpos_ = solar_position_library[key]
        else:
            # make sure that no input is nan to avoid very hard-to-understand errors later on
            _req = [
                self.time_index,
                row.lat,
                row.lon,
                row.elev,
                self.sim_data["surface_pressure"][:, loc],
                self.sim_data["surface_air_temperature"][:, loc],
            ]
            assert not any([np.isnan(x).any() if hasattr(x, "__iter__") else np.isnan(x) for x in _req]), (
                f"Arguments for pvlib.solarposition.spa_python() may not be NaN."
            )
            _solpos_ = pvlib.solarposition.spa_python(
                self.time_index,
                latitude=row.lat,
                longitude=row.lon,
                altitude=row.elev,
                pressure=self.sim_data["surface_pressure"][:, loc],
                temperature=self.sim_data["surface_air_temperature"][:, loc],
            )
            solar_position_library[key] = _solpos_

        self.sim_data["solar_azimuth"][:, loc] = _solpos_["azimuth"]
        self.sim_data["apparent_solar_zenith"][:, loc] = _solpos_["apparent_zenith"]
        # self.sim_data['apparent_solar_elevation'][:, loc] = _solpos_["apparent_elevation"]

    assert not np.isnan(self.sim_data["solar_azimuth"]).any()
    assert not np.isnan(self.sim_data["apparent_solar_zenith"]).any()
    # assert not np.isnan(self.sim_data['apparent_solar_elevation']).any()

    return self

diffuse_horizontal_irradiance_from_trigonometry

diffuse_horizontal_irradiance_from_trigonometry()

diffuse_horizontal_irradiance_from_trigonometry(self)

Calculates the diffuse horizontal irradiance from global horizontal irradiance, direct normal irradiance and apparent zenith.

[TODO: Add a simple equation such as the one given in 'direct_normal_irradiance_from_trigonometry']

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'global_horizontal_irradiance', 'direct_normal_irradiance' and 'apparent_solar_zenith'.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def diffuse_horizontal_irradiance_from_trigonometry(self):
    """

    diffuse_horizontal_irradiance_from_trigonometry(self)

    Calculates the diffuse horizontal irradiance from global horizontal irradiance, direct normal irradiance and apparent zenith.

    [TODO: Add a simple equation such as the one given in 'direct_normal_irradiance_from_trigonometry']

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'global_horizontal_irradiance', 'direct_normal_irradiance' and
    'apparent_solar_zenith'.

    """
    assert "global_horizontal_irradiance" in self.sim_data
    assert "direct_normal_irradiance" in self.sim_data
    assert "apparent_solar_zenith" in self.sim_data

    ghi = self.sim_data["global_horizontal_irradiance"]
    dni = self.sim_data["direct_normal_irradiance"]
    elev = np.radians(90 - self.sim_data["apparent_solar_zenith"])

    self.sim_data["diffuse_horizontal_irradiance"] = ghi - dni * np.sin(elev)
    self.sim_data["diffuse_horizontal_irradiance"][self.sim_data["diffuse_horizontal_irradiance"] < 0] = 0

    return self

direct_normal_irradiance_from_trigonometry

direct_normal_irradiance_from_trigonometry()

direct_normal_irradiance_from_trigonometry(self):

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required columns in the placements dataframe to use this functions are 'lon', 'lat' and 'elev'. Required data in the sim_data dictionary are 'direct_horizontal_irradiance' and 'apparent_solar_zenith'.

Calculates the direct normal irradiance from the following equation: .. math:: dir_nor_irr = dir_hor_irr / cos( solar_zenith )

Where:
dir_nor_irr  -> The direct irradiance on the normal plane
dir_hor_irr  -> The direct irradiance on the horizontal plane
solar_zenith -> The solar zenith angle in radians
Source code in reskit/csp/workflows/csp_workflow_manager.py
def direct_normal_irradiance_from_trigonometry(self):
    """

    direct_normal_irradiance_from_trigonometry(self):

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required columns in the placements dataframe to use this functions are 'lon', 'lat' and 'elev'.
    Required data in the sim_data dictionary are 'direct_horizontal_irradiance' and 'apparent_solar_zenith'.

    Calculates the direct normal irradiance from the following equation:
        .. math:: dir_nor_irr = dir_hor_irr / cos( solar_zenith )

        Where:
        dir_nor_irr  -> The direct irradiance on the normal plane
        dir_hor_irr  -> The direct irradiance on the horizontal plane
        solar_zenith -> The solar zenith angle in radians

    """
    # TODO: This can also cover the case when we know GHI & DiffHI
    assert "direct_horizontal_irradiance" in self.sim_data
    assert "solar_zenith_degree" in self.sim_data

    dni_flat = self.sim_data["direct_horizontal_irradiance"]
    zen = np.radians(self.sim_data["solar_zenith_degree"])

    self.sim_data["direct_normal_irradiance"] = dni_flat / np.cos(zen)

    index_out = (dni_flat < 25) & (np.cos(zen) < 0.05)
    self.sim_data["direct_normal_irradiance"][index_out] = 0

    sel = ~np.isfinite(self.sim_data["direct_normal_irradiance"])
    sel = np.logical_or(sel, self.sim_data["direct_normal_irradiance"] < 0)
    sel = np.logical_or(sel, self.sim_data["direct_normal_irradiance"] > 1600)

    self.sim_data["direct_normal_irradiance"][sel] = 0

    return self

estimate_azimuth_from_latitude

estimate_azimuth_from_latitude()

estimate_azimuth_from_latitude(self)

Estimates the azimuth of the placements of the instance. For a positive latitude the azimuth is set to 180. For a negative latitude the azimuth is set to 0.

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
Source code in reskit/solar/workflows/solar_workflow_manager.py
def estimate_azimuth_from_latitude(self):
    """

    estimate_azimuth_from_latitude(self)

    Estimates the azimuth of the placements of the instance.
    For a positive latitude the azimuth is set to 180.
    For a negative latitude the azimuth is set to 0.

    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    """
    self.placements["azimuth"] = 180

    self.placements["azimuth"].values[self.locs.lats < 0] = 0
    return self

estimate_plane_of_array_irradiances

estimate_plane_of_array_irradiances(
    transposition_model="perez", albedo=0.25, **kwargs
)

estimate_plane_of_array_irradiances(self, transposition_model="perez", albedo=0.25, **kwargs)

Estimates the plane of array irradiance using the pvlib.irradiance.get_total_irradiance() function [1].

Parameters:

  • transportion_model

                default "perez"
    
  • albedo

    default 0.25
    Surface albedo [1].
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required data in the sim_data dictionary are 'apparent_solar_zenith', 'solar_azimuth', 'direct_normal_irradiance', 'global_horizontal_irradiance', 'diffuse_horizontal_irradiance', 'extra_terrestrial_irradiance' and 'air_mass'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.get_total_irradiance.html

Source code in reskit/solar/workflows/solar_workflow_manager.py
def estimate_plane_of_array_irradiances(self, transposition_model="perez", albedo=0.25, **kwargs):
    """
    estimate_plane_of_array_irradiances(self, transposition_model="perez", albedo=0.25, **kwargs)

    Estimates the plane of array irradiance using the pvlib.irradiance.get_total_irradiance() function [1].


    Parameters
    ----------
    transportion_model: str, optional
                        default "perez"

    albedo: numeric, optional
            default 0.25
            Surface albedo [1].

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required data in the sim_data dictionary are 'apparent_solar_zenith', 'solar_azimuth', 'direct_normal_irradiance',
    'global_horizontal_irradiance', 'diffuse_horizontal_irradiance', 'extra_terrestrial_irradiance' and 'air_mass'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.irradiance.get_total_irradiance.html

    """
    assert "apparent_solar_zenith" in self.sim_data
    assert "solar_azimuth" in self.sim_data
    assert "direct_normal_irradiance" in self.sim_data
    assert "global_horizontal_irradiance" in self.sim_data
    assert "diffuse_horizontal_irradiance" in self.sim_data
    assert "extra_terrestrial_irradiance" in self.sim_data
    assert "air_mass" in self.sim_data

    azimuth = self.sim_data.get("system_azimuth", self.placements["azimuth"].values)
    tilt = self.sim_data.get("system_tilt", self.placements["tilt"].values)

    poa = pvlib.irradiance.get_total_irradiance(
        surface_tilt=tilt,
        surface_azimuth=azimuth,
        solar_zenith=self.sim_data["apparent_solar_zenith"],
        solar_azimuth=self.sim_data["solar_azimuth"],
        dni=self.sim_data["direct_normal_irradiance"],
        ghi=self.sim_data["global_horizontal_irradiance"],
        dhi=self.sim_data["diffuse_horizontal_irradiance"],
        dni_extra=self.sim_data["extra_terrestrial_irradiance"],
        airmass=self.sim_data["air_mass"],
        albedo=albedo,
        model=transposition_model,
        **kwargs,
    )

    for key in poa.keys():
        # This should set: 'poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', and 'poa_ground_diffuse'

        tmp = poa[key]
        tmp[np.isnan(tmp)] = 0

        self.sim_data[key] = tmp

    self._fix_bad_plane_of_array_values()

    return self

estimate_tilt_from_latitude

estimate_tilt_from_latitude(convention)

estimate_tilt_from_latitude(self, convention)

Estimates the tilt of the solar panels based on the latitude of the placements of the instance.

Parameters:

  • convention

    (str) –
         The calculation method used to suggest system tilts.
         Option 1 of convention is "Ryberg2020".
         Option 2 of convention is a string consumable by 'eval'. This string can use the variable latitude.
         For example "latitude*0.76".
         Option 3 of convention is a path to a rasterfile.
         To get more information check out reskit.solar.location_to_tilt for more information.
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
Source code in reskit/solar/workflows/solar_workflow_manager.py
def estimate_tilt_from_latitude(self, convention):
    """

    estimate_tilt_from_latitude(self, convention)

    Estimates the tilt of the solar panels based on the latitude of the placements of the instance.

    Parameters
    ----------
    convention : str, optional
                 The calculation method used to suggest system tilts.
                 Option 1 of convention is "Ryberg2020".
                 Option 2 of convention is a string consumable by 'eval'. This string can use the variable latitude.
                 For example "latitude*0.76".
                 Option 3 of convention is a path to a rasterfile.
                 To get more information check out reskit.solar.location_to_tilt for more information.



    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    """
    self.placements["tilt"] = rk_solar_core.system_design.location_to_tilt(self.locs, convention=convention)
    return self

extract_raster_values_at_placements

extract_raster_values_at_placements(raster, **kwargs)

Extracts pixel values at each of the configured placements from the specified raster file

Source code in reskit/workflow_manager.py
def extract_raster_values_at_placements(self, raster, **kwargs):
    """Extracts pixel values at each of the configured placements from the specified raster file"""
    return gk.raster.interpolateValues(raster, points=self.locs, **kwargs)

filter_positive_solar_elevation

filter_positive_solar_elevation()

filter_positive_solar_elevation(self)

Filters positive solar elevations so that future operations are only executed for time steps when the sun is above (or at least near-to) the horizon

Parameters:

  • None

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object
Notes

Required data in the sim_data dictionary are 'apparent_solar_zenith'.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def filter_positive_solar_elevation(self):
    """

    filter_positive_solar_elevation(self)

    Filters positive solar elevations so that future operations are only executed for time steps when the sun is above (or at least near-to) the horizon


    Parameters
    ----------
    None

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object

    Notes
    -----
    Required data in the sim_data dictionary are 'apparent_solar_zenith'.



    """
    if self._time_sel_ is not None:
        warnings.warn("Filtering already applied, skipping...")
        return self
    assert "apparent_solar_zenith" in self.sim_data

    self._time_sel_ = (self.sim_data["apparent_solar_zenith"] < 95).any(axis=1)

    for key in self.sim_data.keys():
        self.sim_data[key] = self.sim_data[key][self._time_sel_, :]

    self._time_index_ = self.time_index[self._time_sel_]
    self._set_sim_shape()

    return self

get_scalar_values_from_raster

get_scalar_values_from_raster(
    fp, spatial_interpolation, points=None
)

Auxiliary function to extract raster values with NaN fallback options.

Source code in reskit/workflow_manager.py
def get_scalar_values_from_raster(self, fp, spatial_interpolation, points=None):
    """
    Auxiliary function to extract raster values with NaN fallback options.
    """
    assert isfile(fp), f"File '{fp}' in adjust_variable_to_long_run_average() does not exist."
    # execute with warnings filter since values outside of source data would trigger geokit UserWarning every time
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")

        if points is None:
            points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        else:
            assert isinstance(points, list) and all([isinstance(x, tuple) and len(x) == 2 for x in points]), (
                "points must be a list of (lon, lat) tuples."
            )

        # interpolateValues returns a scalar for a single location; ensure a 1-D array
        # so the nan handling below (and callers) work regardless of the number of points
        _lra = np.atleast_1d(gk.raster.interpolateValues(fp, points, mode=spatial_interpolation))
        # if getting values fails, it could be because of interpolation method.
        # these values will be replaced with the nearest interpolation method
        if np.isnan(_lra).any():
            _lra_near = np.atleast_1d(gk.raster.interpolateValues(fp, self.locs, mode="near"))
            _lra[np.isnan(_lra)] = _lra_near[np.isnan(_lra)]
        # still nans, i.e. the cell itself is nan, but maybe its neighbors are not
        # try the (nan)median of the surrounding cells
        if np.isnan(_lra).any():

            def _nanmedian(vals, xOff, yOff):
                """Aux function to mimic the 3 expected inputs in interpolateValues()"""
                return np.nanmedian(vals)

            points = [(loc.lon, loc.lat) for loc in self.locs._locations]
            _lra_near = np.atleast_1d(gk.raster.interpolateValues(fp, points, mode="func", func=_nanmedian))
            _lra[np.isnan(_lra)] = _lra_near[np.isnan(_lra)]
    return _lra

loadPTRdata

loadPTRdata(datasetname: str)

Loads the dataset with the name datasetname.

Parameters:

  • datasetname

    (str) –

    [description]

Source code in reskit/csp/workflows/csp_workflow_manager.py
def loadPTRdata(self, datasetname: str):
    """Loads the dataset with the name datasetname.

    Parameters
    ----------
    datasetname : str
        [description]
    """
    self.ptr_data = load_dataset(datasetname=datasetname)
    # make list from coefficients from regression
    self.ptr_data["b"] = np.array(
        [
            self.ptr_data["b0"],
            self.ptr_data["b1"],
            self.ptr_data["b2"],
            self.ptr_data["b3"],
            self.ptr_data["b4"],
        ]
    )
    self.placements["datasetname"] = datasetname
    return self.ptr_data

optimize_heat_output_4D

optimize_heat_output_4D()

Calculates the heat usage for different type of plant configuration: SM and TES Calculates the following variables: Annual heat: Total heat, that can be used annually for the plant configuration Direct heat Usage: Heat, that must be directly processed through the plant (storage size limitations) Stored heat: Heat, that is stored and can be used daily at any given time

Returns:

  • [type]

    [description]

Source code in reskit/csp/workflows/csp_workflow_manager.py
def optimize_heat_output_4D(self):
    """Calculates the heat usage for different type of plant configuration: SM and TES
        Calculates the following variables:
            Annual heat: Total heat, that can be used annually for the plant configuration
            Direct heat Usage: Heat, that must be directly processed through the plant (storage size limitations)
            Stored heat: Heat, that is stored and can be used daily at any given time


    Returns
    -------
    [type]
        [description]
    """
    assert "Totex_SF_EUR_per_a" in self.placements.columns
    assert "HeattoPlant_W" in self.sim_data.keys()

    # estimate parameters
    # nominal_sf_efficiency = np.max(self.ptr_data['eta_ptr_max'] \
    #                                 * self.ptr_data['eta_cleaness'] \
    #                                 * np.cos(np.deg2rad(self.sim_data['theta'])) \
    #                                 * self.sim_data['IAM'] \
    #                                 * self.sim_data['eta_shdw'])
    # #nominal_efficiency_power_block = 0.3774 # 37.74% efficiency of the power block at nominal power, from gafurov2013
    # nominal_receiver_heat_losses = 0.06 # 6% losses nominal heat losses, from gafurov2013

    Q_sf_des = self.placements["capacity_sf_W_th"]

    # , 5.5, 6, 6.5, 7]) #np.array([2.1])
    self.sm = np.array([1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5])
    # ([0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])#, 16, 17, 18, 19 ,20]) #np.array([7.5])
    self.tes = np.array([0, 3, 6, 9, 12, 15, 18])

    if not hasattr(self, "opt_data"):
        self.opt_data = {}  # container for opt variables
    self.opt_data["dimensions"] = [
        self.sim_data["HeattoPlant_W"].shape[0],  # time
        self.sim_data["HeattoPlant_W"].shape[1],  # placements
        len(self.sm),  # SM variation
        len(self.tes),  # TES variation
    ]

    # create raw Heat Plant
    # dimensions: [time(hours), placements, SM]
    HeatfromField_W_3D = np.tensordot(
        self.sim_data["HeattoPlant_W"],
        np.ones(shape=(self.opt_data["dimensions"][2])),
        axes=0,
    )

    # calculate max possible heat usage

    # calculate max possible powerplant consumption
    # Consumption = Q_sf_des * 1 / SM
    # dimensions: [time(hours), placements, SM]
    Powerplant_consumption_max_W_3D = np.tensordot(
        np.tensordot(np.ones(self.opt_data["dimensions"][0]), Q_sf_des, axes=0),
        1 / self.sm,
        axes=0,
    )

    # heat for direct usage in power plant
    # sum over heat for direct usage (min from available from field and max produceable from plant)
    # dimensions: [time(hours), placements, SM]
    directHeatUsage_Wh_3D_ts = np.minimum(HeatfromField_W_3D, Powerplant_consumption_max_W_3D)
    Powerplant_consumption_max_W_2D = Powerplant_consumption_max_W_3D[0, :, :]
    del Powerplant_consumption_max_W_3D

    # dimensions: [placements, SM]
    directHeatUsage_Wh_2D = directHeatUsage_Wh_3D_ts.sum(axis=0)

    # dimensions: [ placements, SM, TES]
    directHeatUsage_Wh_3D = np.tensordot(directHeatUsage_Wh_2D, np.ones(self.opt_data["dimensions"][3]), axes=0)
    del directHeatUsage_Wh_2D

    # calculate the heat that must be stored (because plant smaller than field)
    # dimensions: [time(hours), placements, SM]
    # np.maximum(HeatfromField_W_3D - Powerplant_consumption_max_W_3D, 0)
    HeattoStorage_W_3D = np.maximum(HeatfromField_W_3D - directHeatUsage_Wh_3D_ts, 0)
    del HeatfromField_W_3D

    # dimensions: [days, hoursperyear]
    aggregate_by_day = np.eye(365).repeat(24, axis=1)
    self.aggregate_by_day = aggregate_by_day

    # aggregate the stored heat for each day
    # dimensions: [time(days), placements, SM]
    dailyHeatStorable_Wh_3D = np.einsum("ij,jkl", aggregate_by_day, HeattoStorage_W_3D)
    del HeattoStorage_W_3D

    # convert to 4D
    # dimensions: [time(days), placements, SM, TES]
    dailyHeatStorable_Wh_4D = np.tensordot(
        dailyHeatStorable_Wh_3D,
        np.ones(self.opt_data["dimensions"][3]),
        axes=0,
    )
    del dailyHeatStorable_Wh_3D

    # limit storage by thermal storage size
    # dimensions: [time(days), placements, SM, TES]
    storage_Size_Wh_4D = np.tensordot(
        np.tensordot(
            np.tensordot(
                np.ones(aggregate_by_day.shape[0]),
                Q_sf_des,
                axes=0,  # days
            ),
            1 / self.sm,  # np.ones(self.opt_data['dimensions'][2]),
            axes=0,
        ),
        self.tes,
        axes=0,
    )

    # actual stored heat daily
    # dimensions: [time(days), placements, SM, TES]
    dailyHeatStored_Wh_4D = np.minimum(dailyHeatStorable_Wh_4D, storage_Size_Wh_4D)
    del dailyHeatStorable_Wh_4D, storage_Size_Wh_4D

    # daily used direct heat:
    # dimensions: [time(days), placements, SM]
    dailyHeatDirect_Wh_3D = np.einsum("ij,jkl", aggregate_by_day, directHeatUsage_Wh_3D_ts)
    self.opt_data["directHeatUsage_Wh_3D_ts"] = directHeatUsage_Wh_3D_ts
    del directHeatUsage_Wh_3D_ts

    # dimensions: [time(days), placements, SM, TES]
    dailyHeatDirect_Wh_4D = np.tensordot(
        dailyHeatDirect_Wh_3D,
        np.ones(self.opt_data["dimensions"][3]),
        axes=0,
    )
    del dailyHeatDirect_Wh_3D

    # max Heat input to PowerPlant
    # dimensions: [placements, SM, TES]
    maxDailyHeatPlant_W_3D = (
        np.tensordot(
            # Powerplant_consumption_max_W_3D[0,:,:],
            Powerplant_consumption_max_W_2D,
            np.ones(self.opt_data["dimensions"][3]),
            axes=0,
        )
        * 24
    )  # h/day
    # dimensions: [time(days), placements, SM, TES]
    maxDailyHeatPlant_W_4D = np.tensordot(np.ones(aggregate_by_day.shape[0]), maxDailyHeatPlant_W_3D, axes=0)
    del maxDailyHeatPlant_W_3D

    # limit daily output
    # dimensions: [time(days), placements, SM, TES]
    dailyHeatOutput_Wh_4D = np.minimum(
        (dailyHeatDirect_Wh_4D + dailyHeatStored_Wh_4D * self.ptr_data["storage_efficiency_1"]),
        maxDailyHeatPlant_W_4D,
    )
    del maxDailyHeatPlant_W_4D, dailyHeatDirect_Wh_4D

    # dimensions: [placements, SM, TES]
    annualHeat_Wh_3D = dailyHeatOutput_Wh_4D.sum(axis=0)

    # dimensions: [placements, SM, TES]
    self.opt_data["annualHeat_Wh_3D"] = annualHeat_Wh_3D
    # dimensions: [time(days), placements, SM, TES]
    self.opt_data["dailyHeatOutput_Wh_4D"] = dailyHeatOutput_Wh_4D

    # #stored heat per year
    # #dimensions: [placements, SM, TES]
    # annualHeatStored_Wh_3D = dailyHeatStored_Wh_4D.sum(axis=0)

    # #total heat storable per year
    # #dimensions: [placements, SM, TES]
    # annualHeatstoreable_Wh_3D = annualHeatStored_Wh_3D + directHeatUsage_Wh_3D

    # #max Heat input to PowerPlant
    # #dimensions: [placements, SM, TES]
    # maxAnnualHeatPlant_W_3D = np.tensordot(
    #     Powerplant_consumption_max_W_1D, #Powerplant_consumption_max_W_3D[0,:,:],
    #     np.ones(dimensions[3]),
    #     axes=0,
    # ) * aggregate_by_day.shape[1] # hours per  year

    # #total heat usable per year
    # #dimensions: [placements, SM, TES]
    # annualHeat_Wh_3D = np.minimum(annualHeatstoreable_Wh_3D, maxAnnualHeatPlant_W_3D)
    return self

optimize_plant_size

optimize_plant_size(
    onlynightuse=True, fullvariation=False, debug_vars=False
)

Returns the optimal pLant configuration for each placement by finding the lowest expected LCOE: sm_opt, tes opt

Parameters:

  • onlynightuse

    (bool, default: True ) –

    allheat has to be stored, i order to be used contrary to PV, by default True

  • fullvariation

    (bool, default: False ) –

    for plotting purpose, full variation can be set to true to calculate variation over more values, by default False

Source code in reskit/csp/workflows/csp_workflow_manager.py
def optimize_plant_size(self, onlynightuse=True, fullvariation=False, debug_vars=False):
    """Returns the optimal pLant configuration for each placement by finding the lowest expected LCOE: sm_opt, tes opt

    Parameters
    ----------
    onlynightuse : bool, optional
        allheat has to be stored, i order to be used contrary to PV, by default True
    fullvariation : bool, optional
        for plotting purpose, full variation can be set to true to calculate variation over more values, by default False
    """
    # check inputs
    assert not np.isnan(self.placements["capacity_sf_W_th"]).any()
    assert not np.isnan(self.sim_data["HeattoPlant_W"]).any()

    # for developers: use full variations
    if fullvariation:
        self.sm = np.array([1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5])
        self.tes = np.array([5, 6, 9, 12, 15, 18])
    else:
        if onlynightuse:
            self.sm = np.array([1, 1.5, 2, 2.5, 3, 3.5])
            self.tes = np.array([6, 9, 12, 15])
        else:
            self.sm = np.array([2.5, 3, 3.5, 4, 4.5])
            self.tes = np.array([9, 12, 15])
    # make list with all sizing combinations
    sizing_tuples = []
    for sm in self.sm:
        for tes in self.tes:
            sizing_tuples.append((sm, tes))

    assert len(sizing_tuples) == len(self.sm) * len(self.tes)

    # loop all sizing combinations
    # dimensions: [time(days), placements, SM, TES]
    if debug_vars:
        dailyHeatOutput_Wh_4D = np.nan * np.ones(
            shape=(
                len(np.unique(self.time_index.date)),
                len(self.placements),
                len(self.sm),
                len(self.tes),
            )
        )  # TODO: remove
        TOTEX_EUR_per_a_3D = np.nan * np.ones(
            shape=(len(self.placements), len(self.sm), len(self.tes))
        )  # TODO: remove
        Power_output_plant_net_Wh_per_a_3D = np.nan * np.ones(
            shape=(len(self.placements), len(self.sm), len(self.tes))
        )  # TODO: remove
    LCOE_EURct_per_kWh_el_3D = np.nan * np.ones(shape=(len(self.placements), len(self.sm), len(self.tes)))
    for size in sizing_tuples:
        sm = size[0]
        tes = size[1]

        # get index
        i_sm = np.where(self.sm == sm)[0][0]
        i_tes = np.where(self.tes == tes)[0][0]

        ##################################
        ### 1) get opt thermal output  ###
        ##################################

        # get thermal size of the power plant
        Heatflux_powerplant_des_input_W_th = self.placements["capacity_sf_W_th"] / sm
        Heat_Storage_des_Wh_th = Heatflux_powerplant_des_input_W_th * tes

        # get direct power to plant
        if onlynightuse:
            Heatflux_direct_powerplant_W_th = 0
        else:
            Heatflux_direct_powerplant_W_th = np.minimum(
                self.sim_data["HeattoPlant_W"], Heatflux_powerplant_des_input_W_th
            )

        # get heat to be stored
        Heatflux_to_storage_W_th = np.maximum(self.sim_data["HeattoPlant_W"] - Heatflux_direct_powerplant_W_th, 0)

        # aggregate to daily
        aggregate_by_day = np.eye(len(np.unique(self.time_index.date))).repeat(24, axis=1)
        self.aggregate_by_day = aggregate_by_day

        # aggregate the stored heat for each day
        Heat_to_storage_daily_Wh_th = np.einsum("ij,jk", aggregate_by_day, Heatflux_to_storage_W_th)
        Heat_heating_sf_daily_Wh_th = np.einsum("ij,jk", aggregate_by_day, self.sim_data["P_heating_W"])  # 13

        if onlynightuse:
            Heat_direct_powerplant_daily_Wh_th = 0
        else:
            Heat_direct_powerplant_daily_Wh_th = np.einsum(
                "ij,jk", aggregate_by_day, Heatflux_direct_powerplant_W_th
            )
        del Heatflux_direct_powerplant_W_th, Heatflux_to_storage_W_th

        # limit daily heat output from storage by storage size
        Heat_stored_daily_Wh_th = np.minimum(Heat_to_storage_daily_Wh_th, Heat_Storage_des_Wh_th)
        del Heat_to_storage_daily_Wh_th

        # calculate heat which is unstored
        Heat_unstored_daily_Wh_th = np.maximum(
            Heat_stored_daily_Wh_th * self.ptr_data["storage_efficiency_1"] - Heat_heating_sf_daily_Wh_th,
            0,
        )  # 13
        P_backup_heating_daily_Wh_el = np.maximum(
            Heat_heating_sf_daily_Wh_th - Heat_stored_daily_Wh_th * self.ptr_data["storage_efficiency_1"],
            0,
        )  # 13
        del Heat_stored_daily_Wh_th

        # max heat processable by power plant
        if onlynightuse:
            operationalhours_per_day = np.einsum(
                "ij,jk",
                aggregate_by_day,
                (self.sim_data["solar_zenith_degree"] > 90),
            )
        else:
            operationalhours_per_day = 24
        Heat_max_des_powerplant_daily_Wh_th = (
            Heatflux_powerplant_des_input_W_th.values * operationalhours_per_day
        )  # h/day

        # calculate actually used heat
        Heat_total_used_daily_Wh_th = np.minimum(
            (Heat_direct_powerplant_daily_Wh_th + Heat_unstored_daily_Wh_th),
            Heat_max_des_powerplant_daily_Wh_th,
        )
        del (
            Heat_direct_powerplant_daily_Wh_th,
            Heat_unstored_daily_Wh_th,
            Heat_max_des_powerplant_daily_Wh_th,
        )

        # remember that one
        if debug_vars:
            dailyHeatOutput_Wh_4D[:, :, i_sm, i_tes] = Heat_total_used_daily_Wh_th

        ##################################
        ### 2) Electric Output         ###
        ##################################

        # calculate average rel load of the plant
        rel_load_plant_daily_1 = Heat_total_used_daily_Wh_th / (
            Heatflux_powerplant_des_input_W_th.values * operationalhours_per_day
        )

        # calculate plant efficiency
        efficiency_daily_1 = self._get_plant_efficiency(
            rel_load_plant=rel_load_plant_daily_1,
            eta_nom=self.ptr_data["eta_powerplant_1"],
        )
        del rel_load_plant_daily_1

        # gross power output
        Power_output_plant_gross_daily_Wh = Heat_total_used_daily_Wh_th * efficiency_daily_1
        del efficiency_daily_1
        # plant parasitics
        self.sim_data_daily["Parasitics_plant_daily_Wh_el"] = Parasitics_plant_daily_Wh_el = (
            np.einsum("ij,jk", aggregate_by_day, self.sim_data["Parasitics_W_el"]) * 1
        )  # h #13
        # Parasitics_plant_daily_Wh_el += P_backup_heating_daily_Wh_el #13
        # net power output
        Power_output_plant_net_daily_Wh = np.maximum(
            Power_output_plant_gross_daily_Wh - Parasitics_plant_daily_Wh_el, 0
        )
        del Parasitics_plant_daily_Wh_el, Power_output_plant_gross_daily_Wh

        # sum up
        Power_output_plant_net_Wh_per_a = Power_output_plant_net_daily_Wh.sum(axis=0).squeeze()

        if debug_vars:
            Power_output_plant_net_Wh_per_a_3D[:, i_sm, i_tes] = Power_output_plant_net_Wh_per_a

        ##################################
        ### 3) get cost                ###
        ##################################

        TOTEX_EUR_per_a = self._get_totex_from_self(
            sm_manipulation=sm,
            tes_manipulation=tes,
            P_aux_manipulation=P_backup_heating_daily_Wh_el.sum(axis=0),
        )

        if debug_vars:
            TOTEX_EUR_per_a_3D[:, i_sm, i_tes] = TOTEX_EUR_per_a

        ##################################
        ### 4) LCOE                    ###
        ##################################

        # with np.seterr(divide='ignore', invalid='ignore') #TODO: zero division
        LCOE_EUR_per_kWh_el = np.nan_to_num(
            TOTEX_EUR_per_a.values / Power_output_plant_net_Wh_per_a * 1e5, nan=0
        )  # EUR/Wh --> EURct/kWh

        # that's the output of the loop
        LCOE_EURct_per_kWh_el_3D[:, i_sm, i_tes] = LCOE_EUR_per_kWh_el

    # check if all is written
    assert not np.isnan(LCOE_EURct_per_kWh_el_3D).any()

    # container for opt variables
    if not hasattr(self, "opt_data"):
        self.opt_data = {}
    if debug_vars:
        self.opt_data["dailyHeatOutput_Wh_4D_new"] = dailyHeatOutput_Wh_4D
        self.opt_data["TOTEX_EUR_per_a_3D_new"] = TOTEX_EUR_per_a_3D
        self.opt_data["LCOE_EURct_per_kWh_el_3D_new"] = LCOE_EURct_per_kWh_el_3D

    ##################################
    ### 5) opt                     ###
    ##################################

    # find minimum:
    # dimensions: [placements]
    sm_opt = []
    tes_opt = []
    # loop placemnts(I did not find a function which gives the argmin along two axes (1,2))
    for i in range(0, len(self.placements)):
        temp = LCOE_EURct_per_kWh_el_3D[i, :, :]
        # find minimum index
        sm_opt_index, tes_opt_index = np.unravel_index(np.argmin(temp, axis=None), temp.shape)
        # append
        sm_opt.append(self.sm[sm_opt_index])
        tes_opt.append(self.tes[tes_opt_index])

    # set minimum values to placement df
    self.placements["sm_opt"] = sm_opt
    self.placements["tes_opt"] = tes_opt
    self.placements["storage_capacity_kWh_th"] = self.placements["capacity_sf_W_th"] / sm_opt * tes_opt / 1000
    self.placements["power_plant_capacity_W_el"] = (
        self.placements["capacity_sf_W_th"] / sm_opt * self.ptr_data["eta_powerplant_1"]
    )

permit_single_axis_tracking

permit_single_axis_tracking(
    max_angle=90, backtrack=True, gcr=2.0 / 7.0
)

permit_single_axis_tracking(self, max_angle=90, backtrack=True, gcr=2.0 / 7.0)

Permits single axis tracking in the simulation using the pvlib.tracking.singleaxis() function [1].

Parameters:

  • max_angle

       default 90
       A value denoting the maximum rotation angle, in decimal degrees, of the one-axis tracker from its horizontal position
       (horizontal if axis_tilt = 0). A max_angle of 90 degrees allows the tracker to rotate to a vertical position to point the
       panel towards a horizon. max_angle of 180 degrees allows for full rotation [1].
    
  • backtrack

       default True
       Controls whether the tracker has the capability to “backtrack” to avoid row-to-row shading.
       False denotes no backtrack capability. True denotes backtrack capability [1].
    
  • gcr

       default 2.0/7.0
       A value denoting the ground coverage ratio of a tracker system which utilizes backtracking; i.e. the ratio between the
       PV array surface area to total ground area. A tracker system with modules 2 meters wide, centered on the tracking axis,
       with 6 meters between the tracking axes has a gcr of 2/6=0.333. If gcr is not provided, a gcr of 2/7 is default. gcr must be <=1 [1].
    

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required columns in the placements dataframe to use this functions are 'lon', 'lat', 'elev', 'tilt' and 'azimuth'. Required data in the sim_data dictionary are 'apparent_solar_zenith' and 'solar_azimuth'.

References

[1] https://wholmgren-pvlib-python-new.readthedocs.io/en/doc-reorg2/generated/tracking/pvlib.tracking.singleaxis.html

[2] Lorenzo, E et al., 2011, “Tracking and back-tracking”, Prog. in Photovoltaics: Research and Applications, v. 19, pp. 747-753.

Source code in reskit/solar/workflows/solar_workflow_manager.py
def permit_single_axis_tracking(self, max_angle=90, backtrack=True, gcr=2.0 / 7.0):
    """

    permit_single_axis_tracking(self, max_angle=90, backtrack=True, gcr=2.0 / 7.0)

    Permits single axis tracking in the simulation using the pvlib.tracking.singleaxis() function [1].


    Parameters
    ----------
    max_angle: float, optional
               default 90
               A value denoting the maximum rotation angle, in decimal degrees, of the one-axis tracker from its horizontal position
               (horizontal if axis_tilt = 0). A max_angle of 90 degrees allows the tracker to rotate to a vertical position to point the
               panel towards a horizon. max_angle of 180 degrees allows for full rotation [1].

    backtrack: bool, optional
               default True
               Controls whether the tracker has the capability to “backtrack” to avoid row-to-row shading.
               False denotes no backtrack capability. True denotes backtrack capability [1].

    gcr:       float, optional
               default 2.0/7.0
               A value denoting the ground coverage ratio of a tracker system which utilizes backtracking; i.e. the ratio between the
               PV array surface area to total ground area. A tracker system with modules 2 meters wide, centered on the tracking axis,
               with 6 meters between the tracking axes has a gcr of 2/6=0.333. If gcr is not provided, a gcr of 2/7 is default. gcr must be <=1 [1].


    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required columns in the placements dataframe to use this functions are 'lon', 'lat', 'elev', 'tilt' and 'azimuth'.
    Required data in the sim_data dictionary are 'apparent_solar_zenith' and 'solar_azimuth'.

    References
    ----------
    [1] https://wholmgren-pvlib-python-new.readthedocs.io/en/doc-reorg2/generated/tracking/pvlib.tracking.singleaxis.html

    [2]	Lorenzo, E et al., 2011, “Tracking and back-tracking”, Prog. in Photovoltaics: Research and Applications, v. 19, pp. 747-753.

    """
    """See pvlib.tracking.singleaxis for parameter info"""
    assert "apparent_solar_zenith" in self.sim_data
    assert "solar_azimuth" in self.sim_data
    assert "tilt" in self.placements.columns
    assert "azimuth" in self.placements.columns

    self.register_workflow_parameter("tracking_mode", "single_axis")
    self.register_workflow_parameter("tracking_max_angle", max_angle)
    self.register_workflow_parameter("tracking_backtrack", backtrack)
    self.register_workflow_parameter("tracking_gcr", gcr)

    system_tilt = np.empty(self._sim_shape_)
    system_azimuth = np.empty(self._sim_shape_)

    with warnings.catch_warnings():
        warnings.simplefilter("ignore")

        for i in range(self.locs.count):
            placement = self.placements.iloc[i]

            tmp = pvlib.tracking.singleaxis(
                apparent_zenith=pd.Series(
                    self.sim_data["apparent_solar_zenith"][:, i],
                    index=self._time_index_,
                ),
                apparent_azimuth=pd.Series(self.sim_data["solar_azimuth"][:, i], index=self._time_index_),
                # self.placements['tilt'].values,
                axis_tilt=placement.tilt,
                # self.placements['azimuth'].values,
                axis_azimuth=placement.azimuth,
                max_angle=max_angle,
                backtrack=backtrack,
                gcr=gcr,
            )

            system_tilt[:, i] = tmp["surface_tilt"].values
            system_azimuth[:, i] = tmp["surface_azimuth"].values

            # fix nan values. Why are they there???
            s = np.isnan(system_tilt[:, i])
            system_tilt[s, i] = placement.tilt

            s = np.isnan(system_azimuth[:, i])
            system_azimuth[s, i] = placement.azimuth

    self.sim_data["system_tilt"] = system_tilt
    self.sim_data["system_azimuth"] = system_azimuth

    return self

read

read(
    variables: Union[str, List[str]],
    source_type: str,
    source: str,
    set_time_index: bool = False,
    spatial_interpolation_mode: str = "bilinear",
    temporal_reindex_method: str = "nearest",
    time_index_from=None,
    **kwargs,
)

Reads the specified variables from the NetCDF4-style weather dataset, and then extracts those variables for each of the coordinates configured in .placements. The resulting data is then available in .sim_data.

Parameters:

  • variables

    (str or list of strings) –

    The variables (or variables) to be read from the specified source - If a path to a weather source is given, then only the 'standard' variables configured for that source type are available (see the doc string for the weather source you are interested in) - If either 'elevated_wind_speed' or 'surface_wind_speed' is included in the variable list, then the members .elevated_wind_speed_height and .surface_wind_speed_height, respectfully, are also added. These are constants which specify what the 'native' wind speed height is, which depends on the source - A pre-loaded NCSource can also be given, thus allowing for any variable in the source to be specified in the variables list. But the user needs to take care of initializing the NCSource and loading the data they want

  • source_type

    (str) –

    The type of weather datasource which is to be loaded. Can be one of: "ERA5", "SARAH", "MERRA", or 'user' - If a pre-loaded NCSource is given for the source object, then the source_type should be "user"

  • source

    (str or NCSource) –

    The source to read weather variables from

  • set_time_index

    (bool, default: False ) –

    If True, instructs the workflow manager to set the time index to that which is read from the weather source - By default False

  • spatial_interpolation_mode

    (str, default: 'bilinear' ) –

    The spatial interpolation mode to use while reading data from the weather source at each of the placement coordinates - By default "bilinear"

  • temporal_reindex_method

    (str, default: 'nearest' ) –

    In the event of missing data, this algorithm is used to fill in the missing data. - Can be, for example, "nearest", "ffill", "bfill", "interpolate" - By default "nearest"

Returns:

Raises:

  • RuntimeError

    If set_time_index is False but no .time_index exists

  • RuntimeError

    If source_type is unknown

Source code in reskit/workflow_manager.py
def read(
    self,
    variables: Union[str, List[str]],
    source_type: str,
    source: str,
    set_time_index: bool = False,
    spatial_interpolation_mode: str = "bilinear",
    temporal_reindex_method: str = "nearest",
    time_index_from=None,
    **kwargs,
):
    """Reads the specified variables from the NetCDF4-style weather dataset, and then extracts
    those variables for each of the coordinates configured in `.placements`. The resulting
    data is then available in `.sim_data`.

    Parameters
    ----------
    variables : str or list of strings
        The variables (or variables) to be read from the specified source
        - If a path to a weather source is given, then only the 'standard' variables
        configured for that source type are available (see the doc string for the
        weather source you are interested in)
        - If either 'elevated_wind_speed' or 'surface_wind_speed' is included in the
        variable list, then the members `.elevated_wind_speed_height` and
        `.surface_wind_speed_height`, respectfully, are also added. These are constants
        which specify what the 'native' wind speed height is, which depends on the source
        - A pre-loaded NCSource can also be given, thus allowing for any variable in the
        source to be specified in the `variables` list. But the user needs to take care
        of initializing the NCSource and loading the data they want

    source_type : str
        The type of weather datasource which is to be loaded. Can be one of:
        "ERA5", "SARAH", "MERRA", or 'user'
        - If a pre-loaded NCSource is given for the `source` object, then the `source_type`
        should be "user"

    source : str or rk.weather.NCSource
        The source to read weather variables from

    set_time_index : bool, optional
        If True, instructs the workflow manager to set the time index to that which is read
        from the weather source
        - By default False

    spatial_interpolation_mode : str, optional
        The spatial interpolation mode to use while reading data from the weather source at
        each of the placement coordinates
        - By default "bilinear"

    temporal_reindex_method : str, optional
        In the event of missing data, this algorithm is used to fill in the missing data.
        - Can be, for example, "nearest", "ffill", "bfill", "interpolate"
        - By default "nearest"

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)

    Raises
    ------
    RuntimeError
        If set_time_index is False but no `.time_index` exists
    RuntimeError
        If source_type is unknown
    """
    if not set_time_index and self.time_index is None:
        raise RuntimeError("Time index is not available")

    if not isinstance(variables, list):
        variables = [
            variables,
        ]

    if isinstance(source, str) and source_type != "user":
        storage_format = kwargs.pop("storage_format", None)
        is_zarr = storage_format == "zarr" or source.endswith(".zarr") or source.startswith("gs://")
        if source_type == "ERA5":
            source_constructor = rk_weather.Era5ZarrSource if is_zarr else rk_weather.Era5Source
        elif source_type == "SARAH":
            source_constructor = rk_weather.SarahSource
        elif source_type == "MERRA":
            source_constructor = rk_weather.MerraSource
        elif source_type == "ICON-LAM":
            source_constructor = rk_weather.IconlamSource
        else:
            raise RuntimeError("Unknown source_type")

        if source_type == "ERA5":
            time_slice = kwargs.pop("time_slice", None)
            era5_kwargs = dict(kwargs)
            if time_slice is not None:
                if not is_zarr:
                    raise RuntimeError(
                        "'time_slice' is only supported for Zarr-backed ERA5 sources; support for "
                        "netCDF4-backed ERA5 sources is planned. Until then, restrict the time span "
                        "of netCDF4 ERA5 data by selecting the corresponding files instead."
                    )
                era5_kwargs["time_slice"] = time_slice
            source = source_constructor(source, bounds=self.ext, time_index_from=time_index_from, **era5_kwargs)
        else:
            source = source_constructor(source, bounds=self.ext, **kwargs)

        # Load the requested variables
        source.sload(*variables)

    else:  # Assume source is already an initialized NCSource-like object
        missing_variables = [var for var in variables if var not in source.data]
        if missing_variables:
            if hasattr(source, "sload"):
                source.sload(*missing_variables)
            else:
                raise AssertionError(
                    "The given source has no '.sload()' method and is missing the variable(s): "
                    + ", ".join(missing_variables)
                )

    if set_time_index:
        self.set_time_index(source.time_index)

    # read variables
    for var in variables:
        self.sim_data[var] = source.get(
            var,
            self.locs,  # Manipulate locs here
            interpolation=spatial_interpolation_mode,
            force_as_data_frame=True,
        )

        if not set_time_index:
            self.sim_data[var] = self.sim_data[var].reindex(self.time_index, method=temporal_reindex_method)

        self.sim_data[var] = self.sim_data[var].values

        # Special check for wind speed height
        if var == "elevated_wind_speed":
            self.elevated_wind_speed_height = source.ELEVATED_WIND_SPEED_HEIGHT

        if var == "surface_wind_speed":
            self.surface_wind_speed_height = source.SURFACE_WIND_SPEED_HEIGHT

    return self

register_workflow_parameter

register_workflow_parameter(
    key: str, value: Union[str, float]
)

Add a parameter to the WorkflowManager which will be included in the output XArray dataset

Parameters:

  • key

    (str) –

    The workflow parameter's access key

  • value

    (Union[str, float]) –

    The workflow parameter's value. Only strings and floats are allowed

Source code in reskit/workflow_manager.py
def register_workflow_parameter(self, key: str, value: Union[str, float]):
    """Add a parameter to the WorkflowManager which will be included in the output XArray dataset

    Parameters
    ----------
    key : str
        The workflow parameter's access key

    value : Union[str,float]
        The workflow parameter's value. Only strings and floats are allowed
    """
    self.workflow_parameters[key] = value

set_time_index

set_time_index(times: DatetimeIndex)

Sets the time index of the WorkflowManager

Source code in reskit/workflow_manager.py
def set_time_index(self, times: pd.DatetimeIndex):
    """Sets the time index of the WorkflowManager

    Parameters
    ----------
        times : pd.DatetimeIndex
            The timesteps to use throughout the WorkflowManager's life cycle. The
            length of this dataset must match the shape of data which is loaded into
            the WorkflorManager.sim_data member.
    """
    self.time_index = times

    self._time_sel_ = None
    self._time_index_ = self.time_index.copy()
    self._set_sim_shape()

simulate_with_interpolated_single_diode_approximation

simulate_with_interpolated_single_diode_approximation(
    module="WINAICO WSx-240P6", tech_year=2050
)

simulate_with_interpolated_single_diode_approximation(self, module="WINAICO WSx-240P6")

Does the simulation with an interpolated single diode approximation using the pvlib.pvsystem.calcparams_desoto() [1] function and the pvlib.pvsystem.singlediode() [2] function.

Parameters:

  • module

    Must be one of: * A module found in the pvlib.pvsystem.retrieve_sam("CECMod") database * "WINAICO WSx-240P6" -> Good for open-field applications * "LG Electronics LG370Q1C-A5" -> Good for rooftop applications

  • tech_year

    (int, default: 2050 ) –

    If given in combination with the projected module str names "WINAICO WSx-240P6" or "LG Electronics LG370Q1C-A5", the effifiency will be scaled linearly to the given year. Must then be between year of market comparison in analysis (2019) and 2050. Will be ignored when non-projected existing module names or specific parameters are given, can then be None. By default 2050.

Returns:

  • Returns a reference to the invoking SolarWorkflowManager object.
Notes

Required columns in the placements dataframe to use this functions are 'lon', 'lat', 'elev', 'tilt' and 'azimuth'. Required data in the sim_data dictionary are 'poa_global' and 'cell_temperature'.

References

[1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.calcparams_desoto.html

[2] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.singlediode.html

[3] (1, 2) W. De Soto et al., “Improvement and validation of a model for photovoltaic array performance”, Solar Energy, vol 80, pp. 78-88, 2006.

[4] System Advisor Model web page. https://sam.nrel.gov.

[5] A. Dobos, “An Improved Coefficient Calculator for the California Energy Commission 6 Parameter Photovoltaic Module Model”, Journal of Solar Energy Engineering, vol 134, 2012.

[6] O. Madelung, “Semiconductors: Data Handbook, 3rd ed.” ISBN 3-540-40488-0

[7] S.R. Wenham, M.A. Green, M.E. Watt, “Applied Photovoltaics” ISBN 0 86758 909 4

[8] A. Jain, A. Kapoor, “Exact analytical solutions of the parameters of real solar cells using Lambert W-function”, Solar Energy Materials and Solar Cells, 81 (2004) 269-277.

[9] D. King et al, “Sandia Photovoltaic Array Performance Model”, SAND2004-3535, Sandia National Laboratories, Albuquerque, NM

[10] “Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits” JW Bishop, Solar Cell (1988) https://doi.org/10.1016/0379-6787(88)90059-2

Source code in reskit/solar/workflows/solar_workflow_manager.py
def simulate_with_interpolated_single_diode_approximation(
    self,
    module="WINAICO WSx-240P6",
    tech_year=2050,
):
    """
    simulate_with_interpolated_single_diode_approximation(self, module="WINAICO WSx-240P6")

    Does the simulation with an interpolated single diode approximation using the pvlib.pvsystem.calcparams_desoto() [1] function and the
    pvlib.pvsystem.singlediode() [2] function.


    Parameters
    ----------
    module: str
        Must be one of:
            * A module found in the pvlib.pvsystem.retrieve_sam("CECMod") database
            * "WINAICO WSx-240P6" -> Good for open-field applications
            * "LG Electronics LG370Q1C-A5" -> Good for rooftop applications
    tech_year : int, optional
        If given in combination with the projected module str names "WINAICO WSx-240P6" or
        "LG Electronics LG370Q1C-A5", the effifiency will be scaled linearly to the given
        year. Must then be between year of market comparison in analysis (2019) and 2050.
        Will be ignored when non-projected existing module names or specific parameters
        are given, can then be None. By default 2050.

    Returns
    -------
    Returns a reference to the invoking SolarWorkflowManager object.

    Notes
    -----
    Required columns in the placements dataframe to use this functions are 'lon', 'lat', 'elev', 'tilt' and 'azimuth'.
    Required data in the sim_data dictionary are 'poa_global' and 'cell_temperature'.

    References
    ----------
    [1] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.calcparams_desoto.html

    [2] https://pvlib-python.readthedocs.io/en/stable/generated/pvlib.pvsystem.singlediode.html

    [3]	(1, 2) W. De Soto et al., “Improvement and validation of a model for photovoltaic array performance”, Solar Energy, vol 80, pp. 78-88, 2006.

    [4]	System Advisor Model web page. https://sam.nrel.gov.

    [5]	A. Dobos, “An Improved Coefficient Calculator for the California Energy Commission 6 Parameter Photovoltaic Module Model”, Journal of Solar Energy Engineering, vol 134, 2012.

    [6]	O. Madelung, “Semiconductors: Data Handbook, 3rd ed.” ISBN 3-540-40488-0

    [7]	S.R. Wenham, M.A. Green, M.E. Watt, “Applied Photovoltaics” ISBN 0 86758 909 4

    [8]	A. Jain, A. Kapoor, “Exact analytical solutions of the parameters of real solar cells using Lambert W-function”, Solar Energy Materials and Solar Cells, 81 (2004) 269-277.

    [9]	D. King et al, “Sandia Photovoltaic Array Performance Model”, SAND2004-3535, Sandia National Laboratories, Albuquerque, NM

    [10]	“Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits” JW Bishop, Solar Cell (1988) https://doi.org/10.1016/0379-6787(88)90059-2

    """
    """
    TODO: Make it work with multiple module definitions
    """
    assert "poa_global" in self.sim_data
    assert "cell_temperature" in self.sim_data

    self.configure_cec_module(module, tech_year)

    sel = self.sim_data["poa_global"] > 0

    poa = self.sim_data["poa_global"][sel]
    cell_temp = self.sim_data["cell_temperature"][sel]

    # Use RectBivariateSpline to speed up simulation, but at the cost of accuracy (should still be >99.996%)
    maxpoa = np.nanmax(poa)

    _poa = np.concatenate(
        [
            np.logspace(-1, np.log10(maxpoa / 10), 20, endpoint=False),
            np.linspace(maxpoa / 10, maxpoa, 80),
        ]
    )
    _temp = np.linspace(cell_temp.min(), cell_temp.max(), 100)
    poaM, tempM = np.meshgrid(_poa, _temp)

    sotoParams = pvlib.pvsystem.calcparams_desoto(
        effective_irradiance=poaM.flatten(),
        temp_cell=tempM.flatten(),
        alpha_sc=self.module.alpha_sc,
        a_ref=self.module.a_ref,
        I_L_ref=self.module.I_L_ref,
        I_o_ref=self.module.I_o_ref,
        R_sh_ref=self.module.R_sh_ref,
        R_s=self.module.R_s,
        EgRef=1.121,  # PVLIB v0.7.2 Default
        dEgdT=-0.0002677,  # PVLIB v0.7.2 Default
        irrad_ref=1000,  # PVLIB v0.7.2 Default
        temp_ref=25,  # PVLIB v0.7.2 Default
    )

    photoCur, satCur, resSeries, resShunt, nNsVth = sotoParams
    gen = pvlib.pvsystem.singlediode(
        photocurrent=photoCur,
        saturation_current=satCur,
        resistance_series=resSeries,
        resistance_shunt=resShunt,
        nNsVth=nNsVth,
        method="lambertw",  # PVLIB v0.7.2 Default
    )

    interpolator = RectBivariateSpline(
        _temp,
        _poa,
        np.array(gen["p_mp"]).reshape(poaM.shape),
        kx=3,
        ky=3,  # np.array() since type changed between pvlib versions
    )
    self.sim_data["module_dc_power_at_mpp"] = np.zeros_like(self.sim_data["poa_global"])
    self.sim_data["module_dc_power_at_mpp"][sel] = interpolator(cell_temp, poa, grid=False)

    interpolator = RectBivariateSpline(
        _temp,
        _poa,
        np.array(gen["v_mp"]).reshape(poaM.shape),
        kx=3,
        ky=3,  # np.array() since type changed between pvlib versions
    )
    self.sim_data["module_dc_voltage_at_mpp"] = np.zeros_like(self.sim_data["poa_global"])
    self.sim_data["module_dc_voltage_at_mpp"][sel] = interpolator(cell_temp, poa, grid=False)

    self.sim_data["capacity_factor"] = self.sim_data["module_dc_power_at_mpp"] / (
        self.module.I_mp_ref * self.module.V_mp_ref
    )

    # Estimate total system generation
    if "capacity" in self.placements.columns:
        self.sim_data["total_system_generation"] = self.sim_data["capacity_factor"] * np.broadcast_to(
            self.placements.capacity, self._sim_shape_
        )

    if "modules_per_string" in self.placements.columns and "strings_per_inverter" in self.placements.columns:
        total_modules = (
            self.placements.modules_per_string
            * self.placements.strings_per_inverter
            * getattr(self.placements, "number_of_inverters", 1)
        )

        self.sim_data["total_system_generation"] = self.sim_data["module_dc_power_at_mpp"] * np.broadcast_to(
            total_modules, self._sim_shape_
        )

    return self

spatial_disaggregation

spatial_disaggregation(
    variable: str,
    source_high_resolution: Union[str, float, ndarray],
    source_low_resolution: Union[str, float, ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
)

[summary]

Parameters:

  • variable

    (str) –

    [description]

  • source_long_run_average

    (Union[str, float, ndarray]) –

    [description]

  • real_long_run_average

    (Union[str, float, ndarray]) –

    [description]

  • real_lra_scaling

    (float, default: 1 ) –

    [description], by default 1

  • spatial_interpolation

    (str, default: 'linear-spline' ) –

    [description], by default "linear-spline"

Source code in reskit/workflow_manager.py
def spatial_disaggregation(
    self,
    variable: str,
    source_high_resolution: Union[str, float, np.ndarray],
    source_low_resolution: Union[str, float, np.ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
):
    """[summary]

    Parameters
    ----------
    variable : str
        [description]
    source_long_run_average : Union[str, float, np.ndarray]
        [description]
    real_long_run_average : Union[str, float, np.ndarray]
        [description]
    real_lra_scaling : float, optional
        [description], by default 1
    spatial_interpolation : str, optional
        [description], by default "linear-spline"
    """
    # Get values from high resolution tiff file
    if isinstance(source_high_resolution, str):
        points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        correction_values_high_res = gk.raster.interpolateValues(  # TODO change here
            source_high_resolution, points, mode=spatial_interpolation
        )
        # assert not np.isnan(correction_values_high_res).any() and (correction_values_high_res > 0).all()
    else:
        correction_values_high_res = source_high_resolution

    # Get values from low resolution tiff file (meant over eg. ERA5)
    if isinstance(source_low_resolution, str):
        points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        correction_values_low_res = gk.raster.interpolateValues(  # TODO change here
            source_low_resolution, points, mode=spatial_interpolation
        )
        # assert not np.isnan(correction_values_low_res).any() and (correction_values_low_res > 0).all()
    else:
        correction_values_low_res = source_low_resolution

    # correction factors:
    factors = correction_values_high_res / correction_values_low_res
    factors = np.nan_to_num(factors, nan=1 / real_lra_scaling)
    assert (factors > 0).all()

    # update values
    self.sim_data[variable] = self.sim_data[variable] * factors * real_lra_scaling
    return self

to_netcdf

to_netcdf(
    xds: Dataset,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> str

Saves an XArray dataset to netCDF4 format

Note: - The .placements data is automatically added to the XArray dataset along the 'locations' dimension - The workflow_parameters data is automatically added as dimensionless variables - The .sim_data is automatically added along the dimensions (time, locations) - The .time_index is automatically added along the dimension 'time'

Parameters:

  • xds

    (Dataset) –

    The XArray dataset to save

  • output_netcdf_path

    (str, default: None ) –

    If given, the XArray dataset will be written to disc at the specified path - By default None

  • output_variables

    (List[str], default: None ) –

    If given, specifies the variables which should be included in the resulting dataset. Otherwise all suitable variables found in .placements, .workflow_parameters, .sim_data, and .time_index will be included - Only variables of numeric or string type are suitable due to NetCDF4 limitations - By default None

  • custom_attributes

    (dict, default: None ) –

    If given, adds the key-value pairs as attributes to the XArray dataset before saving - These will be added in addition to existing attributes - By default None

Returns:

  • output_netcdf_path

    The resulting output_netcdf_path

Source code in reskit/workflow_manager.py
def to_netcdf(
    self,
    xds: xarray.Dataset,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> str:
    """Saves an XArray dataset to netCDF4 format

    Note:
    - The `.placements` data is automatically added to the XArray dataset along the 'locations' dimension
    - The `workflow_parameters` data is automatically added as dimensionless variables
    - The `.sim_data` is automatically added along the dimensions (time, locations)
    - The `.time_index` is automatically added along the dimension 'time'

    Parameters
    ----------
    xds : xarray.Dataset
        The XArray dataset to save

    output_netcdf_path : str
        If given, the XArray dataset will be written to disc at the specified path
        - By default None

    output_variables : List[str], optional
        If given, specifies the variables which should be included in the resulting
        dataset. Otherwise all suitable variables found in `.placements`, `.workflow_parameters`,
        `.sim_data`, and `.time_index` will be included
        - Only variables of numeric or string type are suitable due to NetCDF4 limitations
        - By default None

    custom_attributes : dict, optional
        If given, adds the key-value pairs as attributes to the XArray dataset before saving
        - These will be added in addition to existing attributes
        - By default None

    Returns
    -------
    output_netcdf_path
        The resulting output_netcdf_path
    """
    encoding = dict()

    # write sim_data
    for key in self.sim_data.keys():
        # check if key in requestet output_variables
        if output_variables is not None:
            if key not in output_variables:
                continue
        encoding[key] = dict(zlib=True)

    # FIXME: WHAT IS THIS FOR?
    # #write sim_data_daily, only if exists
    # if hasattr(self, 'sim_data_daily'):
    #     for key in self.sim_data_daily.keys():
    #         #check if key in requestet output_variables
    #         if output_variables is not None:
    #             if key not in output_variables:
    #                 continue
    #         encoding[key] = dict(zlib=True)

    # Add custom attributes if provided
    if custom_attributes is not None:
        for k, v in custom_attributes.items():
            xds.attrs[k] = v

    if output_netcdf_path is not None:
        xds.to_netcdf(output_netcdf_path, encoding=encoding)
        return output_netcdf_path
    else:
        return xds

to_xarray

to_xarray(
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> Dataset

Generates an XArray dataset from the data currently contained in the WorkflowManager

Note: - The .placements data is automatically added to the XArray dataset along the 'locations' dimension - The workflow_parameters data is automatically added as dimensionless variables - The .sim_data is automatically added along the dimensions (time, locations) - The .time_index is automatically added along the dimension 'time'

Parameters:

  • output_netcdf_path

    (str, default: None ) –

    If given, the XArray dataset will be written to disc at the specified path - By default None

  • output_variables

    (List[str], default: None ) –

    If given, specifies the variables which should be included in the resulting dataset. Otherwise all suitable variables found in .placements, .workflow_parameters, .sim_data, and .time_index will be included - Only variables of numeric or string type are suitable due to NetCDF4 limitations - By default None

  • custom_attributes

    (dict, default: None ) –

    If given, adds the key-value pairs as attributes to the XArray dataset - These will be added in addition to the workflow_parameters - By default None

Returns:

  • Dataset

    The resulting XArray dataset

Source code in reskit/workflow_manager.py
def to_xarray(
    self,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> xarray.Dataset:
    """Generates an XArray dataset from the data currently contained in the WorkflowManager

    Note:
    - The `.placements` data is automatically added to the XArray dataset along the 'locations' dimension
    - The `workflow_parameters` data is automatically added as dimensionless variables
    - The `.sim_data` is automatically added along the dimensions (time, locations)
    - The `.time_index` is automatically added along the dimension 'time'

    Parameters
    ----------
    output_netcdf_path : str, optional
        If given, the XArray dataset will be written to disc at the specified path
        - By default None

    output_variables : List[str], optional
        If given, specifies the variables which should be included in the resulting
        dataset. Otherwise all suitable variables found in `.placements`, `.workflow_parameters`,
        `.sim_data`, and `.time_index` will be included
        - Only variables of numeric or string type are suitable due to NetCDF4 limitations
        - By default None

    custom_attributes : dict, optional
        If given, adds the key-value pairs as attributes to the XArray dataset
        - These will be added in addition to the workflow_parameters
        - By default None

    Returns
    -------
    xarray.Dataset
        The resulting XArray dataset
    """
    if isinstance(output_variables, str):
        output_variables = [output_variables]
    if isinstance(output_variables, list) and not "RESKit_sim_order" in output_variables:
        output_variables.append("RESKit_sim_order")

    times = self.time_index
    if times[0].tz is not None:
        times = [np.datetime64(dt.tz_convert("UTC").tz_convert(None)) for dt in times]
    times_days = np.unique(pd.DatetimeIndex(times).date).astype("datetime64")
    if times_days[0].astype("datetime64[Y]") != times_days[-1].astype("datetime64[Y]"):
        # old tiles where shifted by 1 hour, so the last day of the previous year also appears. catch this problem whti this if clause
        times_days = times_days[1:]
    xds = OrderedDict()
    encoding = dict()

    if "location_id" in self.placements.columns:
        location_coords = self.placements["location_id"].copy()
        del self.placements["location_id"]
    else:
        location_coords = np.arange(self.placements.shape[0])

    # write placements
    for c in self.placements.columns:
        # check if c in requestet output_variables
        if output_variables is not None:
            if c not in output_variables:
                continue
        if np.issubdtype(self.placements[c].dtype, np.number):
            write = True
        else:
            write = True
            for element in self.placements[c]:
                if not isinstance(element, (str, bytearray)):
                    write = False
                    break
        if write:
            xds[c] = xarray.DataArray(
                self.placements[c],
                dims=["location"],
                coords=dict(location=location_coords),
            )

    # write sim_data
    for key in self.sim_data.keys():
        # check if key in requestet output_variables
        if output_variables is not None:
            if key not in output_variables:
                continue

        tmp = np.full((len(self.time_index), self.locs.count), 0.0, dtype=float)
        tmp[self._time_sel_, :] = self.sim_data[key]

        xds[key] = xarray.DataArray(
            tmp,
            dims=["time", "location"],
            coords=dict(time=times, location=location_coords),
        )
        encoding[key] = dict(zlib=True)

    # write sim_data_daily, only if exists
    if hasattr(self, "sim_data_daily"):
        for key in self.sim_data_daily.keys():
            # check if key in requestet output_variables
            if output_variables is not None:
                if key not in output_variables:
                    continue

            tmp = np.full((len(times_days), self.locs.count), np.nan)
            tmp[:, :] = self.sim_data_daily[key]

            xds[key] = xarray.DataArray(
                tmp,
                dims=["time_days", "location"],
                coords=dict(time_days=times_days, location=location_coords),
            )
            encoding[key] = dict(zlib=True)

    if _intermediate_dict:
        return xds

    xds = xarray.Dataset(xds)

    for k, v in self.workflow_parameters.items():
        xds.attrs[k] = v

    # Add custom attributes if provided
    if custom_attributes is not None:
        for k, v in custom_attributes.items():
            xds.attrs[k] = v

    if output_netcdf_path is not None:
        xds.to_netcdf(output_netcdf_path, encoding=encoding)
        return output_netcdf_path
    else:
        return xds