268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457 | def create_sc_net(nodes: list, links: list, demands: list, env:simpy.Environment = None):
"""
Takes the node, link, and demand descriptions, builds the matching supply
chain node, link, and demand objects, and gathers them all into one
dictionary that represents the network.
Each of ``nodes``, ``links``, and ``demands`` must be all one kind — either
all dicts (the description style) or all ready-made objects (``Node`` /
``Link`` / ``Demand`` instances). You cannot mix the two in a single list,
because the fresh ``simpy.Environment`` made for the dict items would not
match the one the ready-made objects were built with. If any list contains
ready-made objects, you must pass ``env`` yourself, and each object's own
``env`` must be that same environment.
Parameters:
nodes (list): The nodes (sites): dicts, or `Node` objects.
links (list): The links between them: dicts, or `Link` objects.
demands (list): The customer demands: dicts, or `Demand` objects.
env (simpy.Environment, optional): A SimPy Environment object. If not provided, a new environment will be created.
Attributes:
global_logger (GlobalLogger): The global logger instance used for logging messages.
supplychainnet (dict): A dictionary representing the supply chain network.
used_ids (set): The IDs used so far, to catch duplicates.
num_suppliers (int): Number of raw-material suppliers (finite and infinite).
num_manufacturers (int): Number of manufacturers.
num_distributors (int): Number of distributors and warehouses.
num_retailers (int): Number of retailers, stores and shops.
Raises:
ValueError: If the SimPy Environment object is not provided or if there are duplicate IDs in nodes, links, or demands.
ValueError: If any of ``nodes`` / ``links`` / ``demands`` mixes dicts and pre-built domain objects.
ValueError: If a pre-built object's ``env`` does not match the provided ``env``.
ValueError: If an invalid node type is encountered.
ValueError: If an invalid source or sink node is specified in a link.
ValueError: If a customer demand names a ``demand_node`` (the site its customers order from) that is not among the nodes.
Returns:
dict: A dictionary representing the supply chain network.
"""
# Reject lists that mix dicts and pre-built objects, right at the start. We
# check every element, not just the first one. An earlier version only
# looked at element 0, so a list like ``[dict, Node_instance, ...]`` slipped
# past the "env required" check (because ``nodes[0]`` was a dict) and then
# built the later object with a fresh env, throwing away the env that object
# was actually created with.
def _check_homogeneous(items, obj_cls, list_name):
has_dict = any(isinstance(x, dict) for x in items)
has_obj = any(isinstance(x, obj_cls) for x in items)
if has_dict and has_obj:
global_logger.error(
f"{list_name} list mixes dicts and {obj_cls.__name__} instances."
)
raise ValueError(
f"{list_name} list mixes dicts and {obj_cls.__name__} instances; "
f"use all dicts or all {obj_cls.__name__} instances."
)
_check_homogeneous(nodes, Node, "nodes")
_check_homogeneous(links, Link, "links")
_check_homogeneous(demands, Demand, "demands")
# ``env`` is required whenever ANY list contains a pre-built domain object,
# regardless of the element's position. Scanning each list (rather than
# indexing [0]) also avoids an IndexError on legitimately empty lists.
any_object = (
any(isinstance(x, Node) for x in nodes)
or any(isinstance(x, Link) for x in links)
or any(isinstance(x, Demand) for x in demands)
)
if any_object and env is None:
global_logger.error("Please provide SimPy Environment object env")
raise ValueError("A SimPy Environment object is required!")
if len(nodes)==0 or len(links)==0 or len(demands)==0:
global_logger.error("Nodes, links, and demands cannot be empty")
raise ValueError("Nodes, links, and demands cannot be empty")
if(env is None):
env = simpy.Environment()
# When the user supplied both ``env`` and pre-built objects, each object's
# own ``env`` must match — otherwise the returned supplychainnet would
# combine processes from two different environments and nothing would run
# consistently. The old code simply ignored the object's env; this check
# surfaces the mismatch loudly.
def _check_env_match(items, obj_cls, list_name):
for i, x in enumerate(items):
if isinstance(x, obj_cls) and getattr(x, "env", None) is not env:
global_logger.error(
f"{list_name}[{i}] ({getattr(x, 'ID', '<no ID>')}) was built against a "
f"different simpy.Environment than the one passed to create_sc_net."
)
raise ValueError(
f"{list_name}[{i}] env does not match the env passed to create_sc_net."
)
_check_env_match(nodes, Node, "nodes")
_check_env_match(links, Link, "links")
_check_env_match(demands, Demand, "demands")
supplychainnet = {"nodes":{},"links":{},"demands":{}} # create empty supply chain network
# Use a set, not a list: checking membership and adding/removing items are
# all fast (constant time), so building a network with many nodes stays
# fast. ``check_duplicate_id`` works with either a set or a list.
used_ids = set()
# Counts of each node category, keyed by the names used in
# ``_NODE_DISPATCH``. Using one dict (instead of four separate ``num_*``
# variables) lets us count with a single ``counters[category] += 1`` rather
# than a long if/else chain.
counters = {"num_suppliers": 0, "num_manufacturers": 0, "num_distributors": 0, "num_retailers": 0}
for node in nodes:
if isinstance(node, dict):
check_duplicate_id(used_ids, node["ID"], "node ID")
node_id = node["ID"]
try:
nt = NodeType(node["node_type"])
except ValueError:
used_ids.remove(node["ID"])
global_logger.error(f"Invalid node type {node['node_type']}")
raise ValueError("Invalid node type")
cls, counter_key, drop_node_type = _NODE_DISPATCH[nt]
# ``Manufacturer.__init__`` is the only constructor that does not
# accept ``node_type``, so for it we drop that key. The
# ``drop_node_type`` flag records this one difference.
kwargs = {k: v for k, v in node.items() if not (drop_node_type and k == "node_type")}
supplychainnet["nodes"][f"{node_id}"] = cls(env=env, **kwargs)
counters[counter_key] += 1
elif isinstance(node, Node):
check_duplicate_id(used_ids, node.ID, "node ID")
node_id = node.ID
supplychainnet["nodes"][f"{node_id}"] = node
try:
nt = NodeType(node.node_type)
except ValueError:
used_ids.remove(node.ID)
global_logger.error(f"Invalid node type {node.node_type}")
raise ValueError("Invalid node type")
counters[_NODE_DISPATCH[nt][1]] += 1
for link in links:
if isinstance(link, dict):
check_duplicate_id(used_ids, link["ID"], "link ID")
source = None
sink = None
node_ids = supplychainnet["nodes"].keys()
if(link["source"] in node_ids):
source_id = link["source"]
source = supplychainnet["nodes"][f"{source_id}"]
if(link["sink"] in node_ids):
sink_id = link["sink"]
sink = supplychainnet["nodes"][f"{sink_id}"]
if(source is None or sink is None):
global_logger.error(f"Invalid source or sink node {link['source']} {link['sink']}")
raise ValueError("Invalid source or sink node")
exclude_keys = {'source', 'sink'}
params = {k: v for k, v in link.items() if k not in exclude_keys}
link_id = params['ID']
supplychainnet["links"][f"{link_id}"] = Link(env=env,source=source,sink=sink,**params)
elif isinstance(link, Link):
check_duplicate_id(used_ids, link.ID, "link ID")
supplychainnet["links"][f"{link.ID}"] = link
# Every link is known now, so each site's supplier-selection policy can
# check its settings against the links (SelectProbabilistic: every link
# has a probability). Mistakes then show before the run, not at the first
# order. A manufacturer does not use its policy yet, so it is skipped.
for node in supplychainnet["nodes"].values():
check_links = getattr(getattr(node, "selection_policy", None), "check_links", None)
if check_links is not None and not isinstance(node, Manufacturer):
check_links()
for d in demands:
if isinstance(d, dict):
check_duplicate_id(used_ids, d["ID"], "demand ID")
demand_node = None # the site this customer demand's customers order from
node_ids = supplychainnet["nodes"].keys()
if d['demand_node'] in node_ids:
demand_node_id = d['demand_node']
demand_node = supplychainnet["nodes"][f"{demand_node_id}"]
if(demand_node is None):
global_logger.error(f"Invalid demand node {d['demand_node']}")
raise ValueError("Invalid demand node")
exclude_keys = {'demand_node','node_type'}
params = {k: v for k, v in d.items() if k not in exclude_keys}
demand_id = params['ID']
supplychainnet["demands"][f"{demand_id}"] = Demand(env=env,demand_node=demand_node,**params)
elif isinstance(d, Demand):
check_duplicate_id(used_ids, d.ID, "demand ID")
supplychainnet["demands"][f"{d.ID}"] = d
supplychainnet["env"] = env
supplychainnet["num_of_nodes"] = sum(counters.values())
supplychainnet["num_of_links"] = len(links)
supplychainnet.update(counters)
return supplychainnet
|