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
31 changes: 31 additions & 0 deletions gis/solar_adoption/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Solar Adoption Model

This model demonstrates a simulation of households adopting solar panels, driven by two key geographic factors:
1. **Environmental (Raster Data):** The economic viability of solar is based on local solar radiation (simulated via a `RasterLayer`).
2. **Social (Vector Data):** The "peer effect" where households are more likely to adopt solar panels if their neighbors—within a certain distance—have already adopted them. Households are represented as `GeoAgents`.

This example is specifically designed to showcase how `mesa-geo` integrates both **Continuous Space (Raster)** environments and **Discrete Space (Vector)** agents interacting with one another.

## How to Run

To run the model, first install the dependencies:

```bash
pip install -r requirements.txt
```

Then run the application:

```bash
solara run app.py
```

Then visit the provided localhost address in your web browser.

## Using the Interface

- **Number of Households:** Controls how many `Household` agents populate the environment.
- **Social Influence Weight:** Adjusts how strongly a household is influenced by neighbors who already have solar panels.
- **Economic Weight (Radiation):** Adjusts how strongly the underlying solar radiation `RasterLayer` influences adoption.

Watch how clusters of adoption naturally form in areas with high solar radiation and slowly spread outward through social influence!
43 changes: 43 additions & 0 deletions gis/solar_adoption/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import mesa_geo as mg

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should preferably be under gis/solar_adoption/solar_adoption. Then the app.py should import these with import solar_adoption.agents for example.



class Household(mg.GeoAgent):
"""Solar panel adoption agent."""

def __init__(self, model, geometry, crs, unique_id=None):
"""Create a new Household agent."""
super().__init__(model, geometry, crs)
self.unique_id = (
unique_id
if unique_id is not None
else self.model.random.randint(1, 100000000)
)
self.has_solar = False
self.solar_radiation = 0.0

def step(self):
if self.has_solar:
return

neighbors = list(
self.model.space.get_neighbors_within_distance(self, distance=500)
)

solar_neighbors = sum(1 for n in neighbors if getattr(n, "has_solar", False))
total_neighbors = len(neighbors)
social_influence = (
solar_neighbors / total_neighbors if total_neighbors > 0 else 0.0
)
economic_viability = self.solar_radiation

prob_adopt = (self.model.social_weight * social_influence) + (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal opinion here: Since you re-evaluate at each step, these probabilities are a bit too high. Ends up with total adoption even with extremely low social + economic weights in a short number of steps. It may help visually to pad this a bit more.

self.model.economic_weight * economic_viability

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding two probabilities together doesn't make a ton of sense from a generative perspective IMO. Might be worth structuring it as a mixture model where each agent chooses to make an economic vs. social vs. noop decision, and then subsequently evaluating the social vs. economic probability. That way you're guaranteed to stay within 0 - 1 and you have a more proper probabilistic model to describe the "solar adoption" decision.

)
prob_adopt += 0.01

if self.model.random.random() < prob_adopt:
self.has_solar = True
self.model.total_adopted += 1

def __repr__(self):
return f"Household {self.unique_id} (Solar: {self.has_solar})"
45 changes: 45 additions & 0 deletions gis/solar_adoption/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from agents import Household
from mesa.visualization import Slider, SolaraViz, make_plot_component
from mesa_geo.visualization import make_geospace_component
from model import SolarAdoption


def solar_portrayal(agent):
"""Portrayal function for households and solar radiation cells."""
if isinstance(agent, Household):
portrayal = {}
if agent.has_solar:
portrayal["color"] = "gold"
portrayal["radius"] = 4
else:
portrayal["color"] = "grey"
portrayal["radius"] = 3
return portrayal
else:
# It's a Raster Cell
val = getattr(agent, "radiation", 0)
r = int(255 * val)
g = int(255 * val)
b = 0
return (r, g, b, 128)


model_params = {
"num_houses": Slider("Number of Households", 100, 10, 500, 10),
"social_weight": Slider("Social Influence Weight", 0.3, 0.0, 1.0, 0.1),
"economic_weight": Slider("Economic Weight (Radiation)", 0.3, 0.0, 1.0, 0.1),
}


model = SolarAdoption()
page = SolaraViz(
model,
name="Solar Adoption",
model_params=model_params,
components=[
make_geospace_component(solar_portrayal, zoom=9),
make_plot_component(["Adopted"]),
],
)

page # noqa
70 changes: 70 additions & 0 deletions gis/solar_adoption/data/generate_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os
import random

import geopandas as gpd
import numpy as np
import rasterio
from rasterio.transform import from_origin
from shapely.geometry import Point

# Paths
script_dir = os.path.dirname(__file__)
raster_path = os.path.join(script_dir, "solar_radiation.tif")
vector_path = os.path.join(script_dir, "households.geojson")


def generate_raster():
"""Generate a procedural raster layer representing solar radiation."""
width = 100
height = 100

# Create a procedural 2D numpy array (e.g. gradient + noise) for solar radiation
# Values from 0 (low radiation) to 1 (high radiation)
y, x = np.mgrid[0:height, 0:width]
gradient = (x + y) / (width + height) # Diagonal gradient
noise = np.random.normal(0, 0.1, (height, width))
radiation_data = np.clip(gradient + noise, 0, 1)

# Reshape to (1, height, width) for Rasterio format
radiation_data = radiation_data.reshape(1, height, width).astype(np.float32)

# Create a Georeferenced Transform for the Raster (e.g., origin at 0,0, pixel size 1000m)
transform = from_origin(0, height * 1000, 1000, 1000)

with rasterio.open(
raster_path,
"w",
driver="GTiff",
height=height,
width=width,
count=1,
dtype=radiation_data.dtype,
crs="epsg:3857",
transform=transform,
) as dataset:
dataset.write(radiation_data)
print(f"Generated {raster_path}")
return transform, width, height


def generate_vector(width, height):
bounds = [0, 0, width * 1000, height * 1000]
minx, miny, maxx, maxy = bounds

num_houses = 500
points = []
for _ in range(num_houses):
# Generate random points within the bounds
x = random.uniform(minx, maxx)
y = random.uniform(miny, maxy)
points.append(Point(x, y))

# Create a GeoDataFrame from points
gdf = gpd.GeoDataFrame(geometry=points, crs="epsg:3857")
gdf.to_file(vector_path, driver="GeoJSON")
print(f"Generated {vector_path}")


if __name__ == "__main__":
t, w, h = generate_raster()
generate_vector(w, h)
Loading
Loading