Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ simenv/
└── provider_simenv/
├── main.py ← entry point
├── model.py ← simulation orchestrator
├── scenario.py ← engine parameters (counts, costs, sigmas)
├── scenario.py ← engine parameters (costs, margins, sigmas)
├── topology.py ← PDL + roster → agent lists and flow graph
├── environment.py ← global state + price aggregation
├── pdl_loader.py ← PDL YAML → events / entities
Expand Down Expand Up @@ -147,9 +147,8 @@ The exporter writes `web/public/bundle.json`. See `web/README.md` for the fronte
`data/input/SimulatorScenarios.csv` is the working table Melodie reads. Change the
**template**, not the live file — every run overwrites the live copy from the template.

Each row is one run. Columns are engine parameters (agent counts, routing, size sigmas,
storage, length). Producer counts use PDL entity ids (`n_brazil_farms`, `n_argentina_farms`,
`n_us_farms`).
Each row is one run. Columns are engine parameters (routing, size sigmas, storage, length).
Agent counts are not CSV columns — the roster derives one agent per PDL entity.

Shocks are not CSV columns. Pass `--pdl`; `EventTracker` applies drought, capacity, and
input-price events from the YAML. The shipped files are `scenarios/s1-soja.pdl.yaml` and
Expand All @@ -159,9 +158,7 @@ builder).

| Parameter | Effect |
|---|---|
| `n_brazil_farms` / `n_argentina_farms` / `n_us_farms` | Producer agent counts |
| `share_santos_port` | Explicit weight for the Santos route; unspecified route weights are resolved per origin |
| `shock_ramp_steps` | Ramp length when a PDL shock is active |
| `size_sigma_brazil_farms` | Log-normal farm-size spread (`0` = identical farms) |
| `wholesaler_storage_capacity` | Max tonnes a wholesaler can hold per step (default 2857 t/day) |
| `period_num` | Number of simulation steps (default 365) |
Expand Down Expand Up @@ -287,7 +284,7 @@ from the repository root. This builds the database container, if it's not alread
## Known Issues / Notes

- **`python main.py` needs the package directory** (`src/provider_simenv/`). After `pip install -e .`, `python -m provider_simenv.main` from the repo root works — `Config` resolves `data/` from `main.py`'s location, not the cwd.
- **`run_stepwise()`** in `model.py` is the designated integration hook for external control (e.g. palaestrAI). It yields a state dict `{step, shock_scale, soja_price, feed_price, ...}` after every simulation step.
- **`run_stepwise()`** in `model.py` is the designated integration hook for external control (e.g. palaestrAI). It yields a state dict `{step, shock_scale, soy_price, feed_price, ...}` after every simulation step.

---

Expand Down
14 changes: 9 additions & 5 deletions src/provider_simenv/agents/farmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,18 @@ def _step_producer(self):

def _step_consumer(self):
"""
Collect feed from all feed traders (equal share per EU farmer),
then compute livestock output proportional to feed received.
Collect feed from all feed traders (equal share across every active
consumer agent), then compute livestock output proportional to feed
received.
"""
active_eu = getattr(self.model, self.list_name).filter(lambda f: f.active)
n_eu = len(active_eu)
n_consumers = sum(
len(getattr(self.model, entry.archetype.name).filter(lambda c: c.active))
for entry in self.model._roster
if entry.archetype.role == ROLE_CONSUMER
)
active_traders = self.model.upstream(self.list_name)
total_feed = sum(t.quantity_available for t in active_traders)
self.feed_received = total_feed / n_eu if n_eu > 0 else 0.0
self.feed_received = total_feed / n_consumers if n_consumers > 0 else 0.0

# 100 units of feed → 100 units of livestock output (baseline).
self.livestock_output = self.feed_received
Expand Down
8 changes: 4 additions & 4 deletions src/provider_simenv/data/input/SimulatorScenarios.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
id,run_num,period_num,n_brazil_farms,n_argentina_farms,n_us_farms,n_wholesalers,n_processors,n_feed_manufacturers,n_feed_traders,n_eu_farmers,share_santos_port,shock_ramp_steps,size_sigma_brazil_farms,farm_size_sigma_eu,farm_size_seed,wholesaler_storage_capacity
0,1,365,10,5,8,1,3,3,3,10,0.7,0,0.0,0.0,42,2857.0
1,1,365,10,5,8,1,3,3,3,10,0.7,0,0.4,0.4,42,2857.0
2,1,365,10,5,8,1,3,3,3,10,0.7,0,0.4,0.4,42,2857.0
id,run_num,period_num,share_santos_port,size_sigma_brazil_farms,farm_size_sigma_eu,farm_size_seed,wholesaler_storage_capacity
0,1,365,0.7,0.0,0.0,42,2857.0
1,1,365,0.7,0.4,0.4,42,2857.0
2,1,365,0.7,0.4,0.4,42,2857.0
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
id,run_num,period_num,n_brazil_farms,n_argentina_farms,n_us_farms,n_wholesalers,n_processors,n_feed_manufacturers,n_feed_traders,n_eu_farmers,share_santos_port,shock_ramp_steps,size_sigma_brazil_farms,farm_size_sigma_eu,farm_size_seed,wholesaler_storage_capacity
0,1,365,10,5,8,1,3,3,3,10,0.7,0,0.0,0.0,42,2857.0
1,1,365,10,5,8,1,3,3,3,10,0.7,0,0.4,0.4,42,2857.0
2,1,365,10,5,8,1,3,3,3,10,0.7,0,0.4,0.4,42,2857.0
id,run_num,period_num,share_santos_port,size_sigma_brazil_farms,farm_size_sigma_eu,farm_size_seed,wholesaler_storage_capacity
0,1,365,0.7,0.0,0.0,42,2857.0
1,1,365,0.7,0.4,0.4,42,2857.0
2,1,365,0.7,0.4,0.4,42,2857.0
9 changes: 5 additions & 4 deletions src/provider_simenv/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ def _roster_pdl_path(self) -> str:

def setup(self):
"""
Populate every AgentList in the derived roster, assign roles, and apply
each archetype's declared params — bindings, literal attrs, and
scenario-sourced attrs — so agents carry no role-specific init.
Populate every AgentList in the derived roster with one agent per roster
entry, assign roles, and apply each archetype's declared params —
bindings, literal attrs, and scenario-sourced attrs — so agents carry no
role-specific init.
"""
for entry in self._roster:
arc = entry.archetype
agent_list = getattr(self, arc.name)
agent_list.setup_agents(getattr(self.scenario, arc.count_attr))
agent_list.setup_agents(1)
# entity-driven capacity binding: an agent reads its own entity's supply shock.
capacity_key = (entry.entity_ids[0], "supply") if len(entry.entity_ids) == 1 else None
origin = entry.entity_ids[0] if len(entry.entity_ids) == 1 else ""
Expand Down
13 changes: 0 additions & 13 deletions src/provider_simenv/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,6 @@ class SupplyChainScenario(Scenario):
# --- Supply chain routing ---
share_santos_port: float = 0.7 # explicit weight for the Santos route

# --- Agent population size ---
n_brazil_farms: int = 10 # South American Farmers
n_wholesalers: int = 1 # originators per regional roster list
n_land_transport: int = 1 # fallback for a derived transport entity
n_sea_transport: int = 1 # fallback for a derived sea crossing
n_processors: int = 3 # Crushers / oil mills
n_feed_manufacturers: int = 3
n_feed_traders: int = 3
n_eu_farmers: int = 10 # EU livestock farmers

# --- Storage capacities (t/step)
# Maximum inventory a wholesaler can hold in a single simulation step
# set to realistic value (e.g. 20_000) to observe capacity-binding behavior under brazil_drought scenario.
Expand Down Expand Up @@ -74,7 +64,6 @@ class SupplyChainScenario(Scenario):
# - higher fixed_costs -> higher baseline price (EU prefers BRA under normal conditions)
# - unaffected by BRA shock -> stable supply when BRA supply is disrupted
# - base_yield acts as capacity ceiling
n_us_farms: int = 8
fixed_costs_us_farms: float = 48000.0
margin_us_farms: float = 0.10

Expand All @@ -84,14 +73,12 @@ class SupplyChainScenario(Scenario):
# - fixed_costs between BRA and USA
# - unaffected by BRA shock
# - No surplus_factor - ARG is a baseline supply, not emergency reserve
n_argentina_farms: int = 5
fixed_costs_argentina_farms: float = 42000.0
margin_argentina_farms: float = 0.10

# generic producer fallback: synthetic sentinels, not a real country's costs
fixed_costs_producer: float = 1.0
margin_producer: float = 0.10
size_sigma_producer: float = 0.0
n_producer: int = 1


42 changes: 12 additions & 30 deletions src/provider_simenv/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ class Archetype:
name: str # model attribute name (parity with the current roster)
agent_class: type
role: str
count_attr: str # scenario attribute holding the instance count
# Declarative agent init, applied to instances by model.setup(); agents
# never read it. Keys (all optional):
# "bindings" semantic slot -> PDL (entity, impact_field) verbatim.
Expand Down Expand Up @@ -163,12 +162,12 @@ def load_roster_sidecar(pdl_path: str | Path) -> RosterSidecar:
"feed_trader": (Trader, ROLE_FEED_TRADER),
}

_DECLARED_ARCHETYPES: dict[str, tuple[str, str, Mapping[str, Any]]] = {
"consumer": ("eu_farmers", "n_eu_farmers", _FARMER_EU),
"processor": ("processors", "n_processors", _PROCESSOR),
"feed_manufacturer": ("feed_manufacturers", "n_feed_manufacturers", _FEED_MFR),
"wholesaler": ("", "n_wholesalers", _WHOLESALER),
"feed_trader": ("feed_traders", "n_feed_traders", _FEED_TRADER),
_DECLARED_ARCHETYPES: dict[str, tuple[str, Mapping[str, Any]]] = {
"consumer": ("", _FARMER_EU),
"processor": ("processors", _PROCESSOR),
"feed_manufacturer": ("feed_manufacturers", _FEED_MFR),
"wholesaler": ("", _WHOLESALER),
"feed_trader": ("feed_traders", _FEED_TRADER),
}

KNOWN_ARCHETYPES: frozenset[str] = frozenset(ARCHETYPE_REGISTRY)
Expand Down Expand Up @@ -277,30 +276,15 @@ class RosterEntry:
_PRODUCER_SCENARIO_ATTRS = ("fixed_costs", "margin", "size_sigma")


def _producer_count_attr(eid: str) -> str:
specific = f"n_{eid}"
if hasattr(SupplyChainScenario, specific):
return specific
logger.warning("producer %r has no %s; falling back to n_producer", eid, specific)
return "n_producer"


def _transport_count_attr(list_name: str, fallback: str) -> str:
specific = f"n_{list_name}"
return specific if hasattr(SupplyChainScenario, specific) else fallback


def _sea_transport_name(edge: SeaEdge) -> str:
return f"sea_transport_{edge.src}__{edge.dst}"


def _sea_transport_archetype(edge: SeaEdge) -> Archetype:
name = _sea_transport_name(edge)
return Archetype(
name,
_sea_transport_name(edge),
Transport,
ROLE_SEA_TRANSPORT,
_transport_count_attr(name, "n_sea_transport"),
_SEA_TRANSPORT,
)

Expand Down Expand Up @@ -369,7 +353,6 @@ def _declared_archetype(
eid,
Transport,
ROLE_LAND_TRANSPORT,
_transport_count_attr(eid, "n_land_transport"),
_land_transport_params(eid, sea_edges),
)
if eid else None
Expand All @@ -379,10 +362,10 @@ def _declared_archetype(
if spec is None or not eid:
arc = None
else:
default_name, count_attr, params = spec
default_name, params = spec
cls, role = ARCHETYPE_REGISTRY[key]
name = eid if key == "wholesaler" else default_name
arc = Archetype(name, cls, role, count_attr, params)
name = default_name or eid
arc = Archetype(name, cls, role, params)
if arc is None:
logger.warning(
"entity %r resolved to archetype %r but has no runtime recipe",
Expand All @@ -395,8 +378,8 @@ def build_roster(pdl_path: str | Path) -> list[RosterEntry]:
"""
Ordered roster from PDL and sidecar entities, followed by sea-transport agents.

Producer kinds split per-entity (each carries its own id-named list, count, and
shock); declared non-producer kinds use their model recipes.
Producer kinds split per-entity (each carries its own id-named list and shock);
declared non-producer kinds use their model recipes.
"""
doc = PDLLoader(pdl_path)._doc
sidecar = load_roster_sidecar(pdl_path)
Expand All @@ -416,7 +399,6 @@ def build_roster(pdl_path: str | Path) -> list[RosterEntry]:
cls, role = ARCHETYPE_REGISTRY["producer"]
arc = Archetype(
eid, cls, role,
_producer_count_attr(eid),
{
"bindings": _producer_input_bindings(eid, dependencies),
"scenario_attrs": _producer_scenario_attrs(eid),
Expand Down
Loading