Source code for cyclopts.random_request_species

"""This module defines a ProblemSpecies subclass and related classes for
reactor-request-based resources exchanges that are randomly populated (i.e., not
related to a specific fuel cycle).

:author: Matthew Gidden <matthew.gidden _at_ gmail.com>
"""
import operator
import re
import collections
import uuid
import random as rnd
import numpy as np
import copy as cp
import itertools

import cyclopts.exchange_instance as exinst
from cyclopts.problems import ProblemSpecies
import cyclopts.cyclopts_io as cycio
from cyclopts.exchange_family import ResourceExchange
from cyclopts.params import Param, BoolParam, CoeffParam, SupConstrParam, \
    PARAM_CTOR_ARGS
import cyclopts.tools as tools

[docs]class RandomRequestPoint(object): """A container class representing a point in parameter space for RandomRequest problem species. """ def __init__(self, n_commods = None, n_request = None, assem_per_req = None, req_qty = None, assem_multi_commod = None, req_multi_commods = None, exclusive = None, n_req_constr = None, n_supply = None, sup_multi = None, sup_multi_commods = None, n_sup_constr = None, sup_constr_val = None, connection = None, constr_coeff = None, pref_coeff = None): """Parameters ---------- n_commods : Param or similar, optional the number of commodities n_request : Param or similar, optional the number of requesters (i.e., RequestGroups) assem_per_req : Param or similar, optional the number of assemblies in a request req_qty : Param or similar, optional the quantity associated with each request assem_multi_commod : BoolParam or similar, optional whether an assembly request can be satisfied by multiple commodities req_multi_commods : Param or similar, optional the number of commodities in a multicommodity zone exclusive : BoolParam or similar, optional the probability that a reactor assembly request is exclusive n_req_constr : Param or similar, optional the number of constraints associated with a given request group n_supply : Param or similar, optional the number of suppliers (i.e., supply ExchangeNodeGroups) sup_multi : BoolParam or similar, optional whether a supplier supplies more than one commodity sup_multi_commods : Param or similar, optional the number of commodities a multicommodity supplier supplies n_sup_constr : Param or similar, optional the number of constraints associated with a given supply group sup_constr_val : SupConstrParam or similar, optional the supply constraint rhs value (as a fraction of the total request amount for a commodity) connection : BoolParam or similar, optional the probability that a possible connection between supply and request nodes is added constr_coeff : CoeffParam or similar, optional constraint coefficients pref_coeff : CoeffParam or similar, optional preference coefficients """ self.n_commods = n_commods \ if n_commods is not None else Param(1) self.n_request = n_request \ if n_request is not None else Param(1) self.assem_per_req = assem_per_req \ if assem_per_req is not None else Param(1) self.req_qty = req_qty \ if req_qty is not None else Param(1) self.assem_multi_commod = assem_multi_commod \ if assem_multi_commod is not None else BoolParam(-1.0) # never true self.req_multi_commods = req_multi_commods \ if req_multi_commods is not None else Param(0) self.exclusive = exclusive \ if exclusive is not None else BoolParam(-1.0) # never true self.n_req_constr = n_req_constr \ if n_req_constr is not None else Param(0) self.n_supply = n_supply \ if n_supply is not None else Param(1) self.sup_multi = sup_multi \ if sup_multi is not None else BoolParam(-1.0) # never true self.sup_multi_commods = sup_multi_commods \ if sup_multi_commods is not None else Param(0) self.n_sup_constr = n_sup_constr \ if n_sup_constr is not None else Param(1) self.sup_constr_val = sup_constr_val \ if sup_constr_val is not None else SupConstrParam(1.0) self.connection = connection \ if connection is not None else BoolParam(1.0) self.constr_coeff = constr_coeff \ if constr_coeff is not None else CoeffParam(1e-10, 2.0) # 1e-10 is 'sufficiently' low self.pref_coeff = pref_coeff \ if pref_coeff is not None else CoeffParam(1e-10, 1.0) def __eq__(self, other): for k, exp in self.__dict__.items(): if k not in other.__dict__: return False # this is a hack because for some reason for CoeffParams, a == b is # true and a != b is true, but I can't get corresponding behavior in # ipython. weird. if not exp == other.__dict__[k]: return False return True def __str__(self): ret = ["{0} = {1}".format(k, getattr(self, k).__str__()) \ for k, v in self.__dict__.items()] return "RandomRequestPoint\n{0}".format("\n".join(ret)) def _dt_convert(self, obj): """Converts a python object to its numpy dtype. Nones are converted to strings, and all strings are set to size == 32. """ if obj == None or isinstance(obj, basestring): return np.dtype('|S32') if isinstance(obj, collections.Sequence): return np.dtype('|S32') else: return np.dtype(type(obj)) def _convert_seq(self, val): """converts a string representation of a sequence into a list""" return [float(i) for i in \ re.sub('[\]\[,]', '', str(val)).split()] def _is_seq_not_str(self, attr): return isinstance(attr, collections.Sequence) \ and not isinstance(attr, basestring)
[docs] def dtype(self): """Returns a numpy dtype describing all composed objects.""" ret = [('paramid', ('str', 16)), ('family', ('str', 30))] # uuid for name, obj in self.__dict__.items(): for subname, subobj in obj.__dict__.items(): if subname.startswith('_'): continue ret.append(("{0}_{1}".format(name, subname), self._dt_convert(getattr(obj, subname)))) return np.dtype(ret)
def export_h5(self, uuid, fam_name): ret = [uuid.bytes, fam_name] for name, obj in self.__dict__.items(): for subname, subobj in obj.__dict__.items(): if subname.startswith('_'): continue attr = getattr(obj, subname) ret.append(str(attr) if self._is_seq_not_str(attr) else attr) return tuple(ret)
[docs] def valid(self): """Provides a best-guess estimate as to whether or not a given data point, as represented by this Sampler, is valid in the domain-defined parameter space. Returns ------- bool whether the sampler's parameters form a valid point in the sampler's parameter space """ conditions = [] # there must be at least as many suppliers as commodities conditions.append(self.n_commods.avg <= self.n_supply.avg) # there must be at least as many requesters as commodities conditions.append(self.n_commods.avg <= \ (1 + self.req_multi_commods.avg) * self.n_request.avg) # there must be at least as many commodities as possible commodities # requestable conditions.append(self.n_commods.avg > self.req_multi_commods.avg) # there must be at least as many commodities as possible commodities # suppliable conditions.append(self.n_commods.avg > self.sup_multi_commods.avg) return all(c for c in conditions)
[docs]class RandomRequestBuilder(object): """A helper class to translate sampling parameters for a reactor request scenario into an instance of GraphParams used by the cyclopts.execute module. The params member can be populated in a single step by the populate() member function, or the various supply/request parameters can be generated by the generate_request and generate_supply member functions and then the params member can be populated with the appropriate arguments to the populate_params member function. This builder can be incorporated with other builders by providing the appropriate offsets. """ def __init__(self, sampler, commod_offset = 0, req_g_offset = 0, sup_g_offset = 0, req_n_offset = 0, sup_n_offset = 0, arc_offset = 0, *args, **kwargs): """Parameters ---------- sampler : ReactorRequestSampler a parameter sampling container params : GraphParams, optional a pre-populated ExecParam object to populate commod_offset : int, optional an offset for commodity ids req_g_offset : int, optional an offset for request group ids sup_g_offset : int, optional an offset for supply group ids req_n_offset : int, optional an offset for request node ids sup_n_offset : int, optional an offset for supply node ids arc_offset : int, optional an offset for arc ids """ self.sampler = sampler self.commod_offset = commod_offset self.req_g_offset = req_g_offset self.sup_g_offset = sup_g_offset self.req_n_offset = req_n_offset self.sup_n_offset = sup_n_offset self.arc_offset = arc_offset
[docs] def valid(self): """Screens the provided sampler to determine if it provides a valid point in solution space (e.g., if parameter C requires that the sum of parameters A and B be less than some value, valid() will return false if that condition is not met). Returns ------- b : bool whether the sampler's parameters form a valid point in the sampler's parameter space """ return len(self.commods) <= len(self.suppliers)
[docs] def generate_request(self, commods, requesters, *args, **kwargs): """Returns all requests as a dictionary of requester ids to a list of assembly requests, where each assembly request is a list of id-commodity two-tuples that can satisfy such a request. Parameters ---------- commods : set the commodities requesters : list the requesters """ s = self.sampler n_ids = self.req_n_ids = tools.Incrementer(self.req_n_offset) requests = collections.defaultdict(list) chosen_commods = set() for g_id in requesters: assems = s.assem_per_req.sample() assem_commods = self._assem_commods(commods, chosen_commods) # modeling assumption # add nodes for i in range(assems): n_nodes = len(assem_commods) if s.assem_multi_commod.sample() \ else 1 mutual_reqs = [] for j in range(n_nodes): commod = assem_commods[j] n_id = n_ids.next() mutual_reqs.append((n_id, commod)) chosen_commods.add(commod) self.reqs_to_commods[n_id] = commod self.commods_to_reqs[commod] = n_id requests[g_id].append(mutual_reqs) return requests
[docs] def generate_supply(self, commods, suppliers, *args, **kwargs): """Returns a mapping from supplier to a list of 2-tuples of supply node id and request node id and a mapping from supply group id to a list of commodities supplied by the supplier. Parameters ---------- commods : set the commodities suppliers : list the suppliers """ # modeling assumption commod_assign = self._assign_supply_commods(commods, suppliers) # modeling assumption supply = self._select_supply(commod_assign) return supply, commod_assign
[docs] def populate_params(self, request, supply, supplier_commods): """Populates params (the GraphParams structure) given known supply and request Parameters ---------- request : as returned by generate_request supply : as returned by generate_supply supplier_commods : commods supplied by suppliers, as returned by generate_supply """ s = self.sampler n_node_ucaps = {} # number of capacities per node req_qtys = {} # req node id to req qty self.groups = [] self.nodes = [] self.arcs = [] req = True bid = False # populate request params for g_id, multi_reqs in request.items(): total_grp_qty = self._req_grp_qty(multi_reqs) n_constr = s.n_req_constr.sample() # add qty as first constraint -- required for clp/cbc caps = np.append(total_grp_qty, self._req_constr_vals(n_constr, multi_reqs)) grp = exinst.ExGroup(g_id, req, total_grp_qty) for cap in caps: grp.AddCap(cap) self.groups.append(grp) exid = tools.Incrementer() for reqs in multi_reqs: # mutually satisfying requests for n_id, commod in reqs: n_node_ucaps[n_id] = n_constr req_qty = self._req_node_qty() excl = s.exclusive.sample() # exclusive or not excl_id = exid.next() if excl else -1 # need unique exclusive id self.nodes.append( exinst.ExNode(n_id, g_id, req, req_qty, excl, excl_id)) req_qtys[n_id] = req_qty # populate supply params and arc relations a_ids = tools.Incrementer(self.arc_offset) commod_demand = self._commod_demand() supplier_capacity = \ self._supplier_capacity(commod_demand, supplier_commods) for g_id, sups in supply.items(): caps = self._sup_constr_vals(supplier_capacity[g_id], s.n_sup_constr.sample()) grp = exinst.ExGroup(g_id, bid) for cap in caps: grp.AddCap(cap) self.groups.append(grp) for v_id, u_id in sups: req_qty = req_qtys[u_id] n_node_ucaps[v_id] = len(caps) self.nodes.append(exinst.ExNode(v_id, g_id, bid, req_qty)) # arc from u-v node # add qty as first constraint -- required for clp/cbc ucaps = np.append( [self._req_def_constr(req_qty)], [s.constr_coeff.sample() for i in range(n_node_ucaps[u_id])]) vcaps = [s.constr_coeff.sample() for i in range(n_node_ucaps[v_id])] self.arcs.append( exinst.ExArc(a_ids.next(), u_id, ucaps, v_id, vcaps, s.pref_coeff.sample())) return self.groups, self.nodes, self.arcs
[docs] def build(self, *args, **kwargs): """Builds Group, Node, and Arc components of a Reactor-Request based Resource Exchange """ s = self.sampler commods = self.commods = \ set(range(self.commod_offset, self.commod_offset + s.n_commods.sample())) self.n_request = s.n_request.sample() # number of request groups self.n_supply = s.n_supply.sample() # number of supply groups # the request quantity per assembly (i.e., kg of fuel per assembly request) self.req_qty = s.req_qty.sample() req_g_ids = tools.Incrementer(self.req_g_offset) # request groups requesters = self.requesters = \ [req_g_ids.next() for i in range(self.n_request)] # include request values because ids are global sup_g_ids = \ tools.Incrementer(self.req_g_offset + self.sup_g_offset + self.n_request) # supply groups suppliers = self.suppliers = \ [sup_g_ids.next() for i in range(self.n_supply)] self.arc_ids = tools.Incrementer(self.arc_offset) self.reqs_to_commods = {} # requests to their commodities self.commods_to_reqs = {} # commodities to all requests self.sups_to_commods = {} # supply to their commodities request = self.generate_request(commods, requesters) supply, supplier_commods = self.generate_supply(commods, suppliers) requested_commods = set(v for k, v in self.reqs_to_commods.items()) supplied_commods = set(v for k, v in self.sups_to_commods.items()) if (requested_commods != set(commods)): raise ValueError(("All commmodities are not requested; " "Commodities: {0}, " "requested commodities {1}.".format( set(commods), requested_commods))) if (supplied_commods != set(commods)): raise ValueError(("All commmodities are not supplied; " "Commodities: {0}, " "supplied commodities {1}.".format( set(commods), supplied_commods))) return self.populate_params(request, supply, supplier_commods) # # Encapsulated assumptions #
def _assem_commods(self, commods, chosen_commods = set()): """Returns a list of commodities that satisfy assembly requests with the primary commodity as the first element. This is a basic modeling assumption for reactor request exchange building. Parameters ---------- chosen_commods : set a set of commodities that have already been chosen; this set is used to guarantee that there is at least one requester for each commodity commods : set the commodities """ s = self.sampler left = cp.deepcopy(commods) # choose a new commodity if we need to, otherwise choose a random one if len(chosen_commods) != len(commods): pool = commods.difference(chosen_commods) first_commod = rnd.sample(pool, 1)[0] else: first_commod = rnd.sample(left, 1)[0] assem_commods = [first_commod] left.remove(first_commod) # other commodities for assemblies that can be satisfied by # multiple commodities assem_commods += rnd.sample(left, s.req_multi_commods.sample()) return assem_commods def _assign_supply_commods(self, commods, suppliers): """Returns a mapping from supplier to a list commodities it supplies. This is a basic modeling assumption for reactor request exchange building. Parameters ---------- commods : set the commodities suppliers : list the suppliers """ if len(commods) > len(suppliers): raise ValueError("There must be at least as many suppliers as commodities.") s = self.sampler c_list = list(commods) rnd.shuffle(c_list) # get a random ordering assign = {} i = 0 # give each supplier a primary commodity, guaranteeing that all # commodities have at least one supplier, and some number of randomly # sampled secondary commodities if applicable for sup in suppliers: primary = c_list[i % len(commods)] i += 1 n_extra = s.sup_multi_commods.sample() \ if s.sup_multi.sample() else 0 pool = cp.copy(commods) pool.remove(primary) secondaries = rnd.sample(pool, n_extra) assign[sup] = [primary] + secondaries return assign def _select_supply(self, commod_assign): """Returns a mapping from supplier to a list of 2-tuples of supply node id and request node id. This is a basic modeling assumption for sreactor request exchange building. Parameters ---------- commod_assign : as returned by _assign_supply_commods """ s = self.sampler # include request values because ids are global n_ids = self.sup_n_ids = tools.Incrementer(self.req_n_offset + self.sup_n_offset + len(self.reqs_to_commods)) supply = collections.defaultdict(list) # supplier group id to arcs possible_supply = collections.defaultdict(list) # req to supplier group id for g_id, g_commods in commod_assign.items(): for req, commod in self.reqs_to_commods.items(): if commod in g_commods: possible_supply[req].append(g_id) for req, g_ids in possible_supply.items(): rnd.shuffle(g_ids) # guarantees all reqs are connected to at least 1 supplier s_id = n_ids.next() supply[g_ids[0]].append((s_id, req)) self.sups_to_commods[s_id] = self.reqs_to_commods[req] for i in range(1, len(g_ids)): if s.connection.sample(): s_id = n_ids.next() supply[g_ids[i]].append((s_id, req)) self.sups_to_commods[s_id] = self.reqs_to_commods[req] return supply def _req_grp_qty(self, reqs): """Returns the request quantity for a request group. Parameters ---------- reqs : list the requests associated with the group """ # assumes 1 assembly == 1 mass unit, all constr vals equivalent. note # that reqs = [ [satisfying commods] ], so one entry per actual request return self.req_qty * len(reqs) def _req_constr_vals(self, n_constr, reqs): """Returns a list of rhs constraint values for a request group. Parameters ---------- n_constr : int the number of constraints for the group reqs : list the requests associated with the group """ # note that reqs = [ [satisfying commods] ], so one entry per actual # request constr_val = len(reqs) return np.array([constr_val for i in range(n_constr)]) def _req_node_qty(self): """Returns the quantity for an individual request.""" # change these if all assemblies have mass != 1 return self.req_qty def _req_def_constr(self, req_qty): """Returns the default unit constraint value for a request. Parameters ---------- req_qty : float the request quantity """ # change these if all assemblies have mass != 1 # --- # ^ I don't actually think so, this looks like the unit capacity, which # should always be 1 if all requests have the same mass return 1 def _commod_demand(self): """Returns a mapping from commodities to the total demand of all nodes for that commodity. """ commod_demand = collections.defaultdict(float) for req, commod in self.reqs_to_commods.items(): commod_demand[commod] += 1 # need to change if assembly size != 1 return commod_demand def _supplier_capacity(self, commod_demand, supplier_commods): """Returns a mapping from suppliers to their maximum supply capacity. Parameters ---------- commod_demand : dict a mapping of commodity to total demand over all request nodes supplier_commods : dict a mapping of suppliers to the commodities they supply Details ------- The maximum supply capacity is calculated as the maximum demand for all commodities that can be supplied. """ factor = self.req_qty return {sup: factor * max([commod_demand[c] for c in commods]) \ for sup, commods in supplier_commods.items()} def _sup_constr_vals(self, capacity, n_constr): """Returns a list of rhs constraint values for a supplier. Parameters ---------- capacity : float the baseline supplier capacity n_constr : int the number of constraints """ # assumes that all supplier constraint values are based of a baseline # constraint value s = self.sampler return np.array([s.sup_constr_val.sample() * capacity \ for i in range(n_constr)])
[docs]class RandomRequest(ProblemSpecies): """A problem species for random (non-fuel cycle specific) reactor request exchanges.""" def __init__(self): super(RandomRequest, self).__init__() self._params_it = None self._n_points = None self.tbl_name = 'RandomRequestParameters' def _get_param_dict(self, rc_dict): """Provides a dictionary of parameter names to all constructor arguments for a resource exchange range of instances. Parameters ---------- rc_dict : dict A dictionary of attributes, e.g. from a RunControl object Returns ------- params_dict : dict A dictionary whose keys are parameter names and values are lists of ranges of constructor arguments. """ params_dict = {} s = RandomRequestPoint() for k, v in rc_dict.items(): name = k attr = v if hasattr(s, name): vals = [] args = PARAM_CTOR_ARGS[type(getattr(s, name))] for arg in args: if arg in attr: vals += [attr[arg]] if len(vals) > 0: params_dict[name] = vals else: print("Found an entry named {0} that " "is unknown to the parser.".format(k)) return params_dict def _param_gen(self, params_dict): """Returns input for _add_subtree() given input for build()""" params_dict = {k: [i for i in itertools.product(*v)] \ for k, v in params_dict.items()} s = RandomRequestPoint() for k, v in params_dict.items(): yield k, [type(getattr(s, k))(*args) for args in v] @property
[docs] def family(self): """Returns ------- family : ResourceExchange An instance of this species' family """ return ResourceExchange()
@property
[docs] def name(self): """Returns ------- name : string The name of this species """ return 'RandomRequest'
[docs] def register_tables(self, h5file, prefix): """Parameters ---------- h5file : PyTables File the hdf5 file prefix : string the absolute path to the group for tables of this species Returns ------- tables : list of cyclopts_io.Tables All tables that could be written to by this species. """ return [cycio.Table(h5file, '/'.join([prefix, self.tbl_name]), RandomRequestPoint().dtype())]
[docs] def read_space(self, space_dict): """Parameters ---------- space_dict : dict A dictionary container resulting from the reading in of a run control file """ pdict = self._get_param_dict(space_dict) self._n_points = reduce(operator.mul, (len(l) for _, val in pdict.iteritems() \ for l in val), 1) self._params_it = self._param_gen(pdict)
@property
[docs] def n_points(self): """Returns ------- n : int The total number of points in the parameter space """ return self._n_points
[docs] def points(self): """Returns ------- point : RandomRequestPoint A representation of a point in parameter space to be used by this species """ pairings = [[(name, param) for param in params] \ for name, params in self._params_it] for prod in itertools.product(*pairings): point = RandomRequestPoint() for name, param in prod: setattr(point, name, param) if point.valid(): yield point
[docs] def record_point(self, point, param_uuid, tables): """Parameters ---------- point : RandomRequestPoint A representation of a point in parameter space param_uuid : uuid The uuid of the point in parameter space tables : list of cyclopts_io.Table The tables that can be written to """ tables[self.tbl_name].append_data( [point.export_h5(param_uuid, self.family.name)])
[docs] def gen_inst(self, point): """Parameters ---------- point : RandomRequestPoint A representation of a point in parameter space Returns ------- inst : tuple of lists of ExGroups, ExNodes, and ExArgs A representation of a problem instance to be used by this species' family """ builder = RandomRequestBuilder(point) builder.build() return builder.groups, builder.nodes, builder.arcs