@@ -554,15 +554,167 @@ def demand_limiting_capacity(
554554 else capacity
555555 )
556556
557- # This constraint is independent of the production
558- production = 0
557+ # An adjustment is required to account for technologies that have multiple output
558+ # commodities
559+ b = modify_dlc (technologies = capacity , demand = b )
559560
560561 return xr .Dataset (
561- dict (capacity = capacity , production = production , b = b ),
562+ dict (capacity = capacity , b = b ),
562563 attrs = dict (kind = ConstraintKind .UPPER_BOUND ),
563564 )
564565
565566
567+ def modify_dlc (technologies : xr .DataArray , demand : xr .DataArray ) -> xr .DataArray :
568+ """Modifies DLC constraint to account for techs with multiple output commodities.
569+
570+ Adjusts the commodity-level DLC based on the commodity output ratios of the
571+ available technologies, to allow for appropriate production of side-products.
572+
573+ Args:
574+ technologies: DataArray with dimensions "commodity" and "replacement". This
575+ defines the fixed commodity outputs for each potential replacement
576+ technology.
577+ demand: DataArray with dimension "commodity", which defines the demand for each
578+ commodity.
579+
580+ Returns:
581+ DataArray with dimension "commodity", which defines the new demand-limiting
582+ capacity constraint for each commodity.
583+
584+ Example:
585+ Let's consider a simple example of a refinery sector with two alternative
586+ technologies that each produce two commodities: gasoline and diesel.
587+
588+ We define the technologies DataArray as follows:
589+ >>> import xarray as xr
590+ >>> technologies = xr.DataArray(
591+ ... data=[[1, 5], [0.5, 1]],
592+ ... dims=['replacement', 'commodity'],
593+ ... coords={'replacement': ['technology1', 'technology2'],
594+ ... 'commodity': ['gasoline', 'diesel']},
595+ ... )
596+
597+ technology1 produces 1 unit of gasoline and 5 units of diesel (per unit of
598+ activity), whereas technology2 produces 0.5 units of gasoline and 1 unit of
599+ diesel.
600+
601+ In this scenario, let's also define the demand for gasoline and diesel as
602+ follows (1 unit of demand for gasoline and 0 units for diesel):
603+ >>> demand = xr.DataArray(
604+ ... data=[1, 0],
605+ ... dims=['commodity'],
606+ ... coords={'commodity': ['gasoline', 'diesel']},
607+ ... )
608+
609+ The aim of the demand-limiting capacity (DLC) constraint is to limit the
610+ capacity of each technology so that supply is sufficient to meet the demand for
611+ each commodity, and no more.
612+
613+ However, in this case we have a problem. The demand for gasoline can be met by
614+ either technology1 or technology2 (as both produce gasoline), but doing so would
615+ require producing up to 5 units of diesel (if all demand was met by
616+ technology1), which would exceed the diesel demand (0). Therefore, to allow the
617+ model to meet the demand for gasoline via either technology, we must relax the
618+ DLC constraint on diesel (to 5 units).
619+
620+ In general, for an arbitrary set of technologies and commodity demands, the
621+ DLC of each commodity needs to be sufficiently high to permit any technology to
622+ act in service of any appropriate commodity demand, and no higher.
623+
624+ The first step is to calculate the commodity output ratios for each technology:
625+ >>> output_ratios = technologies.rename({"commodity": "commodity2"}) / technologies
626+ >>> output_ratios
627+ <xarray.DataArray (replacement: 2, commodity2: 2, commodity: 2)> Size: 64B
628+ array([[[1. , 0.2],
629+ [5. , 1. ]],
630+ <BLANKLINE>
631+ [[1. , 0.5],
632+ [2. , 1. ]]])
633+ Coordinates:
634+ * replacement (replacement) <U11 88B 'technology1' 'technology2'
635+ * commodity2 (commodity2) <U8 64B 'gasoline' 'diesel'
636+ * commodity (commodity) <U8 64B 'gasoline' 'diesel'
637+
638+ We introduce the dimension "commodity2" to compare the outputs of each commodity
639+ against every other commodity. For example, for technology1, producing 1 unit of
640+ gasoline leads to 5 units of diesel, whereas producing 1 unit of diesel leads to
641+ 0.2 units of gasoline.
642+
643+ Multiplying these output ratios by the demand, we get the full outputs that each
644+ technology would produce whilst acting in service of each commodity-level
645+ demand:
646+ >>> outputs = output_ratios * demand
647+ >>> outputs
648+ <xarray.DataArray (replacement: 2, commodity2: 2, commodity: 2)> Size: 64B
649+ array([[[1., 0.],
650+ [5., 0.]],
651+ <BLANKLINE>
652+ [[1., 0.],
653+ [2., 0.]]])
654+ Coordinates:
655+ * replacement (replacement) <U11 88B 'technology1' 'technology2'
656+ * commodity2 (commodity2) <U8 64B 'gasoline' 'diesel'
657+ * commodity (commodity) <U8 64B 'gasoline' 'diesel'
658+
659+ In this case, meeting the gasoline demand with technology1 would require
660+ producing 1 unit of gasoline and 5 units of diesel, whereas meeting the gasoline
661+ demand with technology2 would require producing 1 unit of gasoline and 2 units
662+ of diesel. Since there is no diesel demand, all values for commodity = "diesel"
663+ are zero.
664+
665+ Then, taking a maximum over the "commodity" dimension, we get the maximum
666+ potential outputs of each technology:
667+ >>> max_outputs = outputs.max("commodity")
668+ >>> max_outputs
669+ <xarray.DataArray (replacement: 2, commodity2: 2)> Size: 32B
670+ array([[1., 5.],
671+ [1., 2.]])
672+ Coordinates:
673+ * replacement (replacement) <U11 88B 'technology1' 'technology2'
674+ * commodity2 (commodity2) <U8 64B 'gasoline' 'diesel'
675+
676+ In this case, this is just the outputs of each technology when acting in service
677+ of the gasoline demand.
678+
679+ Finally, summing over the "replacement" dimension, we get the maximum potential
680+ outputs of each commodity:
681+ >>> dlc = max_outputs.max("replacement").rename({"commodity2": "commodity"})
682+ >>> dlc
683+ <xarray.DataArray (commodity: 2)> Size: 16B
684+ array([1., 5.])
685+ Coordinates:
686+ * commodity (commodity) <U8 64B 'gasoline' 'diesel'
687+
688+ In this case, we get the maximum potential production of diesel as 5 units,
689+ which would occur as a side-product when technology1 is acting in service of the
690+ gasoline demand. This becomes the new DLC constraint.
691+
692+ Putting this all together:
693+ >>> from muse.constraints import modify_dlc
694+ >>> modify_dlc(technologies, demand)
695+ <xarray.DataArray (commodity: 2)> Size: 16B
696+ array([1., 5.])
697+ Coordinates:
698+ * commodity (commodity) <U8 64B 'gasoline' 'diesel'
699+ """ # noqa: E501
700+ # Calculate commodity output ratios for each technology
701+ output_ratios = technologies .rename ({"commodity" : "commodity2" }) / technologies
702+ output_ratios = output_ratios .where (np .isfinite (output_ratios ), 0 ) # this is
703+ # necessary for technologies that do not produce every commodity, which would lead
704+ # to an "infinite" ratio between commodities
705+
706+ # Calculate the full outputs of each technology acting in service of each commodity
707+ # demand
708+ outputs = output_ratios * demand
709+
710+ # Maximum potential outputs for each technology
711+ max_outputs = outputs .max ("commodity" )
712+
713+ # Maximum potential production of each commodity -> demand-limiting capacity
714+ b = max_outputs .max ("replacement" ).rename ({"commodity2" : "commodity" })
715+ return b
716+
717+
566718@register_constraints
567719def minimum_service (
568720 demand : xr .DataArray ,
0 commit comments