Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import math | |
| import random | |
| import uuid | |
| from typing import Any, Dict, List, Optional, Tuple | |
| from .models import ( | |
| DeliveryWindow, | |
| ManufacturingAction, | |
| ManufacturingObservation, | |
| ManufacturingReward, | |
| PendingOrder, | |
| PlatformState, | |
| ) | |
| # βββ Tuneable constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PRODUCE_MATERIAL_COST = 15.0 | |
| PRODUCE_ENERGY_COST = 10.0 | |
| PRODUCE_COMPONENT_GAIN = 12.0 | |
| ASSEMBLE_COMPONENT_COST = 10.0 | |
| ASSEMBLE_ENERGY_COST = 12.0 | |
| DELIVER_ENERGY_COST = 8.0 | |
| RECHARGE_BASE = 20.0 | |
| ENERGY_MAX = 100.0 | |
| MATERIAL_MAX = 100.0 | |
| COMPONENT_MAX = 100.0 | |
| PRODUCT_MAX = 10 | |
| MATERIAL_REFILL_RATE = 5.0 # materials added each step passively | |
| PRODUCE_MATERIAL_THRESHOLD = PRODUCE_MATERIAL_COST | |
| ASSEMBLE_COMPONENT_THRESHOLD = ASSEMBLE_COMPONENT_COST | |
| # βββ Product types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SIMPLE_PRODUCTS = ["alloy_panel", "circuit_board", "fuel_cell"] | |
| ASSEMBLED_PRODUCTS = ["solar_array", "thruster_module", "habitat_unit"] | |
| ALL_PRODUCTS = SIMPLE_PRODUCTS + ASSEMBLED_PRODUCTS | |
| class ManufacturingFactoryEnv: | |
| """Core simulation engine for the Space Manufacturing RL environment.""" | |
| def __init__( | |
| self, | |
| num_platforms: int, | |
| max_steps: int, | |
| seed: int, | |
| pending_orders: List[PendingOrder], | |
| delivery_windows: List[DeliveryWindow], | |
| solar_zones: Optional[Dict[str, float]] = None, | |
| ) -> None: | |
| self.num_platforms = num_platforms | |
| self.max_steps = max_steps | |
| self.seed = seed | |
| self._initial_orders = list(pending_orders) | |
| self._initial_windows = list(delivery_windows) | |
| self._solar_zones: Dict[str, float] = solar_zones or {"zone_a": 0.8} | |
| # mutable state (initialised in reset) | |
| self.platforms: List[PlatformState] = [] | |
| self.pending_orders: List[PendingOrder] = [] | |
| self.delivery_windows: List[DeliveryWindow] = [] | |
| self.step_count: int = 0 | |
| self.done: bool = False | |
| self.total_reward: float = 0.0 | |
| self.episode_id: str = "" | |
| self.metrics: Dict[str, Any] = {} | |
| self._rng = random.Random(seed) | |
| # ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def reset(self) -> ManufacturingObservation: | |
| self._rng = random.Random(self.seed) | |
| self.step_count = 0 | |
| self.done = False | |
| self.total_reward = 0.0 | |
| self.episode_id = str(uuid.uuid4()) | |
| self.metrics = { | |
| "production_runs": 0, | |
| "assemblies_completed": 0, | |
| "deliveries_completed": 0, | |
| "on_time_deliveries": 0, | |
| "invalid_actions": 0, | |
| "total_steps": 0, | |
| } | |
| self.platforms = [self._init_platform(i) for i in range(self.num_platforms)] | |
| self.pending_orders = [o.model_copy() for o in self._initial_orders] | |
| self.delivery_windows = [w.model_copy() for w in self._initial_windows] | |
| return self._build_observation(reward=0.0) | |
| def step( | |
| self, action: ManufacturingAction | |
| ) -> Tuple[ManufacturingObservation, ManufacturingReward, bool, Dict[str, Any]]: | |
| if self.done: | |
| obs = self._build_observation(reward=0.0) | |
| return obs, ManufacturingReward(value=0.0), True, {} | |
| step_reward = 0.0 | |
| components: Dict[str, float] = {} | |
| for pid, platform in enumerate(self.platforms): | |
| act = action.platform_actions.get(pid, "recharge") | |
| r, c = self._apply_action(platform, act) | |
| step_reward += r | |
| for k, v in c.items(): | |
| components[k] = components.get(k, 0.0) + v | |
| # Passive material refill | |
| for p in self.platforms: | |
| p.material_stock = min(MATERIAL_MAX, p.material_stock + MATERIAL_REFILL_RATE) | |
| # Energy penalty for critically low | |
| for p in self.platforms: | |
| if p.energy < 10.0: | |
| step_reward -= 1.5 | |
| components["energy_critical"] = components.get("energy_critical", 0.0) - 1.5 | |
| # Expire overdue delivery windows (soft penalty) | |
| still_open: List[DeliveryWindow] = [] | |
| for w in self.delivery_windows: | |
| if w.deadline >= self.step_count: | |
| still_open.append(w) | |
| self.delivery_windows = still_open | |
| self.step_count += 1 | |
| self.metrics["total_steps"] = self.step_count | |
| self.total_reward += step_reward | |
| if self.step_count >= self.max_steps: | |
| self.done = True | |
| elif not self.pending_orders: | |
| self.done = True # all orders delivered β stop early | |
| reward_obj = ManufacturingReward(value=step_reward, components=components) | |
| obs = self._build_observation(reward=step_reward) | |
| return obs, reward_obj, self.done, {"metrics": dict(self.metrics)} | |
| # ββ Internal helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _init_platform(self, pid: int) -> PlatformState: | |
| angle = (2 * math.pi / self.num_platforms) * pid | |
| pos = [round(math.cos(angle) * 400, 2), round(math.sin(angle) * 400, 2), 0.0] | |
| return PlatformState( | |
| id=pid, | |
| position=pos, | |
| energy=self._rng.uniform(60.0, 90.0), | |
| material_stock=self._rng.uniform(30.0, 60.0), | |
| component_stock=0.0, | |
| product_stock=0, | |
| last_action=None, | |
| ) | |
| def _apply_action( | |
| self, platform: PlatformState, action: str | |
| ) -> Tuple[float, Dict[str, float]]: | |
| reward = 0.0 | |
| components: Dict[str, float] = {} | |
| platform.last_action = action | |
| if action == "produce": | |
| if platform.material_stock < PRODUCE_MATERIAL_THRESHOLD: | |
| reward -= 2.0 | |
| components["invalid_action"] = components.get("invalid_action", 0.0) - 2.0 | |
| self.metrics["invalid_actions"] += 1 | |
| else: | |
| platform.material_stock -= PRODUCE_MATERIAL_COST | |
| platform.component_stock = min( | |
| COMPONENT_MAX, platform.component_stock + PRODUCE_COMPONENT_GAIN | |
| ) | |
| platform.energy = max(0.0, platform.energy - PRODUCE_ENERGY_COST) | |
| reward += 3.0 | |
| components["production"] = components.get("production", 0.0) + 3.0 | |
| self.metrics["production_runs"] += 1 | |
| elif action == "assemble": | |
| if platform.component_stock < ASSEMBLE_COMPONENT_THRESHOLD: | |
| reward -= 2.0 | |
| components["invalid_action"] = components.get("invalid_action", 0.0) - 2.0 | |
| self.metrics["invalid_actions"] += 1 | |
| else: | |
| platform.component_stock -= ASSEMBLE_COMPONENT_COST | |
| platform.product_stock = min(PRODUCT_MAX, platform.product_stock + 1) | |
| platform.energy = max(0.0, platform.energy - ASSEMBLE_ENERGY_COST) | |
| reward += 5.0 | |
| components["assembly"] = components.get("assembly", 0.0) + 5.0 | |
| self.metrics["assemblies_completed"] += 1 | |
| elif action == "deliver": | |
| if platform.product_stock < 1: | |
| reward -= 2.0 | |
| components["invalid_action"] = components.get("invalid_action", 0.0) - 2.0 | |
| self.metrics["invalid_actions"] += 1 | |
| else: | |
| platform.product_stock -= 1 | |
| platform.energy = max(0.0, platform.energy - DELIVER_ENERGY_COST) | |
| self.metrics["deliveries_completed"] += 1 | |
| # Check if any window is still open β on-time bonus | |
| on_time = False | |
| for w in self.delivery_windows: | |
| if w.deadline >= self.step_count: | |
| on_time = True | |
| self.metrics["on_time_deliveries"] += 1 | |
| self.delivery_windows.remove(w) | |
| break | |
| if on_time: | |
| reward += 8.0 | |
| components["on_time_delivery"] = ( | |
| components.get("on_time_delivery", 0.0) + 8.0 | |
| ) | |
| else: | |
| reward += 3.0 | |
| components["late_delivery"] = components.get("late_delivery", 0.0) + 3.0 | |
| # Consume a pending order if matched | |
| if self.pending_orders: | |
| self.pending_orders.pop(0) | |
| elif action == "recharge": | |
| zone = self._zone_for_platform(platform) | |
| irradiance = self._solar_zones.get(zone, 0.5) | |
| gain = RECHARGE_BASE * irradiance | |
| was_low = platform.energy < 25.0 | |
| platform.energy = min(ENERGY_MAX, platform.energy + gain) | |
| if was_low: | |
| reward += 1.0 | |
| components["timely_recharge"] = components.get("timely_recharge", 0.0) + 1.0 | |
| else: | |
| # Unknown action treated as invalid | |
| reward -= 2.0 | |
| components["invalid_action"] = components.get("invalid_action", 0.0) - 2.0 | |
| self.metrics["invalid_actions"] += 1 | |
| # Idle penalty β recharge when actionable work exists | |
| if action == "recharge" and platform.energy > 50.0 and ( | |
| platform.material_stock >= PRODUCE_MATERIAL_THRESHOLD | |
| or platform.component_stock >= ASSEMBLE_COMPONENT_THRESHOLD | |
| or platform.product_stock > 0 | |
| ): | |
| reward -= 0.3 | |
| components["idle_penalty"] = components.get("idle_penalty", 0.0) - 0.3 | |
| return reward, components | |
| def _zone_for_platform(self, platform: PlatformState) -> str: | |
| zones = list(self._solar_zones.keys()) | |
| idx = platform.id % len(zones) | |
| return zones[idx] | |
| def _build_observation(self, reward: float) -> ManufacturingObservation: | |
| return ManufacturingObservation( | |
| platforms=[p.model_copy() for p in self.platforms], | |
| time_step=self.step_count, | |
| delivery_windows=list(self.delivery_windows), | |
| solar_conditions=dict(self._solar_zones), | |
| pending_orders=list(self.pending_orders), | |
| total_reward=self.total_reward, | |
| reward=reward, | |
| done=self.done, | |
| metadata=dict(self.metrics), | |
| ) | |