Skip to content

Commit

Permalink
Merge pull request #168 from LarrySnyder/sets
Browse files Browse the repository at this point in the history
Sets
  • Loading branch information
LarrySnyder authored May 29, 2024
2 parents 88a1e2b + 63aef50 commit da7de61
Show file tree
Hide file tree
Showing 51 changed files with 67,120 additions and 900 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Option to suppress dummy-product indices in simulation output.
- ``SupplyChainNetwork.nodes_by_index`` dict, which returns a ``SupplyChainNode`` object for a given index.
Use this in place of ``SupplyChainNetwork.get_node_from_index()``, which will be deprecated.

### Changed
- Various speedups.

### Fixed
- Bug that caused ``mwor_system()`` to crash when ``demand_source`` is provided as argument
Expand Down
48 changes: 24 additions & 24 deletions datasets/stockpyl_instances 2.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions docs/tutorial/tutorial_meio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ You can then add these nodes to a |class_network|:

The network now contains both nodes, and a (directed) edge from ``my_node`` to ``my_other_node``. Nodes can
be accessed either from a |class_network| object's :attr:`~stockpyl.supply_chain_network.SupplyChainNetwork.nodes`
attribute (a list of nodes), or using its member function :meth:`~stockpyl.supply_chain_network.SupplyChainNetwork.get_node_from_index`:
attribute (a list of nodes), or using its :attr:`~stockpyl.supply_chain_network.SupplyChainNetwork.nodes_by_index`
attribute (a dict whose keys are indices and whose values are node objects):

.. doctest::

Expand All @@ -58,7 +59,7 @@ attribute (a list of nodes), or using its member function :meth:`~stockpyl.suppl
1
>>> n1.local_holding_cost
3
>>> n2 = network.get_node_from_index(2)
>>> n2 = network.nodes_by_index[2]
>>> n2.index
2
>>> n2.echelon_holding_cost
Expand Down
22 changes: 11 additions & 11 deletions docs/tutorial/tutorial_sim.rst
Original file line number Diff line number Diff line change
Expand Up @@ -516,25 +516,25 @@ methods are the relevant nodes/products, but these arguments can be omitted if t

.. doctest::

>>> network.get_node_from_index(1).state_vars[3].get_inventory_level()
>>> network.nodes_by_index[1].state_vars[3].get_inventory_level()
15
>>> network.get_node_from_index(1).state_vars[3].get_order_quantity(predecessor=3)
>>> network.nodes_by_index[1].state_vars[3].get_order_quantity(predecessor=3)
6
>>> network.get_node_from_index(2).state_vars[4].get_inbound_shipment()
>>> network.nodes_by_index[2].state_vars[4].get_inbound_shipment()
13
>>> network.get_node_from_index(2).state_vars[6].get_inbound_order()
>>> network.nodes_by_index[2].state_vars[6].get_inbound_order()
8
>>> network.get_node_from_index(3).state_vars[6].get_inbound_order(successor=2)
>>> network.nodes_by_index[3].state_vars[6].get_inbound_order(successor=2)
14

Some state variables, in particular those that are not indexed by any node or product,
are accessed by using the attribute directly:

.. doctest::

>>> network.get_node_from_index(1).state_vars[3].holding_cost
>>> network.nodes_by_index[1].state_vars[3].holding_cost
30
>>> network.get_node_from_index(3).state_vars[5].total_cost
>>> network.nodes_by_index[3].state_vars[5].total_cost
46.0


Expand Down Expand Up @@ -599,7 +599,7 @@ disruptions. First, type-OP disruptions:
... policy_type='BS',
... base_stock_level=[25, 25]
... )
>>> network.get_node_from_index(2).disruption_process = DisruptionProcess(
>>> network.nodes_by_index[2].disruption_process = DisruptionProcess(
... random_process_type='M',
... disruption_type='OP',
... disruption_probability=0.1,
Expand Down Expand Up @@ -629,7 +629,7 @@ Next, type-SP disruptions:

.. doctest::

>>> network.get_node_from_index(2).disruption_process.disruption_type='SP'
>>> network.nodes_by_index[2].disruption_process.disruption_type='SP'
>>> _ = simulation(network=network, num_periods=T, rand_seed=42, progress_bar=False)
>>> write_results(network=network, num_periods=T, periods_to_print=list(range(7, 16)), columns_to_print=['DISR', 'IO', 'OQ', 'IS', 'OS', 'IL', 'ODI'])
t | i=1 DISR IO:2|-2 OQ:EXT|-3 IS:EXT|-3 OS:2|-2 IL:-2 ODI:2|-2 | i=2 DISR IO:EXT|-4 OQ:1|-2 IS:1|-2 OS:EXT|-4 IL:-4 ODI:EXT|-4
Expand All @@ -651,7 +651,7 @@ Next, type-TP disruptions:

.. doctest::

>>> network.get_node_from_index(2).disruption_process.disruption_type='TP'
>>> network.nodes_by_index[2].disruption_process.disruption_type='TP'
>>> _ = simulation(network=network, num_periods=T, rand_seed=42, progress_bar=False)
>>> write_results(network=network, num_periods=T, periods_to_print=list(range(7, 16)), columns_to_print=['DISR', 'IO', 'OQ', 'IS', 'ISPL', 'OS', 'IL'])
t | i=1 DISR IO:2|-2 OQ:EXT|-3 IS:EXT|-3 ISPL:EXT|-3 OS:2|-2 IL:-2 | i=2 DISR IO:EXT|-4 OQ:1|-2 IS:1|-2 ISPL:1|-2 OS:EXT|-4 IL:-4
Expand All @@ -673,7 +673,7 @@ Finally, type-RP disruptions:

.. doctest::

>>> network.get_node_from_index(2).disruption_process.disruption_type='RP'
>>> network.nodes_by_index[2].disruption_process.disruption_type='RP'
>>> _ = simulation(network=network, num_periods=T, rand_seed=42, progress_bar=False)
>>> write_results(network=network, num_periods=T, periods_to_print=list(range(7, 16)), columns_to_print=['DISR', 'IO', 'OQ', 'IS', 'OS', 'IL', 'ISPL', 'IDI'])
t | i=1 DISR IO:2|-2 OQ:EXT|-3 IS:EXT|-3 ISPL:EXT|-3 IDI:EXT|-3 OS:2|-2 IL:-2 | i=2 DISR IO:EXT|-4 OQ:1|-2 IS:1|-2 ISPL:1|-2 IDI:1|-2 OS:EXT|-4 IL:-4
Expand Down
2 changes: 1 addition & 1 deletion src/stockpyl/aux_files/stockpyl_instances_metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@
base_stock_level=[7, 13, 11],
initial_inventory_level=[7, 13, 11]
)
instance.get_node_from_index(0).demand_source.round_to_int = True
instance.nodes_by_index[0].demand_source.round_to_int = True
.. collapse:: Assembly system from Figure 1 in Rosling (1989)
Expand Down
19,985 changes: 19,984 additions & 1 deletion src/stockpyl/datasets/stockpyl_instances.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions src/stockpyl/gsm_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ def inbound_cst(tree, node_index, cst):
for k in node_index:
# Determine inbound CST (= max of CST for all predecessors, and external
# inbound CST).
k_node = tree.get_node_from_index(k)
k_node = tree.nodes_by_index[k]
SI[k] = k_node.external_inbound_cst
if len(k_node.predecessors()) > 0:
if len(k_node.predecessor_indices()) > 0:
SI[k] = max(SI[k], np.max([cst[i] for i in k_node.predecessor_indices()]))

if n_is_iterable:
Expand Down Expand Up @@ -252,7 +252,7 @@ def net_lead_time(tree, node_index, cst):
nlt = {}
for k in node_index:
# Determine NLT.
nlt[k] = SI[k] + tree.get_node_from_index(k).processing_time - cst[k]
nlt[k] = SI[k] + tree.nodes_by_index[k].processing_time - cst[k]

if n_is_iterable:
return nlt
Expand Down Expand Up @@ -309,7 +309,7 @@ def cst_to_base_stock_levels(tree, node_index, cst):

base_stock_level = {}
for k in node_index:
base_stock_level[k] = tree.get_node_from_index(k).net_demand_mean * nlt[k] + ss[k]
base_stock_level[k] = tree.nodes_by_index[k].net_demand_mean * nlt[k] + ss[k]

if n_is_iterable:
return base_stock_level
Expand Down Expand Up @@ -365,7 +365,7 @@ def safety_stock_levels(tree, node_index, cst):

safety_stock_level = {}
for k in node_index:
node_k = tree.get_node_from_index(k)
node_k = tree.nodes_by_index[k]
safety_stock_level[k] = node_k.demand_bound_constant * \
node_k.net_demand_standard_deviation * \
math.sqrt(nlt[k])
Expand Down
12 changes: 6 additions & 6 deletions src/stockpyl/gsm_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,11 @@ def _cst_dp_serial(network):
# Calculate max replenishment times (max replenishment time for node k = SI_N + sum
# of processing times at nodes k, ..., N).
for k_index in range(num_nodes, 0, -1):
k = network.get_node_from_index(k_index)
k = network.nodes_by_index[k_index]
if k_index == num_nodes:
k.max_replenishment_time = k.external_inbound_cst + k.processing_time
else:
upstream = network.get_node_from_index(k_index + 1)
upstream = network.nodes_by_index[k_index + 1]
k.max_replenishment_time = upstream.max_replenishment_time + k.processing_time

# Initialize best_cst_adjacent.
Expand All @@ -229,13 +229,13 @@ def _cst_dp_serial(network):
best_S = {k_index: {} for k_index in network.node_indices}

# Get shortcuts to some parameters (for conveience).
sigma = network.get_node_from_index(1).demand_source.standard_deviation
sigma = network.nodes_by_index[1].demand_source.standard_deviation

# Loop through stages.
for k_index in range(1, num_nodes + 1):

# Get shortcuts to node (for convenience).
k = network.get_node_from_index(k_index)
k = network.nodes_by_index[k_index]

# Determine range of SI values to check. (For node N, it's only external_inbound_cst;
# for all other nodes, it's 0, ..., max_replenishment_time - T.)
Expand Down Expand Up @@ -290,7 +290,7 @@ def _cst_dp_serial(network):
for k_index in range(num_nodes, 0, -1):

# Get node k.
k = network.get_node_from_index(k_index)
k = network.nodes_by_index[k_index]

# Determine SI.
if k_index == num_nodes:
Expand All @@ -302,7 +302,7 @@ def _cst_dp_serial(network):
opt_cst[k_index] = best_S[k_index][SI]

# Get optimal cost.
opt_cost = theta[num_nodes][network.get_node_from_index(num_nodes).external_inbound_cst]
opt_cost = theta[num_nodes][network.nodes_by_index[num_nodes].external_inbound_cst]

return opt_cst, opt_cost

20 changes: 11 additions & 9 deletions src/stockpyl/gsm_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def _cst_dp_tree(tree):
for k_index in range(min_k_index, max_k_index + 1):

# Get shortcuts to some parameters (for convenience).
k = tree.get_node_from_index(k_index)
k = tree.nodes_by_index[k_index]
max_replen_time = k.max_replenishment_time
proc_time = k.processing_time

Expand Down Expand Up @@ -225,7 +225,7 @@ def _cst_dp_tree(tree):
best_cst_adjacent[k_index][max_replen_time - proc_time]

# Determine best value of SI for final stage.
max_k_node = tree.get_node_from_index(max_k_index)
max_k_node = tree.nodes_by_index[max_k_index]
SI_dict = {SI: theta_in[max_k_index][SI] for SI in
range(max_k_node.max_replenishment_time -
max_k_node.processing_time + 1)} # smaller range of SI
Expand All @@ -245,15 +245,15 @@ def _cst_dp_tree(tree):
for k_index in range(max_k_index, min_k_index-1, -1):

# Get node k.
k = tree.get_node_from_index(k_index)
k = tree.nodes_by_index[k_index]

# Get p(k_index), and determine whether p(k_index) and p(p(k_index)) are upstream or
# downstream (for convenience).
if k_index < max_k_index:
pk = k.larger_adjacent_node
pk_is_downstream = k.larger_adjacent_node_is_downstream
if pk < max_k_index:
ppk_is_downstream = tree.get_node_from_index(pk).larger_adjacent_node_is_downstream
ppk_is_downstream = tree.nodes_by_index[pk].larger_adjacent_node_is_downstream

# Where is p(k_index)?
if k_index == max_k_index:
Expand Down Expand Up @@ -346,7 +346,7 @@ def _calculate_theta_out(tree, k_index, S, theta_in_partial, theta_out_partial):
"""

# Get node k_index, for convenience.
k = tree.get_node_from_index(k_index)
k = tree.nodes_by_index[k_index]

# Initialize min_c.
min_c = float('inf')
Expand Down Expand Up @@ -444,7 +444,7 @@ def _calculate_theta_in(tree, k_index, SI, theta_in_partial, theta_out_partial):
"""

# Get node k_index, for convenience.
k = tree.get_node_from_index(k_index)
k = tree.nodes_by_index[k_index]

# Initialize min_c.
min_c = float('inf')
Expand Down Expand Up @@ -532,7 +532,7 @@ def _calculate_c(tree, k_index, S, SI, theta_in_partial, theta_out_partial):
"""

# Get node k, for convenience.
k = tree.get_node_from_index(k_index)
k = tree.nodes_by_index[k_index]

# Initialize output dicts.
best_upstream_S = {}
Expand Down Expand Up @@ -721,7 +721,7 @@ def relabel_nodes(tree, start_index=0, force_relabel=False):
# Fill attributes of relabeled tree.
larger_adjacent, downstream = _find_larger_adjacent_nodes(relabeled_tree)
for k in tree.nodes:
relabeled_node = relabeled_tree.get_node_from_index(new_labels[k.index])
relabeled_node = relabeled_tree.nodes_by_index[new_labels[k.index]]
relabeled_node.original_label = k.index
if new_labels[k.index] < np.max(list(new_labels.values())):
relabeled_node.larger_adjacent_node = larger_adjacent[relabeled_node.index]
Expand Down Expand Up @@ -900,6 +900,8 @@ def _net_demand(tree):
# Make temp copy of tree.
temp_tree = copy.deepcopy(tree)

# TODO: add check that the node list gets smaller each iteration -- otherwise there's a bug, but finding it can be hard because of the infinite loop

# Loop through temp_tree. At each iteration, handle leaf nodes (nodes with
# no _successors), adding their net_means and net_variances to those of their
# _predecessors. Then remove the leaf nodes and iterate.
Expand Down Expand Up @@ -984,7 +986,7 @@ def gsm_to_ssm(tree, p=None):
SSM_tree.add_node(SupplyChainNode(n.index, name=n.name, network=SSM_tree,
shipment_lead_time=n.processing_time+n.external_inbound_cst,
echelon_holding_cost=n.local_holding_cost-upstream_h))
SSM_node = SSM_tree.get_node_from_index(n.index)
SSM_node = SSM_tree.nodes_by_index[n.index]
SSM_node.demand_source = copy.deepcopy(n.demand_source)
if p is not None:
if n.demand_source is not None:
Expand Down
52 changes: 52 additions & 0 deletions src/stockpyl/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1319,3 +1319,55 @@ def round_dict_values(the_dict, round_type=None):

return new_dict


### JSON-RELATED FUNCTIONS ###

def serialize_set(obj):
"""Serialize a set by converting it to a dict of the form
``{'type': 'set', 'elements': elements}``, where ``elements`` are
the elements of the set.
This is used for serializing objects so they can be saved in JSON format.
To use: ``json.dump(json_contents, file, default=serialize_set)``.
Parameters
----------
obj : Any
The object to serialize.
Returns
-------
dict
Dictionary representation of ``obj``, if ``obj`` is a set.
"""
if is_set(obj):
return {
'type': 'set',
'elements': list(obj)
}


def deserialize_set(obj):
"""Deserialize a set of the form ``{'type': 'set', 'elements': elements}``
by converting it to a set of the form ``{elements}``.
This is used for deserializing objects to they can be loaded from JSON format.
To use: ``json.load(file, object_hook=deserialize_set)``.
Parameters
----------
obj : Any
The object to deserialize.
Returns
-------
set
Set representation of ``obj``, if ``obj`` is a dict of the appropriate form.
"""
# https://realpython.com/python-serialize-data/
if is_dict(obj) and 'type' in obj and obj['type'] == 'set' and 'elements' in obj:
return set(obj['elements'])
else:
return obj


7 changes: 4 additions & 3 deletions src/stockpyl/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

from stockpyl.supply_chain_network import *
from stockpyl.supply_chain_node import *
from stockpyl.helpers import is_set, is_dict, serialize_set, deserialize_set


def load_instance(instance_name, filepath=None, ignore_state_vars=True):
"""Load an instance from a JSON file.
Expand Down Expand Up @@ -93,7 +95,7 @@ def load_instance(instance_name, filepath=None, ignore_state_vars=True):

# Load data from JSON.
with open(new_path) as f:
json_contents = json.load(f)
json_contents = json.load(f, object_hook=deserialize_set)

# Look for instance. (https://stackoverflow.com/a/8653568/3453768)
instance_index = next((i for i, item in enumerate(json_contents["instances"]) \
Expand Down Expand Up @@ -252,13 +254,12 @@ def save_instance(instance_name, instance_data, instance_description='', filepat

# Write all instances to JSON.
with open(filepath, 'w') as f:
json.dump(json_contents, f)
json.dump(json_contents, f, default=serialize_set)

# Close file.
f.close()



def _stockpyl_instances_json_path():
"""Determine the path to the JSON file containing the instances.
Expand Down
2 changes: 1 addition & 1 deletion src/stockpyl/meio_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def meio_by_coordinate_descent(network, initial_solution=None,
if nto_lo[n_ind] is None:
nto_lo[n_ind] = 0
if nto_hi[n_ind] is None:
n = network.get_node_from_index(n_ind)
n = network.nodes_by_index[n_ind]
nto_hi[n_ind] = 3 * n.lead_time * float(np.sum([s.demand_source.mean for s in network.sink_nodes]))

# Determine initial solution.
Expand Down
Loading

0 comments on commit da7de61

Please sign in to comment.