stokasta 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stokasta-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2025-2026 Ziang Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.5
2
+ Name: stokasta
3
+ Version: 0.0.1
4
+ Summary: Inventory-control problems as Gymnasium environments (0.0.1: one environment; first full release in preparation)
5
+ Project-URL: Homepage, https://github.com/stokasta
6
+ Author: Ziang Liu
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: gymnasium>=1.0
15
+ Requires-Dist: numpy>=2.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Stokasta
19
+
20
+ *Stokasta* (sto-KAS-ta) is Esperanto for *stochastic*.
21
+
22
+ Stokasta is a benchmark of inventory-control problems as Gymnasium
23
+ environments. Version 0.0.1 ships one of them: the single-item dynamic
24
+ inventory model with a fixed ordering cost and backorders, whose optimal
25
+ policy is (s, S) (Scarf 1960; Snyder and Shen 2019, Example 4.7).
26
+
27
+ ```python
28
+ import gymnasium as gym
29
+ import stokasta # registers the environments
30
+
31
+ env = gym.make("stokasta/DynamicInventory-v0")
32
+ obs, info = env.reset(seed=0)
33
+ obs, reward, terminated, truncated, info = env.step(10) # order 10 units
34
+ ```
35
+
36
+ The reward is the negative of the period's holding, backorder and fixed
37
+ ordering cost. The first full release, in preparation at
38
+ https://github.com/stokasta, adds the other models, their exact Markov
39
+ decision process export, and a problem set with reference costs.
@@ -0,0 +1,22 @@
1
+ # Stokasta
2
+
3
+ *Stokasta* (sto-KAS-ta) is Esperanto for *stochastic*.
4
+
5
+ Stokasta is a benchmark of inventory-control problems as Gymnasium
6
+ environments. Version 0.0.1 ships one of them: the single-item dynamic
7
+ inventory model with a fixed ordering cost and backorders, whose optimal
8
+ policy is (s, S) (Scarf 1960; Snyder and Shen 2019, Example 4.7).
9
+
10
+ ```python
11
+ import gymnasium as gym
12
+ import stokasta # registers the environments
13
+
14
+ env = gym.make("stokasta/DynamicInventory-v0")
15
+ obs, info = env.reset(seed=0)
16
+ obs, reward, terminated, truncated, info = env.step(10) # order 10 units
17
+ ```
18
+
19
+ The reward is the negative of the period's holding, backorder and fixed
20
+ ordering cost. The first full release, in preparation at
21
+ https://github.com/stokasta, adds the other models, their exact Markov
22
+ decision process export, and a problem set with reference costs.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "stokasta"
7
+ version = "0.0.1"
8
+ description = "Inventory-control problems as Gymnasium environments (0.0.1: one environment; first full release in preparation)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Ziang Liu" }]
14
+ dependencies = ["numpy>=2.0", "gymnasium>=1.0"]
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "Topic :: Scientific/Engineering :: Mathematics",
19
+ "Programming Language :: Python :: 3",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/stokasta"
@@ -0,0 +1,17 @@
1
+ """Stokasta: inventory-control problems as Gymnasium environments.
2
+
3
+ Version 0.0.1 ships one environment, registered as
4
+ ``stokasta/DynamicInventory-v0`` (``stokasta.envs.dynamic_inventory:
5
+ DynamicInventoryEnv``). The first full release adds the other models, their
6
+ exact Markov decision process export, and a problem set with reference costs.
7
+ """
8
+
9
+ from gymnasium.envs.registration import register
10
+
11
+ __version__ = "0.0.1"
12
+
13
+ register(
14
+ id="stokasta/DynamicInventory-v0",
15
+ entry_point="stokasta.envs.dynamic_inventory:DynamicInventoryEnv",
16
+ max_episode_steps=1000,
17
+ )
@@ -0,0 +1,3 @@
1
+ from stokasta.envs.dynamic_inventory import DynamicInventoryEnv
2
+
3
+ __all__ = ["DynamicInventoryEnv"]
@@ -0,0 +1,164 @@
1
+ """Dynamic inventory: one item, a fixed cost per order, zero lead time, backorders.
2
+
3
+ The textbook model whose optimal policy is ``(s, S)`` (Scarf 1960), in the
4
+ form of Snyder and Shen's (2019) Example 4.7. The state is the inventory
5
+ level, negative for a backlog; an order arrives before the period's demand,
6
+ and holding and backorder costs are charged on the level after it. The reward
7
+ is the negative of the period's cost.
8
+
9
+ References
10
+ ----------
11
+ L. V. Snyder and Z. J. M. Shen, Fundamentals of Supply Chain Theory. Wiley,
12
+ 2019, Example 4.7, p. 121.
13
+
14
+ H. Scarf, "The optimality of (s, S) policies in the dynamic inventory
15
+ problem," Mathematical Methods in the Social Sciences, pp. 196-202, 1960.
16
+ """
17
+
18
+ from typing import Any
19
+
20
+ import gymnasium as gym
21
+ import numpy as np
22
+ from gymnasium import spaces
23
+
24
+
25
+ class DynamicInventoryEnv(gym.Env):
26
+ """
27
+ Parameters
28
+ ----------
29
+ holding_cost: float, default=1
30
+ Cost per unit on hand at the end of a period.
31
+
32
+ shortage_cost: float, default=4
33
+ Cost per unit of backlog at the end of a period, charged every period
34
+ the unit stays owed -- a unit short for ``k`` periods costs ``k`` times
35
+ this, not once per unit of unmet demand.
36
+
37
+ ordering_cost: float, default=5
38
+ Fixed cost of an order, charged whenever the order quantity is
39
+ positive, whatever its size. There is no per-unit cost.
40
+
41
+ demand: dict, optional
42
+ Per-period demand, as ``{"type": "poisson", "mean": m}``. Defaults to
43
+ Poisson with mean 6. Version 0.0.1 supports Poisson demand only.
44
+
45
+ min_state: int, default=-20
46
+ Floor of the inventory level: the next level is clipped up to it, which
47
+ forgives the backlog below it. At most 0 (see ``max_state``).
48
+
49
+ max_state: int, default=20
50
+ Ceiling of the inventory level: the next level is clipped down to it,
51
+ which disposes of the stock above it for free. At least 0: the box
52
+ ``[min_state, max_state]`` has to hold the level 0 the default
53
+ :meth:`reset` starts from, and a box without it raises ``ValueError``.
54
+
55
+ max_action: int, default=30
56
+ Largest order per period; sets the action space
57
+ ``{0, ..., max_action}``, and :meth:`step` clips any larger order.
58
+
59
+ random_init: bool, default=False
60
+ Whether :meth:`reset` draws the initial level uniformly from the box
61
+ instead of starting at 0. For training; ``options={"random": ...}``
62
+ overrides it for one reset.
63
+ """
64
+
65
+ metadata = {"render_modes": []}
66
+
67
+ def __init__(
68
+ self,
69
+ render_mode=None,
70
+ holding_cost=1,
71
+ shortage_cost=4,
72
+ ordering_cost=5,
73
+ demand=None,
74
+ min_state=-20,
75
+ max_state=20,
76
+ max_action=30,
77
+ random_init=False,
78
+ ):
79
+ self.render_mode = render_mode
80
+ self.random_init = bool(random_init)
81
+
82
+ self.holding_cost = holding_cost
83
+ self.shortage_cost = shortage_cost
84
+ self.ordering_cost = ordering_cost
85
+
86
+ spec = {"type": "poisson", "mean": 6} if demand is None else dict(demand)
87
+ if spec.get("type") != "poisson" or set(spec) != {"type", "mean"}:
88
+ raise ValueError(
89
+ f"demand must be {{'type': 'poisson', 'mean': m}} in version "
90
+ f"0.0.1, got {demand!r}"
91
+ )
92
+ if not spec["mean"] > 0:
93
+ raise ValueError(f"the Poisson mean must be positive, got {spec['mean']}")
94
+ self.demand_mean = float(spec["mean"])
95
+
96
+ if not min_state <= 0 <= max_state:
97
+ raise ValueError(
98
+ f"the box [min_state, max_state] = [{min_state}, {max_state}] "
99
+ f"must contain 0, the level reset() starts from"
100
+ )
101
+
102
+ self.min_state = min_state
103
+ self.max_state = max_state
104
+ self.max_action = max_action
105
+
106
+ self.observation_space = spaces.Box(
107
+ low=np.array([self.min_state], dtype=np.int64),
108
+ high=np.array([self.max_state], dtype=np.int64),
109
+ shape=(1,),
110
+ dtype=np.int64,
111
+ )
112
+ self.action_space = spaces.Discrete(self.max_action + 1)
113
+
114
+ def reset(self, *, seed: int | None = None, options: dict[str, Any] | None = None):
115
+ super().reset(seed=seed)
116
+
117
+ use_random = (
118
+ self.random_init
119
+ if options is None
120
+ else bool(options.get("random", self.random_init))
121
+ )
122
+
123
+ if use_random:
124
+ observation = self.np_random.integers(
125
+ self.min_state, self.max_state + 1, size=(1,), dtype=np.int64
126
+ )
127
+ else:
128
+ observation = np.zeros((1,), dtype=np.int64)
129
+
130
+ self.observation = observation
131
+ return observation, {}
132
+
133
+ def step(self, action):
134
+ demand = int(self.np_random.poisson(self.demand_mean))
135
+ action_value = min(max(int(np.asarray(action).item()), 0), self.max_action)
136
+
137
+ state = np.copy(self.observation)
138
+ next_state, reward = self._transition_reward(state, action_value, demand)
139
+
140
+ self.observation = next_state
141
+
142
+ info = {
143
+ "state": state,
144
+ "demand": demand,
145
+ "action": action_value,
146
+ "reward": reward,
147
+ }
148
+ return next_state, reward, False, False, info
149
+
150
+ def _transition_reward(self, state, action, demand):
151
+ level = int(state[0]) + action - demand
152
+ on_hand = max(0, level)
153
+ shortage = max(0, -level)
154
+
155
+ next_state = np.array(
156
+ [min(max(level, self.min_state), self.max_state)], dtype=np.int64
157
+ )
158
+
159
+ reward = -(
160
+ self.holding_cost * on_hand
161
+ + self.shortage_cost * shortage
162
+ + self.ordering_cost * (action > 0)
163
+ )
164
+ return next_state, float(reward)