Center of Gravity
Find the best locations for warehouses, distribution centres or other facilities by minimizing demand-weighted distance to your demand points.
When to use this¶
- You're deciding where to open a new warehouse or DC
- You want to check whether your current facility locations are sensible
- You're expanding the network and need to pick the next location
What you need¶
- Location records with latitude and longitude, from Map Locations
- Demand policy rows with a Requested Quantity, from Map Demand Policy. They are the weights, and they are required
Settings¶
| Setting | Options | Default | What it does |
|---|---|---|---|
| Location Mode | Propose New Locations, Use Existing Locations, Expand Network | Propose New Locations | Propose New Locations computes brand-new coordinates. Use Existing Locations picks the best of the locations you already have. Expand Network keeps the current facilities and adds new ones. |
| Maximum Locations | a whole number | 1 |
How many facilities to find. |
| Include Location Types | any types in your data | all of them | Limit the analysis to specific location types — only retailers, say. |
| Existing Facility Types | any types in your data | none | For Expand Network: the location types that stay where they are. |
What you get¶
A list of proposed facility locations with coordinates, and the total weighted distance across the network. Results are saved with the scenario when the node runs in a workflow, as a result table on the scenario.
Workflow wiring¶
This node takes scenario data — wire it after Create Scenario, Load Scenario, or another analysis step. Do not wire it straight to a Map node.
flowchart LR
n1["Import Data (locations.csv)"] --> n2["Map Locations"]
n3["Create Scenario"] --> n4["Center of Gravity"]
n5["Import Data (demand.csv)"] --> n6["Map Demand Policy"]
n2 --> n3
n6 --> n3
Location records must carry latitude and longitude. The demand policy supplies the weights through its Requested Quantity.
Common mistakes¶
- Wiring this node to a Map node instead of through Create Scenario
- Missing latitude or longitude on location records — the run fails
- Providing no demand. A demand policy with a Requested Quantity is required: this node weights by demand and does not fall back to an unweighted centroid
- Choosing Expand Network and leaving Existing Facility Types empty. Expand keeps existing facilities in place and needs to be told which ones; with nothing named it finds none and the run fails
What it does not do¶
- It computes coordinates only. It does not modify the scenario's locations, links or flows.
- Proposed locations arrive as an analysis result beside the scenario. They are not part of the network: no lanes, no supply, no cost.
- So it cannot answer what is the cost impact of a new warehouse? on its own. For that, bridge it into Network Optimization.
Chaining into Network Optimization¶
To evaluate the cost and flow impact of the proposed locations, chain three steps:
flowchart LR
n1["Load Scenario"] --> n2["Center of Gravity"]
n2 --> n3["Scenario Script (bridge, into Scenario Data)"]
n3 --> n4["Network Optimization"]
n4 --> n5["Create Scenario"]
The bridge script reads the proposed locations from the result, adds them to the scenario's locations, and creates lanes with distance-based costs so the optimizer can route through them. The script below is complete; the names it uses are the ones a script sees, so copy it as it is.
def main(scenario_data):
"""Integrate proposed locations into the network for optimization."""
import math
def haversine_km(lat1, lon1, lat2, lon2):
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (
math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1))
* math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2
)
return R * 2 * math.asin(math.sqrt(a))
cog = scenario_data.get("_cog_result", {})
new_locs = cog.get("optimal_locations", [])
locations = scenario_data.get("locations", [])
existing_locations = list(locations) # snapshot before appending
lanes = scenario_data.get("lanes", [])
for loc in new_locs:
locations.append(
{
"id": loc["id"],
"latitude": loc["latitude"],
"longitude": loc["longitude"],
"type": loc.get("type", "Warehouse"),
}
)
# Outbound: proposed location -> demand points.
for existing in existing_locations:
if existing.get("type") in ("Customer", "Retailer"):
dist = haversine_km(
loc["latitude"],
loc["longitude"],
float(existing["latitude"]),
float(existing["longitude"]),
)
lanes.append(
{
"source": loc["id"],
"target": existing["id"],
"product": "ALL",
"unit_cost": round(dist * 1.5, 2),
}
)
# Inbound: supply points -> proposed location.
for existing in existing_locations:
if existing.get("type") in ("Supplier", "Factory", "Warehouse"):
dist = haversine_km(
float(existing["latitude"]),
float(existing["longitude"]),
loc["latitude"],
loc["longitude"],
)
lanes.append(
{
"source": existing["id"],
"target": loc["id"],
"product": "ALL",
"unit_cost": round(dist * 1.5, 2),
}
)
scenario_data["locations"] = locations
scenario_data["lanes"] = lanes
return scenario_data
Every lane the bridge writes carries a per-unit cost and a product, because a
lane needs both; ALL opens the lane to every product.