diff --git a/engine/engine.py b/engine/engine.py index cacac7a..a4d568d 100644 --- a/engine/engine.py +++ b/engine/engine.py @@ -1,30 +1,39 @@ from sys import platform import numpy as np -from .lib.demand import generate_demand, estimate_demand +from .lib.demand import generate_demand_for_actor, estimate_demand from .lib.behavior import sample_behavior from logging import INFO, getLogger logger = getLogger(__name__) logger.setLevel(INFO) - class MarketEngine(): + """implements separate demand distributions for humans and agents per Section 3.1.1""" + def __init__(self, - alpha = 0.5, - N = 100, - demand_distribution = (50, 10), - demand_sampling_function = np.random.normal): - self.Nagents = int(N*alpha) - self.Nhumans = int(N*(1-alpha)) - self.demand = (demand_sampling_function, demand_distribution) + alpha: float, + N: int, + human_params: tuple, + agent_params: tuple, + demand_distribution = np.random.normal, + noise_std: float = 1.0): + # no defaults for D_H, D_A - force explicit experiment design + self.alpha = alpha + self.Nagents = int(N * alpha) + self.Nhumans = int(N * (1 - alpha)) + self.human_params = human_params + self.agent_params = agent_params + self.noise_std = noise_std + self.demand_dist = demand_distribution def act(self, prices): - demand = generate_demand(prices, *self.demand) - sample_n = lambda n, human: [sample_behavior(demand, human=human) for _ in range(n)] - human_t, agent_t = sample_n(self.Nhumans, True), sample_n(self.Nagents, False) - trajectories = human_t + agent_t - demand_estimate = estimate_demand(trajectories) - return demand_estimate + # generate separate demands d() per actor type + demand_h = generate_demand_for_actor(prices, self.human_params, self.noise_std, distribution_method = self.demand_dist) + demand_a = generate_demand_for_actor(prices, self.agent_params, self.noise_std, distribution_method = self.demand_dist) + # sample behavior trajectories from each demand distribution + human_t = [sample_behavior(demand_h, human=True) for _ in range(self.Nhumans)] + agent_t = [sample_behavior(demand_a, human=False) for _ in range(self.Nagents)] + return estimate_demand(human_t + agent_t) def measure(self): pass @@ -60,7 +69,7 @@ class Limbo(): if __name__ == "__main__": platform = PricingEngine() - market = MarketEngine() + market = MarketEngine(alpha=0.3, N=100, human_params=(50, 10), agent_params=(45, 15)) limbo = Limbo(platform, market) for _ in range(10): limbo.step() diff --git a/engine/lib/__init__.py b/engine/lib/__init__.py index 8e17835..4e8fd99 100644 --- a/engine/lib/__init__.py +++ b/engine/lib/__init__.py @@ -1,3 +1,3 @@ -from .demand import generate_demand, estimate_demand +from .demand import estimate_demand, generate_demand_for_actor from .behavior import sample_behavior from .render import DashboardRenderer, style_axis diff --git a/engine/lib/behavior.py b/engine/lib/behavior.py index 1822dde..f7dcc65 100644 --- a/engine/lib/behavior.py +++ b/engine/lib/behavior.py @@ -1,7 +1,7 @@ from sim.rl.behavior_loader.models import BehaviorModel, AgentBehaviorModel, aggregate_event_transitions import pandas as pd import numpy as np -from .demand import generate_demand +from .demand import generate_demand_for_actor base_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments" human_dir, agent_dir = f"{base_dir}/collected_data/", f"{base_dir}/agents/collected_data/" @@ -41,7 +41,7 @@ def sample_behavior(condition, human=True, max_len=40): return trajectory if __name__ == "__main__": - t=sample_behavior(generate_demand(np.array([10,20,30])), human=True) + t=sample_behavior(generate_demand_for_actor(np.array([10,20,30])), human=True) print(t) - t=sample_behavior(generate_demand(np.array([10,20,30])), human=False) + t=sample_behavior(generate_demand_for_actor(np.array([10,20,30])), human=False) print(t) diff --git a/engine/lib/demand.py b/engine/lib/demand.py index 7215f7c..d9f7edb 100644 --- a/engine/lib/demand.py +++ b/engine/lib/demand.py @@ -3,15 +3,15 @@ import numpy as np from logging import getLogger logger = getLogger(__name__) -def generate_demand(prices, distribution_method = np.random.normal, distribution_params = (50.0, 10.0)): - # assumption 1: each product has an intrinsic valuation drawn from a normal distribution centered at 50 - product_valuations = distribution_method(*distribution_params, size=len(prices)) - # assumption 2: demand decreases as price increases, following a simple linear model - demand = np.maximum(0, product_valuations - prices) # demand cannot be negative +def generate_demand_for_actor(prices: np.ndarray, params: tuple, noise_std: float = 1.0, distribution_method=np.random.normal) -> np.ndarray: + """d(p;0) = max(0, valuation - price) + epsi for single actor type + params: (mean, std) for valuation distribution D_H or D_A""" + val = distribution_method(*params, size=len(prices)) + noise = distribution_method(0, noise_std, len(prices)) + demand = np.maximum(0, val - prices + noise) total = np.sum(demand) - demand = demand / total * 100 if total > 0 else demand # normalize to percentage, avoid div by zero - logger.info(f"Generated demand for prices {prices}: {demand} with valuations from distribution {distribution_params}") - return demand + return demand / total * 100 if total > 0 else demand + def estimate_demand(trajectories): demand_estimate = {} @@ -29,17 +29,16 @@ def estimate_demand(trajectories): if __name__ == "__main__": np.random.seed(42) prices = np.array([20.0, 35.0, 50.0, 65.0]) - demand = generate_demand(prices) - print("Generated Demand:", demand) + # demo actor-specific demands + human_params, agent_params = (50, 10), (45, 15) + demand_h = generate_demand_for_actor(prices, human_params) + demand_a = generate_demand_for_actor(prices, agent_params) + print("Human Demand:", demand_h) + print("Agent Demand:", demand_a) from .behavior import sample_behavior - N, alphat =200, 0.1 - trajectories = [] - for _ in range(int(N*(1 - alphat))): - trajectories.append(sample_behavior(demand, human=True)) - for _ in range(int(N*alphat)): - trajectories.append(sample_behavior(demand, human=False)) - demand_estimate = estimate_demand(trajectories) + N, alpha = 200, 0.3 + n_h, n_a = int(N * (1 - alpha)), int(N * alpha) + human_t = [sample_behavior(demand_h, human=True) for _ in range(n_h)] + agent_t = [sample_behavior(demand_a, human=False) for _ in range(n_a)] + demand_estimate = estimate_demand(human_t + agent_t) print("Estimated Demand from Behavior:", demand_estimate) - delta = {k: demand_estimate.get(k, 0) - demand[i] for i, k in enumerate(range(len(prices)))} - delta = np.mean([np.abs(v) for v in delta.values()]) - print("Demand Delta:", delta) diff --git a/engine/wrapper.py b/engine/wrapper.py index 7221a8a..9bf3048 100644 --- a/engine/wrapper.py +++ b/engine/wrapper.py @@ -14,6 +14,9 @@ class PHANTOM(gym.Env): n_products: int = 10, alpha: float = 0.3, N: int = 100, + human_params: tuple = (50.0, 10.0), + agent_params: tuple = (45.0, 15.0), + noise_std: float = 1.0, price_bounds: tuple = (10.0, 150.0), lambda_coi: float = 0.1, coi_window: int = 10, @@ -26,8 +29,13 @@ class PHANTOM(gym.Env): self.render_mode = render_mode self.alpha = alpha self.N = N + self.human_params = human_params + self.agent_params = agent_params - self.market = MarketEngine(alpha=alpha, N=N) + self.market = MarketEngine( + alpha=alpha, N=N, + human_params=human_params, agent_params=agent_params, noise_std=noise_std + ) self._platform_stub = PricingEngine() self._limbo = Limbo(self._platform_stub, self.market)