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
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
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