Esempio n. 1
0
def _check_tech_final(model_run, tech_id, tech_config, loc_id, model_warnings,
                      errors, comments):
    """
    Checks individual tech/tech groups at specific nodes.
    NOTE: Updates `model_warnings` and `errors` lists in-place.
    """
    if tech_id not in model_run.techs:
        model_warnings.append(
            "Tech {} was removed by setting ``exists: False`` - not checking "
            "the consistency of its constraints at node {}.".format(
                tech_id, loc_id))
        return model_warnings, errors

    required = model_run.techs[tech_id].required_constraints
    allowed = model_run.techs[tech_id].allowed_constraints
    allowed_costs = model_run.techs[tech_id].allowed_costs

    # Error if required constraints are not defined
    for r in required:
        # If it's a string, it must be defined
        single_ok = isinstance(r, str) and r in tech_config.get(
            "constraints", {})
        # If it's a list of strings, one of them must be defined
        multiple_ok = isinstance(r, list) and any(
            [i in tech_config.get("constraints", {}) for i in r])
        if not single_ok and not multiple_ok:
            errors.append("`{}` at `{}` fails to define "
                          "all required constraints: {}".format(
                              tech_id, loc_id, required))

    # Warn if defining a carrier ratio for a conversion_plus tech,
    # but applying it to a carrier that isn't one of the carriers specified by that tech
    # e.g. carrier_ratios.carrier_in_2.cooling when cooling isn't a carrier`
    defined_carriers = get_all_carriers(model_run.techs[tech_id].essentials)
    carriers_in_ratios = [
        i.split(".")[-1] for i in tech_config.get_key(
            "constraints.carrier_ratios", AttrDict()).as_dict_flat().keys()
    ]
    for carrier in carriers_in_ratios:
        if carrier not in defined_carriers:
            model_warnings.append(
                "Tech `{t}` gives a carrier ratio for `{c}`, but does not actually "
                "configure `{c}` as a carrier.".format(t=tech_id, c=carrier))

    # If a technology is defined by units (i.e. integer decision variable), it must define energy_cap_per_unit
    if (any(["units_" in k for k in tech_config.get("constraints", {}).keys()])
            and "energy_cap_per_unit" not in tech_config.get(
                "constraints", {}).keys()):
        errors.append(
            "`{}` at `{}` fails to define energy_cap_per_unit when specifying "
            "technology in units_max/min/equals".format(
                tech_id, loc_id, required))

    # If a technology is defined by units & is a storage tech, it must define storage_cap_per_unit
    if (any(["units_" in k for k in tech_config.get("constraints", {}).keys()])
            and model_run.techs[tech_id].essentials.parent
            in ["storage", "supply_plus"] and any([
                "storage" in k
                for k in tech_config.get("constraints", {}).keys()
            ]) and "storage_cap_per_unit" not in tech_config.get(
                "constraints", {}).keys()):
        errors.append(
            "`{}` at `{}` fails to define storage_cap_per_unit when specifying "
            "technology in units_max/min/equals".format(
                tech_id, loc_id, required))

    # Gather remaining unallowed constraints
    remaining = set(tech_config.get("constraints", {})) - set(allowed)

    # Error if something is defined that's not allowed, but is in defaults
    # Warn if something is defined that's not allowed, but is not in defaults
    # (it could be a misspelling)
    for k in remaining:
        if k in DEFAULTS.techs.default_tech.constraints.keys():
            errors.append("`{}` at `{}` defines non-allowed "
                          "constraint `{}`".format(tech_id, loc_id, k))
        else:
            model_warnings.append(
                "`{}` at `{}` defines unrecognised "
                "constraint `{}` - possibly a misspelling?".format(
                    tech_id, loc_id, k))

    # Error if an `export` statement does not match the given carrier_outs
    if "export_carrier" in tech_config.get("constraints", {}):
        essentials = model_run.techs[tech_id].essentials
        export = tech_config.constraints.export_carrier
        if export and export not in [
                essentials.get_key(k, "")
                for k in ["carrier_out", "carrier_out_2", "carrier_out_3"]
        ]:
            errors.append("`{}` at `{}` is attempting to export a carrier "
                          "not given as an output carrier: `{}`".format(
                              tech_id, loc_id, export))

    # Error if non-allowed costs are defined
    for cost_class in tech_config.get_key("costs", {}):
        for k in tech_config.costs[cost_class]:
            if k not in allowed_costs:
                errors.append("`{}` at `{}` defines non-allowed "
                              "{} cost: `{}`".format(tech_id, loc_id,
                                                     cost_class, k))

    # Error if non-allowed `resource_unit` is defined
    if tech_config.switches.get("resource_unit", "energy") not in [
            "energy",
            "energy_per_cap",
            "energy_per_area",
    ]:
        errors.append(
            "`{}` is an unknown resource unit for `{}` at `{}`. "
            "Only `energy`, `energy_per_cap`, or `energy_per_area` is allowed."
            .format(tech_config.switches.resource_unit, tech_id, loc_id))

    return model_warnings, errors
Esempio n. 2
0
def check_initial(config_model):
    """
    Perform initial checks of model and run config dicts.

    Returns
    -------
    model_warnings : list
        possible problems that do not prevent the model run
        from continuing
    errors : list
        serious issues that should raise a ModelError

    """
    errors = []
    model_warnings = []

    # Check for version mismatch
    model_version = config_model.model.get("calliope_version", False)
    if model_version:
        if not str(model_version) in __version__:
            model_warnings.append(
                "Model configuration specifies calliope_version={}, "
                "but you are running {}. Proceed with caution!".format(
                    model_version, __version__))

    # Check top-level keys
    for k in config_model.keys():
        if k not in [
                "model",
                "run",
                "nodes",
                "locations",  # TODO: remove in v0.7.1
                "tech_groups",
                "techs",
                "links",
                "overrides",
                "scenarios",
                "config_path",
        ]:
            model_warnings.append(
                "Unrecognised top-level configuration item: {}".format(k))

    # Check that all required top-level keys are specified
    for k in ["model", "run", "nodes", "techs"]:
        if k not in config_model.keys():
            if k == "nodes" and "locations" in config_model.keys():
                # TODO: remove in v0.7.1
                warnings.warn(
                    "`locations` has been renamed to `nodes` and will stop working "
                    "in v0.7.1. Please update your model configuration accordingly.",
                    DeprecationWarning,
                )
            else:
                errors.append(
                    "Model is missing required top-level configuration item: {}"
                    .format(k))

    # Check run configuration
    # Exclude solver_options and objective_options.cost_class from checks,
    # as we don't know all possible options for all solvers
    for k in config_model["run"].keys_nested():
        if (k not in DEFAULTS["run"].keys_nested()
                and "solver_options" not in k
                and "objective_options.cost_class" not in k):
            model_warnings.append(
                "Unrecognised setting in run configuration: {}".format(k))

    # Check model configuration, but top-level keys only
    for k in config_model["model"].keys():
        if k not in DEFAULTS["model"].keys():
            model_warnings.append(
                "Unrecognised setting in model configuration: {}".format(k))
    # If spores run mode is selected, check the correct definition of all needed parameters
    if config_model.run.mode == "spores":
        # Check that spores number is greater than 0, otherwise raise warning
        if config_model.run.spores_options.spores_number == 0:
            model_warnings.append(
                "spores run mode is selected, but a number of 0 spores is requested"
            )
        # Check that slack cost is greater than 0, otherwise set to default (0.1) and raise warning
        if config_model.run.spores_options.slack <= 0:
            config_model.run.spores_options.slack = 0.1
            model_warnings.append(
                "Slack must be greater than zero, setting slack to default value of 0.1 "
            )
        # Check that score_cost_class is a string
        _spores_cost_class = config_model.run.spores_options.get(
            "score_cost_class", {})
        if not isinstance(_spores_cost_class, str):
            errors.append(
                "`run.spores_options.score_cost_class` must be a string")

    # Only ['in', 'out', 'in_2', 'out_2', 'in_3', 'out_3']
    # are allowed as carrier tiers
    for key in config_model.as_dict_flat().keys():
        if "essentials.carrier_" in key and key.split(".carrier_")[-1].split(
                ".")[0] not in ["in", "out", "in_2", "out_2", "in_3", "out_3"]:
            errors.append(
                "Invalid carrier tier found at {}. Only "
                "'carrier_' + ['in', 'out', 'in_2', 'out_2', 'in_3', 'out_3'] "
                "is valid.".format(key))

    # No techs may have the same identifier as a tech_group
    name_overlap = set(config_model.tech_groups.keys()) & set(
        config_model.techs.keys())
    if name_overlap:
        errors.append("tech_groups and techs with "
                      "the same name exist: {}".format(name_overlap))

    # Checks for techs and tech_groups:
    # * All user-defined tech and tech_groups must specify a parent
    # * techs cannot be parents, only tech groups can
    # * No carrier may be called 'resource'
    default_tech_groups = list(DEFAULTS.tech_groups.keys())
    for tg_name, tg_config in config_model.tech_groups.items():
        if tg_name in default_tech_groups:
            continue
        if not tg_config.get_key("essentials.parent"):
            errors.append("tech_group {} does not define "
                          "`essentials.parent`".format(tg_name))
        elif tg_config.get_key(
                "essentials.parent") in config_model.techs.keys():
            errors.append(
                "tech_group `{}` has a tech as a parent, only another tech_group "
                "is allowed".format(tg_name))
        if "resource" in get_all_carriers(tg_config.essentials):
            errors.append("No carrier called `resource` may "
                          "be defined (tech_group: {})".format(tg_name))

    for t_name, t_config in config_model.techs.items():
        for key in t_config.keys():
            if key not in DEFAULTS.techs.default_tech.keys():
                model_warnings.append(
                    "Unknown key `{}` defined for tech {}.".format(
                        key, t_name))
        if not t_config.get_key("essentials.parent"):
            errors.append("tech {} does not define "
                          "`essentials.parent`".format(t_name))
        elif t_config.get_key(
                "essentials.parent") in config_model.techs.keys():
            errors.append(
                "tech `{}` has another tech as a parent, only a tech_group "
                "is allowed".format(tg_name))
        if "resource" in get_all_carriers(t_config.essentials):
            errors.append("No carrier called `resource` may "
                          "be defined (tech: {})".format(t_name))

    # Check whether any unrecognised mid-level keys are defined in techs, nodes, or links
    for k, v in config_model.get("nodes", {}).items():
        unrecognised_keys = [
            i for i in v.keys() if i not in DEFAULTS.nodes.default_node.keys()
        ]
        if len(unrecognised_keys) > 0:
            errors.append(
                "Node `{}` contains unrecognised keys {}. "
                "These could be mispellings or a technology not defined "
                "under the `techs` key.".format(k, unrecognised_keys))
        for loc_tech_key, loc_tech_val in v.get("techs", {}).items():
            if loc_tech_val is None:
                continue
            unrecognised_keys = [
                i for i in loc_tech_val.keys()
                if i not in DEFAULTS.techs.default_tech.keys()
            ]
            if len(unrecognised_keys) > 0:
                errors.append(
                    "Technology `{}` in node `{}` contains unrecognised keys {}; "
                    "these are most likely mispellings".format(
                        loc_tech_key, k, unrecognised_keys))

    default_link = DEFAULTS.links["default_node_from,default_node_to"]
    for k, v in config_model.get("links", {}).items():
        unrecognised_keys = [
            i for i in v.keys() if i not in default_link.keys()
        ]
        if len(unrecognised_keys) > 0:
            errors.append(
                "Link `{}` contains unrecognised keys {}. "
                "These could be mispellings or a technology not defined "
                "under the `techs` key.".format(k, unrecognised_keys))
        for link_tech_key, link_tech_val in v.get("techs", {}).items():
            if link_tech_val is None:
                continue
            unrecognised_keys = [
                i for i in link_tech_val.keys()
                if i not in default_link.techs.default_tech.keys()
                and i not in DEFAULTS.techs.default_tech.keys()
            ]
            if len(unrecognised_keys) > 0:
                errors.append(
                    "Technology `{}` in link `{}` contains unrecognised keys {}; "
                    "these are most likely mispellings".format(
                        link_tech_key, k, unrecognised_keys))

    # Error if a technology is defined twice, in opposite directions
    link_techs = [
        tuple(sorted(j.strip() for j in k.split(","))) + (i, )
        for k, v in config_model.get("links", {}).items()
        for i in v.get("techs", {}).keys()
    ]
    if len(link_techs) != len(set(link_techs)):
        duplicated_techs = np.array(link_techs)[pd.Series(
            link_techs).duplicated().values]
        duplicated_techs = set([i[-1] for i in duplicated_techs])
        tech_end = "y" if len(duplicated_techs) == 1 else "ies"
        errors.append(
            "Technolog{} {} defined twice on a link defined in both directions "
            "(e.g. `A,B` and `B,A`). A technology can only be defined on one link "
            "even if it allows unidirectional flow in each direction "
            "(i.e. `one_way: true`).".format(tech_end,
                                             ", ".join(duplicated_techs)))

    # Error if a constraint is loaded from file that must not be
    allowed_from_file = DEFAULTS.model.file_allowed
    for k, v in config_model.as_dict_flat().items():
        if "file=" in str(v):

            possible_identifiers = k.split(".")
            is_time_varying = any("_time_varying" in i
                                  for i in possible_identifiers)
            if is_time_varying:
                model_warnings.append(
                    "Using custom constraint `{}` with time-varying data.".
                    format(k))
            elif (not set(possible_identifiers).intersection(allowed_from_file)
                  and not is_time_varying):
                errors.append(
                    "Cannot load data from file for configuration `{}`.".
                    format(k))

    # We no longer allow cost_class in objective_obtions to be a string
    _cost_class = config_model.run.objective_options.get("cost_class", {})

    if not isinstance(_cost_class, dict):
        errors.append(
            "`run.objective_options.cost_class` must be a dictionary."
            "If you want to minimise or maximise with a single cost class, "
            'use e.g. "{monetary: 1}", which gives the monetary cost class a weight '
            "of 1 in the objective, and ignores any other cost classes.")
    else:
        # This next check is only run if we have confirmed that cost_class is
        # a dict, as it errors otherwise

        # For cost minimisation objective, check for cost_class: None and set to one
        for k, v in _cost_class.items():
            if v is None:
                config_model.run.objective_options.cost_class[k] = 1
                model_warnings.append(
                    "cost class {} has weight = None, setting weight to 1".
                    format(k))

    if (isinstance(_cost_class, dict) and _cost_class.get("monetary", 0) == 1
            and len(_cost_class.keys()) > 1):
        # Warn that {monetary: 1} is still in the objective, since it is not
        # automatically overidden on setting another objective.
        model_warnings.append(
            "Monetary cost class with a weight of 1 is still included "
            "in the objective. If you want to remove the monetary cost class, "
            'add `{"monetary": 0}` to the dictionary nested under '
            " `run.objective_options.cost_class`.")

    # Don't allow time clustering with cyclic storage if not also using
    # storage_inter_cluster
    storage_inter_cluster = "model.time.function_options.storage_inter_cluster"
    if (config_model.get_key("model.time.function", None) == "apply_clustering"
            and config_model.get_key("run.cyclic_storage", True)
            and not config_model.get_key(storage_inter_cluster, True)):
        errors.append(
            "When time clustering, cannot have cyclic storage constraints if "
            "`storage_inter_cluster` decision variable is not activated.")

    return model_warnings, errors
Esempio n. 3
0
def generate_loc_tech_sets(model_run, simple_sets):
    """
    Generate loc-tech sets for a given pre-processed ``model_run``

    Parameters
    ----------
    model_run : AttrDict
    simple_sets : AttrDict
        Simple sets returned by ``generate_simple_sets(model_run)``.

    """
    sets = AttrDict()

    ##
    # First deal with transmission techs, which can show up only in
    # loc_techs_transmission, loc_techs_milp, and loc_techs_purchase
    ##

    # All `tech:loc` expanded transmission technologies
    sets.loc_techs_transmission = set(
        concat_iterable(
            [
                (i, u, j) for i, j, u in product(  # (loc, loc, tech) product
                    simple_sets.locs,
                    simple_sets.locs,
                    simple_sets.techs_transmission_names,
                ) if model_run.get_key(
                    "locations.{}.links.{}.techs.{}".format(i, j, u), None)
            ],
            ["::", ":"],
        ))

    # A dict of transmission tech config objects
    # to make parsing for set membership easier
    loc_techs_transmission_config = {
        k: model_run.get_key(
            "locations.{loc_from}.links.{loc_to}.techs.{tech}".format(
                **split_loc_techs_transmission(k)))
        for k in sets.loc_techs_transmission
    }

    ##
    # Now deal with the rest of the techs and other sets
    ##

    # Only loc-tech combinations that actually exist
    sets.loc_techs_non_transmission = set(
        concat_iterable(
            [(l, t) for l, t in product(simple_sets.locs,
                                        simple_sets.techs_non_transmission)
             if model_run.get_key("locations.{}.techs.{}".format(l, t), None)],
            ["::"],
        ))

    sets.loc_techs = sets.loc_techs_non_transmission | sets.loc_techs_transmission

    # A dict of non-transmission tech config objects
    # to make parsing for set membership easier
    loc_techs_config = {
        k: model_run.get_key("locations.{}.techs.{}".format(*k.split("::")))
        for k in sets.loc_techs_non_transmission
    }

    loc_techs_all_config = {
        **loc_techs_config,
        **loc_techs_transmission_config
    }

    ##
    # Sets based on membership in abstract base technology groups
    ##

    for group in [
            "storage",
            "demand",
            "supply",
            "supply_plus",
            "conversion",
            "conversion_plus",
    ]:
        tech_set = set(
            k for k in sets.loc_techs_non_transmission
            if model_run.techs[k.split("::")[1]].inheritance[-1] == group)
        sets["loc_techs_{}".format(group)] = tech_set

    sets.loc_techs_non_conversion = (set(
        k for k in sets.loc_techs_non_transmission if k not in
        sets.loc_techs_conversion and k not in sets.loc_techs_conversion_plus)
                                     | sets.loc_techs_transmission)

    # Techs that introduce energy into the system
    sets.loc_techs_supply_all = sets.loc_techs_supply | sets.loc_techs_supply_plus

    # Techs that change the energy carrier in the system
    sets.loc_techs_conversion_all = (sets.loc_techs_conversion
                                     | sets.loc_techs_conversion_plus)
    # All techs that can be used to generate a carrier (not just store or move it)
    sets.loc_techs_supply_conversion_all = (sets.loc_techs_supply_all
                                            | sets.loc_techs_conversion_all)

    ##
    # Sets based on specific constraints being active
    ##

    # Technologies that specify resource_area constraints
    sets.loc_techs_area = set(
        k for k in sets.loc_techs_non_transmission
        if (any(
            "resource_area" in i
            for i in loc_techs_config[k].keys_nested()) or loc_techs_config[k].
            constraints.get("resource_unit", "energy") == "energy_per_area"))

    # Technologies that define storage, which can include `supply_plus`
    # and `storage` groups.
    sets.loc_techs_store = (set(k for k in sets.loc_techs_supply_plus if any(
        "storage_" in i
        for i in loc_techs_config[k].constraints.keys_nested()))
                            | sets.loc_techs_storage)

    # technologies that specify a finite resource
    sets.loc_techs_finite_resource = set(
        k for k in sets.loc_techs_non_transmission
        if loc_techs_config[k].constraints.get("resource") and not (
            loc_techs_config[k].constraints.get("resource") in ["inf", np.inf])
    )

    # `supply` technologies that specify a finite resource
    sets.loc_techs_finite_resource_supply = sets.loc_techs_finite_resource.intersection(
        sets.loc_techs_supply)

    # `demand` technologies that specify a finite resource
    sets.loc_techs_finite_resource_demand = sets.loc_techs_finite_resource.intersection(
        sets.loc_techs_demand)

    # `supply_plus` technologies that specify a finite resource
    sets.loc_techs_finite_resource_supply_plus = sets.loc_techs_finite_resource.intersection(
        sets.loc_techs_supply_plus)

    # Technologies that define ramping constraints
    sets.loc_techs_ramping = set(
        k for k in sets.loc_techs_non_transmission
        if "energy_ramping" in loc_techs_config[k].constraints)

    # Technologies that allow export
    sets.loc_techs_export = set(
        k for k in sets.loc_techs_non_transmission
        if "export_carrier" in loc_techs_config[k].constraints)

    # Technologies that allow purchasing discrete units
    # NB: includes transmission techs!
    loc_techs_purchase = set(k for k in sets.loc_techs_non_transmission if any(
        ".purchase" in i
        for i in loc_techs_config[k].get("costs", AttrDict()).keys_nested(
        )) and not any("units_" in i for i in loc_techs_config[k].get(
            "constraints", AttrDict()).keys_nested()))

    transmission_purchase = set(
        k for k in sets.loc_techs_transmission
        if any(".purchase" in i for i in loc_techs_transmission_config[k].get(
            "costs", AttrDict()).keys_nested()) and not any(
                "units_" in i for i in loc_techs_transmission_config[k].get(
                    "constraints", AttrDict()).keys_nested()))

    sets.loc_techs_purchase = loc_techs_purchase | transmission_purchase

    # Technologies with MILP constraints
    loc_techs_milp = set(k for k in sets.loc_techs_non_transmission if any(
        "units_" in i for i in loc_techs_config[k].constraints.keys_nested()))

    transmission_milp = set(k for k in sets.loc_techs_transmission if any(
        "units_" in i
        for i in loc_techs_transmission_config[k].constraints.keys_nested()))

    sets.loc_techs_milp = loc_techs_milp | transmission_milp

    # Technologies with forced asynchronous production/consumption of energy
    loc_techs_storage_asynchronous_prod_con = set(
        k for k in sets.loc_techs_store if "force_asynchronous_prod_con" in
        loc_techs_config[k].constraints.keys_nested())

    loc_techs_transmission_asynchronous_prod_con = set(
        k for k in sets.loc_techs_transmission if "force_asynchronous_prod_con"
        in loc_techs_transmission_config[k].constraints.keys_nested())
    sets.loc_techs_asynchronous_prod_con = (
        loc_techs_storage_asynchronous_prod_con
        | loc_techs_transmission_asynchronous_prod_con)

    ##
    # Sets based on specific costs being active
    # NB includes transmission techs
    ##

    loc_techs_costs = set(k for k in sets.loc_techs_non_transmission if any(
        "costs" in i for i in loc_techs_config[k].keys()))

    loc_techs_transmission_costs = set(
        k for k in sets.loc_techs_transmission
        if any("costs" in i for i in loc_techs_transmission_config[k].keys()))

    # Any capacity or fixed annual costs
    loc_techs_investment_costs = set(k for k in loc_techs_costs if any(
        "_cap" in i or ".purchase" in i or "_area" in i
        for i in loc_techs_config[k].costs.keys_nested()))
    loc_techs_transmission_investment_costs = set(
        k for k in loc_techs_transmission_costs
        if any("_cap" in i or ".purchase" in i or "_area" in i
               for i in loc_techs_transmission_config[k].costs.keys_nested()))

    # Any operation and maintenance
    loc_techs_om_costs = set(k for k in loc_techs_costs if any(
        "om_" in i or "export" in i
        for i in loc_techs_config[k].costs.keys_nested()))
    loc_techs_transmission_om_costs = set(
        k for k in loc_techs_transmission_costs
        if any("om_" in i
               for i in loc_techs_transmission_config[k].costs.keys_nested()))

    # Any export costs
    sets.loc_techs_costs_export = set(k for k in loc_techs_costs if any(
        "export" in i for i in loc_techs_config[k].costs.keys_nested()))

    sets.loc_techs_cost = loc_techs_costs | loc_techs_transmission_costs
    sets.loc_techs_investment_cost = (loc_techs_investment_costs |
                                      loc_techs_transmission_investment_costs)
    sets.loc_techs_om_cost = loc_techs_om_costs | loc_techs_transmission_om_costs

    ##
    # Subsets of costs for different abstract base technologies
    ##

    sets.loc_techs_om_cost_conversion = loc_techs_om_costs.intersection(
        sets.loc_techs_conversion)
    sets.loc_techs_om_cost_conversion_plus = loc_techs_om_costs.intersection(
        sets.loc_techs_conversion_plus)
    sets.loc_techs_om_cost_supply = loc_techs_om_costs.intersection(
        sets.loc_techs_supply)
    sets.loc_techs_om_cost_supply_plus = loc_techs_om_costs.intersection(
        sets.loc_techs_supply_plus)

    ##
    # Subsets of `conversion_plus` technologies
    ##

    # `conversion_plus` technologies with secondary carrier(s) out
    sets.loc_techs_out_2 = set(k for k in sets.loc_techs_conversion_plus
                               if "carrier_out_2" in model_run.techs[k.split(
                                   "::")[1].split(":")[0]].essentials)

    # `conversion_plus` technologies with tertiary carrier(s) out
    sets.loc_techs_out_3 = set(k for k in sets.loc_techs_conversion_plus
                               if "carrier_out_3" in model_run.techs[k.split(
                                   "::")[1].split(":")[0]].essentials)

    # `conversion_plus` technologies with secondary carrier(s) in
    sets.loc_techs_in_2 = set(k for k in sets.loc_techs_conversion_plus
                              if "carrier_in_2" in model_run.techs[k.split(
                                  "::")[1].split(":")[0]].essentials)

    # `conversion_plus` technologies with tertiary carrier(s) in
    sets.loc_techs_in_3 = set(k for k in sets.loc_techs_conversion_plus
                              if "carrier_in_3" in model_run.techs[k.split(
                                  "::")[1].split(":")[0]].essentials)

    ##
    # `loc_tech_carrier` sets
    ##

    # loc_tech_carriers for all technologies that have energy_prod=True
    sets.loc_tech_carriers_prod = set(
        "{}::{}".format(k, carrier) for k in sets.loc_techs
        if loc_techs_all_config[k].constraints.get_key("energy_prod", False)
        for carrier in get_all_carriers(model_run.techs[k.split("::")[1].split(
            ":")[0]].essentials,
                                        direction="out"))

    # loc_tech_carriers for all technologies that have energy_con=True
    sets.loc_tech_carriers_con = set(
        "{}::{}".format(k, carrier) for k in sets.loc_techs
        if loc_techs_all_config[k].constraints.get_key("energy_con", False)
        for carrier in get_all_carriers(model_run.techs[k.split("::")[1].split(
            ":")[0]].essentials,
                                        direction="in"))

    # loc_tech_carriers for all supply technologies
    sets.loc_tech_carriers_supply_all = set(
        "{}::{}".format(k, carrier) for k in sets.loc_techs_supply_all
        for carrier in get_all_carriers(model_run.techs[k.split("::")[1].split(
            ":")[0]].essentials,
                                        direction="out"))

    # loc_tech_carriers for all conversion technologies
    sets.loc_tech_carriers_conversion_all = set(
        "{}::{}".format(k, carrier) for k in sets.loc_techs_conversion_all
        for carrier in get_all_carriers(model_run.techs[k.split("::")[1].split(
            ":")[0]].essentials,
                                        direction="out"))

    # loc_tech_carriers for all supply and conversion technologies
    sets.loc_tech_carriers_supply_conversion_all = (
        sets.loc_tech_carriers_supply_all
        | sets.loc_tech_carriers_conversion_all)
    # loc_tech_carriers for all demand technologies
    sets.loc_tech_carriers_demand = set(
        "{}::{}".format(k, carrier) for k in sets.loc_techs_demand
        for carrier in get_all_carriers(model_run.techs[k.split("::")[1].split(
            ":")[0]].essentials,
                                        direction="in"))

    # loc_tech_carriers for all technologies that have export
    sets.loc_tech_carriers_export = set(
        "{}::{}".format(k, loc_techs_all_config[k].constraints.export_carrier)
        for k in sets.loc_techs if loc_techs_all_config[k].constraints.get_key(
            "export_carrier", False))

    # loc_tech_carriers for `conversion_plus` technologies
    sets.loc_tech_carriers_conversion_plus = set(
        k for k in sets.loc_tech_carriers_con | sets.loc_tech_carriers_prod
        if k.rsplit("::", 1)[0] in sets.loc_techs_conversion_plus)

    # loc_carrier combinations that exist with either a con or prod tech
    sets.loc_carriers = set("{0}::{2}".format(*k.split("::"))
                            for k in sets.loc_tech_carriers_prod
                            | sets.loc_tech_carriers_con)

    return sets