Skip to content

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.